pg_eventstore 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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