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 +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
|