Add namespace event filtering support
All checks were successful
CI / build (pull_request) Successful in 21s
All checks were successful
CI / build (pull_request) Successful in 21s
Add SubscriptionFilter type and SubscribeWithFilter method to enable filtering events by type and actor pattern within namespace subscriptions. - SubscriptionFilter supports event type filtering (e.g., only "OrderPlaced") - SubscriptionFilter supports actor ID prefix patterns (e.g., "order-*") - Filters are combined with AND logic - NATSEventBus uses NATS subjects for server-side filtering when possible - Comprehensive test coverage for filtering functionality Closes #21 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,19 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// NATSEventBus is an EventBus that broadcasts events across all cluster nodes using NATS
|
||||
// NATSEventBus is an EventBus that broadcasts events across all cluster nodes using NATS.
|
||||
//
|
||||
// # Server-Side Filtering
|
||||
//
|
||||
// When using SubscribeWithFilter, the NATSEventBus attempts to apply filters at
|
||||
// the NATS subject level where possible for efficient event delivery:
|
||||
//
|
||||
// Event type filtering: When a filter specifies exactly one event type,
|
||||
// NATSEventBus subscribes to a type-specific NATS subject (e.g.,
|
||||
// "aether.events.namespace.OrderPlaced"), reducing network traffic.
|
||||
//
|
||||
// For multiple event types or actor patterns, filtering is applied client-side
|
||||
// after receiving events from NATS.
|
||||
type NATSEventBus struct {
|
||||
*EventBus // Embed base EventBus for local subscriptions
|
||||
nc *nats.Conn // NATS connection
|
||||
@@ -47,28 +59,54 @@ func NewNATSEventBus(nc *nats.Conn) (*NATSEventBus, error) {
|
||||
return neb, nil
|
||||
}
|
||||
|
||||
// Subscribe creates a local subscription and ensures NATS subscription exists for the namespace
|
||||
// Subscribe creates a local subscription and ensures NATS subscription exists for the namespace.
|
||||
// All events published to the namespace will be delivered.
|
||||
func (neb *NATSEventBus) Subscribe(namespaceID string) <-chan *Event {
|
||||
return neb.SubscribeWithFilter(namespaceID, SubscriptionFilter{})
|
||||
}
|
||||
|
||||
// SubscribeWithFilter creates a filtered subscription for a namespace.
|
||||
//
|
||||
// For single event type filters, NATS subject-based filtering is used for
|
||||
// efficiency (server-side filtering). For multiple event types or actor
|
||||
// patterns, filtering is applied client-side.
|
||||
func (neb *NATSEventBus) SubscribeWithFilter(namespaceID string, filter SubscriptionFilter) <-chan *Event {
|
||||
neb.mutex.Lock()
|
||||
defer neb.mutex.Unlock()
|
||||
|
||||
// Create local subscription first
|
||||
ch := neb.EventBus.Subscribe(namespaceID)
|
||||
// Create local subscription with filter
|
||||
ch := neb.EventBus.SubscribeWithFilter(namespaceID, filter)
|
||||
|
||||
// Determine which NATS subject(s) to subscribe to
|
||||
// For single event type, we can use a more specific subject for server-side filtering
|
||||
var subjects []string
|
||||
if len(filter.EventTypes) == 1 {
|
||||
// Server-side filtering: subscribe to type-specific subject
|
||||
subjects = []string{fmt.Sprintf("aether.events.%s.%s", namespaceID, filter.EventTypes[0])}
|
||||
} else if len(filter.EventTypes) > 1 {
|
||||
// Subscribe to each event type's subject for server-side filtering
|
||||
for _, et := range filter.EventTypes {
|
||||
subjects = append(subjects, fmt.Sprintf("aether.events.%s.%s", namespaceID, et))
|
||||
}
|
||||
} else {
|
||||
// No event type filter - subscribe to wildcard for all events in namespace
|
||||
subjects = []string{fmt.Sprintf("aether.events.%s.>", namespaceID)}
|
||||
}
|
||||
|
||||
// Check if this is the first subscriber for this namespace
|
||||
count := neb.namespaceSubscribers[namespaceID]
|
||||
if count == 0 {
|
||||
// First subscriber - create NATS subscription
|
||||
subject := fmt.Sprintf("aether.events.%s", namespaceID)
|
||||
|
||||
sub, err := neb.nc.Subscribe(subject, func(msg *nats.Msg) {
|
||||
neb.handleNATSEvent(msg)
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[NATSEventBus] Failed to subscribe to NATS subject %s: %v", subject, err)
|
||||
} else {
|
||||
neb.subscriptions = append(neb.subscriptions, sub)
|
||||
log.Printf("[NATSEventBus] Node %s subscribed to %s", neb.nodeID, subject)
|
||||
// First subscriber - create NATS subscriptions
|
||||
for _, subject := range subjects {
|
||||
sub, err := neb.nc.Subscribe(subject, func(msg *nats.Msg) {
|
||||
neb.handleNATSEvent(msg)
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[NATSEventBus] Failed to subscribe to NATS subject %s: %v", subject, err)
|
||||
} else {
|
||||
neb.subscriptions = append(neb.subscriptions, sub)
|
||||
log.Printf("[NATSEventBus] Node %s subscribed to %s", neb.nodeID, subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,17 +147,19 @@ func (neb *NATSEventBus) handleNATSEvent(msg *nats.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Forward to local EventBus subscribers
|
||||
// Forward to local EventBus subscribers (filtering happens there)
|
||||
neb.EventBus.Publish(eventMsg.NamespaceID, eventMsg.Event)
|
||||
}
|
||||
|
||||
// Publish publishes an event both locally and to NATS for cross-node broadcasting
|
||||
// Publish publishes an event both locally and to NATS for cross-node broadcasting.
|
||||
// Events are published to a type-specific subject to enable server-side filtering.
|
||||
func (neb *NATSEventBus) Publish(namespaceID string, event *Event) {
|
||||
// First publish locally
|
||||
neb.EventBus.Publish(namespaceID, event)
|
||||
|
||||
// Then publish to NATS for other nodes
|
||||
subject := fmt.Sprintf("aether.events.%s", namespaceID)
|
||||
// Use type-specific subject for server-side filtering
|
||||
subject := fmt.Sprintf("aether.events.%s.%s", namespaceID, event.EventType)
|
||||
|
||||
eventMsg := eventMessage{
|
||||
NodeID: neb.nodeID,
|
||||
|
||||
Reference in New Issue
Block a user