Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ serde_json = { version = "1.0", features = ["preserve_order"] }
uuid = { version = "1.0", features = ["v4", "serde"] }

# duroxide integration
duroxide = "=0.1.29"
# Using git dependency on pinodeca/continue-parent-link branch which preserves the
# parent link when a sub-orchestration calls continue_as_new, unblocking the
# sub-orchestration approach for df.loop.
# Compatibility: duroxide-pg 0.1.34 has been verified to compile and run correctly
# against this branch (same public API as 0.1.29 — PR #31 is a runtime-only change).
# Once the branch is merged and a new duroxide release is published, revert both
# entries back to crates.io version pins as a compatible pair.
duroxide = { git = "https://github.com/microsoft/duroxide.git", branch = "pinodeca/continue-parent-link" }
duroxide-pg = "=0.1.34"
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }

Expand All @@ -61,6 +68,11 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies]
pgrx-tests = "=0.16.1"

# Override the crates.io duroxide with the git branch for both the direct dependency
# and duroxide-pg's transitive dependency on duroxide.
[patch.crates-io]
duroxide = { git = "https://github.com/microsoft/duroxide.git", branch = "pinodeca/continue-parent-link" }

[profile.dev]
panic = "unwind"

Expand Down
9 changes: 9 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,15 @@ SELECT df.start(
> )
> ```

### How Loops Execute

Each loop iteration advances via *continue-as-new*, which restarts the loop with fresh state while preserving durability. Where that restart happens depends on whether the loop is the **root** of the function:

- A **root loop** (the outermost node, e.g. `df.start(df.loop(...))` or the `@>` prefix) runs inline on the function's own orchestration. There is no surrounding work to preserve, so each iteration simply restarts the function.
- A **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.

This is transparent to your workflow; it only affects observability. The child sub-orchestration is an internal durable instance: it does **not** appear in `df.list_instances()` (which lists only the instances you started with `df.start()`). Instead, the loop node's status in `df.instance_nodes()` / `df.explain()` reflects the child's progress, so you observe the loop through its parent instance as usual.

### Stopping a Loop Externally

```sql
Expand Down
16 changes: 12 additions & 4 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,10 +425,18 @@ Return columns:
**`status_details` JSON contract.** Written by the worker through the
`update-node-status` activity and stored verbatim in `df.nodes.status_details`:

- `execution_id` — the node's full segmented execution path, e.g.
`a1b2c3d4::1::7f9a0012::1`. Parse it positionally: the second `::`-token is the
root loop generation (used to detect superseded loop iterations), and the
trailing segments encode `JOIN`/`RACE` sub-orchestration lineage.
- `execution_id` — the node's full execution stamp, `{instance_path}::{generation}`,
e.g. `a1b2c3d4::1::7f9a0012::2`. The **last** `::`-token is the generation
(continue-as-new count) of the orchestration that transitioned the node, and the
preceding `{instance_path}` is that orchestration's instance id. `instance_path`
encodes sub-orchestration lineage: it starts with the root function instance id and
appends a `::{parent_generation}::{branch_or_loop_node_id}` segment for each nested
`JOIN`/`RACE` branch and each non-root `df.loop()` (which runs as its own child
sub-orchestration). Instance ids and node ids are 8-char hex and never contain `::`,
so the path is unambiguous. Supersession is evaluated **per scope**: a node is
superseded when a newer generation exists for its own `instance_path`, or when any
ancestor scope in its path has advanced to a newer generation. For a plain root-level
loop this reduces to the second `::`-token being the loop generation.

`inferred_status` and `inferred_status_from_ancestor_id` are **computed at read
time** and are not stored in `df.nodes.status_details`.
Expand Down
11 changes: 7 additions & 4 deletions sql/pg_durable--0.2.3--0.2.4.sql
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,13 @@ CREATE INDEX idx_instances_label ON df.instances(label, created_at DESC, id) WHE
-- Upgrade ordering (in-flight instances): the worker's orchestration history
-- changed shape in this release -- update_node_status activity inputs gained an
-- execution_id field, and JOIN/RACE branch sub-orchestrations now use
-- deterministic composed instance ids instead of auto-generated ones. duroxide
-- replays by exact equality on recorded inputs/ids, so instances in flight across
-- the upgrade cannot resume; drain or recreate them before upgrading (the same
-- constraint documented for issue #129).
-- deterministic composed instance ids instead of auto-generated ones. Non-root
-- df.loop() nodes also run as their own child sub-orchestration (with a
-- deterministic composed instance id) that stamps the loop node directly, instead
-- of the parent stamping it inline. This adds no new DDL -- status_details already
-- covers the loop node's stamps. duroxide replays by exact equality on recorded
-- inputs/ids, so instances in flight across the upgrade cannot resume; drain or
-- recreate them before upgrading (the same constraint documented for issue #129).
-- ============================================================================
ALTER TABLE df.nodes ADD COLUMN status_details JSONB;

Expand Down
Loading
Loading