pg_eventstore 0.3.0 → 0.5.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 +14 -0
- data/CODE_OF_CONDUCT.md +1 -1
- data/README.md +2 -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/13_remove_duplicated_index.sql +1 -0
- data/db/migrations/9_create_subscriptions.sql +46 -0
- data/docs/configuration.md +42 -21
- data/docs/linking_events.md +96 -0
- data/docs/reading_events.md +56 -0
- data/docs/subscriptions.md +170 -0
- data/lib/pg_eventstore/callbacks.rb +122 -0
- data/lib/pg_eventstore/client.rb +32 -2
- data/lib/pg_eventstore/commands/append.rb +3 -11
- data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +22 -0
- data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +24 -0
- data/lib/pg_eventstore/commands/link_to.rb +33 -0
- data/lib/pg_eventstore/commands/regular_stream_read_paginated.rb +63 -0
- data/lib/pg_eventstore/commands/system_stream_read_paginated.rb +62 -0
- data/lib/pg_eventstore/commands.rb +5 -0
- data/lib/pg_eventstore/config.rb +35 -3
- data/lib/pg_eventstore/errors.rb +80 -0
- data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +10 -22
- data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +69 -29
- data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
- data/lib/pg_eventstore/pg_connection.rb +20 -3
- data/lib/pg_eventstore/queries/event_queries.rb +18 -34
- data/lib/pg_eventstore/queries/event_type_queries.rb +24 -0
- data/lib/pg_eventstore/queries/preloader.rb +37 -0
- data/lib/pg_eventstore/queries/stream_queries.rb +14 -1
- data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
- data/lib/pg_eventstore/queries/subscription_queries.rb +166 -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 +7 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +17 -22
- data/lib/pg_eventstore/sql_builder.rb +54 -10
- data/lib/pg_eventstore/stream.rb +2 -1
- 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 +5 -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 +38 -4
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# Implements simple background jobs runner. The job execution is done via declaring a callback on the specific
|
5
|
+
# action. The implementation also allows you to hook into different places of life cycle of the runner by defining
|
6
|
+
# callbacks on various actions. Here is the list of available actions:
|
7
|
+
# - :before_runner_started. Happens before the runner's state switches from "initial"/"stopped" to "running" and
|
8
|
+
# runner's thread is started. It is also fired when the runner is restoring - right after :before_runner_restored
|
9
|
+
# action.
|
10
|
+
# - :after_runner_stopped. Happens after runner's state got switched from "running"/"dead" to "stopped" and runner's
|
11
|
+
# thread is terminated.
|
12
|
+
# - :before_runner_restored. Happens before runner's state gets switched from "dead" to "running" and runner's
|
13
|
+
# thread is started.
|
14
|
+
# - :process_async. Happens each @run_interval seconds within runner's thread.
|
15
|
+
# - :after_runner_died. Happens when runner's state switches to "dead" because of exception inside runner's thread.
|
16
|
+
# Callback function must be able to accept one argument - the exception which caused the runner to die will be
|
17
|
+
# passed.
|
18
|
+
# - :change_state. It happens each time the runner changes the state. Callback function must be able to accept one
|
19
|
+
# argument - current state will be passed.
|
20
|
+
#
|
21
|
+
# Example of BasicRunner usage:
|
22
|
+
# class MyAwesomeRunner
|
23
|
+
# extend Forwardable
|
24
|
+
#
|
25
|
+
# def_delegators :@basic_runner, :start, :stop, :wait_for_finish, :stop_async, :restore
|
26
|
+
#
|
27
|
+
# def initialize
|
28
|
+
# @basic_runner = PgEventstore::BasicRunner.new(1, 2)
|
29
|
+
# @jobs_performed = 0
|
30
|
+
# attach_runner_callbacks
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# private
|
34
|
+
#
|
35
|
+
# def attach_runner_callbacks
|
36
|
+
# @basic_runner.define_callback(:change_state, :after, method(:state_changed))
|
37
|
+
# @basic_runner.define_callback(:process_async, :before, method(:process_action))
|
38
|
+
# @basic_runner.define_callback(:process_async, :after, method(:count_jobs))
|
39
|
+
# @basic_runner.define_callback(:before_runner_started, :before, method(:before_runner_started))
|
40
|
+
# @basic_runner.define_callback(:after_runner_stopped, :before, method(:after_runner_stopped))
|
41
|
+
# @basic_runner.define_callback(:after_runner_died, :before, method(:after_runner_died))
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# def process_action
|
45
|
+
# raise "What's the point? I can not handle this any more!" if @jobs_performed >= 3
|
46
|
+
# puts "Doing some heavy lifting job"
|
47
|
+
# sleep 2 # Simulate long running job
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# def count_jobs
|
51
|
+
# @jobs_performed += 1
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# # @param state [String]
|
55
|
+
# def state_changed(state)
|
56
|
+
# puts "New state is #{state.inspect}"
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# def before_runner_started
|
60
|
+
# puts "Doing some preparations..."
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# def after_runner_stopped
|
64
|
+
# puts "You job is not processing any more. Total jobs performed: #{@jobs_performed}. Bye-bye!"
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# def after_runner_died(error)
|
68
|
+
# puts "Error occurred: #{error.inspect}"
|
69
|
+
# end
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# runner = MyAwesomeRunner.new
|
73
|
+
# runner.start # to start your background runner to process the job, defined by #process_action method
|
74
|
+
# runner.stop # to stop the runner
|
75
|
+
#
|
76
|
+
# See {PgEventstore::RunnerState} for the list of available states
|
77
|
+
# See {PgEventstore::CallbacksExtension} and {PgEventstore::Callbacks} for more info about how to use callbacks
|
78
|
+
class BasicRunner
|
79
|
+
extend Forwardable
|
80
|
+
include Extensions::CallbacksExtension
|
81
|
+
|
82
|
+
def_delegators :@state, :initial?, :running?, :halting?, :stopped?, :dead?
|
83
|
+
|
84
|
+
# @param run_interval [Integer, Float] seconds. Determines how often to run async task. Async task is determined by
|
85
|
+
# :after_runner_stopped callback
|
86
|
+
# @param async_shutdown_time [Integer, Float] seconds. Determines how long to wait for the async shutdown to wait
|
87
|
+
# for the runner to finish.
|
88
|
+
def initialize(run_interval, async_shutdown_time)
|
89
|
+
@run_interval = run_interval
|
90
|
+
@async_shutdown_time = async_shutdown_time
|
91
|
+
@state = RunnerState.new
|
92
|
+
@mutex = Thread::Mutex.new
|
93
|
+
delegate_change_state_cbx
|
94
|
+
end
|
95
|
+
|
96
|
+
# Start asynchronous runner. If the runner is dead - please use #restore to restart it.
|
97
|
+
# @return [self]
|
98
|
+
def start
|
99
|
+
synchronize do
|
100
|
+
return self unless @state.initial? || @state.stopped?
|
101
|
+
|
102
|
+
callbacks.run_callbacks(:before_runner_started)
|
103
|
+
_start
|
104
|
+
end
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
# Stop asynchronous runner. This operation is immediate and it won't be waiting for current job to finish - it will
|
109
|
+
# instantly halt it. If you care about the result of your async job - use #stop_async instead.
|
110
|
+
# @return [self]
|
111
|
+
def stop
|
112
|
+
synchronize do
|
113
|
+
return self unless @state.running? || @state.dead?
|
114
|
+
|
115
|
+
@runner&.exit
|
116
|
+
@runner = nil
|
117
|
+
@state.stopped!
|
118
|
+
callbacks.run_callbacks(:after_runner_stopped)
|
119
|
+
end
|
120
|
+
self
|
121
|
+
end
|
122
|
+
|
123
|
+
# Asynchronously stop asynchronous runner. This operation spawns another thread to gracefully stop the runner. It
|
124
|
+
# will wait up to @async_shutdown_time seconds before force-stopping the runner.
|
125
|
+
# @return [self]
|
126
|
+
def stop_async
|
127
|
+
synchronize do
|
128
|
+
return self unless @state.running? || @state.dead?
|
129
|
+
|
130
|
+
@state.halting!
|
131
|
+
Thread.new do
|
132
|
+
stopping_at = Time.now.utc
|
133
|
+
halt = false
|
134
|
+
loop do
|
135
|
+
synchronize do
|
136
|
+
# Give the runner up to @async_shutdown_time seconds for graceful shutdown
|
137
|
+
@runner&.exit if Time.now.utc - stopping_at > @async_shutdown_time
|
138
|
+
|
139
|
+
unless @runner&.alive?
|
140
|
+
@state.stopped!
|
141
|
+
@runner = nil
|
142
|
+
callbacks.run_callbacks(:after_runner_stopped)
|
143
|
+
halt = true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
break if halt
|
147
|
+
sleep 0.1
|
148
|
+
end
|
149
|
+
end
|
150
|
+
self
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Restores the runner after its death.
|
155
|
+
# @return [self]
|
156
|
+
def restore
|
157
|
+
synchronize do
|
158
|
+
return self unless @state.dead?
|
159
|
+
|
160
|
+
callbacks.run_callbacks(:before_runner_restored)
|
161
|
+
_start
|
162
|
+
end
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
# Wait until the runner switches the state to either "stopped" or "dead". This operation is synchronous.
|
167
|
+
# @return [self]
|
168
|
+
def wait_for_finish
|
169
|
+
loop do
|
170
|
+
continue = synchronize do
|
171
|
+
@state.halting? || @state.running?
|
172
|
+
end
|
173
|
+
break unless continue
|
174
|
+
|
175
|
+
sleep 0.1
|
176
|
+
end
|
177
|
+
self
|
178
|
+
end
|
179
|
+
|
180
|
+
# @return [String]
|
181
|
+
def state
|
182
|
+
@state.to_s
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def synchronize
|
188
|
+
@mutex.synchronize { yield }
|
189
|
+
end
|
190
|
+
|
191
|
+
# @return [void]
|
192
|
+
def _start
|
193
|
+
@state.running!
|
194
|
+
@runner = Thread.new do
|
195
|
+
loop do
|
196
|
+
Thread.current.exit unless @state.running?
|
197
|
+
sleep @run_interval
|
198
|
+
|
199
|
+
callbacks.run_callbacks(:process_async)
|
200
|
+
end
|
201
|
+
rescue => error
|
202
|
+
synchronize do
|
203
|
+
raise unless @state.halting? || @state.running?
|
204
|
+
|
205
|
+
@state.dead!
|
206
|
+
callbacks.run_callbacks(:after_runner_died, error)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Delegates :change_state action to the runner
|
212
|
+
def delegate_change_state_cbx
|
213
|
+
@state.define_callback(:change_state, :before, method(:change_state))
|
214
|
+
end
|
215
|
+
|
216
|
+
def change_state(...)
|
217
|
+
callbacks.run_callbacks(:change_state, ...)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module CommandHandlers
|
5
|
+
class SubscriptionFeederCommands
|
6
|
+
AVAILABLE_COMMANDS = %w[StopAll StartAll].freeze
|
7
|
+
|
8
|
+
# @param config_name [Symbol]
|
9
|
+
# @param subscription_feeder [PgEventstore::SubscriptionFeeder]
|
10
|
+
def initialize(config_name, subscription_feeder)
|
11
|
+
@config_name = config_name
|
12
|
+
@subscription_feeder = subscription_feeder
|
13
|
+
end
|
14
|
+
|
15
|
+
# Look up commands for the given SubscriptionFeeder and execute them
|
16
|
+
# @return [void]
|
17
|
+
def process
|
18
|
+
queries.find_commands(@subscription_feeder.id).each do |command|
|
19
|
+
unless AVAILABLE_COMMANDS.include?(command[:name])
|
20
|
+
PgEventstore.logger&.warn(
|
21
|
+
"#{self.class.name}: Don't know how to handle #{command[:name].inspect}. Details: #{command.inspect}."
|
22
|
+
)
|
23
|
+
next
|
24
|
+
end
|
25
|
+
send(Utils.underscore_str(command[:name]))
|
26
|
+
ensure
|
27
|
+
queries.delete(command[:id])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# @return [PgEventstore::SubscriptionsSetCommandQueries]
|
34
|
+
def queries
|
35
|
+
SubscriptionsSetCommandQueries.new(connection)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [PgEventstore::Connection]
|
39
|
+
def connection
|
40
|
+
PgEventstore.connection(@config_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def stop_all
|
44
|
+
@subscription_feeder.stop_all
|
45
|
+
end
|
46
|
+
|
47
|
+
def start_all
|
48
|
+
@subscription_feeder.start_all
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module CommandHandlers
|
5
|
+
class SubscriptionRunnersCommands
|
6
|
+
AVAILABLE_COMMANDS = %w[StopRunner RestoreRunner StartRunner].freeze
|
7
|
+
|
8
|
+
# @param config_name [Symbol]
|
9
|
+
# @param runners [Array<PgEventstore::SubscriptionRunner>]
|
10
|
+
def initialize(config_name, runners)
|
11
|
+
@config_name = config_name
|
12
|
+
@runners = runners
|
13
|
+
end
|
14
|
+
|
15
|
+
# Look up commands for all given SubscriptionRunner-s and execute them
|
16
|
+
# @return [void]
|
17
|
+
def process
|
18
|
+
queries.find_commands(@runners.map(&:id)).each do |command|
|
19
|
+
unless AVAILABLE_COMMANDS.include?(command[:name])
|
20
|
+
PgEventstore.logger&.warn(
|
21
|
+
"#{self.class.name}: Don't know how to handle #{command[:name].inspect}. Details: #{command.inspect}."
|
22
|
+
)
|
23
|
+
next
|
24
|
+
end
|
25
|
+
send(Utils.underscore_str(command[:name]), command[:subscription_id])
|
26
|
+
ensure
|
27
|
+
queries.delete(command[:id])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# @return [PgEventstore::SubscriptionCommandQueries]
|
34
|
+
def queries
|
35
|
+
SubscriptionCommandQueries.new(connection)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [PgEventstore::Connection]
|
39
|
+
def connection
|
40
|
+
PgEventstore.connection(@config_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param subscription_id [Integer]
|
44
|
+
# @return [PgEventstore::SubscriptionRunner, nil]
|
45
|
+
def find_subscription_runner(subscription_id)
|
46
|
+
@runners.find { |runner| runner.id == subscription_id }
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param subscription_id [Integer]
|
50
|
+
# @return [void]
|
51
|
+
def start_runner(subscription_id)
|
52
|
+
find_subscription_runner(subscription_id)&.start
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param subscription_id [Integer]
|
56
|
+
# @return [void]
|
57
|
+
def restore_runner(subscription_id)
|
58
|
+
find_subscription_runner(subscription_id)&.restore
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param subscription_id [Integer]
|
62
|
+
# @return [void]
|
63
|
+
def stop_runner(subscription_id)
|
64
|
+
find_subscription_runner(subscription_id)&.stop_async
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'command_handlers/subscription_feeder_commands'
|
4
|
+
require_relative 'command_handlers/subscription_runners_commands'
|
5
|
+
|
6
|
+
module PgEventstore
|
7
|
+
# This class implements the runner which processes remote commands in the background. This allows you to remotely
|
8
|
+
# control such actions as stop, start and restart of your Subscriptions.
|
9
|
+
# @!visibility private
|
10
|
+
class CommandsHandler
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
RESTART_DELAY = 5 # seconds
|
14
|
+
|
15
|
+
def_delegators :@basic_runner, :start, :stop, :state, :stop_async, :wait_for_finish
|
16
|
+
|
17
|
+
# @param config_name [Symbol]
|
18
|
+
# @param subscription_feeder [PgEventstore::SUbscriptionFeeder]
|
19
|
+
# @param runners [Array<PgEventstore::SubscriptionRunner>]
|
20
|
+
def initialize(config_name, subscription_feeder, runners)
|
21
|
+
@config_name = config_name
|
22
|
+
@subscription_feeder = subscription_feeder
|
23
|
+
@runners = runners
|
24
|
+
@basic_runner = BasicRunner.new(1, 0)
|
25
|
+
attach_runner_callbacks
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def attach_runner_callbacks
|
31
|
+
@basic_runner.define_callback(:process_async, :before, method(:process_async))
|
32
|
+
@basic_runner.define_callback(:after_runner_died, :before, method(:after_runner_died))
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_async
|
36
|
+
subscription_feeder_commands.process
|
37
|
+
subscription_runners_commands.process
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param error [StandardError]
|
41
|
+
# @return [void]
|
42
|
+
def after_runner_died(error)
|
43
|
+
PgEventstore.logger&.error "#{self.class.name}: Error occurred: #{error.message}"
|
44
|
+
PgEventstore.logger&.error "#{self.class.name}: Backtrace: #{error.backtrace&.join("\n")}"
|
45
|
+
PgEventstore.logger&.error "#{self.class.name}: Trying to auto-repair in #{RESTART_DELAY} seconds..."
|
46
|
+
Thread.new do
|
47
|
+
sleep RESTART_DELAY
|
48
|
+
@basic_runner.restore
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [PgEventstore::CommandHandlers::SubscriptionFeederCommands]
|
53
|
+
def subscription_feeder_commands
|
54
|
+
CommandHandlers::SubscriptionFeederCommands.new(@config_name, @subscription_feeder)
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [PgEventstore::CommandHandlers::SubscriptionRunnersCommands]
|
58
|
+
def subscription_runners_commands
|
59
|
+
CommandHandlers::SubscriptionRunnersCommands.new(@config_name, @runners)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -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
|