pg_eventstore 0.10.1 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +26 -0
  4. data/db/migrations/1_create_events.sql +12 -11
  5. data/db/migrations/2_create_subscriptions.sql +6 -2
  6. data/db/migrations/3_create_subscription_commands.sql +9 -5
  7. data/db/migrations/4_create_subscriptions_set_commands.sql +1 -1
  8. data/db/migrations/5_partitions.sql +1 -0
  9. data/docs/configuration.md +1 -1
  10. data/docs/how_it_works.md +14 -1
  11. data/lib/pg_eventstore/commands/append.rb +1 -1
  12. data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +30 -8
  13. data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +8 -10
  14. data/lib/pg_eventstore/commands/link_to.rb +14 -7
  15. data/lib/pg_eventstore/errors.rb +10 -12
  16. data/lib/pg_eventstore/event.rb +9 -1
  17. data/lib/pg_eventstore/event_deserializer.rb +1 -0
  18. data/lib/pg_eventstore/queries/event_queries.rb +33 -6
  19. data/lib/pg_eventstore/queries/links_resolver.rb +53 -0
  20. data/lib/pg_eventstore/queries/partition_queries.rb +8 -0
  21. data/lib/pg_eventstore/queries/subscription_command_queries.rb +27 -7
  22. data/lib/pg_eventstore/queries/subscription_queries.rb +70 -35
  23. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +13 -1
  24. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +18 -4
  25. data/lib/pg_eventstore/queries.rb +1 -0
  26. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +4 -17
  27. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +10 -2
  28. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +9 -7
  29. data/lib/pg_eventstore/subscriptions/commands_handler.rb +3 -2
  30. data/lib/pg_eventstore/subscriptions/events_processor.rb +10 -2
  31. data/lib/pg_eventstore/subscriptions/subscription.rb +29 -12
  32. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +20 -16
  33. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +1 -1
  34. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +3 -4
  35. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +22 -1
  36. data/lib/pg_eventstore/version.rb +1 -1
  37. data/lib/pg_eventstore/web/application.rb +180 -0
  38. data/lib/pg_eventstore/web/paginator/base_collection.rb +56 -0
  39. data/lib/pg_eventstore/web/paginator/event_types_collection.rb +50 -0
  40. data/lib/pg_eventstore/web/paginator/events_collection.rb +105 -0
  41. data/lib/pg_eventstore/web/paginator/helpers.rb +119 -0
  42. data/lib/pg_eventstore/web/paginator/stream_contexts_collection.rb +48 -0
  43. data/lib/pg_eventstore/web/paginator/stream_ids_collection.rb +50 -0
  44. data/lib/pg_eventstore/web/paginator/stream_names_collection.rb +51 -0
  45. data/lib/pg_eventstore/web/public/fonts/vendor/FontAwesome.otf +0 -0
  46. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.eot +0 -0
  47. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.svg +685 -0
  48. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.ttf +0 -0
  49. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.woff +0 -0
  50. data/lib/pg_eventstore/web/public/fonts/vendor/fontawesome-webfont.woff2 +0 -0
  51. data/lib/pg_eventstore/web/public/images/favicon.ico +0 -0
  52. data/lib/pg_eventstore/web/public/javascripts/gentelella.js +334 -0
  53. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +162 -0
  54. data/lib/pg_eventstore/web/public/javascripts/vendor/bootstrap.bundle.min.js +7 -0
  55. data/lib/pg_eventstore/web/public/javascripts/vendor/bootstrap.bundle.min.js.map +1 -0
  56. data/lib/pg_eventstore/web/public/javascripts/vendor/jquery.autocomplete.min.js +8 -0
  57. data/lib/pg_eventstore/web/public/javascripts/vendor/jquery.min.js +4 -0
  58. data/lib/pg_eventstore/web/public/javascripts/vendor/jquery.min.js.map +1 -0
  59. data/lib/pg_eventstore/web/public/javascripts/vendor/select2.full.min.js +2 -0
  60. data/lib/pg_eventstore/web/public/stylesheets/pg_eventstore.css +5 -0
  61. data/lib/pg_eventstore/web/public/stylesheets/vendor/bootstrap.min.css +7 -0
  62. data/lib/pg_eventstore/web/public/stylesheets/vendor/bootstrap.min.css.map +1 -0
  63. data/lib/pg_eventstore/web/public/stylesheets/vendor/font-awesome.min.css +4 -0
  64. data/lib/pg_eventstore/web/public/stylesheets/vendor/font-awesome.min.css.map +7 -0
  65. data/lib/pg_eventstore/web/public/stylesheets/vendor/gentelella.min.css +13 -0
  66. data/lib/pg_eventstore/web/public/stylesheets/vendor/select2-bootstrap4.min.css +3 -0
  67. data/lib/pg_eventstore/web/public/stylesheets/vendor/select2.min.css +2 -0
  68. data/lib/pg_eventstore/web/subscriptions/helpers.rb +76 -0
  69. data/lib/pg_eventstore/web/subscriptions/set_collection.rb +34 -0
  70. data/lib/pg_eventstore/web/subscriptions/subscriptions.rb +33 -0
  71. data/lib/pg_eventstore/web/subscriptions/subscriptions_set.rb +33 -0
  72. data/lib/pg_eventstore/web/subscriptions/subscriptions_to_set_association.rb +32 -0
  73. data/lib/pg_eventstore/web/views/home/dashboard.erb +147 -0
  74. data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +15 -0
  75. data/lib/pg_eventstore/web/views/home/partials/events.erb +22 -0
  76. data/lib/pg_eventstore/web/views/home/partials/pagination_links.erb +3 -0
  77. data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +31 -0
  78. data/lib/pg_eventstore/web/views/layouts/application.erb +116 -0
  79. data/lib/pg_eventstore/web/views/subscriptions/index.erb +220 -0
  80. data/lib/pg_eventstore/web.rb +22 -0
  81. data/lib/pg_eventstore.rb +5 -0
  82. data/pg_eventstore.gemspec +2 -1
  83. metadata +61 -2
@@ -22,11 +22,7 @@ module PgEventstore
22
22
  # @param attrs [Hash]
23
23
  # @return [Hash, nil]
24
24
  def find_by(attrs)
25
- builder = SQLBuilder.new.select('*').from('subscriptions')
26
- attrs.each do |attr, val|
27
- builder.where("#{attr} = ?", val)
28
- end
29
-
25
+ builder = find_by_attrs_builder(attrs).limit(1)
30
26
  pg_result = connection.with do |conn|
31
27
  conn.exec_params(*builder.to_exec_params)
32
28
  end
@@ -35,6 +31,25 @@ module PgEventstore
35
31
  deserialize(pg_result.to_a.first)
36
32
  end
37
33
 
34
+ # @param attrs [Hash]
35
+ # @return [Array<Hash>]
36
+ def find_all(attrs)
37
+ builder = find_by_attrs_builder(attrs)
38
+ pg_result = connection.with do |conn|
39
+ conn.exec_params(*builder.to_exec_params)
40
+ end
41
+ return [] if pg_result.ntuples.zero?
42
+
43
+ pg_result.map(&method(:deserialize))
44
+ end
45
+
46
+ # @return [Array<String>]
47
+ def set_collection
48
+ connection.with do |conn|
49
+ conn.exec('SELECT set FROM subscriptions GROUP BY set ORDER BY set ASC')
50
+ end.map { |attrs| attrs['set'] }
51
+ end
52
+
38
53
  # @param id [Integer]
39
54
  # @return [Hash]
40
55
  # @raise [PgEventstore::RecordNotFound]
@@ -58,40 +73,61 @@ module PgEventstore
58
73
 
59
74
  # @param id [Integer]
60
75
  # @param attrs [Hash]
61
- def update(id, attrs)
76
+ # @param locked_by [Integer]
77
+ # @return [Hash]
78
+ # @raise [PgEventstore::RecordNotFound]
79
+ # @raise [PgEventstore::WrongLockIdError]
80
+ def update(id, attrs:, locked_by:)
62
81
  attrs = { updated_at: Time.now.utc }.merge(attrs)
63
82
  attrs_sql = attrs.keys.map.with_index(1) do |attr, index|
64
83
  "#{attr} = $#{index}"
65
84
  end.join(', ')
66
85
  sql =
67
86
  "UPDATE subscriptions SET #{attrs_sql} WHERE id = $#{attrs.keys.size + 1} RETURNING *"
68
- pg_result = connection.with do |conn|
69
- conn.exec_params(sql, [*attrs.values, id])
87
+ updated_attrs = transaction_queries.transaction do
88
+ pg_result = connection.with do |conn|
89
+ conn.exec_params(sql, [*attrs.values, id])
90
+ end
91
+ raise(RecordNotFound.new("subscriptions", id)) if pg_result.ntuples.zero?
92
+
93
+ updated_attrs = pg_result.to_a.first
94
+ unless updated_attrs['locked_by'] == locked_by
95
+ # Subscription is force-locked by someone else. We have to roll back such transaction
96
+ raise(WrongLockIdError.new(updated_attrs['set'], updated_attrs['name'], updated_attrs['locked_by']))
97
+ end
98
+ updated_attrs
70
99
  end
71
- raise(RecordNotFound.new("subscriptions", id)) if pg_result.ntuples.zero?
72
100
 
73
- deserialize(pg_result.to_a.first)
101
+ deserialize(updated_attrs)
74
102
  end
75
103
 
76
- # @param query_options [Array<Array<Integer, Hash>>] array of runner ids and query options
77
- # @return [Array<Hash>] array of raw events
104
+ # @param query_options [Hash{Integer => Hash}] runner_id/query options association
105
+ # @return [Hash{Integer => Hash}] runner_id/events association
78
106
  def subscriptions_events(query_options)
79
- return [] if query_options.empty?
107
+ return {} if query_options.empty?
80
108
 
81
109
  final_builder = union_builders(query_options.map { |id, opts| query_builder(id, opts) })
82
- connection.with do |conn|
110
+ raw_events = connection.with do |conn|
83
111
  conn.exec_params(*final_builder.to_exec_params)
84
112
  end.to_a
113
+ raw_events.group_by { _1['runner_id'] }.to_h do |runner_id, runner_raw_events|
114
+ next [runner_id, runner_raw_events] unless query_options[runner_id][:resolve_link_tos]
115
+
116
+ [runner_id, links_resolver.resolve(runner_raw_events)]
117
+ end
85
118
  end
86
119
 
87
120
  # @param id [Integer] subscription's id
88
- # @param lock_id [String] UUIDv4 id of the subscriptions set which reserves the subscription
121
+ # @param lock_id [Integer] id of the subscriptions set which reserves the subscription
89
122
  # @param force [Boolean] whether to lock the subscription despite on #locked_by value
90
- # @return [String] UUIDv4 lock id
123
+ # @return [Integer] lock id
91
124
  # @raise [SubscriptionAlreadyLockedError] in case the Subscription is already locked
92
- def lock!(id, lock_id, force = false)
125
+ def lock!(id, lock_id, force: false)
93
126
  transaction_queries.transaction do
94
127
  attrs = find!(id)
128
+ # We don't care who locked the Subscription - whether it is the same SubscriptionsSet or not - multiple locks
129
+ # must not happen even with the same SubscriptionsSet. We later assume this to reset Subscription's stats, for
130
+ # example.
95
131
  if attrs[:locked_by] && !force
96
132
  raise SubscriptionAlreadyLockedError.new(attrs[:set], attrs[:name], attrs[:locked_by])
97
133
  end
@@ -102,22 +138,11 @@ module PgEventstore
102
138
  lock_id
103
139
  end
104
140
 
105
- # @param id [Integer] subscription's id
106
- # @param lock_id [String] UUIDv4 id of the set which reserved the subscription after itself
141
+ # @param id [Integer]
107
142
  # @return [void]
108
- # @raise [SubscriptionUnlockError] in case the Subscription is locked by some SubscriptionsSet, other than the one,
109
- # persisted in memory
110
- def unlock!(id, lock_id)
111
- transaction_queries.transaction do
112
- attrs = find!(id)
113
- # Normally this should never happen as locking/unlocking happens within the same process. This is done only for
114
- # the matter of consistency.
115
- unless attrs[:locked_by] == lock_id
116
- raise SubscriptionUnlockError.new(attrs[:set], attrs[:name], lock_id, attrs[:locked_by])
117
- end
118
- connection.with do |conn|
119
- conn.exec_params('UPDATE subscriptions SET locked_by = $1 WHERE id = $2', [nil, id])
120
- end
143
+ def delete(id)
144
+ connection.with do |conn|
145
+ conn.exec_params('DELETE FROM subscriptions WHERE id = $1', [id])
121
146
  end
122
147
  end
123
148
 
@@ -144,9 +169,9 @@ module PgEventstore
144
169
  TransactionQueries.new(connection)
145
170
  end
146
171
 
147
- # @return [PgEventstore::EventTypeQueries]
148
- def event_type_queries
149
- EventTypeQueries.new(connection)
172
+ # @return [PgEventstore::LinksResolver]
173
+ def links_resolver
174
+ LinksResolver.new(connection)
150
175
  end
151
176
 
152
177
  # @param hash [Hash]
@@ -154,5 +179,15 @@ module PgEventstore
154
179
  def deserialize(hash)
155
180
  hash.transform_keys(&:to_sym)
156
181
  end
182
+
183
+ # @param attrs [Hash]
184
+ # @return [PgEventstore::SQLBuilder]
185
+ def find_by_attrs_builder(attrs)
186
+ builder = SQLBuilder.new.select('*').from('subscriptions').order('id ASC')
187
+ attrs.each do |attr, val|
188
+ builder.where("#{attr} = ?", val)
189
+ end
190
+ builder
191
+ end
157
192
  end
158
193
  end
@@ -11,6 +11,13 @@ module PgEventstore
11
11
  @connection = connection
12
12
  end
13
13
 
14
+ # @return [Hash]
15
+ def find_or_create_by(...)
16
+ transaction_queries.transaction do
17
+ find_by(...) || create(...)
18
+ end
19
+ end
20
+
14
21
  # @param subscriptions_set_id [Integer]
15
22
  # @param command_name [String]
16
23
  # @return [Hash, nil]
@@ -31,7 +38,7 @@ module PgEventstore
31
38
  # @param subscriptions_set_id [Integer]
32
39
  # @param command_name [String]
33
40
  # @return [Hash]
34
- def create_by(subscriptions_set_id:, command_name:)
41
+ def create(subscriptions_set_id:, command_name:)
35
42
  sql = <<~SQL
36
43
  INSERT INTO subscriptions_set_commands (name, subscriptions_set_id)
37
44
  VALUES ($1, $2)
@@ -72,5 +79,10 @@ module PgEventstore
72
79
  def deserialize(hash)
73
80
  hash.transform_keys(&:to_sym)
74
81
  end
82
+
83
+ # @return [PgEventstore::TransactionQueries]
84
+ def transaction_queries
85
+ TransactionQueries.new(connection)
86
+ end
75
87
  end
76
88
  end
@@ -14,7 +14,7 @@ module PgEventstore
14
14
  # @param attrs [Hash]
15
15
  # @return [Array<Hash>]
16
16
  def find_all(attrs)
17
- builder = SQLBuilder.new.select('*').from('subscriptions_set')
17
+ builder = SQLBuilder.new.select('*').from('subscriptions_set').order('name ASC, id ASC')
18
18
  attrs.each do |attr, val|
19
19
  builder.where("#{attr} = ?", val)
20
20
  end
@@ -25,13 +25,22 @@ module PgEventstore
25
25
  pg_result.to_a.map(&method(:deserialize))
26
26
  end
27
27
 
28
+ # @return [Array<String>]
29
+ def set_names
30
+ builder = SQLBuilder.new.select('name').from('subscriptions_set').group('name').order('name ASC')
31
+
32
+ connection.with do |conn|
33
+ conn.exec_params(*builder.to_exec_params)
34
+ end.map { |attrs| attrs['name'] }
35
+ end
36
+
28
37
  # The same as #find_all, but returns first result
29
38
  # @return [Hash, nil]
30
39
  def find_by(...)
31
40
  find_all(...).first
32
41
  end
33
42
 
34
- # @param id [String] UUIDv4
43
+ # @param id [Integer]
35
44
  # @return [Hash]
36
45
  # @raise [PgEventstore::RecordNotFound]
37
46
  def find!(id)
@@ -52,7 +61,7 @@ module PgEventstore
52
61
  deserialize(pg_result.to_a.first)
53
62
  end
54
63
 
55
- # @param id [String] UUIDv4
64
+ # @param id [Integer]
56
65
  # @param attrs [Hash]
57
66
  def update(id, attrs)
58
67
  attrs = { updated_at: Time.now.utc }.merge(attrs)
@@ -70,7 +79,7 @@ module PgEventstore
70
79
  deserialize(pg_result.to_a.first)
71
80
  end
72
81
 
73
- # @return id [Integer]
82
+ # @param id [Integer]
74
83
  # @return [void]
75
84
  def delete(id)
76
85
  connection.with do |conn|
@@ -85,5 +94,10 @@ module PgEventstore
85
94
  def deserialize(hash)
86
95
  hash.transform_keys(&:to_sym)
87
96
  end
97
+
98
+ # @return [PgEventstore::TransactionQueries]
99
+ def transaction_queries
100
+ TransactionQueries.new(connection)
101
+ end
88
102
  end
89
103
  end
@@ -9,6 +9,7 @@ require_relative 'queries/subscription_queries'
9
9
  require_relative 'queries/subscriptions_set_queries'
10
10
  require_relative 'queries/subscription_command_queries'
11
11
  require_relative 'queries/subscriptions_set_command_queries'
12
+ require_relative 'queries/links_resolver'
12
13
 
13
14
  module PgEventstore
14
15
  # @!visibility private
@@ -28,10 +28,9 @@ module PgEventstore
28
28
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
29
29
  def all_stream_filtering(options)
30
30
  event_filter = new
31
- options in { filter: { event_types: Array => event_type_ids } }
32
- event_filter.add_event_types(event_type_ids)
31
+ options in { filter: { event_types: Array => event_types } }
32
+ event_filter.add_event_types(event_types)
33
33
  event_filter.add_limit(options[:max_count])
34
- event_filter.resolve_links(options[:resolve_link_tos])
35
34
  options in { filter: { streams: Array => streams } }
36
35
  streams&.each { |attrs| event_filter.add_stream_attrs(**attrs) }
37
36
  event_filter.add_global_position(options[:from_position], options[:direction])
@@ -44,10 +43,9 @@ module PgEventstore
44
43
  # @return [PgEventstore::QueryBuilders::EventsFiltering]
45
44
  def specific_stream_filtering(stream, options)
46
45
  event_filter = new
47
- options in { filter: { event_types: Array => event_type_ids } }
48
- event_filter.add_event_types(event_type_ids)
46
+ options in { filter: { event_types: Array => event_types } }
47
+ event_filter.add_event_types(event_types)
49
48
  event_filter.add_limit(options[:max_count])
50
- event_filter.resolve_links(options[:resolve_link_tos])
51
49
  event_filter.add_stream_attrs(**stream.to_hash)
52
50
  event_filter.add_revision(options[:from_revision], options[:direction])
53
51
  event_filter.add_stream_direction(options[:direction])
@@ -128,17 +126,6 @@ module PgEventstore
128
126
  @sql_builder.limit(limit)
129
127
  end
130
128
 
131
- # @param should_resolve [Boolean]
132
- # @return [void]
133
- def resolve_links(should_resolve)
134
- return unless should_resolve
135
-
136
- @sql_builder.
137
- unselect.
138
- select("(COALESCE(original_events.*, events.*)).*").
139
- join("LEFT JOIN events original_events ON original_events.id = events.link_id")
140
- end
141
-
142
129
  # @return [PgEventstore::SQLBuilder]
143
130
  def to_sql_builder
144
131
  @sql_builder
@@ -3,7 +3,7 @@
3
3
  module PgEventstore
4
4
  module CommandHandlers
5
5
  class SubscriptionFeederCommands
6
- AVAILABLE_COMMANDS = %w[StopAll StartAll].freeze
6
+ AVAILABLE_COMMANDS = %i[StopAll StartAll Restore Stop].to_h { [_1, _1.to_s] }.freeze
7
7
 
8
8
  # @param config_name [Symbol]
9
9
  # @param subscription_feeder [PgEventstore::SubscriptionFeeder]
@@ -16,7 +16,7 @@ module PgEventstore
16
16
  # @return [void]
17
17
  def process
18
18
  queries.find_commands(@subscription_feeder.id).each do |command|
19
- unless AVAILABLE_COMMANDS.include?(command[:name])
19
+ unless AVAILABLE_COMMANDS.values.include?(command[:name])
20
20
  PgEventstore.logger&.warn(
21
21
  "#{self.class.name}: Don't know how to handle #{command[:name].inspect}. Details: #{command.inspect}."
22
22
  )
@@ -47,6 +47,14 @@ module PgEventstore
47
47
  def start_all
48
48
  @subscription_feeder.start_all
49
49
  end
50
+
51
+ def restore
52
+ @subscription_feeder.restore
53
+ end
54
+
55
+ def stop
56
+ @subscription_feeder.stop
57
+ end
50
58
  end
51
59
  end
52
60
  end
@@ -3,20 +3,22 @@
3
3
  module PgEventstore
4
4
  module CommandHandlers
5
5
  class SubscriptionRunnersCommands
6
- AVAILABLE_COMMANDS = %w[StopRunner RestoreRunner StartRunner].freeze
6
+ AVAILABLE_COMMANDS = %i[Start Stop Restore].to_h { [_1, _1.to_s] }.freeze
7
7
 
8
8
  # @param config_name [Symbol]
9
9
  # @param runners [Array<PgEventstore::SubscriptionRunner>]
10
- def initialize(config_name, runners)
10
+ # @param subscriptions_set_id [Integer]
11
+ def initialize(config_name, runners, subscriptions_set_id)
11
12
  @config_name = config_name
12
13
  @runners = runners
14
+ @subscriptions_set_id = subscriptions_set_id
13
15
  end
14
16
 
15
17
  # Look up commands for all given SubscriptionRunner-s and execute them
16
18
  # @return [void]
17
19
  def process
18
- queries.find_commands(@runners.map(&:id)).each do |command|
19
- unless AVAILABLE_COMMANDS.include?(command[:name])
20
+ queries.find_commands(@runners.map(&:id), subscriptions_set_id: @subscriptions_set_id).each do |command|
21
+ unless AVAILABLE_COMMANDS.values.include?(command[:name])
20
22
  PgEventstore.logger&.warn(
21
23
  "#{self.class.name}: Don't know how to handle #{command[:name].inspect}. Details: #{command.inspect}."
22
24
  )
@@ -48,19 +50,19 @@ module PgEventstore
48
50
 
49
51
  # @param subscription_id [Integer]
50
52
  # @return [void]
51
- def start_runner(subscription_id)
53
+ def start(subscription_id)
52
54
  find_subscription_runner(subscription_id)&.start
53
55
  end
54
56
 
55
57
  # @param subscription_id [Integer]
56
58
  # @return [void]
57
- def restore_runner(subscription_id)
59
+ def restore(subscription_id)
58
60
  find_subscription_runner(subscription_id)&.restore
59
61
  end
60
62
 
61
63
  # @param subscription_id [Integer]
62
64
  # @return [void]
63
- def stop_runner(subscription_id)
65
+ def stop(subscription_id)
64
66
  find_subscription_runner(subscription_id)&.stop_async
65
67
  end
66
68
  end
@@ -11,6 +11,7 @@ module PgEventstore
11
11
  extend Forwardable
12
12
 
13
13
  RESTART_DELAY = 5 # seconds
14
+ PULL_INTERVAL = 1
14
15
 
15
16
  def_delegators :@basic_runner, :start, :stop, :state, :stop_async, :wait_for_finish
16
17
 
@@ -21,7 +22,7 @@ module PgEventstore
21
22
  @config_name = config_name
22
23
  @subscription_feeder = subscription_feeder
23
24
  @runners = runners
24
- @basic_runner = BasicRunner.new(1, 0)
25
+ @basic_runner = BasicRunner.new(PULL_INTERVAL, 0)
25
26
  attach_runner_callbacks
26
27
  end
27
28
 
@@ -56,7 +57,7 @@ module PgEventstore
56
57
 
57
58
  # @return [PgEventstore::CommandHandlers::SubscriptionRunnersCommands]
58
59
  def subscription_runners_commands
59
- CommandHandlers::SubscriptionRunnersCommands.new(@config_name, @runners)
60
+ CommandHandlers::SubscriptionRunnersCommands.new(@config_name, @runners, @subscription_feeder.id)
60
61
  end
61
62
  end
62
63
  end
@@ -20,7 +20,7 @@ module PgEventstore
20
20
  # @param raw_events [Array<Hash>]
21
21
  # @return [void]
22
22
  def feed(raw_events)
23
- callbacks.run_callbacks(:feed, raw_events.last&.dig('global_position'))
23
+ callbacks.run_callbacks(:feed, global_position(raw_events.last))
24
24
  @raw_events.push(*raw_events)
25
25
  end
26
26
 
@@ -35,7 +35,7 @@ module PgEventstore
35
35
  # @param raw_event [Hash]
36
36
  # @return [void]
37
37
  def process_event(raw_event)
38
- callbacks.run_callbacks(:process, raw_event['global_position']) do
38
+ callbacks.run_callbacks(:process, global_position(raw_event)) do
39
39
  @handler.call(raw_event)
40
40
  end
41
41
  end
@@ -68,5 +68,13 @@ module PgEventstore
68
68
  def change_state(...)
69
69
  callbacks.run_callbacks(:change_state, ...)
70
70
  end
71
+
72
+ # @param raw_event [Hash, nil]
73
+ # @return [Integer, nil]
74
+ def global_position(raw_event)
75
+ return unless raw_event
76
+
77
+ raw_event['link'] ? raw_event['link']['global_position'] : raw_event['global_position']
78
+ end
71
79
  end
72
80
  end
@@ -63,8 +63,8 @@ module PgEventstore
63
63
  # processor
64
64
  attribute(:last_chunk_greatest_position)
65
65
  # @!attribute locked_by
66
- # @return [String, nil] UUIDv4. The id of subscription manager which obtained the lock of the Subscription. _nil_
67
- # value means that the Subscription isn't locked yet by any subscription manager.
66
+ # @return [Integer, nil] The id of subscription manager which obtained the lock of the Subscription. _nil_ value
67
+ # means that the Subscription isn't locked yet by any subscription manager.
68
68
  attribute(:locked_by)
69
69
  # @!attribute created_at
70
70
  # @return [Time]
@@ -80,7 +80,7 @@ module PgEventstore
80
80
  # @param attrs [Hash]
81
81
  # @return [Hash]
82
82
  def update(attrs)
83
- assign_attributes(subscription_queries.update(id, attrs))
83
+ assign_attributes(subscription_queries.update(id, attrs: attrs, locked_by: locked_by))
84
84
  end
85
85
 
86
86
  # @param attrs [Hash]
@@ -93,20 +93,13 @@ module PgEventstore
93
93
 
94
94
  # Locks the Subscription by the given lock id
95
95
  # @return [PgEventstore::Subscription]
96
- def lock!(lock_id, force = false)
96
+ def lock!(lock_id, force: false)
97
97
  self.id = subscription_queries.find_or_create_by(set: set, name: name)[:id]
98
- self.locked_by = subscription_queries.lock!(id, lock_id, force)
98
+ self.locked_by = subscription_queries.lock!(id, lock_id, force: force)
99
99
  reset_runtime_attributes
100
100
  self
101
101
  end
102
102
 
103
- # Unlocks the Subscription.
104
- # @return [void]
105
- def unlock!
106
- subscription_queries.unlock!(id, locked_by)
107
- self.locked_by = nil
108
- end
109
-
110
103
  # Dup the current object without assigned connection
111
104
  # @return [PgEventstore::Subscription]
112
105
  def dup
@@ -119,6 +112,27 @@ module PgEventstore
119
112
  self
120
113
  end
121
114
 
115
+ # @return [Integer]
116
+ def hash
117
+ id.hash
118
+ end
119
+
120
+ # @param another [Object]
121
+ # @return [Boolean]
122
+ def eql?(another)
123
+ return false unless another.is_a?(Subscription)
124
+
125
+ hash == another.hash
126
+ end
127
+
128
+ # @param another [PgEventstore::SubscriptionsSet]
129
+ # @return [Boolean]
130
+ def ==(another)
131
+ return false unless another.is_a?(Subscription)
132
+
133
+ id == another.id
134
+ end
135
+
122
136
  private
123
137
 
124
138
  def reset_runtime_attributes
@@ -130,6 +144,9 @@ module PgEventstore
130
144
  chunk_query_interval: chunk_query_interval,
131
145
  last_chunk_fed_at: Time.at(0).utc,
132
146
  last_chunk_greatest_position: nil,
147
+ last_error: nil,
148
+ last_error_occurred_at: nil,
149
+ time_between_restarts: time_between_restarts,
133
150
  state: RunnerState::STATES[:initial]
134
151
  )
135
152
  end
@@ -3,10 +3,11 @@
3
3
  module PgEventstore
4
4
  # This class is responsible for starting/stopping all SubscriptionRunners. The background runner of it is responsible
5
5
  # for events pulling and feeding those SubscriptionRunners.
6
- # @!visibility private
7
6
  class SubscriptionFeeder
8
7
  extend Forwardable
9
8
 
9
+ HEARTBEAT_INTERVAL = 10 # seconds
10
+
10
11
  def_delegators :subscriptions_set, :id
11
12
  def_delegators :@basic_runner, :start, :stop, :restore, :state, :wait_for_finish, :stop_async
12
13
 
@@ -17,12 +18,13 @@ module PgEventstore
17
18
  def initialize(config_name:, set_name:, max_retries:, retries_interval:)
18
19
  @config_name = config_name
19
20
  @runners = []
20
- @set_name = set_name
21
- @max_retries = max_retries
22
- @retries_interval = retries_interval
21
+ @subscriptions_set_attrs = {
22
+ name: set_name, max_restarts_number: max_retries, time_between_restarts: retries_interval
23
+ }
23
24
  @commands_handler = CommandsHandler.new(@config_name, self, @runners)
24
- @basic_runner = BasicRunner.new(1, 0)
25
+ @basic_runner = BasicRunner.new(0.2, 0)
25
26
  @force_lock = false
27
+ @refreshed_at = Time.at(0)
26
28
  attach_runner_callbacks
27
29
  end
28
30
 
@@ -77,18 +79,12 @@ module PgEventstore
77
79
  # Locks all Subscriptions behind the current SubscriptionsSet
78
80
  # @return [void]
79
81
  def lock_all
80
- @runners.each { |runner| runner.lock!(subscriptions_set.id, @force_lock) }
81
- end
82
-
83
- # @return [void]
84
- def unlock_all
85
- @runners.each(&:unlock!)
82
+ @runners.each { |runner| runner.lock!(subscriptions_set.id, force: @force_lock) }
86
83
  end
87
84
 
88
85
  # @return [PgEventstore::SubscriptionsSet]
89
86
  def subscriptions_set
90
- @subscriptions_set ||= SubscriptionsSet.using_connection(@config_name).
91
- create(name: @set_name, max_restarts_number: @max_retries, time_between_restarts: @retries_interval)
87
+ @subscriptions_set ||= SubscriptionsSet.using_connection(@config_name).create(**@subscriptions_set_attrs)
92
88
  end
93
89
 
94
90
  # @return [PgEventstore::SubscriptionRunnersFeeder]
@@ -102,6 +98,7 @@ module PgEventstore
102
98
  @basic_runner.define_callback(:before_runner_started, :before, method(:before_runner_started))
103
99
  @basic_runner.define_callback(:after_runner_died, :before, method(:after_runner_died))
104
100
  @basic_runner.define_callback(:after_runner_died, :after, method(:restart_runner))
101
+ @basic_runner.define_callback(:process_async, :before, method(:ping_subscriptions_set))
105
102
  @basic_runner.define_callback(:process_async, :before, method(:process_async))
106
103
  @basic_runner.define_callback(:after_runner_stopped, :before, method(:after_runner_stopped))
107
104
  @basic_runner.define_callback(:before_runner_restored, :after, method(:update_runner_restarts))
@@ -141,13 +138,20 @@ module PgEventstore
141
138
  feeder.feed(@runners)
142
139
  end
143
140
 
141
+ # @return [void]
142
+ def ping_subscriptions_set
143
+ return unless subscriptions_set.updated_at > Time.now.utc - HEARTBEAT_INTERVAL
144
+
145
+ subscriptions_set.update(updated_at: Time.now.utc)
146
+ @refreshed_at = Time.now.utc
147
+ end
148
+
144
149
  # @return [void]
145
150
  def after_runner_stopped
146
- @commands_handler.stop
151
+ @runners.each(&:stop_async).each(&:wait_for_finish)
147
152
  @subscriptions_set&.delete
148
153
  @subscriptions_set = nil
149
- @runners.each(&:stop_async).each(&:wait_for_finish)
150
- unlock_all
154
+ @commands_handler.stop
151
155
  end
152
156
 
153
157
  # @return [void]
@@ -16,7 +16,7 @@ module PgEventstore
16
16
  attr_reader :subscription
17
17
 
18
18
  def_delegators :@events_processor, :start, :stop, :stop_async, :feed, :wait_for_finish, :restore, :state, :running?
19
- def_delegators :@subscription, :lock!, :unlock!, :id
19
+ def_delegators :@subscription, :lock!, :id
20
20
 
21
21
  # @param stats [PgEventstore::SubscriptionHandlerPerformance]
22
22
  # @param events_processor [PgEventstore::EventsProcessor]
@@ -15,11 +15,10 @@ module PgEventstore
15
15
  runners = runners.select(&:running?).select(&:time_to_feed?)
16
16
  return if runners.empty?
17
17
 
18
- runners_query_options = runners.map { |runner| [runner.id, runner.next_chunk_query_opts] }
19
- raw_events = subscription_queries.subscriptions_events(runners_query_options)
20
- grouped_events = raw_events.group_by { |attrs| attrs['runner_id'] }
18
+ runners_query_options = runners.to_h { |runner| [runner.id, runner.next_chunk_query_opts] }
19
+ grouped_events = subscription_queries.subscriptions_events(runners_query_options)
21
20
  runners.each do |runner|
22
- runner.feed(grouped_events[runner.id]) if grouped_events[runner.id]
21
+ runner.feed(grouped_events[runner.id] || [])
23
22
  end
24
23
  end
25
24