Skip to content

Structured concurrency for bocpy: scoped behaviors() + Terminator #47

Description

@matajoh

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:

  1. 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.
  2. 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 a with behaviors(): block raises instead of silently deadlocking.
  • A user Terminator that is still alive blocks the next runtime start().
  • A behavior body declaring a parameter literally named terminator raises at decoration time (reserved name).

Feedback welcome

  • Naming: behaviors() vs. something like scope() / nursery()? scope.value vs. scope.result?
  • Should stats / noticeboard snapshots 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    designDesign conversations

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions