Skip to content

Support durable, indexed event history for reliable replay by downstream consumers #1020

Description

@f3r10

I was looking into ldk-node's EventQueue (src/event.rs) and noticed that it's a strict FIFO: next_event_async() reads the front event, and event_handled() pops and discards it, only persisting whatever's left in the queue. Once an event is marked handled, there's no record of it anywhere — no stable ID, no way to look it up again.

That seems fine for the built-in Node API, where one in-process handler processes events sequentially as they arrive. But it looks like it becomes a problem for anything built on top of ldk-node that wants to forward events to its own external subscribers reliably.

The concrete case I ran into: ldk-server exposes a SubscribeEvents gRPC API that streams Node events to external clients (accounting systems, hodl-invoice handlers, etc). Right now it drains next_event_async(), immediately calls event_handled(), and fans the event out over a bounded in-memory broadcast channel.
Any subscriber that disconnects, restarts, or falls behind loses those events for good, since ldk-node has already thrown away the only copy. There's more detail on the downstream problem in lightningdevkit/ldk-server#245.

So it seems like this would need to be solved here first before ldk-server (or anything else) could build reliable delivery on top of it. Here's roughly what I was thinking, but I'm not sure it's the right shape:

  • Assign each Event a globally monotonic, persistent u64 index when it's added to the queue in add_event, stable across restarts.
  • Instead of discarding events in event_handled(), keep them around for some bounded/configurable retention window (by count and/or age) so they're still queryable after being processed.
  • Add a way to fetch events after a given index, e.g. EventQueue::events_since(index: u64) -> Vec<(u64, Event)>, so a downstream consumer that persists "last processed index" can catch up on a gap and then go back to live delivery via next_event_async().
  • Figure out what happens when someone asks for an index older than the retention window — probably some "cursor too old" error so they know they need to resync another way.

Still not sure about retention defaults/configurability, or whether this should live on EventQueue directly vs. some separate opt-in store so it doesn't add overhead for consumers that don't need replay.

Would love to hear if this direction makes sense, or if I'm overcomplicating something that's already handled elsewhere. If it seems reasonable I'd like to try putting together a PR for it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions