Skip to content

refactor(standalone): dispatch internal work in-process instead of over HTTP - #5963

Open
adrians5j wants to merge 3 commits into
nextfrom
refactor/standalone-internal-dispatch
Open

adrians5j wants to merge 3 commits into
nextfrom
refactor/standalone-internal-dispatch

Conversation

@adrians5j

@adrians5j adrians5j commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Summary

The standalone server reached its own per-request stack by sending HTTP requests to itself. Background tasks, scheduled actions, scheduler boot recovery and the empty-trash timer each POSTed to a route on localhost:PORT. Each route was guarded by a token generated per process, and background tasks started a worker thread per task just to run that POST loop.

This PR replaces all of that with in-process dispatch, the same shape AWS already uses for non-HTTP Lambda invocations.

How it works

  • EventDispatcher. A new abstraction in event-handler-core. createServerHandler registers it on the root container. dispatch(event) calls HandlerApp.handle(event), which builds a fresh request container, matches the event type and runs its handler.
  • Isolation. Dispatch runs inside an AsyncLocalStorage.snapshot() taken at boot. Without this, a task triggered inside withoutAuthorization(...) or withIdentity(...) would carry that override into its own request. The HTTP hop gave this isolation by accident; now it's deliberate, and there's a test for it.
  • Background tasks. InProcessTaskService hands the task to a root TaskLoop. The loop dispatches one BackgroundTaskEvent per iteration and waits as long as the runner asks. There is no worker thread any more.
  • Scheduler. When Bree fires, it dispatches a ScheduledActionEvent. The handler is the existing AWS one, ScheduledActionLambdaHandler, unchanged. Boot recovery is a ScheduledActionRecoverEvent. It runs in the background, so it no longer needs setTimeout(1000) to wait for listen().
  • Empty trash. The timer dispatches an EmptyTrashBinsEvent.
  • Shared event types. BackgroundTaskEventType and ScheduledActionEventType, with their handler abstractions, moved from event-handler-aws to event-handler-core. That leaves api-scheduler with no dependency on event-handler-aws.

Removed

  • the four routes: /background-task, /scheduled-action-run, /scheduled-action-recover and /empty-trash-bins;
  • the three internal tokens;
  • the background-task worker (TaskOrchestrator, workerEntry);
  • the localhost:PORT self-callback helpers.
sequenceDiagram
    participant A as Admin
    participant R as Request (main thread)
    participant L as TaskLoop (root singleton)
    participant D as EventDispatcher
    participant DB as Database

    A->>R: triggerTask(definition, input, delay)
    R->>DB: create task record (pending)
    R->>L: start(BackgroundTaskEvent)
    R-->>A: response
    loop until done, aborted or error
        L->>D: dispatch(BackgroundTaskEvent)
        Note over D: fresh request container, TaskRunner
        D->>DB: task progress / result
        D-->>L: { status, wait, delay }
        Note over L: continue: sleep `wait`, send back `delay`
    end
Loading

Bug fixes found on the way

  • TenantContext restore (separate commit, api-core). withTenant, withRootTenant and withEachTenant put the previous tenant back through setTenant, which dereferences it. Work outside an HTTP request starts with no tenant, so the restore threw a TypeError after the callback had already done its work. I found it running a second API process with its console visible: the empty-trash trigger failed every time. This affects AWS too. EventBridge (empty trash) and EventBridge Scheduler (scheduled publish) invocations have no tenant loader either, so they most likely do their work and then fail the invocation.
  • Scheduler recovery paging. Recovery read only the first 1,000 pending actions, and anything past that was lost on restart. It now pages through all of them with the cursor. CodeRabbit flagged this.

Tests

  • New:

    • DispatchingTaskLoop: loop until done, the delay hand-off, wait, aborted, error, a throwing iteration.
    • InProcessTaskService.
    • EventDispatcher on the Node server: a dispatched event reaches its handler, async context doesn't leak, an unknown event is rejected.
    • TenantContext with no tenant set (3 cases; all fail without the fix).
  • Passing:

    Package Tests
    event-handler-core 88
    event-handler-aws 61
    event-handler-standalone 13
    background-tasks-standalone 13
    background-tasks-aws 12
    api-scheduler 1
    api-scheduler-aws 30
    api-scheduler-standalone 16
    api-event-handler-aws 6
    api-websockets-standalone 43
    api-core 270
  • Live, against this branch on a local standalone server (SQLite):

    • triggerTask(testingRun) started 13 ms after it was created.
    • With delay: 10, it started 10.01 s after it was created.
    • A scheduled publish fired 28 ms after its scheduled time.
    • A publish scheduled before an API restart was re-armed by boot recovery and fired on time after the restart.
    • AI image enrichment worked end to end: upload, task, websocket push "Image enriched" after 6 s.
    • Boot recovery logs re-armed N, and the empty-trash trigger no longer fails.

Notes for review

  • Logging. The root task loop, the scheduler wiring and the trash timer log with console.error. Root singletons have no DI Logger; that's Self-hosted scheduler: revisit the boot-time consoleLogger shim #5446.
  • RequestContainer in the task handler. InProcessBackgroundTaskHandler takes RequestContainer to build TaskRunner's legacy context object, the same as BackgroundTaskLambdaHandler on AWS.
  • Naming. I kept the name ScheduledActionLambdaHandler even though it now also runs on standalone, to keep the AWS diff small.
  • Scope. This PR changes plumbing only. Task concurrency limits, task recovery after a restart, abort reaching a running task, and recovering non-root tenants' schedules are still open. They're the next steps on the audit list.
  • Possible flake. streaming.test.ts "stalled client" failed once in 16 runs, on the first run after a new test file was added. That looks like cold-start transform time eating its 3 s window. It's unrelated to this change, but I'm noting it in case CI hits it.

Workspace: wby-next9 · audit standalone handlers

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Standalone background tasks now run in-process, continuing through repeated task steps until they finish or stop.
    • Scheduled actions can be recovered in the background when the standalone server starts.
    • Background tasks, scheduled actions, and trash-bin cleanup now use event-based handling in standalone environments.
  • Bug Fixes
    • Tenant context is restored correctly after scoped operations, including when no tenant was set beforehand.

adrians5j and others added 3 commits October 8, 2026 23:29
withTenant, withRootTenant and withEachTenant put the previous tenant back
through setTenant, which dereferences it. Work outside an HTTP request starts
with no tenant: a background task, a scheduled action, an EventBridge event.
For those the restore threw a TypeError after the callback had already done
its work, so the invocation failed even though it succeeded.

The helpers now restore through a private restoreTenant that accepts null
and skips the disabled-tenant check, which only matters when switching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er HTTP

The standalone server reached its per-request stack from outside a request
by POSTing to itself: /background-task, /scheduled-action-run,
/scheduled-action-recover and /empty-trash-bins, each guarded by a
per-process token and assuming localhost:PORT was reachable. Background
tasks also spawned a worker thread per task just to drive that POST loop.

Now everything goes through an EventDispatcher that createServerHandler
registers on the root container. It hands an event to the handler app,
which builds a fresh request container for it, the way Lambda invocations
work on AWS. Dispatch runs in an AsyncLocalStorage snapshot taken at boot,
so identity and authorization overrides active in the caller don't leak in.

- Background tasks: a root TaskLoop dispatches one BackgroundTaskEvent per
  iteration and waits as the runner asks. No worker thread.
- Scheduler: Bree fires a ScheduledActionEvent, handled by the same handler
  AWS uses. Boot recovery is a ScheduledActionRecoverEvent, run in the
  background without waiting for listen().
- Empty trash: the timer dispatches an EmptyTrashBinsEvent.

BackgroundTaskEventType and ScheduledActionEventType, with their handler
abstractions, move from event-handler-aws to event-handler-core so both
hosting types share them. api-scheduler no longer depends on
event-handler-aws.

The four routes, three internal tokens, the worker and the localhost
self-callback helpers are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… first 1000

Recovery read one page of 1000. Anything past it was lost on restart. It now
pages through the list with the cursor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

🚓 Slop Cop

⚠️ 4 thing(s) worth a look before merging.

Coherent refactor matching its stated scope; a couple of minor style nits and already-disclosed console-logging tradeoffs, nothing indicating accidental content or integrity risk.

🚨 Should this be in the PR?

🟡 Low — Large deletions match stated refactor scope

The diff deletes ~1515 lines (worker threads, HTTP self-callback routes, internal token abstractions, and their tests) but this matches the PR's explicit description of removing routes, tokens, and the worker-based task orchestrator in favor of in-process dispatch. No unrelated files or unexplained deletions were found; the footprint is coherent with the stated intent.

🟡 Low — console.error/console.log used in root singleton wiring

schedulerServer.ts, bulkActionsServer.ts, and DispatchingTaskLoop.ts use console.error/console.log directly instead of the DI Logger (no-console-in-backend.md). The PR author explicitly flags this in 'Notes for review' as a known tracked issue (#5446) since root singletons have no DI Logger available, so this is a deliberate, disclosed tradeoff rather than an accidental leftover.

📏 Code-style rule checks

🟡 Low — Multiple named imports on one line

packages/api-event-handler-standalone/src/scheduler/RecoverScheduledActionsHandler.ts imports RawTenantId and RequestTenantLoader in a single import statement from the same module path, combining with a prior line from a different module. Per one-import-per-line.md, each named import should be on its own line; several other files in this PR (e.g. registerInboundEventTypes.ts, schedulerServer.ts) correctly split these, making this one inconsistent.

🟡 Low — Potential mixed-layer dependency in RecoverScheduledActionsHandler

RecoverScheduledActionsHandlerImpl composes several use cases (GetRootTenantUseCase, ListScheduledActionsUseCase) and primitives (RawTenantId, RequestTenantLoader, IdentityContext) alongside SchedulerSingleton, a stateful service that isn't a use case or repository. This may be intentional (SchedulerSingleton is a domain service, similar to the allowed exception in no-mixed-layers-in-dependencies.md), so flagged as low-confidence only.

Automated, non-blocking heads-up from an LLM. It can be wrong — use your judgment. Regenerates on every push.

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7100ddda-d7cd-4309-b770-01263626a564
📥 Commits

Reviewing files that changed from the base of the PR and between f0f98d2 and 1845614.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (63)
  • ai-context/core-features-reference.md
  • packages/api-core/__tests__/tenancy/tenantContext.test.ts
  • packages/api-core/src/features/tenancy/TenantContext/TenantContext.ts
  • packages/api-event-handler-aws/__tests__/registerInboundEventTypes.test.ts
  • packages/api-event-handler-aws/src/composition/registerInboundEventTypes.ts
  • packages/api-event-handler-standalone/package.json
  • packages/api-event-handler-standalone/src/bulkActions/bulkActionsServer.ts
  • packages/api-event-handler-standalone/src/createWebinyApiHandler.ts
  • packages/api-event-handler-standalone/src/scheduler/RecoverScheduledActionsHandler.ts
  • packages/api-event-handler-standalone/src/scheduler/ScheduledActionRecoverEventHandler.ts
  • packages/api-event-handler-standalone/src/scheduler/ScheduledActionRecoverEventType.ts
  • packages/api-event-handler-standalone/src/scheduler/ScheduledActionRecoverRoute.ts
  • packages/api-event-handler-standalone/src/scheduler/ScheduledActionRunRoute.ts
  • packages/api-event-handler-standalone/src/scheduler/abstractions/InternalToken.ts
  • packages/api-event-handler-standalone/src/scheduler/schedulerServer.ts
  • packages/api-headless-cms-bulk-actions-standalone/package.json
  • packages/api-headless-cms-bulk-actions-standalone/src/BulkActionsInternalToken.ts
  • packages/api-headless-cms-bulk-actions-standalone/src/EmptyTrashBinRoute.ts
  • packages/api-headless-cms-bulk-actions-standalone/src/EmptyTrashBinsEventHandler.ts
  • packages/api-headless-cms-bulk-actions-standalone/src/EmptyTrashBinsEventType.ts
  • packages/api-headless-cms-bulk-actions-standalone/src/EmptyTrashBinsFeature.ts
  • packages/api-headless-cms-bulk-actions-standalone/src/TriggerEmptyTrashBinsHandler.ts
  • packages/api-headless-cms-bulk-actions-standalone/src/index.ts
  • packages/api-headless-cms-bulk-actions-standalone/tsconfig.build.json
  • packages/api-headless-cms-bulk-actions-standalone/tsconfig.json
  • packages/api-scheduler-aws/__tests__/eventHandler.test.ts
  • packages/api-scheduler/package.json
  • packages/api-scheduler/src/ScheduledActionLambdaHandler.ts
  • packages/api-scheduler/tsconfig.build.json
  • packages/api-scheduler/tsconfig.json
  • packages/background-tasks-aws/__tests__/backgroundTaskLambdaHandler.test.ts
  • packages/background-tasks-aws/__tests__/backgroundTasksAwsFeature.test.ts
  • packages/background-tasks-aws/src/BackgroundTaskLambdaHandler.ts
  • packages/background-tasks-standalone/__tests__/backgroundTaskRoute.test.ts
  • packages/background-tasks-standalone/__tests__/dispatchingTaskLoop.test.ts
  • packages/background-tasks-standalone/__tests__/inProcessTaskService.test.ts
  • packages/background-tasks-standalone/__tests__/taskOrchestrator.test.ts
  • packages/background-tasks-standalone/package.json
  • packages/background-tasks-standalone/src/BackgroundTasksStandaloneFeature.ts
  • packages/background-tasks-standalone/src/domain/InternalToken.ts
  • packages/background-tasks-standalone/src/domain/TaskLoop.ts
  • packages/background-tasks-standalone/src/handlers/InProcessBackgroundTaskHandler.ts
  • packages/background-tasks-standalone/src/loop/DispatchingTaskLoop.ts
  • packages/background-tasks-standalone/src/routes/BackgroundTaskRoute.ts
  • packages/background-tasks-standalone/src/service/InProcessTaskService.ts
  • packages/background-tasks-standalone/src/service/WorkerTaskService.ts
  • packages/background-tasks-standalone/src/worker/TaskOrchestrator.ts
  • packages/background-tasks-standalone/src/worker/TaskOrchestratorMessage.ts
  • packages/background-tasks-standalone/src/worker/workerEntry.ts
  • packages/event-handler-aws/src/abstractions/handlers/index.ts
  • packages/event-handler-aws/src/eventTypes/index.ts
  • packages/event-handler-core/src/features/backgroundTask/BackgroundTaskEventHandler.ts
  • packages/event-handler-core/src/features/backgroundTask/BackgroundTaskEventType.ts
  • packages/event-handler-core/src/features/backgroundTask/index.ts
  • packages/event-handler-core/src/features/dispatch/EventDispatcher.ts
  • packages/event-handler-core/src/features/dispatch/index.ts
  • packages/event-handler-core/src/features/scheduledAction/ScheduledActionEventHandler.ts
  • packages/event-handler-core/src/features/scheduledAction/ScheduledActionEventType.ts
  • packages/event-handler-core/src/features/scheduledAction/index.ts
  • packages/event-handler-core/src/index.ts
  • packages/event-handler-standalone/__tests__/eventDispatcher.test.ts
  • packages/event-handler-standalone/src/createServerHandler.ts
  • packages/event-handler-standalone/src/server/HandlerAppEventDispatcher.ts
💤 Files with no reviewable changes (19)
  • packages/api-headless-cms-bulk-actions-standalone/src/BulkActionsInternalToken.ts
  • packages/api-event-handler-standalone/src/scheduler/abstractions/InternalToken.ts
  • packages/background-tasks-standalone/src/worker/TaskOrchestratorMessage.ts
  • packages/background-tasks-standalone/tests/backgroundTaskRoute.test.ts
  • packages/api-scheduler/tsconfig.json
  • packages/background-tasks-standalone/package.json
  • packages/background-tasks-standalone/src/worker/workerEntry.ts
  • packages/api-event-handler-standalone/src/scheduler/ScheduledActionRunRoute.ts
  • packages/background-tasks-standalone/src/service/WorkerTaskService.ts
  • packages/background-tasks-standalone/src/worker/TaskOrchestrator.ts
  • packages/background-tasks-standalone/src/domain/InternalToken.ts
  • packages/api-scheduler/tsconfig.build.json
  • packages/background-tasks-standalone/src/routes/BackgroundTaskRoute.ts
  • packages/api-scheduler/package.json
  • packages/api-event-handler-standalone/src/scheduler/ScheduledActionRecoverRoute.ts
  • packages/event-handler-aws/src/abstractions/handlers/index.ts
  • packages/background-tasks-standalone/tests/taskOrchestrator.test.ts
  • packages/api-headless-cms-bulk-actions-standalone/src/EmptyTrashBinRoute.ts
  • packages/event-handler-aws/src/eventTypes/index.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The pull request adds event dispatch to the core and standalone event-handler packages. It replaces standalone worker and internal HTTP-route paths for background tasks, scheduled actions, and empty-trash triggers. It also changes tenant-context restoration when callbacks start without a tenant.

Changes

Standalone event runtime

Layer / File(s) Summary
Core event dispatch and package integration
packages/event-handler-core/src/features/*, packages/event-handler-standalone/src/*, packages/event-handler-aws/src/*, packages/api-scheduler/*, packages/background-tasks-aws/*, packages/api-event-handler-aws/*
The core package exports event definitions and the EventDispatcher abstraction. The standalone server registers a dispatcher that calls the handler app using a captured boot context. AWS and scheduler packages import background-task and scheduled-action event definitions from the core package.
In-process background-task execution
packages/background-tasks-standalone/src/*, packages/background-tasks-standalone/__tests__/*, ai-context/core-features-reference.md
The standalone feature registers an in-process task service, event handler, and dispatching loop. The loop handles continuation, waits, stop statuses, errors, and its duration limit. Tests cover task dispatch and loop behavior. Worker and HTTP-route implementations and their tests are removed.
Scheduled-action dispatch and recovery
packages/api-event-handler-standalone/src/scheduler/*, packages/api-event-handler-standalone/src/createWebinyApiHandler.ts, packages/api-event-handler-standalone/package.json
Bree timer callbacks dispatch scheduled-action events. Startup dispatches a recovery event without waiting for the result. The recovery handler lists root-tenant actions in pages of 1,000 and returns a recovered count. The scheduled-action routes and internal-token abstraction are removed.
Empty-trash event handling
packages/api-headless-cms-bulk-actions-standalone/src/*, packages/api-headless-cms-bulk-actions-standalone/package.json, packages/api-headless-cms-bulk-actions-standalone/tsconfig*.json, packages/api-event-handler-standalone/src/bulkActions/*
The bulk-actions package adds an event type, handler, and feature. The standalone periodic trigger dispatches the event. The token-protected HTTP route and token abstraction are removed. Package dependencies and TypeScript project ordering are updated.

Tenant-context restoration

Layer / File(s) Summary
Restore unset tenant context
packages/api-core/src/features/tenancy/TenantContext/TenantContext.ts, packages/api-core/__tests__/tenancy/tenantContext.test.ts
The tenant-scoped methods save the current tenant directly and restore it in finally. Tests verify that calls starting without a tenant leave the context unset.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~50 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant InProcessTaskService
  participant TaskLoop
  participant EventDispatcher
  participant HandlerApp
  participant InProcessBackgroundTaskHandler
  InProcessTaskService->>TaskLoop: start task event
  TaskLoop->>EventDispatcher: dispatch task event
  EventDispatcher->>HandlerApp: handle task event
  HandlerApp->>InProcessBackgroundTaskHandler: invoke matched handler
  InProcessBackgroundTaskHandler-->>EventDispatcher: return task result
  EventDispatcher-->>TaskLoop: return task result
Loading

Merge Risk: ⚪ Minimal · up to 18456

No confirmed issue remains that should block merging after normal checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 39 files. (5 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: replacing standalone internal HTTP calls with in-process dispatch.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 39 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
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