eventbus

package
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 27, 2026 License: MIT Imports: 9 Imported by: 0

README

EventBus Package

Asynchronous, non-blocking event bus for observability and instrumentation events in the LLM proxy.

Purpose & Responsibilities

  • Non-blocking Event Publishing: Events published asynchronously to avoid blocking request handling
  • Fan-out Broadcasting: Multiple subscribers receive all published events (in-memory backend only)
  • Backend Flexibility: In-memory (single process) or Redis Streams (distributed with guaranteed delivery)
  • Graceful Degradation: Buffer overflow drops events rather than blocking (in-memory backend)
  • At-least-once Delivery: Redis Streams backend with consumer groups, acknowledgment, and crash recovery

Architecture

flowchart TB
    subgraph Publishers
        P1[Proxy Instance 1]
        P2[Proxy Instance 2]
    end
    
    subgraph EventBus["Event Bus (In-Memory, Redis List, or Redis Streams)"]
        Buffer[Event Buffer]
    end
    
    subgraph Subscribers
        D[Dispatcher Service]
        M[Metrics Collector]
    end
    
    P1 -->|Publish| Buffer
    P2 -->|Publish| Buffer
    Buffer -->|Subscribe| D
    Buffer -->|Subscribe| M

Implementations

In-Memory EventBus

Best for single-process deployments and local development.

Feature Behavior
Delivery Single process only
Buffer Configurable size (default: 1000)
Overflow Events dropped (non-blocking)
Retry Up to 3 retries with exponential backoff
Shutdown Graceful close of subscriber channels

Key Functions: NewInMemoryEventBus(bufferSize), Publish(), Subscribe(), Stop(), Stats()

Best for distributed deployments requiring durable, guaranteed delivery with consumer groups.

Feature Behavior
Delivery At-least-once with acknowledgment
Storage Redis Streams with JSON serialization
Consumer Groups Multiple dispatcher instances share workload
Acknowledgment XACK after successful processing
Pending Recovery Claims stuck messages from crashed consumers
Trimming Automatic via MaxLen (approximate)
Blocking Read Configurable block timeout for efficiency

Key Functions: NewRedisStreamsEventBus(), Publish(), Subscribe(), Stop(), Acknowledge(), StreamLength(), PendingCount()

Consumer Group Semantics

Redis Streams implements the competing consumers pattern:

  1. Publishing: Events are added to stream via XADD
  2. Consuming: Dispatchers read via XREADGROUP with consumer groups
  3. Acknowledgment: Successfully processed messages are acknowledged with XACK
  4. Recovery: Pending messages from crashed consumers are claimed via XCLAIM
sequenceDiagram
    participant Proxy
    participant Stream as Redis Stream
    participant D1 as Dispatcher 1
    participant D2 as Dispatcher 2
    
    Proxy->>Stream: XADD event
    D1->>Stream: XREADGROUP (blocks)
    Stream->>D1: Event (assigned to D1)
    D1->>D1: Process Event
    D1->>Stream: XACK (acknowledge)
    
    Note over D2: D2 claims stuck messages<br/>if D1 crashes
    D2->>Stream: XCLAIM pending msgs

Event Schema

The Event struct captures HTTP request/response data for observability:

Field Type Description
LogID int64 Monotonic event ID (Redis only)
RequestID string Unique request identifier
Method string HTTP method
Path string Request path
Status int Response status code
Duration time.Duration Request duration
RequestBody []byte Request body
ResponseBody []byte Response body
ResponseHeaders http.Header Response headers

Configuration Options

General Options
Environment Variable Description Default
OBSERVABILITY_BUFFER_SIZE Event buffer size for in-memory bus 1000
LLM_PROXY_EVENT_BUS Backend type: in-memory or redis-streams redis-streams
REDIS_ADDR Redis server address localhost:6379
REDIS_DB Redis database number 0
Redis Streams Options
Environment Variable Description Default
REDIS_STREAM_KEY Stream key name llm-proxy-events
REDIS_CONSUMER_GROUP Consumer group name llm-proxy-dispatchers
REDIS_CONSUMER_NAME Consumer name (auto-generated if empty) ``
REDIS_STREAM_MAX_LEN Max stream length (0 = unlimited) 10000
REDIS_STREAM_BLOCK_TIME Block timeout for XREADGROUP 5s
REDIS_STREAM_CLAIM_TIME Min idle time before claiming pending messages 30s
REDIS_STREAM_BATCH_SIZE Batch size for reading messages 100

Integration Flow

sequenceDiagram
    participant Client
    participant Proxy
    participant Middleware
    participant EventBus
    participant Dispatcher
    participant Backend
    
    Client->>Proxy: HTTP Request
    Proxy->>Middleware: Process Request
    Middleware->>EventBus: Publish(Event)
    EventBus-->>Middleware: (non-blocking)
    Middleware->>Proxy: Response
    Proxy->>Client: HTTP Response
    
    EventBus->>Dispatcher: Event (via Subscribe)
    Dispatcher->>Backend: SendEvents(batch)

Testing Guidance

  • Unit Tests: Create a mock implementing EventBus interface to capture published events
  • Integration Tests: Use NewInMemoryEventBus with small buffer size to test overflow behavior
  • Redis Streams Tests: Use miniredis for integration testing with Redis Streams
  • Existing Tests: See eventbus_test.go and redis_streams_test.go for comprehensive examples

Files

File Description
eventbus.go Core interfaces, Event struct, In-Memory and Redis List implementations
redis_streams.go Redis Streams implementation with consumer groups

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Event

type Event struct {
	LogID           int64 // Monotonic event log ID
	RequestID       string
	Method          string
	Path            string
	Status          int
	Duration        time.Duration
	ResponseHeaders http.Header
	ResponseBody    []byte
	RequestBody     []byte
}

Event represents an observability event emitted by the proxy.

type EventBus

type EventBus interface {
	Publish(ctx context.Context, evt Event)
	Subscribe() <-chan Event
	Stop()
}

EventBus is a simple interface for publishing events to subscribers.

type InMemoryEventBus

type InMemoryEventBus struct {
	// contains filtered or unexported fields
}

InMemoryEventBus is an EventBus implementation backed by a buffered channel and fan-out broadcasting to multiple subscribers. Events are dispatched asynchronously to avoid blocking the request path.

func NewInMemoryEventBus

func NewInMemoryEventBus(bufferSize int) *InMemoryEventBus

NewInMemoryEventBus creates a new in-memory event bus with the given buffer size.

func (*InMemoryEventBus) Publish

func (b *InMemoryEventBus) Publish(ctx context.Context, evt Event)

Publish sends an event to the bus without blocking if the buffer is full.

func (*InMemoryEventBus) Stats

func (b *InMemoryEventBus) Stats() (published, dropped int)

Stats returns the number of published and dropped events.

func (*InMemoryEventBus) Stop

func (b *InMemoryEventBus) Stop()

Stop gracefully stops the event bus and closes all subscriber channels.

func (*InMemoryEventBus) Subscribe

func (b *InMemoryEventBus) Subscribe() <-chan Event

Subscribe returns a channel that receives events published to the bus. Each subscriber receives all events.

type RedisStreamsClient

type RedisStreamsClient interface {
	// XAdd adds an entry to a stream
	XAdd(ctx context.Context, args *redis.XAddArgs) (string, error)
	// XReadGroup reads entries from a stream using a consumer group
	XReadGroup(ctx context.Context, args *redis.XReadGroupArgs) ([]redis.XStream, error)
	// XAck acknowledges processed messages
	XAck(ctx context.Context, stream, group string, ids ...string) (int64, error)
	// XGroupCreateMkStream creates a consumer group (and the stream if needed)
	XGroupCreateMkStream(ctx context.Context, stream, group, start string) error
	// XPending returns pending entries for a consumer group
	XPending(ctx context.Context, stream, group string) (*redis.XPending, error)
	// XPendingExt returns detailed pending entries
	XPendingExt(ctx context.Context, args *redis.XPendingExtArgs) ([]redis.XPendingExt, error)
	// XClaim claims pending messages for a consumer
	XClaim(ctx context.Context, args *redis.XClaimArgs) ([]redis.XMessage, error)
	// XLen returns the length of a stream
	XLen(ctx context.Context, stream string) (int64, error)
	// XInfoGroups returns consumer group info for a stream
	XInfoGroups(ctx context.Context, stream string) ([]redis.XInfoGroup, error)
}

RedisStreamsClient interface for Redis Streams operations. This abstraction allows for easy mocking in tests.

type RedisStreamsClientAdapter

type RedisStreamsClientAdapter struct {
	Client *redis.Client
}

RedisStreamsClientAdapter adapts go-redis/v9 Client to the RedisStreamsClient interface.

func (*RedisStreamsClientAdapter) XAck

func (a *RedisStreamsClientAdapter) XAck(ctx context.Context, stream, group string, ids ...string) (int64, error)

XAck acknowledges processed messages

func (*RedisStreamsClientAdapter) XAdd

XAdd adds an entry to a stream

func (*RedisStreamsClientAdapter) XClaim

XClaim claims pending messages for a consumer

func (*RedisStreamsClientAdapter) XGroupCreateMkStream

func (a *RedisStreamsClientAdapter) XGroupCreateMkStream(ctx context.Context, stream, group, start string) error

XGroupCreateMkStream creates a consumer group (and the stream if needed)

func (*RedisStreamsClientAdapter) XInfoGroups

func (a *RedisStreamsClientAdapter) XInfoGroups(ctx context.Context, stream string) ([]redis.XInfoGroup, error)

XInfoGroups returns consumer group info for a stream

func (*RedisStreamsClientAdapter) XLen

func (a *RedisStreamsClientAdapter) XLen(ctx context.Context, stream string) (int64, error)

XLen returns the length of a stream

func (*RedisStreamsClientAdapter) XPending

func (a *RedisStreamsClientAdapter) XPending(ctx context.Context, stream, group string) (*redis.XPending, error)

XPending returns pending entries for a consumer group

func (*RedisStreamsClientAdapter) XPendingExt

XPendingExt returns detailed pending entries

func (*RedisStreamsClientAdapter) XReadGroup

XReadGroup reads entries from a stream using a consumer group

type RedisStreamsConfig

type RedisStreamsConfig struct {
	StreamKey        string        // Redis stream key name
	ConsumerGroup    string        // Consumer group name
	ConsumerName     string        // Unique consumer name within the group
	MaxLen           int64         // Max stream length (0 = unlimited, uses MAXLEN ~ approximation)
	BlockTimeout     time.Duration // Block timeout for XREADGROUP (0 = non-blocking)
	ClaimMinIdleTime time.Duration // Minimum idle time before claiming pending messages
	BatchSize        int64         // Number of messages to read at once
}

RedisStreamsConfig holds configuration for Redis Streams event bus.

func DefaultRedisStreamsConfig

func DefaultRedisStreamsConfig() RedisStreamsConfig

DefaultRedisStreamsConfig returns default configuration.

type RedisStreamsEventBus

type RedisStreamsEventBus struct {
	// contains filtered or unexported fields
}

RedisStreamsEventBus implements EventBus using Redis Streams. It provides durable, distributed event delivery with consumer groups, acknowledgment, and at-least-once delivery semantics.

func NewRedisStreamsEventBus

func NewRedisStreamsEventBus(client RedisStreamsClient, config RedisStreamsConfig) *RedisStreamsEventBus

NewRedisStreamsEventBus creates a new Redis Streams event bus.

func (*RedisStreamsEventBus) Acknowledge

func (b *RedisStreamsEventBus) Acknowledge(ctx context.Context, messageID string) error

Acknowledge manually acknowledges a message by ID. This is useful when external code handles message processing and acknowledgment.

func (*RedisStreamsEventBus) Client

Client returns the underlying RedisStreamsClient.

func (*RedisStreamsEventBus) EnsureConsumerGroup

func (b *RedisStreamsEventBus) EnsureConsumerGroup(ctx context.Context) error

EnsureConsumerGroup creates the consumer group if it doesn't exist. This should be called before starting to consume messages.

func (*RedisStreamsEventBus) PendingCount

func (b *RedisStreamsEventBus) PendingCount(ctx context.Context) (int64, error)

PendingCount returns the number of pending messages in the consumer group.

func (*RedisStreamsEventBus) Publish

func (b *RedisStreamsEventBus) Publish(ctx context.Context, evt Event)

Publish adds an event to the Redis stream using XADD.

func (*RedisStreamsEventBus) Stats

func (b *RedisStreamsEventBus) Stats() (published, dropped int)

Stats returns the number of published and dropped events.

func (*RedisStreamsEventBus) Stop

func (b *RedisStreamsEventBus) Stop()

Stop gracefully stops the event bus and closes all subscriber channels.

func (*RedisStreamsEventBus) StreamLength

func (b *RedisStreamsEventBus) StreamLength(ctx context.Context) (int64, error)

StreamLength returns the current length of the stream.

func (*RedisStreamsEventBus) Subscribe

func (b *RedisStreamsEventBus) Subscribe() <-chan Event

Subscribe returns a channel that receives events from the stream. This starts a background goroutine that reads from the stream using consumer groups.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL