Structured concurrency for bocpy: scoped `behaviors()` + `Terminator`
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 184
- Forks
- 9
- PR merge metrics
- No merged PRs in 30d
Description
Problem
bocpy today gives you @when(*cowns) and a single process-global rundown: wait() blocks until everything in the process has quiesced. That coarse rendezvous is fine for scripts and demos, but it has two sharp limits:
- You can't wait for just your work. There's no way to say "block until this chunk of behaviors finishes" without also waiting for every unrelated behavior the same process happens to be running.
- You can't wrap bocpy code in a synchronous API. Exposing behaviors and cowns to bocpy-aware consumers works beautifully. But if you want to hand a plain, synchronous function to a non-bocpy caller -- one that internally schedules behaviors and returns a finished result -- your only rendezvous is
wait()/quiesce(), both of which drain the entire process, not just your call's work. So you can't cleanly build a blocking facade over bocpy internals.
We want structured concurrency: a first-class way to scope a unit of work, wait for exactly that work (including anything it transitively spawns), and get a result back -- without disturbing the rest of the process.
Proposed surface
A behaviors() scope
from bocpy import Cown, when, behaviors
c = Cown(0)
with behaviors() as scope:
@when(c)
def increment(c):
c.value += 1
@when(c) # transitively belongs to `scope`
def announce(c):
print(f"c is {c.value}")
# The `with` block exits only once both behaviors have finished.
# Behaviors running elsewhere in the process keep going undisturbed.
Work scheduled from inside the scope -- even a deep chain of @when callbacks hopping across worker sub-interpreters -- belongs to the same scope, so the block correctly waits for all transitive work.
An explicit Terminator
The scope handle is a first-class object you can also build and pass by hand:
from bocpy import Terminator, when
t = Terminator()
@when(c, terminator=t)
def isolated(c):
...
assert t.wait(timeout=2.0)
A result value out of the scope
Pass the scope as one of a behavior's cowns and assign scope.value:
with behaviors() as scope:
@when(a, b, scope)
def summarise(a, b, scope):
scope.value = a.value + b.value
print(scope.value) # None if nothing wrote it
scope is acquired like any other cown, so the write is race-free and committed by the time the block exits. Single slot, last-writer-wins; the same discipline makes reductions (scope.value = (scope.value or 0) + k) safe.
Optional per-scope snapshots
with behaviors(stats=True, noticeboard=True) as scope:
do_pipeline()
scope.stats # scheduler-stats delta for just this scope's window
scope.noticeboard # noticeboard snapshot taken at quiescence
What stays the same
@when,Cown,whencall,send/receive, and the noticeboard keep their exact signatures and semantics.- Omitting
terminator=/ not opening a scope -> today's process-global behavior. - The public C ABI is untouched; downstream C extensions need no changes.
Guard rails (behavior changes to be aware of)
- Calling
wait()/quiesce()inside awith behaviors():block raises instead of silently deadlocking. - A user
Terminatorthat is still alive blocks the next runtimestart(). - A behavior body declaring a parameter literally named
terminatorraises at decoration time (reserved name).
Feedback welcome
- Naming:
behaviors()vs. something likescope()/nursery()?scope.valuevs.scope.result? - Should
stats/noticeboardsnapshots ship in the first cut, or land as a follow-up? - Anything in the guard rails above that would surprise you in real code?
The design is written up in full in PLAN.md on the branch -- this issue is the short version to drive discussion while the work is in flight.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with PLAN.md on the branch, then trace the existing @when, wait(), and quiesce() behavior described in the issue. The proposed work spans behaviors() scopes, Terminator propagation, result values, and optional snapshots across worker sub-interpreters. Done means the agreed design is implemented without changing existing APIs, with the guard rails and transitive waiting behavior covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- distributed-systems
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100