Skip to content

refactor(prefect): runtime-resolve flow-timeout + download retries#285

Merged
jqnatividad merged 1 commit into
mainfrom
move-import-time-constants
May 16, 2026
Merged

refactor(prefect): runtime-resolve flow-timeout + download retries#285
jqnatividad merged 1 commit into
mainfrom
move-import-time-constants

Conversation

@jqnatividad

Copy link
Copy Markdown
Collaborator

Summary

Addresses the "import-time constants" follow-up on #282. Two of the three knobs that were baked into the @flow / @task decorators at module import are now re-resolved per flow run, so env / ckan.ini changes take effect on the very next run — even on work-pool types that reuse a long-lived interpreter. (The default process worker re-imports per run anyway, so behavior there is unchanged.)

What moved to runtime resolution

  • Download retries (DATAPUSHER_PLUS_DOWNLOAD_RETRIES / ckanext.datapusher_plus.download_retries): the flow body now applies the count via download_task.with_options(retries=download_retries)(...) instead of the @task(retries=…) decorator.
  • Flow timeout (DATAPUSHER_PLUS_FLOW_TIMEOUT_SECONDS / ckanext.datapusher_plus.flow_timeout): the @flow decorator's timeout_seconds stays as Prefect's hard-stop ceiling (the only thing that can kill a stage hung mid-execution). Added a soft _check_deadline() between stages that uses a runtime-resolved value. A shorter deadline takes effect immediately on any worker type; raising it above the import-time ceiling still requires a restart on interpreter-reusing pools.

What stayed import-time, and why

  • Database retries (_TASK_RETRY_DATABASE): Task.with_options() in Prefect 3.x preserves on_completion / on_failure / on_running hooks but does not preserve on_rollback_hooks. database_task carries the rollback hook that drops the partial datastore table on transactional failure (@database_task.on_rollback). Silently breaking that contract for a per-run config refresh would be a net loss. Both the constants block and the @task decorator comment now spell this out.

Verified empirically in dpp-test:

>>> t = task(lambda: None); t.on_rollback(my_hook)
>>> t.with_options(retries=5).on_rollback_hooks
[]

Tests

Two new cases in tests/test_prefect_flow.py:

  1. test_flow_resolves_retry_counts_per_run — spies on download_task.with_options and asserts it's called with the env-resolved retry count.
  2. test_flow_aborts_when_soft_deadline_exceeded — sets the deadline env var to "0" and asserts mark_job_as_errored fires with the deadline message before the flow propagates JobError.

Verified locally in the dpp-test ckan-dev container: 52/52 unit tests pass (50 prior + 2 new).

Test plan

  • ci.yml (DataPusher+ Integration CI) — 7-file quick-dir matrix still passes; in particular the rollback path on database_task still fires (regression test for the asymmetry that made me revert the database-retries change).

🤖 Generated with Claude Code

Addresses the "import-time constants" follow-up on #282: two of the
three knobs that were baked in at module import are now re-resolved
per flow run, so env / ckan.ini changes take effect on the very next
run even on work-pool types that reuse a long-lived interpreter (the
default `process` worker re-imports per run, so behavior there is
unchanged).

**Download retries (`DATAPUSHER_PLUS_DOWNLOAD_RETRIES` /
`ckanext.datapusher_plus.download_retries`)**: removed `retries=` from
the `@task` decorator on `download_task`; the flow body now applies it
via `download_task.with_options(retries=download_retries)(...)`,
resolving the count via the existing `_resolve_int` helper at flow
start. The `_TASK_RETRY_DOWNLOAD` module constant is gone.

**Flow timeout (`DATAPUSHER_PLUS_FLOW_TIMEOUT_SECONDS` /
`ckanext.datapusher_plus.flow_timeout`)**: kept the `@flow` decorator's
`timeout_seconds=_FLOW_TIMEOUT_SECONDS` as Prefect's hard-stop ceiling
(necessary because Prefect's decorator-based timeout is the only thing
that can kill a stage hung mid-execution). Added a complementary
soft-deadline check `_check_deadline()` that runs between stages using
a runtime-resolved value: so a *shorter* deadline picks up immediately
on any worker type, while raising the ceiling above the import-time
value still requires a restart on interpreter-reusing pools.

**Database retries**: intentionally *not* moved. `Task.with_options()`
in Prefect 3.x preserves `on_completion` / `on_failure` / `on_running`
but does *not* preserve `on_rollback_hooks`. `database_task` carries
the rollback hook that drops the partial datastore table on
transactional failure, and silently breaking that contract would be
worse than the import-time limitation. Both the constants block and
the `@task` decorator comment now spell this out. The full audit:

  with_options(retries=N) call site -> task | safe?
  --------------------------------------------+------
  download_task                               | yes (no on_rollback)
  database_task                               | no (has on_rollback)
  indexing/formula/metadata_task              | no (have on_rollback,
                                              |     not retry-eligible
                                              |     anyway)

Tests: two new cases in `test_prefect_flow.py` —
1. `test_flow_resolves_retry_counts_per_run`: spies on
   `download_task.with_options` and asserts it's called with the env-
   resolved retry count.
2. `test_flow_aborts_when_soft_deadline_exceeded`: sets the deadline
   env var to "0" and asserts `mark_job_as_errored` fires with the
   deadline message before the flow propagates the JobError. (Uses 0
   instead of patching `time.monotonic` because Prefect's flow
   infrastructure also calls `monotonic` during setup, which exhausts
   a small fake-iter before the flow body runs.)

Verified locally in `dpp-test`: 52/52 (50 prior + 2 new).

Tracking: #282.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jqnatividad jqnatividad merged commit 50a3e08 into main May 16, 2026
1 check passed
@jqnatividad jqnatividad deleted the move-import-time-constants branch May 16, 2026 03:10
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.

1 participant