package realtime import ( "context" "errors" "log/slog" "github.com/oklog/ulid/v2" ) // ManagedRelay is a Redis-backed realtime relay with explicit goroutine // lifecycle management. type ManagedRelay interface { RelayPublisher Broadcaster NodeID() string Start(context.Context) Stop() Wait() } // MirroredRelay is a temporary rollout helper: it starts two relay backends, // reads from both, and publishes every event to both with the same event id. // Client-side dedup keeps loopback delivery idempotent. type MirroredRelay struct { primary ManagedRelay mirror ManagedRelay } func NewMirroredRelay(primary, mirror ManagedRelay) *MirroredRelay { return &MirroredRelay{primary: primary, mirror: mirror} } func (r *MirroredRelay) NodeID() string { return r.primary.NodeID() } func (r *MirroredRelay) SetDaemonRuntimeDeliverer(d DaemonRuntimeDeliverer) { if setter, ok := r.primary.(interface{ SetDaemonRuntimeDeliverer(DaemonRuntimeDeliverer) }); ok { setter.SetDaemonRuntimeDeliverer(d) } if setter, ok := r.mirror.(interface{ SetDaemonRuntimeDeliverer(DaemonRuntimeDeliverer) }); ok { setter.SetDaemonRuntimeDeliverer(d) } } func (r *MirroredRelay) Start(ctx context.Context) { r.primary.Start(ctx) r.mirror.Start(ctx) M.NodeID.Store(r.NodeID()) } func (r *MirroredRelay) Stop() { r.primary.Stop() r.mirror.Stop() } func (r *MirroredRelay) Wait() { r.primary.Wait() r.mirror.Wait() } func (r *MirroredRelay) BroadcastToScope(scopeType, scopeID string, message []byte) { _ = r.PublishWithID(scopeType, scopeID, "", message, ulid.Make().String()) } func (r *MirroredRelay) BroadcastToWorkspace(workspaceID string, message []byte) { r.BroadcastToScope(ScopeWorkspace, workspaceID, message) } func (r *MirroredRelay) SendToUser(userID string, message []byte, excludeWorkspace ...string) { exclude := "" if len(excludeWorkspace) > 0 { exclude = excludeWorkspace[0] } _ = r.PublishWithID(ScopeUser, userID, exclude, message, ulid.Make().String()) } func (r *MirroredRelay) Broadcast(message []byte) { _ = r.PublishWithID("global", "all", "", message, ulid.Make().String()) } func (r *MirroredRelay) PublishWithID(scopeType, scopeID, exclude string, frame []byte, id string) error { primaryErr := r.primary.PublishWithID(scopeType, scopeID, exclude, frame, id) if scopeType == ScopeDaemonRuntime { return primaryErr } mirrorErr := r.mirror.PublishWithID(scopeType, scopeID, exclude, frame, id) if primaryErr != nil { M.RedisMirrorPrimaryErrors.Add(1) slog.Warn("realtime/redis mirror: primary publish failed", "error", primaryErr, "scope", scopeType, "scope_id", scopeID, "event_id", id) } if mirrorErr != nil { M.RedisMirrorSecondaryErrors.Add(1) slog.Warn("realtime/redis mirror: secondary publish failed", "error", mirrorErr, "scope", scopeType, "scope_id", scopeID, "event_id", id) } if (primaryErr == nil) != (mirrorErr == nil) { M.RedisMirrorDivergenceTotal.Add(1) slog.Warn( "realtime/redis mirror: divergent publish result", "primary_error", primaryErr, "secondary_error", mirrorErr, "scope", scopeType, "scope_id", scopeID, "event_id", id, ) } return errors.Join(primaryErr, mirrorErr) } var _ ManagedRelay = (*RedisRelay)(nil) var _ ManagedRelay = (*ShardedStreamRelay)(nil) var _ ManagedRelay = (*MirroredRelay)(nil)