sequent 1.0.0 → 2.0.0
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 +4 -4
- data/lib/sequent/configuration.rb +7 -10
- data/lib/sequent/core/aggregate_repository.rb +3 -9
- data/lib/sequent/core/aggregate_snapshotter.rb +14 -8
- data/lib/sequent/core/base_command_handler.rb +4 -6
- data/lib/sequent/core/base_event_handler.rb +1 -1
- data/lib/sequent/core/command_service.rb +35 -33
- data/lib/sequent/core/core.rb +1 -0
- data/lib/sequent/core/event_publisher.rb +63 -0
- data/lib/sequent/core/event_store.rb +36 -57
- data/lib/sequent/core/record_sessions/active_record_session.rb +31 -1
- data/lib/sequent/core/record_sessions/replay_events_session.rb +8 -5
- data/lib/sequent/core/sequent_oj.rb +1 -1
- data/lib/sequent/sequent.rb +1 -0
- data/lib/sequent/support/view_projection.rb +4 -4
- data/lib/sequent/test/command_handler_helpers.rb +15 -11
- data/lib/sequent/test/event_handler_helpers.rb +2 -2
- data/lib/sequent/util/skip_if_already_processing.rb +15 -0
- data/lib/sequent/util/util.rb +1 -0
- data/lib/version.rb +1 -1
- metadata +8 -6
- data/lib/sequent/core/snapshots.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f3c785214713fa790ec8e000314e79730913df4
|
4
|
+
data.tar.gz: 0da86f6f53cacc4c007020e021cb849931154a67
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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.
|
44
|
-
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 +=
|
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
|
-
|
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
|
-
|
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
|
-
|
11
|
+
##
|
12
|
+
# Take snapshot of given aggregate
|
13
|
+
class TakeSnapshot < Sequent::Core::Command
|
14
|
+
end
|
8
15
|
|
9
|
-
|
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 =
|
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
|
-
|
23
|
+
protected
|
24
24
|
|
25
|
-
def
|
26
|
-
|
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 =
|
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
|
-
|
38
|
-
|
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
|
|
data/lib/sequent/core/core.rb
CHANGED
@@ -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
|
-
|
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 }
|
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
|
-
|
74
|
-
|
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
|
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]
|
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
|
189
|
-
|
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
|
-
|
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
|
-
|
291
|
-
|
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]
|
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
|
data/lib/sequent/sequent.rb
CHANGED
@@ -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(
|
47
|
-
stream_records = quote_table_name(
|
48
|
-
snapshot_event_type = quote(
|
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
|
-
|
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
|
-
#
|
26
|
-
#
|
27
|
-
#
|
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
|
-
|
133
|
+
Sequent.configuration.event_store.given_events(events.flatten(1))
|
126
134
|
end
|
127
135
|
|
128
136
|
def when_command command
|
129
|
-
|
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(
|
142
|
+
expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
|
139
143
|
|
140
|
-
|
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(
|
44
|
+
expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
|
45
45
|
|
46
|
-
|
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'
|
data/lib/version.rb
CHANGED
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:
|
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-
|
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:
|
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:
|
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.
|
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
|