All checks were successful
CI / build (pull_request) Successful in 20s
- Add UpdateVersionCache method to JetStreamEventStore for cache synchronization - Add SubscribeToEventStored convenience helper to NATSEventBus - Create integration tests for cross-node broadcasting scenarios - Add example demonstrating NATSEventBus + JetStreamEventStore integration
317 lines
8.5 KiB
Go
317 lines
8.5 KiB
Go
//go:build integration
|
|
// +build integration
|
|
|
|
package examples
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"git.flowmade.one/flowmade-one/aether"
|
|
"git.flowmade.one/flowmade-one/aether/store"
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// CrossNodeBroadcasting demonstrates how to implement cross-node event broadcasting
|
|
// using NATSEventBus with JetStreamEventStore. This example shows how events persist
|
|
// to JetStream and are then broadcast to other nodes in the cluster via NATS.
|
|
//
|
|
// Key Concepts:
|
|
// 1. NATSEventBus wraps EventBus to add NATS publishing
|
|
// 2. JetStreamEventStore with broadcaster publishes EventStored to NATS
|
|
// 3. Other nodes receive these events via NATS subscription
|
|
// 4. Version cache is updated on remote events to maintain consistency
|
|
//
|
|
// Usage:
|
|
// go run examples/cross_node_broadcasting.go
|
|
func CrossNodeBroadcastingExample() {
|
|
// Connect to NATS
|
|
nc, err := nats.Connect(nats.DefaultURL)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to NATS: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
// Create NATS event bus (this will broadcast to all nodes)
|
|
natsBus, err := aether.NewNATSEventBus(nc)
|
|
if err != nil {
|
|
log.Fatalf("Failed to create NATS event bus: %v", err)
|
|
}
|
|
defer natsBus.Stop()
|
|
|
|
// Create JetStream event store WITH broadcaster
|
|
// This enables EventStored events to be published to NATS
|
|
store, err := store.NewJetStreamEventStoreWithBroadcaster(
|
|
nc,
|
|
"events",
|
|
natsBus,
|
|
"tenant-abc", // Optional namespace for isolation
|
|
)
|
|
if err != nil {
|
|
log.Fatalf("Failed to create event store: %v", err)
|
|
}
|
|
|
|
// Subscribe to EventStored events to update version cache
|
|
// This keeps the version cache synchronized across nodes
|
|
eventStoredCh := natsBus.SubscribeWithFilter(
|
|
"tenant-abc",
|
|
&aether.SubscriptionFilter{
|
|
EventTypes: []string{aether.EventTypeEventStored},
|
|
},
|
|
)
|
|
|
|
go func() {
|
|
for event := range eventStoredCh {
|
|
actorID := event.Data["actorId"].(string)
|
|
version := int64(event.Data["version"].(float64))
|
|
store.UpdateVersionCache(actorID, version)
|
|
}
|
|
}()
|
|
|
|
// Now save an event - it will be:
|
|
// 1. Persisted to JetStream
|
|
// 2. Published to NATS as EventStored
|
|
// 3. Received by other nodes via NATS
|
|
// 4. Used to update version cache
|
|
|
|
event := &aether.Event{
|
|
ID: "event-1",
|
|
EventType: "OrderPlaced",
|
|
ActorID: "order-123",
|
|
Version: 1,
|
|
Data: map[string]interface{}{
|
|
"total": 100.00,
|
|
"item": "widget",
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
err = store.SaveEvent(event)
|
|
if err != nil {
|
|
log.Fatalf("Failed to save event: %v", err)
|
|
}
|
|
|
|
log.Println("Event saved to JetStream and broadcast to NATS")
|
|
|
|
// Other nodes in the cluster will receive this event via NATS
|
|
// and update their version cache accordingly
|
|
|
|
// Subscribe to events in this namespace
|
|
eventCh := natsBus.Subscribe("tenant-abc")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
select {
|
|
case receivedEvent := <-eventCh:
|
|
log.Printf("Received event via NATS: %s (version %d)",
|
|
receivedEvent.EventType, receivedEvent.Version)
|
|
case <-ctx.Done():
|
|
log.Println("Timeout waiting for event")
|
|
}
|
|
}
|
|
|
|
// CrossNodeBroadcastingMultiNode demonstrates a multi-node cluster setup.
|
|
// This simulates multiple nodes connecting to the same NATS cluster.
|
|
// In production, these would be separate processes/machines.
|
|
//
|
|
// This example requires a running NATS server with JetStream enabled.
|
|
func CrossNodeBroadcastingMultiNode() {
|
|
// Connect to NATS
|
|
nc, err := nats.Connect(nats.DefaultURL)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to NATS: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
nodeID := "node-1"
|
|
log.Printf("Starting %s", nodeID)
|
|
|
|
// Each node creates its own NATS event bus and event store
|
|
natsBus, err := aether.NewNATSEventBus(nc)
|
|
if err != nil {
|
|
log.Fatalf("Failed to create NATS event bus: %v", err)
|
|
}
|
|
defer natsBus.Stop()
|
|
|
|
store, err := store.NewJetStreamEventStoreWithBroadcaster(
|
|
nc,
|
|
"events",
|
|
natsBus,
|
|
"tenant-abc",
|
|
)
|
|
if err != nil {
|
|
log.Fatalf("Failed to create event store: %v", err)
|
|
}
|
|
|
|
// Setup EventStored subscription for cache synchronization
|
|
eventStoredCh := natsBus.SubscribeWithFilter(
|
|
"tenant-abc",
|
|
&aether.SubscriptionFilter{
|
|
EventTypes: []string{aether.EventTypeEventStored},
|
|
},
|
|
)
|
|
|
|
go func() {
|
|
for event := range eventStoredCh {
|
|
actorID := event.Data["actorId"].(string)
|
|
version := int64(event.Data["version"].(float64))
|
|
store.UpdateVersionCache(actorID, version)
|
|
log.Printf("[%s] Received EventStored for %s v%d", nodeID, actorID, version)
|
|
}
|
|
}()
|
|
|
|
// Subscribe to actual events
|
|
eventCh := natsBus.Subscribe("tenant-abc")
|
|
log.Printf("[%s] Subscribed to tenant-abc", nodeID)
|
|
|
|
// Save an event
|
|
savedEvent := &aether.Event{
|
|
ID: fmt.Sprintf("event-%s-1", nodeID),
|
|
EventType: "OrderPlaced",
|
|
ActorID: "order-123",
|
|
Version: 1,
|
|
Data: map[string]interface{}{
|
|
"node": nodeID,
|
|
"total": 100.00,
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
err = store.SaveEvent(savedEvent)
|
|
if err != nil {
|
|
log.Fatalf("[%s] Failed to save event: %v", nodeID, err)
|
|
}
|
|
|
|
log.Printf("[%s] Saved event to JetStream", nodeID)
|
|
|
|
// Wait to receive the event (either from local or remote)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
select {
|
|
case receivedEvent := <-eventCh:
|
|
log.Printf("[%s] Received: %s v%d from %s",
|
|
nodeID,
|
|
receivedEvent.EventType,
|
|
receivedEvent.Version,
|
|
receivedEvent.GetCorrelationID(),
|
|
)
|
|
case <-ctx.Done():
|
|
log.Printf("[%s] Timeout waiting for event", nodeID)
|
|
}
|
|
}
|
|
|
|
// DistributedOrderProcessing demonstrates a realistic scenario where
|
|
// multiple nodes process orders for the same actor. This shows how
|
|
// cross-node broadcasting ensures consistency.
|
|
func DistributedOrderProcessing() {
|
|
// Connect to NATS
|
|
nc, err := nats.Connect(nats.DefaultURL)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to NATS: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
// Node 1: Order creation
|
|
node1Bus, _ := aether.NewNATSEventBus(nc)
|
|
node1Store, _ := store.NewJetStreamEventStoreWithBroadcaster(
|
|
nc, "events", node1Bus, "orders",
|
|
)
|
|
defer node1Bus.Stop()
|
|
|
|
// Setup EventStored subscription for Node 1
|
|
eventStoredCh1 := node1Bus.SubscribeWithFilter(
|
|
"orders",
|
|
&aether.SubscriptionFilter{
|
|
EventTypes: []string{aether.EventTypeEventStored},
|
|
},
|
|
)
|
|
go func() {
|
|
for event := range eventStoredCh1 {
|
|
actorID := event.Data["actorId"].(string)
|
|
version := int64(event.Data["version"].(float64))
|
|
node1Store.UpdateVersionCache(actorID, version)
|
|
}
|
|
}()
|
|
|
|
// Node 2: Order processing (different node)
|
|
node2Bus, _ := aether.NewNATSEventBus(nc)
|
|
node2Store, _ := store.NewJetStreamEventStoreWithBroadcaster(
|
|
nc, "events", node2Bus, "orders",
|
|
)
|
|
defer node2Bus.Stop()
|
|
|
|
// Setup EventStored subscription for Node 2
|
|
eventStoredCh2 := node2Bus.SubscribeWithFilter(
|
|
"orders",
|
|
&aether.SubscriptionFilter{
|
|
EventTypes: []string{aether.EventTypeEventStored},
|
|
},
|
|
)
|
|
go func() {
|
|
for event := range eventStoredCh2 {
|
|
actorID := event.Data["actorId"].(string)
|
|
version := int64(event.Data["version"].(float64))
|
|
node2Store.UpdateVersionCache(actorID, version)
|
|
}
|
|
}()
|
|
|
|
// Node 2 also subscribes to events to receive updates
|
|
node2EventCh := node2Bus.Subscribe("orders")
|
|
|
|
// Node 1 creates an order
|
|
orderPlaced := &aether.Event{
|
|
ID: "order-created",
|
|
EventType: "OrderPlaced",
|
|
ActorID: "order-456",
|
|
Version: 1,
|
|
Data: map[string]interface{}{
|
|
"total": 100.00,
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := node1Store.SaveEvent(orderPlaced); err != nil {
|
|
log.Fatalf("Failed to create order: %v", err)
|
|
}
|
|
log.Println("Node 1: Created order-456")
|
|
|
|
// Node 2 receives the OrderPlaced event via NATS
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
select {
|
|
case event := <-node2EventCh:
|
|
log.Printf("Node 2: Received %s v%d", event.EventType, event.Version)
|
|
|
|
// Node 2 processes the order (must use version 2)
|
|
orderProcessed := &aether.Event{
|
|
ID: "order-processed",
|
|
EventType: "OrderProcessed",
|
|
ActorID: "order-456",
|
|
Version: 2, // Must be > 1
|
|
Data: map[string]interface{}{
|
|
"status": "shipped",
|
|
},
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
if err := node2Store.SaveEvent(orderProcessed); err != nil {
|
|
log.Fatalf("Node 2: Failed to process order: %v", err)
|
|
}
|
|
log.Println("Node 2: Processed order-456")
|
|
|
|
case <-ctx.Done():
|
|
log.Println("Node 2: Timeout waiting for order event")
|
|
}
|
|
|
|
// Verify event stream consistency
|
|
events, err := node1Store.GetEvents("order-456", 0)
|
|
if err != nil {
|
|
log.Fatalf("Failed to get events: %v", err)
|
|
}
|
|
log.Printf("Node 1: Event stream has %d events", len(events))
|
|
} |