173 lines
6.3 KiB
Markdown
173 lines
6.3 KiB
Markdown
# Task EP.1.4.1 Transactional Outbox Activation & Worker Runtime - 2026-07-13
|
|
|
|
## Scope
|
|
|
|
- activated Activity Business Event publication through transaction-aware outbox enqueue
|
|
- added lease-based outbox claiming fields to `business_event_outbox`
|
|
- added projection outbox worker runtime
|
|
- added manual projection operations service contracts
|
|
- added PostgreSQL `FOR UPDATE SKIP LOCKED` claim strategy in Drizzle store
|
|
- preserved existing in-process Business Event publisher for compatibility and tests
|
|
- preserved current Activity API response contracts, follow-up APIs, dashboard/report datasets, notifications, approvals, and recent-activity behavior
|
|
|
|
## Review Summary
|
|
|
|
Reviewed before and during implementation:
|
|
|
|
- `AGENTS.md`
|
|
- `plans/task-ep.1.4.1.md`
|
|
- `docs/implementation/task-ep1.3-business-event-foundation-2026-07-13.md`
|
|
- `docs/implementation/task-ep1.4-projection-foundation-delivery-reliability-2026-07-13.md`
|
|
- `docs/standards/engineering-constitution.md`
|
|
- `docs/standards/project-foundations.md`
|
|
- `docs/standards/architecture-rules.md`
|
|
- `docs/standards/task-review-checklist.md`
|
|
- `docs/security/crm-authorization-boundaries.md`
|
|
- existing Activity service mutation paths
|
|
- existing Business Event publisher and projection runtime
|
|
|
|
## Implementation Summary
|
|
|
|
### Activity Transactional Outbox Adoption
|
|
|
|
Activity mutation paths now enqueue Business Events to the outbox inside the same `db.transaction()` as the Activity row mutation:
|
|
|
|
- create
|
|
- update
|
|
- assign/reassign
|
|
- reschedule
|
|
- complete
|
|
- cancel
|
|
- delete
|
|
|
|
The Activity API still returns hydrated Activity records after commit. Projection consumers are not run synchronously from Activity source mutations.
|
|
|
|
### Transaction-Aware Publisher
|
|
|
|
Added:
|
|
|
|
- `src/features/foundation/projections/server/transactional-publisher.ts`
|
|
|
|
The publisher validates registry and payload contracts, then inserts into `business_event_outbox` using the supplied transaction/client. This prevents accidental double-publication through the in-process dispatcher.
|
|
|
|
### Worker Runtime
|
|
|
|
Added:
|
|
|
|
- `src/features/foundation/projections/worker.ts`
|
|
|
|
Worker behavior:
|
|
|
|
- claims available events in batches
|
|
- applies a lease with `claimedBy`, `claimedAt`, and `claimExpiresAt`
|
|
- dispatches claimed committed events through `ProjectionConsumerRuntime`
|
|
- does not auto-start unless `PROJECTION_WORKER_ENABLED=true`
|
|
- supports graceful stop semantics for the in-process loop
|
|
|
|
Default operational config:
|
|
|
|
- polling interval: `3000ms`
|
|
- batch size: `25`
|
|
- max concurrent events: `1`
|
|
- max concurrent consumers: `4`
|
|
- claim lease: `60000ms`
|
|
- stale claim: `90000ms`
|
|
- graceful shutdown: `30000ms`
|
|
|
|
### Multi-Instance Claiming
|
|
|
|
Added schema fields:
|
|
|
|
- `business_event_outbox.claimed_by`
|
|
- `business_event_outbox.claimed_at`
|
|
- `business_event_outbox.claim_expires_at`
|
|
|
|
Generated migration:
|
|
|
|
- `drizzle/0004_sharp_mercury.sql`
|
|
|
|
Database store uses PostgreSQL-safe claiming with `FOR UPDATE SKIP LOCKED`. Expired `claimed` or `processing` leases are recoverable.
|
|
|
|
### Manual Operations Contracts
|
|
|
|
Added:
|
|
|
|
- `src/features/foundation/projections/operations.ts`
|
|
|
|
Contracts:
|
|
|
|
- `drainOutbox`
|
|
- `retryEvent`
|
|
- `retryConsumer`
|
|
- `retryDeadLetter`
|
|
- `resolveDeadLetter`
|
|
|
|
Manual operations require `systemRole: "super_admin"` at the service-contract boundary. No public UI/API route was added in this task.
|
|
|
|
### Delivery State Machine
|
|
|
|
Outbox lifecycle:
|
|
|
|
```text
|
|
pending -> claimed -> processing -> completed
|
|
pending -> claimed -> processing -> retry_scheduled
|
|
pending -> claimed -> processing -> dead_letter
|
|
claimed / processing -> lease expired -> claimable again
|
|
```
|
|
|
|
Checkpoint lifecycle remains:
|
|
|
|
```text
|
|
pending -> processing -> completed
|
|
pending -> processing -> retry_scheduled
|
|
pending -> processing -> skipped_unsupported_version
|
|
pending -> processing -> dead_letter
|
|
```
|
|
|
|
Completed checkpoints are not rerun when another consumer fails.
|
|
|
|
### Worker Startup Strategy
|
|
|
|
Current strategy:
|
|
|
|
- no automatic worker startup in Next.js request/build/test paths
|
|
- dedicated process/service should instantiate `ProjectionOutboxWorker` and call `start()`
|
|
- scheduled CLI drain can call `drainOnce()` for UAT or operational repair
|
|
|
|
This avoids duplicate unmanaged loops during hot reload, build, migrations, tests, or serverless request execution.
|
|
|
|
### SLA Baseline
|
|
|
|
Initial non-binding targets:
|
|
|
|
- event available after commit: immediate
|
|
- polling interval: 1-5 seconds, default 3 seconds
|
|
- normal Activity projection delivery: under 10 seconds
|
|
- retry scheduling accuracy: within one polling interval
|
|
- stale claim recovery: lease duration plus one polling interval
|
|
- worker heartbeat/health integration: future operational surface
|
|
|
|
## Compatibility Notes
|
|
|
|
- Activity events are not emitted through both outbox and in-process publisher in production service paths.
|
|
- Existing in-process publisher remains available for tests and explicitly approved compatibility paths.
|
|
- Existing Dashboard, Report, Notification, Approval, Follow-up, and recent-activity behavior remains unchanged.
|
|
- No final Timeline, Calendar, Notification expansion, My Day, Manager Workspace, Executive Workspace, Forecast, or Relationship Health feature was implemented.
|
|
|
|
## Verification
|
|
|
|
- `node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test src/features/foundation/business-events/*.test.ts src/features/foundation/projections/*.test.ts src/features/crm/activities/server/business-events.test.ts`
|
|
- `npx oxlint src/features/foundation/projections src/features/crm/activities/server/business-events.ts src/features/crm/activities/server/repository.ts src/features/crm/activities/server/service.ts src/db/schema.ts`
|
|
- `npm run db:generate`
|
|
|
|
## Typecheck Note
|
|
|
|
`npm run typecheck` is currently blocked by generated `.next/dev/types/routes.d.ts` syntax errors unrelated to the EP.1.4.1 source changes. Targeted runtime tests and scoped lint passed for the changed files.
|
|
|
|
## Residual Risks / Follow-up
|
|
|
|
- Worker process entrypoint/CLI command is not yet wired into `package.json`; runtime modules are ready for a dedicated process or scheduled drain task.
|
|
- Manual retry/dead-letter APIs are intentionally not exposed publicly yet.
|
|
- Health snapshots are recorded by runtime; a user-facing operational health page remains future work.
|
|
- Future source domains must adopt the same transaction-aware publisher through dedicated cutover tasks.
|