Skip to content

Add failure-isolated loop iterations - #377

Merged
Todd J. Green (tjgreen42) merged 12 commits into
mainfrom
loop-continue-on-failure
Sep 10, 2026
Merged

Add failure-isolated loop iterations#377
Todd J. Green (tjgreen42) merged 12 commits into
mainfrom
loop-continue-on-failure

Conversation

@tjgreen42

@tjgreen42 Todd J. Green (tjgreen42) commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Long-running scheduled workflows should survive an isolated iteration failure instead of terminating permanently. This is motivated by the pg_textsearch compaction backstop in timescale/pg_textsearch#476, where one failed compaction must not prevent later scheduled attempts.

Experimental: The continue_on_failure syntax is subject to change in future releases.

Summary

  • add the unified df.loop(body, condition DEFAULT NULL, continue_on_failure DEFAULT false) API for infinite and conditional loops
  • run each opted-in body iteration as a child orchestration; consume errors returned by body SQL, HTTP, and multipart activities, skip the condition for that iteration, and continue from the top
  • merge named results after successful bodies so the condition can use them; keep condition failures, including nested-loop conditions, fatal
  • keep malformed graph/protocol data, child-ID collisions, and orchestration/runtime failures fatal
  • deterministically prioritize fatal JOIN sibling failures over df.break() and recoverable activity failures inside failure-isolated iterations, while preserving historical first-error behavior elsewhere
  • raise the loop iteration backstop from 100,000 to 8,388,608 (2^23), approximately 80 years at five-minute intervals
  • update the SQL upgrade path, documentation, examples, architecture description, and pg_durable SQL skill

Upgrade note

Histories below iteration 100,000 replay unchanged. A loop that already recorded the old terminal-failure path at that boundary cannot replay under the new binary; drain such long-running loops before upgrading when continuity is required.

Testing

  • 327 unit tests passed
  • 12 loop-focused E2E tests passed
  • all 109 upgrade tests passed

Distinguish body activity failures from child runtime and protocol faults so continue-on-failure loops only consume expected application failures.

Reject conditional loops combined with failure continuation, document the new loop policy and lifetime, and pin deterministic child input serialization.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not at the point of pg_durable understanding where I can confidently comment as to whether the change makes sense to do it this way. Really, the only thing I can comment on is general style/approach in the abstract. Because of this, I'm just going to submit this review as "comment" rather than "approving" but it seems mostly viable (assuming you look at the genuine problems and either address them, or don't think we need to).

I am glad I took a look though, since I hadn't considered handling signature changes that way. It's still not practical for cases like mine, (df.http is GRANT city, and it's apparently hard to migrate those properly), but it's a really good technique to know.

I also asked a robot to take a look at it on my end, and it surfaced two things, both of which seem to make sense to me, but I couldn't say for sure.

Comment thread src/dsl.rs
Comment thread .agents/skills/pg-durable-sql/SKILL.md
Comment thread src/orchestrations/execute_function_graph.rs
Comment thread src/orchestrations/execute_function_graph.rs Outdated
Comment thread src/orchestrations/execute_function_graph.rs Outdated
Thom Chiovoloni (thomcc-work) added a commit to thomcc-work/pg_durable that referenced this pull request Sep 10, 2026
Several of my tasks require passing additional options to `df.http` and
`df.http_multipart`. Unfortunately, adding new parameters to these
functions is Hard. The normal approach (for example taken in microsoft#377 with
the `df.loop`) is to keep the old function around, renamed (but with the
same wrapper name). This is fine (and even avoids issues when
`ALTER EXTENSION UPDATE` is not run), but `df.http` and
`df.http_multipart` are functions that will likely have a bunch
of `GRANT`s (and those grants are semantically meaningful to
`pg_durable` beyond our ability to call the functions).

If we try to capture and re-issue[^1] those grants from an extension, PG
will record the grants as coming from the extension update script, and
assume it doesn't need to provide them in pg_dump, so then the
pg_restore won't have them, meaning logical restore and/or PG upgrades
will be broken.

The AI suggested the right approach was some catalog feng shui but that
it would take a while to engineer. I asked on the PostgreSQL discord,
and one of the PG committers (rhass) told me that this (trying to copy
grants from one function to another) was something that you should
never do, and to just have users reissue the grants on update.

So... instead of that, we just add a combinator function that
manipulates the durofut JSON directly to add the options. For
example, you'd use it like:

```sql
df.with_http_options(
    df.http(...),
    '{"options": "here"}'::jsonb
);
```

This admittedly is less ergonomic than adding an `options =>` parameter
for `df.http`, but... well, yeah.

If we want, we could make this into an operator, e.g. allowing
`df.http(...) <some-operator> '{"options": "here"}'` or something like
that? I don't have strong feelings.

[^1]: This is ignoring the fact that the permissions may be different
    now, for example even ignoring the next issue this won't work quite
    right if a delegator of a grant became a superuser.
@tjgreen42
Todd J. Green (tjgreen42) merged commit 807be96 into main Sep 10, 2026
14 checks passed
Thom Chiovoloni (thomcc-work) added a commit to thomcc-work/pg_durable that referenced this pull request Sep 10, 2026
Several of my tasks require passing additional options to `df.http` and
`df.http_multipart`. Unfortunately, adding new parameters to these
functions is Hard. The normal approach (for example taken in microsoft#377 with
the `df.loop`) is to keep the old function around, renamed (but with the
same wrapper name). This is fine (and even avoids issues when
`ALTER EXTENSION UPDATE` is not run), but `df.http` and
`df.http_multipart` are functions that will likely have a bunch
of `GRANT`s (and those grants are semantically meaningful to
`pg_durable` beyond our ability to call the functions).

If we try to capture and re-issue[^1] those grants from an extension, PG
will record the grants as coming from the extension update script, and
assume it doesn't need to provide them in pg_dump, so then the
pg_restore won't have them, meaning logical restore and/or PG upgrades
will be broken.

The AI suggested the right approach was some catalog feng shui but that
it would take a while to engineer. I asked on the PostgreSQL discord,
and one of the PG committers (rhass) told me that this (trying to copy
grants from one function to another) was something that you should
never do, and to just have users reissue the grants on update.

So... instead of that, we just add a combinator function that
manipulates the durofut JSON directly to add the options. For
example, you'd use it like:

```sql
df.with_http_options(
    df.http(...),
    '{"options": "here"}'::jsonb
);
```

This admittedly is less ergonomic than adding an `options =>` parameter
for `df.http`, but... well, yeah.

If we want, we could make this into an operator, e.g. allowing
`df.http(...) <some-operator> '{"options": "here"}'` or something like
that? I don't have strong feelings.

[^1]: This is ignoring the fact that the permissions may be different
    now, for example even ignoring the next issue this won't work quite
    right if a delegator of a grant became a superuser.
Thom Chiovoloni (thomcc-work) added a commit to thomcc-work/pg_durable that referenced this pull request Sep 10, 2026
Several of my tasks require passing additional options to `df.http` and
`df.http_multipart`. Unfortunately, adding new parameters to these
functions is Hard. The normal approach (for example taken in microsoft#377 with
the `df.loop`) is to keep the old function around, renamed (but with the
same wrapper name). This is fine (and even avoids issues when
`ALTER EXTENSION UPDATE` is not run), but `df.http` and
`df.http_multipart` are functions that will likely have a bunch
of `GRANT`s (and those grants are semantically meaningful to
`pg_durable` beyond our ability to call the functions).

If we try to capture and re-issue[^1] those grants from an extension, PG
will record the grants as coming from the extension update script, and
assume it doesn't need to provide them in pg_dump, so then the
pg_restore won't have them, meaning logical restore and/or PG upgrades
will be broken.

The AI suggested the right approach was some catalog feng shui but that
it would take a while to engineer. I asked on the PostgreSQL discord,
and one of the PG committers (rhass) told me that this (trying to copy
grants from one function to another) was something that you should
never do, and to just have users reissue the grants on update.

So... instead of that, we just add a combinator function that
manipulates the durofut JSON directly to add the options. For
example, you'd use it like:

```sql
df.with_http_options(
    df.http(...),
    '{"options": "here"}'::jsonb
);
```

This admittedly is less ergonomic than adding an `options =>` parameter
for `df.http`, but... well, yeah.

If we want, we could make this into an operator, e.g. allowing
`df.http(...) <some-operator> '{"options": "here"}'` or something like
that? I don't have strong feelings.

[^1]: This is ignoring the fact that the permissions may be different
    now, for example even ignoring the next issue this won't work quite
    right if a delegator of a grant became a superuser.
Thom Chiovoloni (thomcc-work) added a commit to thomcc-work/pg_durable that referenced this pull request Sep 10, 2026
Several of my tasks require passing additional options to `df.http` and
`df.http_multipart`. Unfortunately, adding new parameters to these
functions is Hard. The normal approach (for example taken in microsoft#377 with
the `df.loop`) is to keep the old function around, renamed (but with the
same wrapper name). This is fine (and even avoids issues when
`ALTER EXTENSION UPDATE` is not run), but `df.http` and
`df.http_multipart` are functions that will likely have a bunch
of `GRANT`s (and those grants are semantically meaningful to
`pg_durable` beyond our ability to call the functions).

If we try to capture and re-issue[^1] those grants from an extension, PG
will record the grants as coming from the extension update script, and
assume it doesn't need to provide them in pg_dump, so then the
pg_restore won't have them, meaning logical restore and/or PG upgrades
will be broken.

The AI suggested the right approach was some catalog feng shui but that
it would take a while to engineer. I asked on the PostgreSQL discord,
and one of the PG committers (rhass) told me that this (trying to copy
grants from one function to another) was something that you should
never do, and to just have users reissue the grants on update.

So... instead of that, we just add a combinator function that
manipulates the durofut JSON directly to add the options. For
example, you'd use it like:

```sql
df.with_http_options(
    df.http(...),
    '{"options": "here"}'::jsonb
);
```

This admittedly is less ergonomic than adding an `options =>` parameter
for `df.http`, but... well, yeah.

If we want, we could make this into an operator, e.g. allowing
`df.http(...) <some-operator> '{"options": "here"}'` or something like
that? I don't have strong feelings.

[^1]: This is ignoring the fact that the permissions may be different
    now, for example even ignoring the next issue this won't work quite
    right if a delegator of a grant became a superuser.
Thom Chiovoloni (thomcc-work) added a commit to thomcc-work/pg_durable that referenced this pull request Sep 10, 2026
Several of my tasks require passing additional options to `df.http` and
`df.http_multipart`. Unfortunately, adding new parameters to these
functions is Hard. The normal approach (for example taken in microsoft#377 with
the `df.loop`) is to keep the old function around, renamed (but with the
same wrapper name). This is fine (and even avoids issues when
`ALTER EXTENSION UPDATE` is not run), but `df.http` and
`df.http_multipart` are functions that will likely have a bunch
of `GRANT`s (and those grants are semantically meaningful to
`pg_durable` beyond our ability to call the functions).

If we try to capture and re-issue[^1] those grants from an extension, PG
will record the grants as coming from the extension update script, and
assume it doesn't need to provide them in pg_dump, so then the
pg_restore won't have them, meaning logical restore and/or PG upgrades
will be broken.

The AI suggested the right approach was some catalog feng shui but that
it would take a while to engineer. I asked on the PostgreSQL discord,
and one of the PG committers (rhass) told me that this (trying to copy
grants from one function to another) was something that you should
never do, and to just have users reissue the grants on update.

So... instead of that, we just add a combinator function that
manipulates the durofut JSON directly to add the options. For
example, you'd use it like:

```sql
df.with_http_options(
    df.http(...),
    '{"options": "here"}'::jsonb
);
```

This admittedly is less ergonomic than adding an `options =>` parameter
for `df.http`, but... well, yeah.

If we want, we could make this into an operator, e.g. allowing
`df.http(...) <some-operator> '{"options": "here"}'` or something like
that? I don't have strong feelings.

[^1]: This is ignoring the fact that the permissions may be different
    now, for example even ignoring the next issue this won't work quite
    right if a delegator of a grant became a superuser.
@tjgreen42 Todd J. Green (tjgreen42) mentioned this pull request Sep 10, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants