UN-3445 [GATED-FEAT] PG-queue auxiliary-worker migration (OSS): loader fix + notification seam - #2217
UN-3445 [GATED-FEAT] PG-queue auxiliary-worker migration (OSS): loader fix + notification seam#2217muhammad-ali-e wants to merge 13 commits into
Conversation
#2197) * UN-3798 [FIX] Skip broken file-path task load for PG pluggable workers The top-level worker.py loaded a pluggable worker's tasks.py via spec_from_file_location("tasks", ...) — a bare module name with no parent package — so the plugin's relative imports (from .clients import ...) failed with "attempted relative import with no known parent package", crash-looping every cloud PG pluggable worker (agentic_callback/UN-3754, agentic_studio/ UN-3779, bulk_download/UN-3752) on startup under `python -m pg_queue_consumer`. The tasks are already registered by that point: WorkerBuilder.build_celery_app() (called just above) verifies a pluggable type by importing pluggable_worker.{type}.worker as a proper package (_verify_pluggable_worker_exists → import_module), which runs the plugin's `from . import tasks` and registers the tasks on this same app. So the file-path load is both redundant and broken. Fix: skip the file-path load for pluggable workers; non-pluggable (top-level) workers keep it unchanged. The Celery path is unaffected — it runs via `celery -A pluggable_worker.{type}.worker` (a dotted package import) and never touches this loader; the is_pluggable() file-path branch never ran successfully. Validated on a running dev stack via `python -m pg_queue_consumer`: agentic_callback and agentic_studio now start clean ("tasks already registered … skipping file-path task load" → "ready for Celery"); general (non-pluggable) loads unchanged. Prerequisite for UN-3752 / UN-3754 / UN-3779. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * UN-3798 [FIX] Address #2197 review: accurate loader comment, to_directory(), zero-task guard - Extract the task-load block into load_worker_tasks(worker_type) with guard-clause returns (depth 3 -> 1) and an accurate docstring: pluggable tasks register via WorkerBuilder's package import and Celery binds them on app finalize; the file-path load is skipped for them because it breaks any relative imports in the plugin's tasks.py. Softened the overstated "every worker crashes" and marked the cloud-plugin `from . import tasks` example as illustrative (contract, not internals). - Add WorkerType.to_directory() as the single source for the underscore->hyphen dir mapping; to_import_path() and the file-path loader both read it (no more slicing the import path). + tests. - Add a post-load zero-task check as a WARNING (not a hard raise): pluggable tasks bind on app finalize which can be after this point, so a raise would false-positive on a correctly-configured pluggable worker (the exact regression this PR fixes). - Move `import importlib.util` to the module-top imports. Deferred (per reviewer's hotfix note): the "spec_from_file_location never called for pluggable" regression test — worker.py runs infra init + builds the Celery app at import, so there is no clean seam to exercise the loader in isolation without a larger main()-guard refactor. The extraction creates that seam for a fast-follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…nsport (flag-gated) (#2198) * UN-3753 [GATED-FEAT] Route webhook notifications through PG-queue transport (flag-gated) PG-queue analogue for the buffered-webhook dispatch, gated by the pg_queue_enabled Flipt flag. Flag OFF (prod default) keeps the existing Celery send_task byte-unchanged; flag ON enqueues send_webhook_notification onto the PG `notifications` queue, drained by the PG notification consumer. - Dispatch seam: notification_dispatch.py routes send_webhook_notification through resolve_transport — PG via enqueue_task when enabled for the org, else the prior celery_app.send_task (byte-identical, zero regression). _send_clubbed (the buffer-flush path) uses the seam; _org_identifier resolves the org pk -> string for the Flipt decision, keeping the pk in kwargs (the buffer/worker mark contract). + test_notification_dispatch.py (6 cases). - Sites 2 & 3 (WebhookSend / WebhookBatch internal endpoints) stay on Celery: they use countdown stagger, which the PG queue has no delayed-visibility for. Follow-up. Dev-tested on a live stack: flag-off took the Celery branch (0 PG rows); an enqueued send_webhook_notification was drained by the pg_queue_consumer (WORKER_TYPE=notification), delivered to a sink (200), and the row acked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * UN-3753 [GATED-FEAT] Address #2198 review: error-classing, org-id naming, call-site tests - Robustness (P1): _send_clubbed's broad `except` mislabeled permanent dispatch errors as broker_failure and reverted to PENDING → retry-forever + mislabeled Sentry tracebacks until the attempt cap. Branch on class: ValueError/TypeError (enqueue_task validation / payload serialization) → dead-letter now with a distinct `result=dispatch_error` metric; keep revert-to-PENDING only for genuine transport/broker exceptions. - Type design (P2): renamed the seam's routing param organization_id → org_string_id so it can't be conflated with the org pk in `kwargs["organization_id"]` (the worker buffer-mark contract) — swapping them was a silent mis-route to Celery. - Observability (P2): _org_identifier now logs a warning (with org_pk) when the lookup returns None — a dangling FK is a data anomaly (CASCADE makes it otherwise unreachable), not an expected "org deleted" path; reworded the docstring and narrowed org_pk: int. Fixed the "before the webhook HTTP call" wording. - Comment accuracy (P2): dropped the aspirational "usable by callers that surface it in an API response" from the Returns block (the sole caller discards it). - Simplify (P3): dropped the redundant `or None` on the routing arg (resolve_transport already normalizes falsy); kept the load-bearing `or ""` on enqueue_task's org_id. - Tests (P1): new test_send_clubbed.py locks the two-org-identifier contract (string routes, pk in kwargs), the transient→PENDING vs permanent→DEAD_LETTER recovery split, and _org_identifier (string id / None+warn). 11 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * UN-3753 [GATED-FEAT] Gate notification dead-letter classing to the PG path only Keep the flag-off (Celery) error flow byte-identical, per the "no change to the Celery path unless flag-gated" rule. The seam now raises a typed PermanentDispatchError ONLY on the PG branch (when enqueue_task rejects the message for a permanent reason — priority/exclusivity validation or a payload that won't JSON-serialize). _send_clubbed dead-letters on that exception; every other failure — including any Celery send_task error — falls to the transient PENDING branch exactly as before UN-3753. + seam test that a permanent enqueue ValueError/TypeError is wrapped; the call-site test now raises PermanentDispatchError (the real contract) instead of a raw ValueError. 12 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Summary by CodeRabbit
WalkthroughWebhook notifications now select PG or Celery using an organization string identifier, classify dispatch failures, and reject non-standard JSON numbers. Worker startup centralizes directory mapping, strictly loads non-pluggable tasks, and validates task registration. ChangesWebhook dispatch and queue validation
Worker startup and diagnostics
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant _send_clubbed
participant dispatch_webhook_notification
participant resolve_transport
participant PGQueue
participant Celery
_send_clubbed->>dispatch_webhook_notification: dispatch webhook with org string id
dispatch_webhook_notification->>resolve_transport: resolve transport
resolve_transport-->>dispatch_webhook_notification: PG or Celery
dispatch_webhook_notification->>PGQueue: enqueue with dispatch id
dispatch_webhook_notification->>Celery: send task with original arguments
sequenceDiagram
participant Worker
participant WorkerType
participant tasks_py
participant CeleryRegistry
Worker->>WorkerType: resolve worker directory
Worker->>tasks_py: load non-pluggable tasks.py
tasks_py->>CeleryRegistry: register tasks
Worker->>CeleryRegistry: validate task registration
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
muhammad-ali-e
left a comment
There was a problem hiding this comment.
PR Review Toolkit — automated multi-agent review
Ran six specialist agents (Code Reviewer, Silent-Failure Hunter, Type-Design Analyzer, Test Analyzer, Comment Analyzer, Code Simplifier) plus an independent verification pass against enqueue_task / resolve_transport / is_pg_transport.
Verdict: no blocking issues. The transport-routing seam is correct, the flag-off (Celery) path is byte-identical to the prior send_task, the permanent-vs-transient error taxonomy is sound and SENDING-guarded, and the two-org-identifier contract is well-documented and pinned by tests. Everything below is an improvement, not a defect — the two [Medium] items are the only ones I'd act on before merge.
Findings are inline. Priority: 2 Medium, 6 Low, 1 Nit.
…istry + notification NaN/typing/comment fixes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback addressed (commit 02dcdcb)Worked through the automated review. All changes are either new-in-this-PR code or gated to the PG branch, so the flag-off (Celery) path stays byte-identical. Addressed (9):
Deferred (follow-up ticket): `worker.py` loader test-gap — needs a `main()`/`name` guard refactor to make the module import-safe for unit testing. Tests: 28 backend + 3 worker green; pre-commit clean. |
…N test
Hoist float("nan") out of the pytest.raises block so only enqueue_task is
under assertion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| backend/notification_v2/internal_api_views.py | Routes clubbed notification dispatch through the transport seam and adds bounded transient recovery plus permanent enqueue dead-lettering. |
| backend/notification_v2/notification_dispatch.py | Introduces organization-gated Celery/PG routing while adapting notification retry semantics for eager PG consumption. |
| backend/pg_queue/producer.py | Rejects non-finite JSON values before insertion and logs message-construction failures with dispatch context. |
| workers/queue_backend/pg_queue/consumer.py | Preserves producer task IDs during eager execution and redacts credentials and customer payloads from poison-drop logs. |
| workers/worker.py | Separates pluggable package registration from validated file-based loading for non-pluggable workers. |
| workers/shared/enums/worker_enums_base.py | Centralizes worker enum-to-directory mapping and rejects invalid directory resolution for pluggable worker types. |
Sequence Diagram
sequenceDiagram
participant Flush as Notification buffer flush
participant Resolver as Transport resolver
participant Celery as Celery broker
participant PG as PG queue
participant Worker as Notification worker
participant Backend as Buffer status API
Flush->>Resolver: Resolve using org string ID
alt PG queue enabled
Resolver-->>Flush: PG transport
Flush->>PG: Enqueue notification task
PG->>Worker: "apply(task_id, max_retries=0)"
else Celery fallback
Resolver-->>Flush: Celery transport
Flush->>Celery: send_task with original kwargs
Celery->>Worker: Execute notification task
end
Worker->>Backend: Mark dispatched or dead-letter
Reviews (9): Last reviewed commit: "Revert "UN-3893 [FIX] Make ConcurrencyMo..." | Re-trigger Greptile
There was a problem hiding this comment.
🧹 Nitpick comments (1)
backend/notification_v2/tests/test_send_clubbed.py (1)
75-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPermanent-error test doesn't verify the SENDING guard on the dead-letter update.
Unlike
test_transient_failure_reverts_sending_rows_to_pending, this test only checks.update()kwargs, not.filter()kwargs — so the documentedstatus=BufferStatus.SENDING.valueclobber-guard on the dead-letter path is unpinned.♻️ Proposed addition
def test_permanent_pg_error_dead_letters(self): ... _send() + fkw = buf.objects.filter.call_args.kwargs + assert fkw["status"] == BufferStatus.SENDING.value # Permanent error → terminal DEAD_LETTER, no PENDING revert / refund. ukw = buf.objects.filter.return_value.update.call_args.kwargs assert ukw == {"status": BufferStatus.DEAD_LETTER.value}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/notification_v2/tests/test_send_clubbed.py` around lines 75 - 91, The test_permanent_pg_error_dead_letters test must also verify the SENDING status guard used when dead-lettering. Inspect buf.objects.filter.call_args.kwargs and assert it includes status=BufferStatus.SENDING.value, while preserving the existing update kwargs assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@backend/notification_v2/tests/test_send_clubbed.py`:
- Around line 75-91: The test_permanent_pg_error_dead_letters test must also
verify the SENDING status guard used when dead-lettering. Inspect
buf.objects.filter.call_args.kwargs and assert it includes
status=BufferStatus.SENDING.value, while preserving the existing update kwargs
assertion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 17fd0acc-5a58-48f3-98c2-17484311087e
📒 Files selected for processing (10)
backend/notification_v2/internal_api_views.pybackend/notification_v2/notification_dispatch.pybackend/notification_v2/tests/__init__.pybackend/notification_v2/tests/test_notification_dispatch.pybackend/notification_v2/tests/test_send_clubbed.pybackend/pg_queue/producer.pybackend/pg_queue/tests/test_producer.pyworkers/shared/enums/worker_enums_base.pyworkers/tests/test_worker_enums_directory.pyworkers/worker.py
muhammad-ali-e
left a comment
There was a problem hiding this comment.
Follow-up automated review pass (PR Review Toolkit). One net-new finding below; everything else surfaced was observability-level, a comment nit, a test-coverage gap, or on a file outside this diff — reported to the author out-of-band rather than posted here to keep the bar high.
…odeRabbit) Assert the dead-letter update filters on status=SENDING, mirroring the transient-revert test's guard assertion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the CodeRabbit nitpick (commit 617b2fb): |
…queue_task Move the _json_safe message construction inside the try/except so a serialization ValueError (now reachable via allow_nan=False) gets the same task/queue/org breadcrumb as a DB insert failure, instead of propagating context-free on the orchestrator path. Test asserts the breadcrumb fires and the DB insert is never reached. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the follow-up review finding (commit 1eb15c6): moved the |
…default 5) + enrich poison-drop log Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
workers/tests/test_pg_queue_consumer.py (1)
275-291: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the queue context added by the log change.
The test checks
poison-dropped,read_ct, andorg_id, but it does not checkqueue. Add an assertion for the expected queue value so a regression cannot remove this diagnostic context while the test remains green.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@workers/tests/test_pg_queue_consumer.py` around lines 275 - 291, The test method test_poison_log_enriched_with_org_and_read_ct must also verify the queue context in the poison-drop log. Add an assertion that caplog.text contains the expected queue value for the consumer initialized with ["q"], while preserving the existing poison-dropped, read_ct, and org_id assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@workers/tests/test_pg_queue_consumer.py`:
- Around line 275-291: The test method
test_poison_log_enriched_with_org_and_read_ct must also verify the queue context
in the poison-drop log. Add an assertion that caplog.text contains the expected
queue value for the consumer initialized with ["q"], while preserving the
existing poison-dropped, read_ct, and org_id assertions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3e6d8bfd-e63f-4ae0-8299-e93f86f4790d
📒 Files selected for processing (2)
workers/queue_backend/pg_queue/consumer.pyworkers/tests/test_pg_queue_consumer.py
…o terminal failure doesn't redeliver On the PG branch of dispatch_webhook_notification, kwargs were forwarded verbatim including raise_on_final_failure=True. On PG that re-raise means the OPPOSITE of Celery: the worker already marks the buffers DEAD_LETTER, so re-raising only leaves the row for vt-expiry redelivery — re-POSTing the subscriber up to max_attempts times and tripping a false poison-drop. Override it to False on the PG branch only (the Celery branch keeps kwargs verbatim — byte-identical) so a terminal failure returns None -> the consumer acks -> single POST, matching Celery's external behaviour. Also: the poison-drop log test now asserts the source queue is surfaced (CodeRabbit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vishnuszipstack
left a comment
There was a problem hiding this comment.
PR Review Toolkit — consolidated findings
Ran the Code Reviewer, Silent Failure Hunter, Type Design Analyzer, PR Test Analyzer, Comment Analyzer and Code Simplifier agents over the 12 changed files, then verified each surviving finding against the source. Findings already raised on this PR are omitted.
Two blockers before the flag can be turned on for any org:
- The
raise_on_final_failure=Falseoverride is unreachable for any subscriber withmax_retries >= 1— verified by running the consumer's exacttask.apply(throw=True)shape. The buffers are never dead-lettered and the subscriber is re-POSTed on every vt-expiry redelivery. - Nothing in this repo drains the PG
notificationsqueue — none of the sevenpg-queue-consumercompose services polls it, andrun-worker.shhas no notification PG role. Flag-on means every buffered webhook for that org is enqueued and silently lost.
Also flagged: an unconditional attempt-refund that makes the dispatch cap unreachable, subscriber credentials reaching Sentry and PG-at-rest via the poison-drop path, an empty-registry guard whose stated rationale is contradicted by Celery's auto-finalize, and several test/comment accuracy gaps.
Nice work on the seam tests themselves — they assert forwarded payloads and negatives rather than mock tautologies, and test_permanent_pg_error_dead_letters proving the absence of a refund via exact-dict equality is the right instinct.
…l-branch, task_id passthrough, credential redaction All 14 review findings. Flag-off/Celery stays byte-identical throughout. HIGH - raise_on_final_failure override was unreachable for max_retries>=1: under the consumer's eager task.apply(throw=True) request.retries is always 0, so the in-task retry guard fires on the FIRST failure and Retry propagates out of apply() - the terminal branch never ran, buffers were never dead-lettered, and the row was left for vt-expiry redelivery (re-POSTing the subscriber each time). Also force max_retries=0 on the PG branch so the terminal branch is reached. New workers-side test drives the REAL task through apply() and asserts one POST + one DEAD_LETTER mark, and pins the old broken shape as a regression guard. HIGH - docstring claimed a PG notifications consumer drains the queue; none exists in this repo. Reworded as an explicit deployment prerequisite (flag must stay off until one is deployed, else buffered webhooks are enqueued and silently lost). Idempotency - the consumer now passes the payload's stable task_id to task.apply(). Without it Celery mints a fresh uuid per delivery, so every guard keyed on request.id (fan-out claims, generation_task_id, task-complete markers) deduped nothing across a redelivery. Security - the poison drop logged the full payload, leaking subscriber Authorization / API-key headers and customer webhook bodies to stdout and Sentry. Added _redact_payload (masks auth-ish keys, summarises the body); routing metadata kept. Also: bounded the transient attempt-refund so NOTIFICATION_MAX_DISPATCH_ATTEMPTS is reachable (an unconditional refund made termination impossible); DispatchResult now carries the transport so the ramp metric can compare PG vs Celery; to_directory rejects pluggable types; corrected the empty-registry rationale (app.tasks auto-finalizes) and the stale link_error premises; fixed the conftest defect that forced a leaf test to mutate global Celery state; parametrized NaN/inf coverage over args/kwargs/fairness and the whole WorkerType->directory mapping; asserted the refund and buffer_row_ids that comments claimed but tests never checked. No regression: workers suite 208 failed / 1239 passed before, 208 failed / 1243 passed after (the 208 are pre-existing env failures - no DB, no prometheus_client). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…survives a set env var and duplicate module copies
StateStore guards every set/get/clear with `cls.mode == ConcurrencyMode.THREAD` and
raises RuntimeError("Unknown concurrency mode") otherwise. As a bare Enum that guard
had two runtime failure modes, both invisible until production:
1. mode is read as os.environ.get("CONCURRENCY_MODE", ConcurrencyMode.THREAD), so
SETTING the variable yields a plain str — and "thread" == ConcurrencyMode.THREAD is
False for a bare Enum. Even the CORRECT value took the backend down.
2. The module exists in three copies (backend/utils, workers/shared/utils,
workers/shared/infrastructure) and can be imported under more than one path in a
merged OSS+cloud tree, producing two distinct ConcurrencyMode CLASSES. Members of
different Enum classes never compare equal, so the guard raised on every call.
(2) is what failed ~70 cloud integration tests across unrelated suites
(test_pg_finalization_fixes, dashboard_metrics, manual_review_v2, statistics_service):
the same OSS tests pass in OSS CI and cloud main is green — it only appeared once the
cloud plugins were merged into the OSS tree.
StrEnum members ARE strings, so both cases now compare by value and the defining
class's identity stops mattering. Applied to all three copies, with a regression test
that loads two copies under different names, asserts they are genuinely distinct
classes, and asserts their members still compare equal — plus the env-var round trip.
No behaviour change otherwise: the comparison is strictly more permissive, and the
workers suite is unchanged at 208 failed / 1243 passed (pre-existing env failures:
no DB, no prometheus_client).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e guard survives a set env var and duplicate module copies" This reverts 2a39e26. Two reasons, both raised by review: OUT OF SCOPE. The bug is a pre-existing one in main with no connection to PG queue, the flag, or the aux workers. It was picked up while diagnosing why cloud CI was red and should never have been fixed inside this epic's branch: it muddies a PR whose whole promise is "PG-only, flag-gated", and it touches StateStore — multi-tenant org scoping on the request path, used by the flag-off Celery flow in staging and production. "Probably safe" is not the bar for that path. THE STATED CAUSE DOES NOT HOLD. The commit justified itself with "duplicate module identity produces two ConcurrencyMode classes". On checking: both repos import it under exactly one path (`from utils.local_context import`, 25 sites), cloud ships no overlay copy, and CONCURRENCY_MODE is set nowhere (unfiltered grep, both repos). So within a single module cls.mode and ConcurrencyMode.THREAD are the same class and should compare equal — the raise is unexplained. Shipping the change would have masked the symptom without anyone understanding the cause. Verified separately that this epic did NOT introduce the bug: our branch never touched these files, the three copies predate it (2024-03 / 2025-10), and cloud CI resolves refs/heads/main at run time so it never checks out our OSS branch at all. Tracked in UN-3893 for the owner of the merge/CI setup; cloud #1688 CI stays red on it as a blocked-by rather than something this PR introduced or can fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Unstract test resultsPer-group results
Critical paths
|



What
The OSS slice of the PG-queue auxiliary-worker migration wave (epic UN-3445). Two flag-gated changes, both inert with
pg_queue_enabledoff:workers/worker.py): skip the broken file-path task load for pluggable workers (their tasks are already registered bybuild_celery_app's package import). Non-pluggable workers keep the file-path load. Fixes a startup crash-loop for cloud PG pluggable workers; the Celery path is untouched.notification_v2/notification_dispatch.py): PG whenpg_queue_enabledfor the org, elsecelery_app.send_task(byte-identical).PermanentDispatchError(raised only on the PG branch) dead-letters permanent enqueue errors; the Celery error path is unchanged.Why
Can this PR break any existing features?
pg_queue_enabledFlipt flag; with it off (the production default)resolve_transportfail-closes to Celery and the seams call the samesend_taskas before — byte-identical. The loader fix only skips a load path for pluggable workers (not deployed with the flag off). Dev-tested on a live stack: flag-off produced 0 PG rows (Celery taken); PG round-trips delivered end-to-end.Notes on Testing
notification_v2/tests/(12 tests) pass;pipeline_dispatchregression suite green; worker enum/loader tests pass. Merged currentmainin (clean, no conflicts).Related Issues or PRs