pg_eventstore 0.3.0 → 0.4.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +1 -0
  4. data/db/migrations/10_create_subscription_commands.sql +15 -0
  5. data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
  6. data/db/migrations/12_improve_events_indexes.sql +1 -0
  7. data/db/migrations/9_create_subscriptions.sql +46 -0
  8. data/docs/configuration.md +42 -21
  9. data/docs/subscriptions.md +170 -0
  10. data/lib/pg_eventstore/callbacks.rb +122 -0
  11. data/lib/pg_eventstore/client.rb +2 -2
  12. data/lib/pg_eventstore/config.rb +35 -3
  13. data/lib/pg_eventstore/errors.rb +63 -0
  14. data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +11 -14
  15. data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
  16. data/lib/pg_eventstore/extensions/options_extension.rb +25 -23
  17. data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
  18. data/lib/pg_eventstore/queries/event_queries.rb +5 -26
  19. data/lib/pg_eventstore/queries/event_type_queries.rb +13 -0
  20. data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
  21. data/lib/pg_eventstore/queries/subscription_queries.rb +160 -0
  22. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
  23. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
  24. data/lib/pg_eventstore/queries.rb +6 -0
  25. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +14 -2
  26. data/lib/pg_eventstore/sql_builder.rb +54 -10
  27. data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
  28. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
  29. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
  30. data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
  31. data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
  32. data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
  33. data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
  34. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
  35. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
  36. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
  37. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
  38. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
  39. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
  40. data/lib/pg_eventstore/tasks/setup.rake +1 -1
  41. data/lib/pg_eventstore/utils.rb +66 -0
  42. data/lib/pg_eventstore/version.rb +1 -1
  43. data/lib/pg_eventstore.rb +19 -1
  44. metadata +30 -4
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # This class actually processes events.
5
+ # @!visibility private
6
+ class EventsProcessor
7
+ include Extensions::CallbacksExtension
8
+ extend Forwardable
9
+
10
+ def_delegators :@basic_runner, :state, :start, :stop, :wait_for_finish, :stop_async, :restore, :running?
11
+
12
+ # @param handler [#call]
13
+ def initialize(handler)
14
+ @handler = handler
15
+ @raw_events = []
16
+ @basic_runner = BasicRunner.new(0, 5)
17
+ attach_runner_callbacks
18
+ end
19
+
20
+ # @param raw_events [Array<Hash>]
21
+ # @return [void]
22
+ def feed(raw_events)
23
+ callbacks.run_callbacks(:feed, raw_events.last&.dig('global_position'))
24
+ @raw_events.push(*raw_events)
25
+ end
26
+
27
+ # Number of unprocessed events which are currently in a queue
28
+ # @return [Integer]
29
+ def events_left_in_chunk
30
+ @raw_events.size
31
+ end
32
+
33
+ private
34
+
35
+ # @param raw_event [Hash]
36
+ # @return [void]
37
+ def process_event(raw_event)
38
+ callbacks.run_callbacks(:process, raw_event['global_position']) do
39
+ @handler.call(raw_event)
40
+ end
41
+ end
42
+
43
+ def attach_runner_callbacks
44
+ @basic_runner.define_callback(:process_async, :before, method(:process_async))
45
+ @basic_runner.define_callback(:after_runner_died, :before, method(:after_runner_died))
46
+ @basic_runner.define_callback(:before_runner_restored, :before, method(:before_runner_restored))
47
+ @basic_runner.define_callback(:change_state, :before, method(:change_state))
48
+ end
49
+
50
+ def process_async
51
+ raw_event = @raw_events.shift
52
+ return sleep 0.5 if raw_event.nil?
53
+
54
+ process_event(raw_event)
55
+ rescue
56
+ @raw_events.unshift(raw_event)
57
+ raise
58
+ end
59
+
60
+ def after_runner_died(...)
61
+ callbacks.run_callbacks(:error, ...)
62
+ end
63
+
64
+ def before_runner_restored
65
+ callbacks.run_callbacks(:restart)
66
+ end
67
+
68
+ def change_state(...)
69
+ callbacks.run_callbacks(:change_state, ...)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # Implements different states of a runner.
5
+ # @!visibility private
6
+ class RunnerState
7
+ include Extensions::CallbacksExtension
8
+
9
+ STATES = %i(initial running halting stopped dead).to_h { [_1, _1.to_s.freeze] }.freeze
10
+
11
+ def initialize
12
+ initial!
13
+ end
14
+
15
+ STATES.each do |state, value|
16
+ # Checks whether a runner is in appropriate state
17
+ # @return [Boolean]
18
+ define_method "#{state}?" do
19
+ @state == value
20
+ end
21
+
22
+ # Sets the state.
23
+ # @return [String]
24
+ define_method "#{state}!" do
25
+ set_state(value)
26
+ end
27
+ end
28
+
29
+ # @return [String] string representation of the state
30
+ def to_s
31
+ @state
32
+ end
33
+
34
+ private
35
+
36
+ # @param state [String]
37
+ # @return [String]
38
+ def set_state(state)
39
+ old_state = @state
40
+ @state = state
41
+ callbacks.run_callbacks(:change_state, @state) unless old_state == @state
42
+ @state
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # Defines ruby's representation of subscriptions record.
5
+ class Subscription
6
+ include Extensions::UsingConnectionExtension
7
+ include Extensions::OptionsExtension
8
+
9
+ # @!attribute id
10
+ # @return [Integer]
11
+ attribute(:id)
12
+ # @!attribute set
13
+ # @return [String] Subscription's set. Subscription should have unique pair of set and name.
14
+ attribute(:set)
15
+ # @!attribute name
16
+ # @return [String] Subscription's name. Subscription should have unique pair of set and name.
17
+ attribute(:name)
18
+ # @!attribute total_processed_events
19
+ # @return [Integer] total number of events, processed by this subscription
20
+ attribute(:total_processed_events)
21
+ # @!attribute options
22
+ # @return [Hash] subscription's options to be used to query events. See {SubscriptionManager#subscribe} for the
23
+ # list of available options
24
+ attribute(:options)
25
+ # @!attribute current_position
26
+ # @return [Integer, nil] current Subscription's position. It is updated automatically each time an event is processed
27
+ attribute(:current_position)
28
+ # @!attribute state
29
+ # @return [String, nil] current Subscription's state. It is updated automatically during Subscription's life cycle.
30
+ # See {RunnerState::STATES} for possible values.
31
+ attribute(:state)
32
+ # @!attribute average_event_processing_time
33
+ # @return [Float, nil] a speed of the subscription. Divide 1 by this value to determine how much events are
34
+ # processed by the Subscription per second.
35
+ attribute(:average_event_processing_time)
36
+ # @!attribute restart_count
37
+ # @return [Integer] the number of Subscription's restarts after its failure
38
+ attribute(:restart_count)
39
+ # @!attribute max_restarts_number
40
+ # @return [Integer] maximum number of times the Subscription can be restarted
41
+ attribute(:max_restarts_number)
42
+ # @!attribute time_between_restarts
43
+ # @return [Integer] interval in seconds between retries of failed Subscription
44
+ attribute(:time_between_restarts)
45
+ # @!attribute last_restarted_at
46
+ # @return [Time, nil] last time the Subscription was restarted
47
+ attribute(:last_restarted_at)
48
+ # @!attribute last_error
49
+ # @return [Hash{'class' => String, 'message' => String, 'backtrace' => Array<String>}, nil] the information about
50
+ # last error caused when processing events by the Subscription.
51
+ attribute(:last_error)
52
+ # @!attribute last_error_occurred_at
53
+ # @return [Time, nil] the time when the last error occurred
54
+ attribute(:last_error_occurred_at)
55
+ # @!attribute chunk_query_interval
56
+ # @return [Integer] determines how often to pull events for the given Subscription in seconds
57
+ attribute(:chunk_query_interval)
58
+ # @!attribute chunk_query_interval
59
+ # @return [Time] shows the time when last time events were fed to the event's processor
60
+ attribute(:last_chunk_fed_at)
61
+ # @!attribute last_chunk_greatest_position
62
+ # @return [Integer, nil] shows the greatest global_position of the last event in the last chunk fed to the event's
63
+ # processor
64
+ attribute(:last_chunk_greatest_position)
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.
68
+ attribute(:locked_by)
69
+ # @!attribute created_at
70
+ # @return [Time]
71
+ attribute(:created_at)
72
+ # @!attribute updated_at
73
+ # @return [Time]
74
+ attribute(:updated_at)
75
+
76
+ def options=(val)
77
+ @options = Utils.deep_transform_keys(val, &:to_sym)
78
+ end
79
+
80
+ # @param attrs [Hash]
81
+ # @return [Hash]
82
+ def update(attrs)
83
+ assign_attributes(subscription_queries.update(id, attrs))
84
+ end
85
+
86
+ # @param attrs [Hash]
87
+ # @return [Hash]
88
+ def assign_attributes(attrs)
89
+ attrs.each do |attr, value|
90
+ public_send("#{attr}=", value)
91
+ end
92
+ end
93
+
94
+ # Locks the Subscription by the given lock id
95
+ # @return [PgEventstore::Subscription]
96
+ def lock!(lock_id, force = false)
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)
99
+ reset_runtime_attributes
100
+ self
101
+ end
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
+ # Dup the current object without assigned connection
111
+ # @return [PgEventstore::Subscription]
112
+ def dup
113
+ Subscription.new(**Utils.deep_dup(options_hash))
114
+ end
115
+
116
+ # @return [PgEventstore::Subscription]
117
+ def reload
118
+ assign_attributes(subscription_queries.find!(id))
119
+ self
120
+ end
121
+
122
+ private
123
+
124
+ def reset_runtime_attributes
125
+ update(
126
+ options: options,
127
+ restart_count: 0,
128
+ last_restarted_at: nil,
129
+ max_restarts_number: max_restarts_number,
130
+ chunk_query_interval: chunk_query_interval,
131
+ last_chunk_fed_at: Time.at(0).utc,
132
+ last_chunk_greatest_position: nil,
133
+ state: RunnerState::STATES[:initial]
134
+ )
135
+ end
136
+
137
+ def subscription_queries
138
+ SubscriptionQueries.new(self.class.connection)
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # This class is responsible for starting/stopping all SubscriptionRunners. The background runner of it is responsible
5
+ # for events pulling and feeding those SubscriptionRunners.
6
+ # @!visibility private
7
+ class SubscriptionFeeder
8
+ extend Forwardable
9
+
10
+ def_delegators :subscriptions_set, :id
11
+ def_delegators :@basic_runner, :start, :stop, :restore, :state, :wait_for_finish, :stop_async
12
+
13
+ # @param config_name [Symbol]
14
+ # @param set_name [String]
15
+ # @param max_retries [Integer] max number of retries of failed SubscriptionsSet
16
+ # @param retries_interval [Integer] a delay between retries of failed SubscriptionsSet
17
+ def initialize(config_name:, set_name:, max_retries:, retries_interval:)
18
+ @config_name = config_name
19
+ @runners = []
20
+ @set_name = set_name
21
+ @max_retries = max_retries
22
+ @retries_interval = retries_interval
23
+ @commands_handler = CommandsHandler.new(@config_name, self, @runners)
24
+ @basic_runner = BasicRunner.new(1, 0)
25
+ @force_lock = false
26
+ attach_runner_callbacks
27
+ end
28
+
29
+ # Adds SubscriptionRunner to the set
30
+ # @param runner [PgEventstore::SubscriptionRunner]
31
+ def add(runner)
32
+ assert_proper_state!
33
+ @runners.push(runner)
34
+ runner
35
+ end
36
+
37
+ # Starts all SubscriptionRunners. This is only available if SubscriptionFeeder's runner is alive.
38
+ # @return [void]
39
+ def start_all
40
+ return self unless @basic_runner.running?
41
+
42
+ @runners.each(&:start)
43
+ self
44
+ end
45
+
46
+ # Stops all SubscriptionRunners asynchronous. This is only available if SubscriptionFeeder's runner is alive.
47
+ # @return [void]
48
+ def stop_all
49
+ return self unless @basic_runner.running?
50
+
51
+ @runners.each(&:stop_async)
52
+ self
53
+ end
54
+
55
+ # Sets the force_lock flash to true. If set - all related Subscriptions will ignore their lock state and will be
56
+ # locked by the new SubscriptionsSet.
57
+ def force_lock!
58
+ @force_lock = true
59
+ end
60
+
61
+ # Produces a copy of currently running Subscriptions. This is needed, because original Subscriptions objects are
62
+ # dangerous to use - users may incidentally break their state.
63
+ # @return [Array<PgEventstore::Subscription>]
64
+ def read_only_subscriptions
65
+ @runners.map(&:subscription).map(&:dup)
66
+ end
67
+
68
+ # Produces a copy of current SubscriptionsSet. This is needed, because original SubscriptionsSet object is
69
+ # dangerous to use - users may incidentally break its state.
70
+ # @return [PgEventstore::SubscriptionsSet, nil]
71
+ def read_only_subscriptions_set
72
+ @subscriptions_set&.dup
73
+ end
74
+
75
+ private
76
+
77
+ # Locks all Subscriptions behind the current SubscriptionsSet
78
+ # @return [void]
79
+ 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!)
86
+ end
87
+
88
+ # @return [PgEventstore::SubscriptionsSet]
89
+ 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)
92
+ end
93
+
94
+ # @return [PgEventstore::SubscriptionRunnersFeeder]
95
+ def feeder
96
+ SubscriptionRunnersFeeder.new(@config_name)
97
+ end
98
+
99
+ # @return [void]
100
+ def attach_runner_callbacks
101
+ @basic_runner.define_callback(:change_state, :after, method(:update_subscriptions_set_state))
102
+ @basic_runner.define_callback(:before_runner_started, :before, method(:before_runner_started))
103
+ @basic_runner.define_callback(:after_runner_died, :before, method(:after_runner_died))
104
+ @basic_runner.define_callback(:after_runner_died, :after, method(:restart_runner))
105
+ @basic_runner.define_callback(:process_async, :before, method(:process_async))
106
+ @basic_runner.define_callback(:after_runner_stopped, :before, method(:after_runner_stopped))
107
+ @basic_runner.define_callback(:before_runner_restored, :after, method(:update_runner_restarts))
108
+ end
109
+
110
+ # @return [void]
111
+ def before_runner_started
112
+ lock_all
113
+ @runners.each(&:start)
114
+ @commands_handler.start
115
+ end
116
+
117
+ # @param error [StandardError]
118
+ # @return [void]
119
+ def after_runner_died(error)
120
+ subscriptions_set.update(last_error: Utils.error_info(error), last_error_occurred_at: Time.now.utc)
121
+ end
122
+
123
+ # @param _error [StandardError]
124
+ # @return [void]
125
+ def restart_runner(_error)
126
+ return if subscriptions_set.restart_count >= subscriptions_set.max_restarts_number
127
+
128
+ Thread.new do
129
+ sleep subscriptions_set.time_between_restarts
130
+ restore
131
+ end
132
+ end
133
+
134
+ # @return [void]
135
+ def update_runner_restarts
136
+ subscriptions_set.update(last_restarted_at: Time.now.utc, restart_count: subscriptions_set.restart_count + 1)
137
+ end
138
+
139
+ # @return [void]
140
+ def process_async
141
+ feeder.feed(@runners)
142
+ end
143
+
144
+ # @return [void]
145
+ def after_runner_stopped
146
+ @commands_handler.stop
147
+ @subscriptions_set&.delete
148
+ @subscriptions_set = nil
149
+ @runners.each(&:stop_async).each(&:wait_for_finish)
150
+ unlock_all
151
+ end
152
+
153
+ # @return [void]
154
+ def update_subscriptions_set_state(state)
155
+ subscriptions_set.update(state: state)
156
+ end
157
+
158
+ # This method helps to ensure that no Subscription is added after SubscriptionFeeder's runner is working
159
+ # @return [void]
160
+ # @raise [RuntimeError]
161
+ def assert_proper_state!
162
+ return if @basic_runner.initial? || @basic_runner.stopped?
163
+
164
+ error_message = <<~TEXT
165
+ Could not add subscription - #{subscriptions_set.name}##{subscriptions_set.id} must be either in the initial \
166
+ or in the stopped state, but it is in the #{@basic_runner.state} state now.
167
+ TEXT
168
+ raise error_message
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+
5
+ module PgEventstore
6
+ # This class measures the performance of Subscription's handler and returns the average time required to process an
7
+ # event.
8
+ # @!visibility private
9
+ class SubscriptionHandlerPerformance
10
+ extend Forwardable
11
+
12
+ TIMINGS_TO_KEEP = 100
13
+
14
+ def_delegators :@lock, :synchronize
15
+
16
+ def initialize
17
+ @lock = Thread::Mutex.new
18
+ @timings = []
19
+ end
20
+
21
+ # Yields the given block to measure its execution time
22
+ # @return [Object] the result of yielded block
23
+ def track_exec_time
24
+ result = nil
25
+ time = Benchmark.realtime { result = yield }
26
+ synchronize do
27
+ @timings.shift if @timings.size == TIMINGS_TO_KEEP
28
+ @timings.push(time)
29
+ end
30
+ result
31
+ end
32
+
33
+ # The average time required to process an event.
34
+ # @return [Float]
35
+ def average_event_processing_time
36
+ synchronize { @timings.size.zero? ? 0 : @timings.sum / @timings.size }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module PgEventstore
6
+ # This class connects Subscription and EventsProcessor. Its public API is directed on locking/unlocking related
7
+ # Subscription, starting/stopping/restarting EventsProcessor and calculating options(starting position, number of
8
+ # events to fetch, etc) for the events pulling query.
9
+ # @!visibility private
10
+ class SubscriptionRunner
11
+ extend Forwardable
12
+
13
+ MAX_EVENTS_PER_CHUNK = 1_000
14
+ INITIAL_EVENTS_PER_CHUNK = 10
15
+
16
+ attr_reader :subscription
17
+
18
+ def_delegators :@events_processor, :start, :stop, :stop_async, :feed, :wait_for_finish, :restore, :state, :running?
19
+ def_delegators :@subscription, :lock!, :unlock!, :id
20
+
21
+ # @param stats [PgEventstore::SubscriptionHandlerPerformance]
22
+ # @param events_processor [PgEventstore::EventsProcessor]
23
+ # @param subscription [PgEventstore::Subscription]
24
+ # @param restart_terminator [#call, nil]
25
+ def initialize(stats:, events_processor:, subscription:, restart_terminator: nil)
26
+ @stats = stats
27
+ @events_processor = events_processor
28
+ @subscription = subscription
29
+ @restart_terminator = restart_terminator
30
+
31
+ attach_callbacks
32
+ end
33
+
34
+ # @return [Hash]
35
+ def next_chunk_query_opts
36
+ @subscription.options.merge(from_position: next_chunk_global_position, max_count: estimate_events_number)
37
+ end
38
+
39
+ # @return [Boolean]
40
+ def time_to_feed?
41
+ @subscription.last_chunk_fed_at + @subscription.chunk_query_interval <= Time.now.utc
42
+ end
43
+
44
+ private
45
+
46
+ # @return [Integer]
47
+ def next_chunk_global_position
48
+ (
49
+ @subscription.last_chunk_greatest_position || @subscription.current_position ||
50
+ @subscription.options[:from_position] || 0
51
+ ) + 1
52
+ end
53
+
54
+ # @return [Integer]
55
+ def estimate_events_number
56
+ return INITIAL_EVENTS_PER_CHUNK if @stats.average_event_processing_time.zero?
57
+
58
+ events_per_chunk = @subscription.chunk_query_interval / @stats.average_event_processing_time
59
+ [events_per_chunk, MAX_EVENTS_PER_CHUNK].min - @events_processor.events_left_in_chunk
60
+ end
61
+
62
+ # @return [void]
63
+ def attach_callbacks
64
+ @events_processor.define_callback(:process, :around, method(:track_exec_time))
65
+ @events_processor.define_callback(:process, :after, method(:update_subscription_stats))
66
+ @events_processor.define_callback(:error, :after, method(:update_subscription_error))
67
+ @events_processor.define_callback(:error, :after, method(:restart_subscription))
68
+ @events_processor.define_callback(:feed, :after, method(:update_subscription_chunk_stats))
69
+ @events_processor.define_callback(:restart, :after, method(:update_subscription_restarts))
70
+ @events_processor.define_callback(:change_state, :after, method(:update_subscription_state))
71
+ end
72
+
73
+ # @param action [Proc]
74
+ # @return [void]
75
+ def track_exec_time(action, *)
76
+ @stats.track_exec_time { action.call }
77
+ end
78
+
79
+ # @param current_position [Integer]
80
+ # @return [void]
81
+ def update_subscription_stats(current_position)
82
+ @subscription.update(
83
+ average_event_processing_time: @stats.average_event_processing_time,
84
+ current_position: current_position,
85
+ total_processed_events: @subscription.total_processed_events + 1
86
+ )
87
+ end
88
+
89
+ # @param state [String]
90
+ # @return [void]
91
+ def update_subscription_state(state)
92
+ @subscription.update(state: state)
93
+ end
94
+
95
+ # @return [void]
96
+ def update_subscription_restarts
97
+ @subscription.update(last_restarted_at: Time.now.utc, restart_count: @subscription.restart_count + 1)
98
+ end
99
+
100
+ # @param error [StandardError]
101
+ # @return [void]
102
+ def update_subscription_error(error)
103
+ @subscription.update(last_error: Utils.error_info(error), last_error_occurred_at: Time.now.utc)
104
+ end
105
+
106
+ # @param global_position [Integer, nil]
107
+ # @return [void]
108
+ def update_subscription_chunk_stats(global_position)
109
+ global_position ||= @subscription.last_chunk_greatest_position
110
+ @subscription.update(last_chunk_fed_at: Time.now.utc, last_chunk_greatest_position: global_position)
111
+ end
112
+
113
+ # @param _error [StandardError]
114
+ # @return [void]
115
+ def restart_subscription(_error)
116
+ return if @restart_terminator&.call(@subscription.dup)
117
+ return if @subscription.restart_count >= @subscription.max_restarts_number
118
+
119
+ Thread.new do
120
+ sleep @subscription.time_between_restarts
121
+ restore
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # This class pulls events from db and feeds given SubscriptionRunners
5
+ # @!visibility private
6
+ class SubscriptionRunnersFeeder
7
+ # @param config_name [Symbol]
8
+ def initialize(config_name)
9
+ @config_name = config_name
10
+ end
11
+
12
+ # @param runners [Array<PgEventstore::SubscriptionRunner>]
13
+ # @return [void]
14
+ def feed(runners)
15
+ runners = runners.select(&:running?).select(&:time_to_feed?)
16
+ return if runners.empty?
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'] }
21
+ runners.each do |runner|
22
+ runner.feed(grouped_events[runner.id]) if grouped_events[runner.id]
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # @return [PgEventstore::Connection]
29
+ def connection
30
+ PgEventstore.connection(@config_name)
31
+ end
32
+
33
+ # @return [PgEventstore::SubscriptionQueries]
34
+ def subscription_queries
35
+ SubscriptionQueries.new(connection)
36
+ end
37
+ end
38
+ end