sequent 3.3.1 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sequent +31 -25
- data/lib/notices.rb +6 -0
- data/lib/sequent/application_record.rb +2 -0
- data/lib/sequent/configuration.rb +29 -29
- data/lib/sequent/core/aggregate_repository.rb +24 -14
- data/lib/sequent/core/aggregate_root.rb +16 -7
- data/lib/sequent/core/aggregate_roots.rb +24 -0
- data/lib/sequent/core/aggregate_snapshotter.rb +8 -5
- data/lib/sequent/core/base_command_handler.rb +4 -2
- data/lib/sequent/core/command.rb +30 -11
- data/lib/sequent/core/command_record.rb +12 -4
- data/lib/sequent/core/command_service.rb +41 -25
- data/lib/sequent/core/core.rb +2 -0
- data/lib/sequent/core/current_event.rb +2 -0
- data/lib/sequent/core/event.rb +16 -11
- data/lib/sequent/core/event_publisher.rb +20 -15
- data/lib/sequent/core/event_record.rb +7 -7
- data/lib/sequent/core/event_store.rb +75 -49
- data/lib/sequent/core/ext/ext.rb +9 -1
- data/lib/sequent/core/helpers/array_with_type.rb +4 -1
- data/lib/sequent/core/helpers/association_validator.rb +9 -7
- data/lib/sequent/core/helpers/attribute_support.rb +64 -33
- data/lib/sequent/core/helpers/autoset_attributes.rb +4 -4
- data/lib/sequent/core/helpers/boolean_validator.rb +6 -1
- data/lib/sequent/core/helpers/copyable.rb +2 -2
- data/lib/sequent/core/helpers/date_time_validator.rb +4 -1
- data/lib/sequent/core/helpers/date_validator.rb +6 -1
- data/lib/sequent/core/helpers/default_validators.rb +12 -10
- data/lib/sequent/core/helpers/equal_support.rb +8 -6
- data/lib/sequent/core/helpers/helpers.rb +2 -0
- data/lib/sequent/core/helpers/mergable.rb +6 -4
- data/lib/sequent/core/helpers/message_handler.rb +3 -1
- data/lib/sequent/core/helpers/param_support.rb +19 -15
- data/lib/sequent/core/helpers/secret.rb +14 -12
- data/lib/sequent/core/helpers/string_support.rb +5 -4
- data/lib/sequent/core/helpers/string_to_value_parsers.rb +7 -2
- data/lib/sequent/core/helpers/string_validator.rb +6 -1
- data/lib/sequent/core/helpers/type_conversion_support.rb +5 -3
- data/lib/sequent/core/helpers/uuid_helper.rb +5 -2
- data/lib/sequent/core/helpers/value_validators.rb +23 -9
- data/lib/sequent/core/persistors/active_record_persistor.rb +19 -9
- data/lib/sequent/core/persistors/persistor.rb +16 -14
- data/lib/sequent/core/persistors/persistors.rb +2 -0
- data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +70 -47
- data/lib/sequent/core/projector.rb +25 -22
- data/lib/sequent/core/random_uuid_generator.rb +2 -0
- data/lib/sequent/core/sequent_oj.rb +2 -0
- data/lib/sequent/core/stream_record.rb +9 -3
- data/lib/sequent/core/transactions/active_record_transaction_provider.rb +7 -9
- data/lib/sequent/core/transactions/no_transactions.rb +2 -1
- data/lib/sequent/core/transactions/transactions.rb +2 -0
- data/lib/sequent/core/value_object.rb +8 -10
- data/lib/sequent/core/workflow.rb +7 -5
- data/lib/sequent/generator/aggregate.rb +16 -10
- data/lib/sequent/generator/command.rb +26 -19
- data/lib/sequent/generator/event.rb +19 -17
- data/lib/sequent/generator/generator.rb +6 -0
- data/lib/sequent/generator/project.rb +3 -1
- data/lib/sequent/generator/template_project/Gemfile +1 -1
- data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +1 -1
- data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +1 -1
- data/lib/sequent/generator.rb +3 -4
- data/lib/sequent/migrations/executor.rb +30 -9
- data/lib/sequent/migrations/functions.rb +5 -6
- data/lib/sequent/migrations/migrate_events.rb +12 -9
- data/lib/sequent/migrations/migrations.rb +2 -1
- data/lib/sequent/migrations/planner.rb +33 -23
- data/lib/sequent/migrations/projectors.rb +4 -3
- data/lib/sequent/migrations/sql.rb +2 -0
- data/lib/sequent/migrations/view_schema.rb +93 -44
- data/lib/sequent/rake/migration_tasks.rb +59 -23
- data/lib/sequent/rake/tasks.rb +5 -2
- data/lib/sequent/sequent.rb +6 -1
- data/lib/sequent/support/database.rb +39 -17
- data/lib/sequent/support/view_projection.rb +6 -3
- data/lib/sequent/support/view_schema.rb +2 -0
- data/lib/sequent/support.rb +2 -0
- data/lib/sequent/test/command_handler_helpers.rb +39 -17
- data/lib/sequent/test/event_handler_helpers.rb +10 -4
- data/lib/sequent/test/event_stream_helpers.rb +7 -3
- data/lib/sequent/test/time_comparison.rb +12 -5
- data/lib/sequent/test.rb +2 -0
- data/lib/sequent/util/dry_run.rb +194 -0
- data/lib/sequent/util/printer.rb +6 -5
- data/lib/sequent/util/skip_if_already_processing.rb +21 -5
- data/lib/sequent/util/timer.rb +2 -0
- data/lib/sequent/util/util.rb +3 -0
- data/lib/sequent.rb +4 -0
- data/lib/version.rb +3 -1
- metadata +110 -59
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/hash_with_indifferent_access'
|
2
4
|
|
3
5
|
module Sequent
|
@@ -16,20 +18,25 @@ module Sequent
|
|
16
18
|
end
|
17
19
|
|
18
20
|
def self.read_config(env)
|
19
|
-
fail ArgumentError
|
21
|
+
fail ArgumentError, 'env is mandatory' unless env
|
20
22
|
|
21
23
|
database_yml = File.join(Sequent.configuration.database_config_directory, 'database.yml')
|
22
|
-
YAML.
|
24
|
+
config = YAML.safe_load(ERB.new(File.read(database_yml)).result, aliases: true)[env]
|
25
|
+
if Gem.loaded_specs['activerecord'].version >= Gem::Version.create('6.1.0')
|
26
|
+
ActiveRecord::Base.configurations.resolve(config).configuration_hash.with_indifferent_access
|
27
|
+
else
|
28
|
+
ActiveRecord::Base.resolve_config_for_connection(config)
|
29
|
+
end
|
23
30
|
end
|
24
31
|
|
25
32
|
def self.create!(db_config)
|
26
|
-
ActiveRecord::Base.establish_connection(db_config.merge(
|
27
|
-
ActiveRecord::Base.connection.create_database(db_config[
|
33
|
+
ActiveRecord::Base.establish_connection(db_config.merge(database: 'postgres'))
|
34
|
+
ActiveRecord::Base.connection.create_database(db_config[:database])
|
28
35
|
end
|
29
36
|
|
30
37
|
def self.drop!(db_config)
|
31
|
-
ActiveRecord::Base.establish_connection(db_config.merge(
|
32
|
-
ActiveRecord::Base.connection.drop_database(db_config[
|
38
|
+
ActiveRecord::Base.establish_connection(db_config.merge(database: 'postgres'))
|
39
|
+
ActiveRecord::Base.connection.drop_database(db_config[:database])
|
33
40
|
end
|
34
41
|
|
35
42
|
def self.establish_connection(db_config)
|
@@ -46,9 +53,8 @@ module Sequent
|
|
46
53
|
|
47
54
|
def self.create_schema(schema)
|
48
55
|
sql = "CREATE SCHEMA IF NOT EXISTS #{schema}"
|
49
|
-
|
50
|
-
|
51
|
-
end
|
56
|
+
user = configuration_hash[:username]
|
57
|
+
sql += %( AUTHORIZATION "#{user}") if user
|
52
58
|
execute_sql(sql)
|
53
59
|
end
|
54
60
|
|
@@ -57,27 +63,41 @@ module Sequent
|
|
57
63
|
end
|
58
64
|
|
59
65
|
def self.with_schema_search_path(search_path, db_config, env = ENV['RACK_ENV'])
|
60
|
-
fail ArgumentError
|
66
|
+
fail ArgumentError, 'env is required' unless env
|
61
67
|
|
62
68
|
disconnect!
|
63
|
-
original_search_paths = db_config[
|
64
|
-
|
65
|
-
|
69
|
+
original_search_paths = db_config[:schema_search_path].dup
|
70
|
+
|
71
|
+
if ActiveRecord::VERSION::MAJOR < 6
|
72
|
+
ActiveRecord::Base.configurations[env.to_s] =
|
73
|
+
ActiveSupport::HashWithIndifferentAccess.new(db_config).stringify_keys
|
74
|
+
end
|
75
|
+
|
76
|
+
db_config[:schema_search_path] = search_path
|
77
|
+
|
66
78
|
ActiveRecord::Base.establish_connection db_config
|
67
79
|
|
68
80
|
yield
|
69
81
|
ensure
|
70
82
|
disconnect!
|
71
|
-
db_config[
|
83
|
+
db_config[:schema_search_path] = original_search_paths
|
72
84
|
establish_connection(db_config)
|
73
85
|
end
|
74
86
|
|
75
87
|
def self.schema_exists?(schema)
|
76
88
|
ActiveRecord::Base.connection.execute(
|
77
|
-
"SELECT schema_name FROM information_schema.schemata WHERE schema_name like '#{schema}'"
|
89
|
+
"SELECT schema_name FROM information_schema.schemata WHERE schema_name like '#{schema}'",
|
78
90
|
).count == 1
|
79
91
|
end
|
80
92
|
|
93
|
+
def self.configuration_hash
|
94
|
+
if Gem.loaded_specs['activesupport'].version >= Gem::Version.create('6.1.0')
|
95
|
+
ActiveRecord::Base.connection_db_config.configuration_hash
|
96
|
+
else
|
97
|
+
ActiveRecord::Base.connection_config
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
81
101
|
def schema_exists?(schema)
|
82
102
|
self.class.schema_exists?(schema)
|
83
103
|
end
|
@@ -94,9 +114,11 @@ module Sequent
|
|
94
114
|
self.class.execute_sql(sql)
|
95
115
|
end
|
96
116
|
|
97
|
-
def migrate(migrations_path, verbose: true)
|
117
|
+
def migrate(migrations_path, schema_migration: ActiveRecord::SchemaMigration, verbose: true)
|
98
118
|
ActiveRecord::Migration.verbose = verbose
|
99
|
-
if ActiveRecord::VERSION::MAJOR >=
|
119
|
+
if ActiveRecord::VERSION::MAJOR >= 6
|
120
|
+
ActiveRecord::MigrationContext.new([migrations_path], schema_migration).up
|
121
|
+
elsif ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2
|
100
122
|
ActiveRecord::MigrationContext.new([migrations_path]).up
|
101
123
|
else
|
102
124
|
ActiveRecord::Migrator.migrate(migrations_path)
|
@@ -1,9 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'postgresql_cursor'
|
2
4
|
|
3
5
|
module Sequent
|
4
6
|
module Support
|
5
7
|
class ViewProjection
|
6
8
|
attr_reader :name, :version, :schema_definition
|
9
|
+
|
7
10
|
def initialize(options)
|
8
11
|
@name = options.fetch(:name)
|
9
12
|
@version = options.fetch(:version)
|
@@ -20,7 +23,7 @@ module Sequent
|
|
20
23
|
ordering = Events::ORDERED_BY_STREAM
|
21
24
|
event_store.replay_events_from_cursor(
|
22
25
|
block_size: 10_000,
|
23
|
-
get_events: ->
|
26
|
+
get_events: -> { ordering[event_store] },
|
24
27
|
)
|
25
28
|
end
|
26
29
|
end
|
@@ -42,13 +45,13 @@ module Sequent
|
|
42
45
|
module Events
|
43
46
|
extend ActiveRecord::ConnectionAdapters::Quoting
|
44
47
|
|
45
|
-
ORDERED_BY_STREAM =
|
48
|
+
ORDERED_BY_STREAM = ->(_event_store) do
|
46
49
|
event_records = quote_table_name(Sequent.configuration.event_record_class.table_name)
|
47
50
|
stream_records = quote_table_name(Sequent.configuration.stream_record_class.table_name)
|
48
51
|
snapshot_event_type = quote(Sequent.configuration.snapshot_event_class)
|
49
52
|
|
50
53
|
Sequent.configuration.event_record_class
|
51
|
-
.select(
|
54
|
+
.select('event_type, event_json')
|
52
55
|
.joins("INNER JOIN #{stream_records} ON #{event_records}.stream_record_id = #{stream_records}.id")
|
53
56
|
.where("event_type <> #{snapshot_event_type}")
|
54
57
|
.order!("#{stream_records}.id, #{event_records}.sequence_number")
|
data/lib/sequent/support.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'thread_safe'
|
2
4
|
require 'sequent/core/event_store'
|
3
5
|
|
@@ -36,7 +38,6 @@ module Sequent
|
|
36
38
|
#
|
37
39
|
# end
|
38
40
|
module CommandHandlerHelpers
|
39
|
-
|
40
41
|
class FakeEventStore
|
41
42
|
extend Forwardable
|
42
43
|
|
@@ -89,21 +90,31 @@ module Sequent
|
|
89
90
|
end
|
90
91
|
|
91
92
|
def stream_exists?(aggregate_id)
|
92
|
-
@event_streams.
|
93
|
+
@event_streams.key?(aggregate_id)
|
94
|
+
end
|
95
|
+
|
96
|
+
def events_exists?(aggregate_id)
|
97
|
+
@event_streams[aggregate_id].present?
|
93
98
|
end
|
94
99
|
|
95
100
|
private
|
96
101
|
|
97
102
|
def to_event_streams(events)
|
98
|
-
# Specs use a simple list of given events.
|
103
|
+
# Specs use a simple list of given events.
|
104
|
+
# We need a mapping from StreamRecord to the associated events for the event store.
|
99
105
|
streams_by_aggregate_id = {}
|
100
106
|
events.map do |event|
|
101
107
|
event_stream = streams_by_aggregate_id.fetch(event.aggregate_id) do |aggregate_id|
|
102
108
|
streams_by_aggregate_id[aggregate_id] =
|
103
109
|
find_event_stream(aggregate_id) ||
|
104
110
|
begin
|
105
|
-
aggregate_type =
|
106
|
-
|
111
|
+
aggregate_type = aggregate_type_for_event(event)
|
112
|
+
unless aggregate_type
|
113
|
+
fail <<~EOS
|
114
|
+
Cannot find aggregate type associated with creation event #{event}, did you include an event handler in your aggregate for this event?
|
115
|
+
EOS
|
116
|
+
end
|
117
|
+
|
107
118
|
Sequent::Core::EventStream.new(aggregate_type: aggregate_type.name, aggregate_id: aggregate_id)
|
108
119
|
end
|
109
120
|
end
|
@@ -111,10 +122,10 @@ module Sequent
|
|
111
122
|
end
|
112
123
|
end
|
113
124
|
|
114
|
-
def
|
125
|
+
def aggregate_type_for_event(event)
|
115
126
|
@event_to_aggregate_type ||= ThreadSafe::Cache.new
|
116
127
|
@event_to_aggregate_type.fetch_or_store(event.class) do |klass|
|
117
|
-
Sequent::Core::
|
128
|
+
Sequent::Core::AggregateRoots.all.find { |x| x.message_mapping.key?(klass) }
|
118
129
|
end
|
119
130
|
end
|
120
131
|
|
@@ -129,30 +140,41 @@ module Sequent
|
|
129
140
|
end
|
130
141
|
end
|
131
142
|
|
132
|
-
def given_events
|
143
|
+
def given_events(*events)
|
133
144
|
Sequent.configuration.event_store.given_events(events.flatten(1))
|
134
145
|
end
|
135
146
|
|
136
|
-
def when_command
|
147
|
+
def when_command(command)
|
137
148
|
Sequent.configuration.command_service.execute_commands command
|
138
149
|
end
|
139
150
|
|
140
151
|
def then_events(*expected_events)
|
141
|
-
expected_classes = expected_events.flatten(1).map { |event| event.
|
152
|
+
expected_classes = expected_events.flatten(1).map { |event| event.instance_of?(Class) ? event : event.class }
|
142
153
|
expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
|
143
154
|
|
144
|
-
Sequent
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
155
|
+
Sequent
|
156
|
+
.configuration
|
157
|
+
.event_store
|
158
|
+
.stored_events
|
159
|
+
.zip(expected_events.flatten(1))
|
160
|
+
.each_with_index do |(actual, expected), index|
|
161
|
+
next if expected.instance_of?(Class)
|
162
|
+
|
163
|
+
actual_hash = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload))
|
164
|
+
expected_hash = Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload))
|
165
|
+
next unless expected
|
166
|
+
|
167
|
+
# rubocop:disable Layout/LineLength
|
168
|
+
expect(actual_hash)
|
169
|
+
.to eq(expected_hash),
|
170
|
+
"#{index + 1}th Event of type #{actual.class} not equal\nexpected: #{expected_hash.inspect}\n got: #{actual_hash.inspect}"
|
171
|
+
# rubocop:enable Layout/LineLength
|
172
|
+
end
|
150
173
|
end
|
151
174
|
|
152
175
|
def then_no_events
|
153
176
|
then_events
|
154
177
|
end
|
155
|
-
|
156
178
|
end
|
157
179
|
end
|
158
180
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Sequent
|
2
4
|
module Test
|
3
5
|
##
|
@@ -26,7 +28,6 @@ module Sequent
|
|
26
28
|
# end
|
27
29
|
# end
|
28
30
|
module WorkflowHelpers
|
29
|
-
|
30
31
|
class FakeTransactionProvider
|
31
32
|
def initialize
|
32
33
|
@after_commit_blocks = []
|
@@ -55,12 +56,17 @@ module Sequent
|
|
55
56
|
end
|
56
57
|
|
57
58
|
def then_events(*expected_events)
|
58
|
-
expected_classes = expected_events.flatten(1).map { |event| event.
|
59
|
+
expected_classes = expected_events.flatten(1).map { |event| event.instance_of?(Class) ? event : event.class }
|
59
60
|
expect(Sequent.configuration.event_store.stored_events.map(&:class)).to eq(expected_classes)
|
60
61
|
|
61
62
|
Sequent.configuration.event_store.stored_events.zip(expected_events.flatten(1)).each do |actual, expected|
|
62
|
-
next if expected.
|
63
|
-
|
63
|
+
next if expected.instance_of?(Class)
|
64
|
+
|
65
|
+
next unless expected
|
66
|
+
|
67
|
+
expect(
|
68
|
+
Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(actual.payload)),
|
69
|
+
).to eq(Sequent::Core::Oj.strict_load(Sequent::Core::Oj.dump(expected.payload)))
|
64
70
|
end
|
65
71
|
end
|
66
72
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Sequent
|
2
4
|
module Test
|
3
5
|
##
|
@@ -41,12 +43,14 @@ module Sequent
|
|
41
43
|
@events = []
|
42
44
|
end
|
43
45
|
|
46
|
+
# rubocop:disable Style/MissingRespondToMissing
|
44
47
|
def method_missing(name, *args, &block)
|
45
48
|
args = prepare_arguments(args)
|
46
49
|
@events << FactoryBot.build(name, *args, &block)
|
47
50
|
end
|
51
|
+
# rubocop:enable Style/MissingRespondToMissing
|
48
52
|
|
49
|
-
|
53
|
+
private
|
50
54
|
|
51
55
|
def prepare_arguments(args)
|
52
56
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
@@ -68,10 +72,10 @@ module Sequent
|
|
68
72
|
given_events(*event_stream(aggregate_id: aggregate_id, &block))
|
69
73
|
end
|
70
74
|
|
71
|
-
def self.included(
|
75
|
+
def self.included(_spec)
|
72
76
|
require 'factory_bot'
|
73
77
|
rescue LoadError
|
74
|
-
raise ArgumentError,
|
78
|
+
raise ArgumentError, 'Factory bot is required to use the event stream helpers'
|
75
79
|
end
|
76
80
|
end
|
77
81
|
end
|
@@ -1,20 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Sequent
|
2
4
|
module Test
|
3
5
|
module DateTimePatches
|
4
6
|
module Normalize
|
5
7
|
def normalize
|
6
|
-
in_time_zone(
|
8
|
+
in_time_zone('UTC')
|
7
9
|
end
|
8
10
|
end
|
9
11
|
|
10
12
|
module Compare
|
11
|
-
|
13
|
+
# rubocop:disable Style/Alias
|
14
|
+
alias :'___<=>' :'<=>'
|
15
|
+
# rubocop:enable Style/Alias
|
12
16
|
|
13
17
|
# omit nsec in datetime comparisons
|
14
18
|
def <=>(other)
|
15
|
-
if other
|
19
|
+
if other&.is_a?(DateTimePatches::Normalize)
|
16
20
|
result = normalize.iso8601 <=> other.normalize.iso8601
|
17
21
|
return result unless result == 0
|
22
|
+
|
18
23
|
# use usec here, which *truncates* the nsec (ie. like Postgres)
|
19
24
|
return normalize.usec <=> other.normalize.usec
|
20
25
|
end
|
@@ -35,6 +40,8 @@ class DateTime
|
|
35
40
|
prepend Sequent::Test::DateTimePatches::Compare
|
36
41
|
end
|
37
42
|
|
38
|
-
|
39
|
-
|
43
|
+
module ActiveSupport
|
44
|
+
class TimeWithZone
|
45
|
+
prepend Sequent::Test::DateTimePatches::Normalize
|
46
|
+
end
|
40
47
|
end
|
data/lib/sequent/test.rb
CHANGED
@@ -0,0 +1,194 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../test/command_handler_helpers'
|
4
|
+
|
5
|
+
module Sequent
|
6
|
+
module Util
|
7
|
+
##
|
8
|
+
# Dry run provides the ability to inspect what would
|
9
|
+
# happen if the given commands would be executed
|
10
|
+
# without actually committing the results.
|
11
|
+
# You can inspect which commands are executed
|
12
|
+
# and what the resulting events would be
|
13
|
+
# with theSequent::Projector's and Sequent::Workflow's
|
14
|
+
# that would be invoked (without actually invoking them).
|
15
|
+
#
|
16
|
+
# Since the Workflow's are not actually invoked new commands
|
17
|
+
# resulting from this Workflow will of course not be in the result.
|
18
|
+
#
|
19
|
+
# Caution: Since the Sequent Configuration is shared between threads this method
|
20
|
+
# is not Thread safe.
|
21
|
+
#
|
22
|
+
# Example usage:
|
23
|
+
#
|
24
|
+
# result = Sequent.dry_run(create_foo_command, ping_foo_command)
|
25
|
+
#
|
26
|
+
# result.print(STDOUT)
|
27
|
+
#
|
28
|
+
module DryRun
|
29
|
+
EventInvokedHandler = Struct.new(:event, :handler)
|
30
|
+
|
31
|
+
##
|
32
|
+
# Proxies the given EventStore implements commit_events
|
33
|
+
# that instead of publish and store just publishes the events.
|
34
|
+
class EventStoreProxy
|
35
|
+
attr_reader :command_with_events, :event_store
|
36
|
+
|
37
|
+
delegate :load_events_for_aggregates,
|
38
|
+
:load_events,
|
39
|
+
:publish_events,
|
40
|
+
:stream_exists?,
|
41
|
+
to: :event_store
|
42
|
+
|
43
|
+
def initialize(result)
|
44
|
+
@event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
|
45
|
+
@command_with_events = {}
|
46
|
+
@result = result
|
47
|
+
end
|
48
|
+
|
49
|
+
def commit_events(command, streams_with_events)
|
50
|
+
event_store.commit_events(command, streams_with_events)
|
51
|
+
|
52
|
+
new_events = streams_with_events.flat_map { |_, events| events }
|
53
|
+
@result.published_command_with_events(command, new_events)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Records which Projector's and Workflow's are executed
|
59
|
+
#
|
60
|
+
class RecordingEventPublisher < Sequent::Core::EventPublisher
|
61
|
+
attr_reader :projectors, :workflows
|
62
|
+
|
63
|
+
def initialize(result)
|
64
|
+
super()
|
65
|
+
@result = result
|
66
|
+
end
|
67
|
+
|
68
|
+
def process_event(event)
|
69
|
+
Sequent.configuration.event_handlers.each do |handler|
|
70
|
+
next unless handler.class.handles_message?(event)
|
71
|
+
|
72
|
+
if handler.is_a?(Sequent::Workflow)
|
73
|
+
@result.invoked_workflow(EventInvokedHandler.new(event, handler.class))
|
74
|
+
elsif handler.is_a?(Sequent::Projector)
|
75
|
+
@result.invoked_projector(EventInvokedHandler.new(event, handler.class))
|
76
|
+
else
|
77
|
+
fail "Unrecognized event_handler #{handler.class} called for event #{event.class}"
|
78
|
+
end
|
79
|
+
rescue StandardError
|
80
|
+
raise PublishEventError.new(handler.class, event)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Contains the result of a dry run.
|
87
|
+
#
|
88
|
+
# @see #tree
|
89
|
+
# @see #print
|
90
|
+
#
|
91
|
+
class Result
|
92
|
+
EventCalledHandlers = Struct.new(:event, :projectors, :workflows)
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
@command_with_events = {}
|
96
|
+
@event_invoked_projectors = []
|
97
|
+
@event_invoked_workflows = []
|
98
|
+
end
|
99
|
+
|
100
|
+
def invoked_projector(event_invoked_handler)
|
101
|
+
event_invoked_projectors << event_invoked_handler
|
102
|
+
end
|
103
|
+
|
104
|
+
def invoked_workflow(event_invoked_handler)
|
105
|
+
event_invoked_workflows << event_invoked_handler
|
106
|
+
end
|
107
|
+
|
108
|
+
def published_command_with_events(command, events)
|
109
|
+
command_with_events[command] = events
|
110
|
+
end
|
111
|
+
|
112
|
+
##
|
113
|
+
# Returns the command with events as a tree structure.
|
114
|
+
#
|
115
|
+
# {
|
116
|
+
# command => [
|
117
|
+
# EventCalledHandlers,
|
118
|
+
# EventCalledHandlers,
|
119
|
+
# EventCalledHandlers,
|
120
|
+
# ]
|
121
|
+
# }
|
122
|
+
#
|
123
|
+
# The EventCalledHandlers contains an Event with the
|
124
|
+
# lists of `Sequent::Projector`s and `Sequent::Workflow`s
|
125
|
+
# that were called.
|
126
|
+
#
|
127
|
+
def tree
|
128
|
+
command_with_events.reduce({}) do |memo, (command, events)|
|
129
|
+
events_to_handlers = events.map do |event|
|
130
|
+
for_current_event = ->(pair) { pair.event == event }
|
131
|
+
EventCalledHandlers.new(
|
132
|
+
event,
|
133
|
+
event_invoked_projectors.select(&for_current_event).map(&:handler),
|
134
|
+
event_invoked_workflows.select(&for_current_event).map(&:handler),
|
135
|
+
)
|
136
|
+
end
|
137
|
+
memo[command] = events_to_handlers
|
138
|
+
memo
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Prints the output from #tree to the given `io`
|
144
|
+
#
|
145
|
+
def print(io)
|
146
|
+
tree.each_with_index do |(command, event_called_handlerss), index|
|
147
|
+
io.puts '+++++++++++++++++++++++++++++++++++' if index == 0
|
148
|
+
io.puts "Command: #{command.class} resulted in #{event_called_handlerss.length} events"
|
149
|
+
event_called_handlerss.each_with_index do |event_called_handlers, i|
|
150
|
+
io.puts '' if i > 0
|
151
|
+
io.puts "-- Event #{event_called_handlers.event.class} was handled by:"
|
152
|
+
io.puts "-- Projectors: [#{event_called_handlers.projectors.join(', ')}]"
|
153
|
+
io.puts "-- Workflows: [#{event_called_handlers.workflows.join(', ')}]"
|
154
|
+
end
|
155
|
+
|
156
|
+
io.puts '+++++++++++++++++++++++++++++++++++'
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
attr_reader :command_with_events, :event_invoked_projectors, :event_invoked_workflows
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Main method of the DryRun.
|
167
|
+
#
|
168
|
+
# Caution: Since the Sequent Configuration is changed and is shared between threads this method
|
169
|
+
# is not Thread safe.
|
170
|
+
#
|
171
|
+
# After invocation the sequent configuration is reset to the state it was before
|
172
|
+
# invoking this method.
|
173
|
+
#
|
174
|
+
# @param commands - the commands to dry run
|
175
|
+
# @return Result - the Result of the dry run. See Result.
|
176
|
+
#
|
177
|
+
def self.these_commands(commands)
|
178
|
+
current_event_store = Sequent.configuration.event_store
|
179
|
+
current_event_publisher = Sequent.configuration.event_publisher
|
180
|
+
result = Result.new
|
181
|
+
|
182
|
+
Sequent.configuration.event_store = EventStoreProxy.new(result)
|
183
|
+
Sequent.configuration.event_publisher = RecordingEventPublisher.new(result)
|
184
|
+
|
185
|
+
Sequent.command_service.execute_commands(*commands)
|
186
|
+
|
187
|
+
result
|
188
|
+
ensure
|
189
|
+
Sequent.configuration.event_store = current_event_store
|
190
|
+
Sequent.configuration.event_publisher = current_event_publisher
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
data/lib/sequent/util/printer.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Sequent
|
2
4
|
module Util
|
3
5
|
module Printer
|
4
6
|
def recursively_print(e)
|
5
|
-
logger.error "#{e
|
7
|
+
logger.error "#{e}\n\n#{e.backtrace.join("\n")}"
|
6
8
|
|
7
|
-
while e.cause
|
8
|
-
logger.error
|
9
|
-
logger.error "#{e.cause
|
9
|
+
while e.cause
|
10
|
+
logger.error '+++++++++++++++ CAUSE +++++++++++++++'
|
11
|
+
logger.error "#{e.cause}\n\n#{e.cause.backtrace.join("\n")}"
|
10
12
|
e = e.cause
|
11
13
|
end
|
12
14
|
end
|
13
15
|
end
|
14
16
|
end
|
15
17
|
end
|
16
|
-
|
@@ -1,15 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Sequent
|
2
4
|
module Util
|
3
|
-
|
4
|
-
|
5
|
+
##
|
6
|
+
# Returns if the current Thread is already processing work
|
7
|
+
# given the +processing_key+ otherwise
|
8
|
+
# it yields the given +&block+.
|
9
|
+
#
|
10
|
+
# Useful in a Queue and Processing strategy
|
11
|
+
def self.skip_if_already_processing(processing_key)
|
12
|
+
return if Thread.current[processing_key]
|
5
13
|
|
6
14
|
begin
|
7
|
-
Thread.current[
|
15
|
+
Thread.current[processing_key] = true
|
8
16
|
|
9
|
-
|
17
|
+
yield
|
10
18
|
ensure
|
11
|
-
Thread.current[
|
19
|
+
Thread.current[processing_key] = nil
|
12
20
|
end
|
13
21
|
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Reset the given +processing_key+ for the current Thread.
|
25
|
+
#
|
26
|
+
# Usefull to make a block protected by +skip_if_already_processing+ reentrant.
|
27
|
+
def self.done_processing(processing_key)
|
28
|
+
Thread.current[processing_key] = nil
|
29
|
+
end
|
14
30
|
end
|
15
31
|
end
|
data/lib/sequent/util/timer.rb
CHANGED
data/lib/sequent/util/util.rb
CHANGED
data/lib/sequent.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'sequent/application_record'
|
2
4
|
require_relative 'sequent/sequent'
|
3
5
|
require_relative 'sequent/core/core'
|
4
6
|
require_relative 'sequent/util/util'
|
5
7
|
require_relative 'sequent/migrations/migrations'
|
8
|
+
|
9
|
+
require_relative 'notices'
|