pg_eventstore 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/CODE_OF_CONDUCT.md +1 -1
  4. data/README.md +2 -0
  5. data/db/migrations/10_create_subscription_commands.sql +15 -0
  6. data/db/migrations/11_create_subscriptions_set_commands.sql +15 -0
  7. data/db/migrations/12_improve_events_indexes.sql +1 -0
  8. data/db/migrations/13_remove_duplicated_index.sql +1 -0
  9. data/db/migrations/9_create_subscriptions.sql +46 -0
  10. data/docs/configuration.md +42 -21
  11. data/docs/linking_events.md +96 -0
  12. data/docs/reading_events.md +56 -0
  13. data/docs/subscriptions.md +170 -0
  14. data/lib/pg_eventstore/callbacks.rb +122 -0
  15. data/lib/pg_eventstore/client.rb +32 -2
  16. data/lib/pg_eventstore/commands/append.rb +3 -11
  17. data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +22 -0
  18. data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +24 -0
  19. data/lib/pg_eventstore/commands/link_to.rb +33 -0
  20. data/lib/pg_eventstore/commands/regular_stream_read_paginated.rb +63 -0
  21. data/lib/pg_eventstore/commands/system_stream_read_paginated.rb +62 -0
  22. data/lib/pg_eventstore/commands.rb +5 -0
  23. data/lib/pg_eventstore/config.rb +35 -3
  24. data/lib/pg_eventstore/errors.rb +80 -0
  25. data/lib/pg_eventstore/{pg_result_deserializer.rb → event_deserializer.rb} +10 -22
  26. data/lib/pg_eventstore/extensions/callbacks_extension.rb +95 -0
  27. data/lib/pg_eventstore/extensions/options_extension.rb +69 -29
  28. data/lib/pg_eventstore/extensions/using_connection_extension.rb +35 -0
  29. data/lib/pg_eventstore/pg_connection.rb +20 -3
  30. data/lib/pg_eventstore/queries/event_queries.rb +18 -34
  31. data/lib/pg_eventstore/queries/event_type_queries.rb +24 -0
  32. data/lib/pg_eventstore/queries/preloader.rb +37 -0
  33. data/lib/pg_eventstore/queries/stream_queries.rb +14 -1
  34. data/lib/pg_eventstore/queries/subscription_command_queries.rb +81 -0
  35. data/lib/pg_eventstore/queries/subscription_queries.rb +166 -0
  36. data/lib/pg_eventstore/queries/subscriptions_set_command_queries.rb +76 -0
  37. data/lib/pg_eventstore/queries/subscriptions_set_queries.rb +89 -0
  38. data/lib/pg_eventstore/queries.rb +7 -0
  39. data/lib/pg_eventstore/query_builders/events_filtering_query.rb +17 -22
  40. data/lib/pg_eventstore/sql_builder.rb +54 -10
  41. data/lib/pg_eventstore/stream.rb +2 -1
  42. data/lib/pg_eventstore/subscriptions/basic_runner.rb +220 -0
  43. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_feeder_commands.rb +52 -0
  44. data/lib/pg_eventstore/subscriptions/command_handlers/subscription_runners_commands.rb +68 -0
  45. data/lib/pg_eventstore/subscriptions/commands_handler.rb +62 -0
  46. data/lib/pg_eventstore/subscriptions/events_processor.rb +72 -0
  47. data/lib/pg_eventstore/subscriptions/runner_state.rb +45 -0
  48. data/lib/pg_eventstore/subscriptions/subscription.rb +141 -0
  49. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +171 -0
  50. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +39 -0
  51. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +125 -0
  52. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +38 -0
  53. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +105 -0
  54. data/lib/pg_eventstore/subscriptions/subscriptions_set.rb +97 -0
  55. data/lib/pg_eventstore/tasks/setup.rake +5 -1
  56. data/lib/pg_eventstore/utils.rb +66 -0
  57. data/lib/pg_eventstore/version.rb +1 -1
  58. data/lib/pg_eventstore.rb +19 -1
  59. metadata +38 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e22bb2356955ece89a16f5d43b803670d0304eaf27d2fedbf1daf12aa908812
4
- data.tar.gz: 31d95e0380be2ad8dd7306a1b04a8a14e6f4f32f6bcdb8864e42205c0a1fe9bc
3
+ metadata.gz: e1716f5a502b55d670c94cd1950f03dfa1e307050a665435840004e1eb6e2158
4
+ data.tar.gz: d76b116150c351e00fa72b88c03472dbe987140c88b00e1211e81e7eb580a8cb
5
5
  SHA512:
6
- metadata.gz: 8cda13e213beec47c83818301b415d1dbf5b80e0659e548c6d166a3e136172c38802b697353f13344bb0d6b15b99b54f59e950ab9fb2f07e7bf494adedb2c280
7
- data.tar.gz: 5f0743afb46b2dcd00c9c164b51beb6f4ea6284b839a0036418d46e60e7d7b8a53d378a1eb265fc063364970ae6fb2b0091fabae51429775bec2f8c1c2e843fa
6
+ metadata.gz: 131c82d96b36d105a8b5f90829864ceca96209a5dfd09ed56b27e55f7f6dc755f30fffc10c837f62017ee557a7611518f5550b43d21af00112399e7f0fe3ede1
7
+ data.tar.gz: ce52feba0bee2e4737a9e8edae5fa880ca1d66cd15e07045bd6bd965713a7f5aed0370e119d3edf63760340f369c18c242555ae0ef0d3c94cec9447791d39ec0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.5.0] - 2024-02-05
4
+
5
+ - Fix event class resolving when providing `resolve_link_tos: true` option
6
+ - Return correct stream revision of the `Event#stream` object of the appended event
7
+ - Implement events linking feature
8
+ - Implement paginated read
9
+ - Remove duplicated `idx_events_event_type_id` index
10
+
11
+ ## [0.4.0] - 2024-01-29
12
+
13
+ - Implement asynchronous subscriptions. Refer to the documentation for more info
14
+
1
15
  ## [0.3.0] - 2024-01-24
2
16
 
3
17
  - Log SQL queries when `PgEvenstore.logger` is set and it is in `:debug` mode
data/CODE_OF_CONDUCT.md CHANGED
@@ -39,7 +39,7 @@ This Code of Conduct applies within all community spaces, and also applies when
39
39
 
40
40
  ## Enforcement
41
41
 
42
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at intale.a@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at ivan.dzyzenko@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
43
 
44
44
  All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
45
 
data/README.md CHANGED
@@ -52,7 +52,9 @@ Documentation chapters:
52
52
  - [Configuration](docs/configuration.md)
53
53
  - [Events and streams definitions](docs/events_and_streams.md)
54
54
  - [Appending events](docs/appending_events.md)
55
+ - [Linking events](docs/linking_events.md)
55
56
  - [Reading events](docs/reading_events.md)
57
+ - [Subscriptions](docs/subscriptions.md)
56
58
  - [Writing middlewares](docs/writing_middleware.md)
57
59
  - [How to make multiple commands atomic](docs/multiple_commands.md)
58
60
 
@@ -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 @@
1
+ DROP INDEX idx_events_event_type_id;
@@ -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);
@@ -2,21 +2,23 @@
2
2
 
3
3
  Configuration options:
4
4
 
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
- | logger | `Logger` | `nil` | A logger that logs messages from `pg_eventstore` gem. |
12
- | connection_pool_size | Integer | `5` | Max number of connections per ruby process. It must equal the number of threads of your application. |
13
- | connection_pool_timeout | Integer | `5` | Time in seconds to wait for the connection in 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. |
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,96 @@
1
+ # Linking Events
2
+
3
+ ## Linking single event
4
+
5
+ You can create a link to an existing event. Next example demonstrates how you can link an existing event from another stream:
6
+
7
+ ```ruby
8
+ class SomethingHappened < PgEventstore::Event
9
+ end
10
+
11
+ event = SomethingHappened.new(
12
+ type: 'some-event', data: { title: 'Something happened' }
13
+ )
14
+
15
+ events_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
16
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: '1')
17
+ # Persist our event
18
+ event = PgEventstore.client.append_to_stream(events_stream, event)
19
+
20
+ # Link event into your projection
21
+ PgEventstore.client.link_to(projection_stream, event)
22
+ ```
23
+
24
+ The linked event can later be fetched by providing the `:resolve_link_tos` option when reading from the stream:
25
+
26
+ ```ruby
27
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: '1')
28
+ PgEventstore.client.read(projection_stream, options: { resolve_link_tos: true })
29
+ ```
30
+
31
+ If you don't provide the `:resolve_link_tos` option, the linked event will be returned instead of the original one.
32
+
33
+ ## Linking multiple events
34
+
35
+ You can provide an array of events to link to the target stream:
36
+
37
+ ```ruby
38
+ class SomethingHappened < PgEventstore::Event
39
+ end
40
+
41
+ events = 3.times.map { |i| SomethingHappened.new(type: 'some-event', data: { title: "Something happened-#{i}" }) }
42
+ events_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
43
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: '1')
44
+ events = PgEventstore.client.append_to_stream(events_stream, events)
45
+ # Link events
46
+ PgEventstore.client.link_to(projection_stream, events)
47
+ ```
48
+
49
+ ## Handling concurrency
50
+
51
+ Linking concurrency is implemented the same way as appending concurrency. You can check [**Handling concurrency**](appending_events.md#handling-concurrency) chapter of **Appending Events** section.
52
+
53
+ Example:
54
+
55
+ ```ruby
56
+ require 'securerandom'
57
+ class SomethingHappened < PgEventstore::Event
58
+ end
59
+
60
+ event1 = SomethingHappened.new
61
+ event2 = SomethingHappened.new
62
+
63
+ events_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
64
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: SecureRandom.uuid)
65
+
66
+ event1, event2 = PgEventstore.client.append_to_stream(events_stream, [event1, event2])
67
+
68
+ # Links first event
69
+ PgEventstore.client.link_to(projection_stream, event1, options: { expected_revision: :no_stream })
70
+ # Raises PgEventstore::WrongExpectedVersionError error because stream already exists
71
+ PgEventstore.client.link_to(projection_stream, event2, options: { expected_revision: :no_stream })
72
+ ```
73
+
74
+ ## Middlewares
75
+
76
+ If you would like to modify your link events before they get persisted - you should use the `:middlewares` argument which allows you to pass the list of middlewares you would like to use. **By default no middlewares will be applied to the link event despite on `config.middlewares` option**.
77
+
78
+ Let's say you have these registered middlewares:
79
+
80
+ ```ruby
81
+ PgEventstore.configure do |config|
82
+ config.middlewares = { foo: FooMiddleware.new, bar: BarMiddleware.new, baz: BazMiddleware.new }
83
+ end
84
+ ```
85
+
86
+ And you want to use `FooMiddleware` and `BazMiddleware`. You simply have to provide an array of corresponding middleware keys you would like to use:
87
+
88
+ ```ruby
89
+ events_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeStream', stream_id: '1')
90
+ projection_stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'MyAwesomeProjection', stream_id: '1')
91
+
92
+ event = PgEventstore.client.append_to_stream(events_stream, PgEventstore::Event.new)
93
+ PgEventstore.client.link_to(projection_stream, event, middlewares: %i[foo baz])
94
+ ```
95
+
96
+ See [Writing middleware](writing_middleware.md) chapter for info about what is middleware and how to implement it.
@@ -159,3 +159,59 @@ You can also mix filtering by stream's attributes and event types. The result wi
159
159
  ```ruby
160
160
  PgEventstore.client.read(PgEventstore::Stream.all_stream, options: { filter: { streams: [{ context: 'MyAwesomeContext' }], event_types: %w[Foo Bar] } })
161
161
  ```
162
+
163
+
164
+ ## Pagination
165
+
166
+ You can use `#read_paginated` to iterate over all (filtered) events. It yields each batch of records that was found according to the filter options:
167
+
168
+ ```ruby
169
+ # Read from the specific stream
170
+ stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
171
+ PgEventstore.client.read_paginated(stream).each do |events|
172
+ events.each do |event|
173
+ # iterate through events
174
+ end
175
+ end
176
+
177
+ # Read from "all" stream
178
+ PgEventstore.client.read_paginated(PgEventstore::Stream.all_stream).each do |events|
179
+ events.each do |event|
180
+ # iterate through events
181
+ end
182
+ end
183
+ ```
184
+
185
+ Options are the same as for `#read` method. Several examples:
186
+
187
+ ```ruby
188
+ # Read "Foo" events only from the specific stream
189
+ stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'User', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
190
+ PgEventstore.client.read_paginated(stream, options: { filter: { event_types: ['Foo'] } }).each do |events|
191
+ events.each do |event|
192
+ # iterate through events
193
+ end
194
+ end
195
+
196
+ # Backwards read from "all" stream
197
+ PgEventstore.client.read_paginated(PgEventstore::Stream.all_stream, options: { direction: 'Backwards' }).each do |events|
198
+ events.each do |event|
199
+ # iterate through events
200
+ end
201
+ end
202
+
203
+ # Set batch size to 100
204
+ PgEventstore.client.read_paginated(PgEventstore::Stream.all_stream, options: { max_count: 100 }).each do |events|
205
+ events.each do |event|
206
+ # iterate through events
207
+ end
208
+ end
209
+
210
+ # Reading from projection stream
211
+ projection_stream = PgEventstore::Stream.new(context: 'MyAwesomeContext', stream_name: 'MyAwesomeProjection', stream_id: 'f37b82f2-4152-424d-ab6b-0cc6f0a53aae')
212
+ PgEventstore.client.read_paginated(projection_stream, options: { resolve_link_tos: true }).each do |events|
213
+ events.each do |event|
214
+ # iterate through events
215
+ end
216
+ end
217
+ ```
@@ -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.