Documentation
¶
Overview ¶
Package redis provides a Redis- or Valkey-backed gateway.Outbox, so a connection's unacknowledged tail survives a node failure: a client that reconnects to a different process still finds the messages the original process had sent but not yet had acknowledged, and they are redelivered. It is a separate package specifically so that importing the root gateway package never pulls in github.com/redis/go-redis/v9 for applications that use gateway.MemoryOutbox, or no Outbox at all.
Data model ¶
Each connection id is one Redis hash. Its message fields are keyed by a message id this Outbox mints (see Outbox.Append) and hold the value "<seq>:<payload>", where seq is the decimal sequence and payload is the raw bytes after the first colon (a colon never appears in the decimal prefix, so the split is unambiguous and payload stays binary-safe). Two reserved fields never collide with a minted message id (see seqField): seqField holds the per-connection sequence counter, and ackGenField holds the Ack generation fencing floor (see gateway.Outbox.Ack). The hash shape is chosen over a sorted set because gateway.Outbox.Ack removes by message id, which a hash does in one HDEL; a sorted set keyed by Seq would force a scan to find the member carrying a given id. Ordering by Seq, which Unacked must return, is recovered by sorting the (small, in-flight) tail on the client after HGETALL rather than paid for on every write.
The sequence is assigned server-side by HINCRBY on seqField inside the append script, not by the calling process, so it stays monotonic across every process and node appending to the same connection through this Redis and across a restart of any of them. That is what makes the persisted Seq trustworthy for a client that dedupes on it: a per-process counter would restart at 1 on reboot and collide with a still-stored message. The counter lives in the same hash as the messages, so it is reclaimed together with them by DropConn's DEL or the TTL, and every operation still touches exactly one key, so a Redis Cluster routes each to one slot with no hash tags.
The Ack generation floor recorded in ackGenField is checked and raised atomically with the message removal it gates, inside ackScript, so an Ack whose generation trails a previously accepted one for the same connection is rejected rather than applied - see gateway.ErrStaleOwner. ackScript is a true no-op for a connection hash that does not exist, so a stream of acks for connection ids this Outbox never appended to cannot create keys.
Only HINCRBY/HSET/HGET/HGETALL/HDEL/EXISTS/DEL and PEXPIRE are used, all present and identical on Redis 7.2 and Valkey 8.
Expiry ¶
With WithTTL, the per-connection hash carries a Redis TTL re-armed on every Append, so a connection that never reconnects to drain or DropConn its tail is reclaimed by the server rather than leaking. Without it (the default) a hash lives until Ack empties it or DropConn deletes it. The TTL bounds at-least-once storage, not delivery: a message whose key expires before the client reconnects is simply not redelivered, the same outcome as if the client had never come back.
Index ¶
- Constants
- type Option
- type Outbox
- func (o *Outbox) Ack(ctx context.Context, connID, msgID string, generation uint64) error
- func (o *Outbox) AdvanceGeneration(ctx context.Context, connID string, generation uint64) error
- func (o *Outbox) Append(ctx context.Context, connID string, payload []byte) (string, uint64, error)
- func (o *Outbox) DropConn(ctx context.Context, connID string) error
- func (o *Outbox) Unacked(ctx context.Context, connID string) ([]gateway.PersistedMessage, error)
Constants ¶
const DefaultKeyPrefix = "gateway:outbox:"
DefaultKeyPrefix namespaces outbox keys away from every other kind of key a gateway deployment may keep in the same Redis database (notably the presence and coordinator backends).
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Option ¶
type Option func(*Outbox)
Option configures an Outbox created with New.
func WithKeyPrefix ¶
WithKeyPrefix namespaces every key this Outbox reads or writes, so multiple gateway deployments (or unrelated applications) can share one Redis instance/database without colliding. Defaults to DefaultKeyPrefix.
func WithTTL ¶
WithTTL bounds how long an unacknowledged tail is retained after its last Append, so a connection that never reconnects to drain it cannot leak storage. The TTL is re-armed on every Append. A non-positive duration (the default) means the tail lives until Ack empties it or DropConn deletes it.
type Outbox ¶
type Outbox struct {
// contains filtered or unexported fields
}
Outbox is a gateway.Outbox backed by a Redis or Valkey client. It is safe for concurrent use.
func New ¶
func New(client goredis.UniversalClient, opts ...Option) *Outbox
New creates an Outbox backed by client. client may be a *redis.Client, *redis.ClusterClient, *redis.Ring, or any other goredis.UniversalClient implementation, pointed at either a Redis or a Valkey server; every operation touches exactly one key, so the cluster case needs no hash tags.
func (*Outbox) Ack ¶
Ack removes the message identified by msgID from connID, fenced by generation (see the gateway.Outbox.Ack doc comment): a generation trailing the floor already recorded for connID is rejected with gateway.ErrStaleOwner instead of applied. It is otherwise idempotent: acking an unknown id on a connection this Outbox has appended to (already dropped, expired, or never present) deletes nothing and is not an error, and acking a connID this Outbox has never appended to at all is a true no-op that records no fencing state and creates no key.
func (*Outbox) AdvanceGeneration ¶
AdvanceGeneration implements gateway.OutboxGenerationAdvancer. See advanceGenerationScript.
func (*Outbox) Append ¶
Append records payload as unacknowledged for connID under a freshly minted message id, stamping it with a server-assigned sequence. It re-arms the connection key's TTL when one is configured.
func (*Outbox) DropConn ¶
DropConn removes all outbox state for connID. It is idempotent: dropping a connection with no stored tail is not an error.