Loading…
Loading…
Manages asynchronous job dispatching between the gateway and backend relays using a database table. Defines the lifecycle for enqueuing, claiming, and
The Relay Dispatch Repository provides a persistent, transactional job queue for asynchronous tasks delegated from the API gateway to a backend "relay" component. It uses the s2r_relay_dispatch database table to manage the lifecycle of these jobs.
This component solves the problem of decoupling the gateway from backend services for operations that may be long-running. Instead of holding a request open, the gateway can enqueue a job, return an identifier to the API consumer, and allow a separate relay process to execute the work and post the result.
The repository manages the flow of jobs through several states:
pending status.pending jobs, transitioning them to claimed.succeeded or failed.expired.NOTE
This component and its functionality are only active when the platform's SaaS capability is licensed and enabled. When not licensed, the component is registered but remains unused.
A dispatch job progresses through a well-defined set of states, managed by the methods of the repository. The primary state transitions are from pending to claimed, and finally to a terminal state (succeeded, failed, or expired).
stateDiagram-v2
direction LR
[*] --> pending: enqueue()
pending --> claimed: claim()
pending --> expired: markExpired()
claimed --> succeeded: complete()
claimed --> failed: complete()
claimed --> expired: markExpired()
succeeded --> [*]
failed --> [*]
expired --> [*]The repository orchestrates the job lifecycle through a series of database operations. The interaction between the gateway, the repository, and the relay follows a specific sequence.
sequenceDiagram
participant Gateway
participant Relay Dispatch Repository
participant Relay
Gateway->>Relay Dispatch Repository: 1. enqueue(job details)
note right of Gateway: Creates a 'pending' job
Relay Dispatch Repository-->>Gateway: job_id
loop Poll for work
Relay->>Relay Dispatch Repository: 2. claim(relay_id)
end
note left of Relay: Atomically claims oldest 'pending' jobs
Relay Dispatch Repository-->>Relay: List of DispatchJob
note left of Relay: Executes the task defined in the job
Relay->>Relay Dispatch Repository: 3. complete(job_id, result)
note right of Gateway: Updates job to 'succeeded' or 'failed'
loop Poll for result
Gateway->>Relay Dispatch Repository: 4. pollStatus(job_id)
end
Relay Dispatch Repository-->>Gateway: DispatchResultAll repository operations are strictly scoped to a single tenant to ensure data isolation in a multi-tenant environment.
app.tenant_id). All subsequent SQL queries within the repository are filtered by this bound tenant ID.DISPATCH_TENANT_SCOPE_SQL) that enforces tenant boundaries. This acts as a defense-in-depth measure, ensuring that even a connection with elevated privileges cannot access another tenant's jobs. This predicate also requires that a tenant context is bound, preventing operations from unintentionally targeting a default tenant.claim and complete methods are pinned to a specific relay_id in addition to the tenant ID. This prevents one relay from claiming or completing jobs intended for a different relay, even if they both belong to the same tenant.The repository is designed to support concurrent relay workers and idempotent operations.
claim method uses a FOR UPDATE SKIP LOCKED clause in its SQL query. This allows multiple relay instances to poll the s2r_relay_dispatch table simultaneously. When one relay worker locks a set of rows to claim them, other workers will skip those locked rows and attempt to claim the next available ones, preventing race conditions and ensuring no job is processed more than once.complete method is idempotent. It will only update a job if its current status is claimed. If a relay attempts to complete a job that has already been completed (or expired), the operation affects zero rows and is treated as a no-op.The RelayDispatchRepository exposes methods for managing the entire lifecycle of a dispatch job.
enqueueEnqueues a new dispatch job in the pending state. This is typically called by the gateway when it receives a request to be processed asynchronously.
| Parameter | Type | Description |
|---|---|---|
tenantId | UUID | The ID of the tenant for whom the job is being created. This must match the tenant context of the connection. |
relayId | String | The identifier of the relay that is designated to execute this job. |
correlationId | String | An optional correlation ID to trace the request through the system. Can be null. |
method | String | The HTTP method the relay must use for the backend request (e.g., GET, POST). |
targetRef | String | A reference to the backend target, which the relay resolves locally. |
requestHeaders | Map<String, Object> | A map of HTTP headers to be forwarded with the request. Serialized to JSONB. Can be null. |
requestBody | String | The request body to be forwarded. Can be null. |
expiresAt | OffsetDateTime | The timestamp after which the job is considered expired if not completed. |
Returns: UUID - The unique identifier generated for the new job.
claimAtomically claims one or more pending jobs for a specific relay, transitioning their status to claimed. Jobs are claimed in the order they were created (oldest first).
| Parameter | Type | Description |
|---|---|---|
relayId | String | The ID of the relay attempting to claim jobs. |
max | int | The maximum number of jobs to claim. The effective minimum is 1. |
Returns: List<DispatchJob> - A list of the jobs that were successfully claimed. The list is empty if no pending jobs were available for the specified relay.
completeRecords the final result of a claimed job, transitioning its status to succeeded or failed.
IMPORTANT
This operation is restricted to the specific relay_id that originally claimed the job. An attempt by another relay to complete the job will fail. The operation is also idempotent; subsequent calls for an already-completed job will have no effect.
| Parameter | Type | Description |
|---|---|---|
id | UUID | The ID of the job to complete. |
relayId | String | The ID of the relay posting the result. Must match the relay the job was dispatched to. |
status | String | The final status of the job. Should be succeeded or failed. |
responseStatus | Integer | The HTTP status code from the backend response. Can be null. |
responseHeaders | Map<String, Object> | A map of HTTP headers from the backend response. Serialized to JSONB. Can be null. |
responseBody | String | The response body from the backend. Can be null. |
error | String | A description of the error if the job failed. Can be null. |
Returns: int - The number of rows updated. Returns 1 on a successful completion or 0 if the job was not found, was not in the claimed state, or did not belong to the calling relay.
pollStatusRetrieves the current status and result of a job. This is used by the gateway-side coordinator to monitor the job's progress.
| Parameter | Type | Description |
|---|---|---|
id | UUID | The ID of the job to poll. |
Returns: Optional<DispatchResult> - An Optional containing the job's result if found, or an empty Optional if no job with the given ID exists for the current tenant.
markExpiredTransitions a non-terminal (pending or claimed) job to the expired state. This is a best-effort cleanup action typically called by the gateway-side coordinator when a job's expiresAt deadline has passed.
| Parameter | Type | Description |
|---|---|---|
id | UUID | The ID of the job to mark as expired. |
Returns: int - The number of rows updated. Returns 1 if the job was successfully marked as expired, or 0 if the job had already reached a terminal state.
The repository uses two primary record types to represent job data.
DispatchJobRepresents a job that has been claimed by a relay and is ready for execution. This is the data returned by the claim method.
| Field | Type | Description |
|---|---|---|
id | UUID | The unique job identifier. |
relayId | String | The ID of the relay this job is assigned to. |
correlationId | String | The request correlation ID. |
method | String | The HTTP method for the backend request. |
targetRef | String | The backend target reference. |
requestHeaders | Map<String, Object> | The HTTP headers for the backend request. |
requestBody | String | The body for the backend request. |
expiresAt | OffsetDateTime | The expiration timestamp for the job. |
DispatchResultRepresents a snapshot of a job's status and final result. This is the data returned by the pollStatus method.
| Field | Type | Description |
|---|---|---|
id | UUID | The unique job identifier. |
status | String | The current status of the job (e.g., pending, claimed, succeeded). |
responseStatus | Integer | The HTTP status code from the backend response. null if not completed. |
responseHeaders | Map<String, Object> | The HTTP headers from the backend response. null if not completed. |
responseBody | String | The body of the backend response. null if not completed. |
error | String | An error message if the job failed. null otherwise. |
The isTerminal() method on a DispatchResult returns true if the status is succeeded, failed, or expired.
All repository methods require a tenant context to be bound to the database connection. If the context is missing, operations will fail. The source indicates this may result in an S2R-TENANCY-UNBOUND error. Ensure that the system correctly resolves and binds the tenant ID before calling any repository methods.
complete() or markExpired() Affects 0 RowsIf a call to complete() or markExpired() returns 0, it indicates that no database rows were updated. This is not necessarily an error and can occur under the following conditions:
succeeded, failed, or expired).complete(), the job's status was not claimed, or the relayId provided did not match the one that originally claimed the job.The requestHeaders and responseHeaders maps are serialized to and from JSON. If the map contains data that cannot be serialized, enqueue or complete will throw an IllegalArgumentException. Similarly, if the JSON data in the database is malformed, claim or pollStatus will throw an IllegalArgumentException during deserialization.