sequent 7.1.1 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequent +6 -107
  3. data/db/sequent_8_migration.sql +120 -0
  4. data/db/sequent_pgsql.sql +416 -0
  5. data/db/sequent_schema.rb +11 -57
  6. data/db/sequent_schema_indexes.sql +37 -0
  7. data/db/sequent_schema_partitions.sql +34 -0
  8. data/db/sequent_schema_tables.sql +74 -0
  9. data/lib/sequent/cli/app.rb +132 -0
  10. data/lib/sequent/cli/sequent_8_migration.rb +180 -0
  11. data/lib/sequent/configuration.rb +11 -8
  12. data/lib/sequent/core/aggregate_repository.rb +2 -2
  13. data/lib/sequent/core/aggregate_root.rb +32 -9
  14. data/lib/sequent/core/aggregate_snapshotter.rb +8 -6
  15. data/lib/sequent/core/command_record.rb +27 -18
  16. data/lib/sequent/core/command_service.rb +2 -2
  17. data/lib/sequent/core/event_publisher.rb +1 -1
  18. data/lib/sequent/core/event_record.rb +37 -17
  19. data/lib/sequent/core/event_store.rb +101 -119
  20. data/lib/sequent/core/helpers/array_with_type.rb +1 -1
  21. data/lib/sequent/core/helpers/association_validator.rb +2 -2
  22. data/lib/sequent/core/helpers/attribute_support.rb +8 -8
  23. data/lib/sequent/core/helpers/equal_support.rb +3 -3
  24. data/lib/sequent/core/helpers/message_matchers/has_attrs.rb +2 -0
  25. data/lib/sequent/core/helpers/message_router.rb +2 -2
  26. data/lib/sequent/core/helpers/param_support.rb +1 -3
  27. data/lib/sequent/core/helpers/pgsql_helpers.rb +32 -0
  28. data/lib/sequent/core/helpers/string_support.rb +1 -1
  29. data/lib/sequent/core/helpers/string_to_value_parsers.rb +1 -1
  30. data/lib/sequent/core/persistors/active_record_persistor.rb +1 -1
  31. data/lib/sequent/core/persistors/replay_optimized_postgres_persistor.rb +3 -4
  32. data/lib/sequent/core/projector.rb +1 -1
  33. data/lib/sequent/core/snapshot_record.rb +44 -0
  34. data/lib/sequent/core/snapshot_store.rb +105 -0
  35. data/lib/sequent/core/stream_record.rb +10 -15
  36. data/lib/sequent/dry_run/read_only_replay_optimized_postgres_persistor.rb +1 -1
  37. data/lib/sequent/dry_run/view_schema.rb +2 -3
  38. data/lib/sequent/generator/project.rb +5 -7
  39. data/lib/sequent/generator/template_aggregate/template_aggregate/commands.rb +2 -0
  40. data/lib/sequent/generator/template_aggregate/template_aggregate/events.rb +2 -0
  41. data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate.rb +2 -0
  42. data/lib/sequent/generator/template_aggregate/template_aggregate/template_aggregate_command_handler.rb +2 -0
  43. data/lib/sequent/generator/template_aggregate/template_aggregate.rb +2 -0
  44. data/lib/sequent/generator/template_project/Gemfile +7 -5
  45. data/lib/sequent/generator/template_project/Rakefile +4 -2
  46. data/lib/sequent/generator/template_project/app/projectors/post_projector.rb +2 -0
  47. data/lib/sequent/generator/template_project/app/records/post_record.rb +2 -0
  48. data/lib/sequent/generator/template_project/config/initializers/sequent.rb +3 -8
  49. data/lib/sequent/generator/template_project/db/migrations.rb +3 -3
  50. data/lib/sequent/generator/template_project/lib/post/commands.rb +2 -0
  51. data/lib/sequent/generator/template_project/lib/post/events.rb +2 -0
  52. data/lib/sequent/generator/template_project/lib/post/post.rb +2 -0
  53. data/lib/sequent/generator/template_project/lib/post/post_command_handler.rb +2 -0
  54. data/lib/sequent/generator/template_project/lib/post.rb +2 -0
  55. data/lib/sequent/generator/template_project/my_app.rb +2 -1
  56. data/lib/sequent/generator/template_project/spec/app/projectors/post_projector_spec.rb +2 -0
  57. data/lib/sequent/generator/template_project/spec/lib/post/post_command_handler_spec.rb +9 -2
  58. data/lib/sequent/generator/template_project/spec/spec_helper.rb +4 -7
  59. data/lib/sequent/generator.rb +1 -1
  60. data/lib/sequent/internal/aggregate_type.rb +12 -0
  61. data/lib/sequent/internal/command_type.rb +12 -0
  62. data/lib/sequent/internal/event_type.rb +12 -0
  63. data/lib/sequent/internal/internal.rb +14 -0
  64. data/lib/sequent/internal/partitioned_aggregate.rb +26 -0
  65. data/lib/sequent/internal/partitioned_command.rb +16 -0
  66. data/lib/sequent/internal/partitioned_event.rb +29 -0
  67. data/lib/sequent/migrations/grouper.rb +90 -0
  68. data/lib/sequent/migrations/sequent_schema.rb +2 -1
  69. data/lib/sequent/migrations/view_schema.rb +76 -77
  70. data/lib/sequent/rake/migration_tasks.rb +49 -24
  71. data/lib/sequent/sequent.rb +1 -0
  72. data/lib/sequent/support/database.rb +20 -16
  73. data/lib/sequent/test/time_comparison.rb +1 -1
  74. data/lib/sequent/util/timer.rb +1 -1
  75. data/lib/version.rb +1 -1
  76. metadata +102 -21
  77. data/lib/sequent/generator/template_project/db/sequent_schema.rb +0 -52
  78. data/lib/sequent/generator/template_project/ruby-version +0 -1
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/setup'
2
4
  Bundler.setup
3
5
 
@@ -8,12 +10,7 @@ require 'database_cleaner'
8
10
 
9
11
  require_relative '../my_app'
10
12
 
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
13
+ Sequent::Test::DatabaseHelpers.maintain_test_database_schema(env: 'test')
17
14
 
18
15
  module DomainTests
19
16
  def self.included(base)
@@ -23,7 +20,7 @@ end
23
20
 
24
21
  RSpec.configure do |config|
25
22
  config.include Sequent::Test::CommandHandlerHelpers
26
- config.include DomainTests, file_path: /spec\/lib/
23
+ config.include DomainTests, file_path: %r{/spec\/lib/}
27
24
 
28
25
  # Domain tests run with a clean sequent configuration and the in memory FakeEventStore
29
26
  config.around :each, :domain_tests do |example|
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './generator/generator'
3
+ require_relative 'generator/generator'
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require_relative '../application_record'
5
+
6
+ module Sequent
7
+ module Internal
8
+ class AggregateType < Sequent::ApplicationRecord
9
+ self.inheritance_column = nil
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require_relative '../application_record'
5
+
6
+ module Sequent
7
+ module Internal
8
+ class CommandType < Sequent::ApplicationRecord
9
+ self.inheritance_column = nil
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require_relative '../application_record'
5
+
6
+ module Sequent
7
+ module Internal
8
+ class EventType < Sequent::ApplicationRecord
9
+ self.inheritance_column = nil
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'aggregate_type'
4
+ require_relative 'command_type'
5
+ require_relative 'event_type'
6
+ require_relative 'partitioned_aggregate'
7
+ require_relative 'partitioned_command'
8
+ require_relative 'partitioned_event'
9
+
10
+ module Sequent
11
+ module Internal
12
+ end
13
+ private_constant :Internal
14
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require_relative '../application_record'
5
+
6
+ module Sequent
7
+ module Internal
8
+ class PartitionedAggregate < Sequent::ApplicationRecord
9
+ self.table_name = :aggregates
10
+ self.primary_key = %i[aggregate_id]
11
+
12
+ belongs_to :aggregate_type
13
+ if Gem.loaded_specs['activerecord'].version < Gem::Version.create('7.2')
14
+ has_many :partitioned_events,
15
+ inverse_of: :partitioned_aggregate,
16
+ primary_key: %w[events_partition_key aggregate_id],
17
+ query_constraints: %w[partition_key aggregate_id]
18
+ else
19
+ has_many :partitioned_events,
20
+ inverse_of: :partitioned_aggregate,
21
+ primary_key: %i[events_partition_key aggregate_id],
22
+ foreign_key: %i[partition_key aggregate_id]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require_relative '../application_record'
5
+
6
+ module Sequent
7
+ module Internal
8
+ class PartitionedCommand < Sequent::ApplicationRecord
9
+ self.table_name = :commands
10
+
11
+ belongs_to :command_type
12
+ has_many :partitioned_events,
13
+ inverse_of: :partitioned_command
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require_relative '../application_record'
5
+
6
+ module Sequent
7
+ module Internal
8
+ class PartitionedEvent < Sequent::ApplicationRecord
9
+ self.table_name = :events
10
+ self.primary_key = %i[partition_key aggregate_id sequence_number]
11
+
12
+ belongs_to :event_type
13
+ belongs_to :partitioned_command,
14
+ inverse_of: :partitioned_events,
15
+ foreign_key: :command_id
16
+ if Gem.loaded_specs['activerecord'].version < Gem::Version.create('7.2')
17
+ belongs_to :partitioned_aggregate,
18
+ inverse_of: :partitioned_events,
19
+ primary_key: %w[partition_key aggregate_id],
20
+ query_constraints: %w[events_partition_key aggregate_id]
21
+ else
22
+ belongs_to :partitioned_aggregate,
23
+ inverse_of: :partitioned_events,
24
+ primary_key: %w[partition_key aggregate_id],
25
+ foreign_key: %w[events_partition_key aggregate_id]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequent
4
+ module Migrations
5
+ module Grouper
6
+ GroupEndpoint = Data.define(:partition_key, :aggregate_id) do
7
+ def <=>(other)
8
+ return unless other.is_a?(self.class)
9
+
10
+ [partition_key, aggregate_id] <=> [other.partition_key, other.aggregate_id]
11
+ end
12
+
13
+ include Comparable
14
+
15
+ def to_s
16
+ "(#{partition_key}, #{aggregate_id})"
17
+ end
18
+ end
19
+
20
+ # Generate approximately equally sized groups based on the
21
+ # events partition keys and the number of events per partition
22
+ # key. Each group is defined by a lower bound (partition-key,
23
+ # aggregate-id) and upper bound (partition-key, aggregate-id)
24
+ # (inclusive).
25
+ #
26
+ # For splitting a partition into equal sized groups the
27
+ # assumption is made that aggregate-ids and their events are
28
+ # equally distributed.
29
+ def self.group_partitions(partitions, target_group_size)
30
+ return [] unless partitions.present?
31
+
32
+ partitions = partitions.sort.map do |key, count|
33
+ PartitionData.new(key:, original_size: count, remaining_size: count, lower_bound: 0)
34
+ end
35
+
36
+ partition = partitions.shift
37
+ current_start = GroupEndpoint.new(partition.key, LOWEST_UUID)
38
+ current_size = 0
39
+
40
+ result = []
41
+ while partition.present?
42
+ if current_size + partition.remaining_size < target_group_size
43
+ current_size += partition.remaining_size
44
+ if partitions.empty?
45
+ result << (current_start..GroupEndpoint.new(partition.key, HIGHEST_UUID))
46
+ break
47
+ end
48
+ partition = partitions.shift
49
+ elsif current_size + partition.remaining_size == target_group_size
50
+ result << (current_start..GroupEndpoint.new(partition.key, HIGHEST_UUID))
51
+
52
+ partition = partitions.shift
53
+ break unless partition
54
+
55
+ current_start = GroupEndpoint.new(partition.key, LOWEST_UUID)
56
+ current_size = 0
57
+ else
58
+ taken = target_group_size - current_size
59
+ upper_bound = partition.lower_bound + (UUID_COUNT * taken / partition.original_size)
60
+
61
+ result << (current_start..GroupEndpoint.new(partition.key, number_to_uuid(upper_bound - 1)))
62
+
63
+ remaining_size = partition.remaining_size - taken
64
+ partition = partition.with(remaining_size:, lower_bound: upper_bound)
65
+ current_start = GroupEndpoint.new(partition.key, number_to_uuid(upper_bound))
66
+ current_size = 0
67
+ end
68
+ end
69
+ result
70
+ end
71
+
72
+ PartitionData = Data.define(:key, :original_size, :remaining_size, :lower_bound)
73
+
74
+ def self.number_to_uuid(number)
75
+ fail ArgumentError, number unless (0..UUID_COUNT - 1).include? number
76
+
77
+ s = format('%032x', number)
78
+ "#{s[0..7]}-#{s[8..11]}-#{s[12..15]}-#{s[16..19]}-#{s[20..]}"
79
+ end
80
+
81
+ def self.uuid_to_number(uuid)
82
+ Integer(uuid.gsub('-', ''), 16)
83
+ end
84
+
85
+ UUID_COUNT = 2**128
86
+ LOWEST_UUID = number_to_uuid(0)
87
+ HIGHEST_UUID = number_to_uuid(UUID_COUNT - 1)
88
+ end
89
+ end
90
+ end
@@ -19,7 +19,8 @@ module Sequent
19
19
  Sequent::Support::Database.establish_connection(db_config)
20
20
 
21
21
  event_store_schema = Sequent.configuration.event_store_schema_name
22
- schema_exists = Sequent::Support::Database.schema_exists?(event_store_schema)
22
+ event_records_table = Sequent.configuration.event_record_class.table_name
23
+ schema_exists = Sequent::Support::Database.schema_exists?(event_store_schema, event_records_table)
23
24
 
24
25
  FAIL_IF_EXISTS.call(event_store_schema) if schema_exists && fail_if_exists
25
26
  return if schema_exists
@@ -8,11 +8,12 @@ require_relative '../support/database'
8
8
  require_relative '../sequent'
9
9
  require_relative '../util/timer'
10
10
  require_relative '../util/printer'
11
- require_relative './projectors'
11
+ require_relative 'projectors'
12
12
  require_relative 'planner'
13
13
  require_relative 'executor'
14
14
  require_relative 'sql'
15
15
  require_relative 'versions'
16
+ require_relative 'grouper'
16
17
 
17
18
  module Sequent
18
19
  module Migrations
@@ -60,16 +61,6 @@ module Sequent
60
61
  #
61
62
  # end
62
63
  class ViewSchema
63
- # Corresponds with the index on aggregate_id column in the event_records table
64
- #
65
- # Since we replay in batches of the first 3 chars of the uuid we created an index on
66
- # these 3 characters. Hence the name ;-)
67
- #
68
- # This also means that the online replay is divided up into 16**3 groups
69
- # This might seem a lot for starting event store, but when you will get more
70
- # events, you will see that this is pretty good partitioned.
71
- LENGTH_OF_SUBSTRING_INDEX_ON_AGGREGATE_ID_IN_EVENT_STORE = 3
72
-
73
64
  include Sequent::Util::Timer
74
65
  include Sequent::Util::Printer
75
66
  include Sql
@@ -143,11 +134,10 @@ module Sequent
143
134
  # Utility method that replays events for all managed_tables from all Sequent::Core::Projector's
144
135
  #
145
136
  # This method is mainly useful in test scenario's or development tasks
146
- def replay_all!(group_exponent: 1)
137
+ def replay_all!
147
138
  replay!(
148
139
  Sequent.configuration.online_replay_persistor_class.new,
149
140
  projectors: Core::Migratable.projectors,
150
- groups: groups(group_exponent: group_exponent),
151
141
  )
152
142
  end
153
143
 
@@ -205,7 +195,6 @@ module Sequent
205
195
  if plan.projectors.any?
206
196
  replay!(
207
197
  Sequent.configuration.online_replay_persistor_class.new,
208
- groups: groups,
209
198
  maximum_xact_id_exclusive: Versions.running.first.xmin_xact_id,
210
199
  )
211
200
  end
@@ -216,17 +205,15 @@ module Sequent
216
205
  Versions.end_online!(Sequent.new_version)
217
206
  end
218
207
  Sequent.logger.info("Done migrate_online for version #{Sequent.new_version}")
219
- rescue ConcurrentMigration
220
- # Do not rollback the migration when this is a concurrent migration as the other one is running
221
- raise
222
- rescue InvalidMigrationDefinition
223
- # Do not rollback the migration when since there is nothing to rollback
208
+ rescue ConcurrentMigration, InvalidMigrationDefinition
209
+ # ConcurrentMigration: Do not rollback the migration when this is a concurrent migration
210
+ # as the other one is running
211
+ # InvalidMigrationDefinition: Do not rollback the migration when since there is nothing to rollback
224
212
  raise
225
213
  rescue Exception => e # rubocop:disable Lint/RescueException
226
214
  rollback_migration
227
215
  raise e
228
216
  end
229
-
230
217
  ##
231
218
  # Last part of a view schema migration
232
219
  #
@@ -259,7 +246,6 @@ module Sequent
259
246
  if plan.projectors.any?
260
247
  replay!(
261
248
  Sequent.configuration.offline_replay_persistor_class.new,
262
- groups: groups(group_exponent: 1),
263
249
  minimum_xact_id_inclusive: Versions.running.first.xmin_xact_id,
264
250
  )
265
251
  end
@@ -309,18 +295,33 @@ module Sequent
309
295
 
310
296
  def replay!(
311
297
  replay_persistor,
312
- groups:,
313
298
  projectors: plan.projectors,
314
299
  minimum_xact_id_inclusive: nil,
315
300
  maximum_xact_id_exclusive: nil
316
301
  )
317
- logger.info "groups: #{groups.size}"
302
+ event_types = projectors.flat_map { |projector| projector.message_mapping.keys }.uniq.map(&:name)
303
+ group_target_size = Sequent.configuration.replay_group_target_size
304
+ event_type_ids = Internal::EventType.where(type: event_types).pluck(:id)
305
+
306
+ partitions_query = Internal::PartitionedEvent.where(event_type_id: event_type_ids)
307
+ partitions_query = xact_id_filter(partitions_query, minimum_xact_id_inclusive, maximum_xact_id_exclusive)
308
+
309
+ partitions = partitions_query.group(:partition_key).order(:partition_key).count
310
+ event_count = partitions.values.sum
311
+
312
+ groups = Sequent::Migrations::Grouper.group_partitions(partitions, group_target_size)
313
+
314
+ if groups.empty?
315
+ groups = [nil..nil]
316
+ else
317
+ groups.prepend(nil..groups.first.begin)
318
+ groups.append(groups.last.end..nil)
319
+ end
318
320
 
319
321
  with_sequent_config(replay_persistor, projectors) do
320
- logger.info 'Start replaying events'
322
+ logger.info "Start replaying #{event_count} events in #{groups.size} groups"
321
323
 
322
- time("#{groups.size} groups replayed") do
323
- event_types = projectors.flat_map { |projector| projector.message_mapping.keys }.uniq.map(&:name)
324
+ time("#{event_count} events in #{groups.size} groups replayed") do
324
325
  disconnect!
325
326
 
326
327
  @connected = false
@@ -328,24 +329,23 @@ module Sequent
328
329
  result = Parallel.map_with_index(
329
330
  groups,
330
331
  in_processes: Sequent.configuration.number_of_replay_processes,
331
- ) do |aggregate_prefixes, index|
332
+ ) do |group, index|
332
333
  @connected ||= establish_connection
333
334
  msg = <<~EOS.chomp
334
- Group (#{aggregate_prefixes.first}-#{aggregate_prefixes.last}) #{index + 1}/#{groups.size} replayed
335
+ Group #{group} (#{index + 1}/#{groups.size}) replayed
335
336
  EOS
336
337
  time(msg) do
337
338
  replay_events(
338
- aggregate_prefixes,
339
- event_types,
340
- minimum_xact_id_inclusive,
341
- maximum_xact_id_exclusive,
339
+ -> {
340
+ event_stream(group, event_type_ids, minimum_xact_id_inclusive, maximum_xact_id_exclusive)
341
+ },
342
342
  replay_persistor,
343
343
  &on_progress
344
344
  )
345
345
  end
346
346
  nil
347
347
  rescue StandardError => e
348
- logger.error "Replaying failed for ids: ^#{aggregate_prefixes.first} - #{aggregate_prefixes.last}"
348
+ logger.error "Replaying failed for group: #{group}"
349
349
  logger.error '+++++++++++++++ ERROR +++++++++++++++'
350
350
  recursively_print(e)
351
351
  raise Parallel::Kill # immediately kill all sub-processes
@@ -357,19 +357,14 @@ module Sequent
357
357
  end
358
358
 
359
359
  def replay_events(
360
- aggregate_prefixes,
361
- event_types,
362
- minimum_xact_id_inclusive,
363
- maximum_xact_id_exclusive,
360
+ get_events,
364
361
  replay_persistor,
365
362
  &on_progress
366
363
  )
367
364
  Sequent.configuration.event_store.replay_events_from_cursor(
368
365
  block_size: 1000,
369
- get_events: -> {
370
- event_stream(aggregate_prefixes, event_types, minimum_xact_id_inclusive, maximum_xact_id_exclusive)
371
- },
372
- on_progress: on_progress,
366
+ get_events:,
367
+ on_progress:,
373
368
  )
374
369
 
375
370
  replay_persistor.commit
@@ -387,30 +382,6 @@ module Sequent
387
382
  Versions.rollback!(Sequent.new_version)
388
383
  end
389
384
 
390
- def groups(group_exponent: 3, limit: nil, offset: nil)
391
- number_of_groups = 16**group_exponent
392
- groups = groups_of_aggregate_id_prefixes(number_of_groups)
393
- groups = groups.drop(offset) unless offset.nil?
394
- groups = groups.take(limit) unless limit.nil?
395
- groups
396
- end
397
-
398
- def groups_of_aggregate_id_prefixes(number_of_groups)
399
- all_prefixes = (0...16**LENGTH_OF_SUBSTRING_INDEX_ON_AGGREGATE_ID_IN_EVENT_STORE).to_a.map do |i|
400
- i.to_s(16)
401
- end
402
- all_prefixes = all_prefixes.map { |s| s.length == 3 ? s : "#{'0' * (3 - s.length)}#{s}" }
403
-
404
- logger.info "Number of groups #{number_of_groups}"
405
-
406
- logger.debug "Prefixes: #{all_prefixes.length}"
407
- if number_of_groups > all_prefixes.length
408
- fail "Can not have more groups #{number_of_groups} than number of prefixes #{all_prefixes.length}"
409
- end
410
-
411
- all_prefixes.each_slice(all_prefixes.length / number_of_groups).to_a
412
- end
413
-
414
385
  def in_view_schema(&block)
415
386
  Sequent::Support::Database.with_schema_search_path(view_schema, db_config, &block)
416
387
  end
@@ -451,27 +422,55 @@ module Sequent
451
422
  Sequent::Configuration.restore(old_config)
452
423
  end
453
424
 
454
- def event_stream(aggregate_prefixes, event_types, minimum_xact_id_inclusive, maximum_xact_id_exclusive)
455
- fail ArgumentError, 'aggregate_prefixes is mandatory' unless aggregate_prefixes.present?
425
+ def event_stream(group, event_type_ids, minimum_xact_id_inclusive, maximum_xact_id_exclusive)
426
+ fail ArgumentError, 'group is mandatory' if group.nil?
456
427
 
457
- event_stream = Sequent.configuration.event_record_class.where(event_type: event_types)
458
- event_stream = event_stream.where(<<~SQL, aggregate_prefixes)
459
- substring(aggregate_id::text from 1 for #{LENGTH_OF_SUBSTRING_INDEX_ON_AGGREGATE_ID_IN_EVENT_STORE}) in (?)
460
- SQL
461
- if minimum_xact_id_inclusive && maximum_xact_id_exclusive
428
+ event_stream = Internal::PartitionedEvent
429
+ .joins('JOIN event_types ON events.event_type_id = event_types.id')
430
+ .where(
431
+ event_type_id: event_type_ids,
432
+ )
433
+ if group.begin && group.end
434
+ event_stream = event_stream.where(
435
+ '(events.partition_key, events.aggregate_id) BETWEEN (?, ?) AND (?, ?)',
436
+ group.begin.partition_key,
437
+ group.begin.aggregate_id,
438
+ group.end.partition_key,
439
+ group.end.aggregate_id,
440
+ )
441
+ elsif group.end
442
+ event_stream = event_stream.where(
443
+ '(events.partition_key, events.aggregate_id) < (?, ?)',
444
+ group.end.partition_key,
445
+ group.end.aggregate_id,
446
+ )
447
+ elsif group.begin
462
448
  event_stream = event_stream.where(
449
+ '(events.partition_key, events.aggregate_id) > (?, ?)',
450
+ group.begin.partition_key,
451
+ group.begin.aggregate_id,
452
+ )
453
+ end
454
+ event_stream = xact_id_filter(event_stream, minimum_xact_id_inclusive, maximum_xact_id_exclusive)
455
+ event_stream
456
+ .order('events.partition_key', 'events.aggregate_id', 'events.sequence_number')
457
+ .select('event_types.type AS event_type, enrich_event_json(events) AS event_json')
458
+ end
459
+
460
+ def xact_id_filter(events_query, minimum_xact_id_inclusive, maximum_xact_id_exclusive)
461
+ if minimum_xact_id_inclusive && maximum_xact_id_exclusive
462
+ events_query.where(
463
463
  'xact_id >= ? AND xact_id < ?',
464
464
  minimum_xact_id_inclusive,
465
465
  maximum_xact_id_exclusive,
466
466
  )
467
467
  elsif minimum_xact_id_inclusive
468
- event_stream = event_stream.where('xact_id >= ?', minimum_xact_id_inclusive)
468
+ events_query.where('xact_id >= ?', minimum_xact_id_inclusive)
469
469
  elsif maximum_xact_id_exclusive
470
- event_stream = event_stream.where('xact_id IS NULL OR xact_id < ?', maximum_xact_id_exclusive)
470
+ events_query.where('xact_id IS NULL OR xact_id < ?', maximum_xact_id_exclusive)
471
+ else
472
+ events_query
471
473
  end
472
- event_stream
473
- .order('aggregate_id ASC, sequence_number ASC')
474
- .select('id, event_type, event_json, sequence_number')
475
474
  end
476
475
 
477
476
  ## shortcut methods
@@ -125,6 +125,11 @@ module Sequent
125
125
  end
126
126
  end
127
127
 
128
+ desc 'Aborts if a migration is pending'
129
+ task abort_if_pending_migrations: [:create_and_migrate_sequent_view_schema] do
130
+ abort if Sequent.new_version != Sequent::Migrations::Versions.current_version
131
+ end
132
+
128
133
  desc <<-EOS
129
134
  Shows the current status of the migrations
130
135
  EOS
@@ -192,18 +197,15 @@ module Sequent
192
197
 
193
198
  Pass a regular expression as parameter to select the projectors to run, otherwise all projectors are selected.
194
199
  EOS
195
- task :dryrun, %i[regex group_exponent limit offset] => ['sequent:init', :init] do |_task, args|
200
+ task :dryrun, %i[regex group_target_size] => ['sequent:init', :init] do |_task, args|
196
201
  ensure_sequent_env_set!
197
202
 
198
203
  db_config = Sequent::Support::Database.read_config(@env)
199
204
  view_schema = Sequent::DryRun::ViewSchema.new(db_config: db_config)
200
205
 
201
- view_schema.migrate_dryrun(
202
- regex: args[:regex],
203
- group_exponent: (args[:group_exponent] || 3).to_i,
204
- limit: args[:limit]&.to_i,
205
- offset: args[:offset]&.to_i,
206
- )
206
+ Sequent.configuration.replay_group_target_size = group_target_size
207
+
208
+ view_schema.migrate_dryrun(regex: args[:regex])
207
209
  end
208
210
  end
209
211
 
@@ -213,31 +215,54 @@ module Sequent
213
215
  EOS
214
216
  task :init
215
217
 
216
- task :set_snapshot_threshold, %i[aggregate_type threshold] => ['sequent:init', :init] do |_t, args|
217
- aggregate_type = args['aggregate_type']
218
- threshold = args['threshold']
218
+ task :connect, ['sequent:init', :init, :set_env_var] do
219
+ ensure_sequent_env_set!
220
+
221
+ Sequent.configuration.command_handlers << Sequent::Core::AggregateSnapshotter.new
222
+
223
+ db_config = Sequent::Support::Database.read_config(@env)
224
+ Sequent::Support::Database.establish_connection(db_config)
225
+ end
226
+
227
+ desc <<~EOS
228
+ Takes up-to `limit` snapshots, starting with the highest priority aggregates (based on snapshot outdated time and number of events)
229
+ EOS
230
+ task :take_snapshots, %i[limit] => :connect do |_t, args|
231
+ limit = args['limit']&.to_i
219
232
 
220
- unless aggregate_type
233
+ unless limit
221
234
  fail ArgumentError,
222
- 'usage rake sequent:snapshots:set_snapshot_threshold[AggregegateType,threshold]'
235
+ 'usage rake sequent:snapshots:take_snapshots[limit]'
223
236
  end
224
- unless threshold
237
+
238
+ aggregate_ids = Sequent.configuration.event_store.select_aggregates_for_snapshotting(limit:)
239
+
240
+ Sequent.logger.info "Taking #{aggregate_ids.size} snapshots"
241
+ aggregate_ids.each do |aggregate_id|
242
+ Sequent.command_service.execute_commands(Sequent::Core::TakeSnapshot.new(aggregate_id:))
243
+ end
244
+ end
245
+
246
+ desc <<~EOS
247
+ Takes a new snapshot for the aggregate specified by `aggregate_id`
248
+ EOS
249
+ task :take_snapshot, %i[aggregate_id] => :connect do |_t, args|
250
+ aggregate_id = args['aggregate_id']
251
+
252
+ unless aggregate_id
225
253
  fail ArgumentError,
226
- 'usage rake sequent:snapshots:set_snapshot_threshold[AggregegateType,threshold]'
254
+ 'usage rake sequent:snapshots:take_snapshot[aggregate_id]'
227
255
  end
228
256
 
229
- execute <<~EOS
230
- UPDATE #{Sequent.configuration.stream_record_class} SET snapshot_threshold = #{threshold.to_i} WHERE aggregate_type = '#{aggregate_type}'
231
- EOS
257
+ Sequent.command_service.execute_commands(Sequent::Core::TakeSnapshot.new(aggregate_id:))
232
258
  end
233
259
 
234
- task delete_all: ['sequent:init', :init] do
235
- result = Sequent::ApplicationRecord
236
- .connection
237
- .execute(<<~EOS)
238
- DELETE FROM #{Sequent.configuration.event_record_class.table_name} WHERE event_type = 'Sequent::Core::SnapshotEvent'
239
- EOS
240
- Sequent.logger.info "Deleted #{result.cmd_tuples} aggregate snapshots from the event store"
260
+ desc <<~EOS
261
+ Delete all aggregate snapshots, which can negatively impact performance of a running system.
262
+ EOS
263
+ task delete_all: :connect do
264
+ Sequent.configuration.event_store.delete_all_snapshots
265
+ Sequent.logger.info 'Deleted all aggregate snapshots from the event store'
241
266
  end
242
267
  end
243
268
  end
@@ -8,6 +8,7 @@ require_relative 'core/aggregate_root'
8
8
  require_relative 'core/projector'
9
9
  require_relative 'core/workflow'
10
10
  require_relative 'core/value_object'
11
+ require_relative 'internal/internal'
11
12
  require_relative 'migrations/migrations'
12
13
 
13
14
  module Sequent