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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/CODE_OF_CONDUCT.md +1 -1
  4. data/README.md +2 -0
  5. data/db/migrations/10_create_subscription_commands.sql +15 -0
  6. data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
  7. data/db/migrations/12_improve_events_indexes.sql +1 -0
  8. data/db/migrations/13_remove_duplicated_index.sql +1 -0
  9. data/db/migrations/9_create_subscriptions.sql +46 -0
  10. data/docs/configuration.md +42 -21
  11. data/docs/linking_events.md +96 -0
  12. data/docs/reading_events.md +56 -0
  13. data/docs/subscriptions.md +170 -0
  14. data/lib/pg_eventstore/callbacks.rb +122 -0
  15. data/lib/pg_eventstore/client.rb +32 -2
  16. data/lib/pg_eventstore/commands/append.rb +3 -11
  17. data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +22 -0
  18. data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +24 -0
  19. data/lib/pg_eventstore/commands/link_to.rb +33 -0
  20. data/lib/pg_eventstore/commands/regular_stream_read_paginated.rb +63 -0
  21. data/lib/pg_eventstore/commands/system_stream_read_paginated.rb +62 -0
  22. data/lib/pg_eventstore/commands.rb +5 -0
  23. data/lib/pg_eventstore/config.rb +35 -3
  24. data/lib/pg_eventstore/errors.rb +80 -0
  25. data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +10 -22
  26. data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
  27. data/lib/pg_eventstore/extensions/options_extension.rb +69 -29
  28. data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
  29. data/lib/pg_eventstore/pg_connection.rb +20 -3
  30. data/lib/pg_eventstore/queries/event_queries.rb +18 -34
  31. data/lib/pg_eventstore/queries/event_type_queries.rb +24 -0
  32. data/lib/pg_eventstore/queries/preloader.rb +37 -0
  33. data/lib/pg_eventstore/queries/stream_queries.rb +14 -1
  34. data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
  35. data/lib/pg_eventstore/queries/subscription_queries.rb +166 -0
  36. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
  37. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
  38. data/lib/pg_eventstore/queries.rb +7 -0
  39. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +17 -22
  40. data/lib/pg_eventstore/sql_builder.rb +54 -10
  41. data/lib/pg_eventstore/stream.rb +2 -1
  42. data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
  43. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
  44. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
  45. data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
  46. data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
  47. data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
  48. data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
  49. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
  50. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
  51. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
  52. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
  53. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
  54. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
  55. data/lib/pg_eventstore/tasks/setup.rake +5 -1
  56. data/lib/pg_eventstore/utils.rb +66 -0
  57. data/lib/pg_eventstore/version.rb +1 -1
  58. data/lib/pg_eventstore.rb +19 -1
  59. 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}"].sort.each do |f_name|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEventstore
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end