sequent 1.0.0 → 2.0.0

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: f1c376e79b2afb9c653058f4cb38e7d24bb7d742
4
- data.tar.gz: 751c997c10407d7dd073b70b268ed69d16d1b459
3
+ metadata.gz: 2f3c785214713fa790ec8e000314e79730913df4
4
+ data.tar.gz: 0da86f6f53cacc4c007020e021cb849931154a67
5
5
  SHA512:
6
- metadata.gz: 042064fb131306a5197e035330dceb3e467c2031f73932777286ec72219545ef7fcc5bbd3b0322177801d994e87adf66735be07e4b03fbf2e725d209b11e8b09
7
- data.tar.gz: 777558493270ceeb8e80038e6b2336bfebbdbb103e74c341f676e53599971e8723862b3b6aa424d008f294ff78108a580652f31a90600fa90a275387696ba5f2
6
+ metadata.gz: a422b778b039fdb34d9b6782dc06861a6c17472c15773de773d79e5a902854d9792eec2d9463cc93d6ac2fec1bd4b2ec2b54efcb2a4f3adb0d1e2798dddb7ab0
7
+ data.tar.gz: 46c9c7acae7f5148fffbe715d49cf80f245569ac77e0824dfae22fb42949a8e14684f22a850519803b3be42f2364f50a39fb387e68ae7b7c13a0a10ed55b176f
@@ -5,14 +5,15 @@ require_relative 'core/aggregate_repository'
5
5
 
6
6
  module Sequent
7
7
  class Configuration
8
- attr_reader :aggregate_repository
8
+ attr_accessor :aggregate_repository
9
9
 
10
10
  attr_accessor :event_store,
11
11
  :command_service,
12
12
  :event_record_class,
13
13
  :stream_record_class,
14
14
  :snapshot_event_class,
15
- :transaction_provider
15
+ :transaction_provider,
16
+ :event_publisher
16
17
 
17
18
  attr_accessor :command_handlers,
18
19
  :command_filters
@@ -40,20 +41,16 @@ module Sequent
40
41
  self.command_filters = []
41
42
  self.event_handlers = []
42
43
 
43
- self.event_store = Sequent::Core::EventStore.new(self)
44
- self.command_service = Sequent::Core::CommandService.new(self)
44
+ self.aggregate_repository = Sequent::Core::AggregateRepository.new
45
+ self.event_store = Sequent::Core::EventStore.new
46
+ self.command_service = Sequent::Core::CommandService.new
45
47
  self.event_record_class = Sequent::Core::EventRecord
46
48
  self.stream_record_class = Sequent::Core::StreamRecord
47
49
  self.snapshot_event_class = Sequent::Core::SnapshotEvent
48
50
  self.transaction_provider = Sequent::Core::Transactions::NoTransactions.new
49
51
  self.uuid_generator = Sequent::Core::RandomUuidGenerator
52
+ self.event_publisher = Sequent::Core::EventPublisher.new
50
53
  self.disable_event_handlers = false
51
54
  end
52
-
53
- def event_store=(event_store)
54
- @event_store = event_store
55
- @aggregate_repository = Sequent::Core::AggregateRepository.new(event_store)
56
- self.command_handlers.each { |c| c.repository = @aggregate_repository }
57
- end
58
55
  end
59
56
  end
@@ -16,8 +16,6 @@ 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
-
21
19
  class NonUniqueAggregateId < StandardError
22
20
  def initialize(existing, new)
23
21
  super "Duplicate aggregate #{new} with same key as existing #{existing}"
@@ -30,10 +28,6 @@ module Sequent
30
28
  end
31
29
  end
32
30
 
33
- def initialize(event_store)
34
- @event_store = event_store
35
- end
36
-
37
31
  # Adds the given aggregate to the repository (or unit of work).
38
32
  #
39
33
  # Only when +commit+ is called all aggregates in the unit of work are 'processed'
@@ -80,7 +74,7 @@ module Sequent
80
74
  _aggregates = aggregates.values_at(*_aggregate_ids).compact
81
75
  _query_ids = _aggregate_ids - _aggregates.map(&:id)
82
76
 
83
- _aggregates += @event_store.load_events_for_aggregates(_query_ids).map do |stream, events|
77
+ _aggregates += Sequent.configuration.event_store.load_events_for_aggregates(_query_ids).map do |stream, events|
84
78
  aggregate_class = Class.const_get(stream.aggregate_type)
85
79
  aggregate_class.load_from_history(stream, events)
86
80
  end
@@ -104,7 +98,7 @@ module Sequent
104
98
  ##
105
99
  # Returns whether the event store has an aggregate with the given id
106
100
  def contains_aggregate?(aggregate_id)
107
- @event_store.stream_exists?(aggregate_id)
101
+ Sequent.configuration.event_store.stream_exists?(aggregate_id)
108
102
  end
109
103
 
110
104
  # Gets all uncommitted_events from the 'registered' aggregates
@@ -136,7 +130,7 @@ module Sequent
136
130
  end
137
131
 
138
132
  def store_events(command, streams_with_events)
139
- @event_store.commit_events(command, streams_with_events)
133
+ Sequent.configuration.event_store.commit_events(command, streams_with_events)
140
134
  end
141
135
  end
142
136
  end
@@ -1,18 +1,20 @@
1
1
  module Sequent
2
2
  module Core
3
+
4
+ ##
5
+ # Take up to `limit` snapshots when needed. Throws `:done` when done.
6
+ #
3
7
  class SnapshotCommand < Sequent::Core::BaseCommand
4
8
  attrs limit: Integer
5
9
  end
6
10
 
7
- class AggregateSnapshotter < BaseCommandHandler
11
+ ##
12
+ # Take snapshot of given aggregate
13
+ class TakeSnapshot < Sequent::Core::Command
14
+ end
8
15
 
9
- def self.handles_message?(message)
10
- message.is_a? SnapshotCommand
11
- end
16
+ class AggregateSnapshotter < BaseCommandHandler
12
17
 
13
- ##
14
- # Take up to `limit` snapshots when needed. Throws `:done` when done.
15
- #
16
18
  on SnapshotCommand do |command|
17
19
  aggregate_ids = repository.event_store.aggregates_that_need_snapshots(@last_aggregate_id, command.limit)
18
20
  aggregate_ids.each do |aggregate_id|
@@ -22,8 +24,12 @@ module Sequent
22
24
  throw :done if @last_aggregate_id.nil?
23
25
  end
24
26
 
27
+ on TakeSnapshot do |command|
28
+ take_snapshot!(command.aggregate_id)
29
+ end
30
+
25
31
  def take_snapshot!(aggregate_id)
26
- aggregate = @repository.load_aggregate(aggregate_id)
32
+ aggregate = repository.load_aggregate(aggregate_id)
27
33
  Sequent.logger.info "Taking snapshot for aggregate #{aggregate}"
28
34
  aggregate.take_snapshot!
29
35
  rescue => e
@@ -20,16 +20,14 @@ module Sequent
20
20
  include Sequent::Core::Helpers::SelfApplier,
21
21
  Sequent::Core::Helpers::UuidHelper
22
22
 
23
- attr_accessor :repository
23
+ protected
24
24
 
25
- def initialize(repository = Sequent.configuration.aggregate_repository)
26
- @repository = repository
25
+ def repository
26
+ Sequent.configuration.aggregate_repository
27
27
  end
28
28
 
29
- protected
30
-
31
29
  def do_with_aggregate(command, clazz = nil, aggregate_id = nil)
32
- aggregate = @repository.load_aggregate(aggregate_id.nil? ? command.aggregate_id : aggregate_id, clazz)
30
+ aggregate = repository.load_aggregate(aggregate_id.nil? ? command.aggregate_id : aggregate_id, clazz)
33
31
  yield aggregate if block_given?
34
32
  end
35
33
  end
@@ -42,7 +42,7 @@ module Sequent
42
42
  @record_session = record_session
43
43
  end
44
44
 
45
- def_delegators :@record_session, :update_record, :create_record, :create_or_update_record, :get_record!, :get_record,
45
+ def_delegators :@record_session, :update_record, :create_record, :create_records, :create_or_update_record, :get_record!, :get_record,
46
46
  :delete_all_records, :update_all_records, :do_with_records, :do_with_record, :delete_record,
47
47
  :find_records, :last_record, :execute
48
48
 
@@ -13,19 +13,6 @@ module Sequent
13
13
  # * Unit of Work is cleared
14
14
  #
15
15
  class CommandService
16
- attr_accessor :configuration
17
-
18
- # Create a command service with the given configuration.
19
- #
20
- # +event_store+ The Sequent::Core::EventStore
21
- # +aggregate_repository+ The Sequent::Core::AggregateRepository
22
- # +transaction_provider+ How to do transaction management.
23
- # +command_handlers+ List of command handlers that need to handle commands
24
- # +command_filters+ List of filter that respond_to :execute(command). Can be useful to do extra checks (security and such).
25
- def initialize(configuration)
26
- self.configuration = configuration
27
- end
28
-
29
16
  # Executes the given commands in a single transactional block as implemented by the +transaction_provider+
30
17
  #
31
18
  # For each command:
@@ -34,21 +21,8 @@ module Sequent
34
21
  # * If the command is valid all +command_handlers+ that +handles_message?+ is invoked
35
22
  # * The +repository+ commits the command and all uncommitted_events resulting from the command
36
23
  def execute_commands(*commands)
37
- begin
38
- transaction_provider.transactional do
39
- commands.each do |command|
40
- filters.each { |filter| filter.execute(command) }
41
-
42
- raise CommandNotValid.new(command) unless command.valid?
43
- parsed_command = command.parse_attrs_to_correct_types
44
- command_handlers.select { |h| h.class.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
45
- repository.commit(parsed_command)
46
- end
47
- end
48
- ensure
49
- repository.clear
50
- end
51
-
24
+ commands.each { |command| command_queue.push(command) }
25
+ process_commands
52
26
  end
53
27
 
54
28
  def remove_event_handler(clazz)
@@ -57,24 +31,52 @@ module Sequent
57
31
 
58
32
  private
59
33
 
34
+ def process_commands
35
+ Sequent::Util.skip_if_already_processing(:command_service_process_commands) do
36
+ begin
37
+ transaction_provider.transactional do
38
+ while(!command_queue.empty?) do
39
+ process_command(command_queue.pop)
40
+ end
41
+ end
42
+ ensure
43
+ command_queue.clear
44
+ repository.clear
45
+ end
46
+ end
47
+ end
48
+
49
+ def process_command(command)
50
+ filters.each { |filter| filter.execute(command) }
51
+
52
+ raise CommandNotValid.new(command) unless command.valid?
53
+ parsed_command = command.parse_attrs_to_correct_types
54
+ command_handlers.select { |h| h.class.handles_message?(parsed_command) }.each { |h| h.handle_message parsed_command }
55
+ repository.commit(parsed_command)
56
+ end
57
+
58
+ def command_queue
59
+ Thread.current[:command_service_commands] ||= Queue.new
60
+ end
61
+
60
62
  def event_store
61
- configuration.event_store
63
+ Sequent.configuration.event_store
62
64
  end
63
65
 
64
66
  def repository
65
- configuration.aggregate_repository
67
+ Sequent.configuration.aggregate_repository
66
68
  end
67
69
 
68
70
  def filters
69
- configuration.command_filters
71
+ Sequent.configuration.command_filters
70
72
  end
71
73
 
72
74
  def transaction_provider
73
- configuration.transaction_provider
75
+ Sequent.configuration.transaction_provider
74
76
  end
75
77
 
76
78
  def command_handlers
77
- configuration.command_handlers
79
+ Sequent.configuration.command_handlers
78
80
  end
79
81
  end
80
82
 
@@ -16,3 +16,4 @@ require_relative 'command_record'
16
16
  require_relative 'aggregate_snapshotter'
17
17
  require_relative 'workflow'
18
18
  require_relative 'random_uuid_generator'
19
+ require_relative 'event_publisher'
@@ -0,0 +1,63 @@
1
+ module Sequent
2
+ module Core
3
+ #
4
+ # EventPublisher ensures that, for every thread, events will be published in the order in which they are queued for publishing.
5
+ #
6
+ # This potentially introduces a wrinkle into your plans: You therefore should not split a "unit of work" across multiple threads.
7
+ #
8
+ # If you want other behaviour, you are free to implement your own version of EventPublisher and configure Sequent to use it.
9
+ #
10
+ class EventPublisher
11
+ class PublishEventError < RuntimeError
12
+ attr_reader :event_handler_class, :event
13
+
14
+ def initialize(event_handler_class, event)
15
+ @event_handler_class = event_handler_class
16
+ @event = event
17
+ end
18
+
19
+ def message
20
+ "Event Handler: #{@event_handler_class.inspect}\nEvent: #{@event.inspect}\nCause: #{cause.inspect}"
21
+ end
22
+ end
23
+
24
+ def publish_events(events)
25
+ return if configuration.disable_event_handlers
26
+ events.each { |event| events_queue.push(event) }
27
+ process_events
28
+ end
29
+
30
+ private
31
+
32
+ def events_queue
33
+ Thread.current[:events_queue] ||= Queue.new
34
+ end
35
+
36
+ def process_events
37
+ Sequent::Util.skip_if_already_processing(:events_queue_lock) do
38
+ begin
39
+ while(!events_queue.empty?) do
40
+ process_event(events_queue.pop)
41
+ end
42
+ ensure
43
+ events_queue.clear
44
+ end
45
+ end
46
+ end
47
+
48
+ def process_event(event)
49
+ configuration.event_handlers.each do |handler|
50
+ begin
51
+ handler.handle_message event
52
+ rescue
53
+ raise PublishEventError.new(handler.class, event)
54
+ end
55
+ end
56
+ end
57
+
58
+ def configuration
59
+ Sequent.configuration
60
+ end
61
+ end
62
+ end
63
+ end
@@ -9,19 +9,6 @@ module Sequent
9
9
  include ActiveRecord::ConnectionAdapters::Quoting
10
10
  extend Forwardable
11
11
 
12
- class PublishEventError < RuntimeError
13
- attr_reader :event_handler_class, :event
14
-
15
- def initialize(event_handler_class, event)
16
- @event_handler_class = event_handler_class
17
- @event = event
18
- end
19
-
20
- def message
21
- "Event Handler: #{@event_handler_class.inspect}\nEvent: #{@event.inspect}\nCause: #{cause.inspect}"
22
- end
23
- end
24
-
25
12
  class OptimisticLockingError < RuntimeError
26
13
  end
27
14
 
@@ -38,11 +25,7 @@ module Sequent
38
25
 
39
26
  end
40
27
 
41
- attr_accessor :configuration
42
- def_delegators :@configuration, :stream_record_class, :event_record_class, :snapshot_event_class, :event_handlers
43
-
44
- def initialize(configuration = Sequent.configuration)
45
- self.configuration = configuration
28
+ def initialize
46
29
  @event_types = ThreadSafe::Cache.new
47
30
  end
48
31
 
@@ -55,7 +38,7 @@ module Sequent
55
38
  #
56
39
  def commit_events(command, streams_with_events)
57
40
  store_events(command, streams_with_events)
58
- publish_events(streams_with_events.flat_map { |_, events| events }, event_handlers)
41
+ publish_events(streams_with_events.flat_map { |_, events| events })
59
42
  end
60
43
 
61
44
  ##
@@ -68,18 +51,10 @@ module Sequent
68
51
  def load_events_for_aggregates(aggregate_ids)
69
52
  return [] if aggregate_ids.none?
70
53
 
71
- streams = stream_record_class.where(aggregate_id: aggregate_ids)
54
+ streams = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_ids)
72
55
 
73
- events = event_record_class.connection.select_all(%Q{
74
- SELECT event_type, event_json
75
- FROM #{quote_table_name event_record_class.table_name}
76
- WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.join(",")})
77
- AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
78
- FROM #{quote_table_name event_record_class.table_name}
79
- WHERE event_type = #{quote snapshot_event_class.name}
80
- AND aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.join(",")})), 0)
81
- ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
82
- }).map! do |event_hash|
56
+ query = aggregate_ids.uniq.map { |aggregate_id| aggregate_query(aggregate_id) }.join(" UNION ALL ")
57
+ events = Sequent.configuration.event_record_class.connection.select_all(query).map! do |event_hash|
83
58
  deserialize_event(event_hash)
84
59
  end
85
60
 
@@ -88,8 +63,21 @@ WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.
88
63
  .map { |aggregate_id, _events| [streams.find { |stream_record| stream_record.aggregate_id == aggregate_id }.event_stream, _events] }
89
64
  end
90
65
 
66
+ def aggregate_query(aggregate_id)
67
+ %Q{(
68
+ SELECT event_type, event_json
69
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS o
70
+ WHERE aggregate_id = #{quote(aggregate_id)}
71
+ AND sequence_number >= COALESCE((SELECT MAX(sequence_number)
72
+ FROM #{quote_table_name Sequent.configuration.event_record_class.table_name} AS i
73
+ WHERE event_type = #{quote Sequent.configuration.snapshot_event_class.name}
74
+ AND i.aggregate_id = #{quote(aggregate_id)}), 0)
75
+ ORDER BY sequence_number ASC, (CASE event_type WHEN #{quote Sequent.configuration.snapshot_event_class.name} THEN 0 ELSE 1 END) ASC
76
+ )}
77
+ end
78
+
91
79
  def stream_exists?(aggregate_id)
92
- stream_record_class.exists?(aggregate_id: aggregate_id)
80
+ Sequent.configuration.stream_record_class.exists?(aggregate_id: aggregate_id)
93
81
  end
94
82
 
95
83
  ##
@@ -100,7 +88,7 @@ WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.
100
88
  def replay_events
101
89
  warn "[DEPRECATION] `replay_events` is deprecated in favor of `replay_events_from_cursor`"
102
90
  events = yield.map { |event_hash| deserialize_event(event_hash) }
103
- publish_events(events, event_handlers)
91
+ publish_events(events)
104
92
  end
105
93
 
106
94
  ##
@@ -118,7 +106,7 @@ WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.
118
106
  ids_replayed = []
119
107
  cursor.each_row(block_size: block_size).each do |record|
120
108
  event = deserialize_event(record)
121
- publish_events([event], event_handlers)
109
+ publish_events([event])
122
110
  progress += 1
123
111
  ids_replayed << record['id']
124
112
  if progress % block_size == 0
@@ -141,25 +129,25 @@ WHERE aggregate_id in (#{aggregate_ids.map{ |aggregate_id| quote(aggregate_id)}.
141
129
  # Returns the ids of aggregates that need a new snapshot.
142
130
  #
143
131
  def aggregates_that_need_snapshots(last_aggregate_id, limit = 10)
144
- stream_table = quote_table_name stream_record_class.table_name
145
- event_table = quote_table_name event_record_class.table_name
132
+ stream_table = quote_table_name Sequent.configuration.stream_record_class.table_name
133
+ event_table = quote_table_name Sequent.configuration.event_record_class.table_name
146
134
  query = %Q{
147
135
  SELECT aggregate_id
148
136
  FROM #{stream_table} stream
149
137
  WHERE aggregate_id::varchar > COALESCE(#{quote last_aggregate_id}, '')
150
138
  AND snapshot_threshold IS NOT NULL
151
139
  AND snapshot_threshold <= (
152
- (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) -
153
- 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))
140
+ (SELECT MAX(events.sequence_number) FROM #{event_table} events WHERE events.event_type <> #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = events.aggregate_id) -
141
+ COALESCE((SELECT MAX(snapshots.sequence_number) FROM #{event_table} snapshots WHERE snapshots.event_type = #{quote Sequent.configuration.snapshot_event_class.name} AND stream.aggregate_id = snapshots.aggregate_id), 0))
154
142
  ORDER BY aggregate_id
155
143
  LIMIT #{quote limit}
156
144
  FOR UPDATE
157
145
  }
158
- event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
146
+ Sequent.configuration.event_record_class.connection.select_all(query).map { |x| x['aggregate_id'] }
159
147
  end
160
148
 
161
149
  def find_event_stream(aggregate_id)
162
- record = stream_record_class.where(aggregate_id: aggregate_id).first
150
+ record = Sequent.configuration.stream_record_class.where(aggregate_id: aggregate_id).first
163
151
  if record
164
152
  record.event_stream
165
153
  else
@@ -170,7 +158,7 @@ SELECT aggregate_id
170
158
  private
171
159
 
172
160
  def column_names
173
- @column_names ||= event_record_class.column_names.reject { |c| c == 'id' }
161
+ @column_names ||= Sequent.configuration.event_record_class.column_names.reject { |c| c == 'id' }
174
162
  end
175
163
 
176
164
  def deserialize_event(event_hash)
@@ -185,24 +173,15 @@ SELECT aggregate_id
185
173
  @event_types.fetch_or_store(event_type) { |k| Class.const_get(k) }
186
174
  end
187
175
 
188
- def publish_events(events, event_handlers)
189
- return if configuration.disable_event_handlers
190
- event_handlers.each do |handler|
191
- events.each do |event|
192
- begin
193
- handler.handle_message event
194
- rescue
195
- raise PublishEventError.new(handler.class, event)
196
- end
197
- end
198
- end
176
+ def publish_events(events)
177
+ Sequent.configuration.event_publisher.publish_events(events)
199
178
  end
200
179
 
201
180
  def store_events(command, streams_with_events = [])
202
181
  command_record = CommandRecord.create!(command: command)
203
182
  event_records = streams_with_events.flat_map do |event_stream, uncommitted_events|
204
183
  unless event_stream.stream_record_id
205
- stream_record = stream_record_class.new
184
+ stream_record = Sequent.configuration.stream_record_class.new
206
185
  stream_record.event_stream = event_stream
207
186
  stream_record.save!
208
187
  event_stream.stream_record_id = stream_record.id
@@ -214,21 +193,21 @@ SELECT aggregate_id
214
193
  aggregate_id: event.aggregate_id,
215
194
  sequence_number: event.sequence_number,
216
195
  event_type: event.class.name,
217
- event_json: event_record_class.serialize_to_json(event),
196
+ event_json: Sequent.configuration.event_record_class.serialize_to_json(event),
218
197
  created_at: event.created_at
219
198
  }
220
199
  values = values.merge(organization_id: event.organization_id) if event.respond_to?(:organization_id)
221
200
 
222
- event_record_class.new(values)
201
+ Sequent.configuration.event_record_class.new(values)
223
202
  end
224
203
  end
225
- connection = event_record_class.connection
204
+ connection = Sequent.configuration.event_record_class.connection
226
205
  values = event_records
227
206
  .map { |r| "(#{column_names.map { |c| connection.quote(r[c.to_sym]) }.join(',')})" }
228
207
  .join(',')
229
208
  columns = column_names.map { |c| connection.quote_column_name(c) }.join(',')
230
- sql = %Q{insert into #{connection.quote_table_name(event_record_class.table_name)} (#{columns}) values #{values}}
231
- event_record_class.connection.insert(sql)
209
+ sql = %Q{insert into #{connection.quote_table_name(Sequent.configuration.event_record_class.table_name)} (#{columns}) values #{values}}
210
+ Sequent.configuration.event_record_class.connection.insert(sql)
232
211
  rescue ActiveRecord::RecordNotUnique
233
212
  fail OptimisticLockingError.new
234
213
  end
@@ -3,6 +3,7 @@ require 'active_record'
3
3
  module Sequent
4
4
  module Core
5
5
  module RecordSessions
6
+
6
7
  #
7
8
  # Session objects are used to update view state
8
9
  #
@@ -33,6 +34,21 @@ module Sequent
33
34
  record
34
35
  end
35
36
 
37
+ def create_records(record_class, array_of_value_hashes)
38
+ table = record_class.arel_table
39
+
40
+ query = array_of_value_hashes.map do |values|
41
+ insert_manager = new_insert_manager
42
+ insert_manager.into(table)
43
+ insert_manager.insert(values.map do |key, value|
44
+ convert_to_values(key, table, value)
45
+ end)
46
+ insert_manager.to_sql
47
+ end.join(";")
48
+
49
+ execute(query)
50
+ end
51
+
36
52
  def create_or_update_record(record_class, values, created_at = Time.now)
37
53
  record = get_record(record_class, values)
38
54
  unless record
@@ -91,8 +107,22 @@ module Sequent
91
107
  record_class.unscoped.new(values)
92
108
  end
93
109
 
94
- end
110
+ def new_insert_manager
111
+ if ActiveRecord::VERSION::MAJOR <= 4
112
+ Arel::InsertManager.new(ActiveRecord::Base)
113
+ else
114
+ Arel::InsertManager.new
115
+ end
116
+ end
95
117
 
118
+ def convert_to_values(key, table, value)
119
+ if ActiveRecord::VERSION::MAJOR <= 4
120
+ [table[key], value]
121
+ else
122
+ [table[key], table.type_cast_for_database(key, value)]
123
+ end
124
+ end
125
+ end
96
126
  end
97
127
  end
98
128
  end
@@ -1,5 +1,6 @@
1
1
  require 'set'
2
2
  require 'active_record'
3
+ require 'csv'
3
4
 
4
5
  module Sequent
5
6
  module Core
@@ -198,6 +199,10 @@ module Sequent
198
199
  record
199
200
  end
200
201
 
202
+ def create_records(record_class, array_of_value_hashes)
203
+ array_of_value_hashes.each { |values| create_record(record_class, values) }
204
+ end
205
+
201
206
  def create_or_update_record(record_class, values, created_at = Time.now)
202
207
  record = get_record(record_class, values)
203
208
  unless record
@@ -287,10 +292,8 @@ module Sequent
287
292
  csv = CSV.new("")
288
293
  column_names = clazz.column_names.reject { |name| name == "id" }
289
294
  records.each do |obj|
290
- begin
291
- csv << column_names.map do |column_name|
292
- @column_cache[clazz.name][column_name].type_cast_for_database(obj[column_name])
293
- end
295
+ csv << column_names.map do |column_name|
296
+ ActiveRecord::Base.connection.type_cast(obj[column_name], @column_cache[clazz.name][column_name])
294
297
  end
295
298
  end
296
299
 
@@ -311,7 +314,7 @@ module Sequent
311
314
  prepared_values = (1..column_names.size).map { |i| "$#{i}" }.join(",")
312
315
  records.each do |r|
313
316
  values = column_names.map do |column_name|
314
- @column_cache[clazz.name][column_name].type_cast_for_database(r[column_name.to_sym])
317
+ ActiveRecord::Base.connection.type_cast(r[column_name.to_sym], @column_cache[clazz.name][column_name])
315
318
  end
316
319
  inserts << values
317
320
  end
@@ -6,7 +6,7 @@ module Sequent
6
6
  class Oj
7
7
  ::Oj.default_options = {
8
8
  bigdecimal_as_decimal: false,
9
- mode: :compat,
9
+ mode: :rails,
10
10
  }
11
11
 
12
12
  def self.strict_load(json)
@@ -1,4 +1,5 @@
1
1
  require_relative 'core/core'
2
+ require_relative 'util/util'
2
3
  require_relative 'migrations/migrations'
3
4
  require_relative 'configuration'
4
5
 
@@ -43,11 +43,11 @@ module Sequent
43
43
  extend ActiveRecord::ConnectionAdapters::Quoting
44
44
 
45
45
  ORDERED_BY_STREAM = lambda do |event_store|
46
- event_records = quote_table_name(event_store.event_record_class.table_name)
47
- stream_records = quote_table_name(event_store.stream_record_class.table_name)
48
- snapshot_event_type = quote(event_store.snapshot_event_class)
46
+ event_records = quote_table_name(Sequent.configuration.event_record_class.table_name)
47
+ stream_records = quote_table_name(Sequent.configuration.stream_record_class.table_name)
48
+ snapshot_event_type = quote(Sequent.configuration.snapshot_event_class)
49
49
 
50
- event_store.event_record_class
50
+ Sequent.configuration.event_record_class
51
51
  .select("event_type, event_json")
52
52
  .joins("INNER JOIN #{stream_records} ON #{event_records}.stream_record_id = #{stream_records}.id")
53
53
  .where("event_type <> #{snapshot_event_type}")
@@ -1,4 +1,5 @@
1
1
  require 'thread_safe'
2
+ require 'sequent/core/event_store'
2
3
 
3
4
  module Sequent
4
5
  module Test
@@ -22,9 +23,9 @@ module Sequent
22
23
  # describe InvoiceCommandHandler do
23
24
  #
24
25
  # before :each do
25
- # @event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
26
- # @repository = Sequent::Core::AggregateRepository.new(@event_store)
27
- # @command_handler = InvoiceCommandHandler.new(@repository)
26
+ # Sequent.configuration.event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
27
+ # Sequent.configuration.command_handlers = [] # add your command handlers here
28
+ # Sequent.configuration.event_handlers = [] # add you event handlers (eg, workflows) here
28
29
  # end
29
30
  #
30
31
  # it "marks an invoice as paid" do
@@ -37,6 +38,8 @@ module Sequent
37
38
  module CommandHandlerHelpers
38
39
 
39
40
  class FakeEventStore
41
+ extend Forwardable
42
+
40
43
  def initialize
41
44
  @event_streams = {}
42
45
  @all_events = {}
@@ -73,6 +76,11 @@ module Sequent
73
76
  @all_events[event_stream.aggregate_id] += serialized
74
77
  @stored_events += serialized
75
78
  end
79
+ publish_events(streams_with_events.flat_map { |_, events| events })
80
+ end
81
+
82
+ def publish_events(events)
83
+ Sequent.configuration.event_publisher.publish_events(events)
76
84
  end
77
85
 
78
86
  def given_events(events)
@@ -122,22 +130,18 @@ module Sequent
122
130
  end
123
131
 
124
132
  def given_events *events
125
- @event_store.given_events(events.flatten(1))
133
+ Sequent.configuration.event_store.given_events(events.flatten(1))
126
134
  end
127
135
 
128
136
  def when_command command
129
- raise "@command_handler is mandatory when using the #{self.class}" unless @command_handler
130
- raise "Command handler #{@command_handler} cannot handle command #{command}, please configure the command type (forgot an include in the command class?)" unless @command_handler.class.handles_message?(command)
131
- @command_handler.handle_message(command)
132
- @repository.commit(command)
133
- @repository.clear
137
+ Sequent.configuration.command_service.execute_commands command
134
138
  end
135
139
 
136
140
  def then_events(*expected_events)
137
141
  expected_classes = expected_events.flatten(1).map { |event| event.class == Class ? event : event.class }
138
- expect(@event_store.stored_events.map(&:class)).to eq(expected_classes)
142
+ expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
139
143
 
140
- @event_store.stored_events.zip(expected_events.flatten(1)).each_with_index do |(actual, expected), index|
144
+ Sequent.configuration.event_store.stored_events.zip(expected_events.flatten(1)).each_with_index do |(actual, expected), index|
141
145
  next if expected.class == Class
142
146
  _actual = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))
143
147
  _expected = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))
@@ -41,9 +41,9 @@ module Sequent
41
41
 
42
42
  def then_events(*expected_events)
43
43
  expected_classes = expected_events.flatten(1).map { |event| event.class == Class ? event : event.class }
44
- expect(@event_store.stored_events.map(&:class)).to eq(expected_classes)
44
+ expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
45
45
 
46
- @event_store.stored_events.zip(expected_events.flatten(1)).each do |actual, expected|
46
+ Sequent.configuration.event_store.stored_events.zip(expected_events.flatten(1)).each do |actual, expected|
47
47
  next if expected.class == Class
48
48
  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
49
49
  end
@@ -0,0 +1,15 @@
1
+ module Sequent
2
+ module Util
3
+ def self.skip_if_already_processing(already_processing_key, &block)
4
+ return if Thread.current[already_processing_key]
5
+
6
+ begin
7
+ Thread.current[already_processing_key] = true
8
+
9
+ block.yield
10
+ ensure
11
+ Thread.current[already_processing_key] = nil
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ require_relative 'skip_if_already_processing'
@@ -1,3 +1,3 @@
1
1
  module Sequent
2
- VERSION = '1.0.0'
2
+ VERSION = '2.0.0'
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: 1.0.0
4
+ version: 2.0.0
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: 2017-06-30 00:00:00.000000000 Z
13
+ date: 2017-11-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 2.17.5
89
+ version: 3.3.9
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 2.17.5
96
+ version: 3.3.9
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: thread_safe
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -214,6 +214,7 @@ files:
214
214
  - lib/sequent/core/command_service.rb
215
215
  - lib/sequent/core/core.rb
216
216
  - lib/sequent/core/event.rb
217
+ - lib/sequent/core/event_publisher.rb
217
218
  - lib/sequent/core/event_record.rb
218
219
  - lib/sequent/core/event_store.rb
219
220
  - lib/sequent/core/ext/ext.rb
@@ -239,7 +240,6 @@ files:
239
240
  - lib/sequent/core/record_sessions/record_sessions.rb
240
241
  - lib/sequent/core/record_sessions/replay_events_session.rb
241
242
  - lib/sequent/core/sequent_oj.rb
242
- - lib/sequent/core/snapshots.rb
243
243
  - lib/sequent/core/stream_record.rb
244
244
  - lib/sequent/core/transactions/active_record_transaction_provider.rb
245
245
  - lib/sequent/core/transactions/no_transactions.rb
@@ -259,6 +259,8 @@ files:
259
259
  - lib/sequent/test/event_handler_helpers.rb
260
260
  - lib/sequent/test/event_stream_helpers.rb
261
261
  - lib/sequent/test/time_comparison.rb
262
+ - lib/sequent/util/skip_if_already_processing.rb
263
+ - lib/sequent/util/util.rb
262
264
  - lib/version.rb
263
265
  homepage: https://github.com/zilverline/sequent
264
266
  licenses:
@@ -280,7 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
280
282
  version: '0'
281
283
  requirements: []
282
284
  rubyforge_project:
283
- rubygems_version: 2.4.5
285
+ rubygems_version: 2.5.2
284
286
  signing_key:
285
287
  specification_version: 4
286
288
  summary: Event sourcing framework for Ruby
@@ -1,23 +0,0 @@
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