sequent 7.1.1 → 8.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.
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