Skip to main content

Critical Section

View as Markdown

Use a Critical Section when parallel branches of one Flow must take turns on a sequence of Steps. Only one branch may be inside the sequence at a time.

An example is several reviewers who edit the same contract. Each edit takes three Steps. The first takes the turn. The second makes the change. The third saves it and hands the turn on. Two edits must never interleave.

Why not an Attribute lock​

An Attribute lock, such as ExecuteLockAttributes, covers one Step method. It does not cover a WaitFor, and it does not span several Steps. Use a lock when the protected work fits in one Execute. Use a token when the section spans Steps.

How it works​

A dedicated Channel holds exactly one message. That message is the token.

  • One Step publishes the token once, before any branch enters.
  • A handoff Step waits for the token with ForOne. One message satisfies one wait, so only one branch gets it. The other branches keep waiting.
  • The handoff Step does no protected work. Its Execute only moves to the first Step that does.
  • The last Step of the section publishes the token back. One waiting branch consumes it and enters.
Pseudocode
StartContractEdits.execute(edits)
ContractEditToken.publish("contract")
goToMany(for each edit: EnterContractEdit(edit), FinishContractEdits(count))

EnterContractEdit.waitFor -> Until(ContractEditToken.ForOne())
EnterContractEdit.execute -> goTo(EditContract) // handoff only, no protected work
EditContract.execute -> edit the contract; goTo(SaveContractEdit)
SaveContractEdit.execute -> save the edit; ContractEditToken.publish("contract"); deadEnd()

ReleaseContractEditToken.execute -> ContractEditToken.publish("contract"); deadEnd()
// the ExecuteFailure route of EnterContractEdit, EditContract, and SaveContractEdit

Dex does not promise which waiting branch gets the token next. Do not rely on an order of entry.

Every exit returns the token​

The token is an ordinary message. A branch that leaves the section without publishing it back blocks every other branch forever. List each way out of the section, and publish the token on each one.

  • Normal exit. The last Step of the section publishes the token.
  • Execute failure. Route it with ExecuteFailure to a Step that publishes the token. Configure that route on every Step of the section. In the example, EnterContractEdit, EditContract, and SaveContractEdit all move to ReleaseContractEditToken.
  • WaitFor failure. When a later Step of the section has its own WaitFor, set its WaitForFailure policy to proceed to its Execute. That Execute sees the failure and publishes the token.
  • Early exit. An Execute that leaves the section with a business decision publishes the token in the same Execute.
  • Cancellation of the handoff. Canceling the handoff Step is safe, because it does no protected work. If Dex cancels it after its wait consumed the token but before Dex saved its Execute result, Dex returns the token to the front of the Channel. Another branch enters.
  • Cancellation inside the section. Never cancel a Step that does protected work. A canceled Step publishes nothing, and its failure route does not run. The Step or RPC that cancels it cannot publish the token for it either. Dex decides which Steps to cancel only when it saves the result that cancels them. By then the canceled Step may already have published the token, and a second token would let two branches into the section at once. To stop a branch early, send it a stop request that its Steps check, such as an Attribute that an RPC sets. The Step that sees the request leaves through an early exit that publishes the token.
  • Closing the Flow. ForceComplete, ForceFail, a Flow timeout, and a Client StopFlow end every branch. No branch waits for the token any more.

Dex saves a method's Channel publications together with its result, or not at all. A failed attempt publishes nothing, so a retry cannot create a second token.

Dex cannot stop a canceled method's calls to external systems. A canceled Execute can keep running and writing after Dex has returned its token. This is another reason never to cancel a Step that does protected work.

Rules​

  • Publish exactly one token, in the Step that starts the branches or before. A second token lets two branches into the section at once.
  • Use the token Channel for the token only.
  • Keep the section short. Every other branch waits while one branch is inside.
  • The token's value does not matter. Keep the protected state in Attributes.

Example​

ContractEditFlow starts with a list of edits, one per reviewer. StartContractEdits publishes the token and starts one EnterContractEdit per edit, plus the join FinishContractEdits. EnterContractEdit hands off to EditContract, which records when the edit started. The join completes the Flow with every edit's record. The edit intervals never overlap.

Flags on an edit simulate the other exits. A failed handoff and an edit whose SaveContractEdit fails twice both go to ReleaseContractEditToken. A withdrawn edit leaves early from EditContract. Each of these returns the token, and the other reviewers still get their turn.

contract_edit_flow.pyPython
CONTRACT_EDIT_TOKEN = Channel("contractEditToken", str)
CONTRACT_EDIT_FINISHED = Channel("contractEditFinished", ContractEditRecord)
CONTRACT_EDIT_RETRY = RetryPolicy(
initial_interval=timedelta(seconds=1), maximum_attempts=2
)


class EnterContractEdit(Step[ContractEdit]):
"""Takes the token and hands off; it does no protected work, so canceling it is safe."""

def get_step_options(self) -> StepOptions:
return StepOptions(execute_retry=CONTRACT_EDIT_RETRY).on_execute_failure_proceed_to(
ReleaseContractEditToken
)

def wait_for(self, context: Context, edit: ContractEdit) -> Wait:
return Wait.until(CONTRACT_EDIT_TOKEN.for_one())

def execute(self, context: Context, edit: ContractEdit) -> StepDecision:
if edit.should_fail_handoff:
raise ContractEditError("contract handoff failed")
return go_to(EditContract, edit)


class EditContract(Step[ContractEdit]):
"""The first Step that works on the contract. Never cancel it."""

def get_step_options(self) -> StepOptions:
return StepOptions(execute_retry=CONTRACT_EDIT_RETRY).on_execute_failure_proceed_to(
ReleaseContractEditToken

Example: examples/python/dex_examples/patterns/critical-section/contract_edit_flow.py

Anti-patterns​

  • A boolean Attribute such as "isEditing" that Steps check and set. Nothing waits on it, and two branches can both see it clear.
  • Publishing a new token to recover from a lost one while the old one may still exist. Two tokens let two branches into the section at once.