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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +49 -0
- data/db/migrations/7_support_reading_streams_system_stream.sql +2 -0
- data/docs/reading_events.md +16 -1
- data/docs/subscriptions.md +14 -56
- data/exe/pg-eventstore +16 -0
- data/lib/pg_eventstore/callbacks.rb +16 -0
- data/lib/pg_eventstore/cli/commands/base_command.rb +45 -0
- data/lib/pg_eventstore/cli/commands/callback_handlers/start_cmd_handlers.rb +38 -0
- data/lib/pg_eventstore/cli/commands/help_command.rb +22 -0
- data/lib/pg_eventstore/cli/commands/start_subscriptions_command.rb +96 -0
- data/lib/pg_eventstore/cli/commands/stop_subscriptions_command.rb +22 -0
- data/lib/pg_eventstore/cli/commands.rb +6 -0
- data/lib/pg_eventstore/cli/exit_codes.rb +12 -0
- data/lib/pg_eventstore/cli/parser_options/base_options.rb +46 -0
- data/lib/pg_eventstore/cli/parser_options/default_options.rb +10 -0
- data/lib/pg_eventstore/cli/parser_options/metadata.rb +34 -0
- data/lib/pg_eventstore/cli/parser_options/subscription_options.rb +31 -0
- data/lib/pg_eventstore/cli/parser_options.rb +6 -0
- data/lib/pg_eventstore/cli/parsers/base_parser.rb +33 -0
- data/lib/pg_eventstore/cli/parsers/default_parser.rb +24 -0
- data/lib/pg_eventstore/cli/parsers/subscription_parser.rb +24 -0
- data/lib/pg_eventstore/cli/parsers.rb +5 -0
- data/lib/pg_eventstore/cli/try_to_delete_subscriptions_set.rb +75 -0
- data/lib/pg_eventstore/cli/try_unlock_subscriptions_set.rb +16 -0
- data/lib/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rb +67 -0
- data/lib/pg_eventstore/cli.rb +42 -0
- data/lib/pg_eventstore/commands/read.rb +1 -1
- data/lib/pg_eventstore/extensions/options_extension.rb +53 -8
- data/lib/pg_eventstore/queries/event_queries.rb +1 -10
- data/lib/pg_eventstore/query_builders/events_filtering.rb +27 -0
- data/lib/pg_eventstore/rspec/has_option_matcher.rb +42 -41
- data/lib/pg_eventstore/stream.rb +8 -0
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +13 -1
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +1 -1
- data/lib/pg_eventstore/subscriptions/queries/subscription_command_queries.rb +1 -1
- data/lib/pg_eventstore/subscriptions/queries/subscriptions_set_command_queries.rb +3 -1
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +10 -50
- data/lib/pg_eventstore/subscriptions/subscription_feeder_commands/ping.rb +22 -0
- data/lib/pg_eventstore/subscriptions/subscription_feeder_commands.rb +1 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_lifecycle.rb +7 -2
- data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +44 -10
- data/lib/pg_eventstore/subscriptions/subscriptions_set_lifecycle.rb +1 -1
- data/lib/pg_eventstore/utils.rb +32 -0
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +12 -2
- data/lib/pg_eventstore/web/paginator/events_collection.rb +18 -5
- data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +24 -2
- data/lib/pg_eventstore/web/views/home/dashboard.erb +9 -0
- data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +1 -1
- data/lib/pg_eventstore/web/views/home/partials/events.erb +9 -6
- data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +3 -3
- data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +15 -0
- data/lib/pg_eventstore/web/views/layouts/application.erb +3 -3
- data/lib/pg_eventstore/web/views/subscriptions/index.erb +6 -6
- data/lib/pg_eventstore.rb +5 -2
- data/sig/pg_eventstore/callbacks.rbs +4 -0
- data/sig/pg_eventstore/cli/commands/base_command.rbs +13 -0
- data/sig/pg_eventstore/cli/commands/callback_handlers/start_cmd_handlers.rbs +14 -0
- data/sig/pg_eventstore/cli/commands/help_command.rbs +13 -0
- data/sig/pg_eventstore/cli/commands/start_subscriptions_command.rbs +28 -0
- data/sig/pg_eventstore/cli/commands/stop_subscriptions_command.rbs +11 -0
- data/sig/pg_eventstore/cli/exit_codes.rbs +8 -0
- data/sig/pg_eventstore/cli/parser_options/base_options.rbs +15 -0
- data/sig/pg_eventstore/cli/parser_options/default_options.rbs +8 -0
- data/sig/pg_eventstore/cli/parser_options/metadata.rbs +11 -0
- data/sig/pg_eventstore/cli/parser_options/subscription_options.rbs +9 -0
- data/sig/pg_eventstore/cli/parsers/base_parser.rbs +18 -0
- data/sig/pg_eventstore/cli/parsers/default_parser.rbs +9 -0
- data/sig/pg_eventstore/cli/parsers/subscription_parser.rbs +9 -0
- data/sig/pg_eventstore/cli/try_to_delete_subscriptions_set.rbs +24 -0
- data/sig/pg_eventstore/cli/try_unlock_subscriptions_set.rbs +7 -0
- data/sig/pg_eventstore/cli/wait_for_subscriptions_set_shutdown.rbs +26 -0
- data/sig/pg_eventstore/cli.rbs +10 -0
- data/sig/pg_eventstore/extensions/options_extension.rbs +18 -1
- data/sig/pg_eventstore/queries/event_queries.rbs +0 -5
- data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +6 -0
- data/sig/pg_eventstore/stream.rbs +3 -0
- data/sig/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rbs +8 -0
- data/sig/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rbs +7 -1
- data/sig/pg_eventstore/subscriptions/queries/subscription_command_queries.rbs +1 -1
- data/sig/pg_eventstore/subscriptions/queries/subscriptions_set_command_queries.rbs +1 -1
- data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +10 -11
- data/sig/pg_eventstore/subscriptions/subscription_feeder_commands/ping.rbs +11 -0
- data/sig/pg_eventstore/subscriptions/subscriptions_lifecycle.rbs +1 -1
- data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +13 -1
- data/sig/pg_eventstore/utils.rbs +2 -0
- data/sig/pg_eventstore/web/application.rbs +27 -0
- data/sig/pg_eventstore/web/paginator/base_collection.rbs +0 -9
- data/sig/pg_eventstore/web/paginator/event_types_collection.rbs +9 -0
- data/sig/pg_eventstore/web/paginator/events_collection.rbs +3 -1
- data/sig/pg_eventstore.rbs +2 -8
- metadata +47 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff514958069cbf4b819b2ad959eac80ca9f24445bca19690ed4cb3ff23989804
|
4
|
+
data.tar.gz: bd91be800c3739a43edf4c17c996156642f00f0d977446201c84784a1bcc6e74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
data/docs/reading_events.md
CHANGED
@@ -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
|
|
data/docs/subscriptions.md
CHANGED
@@ -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
|
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.
|
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
|
-
|
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(
|
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.
|
103
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
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,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,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
|