Skip to content

MailboxRPC 1/7: durability foundation - #48

Merged
Roasbeef merged 22 commits into
mainfrom
durability
Feb 10, 2026
Merged

MailboxRPC 1/7: durability foundation#48
Roasbeef merged 22 commits into
mainfrom
durability

Conversation

@Roasbeef

Copy link
Copy Markdown
Member

In this PR, we implement a fault-tolerant durable actor layer. This is intended to slot right into the existing actors that we already use today. It handles a lot of book keeping under the hood to make sure that things are done safe.

This is quite a large PR (even granted it's mostly tests, the auto gen sql code, and the docs), so I'll split it up, but I wanted to put it all up at once so it was easy to navigate, etc.

TLV Codec Layer

In order to use this new framework, all messages that need to be written persistently will implement a new TLV-based interface and register that with the codec. The codex is used to encode/decode messages to/from the persistent outbox that all actors now maintain.

This may also cause a shift in the way we make the struct hierarchy of messages. Right now each package has their own message type/interface. We may end up actually merging this into a single package, then use type aliases in each actor package so it's easy to find which messages a given actor actually cares out.

See this getting started guide re the updates needed to existing actors to start using this new framework, and items that devs need to keep in mind.

DurableActor Transactional Message Processing

A DurableActor's message processing will take place in a single database transaction, so we'll want to think carefully about any network access to blocking that takes place. Expansive changes in existing actors (that need to be durable) shouldn't be required.

The underlying actor framework does a lot of the heavy lifting here. It handles reading from the persistent mailbox, ACK'ing messages after processing has finished, suppressing duplicate messages, handling retransmission, etc, etc.

The main call site that'll change slightly is when an actor is processing the outbox messages from the FSM. As mentioned above, as everything is run in a single db transaction, if the actor crashes at any point, post restart it'll consume the last un ACK'd message, resuming as normal.

There's also a special Restart message sent by the runtime when an actor is restarted. The actor should process this, then do any sort of init activities such as requesting for confirmations, etc.

Actors can also optionally checkpoint some state. We may want to use this to checkpoint actual attributes in the actor, leaving the FSM state to dedicated DB methods (as the check pointed state is just TLV atm).

Along the way, Tell now returns an error, as it's possible we fail to write to the db for w/e reason.

OutboxPublisher

The OutboxPublisher is a key component of the entire architecture. There's a single global instance of this atm, but it's designed s.t we can have many instances of them. It handles taking a message from the outbox of an actor (in reality there's actually a global outbox) and writing it to the mailbox/inbox of the target actor. It'll continue to delver on start up until a message is fully ACK'd by the destination actor. This is also used to implement durable asks as mentioned below.

DurableAsks

The final gap is to make sure that asks are durable. Today, the outbox abstraction is basically a Tell. It's set and forget, and doesn't allow for responses w/o a custom flow.

Durable acks fill this gap. When you need to reliably hear back about the response to a message (eg: asking an actor actor for a signature of something, or transaction publish, etc) you should reach for this. It's like a normal ask, but you also provide the response ID of the actor. A durable ask doesn't return a future like a normal ask. Instead, the response comes back across as a normal message for the actor.

This reuses the OutboxPublisher layer to handle delivery until the response is ack'd, etc, etc.

actortest package

A new internal/actortest package has been added to test all the relevant scenarios and invariants with a real database. It can also double as some example code re using all the functionality mentioned above.

Fixes https://github.com/lightninglabs/darepo/issues/4

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @Roasbeef, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust, fault-tolerant actor system designed to prevent message loss and ensure consistent state across crashes. It achieves this by integrating durable messaging patterns such as persistent mailboxes, transactional outboxes for inter-actor communication, and FSM state checkpointing. The new system provides developers with a reliable foundation for building resilient distributed applications, abstracting away complex recovery logic and guaranteeing message processing semantics.

Highlights

  • DurableActor Abstraction: Introduces a new DurableActor abstraction layer that provides fault-tolerant actor implementations. This layer handles message persistence, lease-based delivery, deduplication, panic recovery, and FSM state checkpointing to ensure exactly-once processing semantics even in the event of crashes.
  • TLV Codec Layer: All messages intended for durable actors must now implement a new TLVMessage interface. A MessageCodec is introduced to handle the serialization and deserialization of these TLV-encoded messages, enabling persistent storage and backward compatibility.
  • Transactional Message Processing: The DurableActor's message processing is wrapped in a single database transaction (if the underlying DeliveryStore supports it). This ensures atomic updates to FSM state, outbox messages, and message acknowledgments, preventing data inconsistencies on crash.
  • OutboxPublisher for CDC: A new OutboxPublisher component is added, implementing a Change Data Capture (CDC) pattern. It monitors a transactional outbox table, delivering messages to target actors asynchronously after the originating transaction commits, ensuring reliable inter-actor communication.
  • DurableAsks for Crash-Safe Request-Response: The DurableAsk mechanism provides crash-safe request-response semantics. Unlike traditional 'Ask' which uses in-memory promises, DurableAsk persists callback metadata and routes responses via the outbox/mailbox system, ensuring responses are delivered even if the caller crashes and restarts.
  • Comprehensive Testing Package: A new internal/actortest package is introduced, providing end-to-end integration tests for the entire durable actor system. This package includes a CounterActor example that demonstrates all new features and verifies invariants like at-least-once delivery and exactly-once processing.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an impressive and comprehensive pull request that introduces a well-designed durable actor framework. The separation of concerns into layers like the DeliveryStore, DurableMailbox, and OutboxPublisher is excellent. The use of a transactional outbox (CDC pattern) and lease-based delivery provides strong guarantees for fault tolerance and exactly-once processing semantics. The addition of DurableAsk for crash-safe request-response is a significant feature. The code is accompanied by exceptionally thorough tests, including property-based tests, and high-quality documentation that clearly explains the architecture and developer usage. I have found one critical issue regarding a TLV type collision that must be addressed, and one medium-severity point for clarification. Overall, this is a very strong contribution.

Comment thread baselib/actor/restart.go Outdated
Comment thread baselib/actor/delivery.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread baselib/actor/durable_actor.go Outdated
@@ -0,0 +1,29 @@
-- Rollback durable mailbox migration.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't these migrations live in the actor package itself? golang-migrate supports reading from multiple filesystems and versions via the migrations table config.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, just was following the pattern of putting all the db stuff in a single location. I kinda like them to live here as the actor package just declares interfaces that we create/satisfy elsewhere, then pass them in.


-- callback_actor_id is set for DurableAsk messages to route the response.
-- The response will be delivered to this actor's mailbox via outbox.
-- NULL for regular Ask/Tell messages.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULL for regular Ask/Tell messages. wouldn't these not be persisted though?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tell messages are still persisted in the outbox to be delivered. This is for cases where the actor that 's processing needs to write another message into the mailbox for a response.


-- promise_id is set for Ask messages to track the response.
-- NULL for Tell (fire-and-forget) messages.
promise_id TEXT,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we really need to persist ask messages or if they could be dropped in general. As a durable tell can just retrigger a new tell in the receiving actor and the tell would carry the callers id?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question, we may not actually need them at all depending on exactly what the actor needs. I added it as I realized there was a gap where on restart, the destination would process the ask, but then the response would never make its way back to the sender.

I can remove it if we don't think we need it at all.

The case where it's helpful is if after writing a message to an outbox, the actor reliably expects a response to come back across. You can emulate this with a double tell basically, but you'd need to include the "sender address" in the tell, then for the sender to know that this is a response to a request it sent, and not just any other event.

Comment thread db/actor_delivery_store.go
@@ -0,0 +1,51 @@
package actor

// TxEnvironment is an interface that environments can implement to support

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really is a cool feature!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is a big part of the "magic". It's what lets the actors be written basically as normal, ensuring that all their db queries/modifcations run in the same context as the outbox interaction.

Comment thread baselib/actor/ask_response.go
@bhandras bhandras changed the title multi: add new DurableActor abstraction layer to implement fault tolerant actors via a CDC/OLEP layer MailboxRPC 1/8: durability foundation Jan 16, 2026
@bhandras bhandras changed the title MailboxRPC 1/8: durability foundation MailboxRPC 1/7: durability foundation Jan 17, 2026
@bhandras
bhandras force-pushed the durability branch 2 times, most recently from ba42f3c to 3a5dbec Compare January 17, 2026 14:31
@bhandras

Copy link
Copy Markdown
Member

@codex review pls

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@bhandras bhandras left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a few comments i consider higher importance. It's a huge PR so still going through it in detail which means i might add more over time as i process things but hope this is already enough as a start.

Comment thread baselib/actor/durable_actor.go Outdated
Comment thread baselib/actor/durable_mailbox.go
Comment thread db/sqlc/queries/mailbox.sql Outdated
Comment thread db/sqlc/queries/mailbox.sql Outdated
Comment thread db/sqlc/migrations/000004_durable_mailbox.up.sql Outdated
Comment thread db/sqlc/migrations/000003_durable_mailbox.up.sql Outdated
Comment thread baselib/actor/outbox_publisher.go
Comment thread db/sqlc/migrations/000004_durable_mailbox.up.sql Outdated
Comment thread db/sqlc/migrations/000004_durable_mailbox.up.sql
Comment thread db/sqlc/migrations/000004_durable_mailbox.up.sql
Comment thread db/sqlc/migrations/000004_durable_mailbox.up.sql
Comment thread baselib/actor/delivery.go
Comment thread baselib/actor/delivery.go
Comment thread baselib/actor/delivery.go
Comment thread baselib/actor/delivery.go
Comment thread baselib/actor/durable_mailbox.go
Roasbeef and others added 4 commits February 5, 2026 18:03
This commit introduces a comprehensive end-to-end test suite that
validates the durable actor system using a simple counter actor as the
test subject. The tests exercise the full stack from message delivery
through persistence and back.

The counter actor maintains an integer value and responds to Inc, Dec,
Get, and Reset messages. Despite its simplicity, this actor provides
sufficient complexity to test all durability features: state that must
survive restarts, messages that must not be processed twice, and
request-response patterns that span crashes.

The test suite covers several categories of scenarios:

Basic operation tests validate that the counter increments, decrements,
and reports values correctly under normal conditions. These establish
baseline functionality before testing failure modes.

Durability tests simulate crashes by stopping and restarting actors,
verifying that persisted messages are redelivered and that FSM state
restores from checkpoints. This validates the restart flow including
RestartMessage processing and deduplication.

Exactly-once tests send duplicate messages and verify the counter only
reflects one increment. This validates the deduplication layer using
the processed_messages table.

DurableAsk tests verify that request-response works across restarts.
A caller sends a DurableAsk, the target crashes before responding, and
after restart the response still arrives at the caller's mailbox.

Property-based tests using rapid generate random message sequences and
verify invariants hold regardless of ordering. This provides confidence
that edge cases in timing and sequencing are handled correctly.

The messages implement TLVMessage with compact encoding for efficient
storage. The test harness provides helper methods for setting up actors
with in-memory SQLite databases, simplifying test isolation.
This commit adds comprehensive documentation for the durable actor
system across three files targeting different audiences and use cases.

The actor_delivery_store.md in the db directory documents the database
schema with an entity-relationship diagram showing how the six tables
relate to each other. It explains each table's purpose, column semantics,
and the indexes that support efficient queries. This serves as a
reference for database administrators and developers debugging
persistence issues.

The durable_actor_architecture.md provides a deep dive into the system's
design decisions and patterns. It covers the CDC pattern with
transactional outbox, explaining why atomicity between FSM state and
outgoing messages matters. The lease-based delivery section explains
the tradeoffs in lease duration and heartbeat intervals. The crash
recovery section walks through the restart flow with sequence diagrams.
The DurableAsk pattern gets special attention, explaining how
CallbackActorID and CorrelationID work together to route responses.

The durable_actor_quickstart.md targets developers who need to make
their actors durable. It opens with what the runtime handles
automatically versus what developers must implement. The TLVMessage
implementation section provides copy-paste examples for encoding and
decoding. The codec registration section emphasizes that AskResponse
must be registered for DurableAsk callers. The RestartMessage section
explains the priority-based recovery flow. Throughout, the document
uses mermaid diagrams to visualize message flow and component
relationships.
This is a single fixup commit to make make lint pass after rebasing\nPR48's durability branch onto current main.\n\nChanges include minor formatting, line wrapping, and error handling\nadjustments surfaced by the linter.
@Roasbeef

Roasbeef commented Feb 6, 2026

Copy link
Copy Markdown
Member Author

Durability Branch Deep Review (Codex 5.3)

Reviewer: Codex 5.3 (CodexDurability agent)
Date: 2026-02-06
Branch: durability (abdad2a)
Baseline: origin/main (b1b61e1)
Scope: durable mailbox, durable actor runtime, transactional outbox, DB store, message delivery semantics, integration wiring

Method and Validation

Reviewed code paths and schema from first principles against crash-stop, retry, and concurrent-delivery failure models. Focused on:

  • OS/filesystem durability behavior (SQLite WAL/fsync configuration)
  • Database transaction boundaries and atomicity
  • At-least-once vs exactly-once semantics
  • Lease safety and stale-ack prevention
  • Cross-actor message propagation via CDC outbox
  • Request/response durability (Ask and DurableAsk)

Validation commands run:

  • go test ./... -count=1 (pass)
  • go test ./baselib/actor -count=1 (pass)
  • go test ./db -count=1 (pass)
  • go test ./internal/actortest -count=1 (pass)
  • go test -race ./baselib/actor -run 'TestDurableActor' -count=1 (pass)

Executive Summary

This branch is a substantial and thoughtful durability foundation. The lease model, persistence interfaces, tests, and docs are all strong. However, there are several correctness gaps where documented guarantees (especially "exactly-once processing" and "crash-safe DurableAsk") are not currently met under realistic failure modes.

Most important: current outbox delivery and Ask/DurableAsk completion order can acknowledge success before durable commit/finalization, and can duplicate cross-actor effects when retries occur.

Findings (Ordered by Severity)

1) Critical: DurableAsk can acknowledge success even when response outbox write failed (non-tx path)

Property violated: crash-safe request/response durability.

In processWithoutTransaction, if writeAskResponseToOutbox fails, code logs a warning and still calls Ack and MarkProcessed. That permanently drops the response while treating the request as complete.

Recommended fix: For DurableAsk, treat outbox-write failure as processing failure. Nack for retry, do not ack/mark-processed when the response cannot be durably enqueued.

2) Critical: Outbox retry path can duplicate downstream effects because message identity is not preserved end-to-end

Property violated: exactly-once effects across actor boundaries.

OutboxPublisher delivers by calling target Tell(decoded). Target mailbox creates a fresh message ID for each delivery attempt. If publisher delivers successfully but fails before CompleteOutbox, retry will re-deliver same logical message with a new mailbox ID. Dedup keyed by mailbox ID cannot collapse this duplicate.

Recommended fix: Preserve and propagate stable delivery identity from outbox to inbox. Use domain_key/version as enforced idempotency key on receiver side.

3) High: Ask promise can complete before durable ack/transaction commit

Property violated: linearizable request completion / truthful success signaling.

Delivery.Ack completes in-memory promise before mailbox ack deletion succeeds. In tx path, commit happens after this. If commit fails, caller may already have a success while durable state rolled back.

Recommended fix: Defer promise completion until after durable ack/commit success.

4) High: Potential data race on Delivery state between heartbeat and ack/nack paths

Property violated: thread safety under concurrent lease extension and completion.

heartbeat goroutine calls delivery.Extend while main processing goroutine may call Ack/Nack. Delivery.acked and LeaseUntil are mutated/read without synchronization.

Recommended fix: Guard Delivery mutable fields with mutex/atomic discipline.

5) High: Poison decode/type-mismatch messages can become unreachable, not dead-lettered

Property violated: bounded retries with terminal observability.

Decode/type-cast errors call NackMessage directly. Once attempts reaches max_attempts, lease query stops selecting the row, but no dead-letter move occurs.

Recommended fix: On decode/type failure, explicitly dead-letter when attempts exhausted.

6) Medium: Outbox claim protocol has no in-flight lease/status transition

Property violated: single-claimer semantics and ordering under multiple publishers.

ClaimOutboxBatch increments delivery_attempts but keeps status='pending' until completion. With multiple publisher workers/processes, same pending rows can be re-claimed before completion.

Recommended fix: Introduce explicit in-flight state with claim lease token + expiry.

7) Medium: Time fields are truncated to int32 unix seconds (2038 and precision risk)

Most persisted times are cast to int32. This introduces Y2038 overflow risk and sub-second precision loss.

Recommended fix: Use int64 for persisted unix timestamps end-to-end.

8) Medium: Ask promise registry leak on enqueue failure

Ask promise is inserted into promiseRegistry before durable enqueue. If enqueue fails, method returns false without removing registry entry.

Recommended fix: Register promise only after successful enqueue, or cleanup registry on error.

9) Medium: Send failure modes collapse into ErrMailboxFull at caller edge

Encode/store failures return false from mailbox, then actor refs map that to ErrMailboxFull if context/termination checks do not match.

Recommended fix: Plumb concrete send error from mailbox to refs, preserving cause.

Strengths

  • Strong lease-token stale-ack guard model
  • Good SQLite durability posture (WAL, synchronous=full, fullfsync=true)
  • Extensive tests across unit/e2e/property dimensions

Bottom Line

The branch is very strong groundwork and close to production-grade durability, but it is not yet safe to claim exactly-once end-to-end behavior under crash, retry, and partial-failure conditions. The fixes above are tractable and mostly localized to delivery finalization ordering and outbox/inbox identity handling.

…tion

In this commit we address two closely related safety issues in the
Delivery type identified by the Codex 5.3 deep review:

The heartbeat goroutine calls Extend and reads LeaseUntil concurrently
with the main processing goroutine calling Ack or Nack. A sync.Mutex
now guards all accesses to the mutable acked and LeaseUntil fields,
preventing data races detected by -race.

Promise completion (d.Promise.Complete) was previously called inside
Ack before the durable ack succeeded. If AckMessage failed or the
transaction rolled back, callers would observe a "successful" result
for an operation that was never durably committed. The promise is now
completed only after AckMessage succeeds. A new deferPromise flag
allows the transaction path in DurableActor to suppress in-Ack
completion entirely, handling it after ExecTx returns.
Two correctness issues from the Codex review are fixed here:

DurableAsk messages that fail their outbox write were previously logged
and then acked, permanently dropping the response while the sender
believes the request was processed. Both processWithoutTransaction and
handleResult now nack for retry when the outbox write fails, giving
the actor a chance to succeed on redelivery.

The transaction path (processInTransaction) now sets deferPromise on
the delivery before entering ExecTx, suppressing the in-Ack promise
completion. The behavior result is captured outside the closure and
the promise is completed only after ExecTx returns successfully. If
the transaction rolls back, the delivery is nacked and no promise
completion leaks to the caller.

Additionally, DurableAsk messages in the non-tx path now defer
MarkProcessed until after the outbox write succeeds in handleResult,
preventing a scenario where the message is marked "done" but the
response outbox row was never written.
…edup

Three mailbox safety improvements from the Codex 5.3 review:

Poison messages (decode or type-cast failures) were previously nacked
indefinitely, stranding them in the mailbox. A new handlePoisonMessage
method checks attempts against max_attempts and dead-letters exhausted
messages instead of retrying forever. Messages not yet at max attempts
are nacked with a 60s backoff for transient issues.

The promise registry leaked entries when EnqueueMessage failed. After
a failed enqueue, Send now removes the registry entry so repeated
failures don't accumulate unbounded stale promises.

Outbox delivery retries could create duplicate inbox messages because
the OutboxPublisher's Tell generated a fresh UUID on each attempt. The
publisher now injects the outbox row ID into the context via
WithOutboxID, and DurableMailbox.Send uses it as the inbox message ID
when present. The EnqueueMailboxMessage SQL query gains ON CONFLICT
(id) DO NOTHING so duplicate inserts are silent no-ops, matching the
pattern already used by InsertAskResult.
Generated by make sqlc after adding ON CONFLICT (id) DO NOTHING to
EnqueueMailboxMessage.
Fourteen new tests covering the five fixes from the Codex 5.3 deep
review, all passing with -race:

DurableAsk outbox safety (Fix #1):
  - TestDurableAskNacksOnOutboxWriteFailure

Promise completion ordering (Fix #3):
  - TestPromiseCompletionDeferredUntilAfterAck
  - TestPromiseNotCompletedOnAckFailure
  - TestPromiseCompletionDeferredInTxPath
  - TestPromiseNotCompletedOnTxFailure

Delivery mutex (Fix #4):
  - TestDeliveryConcurrentExtendAndAck
  - TestDeliveryConcurrentExtendAndNack

Poison message handling (Fix #5):
  - TestDurableMailboxPoisonMessageDeadLetter
  - TestDurableMailboxPoisonMessageNackBeforeMax

Promise registry cleanup (Fix #8):
  - TestDurableMailboxPromiseRegistryCleanupOnEnqueueFailure

Outbox ID deduplication (Fix #2):
  - TestDurableMailboxSendUsesOutboxIDFromContext
  - TestDurableMailboxSendDuplicateOutboxIDIsIdempotent
  - TestDurableMailboxSendWithoutOutboxIDGeneratesFreshID
  - TestOutboxPublisherPropagatesOutboxID

The mock delivery store is updated with ON CONFLICT DO NOTHING
semantics for EnqueueMessage (matching the real SQL) and per-operation
error injection fields for outbox and enqueue failures.
@Roasbeef

Roasbeef commented Feb 6, 2026

Copy link
Copy Markdown
Member Author

Durability Review Fixes Pushed

Five commits addressing findings from the Codex 5.3 deep review:

baselib/actor: harden Delivery with mutex and deferred promise completion

baselib/actor: nack DurableAsk on outbox failure and defer tx promise

baselib/actor: harden mailbox with poison dead-lettering and outbox dedup

db/sqlc: regenerate queries after mailbox ON CONFLICT change

  • Generated code from make sqlc

baselib/actor: add tests for durability review fixes

  • 14 new tests covering all 5 fixes, all passing with -race (7.3s total)
  • Mock store updated with ON CONFLICT semantics and per-operation error injection

Remaining items (lower severity, separate PRs):

Two issues found by Codex round-2 review:

The txDelivery created inside handleResultInTx did not copy the
deferPromise flag from the original delivery. This meant txDelivery.Ack
completed the in-memory promise inside the ExecTx callback, before the
transaction committed. The flag is now propagated so promise completion
is correctly deferred to processInTransaction's post-commit block.

DurableAsk messages in the tx path wrote the response to the outbox
but then fell through to the Tell retry policy. If the behavior
returned an error, the message would be nacked and reprocessed,
producing duplicate outbox responses for the same correlation ID.
DurableAsk is now handled as a self-contained block in handleResultInTx
that writes the response, marks processed, and acks in one path with
no fallthrough to the Tell retry logic.

Two regression tests added:
  - TestTxPathDeferPromisePropagatedToTxDelivery
  - TestTxDurableAskDoesNotRetryAfterOutboxWrite
The errcheck linter flagged three unchecked Tell return values
introduced during the durability branch rebase. Each call site now
logs a warning on failure rather than silently discarding the error.
Comment thread baselib/actor/delivery.go Outdated
Comment thread db/sqlc/queries/mailbox.sql
Comment thread db/sqlc/models.go Outdated
Comment thread baselib/actor/durable_actor.go

@bhandras bhandras left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM pending last set of codex comments.

@Roasbeef

Copy link
Copy Markdown
Member Author

Re: Mailbox.Send returning bool instead of error

Agreed this is worth fixing — DurableMailbox.Send currently returns bool which loses the underlying error context (encode failure, DB error, context cancellation all collapse to false). This makes debugging delivery failures harder and prevents callers from distinguishing transient vs permanent failures.

Tracking this as a follow-up since it touches the Mailbox interface contract and all callers (Tell, Ask, DurableAsk, outbox publisher, tests). See the linked issue for the full scope.

Move AckMessage (lease validation) before SaveAskResult in Delivery.Ack
to prevent stale lease holders from persisting results. Previously a
stale holder could race with a new lease holder: the stale Ack would
succeed because SaveAskResult used ON CONFLICT DO NOTHING, silently
poisoning the result. Now the lease check gates the write.
Address two PR review comments:

1. Change all timestamp columns from INTEGER to BIGINT in the durable
   mailbox migration so sqlc generates int64 instead of int32. Update
   ActorDeliveryStore and TxActorDeliveryStore to remove unnecessary
   int32 casts (Unix() already returns int64).

2. Add claim_token and claimed_until columns to outbox_messages for
   concurrent publisher safety. ClaimOutboxBatch now sets a per-batch
   UUIDv7 claim token with configurable lease duration (default 30s).
   CompleteOutbox and FailOutbox validate the claim token to prevent
   stale publishers from mutating messages they no longer own.

   Update DeliveryStore interface with OutboxClaimParams struct,
   OutboxPublisher with ClaimDuration config, and all tests.
@Roasbeef
Roasbeef merged commit 53fb6f5 into main Feb 10, 2026
16 checks passed
@bhandras
bhandras deleted the durability branch February 20, 2026 16:20
ellemouton added a commit that referenced this pull request Mar 17, 2026
[2/?] batch: add various VTXO tree helpers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants