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
feat: add ckanext.datapusher_plus.prefect_enabled to run jobs without Prefect
Submitting a resource fails outright on deployments where CKAN cannot
write $PREFECT_HOME:
ERROR [ckanext.datapusher_plus.logic.action] Error submitting job to
DataPusher: [Errno 13] Permission denied: '/root/.prefect/profiles.toml'
That is not a Prefect outage — `submit_flow_run`'s `from
prefect.deployments import run_deployment` runs Prefect's settings
bootstrap, which creates `$PREFECT_HOME/profiles.toml` (default
`$HOME/.prefect`). A CKAN process running with HOME=/root but no write
access there cannot even import the library, so no amount of server
configuration helps.
Add `ckanext.datapusher_plus.prefect_enabled` (default true). Setting it
to false turns Prefect off entirely: `datapusher_submit` enqueues the job
on CKAN's own background queue and the new `jobs/local_runner.py` runs the
same nine ingestion stages in-process, with nothing on the path importing
`prefect`. Operators run `ckan jobs worker` instead of a Prefect server,
worker, and work pool.
The local runner keeps the Jobs/Logs tables, the datapusher_hook
callbacks, the complete-with-skip contract, the flow_timeout deadline, and
the post-database-failure datastore cleanup. It does not provide per-stage
retries, result caching, the run graph/artifacts/events, or
human-in-the-loop PII review — with Prefect off, a job crossing
pii_review_threshold aborts before any datastore write rather than
suspending for an approval that can never arrive. A database-stage failure
deliberately leaves the datastore alone (that stage can fail before
touching anything, and dropping there would destroy data the run never
wrote). The task_status row records the RQ job id as `rq_job_id` rather
than `flow_run_id`, so nothing builds a Prefect-UI deep link out of it.
The Prefect-free half of the pipeline moves out of prefect_flow.py into
the new jobs/pipeline_core.py — the CKAN status callback, input
validation, RuntimeContext construction, the stage invoker and its
StageAbort signal, and the datastore rollback body — shared verbatim by
both runners. prefect_flow keeps its historical private names as aliases,
so custom flows composed from its @task primitives are unaffected; tests
that patched prefect_flow.dsu / .QSVCommand / .Path now patch
pipeline_core.
Unit suite: 307 passed, 1 failed (test_quoted_csv_inference_matrix, a
pre-existing qsv-binary-dependent failure present on the base commit too).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,7 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
7
7
## [Unreleased]
8
8
9
+
### Added
10
+
* New `ckanext.datapusher_plus.prefect_enabled` setting (default `true`). Setting it to `false` turns Prefect off entirely: `datapusher_submit` enqueues the job on CKAN's own background-job queue and the new `ckanext/datapusher_plus/jobs/local_runner.py` executes the same nine ingestion stages in-process, so no Prefect server, worker, work pool — or `import prefect` — is involved. Operators run `ckan jobs worker` instead. This is the fix for deployments where importing Prefect *itself* fails, e.g. a CKAN process running with `HOME=/root` but no write access there, which surfaces as `Error submitting job to DataPusher: [Errno 13] Permission denied: '/root/.prefect/profiles.toml'` (the alternative fix, if you want to keep Prefect, is to point `PREFECT_HOME` at a writable directory). The local runner keeps the `Jobs`/`Logs` tables, the `datapusher_hook` callbacks, the complete-with-skip contract, the `flow_timeout` deadline, and the post-database-failure datastore cleanup; it does *not* provide per-stage retries, result caching, the Prefect run graph/artifacts/events, or human-in-the-loop PII review — with Prefect off, a job crossing `pii_review_threshold` aborts before any datastore write rather than suspending for approval. The task_status row records the RQ job id as `rq_job_id` (not `flow_run_id`), so nothing builds a Prefect-UI deep link out of it. See [Running without Prefect](README.md#running-without-prefect).
11
+
9
12
### Changed
13
+
* The Prefect-free half of the pipeline moved out of `jobs/prefect_flow.py` into the new `jobs/pipeline_core.py` (the CKAN status callback, input validation, `RuntimeContext` construction, the stage invoker and its `StageAbort` signal, and the datastore rollback body), shared verbatim by the Prefect flow and the local runner. `prefect_flow` keeps its historical private names as aliases, so custom flows composed from its `@task` primitives are unaffected. Tests that patched `prefect_flow.dsu` / `.QSVCommand` / `.Path` now patch `pipeline_core`.
10
14
* Auto-indexing now uses a closed cardinality range `[auto_index_min_threshold, auto_index_threshold]` instead of a single upper bound (issue #142). Defaults bumped: `auto_index_threshold` `3` → `10` and the new `auto_index_min_threshold` defaults to `3`. The new lower bound skips the useless-single-value-column case @EricSoroos flagged in #142 (a 1-value text column previously produced a 10–40MB B-tree the Postgres planner would never choose). The upper bump from 3 → 10 widens the DataTables-SearchBuilder filtering sweet spot to cover typical enum-shaped columns (status = 3–10 values, common enums under ~10). **Operator-facing impact at default settings:** columns with cardinality 1–2 lose their auto-index (intentional — the indexes were dead weight); columns with cardinality 4–10 gain an auto-index. No migration is required; the change re-applies on the next resubmit. To restore pre-#142 behavior (no lower floor) set `ckanext.datapusher_plus.auto_index_min_threshold = 0`. The setting `auto_index_threshold = -1` ("index every column") still works but now hits the `min_threshold = 3` floor unless paired with `auto_index_min_threshold = 0`.
11
15
***BREAKING** Bumped `MINIMUM_QSV_VERSION` from `4.0.0` to `20.0.0`. Operators must upgrade their `qsv` binary at the path configured by `ckanext.datapusher_plus.qsv_bin` before deploying this version — DP+ will refuse to start otherwise. See [qsv 20.0.0 release notes](https://github.com/dathere/qsv/releases/tag/20.0.0) and the migration notes below. ([README install snippet](README.md#option-2-install-prebuilt-qsv-binaries) updated accordingly.)
12
16
* **BREAKING** Bumped `MINIMUM_QSV_VERSION` from `20.0.0` to `20.1.0` (and bumped the qsv version installed by `Dockerfile.worker`, `.github/workflows/ci.yml`, and `.github/workflows/main.yml` to match). Operators must upgrade their `qsv` binary at the path configured by `ckanext.datapusher_plus.qsv_bin` before deploying this version — DP+ will refuse to start otherwise with `JobError: At least qsv version 20.1.0 required. Found 20.0.0.`. qsv 20.1.0 itself introduces no breaking changes against 20.0.0 (per the [20.1.0 release notes](https://github.com/dathere/qsv/releases/tag/20.1.0): "pipelines built on 20.0.0 will upgrade in place"), so the upgrade is binary-swap-only — no data re-ingestion is required. The reason this is still flagged as **BREAKING** is the minimum-version gate, not the qsv behavior. User-visible improvement justifying the floor bump: qsv-dateparser 0.14 → 0.15 in qsv 20.1.0 adds recognition for ISO 8601 `T`-separated datetimes without a timezone suffix (e.g. `2024-10-11T14:30:00`) — qsv 20.0.0 misclassified these as `String` during `qsv stats --infer-dates`, which in DP+ surfaced as a date-typed column being demoted to text on certain CSV shapes (one of the gaps documented in the issue #173 regression test, now closed). The regression-test matrix in `tests/test_issue_173_date_format_inference.py` is updated to the new baseline.
local_runner.py → The `prefect_enabled = false` path. `enqueue_job` puts the job on
76
+
CKAN's RQ queue (`ckan jobs worker` runs it); `run_job` executes the
77
+
nine stages sequentially over one live RuntimeContext, owning the
78
+
same Jobs-row state machine and callbacks as the flow. No retries,
79
+
caching, artifacts, events, or PII suspend-for-review.
70
80
context.py → ProcessingContext — per-run mutable state shared across stages.
71
81
runtime_context.py → JobInput (frozen, JSON-serializable flow input), the per-stage
72
82
`*Result` dataclasses (DownloadResult, AnalyzeResult, …), the
@@ -96,6 +106,8 @@ stages/
96
106
97
107
Operators can register a custom flow via `ckanext.datapusher_plus.prefect_flow`; the per-stage `@task` functions in `prefect_flow.py` are the public composable primitives.
98
108
109
+
Prefect can also be turned off entirely with `ckanext.datapusher_plus.prefect_enabled = false`, which routes submissions to `jobs/local_runner.py` on CKAN's RQ worker. When touching the pipeline, keep shared logic in `jobs/pipeline_core.py` so both runners stay in sync — and keep that module free of any `prefect` import, since the disabled mode exists for hosts where importing Prefect itself fails.
Copy file name to clipboardExpand all lines: CONFIG.md
+25Lines changed: 25 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -58,6 +58,27 @@ When DRUF is enabled, the following templates are overridden:
58
58
- Works with standard CKAN installations
59
59
- Compatible with ckanext-scheming
60
60
61
+
### Turning Prefect off
62
+
63
+
By default (v3.0+) ingestion jobs are orchestrated by a Prefect server + worker. Setting `prefect_enabled = false` runs them in-process on CKAN's own background-job worker instead, over the same ingestion stages, with nothing in the path importing `prefect`.
64
+
65
+
**Configuration:**
66
+
```ini
67
+
# Orchestrate jobs with Prefect (default: true)
68
+
ckanext.datapusher_plus.prefect_enabled = false
69
+
```
70
+
71
+
**What it does:**
72
+
-`datapusher_submit` enqueues the job on CKAN's RQ queue instead of creating a Prefect flow run
73
+
-`ckanext/datapusher_plus/jobs/local_runner.py` executes the nine stages sequentially in the worker process
74
+
- The `Jobs`/`Logs` tables, the job-status page, and the `datapusher_hook` callbacks behave identically
75
+
76
+
**Requirements:**
77
+
- A running CKAN worker: `ckan -c /etc/ckan/default/ckan.ini jobs worker`
78
+
- No Prefect server, worker, or work pool (`datapusher_plus prefect-deploy` refuses to run in this mode)
79
+
80
+
**What you give up:** per-stage retries, result caching / re-run-from-failed-stage, the Prefect run graph, artifacts and `datapusher.*` events, and human-in-the-loop PII review (a job crossing `pii_review_threshold` aborts before any datastore write instead of waiting for approval). See [Running without Prefect](README.md#running-without-prefect) for the full comparison — including the common trigger for wanting it, a `PermissionError` on `$PREFECT_HOME/profiles.toml` when CKAN cannot write `$HOME/.prefect`.
81
+
61
82
## Example Configuration
62
83
63
84
Add these lines to your CKAN configuration file (e.g., `/etc/ckan/default/ckan.ini`):
DataPusher+ v3.0 replaces the v2 RQ-based background worker with a [Prefect 3](https://docs.prefect.io/v3/) flow. RQ is no longer used by DP+ itself (CKAN continues to ship RQ for unrelated extensions).
573
+
DataPusher+ v3.0 replaces the v2 RQ-based background worker with a [Prefect 3](https://docs.prefect.io/v3/) flow. RQ is no longer used by DP+ itself (CKAN continues to ship RQ for unrelated extensions) — unless you set `ckanext.datapusher_plus.prefect_enabled = false`, which runs ingestions on CKAN's RQ worker instead of Prefect; see [Running without Prefect](#running-without-prefect).
574
574
575
575
### Why Prefect
576
576
@@ -733,6 +733,7 @@ The **default** DP+ flow does NOT call these subflows — it inlines the underly
733
733
734
734
| Key | Default | Purpose |
735
735
|---|---|---|
736
+
|`ckanext.datapusher_plus.prefect_enabled`|`true`| Orchestrate ingestions with Prefect. `false` runs them in-process on CKAN's own job worker with no Prefect import at all — see [Running without Prefect](#running-without-prefect). |
736
737
|`ckanext.datapusher_plus.prefect_deployment_name`|`datapusher-plus/datapusher-plus`| Fully-qualified Prefect deployment name (`<flow>/<deployment>`). |
737
738
|`ckanext.datapusher_plus.prefect_work_pool`|`datapusher-plus`| Work-pool name workers subscribe to. |
738
739
|`ckanext.datapusher_plus.prefect_flow`|_(unset)_|`module.path:flow_name` entrypoint of a custom flow. |
@@ -765,11 +766,61 @@ takes effect on the next flow run without a worker restart.
765
766
766
767
Resolution order: Prefect Variable -> env var -> `ckan.ini` -> built-in default. Variable lookup failures (Prefect server unreachable, name absent, value not int-parseable) silently fall through to the next priority — operators with no Prefect Variables set see no behaviour change.
767
768
769
+
## Running without Prefect
770
+
771
+
Prefect can be turned off entirely:
772
+
773
+
```ini
774
+
ckanext.datapusher_plus.prefect_enabled = false
775
+
```
776
+
777
+
Submissions are then enqueued on **CKAN's own background-job queue** and executed in-process by `ckanext/datapusher_plus/jobs/local_runner.py`, which runs the same nine ingestion stages in the same order. Nothing in the submit or job path imports `prefect`.
778
+
779
+
Instead of a Prefect server + worker, you run CKAN's built-in worker:
780
+
781
+
```bash
782
+
ckan -c /etc/ckan/default/ckan.ini jobs worker
783
+
```
784
+
785
+
That is the only operational change — `ckan datapusher_plus submit` / `resubmit`, the DRUF workflow, the job-status page, the `Jobs`/`Logs` tables, and the `datapusher_hook` callbacks all behave as before. `ckan datapusher_plus prefect-deploy` refuses to run in this mode (there is no deployment to register).
786
+
787
+
### When you'd want this
788
+
789
+
***Prefect can't be run at all** — including the case where merely *importing* it fails. A CKAN process running with `HOME=/root` but no write access there fails submission with:
Prefect writes its profile store to `$PREFECT_HOME` (default `$HOME/.prefect`) the first time it is imported. If you'd rather keep Prefect, the alternative fix is to point `PREFECT_HOME` at a directory the CKAN user can write and restart CKAN and the worker:
796
+
797
+
```bash
798
+
PREFECT_HOME=/var/lib/ckan/prefect
799
+
```
800
+
801
+
***Small or single-node deployments** where a second orchestration service isn't worth the operational surface.
802
+
***Air-gapped or locked-down hosts** where the Prefect server isn't permitted.
803
+
804
+
### What you give up
805
+
806
+
| Capability | With Prefect | With `prefect_enabled = false`|
807
+
|---|---|---|
808
+
| Per-stage retries / backoff | Yes | No — the job fails and is resubmitted |
809
+
| Result caching, re-run from failed stage | Yes | No |
810
+
| Run graph, artifacts, `datapusher.*` events | Yes | No (events are no-ops) |
811
+
| Human-in-the-loop PII review (`pii_review_threshold`) | Suspends for approval | Aborts the job before any datastore write |
812
+
| Datastore cleanup after a failed write group | Transactional rollback | Same cleanup, minus the database stage's own failure (see below) |
Rollback difference in detail: when a stage *after* the database load fails, the local runner drops the half-built datastore table and restores a stashed Data Dictionary, exactly as the Prefect `on_rollback` hook does. When the **database stage itself** raises, it leaves the datastore alone — that stage can fail before touching anything (e.g. "could not connect to the Datastore"), and dropping there would destroy data the run never wrote.
816
+
768
817
### Troubleshooting
769
818
770
819
| Symptom | Likely cause | Fix |
771
820
|---|---|---|
772
821
|`datapusher_submit` returns `False` with a Prefect connection error in the CKAN log | The Prefect server is unreachable from CKAN | Check `PREFECT_API_URL` and that the Prefect server is healthy at `<API>/health`. |
822
+
|`Error submitting job to DataPusher: [Errno 13] Permission denied: '/root/.prefect/profiles.toml'`| CKAN's process can't write `$PREFECT_HOME`, so `import prefect` fails | Set `PREFECT_HOME` to a writable directory, or turn Prefect off with `ckanext.datapusher_plus.prefect_enabled = false` (see [Running without Prefect](#running-without-prefect)). |
823
+
| With `prefect_enabled = false`, jobs stay `pending` forever | No CKAN background worker is running | Start `ckan -c /etc/ckan/default/ckan.ini jobs worker`. |
773
824
| Flow run sits in `Scheduled` forever | No worker is polling the configured work pool | Start `prefect worker start -p datapusher-plus` on a host with the `datapusher-plus` package installed. |
774
825
| Flow run goes straight to `Failed` with "QSV binary not found" | The worker process can't see the qsv binary | Set `ckanext.datapusher_plus.qsv_bin` in the CKAN config the worker reads, or install qsv in the worker's PATH. |
775
826
| Re-run from a failed task re-downloads the file | Result storage block isn't registered, so persisted results aren't being read | Re-run `ckan datapusher_plus prefect-deploy` — it calls `ensure_result_storage_block`. |
0 commit comments