Parent epic: #337
Delivery step 1; prerequisite for focused contracts #343 and #344, filesystem work #339/#345, and future PostgreSQL work #333.
Problem
Current HTTP/SMPP ingress acknowledges process-local queue insertion. Router and worker queues destructively dequeue messages; retries, multipart state, and asynchronous provider work are process-local. A durable source spool alone preserves acceptance, but resuming an expensive selected batch or a recorded destination requires separately defined storage boundaries. The existing code also injects concrete InMemoryQueueProvider in ingress, routing, worker startup and resource wiring.
Goal
Define the backend-neutral outbound storage architecture and deliver its first working memory/memory/memory vertical slice. Add the initial interfaces/classes, lifecycle coordinator/wiring, global backend selection and startup validation, and memory-backed implementations using the existing non-durable behavior. This first slice makes the abstraction executable and easy to review; #343 and #344 deepen the contracts and tests before physical backends are implemented.
Architecture and initial interfaces
Define three globally selected, startup-only stage roles, with public configuration semantics equivalent to:
sendium.sms.pending.backend=memory|file|postgresql
sendium.sms.router-queue.backend=memory|file|postgresql
sendium.sms.routed-work.backend=memory|file|postgresql
The API should expose pending admission/completion, bounded selection into a router backlog, destination recording/completion, lifecycle coordination, backend identity/readiness, and actionable failures, without leaking SQL, paths, CBOR, or physical queue classes. Choose clear interfaces, backend registration/selection, and a single startup profile validator; avoid building speculative durable operations or codecs in this issue.
accepted pending -> selected for router queue -> routed to destination(s) -> terminal completion
- The accepted pending source remains authoritative until all destinations and required existing DLR handoff are terminal.
- Selection chooses bounded eligible, priority-aware work; when persisted in a later backend, its committed batch is recoverable without repeating expensive selection. Taking work from an in-memory router queue is not a separate persisted state in this single-process design.
- Routing records all chosen destinations, including copied routes. When persisted in a later backend, unfinished destinations resume without rerouting.
- On graceful shutdown, return messages taken for routing but not yet assigned a destination to the router queue. Messages already routed remain with their chosen worker queue; drain unfinished in-flight worker work back to that queue. Before shutdown completes, persist each queue when its configured backend is durable so restart resumes at the correct stage. Memory-backed stages retain their documented non-durable behavior.
- Document how HTTP/SMPP admission, multipart, routing/filters, worker dispatch, provider outcome and existing PostgreSQL
DlrStorage cross the abstraction. Startup recovery, capacity/backpressure and ownership failures must be visible.
First working profile and rollout
| Pending / selected router / routed work |
Future restart guarantee |
memory/memory/memory |
Non-durable; available in this issue |
file/memory/memory |
Recover accepted work; reselect and reroute (#339) |
file/file/memory |
Recover selected batch; reroute (#345) |
file/memory/file |
Reselect not-yet-routed work; recover destinations (#345) |
file/file/file |
Recover selected batch and destinations (#345) |
Durable profiles require surviving storage and remain at least once: provider submission before terminal completion may repeat. Retry count/delay, host/power-loss recovery and provider outcome checkpoints are not part of the initial lifecycle.
Deliverables and acceptance criteria
Out of scope
Detailed pending/multipart admission contract, versioned CBOR schema and tests (#343); detailed durable batch selection/routed-work transition and failure-boundary tests (#344); physical file formats and backends (#339/#345), PostgreSQL schema/queries (#333), DLR backend redesign, multi-process leases, and exactly-once provider delivery.
Parent epic: #337
Delivery step 1; prerequisite for focused contracts #343 and #344, filesystem work #339/#345, and future PostgreSQL work #333.
Problem
Current HTTP/SMPP ingress acknowledges process-local queue insertion. Router and worker queues destructively dequeue messages; retries, multipart state, and asynchronous provider work are process-local. A durable source spool alone preserves acceptance, but resuming an expensive selected batch or a recorded destination requires separately defined storage boundaries. The existing code also injects concrete
InMemoryQueueProviderin ingress, routing, worker startup and resource wiring.Goal
Define the backend-neutral outbound storage architecture and deliver its first working
memory/memory/memoryvertical slice. Add the initial interfaces/classes, lifecycle coordinator/wiring, global backend selection and startup validation, and memory-backed implementations using the existing non-durable behavior. This first slice makes the abstraction executable and easy to review; #343 and #344 deepen the contracts and tests before physical backends are implemented.Architecture and initial interfaces
Define three globally selected, startup-only stage roles, with public configuration semantics equivalent to:
The API should expose pending admission/completion, bounded selection into a router backlog, destination recording/completion, lifecycle coordination, backend identity/readiness, and actionable failures, without leaking SQL, paths, CBOR, or physical queue classes. Choose clear interfaces, backend registration/selection, and a single startup profile validator; avoid building speculative durable operations or codecs in this issue.
DlrStoragecross the abstraction. Startup recovery, capacity/backpressure and ownership failures must be visible.First working profile and rollout
memory/memory/memoryis the only supported profile at this step. Log the effective profile and a prominent non-durable warning at startup.file/file/memory, during startup with a clear error naming the unsupported profile and supported choices. Never replace a requested file/PostgreSQL backend with memory, even with a warning.file/memory/memory; feat(storage): persist selected-router and routed-work state on filesystem #345 adds the other planned file-pending/file-or-memory stage mixes as opt-in profiles. All three outbound selectors default tomemorywhen omitted;memory/memory/memoryremains the non-durable default throughout delivery. feat(storage): implement PostgreSQL outbound stage backends #333 adds explicitly supported PostgreSQL combinations. Backend changes with outstanding durable records need a drain/migration rule; they must never silently strand work.memory/memory/memoryfile/memory/memoryfile/file/memoryfile/memory/filefile/file/fileDurable profiles require surviving storage and remain at least once: provider submission before terminal completion may repeat. Retry count/delay, host/power-loss recovery and provider outcome checkpoints are not part of the initial lifecycle.
Deliverables and acceptance criteria
file/file/memory, partial-file and PostgreSQL requests fail before admission. Startup logs identify the effective non-durable profile; there is no silent fallback.Out of scope
Detailed pending/multipart admission contract, versioned CBOR schema and tests (#343); detailed durable batch selection/routed-work transition and failure-boundary tests (#344); physical file formats and backends (#339/#345), PostgreSQL schema/queries (#333), DLR backend redesign, multi-process leases, and exactly-once provider delivery.