sequent 2.1.0 → 3.0.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 +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
|