Skip to content

fix: use ContextVar for client config overrides and propagate context to thread pool#162

Open
halfbaked wants to merge 1 commit into
ClickHouse:mainfrom
halfbaked:fix/config-override-async-and-context-propagation
Open

fix: use ContextVar for client config overrides and propagate context to thread pool#162
halfbaked wants to merge 1 commit into
ClickHouse:mainfrom
halfbaked:fix/config-override-async-and-context-propagation

Conversation

@halfbaked

Copy link
Copy Markdown

Problem

CLIENT_CONFIG_OVERRIDES_KEY (introduced for per-request client config overrides via middleware) does not work. Two bugs prevent the override from being applied:

Bug 1: Async/sync mismatch in create_clickhouse_client

create_clickhouse_client() is a sync function that calls ctx.get_state(CLIENT_CONFIG_OVERRIDES_KEY) without await:

session_config_overrides = ctx.get_state(CLIENT_CONFIG_OVERRIDES_KEY)

Context.get_state is async def, so calling it without await returns a coroutine object, not the dict. The override is silently ignored. The unit tests pass because MagicMock.get_state.return_value returns synchronously, masking the bug.

Bug 2: QUERY_EXECUTOR.submit doesn't propagate contextvars

future = QUERY_EXECUTOR.submit(execute_query, query)

ThreadPoolExecutor.submit does not copy the current context into worker threads. Even if Bug 1 were fixed, ContextVar values set by middleware in the async context are invisible to execute_query running in the thread pool.

Fix

  1. Replace ctx.get_state/set_state with a module-level ContextVar (_client_config_overrides_var). This is synchronous and works naturally with contextvars propagation.

  2. Wrap ThreadPoolExecutor in _ContextPropagatingExecutor that uses contextvars.copy_context().run() to propagate the calling context into worker threads.

How middleware should use it

from mcp_clickhouse.mcp_server import _client_config_overrides_var

class MyMiddleware(Middleware):
    async def on_call_tool(self, context, call_next):
        token = _client_config_overrides_var.set({"username": "other_user", "password": "..."})
        try:
            return await call_next(context)
        finally:
            _client_config_overrides_var.reset(token)

Testing

  • Updated unit tests to use ContextVar directly instead of mocked ctx.get_state
  • Added test for username/password override (the primary use case for per-user credential passthrough)
  • Existing integration tests unchanged

Related

… to thread pool

Two bugs prevented CLIENT_CONFIG_OVERRIDES_KEY from working:

1. create_clickhouse_client() called ctx.get_state() without await.
   get_state is async, so calling it from a sync function returns a
   coroutine object instead of the actual value. The override was
   silently ignored. Unit tests masked this because MagicMock.get_state
   returns values synchronously.

2. QUERY_EXECUTOR.submit() did not propagate contextvars to worker
   threads. Even if bug 1 were fixed, the override set by middleware
   in the async context would be invisible to execute_query running
   in the thread pool.

Fix:
- Replace ctx.get_state/set_state with a module-level ContextVar
  (_client_config_overrides_var) that middleware can set synchronously.
- Wrap ThreadPoolExecutor in _ContextPropagatingExecutor that uses
  contextvars.copy_context().run() to propagate the calling context
  into worker threads.
- Update tests to use the ContextVar directly instead of mocked
  ctx.get_state.

Middleware should now use:
  from mcp_clickhouse.mcp_server import _client_config_overrides_var
  token = _client_config_overrides_var.set({...})
  try:
      result = await call_next(context)
  finally:
      _client_config_overrides_var.reset(token)
@CLAassistant

CLAassistant commented Apr 3, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants