├── .gitignore ├── rabbit ├── prefetch.go ├── doc.go ├── retry.go ├── metrics.go ├── reconnection.go ├── commandbus_test.go ├── eventbus_test.go ├── commandbus.go └── eventbus.go ├── couchbase ├── doc.go └── provider.go ├── uuid.go ├── scripts └── test ├── logger.go ├── doc.go ├── go.mod ├── commands_test.go ├── events_test.go ├── Makefile ├── correlation.go ├── inmemory-commandbus.go ├── inmemory-eventbus.go ├── metrics.go ├── inmemory-eventstreamrepo_test.go ├── inmemory-eventbus_test.go ├── inmemory-commandbus_test.go ├── event-source-based.go ├── inmemory-eventstreamrepository.go ├── type-registry.go ├── go.sum ├── eventsourcing.go ├── cqrs-testreadmodels_test.go ├── cqrs-testentities_test.go ├── commands.go ├── events.go ├── README.md ├── cqrs_test.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | 3 | -------------------------------------------------------------------------------- /rabbit/prefetch.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | // Prefetch amount of messages to prefetch 4 | var Prefetch = 100 5 | -------------------------------------------------------------------------------- /rabbit/doc.go: -------------------------------------------------------------------------------- 1 | // Package rabbit provides an event and command bus for the CQRS and Event Sourcing framework 2 | // 3 | // Current version: experimental 4 | // 5 | package rabbit 6 | -------------------------------------------------------------------------------- /couchbase/doc.go: -------------------------------------------------------------------------------- 1 | // Package couchbase provides an event sourcing implementation in couchbase for the CQRS and Event Sourcing framework 2 | // 3 | // Current version: experimental 4 | // 5 | package couchbase 6 | -------------------------------------------------------------------------------- /uuid.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | import uuid "github.com/satori/go.uuid" 4 | 5 | // NewUUIDString returns a new UUID 6 | func NewUUIDString() string { 7 | newUUID := uuid.NewV4() 8 | return newUUID.String() 9 | } 10 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | PKGS=$(go list ./... | grep -v /vendor) 4 | 5 | CGO_ENABLED=1 go test -v -race $PKGS -cover 6 | 7 | LINTERS_FLAGS=$GOLANGCI_LINT_FLAGS 8 | 9 | echo "Checking $GOLANGCI_LINT_FLAGS_OVERRIDE" 10 | 11 | if [ -e "$GOLANGCI_LINT_FLAGS_OVERRIDE" ]; then 12 | LINTERS_FLAGS=$(cat $GOLANGCI_LINT_FLAGS_OVERRIDE) 13 | fi 14 | 15 | echo "Linter flags = $LINTERS_FLAGS" 16 | 17 | if ! ./bin/golangci-lint run --enable-all $LINTERS_FLAGS; then 18 | echo "ERROR -- Failed linters" 19 | exit 2 20 | fi 21 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | type Logger interface { 4 | Debugf(v ...interface{}) 5 | Println(v ...interface{}) 6 | Printf(format string, v ...interface{}) 7 | } 8 | 9 | type noopLogger struct{} 10 | 11 | func (n noopLogger) Debugf(_ ...interface{}) { 12 | } 13 | func (n noopLogger) Println(_ ...interface{}) { 14 | } 15 | 16 | func (n noopLogger) Printf(format string, v ...interface{}) { 17 | } 18 | 19 | func defaultLogger() func() Logger { 20 | logger := &noopLogger{} 21 | return func() Logger { 22 | return logger 23 | } 24 | } 25 | 26 | var PackageLogger func() Logger = defaultLogger() 27 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package cqrs provides a CQRS and Event Sourcing framework written in go influenced by the cqrs journey guide 2 | // 3 | // For a full guide visit http://gitlab.brainloop.com/pkg/cqrs 4 | // 5 | // import "gitlab.brainloop.com/pkg/cqrs" 6 | // 7 | // func NewAccount(firstName string, lastName string, emailAddress string, passwordHash []byte, initialBalance float64) *Account { 8 | // account := new(Account) 9 | // account.EventSourceBased = cqrs.NewEventSourceBased(account) 10 | // 11 | // event := AccountCreatedEvent{firstName, lastName, emailAddress, passwordHash, initialBalance} 12 | // account.Update(event) 13 | // return account 14 | // } 15 | // 16 | package cqrs 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/andrewwebber/cqrs 2 | 3 | require ( 4 | github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c // indirect 5 | github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a // indirect 6 | github.com/couchbaselabs/go-couchbase v0.0.0-20190117181324-d904413d884d 7 | github.com/davecgh/go-spew v1.1.1 // indirect 8 | github.com/kr/pretty v0.1.0 // indirect 9 | github.com/pkg/errors v0.8.1 // indirect 10 | github.com/prometheus/client_golang v0.9.2 11 | github.com/satori/go.uuid v1.2.0 12 | github.com/streadway/amqp v0.0.0-20181205114330-a314942b2fd9 13 | github.com/stretchr/testify v1.3.0 14 | golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc 15 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /commands_test.go: -------------------------------------------------------------------------------- 1 | package cqrs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/andrewwebber/cqrs" 7 | ) 8 | 9 | type SampleMessageCommand struct { 10 | Message string 11 | } 12 | 13 | func TestCommandDispatcher(t *testing.T) { 14 | dispatcher := cqrs.NewMapBasedCommandDispatcher() 15 | success := false 16 | dispatcher.RegisterCommandHandler(SampleMessageCommand{}, func(command cqrs.Command) error { 17 | cqrs.PackageLogger().Debugf("Received Command : ", command.Body.(SampleMessageCommand).Message) 18 | success = true 19 | return nil 20 | }) 21 | 22 | err := dispatcher.DispatchCommand(cqrs.Command{Body: SampleMessageCommand{"Hello world"}}) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | if !success { 27 | t.Fatal("Expected success") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rabbit/retry.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import ( 4 | "log" 5 | "math" 6 | "time" 7 | ) 8 | 9 | // ConnectionStringResolver is a function that dynamically returns a connection string 10 | type ConnectionStringResolver func() (string, error) 11 | 12 | type function func() error 13 | 14 | func exponential(operation function, maxRetries int) error { 15 | var err error 16 | var sleepTime int 17 | for i := 0; i < maxRetries; i++ { 18 | err = operation() 19 | if err == nil { 20 | return nil 21 | } 22 | if i == 0 { 23 | sleepTime = 1 24 | } else { 25 | sleepTime = int(math.Exp2(float64(i)) * 100) 26 | } 27 | time.Sleep(time.Duration(sleepTime) * time.Millisecond) 28 | log.Printf("Retry exponential: Attempt %d, sleep %d", i, sleepTime) 29 | } 30 | 31 | return err 32 | } 33 | -------------------------------------------------------------------------------- /events_test.go: -------------------------------------------------------------------------------- 1 | package cqrs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/andrewwebber/cqrs" 7 | ) 8 | 9 | type SampleMessageReceivedEvent struct { 10 | Message string 11 | } 12 | 13 | func TestDefaultVersionedEventDispatcher(t *testing.T) { 14 | dispatcher := cqrs.NewVersionedEventDispatcher() 15 | success := false 16 | dispatcher.RegisterEventHandler(SampleMessageReceivedEvent{}, func(event cqrs.VersionedEvent) error { 17 | cqrs.PackageLogger().Debugf("Received Message : ", event.Event.(SampleMessageReceivedEvent).Message) 18 | success = true 19 | return nil 20 | }) 21 | 22 | err := dispatcher.DispatchEvent(cqrs.VersionedEvent{Event: SampleMessageReceivedEvent{"Hello world"}}) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | if !success { 27 | t.Fatal("Expected success") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export CGO_ENABLED:=0 2 | export GO111MODULE=on 3 | export GOLANGCI_LINT_FLAGS:=-D gochecknoglobals -D lll 4 | export GOLANGCI_LINT_FLAGS_OVERRIDE:=./.golangci 5 | 6 | BUILD_DATE=$(shell date +%Y%m%d-%H:%M:%S) 7 | GROUP=andrewwebber 8 | PROJECTNAME=cqrs 9 | 10 | REPO=github.com/$(GROUP)/$(PROJECTNAME) 11 | 12 | GOLANGCI_LINT_VERSION="v1.12.5" 13 | 14 | all: build 15 | 16 | build: bin/$(PROJECTNAME) 17 | 18 | .PHONY: bin/$(PROJECTNAME) 19 | bin/$(PROJECTNAME): 20 | @go build -o bin/$(PROJECTNAME) -v $(REPO) 21 | 22 | .PHONY: mod-update 23 | mod-update: 24 | @go get -u 25 | @go mod tidy 26 | 27 | test: bin/golangci-lint 28 | @./scripts/test 29 | 30 | bin/golangci-lint: 31 | @curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s $(GOLANGCI_LINT_VERSION) 32 | 33 | clean: 34 | @rm -rf bin 35 | 36 | 37 | .PHONY: all build clean test 38 | -------------------------------------------------------------------------------- /correlation.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // CQRSErrorEventType ... 8 | const CQRSErrorEventType = "cqrs.ErrorEvent" 9 | 10 | // ErrorEvent is a generic event raised within the CQRS framework 11 | type ErrorEvent struct { 12 | Message string 13 | } 14 | 15 | // DeliverCQRSError will deliver a CQRS error 16 | func DeliverCQRSError(correlationID string, err error, repo EventSourcingRepository) { 17 | err = repo.GetEventStreamRepository().SaveIntegrationEvent(VersionedEvent{ 18 | ID: "ve:" + NewUUIDString(), 19 | CorrelationID: correlationID, 20 | SourceID: "", 21 | Version: 0, 22 | EventType: CQRSErrorEventType, 23 | Created: time.Now(), 24 | 25 | Event: ErrorEvent{Message: err.Error()}}) 26 | 27 | if err != nil { 28 | PackageLogger().Debugf("ERROR saving integration event: %v\n", err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /inmemory-commandbus.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | // InMemoryCommandBus provides an inmemory implementation of the CommandPublisher CommandReceiver interfaces 4 | type InMemoryCommandBus struct { 5 | publishedCommandsChannel chan Command 6 | startReceiving bool 7 | } 8 | 9 | // NewInMemoryCommandBus constructor 10 | func NewInMemoryCommandBus() *InMemoryCommandBus { 11 | publishedCommandsChannel := make(chan Command, 0) 12 | return &InMemoryCommandBus{publishedCommandsChannel, false} 13 | } 14 | 15 | // PublishCommands publishes Commands to the Command bus 16 | func (bus *InMemoryCommandBus) PublishCommands(commands []Command) error { 17 | for _, command := range commands { 18 | bus.publishedCommandsChannel <- command 19 | } 20 | 21 | return nil 22 | } 23 | 24 | // ReceiveCommands starts a go routine that monitors incoming Commands and routes them to a receiver channel specified within the options 25 | func (bus *InMemoryCommandBus) ReceiveCommands(options CommandReceiverOptions) error { 26 | go func() { 27 | for { 28 | select { 29 | case ch := <-options.Close: 30 | ch <- nil 31 | case command := <-bus.publishedCommandsChannel: 32 | err := options.ReceiveCommand(command) 33 | if err != nil { 34 | 35 | } 36 | } 37 | } 38 | }() 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /inmemory-eventbus.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | // InMemoryEventBus provides an inmemory implementation of the VersionedEventPublisher VersionedEventReceiver interfaces 4 | type InMemoryEventBus struct { 5 | publishedEventsChannel chan VersionedEvent 6 | startReceiving bool 7 | } 8 | 9 | // NewInMemoryEventBus constructor 10 | func NewInMemoryEventBus() *InMemoryEventBus { 11 | publishedEventsChannel := make(chan VersionedEvent, 0) 12 | return &InMemoryEventBus{publishedEventsChannel, false} 13 | } 14 | 15 | // PublishEvents publishes events to the event bus 16 | func (bus *InMemoryEventBus) PublishEvents(events []VersionedEvent) error { 17 | for _, event := range events { 18 | bus.publishedEventsChannel <- event 19 | } 20 | 21 | return nil 22 | } 23 | 24 | // ReceiveEvents starts a go routine that monitors incoming events and routes them to a receiver channel specified within the options 25 | func (bus *InMemoryEventBus) ReceiveEvents(options VersionedEventReceiverOptions) error { 26 | 27 | go func() { 28 | for { 29 | select { 30 | case ch := <-options.Close: 31 | ch <- nil 32 | case versionedEvent := <-bus.publishedEventsChannel: 33 | if err := options.ReceiveEvent(versionedEvent); err != nil { 34 | 35 | } 36 | } 37 | } 38 | }() 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | metricsCommandsDispatched *prometheus.CounterVec 7 | metricsCommandsFailed *prometheus.CounterVec 8 | metricsEventsDispatched *prometheus.CounterVec 9 | metricsEventsFailed *prometheus.CounterVec 10 | ) 11 | 12 | func init() { 13 | metricsCommandsDispatched = prometheus.NewCounterVec(prometheus.CounterOpts{ 14 | Name: "cqrs_commands_dispatched", 15 | Subsystem: "ix", 16 | Help: "CQRS Commands Dispatched", 17 | }, []string{"command"}) 18 | 19 | metricsCommandsFailed = prometheus.NewCounterVec(prometheus.CounterOpts{ 20 | Name: "cqrs_commands_failed", 21 | Subsystem: "ix", 22 | Help: "CQRS Commands Failed", 23 | }, []string{"command"}) 24 | 25 | metricsEventsDispatched = prometheus.NewCounterVec(prometheus.CounterOpts{ 26 | Name: "cqrs_events_dispatched", 27 | Subsystem: "ix", 28 | Help: "CQRS Events Dispatched", 29 | }, []string{"event"}) 30 | 31 | metricsEventsFailed = prometheus.NewCounterVec(prometheus.CounterOpts{ 32 | Name: "cqrs_events_failed", 33 | Subsystem: "ix", 34 | Help: "CQRS Events Failed", 35 | }, []string{"event"}) 36 | 37 | prometheus.MustRegister(metricsCommandsDispatched, metricsCommandsFailed, metricsEventsDispatched, metricsEventsFailed) 38 | } 39 | -------------------------------------------------------------------------------- /rabbit/metrics.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | metricsCommandsPublished *prometheus.CounterVec 7 | metricsCommandsFailed *prometheus.CounterVec 8 | metricsEventsPublished *prometheus.CounterVec 9 | metricsEventsFailed *prometheus.CounterVec 10 | ) 11 | 12 | func init() { 13 | metricsCommandsPublished = prometheus.NewCounterVec(prometheus.CounterOpts{ 14 | Name: "rabbit_commands_published", 15 | Subsystem: "ix", 16 | Help: "CQRS Commands Published", 17 | }, []string{"command"}) 18 | 19 | metricsCommandsFailed = prometheus.NewCounterVec(prometheus.CounterOpts{ 20 | Name: "rabbit_commands_failed", 21 | Subsystem: "ix", 22 | Help: "CQRS Commands Failed", 23 | }, []string{"command"}) 24 | 25 | metricsEventsPublished = prometheus.NewCounterVec(prometheus.CounterOpts{ 26 | Name: "rabbit_events_published", 27 | Subsystem: "ix", 28 | Help: "CQRS Events Published", 29 | }, []string{"event"}) 30 | 31 | metricsEventsFailed = prometheus.NewCounterVec(prometheus.CounterOpts{ 32 | Name: "rabbit_events_failed", 33 | Subsystem: "ix", 34 | Help: "CQRS Events Failed", 35 | }, []string{"event"}) 36 | 37 | prometheus.MustRegister(metricsCommandsPublished, metricsCommandsFailed, metricsEventsPublished, metricsEventsFailed) 38 | } 39 | -------------------------------------------------------------------------------- /inmemory-eventstreamrepo_test.go: -------------------------------------------------------------------------------- 1 | package cqrs_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/andrewwebber/cqrs" 8 | ) 9 | 10 | func TestInMemoryEventStreamRepository(t *testing.T) { 11 | typeRegistry := cqrs.NewTypeRegistry() 12 | persistance := cqrs.NewInMemoryEventStreamRepository() 13 | repository := cqrs.NewRepository(persistance, typeRegistry) 14 | 15 | hashedPassword, err := GetHashForPassword("$ThisIsMyPassword1") 16 | accountID := "5058e029-d329-4c4b-b111-b042e48b0c5f" 17 | 18 | if err != nil { 19 | t.Fatal("Error: ", err) 20 | } 21 | 22 | cqrs.PackageLogger().Debugf("Get hash for user...") 23 | 24 | cqrs.PackageLogger().Debugf("Create new account...") 25 | account := NewAccount("John", "Snow", "john.snow@cqrs.example", hashedPassword, 0.0) 26 | account.SetID(accountID) 27 | err = account.ChangePassword("$ThisIsANOTHERPassword") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | _, err = repository.Save(account, "correlationID") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | accountFromHistory, err := NewAccountFromHistory(accountID, repository) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | if string(accountFromHistory.PasswordHash) != string(account.PasswordHash) { 42 | t.Fatal("Expected PasswordHash to match") 43 | } 44 | 45 | if events, err := persistance.AllIntegrationEventsEverPublished(); err != nil { 46 | t.Fatal(err) 47 | } else { 48 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", events)) 49 | } 50 | 51 | correlationEvents, err := persistance.GetIntegrationEventsByCorrelationID("correlationID") 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | if len(correlationEvents) == 0 { 57 | t.Fatal("Expeced correlation events") 58 | } 59 | 60 | cqrs.PackageLogger().Debugf("GetIntegrationEventsByCorrelationID") 61 | for _, correlationEvent := range correlationEvents { 62 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", correlationEvent)) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rabbit/reconnection.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import ( 4 | "github.com/andrewwebber/cqrs" 5 | "github.com/streadway/amqp" 6 | ) 7 | 8 | type reconnectionAttempt struct { 9 | context int 10 | response chan reconnectionAttemptResponse 11 | } 12 | 13 | type reconnectionAttemptResponse struct { 14 | connection *amqp.Connection 15 | newContext int 16 | } 17 | 18 | type reconnectedHandler func(*amqp.Connection, int) 19 | 20 | func initializeReconnectionManagement(commString ConnectionStringResolver, reconnected reconnectedHandler) chan reconnectionAttempt { 21 | reconnectCh := make(chan reconnectionAttempt) 22 | go func(receive <-chan reconnectionAttempt) { 23 | initialContext := 0 24 | cqrs.PackageLogger().Debugf("Initial context %v", initialContext) 25 | var conn *amqp.Connection 26 | var err error 27 | var connectionString string 28 | for reconnectIssued := range receive { 29 | if reconnectIssued.context == initialContext { 30 | cqrs.PackageLogger().Debugf("Starting rabbitmq reconnection") 31 | retryErr := exponential(func() error { 32 | connectionString, err = commString() 33 | cqrs.PackageLogger().Debugf("Connecting to rabbitmq %v", connectionString) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | conn, err = amqp.Dial(connectionString) 39 | return err 40 | }, 10) 41 | 42 | for retryErr != nil { 43 | retryErr = exponential(func() error { 44 | connectionString, err = commString() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | conn, err = amqp.Dial(connectionString) 50 | return err 51 | }, 10) 52 | } 53 | initialContext = initialContext + 1 54 | cqrs.PackageLogger().Debugf("Rabbitmq reconnection successfull") 55 | reconnected(conn, initialContext) 56 | } 57 | 58 | reconnectIssued.response <- reconnectionAttemptResponse{connection: conn, newContext: initialContext} 59 | } 60 | }(reconnectCh) 61 | return reconnectCh 62 | } 63 | -------------------------------------------------------------------------------- /inmemory-eventbus_test.go: -------------------------------------------------------------------------------- 1 | package cqrs_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/andrewwebber/cqrs" 10 | ) 11 | 12 | type SampleEvent struct { 13 | Message string 14 | } 15 | 16 | func TestInMemoryEventBus(t *testing.T) { 17 | bus := cqrs.NewInMemoryEventBus() 18 | eventType := reflect.TypeOf(SampleEvent{}) 19 | 20 | // Create communication channels 21 | // 22 | // for closing the queue listener, 23 | closeChannel := make(chan chan error) 24 | // receiving errors from the listener thread (go routine) 25 | errorChannel := make(chan error) 26 | // and receiving events from the queue 27 | receiveEventChannel := make(chan cqrs.VersionedEventTransactedAccept) 28 | eventHandler := func(event cqrs.VersionedEvent) error { 29 | accepted := make(chan bool) 30 | receiveEventChannel <- cqrs.VersionedEventTransactedAccept{Event: event, ProcessedSuccessfully: accepted} 31 | if <-accepted { 32 | return nil 33 | } 34 | 35 | return errors.New("Unsuccessful") 36 | } 37 | 38 | // Start receiving events by passing these channels to the worker thread (go routine) 39 | if err := bus.ReceiveEvents(cqrs.VersionedEventReceiverOptions{TypeRegistry: nil, Close: closeChannel, Error: errorChannel, ReceiveEvent: eventHandler}); err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | // Publish a simple event 44 | cqrs.PackageLogger().Debugf("Publishing events") 45 | go func() { 46 | if err := bus.PublishEvents([]cqrs.VersionedEvent{{ 47 | EventType: eventType.String(), 48 | Event: SampleEvent{"TestInMemoryEventBus"}}}); err != nil { 49 | t.Fatal(err) 50 | } 51 | }() 52 | 53 | // If we dont receive a message within 5 seconds this test is a failure. Use a channel to signal the timeout 54 | timeout := make(chan bool, 1) 55 | go func() { 56 | time.Sleep(5 * time.Second) 57 | timeout <- true 58 | }() 59 | 60 | // Wait on multiple channels using the select control flow. 61 | select { 62 | // Test timeout 63 | case <-timeout: 64 | t.Fatal("Test timed out") 65 | // Version event received channel receives a result with a channel to respond to, signifying successful processing of the message. 66 | // This should eventually call an event handler. See cqrs.NewVersionedEventDispatcher() 67 | case event := <-receiveEventChannel: 68 | sampleEvent := event.Event.Event.(SampleEvent) 69 | cqrs.PackageLogger().Debugf(sampleEvent.Message) 70 | event.ProcessedSuccessfully <- true 71 | // Receiving on this channel signifys an error has occured work processor side 72 | case err := <-errorChannel: 73 | t.Fatal(err) 74 | } 75 | 76 | closeResponse := make(chan error) 77 | closeChannel <- closeResponse 78 | <-closeResponse 79 | } 80 | -------------------------------------------------------------------------------- /inmemory-commandbus_test.go: -------------------------------------------------------------------------------- 1 | package cqrs_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/andrewwebber/cqrs" 10 | ) 11 | 12 | type SampleCommand struct { 13 | Message string 14 | } 15 | 16 | func TestInMemoryCommandBus(t *testing.T) { 17 | bus := cqrs.NewInMemoryCommandBus() 18 | CommandType := reflect.TypeOf(SampleCommand{}) 19 | 20 | // Create communication channels 21 | // 22 | // for closing the queue listener, 23 | closeChannel := make(chan chan error) 24 | // receiving errors from the listener thread (go routine) 25 | errorChannel := make(chan error) 26 | // and receiving Commands from the queue 27 | receiveCommandChannel := make(chan cqrs.CommandTransactedAccept) 28 | commandHandler := func(command cqrs.Command) error { 29 | accepted := make(chan bool) 30 | receiveCommandChannel <- cqrs.CommandTransactedAccept{Command: command, ProcessedSuccessfully: accepted} 31 | if <-accepted { 32 | return nil 33 | } 34 | 35 | return errors.New("Unsuccessful") 36 | } 37 | 38 | // Start receiving Commands by passing these channels to the worker thread (go routine) 39 | if err := bus.ReceiveCommands(cqrs.CommandReceiverOptions{TypeRegistry: nil, Close: closeChannel, Error: errorChannel, ReceiveCommand: commandHandler}); err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | // Publish a simple Command 44 | cqrs.PackageLogger().Debugf("Publishing Commands") 45 | go func() { 46 | if err := bus.PublishCommands([]cqrs.Command{{ 47 | CommandType: CommandType.String(), 48 | Created: time.Now().UTC(), 49 | Body: SampleCommand{"TestInMemoryCommandBus"}}}); err != nil { 50 | t.Fatal(err) 51 | } 52 | }() 53 | 54 | // If we dont receive a message within 5 seconds this test is a failure. Use a channel to signal the timeout 55 | timeout := make(chan bool, 1) 56 | go func() { 57 | time.Sleep(5 * time.Second) 58 | timeout <- true 59 | }() 60 | 61 | // Wait on multiple channels using the select control flow. 62 | select { 63 | // Test timeout 64 | case <-timeout: 65 | t.Fatal("Test timed out") 66 | // Version Command received channel receives a result with a channel to respond to, signifying successful processing of the message. 67 | // This should Commandually call an Command handler. See cqrs.NewVersionedCommandDispatcher() 68 | case command := <-receiveCommandChannel: 69 | sampleCommand := command.Command.Body.(SampleCommand) 70 | cqrs.PackageLogger().Debugf(sampleCommand.Message) 71 | command.ProcessedSuccessfully <- true 72 | // Receiving on this channel signifys an error has occured work processor side 73 | case err := <-errorChannel: 74 | t.Fatal(err) 75 | } 76 | 77 | closeResponse := make(chan error) 78 | closeChannel <- closeResponse 79 | <-closeResponse 80 | } 81 | -------------------------------------------------------------------------------- /event-source-based.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // EventSourced providers an interface for event sourced aggregate types 8 | type EventSourced interface { 9 | ID() string 10 | SetID(string) 11 | Version() int 12 | SetVersion(int) 13 | Events() []interface{} 14 | CallEventHandler(event interface{}) 15 | SetSource(interface{}) 16 | WantsToSaveSnapshot() bool 17 | SuggestSaveSnapshot() 18 | } 19 | 20 | // EventSourceBased provider a base class for aggregate times wishing to contain basis helper functionality for event sourcing 21 | type EventSourceBased struct { 22 | id string 23 | version int 24 | events []interface{} 25 | source interface{} 26 | handlersCache HandlersCache 27 | saveSnapshot bool 28 | } 29 | 30 | // NewEventSourceBased constructor 31 | func NewEventSourceBased(source interface{}) EventSourceBased { 32 | return NewEventSourceBasedWithID(source, NewUUIDString()) 33 | } 34 | 35 | // NewEventSourceBasedWithID constructor 36 | func NewEventSourceBasedWithID(source interface{}, id string) EventSourceBased { 37 | return EventSourceBased{id, 0, []interface{}{}, source, createHandlersCache(source), false} 38 | } 39 | 40 | // Update should be called to change the state of an aggregate type 41 | func (s *EventSourceBased) Update(versionedEvent interface{}) { 42 | s.CallEventHandler(versionedEvent) 43 | s.events = append(s.events, versionedEvent) 44 | } 45 | 46 | // CallEventHandler routes an event to an aggregate's event handler 47 | func (s *EventSourceBased) CallEventHandler(event interface{}) { 48 | eventType := reflect.TypeOf(event) 49 | 50 | if handler, ok := s.handlersCache[eventType]; ok { 51 | handler(s.source, event) 52 | } else { 53 | panic("No handler found for event type " + eventType.String()) 54 | } 55 | } 56 | 57 | // ID provider the aggregate's ID 58 | func (s *EventSourceBased) ID() string { 59 | return s.id 60 | } 61 | 62 | // SetID sets the aggregate's ID 63 | func (s *EventSourceBased) SetID(id string) { 64 | s.id = id 65 | } 66 | 67 | // Version provider the aggregate's Version 68 | func (s *EventSourceBased) Version() int { 69 | return s.version 70 | } 71 | 72 | // SetSource ... 73 | func (s *EventSourceBased) SetSource(source interface{}) { 74 | s.source = source 75 | } 76 | 77 | // SetVersion sets the aggregate's Version 78 | func (s *EventSourceBased) SetVersion(version int) { 79 | s.version = version 80 | } 81 | 82 | // Events returns a slice of newly created events since last deserialization 83 | func (s *EventSourceBased) Events() []interface{} { 84 | return s.events 85 | } 86 | 87 | // WantsToSaveSnapshot returns whether the aggregate suggests to persist a snapshot upon the next save. 88 | func (s *EventSourceBased) WantsToSaveSnapshot() bool { 89 | return s.saveSnapshot 90 | } 91 | 92 | // SuggestSaveSnapshot records that the aggregate suggests a save of the snapshot upon the next save. 93 | func (s *EventSourceBased) SuggestSaveSnapshot() { 94 | s.saveSnapshot = true 95 | } 96 | -------------------------------------------------------------------------------- /rabbit/commandbus_test.go: -------------------------------------------------------------------------------- 1 | package rabbit_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/andrewwebber/cqrs" 10 | "github.com/andrewwebber/cqrs/rabbit" 11 | ) 12 | 13 | type SampleCommand struct { 14 | Message string 15 | } 16 | 17 | // Simple test for publishing and received versioned events using rabbitmq 18 | func TestCommandBus(t *testing.T) { 19 | 20 | // Create a new event bus 21 | connectionString := func() (string, error) { return "amqp://guest:guest@localhost:5672/", nil } 22 | bus := rabbit.NewCommandBus(connectionString, "rabbit_testcommands", "testing.commands") 23 | 24 | // Register types 25 | commandType := reflect.TypeOf(SampleCommand{}) 26 | commandTypeCache := cqrs.NewTypeRegistry() 27 | commandTypeCache.RegisterType(SampleCommand{}) 28 | 29 | // Create communication channels 30 | // 31 | // for closing the queue listener, 32 | closeChannel := make(chan chan error) 33 | // receiving errors from the listener thread (go routine) 34 | errorChannel := make(chan error) 35 | // and receiving commands from the queue 36 | receiveCommandChannel := make(chan cqrs.CommandTransactedAccept) 37 | commandHandler := func(command cqrs.Command) error { 38 | accepted := make(chan bool) 39 | receiveCommandChannel <- cqrs.CommandTransactedAccept{Command: command, ProcessedSuccessfully: accepted} 40 | t.Log("Send Message") 41 | if <-accepted { 42 | return nil 43 | } 44 | 45 | t.Fatal("Unsuccessful") 46 | return errors.New("Unsuccessful") 47 | } 48 | // Start receiving events by passing these channels to the worker thread (go routine) 49 | if err := bus.ReceiveCommands(cqrs.CommandReceiverOptions{TypeRegistry: commandTypeCache, Close: closeChannel, Error: errorChannel, ReceiveCommand: commandHandler, Exclusive: false, ListenerCount: 1}); err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | // Publish a simple event to the exchange http://www.rabbitmq.com/tutorials/tutorial-three-python.html 54 | t.Log("Publishing Commands") 55 | go func() { 56 | if err := bus.PublishCommands([]cqrs.Command{{ 57 | CommandType: commandType.String(), 58 | Body: SampleCommand{"rabbit_TestCommandBus"}}}); err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | t.Log("Command published") 63 | }() 64 | 65 | // If we dont receive a message within 5 seconds this test is a failure. Use a channel to signal the timeout 66 | timeout := make(chan bool, 1) 67 | go func() { 68 | time.Sleep(10 * time.Second) 69 | timeout <- true 70 | }() 71 | 72 | // Wait on multiple channels using the select control flow. 73 | select { 74 | // Test timeout 75 | case <-timeout: 76 | t.Fatal("Test timed out") 77 | // Version event received channel receives a result with a channel to respond to, signifying successful processing of the message. 78 | // This should eventually call an event handler. See cqrs.NewVersionedEventDispatcher() 79 | case command := <-receiveCommandChannel: 80 | sampleCommand := command.Command.Body.(SampleCommand) 81 | t.Log(sampleCommand.Message) 82 | command.ProcessedSuccessfully <- true 83 | // Receiving on this channel signifys an error has occured work processor side 84 | case err := <-errorChannel: 85 | t.Fatal(err) 86 | } 87 | 88 | closeAck := make(chan error) 89 | closeChannel <- closeAck 90 | <-closeAck 91 | } 92 | -------------------------------------------------------------------------------- /rabbit/eventbus_test.go: -------------------------------------------------------------------------------- 1 | package rabbit_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/andrewwebber/cqrs" 10 | "github.com/andrewwebber/cqrs/rabbit" 11 | ) 12 | 13 | type SampleEvent struct { 14 | Message string 15 | } 16 | 17 | // Simple test for publishing and received versioned events using rabbitmq 18 | func TestEventBus(t *testing.T) { 19 | if testing.Short() { 20 | t.SkipNow() 21 | } 22 | 23 | // Create a new event bus 24 | connectionString := func() (string, error) { return "amqp://guest:guest@localhost:5672/", nil } 25 | bus := rabbit.NewEventBus(connectionString, "rabbit_testevents", "testing.events") 26 | 27 | // Register types 28 | eventType := reflect.TypeOf(SampleEvent{}) 29 | eventTypeCache := cqrs.NewTypeRegistry() 30 | eventTypeCache.RegisterType(SampleEvent{}) 31 | 32 | // Create communication channels 33 | // 34 | // for closing the queue listener, 35 | closeChannel := make(chan chan error) 36 | // receiving errors from the listener thread (go routine) 37 | errorChannel := make(chan error) 38 | // and receiving events from the queue 39 | receiveEventChannel := make(chan cqrs.VersionedEventTransactedAccept) 40 | eventHandler := func(event cqrs.VersionedEvent) error { 41 | accepted := make(chan bool) 42 | receiveEventChannel <- cqrs.VersionedEventTransactedAccept{Event: event, ProcessedSuccessfully: accepted} 43 | if <-accepted { 44 | cqrs.PackageLogger().Debugf("event process successfully") 45 | return nil 46 | } 47 | 48 | t.Fatal("Unsuccessful") 49 | return errors.New("Unsuccessful") 50 | } 51 | // Start receiving events by passing these channels to the worker thread (go routine) 52 | if err := bus.ReceiveEvents(cqrs.VersionedEventReceiverOptions{TypeRegistry: eventTypeCache, Close: closeChannel, Error: errorChannel, ReceiveEvent: eventHandler, Exclusive: false, ListenerCount: 1}); err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | // Publish a simple event to the exchange http://www.rabbitmq.com/tutorials/tutorial-three-python.html 57 | t.Log("Publishing events") 58 | go func() { 59 | if err := bus.PublishEvents([]cqrs.VersionedEvent{{ 60 | EventType: eventType.String(), 61 | Event: SampleEvent{"rabbit_TestEventBus"}}}); err != nil { 62 | t.Fatal(err) 63 | } 64 | t.Log("Events published") 65 | }() 66 | 67 | // If we dont receive a message within 5 seconds this test is a failure. Use a channel to signal the timeout 68 | timeout := make(chan bool, 1) 69 | go func() { 70 | time.Sleep(10 * time.Second) 71 | timeout <- true 72 | }() 73 | 74 | // Wait on multiple channels using the select control flow. 75 | select { 76 | // Test timeout 77 | case <-timeout: 78 | t.Fatal("Test timed out") 79 | // Version event received channel receives a result with a channel to respond to, signifying successful processing of the message. 80 | // This should eventually call an event handler. See cqrs.NewVersionedEventDispatcher() 81 | case event := <-receiveEventChannel: 82 | sampleEvent := event.Event.Event.(SampleEvent) 83 | t.Log(sampleEvent.Message) 84 | event.ProcessedSuccessfully <- true 85 | // Receiving on this channel signifys an error has occured work processor side 86 | case err := <-errorChannel: 87 | t.Fatal(err) 88 | } 89 | 90 | closeAck := make(chan error) 91 | closeChannel <- closeAck 92 | <-closeAck 93 | } 94 | -------------------------------------------------------------------------------- /inmemory-eventstreamrepository.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | import ( 4 | "errors" 5 | "sort" 6 | "sync" 7 | ) 8 | 9 | // InMemoryEventStreamRepository provides an inmemory event sourcing repository 10 | type InMemoryEventStreamRepository struct { 11 | lock sync.Mutex 12 | store map[string][]VersionedEvent 13 | correlation map[string][]VersionedEvent 14 | integrationEvents []VersionedEvent 15 | eventSourcedStore map[string]EventSourced 16 | } 17 | 18 | // NewInMemoryEventStreamRepository constructor 19 | func NewInMemoryEventStreamRepository() *InMemoryEventStreamRepository { 20 | store := make(map[string][]VersionedEvent) 21 | correlation := make(map[string][]VersionedEvent) 22 | eventSourcedStore := make(map[string]EventSourced) 23 | return &InMemoryEventStreamRepository{sync.Mutex{}, store, correlation, []VersionedEvent{}, eventSourcedStore} 24 | } 25 | 26 | // AllIntegrationEventsEverPublished returns all events ever published 27 | func (r *InMemoryEventStreamRepository) AllIntegrationEventsEverPublished() ([]VersionedEvent, error) { 28 | r.lock.Lock() 29 | defer r.lock.Unlock() 30 | log := r.integrationEvents 31 | sort.Sort(ByCreated(log)) 32 | return log, nil 33 | } 34 | 35 | // SaveIntegrationEvent persists an integration event 36 | func (r *InMemoryEventStreamRepository) SaveIntegrationEvent(event VersionedEvent) error { 37 | r.lock.Lock() 38 | defer r.lock.Unlock() 39 | 40 | r.integrationEvents = append(r.integrationEvents, event) 41 | events := r.correlation[event.CorrelationID] 42 | events = append(events, event) 43 | r.correlation[event.CorrelationID] = events 44 | 45 | PackageLogger().Debugf("Saving SaveIntegrationEvent event ", event.CorrelationID, events) 46 | 47 | return nil 48 | } 49 | 50 | // GetIntegrationEventsByCorrelationID returns all integration events with a matching correlationID 51 | func (r *InMemoryEventStreamRepository) GetIntegrationEventsByCorrelationID(correlationID string) ([]VersionedEvent, error) { 52 | events, _ := r.correlation[correlationID] 53 | return events, nil 54 | } 55 | 56 | // Save persists an event sourced object into the repository 57 | func (r *InMemoryEventStreamRepository) Save(id string, newEvents []VersionedEvent) error { 58 | for _, event := range newEvents { 59 | if err := r.SaveIntegrationEvent(event); err != nil { 60 | return err 61 | } 62 | } 63 | 64 | r.lock.Lock() 65 | defer r.lock.Unlock() 66 | 67 | if events, ok := r.store[id]; ok { 68 | events = append(events, newEvents...) 69 | 70 | r.store[id] = events 71 | return nil 72 | } 73 | 74 | r.store[id] = newEvents 75 | return nil 76 | } 77 | 78 | // Get retrieves events assoicated with an event sourced object by ID 79 | func (r *InMemoryEventStreamRepository) Get(id string, fromVersion int) ([]VersionedEvent, error) { 80 | r.lock.Lock() 81 | defer r.lock.Unlock() 82 | 83 | var events []VersionedEvent 84 | 85 | if allEvents, ok := r.store[id]; ok { 86 | for _, event := range allEvents { 87 | if event.Version < fromVersion { 88 | continue 89 | } 90 | 91 | events = append(events, event) 92 | } 93 | 94 | return events, nil 95 | } 96 | 97 | return nil, errors.New("not found") 98 | } 99 | 100 | // SaveSnapshot ... 101 | func (r *InMemoryEventStreamRepository) SaveSnapshot(eventsourced EventSourced) error { 102 | r.eventSourcedStore[eventsourced.ID()] = eventsourced 103 | return nil 104 | } 105 | 106 | // GetSnapshot ... 107 | func (r *InMemoryEventStreamRepository) GetSnapshot(id string) (EventSourced, error) { 108 | value, ok := r.eventSourcedStore[id] 109 | 110 | if !ok { 111 | return nil, errors.New("not found") 112 | } 113 | 114 | return value, nil 115 | } 116 | -------------------------------------------------------------------------------- /type-registry.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | var methodHandlerPrefix = "Handle" 10 | 11 | // HandlersCache is a map of types to functions that will be used to route event sourcing events 12 | type HandlersCache map[reflect.Type]func(source interface{}, event interface{}) 13 | 14 | // TypeCache is a map of strings to reflect.Type structures 15 | type TypeCache map[string]reflect.Type 16 | 17 | // TypeRegistry providers a helper registry for mapping event types and handlers after performance json serializaton 18 | type TypeRegistry interface { 19 | GetHandlers(interface{}) HandlersCache 20 | GetTypeByName(string) (reflect.Type, bool) 21 | RegisterAggregate(aggregate interface{}, events ...interface{}) 22 | RegisterEvents(events ...interface{}) 23 | RegisterType(interface{}) 24 | } 25 | 26 | type defaultTypeRegistry struct { 27 | HandlersDirectory map[reflect.Type]HandlersCache 28 | Types TypeCache 29 | } 30 | 31 | var cachedRegistry *defaultTypeRegistry 32 | 33 | // NewTypeRegistry constructs a new TypeRegistry 34 | func NewTypeRegistry() TypeRegistry { 35 | return newTypeRegistry() 36 | } 37 | 38 | func newTypeRegistry() *defaultTypeRegistry { 39 | if cachedRegistry == nil { 40 | handlersDirectory := make(map[reflect.Type]HandlersCache, 0) 41 | types := make(TypeCache, 0) 42 | 43 | cachedRegistry = &defaultTypeRegistry{handlersDirectory, types} 44 | } 45 | 46 | return cachedRegistry 47 | } 48 | 49 | func (r *defaultTypeRegistry) GetHandlers(source interface{}) HandlersCache { 50 | sourceType := reflect.TypeOf(source) 51 | var handlers HandlersCache 52 | 53 | handlerChan := make(chan bool) 54 | quit := make(chan struct{}) 55 | 56 | defer close(quit) 57 | go func(handlers *HandlersCache) { 58 | select { 59 | case handlerChan <- internalGetHandlers(r, sourceType, source, handlers): 60 | //do nothing, this just blocks data race 61 | case <-quit: 62 | fmt.Println("quit") 63 | } 64 | }(&handlers) 65 | 66 | //wait for channel to do it's stuff 67 | <-handlerChan 68 | return handlers 69 | } 70 | 71 | func internalGetHandlers(r *defaultTypeRegistry, sourceType reflect.Type, source interface{}, handlers *HandlersCache) bool { 72 | if value, ok := r.HandlersDirectory[sourceType]; ok { 73 | *handlers = value 74 | } else { 75 | *handlers = createHandlersCache(source) 76 | r.HandlersDirectory[sourceType] = *handlers 77 | } 78 | 79 | return true 80 | } 81 | 82 | func (r *defaultTypeRegistry) GetTypeByName(typeName string) (reflect.Type, bool) { 83 | typeValue, ok := r.Types[typeName] 84 | return typeValue, ok 85 | } 86 | 87 | func (r *defaultTypeRegistry) RegisterType(source interface{}) { 88 | rawType := reflect.TypeOf(source) 89 | r.Types[rawType.String()] = rawType 90 | PackageLogger().Debugf("Type Registered - %s", rawType.String()) 91 | } 92 | 93 | func (r *defaultTypeRegistry) RegisterAggregate(aggregate interface{}, events ...interface{}) { 94 | r.RegisterType(aggregate) 95 | 96 | r.RegisterEvents(events) 97 | } 98 | 99 | func (r *defaultTypeRegistry) RegisterEvents(events ...interface{}) { 100 | for _, event := range events { 101 | r.RegisterType(event) 102 | } 103 | } 104 | 105 | func createHandlersCache(source interface{}) HandlersCache { 106 | sourceType := reflect.TypeOf(source) 107 | handlers := make(HandlersCache) 108 | 109 | methodCount := sourceType.NumMethod() 110 | for i := 0; i < methodCount; i++ { 111 | method := sourceType.Method(i) 112 | 113 | if strings.HasPrefix(method.Name, methodHandlerPrefix) { 114 | // func (source *MySource) HandleMyEvent(e MyEvent). 115 | if method.Type.NumIn() == 2 { 116 | eventType := method.Type.In(1) 117 | handler := func(source interface{}, event interface{}) { 118 | sourceValue := reflect.ValueOf(source) 119 | eventValue := reflect.ValueOf(event) 120 | 121 | method.Func.Call([]reflect.Value{sourceValue, eventValue}) 122 | } 123 | 124 | handlers[eventType] = handler 125 | } 126 | } 127 | } 128 | 129 | return handlers 130 | } 131 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= 2 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 3 | github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c h1:K4FIibkr4//ziZKOKmt4RL0YImuTjLLBtwElf+F2lSQ= 4 | github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= 5 | github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a h1:Y5XsLCEhtEI8qbD9RP3Qlv5FXdTDHxZM9UPUnMRgBp8= 6 | github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= 7 | github.com/couchbaselabs/go-couchbase v0.0.0-20190117181324-d904413d884d h1:lsBRLJe/ET6DjCaRblGwls80dOcOzhFVNJrO6uaMrMQ= 8 | github.com/couchbaselabs/go-couchbase v0.0.0-20190117181324-d904413d884d/go.mod h1:mby/05p8HE5yHEAKiIH/555NoblMs7PtW6NrYshDruc= 9 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 14 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 16 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 17 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 18 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 21 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 22 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 23 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= 27 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 28 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= 29 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 30 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= 31 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 32 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= 33 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 34 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 35 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 36 | github.com/streadway/amqp v0.0.0-20181205114330-a314942b2fd9 h1:37QTz/gdHBLQcsmgMTnQDSWCtKzJ7YnfI2M2yTdr4BQ= 37 | github.com/streadway/amqp v0.0.0-20181205114330-a314942b2fd9/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 40 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 41 | golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M= 42 | golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 43 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0= 44 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 45 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= 46 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 48 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | -------------------------------------------------------------------------------- /eventsourcing.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "time" 7 | ) 8 | 9 | // EventSourcingRepository is a repository for event source based aggregates 10 | type EventSourcingRepository interface { 11 | GetEventStreamRepository() EventStreamRepository 12 | GetTypeRegistry() TypeRegistry 13 | Save(EventSourced, string) ([]VersionedEvent, error) 14 | Get(string, EventSourced) error 15 | GetSnapshot(id string) (EventSourced, error) 16 | } 17 | 18 | // EventStreamRepository is a persistance layer for events associated with aggregates by ID 19 | type EventStreamRepository interface { 20 | VersionedEventPublicationLogger 21 | Save(string, []VersionedEvent) error 22 | Get(string, int) ([]VersionedEvent, error) 23 | SaveSnapshot(EventSourced) error 24 | GetSnapshot(string) (EventSourced, error) 25 | } 26 | 27 | type defaultEventSourcingRepository struct { 28 | Registry TypeRegistry 29 | EventRepository EventStreamRepository 30 | Publisher VersionedEventPublisher 31 | } 32 | 33 | // NewRepository constructs an EventSourcingRepository 34 | func NewRepository(eventStreamRepository EventStreamRepository, registry TypeRegistry) EventSourcingRepository { 35 | return NewRepositoryWithPublisher(eventStreamRepository, nil, registry) 36 | } 37 | 38 | // NewRepositoryWithPublisher constructs an EventSourcingRepository with a VersionedEventPublisher to dispatch events once persisted to the EventStreamRepository 39 | func NewRepositoryWithPublisher(eventStreamRepository EventStreamRepository, publisher VersionedEventPublisher, registry TypeRegistry) EventSourcingRepository { 40 | return defaultEventSourcingRepository{registry, eventStreamRepository, publisher} 41 | } 42 | 43 | func (r defaultEventSourcingRepository) GetEventStreamRepository() EventStreamRepository { 44 | return r.EventRepository 45 | } 46 | 47 | func (r defaultEventSourcingRepository) GetTypeRegistry() TypeRegistry { 48 | return r.Registry 49 | } 50 | 51 | func (r defaultEventSourcingRepository) Save(source EventSourced, correlationID string) ([]VersionedEvent, error) { 52 | id := source.ID() 53 | if len(correlationID) == 0 { 54 | correlationID = "cid:" + NewUUIDString() 55 | } 56 | 57 | saveSnapshot := source.WantsToSaveSnapshot() 58 | 59 | currentVersion := source.Version() + 1 60 | var latestVersion int 61 | var events []VersionedEvent 62 | for i, event := range source.Events() { 63 | eventType := reflect.TypeOf(event) 64 | latestVersion = currentVersion + i 65 | versionedEvent := VersionedEvent{ 66 | ID: "ve:" + NewUUIDString(), 67 | CorrelationID: correlationID, 68 | SourceID: id, 69 | Version: latestVersion, 70 | EventType: eventType.String(), 71 | Created: time.Now().UTC(), 72 | 73 | Event: event} 74 | 75 | events = append(events, versionedEvent) 76 | 77 | if latestVersion%5 == 0 { 78 | PackageLogger().Debugf("Latest version %v", latestVersion) 79 | saveSnapshot = true 80 | } 81 | 82 | source.SetVersion(latestVersion) 83 | } 84 | 85 | //PackageLogger().Debugf(stringhelper.PrintJSON("defaultEventSourcingRepository.Save() Ctx Here", ctx)) 86 | //PackageLogger().Debugf(stringhelper.PrintJSON("defaultEventSourcingRepository.Save() Events Here:", events)) 87 | //PackageLogger().Debugf(stringhelper.PrintJSON("Source looks like: ", source)) 88 | 89 | if len(events) > 0 { 90 | start := time.Now() 91 | if err := r.EventRepository.Save(id, events); err != nil { 92 | return nil, err 93 | } 94 | end := time.Now() 95 | PackageLogger().Debugf("defaultEventSourcingRepository.Save() - Save Events Took [%dms]", end.Sub(start)/time.Millisecond) 96 | } 97 | 98 | if saveSnapshot { 99 | // only save snapshot if actual aggregate events have been persisted (aka accepted)! 100 | saveSnap := func() { 101 | start := time.Now() 102 | PackageLogger().Debugf("Saving version %v", source.Version()) 103 | if err := r.EventRepository.SaveSnapshot(source); err != nil { 104 | PackageLogger().Debugf("Unable to save snapshot: %v", err) 105 | // Saving the snapshot is not critical. Continue with process... 106 | } 107 | end := time.Now() 108 | PackageLogger().Debugf("defaultEventSourcingRepository.Save() - Save Snapshot Took [%dms]", end.Sub(start)/time.Millisecond) 109 | 110 | } 111 | saveSnap() 112 | } 113 | 114 | if r.Publisher == nil { 115 | return nil, nil 116 | } 117 | 118 | start := time.Now() 119 | 120 | if err := r.Publisher.PublishEvents(events); err != nil { 121 | return nil, err 122 | } 123 | 124 | end := time.Now() 125 | PackageLogger().Debugf("defaultEventSourcingRepository.Save() - Publish Events Took [%dms]", end.Sub(start)/time.Millisecond) 126 | 127 | return events, nil 128 | } 129 | 130 | func (r defaultEventSourcingRepository) GetSnapshot(id string) (EventSourced, error) { 131 | // We don't need to error when we cant get the snapshot but lets at least record the issue. 132 | snapshot, err := r.EventRepository.GetSnapshot(id) 133 | if err != nil { 134 | PackageLogger().Debugf("eventsoucing: GetSnapshot(): Unable to load snapshot: [%s] %v", id, err) 135 | return nil, err 136 | } 137 | 138 | return snapshot, err 139 | } 140 | 141 | func (r defaultEventSourcingRepository) Get(id string, source EventSourced) error { 142 | 143 | PackageLogger().Debugf("defaultEventSourcingRepository.Get() - Get events from version %v", source.Version()) 144 | 145 | start := time.Now() 146 | events, err := r.EventRepository.Get(id, source.Version()+1) 147 | if err != nil { 148 | return err 149 | } 150 | end := time.Now() 151 | PackageLogger().Debugf("defaultEventSourcingRepository.Get() - Got %v events took [%dms]", len(events), end.Sub(start)/time.Millisecond) 152 | 153 | if events == nil { 154 | PackageLogger().Debugf("No events to process") 155 | return nil 156 | } 157 | 158 | start = time.Now() 159 | 160 | handlers := r.Registry.GetHandlers(source) 161 | for _, event := range events { 162 | eventType := reflect.TypeOf(event.Event) 163 | handler, ok := handlers[eventType] 164 | if !ok { 165 | errorMessage := "Cannot find handler for event type " + event.EventType 166 | PackageLogger().Debugf(errorMessage) 167 | return errors.New(errorMessage) 168 | } 169 | 170 | handler(source, event.Event) 171 | } 172 | 173 | source.SetVersion(events[len(events)-1].Version) 174 | 175 | end = time.Now() 176 | PackageLogger().Debugf("defaultEventSourcingRepository.Get() - Get Handlers Took [%dms]", end.Sub(start)/time.Millisecond) 177 | 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /cqrs-testreadmodels_test.go: -------------------------------------------------------------------------------- 1 | package cqrs_test 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/andrewwebber/cqrs" 8 | ) 9 | 10 | type AccountReadModel struct { 11 | ID string 12 | FirstName string 13 | LastName string 14 | EmailAddress string 15 | Balance float64 16 | } 17 | 18 | func (account *AccountReadModel) String() string { 19 | return fmt.Sprintf("AccountModel::Account %s with Email Address %s has balance %f", account.ID, account.EmailAddress, account.Balance) 20 | } 21 | 22 | type ReadModelAccounts struct { 23 | accounts map[string]*AccountReadModel 24 | lock sync.RWMutex 25 | } 26 | 27 | func (model *ReadModelAccounts) String() string { 28 | model.lock.RLock() 29 | defer model.lock.RUnlock() 30 | 31 | result := "Account Model::" 32 | 33 | for key := range model.accounts { 34 | result += model.accounts[key].String() + "\n" 35 | } 36 | 37 | return result 38 | } 39 | 40 | func NewReadModelAccounts() *ReadModelAccounts { 41 | return &ReadModelAccounts{make(map[string]*AccountReadModel), sync.RWMutex{}} 42 | } 43 | 44 | func NewReadModelAccountsFromHistory(events []cqrs.VersionedEvent) (*ReadModelAccounts, error) { 45 | publisher := NewReadModelAccounts() 46 | if error := publisher.UpdateViewModel(events); error != nil { 47 | return nil, error 48 | } 49 | 50 | return publisher, nil 51 | } 52 | 53 | func (model *ReadModelAccounts) GetAccount(id string) *AccountReadModel { 54 | model.lock.RLock() 55 | defer model.lock.RUnlock() 56 | account, _ := model.accounts[id] 57 | return account 58 | } 59 | 60 | func (model *ReadModelAccounts) UpdateViewModel(events []cqrs.VersionedEvent) error { 61 | model.lock.Lock() 62 | defer model.lock.Unlock() 63 | 64 | for _, event := range events { 65 | cqrs.PackageLogger().Debugf("Accounts Model received event : " + event.EventType) 66 | switch event.Event.(type) { 67 | default: 68 | return nil 69 | case AccountCreatedEvent: 70 | model.UpdateViewModelOnAccountCreatedEvent(event.SourceID, event.Event.(AccountCreatedEvent)) 71 | case AccountCreditedEvent: 72 | model.UpdateViewModelOnAccountCreditedEvent(event.SourceID, event.Event.(AccountCreditedEvent)) 73 | case AccountDebitedEvent: 74 | model.UpdateViewModelOnAccountDebitedEvent(event.SourceID, event.Event.(AccountDebitedEvent)) 75 | case EmailAddressChangedEvent: 76 | model.UpdateViewModelOnEmailAddressChangedEvent(event.SourceID, event.Event.(EmailAddressChangedEvent)) 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (model *ReadModelAccounts) UpdateViewModelOnAccountCreatedEvent(accountID string, event AccountCreatedEvent) { 84 | model.accounts[accountID] = &AccountReadModel{accountID, event.FirstName, event.LastName, event.EmailAddress, event.InitialBalance} 85 | } 86 | 87 | func (model *ReadModelAccounts) UpdateViewModelOnAccountCreditedEvent(accountID string, event AccountCreditedEvent) { 88 | if model.accounts[accountID] == nil { 89 | cqrs.PackageLogger().Debugf("Could not find account with ID " + accountID) 90 | return 91 | } 92 | 93 | model.accounts[accountID].Balance += event.Amount 94 | } 95 | 96 | func (model *ReadModelAccounts) UpdateViewModelOnAccountDebitedEvent(accountID string, event AccountDebitedEvent) { 97 | if model.accounts[accountID] == nil { 98 | cqrs.PackageLogger().Debugf("Could not find account with ID " + accountID) 99 | return 100 | } 101 | 102 | model.accounts[accountID].Balance -= event.Amount 103 | } 104 | 105 | func (model *ReadModelAccounts) UpdateViewModelOnEmailAddressChangedEvent(accountID string, event EmailAddressChangedEvent) { 106 | if model.accounts[accountID] == nil { 107 | cqrs.PackageLogger().Debugf("Could not find account with ID " + accountID) 108 | return 109 | } 110 | 111 | model.accounts[accountID].EmailAddress = event.NewEmailAddress 112 | } 113 | 114 | type User struct { 115 | ID string 116 | FirstName string 117 | LastName string 118 | EmailAddress string 119 | PasswordHash []byte 120 | } 121 | 122 | func (user *User) String() string { 123 | return fmt.Sprintf("UserModel::User %s with Email Address %s and Password Hash %v", user.ID, user.EmailAddress, user.PasswordHash) 124 | } 125 | 126 | type UsersModel struct { 127 | Users map[string]*User 128 | lock sync.Mutex 129 | } 130 | 131 | func (model *UsersModel) String() string { 132 | model.lock.Lock() 133 | defer model.lock.Unlock() 134 | 135 | result := "User Model::" 136 | for key := range model.Users { 137 | result += model.Users[key].String() + "\n" 138 | } 139 | 140 | return result 141 | } 142 | 143 | func NewUsersModel() *UsersModel { 144 | return &UsersModel{make(map[string]*User), sync.Mutex{}} 145 | } 146 | 147 | func NewUsersModelFromHistory(events []cqrs.VersionedEvent) (*UsersModel, error) { 148 | publisher := NewUsersModel() 149 | if error := publisher.UpdateViewModel(events); error != nil { 150 | return nil, error 151 | } 152 | 153 | return publisher, nil 154 | } 155 | 156 | func (model *UsersModel) UpdateViewModel(events []cqrs.VersionedEvent) error { 157 | model.lock.Lock() 158 | defer model.lock.Unlock() 159 | 160 | for _, event := range events { 161 | cqrs.PackageLogger().Debugf("User Model received event : ", event.EventType) 162 | switch event.Event.(type) { 163 | default: 164 | return nil 165 | case AccountCreatedEvent: 166 | model.UpdateViewModelOnAccountCreatedEvent(event.SourceID, event.Event.(AccountCreatedEvent)) 167 | case EmailAddressChangedEvent: 168 | model.UpdateViewModelOnEmailAddressChangedEvent(event.SourceID, event.Event.(EmailAddressChangedEvent)) 169 | case PasswordChangedEvent: 170 | model.UpdateViewModelOnPasswordChangedEvent(event.SourceID, event.Event.(PasswordChangedEvent)) 171 | } 172 | } 173 | 174 | return nil 175 | } 176 | 177 | func (model *UsersModel) UpdateViewModelOnAccountCreatedEvent(accountID string, event AccountCreatedEvent) { 178 | model.Users[accountID] = &User{accountID, event.FirstName, event.LastName, event.EmailAddress, event.PasswordHash} 179 | } 180 | 181 | func (model *UsersModel) UpdateViewModelOnEmailAddressChangedEvent(accountID string, event EmailAddressChangedEvent) { 182 | 183 | if model.Users[accountID] == nil { 184 | return 185 | } 186 | 187 | model.Users[accountID].EmailAddress = event.NewEmailAddress 188 | } 189 | 190 | func (model *UsersModel) UpdateViewModelOnPasswordChangedEvent(accountID string, event PasswordChangedEvent) { 191 | 192 | if model.Users[accountID] == nil { 193 | return 194 | } 195 | 196 | model.Users[accountID].PasswordHash = event.NewPasswordHash 197 | } 198 | -------------------------------------------------------------------------------- /cqrs-testentities_test.go: -------------------------------------------------------------------------------- 1 | package cqrs_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/andrewwebber/cqrs" 9 | 10 | "golang.org/x/crypto/bcrypt" 11 | ) 12 | 13 | type ChangeEmailAddressCommand struct { 14 | AccountID string 15 | NewEmailAddress string 16 | } 17 | 18 | type CreateAccountCommand struct { 19 | FirstName string 20 | LastName string 21 | EmailAddress string 22 | PasswordHash []byte 23 | InitialBalance float64 24 | } 25 | 26 | type ChangePasswordCommand struct { 27 | AccountID string 28 | NewPassword string 29 | } 30 | 31 | type CreditAccountCommand struct { 32 | AccountID string 33 | Amount float64 34 | } 35 | 36 | type DebitAccountCommand struct { 37 | AccountID string 38 | Amount float64 39 | } 40 | 41 | type AccountCreatedEvent struct { 42 | FirstName string 43 | LastName string 44 | EmailAddress string 45 | PasswordHash []byte 46 | InitialBalance float64 47 | } 48 | 49 | type EmailAddressChangedEvent struct { 50 | PreviousEmailAddress string 51 | NewEmailAddress string 52 | } 53 | 54 | type PasswordChangedEvent struct { 55 | NewPasswordHash []byte 56 | } 57 | 58 | type AccountCreditedEvent struct { 59 | Amount float64 60 | } 61 | 62 | type AccountDebitedEvent struct { 63 | Amount float64 64 | } 65 | 66 | type Account struct { 67 | cqrs.EventSourceBased 68 | 69 | FirstName string 70 | LastName string 71 | EmailAddress string 72 | PasswordHash []byte 73 | Balance float64 74 | } 75 | 76 | func (account *Account) CopyFrom(source interface{}) { 77 | 78 | account.FirstName = reflect.Indirect(reflect.ValueOf(source)).FieldByName("FirstName").Interface().(string) 79 | account.LastName = reflect.Indirect(reflect.ValueOf(source)).FieldByName("LastName").Interface().(string) 80 | account.EmailAddress = reflect.Indirect(reflect.ValueOf(source)).FieldByName("EmailAddress").Interface().(string) 81 | account.PasswordHash = reflect.Indirect(reflect.ValueOf(source)).FieldByName("PasswordHash").Interface().([]byte) 82 | account.Balance = reflect.Indirect(reflect.ValueOf(source)).FieldByName("Balance").Interface().(float64) 83 | 84 | /*cqrs.PackageLogger().Debugf("valueOfSource", valueOfSource) 85 | fieldValue := valueOfSource 86 | cqrs.PackageLogger().Debugf("fieldValue", fieldValue) 87 | fieldValueInterface := fieldValue 88 | cqrs.PackageLogger().Debugf("fieldValueInterface", fieldValueInterface) 89 | firstName := fieldValueInterface.(string) 90 | cqrs.PackageLogger().Debugf("firstName", firstName)*/ 91 | 92 | /*account.LastName = reflect.Indirect(reflect.ValueOf(source).FieldByName("LastName")).Interface().(string) 93 | account.EmailAddress = reflect.Indirect(reflect.ValueOf(source).FieldByName("EmailAddress")).Interface().(string) 94 | account.PasswordHash = reflect.Indirect(reflect.ValueOf(source).FieldByName("PasswordHash")).Interface().([]byte) 95 | account.Balance = reflect.Indirect(reflect.ValueOf(source).FieldByName("FirstName")).Interface().(float64)*/ 96 | } 97 | 98 | func (account *Account) String() string { 99 | return fmt.Sprintf("Account %s with Email Address %s has balance %f", account.ID(), account.EmailAddress, account.Balance) 100 | } 101 | 102 | func NewAccount(firstName string, lastName string, emailAddress string, passwordHash []byte, initialBalance float64) *Account { 103 | account := new(Account) 104 | account.EventSourceBased = cqrs.NewEventSourceBased(account) 105 | 106 | event := AccountCreatedEvent{firstName, lastName, emailAddress, passwordHash, initialBalance} 107 | account.Update(event) 108 | return account 109 | } 110 | 111 | func NewAccountFromHistory(id string, repository cqrs.EventSourcingRepository) (*Account, error) { 112 | account := new(Account) 113 | account.EventSourceBased = cqrs.NewEventSourceBasedWithID(account, id) 114 | 115 | snapshot, err := repository.GetSnapshot(id) 116 | if err == nil { 117 | cqrs.PackageLogger().Debugf("Loaded snapshot: %+v", snapshot) 118 | account.SetVersion(snapshot.Version()) 119 | account.CopyFrom(snapshot) 120 | cqrs.PackageLogger().Debugf("Updated account: %+v ", account) 121 | } 122 | 123 | if err := repository.Get(id, account); err != nil { 124 | return nil, err 125 | } 126 | 127 | cqrs.PackageLogger().Debugf("Loaded account: %+v", account) 128 | 129 | return account, nil 130 | } 131 | 132 | func (account *Account) HandleAccountCreatedEvent(event AccountCreatedEvent) { 133 | account.EmailAddress = event.EmailAddress 134 | account.FirstName = event.FirstName 135 | account.LastName = event.LastName 136 | account.PasswordHash = event.PasswordHash 137 | } 138 | 139 | func (account *Account) ChangeEmailAddress(newEmailAddress string) error { 140 | if len(newEmailAddress) < 1 { 141 | return errors.New("invalid newEmailAddress length") 142 | } 143 | 144 | account.Update(EmailAddressChangedEvent{account.EmailAddress, newEmailAddress}) 145 | return nil 146 | } 147 | 148 | func (account *Account) HandleEmailAddressChangedEvent(event EmailAddressChangedEvent) { 149 | account.EmailAddress = event.NewEmailAddress 150 | } 151 | 152 | func (account *Account) CheckPassword(password string) bool { 153 | 154 | passwordBytes := []byte(password) 155 | 156 | // Comparing the password with the hash 157 | err := bcrypt.CompareHashAndPassword(account.PasswordHash, passwordBytes) 158 | fmt.Println(err) // nil means it is a match 159 | 160 | return err == nil 161 | } 162 | 163 | func (account *Account) ChangePassword(newPassword string) error { 164 | if len(newPassword) < 1 { 165 | return errors.New("invalid newPassword length") 166 | } 167 | 168 | hashedPassword, err := GetHashForPassword(newPassword) 169 | if err != nil { 170 | return (err) 171 | } 172 | 173 | account.Update(PasswordChangedEvent{hashedPassword}) 174 | 175 | return nil 176 | } 177 | 178 | func GetHashForPassword(password string) ([]byte, error) { 179 | passwordBytes := []byte(password) 180 | // Hashing the password with the cost of 10 181 | hashedPassword, err := bcrypt.GenerateFromPassword(passwordBytes, 10) 182 | if err != nil { 183 | cqrs.PackageLogger().Debugf("Error getting password hash: ", err) 184 | return nil, err 185 | } 186 | 187 | fmt.Println(string(hashedPassword)) 188 | 189 | return hashedPassword, nil 190 | } 191 | 192 | func (account *Account) HandlePasswordChangedEvent(event PasswordChangedEvent) { 193 | account.PasswordHash = event.NewPasswordHash 194 | } 195 | 196 | func (account *Account) Credit(amount float64) error { 197 | if amount <= 0 { 198 | return errors.New("invalid amount - negative credits not supported") 199 | } 200 | 201 | account.Update(AccountCreditedEvent{amount}) 202 | 203 | return nil 204 | } 205 | 206 | func (account *Account) HandleAccountCredited(event AccountCreditedEvent) { 207 | account.Balance += event.Amount 208 | } 209 | 210 | func (account *Account) Debit(amount float64) error { 211 | if amount <= 0 { 212 | return errors.New("invalid amount - negative credits not supported") 213 | } 214 | 215 | if projection := account.Balance - amount; projection < 0 { 216 | return errors.New("negative balance not supported") 217 | } 218 | 219 | account.Update(AccountDebitedEvent{amount}) 220 | 221 | return nil 222 | } 223 | 224 | func (account *Account) HandleAccountDebitedEvent(event AccountDebitedEvent) { 225 | account.Balance -= event.Amount 226 | } 227 | -------------------------------------------------------------------------------- /couchbase/provider.go: -------------------------------------------------------------------------------- 1 | package couchbase 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "reflect" 9 | "sort" 10 | "strings" 11 | "time" 12 | 13 | "github.com/andrewwebber/cqrs" 14 | 15 | couchbase "github.com/couchbaselabs/go-couchbase" 16 | ) 17 | 18 | type cbVersionedEvent struct { 19 | ID string `json:"id"` 20 | CorrelationID string `json:"correlationID"` 21 | SourceID string `json:"sourceID"` 22 | Version int `json:"version"` 23 | EventType string `json:"eventType"` 24 | Created time.Time `json:"time"` 25 | Event json.RawMessage 26 | } 27 | 28 | // EventStreamRepository : a Couchbase Server event stream repository 29 | type EventStreamRepository struct { 30 | bucket *couchbase.Bucket 31 | cbPrefix string 32 | } 33 | 34 | // NewEventStreamRepository creates new Couchbase Server based event stream repository 35 | func NewEventStreamRepository(connectionString string, poolName string, bucketName string, prefix string) (*EventStreamRepository, error) { 36 | c, err := couchbase.Connect(connectionString) 37 | if err != nil { 38 | log.Println(fmt.Sprintf("Error connecting to couchbase : %v", err)) 39 | return nil, err 40 | } 41 | 42 | pool, err := c.GetPool(poolName) 43 | if err != nil { 44 | log.Println(fmt.Sprintf("Error getting pool: %v", err)) 45 | return nil, err 46 | } 47 | 48 | bucket, err := pool.GetBucket(bucketName) 49 | if err != nil { 50 | log.Println(fmt.Sprintf("Error getting bucket: %v", err)) 51 | return nil, err 52 | } 53 | 54 | return &EventStreamRepository{bucket, prefix}, nil 55 | } 56 | 57 | // Save persists an event sourced object into the repository 58 | func (r *EventStreamRepository) Save(sourceID string, events []cqrs.VersionedEvent) error { 59 | latestVersion := events[len(events)-1].Version 60 | for _, versionedEvent := range events { 61 | key := fmt.Sprintf("%s:%s:%d", r.cbPrefix, sourceID, versionedEvent.Version) 62 | added, err := r.bucket.Add(key, 0, versionedEvent) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | if !added { 68 | return cqrs.ErrConcurrencyWhenSavingEvents 69 | } 70 | 71 | if err := r.SaveIntegrationEvent(versionedEvent); err != nil { 72 | return err 73 | } 74 | } 75 | 76 | cbKey := fmt.Sprintf("%s:%s", r.cbPrefix, sourceID) 77 | return r.bucket.Set(cbKey, 0, latestVersion) 78 | } 79 | 80 | // SaveIntegrationEvent persists a published integration event 81 | func (r *EventStreamRepository) SaveIntegrationEvent(event cqrs.VersionedEvent) error { 82 | counter, err := r.bucket.Incr("eventstore:integration", 1, 1, 0) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | key := fmt.Sprintf("eventstore:integration:%d", counter) 88 | if err = r.bucket.Set(key, 0, event); err != nil { 89 | return err 90 | } 91 | 92 | var eventsByCorrelationID map[string]cqrs.VersionedEvent 93 | correlationKey := "eventstore:correlation:" + event.CorrelationID 94 | if err := r.bucket.Get(correlationKey, &eventsByCorrelationID); err != nil { 95 | if IsNotFoundError(err) { 96 | eventsByCorrelationID = make(map[string]cqrs.VersionedEvent) 97 | } else { 98 | return err 99 | } 100 | } 101 | 102 | eventsByCorrelationID[event.ID] = event 103 | 104 | if err := r.bucket.Set(correlationKey, 0, eventsByCorrelationID); err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // GetIntegrationEventsByCorrelationID returns all integration events by correlation ID 112 | func (r *EventStreamRepository) GetIntegrationEventsByCorrelationID(correlationID string) ([]cqrs.VersionedEvent, error) { 113 | var eventsByCorrelationID map[string]cbVersionedEvent 114 | correlationKey := "eventstore:correlation:" + correlationID 115 | if err := r.bucket.Get(correlationKey, &eventsByCorrelationID); err != nil { 116 | return nil, err 117 | } 118 | 119 | typeRegistry := cqrs.NewTypeRegistry() 120 | var events []cqrs.VersionedEvent 121 | for _, raw := range eventsByCorrelationID { 122 | eventType, ok := typeRegistry.GetTypeByName(raw.EventType) 123 | if !ok { 124 | log.Println("Cannot find event type", raw.EventType) 125 | return nil, errors.New("Cannot find event type " + raw.EventType) 126 | } 127 | 128 | eventValue := reflect.New(eventType) 129 | event := eventValue.Interface() 130 | if err := json.Unmarshal(raw.Event, event); err != nil { 131 | log.Println("Error deserializing event ", raw.Event) 132 | return nil, err 133 | } 134 | 135 | versionedEvent := cqrs.VersionedEvent{ 136 | ID: raw.ID, 137 | SourceID: raw.SourceID, 138 | Version: raw.Version, 139 | EventType: raw.EventType, 140 | Created: raw.Created, 141 | Event: reflect.Indirect(eventValue).Interface()} 142 | 143 | events = append(events, versionedEvent) 144 | } 145 | 146 | return events, nil 147 | } 148 | 149 | // AllIntegrationEventsEverPublished retreives all events every persisted 150 | func (r *EventStreamRepository) AllIntegrationEventsEverPublished() ([]cqrs.VersionedEvent, error) { 151 | var counter int 152 | if err := r.bucket.Get("integration", &counter); err != nil { 153 | return nil, err 154 | } 155 | 156 | var result []cqrs.VersionedEvent 157 | for i := 0; i < counter; i++ { 158 | key := fmt.Sprintf("integration::%d", i) 159 | events, err := r.Get(key, 0) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | for _, event := range events { 165 | result = append(result, event) 166 | } 167 | } 168 | 169 | sort.Sort(cqrs.ByCreated(result)) 170 | 171 | return result, nil 172 | } 173 | 174 | func (r *EventStreamRepository) GetSnapshot(id string) (cqrs.EventSourced, error) { 175 | return nil, nil 176 | } 177 | 178 | func (r *EventStreamRepository) SaveSnapshot(eventsourced cqrs.EventSourced) error { 179 | return nil 180 | } 181 | 182 | // Get retrieves events assoicated with an event sourced object by ID 183 | func (r *EventStreamRepository) Get(id string, fromVersion int) ([]cqrs.VersionedEvent, error) { 184 | var version int 185 | cbKey := fmt.Sprintf("%s:%s", r.cbPrefix, id) 186 | if error := r.bucket.Get(cbKey, &version); error != nil { 187 | log.Println("Error getting event source ", id) 188 | return nil, error 189 | } 190 | 191 | var events []cqrs.VersionedEvent 192 | for versionNumber := 1; versionNumber <= version; versionNumber++ { 193 | eventKey := fmt.Sprintf("%s:%s:%d", r.cbPrefix, id, versionNumber) 194 | raw := new(cbVersionedEvent) 195 | 196 | if error := r.bucket.Get(eventKey, raw); error != nil { 197 | log.Println("Error getting event :", eventKey) 198 | return nil, error 199 | } 200 | 201 | typeRegistry := cqrs.NewTypeRegistry() 202 | 203 | eventType, ok := typeRegistry.GetTypeByName(raw.EventType) 204 | if !ok { 205 | log.Println("Cannot find event type", raw.EventType) 206 | return nil, errors.New("Cannot find event type " + raw.EventType) 207 | } 208 | 209 | eventValue := reflect.New(eventType) 210 | event := eventValue.Interface() 211 | if err := json.Unmarshal(raw.Event, event); err != nil { 212 | log.Println("Error deserializing event ", raw.Event) 213 | return nil, err 214 | } 215 | 216 | versionedEvent := cqrs.VersionedEvent{ 217 | ID: raw.ID, 218 | SourceID: raw.SourceID, 219 | Version: raw.Version, 220 | EventType: raw.EventType, 221 | Created: raw.Created, 222 | Event: reflect.Indirect(eventValue).Interface()} 223 | 224 | events = append(events, versionedEvent) 225 | } 226 | 227 | return events, nil 228 | } 229 | 230 | // NotFound error string returned from couchbase when a key cannot be found 231 | const NotFound string = "Not found" 232 | 233 | // IsNotFoundError checks if we get a key not found error from couchbase 234 | func IsNotFoundError(err error) bool { 235 | // No error? 236 | if err == nil { 237 | return false 238 | } 239 | 240 | return strings.Contains(err.Error(), NotFound) 241 | } 242 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | import ( 4 | "reflect" 5 | "time" 6 | ) 7 | 8 | // Command represents an actor intention to alter the state of the system 9 | type Command struct { 10 | MessageID string `json:"messageID"` 11 | CorrelationID string `json:"correlationID"` 12 | CommandType string `json:"commandType"` 13 | Created time.Time `json:"time"` 14 | Body interface{} 15 | } 16 | 17 | // CreateCommand is a helper for creating a new command object with populated default properties 18 | func CreateCommand(body interface{}) Command { 19 | commandType := reflect.TypeOf(body) 20 | return Command{MessageID: "mid:" + NewUUIDString(), 21 | CorrelationID: "cid:" + NewUUIDString(), 22 | CommandType: commandType.String(), 23 | Created: time.Now(), 24 | 25 | Body: body} 26 | } 27 | 28 | // CreateCommandWithCorrelationID is a helper for creating a new command object with populated default properties 29 | func CreateCommandWithCorrelationID(body interface{}, correlationID string) Command { 30 | commandType := reflect.TypeOf(body) 31 | return Command{MessageID: "mid:" + NewUUIDString(), 32 | CorrelationID: correlationID, 33 | CommandType: commandType.String(), 34 | Created: time.Now(), 35 | Body: body} 36 | } 37 | 38 | // CommandPublisher is responsilbe for publishing commands 39 | type CommandPublisher interface { 40 | PublishCommands([]Command) error 41 | } 42 | 43 | // CommandReceiver is responsible for receiving commands 44 | type CommandReceiver interface { 45 | ReceiveCommands(CommandReceiverOptions) error 46 | } 47 | 48 | // CommandBus ... 49 | type CommandBus interface { 50 | CommandReceiver 51 | CommandPublisher 52 | } 53 | 54 | // CommandDispatchManager is responsible for coordinating receiving messages from command receivers and dispatching them to the command dispatcher. 55 | type CommandDispatchManager struct { 56 | commandDispatcher *MapBasedCommandDispatcher 57 | typeRegistry TypeRegistry 58 | receiver CommandReceiver 59 | } 60 | 61 | // CommandDispatcher the internal command dispatcher 62 | func (m *CommandDispatchManager) CommandDispatcher() CommandDispatcher { 63 | return m.commandDispatcher 64 | } 65 | 66 | // CommandReceiverOptions is an initalization structure to communicate to and from a command receiver go routine 67 | type CommandReceiverOptions struct { 68 | TypeRegistry TypeRegistry 69 | Close chan chan error 70 | Error chan error 71 | ReceiveCommand CommandHandler 72 | Exclusive bool 73 | ListenerCount int 74 | } 75 | 76 | // CommandTransactedAccept is the message routed from a command receiver to the command manager. 77 | // Sometimes command receivers designed with reliable delivery require acknowledgements after a message has been received. The success channel here allows for such acknowledgements 78 | type CommandTransactedAccept struct { 79 | Command Command 80 | ProcessedSuccessfully chan bool 81 | } 82 | 83 | // CommandDispatcher is responsible for routing commands from the command manager to call handlers responsible for processing received commands 84 | type CommandDispatcher interface { 85 | DispatchCommand(Command) error 86 | RegisterCommandHandler(event interface{}, handler CommandHandler) 87 | RegisterGlobalHandler(handler CommandHandler) 88 | } 89 | 90 | // CommandHandler is a function that takes a command 91 | type CommandHandler func(Command) error 92 | 93 | // MapBasedCommandDispatcher is a simple implementation of the command dispatcher. Using a map it registered command handlers to command types 94 | type MapBasedCommandDispatcher struct { 95 | registry map[reflect.Type][]CommandHandler 96 | globalHandlers []CommandHandler 97 | } 98 | 99 | // NewMapBasedCommandDispatcher is a constructor for the MapBasedVersionedCommandDispatcher 100 | func NewMapBasedCommandDispatcher() *MapBasedCommandDispatcher { 101 | registry := make(map[reflect.Type][]CommandHandler) 102 | return &MapBasedCommandDispatcher{registry, []CommandHandler{}} 103 | } 104 | 105 | // RegisterCommandHandler allows a caller to register a command handler given a command of the specified type being received 106 | func (m *MapBasedCommandDispatcher) RegisterCommandHandler(command interface{}, handler CommandHandler) { 107 | commandType := reflect.TypeOf(command) 108 | handlers, ok := m.registry[commandType] 109 | if ok { 110 | m.registry[commandType] = append(handlers, handler) 111 | } else { 112 | m.registry[commandType] = []CommandHandler{handler} 113 | } 114 | } 115 | 116 | // RegisterGlobalHandler allows a caller to register a wildcard command handler call on any command received 117 | func (m *MapBasedCommandDispatcher) RegisterGlobalHandler(handler CommandHandler) { 118 | m.globalHandlers = append(m.globalHandlers, handler) 119 | } 120 | 121 | // DispatchCommand executes all command handlers registered for the given command type 122 | func (m *MapBasedCommandDispatcher) DispatchCommand(command Command) error { 123 | bodyType := reflect.TypeOf(command.Body) 124 | if handlers, ok := m.registry[bodyType]; ok { 125 | for _, handler := range handlers { 126 | if err := handler(command); err != nil { 127 | metricsCommandsFailed.WithLabelValues(command.CommandType).Inc() 128 | return err 129 | } 130 | } 131 | } 132 | 133 | for _, handler := range m.globalHandlers { 134 | if err := handler(command); err != nil { 135 | metricsCommandsFailed.WithLabelValues(command.CommandType).Inc() 136 | return err 137 | } 138 | } 139 | 140 | metricsCommandsDispatched.WithLabelValues(command.CommandType).Inc() 141 | 142 | return nil 143 | } 144 | 145 | // NewCommandDispatchManager is a constructor for the CommandDispatchManager 146 | func NewCommandDispatchManager(receiver CommandReceiver, registry TypeRegistry) *CommandDispatchManager { 147 | return &CommandDispatchManager{NewMapBasedCommandDispatcher(), registry, receiver} 148 | } 149 | 150 | // RegisterCommandHandler allows a caller to register a command handler given a command of the specified type being received 151 | func (m *CommandDispatchManager) RegisterCommandHandler(command interface{}, handler CommandHandler) { 152 | m.typeRegistry.RegisterType(command) 153 | m.commandDispatcher.RegisterCommandHandler(command, handler) 154 | } 155 | 156 | // RegisterGlobalHandler allows a caller to register a wildcard command handler call on any command received 157 | func (m *CommandDispatchManager) RegisterGlobalHandler(handler CommandHandler) { 158 | m.commandDispatcher.RegisterGlobalHandler(handler) 159 | } 160 | 161 | // Listen starts a listen loop processing channels related to new incoming events, errors and stop listening requests 162 | func (m *CommandDispatchManager) Listen(stop <-chan bool, exclusive bool, listenerCount int) error { 163 | // Create communication channels 164 | // 165 | // for closing the queue listener, 166 | closeChannel := make(chan chan error) 167 | // receiving errors from the listener thread (go routine) 168 | errorChannel := make(chan error) 169 | 170 | // Command received channel receives a result with a channel to respond to, signifying successful processing of the message. 171 | // This should eventually call a command handler. See cqrs.NewVersionedCommandDispatcher() 172 | receiveCommandHandler := func(command Command) error { 173 | PackageLogger().Debugf("CommandDispatchManager.DispatchCommand: %v", command.CorrelationID) 174 | err := m.commandDispatcher.DispatchCommand(command) 175 | if err != nil { 176 | PackageLogger().Debugf("Error dispatching command: %v", err) 177 | } 178 | 179 | return err 180 | } 181 | 182 | // Start receiving commands by passing these channels to the worker thread (go routine) 183 | options := CommandReceiverOptions{m.typeRegistry, closeChannel, errorChannel, receiveCommandHandler, exclusive, listenerCount} 184 | if err := m.receiver.ReceiveCommands(options); err != nil { 185 | return err 186 | } 187 | go func() { 188 | for { 189 | // Wait on multiple channels using the select control flow. 190 | select { 191 | case <-stop: 192 | PackageLogger().Debugf("CommandDispatchManager.Stopping") 193 | closeSignal := make(chan error) 194 | closeChannel <- closeSignal 195 | PackageLogger().Debugf("CommandDispatchManager.Stopped") 196 | <-closeSignal 197 | // Receiving on this channel signifys an error has occured worker processor side 198 | case err := <-errorChannel: 199 | PackageLogger().Debugf("CommandDispatchManager.ErrorReceived: %s", err) 200 | 201 | } 202 | } 203 | }() 204 | 205 | return nil 206 | } 207 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package cqrs 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "time" 7 | ) 8 | 9 | // ErrConcurrencyWhenSavingEvents is raised when a concurrency error has occured when saving events 10 | var ErrConcurrencyWhenSavingEvents = errors.New("concurrency error saving event") 11 | 12 | // ErrNonePendingWhenSavingEvents is raised when a save is issued but no events are pending for the eventsourced entity. 13 | var ErrNonePendingWhenSavingEvents = errors.New("no events pending error saving event") 14 | 15 | // VersionedEvent represents an event in the past for an aggregate 16 | type VersionedEvent struct { 17 | ID string `json:"id"` 18 | CorrelationID string `json:"correlationID"` 19 | SourceID string `json:"sourceID"` 20 | Actor string `json:"actor"` 21 | OnBehalfOf string `json:"onbehalfof"` 22 | Version int `json:"version"` 23 | EventType string `json:"eventType"` 24 | Created time.Time `json:"time"` 25 | Event interface{} 26 | } 27 | 28 | // ByCreated is an alias for sorting VersionedEvents by the create field 29 | type ByCreated []VersionedEvent 30 | 31 | func (c ByCreated) Len() int { return len(c) } 32 | func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 33 | func (c ByCreated) Less(i, j int) bool { return c[i].Created.Before(c[j].Created) } 34 | 35 | // VersionedEventPublicationLogger is responsible to retreiving all events ever published to facilitate readmodel reconstruction 36 | type VersionedEventPublicationLogger interface { 37 | SaveIntegrationEvent(VersionedEvent) error 38 | AllIntegrationEventsEverPublished() ([]VersionedEvent, error) 39 | GetIntegrationEventsByCorrelationID(correlationID string) ([]VersionedEvent, error) 40 | } 41 | 42 | // VersionedEventPublisher is responsible for publishing events that have been saved to the event store\repository 43 | type VersionedEventPublisher interface { 44 | PublishEvents([]VersionedEvent) error 45 | } 46 | 47 | // VersionedEventReceiver is responsible for receiving globally published events 48 | type VersionedEventReceiver interface { 49 | ReceiveEvents(VersionedEventReceiverOptions) error 50 | } 51 | 52 | // EventBus ... 53 | type EventBus interface { 54 | VersionedEventPublisher 55 | VersionedEventReceiver 56 | } 57 | 58 | // VersionedEventDispatchManager is responsible for coordinating receiving messages from event receivers and dispatching them to the event dispatcher. 59 | type VersionedEventDispatchManager struct { 60 | versionedEventDispatcher *MapBasedVersionedEventDispatcher 61 | typeRegistry TypeRegistry 62 | receiver VersionedEventReceiver 63 | } 64 | 65 | // VersionedEventDispatcher the internal versioned event dispatcher 66 | func (m *VersionedEventDispatchManager) VersionedEventDispatcher() VersionedEventDispatcher { 67 | return m.versionedEventDispatcher 68 | } 69 | 70 | // VersionedEventTransactedAccept is the message routed from an event receiver to the event manager. 71 | // Sometimes event receivers designed with reliable delivery require acknowledgements after a message has been received. The success channel here allows for such acknowledgements 72 | type VersionedEventTransactedAccept struct { 73 | Event VersionedEvent 74 | ProcessedSuccessfully chan bool 75 | } 76 | 77 | // VersionedEventReceiverOptions is an initalization structure to communicate to and from an event receiver go routine 78 | type VersionedEventReceiverOptions struct { 79 | TypeRegistry TypeRegistry 80 | Close chan chan error 81 | Error chan error 82 | ReceiveEvent VersionedEventHandler 83 | Exclusive bool 84 | ListenerCount int 85 | } 86 | 87 | // VersionedEventDispatcher is responsible for routing events from the event manager to call handlers responsible for processing received events 88 | type VersionedEventDispatcher interface { 89 | DispatchEvent(VersionedEvent) error 90 | RegisterEventHandler(event interface{}, handler VersionedEventHandler) 91 | RegisterGlobalHandler(handler VersionedEventHandler) 92 | } 93 | 94 | // MapBasedVersionedEventDispatcher is a simple implementation of the versioned event dispatcher. Using a map it registered event handlers to event types 95 | type MapBasedVersionedEventDispatcher struct { 96 | registry map[reflect.Type][]VersionedEventHandler 97 | globalHandlers []VersionedEventHandler 98 | } 99 | 100 | // VersionedEventHandler is a function that takes a versioned event 101 | type VersionedEventHandler func(VersionedEvent) error 102 | 103 | // NewVersionedEventDispatcher is a constructor for the MapBasedVersionedEventDispatcher 104 | func NewVersionedEventDispatcher() *MapBasedVersionedEventDispatcher { 105 | registry := make(map[reflect.Type][]VersionedEventHandler) 106 | return &MapBasedVersionedEventDispatcher{registry, []VersionedEventHandler{}} 107 | } 108 | 109 | // RegisterEventHandler allows a caller to register an event handler given an event of the specified type being received 110 | func (m *MapBasedVersionedEventDispatcher) RegisterEventHandler(event interface{}, handler VersionedEventHandler) { 111 | eventType := reflect.TypeOf(event) 112 | handlers, ok := m.registry[eventType] 113 | if ok { 114 | m.registry[eventType] = append(handlers, handler) 115 | } else { 116 | m.registry[eventType] = []VersionedEventHandler{handler} 117 | } 118 | } 119 | 120 | // RegisterGlobalHandler allows a caller to register a wildcard event handler call on any event received 121 | func (m *MapBasedVersionedEventDispatcher) RegisterGlobalHandler(handler VersionedEventHandler) { 122 | m.globalHandlers = append(m.globalHandlers, handler) 123 | } 124 | 125 | // DispatchEvent executes all event handlers registered for the given event type 126 | func (m *MapBasedVersionedEventDispatcher) DispatchEvent(event VersionedEvent) error { 127 | eventType := reflect.TypeOf(event.Event) 128 | if handlers, ok := m.registry[eventType]; ok { 129 | for _, handler := range handlers { 130 | if err := handler(event); err != nil { 131 | metricsEventsFailed.WithLabelValues(event.EventType).Inc() 132 | return err 133 | } 134 | } 135 | } 136 | 137 | for _, handler := range m.globalHandlers { 138 | if err := handler(event); err != nil { 139 | metricsEventsFailed.WithLabelValues(event.EventType).Inc() 140 | return err 141 | } 142 | } 143 | 144 | metricsEventsDispatched.WithLabelValues(event.EventType).Inc() 145 | 146 | return nil 147 | } 148 | 149 | // NewVersionedEventDispatchManager is a constructor for the VersionedEventDispatchManager 150 | func NewVersionedEventDispatchManager(receiver VersionedEventReceiver, registry TypeRegistry) *VersionedEventDispatchManager { 151 | return &VersionedEventDispatchManager{NewVersionedEventDispatcher(), registry, receiver} 152 | } 153 | 154 | // RegisterEventHandler allows a caller to register an event handler given an event of the specified type being received 155 | func (m *VersionedEventDispatchManager) RegisterEventHandler(event interface{}, handler VersionedEventHandler) { 156 | m.typeRegistry.RegisterType(event) 157 | m.versionedEventDispatcher.RegisterEventHandler(event, handler) 158 | } 159 | 160 | // RegisterGlobalHandler allows a caller to register a wildcard event handler call on any event received 161 | func (m *VersionedEventDispatchManager) RegisterGlobalHandler(handler VersionedEventHandler) { 162 | m.versionedEventDispatcher.RegisterGlobalHandler(handler) 163 | } 164 | 165 | // Listen starts a listen loop processing channels related to new incoming events, errors and stop listening requests 166 | func (m *VersionedEventDispatchManager) Listen(stop <-chan bool, exclusive bool, listenerCount int) error { 167 | // Create communication channels 168 | // 169 | // for closing the queue listener, 170 | closeChannel := make(chan chan error) 171 | // receiving errors from the listener thread (go routine) 172 | errorChannel := make(chan error) 173 | 174 | // Version event received channel receives a result with a channel to respond to, signifying successful processing of the message. 175 | // This should eventually call an event handler. See cqrs.NewVersionedEventDispatcher() 176 | versionedEventHandler := func(event VersionedEvent) error { 177 | err := m.versionedEventDispatcher.DispatchEvent(event) 178 | if err != nil { 179 | PackageLogger().Debugf("Error dispatching event: %v", err) 180 | } 181 | 182 | return err 183 | } 184 | 185 | // Start receiving events by passing these channels to the worker thread (go routine) 186 | options := VersionedEventReceiverOptions{m.typeRegistry, closeChannel, errorChannel, versionedEventHandler, exclusive, listenerCount} 187 | if err := m.receiver.ReceiveEvents(options); err != nil { 188 | return err 189 | } 190 | 191 | go func() { 192 | for { 193 | // Wait on multiple channels using the select control flow. 194 | select { 195 | //PackageLogger().Debugf(nil, "EventDispatchManager.DispatchSuccessful") 196 | case <-stop: 197 | PackageLogger().Debugf("EventDispatchManager.Stopping") 198 | closeSignal := make(chan error) 199 | closeChannel <- closeSignal 200 | PackageLogger().Debugf("EventDispatchManager.Stopped") 201 | <-closeSignal 202 | // Receiving on this channel signifys an error has occured worker processor side 203 | case err := <-errorChannel: 204 | PackageLogger().Debugf("EventDispatchManager.ErrorReceived: %v", err) 205 | } 206 | } 207 | }() 208 | 209 | return nil 210 | } 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CQRS framework in go 2 | ======= 3 | 4 | [![Join the chat at https://gitter.im/andrewwebber/cqrs](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/andrewwebber/cqrs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | [![GoDoc](https://godoc.org/github.com/andrewwebber/cqrs?status.svg)](https://godoc.org/github.com/andrewwebber/cqrs) 6 | [![Build Status](https://drone.io/github.com/andrewwebber/cqrs/status.png?foo=bar)](https://drone.io/github.com/andrewwebber/cqrs/latest) 7 | 8 | # Project Summary 9 | The package provides a framework for quickly implementing a CQRS style application. 10 | The framework attempts to provides helpful functions to facilitate: 11 | - Event Sourcing 12 | - Command issuing and processing 13 | - Event publishing 14 | - Read model generation from published events 15 | 16 | --- 17 | 18 | ## Example code 19 | [Example test scenario (inmemory)](https://github.com/andrewwebber/cqrs/blob/master/cqrs_test.go) 20 | 21 | [Example test scenario (couchbase, rabbitmq)](https://github.com/andrewwebber/cqrs/blob/master/example/example_test.go) 22 | 23 | [Example CQRS scaleout/concurrent test](https://github.com/andrewwebber/cqrs-scaleout) 24 | 25 | ## Test Scenario 26 | The example test scenario is of a simple bank account that seeks to track, using event sourcing, a 27 | customers balance and login password 28 | 29 | The are two main areas of concern at the application level, the **Write model** and **Read model**. 30 | The read model is aimed to facilitate fast reads (read model projections) 31 | The write model is where the business logic get executed and asynchronously notifies the read models 32 | 33 | ## Write model - Using Event Sourcing 34 | ### Account 35 | ```go 36 | type Account struct { 37 | cqrs.EventSourceBased 38 | 39 | FirstName string 40 | LastName string 41 | EmailAddress string 42 | PasswordHash []byte 43 | Balance float64 44 | } 45 | ``` 46 | To compensate for golang's lack of inheritance, a combination of type embedding and a call convention 47 | pattern are utilized. 48 | 49 | ```go 50 | func NewAccount(firstName string, lastName string, emailAddress string, passwordHash []byte, initialBalance float64) *Account { 51 | account := new(Account) 52 | account.EventSourceBased = cqrs.NewEventSourceBased(account) 53 | 54 | event := AccountCreatedEvent{firstName, lastName, emailAddress, passwordHash, initialBalance} 55 | account.Update(event) 56 | return account 57 | } 58 | ``` 59 | The 'attached' Update function being called above will now provide the infrastructure for routing events to event handlers. 60 | A function prefixed with 'Handle' and named with the name of the event expected with be called by the infrastructure. 61 | ```go 62 | func (account *Account) HandleAccountCreatedEvent(event AccountCreatedEvent) { 63 | account.EmailAddress = event.EmailAddress 64 | account.FirstName = event.FirstName 65 | account.LastName = event.LastName 66 | account.PasswordHash = event.PasswordHash 67 | } 68 | ``` 69 | The above code results in an account object being created with one single **pending** event namely **AccountCreatedEvent**. 70 | Events will then be persisted once saved to an event sourcing repository. 71 | If a repository is created with an event publisher then events saved for the purposes of event sourcing will also be published 72 | ```go 73 | persistance := cqrs.NewInMemoryEventStreamRepository() 74 | bus := cqrs.NewInMemoryEventBus() 75 | repository := cqrs.NewRepositoryWithPublisher(persistance, bus) 76 | ... 77 | repository.Save(account) 78 | ``` 79 | 80 | ### Account Events 81 | ```go 82 | type AccountCreatedEvent struct { 83 | FirstName string 84 | LastName string 85 | EmailAddress string 86 | PasswordHash []byte 87 | InitialBalance float64 88 | } 89 | 90 | type EmailAddressChangedEvent struct { 91 | PreviousEmailAddress string 92 | NewEmailAddress string 93 | } 94 | 95 | type PasswordChangedEvent struct { 96 | NewPasswordHash []byte 97 | } 98 | 99 | type AccountCreditedEvent struct { 100 | Amount float64 101 | } 102 | 103 | type AccountDebitedEvent struct { 104 | Amount float64 105 | } 106 | ``` 107 | Events souring events are raised using the embedded **Update** function. These events will eventually be published to the read models indirectly via an event bus 108 | 109 | ```go 110 | func (account *Account) ChangePassword(newPassword string) error { 111 | if len(newPassword) < 1 { 112 | return errors.New("Invalid newPassword length") 113 | } 114 | 115 | hashedPassword, err := GetHashForPassword(newPassword) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | account.Update(PasswordChangedEvent{hashedPassword}) 121 | 122 | return nil 123 | } 124 | 125 | func (account *Account) HandlePasswordChangedEvent(event PasswordChangedEvent) { 126 | account.PasswordHash = event.NewPasswordHash 127 | } 128 | ``` 129 | 130 | Again the calling convention routes our **PasswordChangedEvent** to the corresponding **HandlePasswordChangedEvent** instance function 131 | 132 | ## Read Model 133 | ### Accounts projection 134 | ```go 135 | type ReadModelAccounts struct { 136 | Accounts map[string]*AccountReadModel 137 | } 138 | 139 | type AccountReadModel struct { 140 | ID string 141 | FirstName string 142 | LastName string 143 | EmailAddress string 144 | Balance float64 145 | } 146 | ``` 147 | ### Users projection 148 | ```go 149 | type UsersModel struct { 150 | Users map[string]*User 151 | } 152 | 153 | type User struct { 154 | ID string 155 | FirstName string 156 | LastName string 157 | EmailAddress string 158 | PasswordHash []byte 159 | } 160 | ``` 161 | 162 | ## Infrastructure 163 | There are a number of key elements to the CQRS infrastructure. 164 | - Event sourcing repository (a repository for event sourcing based business objects) 165 | - Event publisher (publishes new events to an event bus) 166 | - Event handler (dispatches received events to call handlers) 167 | - Command publisher (publishes new commands to a command bus) 168 | - Command handler (dispatches received commands to call handlers) 169 | 170 | ### Event sourcing and integration events 171 | Nested packages within this repository show example implementations using Couchbase Server and RabbitMQ. 172 | The core library includes in-memory implementations for testing and quick prototyping 173 | ```go 174 | persistance := cqrs.NewInMemoryEventStreamRepository() 175 | bus := cqrs.NewInMemoryEventBus() 176 | repository := cqrs.NewRepositoryWithPublisher(persistance, bus) 177 | ``` 178 | 179 | With the infrastructure implementations instantiated a stock event dispatcher is provided to route received 180 | events to call handlers 181 | ```go 182 | readModel := NewReadModelAccounts() 183 | usersModel := NewUsersModel() 184 | 185 | eventDispatcher := cqrs.NewVersionedEventDispatchManager(bus) 186 | eventDispatcher.RegisterEventHandler(AccountCreatedEvent{}, func(event cqrs.VersionedEvent) error { 187 | readModel.UpdateViewModel([]cqrs.VersionedEvent{event}) 188 | usersModel.UpdateViewModel([]cqrs.VersionedEvent{event}) 189 | return nil 190 | }) 191 | ``` 192 | 193 | We can also register a **global** handler to be called for all events. 194 | This becomes useful when logging system wide events and when our read models are smart enough to filter out irrelevant events 195 | ```go 196 | integrationEventsLog := cqrs.NewInMemoryEventStreamRepository() 197 | eventDispatcher.RegisterGlobalHandler(func(event cqrs.VersionedEvent) error { 198 | integrationEventsLog.SaveIntegrationEvent(event) 199 | readModel.UpdateViewModel([]cqrs.VersionedEvent{event}) 200 | usersModel.UpdateViewModel([]cqrs.VersionedEvent{event}) 201 | return nil 202 | }) 203 | ``` 204 | 205 | Within your read models the idea is that you implement the updating of your pre-pared read model based upon the 206 | incoming event notifications 207 | 208 | ### Commands 209 | 210 | Commands are processed by command handlers similar to event handlers. 211 | We can make direct changes to our write model and indirect changes to our read models by correctly processing commands and then raising integration events upon command completion. 212 | 213 | ```go 214 | commandBus := cqrs.NewInMemoryCommandBus() 215 | commandDispatcher := cqrs.NewCommandDispatchManager(commandBus) 216 | RegisterCommandHandlers(commandDispatcher, repository) 217 | ``` 218 | 219 | Commands can be issued using a command bus. Typically a command is a simple struct. 220 | The application layer command struct is then wrapped within a cqrs.Command using the cqrs.CreateCommand helper function 221 | 222 | ```go 223 | changePasswordCommand := cqrs.CreateCommand( 224 | ChangePasswordCommand{accountID, "$ThisIsANOTHERPassword"}) 225 | commandBus.PublishCommands([]cqrs.Command{changePasswordCommand}) 226 | ``` 227 | 228 | The corresponding command handler for the **ChangePassword** command plays the role of a DDD aggregate root; responsible for the consistency and lifetime of aggregates and entities within the system) 229 | ```go 230 | commandDispatcher.RegisterCommandHandler(ChangePasswordCommand{}, func(command cqrs.Command) error { 231 | changePasswordCommand := command.Body.(ChangePasswordCommand) 232 | // Load account from storage 233 | account, err := NewAccountFromHistory(changePasswordCommand.AccountID, repository) 234 | if err != nil { 235 | return err 236 | } 237 | 238 | account.ChangePassword(changePasswordCommand.NewPassword) 239 | 240 | // Persist new events 241 | repository.Save(account) 242 | return nil 243 | }) 244 | ``` 245 | 246 | As the read models become consistant, within the tests, we check at the end of the test if everything is in sync 247 | ```go 248 | if account.EmailAddress != lastEmailAddress { 249 | t.Fatal("Expected emailaddress to be ", lastEmailAddress) 250 | } 251 | 252 | if account.Balance != readModel.Accounts[accountID].Balance { 253 | t.Fatal("Expected readmodel to be synced with write model") 254 | } 255 | ``` 256 | -------------------------------------------------------------------------------- /rabbit/commandbus.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/andrewwebber/cqrs" 13 | 14 | "github.com/streadway/amqp" 15 | ) 16 | 17 | // RawCommand represents an actor intention to alter the state of the system 18 | type RawCommand struct { 19 | MessageID string `json:"messageID"` 20 | CorrelationID string `json:"correlationID"` 21 | CommandType string `json:"commandType"` 22 | Created time.Time `json:"time"` 23 | Body json.RawMessage 24 | } 25 | 26 | // CommandBus ... 27 | type CommandBus struct { 28 | resolver ConnectionStringResolver 29 | name string 30 | exchange string 31 | channel *amqp.Channel 32 | reconnect chan reconnectionAttempt 33 | conn *amqp.Connection 34 | reconnectContext int 35 | healthyconnection uint32 36 | } 37 | 38 | // NewCommandBus will create a new command bus 39 | func NewCommandBus(resolver ConnectionStringResolver, name string, exchange string) *CommandBus { 40 | bus := &CommandBus{resolver: resolver, name: name, exchange: exchange, healthyconnection: 1} 41 | reconnectCh := initializeReconnectionManagement(resolver, func(conn *amqp.Connection, ctx int) { 42 | bus.conn = conn 43 | bus.reconnectContext = ctx 44 | _ = bus.connect(bus.conn) 45 | }) 46 | respCh := make(chan reconnectionAttemptResponse) 47 | reconnectCh <- reconnectionAttempt{context: 0, response: respCh} 48 | <-respCh 49 | bus.reconnect = reconnectCh 50 | 51 | return bus 52 | } 53 | 54 | func (bus *CommandBus) getConnectionString() (string, error) { 55 | var connectionString string 56 | retryError := exponential(func() error { 57 | result, err := bus.resolver() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | connectionString = result 63 | return err 64 | }, 5) 65 | 66 | return connectionString, retryError 67 | } 68 | 69 | // PublishCommands will publish commands 70 | func (bus *CommandBus) PublishCommands(commands []cqrs.Command) error { 71 | 72 | for _, command := range commands { 73 | encodedCommand, err := json.Marshal(command) 74 | if err != nil { 75 | return fmt.Errorf("json.Marshal: %v", err) 76 | } 77 | 78 | // Prepare this message to be persistent. Your publishing requirements may 79 | // be different. 80 | msg := amqp.Publishing{ 81 | DeliveryMode: amqp.Persistent, 82 | Timestamp: time.Now().UTC(), 83 | ContentEncoding: "UTF-8", 84 | ContentType: "text/plain", 85 | Body: encodedCommand, 86 | } 87 | 88 | retryError := exponential(func() error { 89 | err = bus.channel.Publish(bus.exchange, bus.name, true, false, msg) 90 | 91 | if err != nil { 92 | atomic.CompareAndSwapUint32(&bus.healthyconnection, 1, 0) 93 | respCh := make(chan reconnectionAttemptResponse) 94 | bus.reconnect <- reconnectionAttempt{context: bus.reconnectContext, response: respCh} 95 | resp := <-respCh 96 | bus.conn = resp.connection 97 | bus.reconnectContext = resp.newContext 98 | 99 | connErr := bus.connect(bus.conn) 100 | if connErr == nil { 101 | cqrs.PackageLogger().Debugf("RabbitMQ: Reconnected") 102 | atomic.CompareAndSwapUint32(&bus.healthyconnection, 0, 1) 103 | } else { 104 | cqrs.PackageLogger().Debugf("RabbitMQ: Reconnect Failed %v", err) 105 | } 106 | } 107 | 108 | return err 109 | }, 3) 110 | 111 | if retryError != nil { 112 | metricsCommandsFailed.WithLabelValues(command.CommandType).Inc() 113 | return fmt.Errorf("bus.publish: %v", err) 114 | } 115 | 116 | metricsCommandsPublished.WithLabelValues(command.CommandType).Inc() 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func (bus *CommandBus) connect(conn *amqp.Connection) error { 123 | // This waits for a server acknowledgment which means the sockets will have 124 | // flushed all outbound publishings prior to returning. It's important to 125 | // block on Close to not lose any publishings. 126 | //defer conn.Close() 127 | 128 | c, err := conn.Channel() 129 | if err != nil { 130 | return fmt.Errorf("channel.open: %s", err) 131 | } 132 | 133 | // We declare our topology on both the publisher and consumer to ensure they 134 | // are the same. This is part of AMQP being a programmable messaging model. 135 | // 136 | // See the Channel.Consume example for the complimentary declare. 137 | err = c.ExchangeDeclare(bus.exchange, "topic", true, false, false, false, nil) 138 | if err != nil { 139 | return fmt.Errorf("exchange.declare: %v", err) 140 | } 141 | 142 | bus.channel = c 143 | 144 | return nil 145 | } 146 | 147 | // ReceiveCommands will recieve commands 148 | func (bus *CommandBus) ReceiveCommands(options cqrs.CommandReceiverOptions) error { 149 | conn := bus.conn 150 | 151 | listenerStart := time.Now() 152 | var wg sync.WaitGroup 153 | for n := 0; n < options.ListenerCount; n++ { 154 | wg.Add(1) 155 | go func(reconnectionChannel chan<- reconnectionAttempt) { 156 | reconnectionContext := 0 157 | c, commands, err := bus.consumeCommandsQueue(conn, options.Exclusive) 158 | if err != nil { 159 | return 160 | } 161 | 162 | wg.Done() 163 | 164 | notifyClose := conn.NotifyClose(make(chan *amqp.Error)) 165 | reconnect := func() { 166 | atomic.CompareAndSwapUint32(&bus.healthyconnection, 1, 0) 167 | respCh := make(chan reconnectionAttemptResponse) 168 | reconnectionChannel <- reconnectionAttempt{context: reconnectionContext, response: respCh} 169 | resp := <-respCh 170 | conn = resp.connection 171 | reconnectionContext = resp.newContext 172 | 173 | cR, commandsR, errR := bus.consumeCommandsQueue(conn, options.Exclusive) 174 | if errR == nil { 175 | c, commands, _ = cR, commandsR, errR 176 | } 177 | 178 | notifyClose = conn.NotifyClose(make(chan *amqp.Error)) 179 | atomic.CompareAndSwapUint32(&bus.healthyconnection, 0, 1) 180 | } 181 | 182 | for { 183 | select { 184 | case ch := <-options.Close: 185 | cqrs.PackageLogger().Debugf("Close requested") 186 | defer closeConnection(conn) 187 | ch <- c.Cancel(bus.name, false) 188 | return 189 | 190 | case <-notifyClose: 191 | reconnect() 192 | 193 | case m, more := <-commands: 194 | if more { 195 | go func(message amqp.Delivery) { 196 | var raw RawCommand 197 | if errUnmarshalRaw := json.Unmarshal(message.Body, &raw); errUnmarshalRaw != nil { 198 | options.Error <- fmt.Errorf("json.Unmarshal received command: %v", errUnmarshalRaw) 199 | } else { 200 | commandType, ok := options.TypeRegistry.GetTypeByName(raw.CommandType) 201 | if !ok { 202 | cqrs.PackageLogger().Debugf("CommandBus.Cannot find command type", raw.CommandType) 203 | options.Error <- errors.New("Cannot find command type " + raw.CommandType) 204 | } else { 205 | commandValue := reflect.New(commandType) 206 | commandBody := commandValue.Interface() 207 | if errUnmarshalBody := json.Unmarshal(raw.Body, commandBody); errUnmarshalBody != nil { 208 | options.Error <- errors.New("Error deserializing command " + raw.CommandType) 209 | } else { 210 | command := cqrs.Command{ 211 | MessageID: raw.MessageID, 212 | CorrelationID: raw.CorrelationID, 213 | CommandType: raw.CommandType, 214 | Created: raw.Created, 215 | 216 | Body: reflect.Indirect(commandValue).Interface()} 217 | 218 | start := time.Now() 219 | execErr := options.ReceiveCommand(command) 220 | result := execErr == nil 221 | if result { 222 | err = message.Ack(false) 223 | if err != nil { 224 | cqrs.PackageLogger().Debugf("ERROR: Message ack returned error: %v\n", err) 225 | } 226 | elapsed := time.Since(start) 227 | // stats := map[string]string{ 228 | // "CQRS_LOG": "true", 229 | // "CQRS_DURATION": fmt.Sprintf("%s", elapsed), 230 | // "CQRS_TYPE": raw.CommandType, 231 | // "CQRS_CREATED": fmt.Sprintf("%s", raw.Created), 232 | // "CQRS_CORR": raw.CorrelationID} 233 | cqrs.PackageLogger().Debugf(fmt.Sprintf("CommandBus Message Took %s", elapsed)) 234 | } else { 235 | err = message.Reject(true) 236 | if err != nil { 237 | cqrs.PackageLogger().Debugf("ERROR: Message reject returned error: %v\n", err) 238 | } 239 | } 240 | } 241 | } 242 | } 243 | }(m) 244 | } else { 245 | c, commands, _ = bus.consumeCommandsQueue(conn, options.Exclusive) 246 | for err != nil { 247 | reconnect() 248 | c, commands, _ = bus.consumeCommandsQueue(conn, options.Exclusive) 249 | <-time.After(1 * time.Second) 250 | } 251 | } 252 | } 253 | } 254 | }(bus.reconnect) 255 | } 256 | 257 | wg.Wait() 258 | listenerElapsed := time.Since(listenerStart) 259 | cqrs.PackageLogger().Debugf("Receiving commands - %s", listenerElapsed) 260 | 261 | return nil 262 | } 263 | 264 | func (bus *CommandBus) consumeCommandsQueue(conn *amqp.Connection, exclusive bool) (*amqp.Channel, <-chan amqp.Delivery, error) { 265 | 266 | c, err := conn.Channel() 267 | if err != nil { 268 | return nil, nil, fmt.Errorf("channel.open: %s", err) 269 | } 270 | 271 | // We declare our topology on both the publisher and consumer to ensure they 272 | // are the same. This is part of AMQP being a programmable messaging model. 273 | // 274 | // See the Channel.Consume example for the complimentary declare. 275 | err = c.ExchangeDeclare(bus.exchange, "topic", true, false, false, false, nil) 276 | if err != nil { 277 | return nil, nil, fmt.Errorf("exchange.declare: %v", err) 278 | } 279 | 280 | if _, err = c.QueueDeclare(bus.name, true, false, false, false, nil); err != nil { 281 | return nil, nil, fmt.Errorf("queue.declare: %v", err) 282 | } 283 | 284 | if err = c.QueueBind(bus.name, bus.name, bus.exchange, false, nil); err != nil { 285 | return nil, nil, fmt.Errorf("queue.bind: %v", err) 286 | } 287 | 288 | commands, err := c.Consume(bus.name, "", false, exclusive, false, false, nil) 289 | if err != nil { 290 | return nil, nil, fmt.Errorf("basic.consume: %v", err) 291 | } 292 | 293 | if err := c.Qos(Prefetch, 0, false); err != nil { 294 | return nil, nil, fmt.Errorf("Qos: %v", err) 295 | } 296 | 297 | return c, commands, nil 298 | } 299 | 300 | func closeConnection(conn *amqp.Connection) { 301 | err := conn.Close() 302 | if err != nil { 303 | cqrs.PackageLogger().Debugf("Couldn't close conn: %v\n", err) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /rabbit/eventbus.go: -------------------------------------------------------------------------------- 1 | package rabbit 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/andrewwebber/cqrs" 13 | 14 | "github.com/streadway/amqp" 15 | ) 16 | 17 | // RawVersionedEvent ... 18 | type RawVersionedEvent struct { 19 | ID string `json:"id"` 20 | CorrelationID string `json:"correlationID"` 21 | SourceID string `json:"sourceID"` 22 | Version int `json:"version"` 23 | EventType string `json:"eventType"` 24 | Created time.Time `json:"time"` 25 | Event json.RawMessage 26 | } 27 | 28 | // EventBus ... 29 | type EventBus struct { 30 | resolver ConnectionStringResolver 31 | name string 32 | exchange string 33 | channel *amqp.Channel 34 | reconnect chan reconnectionAttempt 35 | conn *amqp.Connection 36 | reconnectContext int 37 | healthyconnection uint32 38 | } 39 | 40 | // NewEventBus ... 41 | func NewEventBus(resolver ConnectionStringResolver, name string, exchange string) *EventBus { 42 | bus := &EventBus{resolver: resolver, name: name, exchange: exchange, healthyconnection: 1} 43 | reconnectCh := initializeReconnectionManagement(resolver, func(conn *amqp.Connection, ctx int) { 44 | bus.conn = conn 45 | bus.reconnectContext = ctx 46 | _ = bus.connect(bus.conn) 47 | }) 48 | respCh := make(chan reconnectionAttemptResponse) 49 | reconnectCh <- reconnectionAttempt{context: 0, response: respCh} 50 | <-respCh 51 | bus.reconnect = reconnectCh 52 | 53 | return bus 54 | } 55 | 56 | func (bus *EventBus) getConnectionString() (string, error) { 57 | var connectionString string 58 | retryError := exponential(func() error { 59 | result, err := bus.resolver() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | connectionString = result 65 | return err 66 | }, 5) 67 | 68 | return connectionString, retryError 69 | } 70 | 71 | // PublishEvents will publish events 72 | func (bus *EventBus) PublishEvents(events []cqrs.VersionedEvent) error { 73 | 74 | for _, event := range events { 75 | encodedEvent, err := json.Marshal(event) 76 | if err != nil { 77 | return fmt.Errorf("json.Marshal: %v", err) 78 | } 79 | 80 | // Prepare this message to be persistent. Your publishing requirements may 81 | // be different. 82 | msg := amqp.Publishing{ 83 | DeliveryMode: amqp.Persistent, 84 | Timestamp: time.Now().UTC(), 85 | ContentEncoding: "UTF-8", 86 | ContentType: "text/plain", 87 | Body: encodedEvent, 88 | } 89 | 90 | retryError := exponential(func() error { 91 | err = bus.channel.Publish(bus.exchange, bus.name, true, false, msg) 92 | 93 | if err != nil { 94 | atomic.CompareAndSwapUint32(&bus.healthyconnection, 1, 0) 95 | respCh := make(chan reconnectionAttemptResponse) 96 | bus.reconnect <- reconnectionAttempt{context: bus.reconnectContext, response: respCh} 97 | resp := <-respCh 98 | bus.conn = resp.connection 99 | bus.reconnectContext = resp.newContext 100 | 101 | connErr := bus.connect(bus.conn) 102 | if connErr == nil { 103 | cqrs.PackageLogger().Debugf("RabbitMQ: Reconnected") 104 | atomic.CompareAndSwapUint32(&bus.healthyconnection, 0, 1) 105 | } else { 106 | cqrs.PackageLogger().Debugf("RabbitMQ: Reconnect Failed %v", err) 107 | } 108 | } 109 | 110 | return err 111 | }, 3) 112 | 113 | if retryError != nil { 114 | metricsEventsFailed.WithLabelValues(event.EventType).Inc() 115 | return fmt.Errorf("bus.publish: %v", err) 116 | } 117 | 118 | metricsEventsPublished.WithLabelValues(event.EventType).Inc() 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (bus *EventBus) connect(conn *amqp.Connection) error { 125 | 126 | // This waits for a server acknowledgment which means the sockets will have 127 | // flushed all outbound publishings prior to returning. It's important to 128 | // block on Close to not lose any publishings. 129 | //defer conn.Close() 130 | 131 | c, err := conn.Channel() 132 | if err != nil { 133 | return fmt.Errorf("channel.open: %s", err) 134 | } 135 | 136 | // We declare our topology on both the publisher and consumer to ensure they 137 | // are the same. This is part of AMQP being a programmable messaging model. 138 | // 139 | // See the Channel.Consume example for the complimentary declare. 140 | err = c.ExchangeDeclare(bus.exchange, "fanout", true, false, false, false, nil) 141 | if err != nil { 142 | return fmt.Errorf("exchange.declare: %v", err) 143 | } 144 | 145 | bus.channel = c 146 | 147 | return nil 148 | } 149 | 150 | // ReceiveEvents will receive events 151 | func (bus *EventBus) ReceiveEvents(options cqrs.VersionedEventReceiverOptions) error { 152 | conn := bus.conn 153 | listenerStart := time.Now() 154 | var wg sync.WaitGroup 155 | for n := 0; n < options.ListenerCount; n++ { 156 | wg.Add(1) 157 | go func(reconnectionChannel chan<- reconnectionAttempt) { 158 | reconnectionContext := 0 159 | c, events, err := bus.consumeEventsQueue(conn, options.Exclusive) 160 | if err != nil { 161 | return 162 | } 163 | 164 | wg.Done() 165 | notifyClose := conn.NotifyClose(make(chan *amqp.Error)) 166 | reconnect := func() { 167 | atomic.CompareAndSwapUint32(&bus.healthyconnection, 1, 0) 168 | respCh := make(chan reconnectionAttemptResponse) 169 | reconnectionChannel <- reconnectionAttempt{context: reconnectionContext, response: respCh} 170 | resp := <-respCh 171 | conn = resp.connection 172 | reconnectionContext = resp.newContext 173 | 174 | cR, eventsR, errR := bus.consumeEventsQueue(conn, options.Exclusive) 175 | if errR == nil { 176 | c, events, _ = cR, eventsR, errR 177 | } 178 | notifyClose = conn.NotifyClose(make(chan *amqp.Error)) 179 | atomic.CompareAndSwapUint32(&bus.healthyconnection, 0, 1) 180 | } 181 | 182 | for { 183 | select { 184 | case ch := <-options.Close: 185 | defer closeConnection(conn) 186 | ch <- c.Cancel(bus.name, false) 187 | return 188 | 189 | case <-notifyClose: 190 | reconnect() 191 | 192 | case m, more := <-events: 193 | if more { 194 | go func(message amqp.Delivery) { 195 | var raw RawVersionedEvent 196 | if errUnmarshalRaw := json.Unmarshal(message.Body, &raw); errUnmarshalRaw != nil { 197 | options.Error <- fmt.Errorf("json.Unmarshal received event: %v", errUnmarshalRaw) 198 | } else { 199 | eventType, ok := options.TypeRegistry.GetTypeByName(raw.EventType) 200 | if !ok { 201 | // cqrs.PackageLogger().Debugf(nil, "EventBus.Cannot find event type", raw.EventType) 202 | // options.Error <- errors.New("Cannot find event type " + raw.EventType) 203 | err = message.Ack(false) 204 | if err != nil { 205 | cqrs.PackageLogger().Debugf("ERROR: Message ack failed: %v\n", err) 206 | } 207 | } else { 208 | eventValue := reflect.New(eventType) 209 | event := eventValue.Interface() 210 | if errUnmarshalEvent := json.Unmarshal(raw.Event, event); errUnmarshalEvent != nil { 211 | options.Error <- errors.New("Error deserializing event " + raw.EventType) 212 | } else { 213 | versionedEvent := cqrs.VersionedEvent{ 214 | ID: raw.ID, 215 | CorrelationID: raw.CorrelationID, 216 | SourceID: raw.SourceID, 217 | Version: raw.Version, 218 | EventType: raw.EventType, 219 | Created: raw.Created, 220 | 221 | Event: reflect.Indirect(eventValue).Interface()} 222 | 223 | start := time.Now() 224 | execErr := options.ReceiveEvent(versionedEvent) 225 | result := execErr == nil 226 | if result { 227 | err = message.Ack(false) 228 | if err != nil { 229 | cqrs.PackageLogger().Debugf("ERROR: Message ack returned error: %v\n", err) 230 | } 231 | elapsed := time.Since(start) 232 | // stats := map[string]string{ 233 | // "CQRS_LOG": "true", 234 | // "CQRS_DURATION": fmt.Sprintf("%s", elapsed), 235 | // "CQRS_TYPE": raw.EventType, 236 | // "CQRS_CREATED": fmt.Sprintf("%s", raw.Created), 237 | // "CQRS_CORR": raw.CorrelationID} 238 | cqrs.PackageLogger().Debugf("EventBus Message Took %s", elapsed) 239 | } else { 240 | err = message.Reject(true) 241 | if err != nil { 242 | cqrs.PackageLogger().Debugf("ERROR: Message reject returned error: %v\n", err) 243 | } 244 | } 245 | } 246 | } 247 | } 248 | }(m) 249 | } else { 250 | c, events, err = bus.consumeEventsQueue(conn, options.Exclusive) 251 | for err != nil { 252 | reconnect() 253 | c, events, err = bus.consumeEventsQueue(conn, options.Exclusive) 254 | <-time.After(1 * time.Second) 255 | } 256 | } 257 | } 258 | } 259 | }(bus.reconnect) 260 | } 261 | 262 | wg.Wait() 263 | listenerElapsed := time.Since(listenerStart) 264 | cqrs.PackageLogger().Debugf("Receiving events - %s", listenerElapsed) 265 | 266 | return nil 267 | } 268 | 269 | // DeleteQueue will delete a queue 270 | func (bus *EventBus) DeleteQueue(name string) error { 271 | // Connects opens an AMQP connection from the credentials in the URL. 272 | connectionString, err := bus.getConnectionString() 273 | if err != nil { 274 | return err 275 | } 276 | 277 | conn, err := amqp.Dial(connectionString) 278 | if err != nil { 279 | return fmt.Errorf("connection.open: %s", err) 280 | } 281 | 282 | c, err := conn.Channel() 283 | if err != nil { 284 | return fmt.Errorf("channel.open: %s", err) 285 | } 286 | 287 | _, err = c.QueueDelete(name, false, false, true) 288 | return err 289 | } 290 | 291 | func (bus *EventBus) consumeEventsQueue(conn *amqp.Connection, exclusive bool) (*amqp.Channel, <-chan amqp.Delivery, error) { 292 | 293 | c, err := conn.Channel() 294 | if err != nil { 295 | return nil, nil, fmt.Errorf("channel.open: %s", err) 296 | } 297 | 298 | // We declare our topology on both the publisher and consumer to ensure they 299 | // are the same. This is part of AMQP being a programmable messaging model. 300 | // 301 | // See the Channel.Consume example for the complimentary declare. 302 | err = c.ExchangeDeclare(bus.exchange, "fanout", true, false, false, false, nil) 303 | if err != nil { 304 | return nil, nil, fmt.Errorf("exchange.declare: %v", err) 305 | } 306 | 307 | if _, err = c.QueueDeclare(bus.name, true, false, false, false, nil); err != nil { 308 | return nil, nil, fmt.Errorf("queue.declare: %v", err) 309 | } 310 | 311 | if err = c.QueueBind(bus.name, bus.name, bus.exchange, false, nil); err != nil { 312 | return nil, nil, fmt.Errorf("queue.bind: %v", err) 313 | } 314 | 315 | events, err := c.Consume(bus.name, "", false, exclusive, false, false, nil) 316 | if err != nil { 317 | return nil, nil, fmt.Errorf("basic.consume: %v", err) 318 | } 319 | 320 | if err := c.Qos(Prefetch, 0, false); err != nil { 321 | return nil, nil, fmt.Errorf("Qos: %v", err) 322 | } 323 | 324 | return c, events, nil 325 | } 326 | -------------------------------------------------------------------------------- /cqrs_test.go: -------------------------------------------------------------------------------- 1 | package cqrs_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | 9 | "github.com/andrewwebber/cqrs" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | var accountID = cqrs.NewUUIDString() 15 | 16 | func TestScenario(t *testing.T) { 17 | 18 | log.SetFlags(log.LstdFlags | log.Lshortfile) 19 | 20 | // Type Registry 21 | typeRegistry := cqrs.NewTypeRegistry() 22 | 23 | // Event sourcing 24 | persistance := cqrs.NewInMemoryEventStreamRepository() 25 | bus := cqrs.NewInMemoryEventBus() 26 | repository := cqrs.NewRepositoryWithPublisher(persistance, bus, typeRegistry) 27 | typeRegistry.RegisterAggregate(&Account{}) 28 | typeRegistry.RegisterEvents(AccountCreatedEvent{}, EmailAddressChangedEvent{}, AccountCreditedEvent{}, AccountDebitedEvent{}, PasswordChangedEvent{}) 29 | 30 | // Read Models 31 | readModel := NewReadModelAccounts() 32 | usersModel := NewUsersModel() 33 | 34 | // Command Handlers 35 | commandBus := cqrs.NewInMemoryCommandBus() 36 | commandDispatcher := cqrs.NewCommandDispatchManager(commandBus, typeRegistry) 37 | RegisterCommandHandlers(commandDispatcher, repository) 38 | 39 | // Integration events 40 | eventDispatcher := cqrs.NewVersionedEventDispatchManager(bus, typeRegistry) 41 | integrationEventsLog := cqrs.NewInMemoryEventStreamRepository() 42 | RegisterIntegrationEventHandlers(eventDispatcher, integrationEventsLog, readModel, usersModel) 43 | 44 | commandDispatcherStopChannel := make(chan bool) 45 | eventDispatcherStopChannel := make(chan bool) 46 | go func() { 47 | err := commandDispatcher.Listen(commandDispatcherStopChannel, false, 1) 48 | require.NoError(t, err) 49 | }() 50 | go func() { 51 | err := eventDispatcher.Listen(eventDispatcherStopChannel, false, 1) 52 | require.NoError(t, err) 53 | }() 54 | 55 | cqrs.PackageLogger().Debugf("Dump models") 56 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", readModel)) 57 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", usersModel)) 58 | 59 | cqrs.PackageLogger().Debugf("Find an account") 60 | readModelAccount := readModel.GetAccount(accountID) 61 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", readModelAccount)) 62 | 63 | cqrs.PackageLogger().Debugf("Find a user") 64 | user := usersModel.Users[accountID] 65 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", user)) 66 | 67 | hashedPassword, err := GetHashForPassword("$ThisIsMyPassword1") 68 | if err != nil { 69 | t.Fatal("Error: ", err) 70 | } 71 | 72 | cqrs.PackageLogger().Debugf("Create new account...") 73 | createAccountCommand := cqrs.CreateCommand( 74 | CreateAccountCommand{"John", "Snow", "John.Snow@thewall.eu", hashedPassword, 0.0}) 75 | err = commandBus.PublishCommands([]cqrs.Command{createAccountCommand}) 76 | require.NoError(t, err) 77 | 78 | cqrs.PackageLogger().Debugf("Dump models") 79 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", readModel)) 80 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", usersModel)) 81 | 82 | cqrs.PackageLogger().Debugf("Change Password") 83 | changePasswordCommand := cqrs.CreateCommand( 84 | ChangePasswordCommand{accountID, "$ThisIsANOTHERPassword"}) 85 | err = commandBus.PublishCommands([]cqrs.Command{changePasswordCommand}) 86 | require.NoError(t, err) 87 | 88 | cqrs.PackageLogger().Debugf("Change email address and credit the account") 89 | changeEmailAddressCommand := cqrs.CreateCommand( 90 | ChangeEmailAddressCommand{accountID, "john.snow@the.wall"}) 91 | creditAccountCommand := cqrs.CreateCommand( 92 | CreditAccountCommand{accountID, 50}) 93 | creditAccountCommand2 := cqrs.CreateCommand( 94 | CreditAccountCommand{accountID, 50}) 95 | err = commandBus.PublishCommands([]cqrs.Command{ 96 | changeEmailAddressCommand, 97 | creditAccountCommand, 98 | creditAccountCommand2}) 99 | require.NoError(t, err) 100 | 101 | cqrs.PackageLogger().Debugf("Dump models") 102 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", readModel)) 103 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", usersModel)) 104 | 105 | cqrs.PackageLogger().Debugf("Change the email address, credit 150, debit 200") 106 | lastEmailAddress := "john.snow@golang.org" 107 | changeEmailAddressCommand = cqrs.CreateCommand( 108 | ChangeEmailAddressCommand{accountID, lastEmailAddress}) 109 | creditAccountCommand = cqrs.CreateCommand( 110 | CreditAccountCommand{accountID, 150}) 111 | debitAccountCommand := cqrs.CreateCommand( 112 | DebitAccountCommand{accountID, 200}) 113 | err = commandBus.PublishCommands([]cqrs.Command{ 114 | changeEmailAddressCommand, 115 | creditAccountCommand, 116 | debitAccountCommand}) 117 | require.NoError(t, err) 118 | 119 | cqrs.PackageLogger().Debugf("Dump models") 120 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", readModel)) 121 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", usersModel)) 122 | 123 | time.Sleep(300 * time.Millisecond) 124 | cqrs.PackageLogger().Debugf("Dump history - integration events") 125 | if history, err := repository.GetEventStreamRepository().AllIntegrationEventsEverPublished(); err != nil { 126 | t.Fatal(err) 127 | } else { 128 | for _, event := range history { 129 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", event)) 130 | } 131 | } 132 | 133 | cqrs.PackageLogger().Debugf("GetIntegrationEventsByCorrelationID") 134 | correlationEvents, err := repository.GetEventStreamRepository().GetIntegrationEventsByCorrelationID(debitAccountCommand.CorrelationID) 135 | if err != nil || len(correlationEvents) == 0 { 136 | t.Fatal(err) 137 | } 138 | 139 | for _, correlationEvent := range correlationEvents { 140 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", correlationEvent)) 141 | } 142 | 143 | cqrs.PackageLogger().Debugf("Load the account from history") 144 | account, error := NewAccountFromHistory(accountID, repository) 145 | if error != nil { 146 | t.Fatal(error) 147 | } 148 | 149 | // All events should have been replayed and the email address should be the latest 150 | cqrs.PackageLogger().Debugf("Dump models") 151 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", account)) 152 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", readModel)) 153 | cqrs.PackageLogger().Debugf(fmt.Sprintf("%+v", usersModel)) 154 | 155 | if account.EmailAddress != lastEmailAddress { 156 | t.Fatal("Expected emailaddress to be ", lastEmailAddress) 157 | } 158 | 159 | if account.Balance != readModel.GetAccount(accountID).Balance { 160 | t.Fatal("Expected readmodel to be synced with write model") 161 | } 162 | 163 | eventDispatcherStopChannel <- true 164 | commandDispatcherStopChannel <- true 165 | } 166 | 167 | func RegisterIntegrationEventHandlers(eventDispatcher *cqrs.VersionedEventDispatchManager, integrationEventsLog cqrs.VersionedEventPublicationLogger, readModel *ReadModelAccounts, usersModel *UsersModel) { 168 | eventDispatcher.RegisterGlobalHandler(func(event cqrs.VersionedEvent) error { 169 | if err := integrationEventsLog.SaveIntegrationEvent(event); err != nil { 170 | return err 171 | } 172 | if err := readModel.UpdateViewModel([]cqrs.VersionedEvent{event}); err != nil { 173 | return err 174 | } 175 | if err := usersModel.UpdateViewModel([]cqrs.VersionedEvent{event}); err != nil { 176 | return err 177 | } 178 | return nil 179 | }) 180 | } 181 | 182 | func RegisterCommandHandlers(commandDispatcher *cqrs.CommandDispatchManager, repository cqrs.EventSourcingRepository) { 183 | commandDispatcher.RegisterCommandHandler(CreateAccountCommand{}, func(command cqrs.Command) error { 184 | createAccountCommand := command.Body.(CreateAccountCommand) 185 | cqrs.PackageLogger().Debugf("Processing command - Create account") 186 | account := NewAccount(createAccountCommand.FirstName, 187 | createAccountCommand.LastName, 188 | createAccountCommand.EmailAddress, 189 | createAccountCommand.PasswordHash, 190 | createAccountCommand.InitialBalance) 191 | 192 | cqrs.PackageLogger().Debugf("Set ID...") 193 | account.SetID(accountID) 194 | cqrs.PackageLogger().Debugf("Persist the account") 195 | cqrs.PackageLogger().Debugf("Account %+v", account) 196 | 197 | if _, err := repository.Save(account, command.CorrelationID); err != nil { 198 | return err 199 | } 200 | cqrs.PackageLogger().Debugf(account.String()) 201 | return nil 202 | }) 203 | 204 | commandDispatcher.RegisterCommandHandler(ChangeEmailAddressCommand{}, func(command cqrs.Command) error { 205 | changeEmailAddressCommand := command.Body.(ChangeEmailAddressCommand) 206 | cqrs.PackageLogger().Debugf("Processing command - Change email address") 207 | account, err := NewAccountFromHistory(changeEmailAddressCommand.AccountID, repository) 208 | if err != nil { 209 | return err 210 | } 211 | 212 | if err := account.ChangeEmailAddress(changeEmailAddressCommand.NewEmailAddress); err != nil { 213 | return err 214 | } 215 | cqrs.PackageLogger().Debugf("Account %+v", account) 216 | 217 | cqrs.PackageLogger().Debugf("Persist the account") 218 | if _, err := repository.Save(account, command.CorrelationID); err != nil { 219 | return err 220 | } 221 | cqrs.PackageLogger().Debugf(account.String()) 222 | return nil 223 | }) 224 | 225 | commandDispatcher.RegisterCommandHandler(ChangePasswordCommand{}, func(command cqrs.Command) error { 226 | changePasswordCommand := command.Body.(ChangePasswordCommand) 227 | cqrs.PackageLogger().Debugf("Processing command - Change password") 228 | account, err := NewAccountFromHistory(changePasswordCommand.AccountID, repository) 229 | if err != nil { 230 | return err 231 | } 232 | 233 | cqrs.PackageLogger().Debugf("Account %+v", account) 234 | 235 | if err := account.ChangePassword(changePasswordCommand.NewPassword); err != nil { 236 | return err 237 | } 238 | cqrs.PackageLogger().Debugf("Persist the account") 239 | if _, err := repository.Save(account, command.CorrelationID); err != nil { 240 | return err 241 | } 242 | cqrs.PackageLogger().Debugf(account.String()) 243 | return nil 244 | }) 245 | 246 | commandDispatcher.RegisterCommandHandler(CreditAccountCommand{}, func(command cqrs.Command) error { 247 | creditAccountCommand := command.Body.(CreditAccountCommand) 248 | cqrs.PackageLogger().Debugf("Processing command - Credit account") 249 | account, err := NewAccountFromHistory(creditAccountCommand.AccountID, repository) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | if err := account.Credit(creditAccountCommand.Amount); err != nil { 255 | return err 256 | } 257 | cqrs.PackageLogger().Debugf("Account %+v", account) 258 | 259 | cqrs.PackageLogger().Debugf("Persist the account") 260 | if _, err := repository.Save(account, command.CorrelationID); err != nil { 261 | return err 262 | } 263 | cqrs.PackageLogger().Debugf(account.String()) 264 | return nil 265 | }) 266 | 267 | commandDispatcher.RegisterCommandHandler(DebitAccountCommand{}, func(command cqrs.Command) error { 268 | debitAccountCommand := command.Body.(DebitAccountCommand) 269 | cqrs.PackageLogger().Debugf("Processing command - Debit account") 270 | account, err := NewAccountFromHistory(debitAccountCommand.AccountID, repository) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | if err := account.Debit(debitAccountCommand.Amount); err != nil { 276 | return err 277 | } 278 | 279 | cqrs.PackageLogger().Debugf("Account %+v", account) 280 | 281 | cqrs.PackageLogger().Debugf("Persist the account") 282 | if _, err := repository.Save(account, command.CorrelationID); err != nil { 283 | return err 284 | } 285 | cqrs.PackageLogger().Debugf(account.String()) 286 | return nil 287 | }) 288 | } 289 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------