How should the outbox lifecycle be exposed through an instrumentation SPI? #474
Replies: 2 comments 2 replies
|
After walking through the relevant processing and failure scenarios, I would propose the following direction. The SPI should expose three stable lifecycle concepts:
Polling, partition coordination, batching, and record-key scheduling should stay outside this record-level SPI. We also do not need callbacks for every internal processor. The final attempt outcome gives us the useful lifecycle information without exposing the current processing-chain implementation as public API. The resulting trace structure would look like this: Each retry is a new sibling attempt under the stored scheduling context. We should never keep a span open while a record waits for its next retry. The attempt should report one of five final outcomes:
Unavailable payload types, payload deserialization failures, and unavailable handlers should be compatibility failures. Only handlers that are actually invoked should create an For compatibility, the existing This seems to close the correlation gap from #467 while keeping the SPI small. Does this look like the right lifecycle model before we turn it into a concrete API proposal? |
|
I think the SPI should preserve and extend the observability we already provide. The observability module should implement this framework-neutral SPI using Micrometer. For this reason, I suggest that we first describe the current observations and trace relationships. Processing Scheduler observation
A new observation is created for every polling-cycle invocation. The polling cycle finds record keys and submits one processing task per key to a Spring does not propagate the observation context to executor threads by default. Applications must enable See the Spring Boot context propagation documentation. When propagation is enabled, the Processing Scheduler context is available in the executor threads. This does not create a new span for each processing task. Without propagation, the executor threads do not have an active trace context. flowchart TB
S["Processing Scheduler span"]
S -. "context propagated<br/>no new span" .-> T1["Processing task for key 1"]
S -. "context propagated<br/>no new span" .-> T2["Processing task for key 2"]
S -. "context propagated<br/>no new span" .-> T3["Processing task for key 3"]
Processing exceptions are currently caught and logged internally. Therefore, they do not escape to Scheduling and handler observationsFrom the user’s perspective, scheduling and later processing should belong to the same trace. This makes it easier to connect logs and spans from the original request to the asynchronous handler execution. We already provide part of this relationship. When a record is scheduled:
When a handler is invoked:
Fallback handlers use the same mechanism. flowchart TB
R["Incoming request span"] --> S["Scheduling span"]
S -. "persisted trace context" .-> H["Primary handler span"]
S -. "persisted trace context" .-> F["Fallback handler span"]
Current problemThe persisted trace context is restored only around the primary or fallback handler invocation. Logs produced outside those observations use the Processing Scheduler trace when executor context propagation is enabled. Without propagation, they have no active trace context. They do not use the trace that originally scheduled the record. This is the problem described in issue #467. Proposed record-processing observationI think we should introduce an observation for a complete record-processing attempt. When processing of one record begins, its persisted context should be restored and a record-processing observation should be created. The primary and fallback handler observations should then be children of the record-processing observation. flowchart TB
R["Incoming request span"] --> S["Scheduling span"]
S -. "persisted trace context" .-> P["Record-processing span"]
P --> H["Primary handler span"]
P --> F["Fallback handler span"]
The Processing Scheduler observation remains separate from the record trace. The record-processing observation remains active while Core processes the record. Record-related logs therefore use the trace that originally scheduled the record. Retry handling, completion, fallback coordination, and permanent-failure handling happen inside the record-processing observation. They do not create separate spans by default. Each retry creates a separate record-processing observation: This would keep record-related logs correlated with the original request. It would also provide a place to report the final outcome of each processing attempt, such as completion, retry, fallback, or permanent failure. |
Uh oh!
There was an error while loading. Please reload this page.
Context
Namastack Outbox performs work on both sides of the transactional outbox boundary:
Observability needs to cover more than the eventual handler invocation. Processing a persisted record can involve:
Some failures occur before a handler can be invoked. During a mixed-version deployment, an instance may encounter a persisted payload type or handler ID that it does not know. A payload type may also be available while the stored payload or context can no longer be deserialized.
These cases differ from a delivery failure, because no registered handler has necessarily received the record.
Discussion 473 defines the compatibility problem for unavailable payload types and handlers. Issue 467 shows a related observability gap: processing-chain logs can execute outside the record's trace scope and therefore lose correlation with the originating request.
Before defining a public instrumentation SPI, we need to agree on the lifecycle, failure categories, and outcomes that it must represent.
Decision to Make
We need to decide which Namastack Outbox operations and processing semantics belong to the instrumentation contract.
In particular:
OutboxRecordcannot be created?Terminology
Scheduling operation
An application request to create and persist one or more outbox records for a payload.
Polling cycle
One scheduler execution that identifies work assigned to the current instance and submits record keys for processing.
Whether polling cycles belong to the instrumentation SPI is part of this decision.
Record-processing attempt
An attempt by a scheduler instance to process one persisted outbox record.
The exact beginning and end of an attempt are part of this decision.
Processing stage
A distinct part of record processing, such as materialization, handler resolution, primary delivery, retry evaluation, fallback handling, or completion.
Whether stages become part of the public SPI is part of this decision.
Handler invocation
One invocation of a registered primary or fallback handler.
Delivery failure
A registered handler was invoked and threw an exception.
Compatibility failure
Handler execution could not begin because the processing instance does not have the persisted payload type or registered handler ID.
Deserialization failure
The persisted type is available, but the stored payload or context cannot be deserialized.
Whether this is a compatibility failure, infrastructure failure, invalid-record failure, or a separate category is part of this decision.
Processing outcome
The state or decision reached by a processing attempt, such as completion, retry, compatibility deferral, or permanent failure.
Required Semantics
The selected design should address the following:
Processing Surface to Consider
The decision should explicitly include or exclude each of the following boundaries:
Failure Scenarios to Cover
The decision should define the processing and instrumentation behavior for:
For each scenario, the decision should state:
Questions for Review
OutboxRecordexists?Decision Process
All reactions