redis_ipc

package module
v0.15.3 Latest Latest
Warning

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

Go to latest
Published: Sep 21, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

README

Librescoot Redis IPC Library

Redis-based IPC library for Go with type-safe generics, functional options, and Librescoot-specific patterns for hash-based state management.

Part of the Librescoot open-source platform.

Features

  • Generics: Type-safe subscriptions and queue handlers
  • Functional Options: Flexible client configuration
  • Connection Callbacks: React to connect/disconnect events
  • Context-Free API: Uses client's internal context by default
  • Graceful Shutdown: Wait for handlers to complete
  • Hash State Pattern: Atomic HSET + PUBLISH with change detection
  • Fault Set Management: Redis SET with pub/sub notifications
  • Stream Publishing: XADD with configurable max length
  • Stream Consumption: XREAD with optional consumer groups
  • Transaction Builder: Atomic MULTI/EXEC across HashPublisher, FaultSet, and StreamPublisher
  • Multiplexed Connections: All subscriptions share one pub/sub connection, all queues share one BRPOP loop

Installation

go get github.com/librescoot/redis-ipc

Quick Start

import ipc "github.com/librescoot/redis-ipc"

// Create client with options
client, err := ipc.New(
    ipc.WithAddress("localhost"),
    ipc.WithPort(6379),
    ipc.WithOnConnect(func() { log.Println("connected") }),
    ipc.WithOnDisconnect(func(err error) { log.Println("disconnected") }),
)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

Typed Subscriptions

type VehicleState struct {
    State   string `json:"state"`
    Speed   int    `json:"speed"`
}

// Subscribe with automatic JSON decoding
sub, err := ipc.Subscribe(client, "vehicle:events", func(msg VehicleState) error {
    log.Printf("State: %s, Speed: %d", msg.State, msg.Speed)
    return nil
})
defer sub.Unsubscribe()

// Publish typed messages
ipc.PublishTyped(client, "vehicle:events", VehicleState{State: "ready", Speed: 0})

Queue Processing

type Command struct {
    Action string `json:"action"`
}

// Handle queue items with automatic restart on errors
handler := ipc.HandleRequests(client, "scooter:commands", func(cmd Command) error {
    log.Printf("Command: %s", cmd.Action)
    return nil
})
defer handler.Stop()

// Send to queue
ipc.SendRequest(client, "scooter:commands", Command{Action: "unlock"})

RPC: Call / HandleCalls

Synchronous request/response over Redis. Request leg uses a list (LPUSH/BRPOP) so requests survive brief server restarts; reply leg uses pub/sub (PUBLISH/SUBSCRIBE) so sub-second timeouts work and there's no per-call key bookkeeping.

type SetProfile struct {
    Profile string `json:"profile"`
}
type ProfileApplied struct {
    Profile string `json:"profile"`
    OK      bool   `json:"ok"`
}

// Server side
srv := ipc.HandleCalls[SetProfile, ProfileApplied](client, "motion:rpc:set-profile",
    func(req SetProfile) (ProfileApplied, error) {
        if err := chip.applyProfile(req.Profile); err != nil {
            return ProfileApplied{}, err
        }
        return ProfileApplied{Profile: req.Profile, OK: true}, nil
    },
    ipc.WithCallConcurrency(4), // optional, default 4
)
defer srv.Stop()

// Client side
resp, err := ipc.Call[SetProfile, ProfileApplied](client, "motion:rpc:set-profile",
    SetProfile{Profile: "armed-hibernation"},
    1*time.Second)
if errors.Is(err, ipc.ErrCallTimeout) {
    // server didn't reply in time
} else if ipc.IsCallError(err) {
    // server-side handler returned an error
}

Behaviour notes:

  • Request envelopes carry a deadline; servers drop requests whose deadline has already passed (caller already gave up).
  • If no server is registered, the request sits in the list. When a server starts, it picks the request up — but if its deadline has expired, it's dropped without a reply. The caller hits ErrCallTimeout.
  • Stop() waits for in-flight handlers to finish before returning.
CallServer: many methods, one channel

HandleCalls is one Redis key per method, which spreads a service's RPC surface across a pile of list keys. CallServer is the multi-method shape: one shared request channel per service, the server dispatches by method field on the envelope.

// Server side
srv := ipc.NewCallServer(client, "motion:rpc")
ipc.RegisterCall[GetCalReq, CalResp](srv, "get-calibration", handleGetCal)
ipc.RegisterCall[PrepHibReq, PrepHibResp](srv, "prepare-hibernation", handlePrepHib)
ipc.RegisterCall[Empty, OK](srv, "clear-latch", handleClearLatch)
srv.Start()
defer srv.Stop()

// Client side
resp, err := ipc.CallMethod[Req, Resp](client, "motion:rpc", "get-calibration", req, 1*time.Second)
  • RegisterCall panics if called after Start, or for a duplicate method name — both are programmer errors and silent ignore would be worse than crashing in dev.
  • Unknown method on the server returns a *CallError with Msg = "unknown method: X" to the caller.
  • WithCallServerConcurrency(n) caps in-flight handler invocations across all methods on the server (default 4).
  • Wire-compatible with the per-method Call clients: the envelope's method field is optional. Call/HandleCalls and CallMethod/CallServer can coexist on different channels.

Librescoot Hash Pattern

The Librescoot pattern stores state in Redis hashes and notifies via pub/sub:

  • Publisher: HSET vehicle state "ready" → PUBLISH vehicle "state"
  • Consumer: SUBSCRIBE vehicle → receives "state" → HGET vehicle state
HashPublisher
// Create publisher for "vehicle" hash (publishes to "vehicle" channel)
vehicle := client.NewHashPublisher("vehicle")

// Or with a custom channel name
vehicle := client.NewHashPublisherWithChannel("vehicle", "state")

// Set field and publish atomically
vehicle.Set("state", "ready")

// Only publish if value changed
changed, _ := vehicle.SetIfChanged("state", "ready")

// Batch updates with selective publishing
vehicle.SetManyIfChanged(map[string]any{
    "state":      "parked",
    "kickstand":  "down",
    "brake:left": "off",
})

// Set with automatic timestamp field
vehicle.SetWithTimestamp("state", "ready")
// Sets both "state" and "state:timestamp"

// Delete a single field
vehicle.Delete("old-field")

// Clear entire hash
vehicle.Clear()

// Atomic replace: DEL + HMSET + PUBLISH
vehicle.ReplaceAll(map[string]any{
    "state": "ready",
    "speed": 0,
})

// Set without publishing (silent update). NoPublish() works on every
// HashPublisher method that takes options, including the destructive ones:
vehicle.Set("internal-state", "value", ipc.NoPublish())
vehicle.SetMany(fields, ipc.NoPublish())
vehicle.Delete("retired-field", ipc.NoPublish())   // retire a field quietly
vehicle.Clear(ipc.NoPublish())

// Batch update with single notification
vehicle.SetManyPublishOne(map[string]any{
    "lat": "52.520",
    "lon": "13.405",
}, "location")  // Publishes only "location", not each field
HashWatcher
// Create watcher for "battery:0" hash
watcher := client.NewHashWatcher("battery:0")

// Or with a custom channel name
watcher := client.NewHashWatcherWithChannel("battery:0", "battery")

// Register field-specific handlers
watcher.OnField("state", func(value string) error {
    log.Printf("Battery state: %s", value)
    return nil
})

watcher.OnField("charge", func(value string) error {
    log.Printf("Battery charge: %s%%", value)
    return nil
})

// Catch-all for unhandled fields
watcher.OnAny(func(field, value string) error {
    log.Printf("%s = %s", field, value)
    return nil
})

// Typed handler with automatic JSON decoding
ipc.OnFieldTyped(watcher, "config", func(cfg Config) error {
    return applyConfig(cfg)
})

// Start watching
watcher.Start()
defer watcher.Stop()

// Fetch initial state
all, _ := watcher.FetchAll()

// Fetch a single field
state, _ := watcher.Fetch("state")
StartWithSync

Subscribe first, fetch current state, then process messages. This avoids race conditions: StartWithSync returns only once Redis has confirmed the SUBSCRIBE, and anything published while the initial HGETALL runs is buffered and delivered afterwards.

watcher := client.NewHashWatcher("vehicle")
watcher.OnField("state", handleState)

// Subscribes, fetches HGETALL, calls handlers, then processes messages
watcher.StartWithSync()
Debouncing

Coalesce rapid updates - only the last value is passed to handlers after the quiet period:

watcher := client.NewHashWatcher("vehicle")
watcher.SetDebounce(500 * time.Millisecond)  // Wait 500ms after last update
watcher.OnField("speed", handleSpeed)        // Called once after rapid changes settle
watcher.Start()
FaultSet
// Manage fault codes in a Redis SET with pub/sub notification
faults := client.NewFaultSet("battery:0:fault", "battery:0", "fault")

faults.Add(35)     // SADD + PUBLISH
faults.Remove(35)  // SREM + PUBLISH
faults.Has(35)     // SISMEMBER
faults.All()       // SMEMBERS
faults.Clear()     // DEL + PUBLISH

Redis Streams

StreamPublisher

Publish events to a Redis stream using XADD:

// Create publisher with default max length (1000)
stream := client.NewStreamPublisher("events:faults")

// Or with custom max length
stream := client.NewStreamPublisher("events:faults", ipc.WithMaxLen(5000))

// Publish a map
id, err := stream.Add(map[string]any{
    "group":       "battery:0",
    "code":        "35",
    "description": "NFC Reader Error",
})

// Publish a typed struct (JSON-encoded to fields)
type FaultEvent struct {
    Group       string `json:"group"`
    Code        int    `json:"code"`
    Description string `json:"description"`
}

id, err := ipc.StreamAdd(stream, FaultEvent{
    Group:       "battery:0",
    Code:        35,
    Description: "NFC Reader Error",
})
StreamConsumer

Consume stream messages using XREAD:

// Create consumer
consumer := client.NewStreamConsumer("events:faults",
    ipc.WithBlockTimeout(1 * time.Second),
)

// Set handler
consumer.Handle(func(id string, values map[string]string) error {
    log.Printf("Fault %s: group=%s code=%s", id, values["group"], values["code"])
    return nil
})

// Start from beginning ("0") or only new messages ("$")
consumer.Start("0")

// Or use typed handler
ipc.StreamHandle(consumer, func(id string, evt FaultEvent) error {
    log.Printf("Fault: %+v", evt)
    return nil
})
consumer.Start("$")
Consumer Groups

For multi-instance consumption with acknowledgment:

consumer := client.NewStreamConsumer("events:faults",
    ipc.WithBlockTimeout(1 * time.Second),
    ipc.WithConsumerGroup("uplink-service", "instance-1"),
)

consumer.Handle(handler)

// ">" means only undelivered messages to this group
consumer.Start(">")

Transaction Builder

Tx combines operations from HashPublisher, FaultSet, and StreamPublisher into a single atomic MULTI/EXEC pipeline. Hash caches are only updated after a successful Exec.

battery := client.NewHashPublisher("battery:0")
faults := client.NewFaultSet("battery:0:fault", "battery:0", "fault")
events := client.NewStreamPublisher("events:faults")

// All operations execute atomically
tx := client.NewTx()
changed := tx.HashSetManyIfChanged(battery, map[string]any{
    "state": "fault",
    "code":  "35",
})
tx.FaultAdd(faults, 35)
tx.StreamAdd(events, map[string]any{"event": "fault_detected", "code": "35"})

if err := tx.Exec(); err != nil {
    // Redis error: nothing committed, hash cache unchanged
}
// On success: hash fields written, fault added, stream entry appended, cache updated

// Batch fault add/remove with a single PUBLISH
tx2 := client.NewTx()
tx2.FaultUpdate(faults, []int{36, 37}, []int{35}) // add 36+37, remove 35
tx2.Exec()

// Use a specific context (e.g., for timeout)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tx3 := client.NewTxWithContext(ctx)
tx3.HashSetManyIfChanged(battery, fields)
tx3.Exec()

Message Router

For JSON envelope-based routing ({"type": "...", "data": {...}}):

router := client.NewRouter("events")

ipc.Handle(router, "state", func(s StateMsg) error { ... })
ipc.Handle(router, "error", func(e ErrorMsg) error { ... })

router.Start()
defer router.Stop()

// Publish routed messages
ipc.PublishRouted(client, "events", "state", StateMsg{...})

Connection Model

A Client holds far fewer Redis connections than it has consumers.

  • One pub/sub connection per client. Every HashWatcher, Subscribe[T] and Router registers its channel on a shared subscriber. Ten watchers cost one connection and one SUBSCRIBE per channel, not ten connections. go-redis dedicates a connection to each PubSub object, so the naive shape also cost a health-check ping every 3s per watcher.
  • One BRPOP loop per client. Every HandleRequests, HandleCalls and CallServer joins a shared blocking pop over all registered keys. BRPOP reports which key popped, so the mux routes by key name. The key order rotates each iteration, so a busy queue cannot starve a quiet one.
  • Isolation is preserved. Each subscriber and each queue gets its own goroutine and buffered backlog, so a slow handler stalls only its own channel or queue.

Nothing about this is visible in the API: Start(), Stop(), HandleRequests and friends behave as before. Two consequences worth knowing:

  • WithPoolSize no longer needs padding for blocking consumers. A service with eight command queues used to need WithPoolSize(12) just so its BRPOPs wouldn't exhaust the pool; the default of 3 is now fine.
  • Stop() on one watcher only unsubscribes the underlying channel if no other watcher in the process still wants it.

Configuration Options

client, err := ipc.New(
    ipc.WithURL("redis://localhost:6379"),  // Or just "localhost:6379" or "localhost"
    // Or use separate address/port:
    // ipc.WithAddress("localhost"),
    // ipc.WithPort(6379),
    ipc.WithRetryInterval(5 * time.Second),
    ipc.WithMaxRetries(3),
    ipc.WithPoolSize(3),  // Blocking consumers share one connection, so the default is usually enough
    ipc.WithDialTimeout(2 * time.Second),
    ipc.WithLogger(slog.Default()),
    ipc.WithCodec(ipc.JSONCodec{}),  // or ipc.StringCodec{}
    ipc.WithOnConnect(func() { ... }),
    ipc.WithOnDisconnect(func(err error) { ... }),
)

Direct Redis Operations

All operations use the client's internal context:

// Strings
client.Get("key")
client.Set("key", "value", 0)
client.Incr("counter")

// Hashes
client.HGet("hash", "field")
client.HSet("hash", "field", "value")
client.HGetAll("hash")

// Lists
client.LPush("queue", "value")
client.BRPop(time.Second, "queue")

// Pub/Sub
client.Publish("channel", "message")

// Keys
client.Exists("key")
client.Del("key")
client.Expire("key", time.Hour)

// Health check
client.Ping()

// Raw command
client.Do("PING")

// Access underlying go-redis client (requires context)
client.Raw().Scan(ctx, ...)

Best Practices

Reuse Publishers and Watchers

Create publishers once and store them in your service struct:

// Good: create once, reuse
type Service struct {
    client   *ipc.Client
    powerPub *ipc.HashPublisher
    battPub  *ipc.HashPublisher
}

func NewService(client *ipc.Client) *Service {
    return &Service{
        client:   client,
        powerPub: client.NewHashPublisher("power-manager"),
        battPub:  client.NewHashPublisher("battery:0"),
    }
}

func (s *Service) UpdatePowerState(state string) error {
    return s.powerPub.Set("state", state)
}

// Bad: creates new publisher on every call (wasteful)
func (s *Service) UpdatePowerStateBad(state string) error {
    pub := s.client.NewHashPublisher("power-manager")
    return pub.Set("state", state)
}
Use SetManyIfChanged for Bulk Updates

When updating multiple fields, SetManyIfChanged only publishes changed fields:

// Returns list of actually-changed fields (useful for logging)
changed, err := pub.SetManyIfChanged(map[string]any{
    "state":  newState,
    "speed":  newSpeed,
    "charge": newCharge,
})
if len(changed) > 0 {
    log.Printf("Updated fields: %v", changed)
}
Use ReplaceAll for Complete Hash Replacement

When you need to atomically clear and repopulate a hash (e.g., inhibitor lists):

// Atomic: DEL + HMSET + PUBLISH in one transaction
pub.ReplaceAll(map[string]any{
    "inhibitor1": "reason1",
    "inhibitor2": "reason2",
})

// Clear the hash entirely
pub.ReplaceAll(nil)  // or pub.Clear()
Use StartWithSync for Initial State

When you need current values before processing updates:

watcher := client.NewHashWatcher("vehicle")
watcher.OnField("state", handleState)

// StartWithSync: Subscribe → HGETALL → call handlers → process messages
// Ensures no messages are missed during initialization
watcher.StartWithSync()

// vs Start(): Just subscribes, doesn't fetch initial state
// watcher.Start()

License

This project is dual-licensed. The source code is available under the GNU Affero General Public License v3.0. The maintainers reserve the right to grant separate licenses for commercial distribution; please contact the maintainers to discuss commercial licensing.

AGPL v3

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrCallTimeout = errors.New("redis-ipc: call timed out")

ErrCallTimeout is returned by Call when no response arrives within the caller-specified timeout.

View Source
var ErrClientClosed = fmt.Errorf("redis-ipc: client is closed")

ErrClientClosed is returned when registering a consumer on a client that is shutting down.

View Source
var ErrNil = redis.Nil

ErrNil is returned when a key or field does not exist in Redis.

View Source
var ErrUnknownMethod = errors.New("unknown method")

ErrUnknownMethod is returned (server-side) when a request arrives for a method that has no registered handler. The caller receives a *CallError whose Msg is "unknown method: X".

Functions

func Call added in v0.12.0

func Call[Req, Resp any](c *Client, channel string, req Req, timeout time.Duration) (Resp, error)

Call sends a typed request to a Redis list-backed RPC channel and blocks until the response arrives or the timeout elapses. Returns ErrCallTimeout on timeout, *CallError on a server-side handler error, or a wrapped error for transport / decode failures.

func CallMethod added in v0.13.0

func CallMethod[Req, Resp any](c *Client, channel, method string, req Req, timeout time.Duration) (Resp, error)

CallMethod sends a typed request to a CallServer's per-service channel, addressed to a specific method. Same semantics as Call (sub-second timeouts, ErrCallTimeout / *CallError discrimination), just with an extra method field on the wire so a single channel can host many RPCs.

func IsCallError added in v0.12.0

func IsCallError(err error) bool

IsCallError reports whether err originated as a server-side handler error.

func PublishRouted

func PublishRouted[T any](c *Client, channel, msgType string, data T) error

PublishRouted publishes a message with type routing

func PublishTyped

func PublishTyped[T any](c *Client, channel string, message T, opts ...SetOption) error

PublishTyped publishes a typed message using the client's codec. By default async; use Sync() for synchronous operation.

func RegisterCall added in v0.13.0

func RegisterCall[Req, Resp any](s *CallServer, method string, handler func(Req) (Resp, error))

RegisterCall attaches a typed handler for one method. Must be called before Start. Re-registering the same method panics — that's a clear programmer error rather than a silent override.

func SendRequest

func SendRequest[T any](c *Client, queue string, message T) error

SendRequest sends a typed message to a queue

func StreamAdd

func StreamAdd[T any](sp *StreamPublisher, msg T) (string, error)

StreamAdd publishes a typed message to the stream using JSON encoding.

Types

type CallError added in v0.12.0

type CallError struct{ Msg string }

CallError wraps an error returned by a HandleCalls handler.

func (*CallError) Error added in v0.12.0

func (e *CallError) Error() string

type CallHandler added in v0.12.0

type CallHandler[Req, Resp any] struct {
	// contains filtered or unexported fields
}

CallHandler is the server side of a Call channel. Stop() drains in-flight handlers before returning.

func HandleCalls added in v0.12.0

func HandleCalls[Req, Resp any](c *Client, channel string, handler func(Req) (Resp, error), opts ...CallHandlerOption) *CallHandler[Req, Resp]

HandleCalls registers a typed RPC handler. The request channel joins the client's shared BRPOP loop; each incoming request is dispatched to a goroutine bounded by the concurrency option.

func (*CallHandler[Req, Resp]) Stop added in v0.12.0

func (h *CallHandler[Req, Resp]) Stop()

Stop signals the handler to stop accepting new requests and waits for in-flight handlers to finish.

type CallHandlerOption added in v0.12.0

type CallHandlerOption func(*callHandlerOpts)

CallHandlerOption configures a CallHandler.

func WithCallConcurrency added in v0.12.0

func WithCallConcurrency(n int) CallHandlerOption

WithCallConcurrency caps the number of in-flight handler invocations. Defaults to 4.

type CallServer added in v0.13.0

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

CallServer is the multi-method RPC server. One BRPOP loop, one concurrency-bounded dispatcher, many registered handlers.

func NewCallServer added in v0.13.0

func NewCallServer(c *Client, channel string, opts ...CallServerOption) *CallServer

NewCallServer returns a CallServer bound to a per-service request channel (e.g. "motion:rpc"). Use RegisterCall to attach typed handlers, then Start to begin processing requests.

func (*CallServer) Start added in v0.13.0

func (s *CallServer) Start()

Start joins the request channel to the client's shared BRPOP loop. Idempotent: repeated calls are no-ops.

func (*CallServer) Stop added in v0.13.0

func (s *CallServer) Stop()

Stop signals the server to stop accepting new requests and waits for in-flight handlers to finish.

type CallServerOption added in v0.13.0

type CallServerOption func(*callServerOpts)

CallServerOption configures a CallServer.

func WithCallServerConcurrency added in v0.13.0

func WithCallServerConcurrency(n int) CallServerOption

WithCallServerConcurrency caps in-flight handler invocations across all methods on this server. Default 4.

type Client

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

Client is the main Redis IPC client

func New

func New(opts ...Option) (*Client, error)

New creates a new Redis IPC client with functional options

func (*Client) BLPop

func (c *Client) BLPop(timeout time.Duration, keys ...string) ([]string, error)

BLPop blocks until it can remove and return the first element of a list

func (*Client) BRPop

func (c *Client) BRPop(timeout time.Duration, keys ...string) ([]string, error)

BRPop blocks until it can remove and return the last element of a list

func (*Client) Close

func (c *Client) Close() error

Close gracefully shuts down the client, waiting for handlers to finish

func (*Client) CloseWithTimeout

func (c *Client) CloseWithTimeout(timeout time.Duration) error

CloseWithTimeout shuts down the client with a maximum wait time

func (*Client) Codec

func (c *Client) Codec() Codec

Codec returns the client's codec

func (*Client) Connected

func (c *Client) Connected() bool

Connected returns the current connection status

func (*Client) Context added in v0.7.0

func (c *Client) Context() context.Context

Context returns the client's context for operations.

func (*Client) Decr

func (c *Client) Decr(key string) (int64, error)

Decr decrements the integer value of a key by one

func (*Client) Del

func (c *Client) Del(keys ...string) (int64, error)

Del deletes keys

func (*Client) Do

func (c *Client) Do(cmd string, args ...interface{}) (interface{}, error)

Do executes a Redis command

func (*Client) Exists

func (c *Client) Exists(keys ...string) (int64, error)

Exists checks if keys exist

func (*Client) Expire

func (c *Client) Expire(key string, expiration time.Duration) (bool, error)

Expire sets a key's time to live

func (*Client) Get

func (c *Client) Get(key string) (string, error)

Get retrieves the value of a key

func (*Client) HGet

func (c *Client) HGet(key, field string) (string, error)

HGet retrieves the value of a hash field

func (*Client) HGetAll

func (c *Client) HGetAll(key string) (map[string]string, error)

HGetAll retrieves all fields and values of a hash

func (*Client) HSet

func (c *Client) HSet(key, field string, value interface{}) error

HSet sets the value of a hash field

func (*Client) Hash added in v0.6.0

func (c *Client) Hash(name string) *HashPublisher

Hash returns a cached HashPublisher for the given hash name. Publishers are created on first access and reused thereafter. This is more ergonomic than NewHashPublisher when you don't need to store the publisher in a struct field.

func (*Client) Incr

func (c *Client) Incr(key string) (int64, error)

Incr increments the integer value of a key by one

func (*Client) LPop

func (c *Client) LPop(key string) (string, error)

LPop removes and returns the first element of a list

func (*Client) LPush

func (c *Client) LPush(key string, values ...interface{}) (int64, error)

LPush inserts values at the head of a list

func (*Client) Logger

func (c *Client) Logger() *slog.Logger

Logger returns the client's logger

func (*Client) NewFaultReporter added in v0.14.0

func (c *Client) NewFaultReporter(group string, opts ...FaultReporterOption) *FaultReporter

NewFaultReporter creates a FaultReporter for the given group. Defaults follow the Librescoot convention:

set key:  "<group>:fault"
channel:  "<group>"
payload:  "fault"
stream:   "events:faults"
MAXLEN:   ~1000

func (*Client) NewFaultSet

func (c *Client) NewFaultSet(setKey, channel, payload string) *FaultSet

NewFaultSet creates a fault set manager. Example: NewFaultSet("battery:0:fault", "battery:0", "fault")

func (*Client) NewHashPublisher

func (c *Client) NewHashPublisher(hash string) *HashPublisher

NewHashPublisher creates a publisher for a hash. By convention, the channel name equals the hash name.

func (*Client) NewHashPublisherWithChannel

func (c *Client) NewHashPublisherWithChannel(hash, channel string) *HashPublisher

NewHashPublisherWithChannel creates a publisher with a custom channel name.

func (*Client) NewHashWatcher

func (c *Client) NewHashWatcher(hash string) *HashWatcher

NewHashWatcher creates a watcher for a hash. By convention, the channel name equals the hash name.

func (*Client) NewHashWatcherWithChannel

func (c *Client) NewHashWatcherWithChannel(hash, channel string) *HashWatcher

NewHashWatcherWithChannel creates a watcher with a custom channel name.

func (*Client) NewRouter

func (c *Client) NewRouter(channel string) *Router

NewRouter creates a message router for a channel

func (*Client) NewStreamConsumer

func (c *Client) NewStreamConsumer(stream string, opts ...StreamConsumerOption) *StreamConsumer

NewStreamConsumer creates a consumer for a Redis stream.

func (*Client) NewStreamPublisher

func (c *Client) NewStreamPublisher(stream string, opts ...StreamPublisherOption) *StreamPublisher

NewStreamPublisher creates a publisher for a Redis stream.

func (*Client) NewTx added in v0.11.0

func (c *Client) NewTx() *Tx

NewTx creates a new transaction using the client's context.

func (*Client) NewTxGroup

func (c *Client) NewTxGroup() *TxGroup

NewTxGroup creates a new transaction group

func (*Client) NewTxWithContext added in v0.11.0

func (c *Client) NewTxWithContext(ctx context.Context) *Tx

NewTxWithContext creates a new transaction with a specific context.

func (*Client) Ping added in v0.5.0

func (c *Client) Ping() error

Ping checks if the Redis server is reachable

func (*Client) Publish

func (c *Client) Publish(channel string, message interface{}, opts ...SetOption) (int64, error)

Publish publishes a message to a channel. By default, this is async (fire-and-forget) for performance. Use Sync() option when you need to confirm the publish succeeded or need the subscriber count.

func (*Client) RPop

func (c *Client) RPop(key string) (string, error)

RPop removes and returns the last element of a list

func (*Client) RPush

func (c *Client) RPush(key string, values ...interface{}) (int64, error)

RPush inserts values at the tail of a list

func (*Client) Raw

func (c *Client) Raw() *redis.Client

Raw returns the underlying go-redis client for advanced operations

func (*Client) Set

func (c *Client) Set(key string, value interface{}, expiration time.Duration) error

Set sets the value of a key

func (*Client) WithContext added in v0.7.0

func (c *Client) WithContext(ctx context.Context) *ContextClient

WithContext returns a context-scoped client wrapper. Use this when you need a custom context (e.g., with timeout) for operations.

type Codec

type Codec interface {
	Encode(v any) ([]byte, error)
	Decode(data []byte, v any) error
}

Codec handles message serialization/deserialization

type ContextClient added in v0.7.0

type ContextClient struct {
	*Client
	// contains filtered or unexported fields
}

ContextClient wraps a Client with a specific context for operations.

func (*ContextClient) Context added in v0.7.0

func (cc *ContextClient) Context() context.Context

Context returns the custom context for this wrapper.

type Envelope

type Envelope struct {
	Type string          `json:"type"`
	Data json.RawMessage `json:"data"`
}

Envelope is the standard message format for routed messages

type FaultReporter added in v0.14.0

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

FaultReporter couples a fault set, the events:faults stream, and the per-group pubsub notification into a single primitive. Raise and Clear run as a server-side EVAL transaction: SADD/SREM, branch on the change count, and only emit XADD + PUBLISH on a real state transition. Set and stream cannot drift, even across process restarts or crashing callers.

func (*FaultReporter) All added in v0.14.0

func (fr *FaultReporter) All() ([]int, error)

All returns all fault codes currently in the set, parsed as ints. Returns an error if any member can't be parsed as an integer.

func (*FaultReporter) Clear added in v0.14.0

func (fr *FaultReporter) Clear(code int) error

Clear marks code as absent. Idempotent: returns nil and emits no stream entry if the code is not in the set.

func (*FaultReporter) Group added in v0.14.0

func (fr *FaultReporter) Group() string

Group returns the group name.

func (*FaultReporter) Has added in v0.14.0

func (fr *FaultReporter) Has(code int) (bool, error)

Has reports whether the given fault code is currently in the set.

func (*FaultReporter) Raise added in v0.14.0

func (fr *FaultReporter) Raise(code int, description string) error

Raise marks code as present. Idempotent: returns nil and emits no stream entry if the code is already in the set.

type FaultReporterOption added in v0.14.0

type FaultReporterOption func(*faultReporterOptions)

FaultReporterOption configures a FaultReporter.

func WithFaultChannel added in v0.14.0

func WithFaultChannel(ch string) FaultReporterOption

WithFaultChannel overrides the pubsub channel. Default: "<group>".

func WithFaultMaxLen added in v0.14.0

func WithFaultMaxLen(n int64) FaultReporterOption

WithFaultMaxLen overrides the stream MAXLEN. Default: 1000 (approximate).

func WithFaultPayload added in v0.14.0

func WithFaultPayload(p string) FaultReporterOption

WithFaultPayload overrides the pubsub payload. Default: "fault".

func WithFaultSetKey added in v0.14.0

func WithFaultSetKey(key string) FaultReporterOption

WithFaultSetKey overrides the fault set key. Default: "<group>:fault".

func WithFaultStream added in v0.14.0

func WithFaultStream(s string) FaultReporterOption

WithFaultStream overrides the events stream name. Default: "events:faults".

type FaultSet deprecated

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

FaultSet manages a Redis set of fault codes with pub/sub notification.

Deprecated: prefer FaultReporter, which couples the set, the events:faults stream, and the pubsub notification into one primitive so the stream and set cannot drift. FaultSet stays for callers that don't need the stream.

func (*FaultSet) Add

func (fs *FaultSet) Add(code int) error

Add adds a fault code to the set and publishes notification.

func (*FaultSet) All

func (fs *FaultSet) All() ([]string, error)

All returns all fault codes in the set.

func (*FaultSet) Clear

func (fs *FaultSet) Clear() error

Clear removes all fault codes and publishes notification.

func (*FaultSet) Has

func (fs *FaultSet) Has(code int) (bool, error)

Has checks if a fault code is present.

func (*FaultSet) Remove

func (fs *FaultSet) Remove(code int) error

Remove removes a fault code from the set and publishes notification.

func (*FaultSet) SetMany

func (fs *FaultSet) SetMany(codes []int) error

SetMany atomically adds/removes multiple codes. positive codes are added, negative codes are removed.

type HashPublisher

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

HashPublisher provides atomic HSET + PUBLISH with change detection. This matches the Librescoot pattern where: - State is stored in a Redis hash - Changes are notified via pub/sub with the field name as payload - Consumers fetch the value using HGET

func (*HashPublisher) Channel

func (hp *HashPublisher) Channel() string

Channel returns the channel name.

func (*HashPublisher) Clear added in v0.4.0

func (hp *HashPublisher) Clear(opts ...SetOption) error

Clear deletes the entire hash and publishes "cleared". By default, this is async (fire-and-forget). Use Sync() for confirmation. Use NoPublish() to skip publishing.

func (*HashPublisher) Delete added in v0.4.0

func (hp *HashPublisher) Delete(field string, opts ...SetOption) error

Delete removes a field from the hash and publishes the field name. By default, this is async (fire-and-forget). Use Sync() for confirmation. Use NoPublish() to skip publishing — useful when retiring a field that no consumer should be woken for.

func (*HashPublisher) Get

func (hp *HashPublisher) Get(field string) (string, error)

Get retrieves a field value from the hash.

func (*HashPublisher) GetAll

func (hp *HashPublisher) GetAll() (map[string]string, error)

GetAll retrieves all fields from the hash.

func (*HashPublisher) Hash

func (hp *HashPublisher) Hash() string

Hash returns the hash name.

func (*HashPublisher) ReplaceAll added in v0.4.0

func (hp *HashPublisher) ReplaceAll(fields map[string]any, opts ...SetOption) error

ReplaceAll atomically deletes all fields and sets new ones. This solves the race condition in DEL + HMSET + PUBLISH patterns. Publishes "replaced", or "cleared" if fields is nil or empty. By default, this is async (fire-and-forget). Use Sync() for confirmation. Use NoPublish() to skip publishing.

func (*HashPublisher) Set

func (hp *HashPublisher) Set(field string, value any, opts ...SetOption) error

Set atomically sets a hash field and publishes the field name. By default, this is async (fire-and-forget) for performance. Use Sync() option to wait for Redis confirmation. Use NoPublish() to skip publishing.

func (*HashPublisher) SetIfChanged

func (hp *HashPublisher) SetIfChanged(field string, value any) (bool, error)

SetIfChanged atomically sets a hash field and publishes only if the value changed. Returns true if the value was changed and published.

func (*HashPublisher) SetMany

func (hp *HashPublisher) SetMany(fields map[string]any, opts ...SetOption) error

SetMany atomically sets multiple fields and publishes each one. By default, this is async (fire-and-forget) for performance. Use Sync() option to wait for Redis confirmation. Use NoPublish() to skip publishing.

func (*HashPublisher) SetManyIfChanged

func (hp *HashPublisher) SetManyIfChanged(fields map[string]any) ([]string, error)

SetManyIfChanged atomically sets multiple fields, publishing only changed ones. Returns the list of fields that were actually changed.

func (*HashPublisher) SetManyPublishOne added in v0.5.0

func (hp *HashPublisher) SetManyPublishOne(fields map[string]any, notification string, opts ...SetOption) error

SetManyPublishOne sets multiple fields but publishes only a single notification. Useful for batch updates where you only want one notification at the end. The notification parameter is published as the message payload. By default, this is async (fire-and-forget). Use Sync() for confirmation. Use NoPublish() to skip the notification.

func (*HashPublisher) SetWithTimestamp

func (hp *HashPublisher) SetWithTimestamp(field string, value any, opts ...SetOption) error

SetWithTimestamp atomically sets a field, its timestamp, and publishes. By default, this is async (fire-and-forget). Use Sync() for confirmation. Use NoPublish() to skip publishing.

type HashWatcher

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

HashWatcher subscribes to a channel and fetches hash values on notification. This matches the Librescoot consumer pattern.

func OnFieldTyped

func OnFieldTyped[T any](hw *HashWatcher, field string, handler func(T) error) *HashWatcher

OnFieldTyped registers a typed handler for a specific field. The value is JSON-decoded into the type T.

func (*HashWatcher) Fetch

func (hw *HashWatcher) Fetch(field string) (string, error)

Fetch retrieves a specific field from the hash.

func (*HashWatcher) FetchAll

func (hw *HashWatcher) FetchAll() (map[string]string, error)

FetchAll retrieves all current values from the hash. Useful for initial state sync.

func (*HashWatcher) OnAny

func (hw *HashWatcher) OnAny(handler func(field, value string) error) *HashWatcher

OnAny registers a catch-all handler for any field not explicitly handled.

func (*HashWatcher) OnEvent added in v0.8.0

func (hw *HashWatcher) OnEvent(event string, handler func() error) *HashWatcher

OnEvent registers a handler for an event-only message. Unlike OnField, this does not fetch from the hash - it fires immediately when the message is received. Use for notifications that don't have corresponding hash state.

func (*HashWatcher) OnField

func (hw *HashWatcher) OnField(field string, handler func(value string) error) *HashWatcher

OnField registers a handler for a specific field. When a message is received with this field name, the handler is called with the fetched value from the hash.

func (*HashWatcher) SetDebounce

func (hw *HashWatcher) SetDebounce(d time.Duration) *HashWatcher

SetDebounce sets a debounce duration for handler calls. When set, rapid updates to the same field are coalesced - only the last value is passed to the handler after the quiet period expires.

func (*HashWatcher) Start

func (hw *HashWatcher) Start() error

Start begins listening for messages and fetching values.

func (*HashWatcher) StartWithSync

func (hw *HashWatcher) StartWithSync() error

StartWithSync subscribes, fetches current state, calls handlers, then processes messages. This ensures no messages are missed between initial fetch and subscribe. Order: Subscribe (messages buffer) → HGETALL → call handlers → process buffered messages

The subscription rides the client's shared pub/sub connection, but the ordering guarantee is unchanged and in fact stricter: subscribe() returns only once Redis has confirmed the SUBSCRIBE, and the gated subscriber buffers everything that arrives until release() below.

func (*HashWatcher) Stop

func (hw *HashWatcher) Stop() error

Stop stops the watcher. The underlying channel stays subscribed on the shared connection if another watcher in this process still wants it.

type JSONCodec

type JSONCodec struct{}

JSONCodec is the default codec using JSON

func (JSONCodec) Decode

func (JSONCodec) Decode(data []byte, v any) error

func (JSONCodec) Encode

func (JSONCodec) Encode(v any) ([]byte, error)

type Option

type Option func(*options)

Option configures the client

func WithAddress

func WithAddress(addr string) Option

WithAddress sets the Redis server address

func WithCodec

func WithCodec(codec Codec) Option

WithCodec sets the message codec (default: JSONCodec)

func WithDialTimeout

func WithDialTimeout(d time.Duration) Option

WithDialTimeout sets the connection timeout

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets a custom logger

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets the maximum number of retries for commands

func WithOnConnect

func WithOnConnect(fn func()) Option

WithOnConnect sets a callback invoked when connection is established

func WithOnDisconnect

func WithOnDisconnect(fn func(error)) Option

WithOnDisconnect sets a callback invoked when connection is lost

func WithPoolSize

func WithPoolSize(n int) Option

WithPoolSize sets the connection pool size

func WithPort

func WithPort(port int) Option

WithPort sets the Redis server port

func WithRetryInterval

func WithRetryInterval(d time.Duration) Option

WithRetryInterval sets the connection monitoring interval

func WithURL added in v0.5.0

func WithURL(rawURL string) Option

WithURL sets the Redis server address from a URL string. Supports formats: "redis://host:port", "host:port", or just "host". The redis:// scheme is optional. Port defaults to 6379 if not specified.

type QueueHandler

type QueueHandler[T any] struct {
	// contains filtered or unexported fields
}

QueueHandler processes messages from a Redis list

func HandleRequests

func HandleRequests[T any](c *Client, queue string, handler func(T) error) *QueueHandler[T]

HandleRequests creates a typed queue handler. The queue joins the client's shared BRPOP loop: every queue in the process is served by one blocking connection, with a dedicated goroutine per queue so a slow handler only stalls its own queue.

func (*QueueHandler[T]) Stop

func (qh *QueueHandler[T]) Stop()

Stop stops the queue handler

type Router

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

Router provides message-type based routing for a channel

func Handle

func Handle[T any](r *Router, msgType string, handler func(T) error) *Router

Handle registers a typed handler for a message type

func (*Router) Start

func (r *Router) Start() error

Start begins listening for messages

func (*Router) Stop

func (r *Router) Stop() error

Stop stops the router

type SetOption added in v0.5.0

type SetOption func(*setConfig)

SetOption configures Set/SetMany behavior

func NoPublish added in v0.5.0

func NoPublish() SetOption

NoPublish prevents publishing after the set operation. Use when you want to update state without notifying subscribers.

func Sync added in v0.9.0

func Sync() SetOption

Sync makes the operation synchronous, blocking until Redis confirms. By default, Set/SetMany are async (fire-and-forget) for performance. Use Sync() when you need to confirm the write succeeded.

type StreamConsumer

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

StreamConsumer consumes messages from a Redis stream using XREAD.

func StreamHandle

func StreamHandle[T any](sc *StreamConsumer, handler func(id string, msg T) error) *StreamConsumer

StreamHandle registers a typed handler for stream messages.

func (*StreamConsumer) Handle

func (sc *StreamConsumer) Handle(handler func(id string, values map[string]string) error) *StreamConsumer

Handle sets the message handler function.

func (*StreamConsumer) Running

func (sc *StreamConsumer) Running() bool

Running returns whether the consumer is currently running.

func (*StreamConsumer) Start

func (sc *StreamConsumer) Start(startID string) error

Start begins consuming messages from the stream. startID can be:

  • "$" to read only new messages
  • "0" to read from the beginning
  • A specific message ID to read after that ID
  • ">" for consumer groups (only undelivered messages)

func (*StreamConsumer) Stop

func (sc *StreamConsumer) Stop()

Stop stops the consumer (by cancelling the context passed to Start). Note: The consumer stops when the context passed to Start is cancelled.

type StreamConsumerOption

type StreamConsumerOption func(*streamConsumerOptions)

StreamConsumerOption configures a StreamConsumer

func WithBlockTimeout

func WithBlockTimeout(d time.Duration) StreamConsumerOption

WithBlockTimeout sets the XREAD block timeout

func WithConsumerGroup

func WithConsumerGroup(group, consumer string) StreamConsumerOption

WithConsumerGroup configures consumer group mode (XREADGROUP)

type StreamPublisher

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

StreamPublisher publishes messages to a Redis stream using XADD.

func (*StreamPublisher) Add

func (sp *StreamPublisher) Add(values map[string]any) (string, error)

Add publishes a message to the stream with auto-generated ID. Returns the generated message ID.

func (*StreamPublisher) AddWithID

func (sp *StreamPublisher) AddWithID(id string, values map[string]any) (string, error)

AddWithID publishes a message with a specific ID.

func (*StreamPublisher) Stream

func (sp *StreamPublisher) Stream() string

Stream returns the stream name.

type StreamPublisherOption

type StreamPublisherOption func(*streamPublisherOptions)

StreamPublisherOption configures a StreamPublisher

func WithMaxLen

func WithMaxLen(n int64) StreamPublisherOption

WithMaxLen sets the maximum stream length (MAXLEN option)

type StringCodec

type StringCodec struct{}

StringCodec passes strings through unchanged

func (StringCodec) Decode

func (StringCodec) Decode(data []byte, v any) error

func (StringCodec) Encode

func (StringCodec) Encode(v any) ([]byte, error)

type Subscription

type Subscription[T any] struct {
	// contains filtered or unexported fields
}

Subscription is a typed channel subscription

func Subscribe

func Subscribe[T any](c *Client, channel string, handler func(T) error) (*Subscription[T], error)

Subscribe creates or retrieves a typed subscription

func (*Subscription[T]) Unsubscribe

func (s *Subscription[T]) Unsubscribe() error

Unsubscribe stops the subscription

type Tx added in v0.11.0

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

Tx groups operations from multiple redis-ipc types into one atomic pipeline (MULTI/EXEC). Cache updates are deferred and only applied after a successful Exec.

func (*Tx) Exec added in v0.11.0

func (tx *Tx) Exec() error

Exec executes all queued operations atomically via MULTI/EXEC. On success, applies deferred cache updates. On failure, the cache is unchanged.

func (*Tx) FaultAdd added in v0.11.0

func (tx *Tx) FaultAdd(fs *FaultSet, code int)

FaultAdd queues SADD and PUBLISH for a fault code.

func (*Tx) FaultRemove added in v0.11.0

func (tx *Tx) FaultRemove(fs *FaultSet, code int)

FaultRemove queues SREM and PUBLISH for a fault code.

func (*Tx) FaultUpdate added in v0.11.0

func (tx *Tx) FaultUpdate(fs *FaultSet, adds, removes []int)

FaultUpdate queues a batch of adds and removes with a single PUBLISH.

func (*Tx) HashSetManyIfChanged added in v0.11.0

func (tx *Tx) HashSetManyIfChanged(hp *HashPublisher, fields map[string]any) []string

HashSetManyIfChanged queues HSET for all fields and PUBLISH for each changed field. Returns the list of fields that differ from the cached state. The cache is only updated if Exec succeeds.

func (*Tx) StreamAdd added in v0.11.0

func (tx *Tx) StreamAdd(sp *StreamPublisher, values map[string]any)

StreamAdd queues XADD for a stream message.

type TxGroup

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

TxGroup batches Redis commands for atomic execution

func (*TxGroup) Add

func (g *TxGroup) Add(cmd string, args ...interface{}) *TxGroup

Add queues a Redis command

func (*TxGroup) Exec

func (g *TxGroup) Exec() ([]interface{}, error)

Exec executes all queued commands and returns results

Jump to

Keyboard shortcuts

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