pg_eventstore 0.3.0 → 0.4.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 +4 -0
- data/README.md +1 -0
- data/db/migrations/10_create_subscription_commands.sql +15 -0
- data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
- data/db/migrations/12_improve_events_indexes.sql +1 -0
- data/db/migrations/9_create_subscriptions.sql +46 -0
- data/docs/configuration.md +42 -21
- data/docs/subscriptions.md +170 -0
- data/lib/pg_eventstore/callbacks.rb +122 -0
- data/lib/pg_eventstore/client.rb +2 -2
- data/lib/pg_eventstore/config.rb +35 -3
- data/lib/pg_eventstore/errors.rb +63 -0
- data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +11 -14
- data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +25 -23
- data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
- data/lib/pg_eventstore/queries/event_queries.rb +5 -26
- data/lib/pg_eventstore/queries/event_type_queries.rb +13 -0
- data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
- data/lib/pg_eventstore/queries/subscription_queries.rb +160 -0
- data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
- data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
- data/lib/pg_eventstore/queries.rb +6 -0
- data/lib/pg_eventstore/query_builders/events_filtering_query.rb +14 -2
- data/lib/pg_eventstore/sql_builder.rb +54 -10
- data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
- data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
- data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
- data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
- data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
- data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
- data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
- data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
- data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
- data/lib/pg_eventstore/tasks/setup.rake +1 -1
- data/lib/pg_eventstore/utils.rb +66 -0
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore.rb +19 -1
- metadata +30 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 256d7be42cb993955eb3c387b27fa63bf9992852da2375c785f181b873fe7568
|
4
|
+
data.tar.gz: 4101d8f0706f402a8e97bce4864c2ab2c61a587845e1051826d7d26417c199bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 548512c6c8ec7161380848b1975313222173dd41db13d45c0216948bf769eb513d8593dc0e22263c4cc59c9210a7c90fab21f7a64b26a38217253422457582b4
|
7
|
+
data.tar.gz: 777aecbd9e7ad84882fbf0c20847fc79e8495addac77a5887db3dface85b7dec7aa3679e91849272109f6f6745cdbfd143628d8dc53d78b73559ba9fad31722f
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -53,6 +53,7 @@ Documentation chapters:
|
|
53
53
|
- [Events and streams definitions](docs/events_and_streams.md)
|
54
54
|
- [Appending events](docs/appending_events.md)
|
55
55
|
- [Reading events](docs/reading_events.md)
|
56
|
+
- [Subscriptions](docs/subscriptions.md)
|
56
57
|
- [Writing middlewares](docs/writing_middleware.md)
|
57
58
|
- [How to make multiple commands atomic](docs/multiple_commands.md)
|
58
59
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
CREATE TABLE public.subscription_commands
|
2
|
+
(
|
3
|
+
id bigserial NOT NULL,
|
4
|
+
name character varying NOT NULL,
|
5
|
+
subscription_id bigint NOT NULL,
|
6
|
+
created_at timestamp without time zone NOT NULL DEFAULT now()
|
7
|
+
);
|
8
|
+
|
9
|
+
ALTER TABLE ONLY public.subscription_commands
|
10
|
+
ADD CONSTRAINT subscription_commands_pkey PRIMARY KEY (id);
|
11
|
+
|
12
|
+
CREATE UNIQUE INDEX idx_subscription_commands_subscription_id_and_name ON public.subscription_commands USING btree (subscription_id, name);
|
13
|
+
|
14
|
+
ALTER TABLE ONLY public.subscription_commands
|
15
|
+
ADD CONSTRAINT subscription_commands_subscription_fk FOREIGN KEY (subscription_id) REFERENCES public.subscriptions (id) ON DELETE CASCADE;
|
@@ -0,0 +1,15 @@
|
|
1
|
+
CREATE TABLE public.subscriptions_set_commands
|
2
|
+
(
|
3
|
+
id bigserial NOT NULL,
|
4
|
+
name character varying NOT NULL,
|
5
|
+
subscriptions_set_id uuid NOT NULL,
|
6
|
+
created_at timestamp without time zone NOT NULL DEFAULT now()
|
7
|
+
);
|
8
|
+
|
9
|
+
ALTER TABLE ONLY public.subscriptions_set_commands
|
10
|
+
ADD CONSTRAINT subscriptions_set_commands_pkey PRIMARY KEY (id);
|
11
|
+
|
12
|
+
CREATE UNIQUE INDEX idx_subscr_set_commands_subscriptions_set_id_and_name ON public.subscriptions_set_commands USING btree (subscriptions_set_id, name);
|
13
|
+
|
14
|
+
ALTER TABLE ONLY public.subscriptions_set_commands
|
15
|
+
ADD CONSTRAINT subscriptions_set_commands_subscriptions_set_fk FOREIGN KEY (subscriptions_set_id) REFERENCES public.subscriptions_set (id) ON DELETE CASCADE;
|
@@ -0,0 +1 @@
|
|
1
|
+
CREATE INDEX idx_events_event_type_id_and_global_position ON public.events USING btree (event_type_id, global_position);
|
@@ -0,0 +1,46 @@
|
|
1
|
+
CREATE TABLE public.subscriptions_set
|
2
|
+
(
|
3
|
+
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
|
4
|
+
name character varying NOT NULL,
|
5
|
+
state character varying NOT NULL DEFAULT 'initial',
|
6
|
+
restart_count integer NOT NULL DEFAULT 0,
|
7
|
+
max_restarts_number int2 NOT NULL DEFAULT 10,
|
8
|
+
time_between_restarts int2 NOT NULL DEFAULT 1,
|
9
|
+
last_restarted_at timestamp without time zone,
|
10
|
+
last_error jsonb,
|
11
|
+
last_error_occurred_at timestamp without time zone,
|
12
|
+
created_at timestamp without time zone NOT NULL DEFAULT now(),
|
13
|
+
updated_at timestamp without time zone NOT NULL DEFAULT now()
|
14
|
+
);
|
15
|
+
|
16
|
+
ALTER TABLE ONLY public.subscriptions_set
|
17
|
+
ADD CONSTRAINT subscriptions_set_pkey PRIMARY KEY (id);
|
18
|
+
|
19
|
+
CREATE TABLE public.subscriptions
|
20
|
+
(
|
21
|
+
id bigserial NOT NULL,
|
22
|
+
set character varying NOT NULL,
|
23
|
+
name character varying NOT NULL,
|
24
|
+
options jsonb NOT NULL DEFAULT '{}'::jsonb,
|
25
|
+
total_processed_events bigint NOT NULL DEFAULT 0,
|
26
|
+
current_position bigint,
|
27
|
+
average_event_processing_time float4,
|
28
|
+
state character varying NOT NULL DEFAULT 'initial',
|
29
|
+
restart_count integer NOT NULL DEFAULT 0,
|
30
|
+
max_restarts_number int2 NOT NULL DEFAULT 100,
|
31
|
+
time_between_restarts int2 NOT NULL DEFAULT 1,
|
32
|
+
last_restarted_at timestamp without time zone,
|
33
|
+
last_error jsonb,
|
34
|
+
last_error_occurred_at timestamp without time zone,
|
35
|
+
chunk_query_interval int2 NOT NULL DEFAULT 5,
|
36
|
+
last_chunk_fed_at timestamp without time zone NOT NULL DEFAULT to_timestamp(0),
|
37
|
+
last_chunk_greatest_position bigint,
|
38
|
+
locked_by uuid,
|
39
|
+
created_at timestamp without time zone NOT NULL DEFAULT now(),
|
40
|
+
updated_at timestamp without time zone NOT NULL DEFAULT now()
|
41
|
+
);
|
42
|
+
|
43
|
+
ALTER TABLE ONLY public.subscriptions
|
44
|
+
ADD CONSTRAINT subscriptions_pkey PRIMARY KEY (id);
|
45
|
+
|
46
|
+
CREATE UNIQUE INDEX idx_subscriptions_set_and_name ON public.subscriptions USING btree (set, name);
|
data/docs/configuration.md
CHANGED
@@ -2,21 +2,23 @@
|
|
2
2
|
|
3
3
|
Configuration options:
|
4
4
|
|
5
|
-
| name
|
6
|
-
|
7
|
-
| pg_uri
|
8
|
-
| max_count
|
9
|
-
| middlewares
|
10
|
-
| event_class_resolver
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
| name | value | default value | description |
|
6
|
+
|------------------------------------|---------|--------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
7
|
+
| pg_uri | String | `'postgresql://postgres:postgres@localhost:5432/eventstore'` | PostgreSQL connection string. See PostgreSQL [docs](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS) for more information. |
|
8
|
+
| max_count | Integer | `1000` | Number of events to return in one response when reading from a stream. |
|
9
|
+
| middlewares | Array | `{}` | A hash where a key is a name of your middleware and value is an object that respond to `#serialize` and `#deserialize` methods. See [**Writing middleware**](writing_middleware.md) chapter. |
|
10
|
+
| event_class_resolver | `#call` | `PgEventstore::EventClassResolver.new` | A `#call`-able object that accepts a string and returns an event's class. See **Resolving events classes** chapter bellow for more info. |
|
11
|
+
| connection_pool_size | Integer | `5` | Max number of connections per ruby process. It must equal the number of threads of your application. When using subscriptions it is recommended to set it to the number of subscriptions divided by two or greater. See [**Picking max connections number**](#picking-max-connections-number) chapter of this section. |
|
12
|
+
| connection_pool_timeout | Integer | `5` | Time in seconds to wait for a connection in the pool to be released. If no connections are available during this time - `ConnectionPool::TimeoutError` will be raised. See `connection_pool` gem [docs](https://github.com/mperham/connection_pool#usage) for more info. |
|
13
|
+
| subscription_pull_interval | Integer | `2` | How often to pull new subscription events in seconds. |
|
14
|
+
| subscription_max_retries | Integer | `5` | Max number of retries of failed subscription. |
|
15
|
+
| subscription_retries_interval | Integer | `1` | Interval in seconds between retries of failed subscriptions. |
|
16
|
+
| subscriptions_set_max_retries | Integer | `10` | Max number of retries for failed subscription sets. |
|
17
|
+
| subscriptions_set_retries_interval | Integer | `1` | interval in seconds between retries of failed subscription sets. |
|
14
18
|
|
15
19
|
## Multiple configurations
|
16
20
|
|
17
|
-
`pg_eventstore` allows you to have as many configs as you want. This allows you, for example, to have different
|
18
|
-
databases, or to have a different set of middlewares for specific cases only. To do so, you have to name your
|
19
|
-
configuration, and later provide that name to `PgEventstore` client.
|
21
|
+
`pg_eventstore` allows you to have as many configs as you want. This allows you, for example, to have different databases, or to have a different set of middlewares for specific cases only. To do so, you have to name your configuration, and later provide that name to `PgEventstore` client.
|
20
22
|
|
21
23
|
Setup your configs:
|
22
24
|
|
@@ -42,8 +44,7 @@ PgEventstore.client(:pg_db_2).read(PgEventstore::Stream.all_stream)
|
|
42
44
|
|
43
45
|
### Default config
|
44
46
|
|
45
|
-
If you have one config only - you don't have to bother naming it or passing a config name to the client when performing
|
46
|
-
any operations. You can configure it as usual.
|
47
|
+
If you have one config only - you don't have to bother naming it or passing a config name to the client when performing any operations. You can configure it as usual.
|
47
48
|
|
48
49
|
Setup your default config:
|
49
50
|
|
@@ -64,19 +65,39 @@ EventStoreClient.client.read(PgEventstore::Stream.all_stream)
|
|
64
65
|
|
65
66
|
## Resolving event classes
|
66
67
|
|
67
|
-
During the deserialization process `pg_eventstore` tries to pick the correct class for an event. By default it does it
|
68
|
-
using the `PgEventstore::EventClassResolver` class. All it does is `Object.const_get(event_type)`. By default, if you
|
69
|
-
don't provide the `type` attribute for an event explicitly, it will grab the event's class name, meaning by default:
|
68
|
+
During the deserialization process `pg_eventstore` tries to pick the correct class for an event. By default it does it using the `PgEventstore::EventClassResolver` class. All it does is `Object.const_get(event_type)`. By default, if you don't provide the `type` attribute for an event explicitly, it will grab the event's class name, meaning by default:
|
70
69
|
|
71
70
|
- event's type is event's class name
|
72
|
-
- when instantiating an event - `pg_eventstore` tries to lookup an event class based on the value of event's `type`
|
73
|
-
attribute with a fallback to `PgEventstore::Event` class
|
71
|
+
- when instantiating an event - `pg_eventstore` tries to lookup an event class based on the value of event's `type` attribute with a fallback to `PgEventstore::Event` class
|
74
72
|
|
75
|
-
You can override the default event class resolver by providing any `#call`-able object. It should accept event's type
|
76
|
-
and return event's class based on it. Example:
|
73
|
+
You can override the default event class resolver by providing any `#call`-able object. It should accept event's type and return event's class based on it. Example:
|
77
74
|
|
78
75
|
```ruby
|
79
76
|
PgEventstore.configure do |config|
|
80
77
|
config.event_class_resolver = proc { |event_type| Object.const_get(event_type.gsub('Foo', 'Bar')) rescue PgEventstore::Event }
|
81
78
|
end
|
82
79
|
```
|
80
|
+
|
81
|
+
## Picking max connections number
|
82
|
+
|
83
|
+
A connection is hold from the connection pool to perform the request and it is released back to the connection pool once the request is finished. If you run into the (theoretical) edge case, when all your application's threads (or subscriptions) are performing `pg_eventstore` queries at the same time and all those queries take more than `connection_pool_timeout` seconds to complete, you have to have `connection_pool_size` set to the exact amount of your application's threads (or to the number of subscriptions when using subscriptions) to prevent timeout errors. Practically this is not the case, as all `pg_eventstore` queries are pretty fast. So, a good value for the `connection_pool_size` option is **half the number ** of your application's threads(or half the number of Subscriptions).
|
84
|
+
|
85
|
+
### Exception scenario
|
86
|
+
|
87
|
+
If you are using the [`#multiple`](multiple_commands.md) method - you have to take into account the execution time of the whole block you pass in it. This is because the connection will be released only after the block's execution is finished. So, for example, if you perform several commands within the block, as well as some API request, the connection will be release only after all those steps:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
PgEventstore.client.multiple do
|
91
|
+
# Connection is hold from the connection pool
|
92
|
+
PgEventstore.client.read(some_stream)
|
93
|
+
Stripe::Payment.create(some_attrs)
|
94
|
+
PgEventstore.client.append_to_stream(some_stream, some_event)
|
95
|
+
# Connection is released
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
Taking this into account you may want to increase `connection_pool_size` up to the number of your application's threads(or subscriptions).
|
100
|
+
|
101
|
+
### Usage of external connection pooler
|
102
|
+
|
103
|
+
`pg_eventstore` does not use any session-specific features of PostgreSQL. You can use any PostgreSQL connection pooler you like, such as [PgBouncer](https://www.pgbouncer.org/) for example.
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# Subscriptions
|
2
|
+
|
3
|
+
In order to process new events in your microservices you have to have the ability to listen for them. `pg_eventstore` implements a subscription feature for this matter. It is implemented as a background thread that pulls new events according to your filters from time to time (see `subscription_pull_interval` setting option under [**Configuration**](configuration.md) chapter).
|
4
|
+
|
5
|
+
## PgEventstore::Subscription
|
6
|
+
|
7
|
+
`pg_eventstore` stores various subscription information in the database. The corresponding object that describes the database records is the `PgEventstore::Subscription` object. It is used in the `config.subscription_restart_terminator` setting for example. You can find its attributes summary [here](https://rubydoc.info/gems/pg_eventstore/PgEventstore/Subscription).
|
8
|
+
|
9
|
+
## PgEventstore::SubscriptionsSet
|
10
|
+
|
11
|
+
`pg_eventstore` also stores information about which subscriptions are set. The corresponding object that describes the database records is `PgEventstore::SubscriptionsSet`. You can find its attributes summary [here](https://rubydoc.info/gems/pg_eventstore/PgEventstore/SubscriptionsSet).
|
12
|
+
|
13
|
+
This record is created when you start your subscriptions. All subscriptions created using a single subscriptions manager instance are locked using a single `PgEventstore::SubscriptionsSet`. When subscriptions are locked, they can't be managed anywhere else. When you stop your subscriptions, the `PgEventstore::SubscriptionsSet` is deleted, unlocking the subscriptions. The `SubscriptionSet` also holds information about the state, number of restarts, the restart interval and last error of the background runner which is responsible for pulling the subscription's events. You can set the max number of restarts and the restarts interval of your subscriptions set via `config.subscriptions_set_max_retries` and `config.subscriptions_set_retries_interval` settings. See [**Configuration**](configuration.md) chapter for more info.
|
14
|
+
|
15
|
+
## Creating a subscription
|
16
|
+
|
17
|
+
First step you need to do is to create a `PgEventstore::SubscriptionsManager` object and provide the `subscription_set` keyword argument. Optionally you can provide a config name to use, override the `config.subscriptions_set_max_retries` and `config.subscriptions_set_retries_interval` settings:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
subscriptions_manager = PgEventstore.subscriptions_manager(subscription_set: 'SubscriptionsOfMyAwesomeMicroservice')
|
21
|
+
another_subscriptions_manager = PgEventstore.subscriptions_manager(:my_custom_config, subscription_set: 'SubscriptionsOfMyAwesomeMicroservice', max_retries: 5, retries_interval: 2)
|
22
|
+
```
|
23
|
+
|
24
|
+
The required `subscription_set` option groups your subscriptions into a set. For example, you could refer to your service's name in the subscription set name.
|
25
|
+
|
26
|
+
Now we can use the `#subscribe` method to create the subscription:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
subscriptions_manager.subscribe('MyAwesomeSubscription', handler: proc { |event| puts event })
|
30
|
+
```
|
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.
|
33
|
+
|
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
|
+
|
36
|
+
```ruby
|
37
|
+
subscriptions_manager.subscribe(
|
38
|
+
'MyAwesomeSubscription',
|
39
|
+
handler: proc { |event| puts event },
|
40
|
+
options: { filter: { streams: [{ context: 'MyAwesomeContext' }], event_types: ['Foo', 'Bar'] } }
|
41
|
+
)
|
42
|
+
```
|
43
|
+
|
44
|
+
`:filter` supports the same options as the `#read` method supports when reading from the `"all"` stream. See [*"all" stream filtering*](reading_events.md#all-stream-filtering) section of **Reading events** chapter.
|
45
|
+
|
46
|
+
After you added all necessary subscriptions, it is time to start them:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
subscriptions_manager.start
|
50
|
+
```
|
51
|
+
|
52
|
+
After calling `#start` all subscriptions are locked behind the given subscription set and can't be locked by any other subscription 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
|
+
|
54
|
+
To "unlock" the subscription you should gracefully stop the subscription manager:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
subscriptions_manager.stop
|
58
|
+
```
|
59
|
+
|
60
|
+
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:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
subscriptions_manager.force_lock!
|
64
|
+
subscriptions_manager.start
|
65
|
+
```
|
66
|
+
|
67
|
+
A complete example of the subscription setup process looks like this:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
require 'pg_eventstore'
|
71
|
+
|
72
|
+
PgEventstore.configure do |config|
|
73
|
+
config.pg_uri = ENV.fetch('PG_EVENTSTORE_URI') { 'postgresql://postgres:postgres@localhost:5532/eventstore' }
|
74
|
+
end
|
75
|
+
|
76
|
+
subscriptions_manager = PgEventstore.subscriptions_manager(subscription_set: 'MyAwesomeSubscriptions')
|
77
|
+
subscriptions_manager.subscribe(
|
78
|
+
'Foo events Subscription',
|
79
|
+
handler: proc { |event| p "Foo events Subscription: #{event.inspect}" },
|
80
|
+
options: { filter: { event_types: ['Foo'] } }
|
81
|
+
)
|
82
|
+
subscriptions_manager.subscribe(
|
83
|
+
'"BarCtx" context Subscription',
|
84
|
+
handler: proc { |event| p "'BarCtx' context Subscription: #{event.inspect}" },
|
85
|
+
options: { filter: { streams: [{ context: 'BarCtx' }] }
|
86
|
+
}
|
87
|
+
)
|
88
|
+
subscriptions_manager.force_lock! if ENV['FORCE_LOCK'] == 'true'
|
89
|
+
subscriptions_manager.start
|
90
|
+
|
91
|
+
Kernel.trap('TERM') do
|
92
|
+
puts "Received TERM signal. Stopping Subscriptions Manager and exiting..."
|
93
|
+
# It is important to wrap subscriptions_manager.stop into another Thread, because it uses Thread::Mutex#synchronize
|
94
|
+
# internally, but its usage is not allowed inside Kernel.trap block
|
95
|
+
Thread.new { subscriptions_manager.stop }.join
|
96
|
+
exit
|
97
|
+
end
|
98
|
+
|
99
|
+
loop do
|
100
|
+
sleep 5
|
101
|
+
subscriptions_manager.subscriptions.each do |subscription|
|
102
|
+
puts <<~TEXT
|
103
|
+
Subscription <<#{subscription.name.inspect}>> is at position #{subscription.current_position}. \
|
104
|
+
Events processed: #{subscription.total_processed_events}
|
105
|
+
TEXT
|
106
|
+
end
|
107
|
+
puts "Current SubscriptionsSet: #{subscriptions_manager.subscriptions_set}"
|
108
|
+
puts ""
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
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:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
require 'pg_eventstore'
|
116
|
+
|
117
|
+
PgEventstore.configure do |config|
|
118
|
+
config.pg_uri = ENV.fetch('PG_EVENTSTORE_URI') { 'postgresql://postgres:postgres@localhost:5532/eventstore' }
|
119
|
+
end
|
120
|
+
|
121
|
+
foo_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
|
122
|
+
bar_stream = PgEventstore::Stream.new(context: 'BarCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
|
123
|
+
PgEventstore.client.append_to_stream(foo_stream, PgEventstore::Event.new(type: 'Foo', data: { foo: :bar }))
|
124
|
+
PgEventstore.client.append_to_stream(bar_stream, PgEventstore::Event.new(type: 'Foo', data: { foo: :bar }))
|
125
|
+
```
|
126
|
+
|
127
|
+
You will then see the output of your subscription handlers. To gracefully stop the subscriptions process, use `kill -TERM <pid>` command.
|
128
|
+
|
129
|
+
## Overriding Subscription config values
|
130
|
+
|
131
|
+
You can override `subscription_pull_interval`, `subscription_max_retries`, `subscription_retries_interval` and `subscription_restart_terminator` config values (see [**Configuration**](configuration.md) chapter for details) for the specific subscription by providing the corresponding arguments. Example:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
subscriptions_manager.subscribe(
|
135
|
+
'MyAwesomeSubscription',
|
136
|
+
handler: proc { |event| puts event },
|
137
|
+
# overrides config.subscription_pull_interval
|
138
|
+
pull_interval: 1,
|
139
|
+
# overrides config.subscription_max_retries
|
140
|
+
max_retries: 10,
|
141
|
+
# overrides config.subscription_retries_interval
|
142
|
+
retries_interval: 2,
|
143
|
+
# overrides config.subscription_restart_terminator
|
144
|
+
restart_terminator: proc { |subscription| subscription.last_error['class'] == 'NoMethodError' },
|
145
|
+
)
|
146
|
+
```
|
147
|
+
|
148
|
+
## Middlewares
|
149
|
+
|
150
|
+
If you would like to skip some of your registered middlewares from processing events after they are being pulled by the subscription, you should use the `:middlewares` argument which allows you to override the list of middlewares you would like to use.
|
151
|
+
|
152
|
+
Let's say you have these registered middlewares:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
PgEventstore.configure do |config|
|
156
|
+
config.middlewares = { foo: FooMiddleware.new, bar: BarMiddleware.new, baz: BazMiddleware.new }
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
And you want to skip `FooMiddleware` and `BazMiddleware`. You simply have to provide an array of corresponding middleware keys you would like to use when creating the subscription:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
subscriptions_manager.subscribe('MyAwesomeSubscription', handler: proc { |event| puts event }, middlewares: %i[bar])
|
164
|
+
```
|
165
|
+
|
166
|
+
See the [Writing middleware](writing_middleware.md) chapter for info about what is middleware and how to implement it.
|
167
|
+
|
168
|
+
## How many subscriptions I should put in one process?
|
169
|
+
|
170
|
+
It depends on the nature of your subscription handlers. If they spend more time on ruby code execution than on IO operations, you should limit the number of subscriptions per single process. This can be especially noticed when you rebuild the read models of your microservice, processing all events from the start.
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
# Allows you to define before, around and after callbacks withing the certain action. It is especially useful during
|
5
|
+
# asynchronous programming when you need to be able to react on asynchronous actions from the outside.
|
6
|
+
# Example:
|
7
|
+
# class MyAwesomeClass
|
8
|
+
# attr_reader :callbacks
|
9
|
+
#
|
10
|
+
# def initialize
|
11
|
+
# @callbacks = PgEventstore::Callbacks.new
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# def do_something
|
15
|
+
# Thread.new do
|
16
|
+
# @callbacks.run_callbacks(:something_happened) do
|
17
|
+
# puts "I did something useful!"
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def do_something_else
|
23
|
+
# @callbacks.run_callbacks(:something_else_happened, :foo, bar: :baz) do
|
24
|
+
# puts "Something else happened!"
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# obj = MyAwesomeClass.new
|
30
|
+
# obj.callbacks.define_callback(:something_happened, :before, proc { puts "In before callback" })
|
31
|
+
# obj.callbacks.define_callback(:something_happened, :after, proc { puts "In after callback" })
|
32
|
+
# obj.callbacks.define_callback(
|
33
|
+
# :something_happened, :around,
|
34
|
+
# proc { |action| puts "In around before action"; action.call; puts "In around after action" }
|
35
|
+
# )
|
36
|
+
# obj.do_something
|
37
|
+
# Outputs:
|
38
|
+
# In before callback
|
39
|
+
# In around callback before action
|
40
|
+
# I did something useful!
|
41
|
+
# In around callback after action
|
42
|
+
# In after callback
|
43
|
+
# Please note, that it is important to call *action.call* in around callback. Otherwise action simply won't be called.
|
44
|
+
#
|
45
|
+
# Optionally you can provide any set of arguments to {#run_callbacks} method. They will be passed to your callbacks
|
46
|
+
# functions then. Example:
|
47
|
+
#
|
48
|
+
# obj = MyAwesomeClass.new
|
49
|
+
# obj.callbacks.define_callback(
|
50
|
+
# :something_else_happened, :before,
|
51
|
+
# proc { |*args, **kwargs| puts "In before callback. Args: #{args}, kwargs: #{kwargs}." }
|
52
|
+
# )
|
53
|
+
# obj.callbacks.define_callback(
|
54
|
+
# :something_else_happened, :after,
|
55
|
+
# proc { |*args, **kwargs| puts "In after callback. Args: #{args}, kwargs: #{kwargs}." }
|
56
|
+
# )
|
57
|
+
# obj.callbacks.define_callback(
|
58
|
+
# :something_else_happened, :around,
|
59
|
+
# proc { |action, *args, **kwargs|
|
60
|
+
# puts "In around before action. Args: #{args}, kwargs: #{kwargs}."
|
61
|
+
# action.call
|
62
|
+
# puts "In around after action. Args: #{args}, kwargs: #{kwargs}."
|
63
|
+
# }
|
64
|
+
# )
|
65
|
+
# obj.do_something_else
|
66
|
+
# Outputs:
|
67
|
+
# In before callback. Args: [:foo], kwargs: {:bar=>:baz}.
|
68
|
+
# In around before action. Args: [:foo], kwargs: {:bar=>:baz}.
|
69
|
+
# I did something useful!
|
70
|
+
# In around after action. Args: [:foo], kwargs: {:bar=>:baz}.
|
71
|
+
# In after callback. Args: [:foo], kwargs: {:bar=>:baz}.
|
72
|
+
class Callbacks
|
73
|
+
def initialize
|
74
|
+
@callbacks = {}
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param action [Object] an object, that represents your action. In most cases you want to use a symbol there
|
78
|
+
# @param filter [Symbol] callback filter. Supported values are :before, :after and :around
|
79
|
+
# @param callback [#call]
|
80
|
+
# @return [void]
|
81
|
+
def define_callback(action, filter, callback)
|
82
|
+
@callbacks[action] ||= {}
|
83
|
+
@callbacks[action][filter] ||= []
|
84
|
+
@callbacks[action][filter].push(callback)
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param action [Object] an action to run
|
88
|
+
# @return [Object] the result of passed block
|
89
|
+
def run_callbacks(action, *args, **kwargs, &blk)
|
90
|
+
return (yield if block_given?) unless @callbacks[action]
|
91
|
+
|
92
|
+
run_before_callbacks(action, *args, **kwargs)
|
93
|
+
result = run_around_callbacks(action, *args, **kwargs, &blk)
|
94
|
+
run_after_callbacks(action, *args, **kwargs)
|
95
|
+
result
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def run_before_callbacks(action, *args, **kwargs)
|
101
|
+
@callbacks[action][:before]&.each do |callback|
|
102
|
+
callback.call(*args, **kwargs)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def run_around_callbacks(action, *args, **kwargs, &blk)
|
107
|
+
result = nil
|
108
|
+
stack = [proc { result = yield if block_given? }]
|
109
|
+
@callbacks[action][:around]&.reverse_each&.with_index do |callback, index|
|
110
|
+
stack.push(proc { callback.call(stack[index], *args, **kwargs); result })
|
111
|
+
end
|
112
|
+
stack.last.call(*args, **kwargs)
|
113
|
+
result
|
114
|
+
end
|
115
|
+
|
116
|
+
def run_after_callbacks(action, *args, **kwargs)
|
117
|
+
@callbacks[action][:after]&.each do |callback|
|
118
|
+
callback.call(*args, **kwargs)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/pg_eventstore/client.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative 'commands'
|
4
4
|
require_relative 'event_serializer'
|
5
|
-
require_relative '
|
5
|
+
require_relative 'event_deserializer'
|
6
6
|
require_relative 'queries'
|
7
7
|
|
8
8
|
module PgEventstore
|
@@ -140,7 +140,7 @@ module PgEventstore
|
|
140
140
|
EventQueries.new(
|
141
141
|
connection,
|
142
142
|
EventSerializer.new(middlewares),
|
143
|
-
|
143
|
+
EventDeserializer.new(middlewares, config.event_class_resolver)
|
144
144
|
)
|
145
145
|
end
|
146
146
|
end
|
data/lib/pg_eventstore/config.rb
CHANGED
@@ -6,14 +6,46 @@ module PgEventstore
|
|
6
6
|
|
7
7
|
attr_reader :name
|
8
8
|
|
9
|
-
#
|
9
|
+
# @!attribute pg_uri
|
10
|
+
# @return [String] PostgreSQL connection URI docs
|
11
|
+
# https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS
|
10
12
|
option(:pg_uri) { 'postgresql://postgres:postgres@localhost:5432/eventstore' }
|
13
|
+
# @!attribute max_count
|
14
|
+
# @return [Integer] Number of events to return in one response when reading from a stream
|
11
15
|
option(:max_count) { 1000 }
|
16
|
+
# @!attribute middlewares
|
17
|
+
# @return [Hash{Symbol => <#serialize, #deserialize>}] A set of identified(by key) objects that respond to
|
18
|
+
# #serialize and #deserialize
|
12
19
|
option(:middlewares) { {} }
|
13
|
-
#
|
20
|
+
# @!attribute event_class_resolver
|
21
|
+
# @return [#call] A callable object that must accept a string and return a class. It is used when resolving
|
22
|
+
# event's class during event's deserialization process
|
14
23
|
option(:event_class_resolver) { EventClassResolver.new }
|
24
|
+
# @!attribute connection_pool_size
|
25
|
+
# @return [Integer] Max number of connections per ruby process
|
15
26
|
option(:connection_pool_size) { 5 }
|
16
|
-
|
27
|
+
# @!attribute connection_pool_timeout
|
28
|
+
# @return [Integer] Time in seconds to wait for the connection in pool to be released
|
29
|
+
option(:connection_pool_timeout) { 5 }
|
30
|
+
# @!attribute subscription_pull_interval
|
31
|
+
# @return [Integer] How often Subscription should pull new events
|
32
|
+
option(:subscription_pull_interval) { 2 }
|
33
|
+
# @!attribute subscription_max_retries
|
34
|
+
# @return [Integer] max number of retries of failed Subscription
|
35
|
+
option(:subscription_max_retries) { 100 }
|
36
|
+
# @!attribute subscription_retries_interval
|
37
|
+
# @return [Integer] interval in seconds between retries of failed Subscription
|
38
|
+
option(:subscription_retries_interval) { 1 }
|
39
|
+
# @!attribute subscription_restart_terminator
|
40
|
+
# @return [#call, nil] provide callable object that accepts Subscription object to decide whether to prevent
|
41
|
+
# further Subscription restarts
|
42
|
+
option(:subscription_restart_terminator)
|
43
|
+
# @!attribute subscriptions_set_max_retries
|
44
|
+
# @return [Integer] max number of retries of failed SubscriptionsSet
|
45
|
+
option(:subscriptions_set_max_retries) { 10 }
|
46
|
+
# @!attribute subscriptions_set_retries_interval
|
47
|
+
# @return [Integer] interval in seconds between retries of failed SubscriptionsSet
|
48
|
+
option(:subscriptions_set_retries_interval) { 1 }
|
17
49
|
|
18
50
|
# @param name [Symbol] config's name. Its value matches the appropriate key in PgEventstore.config hash
|
19
51
|
def initialize(name:, **options)
|
data/lib/pg_eventstore/errors.rb
CHANGED
@@ -104,4 +104,67 @@ module PgEventstore
|
|
104
104
|
TEXT
|
105
105
|
end
|
106
106
|
end
|
107
|
+
|
108
|
+
class RecordNotFound < Error
|
109
|
+
attr_reader :table_name, :id
|
110
|
+
|
111
|
+
# @param table_name [String]
|
112
|
+
# @param id [Integer, String]
|
113
|
+
def initialize(table_name, id)
|
114
|
+
@table_name = table_name
|
115
|
+
@id = id
|
116
|
+
super(user_friendly_message)
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [String]
|
120
|
+
def user_friendly_message
|
121
|
+
"Could not find/update #{table_name.inspect} record with #{id.inspect} id."
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class SubscriptionAlreadyLockedError < Error
|
126
|
+
attr_reader :set, :name, :lock_id
|
127
|
+
|
128
|
+
# @param set [String] subscriptions set name
|
129
|
+
# @param name [String] subscription's name
|
130
|
+
# @param lock_id [String] UUIDv4
|
131
|
+
def initialize(set, name, lock_id)
|
132
|
+
@set = set
|
133
|
+
@name = name
|
134
|
+
@lock_id = lock_id
|
135
|
+
super(user_friendly_message)
|
136
|
+
end
|
137
|
+
|
138
|
+
# @return [String]
|
139
|
+
def user_friendly_message
|
140
|
+
<<~TEXT
|
141
|
+
Could not lock Subscription from #{set.inspect} set with #{name.inspect} name. It is already locked by \
|
142
|
+
#{lock_id.inspect} set.
|
143
|
+
TEXT
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class SubscriptionUnlockError < Error
|
148
|
+
attr_reader :set, :name, :expected_locked_by, :actual_locked_by
|
149
|
+
|
150
|
+
# @param set [String] subscription's set name
|
151
|
+
# @param name [String] subscription's name
|
152
|
+
# @param expected_locked_by [String] UUIDv4
|
153
|
+
# @param actual_locked_by [String, nil] UUIDv4
|
154
|
+
def initialize(set, name, expected_locked_by, actual_locked_by)
|
155
|
+
@set = set
|
156
|
+
@name = name
|
157
|
+
@expected_locked_by = expected_locked_by
|
158
|
+
@actual_locked_by = actual_locked_by
|
159
|
+
super(user_friendly_message)
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [String]
|
163
|
+
def user_friendly_message
|
164
|
+
<<~TEXT
|
165
|
+
Failed to unlock Subscription from #{set.inspect} set with #{name.inspect} name by \
|
166
|
+
#{expected_locked_by.inspect} lock id. It is currently locked by #{actual_locked_by.inspect} lock id.
|
167
|
+
TEXT
|
168
|
+
end
|
169
|
+
end
|
107
170
|
end
|