pg_eventstore 1.5.0 → 1.7.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +49 -0
  4. data/db/migrations/7_support_reading_streams_system_stream.sql +2 -0
  5. data/docs/reading_events.md +16 -1
  6. data/docs/subscriptions.md +14 -56
  7. data/exe/pg-eventstore +16 -0
  8. data/lib/pg_eventstore/callbacks.rb +16 -0
  9. data/lib/pg_eventstore/cli/commands/base_command.rb +45 -0
  10. data/lib/pg_eventstore/cli/commands/callback_handlers/start_cmd_handlers.rb +38 -0
  11. data/lib/pg_eventstore/cli/commands/help_command.rb +22 -0
  12. data/lib/pg_eventstore/cli/commands/start_subscriptions_command.rb +96 -0
  13. data/lib/pg_eventstore/cli/commands/stop_subscriptions_command.rb +22 -0
  14. data/lib/pg_eventstore/cli/commands.rb +6 -0
  15. data/lib/pg_eventstore/cli/exit_codes.rb +12 -0
  16. data/lib/pg_eventstore/cli/parser_options/base_options.rb +46 -0
  17. data/lib/pg_eventstore/cli/parser_options/default_options.rb +10 -0
  18. data/lib/pg_eventstore/cli/parser_options/metadata.rb +34 -0
  19. data/lib/pg_eventstore/cli/parser_options/subscription_options.rb +31 -0
  20. data/lib/pg_eventstore/cli/parser_options.rb +6 -0
  21. data/lib/pg_eventstore/cli/parsers/base_parser.rb +33 -0
  22. data/lib/pg_eventstore/cli/parsers/default_parser.rb +24 -0
  23. data/lib/pg_eventstore/cli/parsers/subscription_parser.rb +24 -0
  24. data/lib/pg_eventstore/cli/parsers.rb +5 -0
  25. data/lib/pg_eventstore/cli/try_to_delete_subscriptions_set.rb +75 -0
  26. data/lib/pg_eventstore/cli/try_unlock_subscriptions_set.rb +16 -0
  27. data/lib/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rb +67 -0
  28. data/lib/pg_eventstore/cli.rb +42 -0
  29. data/lib/pg_eventstore/commands/read.rb +1 -1
  30. data/lib/pg_eventstore/extensions/options_extension.rb +53 -8
  31. data/lib/pg_eventstore/queries/event_queries.rb +1 -10
  32. data/lib/pg_eventstore/query_builders/events_filtering.rb +27 -0
  33. data/lib/pg_eventstore/rspec/has_option_matcher.rb +42 -41
  34. data/lib/pg_eventstore/stream.rb +8 -0
  35. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +13 -1
  36. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +1 -1
  37. data/lib/pg_eventstore/subscriptions/queries/subscription_command_queries.rb +1 -1
  38. data/lib/pg_eventstore/subscriptions/queries/subscriptions_set_command_queries.rb +3 -1
  39. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +10 -50
  40. data/lib/pg_eventstore/subscriptions/subscription_feeder_commands/ping.rb +22 -0
  41. data/lib/pg_eventstore/subscriptions/subscription_feeder_commands.rb +1 -0
  42. data/lib/pg_eventstore/subscriptions/subscriptions_lifecycle.rb +7 -2
  43. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +44 -10
  44. data/lib/pg_eventstore/subscriptions/subscriptions_set_lifecycle.rb +1 -1
  45. data/lib/pg_eventstore/utils.rb +32 -0
  46. data/lib/pg_eventstore/version.rb +1 -1
  47. data/lib/pg_eventstore/web/application.rb +12 -2
  48. data/lib/pg_eventstore/web/paginator/events_collection.rb +18 -5
  49. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +24 -2
  50. data/lib/pg_eventstore/web/views/home/dashboard.erb +9 -0
  51. data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +1 -1
  52. data/lib/pg_eventstore/web/views/home/partials/events.erb +9 -6
  53. data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +3 -3
  54. data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +15 -0
  55. data/lib/pg_eventstore/web/views/layouts/application.erb +3 -3
  56. data/lib/pg_eventstore/web/views/subscriptions/index.erb +6 -6
  57. data/lib/pg_eventstore.rb +5 -2
  58. data/sig/pg_eventstore/callbacks.rbs +4 -0
  59. data/sig/pg_eventstore/cli/commands/base_command.rbs +13 -0
  60. data/sig/pg_eventstore/cli/commands/callback_handlers/start_cmd_handlers.rbs +14 -0
  61. data/sig/pg_eventstore/cli/commands/help_command.rbs +13 -0
  62. data/sig/pg_eventstore/cli/commands/start_subscriptions_command.rbs +28 -0
  63. data/sig/pg_eventstore/cli/commands/stop_subscriptions_command.rbs +11 -0
  64. data/sig/pg_eventstore/cli/exit_codes.rbs +8 -0
  65. data/sig/pg_eventstore/cli/parser_options/base_options.rbs +15 -0
  66. data/sig/pg_eventstore/cli/parser_options/default_options.rbs +8 -0
  67. data/sig/pg_eventstore/cli/parser_options/metadata.rbs +11 -0
  68. data/sig/pg_eventstore/cli/parser_options/subscription_options.rbs +9 -0
  69. data/sig/pg_eventstore/cli/parsers/base_parser.rbs +18 -0
  70. data/sig/pg_eventstore/cli/parsers/default_parser.rbs +9 -0
  71. data/sig/pg_eventstore/cli/parsers/subscription_parser.rbs +9 -0
  72. data/sig/pg_eventstore/cli/try_to_delete_subscriptions_set.rbs +24 -0
  73. data/sig/pg_eventstore/cli/try_unlock_subscriptions_set.rbs +7 -0
  74. data/sig/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rbs +26 -0
  75. data/sig/pg_eventstore/cli.rbs +10 -0
  76. data/sig/pg_eventstore/extensions/options_extension.rbs +18 -1
  77. data/sig/pg_eventstore/queries/event_queries.rbs +0 -5
  78. data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +6 -0
  79. data/sig/pg_eventstore/stream.rbs +3 -0
  80. data/sig/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rbs +8 -0
  81. data/sig/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rbs +7 -1
  82. data/sig/pg_eventstore/subscriptions/queries/subscription_command_queries.rbs +1 -1
  83. data/sig/pg_eventstore/subscriptions/queries/subscriptions_set_command_queries.rbs +1 -1
  84. data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +10 -11
  85. data/sig/pg_eventstore/subscriptions/subscription_feeder_commands/ping.rbs +11 -0
  86. data/sig/pg_eventstore/subscriptions/subscriptions_lifecycle.rbs +1 -1
  87. data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +13 -1
  88. data/sig/pg_eventstore/utils.rbs +2 -0
  89. data/sig/pg_eventstore/web/application.rbs +27 -0
  90. data/sig/pg_eventstore/web/paginator/base_collection.rbs +0 -9
  91. data/sig/pg_eventstore/web/paginator/event_types_collection.rbs +9 -0
  92. data/sig/pg_eventstore/web/paginator/events_collection.rbs +3 -1
  93. data/sig/pg_eventstore.rbs +2 -8
  94. metadata +47 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec41b398e1f38d66e73967447ea4f12896ed2cf57eb0dbad8b71acf4257f9cd4
4
- data.tar.gz: ce571ef411adeeb18b7bae463cac339a053d96f1b36e3de0b1023b8551b03ac5
3
+ metadata.gz: ff514958069cbf4b819b2ad959eac80ca9f24445bca19690ed4cb3ff23989804
4
+ data.tar.gz: bd91be800c3739a43edf4c17c996156642f00f0d977446201c84784a1bcc6e74
5
5
  SHA512:
6
- metadata.gz: 9273a4efb9855d219b1a92b5d9081e3d0d62f9de7f21c48e634949d218ed6a8278d81367f794ee8b70efa64541d7a4442ccf09e7b4cc3bcbd39398f5c7980ec2
7
- data.tar.gz: ffb85f8c9063aa6e11b8f900e128b4b873e89db117b39e703de6603fc73c014811b63950ff0a0340b0316da11ed2562418f56f1faf42a04491fb0105d913fced
6
+ metadata.gz: 1d4946c58c314f0bfb04c0c20ca1308e4c8afe915adacee8722fcd703404b26dbc429bdda5bc5a44f94654cb366813065226b42c930620bf30fcd4a345f592b1
7
+ data.tar.gz: 89738f55a626cbcd4eab9d336b7f4b9ff2e3520fc30a7fc7d87dfac5e755cd6431392438d1522cec706188aa855942db4cfd6fa96bc83c5e0b67086d6e05cef8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.7.0]
4
+ - Implement reading from `"$streams"` system stream
5
+ - Disable Host authorization introduced in sinatra v4.1
6
+
7
+ ## [1.6.0]
8
+ - Introduce subscriptions CLI. Type `pg-eventstore subscriptions --help` to see available commands. The main purpose of it is to provide the single way to start/stop subscription processes. Check [Subscriptions](docs/subscriptions.md#creating-a-subscription) docs about the new way to start and keep running a subscriptions process.
9
+
3
10
  ## [1.5.0]
4
11
  - Add ability to toggle link events in the admin UI
5
12
  - Mark linked events in the admin UI with "link" icon
data/README.md CHANGED
@@ -49,6 +49,55 @@ Documentation chapters:
49
49
  - [How to make multiple commands atomic](docs/multiple_commands.md)
50
50
  - [Admin UI](docs/admin_ui.md)
51
51
 
52
+ ## CLI
53
+
54
+ The gem is shipped with its own CLI. Use `pg-eventstore --help` to find out its capabilities.
55
+
56
+ ## RSpec
57
+
58
+ ### Clean up test db
59
+
60
+ The gem provides a class to clean up your `pg_eventstore` test db between tests. Example usage(in your `spec/spec_helper.rb`:
61
+
62
+ ```ruby
63
+ require 'pg_eventstore/rspec/test_helpers'
64
+
65
+ RSpec.configure do |config|
66
+ config.before do
67
+ PgEventstore::TestHelpers.clean_up_db
68
+ end
69
+ end
70
+ ```
71
+
72
+ ### RSpec matcher for OptionsExtension
73
+
74
+ If you would like to be able to test the functional, provided by `PgEventstore::Extensions::OptionsExtension` extension - there is a rspec matcher. Load custom matcher in you `spec_helper.rb`:
75
+
76
+ ```ruby
77
+ require 'pg_eventstore/rspec/has_option_matcher'
78
+ ```
79
+
80
+ Let's say you have next class:
81
+ ```ruby
82
+ class SomeClass
83
+ include PgEventstore::Extensions::OptionsExtension
84
+
85
+ option(:some_opt, metadata: { foo: :bar }) { '1' }
86
+ end
87
+ ```
88
+
89
+ To test that its instance has the proper option with the proper default value and proper metadata you can use this matcher:
90
+ ```ruby
91
+ RSpec.describe SomeClass do
92
+ subject { described_class.new }
93
+
94
+ # Check that :some_opt is present
95
+ it { is_expected.to have_option(:some_opt) }
96
+ # Check that :some_opt is present and has the correct default value
97
+ it { is_expected.to have_option(:some_opt).with_default_value('1').with_metadata(foo: :bar) }
98
+ end
99
+ ```
100
+
52
101
  ## Development
53
102
 
54
103
  After checking out the repo, run:
@@ -0,0 +1,2 @@
1
+ CREATE INDEX idx_events_0_stream_revision_global_position ON events USING btree (stream_revision, global_position) WHERE stream_revision = 0;
2
+ CREATE VIEW "$streams" AS SELECT * FROM events WHERE stream_revision = 0;
@@ -53,7 +53,7 @@ In case a stream with given name does not exist, a `PgEventstore::StreamNotFound
53
53
  ```ruby
54
54
  begin
55
55
  stream = PgEventstore::Stream.new(context: 'non-existing-context', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
56
- PgEventstore.client.read(stream)
56
+ PgEventstore.client.read(stream, options: { max_count: 1 })
57
57
  rescue PgEventstore::StreamNotFoundError => e
58
58
  puts e.message # => Stream #<PgEventstore::Stream:0x01> does not exist.
59
59
  puts e.stream # => #<PgEventstore::Stream:0x01>
@@ -80,6 +80,15 @@ You can read from a specific position of the "all" stream. This is very similar
80
80
  PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { from_position: 9023, direction: 'Backwards' })
81
81
  ```
82
82
 
83
+ ## Reading from "$streams" system stream
84
+
85
+ `"$streams"` is a special stream which consists of events with `stream_revision == 0`. This allows you to effectively query all streams. Example:
86
+
87
+ ```ruby
88
+ stream = PgEventstore::Stream.system_stream("$streams")
89
+ PgEventstore.client.read(stream).map(&:stream) # => array of unique streams
90
+ ```
91
+
83
92
  ## Middlewares
84
93
 
85
94
  If you would like to skip some of your registered middlewares from processing events after they being read from a stream - you should use the `:middlewares` argument which allows you to override the list of middlewares you would like to use.
@@ -160,6 +169,12 @@ You can also mix filtering by stream's attributes and event types. The result wi
160
169
  PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { streams: [{ context: 'MyAwesomeContext' }], event_types: %w[Foo Bar] } })
161
170
  ```
162
171
 
172
+ ### "$streams" stream filtering
173
+
174
+ When reading from `"$streams"` same rules apply as when reading from "all" stream. For example, read all streams which have `context == "MyAwesomeContext"` and start from events with event type either `"Foo"` or `"Bar"`:
175
+ ```ruby
176
+ PgEventstore.client.read(PgEventstore::Stream.system_stream("$streams"), options: { filter: { streams: [{ context: 'MyAwesomeContext' }], event_types: %w[Foo Bar] } })
177
+ ```
163
178
 
164
179
  ## Pagination
165
180
 
@@ -29,7 +29,7 @@ Now we can use the `#subscribe` method to create the subscription:
29
29
  subscriptions_manager.subscribe('MyAwesomeSubscription', handler: proc { |event| puts event })
30
30
  ```
31
31
 
32
- First argument is the subscription's name. **It must be unique within the subscription set**. Second argument is your subscription's handler where you will be processing your events as they arrive. The example shows the minimum set of arguments required to create the subscription.
32
+ First argument is the subscription's name. **It must be unique within the subscriptions set**. Second argument is your subscription's handler where you will be processing your events as they arrive. The example shows the minimum set of arguments required to create the subscription.
33
33
 
34
34
  In the given state it will be listening to all events from all streams. You can define various filters by providing the `:filter` key of `options` argument:
35
35
 
@@ -50,44 +50,25 @@ subscriptions_manager.start
50
50
  # => PgEventstore::BasicRunner
51
51
  ```
52
52
 
53
- After calling `#start` all subscriptions are locked behind the given subscriptions set and can't be locked by any other subscriptions set. This measure is needed to prevent running the same subscription under the same subscription set using different processes/subscription managers. Such situation will lead to a malformed subscription state and will break its position, meaning the same event will be processed several times. In real world the lock attempt may still happen. This can be common scenario when using kubernetes which rolls out new deployment before shutting down old one. In this case `#start` will return `nil`. You can use this behavior to properly handle such cases. Example:
53
+ After calling `#start` all subscriptions are locked behind the given subscriptions set and can't be locked by any other subscriptions set. This measure is needed to prevent running the same subscription under the same subscription set using different processes/subscription managers. Such situation will lead to a malformed subscription state and will break its position, meaning the same event will be processed several times.
54
54
 
55
- ```ruby
56
- timeout = 20 # 20 seconds
57
- deadline = Time.now + timeout
58
- loop do
59
- break if subscriptions_manager.start
60
- if Time.now > deadline
61
- puts "Failed to acquire subscriptions lock within #{timeout} seconds. Exiting now."
62
- exit
63
- end
64
- sleep 2
65
- end
66
- ```
67
-
68
- To "unlock" the subscription you should gracefully stop the subscription manager:
69
-
70
- ```ruby
71
- subscriptions_manager.stop
72
- ```
73
-
74
- If you shut down the process which runs your subscriptions without calling the `#stop` method, subscriptions will remain locked, and the only way to unlock them will be to call the `#force_lock!` method before calling the `#start` method:
55
+ If, for some reason, you want to lock already locked subscription - you can provide `force_lock: true`:
75
56
 
76
57
  ```ruby
77
- subscriptions_manager.force_lock!
58
+ subscriptions_manager = PgEventstore.subscriptions_manager(subscription_set: 'SubscriptionsOfMyAwesomeMicroservice', force_lock: true)
78
59
  subscriptions_manager.start
79
60
  ```
80
61
 
81
62
  A complete example of the subscription setup process looks like this:
82
63
 
83
64
  ```ruby
84
- require 'pg_eventstore'
85
-
86
65
  PgEventstore.configure do |config|
87
66
  config.pg_uri = ENV.fetch('PG_EVENTSTORE_URI') { 'postgresql://postgres:postgres@localhost:5532/eventstore' }
88
67
  end
89
68
 
90
- subscriptions_manager = PgEventstore.subscriptions_manager(subscription_set: 'MyAwesomeSubscriptions')
69
+ subscriptions_manager = PgEventstore.subscriptions_manager(
70
+ subscription_set: 'MyAwesomeSubscriptions'
71
+ )
91
72
  subscriptions_manager.subscribe(
92
73
  'Foo events Subscription',
93
74
  handler: proc { |event| p "Foo events Subscription: #{event.inspect}" },
@@ -99,40 +80,17 @@ subscriptions_manager.subscribe(
99
80
  options: { filter: { streams: [{ context: 'BarCtx' }] }
100
81
  }
101
82
  )
102
- subscriptions_manager.force_lock! if ENV['FORCE_LOCK'] == 'true'
103
- timeout = 20 # 20 seconds
104
- deadline = Time.now + timeout
105
- loop do
106
- break if subscriptions_manager.start
107
- if Time.now > deadline
108
- puts "Failed to acquire subscriptions lock within #{timeout} seconds. Exiting now."
109
- exit
110
- end
111
- sleep 2
112
- end
83
+ subscriptions_manager.start
84
+ ```
113
85
 
114
- Kernel.trap('TERM') do
115
- puts "Received TERM signal. Stopping Subscriptions Manager and exiting..."
116
- # It is important to wrap subscriptions_manager.stop into another Thread, because it uses Thread::Mutex#synchronize
117
- # internally, but its usage is not allowed inside Kernel.trap block
118
- Thread.new { subscriptions_manager.stop }.join
119
- exit
120
- end
86
+ Persist this script into a file(let's say `subscriptions.rb`). Now it is time to start the process which will be processing those subscriptions. `pg_eventstore` has CLI for that purpose:
121
87
 
122
- loop do
123
- sleep 5
124
- subscriptions_manager.subscriptions.each do |subscription|
125
- puts <<~TEXT
126
- Subscription <<#{subscription.name.inspect}>> is at position #{subscription.current_position}. \
127
- Events processed: #{subscription.total_processed_events}
128
- TEXT
129
- end
130
- puts "Current SubscriptionsSet: #{subscriptions_manager.subscriptions_set}"
131
- puts ""
132
- end
88
+ ```bash
89
+ # -r ./subscriptions.rb will load our subscriptions definitions
90
+ pg-eventstore subscriptions start -r ./subscriptions.rb
133
91
  ```
134
92
 
135
- You can save this script in `subscriptions.rb`, run it with `bundle exec ruby subscriptions.rb`, open another ruby console and test posting different events:
93
+ After running that test subscriptions you can open another ruby console and test posting different events:
136
94
 
137
95
  ```ruby
138
96
  require 'pg_eventstore'
data/exe/pg-eventstore ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "pg_eventstore"
5
+ require "pg_eventstore/cli"
6
+ require "logger"
7
+
8
+ logger = Logger.new(STDOUT)
9
+ logger.level = :info
10
+ logger.progname = "pg_eventstore"
11
+ logger.formatter = proc do |severity, time, progname, msg|
12
+ "\e[36m#{progname} | \e[0m#{time.utc.strftime("%FT%TZ")} #{severity}: #{msg}\n"
13
+ end
14
+
15
+ PgEventstore.logger = logger
16
+ Kernel.exit(PgEventstore::CLI.execute(ARGV))
@@ -95,6 +95,22 @@ module PgEventstore
95
95
  result
96
96
  end
97
97
 
98
+ # @param action [Object]
99
+ # @param filter [Symbol]
100
+ # @param callback [#call]
101
+ # @return [void]
102
+ def remove_callback(action, filter, callback)
103
+ return unless @callbacks.dig(action, filter)
104
+
105
+ @callbacks[action][filter].delete(callback)
106
+ end
107
+
108
+ # Clear all defined callbacks
109
+ # @return [void]
110
+ def clear
111
+ @callbacks.clear
112
+ end
113
+
98
114
  private
99
115
 
100
116
  # @return [void]
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module Commands
6
+ class BaseCommand
7
+ module BaseCommandActions
8
+ # @return [Integer] exit code
9
+ def call
10
+ load_external_files
11
+ super
12
+ end
13
+
14
+ private
15
+
16
+ # @return [void]
17
+ def load_external_files
18
+ options.requires.each do |file_path|
19
+ require(file_path)
20
+ end
21
+ end
22
+ end
23
+
24
+ class << self
25
+ def inherited(klass)
26
+ super
27
+ klass.prepend BaseCommandActions
28
+ end
29
+ end
30
+
31
+ attr_reader :options
32
+
33
+ # @param options [PgEventstore::CLI::ParserOptions::BaseOptions]
34
+ def initialize(options)
35
+ @options = options
36
+ end
37
+
38
+ # @return [Integer] exit code
39
+ def call
40
+ raise NotImplementedError
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module Commands
6
+ module CallbackHandlers
7
+ class StartCmdHandlers
8
+ include Extensions::CallbackHandlersExtension
9
+
10
+ class << self
11
+ # @param subscription_managers [Set<PgEventstore::SubscriptionsManager>]
12
+ # @param manager [PgEventstore::SubscriptionsManager]
13
+ # @return [void]
14
+ def register_managers(subscription_managers, manager)
15
+ subscription_managers.add(manager)
16
+ end
17
+
18
+ # @param action [Proc]
19
+ # @param manager [PgEventstore::SubscriptionsManager]
20
+ # @return [void]
21
+ def handle_start_up(action, manager)
22
+ action.call
23
+ rescue SubscriptionAlreadyLockedError => error
24
+ PgEventstore.logger&.error(
25
+ <<~TEXT
26
+ Subscription #{error.name.inspect} from #{error.set.inspect} set is locked under \
27
+ SubscriptionsSet##{error.lock_id}. Trying to unlock...
28
+ TEXT
29
+ )
30
+ raise unless TryUnlockSubscriptionsSet.try_unlock(manager.config_name, error.lock_id)
31
+ retry
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module Commands
6
+ class HelpCommand
7
+ attr_reader :options
8
+
9
+ # @param options [PgEventstore::CLI::ParserOptions::BaseOptions]
10
+ def initialize(options)
11
+ @options = options
12
+ end
13
+
14
+ # @return [Integer] exit code
15
+ def call
16
+ puts options.help
17
+ ExitCodes::SUCCESS
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'callback_handlers/start_cmd_handlers'
4
+
5
+ module PgEventstore
6
+ module CLI
7
+ module Commands
8
+ class StartSubscriptionsCommand < BaseCommand
9
+ # @return [Integer] seconds
10
+ KEEP_ALIVE_INTERVAL = 2
11
+
12
+ def initialize(...)
13
+ super
14
+ @subscription_managers = Set.new
15
+ @running = false
16
+ attach_callbacks
17
+ end
18
+
19
+ # @return [Integer] exit code
20
+ def call
21
+ return ExitCodes::ERROR unless running_subscriptions?
22
+
23
+ @running = true
24
+ setup_killsig
25
+ persist_pid
26
+ keep_process_alive
27
+ ExitCodes::SUCCESS
28
+ rescue SubscriptionAlreadyLockedError => error
29
+ PgEventstore.logger&.error(
30
+ "SubscriptionsSet##{error.lock_id} is still there. Are you stopping it at all?"
31
+ )
32
+ ExitCodes::ERROR
33
+ end
34
+
35
+ private
36
+
37
+ # @return [Boolean]
38
+ def running_subscriptions?
39
+ return true if @subscription_managers.any?(&:running?)
40
+
41
+ PgEventstore.logger&.warn("No subscriptions start ups were detected. Existing...")
42
+ false
43
+ end
44
+
45
+ # @return [void]
46
+ def setup_killsig
47
+ Kernel.trap('TERM') do
48
+ Thread.new do
49
+ PgEventstore.logger&.info("Received TERM signal, stopping subscriptions and exiting...")
50
+ end.join
51
+ # Because the implementation uses Mutex - wrap it into Thread to bypass the limitations of Kernel#trap
52
+ @subscription_managers.map do |manager|
53
+ Thread.new do
54
+ # Initiate graceful shutdown
55
+ manager.stop
56
+ end
57
+ end.each(&:join)
58
+ Utils.remove_file(options.pid_path)
59
+ @running = false
60
+ end
61
+ end
62
+
63
+ # @return [void]
64
+ def persist_pid
65
+ Utils.write_to_file(options.pid_path, Process.pid.to_s)
66
+ end
67
+
68
+ # @return [void]
69
+ def keep_process_alive
70
+ PgEventstore.logger&.info("Startup is successful. Processing subscriptions...")
71
+ loop do
72
+ # SubscriptionsManager#subscriptions_set becomes nil when everything gets stopped.
73
+ if @subscription_managers.all? { |manager| manager.subscriptions_set.nil? }
74
+ PgEventstore.logger&.info("All subscriptions were gracefully shut down. Exiting now...")
75
+ break
76
+ end
77
+ break unless @running
78
+ sleep KEEP_ALIVE_INTERVAL
79
+ end
80
+ end
81
+
82
+ # @return [void]
83
+ def attach_callbacks
84
+ CLI.callbacks.define_callback(
85
+ :start_manager, :before,
86
+ CallbackHandlers::StartCmdHandlers.setup_handler(:register_managers, @subscription_managers)
87
+ )
88
+ CLI.callbacks.define_callback(
89
+ :start_manager, :around,
90
+ CallbackHandlers::StartCmdHandlers.setup_handler(:handle_start_up)
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module Commands
6
+ class StopSubscriptionsCommand < BaseCommand
7
+ # @return [Integer] exit code
8
+ def call
9
+ pid = Utils.read_pid(options.pid_path)&.to_i
10
+ if pid && pid > 0
11
+ PgEventstore.logger&.info("Stopping process #{pid}.")
12
+ Process.kill('TERM', pid)
13
+ return ExitCodes::SUCCESS
14
+ end
15
+
16
+ PgEventstore.logger&.error("Pid file #{options.pid_path.inspect} does not exist or empty.")
17
+ ExitCodes::ERROR
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'commands/base_command'
4
+ require_relative 'commands/help_command'
5
+ require_relative 'commands/start_subscriptions_command'
6
+ require_relative 'commands/stop_subscriptions_command'
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module ExitCodes
6
+ # @return [Integer]
7
+ SUCCESS = 0
8
+ # @return [Integer]
9
+ ERROR = 1
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module ParserOptions
6
+ class BaseOptions
7
+ include Extensions::OptionsExtension
8
+
9
+ option(:help, metadata: Metadata.new(short: '-h', long: '--help', description: 'Prints this help'))
10
+ option(
11
+ :requires,
12
+ metadata: Metadata.new(
13
+ short: '-rFILE_PATH',
14
+ long: '--require=FILE_PATH',
15
+ description: 'Ruby files to load. You can provide this option multiple times to load more files.'
16
+ )
17
+ ) do
18
+ []
19
+ end
20
+
21
+ # @param parser [OptionParser]
22
+ # @return [void]
23
+ def attach_parser_handlers(parser)
24
+ parser.on(*to_parser_opts(:help)) do
25
+ self.help = parser.to_s
26
+ end
27
+ parser.on(*to_parser_opts(:requires)) do |path|
28
+ requires.push(path)
29
+ end
30
+ end
31
+
32
+ # @param option [Symbol]
33
+ # @return [Array<String>]
34
+ def to_parser_opts(option)
35
+ option(option).metadata.to_parser_opts
36
+ end
37
+
38
+ # @param option [Symbol]
39
+ # @return [PgEventstore::Extensions::OptionsExtension::Option]
40
+ def option(option)
41
+ self.class.options[option]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module ParserOptions
6
+ class DefaultOptions < BaseOptions
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module ParserOptions
6
+ class Metadata
7
+ include Extensions::OptionsExtension
8
+
9
+ option(:short)
10
+ option(:long)
11
+ option(:description)
12
+
13
+ # @return [Array<String>]
14
+ def to_parser_opts
15
+ [short, long, description]
16
+ end
17
+
18
+ # @return [Integer]
19
+ def hash
20
+ to_parser_opts.hash
21
+ end
22
+
23
+ # @param another [Object]
24
+ # @return [Boolean]
25
+ def ==(another)
26
+ return false unless another.is_a?(Metadata)
27
+
28
+ to_parser_opts == another.to_parser_opts
29
+ end
30
+ alias eql? ==
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module CLI
5
+ module ParserOptions
6
+ class SubscriptionOptions < BaseOptions
7
+ option(
8
+ :pid_path,
9
+ metadata: Metadata.new(
10
+ short: '-pFILE_PATH',
11
+ long: '--pid=FILE_PATH',
12
+ description: 'Defines pid file path. Defaults to /tmp/pg-es_subscriptions.pid'
13
+ )
14
+ ) do
15
+ '/tmp/pg-es_subscriptions.pid'
16
+ end
17
+
18
+ # @param parser [OptionParser]
19
+ # @return [void]
20
+ def attach_parser_handlers(parser)
21
+ super
22
+ %i[pid_path].each do |option|
23
+ parser.on(*to_parser_opts(option)) do |value|
24
+ public_send("#{option}=", value)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'parser_options/metadata'
4
+ require_relative 'parser_options/base_options'
5
+ require_relative 'parser_options/default_options'
6
+ require_relative 'parser_options/subscription_options'