fix(runner+gui): experiment-collision safety (C4/C5) + PWM range from value_spec (B2)#30
Merged
Merged
Conversation
Two latent concurrency risks surfaced by the device-value-semantics review (C4/C5), now that the Runner Functions buttons run graph functions. C5 — a manual function run briefly forces the flow engine RUNNING to gate exec propagation; a real experiment does the same via SessionState. They must not overlap or they corrupt each other's engine state (a manual run can make FlowEngine.start() early-return so a real experiment never runs its flow; a real stop can be immediately undone by the manual run's state restore). Now: - _run_function_async refuses when a real experiment is RUNNING/PAUSED or a start/stop is mid-flight (new GliderCore.is_experiment_busy covers the window where an experiment is starting but state hasn't flipped to RUNNING yet). - _start_async refuses to start while a manual function is in flight. C4 — FlowEngine.clear() (New/Open) could not see a running function: the runner's body ran in a private task never registered with the engine. Add FlowEngine.track_task(); the runner registers its body task so clear() cancels it, and execute() returns a clean False when cancelled out from under it rather than leaking a CancelledError or driving a torn-down graph. Tests: refusal in both directions + starting-in-progress window; body task is tracked and an external cancel returns False. core+nodes+gui suites green (446).
The device-value-semantics review found two desktop surfaces still hardcoding
PWM 0-255 while the node now clamps to the device's real value_spec('set') — so
on a 12-bit board (0-4095) neither surface could author what the node accepts.
- Graph editor Output-node property panel (node_editor_controller): the PWM
spin's range and label now come from the bound device's value_spec('set')
(falling back to 0-255 when there is no spec), matching what the node writes.
- Desktop DeviceControlPanel: the PWM spinbox/slider range now follows the
selected device's value_spec('set') too.
Tests: a 12-bit device yields a 0-4095 control on both surfaces (and a saved
value the old cap would have clamped is preserved); no-spec devices fall back
to 0-255. GUI suite green (203).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Runner function-run safety (review follow-ups C4 + C5)
Follows the device-value-semantics feature (merged, #27–#29). The multi-agent review of that feature flagged two latent concurrency risks that only matter now that the Runner "Functions" buttons execute graph functions on demand. This PR closes both.
C5 — a manual run and a real experiment must not overlap
A manual function run briefly forces the flow engine into
FlowState.RUNNINGto open the exec-propagation gate, and restores the prior state after. A real experiment does the same throughSessionState. If they overlap:FlowEngine.start()early-return (state already RUNNING), so a real "Start Experiment" runs its recorders but never runs the flow;finallyrestoring RUNNING.Fixes:
_run_function_asyncrefuses to run when a real experiment isRUNNING/PAUSED, or when a start/stop is mid-flight — the newGliderCore.is_experiment_busy(experiment-lock held) covers the window where an experiment is starting butstatehasn't flipped toRUNNINGyet._start_asyncrefuses to start while a manual function is in flight.A manual run changes
FlowStatebut neverSessionState, soself._core.statecleanly distinguishes a real experiment from a manual run.C4 — New/Open must actually stop a running function
FlowEngine.clear()(New/Open) cancels_running_tasks, but the runner's body ran in a privateasyncio.ensure_futuretask that was never registered — soclear()couldn't see it, and it kept driving hardware against a torn-down graph until its own 60s timeout.Fix: add
FlowEngine.track_task(); the runner registers its body task, soclear()cancels it.execute()returns a cleanFalsewhen cancelled out from under it (guardsrun.cancelled()beforerun.result()), instead of leaking aCancelledError.Tests
test_main_window_function_run.py— manual run refused during a real experiment and during a start/stop-in-progress;_start_asyncrefused while a function runs.test_flow_function_runner.py— the body task is registered with the engine and an external cancel yields a cleanFalse;track_taskletsclear()cancel a registered task.core+nodes+gui suites green (446 passed).
Residual (not addressed here, single-operator kiosk)
The two guards close the realistic sequential races. A sub-millisecond simultaneous tap of a Function button and Start still has a tiny window (both entry points set their flags inside separately-scheduled async tasks); on a single-operator touchscreen this is negligible and would otherwise need a heavier lock-based redesign.
Also in this branch — B2: PWM controls follow the device value_spec
The same review found two desktop surfaces still hardcoding PWM
0-255while the node now clamps to the device's realvalue_spec('set')— so on a 12-bit board (0-4095) neither could author what the node accepts:node_editor_controller): the PWM spin's range/label now come from the bound device'svalue_spec('set')(falling back to0-255when there is no spec).DeviceControlPanel: the PWM spinbox/slider range now follows the selected device'svalue_spec('set')too.Tests: a 12-bit device yields a
0-4095control on both surfaces (and a saved value the old cap would have clamped is preserved); no-spec devices fall back to0-255.