sequent 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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