├── README.md ├── actor ├── actor.go └── spec.go ├── aggregate ├── aggregate.go ├── aggregate_test.go ├── command.go ├── command_test.go ├── id.go ├── nodeID.go └── resource_type.go ├── aggregate_actor.go ├── command ├── .gitignore └── command.go ├── event ├── command_done.go ├── event.go └── service_ready.go ├── internals.go ├── magicbus.go ├── magicbus_test.go ├── query └── query.go ├── remote.go ├── repository └── repository.go └── subscribe.go /README.md: -------------------------------------------------------------------------------- 1 | # MagicBus 2 | 3 | > Every day I get in the queue 4 | > -- _The Who, Magic Bus_ 5 | 6 | ## 1. What it is 7 | 8 | MagicBus is a _local command/event bus_, which can be extended to a distributed messaging system by inter-connecting remote buses. 9 | 10 | The idea is that Aggregates (Domain Driven Design term for subsystem) can simply _"plug in"_ and have their events and commands delivered them in _serialized fashion_. 11 | 12 | The bus ***solves the problem of handling concurrency in a concurrent, asynchronous messaging system***. 13 | 14 | This occurs in situations where Aggregates have to be able respond to multiple concurrent events or commands. 15 | 16 | The _concurrency handling_ is employs the [Actor Model](https://www.youtube.com/watch?v=7erJ1DV_Tlo), which is an evolving [Go pattern](https://www.slideshare.net/weaveworks/an-actor-model-in-go-65174438). The bus itself is an Actor, as is every Aggregate that plugs into the bus. 17 | 18 | The bus also implements [Command Query Response Segregation](https://www.youtube.com/watch?v=whCk1Q87_ZI): 19 | - _commands/events_ are handled by the Aggregate directly, 20 | - _queries_ (read model) have to be submitted to a [repository](repository/repository.go), 21 | - Aggregate and Repository share no code or data; the only way the Repository can be updated is via events sent from the Aggregate. 22 | 23 | 24 | ## 2. Implementation status 25 | 26 | The bus itself is ready to use - see the test code. For integration, you probably need to fork and customize the generic sketches in the code to your specific needs. 27 | 28 | ![Magic Bus](https://i.redd.it/wday28h1ruhy.jpg) 29 | -------------------------------------------------------------------------------- /actor/actor.go: -------------------------------------------------------------------------------- 1 | package actor 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/Sirupsen/logrus" 9 | "github.com/eapache/channels" 10 | "github.com/grrtrr/magicbus/aggregate" 11 | "github.com/grrtrr/magicbus/event" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // GLOBAL VARIABLES 16 | var logger = logrus.WithField("module", "actor") 17 | 18 | // ErrShutdown is returned if the actor is no longer accepting events/commands 19 | var ErrShutdown = errors.New("processing loop terminated") 20 | 21 | // New instantiates a new actor in running state 22 | // @ctx: top-level cancellation context 23 | // @cmdHdlr: called when a Command arrives on the Command Channel 24 | // @evtHdlr: called when an Event arrives on the Event Channel 25 | // @ready: whether @cmdHdlr is ready to run immediately - can be unblocked via ServiceReady{} event 26 | func New(ctx context.Context, cmdHdlr func(*aggregate.Command), evtHdlr func(event.Event), ready bool) Actor { 27 | var a = &actor{ 28 | refcnt: 1, // new instances always start with a reference count of 1 29 | actionChan: make(chan func()), 30 | errChan: make(chan error), 31 | eventChan: channels.NewInfiniteChannel(), 32 | commandChan: channels.NewInfiniteChannel(), 33 | } 34 | a.ctx, a.cancel = context.WithCancel(ctx) 35 | 36 | if evtHdlr == nil { 37 | panic("attempt to create Actor with nil Event Handler") 38 | } else if cmdHdlr == nil { 39 | panic("attempt to create Actor with nil Command Handler") 40 | } 41 | go a.loop(cmdHdlr, evtHdlr, ready) 42 | 43 | return a 44 | } 45 | 46 | // actor is the internal implementation that provides the Actor interface 47 | type actor struct { 48 | // Incoming commands 49 | commandChan *channels.InfiniteChannel 50 | 51 | // Incoming events 52 | eventChan *channels.InfiniteChannel 53 | 54 | // Single action channel to process actions addressed to the Actor itself 55 | actionChan chan func() 56 | 57 | // refcnt atomically counts the number of references to this object 58 | refcnt uint32 59 | 60 | // ctx is the cancellation context, which allows to terminate the loop 61 | // cancel is the cancellation function to terminate @ctx 62 | ctx context.Context 63 | cancel context.CancelFunc 64 | 65 | // Any internal errors are published onto the error channel 66 | errChan chan error 67 | } 68 | 69 | // Publish publishes @e onto the Event Bus of @a. 70 | func (a *actor) Publish(e event.Event) error { 71 | if !a.IsActive() { 72 | return ErrShutdown 73 | } 74 | a.eventChan.In() <- e 75 | return nil 76 | } 77 | 78 | // Submit submits @c onto the Command Bus of @a. 79 | func (a *actor) Submit(c *aggregate.Command) error { 80 | if c == nil { 81 | return errors.Errorf("attempt to submit a nil command") 82 | } else if !a.IsActive() { 83 | return ErrShutdown 84 | } 85 | a.commandChan.In() <- c 86 | return nil 87 | } 88 | 89 | // Action puts @action (to change @a's internal state) onto the internal commandbus. 90 | // Returns an error channel. Reading from channel causes synchronous processing. 91 | func (a *actor) Action(action func() error) <-chan error { 92 | var errCh = make(chan error, 1) 93 | 94 | if !a.IsActive() { 95 | errCh <- ErrShutdown 96 | } else { 97 | a.actionChan <- func() { errCh <- action() } 98 | } 99 | return errCh 100 | } 101 | 102 | // Context exposes the internal context to allow creation of child contexts 103 | func (a *actor) Context() context.Context { 104 | return a.ctx 105 | } 106 | 107 | // Err exposes @errChan as read-only channel to the outside 108 | func (a *actor) Err() <-chan error { 109 | return a.errChan 110 | } 111 | 112 | // IsActive returns true if @a is still able to process events/commands 113 | func (a *actor) IsActive() bool { 114 | select { 115 | case <-a.ctx.Done(): 116 | return false 117 | default: 118 | return true 119 | } 120 | } 121 | 122 | // Reference counter access: 123 | // * each actor starts with a reference count of 1 124 | // * Refs() == 1 means the loop is running 125 | // * Refs() == 0 means the actor is dead 126 | // * Refs() > 1 means this actor is referenced by other objects 127 | func (a *actor) Refs() uint32 { return atomic.LoadUint32(&a.refcnt) } 128 | func (a *actor) incRefcnt() uint32 { return atomic.AddUint32(&a.refcnt, 1) } 129 | func (a *actor) decRefcnt() uint32 { return atomic.AddUint32(&a.refcnt, ^uint32(0)) } 130 | 131 | // Shutdown shuts down the actor context/loop 132 | func (a *actor) Shutdown() error { 133 | if !a.IsActive() { 134 | return ErrShutdown 135 | } 136 | 137 | a.cancel() 138 | // NB: do not wait here, since if this function is called from within a 139 | // cmdHdlr, we have a deadlock situation. Rely on loop() to ensure 140 | // that the reference count at end is 1, and then decremented to 0. 141 | return nil 142 | } 143 | 144 | // loop runs until a's context is canceled 145 | func (a *actor) loop(cmdHdlr func(*aggregate.Command), evtHdlr func(event.Event), ready bool) { 146 | const ( 147 | // Time to wait for other objects to release reference to actor 148 | refCntWaitIntvl = 1 * time.Second 149 | 150 | // Maximum number of @refCntWaitIntvl intervals to wait before giving up 151 | refCntMaxWaits = 60 152 | ) 153 | var commandChan <-chan interface{} 154 | 155 | if ready { 156 | commandChan = a.commandChan.Out() 157 | } 158 | 159 | for a.IsActive() { 160 | select { 161 | case action := <-a.actionChan: 162 | if action != nil { 163 | action() 164 | } 165 | case e, ok := <-a.eventChan.Out(): 166 | if !ok || e == nil { 167 | break 168 | } else if evt, ok := e.(event.Event); !ok { 169 | logger.Errorf("non-Event %v on Event channel", e) 170 | a.errChan <- errors.Errorf("non-Event %v on Event channel", e) 171 | } else { 172 | // The ServiceReady event serves to unblock the command channel. 173 | if _, ok = e.(*event.ServiceReady); ok { 174 | commandChan = a.commandChan.Out() 175 | } 176 | evtHdlr(evt) 177 | } 178 | case c, ok := <-commandChan: 179 | if !ok || c == nil { 180 | break 181 | } else if cmd, ok := c.(*aggregate.Command); !ok { 182 | logger.Errorf("non-Command %v on Command channel", c) 183 | a.errChan <- errors.Errorf("non-Command %v on Command channel", c) 184 | } else { 185 | cmdHdlr(cmd) 186 | } 187 | case <-a.ctx.Done(): // will be caught by a.IsActive() 188 | } 189 | } 190 | 191 | // 192 | // Clean-up 193 | // 194 | for cnt := 0; a.Refs() > 1; cnt++ { 195 | if cnt >= refCntMaxWaits { 196 | logger.Fatalf("timed out waiting for active references (%d) to reach 1", a.Refs()) 197 | } 198 | time.Sleep(refCntWaitIntvl) 199 | } 200 | 201 | // Only close the input queues when no more events/commands can be queued 202 | a.eventChan.Close() 203 | a.commandChan.Close() 204 | 205 | close(a.errChan) 206 | 207 | // Drain output channels to terminate the internal goroutines used by InfiniteChannel 208 | for range a.eventChan.Out() { 209 | } 210 | for range a.commandChan.Out() { 211 | } 212 | 213 | a.decRefcnt() // Now the reference count should be 0 214 | } 215 | -------------------------------------------------------------------------------- /actor/spec.go: -------------------------------------------------------------------------------- 1 | // Package actor implements a self-contained actor which responds to incoming commands and events. 2 | package actor 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/grrtrr/magicbus/aggregate" 8 | "github.com/grrtrr/magicbus/event" 9 | ) 10 | 11 | // Actor represents an self-contained actor, following Hewitt's Actor Model 12 | type Actor interface { 13 | // Submit publishes @c onto the command bus 14 | Submit(*aggregate.Command) error 15 | 16 | // Publish publishes @e onto the event bus 17 | Publish(event.Event) error 18 | 19 | // Action attempts to submit an @action to the internal command bus 20 | Action(func() error) <-chan error 21 | 22 | // Shutdown shuts down the actor context/loop 23 | Shutdown() error 24 | 25 | // IsActive returns true as long as the actor is able to accept commands/events 26 | IsActive() bool 27 | 28 | // Refs returns the number of active references (>= 1: active, 0: dead) 29 | Refs() uint32 30 | 31 | // Context returns the internal context. Useful to add nested/child contexts. 32 | Context() context.Context 33 | } 34 | -------------------------------------------------------------------------------- /aggregate/aggregate.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | // Aggregate represents an aggregate entity (a distinct subystem). 4 | type Aggregate interface { 5 | // Returns the cluster-unique ID of this Aggregate 6 | AggregateID() ID 7 | 8 | // HandleCommand lets the Aggregate handle @Command 9 | // @next: if not nil, returns next command-in-sequence to complete 10 | // @result: (only if @next=nil) returns result of operation 11 | // @err: error value (@next/@result are ignored in this case) 12 | HandleCommand(*Command) (next *Command, result interface{}, err error) 13 | } 14 | -------------------------------------------------------------------------------- /aggregate/aggregate_test.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestAggregateZero(t *testing.T) { 10 | var a ID 11 | 12 | // Ensure that IsZero correctly identifies a zero value 13 | if !a.IsZero() { 14 | t.Fatalf("ID '%s' is not zero", a) 15 | } 16 | // Generate a zero ID 17 | a = NewID(ResourceType_INVALID_RESOURCE, "") 18 | if !a.IsZero() { 19 | t.Fatalf("Zero ID not identified as such") 20 | } 21 | 22 | // Aggregate root must not be empty 23 | a = NewID(ResourceType_INVALID_RESOURCE, "ID") 24 | if !a.IsZero() { 25 | t.Fatalf("Zero AggregateRoot in ID not identified as such") 26 | } 27 | 28 | // Non-zero aggregateID 29 | a = NewID(42, "") 30 | if a.IsZero() { 31 | t.Fatalf("ID '%s' is zero", a) 32 | } 33 | } 34 | 35 | func TestEncodingAggregate(t *testing.T) { 36 | var i ID 37 | var s = "10.55.220.225.MEMORY.1" 38 | 39 | // 1. Encode/decode chain 40 | if err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, s)), &i); err != nil { 41 | t.Fatalf("failed to unmarshal legitimate ID %q: %s", s, err) 42 | } else if i.String() != s { 43 | t.Fatalf("unmarshal changed the ID value: %s <> %s", i, s) 44 | } else if b, err := json.Marshal(i); err != nil { 45 | t.Fatalf("failed to marshal %s: %s", i, err) 46 | } else if string(b) != fmt.Sprintf(`"%s"`, s) { 47 | t.Fatalf("encoding did not produce original form: %q", string(b)) 48 | } 49 | 50 | // 2. Ensure it detects invalid forms 51 | if err := json.Unmarshal([]byte(`""`), &i); err == nil { 52 | t.Fatalf("expected error unmarshalling empty string, but got nil") 53 | } else if err := json.Unmarshal([]byte(`"THIS RESOURCE TYPE IS INVALID"`), &i); err == nil { 54 | t.Fatalf("expected error unmarshalling invalid resource type, but got nil") 55 | } else if err := json.Unmarshal([]byte(`"1.2.3.MEMORY.test"`), &i); err == nil { // invalid IP 56 | t.Fatalf("expected error unmarshalling invalid IP address, but got nil") 57 | } else if err := json.Unmarshal([]byte(`"INVALID_RESOURCE"`), &i); err != nil { 58 | t.Fatalf("failed to unmarshal invalid resource specifier: %s", err) 59 | } else if i.String() != "INVALID_RESOURCE" { 60 | t.Fatalf("unable to correctly deserialize invalid resource: got %q", i) 61 | } 62 | 63 | // 3. IP address variations (which we have to test until we have node IDs) 64 | s = "10.55.220.27.CPU" 65 | if err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, s)), &i); err != nil { 66 | t.Fatalf("failed to unmarshal %s resource specifier: %s", s, err) 67 | } else if i.String() != s { 68 | t.Fatalf("unmarshaling garbled input: expected %q, got %q", s, i) 69 | } 70 | s = "10.55.220.27" 71 | if err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, s)), &i); err != nil { 72 | t.Fatalf("failed to unmarshal %s resource specifier: %s", s, err) 73 | } else if i.String() != s+".INVALID_RESOURCE" { 74 | t.Fatalf("unmarshaling garbled input: expected %q, got %q", s, i) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /aggregate/command.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Command is an implementation of command.Command 13 | type Command struct { 14 | // dst designates the receiver of this command 15 | dst ID 16 | 17 | // src is the subystem issuing this command 18 | src ID 19 | 20 | // Args contains the command-specific struct or string 21 | args interface{} 22 | 23 | // Cancellation context 24 | ctx context.Context 25 | } 26 | 27 | // WithContext adds @ctx to @c and returns the transformed result and cancel function. 28 | func (c *Command) WithContext(ctx context.Context) (c1 *Command, cancel context.CancelFunc) { 29 | c.ctx, cancel = context.WithCancel(ctx) 30 | return c, cancel 31 | } 32 | 33 | // NewLocalCommand is the simplest use case: local aggregate, no job tracking. 34 | func NewLocalCommand(aggregate ID, cmdData interface{}) (*Command, error) { 35 | return NewCommand(aggregate, aggregate, cmdData) 36 | } 37 | 38 | // NewCommand creates a command with job tracking. 39 | // @src, dst: source/destination of the command 40 | // @cmdData: either 41 | // a) a (pointer to a) struct - in this case Type() is the type-name of the struct, or 42 | // b) a non-empty string - in this case Type() is the content of the string. 43 | func NewCommand(src, dst ID, cmdData interface{}) (*Command, error) { 44 | var t = getType(cmdData) 45 | 46 | if t == nil { 47 | return nil, errors.Errorf("attempt to submit a nil command") 48 | } 49 | 50 | switch t.Kind() { 51 | case reflect.Struct: 52 | if t.Name() == "" { 53 | return nil, errors.Errorf("attempt to submit anonymous struct as command") 54 | } 55 | case reflect.String: 56 | if fmt.Sprint(cmdData) == "" { 57 | return nil, errors.Errorf("attempt to submit an empty string as command") 58 | } 59 | default: 60 | return nil, errors.Errorf("attempt to submit %s %#+v as command", t.Kind(), cmdData) 61 | } 62 | 63 | return &Command{src: src, dst: dst, args: cmdData, ctx: context.Background()}, nil 64 | 65 | } 66 | 67 | // IsLocal returns true if @cmd is not meant to leave the local bus. 68 | func (c *Command) IsLocal() bool { return c.src.IsLocal() && c.dst.IsLocal() } 69 | func (c *Command) String() string { return c.Type() } 70 | 71 | // ToJSON represents @c as a JSON string 72 | func (c *Command) ToJSON() string { 73 | if b, err := json.Marshal(c.Data()); err != nil { 74 | return fmt.Sprintf("ERROR: failed to serialize %s: %s", c.Type(), err) 75 | } else { 76 | return fmt.Sprintf("%s %s", c.Type(), string(b)) 77 | } 78 | } 79 | 80 | // Getters (no Setters) 81 | func (c *Command) Source() ID { return c.src } 82 | func (c *Command) Dest() ID { return c.dst } 83 | func (c *Command) Context() context.Context { return c.ctx } 84 | 85 | // Implements command.Command 86 | func (c *Command) Data() interface{} { return c.args } 87 | 88 | // Type returns a description of @cmd 89 | func (c *Command) Type() string { 90 | switch t := getType(c.Data()); t.Kind() { 91 | case reflect.String: 92 | return c.Data().(string) 93 | case reflect.Struct: 94 | return t.Name() 95 | default: 96 | return fmt.Sprintf("invalid command %T %#+v", c.Data(), c.Data()) 97 | } 98 | } 99 | 100 | // getType is a helper that extracts the underlying type of @cmd 101 | func getType(cmd interface{}) reflect.Type { 102 | var t = reflect.TypeOf(cmd) 103 | 104 | if t != nil { 105 | for t.Kind() == reflect.Ptr { 106 | t = t.Elem() 107 | } 108 | } 109 | return t 110 | } 111 | -------------------------------------------------------------------------------- /aggregate/command_test.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func simpleNewCommand(a ID, cmdData interface{}) (*Command, error) { 8 | return NewCommand(a, a, cmdData) 9 | } 10 | 11 | func TestCommandCreation(t *testing.T) { 12 | testAggregateID := ID{Type: 42} 13 | 14 | // Must not register a nil command 15 | if _, err := simpleNewCommand(testAggregateID, nil); err == nil { 16 | t.Fatalf("expected error attempting to register a nil command, but got no error") 17 | } 18 | 19 | // Must not register a non-struct command 20 | if _, err := simpleNewCommand(testAggregateID, int(42)); err == nil { 21 | t.Fatalf("expected error attempting to register a non-struct/non-string type, but got no error") 22 | } 23 | 24 | // Must not attempt to register an empty string as a command 25 | if _, err := simpleNewCommand(testAggregateID, ""); err == nil { 26 | t.Fatalf("expected error attempting to register an empty string, but got no error") 27 | } 28 | 29 | // Must not register an anonymous command 30 | if _, err := simpleNewCommand(testAggregateID, struct{}{}); err == nil { 31 | t.Fatalf("expected error attempting to register an anonymous command, but got no error") 32 | } 33 | 34 | // Another anonymous command 35 | var x struct{ foo, bar string } 36 | if _, err := simpleNewCommand(testAggregateID, x); err == nil { 37 | t.Fatalf("expected error attempting to register an anonymous command, but got no error") 38 | } 39 | if _, err := simpleNewCommand(testAggregateID, &x); err == nil { 40 | t.Fatalf("expected error attempting to register an anonymous command, but got no error") 41 | } 42 | 43 | // Named structs 44 | y := namedCommand1{} 45 | if c, err := simpleNewCommand(testAggregateID, y); err != nil { 46 | t.Fatalf("failed to create namedCommand1: %s", err) 47 | } else if n := c.Type(); n != "namedCommand1" { 48 | t.Fatalf("error creating namedCommand1: name %q does not match", n) 49 | } 50 | 51 | // Same using a pointer 52 | if c, err := simpleNewCommand(testAggregateID, &y); err != nil { 53 | t.Fatalf("failed to create namedCommand1: %s", err) 54 | } else if n := c.Type(); n != "namedCommand1" { 55 | t.Fatalf("error creating namedCommand1: name %q does not match", n) 56 | } 57 | 58 | z := namedCommand2{} 59 | if c, err := simpleNewCommand(testAggregateID, &z); err != nil { 60 | t.Fatalf("failed to create namedCommand2: %s", err) 61 | } else if n := c.Type(); n != "namedCommand2" { 62 | t.Fatalf("error creating namedCommand2: name %q does not match", n) 63 | } 64 | } 65 | 66 | type namedCommand1 struct { 67 | field1 string 68 | field2 string 69 | field3 int 70 | } 71 | 72 | type namedCommand2 struct { 73 | foo, bar []byte 74 | field3 int 75 | } 76 | -------------------------------------------------------------------------------- /aggregate/id.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // ID is used to identify a subsystem uniquely across the entire cluster 12 | type ID struct { 13 | // Node on which this Aggregate resides 14 | Node string /* FIXME: NodeID */ 15 | 16 | // Subsystem that this aggregate belongs to (Aggregate Root) 17 | Type ResourceType 18 | 19 | // Unique ID of this aggregate on this node 20 | // This may be empty if the entity is the aggregate root. 21 | ID string 22 | } 23 | 24 | func (a ID) String() string { 25 | if a.Node == "" { 26 | return a.Resource() 27 | } 28 | return fmt.Sprintf("%s.%s", a.Node, a.Resource()) 29 | } 30 | 31 | // Resource specifies the resource(s) (subsystem + ID) identified by @a. 32 | func (a ID) Resource() string { 33 | if a.ID == "" { 34 | return a.Type.String() 35 | } 36 | return fmt.Sprintf("%s.%s", a.Type, a.ID) 37 | } 38 | 39 | // IsZero returns true if @a is not sufficiently specified (initialized). 40 | func (a ID) IsZero() bool { 41 | return a.Node == "" || a.Type == ResourceType_INVALID_RESOURCE 42 | } 43 | 44 | // IsLocal returns true if @a points to this/the local node. 45 | func (a ID) IsLocal() bool { 46 | return a.Node == NodeID() 47 | } 48 | 49 | // NewID returns a new aggregate.ID for this node. 50 | // @aggregateRoot: the subsystem (name of bounded context) of this aggregate 51 | // @entityID: within the @aggregateRoot, unique ID of the aggregate on this node 52 | func NewID(aggregateRoot ResourceType, entityID string) ID { 53 | return ID{Node: NodeID(), Type: aggregateRoot, ID: entityID} 54 | } 55 | 56 | // Implements encoding.TextMarshaler 57 | func (i ID) MarshalText() ([]byte, error) { 58 | return []byte(i.String()), nil 59 | } 60 | 61 | // Implements encoding.TextUnmarshaler 62 | func (i *ID) UnmarshalText(data []byte) (err error) { 63 | *i = ID{} // zero out fields in case @i was reused 64 | 65 | fields := strings.Split(string(data), ".") 66 | switch len(fields) { 67 | case 6: // node is an IP address 68 | i.Node = strings.Join(fields[:4], ".") 69 | if net.ParseIP(i.Node) == nil { 70 | return errors.Errorf("invalid node IP %q in %s", i.Node, string(data)) 71 | } 72 | i.Type, err = ResourceTypeFromString(fields[4]) 73 | i.ID = fields[5] 74 | case 5: // node is an IP address 75 | i.Node = strings.Join(fields[:4], ".") 76 | if net.ParseIP(i.Node) == nil { 77 | return errors.Errorf("invalid node IP %q in %s", i.Node, string(data)) 78 | } 79 | i.Type, err = ResourceTypeFromString(fields[4]) 80 | case 4: // solitary IP address 81 | i.Node = string(data) 82 | if net.ParseIP(i.Node) == nil { 83 | return errors.Errorf("invalid node IP %q in %s", i.Node, string(data)) 84 | } 85 | case 3: // node is a name without dot in it 86 | i.ID = fields[2] 87 | fallthrough 88 | case 2: // Node.Resource 89 | i.Node = fields[0] 90 | i.Type, err = ResourceTypeFromString(fields[1]) 91 | case 1: // Resource only 92 | i.Type, err = ResourceTypeFromString(fields[0]) 93 | default: 94 | return errors.Errorf("invalid Aggregate ID %q", string(data)) 95 | } 96 | return err 97 | } 98 | -------------------------------------------------------------------------------- /aggregate/nodeID.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | /* 4 | * Node-unique ID 5 | */ 6 | 7 | // GLOBAL VARIABLES 8 | var ( 9 | // ID of this node. This typically is the primary IP address, but can also be e.g. /etc/machine-id 10 | nodeID string = "UNKNOWN" 11 | ) 12 | 13 | // Allow other packages to retrieve the ID of this node 14 | func NodeID() string { 15 | return nodeID 16 | } 17 | 18 | // Set node ID to @id. 19 | func SetNodeID(id string) { 20 | nodeID = id 21 | } 22 | -------------------------------------------------------------------------------- /aggregate/resource_type.go: -------------------------------------------------------------------------------- 1 | package aggregate 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // ResourceType specifies the type of an Aggregate. 10 | type ResourceType int32 11 | 12 | const ( 13 | // Indicates that no valid resource type has been assigned yet 14 | ResourceType_INVALID_RESOURCE ResourceType = 0 15 | 16 | // The following are for example only 17 | ResourceType_CPU ResourceType = 1 18 | ResourceType_MEMORY ResourceType = 2 19 | ) 20 | 21 | // FIXME: the ResourceType typically is an enum, as sketched above with the 22 | // begin of the enum literals. You would define your own stringer and 23 | // from/to JSON methods for this type, depending on your needs. 24 | func (r ResourceType) String() string { 25 | switch r { 26 | case ResourceType_INVALID_RESOURCE: 27 | return "INVALID_RESOURCE" 28 | case ResourceType_CPU: 29 | return "CPU" 30 | case ResourceType_MEMORY: 31 | return "MEMORY" 32 | } 33 | return fmt.Sprint(int32(r)) 34 | } 35 | 36 | // ResourceTypeFromString is again project specific. It is the inverse of String(). 37 | func ResourceTypeFromString(s string) (ResourceType, error) { 38 | switch s { 39 | case "INVALID_RESOURCE": 40 | return ResourceType_INVALID_RESOURCE, nil 41 | case "CPU": 42 | return ResourceType_CPU, nil 43 | case "MEMORY": 44 | return ResourceType_MEMORY, nil 45 | } 46 | return ResourceType_INVALID_RESOURCE, errors.Errorf("invalid ResourceType %q", s) 47 | } 48 | -------------------------------------------------------------------------------- /aggregate_actor.go: -------------------------------------------------------------------------------- 1 | package magicbus 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grrtrr/magicbus/actor" 7 | "github.com/grrtrr/magicbus/aggregate" 8 | "github.com/grrtrr/magicbus/event" 9 | ) 10 | 11 | // aggregateActor serializes command/event handling on behalf of a registered Aggregate 12 | type aggregateActor struct { 13 | // Aggregate handled by this actor 14 | aggregate.Aggregate 15 | 16 | // Internal actor object 17 | actor.Actor 18 | } 19 | 20 | // newAggregateActor returns an initialized new Actor 21 | // @ctx: Cancellation context 22 | // @agg: Aggregate represented by this aggregateActor 23 | // @ready: Whether @agg is ready to run its HandleCommand() function. 24 | // If set to false, can be enabled later by sending a ServiceReady event. 25 | func newAggregateActor(ctx context.Context, agg aggregate.Aggregate, ready bool) *aggregateActor { 26 | a := &aggregateActor{Aggregate: agg} 27 | 28 | a.Actor = actor.New(ctx, a.commandHandler, a.eventHandler, ready) 29 | return a 30 | } 31 | 32 | // command-processing callback 33 | func (a *aggregateActor) commandHandler(cmd *aggregate.Command) { 34 | var agId = a.AggregateID() 35 | 36 | // The Dest of a command identifies the matching aggregate, with the only exception 37 | // that a specific command (ID != "") is sent to the "general manager" (ID == ""). 38 | if cmd.Dest() != agId && (agId.ID != "" || cmd.Dest().Type != agId.Type || cmd.Dest().Node != agId.Node) { 39 | logger.Errorf("%s: refusing to handle command - mismatching aggregate ID %s", a.AggregateID(), cmd.Dest()) 40 | return 41 | } else if err := cmd.Context().Err(); err != nil { 42 | logger.Warningf("%s: command canceled (%s)", a.AggregateID(), err) 43 | return 44 | } 45 | nextStep, result, err := a.Aggregate.HandleCommand(cmd) 46 | 47 | // Emit the CommandDone event to notify the (remote) site of completion. 48 | // Note: agId and cmd.Dest() may differ in the case where a new, specific Aggregate is created. 49 | // In this case, agID.ID=="" and cmdID.ID != "", and we have created a new Aggregate to 50 | // handle the event. Thus, the _actual_ source Aggregate is cmd.Dest(). 51 | // If ever changing the creation of specific managers, this MUST also be updated. 52 | Publish(event.NewCmdDone(cmd.Dest() /* see comment above */, cmd, result, err)) 53 | 54 | // Submit the nextStep command only _after_ publishing the events (otherwise the timing is off). 55 | if nextStep != nil { 56 | Submit(cmd.Context(), nextStep) 57 | } 58 | } 59 | 60 | // eventHandler is called by a.actor for each incoming event e whose Dest() matches the AggregateID of @a. 61 | func (a *aggregateActor) eventHandler(e event.Event) { 62 | if _, ok := e.(*event.ServiceReady); ok { // ServiceReady events are not passed on any further. 63 | logger.Debugf("%s: ready to process commands", a.AggregateID()) 64 | } else if eh, ok := a.Aggregate.(event.EventHandler); ok { 65 | eh.HandleEvent(e) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /command/.gitignore: -------------------------------------------------------------------------------- 1 | # Automatically generated enum files: 2 | command_step.go 3 | -------------------------------------------------------------------------------- /command/command.go: -------------------------------------------------------------------------------- 1 | // Package command defines the base structure of a command. 2 | package command 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | ) 8 | 9 | // Command carries a struct or string in Data() and is associated with a Job(). 10 | type Command interface { 11 | // Wraps the specific kind of command (named struct or non-empty string) 12 | Data() interface{} 13 | 14 | // Cancellation context for this command 15 | Context() context.Context 16 | } 17 | 18 | // command.Result groups the result of a command, and the potential error on failure. 19 | type Result struct { 20 | Result interface{} // Return value(s), may be nil 21 | Err error // Non-nil value if the command failed 22 | } 23 | 24 | func (r Result) String() string { 25 | if r.Err != nil { 26 | return fmt.Sprintf("err = %s", r.Err) 27 | } else if r.Result == nil { 28 | return "OK" 29 | } else if s, ok := r.Result.(string); ok { 30 | if s == "" { 31 | return "OK" 32 | } 33 | return s 34 | } 35 | return fmt.Sprintf("result = %v", r.Result) 36 | } 37 | -------------------------------------------------------------------------------- /event/command_done.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/grrtrr/magicbus/aggregate" 8 | "github.com/grrtrr/magicbus/command" 9 | ) 10 | 11 | // CommandDone is the 'Done' event published whenever a command completes. 12 | type CommandDone struct { 13 | Src aggregate.ID // Aggregate reporting this event, the status of a command just run 14 | Dst aggregate.ID // Intended destination Aggregate (issuer of the command to be run) 15 | 16 | Desc string // Descriptive text (used for logging) 17 | Data interface{} // The command data embedded in the original command 18 | Status string // Result: success status as string 19 | Error string // Result: stringified error (empty means no error) 20 | } 21 | 22 | // NewCmdDone is a convenience wrapper that fills in an event from @a and @cmd 23 | func NewCmdDone(src aggregate.ID, cmd *aggregate.Command, result interface{}, err error) Event { 24 | var cd = &CommandDone{ 25 | Src: src, 26 | Dst: cmd.Source(), 27 | Data: cmd.Data(), 28 | Desc: cmd.Type(), 29 | } 30 | 31 | if result != nil { 32 | cd.Status = fmt.Sprint(result) // FIXME: stringification 33 | } 34 | 35 | if err != nil { 36 | cd.Error = err.Error() 37 | } 38 | return cd 39 | } 40 | 41 | func (c *CommandDone) Source() aggregate.ID { return c.Src } 42 | func (c *CommandDone) Dest() aggregate.ID { return c.Dst } 43 | func (c *CommandDone) Result() command.Result { 44 | if c.Error != "" { 45 | return command.Result{Result: c.Status, Err: errors.New(c.Error)} 46 | } 47 | return command.Result{Result: c.Status} 48 | } 49 | 50 | func (c CommandDone) String() string { 51 | return fmt.Sprintf("CommandDone(%s, %s)", c.Desc, c.Result()) 52 | } 53 | -------------------------------------------------------------------------------- /event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "github.com/grrtrr/magicbus/aggregate" 4 | 5 | // Event represents a Domain Event 6 | type Event interface { 7 | // Source specifies origin of this event (may be empty) 8 | Source() aggregate.ID 9 | 10 | // Dest is intended destination aggregate (may not be empty) 11 | Dest() aggregate.ID 12 | } 13 | 14 | // event.Handler receives an event to process. 15 | type Handler func(Event) 16 | 17 | // EventHandler is an optional interface implemented by Aggregates, to process events 18 | // whose destination Dest() equals the ID of the Aggregate. 19 | type EventHandler interface { 20 | HandleEvent(Event) 21 | } 22 | -------------------------------------------------------------------------------- /event/service_ready.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grrtrr/magicbus/aggregate" 7 | ) 8 | 9 | // ServiceReady is sent to enable command processing in Aggregates. 10 | // 11 | // This applies only to Aggregates which were initially registed with 12 | // a 'ready=false' flag, meaning that incoming commands are queued, but 13 | // will not be processed until a ServiceReady event tells the Aggregate 14 | // that it is now time to do so. 15 | type ServiceReady struct { 16 | Aggregate aggregate.ID // Aggregate to unblock 17 | } 18 | 19 | func (s *ServiceReady) Source() aggregate.ID { return s.Aggregate } 20 | func (s *ServiceReady) Dest() aggregate.ID { return s.Source() } 21 | 22 | func (s ServiceReady) String() string { 23 | return fmt.Sprintf("ServiceReady(%s)", s.Aggregate.ID) 24 | } 25 | -------------------------------------------------------------------------------- /internals.go: -------------------------------------------------------------------------------- 1 | package magicbus 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/grrtrr/magicbus/actor" 8 | "github.com/grrtrr/magicbus/aggregate" 9 | "github.com/grrtrr/magicbus/event" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // MagicBus serializes event/command notification on behalf of aggregates, allowing 14 | // event observers to subscribe to asynchronous/immediate event notifications. 15 | type MagicBus struct { 16 | // Internal Actor object 17 | actor.Actor 18 | 19 | // List of command-handling aggregates (map { AggregateID -> aggregateActor }) 20 | aggregates map[aggregate.ID]*aggregateActor 21 | 22 | // List of event observers (map { SubscriptionID -> event.Handler }) 23 | observers map[string]event.Handler 24 | } 25 | 26 | // NewMagicBus instantiates a new bus instance ready to process commands/events. 27 | func NewMagicBus(ctx context.Context) *MagicBus { 28 | m := &MagicBus{ 29 | aggregates: map[aggregate.ID]*aggregateActor{}, 30 | observers: map[string]event.Handler{}, 31 | } 32 | m.Actor = actor.New(ctx, m.commandHandler, m.eventHandler, true) 33 | return m 34 | } 35 | 36 | // command-processing callback 37 | func (m *MagicBus) commandHandler(cmd *aggregate.Command) { 38 | // Try most-specific match (Type + Node + ID) first 39 | if ag, ok := m.aggregates[cmd.Dest()]; ok { 40 | if err := ag.Submit(cmd); err != nil { 41 | logger.Errorf("%s: failed to submit %v: %s", ag.AggregateID(), cmd, err) 42 | } 43 | return 44 | } else if cmd.Dest().ID != "" { 45 | // If there is no specific instance, try the general subsystem (ID == "") 46 | if ag, ok := m.aggregates[aggregate.NewID(cmd.Dest().Type, "")]; ok { 47 | if err := ag.Submit(cmd); err != nil { 48 | logger.Errorf("%s: failed to submit %v: %s", ag.AggregateID(), cmd, err) 49 | } 50 | return 51 | } 52 | } 53 | 54 | // No match means we are unable to handle a legitimate command. 55 | logger.Fatalf("magicbus: no aggregate handler was interested in %s", cmd) 56 | } 57 | 58 | // eventHandler is called my m.actor for each incoming event 59 | func (m *MagicBus) eventHandler(e event.Event) { 60 | // 1. Aggregates receive all events directed to them. 61 | if ag, ok := m.aggregates[e.Dest()]; ok { 62 | if err := ag.Publish(e); err != nil { 63 | logger.Warningf("%s: failed to publish %v: %s", ag.AggregateID(), e, err) 64 | } 65 | } 66 | 67 | // 2. Observers are handled in parallel. 68 | for _, handler := range m.observers { 69 | eventHandler := handler // avoid loop variable alias 70 | go eventHandler(e) 71 | } 72 | } 73 | 74 | func (m *MagicBus) String() string { 75 | var resChan = make(chan string, 1) 76 | 77 | if err := <-m.Action(func() error { 78 | resChan <- fmt.Sprintf("bus (aggregates: %d, subscriptions: %d)", len(m.aggregates), len(m.observers)) 79 | return nil 80 | }); err != nil { 81 | return fmt.Sprintf("bus in error: %s", err) 82 | } 83 | return <-resChan 84 | } 85 | 86 | // Register registers @a to handle commands on the local bus. 87 | // @ready: whether the aggregate is ready to process commands right away 88 | func (m *MagicBus) Register(a aggregate.Aggregate, ready bool) error { 89 | if a == nil { 90 | return errors.Errorf("attempt to register a nil Aggregate") 91 | } else if a.AggregateID().IsZero() { 92 | return errors.Errorf("attempt to register an Aggregate with an empty AggregateID") 93 | } 94 | 95 | return <-m.Action(func() error { 96 | logger.Debugf("magicbus: registering %s", a.AggregateID()) 97 | 98 | // Allow duplicate registration for robustness, reusing the first one. 99 | if _, exists := m.aggregates[a.AggregateID()]; !exists { 100 | m.aggregates[a.AggregateID()] = newAggregateActor(m.Context(), a, ready) 101 | } 102 | return nil 103 | }) 104 | } 105 | 106 | // Unregister removes @a from the bus 107 | func (m *MagicBus) Unregister(id aggregate.ID) error { 108 | return <-m.Action(func() error { 109 | logger.Debugf("magicbus: de-registering %s", id) 110 | 111 | ag, ok := m.aggregates[id] 112 | if !ok { 113 | return nil 114 | } 115 | delete(m.aggregates, id) 116 | return ag.Shutdown() 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /magicbus.go: -------------------------------------------------------------------------------- 1 | // Package magicbus implements a combined EventBus/CommandBus queueing service which guarantees that event/command handlers 2 | // of registered aggregates are run in serialized (non-current) order. 3 | package magicbus 4 | 5 | import ( 6 | "context" 7 | "time" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/grrtrr/magicbus/actor" 11 | "github.com/grrtrr/magicbus/aggregate" 12 | "github.com/grrtrr/magicbus/command" 13 | "github.com/grrtrr/magicbus/event" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // GLOBAL VARIABLES 18 | var ( 19 | localBus *MagicBus 20 | logger = logrus.WithField("module", "magicbus") 21 | ) 22 | 23 | // Init allocates the %localBus. 24 | func Init(ctx context.Context) { 25 | localBus = NewMagicBus(ctx) 26 | } 27 | 28 | // Launch takes command @data, turns it into a Command, and submits it to the local bus. 29 | // The result of the command (via the CommandDone event) is reported via the error channel. 30 | func Launch(ctx context.Context, cmd *aggregate.Command) command.Result { 31 | var resultCh = make(chan command.Result, 1) 32 | 33 | id, err := Observer( // Perform a one-off subscription for the CommandDone event. 34 | func(e event.Event) { 35 | if cd, ok := e.(*event.CommandDone); ok && e.Dest() == cmd.Source() { 36 | resultCh <- cd.Result() 37 | } 38 | }, 39 | ) 40 | if err != nil { 41 | return command.Result{Err: errors.Errorf("failed to subscribe to %s CommandDone event: %s", cmd.Type(), err)} 42 | } 43 | defer Unsubscribe(id) 44 | 45 | if err = Submit(ctx, cmd); err != nil { 46 | return command.Result{Err: errors.Errorf("failed to submit %s: %s", cmd.Type(), err)} 47 | } 48 | 49 | select { 50 | case ret := <-resultCh: 51 | return ret 52 | case <-ctx.Done(): 53 | if ctx.Err() == context.DeadlineExceeded { 54 | return command.Result{Err: errors.Errorf("timed out waiting for %s to complete", cmd.Type())} 55 | } 56 | return command.Result{Err: ctx.Err()} 57 | case <-cmd.Context().Done(): 58 | return command.Result{Err: errors.Errorf("command %s canceled: %s", cmd.Type(), cmd.Context().Err())} 59 | } 60 | } 61 | 62 | // LaunchWait is a variation of Launch which takes a timeout @maxWait instead of a context. 63 | func LaunchWait(cmd *aggregate.Command, maxWait time.Duration) command.Result { 64 | ctx, _ := context.WithTimeout(cmd.Context(), maxWait) 65 | return Launch(ctx, cmd) 66 | } 67 | 68 | // Submit @cmd to the local bus or forward it to a remote bus. 69 | func Submit(ctx context.Context, cmd *aggregate.Command) error { 70 | if !cmd.Dest().IsLocal() { 71 | return remoteSubmit(ctx, cmd) 72 | } 73 | return localBus.Submit(cmd) 74 | } 75 | 76 | // Publish @evt on the local bus, or pass it on to shcomm as STATUS message. 77 | func Publish(evt event.Event) { 78 | if err := func() error { 79 | if !evt.Dest().IsZero() && !evt.Dest().IsLocal() { 80 | return remotePublish(localBus.Context(), evt) 81 | } 82 | return localBus.Publish(evt) 83 | }(); err != nil && err != actor.ErrShutdown { 84 | logger.Errorf("%s: failed to publish event (%s): %s", evt, err) 85 | } 86 | } 87 | 88 | // RegisterAggregate registers @a to handle commands on the local bus. 89 | // @ready: whether the aggregate is ready to process commands right away 90 | func RegisterAggregate(a aggregate.Aggregate, ready bool) { 91 | if err := localBus.Register(a, ready); err != nil { 92 | logger.Fatalf("%s: registration failed: %s", a.AggregateID(), err) 93 | } 94 | } 95 | 96 | // UnregisterAggregate removes Aggregate @id from the bus. 97 | func UnregisterAggregate(id aggregate.ID) { 98 | if err := localBus.Unregister(id); err != nil { 99 | logger.Fatalf("%s: de-registration failed: %s", id, err) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /magicbus_test.go: -------------------------------------------------------------------------------- 1 | package magicbus 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/grrtrr/magicbus/aggregate" 10 | "github.com/grrtrr/magicbus/event" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // GLOBALS 15 | var ( 16 | cmdh = simpleCommand{"hello command"} 17 | cmd1 = simpleCommand{"command one"} 18 | cmd2 = simpleCommand{"command two"} 19 | cmd1Failed = errors.New("command 1 failed") 20 | ) 21 | 22 | func init() { 23 | Init(context.Background()) 24 | aggregate.SetNodeID("testNode") 25 | } 26 | 27 | func TestNewMagicBus(t *testing.T) { 28 | var m = NewMagicBus(context.Background()) 29 | 30 | if r := m.Refs(); r != 1 { 31 | t.Fatalf("reference count (%d) not 1 after start", r) 32 | } else if !m.IsActive() { 33 | t.Fatalf("bus is inactive after start: %s", m) 34 | } 35 | 36 | // Attempt to register aggregate with empty aggregate ID should fail 37 | a := &testAggregate{t: t} 38 | if err := m.Register(a, true); err == nil { 39 | t.Fatalf("expected an error when registering with empty aggregate ID, but got nil") 40 | } 41 | 42 | // Attempt to register an empty command list should fail 43 | if err := m.Register(a, true); err == nil { 44 | t.Fatalf("expected error when registering with an empty command list, but got nil") 45 | } 46 | 47 | a.id = aggregate.NewID(aggregate.ResourceType_CPU, "amd64") 48 | if err := m.Register(a, true); err != nil { 49 | t.Fatalf("failed to register new aggregate: %s", err) 50 | } 51 | t.Logf("new magic bus %s", m) 52 | 53 | // Test command submission on local bus 54 | RegisterAggregate(a, true) // register with the local bus instance 55 | 56 | command1, err := aggregate.NewCommand(a.AggregateID(), a.AggregateID(), cmd1) 57 | if err != nil { 58 | t.Fatalf("failed to set up cmd1: %s", err) 59 | } else if err := m.Submit(command1); err != nil { 60 | t.Fatalf("failed to submit command %s: %s", command1, err) 61 | } 62 | 63 | // Launch test 64 | res := Launch(context.TODO(), command1) 65 | if res.Err != nil { 66 | t.Fatalf("launch failed: %s", res.Err) 67 | } 68 | t.Logf("result of launching %s: %s", cmd1, res) 69 | 70 | // 71 | // Event subscription tests 72 | // 73 | te := mkTestEvent(a.AggregateID(), a.AggregateID(), "Test event") 74 | 75 | // 1. One-off subscription: multiple events result in only 1 handler call 76 | teHdlr := func(e event.Event) { 77 | t.Logf("first event handler received %s", e) 78 | } 79 | id, err := m.observer(teHdlr) 80 | if err != nil { 81 | t.Fatalf("failed to subscribe %s: %s", te, err) 82 | } 83 | t.Logf("%s new event subscription %s", a.AggregateID(), id) 84 | 85 | // 2. First subscription 86 | teHdlr2 := func(e event.Event) { 87 | t.Logf("second event handler received %s", e) 88 | } 89 | id1, err := m.observer(teHdlr2) 90 | if err != nil { 91 | t.Fatalf("failed to subscribe %s: %s", te, err) 92 | } else if id1 == id { 93 | t.Fatalf("event subscriptions not unique: %s", id) 94 | } 95 | t.Logf("%s new event subscription %s", a.AggregateID(), id) 96 | 97 | // Publish event, and wait some time for the handlers to respond 98 | if err := m.Publish(te); err != nil { 99 | t.Fatalf("failed to publish test event: %s", err) 100 | } 101 | 102 | // Publish again 103 | m.Publish(te) 104 | 105 | // Wait for handlers to settle and event to reach aggregate before unsubscription 106 | time.Sleep(1 * time.Second) 107 | 108 | if err := m.unsubscribe(id); err != nil { 109 | t.Fatalf("failed to unsubscribe first event handler: %s", err) 110 | } else if err := m.unsubscribe(id1); err != nil { 111 | t.Fatalf("failed to unsubscribe second event handler: %s", err) 112 | } 113 | 114 | // This should now report 1 aggregate and 0 subscriptions: 115 | t.Logf("magic bus now: %s", m) 116 | 117 | // Shutdown test, with a minimal wait time for Shutdown to kick in. 118 | m.Shutdown() 119 | time.Sleep(100 * time.Millisecond) 120 | if m.IsActive() { 121 | t.Fatalf("bus is active after shutdown") 122 | } else if r := m.Refs(); r != 0 { 123 | t.Fatalf("reference count (%d) not 0 after shutdown", r) 124 | } 125 | } 126 | 127 | // Test command 128 | func mkTestCommand(id aggregate.ID, typ string) *aggregate.Command { 129 | c, err := aggregate.NewCommand(id, id, typ) 130 | if err != nil { 131 | panic(fmt.Sprintf("failed to create command: %s", err)) 132 | } 133 | return c 134 | } 135 | 136 | // We only allow structs as Command type, since the name of the struct 137 | // doubles as command name (type). 138 | type simpleCommand struct { 139 | name string 140 | } 141 | 142 | // Test event 143 | func mkTestEvent(src, dst aggregate.ID, kind string) event.Event { 144 | return &testEvent{src: src, dst: dst, kind: kind} 145 | } 146 | 147 | type testEvent struct { 148 | src, dst aggregate.ID 149 | kind string 150 | } 151 | 152 | func (t *testEvent) Source() aggregate.ID { return t.src } 153 | func (t *testEvent) Dest() aggregate.ID { return t.dst } 154 | func (t testEvent) String() string { return fmt.Sprintf("%s => %s: %q", t.src, t.dst, t.kind) } 155 | 156 | // Test aggregate 157 | type testAggregate struct { 158 | id aggregate.ID 159 | t *testing.T 160 | } 161 | 162 | func (t *testAggregate) AggregateID() aggregate.ID { 163 | return t.id 164 | } 165 | 166 | // FIXME: make an aggregate that fails upon HandleCommand() 167 | func (t *testAggregate) HandleCommand(cmd *aggregate.Command) (*aggregate.Command, interface{}, error) { 168 | 169 | t.t.Logf("%s handling %s command", t.id, cmd) 170 | // NB: CommandDone will be published by aggregate_actor anyway 171 | return nil, nil, nil 172 | } 173 | -------------------------------------------------------------------------------- /query/query.go: -------------------------------------------------------------------------------- 1 | // Package query implements the read-side of our EventSourcing system, 2 | // providing structure and functions to retrieve up-to-date information 3 | // from an Aggregate. 4 | package query 5 | 6 | import "github.com/grrtrr/magicbus/aggregate" 7 | 8 | // query.Type indicates which kind of information (attributes) we are interested in 9 | type Type string 10 | 11 | // query.Argument specifies the input to a Query 12 | type Argument interface { 13 | // QueryType indicates the action of the query - what it is asking for 14 | QueryType() Type 15 | 16 | // Aggregate specifies the target (which aggregate) to query 17 | AggregateID() aggregate.ID 18 | } 19 | 20 | // Handler provides Query @results based on @args 21 | type Handler interface { 22 | Query(args Argument) (results interface{}, err error) 23 | } 24 | -------------------------------------------------------------------------------- /remote.go: -------------------------------------------------------------------------------- 1 | package magicbus 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grrtrr/magicbus/aggregate" 7 | "github.com/grrtrr/magicbus/event" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | /* 12 | * Dealing with remote commands and events. 13 | */ 14 | // remoteSubmit sends @cmd to the remote bus specified by cmd.AggregateID() 15 | func remoteSubmit(ctx context.Context, cmd *aggregate.Command) error { 16 | return errors.Errorf("remoteSubmit NOT IMPLEMENTED YET: integrete your remote method call here") 17 | } 18 | 19 | // remotePublish forwards @evt to the remote event bus specified by @evt.To 20 | func remotePublish(ctx context.Context, evt event.Event) error { 21 | return errors.Errorf("remotePublish NOT IMPLEMENTED YET: integrete your remote method call here") 22 | } 23 | -------------------------------------------------------------------------------- /repository/repository.go: -------------------------------------------------------------------------------- 1 | // Package repository handles read access to query Aggregates registered with the MagicBus 2 | package repository 3 | 4 | import ( 5 | "github.com/Sirupsen/logrus" 6 | "github.com/grrtrr/magicbus" 7 | "github.com/grrtrr/magicbus/aggregate" 8 | "github.com/grrtrr/magicbus/event" 9 | "github.com/grrtrr/magicbus/query" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Global Variables 14 | var ( 15 | logger = logrus.WithField("module", "repository") 16 | // registeredAggregates is populated at package initialization time 17 | registeredAggregates = map[aggregate.ResourceType]query.Handler{} 18 | ) 19 | 20 | // A repository handles information pertaining to the AggregateType() it advertises. 21 | // Any queries relating to the advertised AggregateType() are routed to its query.Handler. 22 | type Repository interface { 23 | // AggregateType returns the subsystem that this repository is reponsible for 24 | AggregateType() aggregate.ResourceType 25 | 26 | // Update is an event.Handler, called each time an event for AggregateType() arrives 27 | Update(event.Event) 28 | 29 | // All queries pertaining to AggregateType() are delegated to Handler 30 | query.Handler 31 | } 32 | 33 | // HandleQuery delegates @q to the appropriate Aggregate (query router) 34 | func HandleQuery(q query.Argument) (results interface{}, err error) { 35 | if repo, ok := registeredAggregates[q.AggregateID().Type]; ok { 36 | return repo.Query(q) 37 | } 38 | return nil, errors.Errorf("no handler registered for %s query", q.AggregateID()) 39 | } 40 | 41 | // RegisterQueryHandler registers @h as handling queries pertaining to @subsystem (at package initialization time) 42 | func RegisterQueryHandler(r Repository) { 43 | if _, err := magicbus.Observer(func(e event.Event) { 44 | if e.Source().Node == aggregate.NodeID() && e.Source().Type == r.AggregateType() { 45 | r.Update(e) 46 | } 47 | }); err != nil { 48 | logger.Fatalf("could not subscribe repository for %s domain-specific events: %s", r.AggregateType(), err) 49 | } 50 | registeredAggregates[r.AggregateType()] = r 51 | 52 | } 53 | -------------------------------------------------------------------------------- /subscribe.go: -------------------------------------------------------------------------------- 1 | package magicbus 2 | 3 | import ( 4 | "github.com/grrtrr/magicbus/event" 5 | uuid "github.com/satori/go.uuid" 6 | ) 7 | 8 | // Each subscription is identified by a cluster-unique ID 9 | type SubscriptionID uuid.UUID 10 | 11 | // NewSubscriptionID returns a new, cluster-unique subscription ID 12 | func NewSubscriptionID() SubscriptionID { 13 | return SubscriptionID(uuid.NewV1()) 14 | } 15 | 16 | func (s SubscriptionID) String() string { 17 | return uuid.UUID(s).String() 18 | } 19 | 20 | func (s SubscriptionID) IsZero() bool { 21 | return uuid.Equal(uuid.UUID(s), uuid.Nil) 22 | } 23 | 24 | // Observer subscribes @hdlr to receive immediate notification of events. 25 | func Observer(hdlr event.Handler) (SubscriptionID, error) { 26 | return localBus.observer(hdlr) 27 | } 28 | 29 | // Unsubscribe removes subscription @id from the local bus. 30 | func Unsubscribe(id SubscriptionID) error { 31 | return localBus.unsubscribe(id) 32 | } 33 | 34 | // Add new observer to @m 35 | func (m *MagicBus) observer(hdlr event.Handler) (SubscriptionID, error) { 36 | var id = NewSubscriptionID() 37 | 38 | return id, <-m.Action(func() error { 39 | m.observers[id.String()] = hdlr 40 | return nil 41 | }) 42 | } 43 | 44 | // Remove subscription records of @id 45 | func (m *MagicBus) unsubscribe(id SubscriptionID) error { 46 | return <-m.Action(func() error { 47 | delete(m.observers, id.String()) 48 | return nil 49 | }) 50 | } 51 | --------------------------------------------------------------------------------