Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 32 additions & 6 deletions clients/ember-go/ember.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,10 +568,30 @@ type SubscribeEvent struct {
Pattern string // only set for pmessage
}

// Subscription holds the channels returned by [Client.Subscribe].
// Events are read from C; any stream error that ends the subscription
// is retrievable via Err() after C is closed.
type Subscription struct {
// C receives events until the stream ends or the context is cancelled.
C <-chan SubscribeEvent
errc <-chan error
}

// Err returns the first error that caused the subscription stream to close,
// or nil if the stream ended cleanly (context cancellation or EOF).
// Call Err only after C has been drained and closed.
func (s *Subscription) Err() error {
if err, ok := <-s.errc; ok {
return err
}
return nil
}

// Subscribe opens a server-streaming subscription for the given channels
// and/or patterns. Returns a channel that yields events until the context
// is cancelled or the stream ends.
func (c *Client) Subscribe(ctx context.Context, channels []string, patterns []string) (<-chan SubscribeEvent, error) {
// and/or patterns. Returns a Subscription whose C field yields events
// until the context is cancelled or the stream ends. Use sub.Err() after
// the channel closes to check whether the stream ended due to an error.
func (c *Client) Subscribe(ctx context.Context, channels []string, patterns []string) (*Subscription, error) {
stream, err := c.rpc.Subscribe(c.ctx(ctx), &pb.SubscribeRequest{
Channels: channels,
Patterns: patterns,
Expand All @@ -581,11 +601,17 @@ func (c *Client) Subscribe(ctx context.Context, channels []string, patterns []st
}

ch := make(chan SubscribeEvent, 64)
errc := make(chan error, 1)
go func() {
defer close(ch)
defer close(errc)
for {
evt, err := stream.Recv()
if err != nil {
evt, recvErr := stream.Recv()
if recvErr != nil {
if ctx.Err() == nil {
// stream closed unexpectedly — propagate the error
errc <- recvErr
}
return
}
se := SubscribeEvent{
Expand All @@ -604,7 +630,7 @@ func (c *Client) Subscribe(ctx context.Context, channels []string, patterns []st
}
}()

return ch, nil
return &Subscription{C: ch, errc: errc}, nil
}

// PubSubChannels returns active channel names, optionally filtered by pattern.
Expand Down
1 change: 1 addition & 0 deletions crates/ember-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ ember-protocol = { workspace = true }
ember-persistence = { workspace = true }
thiserror = { workspace = true }
bytes = { workspace = true }
metrics = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
rand = { workspace = true }
Expand Down
18 changes: 13 additions & 5 deletions crates/ember-core/src/shard/aof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,11 +574,19 @@ pub(super) fn broadcast_replication(
) {
if let Some(ref tx) = *replication_tx {
*replication_offset += 1;
let _ = tx.send(ReplicationEvent {
shard_id,
offset: *replication_offset,
record,
});
if tx
.send(ReplicationEvent {
shard_id,
offset: *replication_offset,
record,
})
.is_err()
{
// no replicas are currently connected — normal during startup
// or after a replica disconnects, but tracked so operators
// can alert on unexpected drops in a replicated deployment.
metrics::counter!("ember_replication_send_failures_total").increment(1);
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion crates/ember-core/src/shard/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ pub(super) fn handle_blocking_pop(

match result {
Ok(Some(data)) => {
// got an element — send to waiter and record the mutation
// got an element — send to waiter and record the mutation.
// try_send can only fail if the client disconnected between
// registering the waiter and the element arriving; safe to ignore.
let _ = waiter.try_send((key.to_owned(), data));
reply.send(ShardResponse::Ok);

Expand Down
Loading