sequent 2.1.0 → 3.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/bin/sequent +65 -0
- data/db/sequent_schema.rb +9 -8
- data/lib/sequent.rb +3 -0
- data/lib/sequent/configuration.rb +67 -0
- data/lib/sequent/core/aggregate_repository.rb +1 -1
- data/lib/sequent/core/core.rb +2 -2
- data/lib/sequent/core/event_store.rb +2 -2
- data/lib/sequent/core/helpers/attribute_support.rb +3 -0
- data/lib/sequent/core/helpers/self_applier.rb +1 -1
- data/lib/sequent/core/{record_sessions/active_record_session.rb → persistors/active_record_persistor.rb} +21 -19
- data/lib/sequent/core/persistors/persistor.rb +84 -0
- data/lib/sequent/core/persistors/persistors.rb +3 -0
- data/lib/sequent/core/{record_sessions/replay_events_session.rb → persistors/replay_optimized_postgres_persistor.rb} +16 -7
- data/lib/sequent/core/projector.rb +96 -0
- data/lib/sequent/generator.rb +2 -0
- data/lib/sequent/generator/aggregate.rb +71 -0
- data/lib/sequent/generator/project.rb +61 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate.rb +4 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/commands.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/events.rb +2 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate.rb +9 -0
- data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate_command_handler.rb +5 -0
- data/lib/sequent/generator/template_project/Gemfile +11 -0
- data/lib/sequent/generator/template_project/Gemfile.lock +72 -0
- data/lib/sequent/generator/template_project/Rakefile +12 -0
- data/lib/sequent/generator/template_project/app/projectors/post_projector.rb +22 -0
- data/lib/sequent/generator/template_project/app/records/post_record.rb +2 -0
- data/lib/sequent/generator/template_project/config/initializers/sequent.rb +13 -0
- data/lib/sequent/generator/template_project/db/database.yml +17 -0
- data/lib/sequent/generator/template_project/db/migrations.rb +17 -0
- data/lib/sequent/generator/template_project/db/sequent_schema.rb +51 -0
- data/lib/sequent/generator/template_project/db/tables/post_records.sql +10 -0
- data/lib/sequent/generator/template_project/lib/post.rb +4 -0
- data/lib/sequent/generator/template_project/lib/post/commands.rb +4 -0
- data/lib/sequent/generator/template_project/lib/post/events.rb +14 -0
- data/lib/sequent/generator/template_project/lib/post/post.rb +24 -0
- data/lib/sequent/generator/template_project/lib/post/post_command_handler.rb +5 -0
- data/lib/sequent/generator/template_project/my_app.rb +11 -0
- data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +32 -0
- data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +20 -0
- data/lib/sequent/generator/template_project/spec/spec_helper.rb +29 -0
- data/lib/sequent/migrations/migrate_events.rb +1 -0
- data/lib/sequent/migrations/migrations.rb +2 -0
- data/lib/sequent/migrations/projectors.rb +18 -0
- data/lib/sequent/migrations/view_schema.rb +364 -0
- data/lib/sequent/rake/migration_tasks.rb +109 -0
- data/lib/sequent/rake/tasks.rb +16 -0
- data/lib/sequent/sequent.rb +53 -13
- data/lib/sequent/support/database.rb +53 -8
- data/lib/sequent/util/printer.rb +16 -0
- data/lib/sequent/util/timer.rb +14 -0
- data/lib/sequent/util/util.rb +2 -0
- data/lib/version.rb +1 -1
- metadata +67 -14
- data/lib/sequent/core/base_event_handler.rb +0 -54
- data/lib/sequent/core/record_sessions/record_sessions.rb +0 -2
@@ -0,0 +1,51 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
|
3
|
+
create_table "event_records", :force => true do |t|
|
4
|
+
t.string "aggregate_id", :null => false
|
5
|
+
t.integer "sequence_number", :null => false
|
6
|
+
t.datetime "created_at", :null => false
|
7
|
+
t.string "event_type", :null => false
|
8
|
+
t.text "event_json", :null => false
|
9
|
+
t.integer "command_record_id", :null => false
|
10
|
+
t.integer "stream_record_id", :null => false
|
11
|
+
end
|
12
|
+
|
13
|
+
execute %Q{
|
14
|
+
CREATE UNIQUE INDEX unique_event_per_aggregate ON event_records (
|
15
|
+
aggregate_id,
|
16
|
+
sequence_number,
|
17
|
+
(CASE event_type WHEN 'Sequent::Core::SnapshotEvent' THEN 0 ELSE 1 END)
|
18
|
+
)
|
19
|
+
}
|
20
|
+
execute %Q{
|
21
|
+
CREATE INDEX snapshot_events ON event_records (aggregate_id, sequence_number DESC) WHERE event_type = 'Sequent::Core::SnapshotEvent'
|
22
|
+
}
|
23
|
+
|
24
|
+
add_index "event_records", ["command_record_id"], :name => "index_event_records_on_command_record_id"
|
25
|
+
add_index "event_records", ["event_type"], :name => "index_event_records_on_event_type"
|
26
|
+
add_index "event_records", ["created_at"], :name => "index_event_records_on_created_at"
|
27
|
+
|
28
|
+
create_table "command_records", :force => true do |t|
|
29
|
+
t.string "user_id"
|
30
|
+
t.string "aggregate_id"
|
31
|
+
t.string "command_type", :null => false
|
32
|
+
t.text "command_json", :null => false
|
33
|
+
t.datetime "created_at", :null => false
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table "stream_records", :force => true do |t|
|
37
|
+
t.datetime "created_at", :null => false
|
38
|
+
t.string "aggregate_type", :null => false
|
39
|
+
t.string "aggregate_id", :null => false
|
40
|
+
t.integer "snapshot_threshold"
|
41
|
+
end
|
42
|
+
|
43
|
+
add_index "stream_records", ["aggregate_id"], :name => "index_stream_records_on_aggregate_id", :unique => true
|
44
|
+
execute %q{
|
45
|
+
ALTER TABLE event_records ADD CONSTRAINT command_fkey FOREIGN KEY (command_record_id) REFERENCES command_records (id)
|
46
|
+
}
|
47
|
+
execute %q{
|
48
|
+
ALTER TABLE event_records ADD CONSTRAINT stream_fkey FOREIGN KEY (stream_record_id) REFERENCES stream_records (id)
|
49
|
+
}
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
CREATE TABLE post_records%SUFFIX% (
|
2
|
+
id serial NOT NULL,
|
3
|
+
aggregate_id uuid NOT NULL,
|
4
|
+
author character varying,
|
5
|
+
title character varying,
|
6
|
+
content character varying,
|
7
|
+
CONSTRAINT post_records_pkey%SUFFIX% PRIMARY KEY (id)
|
8
|
+
);
|
9
|
+
|
10
|
+
CREATE UNIQUE INDEX post_records_keys%SUFFIX% ON post_records%SUFFIX% USING btree (aggregate_id);
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class PostAdded < Sequent::Event
|
2
|
+
end
|
3
|
+
|
4
|
+
class PostAuthorChanged < Sequent::Event
|
5
|
+
attrs author: String
|
6
|
+
end
|
7
|
+
|
8
|
+
class PostTitleChanged < Sequent::Event
|
9
|
+
attrs title: String
|
10
|
+
end
|
11
|
+
|
12
|
+
class PostContentChanged < Sequent::Event
|
13
|
+
attrs content: String
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Post < Sequent::AggregateRoot
|
2
|
+
def initialize(command)
|
3
|
+
super(command.aggregate_id)
|
4
|
+
apply PostAdded
|
5
|
+
apply PostAuthorChanged, author: command.author
|
6
|
+
apply PostTitleChanged, title: command.title
|
7
|
+
apply PostContentChanged, content: command.content
|
8
|
+
end
|
9
|
+
|
10
|
+
on PostAdded do
|
11
|
+
end
|
12
|
+
|
13
|
+
on PostAuthorChanged do |event|
|
14
|
+
@author = event.author
|
15
|
+
end
|
16
|
+
|
17
|
+
on PostTitleChanged do |event|
|
18
|
+
@title = event.title
|
19
|
+
end
|
20
|
+
|
21
|
+
on PostContentChanged do |event|
|
22
|
+
@content = event.content
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
require_relative '../../../app/projectors/post_projector'
|
3
|
+
|
4
|
+
describe PostProjector do
|
5
|
+
let(:aggregate_id) { Sequent.new_uuid }
|
6
|
+
let(:post_projector) { PostProjector.new }
|
7
|
+
let(:post_added) { PostAdded.new(aggregate_id: aggregate_id, sequence_number: 1) }
|
8
|
+
|
9
|
+
context PostAdded do
|
10
|
+
it 'creates a projection' do
|
11
|
+
post_projector.handle_message(post_added)
|
12
|
+
expect(PostRecord.count).to eq(1)
|
13
|
+
record = PostRecord.first
|
14
|
+
expect(record.aggregate_id).to eq(aggregate_id)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context PostTitleChanged do
|
19
|
+
let(:post_title_changed) do
|
20
|
+
PostTitleChanged.new(aggregate_id: aggregate_id, title: 'ben en kim', sequence_number: 2)
|
21
|
+
end
|
22
|
+
|
23
|
+
before { post_projector.handle_message(post_added) }
|
24
|
+
|
25
|
+
it 'updates a projection' do
|
26
|
+
post_projector.handle_message(post_title_changed)
|
27
|
+
expect(PostRecord.count).to eq(1)
|
28
|
+
record = PostRecord.first
|
29
|
+
expect(record.title).to eq('ben en kim')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
require_relative '../../../lib/post'
|
3
|
+
|
4
|
+
describe PostCommandHandler do
|
5
|
+
let(:aggregate_id) { Sequent.new_uuid }
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
Sequent.configuration.command_handlers = [PostCommandHandler.new]
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'creates a post' do
|
12
|
+
when_command AddPost.new(aggregate_id: aggregate_id, author: 'ben', title: 'My first blogpost', content: 'Hello World!')
|
13
|
+
then_events(
|
14
|
+
PostAdded.new(aggregate_id: aggregate_id, sequence_number: 1),
|
15
|
+
PostAuthorChanged.new(aggregate_id: aggregate_id, sequence_number: 2, author: 'ben'),
|
16
|
+
PostTitleChanged,
|
17
|
+
PostContentChanged
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
ENV['RACK_ENV'] ||= 'test'
|
5
|
+
|
6
|
+
require 'sequent/test'
|
7
|
+
require 'database_cleaner'
|
8
|
+
|
9
|
+
require_relative '../my_app'
|
10
|
+
|
11
|
+
db_config = Sequent::Support::Database.read_config('test')
|
12
|
+
Sequent::Support::Database.establish_connection(db_config)
|
13
|
+
|
14
|
+
Sequent::Support::Database.drop_schema!(Sequent.configuration.view_schema_name)
|
15
|
+
|
16
|
+
Sequent::Migrations::ViewSchema.new(db_config: db_config).create_view_tables
|
17
|
+
Sequent.configuration.event_store = Sequent::Test::CommandHandlerHelpers::FakeEventStore.new
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.include Sequent::Test::CommandHandlerHelpers
|
21
|
+
|
22
|
+
config.around do |example|
|
23
|
+
Sequent.configuration.aggregate_repository.clear
|
24
|
+
DatabaseCleaner.strategy = :truncation
|
25
|
+
DatabaseCleaner.cleaning do
|
26
|
+
example.run
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -24,6 +24,7 @@ module Sequent
|
|
24
24
|
##
|
25
25
|
# @param env The string representing the current environment. E.g. "development", "production"
|
26
26
|
def initialize(env)
|
27
|
+
warn '[DEPRECATED] Use of MigrateEvents is deprecated and will be removed from future version. Please use Sequent::Migrations::ViewSchema instead. See the changelog on how to update.'
|
27
28
|
@env = env
|
28
29
|
end
|
29
30
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Sequent
|
2
|
+
module Migrations
|
3
|
+
class Projectors
|
4
|
+
def self.versions
|
5
|
+
fail "Define your own Sequent::Migrations::Projectors class that extends this class and implements this method"
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.version
|
9
|
+
fail "Define your own Sequent::Migrations::Projectors class that extends this class and implements this method"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.projectors_between(old, new)
|
13
|
+
versions.values_at(*Range.new(old + 1, new).to_a.map(&:to_s)).compact.flatten.uniq
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,364 @@
|
|
1
|
+
require 'parallel'
|
2
|
+
require 'postgresql_cursor'
|
3
|
+
|
4
|
+
require_relative '../support/database'
|
5
|
+
require_relative '../sequent'
|
6
|
+
require_relative '../util/timer'
|
7
|
+
require_relative '../util/printer'
|
8
|
+
require_relative './projectors'
|
9
|
+
|
10
|
+
module Sequent
|
11
|
+
module Migrations
|
12
|
+
class MigrationError < RuntimeError; end
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
# Responsible for migration of Projectors between view schema versions.
|
17
|
+
#
|
18
|
+
# A Projector needs migration when for instance:
|
19
|
+
#
|
20
|
+
# - New columns are added
|
21
|
+
# - Structure is changed
|
22
|
+
#
|
23
|
+
# To maintain your migrations you need to:
|
24
|
+
# 1. Create a class that extends `Sequent::Migrations::Projectors` and specify in `Sequent.configuration.migrations_class_name`
|
25
|
+
# 2. Define per version which Projectors you want to migrate
|
26
|
+
# See the definition of `Sequent::Migrations::Projectors.versions` and `Sequent::Migrations::Projectors.version`
|
27
|
+
# 3. Specify in Sequent where your sql files reside (Sequent.configuration.migration_sql_files_directory)
|
28
|
+
# 4. Ensure that you add %SUFFIX% to each name that needs to be unique in postgres (like TABLE names, INDEX names, PRIMARY KEYS)
|
29
|
+
# E.g. `create table foo%SUFFIX% (id serial NOT NULL, CONSTRAINT foo_pkey%SUFFIX% PRIMARY KEY (id))`
|
30
|
+
#
|
31
|
+
class ViewSchema
|
32
|
+
|
33
|
+
# Corresponds with the index on aggregate_id column in the event_records table
|
34
|
+
#
|
35
|
+
# Since we replay in batches of the first 3 chars of the uuid we created an index on
|
36
|
+
# these 3 characters. Hence the name ;-)
|
37
|
+
#
|
38
|
+
# This also means that the online replay is divided up into 16**3 groups
|
39
|
+
# This might seem a lot for starting event store, but when you will get more
|
40
|
+
# events, you will see that this is pretty good partitioned.
|
41
|
+
LENGTH_OF_SUBSTRING_INDEX_ON_AGGREGATE_ID_IN_EVENT_STORE = 3
|
42
|
+
|
43
|
+
include Sequent::Util::Timer
|
44
|
+
include Sequent::Util::Printer
|
45
|
+
|
46
|
+
class Versions < ActiveRecord::Base; end
|
47
|
+
class ReplayedIds < ActiveRecord::Base; end
|
48
|
+
|
49
|
+
attr_reader :view_schema, :db_config, :logger
|
50
|
+
|
51
|
+
def initialize(db_config:)
|
52
|
+
@db_config = db_config
|
53
|
+
@view_schema = Sequent.configuration.view_schema_name
|
54
|
+
@logger = Sequent.logger
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Returns the current version from the database
|
59
|
+
def current_version
|
60
|
+
Versions.order('version desc').limit(1).first&.version || 0
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Utility method that creates all tables in the view schema
|
65
|
+
#
|
66
|
+
# This method is mainly useful in test scenario to just create
|
67
|
+
# the entire view schema without replaying the events
|
68
|
+
def create_view_tables
|
69
|
+
create_view_schema_if_not_exists
|
70
|
+
in_view_schema do
|
71
|
+
Sequent::Core::Migratable.all.flat_map(&:managed_tables).each do |table|
|
72
|
+
statements = sql_file_to_statements("#{Sequent.configuration.migration_sql_files_directory}/#{table.table_name}.sql") { |raw_sql| raw_sql.remove('%SUFFIX%') }
|
73
|
+
statements.each { |statement| exec_sql(statement) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Utility method that replays events for all managed_tables from all Sequent::Core::Projector's
|
80
|
+
#
|
81
|
+
# This method is mainly useful in test scenario's or development tasks
|
82
|
+
def replay_all!
|
83
|
+
replay!(Sequent::Core::Migratable.all, Sequent.configuration.online_replay_persistor_class.new)
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Utility method that creates the view_schema and the meta data tables
|
88
|
+
#
|
89
|
+
# This method is mainly useful during an initial setup of the view schema
|
90
|
+
def create_view_schema_if_not_exists
|
91
|
+
exec_sql(%Q{CREATE SCHEMA IF NOT EXISTS #{view_schema}})
|
92
|
+
in_view_schema do
|
93
|
+
exec_sql(%Q{CREATE TABLE IF NOT EXISTS #{Versions.table_name} (version integer NOT NULL, CONSTRAINT version_pk PRIMARY KEY(version))})
|
94
|
+
exec_sql(%Q{CREATE TABLE IF NOT EXISTS #{ReplayedIds.table_name} (event_id bigint NOT NULL, CONSTRAINT event_id_pk PRIMARY KEY(event_id))})
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# First part of a view schema migration
|
100
|
+
#
|
101
|
+
# Call this method while your application is running.
|
102
|
+
# The online part consists of:
|
103
|
+
#
|
104
|
+
# 1. Ensure any previous migrations are cleaned up
|
105
|
+
# 2. Create new tables for the Projectors which need to be migrated to the new version
|
106
|
+
# These tables will be called `table_name_VERSION`.
|
107
|
+
# 3. Replay all events to populate the tables
|
108
|
+
# It keeps track of all events that are already replayed.
|
109
|
+
#
|
110
|
+
# If anything fails an exception is raised and everything is rolled back
|
111
|
+
#
|
112
|
+
def migrate_online
|
113
|
+
ensure_version_correct!
|
114
|
+
|
115
|
+
in_view_schema do
|
116
|
+
truncate_replay_ids_table!
|
117
|
+
|
118
|
+
drop_old_tables(Sequent.new_version)
|
119
|
+
for_each_table_to_migrate do |table|
|
120
|
+
statements = sql_file_to_statements("#{Sequent.configuration.migration_sql_files_directory}/#{table.table_name}.sql") { |raw_sql| raw_sql.gsub('%SUFFIX%', "_#{Sequent.new_version}") }
|
121
|
+
statements.each { |statement| exec_sql(statement) }
|
122
|
+
table.table_name = "#{table.table_name}_#{Sequent.new_version}"
|
123
|
+
table.reset_column_information
|
124
|
+
end
|
125
|
+
end
|
126
|
+
replay!(projectors_to_migrate, Sequent.configuration.online_replay_persistor_class.new)
|
127
|
+
rescue Exception => e
|
128
|
+
rollback_migration
|
129
|
+
raise e
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Last part of a view schema migration
|
134
|
+
#
|
135
|
+
# +You have to ensure no events are being added to the event store while this method is running.+
|
136
|
+
# For instance put your application in maintenance mode.
|
137
|
+
#
|
138
|
+
# The offline part consists of:
|
139
|
+
#
|
140
|
+
# 1. Replay all events not yet replayed since #migration_online
|
141
|
+
# 2. Within a single transaction do:
|
142
|
+
# 2.1 Rename current tables with the +current version+ as SUFFIX
|
143
|
+
# 2.2 Rename the new tables and remove the +new version+ suffix
|
144
|
+
# 2.3 Add the new version in the +Versions+ table
|
145
|
+
# 3. Performs cleanup of replayed event ids
|
146
|
+
#
|
147
|
+
# If anything fails an exception is raised and everything is rolled back
|
148
|
+
#
|
149
|
+
# When this method succeeds you can safely start the application from Sequent's point of view.
|
150
|
+
#
|
151
|
+
def migrate_offline
|
152
|
+
return if Sequent.new_version == current_version
|
153
|
+
|
154
|
+
ensure_version_correct!
|
155
|
+
|
156
|
+
set_table_names_to_new_version
|
157
|
+
|
158
|
+
# 1 replay events not yet replayed
|
159
|
+
replay!(projectors_to_migrate, Sequent.configuration.offline_replay_persistor_class.new, exclude_ids: true, group_exponent: 1)
|
160
|
+
|
161
|
+
in_view_schema do
|
162
|
+
ActiveRecord::Base.transaction do
|
163
|
+
for_each_table_to_migrate do |table|
|
164
|
+
current_table_name = table.table_name.gsub("_#{Sequent.new_version}", "")
|
165
|
+
# 2 Rename old table
|
166
|
+
exec_sql("ALTER TABLE IF EXISTS #{current_table_name} RENAME TO #{current_table_name}_#{current_version}")
|
167
|
+
# 3 Rename new table
|
168
|
+
exec_sql("ALTER TABLE #{table.table_name} RENAME TO #{current_table_name}")
|
169
|
+
# Use new table from now on
|
170
|
+
table.table_name = current_table_name
|
171
|
+
table.reset_column_information
|
172
|
+
end
|
173
|
+
# 4. Create migration record
|
174
|
+
Versions.create!(version: Sequent.new_version)
|
175
|
+
end
|
176
|
+
|
177
|
+
# 5. Truncate replayed ids
|
178
|
+
truncate_replay_ids_table!
|
179
|
+
end
|
180
|
+
logger.info "Migrated to version #{Sequent.new_version}"
|
181
|
+
rescue Exception => e
|
182
|
+
rollback_migration
|
183
|
+
raise e
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
def set_table_names_to_new_version
|
188
|
+
for_each_table_to_migrate do |table|
|
189
|
+
unless table.table_name.end_with?("_#{Sequent.new_version}")
|
190
|
+
table.table_name = "#{table.table_name}_#{Sequent.new_version}"
|
191
|
+
table.reset_column_information
|
192
|
+
fail MigrationError.new("Table #{table.table_name} does not exist. Did you run migrate_online first?") unless table.table_exists?
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def reset_table_names
|
198
|
+
for_each_table_to_migrate do |table|
|
199
|
+
table.table_name = table.table_name.gsub("_#{Sequent.new_version}", "")
|
200
|
+
table.reset_column_information
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def ensure_version_correct!
|
205
|
+
create_view_schema_if_not_exists
|
206
|
+
new_version = Sequent.new_version
|
207
|
+
|
208
|
+
fail ArgumentError.new("new_version [#{new_version}] must be greater or equal to current_version [#{current_version}]") if new_version < current_version
|
209
|
+
end
|
210
|
+
|
211
|
+
def replay!(projectors, replay_persistor, exclude_ids: false, group_exponent: 3)
|
212
|
+
logger.info "group_exponent: #{group_exponent.inspect}"
|
213
|
+
|
214
|
+
with_sequent_config(replay_persistor, projectors) do
|
215
|
+
logger.info "Start replaying events"
|
216
|
+
|
217
|
+
time("#{16**group_exponent} groups replayed") do
|
218
|
+
event_types = projectors.flat_map { |projector| projector.message_mapping.keys }.uniq.map(&:name)
|
219
|
+
disconnect!
|
220
|
+
|
221
|
+
number_of_groups = 16**group_exponent
|
222
|
+
groups = groups_of_aggregate_id_prefixes(number_of_groups)
|
223
|
+
|
224
|
+
@connected = false
|
225
|
+
# using `map_with_index` because https://github.com/grosser/parallel/issues/175
|
226
|
+
result = Parallel.map_with_index(groups, in_processes: Sequent.configuration.number_of_replay_processes) do |aggregate_prefixes, index|
|
227
|
+
begin
|
228
|
+
@connected ||= establish_connection
|
229
|
+
time("Group (#{aggregate_prefixes.first}-#{aggregate_prefixes.last}) #{index + 1}/#{number_of_groups} replayed") do
|
230
|
+
replay_events(aggregate_prefixes, event_types, exclude_ids, replay_persistor, &insert_ids)
|
231
|
+
end
|
232
|
+
nil
|
233
|
+
rescue => e
|
234
|
+
logger.error "Replaying failed for ids: ^#{aggregate_prefixes.first} - #{aggregate_prefixes.last}"
|
235
|
+
logger.error "+++++++++++++++ ERROR +++++++++++++++"
|
236
|
+
recursively_print(e)
|
237
|
+
raise Parallel::Kill # immediately kill all sub-processes
|
238
|
+
end
|
239
|
+
end
|
240
|
+
establish_connection
|
241
|
+
fail if result.nil?
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def replay_events(aggregate_prefixes, event_types, exclude_already_replayed, replay_persistor, &on_progress)
|
247
|
+
Sequent.configuration.event_store.replay_events_from_cursor(
|
248
|
+
block_size: 1000,
|
249
|
+
get_events: -> { event_stream(aggregate_prefixes, event_types, exclude_already_replayed) },
|
250
|
+
on_progress: on_progress
|
251
|
+
)
|
252
|
+
|
253
|
+
replay_persistor.commit
|
254
|
+
|
255
|
+
# Also commit all specific declared replay persistors on projectors.
|
256
|
+
Sequent.configuration.event_handlers.select { |e| e.class.replay_persistor }.each(&:commit)
|
257
|
+
end
|
258
|
+
|
259
|
+
def rollback_migration
|
260
|
+
disconnect!
|
261
|
+
establish_connection
|
262
|
+
drop_old_tables(Sequent.new_version)
|
263
|
+
|
264
|
+
truncate_replay_ids_table!
|
265
|
+
reset_table_names
|
266
|
+
end
|
267
|
+
|
268
|
+
def truncate_replay_ids_table!
|
269
|
+
exec_sql("truncate table #{ReplayedIds.table_name}")
|
270
|
+
end
|
271
|
+
|
272
|
+
def groups_of_aggregate_id_prefixes(number_of_groups)
|
273
|
+
all_prefixes = (0...16**LENGTH_OF_SUBSTRING_INDEX_ON_AGGREGATE_ID_IN_EVENT_STORE).to_a.map { |i| i.to_s(16) } # first x digits of hex
|
274
|
+
all_prefixes = all_prefixes.map { |s| s.length == 3 ? s : "#{"0" * (3 - s.length)}#{s}" }
|
275
|
+
|
276
|
+
logger.info "Number of groups #{number_of_groups}"
|
277
|
+
|
278
|
+
logger.debug "Prefixes: #{all_prefixes.length}"
|
279
|
+
fail "Can not have more groups #{number_of_groups} than number of prefixes #{all_prefixes.length}" if number_of_groups > all_prefixes.length
|
280
|
+
|
281
|
+
all_prefixes.each_slice(all_prefixes.length/number_of_groups).to_a
|
282
|
+
end
|
283
|
+
|
284
|
+
def for_each_table_to_migrate
|
285
|
+
projectors_to_migrate.flat_map(&:managed_tables).each do |managed_table|
|
286
|
+
yield(managed_table)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def projectors_to_migrate
|
291
|
+
Sequent.migration_class.projectors_between(current_version, Sequent.new_version)
|
292
|
+
end
|
293
|
+
|
294
|
+
def in_view_schema
|
295
|
+
Sequent::Support::Database.with_schema_search_path(view_schema, db_config) do
|
296
|
+
yield
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def sql_file_to_statements(file_location)
|
301
|
+
raw_sql_string = File.read(file_location, encoding: 'bom|utf-8')
|
302
|
+
sql_string = yield(raw_sql_string)
|
303
|
+
sql_string.split(/;$/).reject { |statement| statement.remove("\n").blank? }
|
304
|
+
end
|
305
|
+
|
306
|
+
def drop_old_tables(new_version)
|
307
|
+
versions_to_check = (current_version - 10)..new_version
|
308
|
+
old_tables = versions_to_check.flat_map do |old_version|
|
309
|
+
exec_sql(
|
310
|
+
"select table_name from information_schema.tables where table_schema = '#{Sequent.configuration.view_schema_name}' and table_name LIKE '%_#{old_version}'"
|
311
|
+
).flat_map { |row| row.values }
|
312
|
+
end
|
313
|
+
old_tables.each do |old_table|
|
314
|
+
exec_sql("DROP TABLE #{Sequent.configuration.view_schema_name}.#{old_table} CASCADE")
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def insert_ids
|
319
|
+
->(progress, done, ids) do
|
320
|
+
exec_sql("insert into #{ReplayedIds.table_name} (event_id) values #{ids.map { |id| "(#{id})" }.join(',')}") unless ids.empty?
|
321
|
+
Sequent::Core::EventStore::PRINT_PROGRESS[progress, done, ids] if progress > 0
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def with_sequent_config(replay_persistor, projectors, &block)
|
326
|
+
old_config = Sequent.configuration
|
327
|
+
|
328
|
+
config = Sequent.configuration.dup
|
329
|
+
|
330
|
+
replay_projectors = projectors.map { |projector_class| projector_class.new(projector_class.replay_persistor || replay_persistor) }
|
331
|
+
config.transaction_provider = Sequent::Core::Transactions::NoTransactions.new
|
332
|
+
config.event_handlers = replay_projectors
|
333
|
+
|
334
|
+
Sequent::Configuration.restore(config)
|
335
|
+
|
336
|
+
block.call
|
337
|
+
ensure
|
338
|
+
Sequent::Configuration.restore(old_config)
|
339
|
+
end
|
340
|
+
|
341
|
+
def event_stream(aggregate_prefixes, event_types, exclude_already_replayed)
|
342
|
+
fail ArgumentError.new("aggregate_prefixes is mandatory") unless aggregate_prefixes.present?
|
343
|
+
|
344
|
+
event_stream = Sequent.configuration.event_record_class.where(event_type: event_types)
|
345
|
+
event_stream = event_stream.where("substring(aggregate_id::varchar from 1 for #{LENGTH_OF_SUBSTRING_INDEX_ON_AGGREGATE_ID_IN_EVENT_STORE}) in (?)", aggregate_prefixes)
|
346
|
+
event_stream = event_stream.where("NOT EXISTS (SELECT 1 FROM #{ReplayedIds.table_name} WHERE event_id = event_records.id)") if exclude_already_replayed
|
347
|
+
event_stream.order('sequence_number ASC').select('id, event_type, event_json, sequence_number')
|
348
|
+
end
|
349
|
+
|
350
|
+
## shortcut methods
|
351
|
+
def disconnect!
|
352
|
+
Sequent::Support::Database.disconnect!
|
353
|
+
end
|
354
|
+
|
355
|
+
def establish_connection
|
356
|
+
Sequent::Support::Database.establish_connection(db_config)
|
357
|
+
end
|
358
|
+
|
359
|
+
def exec_sql(sql)
|
360
|
+
ActiveRecord::Base.connection.execute(sql)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|