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,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
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require_relative 'basic_runner'
|
5
|
+
require_relative 'subscription'
|
6
|
+
require_relative 'events_processor'
|
7
|
+
require_relative 'subscription_handler_performance'
|
8
|
+
require_relative 'subscription_runner'
|
9
|
+
require_relative 'runner_state'
|
10
|
+
require_relative 'subscriptions_set'
|
11
|
+
require_relative 'subscription_runners_feeder'
|
12
|
+
require_relative 'subscription_feeder'
|
13
|
+
require_relative 'commands_handler'
|
14
|
+
|
15
|
+
module PgEventstore
|
16
|
+
# The public Subscriptions API, available to the user.
|
17
|
+
class SubscriptionsManager
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
attr_reader :config
|
21
|
+
private :config
|
22
|
+
|
23
|
+
def_delegators :@subscription_feeder, :start, :stop, :force_lock!
|
24
|
+
|
25
|
+
# @param config [PgEventstore::Config]
|
26
|
+
# @param set_name [String]
|
27
|
+
# @param max_retries [Integer, nil] max number of retries of failed SubscriptionsSet
|
28
|
+
# @param retries_interval [Integer, nil] a delay between retries of failed SubscriptionsSet
|
29
|
+
def initialize(config:, set_name:, max_retries: nil, retries_interval: nil)
|
30
|
+
@config = config
|
31
|
+
@set_name = set_name
|
32
|
+
@subscription_feeder = SubscriptionFeeder.new(
|
33
|
+
config_name: config.name,
|
34
|
+
set_name: set_name,
|
35
|
+
max_retries: max_retries || config.subscriptions_set_max_retries,
|
36
|
+
retries_interval: retries_interval || config.subscriptions_set_retries_interval
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param subscription_name [String] subscription's name
|
41
|
+
# @param handler [#call] subscription's handler
|
42
|
+
# @param options [Hash] request options
|
43
|
+
# @option options [Integer, Symbol] :from_position a starting subscription position
|
44
|
+
# @option options [Boolean] :resolve_link_tos When using projections to create new events you
|
45
|
+
# can set whether the generated events are pointers to existing events. Setting this option to true tells
|
46
|
+
# PgEventstore to return the original event instead a link event.
|
47
|
+
# @option options [Hash] :filter provide it to filter events. It works the same way as a :filter option of
|
48
|
+
# {PgEventstore::Client#read} method. Filtering by both - event types and streams are available.
|
49
|
+
# @param middlewares [Array<Symbol>, nil] provide a list of middleware names to override a config's middlewares
|
50
|
+
# @param pull_interval [Integer] an interval in seconds to determine how often to query new events of the given
|
51
|
+
# subscription.
|
52
|
+
# @param max_retries [Integer] max number of retries of failed Subscription
|
53
|
+
# @param retries_interval [Integer] a delay between retries of failed Subscription
|
54
|
+
# @param restart_terminator [#call, nil] a callable object which, when called - accepts PgEventstore::Subscription
|
55
|
+
# object to determine whether restarts should be stopped(true - stops restarts, false - continues restarts)
|
56
|
+
# @return [void]
|
57
|
+
def subscribe(subscription_name, handler:, options: {}, middlewares: nil,
|
58
|
+
pull_interval: config.subscription_pull_interval,
|
59
|
+
max_retries: config.subscription_max_retries,
|
60
|
+
retries_interval: config.subscription_retries_interval,
|
61
|
+
restart_terminator: config.subscription_restart_terminator)
|
62
|
+
subscription = Subscription.using_connection(config.name).new(
|
63
|
+
set: @set_name, name: subscription_name, options: options, chunk_query_interval: pull_interval,
|
64
|
+
max_restarts_number: max_retries, time_between_restarts: retries_interval
|
65
|
+
)
|
66
|
+
runner = SubscriptionRunner.new(
|
67
|
+
stats: SubscriptionHandlerPerformance.new,
|
68
|
+
events_processor: EventsProcessor.new(create_event_handler(middlewares, handler)),
|
69
|
+
subscription: subscription,
|
70
|
+
restart_terminator: restart_terminator
|
71
|
+
)
|
72
|
+
|
73
|
+
@subscription_feeder.add(runner)
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Array<PgEventstore::Subscription>]
|
78
|
+
def subscriptions
|
79
|
+
@subscription_feeder.read_only_subscriptions
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [PgEventstore::SubscriptionsSet, nil]
|
83
|
+
def subscriptions_set
|
84
|
+
@subscription_feeder.read_only_subscriptions_set
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# @param middlewares [Array<Symbol>, nil]
|
90
|
+
# @param handler [#call]
|
91
|
+
# @return [Proc]
|
92
|
+
def create_event_handler(middlewares, handler)
|
93
|
+
deserializer = EventDeserializer.new(select_middlewares(middlewares), config.event_class_resolver)
|
94
|
+
->(raw_event) { handler.call(deserializer.deserialize(raw_event)) }
|
95
|
+
end
|
96
|
+
|
97
|
+
# @param middlewares [Array, nil]
|
98
|
+
# @return [Array<Object<#serialize, #deserialize>>]
|
99
|
+
def select_middlewares(middlewares = nil)
|
100
|
+
return config.middlewares.values unless middlewares
|
101
|
+
|
102
|
+
config.middlewares.slice(*middlewares).values
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# Defines ruby's representation of subscriptions_set record.
|
5
|
+
class SubscriptionsSet
|
6
|
+
include Extensions::UsingConnectionExtension
|
7
|
+
include Extensions::OptionsExtension
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# @param attrs [Hash]
|
11
|
+
# @return [PgEventstore::SubscriptionsSet]
|
12
|
+
def create(attrs)
|
13
|
+
new(**subscriptions_set_queries.create(attrs))
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# @return [PgEventstore::SubscriptionsSetQueries]
|
19
|
+
def subscriptions_set_queries
|
20
|
+
SubscriptionsSetQueries.new(connection)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @!attribute id
|
25
|
+
# @return [String] UUIDv4. It is used to lock the Subscription by updating Subscription#locked_by attribute
|
26
|
+
attribute(:id)
|
27
|
+
# @!attribute name
|
28
|
+
# @return [String] name of the set
|
29
|
+
attribute(:name)
|
30
|
+
# @!attribute state
|
31
|
+
# @return [String]
|
32
|
+
attribute(:state)
|
33
|
+
# @!attribute restart_count
|
34
|
+
# @return [Integer] the number of SubscriptionsSet's restarts after its failure
|
35
|
+
attribute(:restart_count)
|
36
|
+
# @!attribute max_restarts_number
|
37
|
+
# @return [Integer] maximum number of times the SubscriptionsSet can be restarted
|
38
|
+
attribute(:max_restarts_number)
|
39
|
+
# @!attribute time_between_restarts
|
40
|
+
# @return [Integer] interval in seconds between retries of failed SubscriptionsSet
|
41
|
+
attribute(:time_between_restarts)
|
42
|
+
# @!attribute last_restarted_at
|
43
|
+
# @return [Time, nil] last time the SubscriptionsSet was restarted
|
44
|
+
attribute(:last_restarted_at)
|
45
|
+
# @!attribute last_error
|
46
|
+
# @return [Hash{'class' => String, 'message' => String, 'backtrace' => Array<String>}, nil] the information about
|
47
|
+
# last error caused when pulling Subscriptions events.
|
48
|
+
attribute(:last_error)
|
49
|
+
# @!attribute last_error_occurred_at
|
50
|
+
# @return [Time, nil] the time when the last error occurred
|
51
|
+
attribute(:last_error_occurred_at)
|
52
|
+
# @!attribute created_at
|
53
|
+
# @return [Time]
|
54
|
+
attribute(:created_at)
|
55
|
+
# @!attribute updated_at
|
56
|
+
# @return [Time]
|
57
|
+
attribute(:updated_at)
|
58
|
+
|
59
|
+
# @param attrs [Hash]
|
60
|
+
# @return [Hash]
|
61
|
+
def assign_attributes(attrs)
|
62
|
+
attrs.each do |attr, value|
|
63
|
+
public_send("#{attr}=", value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param attrs [Hash]
|
68
|
+
# @return [Hash]
|
69
|
+
def update(attrs)
|
70
|
+
assign_attributes(subscriptions_set_queries.update(id, attrs))
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [void]
|
74
|
+
def delete
|
75
|
+
subscriptions_set_queries.delete(id)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Dup the current object without assigned connection
|
79
|
+
# @return [PgEventstore::SubscriptionsSet]
|
80
|
+
def dup
|
81
|
+
SubscriptionsSet.new(**Utils.deep_dup(options_hash))
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [PgEventstore::SubscriptionsSet]
|
85
|
+
def reload
|
86
|
+
assign_attributes(subscriptions_set_queries.find!(id))
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# @return [PgEventstore::SubscriptionsSetQueries]
|
93
|
+
def subscriptions_set_queries
|
94
|
+
SubscriptionsSetQueries.new(self.class.connection)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -32,7 +32,7 @@ namespace :pg_eventstore do
|
|
32
32
|
conn.exec('SELECT number FROM migrations ORDER BY number DESC LIMIT 1').to_a.dig(0, 'number') || -1
|
33
33
|
|
34
34
|
Dir.chdir migration_files_root do
|
35
|
-
Dir["*.{sql,rb}"].
|
35
|
+
Dir["*.{sql,rb}"].sort_by { |f_name| f_name.split('_').first.to_i }.each do |f_name|
|
36
36
|
number = File.basename(f_name).split('_')[0].to_i
|
37
37
|
next if latest_migration >= number
|
38
38
|
|
@@ -59,6 +59,10 @@ namespace :pg_eventstore do
|
|
59
59
|
DROP TABLE IF EXISTS public.streams;
|
60
60
|
DROP TABLE IF EXISTS public.event_types;
|
61
61
|
DROP TABLE IF EXISTS public.migrations;
|
62
|
+
DROP TABLE IF EXISTS public.subscriptions_set;
|
63
|
+
DROP TABLE IF EXISTS public.subscriptions;
|
64
|
+
DROP TABLE IF EXISTS public.subscription_commands;
|
65
|
+
DROP TABLE IF EXISTS public.subscriptions_set_commands;
|
62
66
|
DROP EXTENSION IF EXISTS "uuid-ossp";
|
63
67
|
DROP EXTENSION IF EXISTS pgcrypto;
|
64
68
|
SQL
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
class Utils
|
5
|
+
class << self
|
6
|
+
# Deep transforms keys of a given Hash
|
7
|
+
# @param object [Object]
|
8
|
+
# @return [Object] a hash with transformed keys
|
9
|
+
def deep_transform_keys(object, &block)
|
10
|
+
case object
|
11
|
+
when Hash
|
12
|
+
object.each_with_object({}) do |(key, value), result|
|
13
|
+
result[yield(key)] = deep_transform_keys(value, &block)
|
14
|
+
end
|
15
|
+
when Array
|
16
|
+
object.map { |e| deep_transform_keys(e, &block) }
|
17
|
+
else
|
18
|
+
object
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Deep dup Array or Hash
|
23
|
+
# @param object [Object]
|
24
|
+
# @return [Object]
|
25
|
+
def deep_dup(object)
|
26
|
+
case object
|
27
|
+
when Hash
|
28
|
+
object.each_with_object({}) do |(key, value), result|
|
29
|
+
result[deep_dup(key)] = deep_dup(value)
|
30
|
+
end
|
31
|
+
when Array
|
32
|
+
object.map { |e| deep_dup(e) }
|
33
|
+
else
|
34
|
+
object.dup
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Converts array to the string containing SQL positional variables
|
39
|
+
# @param array [Array]
|
40
|
+
# @return [String] positional variables, based on array size. Example: "$1, $2, $3"
|
41
|
+
def positional_vars(array)
|
42
|
+
array.size.times.map { |t| "$#{t + 1}" }.join(', ')
|
43
|
+
end
|
44
|
+
|
45
|
+
# Transforms exception instance into a hash
|
46
|
+
# @param error [StandardError]
|
47
|
+
# @return [Hash]
|
48
|
+
def error_info(error)
|
49
|
+
{
|
50
|
+
class: error.class,
|
51
|
+
message: error.message,
|
52
|
+
backtrace: error.backtrace
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param str [String]
|
57
|
+
# @return [String]
|
58
|
+
def underscore_str(str)
|
59
|
+
str = str.dup
|
60
|
+
str[0] = str[0].downcase
|
61
|
+
str.gsub!(/[A-Z]/) { |letter| '_' + letter.downcase }
|
62
|
+
str
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|