eventbridge

package
v2.2.2 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

README

Event Bridge Package

The eventbridge package provides an event bus system for communication between AI Studio (control) and Microgateway (edge) nodes. It uses github.com/simonfxr/pubsub for in-process event buses and integrates with the existing gRPC bidirectional streaming infrastructure for cross-node event forwarding.

Architecture

┌──────────────────────────────────────┐     ┌──────────────────────────────────────┐
│         AI Studio (Control)          │     │        Microgateway (Edge)           │
│                                      │     │                                      │
│  ┌─────────────┐   ┌──────────┐     │     │     ┌──────────┐   ┌─────────────┐  │
│  │ Application │──▶│ EventBus │     │     │     │ EventBus │◀──│ Application │  │
│  └─────────────┘   └────┬─────┘     │     │     └────┬─────┘   └─────────────┘  │
│                         │           │     │          │                          │
│                    ┌────▼─────┐     │     │     ┌────▼─────┐                    │
│                    │  Bridge  │     │     │     │  Bridge  │                    │
│                    └────┬─────┘     │     │     └────┬─────┘                    │
│                         │           │     │          │                          │
│                    ┌────▼─────┐     │     │     ┌────▼─────┐                    │
│                    │  Stream  │◀───────────────▶│  Stream  │                    │
│                    │ Adapter  │     gRPC       │ Adapter  │                    │
│                    └──────────┘     │     │     └──────────┘                    │
└──────────────────────────────────────┘     └──────────────────────────────────────┘

Event Direction

Events are routed based on their Direction:

  • DirLocal (0): Events stay on the local bus, never forwarded to remote nodes
  • DirUp (1): Events flow from edge to control
  • DirDown (2): Events flow from control to edge(s)

Feedback Loop Prevention

The bridge prevents feedback loops through two mechanisms:

  1. Remote → Local conversion: Events received from remote are always republished locally with DirLocal, preventing re-forwarding
  2. Role-based filtering: Control only forwards DirDown events; edges only forward DirUp events

Usage

Publishing Events
// Get the event bus from control server or edge client
bus := controlServer.GetEventBus() // or edgeClient.GetEventBus()

// Publish a local-only event
eventbridge.PublishLocal(bus, "node-id", "internal.log", map[string]string{"msg": "test"})

// Publish an event from edge to control
eventbridge.PublishUp(bus, "edge-123", "metrics.report", MetricsData{CPU: 50})

// Publish an event from control to edges
eventbridge.PublishDown(bus, "control", "config.reload", ConfigUpdate{Version: "1.0"})
Subscribing to Events
bus := controlServer.GetEventBus()

// Subscribe to specific topic
sub := bus.Subscribe("metrics.report", func(ev eventbridge.Event) {
    var data MetricsData
    json.Unmarshal(ev.Payload, &data)
    fmt.Printf("Received metrics from %s: CPU=%d\n", ev.Origin, data.CPU)
})
defer bus.Unsubscribe(sub)

// Subscribe to all events
allSub := bus.SubscribeAll(func(ev eventbridge.Event) {
    fmt.Printf("Event: topic=%s origin=%s\n", ev.Topic, ev.Origin)
})
defer bus.Unsubscribe(allSub)

Components

Event

The canonical event type with routing metadata:

type Event struct {
    ID      string          // UUID for dedup/tracing
    Topic   string          // Logical topic name
    Origin  string          // Node ID that created the event
    Dir     Direction       // Routing direction
    Payload json.RawMessage // Application payload
}
Bus

Interface for the local event bus:

type Bus interface {
    Subscribe(topic string, fn func(Event)) *pubsub.Subscription
    SubscribeAll(fn func(Event)) *pubsub.Subscription
    Unsubscribe(sub *pubsub.Subscription)
    Publish(event Event)
}
Bridge

Connects a local bus to a remote node via gRPC:

bridge := eventbridge.NewBridge(eventbridge.BridgeConfig{
    NodeID:    "edge-001",
    IsControl: false,
    Topics:    []string{"metrics.*", "errors.*"}, // Optional topic filter
}, bus, streamAdapter)
bridge.Start(ctx)
defer bridge.Stop()
StreamAdapter

Adapts gRPC streams to the Bridge interface:

adapter := eventbridge.NewStreamAdapter(func(frame *eventbridge.EventFrame) error {
    return stream.Send(&pb.ControlMessage{
        Message: &pb.ControlMessage_Event{Event: protoFrame},
    })
}, 100) // buffer size

Integration Points

AI Studio (Control Server)

The ControlServer in grpc/control_server.go:

  • Creates an event bus on startup
  • Creates a bridge per edge connection
  • Handles EdgeMessage_Event messages from edges
  • Exposes GetEventBus() for other AI Studio components
Microgateway (Edge Client)

The SimpleEdgeClient in microgateway/internal/grpc/simple_client.go:

  • Creates an event bus on startup
  • Creates a bridge when connecting to control
  • Handles ControlMessage_Event messages from control
  • Exposes GetEventBus() for other Microgateway components

Proto Definition

Events are transmitted using EventFrame in proto/config_sync.proto:

message EventFrame {
  string id = 1;
  string topic = 2;
  string origin = 3;
  int32 dir = 4;    // 0 = Local, 1 = Up, 2 = Down
  bytes payload = 5;
}

Documentation

Overview

Package eventbridge provides an event bus system for communication between AI Studio (control) and Microgateway (edge) nodes using simonfxr/pubsub for in-process event buses and gRPC for cross-node event forwarding.

Index

Constants

View Source
const (
	// BudgetSyncTopic is used for budget synchronization between control and edge nodes
	BudgetSyncTopic = "budget.sync"
)

Well-known event topics for cross-node communication

Variables

This section is empty.

Functions

func PublishDown

func PublishDown(bus Bus, nodeID, topic string, payload interface{}) error

PublishDown is a helper to publish events from control to edges.

func PublishLocal

func PublishLocal(bus Bus, nodeID, topic string, payload interface{}) error

PublishLocal is a helper to publish local-only events that won't be forwarded.

func PublishUp

func PublishUp(bus Bus, nodeID, topic string, payload interface{}) error

PublishUp is a helper to publish events from edge to control.

Types

type Bridge

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

Bridge connects a local event bus to a gRPC stream, forwarding events based on their direction while preventing feedback loops.

The bridge runs two goroutines: - localToRemote: subscribes to local bus and forwards events over gRPC - remoteToLocal: reads from gRPC and republishes into local bus

Feedback loops are prevented by: 1. Remote events are always marked as DirLocal when republished 2. Outbound loop only forwards events matching the node's role:

  • Control node only forwards DirDown events
  • Edge nodes only forward DirUp events

func NewBridge

func NewBridge(config BridgeConfig, bus Bus, stream GRPCStream) *Bridge

NewBridge creates a new bridge instance. The bridge is not started until Start() is called.

func (*Bridge) IsRunning

func (b *Bridge) IsRunning() bool

IsRunning returns whether the bridge is currently running.

func (*Bridge) Start

func (b *Bridge) Start(ctx context.Context)

Start begins the bridge loops. This method is non-blocking. Call Stop() to halt the bridge.

func (*Bridge) Stop

func (b *Bridge) Stop()

Stop halts the bridge loops.

type BridgeConfig

type BridgeConfig struct {
	// NodeID is the identifier for this node ("control" or "edge-xxx")
	NodeID string
	// IsControl indicates if this is the control node (true) or an edge (false)
	IsControl bool
	// Topics is an optional list of topics to export. If empty, all topics are exported.
	Topics []string
}

BridgeConfig configures the bridge behavior.

type Bus

type Bus interface {
	// Subscribe registers a callback for events on a specific topic.
	// Returns a subscription that can be used to unsubscribe.
	Subscribe(topic string, fn func(Event)) *pubsub.Subscription

	// SubscribeAll registers a callback for all events regardless of topic.
	// Returns a subscription that can be used to unsubscribe.
	SubscribeAll(fn func(Event)) *pubsub.Subscription

	// Unsubscribe removes a subscription.
	Unsubscribe(sub *pubsub.Subscription)

	// Publish sends an event to all subscribers of the event's topic.
	Publish(event Event)
}

Bus is the interface for the local event bus. It provides publish/subscribe functionality for Event objects.

type Direction

type Direction int

Direction controls event routing and prevents feedback loops. Events are routed based on their direction: - DirLocal: Events stay on the local bus, never forwarded - DirUp: Events flow from edge to control - DirDown: Events flow from control to edge(s)

const (
	// DirLocal indicates events that should only be processed locally
	DirLocal Direction = iota
	// DirUp indicates events flowing from edge to control
	DirUp
	// DirDown indicates events flowing from control to edge(s)
	DirDown
)

func DirectionFromInt32

func DirectionFromInt32(d int32) Direction

DirectionFromInt32 converts a protobuf int32 to Direction.

func (Direction) String

func (d Direction) String() string

String returns a human-readable representation of the Direction.

type Event

type Event struct {
	// ID is a UUID for deduplication and tracing
	ID string `json:"id"`

	// Topic is the logical topic name (e.g., "config.update", "metrics.report")
	Topic string `json:"topic"`

	// Origin is the node ID that created the event (e.g., "control" or "edge-123")
	Origin string `json:"origin"`

	// Dir controls routing: DirLocal stays local, DirUp goes to control, DirDown goes to edges
	Dir Direction `json:"dir"`

	// Payload is the application-specific data as JSON
	Payload json.RawMessage `json:"payload"`

	// RelayedFrom is set on a control-plane replica when the event was
	// published on another replica and relayed through the cluster event
	// log (pkg/cluster.Relay); it names that replica. It is local metadata:
	// never serialized, never sent to edges or plugins. Consumers that act
	// once for the whole cluster (a checksum recompute, say) skip relayed
	// events; consumers that keep per-replica state act on them.
	RelayedFrom string `json:"-"`

	// FromEdge is set on a control plane for an event one of its edges sent
	// over its stream. The bridge republishes those as DirLocal (so they are
	// never forwarded again), which leaves this the only way to tell them
	// from events published on the control plane itself. A headless control
	// plane relays them to the replicas that host plugins. Local metadata,
	// like RelayedFrom: never serialized.
	FromEdge bool `json:"-"`
}

Event is the canonical event type used on the local bus and for gRPC transport. All application code publishes and subscribes using this Event type.

func NewEvent

func NewEvent(topic, origin string, dir Direction, payload interface{}) (Event, error)

NewEvent creates a new event with a generated UUID.

func NewEventRaw

func NewEventRaw(topic, origin string, dir Direction, payload json.RawMessage) Event

NewEventRaw creates a new event with pre-marshaled JSON payload.

func (*Event) ToEventFrame

func (e *Event) ToEventFrame() *EventFrame

ToEventFrame converts an Event to an EventFrame for wire transmission.

type EventFrame

type EventFrame struct {
	ID      string
	Topic   string
	Origin  string
	Dir     int32 // 0 = Local, 1 = Up, 2 = Down (matches Direction enum)
	Payload []byte
}

EventFrame is the wire format for events over gRPC. This mirrors the protobuf EventFrame message structure.

func (*EventFrame) ToEvent

func (f *EventFrame) ToEvent() Event

ToEvent converts an EventFrame to an Event with DirLocal direction. This is used when receiving events from remote nodes to prevent re-forwarding.

type EventFrameCreator

type EventFrameCreator func(id, topic, origin string, dir int32, payload []byte) interface{}

EventFrameCreator is a function type for creating proto EventFrame objects

type GRPCStream

type GRPCStream interface {
	// SendEvent sends an event frame to the remote end
	SendEvent(frame *EventFrame) error
	// RecvEvent receives an event frame from the remote end (blocking)
	RecvEvent() (*EventFrame, error)
}

GRPCStream is the interface for sending/receiving event frames over gRPC. This abstraction allows the bridge to work with different stream implementations.

type ProtoEventFrame

type ProtoEventFrame interface {
	GetId() string
	GetTopic() string
	GetOrigin() string
	GetDir() int32
	GetPayload() []byte
}

ProtoEventFrame is an interface that matches the generated pb.EventFrame This allows the adapter to work with the proto types without importing them directly

type PubSubBus

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

PubSubBus wraps simonfxr/pubsub as a Bus implementation. It is safe for concurrent use.

func NewBus

func NewBus() *PubSubBus

NewBus creates a new PubSubBus instance.

func (*PubSubBus) Publish

func (b *PubSubBus) Publish(event Event)

Publish sends an event to all subscribers of the event's topic and to all wildcard subscribers.

func (*PubSubBus) Subscribe

func (b *PubSubBus) Subscribe(topic string, fn func(Event)) *pubsub.Subscription

Subscribe registers a callback for events on a specific topic.

func (*PubSubBus) SubscribeAll

func (b *PubSubBus) SubscribeAll(fn func(Event)) *pubsub.Subscription

SubscribeAll registers a callback for all events. This is implemented by subscribing to a special wildcard topic that receives all published events.

func (*PubSubBus) Unsubscribe

func (b *PubSubBus) Unsubscribe(sub *pubsub.Subscription)

Unsubscribe removes a subscription.

type StreamAdapter

type StreamAdapter struct {
	// SendFunc is called to send an event frame over gRPC
	// The frame parameter is an *EventFrame from this package
	SendFunc func(frame *EventFrame) error
	// contains filtered or unexported fields
}

StreamAdapter wraps a gRPC bidirectional stream to implement GRPCStream. It provides a channel-based interface for receiving events that can be populated from the main message processing loop.

func NewStreamAdapter

func NewStreamAdapter(sendFunc func(frame *EventFrame) error, bufferSize int) *StreamAdapter

NewStreamAdapter creates a new StreamAdapter. bufferSize controls the channel buffer for incoming events.

func (*StreamAdapter) Close

func (a *StreamAdapter) Close()

Close closes the event channel, causing RecvEvent to return an error.

func (*StreamAdapter) EnqueueEvent

func (a *StreamAdapter) EnqueueEvent(frame *EventFrame) bool

EnqueueEvent adds an event to the receive channel from the main message loop. This should be called when an event message is received on the gRPC stream. Returns false if the adapter is closed or the channel is full.

func (*StreamAdapter) EnqueueProtoEvent

func (a *StreamAdapter) EnqueueProtoEvent(protoFrame ProtoEventFrame) bool

EnqueueProtoEvent converts a proto EventFrame to our EventFrame and enqueues it. This is a convenience method for use in message processing loops.

func (*StreamAdapter) IsClosed

func (a *StreamAdapter) IsClosed() bool

IsClosed returns whether the adapter has been closed.

func (*StreamAdapter) RecvEvent

func (a *StreamAdapter) RecvEvent() (*EventFrame, error)

RecvEvent implements GRPCStream.RecvEvent by reading from the event channel. This blocks until an event is available or the adapter is closed.

func (*StreamAdapter) SendEvent

func (a *StreamAdapter) SendEvent(frame *EventFrame) error

SendEvent implements GRPCStream.SendEvent by calling the configured SendFunc.

Jump to

Keyboard shortcuts

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