You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
not change where the loop is hosted. When it is `true`, each body iteration
1198
+
runs as a child orchestration. After a successful body iteration, the child's
1199
+
results are merged into the parent result map before the optional condition is
1200
+
evaluated. After a consumed typed application failure from a body activity,
1201
+
the condition is skipped and the parent starts the next iteration. Condition
1202
+
failures, malformed graph or child data, unrecognized child errors, child-ID
1203
+
collisions, and child-runtime or infrastructure failures remain fatal.
1191
1204
1192
1205
This is useful for scheduled maintenance such as a pg_textsearch-style indexer:
1193
1206
@@ -1324,7 +1337,7 @@ Each loop iteration advances via *continue-as-new*, which restarts the loop with
1324
1337
- A fail-fast **root loop** (the function graph's outermost node, e.g. `df.start(df.loop(...))` or `df.start(@>'SELECT work()')`) runs inline on the function's own orchestration. There is no surrounding work to preserve, so each iteration simply restarts the function. The `@>` operator does not make a loop root by itself: `df.seq('SELECT setup()', @>'SELECT work()')` is non-root because the sequence is the graph root.
1325
1338
- A fail-fast **non-root loop** (a loop with prefix/suffix nodes, or one nested inside a `df.if()`, JOIN (`&`), or RACE (`|`) branch) runs as its own **child sub-orchestration**. Only the loop body restarts on each iteration — any work *before* the loop runs exactly once and is never re-executed, and a loop nested in a parallel branch gets its own durable instance.
1326
1339
- Fail-fast iterations execute inline within whichever orchestration hosts the loop. Explicit `continue_on_failure => false` keeps this placement; it does not force a non-root loop into the function's root orchestration.
1327
-
- A loop with `continue_on_failure => true` runs every iteration in a child orchestration. The loop parent remains `running` after a body activity returns an application failure and proceeds to the next iteration. Unrecognized child errors and graph, protocol, runtime, or infrastructure failures remain fatal.
1340
+
- A loop with `continue_on_failure => true` runs every body iteration in a child orchestration. After a successful body iteration, the child's results are merged into the parent result map before the optional condition is evaluated. After a consumed typed body activity failure, the condition is skipped and the loop parent proceeds to the next iteration. Condition failures and unrecognized child, graph, protocol, runtime, or infrastructure failures remain fatal.
1328
1341
1329
1342
These child sub-orchestrations are internal durable instances and do **not**
1330
1343
appear in `df.list_instances()`, which lists only instances started with
Copy file name to clipboardExpand all lines: docs/ARCHITECTURE.md
+34-11Lines changed: 34 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -477,31 +477,36 @@ of every handler having to recognise an in-band JSON break sentinel:
477
477
pubenumNodeError {
478
478
/// df.break() fired. Carries the break value. Caught only by execute_loop_node.
479
479
Break(String),
480
-
/// A genuine failure. Surfaces as a failed instance.
480
+
/// An expected workflow activity failure.
481
+
Application(String),
482
+
/// A graph, protocol, configuration, or runtime failure.
481
483
Failure(String),
482
484
}
483
485
484
486
pubtypeNodeResult=Result<String, NodeError>;
485
487
486
-
// Any `?` on an existing Result<_, String> auto-converts the error to Failure, so
487
-
// activity calls and helpers need no per-call changes.
488
+
// Structural/configuration helpers still convert String errors to Failure.
488
489
implFrom<String> forNodeError {
489
490
fnfrom(e:String) ->Self {
490
491
NodeError::Failure(e)
491
492
}
492
493
}
493
494
```
494
495
495
-
`execute_loop_node` is the only handler that catches `NodeError::Break` (turning it into the
496
-
loop's `Ok` result); `NodeError::Failure` keeps propagating. The orchestration boundary
497
-
functions (`execute` / `execute_subtree`) still return `Result<String, String>` because they
498
-
are registered with duroxide:
496
+
SQL, HTTP, and multipart activity scheduling boundaries explicitly map activity
497
+
errors to `NodeError::Application`; structural/configuration helper errors use
498
+
`NodeError::Failure`. `execute_loop_node` is the only handler that catches
499
+
`NodeError::Break` (turning it into the loop's `Ok` result). The orchestration
500
+
boundary functions (`execute` / `execute_subtree`) still return
501
+
`Result<String, String>` because they are registered with duroxide:
499
502
500
503
-`execute`: an uncaught top-level `Break` becomes a clear `Err` ("df.break() was called
501
504
outside of a loop"), so the instance fails instead of completing with a sentinel value.
502
-
-`execute_subtree` (used by JOIN/RACE branches): a `Break` is carried out-of-band in the
503
-
subtree envelope's `control` field and re-raised as `NodeError::Break` by
504
-
`parse_subtree_envelope` in the parent orchestration.
505
+
-`execute_subtree`: a `Break` is carried out-of-band in the subtree envelope's
506
+
`control` field. `NodeError::Application` is encoded as a namespaced,
507
+
serde-tagged subtree failure so application classification survives nested
508
+
JOIN, RACE, and LOOP boundaries. `NodeError::Failure` remains an unrecognized
509
+
child error and propagates fatally.
505
510
506
511
The orchestration walks the graph recursively:
507
512
@@ -734,7 +739,25 @@ Loops use duroxide's `continue_as_new` to avoid unbounded history growth. Their
734
739
735
740
`execute_subtree` is therefore structurally identical to `execute_function_graph`: both root an execution context at their own node and host an inline root loop. They differ only in the input envelope they re-enter with on `continue_as_new` (`FunctionInput` vs `SubtreeInput`), in the fact that only the root orchestration touches instance-level status, and in where their graph comes from — `execute_function_graph` loads it from `df.nodes` on its first generation, while `execute_subtree` receives it inline from its parent. The graph is loaded exactly once per instance and then carried inline through every child input and every `continue_as_new` generation, so `submitted_by` is fixed for the instance's lifetime and a post-start `df.nodes` tamper is never read. Role deletion and privilege revocation are still enforced per node execution, by connecting *as*`submitted_by` for SQL and by re-checking `EXECUTE` privilege per HTTP request.
736
741
737
-
Both paths call `run_loop_iteration`, which executes the body, catches `NodeError::Break`, evaluates the optional post-body condition, and propagates `NodeError::Failure`. A child stamps its LOOP node `running` on each generation and `completed` or `failed` on exit; because `continue_as_new` returns a future that never resolves, a continuing generation never stamps a terminal status. If a live loop loses a RACE, the parent records the loop node as terminal `failed` with a cancellation reason because duroxide cancellation stops the child before it can run its own terminal stamp.
742
+
Fail-fast loops call `run_loop_iteration`, which executes the body inline,
743
+
catches `NodeError::Break`, evaluates the optional post-body condition, and
744
+
propagates both application and non-application failures. A loop configured
745
+
with `continue_on_failure => true` instead calls
746
+
`run_failure_isolated_body`: each body iteration runs as a fresh
747
+
`execute_subtree` child. On success, the subtree envelope is parsed and its
748
+
named results are merged into the parent result map before the parent evaluates
749
+
the optional condition. On a structurally encoded body application failure,
750
+
the parent consumes the failure, skips the condition because body results may
751
+
be absent, and advances to the next generation. Condition failures, malformed
752
+
envelopes or graph data, child-ID collisions, and other unrecognized
753
+
infrastructure/runtime failures remain fatal.
754
+
755
+
A child stamps its LOOP node `running` on each generation and `completed` or
756
+
`failed` on exit; because `continue_as_new` returns a future that never
757
+
resolves, a continuing generation never stamps a terminal status. If a live
758
+
loop loses a RACE, the parent records the loop node as terminal `failed` with a
759
+
cancellation reason because duroxide cancellation stops the child before it
760
+
can run its own terminal stamp.
738
761
739
762
Node status stamps contain the full composed orchestration lineage: `{root_instance}::{generation}::{child_node}::{generation}...`. Read-time inference and the write fence walk that lineage so stale writes and superseded nested branches are evaluated at every ancestor generation.
0 commit comments