sequent 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 06ef0bc6b614da70a42d8318cd2c4e135f263dc9
4
- data.tar.gz: 00e06d5d6126eaef0a31a191b36872553be04e19
3
+ metadata.gz: 814f54e9a769f6f73fc7c0cab26eebc4b9a191e1
4
+ data.tar.gz: c346224df919223cba73db9fe74097c235bacd32
5
5
  SHA512:
6
- metadata.gz: d399fbc756aabc8ea5e8a81629c59b5fbe322c36e6597f8bd30112941dfd1185e7621b6ea9b90d8fef64756fb05f0f06c03b9b383ec89ec727a0ef5eaa20fec6
7
- data.tar.gz: c4820f86874017e0f8c44e634d81de5bdf7c5e0f983bdba5173502104180c7351a9d4e8ee567ac79261b7451c4ad4e5003f21b0f0f5a22ef38c30480401f0c64
6
+ metadata.gz: 4e39adc404ed14163bd5b54b4ebdc558a12d4d56be8b60ad54140ee1cbc9102d89e17359a08d7a37eeeb299b99788359777d9f6cf778362969cb45b3182d493a
7
+ data.tar.gz: 667ab8ca0e0b5255184f162210d33a6775b26e952e8868a42a6a8a8ef408b24cec46f118c6fa9400b71a01632a0fb32388e2e23a6ab9362301f2247c2730726a
@@ -0,0 +1,53 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ create_table "event_records", :force => true do |t|
4
+ t.string "aggregate_id", :null => false
5
+ t.string "organization_id"
6
+ t.integer "sequence_number", :null => false
7
+ t.datetime "created_at", :null => false
8
+ t.string "event_type", :null => false
9
+ t.text "event_json", :null => false
10
+ t.integer "command_record_id", :null => false
11
+ t.integer "stream_record_id", :null => false
12
+ end
13
+
14
+ create_table "command_records", :force => true do |t|
15
+ t.string "organization_id"
16
+ t.string "user_id"
17
+ t.string "aggregate_id"
18
+ t.string "command_type", :null => false
19
+ t.text "command_json", :null => false
20
+ t.datetime "created_at", :null => false
21
+ end
22
+
23
+ execute %Q{
24
+ CREATE UNIQUE INDEX unique_event_per_aggregate ON event_records (
25
+ aggregate_id,
26
+ sequence_number,
27
+ (CASE event_type WHEN 'Sequent::Core::SnapshotEvent' THEN 0 ELSE 1 END)
28
+ )
29
+ }
30
+ execute %Q{
31
+ CREATE INDEX snapshot_events ON event_records (aggregate_id, sequence_number DESC) WHERE event_type = 'Sequent::Core::SnapshotEvent'
32
+ }
33
+ add_index "event_records", ["command_record_id"], :name => "index_event_records_on_command_record_id"
34
+ add_index "event_records", ["organization_id"], :name => "index_event_records_on_organization_id"
35
+ add_index "event_records", ["event_type"], :name => "index_event_records_on_event_type"
36
+ add_index "event_records", ["created_at"], :name => "index_event_records_on_created_at"
37
+
38
+ create_table "stream_records", :force => true do |t|
39
+ t.datetime "created_at", :null => false
40
+ t.string "aggregate_type", :null => false
41
+ t.string "aggregate_id", :null => false
42
+ t.integer "snapshot_threshold"
43
+ end
44
+
45
+ add_index "stream_records", ["aggregate_id"], :name => "index_stream_records_on_aggregate_id", :unique => true
46
+ execute %q{
47
+ ALTER TABLE event_records ADD CONSTRAINT command_fkey FOREIGN KEY (command_record_id) REFERENCES command_records (id)
48
+ }
49
+ execute %q{
50
+ ALTER TABLE event_records ADD CONSTRAINT stream_fkey FOREIGN KEY (stream_record_id) REFERENCES stream_records (id)
51
+ }
52
+
53
+ end
@@ -0,0 +1,49 @@
1
+ require_relative 'core/event_store'
2
+ require_relative 'core/command_service'
3
+ require_relative 'core/transactions/no_transactions'
4
+ require_relative 'core/aggregate_repository'
5
+
6
+ module Sequent
7
+ class Configuration
8
+ attr_reader :aggregate_repository
9
+
10
+ attr_accessor :event_store,
11
+ :command_service,
12
+ :event_record_class,
13
+ :stream_record_class,
14
+ :snapshot_event_class,
15
+ :transaction_provider
16
+
17
+ attr_accessor :command_handlers,
18
+ :command_filters
19
+
20
+ attr_accessor :event_handlers
21
+
22
+ def self.instance
23
+ @instance ||= new
24
+ end
25
+
26
+ def self.reset
27
+ @instance = new
28
+ end
29
+
30
+ def initialize
31
+ self.command_handlers = []
32
+ self.command_filters = []
33
+ self.event_handlers = []
34
+
35
+ self.event_store = Sequent::Core::EventStore.new(self)
36
+ self.command_service = Sequent::Core::CommandService.new(self)
37
+ self.event_record_class = Sequent::Core::EventRecord
38
+ self.stream_record_class = Sequent::Core::StreamRecord
39
+ self.snapshot_event_class = Sequent::Core::SnapshotEvent
40
+ self.transaction_provider = Sequent::Core::Transactions::NoTransactions.new
41
+ end
42
+
43
+ def event_store=(event_store)
44
+ @event_store = event_store
45
+ @aggregate_repository = Sequent::Core::AggregateRepository.new(event_store)
46
+ self.command_handlers.each { |c| c.repository = @aggregate_repository }
47
+ end
48
+ end
49
+ end
@@ -16,12 +16,20 @@ module Sequent
16
16
  # Key used in thread local
17
17
  AGGREGATES_KEY = 'Sequent::Core::AggregateRepository::aggregates'.to_sym
18
18
 
19
+ attr_reader :event_store
20
+
19
21
  class NonUniqueAggregateId < Exception
20
22
  def initialize(existing, new)
21
23
  super "Duplicate aggregate #{new} with same key as existing #{existing}"
22
24
  end
23
25
  end
24
26
 
27
+ class AggregateNotFound < Exception
28
+ def initialize(id)
29
+ super "Aggregate with id #{id} not found"
30
+ end
31
+ end
32
+
25
33
  def initialize(event_store)
26
34
  @event_store = event_store
27
35
  clear
@@ -33,8 +41,9 @@ module Sequent
33
41
  # and all uncammited_events are stored in the +event_store+
34
42
  #
35
43
  def add_aggregate(aggregate)
36
- if aggregates.has_key?(aggregate.id)
37
- raise NonUniqueAggregateId.new(aggregate, aggregates[aggregate.id]) unless aggregates[aggregate.id].equal?(aggregate)
44
+ existing = aggregates[aggregate.id]
45
+ if existing && !existing.equal?(aggregate)
46
+ raise NonUniqueAggregateId.new(aggregate, aggregates[aggregate.id])
38
47
  else
39
48
  aggregates[aggregate.id] = aggregate
40
49
  end
@@ -47,17 +56,17 @@ module Sequent
47
56
 
48
57
  # Loads aggregate by given id and class
49
58
  # Returns the one in the current Unit Of Work otherwise loads it from history.
50
- #
51
- # If we implement snapshotting this is the place.
52
- def load_aggregate(aggregate_id, clazz)
53
- if aggregates.has_key?(aggregate_id)
54
- result = aggregates[aggregate_id]
55
- raise TypeError, "#{result.class} is not a #{clazz}" unless result.is_a?(clazz)
56
- result
57
- else
58
- events = @event_store.load_events(aggregate_id)
59
- aggregates[aggregate_id] = clazz.load_from_history(events)
59
+ def load_aggregate(aggregate_id, clazz = nil)
60
+ result = aggregates.fetch(aggregate_id) do |aggregate_id|
61
+ stream, events = @event_store.load_events(aggregate_id)
62
+ raise AggregateNotFound.new(aggregate_id) unless stream
63
+ aggregate_class = Class.const_get(stream.aggregate_type)
64
+ aggregates[aggregate_id] = aggregate_class.load_from_history(stream, events)
60
65
  end
66
+
67
+ raise TypeError, "#{result.class} is not a #{clazz}" if result && clazz && !(result.class <= clazz)
68
+
69
+ result
61
70
  end
62
71
 
63
72
  # Gets all uncommitted_events from the 'registered' aggregates
@@ -68,11 +77,13 @@ module Sequent
68
77
  # This is all abstracted away if you use the Sequent::Core::CommandService
69
78
  #
70
79
  def commit(command)
71
- all_events = []
72
- aggregates.each_value { |aggregate| all_events += aggregate.uncommitted_events }
73
- return if all_events.empty?
74
- aggregates.each_value { |aggregate| aggregate.clear_events }
75
- store_events command, all_events
80
+ updated_aggregates = aggregates.values.reject {|x| x.uncommitted_events.empty?}
81
+ return if updated_aggregates.empty?
82
+ streams_with_events = updated_aggregates.map do |aggregate|
83
+ [ aggregate.event_stream, aggregate.uncommitted_events ]
84
+ end
85
+ updated_aggregates.each(&:clear_events)
86
+ store_events command, streams_with_events
76
87
  end
77
88
 
78
89
  # Clears the Unit of Work.
@@ -86,8 +97,8 @@ module Sequent
86
97
  Thread.current[AGGREGATES_KEY]
87
98
  end
88
99
 
89
- def store_events(command, events)
90
- @event_store.commit_events(command, events)
100
+ def store_events(command, streams_with_events)
101
+ @event_store.commit_events(command, streams_with_events)
91
102
  end
92
103
  end
93
104
  end
@@ -1,19 +1,51 @@
1
+ require 'base64'
1
2
  require_relative 'helpers/self_applier'
3
+ require_relative 'stream_record'
2
4
 
3
5
  module Sequent
4
6
  module Core
7
+ module SnapshotConfiguration
8
+ module ClassMethods
9
+ ##
10
+ # Enable snapshots for this aggregate. The aggregate instance
11
+ # must define the *load_from_snapshot* and *save_to_snapshot*
12
+ # methods.
13
+ #
14
+ def enable_snapshots(default_threshold: 20)
15
+ @snapshot_default_threshold = default_threshold
16
+ end
17
+
18
+ def snapshots_enabled?
19
+ !snapshot_default_threshold.nil?
20
+ end
21
+
22
+ attr_reader :snapshot_default_threshold
23
+ end
24
+
25
+ def self.included(host_class)
26
+ host_class.extend(ClassMethods)
27
+ end
28
+ end
29
+
5
30
  # Base class for all your domain classes.
6
31
  #
7
32
  # +load_from_history+ functionality to be loaded_from_history, meaning a stream of events.
8
33
  #
9
34
  class AggregateRoot
10
35
  include Helpers::SelfApplier
11
-
12
- attr_reader :id, :uncommitted_events, :sequence_number
13
-
14
- def self.load_from_history(events)
15
- aggregate_root = allocate() # allocate without calling new
16
- aggregate_root.load_from_history(events)
36
+ include SnapshotConfiguration
37
+
38
+ attr_reader :id, :uncommitted_events, :sequence_number, :event_stream
39
+
40
+ def self.load_from_history(stream, events)
41
+ first, *rest = events
42
+ if first.is_a? SnapshotEvent
43
+ aggregate_root = Marshal.load(Base64.decode64(first.data))
44
+ rest.each { |x| aggregate_root.apply_event(x) }
45
+ else
46
+ aggregate_root = allocate() # allocate without calling new
47
+ aggregate_root.load_from_history(stream, events)
48
+ end
17
49
  aggregate_root
18
50
  end
19
51
 
@@ -21,23 +53,36 @@ module Sequent
21
53
  @id = id
22
54
  @uncommitted_events = []
23
55
  @sequence_number = 1
56
+ @event_stream = EventStream.new aggregate_type: self.class.name,
57
+ aggregate_id: id,
58
+ snapshot_threshold: self.class.snapshot_default_threshold
24
59
  end
25
60
 
26
- def load_from_history(events)
61
+ def load_from_history(stream, events)
27
62
  raise "Empty history" if events.empty?
28
63
  @id = events.first.aggregate_id
29
64
  @uncommitted_events = []
30
- @sequence_number = events.size + 1
31
- events.each { |event| handle_message(event) }
65
+ @sequence_number = 1
66
+ @event_stream = stream
67
+ events.each { |event| apply_event(event) }
32
68
  end
33
69
 
34
70
  def to_s
35
71
  "#{self.class.name}: #{@id}"
36
72
  end
37
73
 
38
-
39
74
  def clear_events
40
- uncommitted_events.clear
75
+ @uncommitted_events = []
76
+ end
77
+
78
+ def take_snapshot!
79
+ snapshot = build_event SnapshotEvent, data: Base64.encode64(Marshal.dump(self))
80
+ @uncommitted_events << snapshot
81
+ end
82
+
83
+ def apply_event(event)
84
+ handle_message(event)
85
+ @sequence_number = event.sequence_number + 1
41
86
  end
42
87
 
43
88
  protected
@@ -54,9 +99,8 @@ module Sequent
54
99
  #
55
100
  def apply(event, params={})
56
101
  event = build_event(event, params) if event.is_a?(Class)
57
- handle_message(event)
102
+ apply_event(event)
58
103
  @uncommitted_events << event
59
- @sequence_number += 1
60
104
  end
61
105
  end
62
106
 
@@ -71,10 +115,10 @@ module Sequent
71
115
  @organization_id = organization_id
72
116
  end
73
117
 
74
- def load_from_history(events)
118
+ def load_from_history(stream, events)
75
119
  raise "Empty history" if events.empty?
76
120
  @organization_id = events.first.organization_id
77
- super(events)
121
+ super
78
122
  end
79
123
 
80
124
  protected
@@ -0,0 +1,34 @@
1
+ module Sequent
2
+ module Core
3
+ class SnapshotCommand < Sequent::Core::BaseCommand
4
+ attrs limit: Integer
5
+ end
6
+
7
+ class AggregateSnapshotter < BaseCommandHandler
8
+
9
+ def handles_message?(message)
10
+ message.is_a? SnapshotCommand
11
+ end
12
+
13
+ ##
14
+ # Take up to `limit` snapshots when needed. Throws `:done` when done.
15
+ #
16
+ on SnapshotCommand do |command|
17
+ aggregate_ids = repository.event_store.aggregates_that_need_snapshots(@last_aggregate_id, command.limit)
18
+ aggregate_ids.each do |aggregate_id|
19
+ take_snapshot!(aggregate_id)
20
+ end
21
+ @last_aggregate_id = aggregate_ids.last
22
+ throw :done if @last_aggregate_id.nil?
23
+ end
24
+
25
+ def take_snapshot!(aggregate_id)
26
+ aggregate = @repository.load_aggregate(aggregate_id)
27
+ Sequent.logger.info "Taking snapshot for aggregate #{aggregate}"
28
+ aggregate.take_snapshot!
29
+ rescue => e
30
+ Sequent.logger.warn "Failed to take snapshot for aggregate #{aggregate_id}: #{e}", e.inspect
31
+ end
32
+ end
33
+ end
34
+ end
@@ -12,7 +12,7 @@ module Sequent
12
12
  # repository.add_aggregate Invoice.new(command.aggregate_id)
13
13
  # end
14
14
  #
15
- # on PayInvoiceCommanddo |command|
15
+ # on PayInvoiceCommand do |command|
16
16
  # do_with_aggregate(command, Invoice) {|invoice|invoice.pay(command.pay_date)}
17
17
  # end
18
18
  # end
@@ -20,7 +20,9 @@ module Sequent
20
20
  include Sequent::Core::Helpers::SelfApplier,
21
21
  Sequent::Core::Helpers::UuidHelper
22
22
 
23
- def initialize(repository)
23
+ attr_accessor :repository
24
+
25
+ def initialize(repository = Sequent.configuration.aggregate_repository)
24
26
  @repository = repository
25
27
  end
26
28
 
@@ -30,10 +32,6 @@ module Sequent
30
32
  yield aggregate if block_given?
31
33
  end
32
34
 
33
- protected
34
- def repository
35
- @repository
36
- end
37
35
  end
38
36
  end
39
37
  end
@@ -2,21 +2,6 @@ require_relative 'transactions/no_transactions'
2
2
 
3
3
  module Sequent
4
4
  module Core
5
-
6
- class CommandServiceConfiguration
7
- attr_accessor :event_store,
8
- :command_handler_classes,
9
- :transaction_provider,
10
- :filters
11
-
12
- def initialize
13
- @command_handler_classes = []
14
- @transaction_provider = Sequent::Core::Transactions::NoTransactions.new
15
- @filters = []
16
- end
17
-
18
- end
19
-
20
5
  #
21
6
  # Single point in the application where subclasses of Sequent::Core::BaseCommand
22
7
  # are executed. This will initiate the entire flow of:
@@ -28,21 +13,7 @@ module Sequent
28
13
  # * Unit of Work is cleared
29
14
  #
30
15
  class CommandService
31
-
32
- class << self
33
- attr_accessor :configuration,
34
- :instance
35
- end
36
-
37
- # Creates a new CommandService and overwrites all existing config.
38
- # The new CommandService can be retrieved via the +CommandService.instance+ method.
39
- #
40
- # If you don't want a singleton you can always instantiate it yourself using the +CommandService.new+.
41
- def self.configure
42
- self.configuration = CommandServiceConfiguration.new
43
- yield(configuration) if block_given?
44
- self.instance = CommandService.new(configuration)
45
- end
16
+ attr_accessor :configuration
46
17
 
47
18
  # +DefaultCommandServiceConfiguration+ Configuration class for the CommandService containing:
48
19
  #
@@ -51,11 +22,7 @@ module Sequent
51
22
  # +transaction_provider+ How to do transaction management. Defaults to Sequent::Core::Transactions::NoTransactions
52
23
  # +filters+ List of filter that respond_to :execute(command). Can be useful to do extra checks (security and such).
53
24
  def initialize(configuration = CommandServiceConfiguration.new)
54
- @event_store = configuration.event_store
55
- @repository = AggregateRepository.new(configuration.event_store)
56
- @filters = configuration.filters
57
- @transaction_provider = configuration.transaction_provider
58
- @command_handlers = configuration.command_handler_classes.map { |handler| handler.new(@repository) }
25
+ self.configuration = configuration
59
26
  end
60
27
 
61
28
  # Executes the given commands in a single transactional block as implemented by the +transaction_provider+
@@ -67,26 +34,47 @@ module Sequent
67
34
  # * The +repository+ commits the command and all uncommitted_events resulting from the command
68
35
  def execute_commands(*commands)
69
36
  begin
70
- @transaction_provider.transactional do
37
+ transaction_provider.transactional do
71
38
  commands.each do |command|
72
- @filters.each { |filter| filter.execute(command) }
39
+ filters.each { |filter| filter.execute(command) }
73
40
 
74
41
  raise CommandNotValid.new(command) unless command.valid?
75
42
  parsed_command = command.parse_attrs_to_correct_types
76
- @command_handlers.select { |h| h.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
77
- @repository.commit(parsed_command)
43
+ command_handlers.select { |h| h.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
44
+ repository.commit(parsed_command)
78
45
  end
79
46
  end
80
47
  ensure
81
- @repository.clear
48
+ repository.clear
82
49
  end
83
50
 
84
51
  end
85
52
 
86
53
  def remove_event_handler(clazz)
87
- @event_store.remove_event_handler(clazz)
54
+ event_store.remove_event_handler(clazz)
88
55
  end
89
56
 
57
+ private
58
+
59
+ def event_store
60
+ configuration.event_store
61
+ end
62
+
63
+ def repository
64
+ configuration.aggregate_repository
65
+ end
66
+
67
+ def filters
68
+ configuration.command_filters
69
+ end
70
+
71
+ def transaction_provider
72
+ configuration.transaction_provider
73
+ end
74
+
75
+ def command_handlers
76
+ configuration.command_handlers
77
+ end
90
78
  end
91
79
 
92
80
  # Raised when BaseCommand.valid? returns false
@@ -106,9 +94,5 @@ module Sequent
106
94
  errors(@command.class.to_s.underscore)
107
95
  end
108
96
  end
109
-
110
97
  end
111
98
  end
112
-
113
-
114
-
@@ -2,15 +2,16 @@ require_relative 'sequent_oj'
2
2
  require_relative 'helpers/helpers'
3
3
  require_relative 'record_sessions/record_sessions'
4
4
  require_relative 'transactions/transactions'
5
+ require_relative 'event'
5
6
  require_relative 'aggregate_repository'
6
7
  require_relative 'aggregate_root'
7
8
  require_relative 'base_command_handler'
8
9
  require_relative 'command'
9
10
  require_relative 'command_service'
10
- require_relative 'event'
11
11
  require_relative 'value_object'
12
12
  require_relative 'base_event_handler'
13
13
  require_relative 'event_store'
14
14
  require_relative 'tenant_event_store'
15
15
  require_relative 'event_record'
16
16
  require_relative 'command_record'
17
+ require_relative 'aggregate_snapshotter'
@@ -62,7 +62,9 @@ module Sequent
62
62
 
63
63
  end
64
64
 
65
+ class SnapshotEvent < Event
66
+ attrs data: String
67
+ end
68
+
65
69
  end
66
70
  end
67
-
68
-
@@ -5,10 +5,9 @@ module Sequent
5
5
  module Core
6
6
 
7
7
  module SerializesEvent
8
-
9
8
  def event
10
9
  payload = Sequent::Core::Oj.strict_load(self.event_json)
11
- Class.const_get(self.event_type.to_sym).deserialize_from_json(payload)
10
+ Class.const_get(self.event_type).deserialize_from_json(payload)
12
11
  end
13
12
 
14
13
  def event=(event)
@@ -19,7 +18,6 @@ module Sequent
19
18
  self.created_at = event.created_at
20
19
  self.event_json = Sequent::Core::Oj.dump(event.attributes)
21
20
  end
22
-
23
21
  end
24
22
 
25
23
  class EventRecord < ActiveRecord::Base
@@ -27,15 +25,12 @@ module Sequent
27
25
 
28
26
  self.table_name = "event_records"
29
27
 
28
+ belongs_to :stream_record
30
29
  belongs_to :command_record
31
30
 
32
- validates_presence_of :aggregate_id, :sequence_number, :event_type, :event_json
33
- validates_numericality_of :sequence_number
34
-
35
-
36
-
31
+ validates_presence_of :aggregate_id, :sequence_number, :event_type, :event_json, :stream_record, :command_record
32
+ validates_numericality_of :sequence_number, only_integer: true, greater_than: 0
37
33
  end
38
34
 
39
35
  end
40
36
  end
41
-
@@ -1,90 +1,105 @@
1
+ require 'forwardable'
1
2
  require_relative 'event_record'
2
3
  require_relative 'sequent_oj'
3
4
 
4
5
  module Sequent
5
6
  module Core
6
- class EventStoreConfiguration
7
- attr_accessor :record_class, :event_handlers
8
-
9
- def initialize(record_class = Sequent::Core::EventRecord, event_handlers = [])
10
- @record_class = record_class
11
- @event_handlers = event_handlers
12
- end
13
- end
14
7
 
15
8
  class EventStore
9
+ include ActiveRecord::ConnectionAdapters::Quoting
10
+ extend Forwardable
16
11
 
17
- class << self
18
- attr_accessor :configuration,
19
- :instance
20
- end
21
-
22
- # Creates a new EventStore and overwrites all existing config.
23
- # The new EventStore can be retrieved via the +EventStore.instance+ method.
24
- #
25
- # If you don't want a singleton you can always instantiate it yourself using the +EventStore.new+.
26
- def self.configure
27
- self.configuration = EventStoreConfiguration.new
28
- yield(configuration) if block_given?
29
- EventStore.instance = EventStore.new(configuration)
30
- end
12
+ attr_accessor :configuration
13
+ def_delegators :@configuration, :stream_record_class, :event_record_class, :snapshot_event_class, :event_handlers
31
14
 
32
- def initialize(configuration = EventStoreConfiguration.new)
33
- @record_class = configuration.record_class
34
- @event_handlers = configuration.event_handlers
15
+ def initialize(configuration = Sequent.configuration)
16
+ self.configuration = configuration
17
+ @event_types = ThreadSafe::Cache.new
35
18
  end
36
19
 
37
20
  ##
38
21
  # Stores the events in the EventStore and publishes the events
39
22
  # to the registered event_handlers.
40
23
  #
41
- def commit_events(command, events)
42
- store_events(command, events)
43
- publish_events(events, @event_handlers)
24
+ # Streams_with_Events is an enumerable of pairs from
25
+ # `StreamRecord` to arrays of uncommitted `Event`s.
26
+ #
27
+ def commit_events(command, streams_with_events)
28
+ store_events(command, streams_with_events)
29
+ publish_events(streams_with_events.flat_map {|_, events| events}, event_handlers)
44
30
  end
45
31
 
46
32
  ##
47
33
  # Returns all events for the aggregate ordered by sequence_number
48
34
  #
49
35
  def load_events(aggregate_id)
50
- event_types = {}
51
- @record_class.connection.select_all("select event_type, event_json from #{@record_class.table_name} where aggregate_id = '#{aggregate_id}' order by sequence_number asc").map! do |event_hash|
52
- event_type = event_hash["event_type"]
53
- event_json = Sequent::Core::Oj.strict_load(event_hash["event_json"])
54
- unless event_types.has_key?(event_type)
55
- event_types[event_type] = Class.const_get(event_type.to_sym)
56
- end
57
- event_types[event_type].deserialize_from_json(event_json)
36
+ stream = stream_record_class.where(aggregate_id: aggregate_id).first!
37
+ events = event_record_class.connection.select_all(%Q{
38
+ SELECT event_type, event_json
39
+ FROM #{quote_table_name event_record_class.table_name}
40
+ WHERE aggregate_id = #{quote aggregate_id}
41
+ AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
42
+ FROM #{quote_table_name event_record_class.table_name}
43
+ WHERE event_type = #{quote snapshot_event_class.name}
44
+ AND aggregate_id = #{quote aggregate_id}), 0)
45
+ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
46
+ }).map! do |event_hash|
47
+ deserialize_event(event_hash)
58
48
  end
49
+ [stream.event_stream, events]
59
50
  end
60
51
 
61
52
  ##
62
53
  # Replays all events in the event store to the registered event_handlers.
63
54
  #
64
- # @param block that returns the event stream.
55
+ # @param block that returns the events.
65
56
  def replay_events
66
- event_stream = yield
67
- event_types = {}
68
- event_stream.each do |event_hash|
69
- event_type = event_hash["event_type"]
70
- payload = Sequent::Core::Oj.strict_load(event_hash["event_json"])
71
- unless event_types.has_key?(event_type)
72
- event_types[event_type] = Class.const_get(event_type.to_sym)
73
- end
74
- event = event_types[event_type].deserialize_from_json(payload)
75
- @event_handlers.each do |handler|
76
- handler.handle_message event
77
- end
78
- end
57
+ events = yield.map {|event_hash| deserialize_event(event_hash)}
58
+ publish_events(events, event_handlers)
59
+ end
60
+
61
+ ##
62
+ # Returns the ids of aggregates that need a new snapshot.
63
+ #
64
+ def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
65
+ stream_table = quote_table_name stream_record_class.table_name
66
+ event_table = quote_table_name event_record_class.table_name
67
+ query = %Q{
68
+ SELECT aggregate_id
69
+ FROM #{stream_table} stream
70
+ WHERE aggregate_id > COALESCE(#{quote last_aggregate_id}, '')
71
+ AND snapshot_threshold IS NOT NULL
72
+ AND snapshot_threshold <= (
73
+ (SELECT MAX(events.sequence_number) FROM #{event_table} events WHERE events.event_type <> #{quote snapshot_event_class.name} AND stream.aggregate_id = events.aggregate_id) -
74
+ COALESCE((SELECT MAX(snapshots.sequence_number) FROM #{event_table} snapshots WHERE snapshots.event_type = #{quote snapshot_event_class.name} AND stream.aggregate_id = snapshots.aggregate_id), 0))
75
+ ORDER BY aggregate_id
76
+ LIMIT #{quote limit}
77
+ FOR UPDATE
78
+ }
79
+ event_record_class.connection.select_all(query).map {|x| x['aggregate_id']}
79
80
  end
80
81
 
81
- protected
82
- def record_class
83
- @record_class
82
+ def find_event_stream(aggregate_id)
83
+ record = stream_record_class.where(aggregate_id: aggregate_id).first
84
+ if record
85
+ record.event_stream
86
+ else
87
+ nil
88
+ end
84
89
  end
85
90
 
86
91
  private
87
92
 
93
+ def deserialize_event(event_hash)
94
+ event_type = event_hash.fetch("event_type")
95
+ event_json = Sequent::Core::Oj.strict_load(event_hash.fetch("event_json"))
96
+ resolve_event_type(event_type).deserialize_from_json(event_json)
97
+ end
98
+
99
+ def resolve_event_type(event_type)
100
+ @event_types.fetch_or_store(event_type) { |k| Class.const_get(k) }
101
+ end
102
+
88
103
  def publish_events(events, event_handlers)
89
104
  events.each do |event|
90
105
  event_handlers.each do |handler|
@@ -93,17 +108,20 @@ module Sequent
93
108
  end
94
109
  end
95
110
 
96
- def to_events(event_records)
97
- event_records.map(&:event)
98
- end
99
-
100
- def store_events(command, events = [])
101
- command_record = Sequent::Core::CommandRecord.create!(:command => command)
102
- events.each do |event|
103
- @record_class.create!(:command_record => command_record, :event => event)
111
+ def store_events(command, streams_with_events = [])
112
+ command_record = CommandRecord.create!(command: command)
113
+ streams_with_events.each do |event_stream, uncommitted_events|
114
+ unless event_stream.stream_record_id
115
+ stream_record = stream_record_class.new
116
+ stream_record.event_stream = event_stream
117
+ stream_record.save!
118
+ event_stream.stream_record_id = stream_record.id
119
+ end
120
+ uncommitted_events.each do |event|
121
+ event_record_class.create!(command_record: command_record, stream_record_id: event_stream.stream_record_id, event: event)
122
+ end
104
123
  end
105
124
  end
106
-
107
125
  end
108
126
 
109
127
  end
@@ -51,4 +51,13 @@ class Array
51
51
  def self.deserialize_from_json(value)
52
52
  value
53
53
  end
54
+
55
+ end
56
+
57
+ class Hash
58
+
59
+ def self.deserialize_from_json(value)
60
+ value
61
+ end
62
+
54
63
  end
@@ -139,5 +139,3 @@ EOS
139
139
  end
140
140
  end
141
141
  end
142
-
143
-
@@ -12,28 +12,30 @@ module Sequent
12
12
  module ParamSupport
13
13
  module ClassMethods
14
14
  def from_params(params = {})
15
- result = allocate
16
- params = HashWithIndifferentAccess.new(params)
17
- result.class.types.each do |attribute, type|
18
- value = params[attribute]
19
-
20
- next if value.blank?
21
- if type.respond_to? :from_params
22
- value = type.from_params(value)
23
- elsif type.is_a? Sequent::Core::Helpers::ArrayWithType
24
- value = value.map { |v| type.item_type.from_params(v) }
25
- end
26
- result.instance_variable_set(:"@#{attribute}", value)
27
- end
28
- result
15
+ allocate.tap { |x| x.from_params(params) }
29
16
  end
30
-
31
17
  end
18
+
32
19
  # extend host class with class methods when we're included
33
20
  def self.included(host_class)
34
21
  host_class.extend(ClassMethods)
35
22
  end
36
23
 
24
+ def from_params(params)
25
+ params = HashWithIndifferentAccess.new(params)
26
+ self.class.types.each do |attribute, type|
27
+ value = params[attribute]
28
+
29
+ next if value.blank?
30
+ if type.respond_to? :from_params
31
+ value = type.from_params(value)
32
+ elsif type.is_a? Sequent::Core::Helpers::ArrayWithType
33
+ value = value.map { |v| type.item_type.from_params(v) }
34
+ end
35
+ instance_variable_set(:"@#{attribute}", value)
36
+ end
37
+ end
38
+
37
39
  def to_params(root)
38
40
  make_params root, as_params
39
41
  end
@@ -19,12 +19,11 @@ module Sequent
19
19
  module ClassMethods
20
20
 
21
21
  def on(*message_classes, &block)
22
- @message_mapping ||= {}
23
- message_classes.each { |message_class| @message_mapping[message_class] = block }
22
+ message_classes.each { |message_class| message_mapping[message_class] = block }
24
23
  end
25
24
 
26
25
  def message_mapping
27
- @message_mapping || {}
26
+ @message_mapping ||= {}
28
27
  end
29
28
  end
30
29
 
@@ -42,4 +41,3 @@ module Sequent
42
41
  end
43
42
  end
44
43
  end
45
-
@@ -0,0 +1,23 @@
1
+ module Sequent
2
+ module Core
3
+ class Snapshots
4
+
5
+ def aggregates_that_need_snapshots(events_since_last_snapshot: 20, limit: 10, last_aggregate_id: nil)
6
+ query = %Q{
7
+ SELECT aggregate_id
8
+ FROM event_records events
9
+ WHERE aggregate_id > '#{last_aggregate_id}'
10
+ GROUP BY aggregate_id
11
+ HAVING MAX(sequence_number) - (COALESCE((SELECT MAX(sequence_number)
12
+ FROM event_records snapshots
13
+ WHERE event_type = 'Sequent::Core::SnapshotEvent'
14
+ AND snapshots.aggregate_id = events.aggregate_id), 0)) > #{events_since_last_snapshot}
15
+ ORDER BY aggregate_id
16
+ LIMIT #{limit};
17
+ }
18
+ @record_class.connection.select_all(query).to_a
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ require 'active_record'
2
+
3
+ module Sequent
4
+ module Core
5
+ class EventStream
6
+ attr_accessor :aggregate_type, :aggregate_id, :snapshot_threshold, :stream_record_id
7
+
8
+ def initialize(aggregate_type:, aggregate_id:, snapshot_threshold: nil, stream_record_id: nil)
9
+ @aggregate_type = aggregate_type
10
+ @aggregate_id = aggregate_id
11
+ @snapshot_threshold = snapshot_threshold
12
+ @stream_record_id = stream_record_id
13
+ end
14
+ end
15
+
16
+ class StreamRecord < ActiveRecord::Base
17
+
18
+ self.table_name = "stream_records"
19
+
20
+ validates_presence_of :aggregate_type, :aggregate_id
21
+ validates_numericality_of :snapshot_threshold, only_integer: true, greater_than: 0, allow_nil: true
22
+
23
+ has_many :events
24
+
25
+ def event_stream
26
+ EventStream.new(aggregate_type: aggregate_type, aggregate_id: aggregate_id, snapshot_threshold: snapshot_threshold, stream_record_id: id)
27
+ end
28
+
29
+ def event_stream=(data)
30
+ self.aggregate_type = data.aggregate_type
31
+ self.aggregate_id = data.aggregate_id
32
+ self.snapshot_threshold = data.snapshot_threshold
33
+ end
34
+ end
35
+ end
36
+ end
@@ -4,15 +4,21 @@
4
4
  module Sequent
5
5
  module Core
6
6
  class TenantEventStore < EventStore
7
-
8
7
  def replay_events_for(organization_id)
9
8
  replay_events do
10
- aggregate_ids = @record_class.connection.select_all("select distinct aggregate_id from #{@record_class.table_name} where organization_id = '#{organization_id}'").map { |hash| hash["aggregate_id"] }
11
- @record_class.connection.select_all("select id, event_type, event_json from #{@record_class.table_name} where aggregate_id in (#{aggregate_ids.map { |id| %Q{'#{id}'} }.join(",")}) order by id")
9
+ event_record_class.connection.select_all(%Q{
10
+ SELECT events.event_type, events.event_json
11
+ FROM #{quote_table_name event_record_class.table_name} events
12
+ WHERE events.aggregate_id IN (SELECT aggregates.aggregate_id
13
+ FROM #{quote_table_name event_record_class.table_name} aggregates
14
+ WHERE aggregates.organization_id = #{quote organization_id}
15
+ AND aggregates.sequence_number = 1
16
+ AND aggregates.event_type <> #{quote snapshot_event_class.name})
17
+ AND events.event_type <> #{quote snapshot_event_class.name}
18
+ ORDER BY events.id
19
+ })
12
20
  end
13
21
  end
14
-
15
22
  end
16
-
17
23
  end
18
24
  end
@@ -36,10 +36,14 @@ module Sequent
36
36
  def execute_migrations(current_version, new_version, &after_migration_block)
37
37
  if current_version != new_version and current_version > 0
38
38
  ((current_version + 1)..new_version).each do |upgrade_to_version|
39
- migration_class = "MigrateToVersion#{upgrade_to_version}".to_sym
40
- if Kernel.const_defined?(migration_class)
39
+ migration_class = begin
40
+ Class.const_get("Database::MigrateToVersion#{upgrade_to_version}")
41
+ rescue NameError
42
+ nil
43
+ end
44
+ if migration_class
41
45
  begin
42
- Kernel.const_get(migration_class).new(@env).migrate
46
+ migration_class.new(@env).migrate
43
47
  ensure
44
48
  after_migration_block.call if after_migration_block
45
49
  end
@@ -47,7 +51,6 @@ module Sequent
47
51
  end
48
52
  end
49
53
  end
50
-
51
54
  end
52
55
  end
53
56
  end
@@ -1,3 +1,29 @@
1
1
  require_relative 'core/core'
2
2
  require_relative 'migrations/migrations'
3
3
  require_relative 'test/test'
4
+ require_relative 'configuration'
5
+
6
+ require 'logger'
7
+
8
+ module Sequent
9
+ def self.logger
10
+ @logger ||= Logger.new(STDOUT).tap {|l| l.level = Logger::INFO }
11
+ end
12
+
13
+ def self.logger=(logger)
14
+ @logger = logger
15
+ end
16
+
17
+ def self.configure
18
+ yield Configuration.instance
19
+ end
20
+
21
+ def self.configuration
22
+ Configuration.instance
23
+ end
24
+
25
+ # Short hand for Sequent.configuration.command_service
26
+ def self.command_service
27
+ configuration.command_service
28
+ end
29
+ end
@@ -1,3 +1,5 @@
1
+ require 'thread_safe'
2
+
1
3
  module Sequent
2
4
  module Test
3
5
  ##
@@ -36,32 +38,68 @@ module Sequent
36
38
 
37
39
  class FakeEventStore
38
40
  def initialize
39
- @all_events = []
41
+ @event_streams = {}
42
+ @all_events = {}
40
43
  @stored_events = []
41
44
  end
42
45
 
43
46
  def load_events(aggregate_id)
44
- deserialize_events(@all_events).select { |event| aggregate_id == event.aggregate_id }
47
+ event_stream = @event_streams[aggregate_id]
48
+ return nil unless event_stream
49
+ [event_stream, deserialize_events(@all_events[aggregate_id])]
50
+ end
51
+
52
+ def find_event_stream(aggregate_id)
53
+ @event_streams[aggregate_id]
45
54
  end
46
55
 
47
56
  def stored_events
48
57
  deserialize_events(@stored_events)
49
58
  end
50
59
 
51
- def commit_events(_, events)
52
- serialized = serialize_events(events)
53
- @all_events += serialized
54
- @stored_events += serialized
60
+ def commit_events(_, streams_with_events)
61
+ streams_with_events.each do |event_stream, events|
62
+ serialized = serialize_events(events)
63
+ @event_streams[event_stream.aggregate_id] = event_stream
64
+ @all_events[event_stream.aggregate_id] ||= []
65
+ @all_events[event_stream.aggregate_id] += serialized
66
+ @stored_events += serialized
67
+ end
55
68
  end
56
69
 
57
70
  def given_events(events)
58
- @all_events += serialize_events(events)
71
+ commit_events(nil, to_event_streams(events))
59
72
  @stored_events = []
60
73
  end
61
74
 
62
75
  private
76
+
77
+ def to_event_streams(events)
78
+ # Specs use a simple list of given events. We need a mapping from StreamRecord to the associated events for the event store.
79
+ streams_by_aggregate_id = {}
80
+ events.map do |event|
81
+ event_stream = streams_by_aggregate_id.fetch(event.aggregate_id) do |aggregate_id|
82
+ streams_by_aggregate_id[aggregate_id] =
83
+ find_event_stream(aggregate_id) ||
84
+ begin
85
+ aggregate_type = FakeEventStore.aggregate_type_for_event(event)
86
+ raise "cannot find aggregate type associated with creation event #{event}, did you include an event handler in your aggregate for this event?" unless aggregate_type
87
+ Sequent::Core::EventStream.new(aggregate_type: aggregate_type.name, aggregate_id: aggregate_id)
88
+ end
89
+ end
90
+ [event_stream, [event]]
91
+ end
92
+ end
93
+
94
+ def self.aggregate_type_for_event(event)
95
+ @event_to_aggregate_type ||= ThreadSafe::Cache.new
96
+ @event_to_aggregate_type.fetch_or_store(event.class) do |klass|
97
+ Sequent::Core::AggregateRoot.descendants.find { |x| x.message_mapping.has_key?(klass) }
98
+ end
99
+ end
100
+
63
101
  def serialize_events(events)
64
- events.map { |event| [event.class.name.to_sym, Sequent::Core::Oj.dump(event)] }
102
+ events.map { |event| [event.class.name, Sequent::Core::Oj.dump(event)] }
65
103
  end
66
104
 
67
105
  def deserialize_events(events)
@@ -73,7 +111,6 @@ module Sequent
73
111
  end
74
112
 
75
113
  def given_events *events
76
- raise ArgumentError.new("events can not be nil") if events.compact.empty?
77
114
  @event_store.given_events(events)
78
115
  end
79
116
 
@@ -85,9 +122,9 @@ module Sequent
85
122
  end
86
123
 
87
124
  def then_events *events
88
- @event_store.stored_events.map(&:class).should == events.map(&:class)
125
+ expect(@event_store.stored_events.map(&:class)).to eq(events.map(&:class))
89
126
  @event_store.stored_events.zip(events).each do |actual, expected|
90
- Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload)).should == Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload)) if expected
127
+ expect(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))).to eq(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))) if expected
91
128
  end
92
129
  end
93
130
 
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sequent
2
- VERSION = '0.1.4'
2
+ VERSION = '0.1.5'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lars Vonk
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2015-04-24 00:00:00.000000000 Z
13
+ date: 2015-06-25 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -68,6 +68,20 @@ dependencies:
68
68
  - - "~>"
69
69
  - !ruby/object:Gem::Version
70
70
  version: '2.10'
71
+ - !ruby/object:Gem::Dependency
72
+ name: thread_safe
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: 0.3.5
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: 0.3.5
71
85
  - !ruby/object:Gem::Dependency
72
86
  name: rspec
73
87
  requirement: !ruby/object:Gem::Requirement
@@ -124,6 +138,20 @@ dependencies:
124
138
  - - "~>"
125
139
  - !ruby/object:Gem::Version
126
140
  version: '10.4'
141
+ - !ruby/object:Gem::Dependency
142
+ name: pry
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: '0.10'
148
+ type: :development
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: '0.10'
127
155
  description: Sequent is a CQRS and event sourcing framework for Ruby.
128
156
  email:
129
157
  - lars.vonk@gmail.com
@@ -133,9 +161,12 @@ executables: []
133
161
  extensions: []
134
162
  extra_rdoc_files: []
135
163
  files:
164
+ - db/sequent_schema.rb
136
165
  - lib/sequent.rb
166
+ - lib/sequent/configuration.rb
137
167
  - lib/sequent/core/aggregate_repository.rb
138
168
  - lib/sequent/core/aggregate_root.rb
169
+ - lib/sequent/core/aggregate_snapshotter.rb
139
170
  - lib/sequent/core/base_command_handler.rb
140
171
  - lib/sequent/core/base_event_handler.rb
141
172
  - lib/sequent/core/command.rb
@@ -167,6 +198,8 @@ files:
167
198
  - lib/sequent/core/record_sessions/record_sessions.rb
168
199
  - lib/sequent/core/record_sessions/replay_events_session.rb
169
200
  - lib/sequent/core/sequent_oj.rb
201
+ - lib/sequent/core/snapshots.rb
202
+ - lib/sequent/core/stream_record.rb
170
203
  - lib/sequent/core/tenant_event_store.rb
171
204
  - lib/sequent/core/transactions/active_record_transaction_provider.rb
172
205
  - lib/sequent/core/transactions/no_transactions.rb