-
Notifications
You must be signed in to change notification settings - Fork 688
UN-3445 [GATED-FEAT] PG-queue auxiliary-worker migration (OSS): loader fix + notification seam #2217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
muhammad-ali-e
wants to merge
13
commits into
main
Choose a base branch
from
feat/UN-3445-pg-queue-aux-workers
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
UN-3445 [GATED-FEAT] PG-queue auxiliary-worker migration (OSS): loader fix + notification seam #2217
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
74f72df
UN-3798 [FIX] Skip broken file-path task load for PG pluggable worker…
muhammad-ali-e 7148e49
UN-3753 [GATED-FEAT] Route webhook notifications through PG-queue tra…
muhammad-ali-e bf04d4b
Merge remote-tracking branch 'origin/main' into feat/UN-3445-pg-queue…
muhammad-ali-e 02dcdcb
UN-3445 [GATED-FEAT] Address PR #2217 review: harden loader empty-reg…
muhammad-ali-e 3e7961c
UN-3445 [GATED-FEAT] Fix SonarCloud S5778: single throwing call in Na…
muhammad-ali-e 617b2fb
UN-3445 [GATED-FEAT] Pin SENDING clobber-guard in dead-letter test (C…
muhammad-ali-e 1eb15c6
UN-3445 [GATED-FEAT] Log serialization failures with breadcrumb in en…
muhammad-ali-e 533cf78
UN-3445 [GATED-FEAT] Make PG consumer max_attempts env-configurable (…
muhammad-ali-e a375e46
UN-3445 [GATED-FEAT] PG webhook: force raise_on_final_failure=False s…
muhammad-ali-e 98ee6a4
Merge branch 'main' into feat/UN-3445-pg-queue-aux-workers
muhammad-ali-e b46e09e
UN-3445 [GATED-FEAT] Address PR #2217 review: PG notification termina…
muhammad-ali-e 2a39e26
UN-3893 [FIX] Make ConcurrencyMode a StrEnum so the StateStore guard …
muhammad-ali-e 425fdc1
Revert "UN-3893 [FIX] Make ConcurrencyMode a StrEnum so the StateStor…
muhammad-ali-e File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| """Transport-routed dispatch for buffered webhook notifications (UN-3753). | ||
|
|
||
| Routes the ``send_webhook_notification`` task through the same | ||
| :func:`resolve_transport` flag as the execution path: the PG queue when | ||
| ``pg_queue_enabled`` for this org, else Celery. **Fail-closed** — with the gate | ||
| off (the production default) it resolves to Celery, behaving exactly like the | ||
| prior unconditional ``celery_app.send_task`` (zero regression). | ||
|
|
||
| DEPLOYMENT PREREQUISITE (PG path): on PG the task lands on the ``notifications`` | ||
| queue and requires a pg-queue consumer configured with | ||
| ``WORKER_PG_QUEUE_CONSUMER_WORKER_TYPE=notification`` / | ||
| ``WORKER_PG_QUEUE_CONSUMER_QUEUE=notifications``. **No such service exists in this | ||
| repo's compose yet** — none of the ``pg-queue-consumer`` services in | ||
| ``docker/docker-compose.yaml`` polls ``notifications``, and ``run-worker.sh``'s | ||
| ``PG_CONSUMER_ROLES`` has no notification role. Until one is deployed the flag must | ||
| stay off for the org: enqueued rows would sit undrained (no TTL sweep covers | ||
| ``pg_queue_message``) and every buffered webhook for that org would be lost. | ||
|
|
||
| ``args`` and ``queue`` are forwarded verbatim on both paths. ``kwargs`` differ in | ||
| exactly two keys, forced on the PG branch only (see the inline note at the branch): | ||
| ``raise_on_final_failure`` → ``False`` and ``max_retries`` → ``0``. On PG the | ||
| payload is additionally JSON-normalized by ``enqueue_task`` (UUIDs/datetimes → str). | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import uuid | ||
| from typing import Any, NamedTuple | ||
|
|
||
| from pg_queue.producer import enqueue_task | ||
| from workflow_manager.workflow_v2.transport import resolve_transport | ||
|
|
||
| from unstract.core.data_models import is_pg_transport | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # The fired task name — mirrors the Celery task registered by the notification | ||
| # worker; kept as a local constant so the backend doesn't import the workers pkg. | ||
| WEBHOOK_NOTIFICATION_TASK = "send_webhook_notification" | ||
|
|
||
|
|
||
| # Transport labels for the dispatch metric. During a percentage ramp the one thing | ||
| # you need from the logs is "are PG-routed notifications succeeding at the same rate | ||
| # as Celery-routed ones?" — a bare task id can't answer that, since a minted PG id | ||
| # and a Celery AsyncResult id are indistinguishable to the caller. | ||
| PG_TRANSPORT = "pg_queue" | ||
| CELERY_TRANSPORT = "celery" | ||
|
|
||
|
|
||
| class DispatchResult(NamedTuple): | ||
| """Which transport actually took the dispatch, plus the resulting task id.""" | ||
|
|
||
| transport: str | ||
| task_id: str | ||
|
|
||
|
|
||
| class PermanentDispatchError(Exception): | ||
| """A dispatch failure that would fail identically on every retry. | ||
|
|
||
| Raised ONLY on the PG path, when ``enqueue_task`` rejects the message for a | ||
| permanent reason (priority range / reply_key+callback exclusivity validation, | ||
| or a payload that can't be JSON-serialized). The Celery path never raises it, | ||
| so a caller can dead-letter on this exception without altering the flag-off | ||
| (Celery) error flow — a Celery ``send_task`` failure stays an ordinary | ||
| ``Exception`` the caller's transient handler owns, exactly as before. | ||
| """ | ||
|
|
||
|
|
||
| def dispatch_webhook_notification( | ||
| *, | ||
| celery_app: Any, | ||
| args: list[Any], | ||
| kwargs: dict[str, Any], | ||
| queue: str, | ||
| org_string_id: str | None, | ||
| ) -> DispatchResult: | ||
| """Dispatch ``send_webhook_notification`` on the resolved transport. | ||
|
|
||
| ``args``/``queue`` are forwarded unchanged on both paths, and on the Celery | ||
| branch ``kwargs`` too — so the flag-off path is byte-identical to the legacy | ||
| ``send_task`` call. The PG branch overrides two retry-semantics kwargs (see | ||
| below); nothing else differs. | ||
|
|
||
| Args: | ||
| celery_app: Injected Celery app (the backend's ``celery_service.app``); | ||
| passed in rather than imported so this seam stays trivially testable. | ||
| args: Positional task args, forwarded verbatim. | ||
| kwargs: Keyword task args. Forwarded as-is on Celery; on PG, | ||
| ``raise_on_final_failure`` is overridden to ``False`` and | ||
| ``max_retries`` to ``0`` (the in-task retry loop is a no-op under the | ||
| consumer's eager ``apply()`` — see the inline note). For the buffered | ||
| path this | ||
| carries ``organization_id`` = the buffer's org **pk** (the worker's | ||
| buffer-mark contract) — deliberately a DIFFERENT identifier from the | ||
| ``org_string_id`` param below (the two must not be conflated). | ||
| queue: Target queue name, forwarded verbatim. | ||
| org_string_id: The org's **string** identifier | ||
| (``Organization.organization_id``), used solely for the Flipt | ||
| transport decision — NOT the org pk carried in ``kwargs``. ``None`` (or | ||
| empty) fails closed to Celery. | ||
|
|
||
| Returns: | ||
| A :class:`DispatchResult` carrying which transport took the dispatch and | ||
| the resulting task id (the Celery ``AsyncResult`` id, or the minted PG task | ||
| id). The transport is what makes the caller's dispatch metric answerable | ||
| during a ramp — the two id flavours are otherwise indistinguishable. | ||
| """ | ||
| # A buffered notification is a single fire-and-forget task with no natural | ||
| # sticky entity, so mint a fresh id to drive Flipt's percentage bucketing and | ||
| # to serve as the PG task id. | ||
| dispatch_id = str(uuid.uuid4()) | ||
| # resolve_transport already normalizes falsy input to Celery, so pass the id | ||
| # straight through (no `or None` needed). | ||
| transport = resolve_transport( | ||
| execution_id=dispatch_id, | ||
| organization_id=org_string_id, | ||
| ) | ||
| # Use the shared is_pg_transport() — the single source for "what counts as PG | ||
| # transport" — rather than opening a second comparison site. | ||
| if is_pg_transport(transport): | ||
| # Two PG-only kwarg overrides, both about the SAME thing: the consumer runs | ||
| # the task eagerly via ``task.apply(..., throw=True)``, where Celery's | ||
| # in-task retry loop does not work and its terminal branch is what we need. | ||
| # | ||
| # 1. ``max_retries`` → 0. Under ``apply()`` ``self.request.retries`` is ALWAYS | ||
| # 0, so the worker's ``if self.request.retries < max_retries`` guard | ||
| # (workers/notification/tasks.py) is true on the FIRST failure for any | ||
| # max_retries >= 1 and calls ``self.retry(...)``, which raises ``Retry``; | ||
| # with ``throw=True`` that propagates straight out of ``apply()`` and the | ||
| # terminal branch is never reached — so the buffers are never marked | ||
| # DEAD_LETTER and ``raise_on_final_failure`` is never even read. The | ||
| # consumer's ``except Exception`` then leaves the row for vt-expiry | ||
| # redelivery, re-POSTing the subscriber every time. Forcing 0 sends the | ||
| # task down the terminal branch on the first failure instead. | ||
| # 2. ``raise_on_final_failure`` → False. On BOTH transports the worker marks | ||
| # the buffers DEAD_LETTER over the internal API *before* it re-raises, so | ||
| # the re-raise is only a FAILURE-state signal for Celery monitoring (no | ||
| # redelivery there). On the PG consumer that same raise is treated as a | ||
| # failure and leaves the row for redelivery, so it must not raise. | ||
| # | ||
| # Together: one POST, buffers dead-lettered, task returns None → the consumer | ||
| # acks (deletes) the row. Retry spacing belongs to the PG layer, not the task. | ||
| # (The Celery branch below keeps kwargs verbatim — byte-identical.) | ||
| pg_kwargs = {**kwargs, "raise_on_final_failure": False, "max_retries": 0} | ||
| try: | ||
| msg_id = enqueue_task( | ||
| task_name=WEBHOOK_NOTIFICATION_TASK, | ||
| queue=queue, | ||
| args=args, | ||
| kwargs=pg_kwargs, | ||
| # enqueue_task types org_id as str; None→"" here satisfies that | ||
| # type only, not runtime — enqueue_task itself re-coerces | ||
| # ``org_id or ""`` at insert (producer.py), so this has no runtime | ||
| # effect. | ||
| org_id=org_string_id or "", | ||
| task_id=dispatch_id, | ||
| ) | ||
| except (ValueError, TypeError) as exc: | ||
|
muhammad-ali-e marked this conversation as resolved.
|
||
| # PG-only permanent failure (enqueue_task validation / JSON encode): | ||
| # re-raise as PermanentDispatchError so the caller dead-letters it. | ||
| # A transient PG error (DB down) is NOT wrapped — it propagates as an | ||
| # ordinary Exception into the caller's retry (revert-to-PENDING) path. | ||
| raise PermanentDispatchError(str(exc)) from exc | ||
| # msg_id correlates this line with the producer's own ``msg_id=`` log, so a | ||
| # dropped notification can be traced across the seam/producer boundary. | ||
| logger.info( | ||
| "Webhook notification enqueued on PG '%s' queue (task_id=%s msg_id=%s)", | ||
| queue, | ||
| dispatch_id, | ||
| msg_id, | ||
| ) | ||
| return DispatchResult(transport=PG_TRANSPORT, task_id=dispatch_id) | ||
| result = celery_app.send_task( | ||
| WEBHOOK_NOTIFICATION_TASK, | ||
| args=args, | ||
| kwargs=kwargs, | ||
| queue=queue, | ||
| ) | ||
| return DispatchResult(transport=CELERY_TRANSPORT, task_id=result.id) | ||
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.