Skip to content

fix(webhook): offload SLA webhook dispatch to Celery task - #244

Merged
usmanimamu17-create merged 1 commit into
ApexChainx:mainfrom
LaGodxy:feat/issue-234-async-webhook-dispatch
Aug 18, 2026
Merged

fix(webhook): offload SLA webhook dispatch to Celery task#244
usmanimamu17-create merged 1 commit into
ApexChainx:mainfrom
LaGodxy:feat/issue-234-async-webhook-dispatch

Conversation

@LaGodxy

@LaGodxy LaGodxy commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #234

Offloads webhook dispatch from the synchronous call path in trigger_sla_violation_webhooks() to the existing dispatch_webhook_delivery Celery task. When a broker is configured (task_always_eager=False), each delivery is enqueued via apply_async instead of blocking the caller. Falls back to synchronous dispatch in eager/single-worker mode or when Celery is unavailable.

Why

The webhook dispatcher called dispatch_delivery() synchronously in a loop, blocking the SLA computation response while outbound HTTP requests completed (10s timeout each). With multiple webhooks, this caused timeout cascades. The existing dispatch_webhook_delivery Celery task was already defined but never wired into the trigger path.

What was built

app/services/webhook_service.py:

File What it contains
dispatch_delivery() Added idempotency guard: skips deliveries already in terminal state (SUCCESS, DEAD_LETTER) to prevent duplicate HTTP calls from concurrent task executions
trigger_sla_violation_webhooks() Detects Celery availability at runtime; enqueues dispatches via dispatch_webhook_delivery.delay() when broker is configured, falls back to synchronous call otherwise

tests/test_webhook_async_dispatch.py: (new)

File What it contains
TestTriggerSlaViolationAsyncDispatch 5 tests: Celery dispatch when broker configured, sync fallback in eager mode, sync fallback when import fails, no-webhook empty return, multiple webhooks each enqueued
TestDispatchDeliveryIdempotency 4 tests: skips SUCCESS, skips DEAD_LETTER, dispatches PENDING, dispatches RETRYING

Integration changes outside <module>/

No existing files modified beyond app/services/webhook_service.py. The existing dispatch_webhook_delivery task in app/tasks/webhook_tasks.py and retry_pending_webhook_deliveries beat task remain unchanged.

Acceptance criteria coverage

  • Webhook dispatch is offloaded to a Celery task (test_celery_dispatches_via_task_when_broker_configured — verifies dispatch_webhook_delivery.delay() is called)
  • SLA calculation endpoints return immediately without waiting for webhook delivery (enqueued via .delay(), not called synchronously)
  • The Celery task handles retries and dead-letter marking correctly (existing dispatch_webhook_delivery task + retry_pending_webhook_deliveries beat task unchanged)
  • A test verifies that dispatching a webhook does not block the calling thread (test_celery_dispatches_via_task_when_broker_configureddispatch_delivery is NOT called synchronously)
  • Fallback to synchronous dispatch when Celery is unavailable (test_fallback_to_sync_when_eager_mode, test_fallback_to_sync_when_celery_import_fails)

Deliberately deferred

  • Converting the webhook service to full async/await (out of scope per issue — focus on Celery offload first)
  • Converting WebhookDispatchLimiter and CircuitBreaker to async primitives (requires async-compatible lock/semaphore, separate concern)

Test plan

  • pytest tests/test_webhook_async_dispatch.py — 9/9 passing (9 new tests)
  • pytest tests/test_webhook_breaker.py tests/test_webhook_dispatch_limiter.py tests/test_webhook_ssrf.py — 20/20 passing (no regressions)

Env vars / Notes

No new env vars. Uses existing CELERY_BROKER_URL and CELERY_TASK_ALWAYS_EAGER. When CELERY_TASK_ALWAYS_EAGER=true (default for local dev), behavior is identical to before — dispatches are synchronous. Set CELERY_TASK_ALWAYS_EAGER=false with a valid Redis broker to get async offload.

The webhook dispatcher in trigger_sla_violation_webhooks() called
dispatch_delivery() synchronously in a loop, blocking the SLA
computation response while outbound HTTP requests completed.

When a broker is configured (task_always_eager=False), dispatches are
now enqueued via the existing dispatch_webhook_delivery Celery task.
Falls back to synchronous dispatch when running in eager mode or when
Celery is unavailable.

Also adds an idempotency guard in dispatch_delivery() to skip
deliveries already in a terminal state (SUCCESS, DEAD_LETTER),
preventing duplicate HTTP calls from concurrent task executions.

@usmanimamu17-create usmanimamu17-create left a comment

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.

LGTM. thanks for contributing.

@usmanimamu17-create
usmanimamu17-create merged commit 301d797 into ApexChainx:main Aug 18, 2026
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.

Webhook dispatcher blocks on synchronous HTTP requests

2 participants