Skip to content

Add per-connection threadpool websocket callback executor.#3826

Open
T4rk1n wants to merge 6 commits into
devfrom
feat/per-websocket-threadpool
Open

Add per-connection threadpool websocket callback executor.#3826
T4rk1n wants to merge 6 commits into
devfrom
feat/per-websocket-threadpool

Conversation

@T4rk1n

@T4rk1n T4rk1n commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@camdecoster camdecoster self-assigned this Jun 25, 2026

@camdecoster camdecoster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any protections in place for the case of a server getting hammered and increasing the thread count continuously (when an app fires four sync callbacks right away)?

Comment thread dash/backends/_fastapi.py Outdated
pending_get_props: Dict[str, queue.Queue] = {}
# Track pending get_props requests. Values are queue.Queue (threadpool /
# sync path) or asyncio.Future (event-loop / async path).
pending_get_props: Dict[str, Any] = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this type be tightened up a bit?

Comment thread dash/backends/_fastapi.py

# The connection's event loop, used to dispatch async callbacks as
# tasks and to resolve get_props futures.
loop = asyncio.get_running_loop()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of enabling debug mode for the loop here (and in a similar place for Quart)? You could use the existing debug config variable to turn this on. This way, users would get warning when a callback is executing slowly. You could also add a way for users to configure loop.slow_callback_duration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, maybe as a follow up.

Comment thread dash/backends/_fastapi.py

# The connection's event loop, used to dispatch async callbacks as
# tasks and to resolve get_props futures.
loop = asyncio.get_running_loop()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option (maybe in tandem) would be to add a watchdog thread outside of the pool that could keep an eye out for bottlenecked callbacks.

Comment thread dash/backends/_fastapi.py Outdated
Comment on lines +741 to +743
executor = self.create_callback_executor(
getattr(dash_app, "_websocket_max_workers", 4)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of moving this inside the try below (along with the assignment to sender_task? If an error occurs before the try, the executor is never shut down.

Comment thread dash/backends/_fastapi.py Outdated
pending_callbacks: Dict[str, concurrent.futures.Future] = {}
# Create a per-connection thread pool executor so that long-lived
# callbacks on one connection cannot starve worker threads for others.
# pylint: disable=protected-access

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete these stale pylint comments.

@sonarqubecloud

Copy link
Copy Markdown

@camdecoster camdecoster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems good overall. I think the debug addition would be good to add later, but this addresses the underlying issue.

@@ -209,7 +215,7 @@ def get_callback_executor(
return self._callback_executor

def shutdown_executor(self, wait: bool = True) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function needed anymore?

Comment thread dash/backends/_fastapi.py
# Create WebSocket callback instance
# Async callbacks (incl. persistent ones) run as tasks on
# the event loop; sync callbacks go to the threadpool.
# pylint: disable=protected-access

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can still remove these stale pylint comments.

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.

WebSocket callback concurrency is bounded by an unconfigurable thread pool

2 participants