Skip to content

feat(streams): operator control server - #353

Open
bmcquilkin-sentry wants to merge 6 commits into
pod-management-v2from
bmcquilkin/runtime/control-server
Open

feat(streams): operator control server#353
bmcquilkin-sentry wants to merge 6 commits into
pod-management-v2from
bmcquilkin/runtime/control-server

Conversation

@bmcquilkin-sentry

Copy link
Copy Markdown
Contributor

Basic control server for the operator to use for blue-green deployments (readyz, status start, stop). Adds --control-port option to runner to determine if the process should use deployment mode (autostart) or operator mode (wait for /start).

@bmcquilkin-sentry
bmcquilkin-sentry requested a review from a team as a code owner July 25, 2026 23:54
Comment thread sentry_streams/sentry_streams/control_server.py Outdated
Comment thread sentry_streams/sentry_streams/control_server.py Outdated
Comment thread sentry_streams/sentry_streams/control_server.py Outdated
Comment thread sentry_streams/sentry_streams/control_server.py Outdated
Comment thread sentry_streams/sentry_streams/control_server.py Outdated
Comment thread sentry_streams/sentry_streams/control_server.py Outdated
Comment thread sentry_streams/sentry_streams/control_server.py Outdated
Comment thread sentry_streams/sentry_streams/control_server.py Outdated
@bmcquilkin-sentry
bmcquilkin-sentry force-pushed the bmcquilkin/runtime/control-server branch from b551a1b to f42d312 Compare July 30, 2026 21:48
@bmcquilkin-sentry
bmcquilkin-sentry changed the base branch from bmcquilkin/arroyo/external-shutdown to pod-management-v2 July 30, 2026 21:48
@bmcquilkin-sentry
bmcquilkin-sentry force-pushed the bmcquilkin/runtime/control-server branch from f42d312 to 9b45422 Compare July 30, 2026 21:50

@fpacifici fpacifici left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please see the comments in line

Comment thread sentry_streams/sentry_streams/adapters/stream_adapter.py Outdated
Comment thread sentry_streams/sentry_streams/adapters/stream_adapter.py Outdated
Comment thread sentry_streams/sentry_streams/adapters/stream_adapter.py Outdated
Comment thread sentry_streams/sentry_streams/adapters/stream_adapter.py Outdated
Comment thread sentry_streams/sentry_streams/adapters/stream_adapter.py Outdated
Comment thread sentry_streams/sentry_streams/control.py Outdated
Comment thread sentry_streams/sentry_streams/runner.py Outdated
/// The ProcessorHandle allows the main thread to stop the StreamingProcessor
/// from a different thread.
handle: Option<ProcessorHandle>,
handle: Mutex<Option<ProcessorHandle>>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do you need a Mutex here ?

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.

Comment on lines +214 to +217
// The GIL is released around the run loop so that a Python control
// thread can call shutdown while this consumer is running:

if let Err(e) = processor.run() {
tracing::error!("StreamProcessor error: {:?}", e);
sentry::capture_error(&e);
let run_error = py.detach(|| match processor.run() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So here is the issue. processor.run() takes the GIL in several places. Did you see this code running successfully with a real pipeline that runs python code ?

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.

Yes, we need detach because otherwise the control server in Python would be locked and not respond.

Comment on lines +208 to +212
*self.handle.lock().unwrap() = Some(processor.get_handle());

if self.shutdown_requested.load(Ordering::Acquire) {
processor.get_handle().signal_shutdown();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don;'t think you need this. You can get multiple handles from the processor. You do not need to share it between run and shutdown.

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.

The run call has a reference to the processor on one thread, shutdown does not have a reference (processor is a local to run) and is on another thread. Run needs to use a mutex to store the handle so that shutdown can access it.

Comment on lines +90 to +94

serve_thread = threading.Thread(
target=_serve_until_shutdown,
name="control-server",
daemon=True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The pipeline-signal thread is non-daemon, which can prevent the process from exiting cleanly if the controller.request_stop() call hangs during shutdown.
Severity: HIGH

Suggested Fix

Change the shutdown_thread to be a daemon thread by setting daemon=True during its initialization. This will allow the Python process to exit even if this thread hangs, as daemon threads are abruptly stopped at shutdown.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: sentry_streams/sentry_streams/runner.py#L90-L94

Potential issue: The `shutdown_thread` in `runner.py` is created as a non-daemon thread
(`daemon=False`). This thread's purpose is to call `controller.request_stop()` upon
receiving a shutdown signal. The main thread waits for this thread to complete using
`join()` with a timeout. However, if the `controller.request_stop()` call hangs (for
instance, due to a deadlock), the `join()` will time out, but the non-daemon thread will
persist. A running non-daemon thread prevents the Python process from exiting, which
will require an external kill signal to terminate the service.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a7b4cda. Configure here.

)
serve_thread.start()
else:
controller.request_start()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Startup signal race crashes process

Medium Severity

In deployment mode, the shutdown thread is started before request_start. If SIGINT/SIGTERM arrives in that window, request_stop moves the runtime to STOPPED, then request_start raises RuntimeStateError (cannot restart runtime that is stopped). The process exits with a traceback instead of a clean shutdown, and this path sits outside the try/finally cleanup.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a7b4cda. Configure here.

@bmcquilkin-sentry
bmcquilkin-sentry force-pushed the bmcquilkin/runtime/control-server branch from a7b4cda to 4937081 Compare July 31, 2026 20:12
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