mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-15 20:52:03 +00:00
feat: add durable outbox dispatcher
This commit is contained in:
@@ -34,6 +34,57 @@ WHERE event_id = $1 AND published_at IS NULL`
|
||||
|
||||
var ErrOutboxEventNotFound = fmt.Errorf("outbox event not found or already published")
|
||||
|
||||
type OutboxDelivery func(context.Context, OutboxEvent) error
|
||||
|
||||
// OutboxDispatcher is the durable-to-transient bridge. Read and Ack are
|
||||
// injectable so ordering can be tested without a live PostgreSQL instance.
|
||||
// Delivery is at-least-once: a crash after delivery and before acknowledgement
|
||||
// leaves the event replayable, while a delivery failure stops the batch.
|
||||
type OutboxDispatcher struct {
|
||||
Read func(context.Context, int) ([]OutboxEvent, error)
|
||||
Ack func(context.Context, string, time.Time) error
|
||||
Deliver OutboxDelivery
|
||||
}
|
||||
|
||||
func NewOutboxDispatcher(db *sql.DB, deliver OutboxDelivery) *OutboxDispatcher {
|
||||
return &OutboxDispatcher{
|
||||
Read: func(ctx context.Context, limit int) ([]OutboxEvent, error) {
|
||||
return ReadUnpublishedOutbox(ctx, db, limit)
|
||||
},
|
||||
Ack: func(ctx context.Context, eventID string, publishedAt time.Time) error {
|
||||
return MarkOutboxPublished(ctx, db, eventID, publishedAt)
|
||||
},
|
||||
Deliver: deliver,
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch publishes at most limit events in the store's stable order and
|
||||
// returns the number acknowledged. A successful delivery followed by an ack
|
||||
// error intentionally leaves that event replayable.
|
||||
func (d *OutboxDispatcher) Dispatch(ctx context.Context, limit int, publishedAt time.Time) (int, error) {
|
||||
if d == nil || d.Read == nil || d.Ack == nil || d.Deliver == nil || limit < 1 || limit > 1000 || publishedAt.IsZero() {
|
||||
return 0, fmt.Errorf("invalid outbox dispatcher")
|
||||
}
|
||||
events, err := d.Read(ctx, limit)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
acknowledged := 0
|
||||
for _, event := range events {
|
||||
if event.EventID == "" {
|
||||
return acknowledged, fmt.Errorf("outbox event has no ID")
|
||||
}
|
||||
if err := d.Deliver(ctx, event); err != nil {
|
||||
return acknowledged, err
|
||||
}
|
||||
if err := d.Ack(ctx, event.EventID, publishedAt); err != nil {
|
||||
return acknowledged, err
|
||||
}
|
||||
acknowledged++
|
||||
}
|
||||
return acknowledged, nil
|
||||
}
|
||||
|
||||
// ReadUnpublishedOutbox returns a bounded, stable ordered batch. It does not
|
||||
// mark rows before delivery: a worker crash therefore leaves events replayable.
|
||||
func ReadUnpublishedOutbox(ctx context.Context, db *sql.DB, limit int) ([]OutboxEvent, error) {
|
||||
|
||||
Reference in New Issue
Block a user