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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +1 -0
- data/db/migrations/10_create_subscription_commands.sql +15 -0
- data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
- data/db/migrations/12_improve_events_indexes.sql +1 -0
- data/db/migrations/9_create_subscriptions.sql +46 -0
- data/docs/configuration.md +42 -21
- data/docs/subscriptions.md +170 -0
- data/lib/pg_eventstore/callbacks.rb +122 -0
- data/lib/pg_eventstore/client.rb +2 -2
- data/lib/pg_eventstore/config.rb +35 -3
- data/lib/pg_eventstore/errors.rb +63 -0
- data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +11 -14
- data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +25 -23
- data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
- data/lib/pg_eventstore/queries/event_queries.rb +5 -26
- data/lib/pg_eventstore/queries/event_type_queries.rb +13 -0
- data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
- data/lib/pg_eventstore/queries/subscription_queries.rb +160 -0
- data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
- data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
- data/lib/pg_eventstore/queries.rb +6 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +14 -2
- data/lib/pg_eventstore/sql_builder.rb +54 -10
- data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
- data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
- data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
- data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
- data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
- data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
- data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
- data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
- data/lib/pg_eventstore/tasks/setup.rake +1 -1
- data/lib/pg_eventstore/utils.rb +66 -0
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore.rb +19 -1
- 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
|