Limit how long retry results are kept
By default Flower keeps every retry result forever. Retention lets you drop old ones. It's opt-in and uses numbered epochs, not clock time:
- Initialize retention once.
- Issue request IDs scoped to the current epoch.
- Advance the minimum epoch. Older request IDs are then rejected for good.
- Collect the retired results in batches.
| API | Contract |
|---|---|
RetryIdentity | {database,incarnation,currentEpoch,minEpoch}. IDs are 32 lowercase hex characters; epochs are nonnegative integers. |
RetentionState | RetryIdentity plus receiptBytes, receiptCount, maxReceiptBytes (number|null), gcCursor (string|null), gcComplete, sessionBytes, sessionCount, gcReceiptsComplete and gcSessionCursor. |
RetentionAction | One of: initialize {database,incarnation,max_receipt_bytes}; advance {incarnation,current_epoch,min_epoch}; collect {incarnation,limit}; set_budget {incarnation,max_receipt_bytes}; reincarnate {incarnation,new_incarnation,fence_attestation}, each with operation. Epochs never go down. reincarnate is for disaster restore only, after you have fenced the old cluster yourself. |
admin.retentionStatus(options?) | Current RetentionState, or null. Admin token. |
admin.controlRetention(expectedRevision, action, options?) | Apply an action if the revision matches. Returns {state, collected}. After a lost response, check status before sending anything else. |
client.refreshRetryIdentity(options?) | Reload the current epoch for new request IDs. Doesn't update old IDs. Fails if retention isn't initialized. |
client.newRequestId(intent?, options?) | Make an epoch-scoped request ID (random intent by default). Save it before sending and reuse it on retry. The same intent in a new epoch is a new request. |
RetrySession | {database,incarnation,id,epoch,acknowledgedThrough,closed}, owned by the caller's principal. Needs access checks: auth.sessions decides who may use sessions, and the check sees $flower.session.open, .status, .ack and .close. |
SessionOptions | RequestOptions plus limit? (default 256 records per call) and abandon? (default false, ACK only). |
client.openRetrySession(id?, options?) | Open a session. Pick and save your own 32-character hex ID first so you can recover a lost response. Reopening an active session is safe; closed ones can't be reopened. |
client.sessionRequestId(session, sequence) | Request ID for a sequence number above the ACK mark. You track sequence numbers; the SDK never advances or ACKs for you. |
client.retrySessionStatus({id,incarnation}, options?) | Current session state, for the owner only. Use it after an uncertain ACK. |
client.acknowledgeRetrySession(session, through, options?) | Drop results up to through. ACK only after you've saved them. Every result up to that point must exist, unless abandon: true, which also cancels unknown outcomes for good. Retries at or below the mark return ALREADY_ACKNOWLEDGED and never run again. |
client.closeRetrySession(session, options?) | Close the session for good. In-flight results may be lost. The ID can't be reused. |
FlowerClientOptions.boundedRetries | Default false. When true, generated request IDs use the current epoch. Your own IDs pass through unchanged. The SDK doesn't refresh the epoch for you. |
Errors to handle:
RETRY_WINDOW_EXPIRED: the result is gone. The mutation may still have committed.HISTORY_MISMATCH: the ID belongs to a different database history.RECEIPT_BUDGET_EXCEEDED: new work is rejected; kept results are never evicted.
Once initialized, unscoped request IDs are rejected. A disaster restore needs a new incarnation; restoring an old backup as-is is unsafe. Backups may still contain deleted results. Details: retention protocol.
Cleaning up cross-group transactions
Records of finished cross-group transactions are removed only when you close and collect them. Prepared transactions without a decision are never released.
| API | Contract |
|---|---|
TransactionClosureTarget | {group,partition:string|null,epoch,addresses?:string[]}. Placement info; not used for routing. |
TransactionClosureState | {history:string|null,nextSequence,closedThrough,pending,blockedReason:string|null,deletedRecords}. pending is null or {through,participants,acknowledged}. blockedReason explains a stall, such as an unfinished transaction or an unreachable peer. |
TransactionClosureAction | {operation:"close",through?:number,maxBytes?:number} or {operation:"collect",maxBytes?:number}. through defaults to all finished transactions; maxBytes defaults to the node's transaction budget. |
admin.transactionClosureStatus(options?) | Current closure state. Use admin.partition(name) for a named database. |
admin.controlTransactionClosure(action, options?) | close tells participants to reject late messages, then advances the floor. collect deletes old records; run it on coordinators and participants. Safe to repeat. Check pending and blockedReason, not just HTTP success. |
An aborted transaction can close only once its request ID can no longer be retried, so old unscoped aborts block closure until retention is initialized.