Online edition for BLOGE
0.9.8-RC1· facts verified 2026-09-15 · 中文
Chapter 12 — Waiting for the World
Promise: You will let a graph release execution resources while it waits for a human approval or webhook, read a real suspended-and-resumed result, and recognize bad correlation keys, duplicate events, and the current
0.9.8-RC1semantic boundary.
Learning goals
By the end of this chapter, you can:
- Explain the resource difference between blocking, polling, and suspension in business terms.
- Read
GraphResultto tell failure, completion, and waiting apart. - Choose
waitorawaitfrom the shape of the external response. - Run a one-variable experiment for a wrong correlation key and a duplicate event.
This chapter adds one variable
The loop from the previous chapter still runs inside one active execution. This chapter adds one fact: the prerequisite for the next step is temporarily outside the system.
A support manager may approve a refund two hours later. A payment gateway may send a callback in ten minutes. A warehouse may report stock tomorrow. The graph should neither keep a worker occupied nor ask a database every few seconds. It must remember what it is waiting for and return control to the runtime.
Start with the problem: a ticket awaiting manager approval
Customer C-17 reports a delayed refund. The system creates ticket T-2048, classifies it, and notifies a manager. It may send a confirmation only after approval; no response within 48 hours enters timeout handling.
Predict the three implementation shapes before reading code:
| Approach | Resources held while waiting | Pressure on other systems | Recovery problem |
|---|---|---|---|
| Block a thread for 48 hours | Worker and in-memory state | None | A process restart erases the wait |
| Poll every 5 seconds | Schedules, connections, and queries | High | Poll progress and deduplication must recover |
| Suspend and await a signal | One waiting record | Low | Identity, state, and correlation must persist |
Suspension is not smarter sleeping. It changes the control model: execution reaches a stable boundary and returns; an external response starts the continuation later.
Run it first: observe the real suspension boundary
The book probe loads the remote examples file ticket-approval-wait.bloge, supplies minimal stand-ins for documentation Operators, and executes twice. The first run stops at the boundary; the second sends an approval signal and continues.
Install the pinned RC1 core and DSL once if they are not in your local Maven repository:
cd submodule/bloge
mvn -pl bloge-core,bloge-dsl -am -DskipTests install
Then run from the book repository:
cd probes/ch11-waiting
mvn test
On 2026-09-14, BLOGE commit cc38fbe5 produced these key lines:
boundary.isSuccess=true
boundary.isSuspended=true
boundary.suspendedNodes={waitApproval=wait}
boundary.waitApproval=SUSPENDED
boundary.sendConfirmation=CANCELLED
resume.isSuccess=true
resume.isSuspended=false
resume.waitApproval=COMPLETED
resume.sendConfirmation=COMPLETED
Read the first three lines carefully:
isSuccess=truemeans no node has failed at this boundary. It does not mean the business process is finished.isSuspended=truemeans at least one node is waiting for a signal.suspendedNodesidentifies both the node,waitApproval, and the observed suspend key,wait.
sendConfirmation=CANCELLED is not a rejected ticket. Scheduling converged at the suspension boundary, so dependent work has not run yet. It becomes COMPLETED only after the second execution sends the signal.
Observation: “successful and suspended” is a valid intermediate result. Checking only
isSuccess()reports unfinished work as complete.
The executable observation is in WaitingForWorldProbeTest.java. It is evidence for this chapter, not production durability or concurrency acceptance.
What does suspension remember?
Think of a restaurant pickup token. A customer need not occupy the counter; the restaurant remembers which order is waiting and where the result belongs.
| Real-world object | BLOGE identity | Purpose |
|---|---|---|
| One process instance | executionId | Finds the same graph execution |
| One waiting point | nodeId | Finds the resume entry |
| Pickup token | suspend/correlation key | Prevents delivery to the wrong process |
| Completed work | checkpoint/status | Avoids blindly repeating finished work |
| External answer | signal payload | Becomes the waiting node's output |
Payload without identity cannot find an execution. Execution identity without a node cannot find the continuation. Missing completed state can repeat an effect that already happened.
Two entry points: wait and await
Both use suspend-and-resume, but answer different questions.
wait: time or a direct signal decides the next step
The ticket example introduces this block:
wait waitApproval = 48h after notifyManager {
signal_key = ctx.ticketId
on_timeout {
decision = "auto-approved"
approver = "system"
}
}
Its intended meaning is: suspend after notifyManager; a human may signal the known wait point, while the timer may produce a timeout result. This shape fits a known waiting node governed by a delay or deadline.
await: an event name and business key decide the next step
A payment provider does not know BLOGE's executionId. It reports “payment confirmed for order O-42.” payment-wait.bloge expresses that correlation:
await awaitPayment {
event "payment.confirmed" where orderId = createOrder.output.orderId
timeout = 15m
on_timeout { status = "timeout" }
}
The webhook handler translates its external protocol into a runtime event:
engine.publishEvent(
"payment.confirmed",
orderId,
Map.of("status", "confirmed", "transactionId", transactionId),
webhookMessageId
);
eventName says what happened, orderId says which business instance it belongs to, and webhookMessageId identifies a duplicate delivery. Do not collapse all three identities into one string.
One picture: suspend, webhook, correlation, resume
Look for two transfers of control:
executereachesawait, records what it is waiting for, and returns that waiting fact to the caller.- A webhook calls
publishEvent; the runtime matches event name and business key, then signals the originalexecutionId + nodeIdand continues downstream.
The webhook endpoint is not a public “continue this graph function.” It authenticates the message, extracts a stable business key, supplies an idempotent message ID, and publishes an event. Correlation state owns the resume identity.
Why resume does not mean start over
A correct signal path maintains three invariants:
- Completed nodes stay completed instead of running again unconditionally.
- Signal payload becomes the waiting node's output and enters the normal downstream data flow.
- One wait can complete only once; late or duplicate messages cannot replace the first accepted result.
In the probe, Map.of("decision", "approved", "approver", "manager-7") becomes waitApproval output. sendConfirmation can then read that answer. This is the exact point where the outside world's response re-enters the graph.
Durable recovery also needs execution checkpoints, a wait store, and a reconstructable graph definition. That is the single new variable in the next chapter; this chapter observes the in-process protocol.
Break one variable: wrong correlation key
Keep the event name and payload, but change orderId from O-42 to O-24:
var matched = engine.publishEvent(
"payment.confirmed", "O-24", payload, "msg-901");
The expected observation is neither an exception nor the wrong order resuming:
matched=[]
awaitPayment=WAITING
downstream.started=false
No match must leave the original correlation waiting. Fix the webhook's key extraction and replay the correct event with a new message ID; do not mutate the waiting record to fit a bad message.
This counterexample prevents cross-order wakeups, which are usually more dangerous than a short delay in payments, logistics, and approvals.
Break one variable: duplicate the event
Now keep the correct key but deliver the same webhookMessageId=msg-902 twice. RC1's EventDeduplicationTest verifies that an identical idempotency key neither reapplies a partial event nor creates a second signal.
Focused results on 2026-09-14:
EventDeduplicationTest Tests run: 5, Failures: 0
EventCorrelationAndOrTest Tests run: 3, Failures: 0
SuspendSignalTest Tests run: 1, Failures: 0
GraphResultContractTest Tests run: 4, Failures: 0
Total Tests run: 13, Failures: 0
Duplicates are a normal webhook delivery shape. Use the sender's stable event identity. Generating a random UUID or receive timestamp at ingestion effectively disables deduplication.
RC1 boundary: stop when source intent and runtime disagree
The probe exposes a gap that the chapter must not hide. The latest remote example declares signal_key = ctx.ticketId, but runtime output is {waitApproval=wait}. At pinned RC1, WaitAwaitCompiler parses signal_key and on_timeout; its current compileWait path constructs WaitOperator only from the duration and does not bind those fields.
This chapter therefore verifies:
- The ticket graph reaches
SUSPENDEDon RC1 and resumes through a node signal. GraphResultseparates success, suspension, and downstream work not yet run.- Core tests cover event correlation, AND/OR aggregation, and duplicate events.
It does not verify:
- The companion DSL's dynamic
signal_key=T-2048is active in RC1. - Its
on_timeoutbusiness payload is injected after 48 hours. - A different process can resume after restart; that requires durable-store evidence in Chapter 13.
The remote examples POM also remains pinned to 0.3.1 and references the renamed bloge-core-ext artifact. A direct build fails dependency resolution; overriding the version to RC1 still cannot resolve that old artifact name. File presence is not proof that the entire companion repository passes on RC1.
Engineering rule: When DSL, compiler, and runtime observation disagree, narrow the claim to the observed behavior at a fixed commit and record the gap.
Transfer the model to your system
Choose one action whose prerequisite lives outside your system: patient consent, customs clearance, or a candidate's signed offer. Fill in four lines:
| Item | Your answer |
|---|---|
| Waiting identity | How executionId and waiting node survive |
| Business correlation key | Stable field that routes the answer correctly |
| Duplicate identity | Sender-provided message or event ID |
| Stop condition | Timeout and the owner authorized to decide its result |
Do not implement yet. Let the business and platform owners confirm these four lines. If nobody authorized “auto-approve on timeout,” leave it undefined instead of letting the framework invent policy.
Lab: replace polling with an event wait
Choose one polling flow and change one variable:
- Input: one stable business key, one external event, and one acceptable timeout.
- Scope: replace only the polling node with
await; keep downstream business nodes intact. - Run: send a wrong key, a correct key, then the same message ID again.
- Artifact: keep all three observations, the key's origin, and the timeout owner.
- Stop if the business cannot define timeout behavior or the sender cannot provide a stable duplicate identity.
Recap
- Suspension releases active execution resources while preserving resume identity and state.
waitis shaped around time or direct signals;awaitaround event name and business correlation.isSuccess=trueandisSuspended=truemay both be true.- A wrong key stays unmatched; a duplicate event is absorbed by idempotency.
- Example intent, compiler behavior, and runtime output must agree before the claim expands.
The next chapter adds one variable: Chapter 13 — Durable Execution persists the facts needed for an eligible process to continue the same execution after restart.
Experiment acceptance card
- Expected and observed: wait returns suspended; the correct signal resumes the same execution.
- Failure and recovery: Send a wrong key or duplicate event; restore the key and handle duplicates idempotently.
- Proof boundary: Proves suspend/signal/resume, not a configured durable store.
- Exercise contract: Ticket event; change one correlation key; deliver suspended and resumed results; stop when the wrong key does nothing and the right key advances once.
Exact fact entry points
WaitingForWorldProbeTest.java— this chapter's suspend/resume observation.ticket-approval-wait.bloge— ticket story DSL.payment-wait.bloge— webhook correlation DSL.GraphResult.java— success, suspension, and node-status contract.EventCorrelationAndOrTest.java— AND/OR resume facts.EventDeduplicationTest.java— duplicate and early-event boundaries.
Coding Agent: Open the versioned task guide.