Repository navigation
Conversation
|
|
||
| private MessageStorageProfile profileAtStartup; | ||
|
|
||
| void start(@Observes @Priority(Interceptor.Priority.APPLICATION) StartupEvent event) { |
| } | ||
| } | ||
|
|
||
| void start(@Observes @Priority(Interceptor.Priority.PLATFORM_BEFORE) StartupEvent event, MessageStorageProfile profile) { |
There was a problem hiding this comment.
The @observes StartupEvent parameter registers early startup logging and profile resolution in CDI. Removing it would remove the observer registration even though the payload is unused. Retaining it; an explanatory source comment is prepared locally.
| }); | ||
| } | ||
|
|
||
| void stop(@Observes @Priority(Interceptor.Priority.PLATFORM_BEFORE) ShutdownEvent event) throws InterruptedException { |
There was a problem hiding this comment.
The @observes ShutdownEvent parameter registers provider draining before worker shutdown. Its payload is intentionally unused, but the parameter and annotation are required for CDI lifecycle invocation. Retaining it; an explanatory source comment is prepared locally.
| return result; | ||
| } | ||
|
|
||
| void start(@Observes @Priority(Interceptor.Priority.APPLICATION + 100) StartupEvent event) { |
There was a problem hiding this comment.
The @observes StartupEvent parameter registers pipeline activation after worker startup. Its payload is intentionally unused, but removing the parameter would remove the CDI observer registration. Retaining it; an explanatory source comment is prepared locally.
| } | ||
|
|
||
| public record Completed(SelectionId selection, Set<SourceId> sources) { | ||
| public Completed { |
There was a problem hiding this comment.
Completed defensively snapshots sources through copySources(), which calls Set.copyOf(). The sources.clear() line cited by the alert intentionally mutates the caller collection in a regression test. Subsequent assertions prove Completed retains both source IDs and completed.sources().clear() throws UnsupportedOperationException. OutboundWorkTest passes; no additional copy is needed.
bb390a2 to
d9f669e
Compare
d9f669e to
a5e09cd
Compare
a5e09cd to
4271407
Compare
Task 1.1 - Define stage contracts, ownership transitions, and the composition boundary. Add single-destination lifecycle interfaces, typed work identities, and embedding contracts. Document stage-consistent recovery and verify public contract boundaries. Refs #338 Signed-off-by: pavlos <pavlos@cytech.gr>
Task 1.2 - Add standalone backend selection, startup validation, and profile logging. Reject unavailable profiles before worker startup, preserve core embedding independence, and verify configuration and startup behavior. Refs #338 Signed-off-by: pavlos <pavlos@cytech.gr>
Task 2.1 - Implement memory pending ownership and bounded router selection. Retain accepted sources through routing, select indexed eligible work within bounded queued and in-flight capacity, and preserve retry ownership through failures. Refs #338 Signed-off-by: pavlos <pavlos@cytech.gr>
Task 2.1a - Implement single-destination memory routed-work storage. Track destination takes, retry-safe forwarding, and terminal cleanup while retaining source ownership and rejecting conflicting assignments. Refs #338 Signed-off-by: pavlos <pavlos@cytech.gr>
Task 2.1b - Implement the concrete outbound lifecycle coordinator. Coordinate owned routing, required handoff, retryable cleanup, and shutdown. Remove downstream-only worker forwarding and document scope ownership rules. Refs #338 Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Signed-off-by: pavlos <pavlos@cytech.gr>
Empty legacy worker loops acquired rate-limit permits before checking their queues. With lifecycle traffic sharing the same worker limiter, idle polling consumed send capacity and delayed actual submissions. Acquire a permit only after obtaining a message, preserving one shared per-worker TPS budget for legacy and coordinated attempts. Avoid rate waiting after shutdown and retain the post-wait check that restores taken work. Verify idle polling, shutdown during dequeue and rate acquisition, and real shared-limiter pacing across legacy and lifecycle sends. The focused SMPP, dispatch and shutdown suite passes 48 tests. Signed-off-by: pavlos <pavlos@cytech.gr>
Taken and routed selections have already left the ready deque. Unconditional removal searched the unrelated router backlog on every provider completion while holding shared storage and coordinator locks. Remove from the ready deque only for queued selections, retaining source-first cleanup, routing capacity accounting and idempotence. Verify queued, taken and routed cleanup preserves unrelated work ordering and capacity; all 80 focused storage and coordinator tests pass. Signed-off-by: pavlos <pavlos@cytech.gr>
Blocking rate waits, worker filters and SMPP sends occupied the shared two-thread scheduler, allowing a slow destination to delay healthy destinations and limiting effective sending concurrency. Reserve worker slots before submitting preparation to bounded dispatcher-owned pools sized by threadCount. Preserve the shared per-worker TPS limiter and parent-owned provider retries and handoffs. Retire stopped-worker pools and shut down owned pools after drain without closing application-owned timer schedulers. Verify destination and timer isolation, three occupied slots with a fourth attempt waiting, pool shutdown and worker replacement. Full reactor verify passes 393 core unit tests, 39 integration tests and 43 app tests. Signed-off-by: pavlos <pavlos@cytech.gr>
A mandatory 100 ms pause after every bounded processing cycle imposed a global staging ceiling even when ready messages and provider capacity were available. Schedule the next bounded cycle immediately after successful routing or provider scheduling. Retain idle, pause and failure backoff so unroutable work cannot trigger a busy loop. Fence queued cycles and rescheduling during shutdown, preserving per-worker TPS and execution limits. Add seven deterministic scheduling cases and retain real HTTP pipeline and shutdown coverage. Full reactor verify passes 393 core unit tests, 39 integration tests and 50 app tests with zero Checkstyle violations. Signed-off-by: pavlos <pavlos@cytech.gr>
Match the credential fixture's synchronized getter contract and annotate multipart listener overrides. Explain why CDI observer parameters and the validated-profile producer dependency remain necessary despite unused payload values. Preserve lifecycle ordering and the existing immutable source snapshots rather than removing dependencies or adding redundant copies to silence analyzer comments. All 42 focused ownership, HTTP and startup-profile tests pass with zero Checkstyle violations. Signed-off-by: pavlos <pavlos@cytech.gr>
Reuse private completion monitors and notification-based drain waits to avoid retaining future copies during backlog polling. Drain per-worker FIFO attempts directly instead of scheduling per-message contention timers, preserving preparation concurrency and asynchronous SMPP windows. Report fatal processing and rescheduling failures through standalone readiness. Cover monitor retention, FIFO and pause scheduling, shutdown drain, and processor failure handling. Full reactor verify passes 396 core unit tests, 39 integration tests and 53 app tests with zero Checkstyle violations. Matched 100-msg/s HTTP+DLR and SMPP+DLR rehearsals each complete all 60,000 messages and receipts without rejection or unfinished work. Signed-off-by: pavlos <pavlos@cytech.gr>
4271407 to
4285125
Compare
Signed-off-by: pavlos <pavlos@cytech.gr>
Summary
Verification
4271407fdec71e82b189b33c99ab0bc927f35469.AWS performance evidence
Both runs below tested application revision
4271407fdec71e82b189b33c99ab0bc927f35469, JVM mode, DLR enabled, offered 350 messages/s, unlimited worker TPS, 120-second warm-up, 600-second measurement and 120-second maximum drain. Sendium ran ont4g.small, the harness onc7g.large, and PostgreSQL 17.11 ondb.t4g.small.Both observations had zero generator misses, rejections, admission failures/timeouts, duplicate receipts or unfinished accepted work after drain. Minute-boundary provider backlog remained small and bounded, and no late forward-progress stall appeared. Evidence validation and cleanup passed.
The existing provider-batching SMPP reference at 350/s is additional context, not a new main-branch matched control: it tested a different application branch and harness revision. These single AWS observations demonstrate the tested workload, not maximum capacity or an indefinite-duration guarantee. HTTP receipt-forwarding latency includes its callback path and must not be compared as an identical boundary to SMPP receipt return.
Architecture: before and after
The standalone outbound path changes from passing messages between runtime queues to coordinating explicitly owned stages, retaining accepted work until provider processing and required handoffs finish. The diagrams compare main
8a67c23with this feature and show shipped defaults.Before: queue-driven processing
HTTP submissions entered the legacy router queue; SMPP submissions first passed through protocol-specific ingress buffering and multipart handling. The router applied routing rules and enqueued messages for destination workers. Workers submitted to providers, while callbacks and worker policies handled outcomes and retries.
flowchart TB http["HTTP"] --> routerQueue["Router queue"] smpp["SMPP"] --> ingressBatch["SMPP buffer (8)<br/>100 events / 100 ms"] ingressBatch --> routerQueue routerQueue --> routing["Routing"] routing --> workerQueue["Worker queue"] workerQueue --> worker["Worker"] worker --> provider["Provider"] provider -. "Outcomes / DLRs" .-> tracking["DLR subsystem"]The queue provider already abstracted queue access. What was missing was a shared, backend-neutral outbound lifecycle record connecting admission, route assignment, in-flight provider processing, and final cleanup. Responsibility was distributed across queue membership, worker state, and callbacks.
After: coordinator-owned stages
The standalone application assembles an
OutboundCoordinatorand three stage stores. Accepted sources remain retained while later stages execute. References such as (1) point to the notes below; numbers in the diagrams show shipped defaults. Store capacities and per-cycle take limits are separate bounds.flowchart TB ingress["HTTP / SMPP"] cycle["Cycle (7)<br/>100 ms idle backoff"] -.-> selected ingress --> pending["Pending (1)<br/>10,000 sources max"] pending -. "Admitted" .-> ack["Client ACK"] pending -->|"100/cycle max"| selected["Selected (2)<br/>1,000 unassigned items max"] selected -->|"100/cycle max"| routing["Routing"] routing --> routed["Routed (3)<br/>10,000 selections max"] routed -->|"100/worker/cycle max"| dispatch["Dispatch (4)<br/>1 preparation slot/worker"] dispatch --> provider["Provider"] provider -. "Retry" .-> dispatch provider --> handoff["Handoff (5)"] handoff --> cleanup["Cleanup (6)"]Only Pending, Selected and Routed are outbound stores. Dispatch, handoff and cleanup are processing phases. The coordinator governs ownership and transitions across them; arrows show progression, not deletion of earlier records.
Separate DLR batches - already present on main
Provider-event persistence and client receipt delivery are separate batches:
flowchart TB events["Provider events"] --> processor["DLR batch (9)<br/>100 events / 500 ms<br/>10,000 outstanding max"] processor --> postgres["PostgreSQL"] postgres -. "Accept/reject commit" .-> handoff["Handoff (5)"] postgres -->|"Final outcome"| delivery["Pending DLR"] delivery --> http["HTTP return (10)<br/>500 claims / 1 s schedule<br/>10 callbacks/host max"] delivery --> smpp["SMPP return (11)"]Diagram notes
PendingMessageStore: up to 10,000 accepted sources, including held multipart parts and in-flight sources. Admit before HTTP 202 / SMPP success. When full, new admissions fail: HTTP 503 or SMPPSTATUS_THROTTLED; they are not acknowledged as accepted. Existing work keeps processing, and cleanup frees space for later admissions. Moving a message to the next stage does not free its pending slot. An aggregate retains every original source ID.SelectedRouterStore: up to 1,000 queued or taken unassigned items. Select up to 100 ready items/cycle, limited by free slots, and route up to 100 takes/cycle through existing routing rules. When full, additional ready work stays in Pending until a routing slot is free. Recording a destination frees that slot but keeps bookkeeping until cleanup. A routing miss or recoverable lookup failure returns the item for another routing attempt; a terminal routing drop discards it. An aggregate is one selected item; held parts wait for reassembly/publication before selection.RoutedWorkStore: up to 10,000 retained selections across all destinations, using the same configured capacity as pending. Records the chosen destination and queues assigned work for that worker. “Unfinished” means still waiting to be sent, awaiting provider outcomes, running retries, or waiting for required handoff/cleanup. Taking an item for dispatch does not remove its ownership record. If destination recording cannot succeed, retain the original assignment intent and retry it without repeating routing lookup; work is not silently dropped or sent without a record.threadCount(default 1), not the take batch size. Busy or paused workers leave work waiting; the TPS limit controls how quickly attempts can send. The shared worker TPS cap defaults to 0 = unlimited. Async responses do not hold preparation slots, but their messages still occupy pending capacity. Provider outcomes drive completion or the configured retry policy; multipart requests and replacement attempts stay under the original source ownership.sendium.message.selection-batch-size, applied separately. Do not wait for batches to fill. Continue immediately after productive routing/dispatch; back off 100 ms when idle, paused or unsuccessful.ProviderDlrProcessor: up to 10,000 outstanding events, including processing/completion. Collect up to 100 events, waiting up to 500 ms from the oldest event, then persist. Acceptance, rejection and final-receipt events share this processor. 100 events is not 100 messages; one message can produce multiple events. Collection delay is not an end-to-end commit-latency guarantee.deliver_smand complete the delivery attempt after the required successfuldeliver_sm_respacknowledgements. This path does not use the HTTP batch-500 limit.Settings behind the diagram
sendium.message.pending.capacitysendium.message.router-queue.capacitysendium.message.selection-batch-sizethreadCount/tpssendium.dlr.batch.sizesendium.dlr.batch.max-delaysendium.dlr.batch.queue-capacityNone of the per-cycle batch values is a messages-per-second cap. The SMPP request window is another independent limit; our performance preset uses a 1,000-request window.
Important boundaries
memory/memory/memoryoutbound storage is implemented. The contracts create a replacement boundary for later backends; they do not make accepted outbound work durable today. Restart can still lose unfinished outbound work.Current guarantees
memory/memory/memory; accepted and in-flight outbound work remains non-durable across process restart.Refs #338
Related epic: #337