sequent 3.3.1 → 4.1.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/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'
|