pg_eventstore 2.0.0 → 3.0.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/.rubocop.yml +10 -1
- data/CHANGELOG.md +186 -20
- data/README.md +25 -15
- data/db/migrations/10_setup_pg_cron.rb +1 -23
- data/db/migrations/15_create_streams_global_index.sql +17 -0
- data/db/migrations/16_populate_streams_global_index.rb +150 -0
- data/db/migrations/17_create_events_global_index.sql +62 -0
- data/db/migrations/18_populate_events_global_index.rb +124 -0
- data/db/migrations/19_populate_events_global_index_p2.rb +25 -0
- data/db/migrations/20_migrate_subscriptions_current_position.sql +19 -0
- data/db/migrations/21_create_partitions_stats.sql +2 -0
- data/db/migrations/22_drop_streams_sql_view.sql +2 -0
- data/db/migrations/23_adjust_partitions_indexes.sql +5 -0
- data/db/migrations/24_drop_legacy_events_indexes.sql +3 -0
- data/db/migrations/25_add_parent_partitions_info_to_partitions.sql +16 -0
- data/db/migrations/26_change_events_stream_revision_type.rb +35 -0
- data/db/migrations/27_drop_events_horizon.sql +3 -0
- data/db/migrations/28_create_maintenance_tasks.sql +8 -0
- data/db/migrations/29_create_event_markers.sql +35 -0
- data/db/migrations/30_improve_partitions_search_in_admin_ui.rb +24 -0
- data/db/migrations/31_drop_events_id_default_value.sql +1 -0
- data/db/migrations/32_create_parittions_triggers.sql +101 -0
- data/docs/AGENTS.fragment.md +696 -0
- data/docs/admin_ui.md +3 -1
- data/docs/appending_events.md +110 -49
- data/docs/configuration.md +69 -36
- data/docs/event_tracing.md +55 -0
- data/docs/events_and_streams.md +54 -30
- data/docs/how_it_works.md +57 -16
- data/docs/linking_events.md +2 -1
- data/docs/maintenance.md +14 -5
- data/docs/multiple_commands.md +17 -8
- data/docs/reading_events.md +208 -43
- data/docs/reading_streams.md +50 -0
- data/docs/replication.md +99 -0
- data/docs/subscriptions.md +165 -35
- data/docs/writing_middleware.md +25 -8
- data/ext/pg_eventstore_ext/extconf.rb +5 -0
- data/ext/pg_eventstore_ext/pg_eventstore_ext.c +74 -0
- data/lib/pg_eventstore/async_runner.rb +63 -0
- data/lib/pg_eventstore/basic_config.rb +15 -0
- data/lib/pg_eventstore/chunks/chunk.rb +31 -0
- data/lib/pg_eventstore/chunks/read_api_events_index_chunk.rb +83 -0
- data/lib/pg_eventstore/chunks/replica_events_index_chunk.rb +35 -0
- data/lib/pg_eventstore/chunks/repository.rb +73 -0
- data/lib/pg_eventstore/chunks/subscription_checkpoint_chunk.rb +48 -0
- data/lib/pg_eventstore/chunks/subscription_events_index_chunk.rb +99 -0
- data/lib/pg_eventstore/chunks.rb +14 -0
- data/lib/pg_eventstore/client.rb +163 -50
- data/lib/pg_eventstore/commands/append.rb +62 -43
- data/lib/pg_eventstore/commands/delete_event.rb +7 -8
- data/lib/pg_eventstore/commands/delete_stream.rb +3 -3
- data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +9 -7
- data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +12 -7
- data/lib/pg_eventstore/commands/link_to.rb +5 -5
- data/lib/pg_eventstore/commands/read.rb +23 -5
- data/lib/pg_eventstore/commands/read_grouped.rb +46 -0
- data/lib/pg_eventstore/commands/read_streams.rb +18 -0
- data/lib/pg_eventstore/commands/read_streams_paginated.rb +56 -0
- data/lib/pg_eventstore/commands/regular_stream_read_paginated.rb +4 -4
- data/lib/pg_eventstore/commands/revision_check/current_revision.rb +51 -0
- data/lib/pg_eventstore/commands/revision_check/event_type_revisions_comparison.rb +46 -0
- data/lib/pg_eventstore/commands/revision_check/expected_revision.rb +135 -0
- data/lib/pg_eventstore/commands/revision_check/stream_revision_check.rb +98 -0
- data/lib/pg_eventstore/commands/revision_check/stream_revision_comparison.rb +36 -0
- data/lib/pg_eventstore/commands/stream_revision.rb +14 -0
- data/lib/pg_eventstore/commands/system_stream_read_paginated.rb +4 -4
- data/lib/pg_eventstore/commands.rb +4 -2
- data/lib/pg_eventstore/config.rb +30 -9
- data/lib/pg_eventstore/connection.rb +21 -2
- data/lib/pg_eventstore/errors.rb +136 -10
- data/lib/pg_eventstore/event.rb +34 -2
- data/lib/pg_eventstore/event_deserializer.rb +5 -4
- data/lib/pg_eventstore/event_global_index.rb +118 -0
- data/lib/pg_eventstore/event_marker.rb +16 -0
- data/lib/pg_eventstore/event_marker_index.rb +25 -0
- data/lib/pg_eventstore/event_serializer.rb +6 -0
- data/lib/pg_eventstore/extensions/acts_as_configurable.rb +88 -0
- data/lib/pg_eventstore/extensions/options_defaults.rb +25 -0
- data/lib/pg_eventstore/extensions/options_extension.rb +2 -2
- data/lib/pg_eventstore/feature_marker.rb +18 -0
- data/lib/pg_eventstore/maintenance.rb +11 -6
- data/lib/pg_eventstore/middleware/event_tracing.rb +53 -0
- data/lib/pg_eventstore/partition.rb +6 -0
- data/lib/pg_eventstore/pg_connection.rb +70 -14
- data/lib/pg_eventstore/queries/event_marker_queries.rb +74 -0
- data/lib/pg_eventstore/queries/event_queries.rb +7 -93
- data/lib/pg_eventstore/queries/events_global_index_queries.rb +131 -0
- data/lib/pg_eventstore/queries/index_filtering_queries.rb +179 -0
- data/lib/pg_eventstore/queries/links_resolver.rb +4 -4
- data/lib/pg_eventstore/queries/maintenance_queries.rb +159 -42
- data/lib/pg_eventstore/queries/partition_queries.rb +45 -103
- data/lib/pg_eventstore/queries/replica_queries.rb +110 -0
- data/lib/pg_eventstore/queries/streams_global_index_queries.rb +163 -0
- data/lib/pg_eventstore/queries/transaction_queries.rb +52 -10
- data/lib/pg_eventstore/queries.rb +29 -1
- data/lib/pg_eventstore/query_builders/basic_filtering.rb +38 -5
- data/lib/pg_eventstore/query_builders/event_markers_filtering.rb +47 -0
- data/lib/pg_eventstore/query_builders/event_markers_index_filtering.rb +275 -0
- data/lib/pg_eventstore/query_builders/event_subscription_positions_filtering.rb +37 -0
- data/lib/pg_eventstore/query_builders/events_filtering.rb +15 -164
- data/lib/pg_eventstore/query_builders/events_global_index_filtering.rb +227 -0
- data/lib/pg_eventstore/query_builders/filters/collection.rb +324 -0
- data/lib/pg_eventstore/query_builders/filters/event_type_filter.rb +37 -0
- data/lib/pg_eventstore/query_builders/filters/filter_row.rb +54 -0
- data/lib/pg_eventstore/query_builders/filters/marker_filter.rb +20 -0
- data/lib/pg_eventstore/query_builders/filters/marker_filter_row.rb +49 -0
- data/lib/pg_eventstore/query_builders/filters/stream_filter.rb +48 -0
- data/lib/pg_eventstore/query_builders/index_based_events_filtering.rb +260 -0
- data/lib/pg_eventstore/query_builders/partitions_filtering.rb +175 -51
- data/lib/pg_eventstore/query_builders/read_cursor/stream_cursor.rb +122 -0
- data/lib/pg_eventstore/query_builders/streams_global_index_filtering.rb +83 -0
- data/lib/pg_eventstore/query_builders/subscription_events_filtering.rb +79 -0
- data/lib/pg_eventstore/query_strategy/async.rb +64 -0
- data/lib/pg_eventstore/query_strategy/foreground.rb +25 -0
- data/lib/pg_eventstore/query_strategy.rb +19 -0
- data/lib/pg_eventstore/raw_event.rb +46 -0
- data/lib/pg_eventstore/rspec/test_helpers.rb +21 -8
- data/lib/pg_eventstore/sql_builder.rb +87 -8
- data/lib/pg_eventstore/stream.rb +13 -16
- data/lib/pg_eventstore/stream_global_index.rb +28 -0
- data/lib/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rb +5 -17
- data/lib/pg_eventstore/subscriptions/callback_handlers/events_subscription_position_worker_handlers.rb +36 -0
- data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rb +12 -0
- data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +21 -7
- data/lib/pg_eventstore/subscriptions/events_processor.rb +20 -18
- data/lib/pg_eventstore/subscriptions/events_processor_consumer/multiple.rb +63 -0
- data/lib/pg_eventstore/subscriptions/events_processor_consumer/replica.rb +68 -0
- data/lib/pg_eventstore/subscriptions/events_processor_consumer/single.rb +62 -0
- data/lib/pg_eventstore/subscriptions/events_processor_consumer.rb +44 -0
- data/lib/pg_eventstore/subscriptions/events_subscription_position_worker.rb +68 -0
- data/lib/pg_eventstore/subscriptions/queries/event_subscription_position_queries.rb +177 -0
- data/lib/pg_eventstore/subscriptions/queries/subscription_queries.rb +78 -54
- data/lib/pg_eventstore/subscriptions/replica_subscription_handler.rb +140 -0
- data/lib/pg_eventstore/subscriptions/replica_subscription_runner.rb +9 -0
- data/lib/pg_eventstore/subscriptions/subscription.rb +2 -1
- data/lib/pg_eventstore/subscriptions/subscription_feed_strategies/collection.rb +36 -0
- data/lib/pg_eventstore/subscriptions/subscription_feed_strategies/index_read_strategy.rb +82 -0
- data/lib/pg_eventstore/subscriptions/subscription_feed_strategies/replication_strategy.rb +75 -0
- data/lib/pg_eventstore/subscriptions/subscription_feed_strategy.rb +31 -0
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +24 -0
- data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +4 -2
- data/lib/pg_eventstore/subscriptions/subscription_runner.rb +20 -9
- data/lib/pg_eventstore/subscriptions/subscription_runner_commands/reset_position.rb +1 -1
- data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +9 -18
- data/lib/pg_eventstore/subscriptions/subscriptions_lifecycle.rb +0 -12
- data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +74 -14
- data/lib/pg_eventstore/{subscriptions/synchronized_array.rb → synchronized_array.rb} +15 -0
- data/lib/pg_eventstore/tasks/setup.rake +2 -2
- data/lib/pg_eventstore/utils.rb +31 -0
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +100 -16
- data/lib/pg_eventstore/web/paginator/base_collection.rb +5 -1
- data/lib/pg_eventstore/web/paginator/event_types_collection.rb +14 -4
- data/lib/pg_eventstore/web/paginator/events_collection.rb +29 -33
- data/lib/pg_eventstore/web/paginator/helpers.rb +16 -9
- data/lib/pg_eventstore/web/paginator/markers_collection.rb +48 -0
- data/lib/pg_eventstore/web/paginator/stream_contexts_collection.rb +13 -2
- data/lib/pg_eventstore/web/paginator/stream_ids_collection.rb +21 -11
- data/lib/pg_eventstore/web/paginator/stream_names_collection.rb +18 -4
- data/lib/pg_eventstore/web/paginator/streams_collection.rb +95 -0
- data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +49 -22
- data/lib/pg_eventstore/web/subscriptions/set_collection.rb +1 -1
- data/lib/pg_eventstore/web/subscriptions/subscriptions.rb +1 -1
- data/lib/pg_eventstore/web/subscriptions/with_state/set_collection.rb +1 -1
- data/lib/pg_eventstore/web/subscriptions/with_state/subscriptions.rb +1 -1
- data/lib/pg_eventstore/web/views/home/dashboard.erb +11 -9
- data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +18 -5
- data/lib/pg_eventstore/web/views/home/partials/events.erb +38 -1
- data/lib/pg_eventstore/web/views/home/partials/markers_filter.erb +18 -0
- data/lib/pg_eventstore/web/views/layouts/application.erb +6 -0
- data/lib/pg_eventstore/web/views/streams/index.erb +110 -0
- data/lib/pg_eventstore/web/views/streams/partials/streams.erb +11 -0
- data/lib/pg_eventstore/web/views/subscriptions/index.erb +4 -1
- data/lib/pg_eventstore/web.rb +2 -0
- data/lib/pg_eventstore.rb +23 -60
- data/pg_eventstore.gemspec +6 -4
- data/sig/interfaces/_raw_events_handler.rbs +3 -0
- data/sig/interfaces/subscription_handler.rbs +1 -1
- data/sig/pg/result.rbs +9 -0
- data/sig/pg_eventstore/async_runner.rbs +13 -0
- data/sig/pg_eventstore/basic_config.rbs +9 -0
- data/sig/pg_eventstore/chunks/chunk.rbs +13 -0
- data/sig/pg_eventstore/chunks/read_api_events_index_chunk.rbs +30 -0
- data/sig/pg_eventstore/chunks/replica_events_index_chunk.rbs +11 -0
- data/sig/pg_eventstore/chunks/repository.rbs +22 -0
- data/sig/pg_eventstore/chunks/subscription_checkpoint_chunk.rbs +18 -0
- data/sig/pg_eventstore/chunks/subscription_events_index_chunk.rbs +37 -0
- data/sig/pg_eventstore/client.rbs +40 -42
- data/sig/pg_eventstore/commands/append.rbs +11 -25
- data/sig/pg_eventstore/commands/event_modifiers/prepare_link_event.rbs +9 -12
- data/sig/pg_eventstore/commands/event_modifiers/prepare_regular_event.rbs +5 -4
- data/sig/pg_eventstore/commands/link_to.rbs +6 -11
- data/sig/pg_eventstore/commands/read.rbs +2 -5
- data/sig/pg_eventstore/commands/read_grouped.rbs +7 -0
- data/sig/pg_eventstore/commands/read_streams.rbs +7 -0
- data/sig/pg_eventstore/commands/read_streams_paginated.rbs +16 -0
- data/sig/pg_eventstore/commands/regular_stream_read_paginated.rbs +9 -17
- data/sig/pg_eventstore/commands/revision_check/current_revision.rbs +25 -0
- data/sig/pg_eventstore/commands/revision_check/event_type_revisions_comparison.rbs +14 -0
- data/sig/pg_eventstore/commands/revision_check/expected_revision.rbs +67 -0
- data/sig/pg_eventstore/commands/revision_check/stream_revision_check.rbs +20 -0
- data/sig/pg_eventstore/commands/revision_check/stream_revision_comparison.rbs +9 -0
- data/sig/pg_eventstore/commands/stream_revision.rbs +7 -0
- data/sig/pg_eventstore/commands/system_stream_read_paginated.rbs +9 -14
- data/sig/pg_eventstore/config.rbs +15 -6
- data/sig/pg_eventstore/connection.rbs +11 -18
- data/sig/pg_eventstore/errors.rbs +64 -34
- data/sig/pg_eventstore/event.rbs +16 -2
- data/sig/pg_eventstore/event_deserializer.rbs +5 -10
- data/sig/pg_eventstore/event_global_index.rbs +60 -0
- data/sig/pg_eventstore/event_marker.rbs +9 -0
- data/sig/pg_eventstore/event_marker_index.rbs +12 -0
- data/sig/pg_eventstore/extensions/acts_as_configurable.rbs +29 -0
- data/sig/pg_eventstore/extensions/options_defaults.rbs +7 -0
- data/sig/pg_eventstore/feature_marker.rbs +10 -0
- data/sig/pg_eventstore/maintenance.rbs +12 -8
- data/sig/pg_eventstore/middleware/event_trace.rbs +14 -0
- data/sig/pg_eventstore/partition.rbs +2 -4
- data/sig/pg_eventstore/pg_connection.rbs +8 -0
- data/sig/pg_eventstore/queries/event_marker_queries.rbs +21 -0
- data/sig/pg_eventstore/queries/event_queries.rbs +4 -36
- data/sig/pg_eventstore/queries/events_global_index_queries.rbs +37 -0
- data/sig/pg_eventstore/queries/index_filtering_queries.rbs +50 -0
- data/sig/pg_eventstore/queries/links_resolver.rbs +5 -9
- data/sig/pg_eventstore/queries/maintenance_queries.rbs +12 -9
- data/sig/pg_eventstore/queries/partition_queries.rbs +32 -67
- data/sig/pg_eventstore/queries/replica_queries.rbs +31 -0
- data/sig/pg_eventstore/queries/streams_global_index_queries.rbs +39 -0
- data/sig/pg_eventstore/queries/transaction_queries.rbs +2 -0
- data/sig/pg_eventstore/queries.rbs +11 -21
- data/sig/pg_eventstore/query_builders/basic_filtering.rbs +10 -3
- data/sig/pg_eventstore/query_builders/event_markers_filtering.rbs +17 -0
- data/sig/pg_eventstore/query_builders/event_markers_index_filtering.rbs +69 -0
- data/sig/pg_eventstore/query_builders/event_subscription_positions_filtering.rbs +15 -0
- data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +9 -51
- data/sig/pg_eventstore/query_builders/events_global_index_filtering.rbs +68 -0
- data/sig/pg_eventstore/query_builders/filters/collection.rbs +84 -0
- data/sig/pg_eventstore/query_builders/filters/event_type_filter.rbs +19 -0
- data/sig/pg_eventstore/query_builders/filters/filter_row.rbs +21 -0
- data/sig/pg_eventstore/query_builders/filters/marker_filter.rbs +13 -0
- data/sig/pg_eventstore/query_builders/filters/marker_filter_row.rbs +19 -0
- data/sig/pg_eventstore/query_builders/filters/stream_filter.rbs +24 -0
- data/sig/pg_eventstore/query_builders/index_based_events_filtering.rbs +60 -0
- data/sig/pg_eventstore/query_builders/partitions_filtering.rbs +29 -10
- data/sig/pg_eventstore/query_builders/read_cursor/stream_cursor.rbs +47 -0
- data/sig/pg_eventstore/query_builders/streams_global_index_filtering.rbs +27 -0
- data/sig/pg_eventstore/query_builders/subscription_events_filtering.rbs +26 -0
- data/sig/pg_eventstore/query_strategy/async.rbs +17 -0
- data/sig/pg_eventstore/query_strategy/foreground.rbs +11 -0
- data/sig/pg_eventstore/query_strategy.rbs +7 -0
- data/sig/pg_eventstore/raw_event.rbs +20 -0
- data/sig/pg_eventstore/sql_builder.rbs +32 -10
- data/sig/pg_eventstore/stream.rbs +10 -18
- data/sig/pg_eventstore/stream_global_index.rbs +14 -0
- data/sig/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rbs +5 -4
- data/sig/pg_eventstore/subscriptions/callback_handlers/events_subscription_position_worker_handlers.rbs +9 -0
- data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rbs +17 -12
- data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +17 -6
- data/sig/pg_eventstore/subscriptions/event_subscription_position_queries.rbs +30 -0
- data/sig/pg_eventstore/subscriptions/events_processor.rbs +12 -5
- data/sig/pg_eventstore/subscriptions/events_processor_consumer/multiple.rbs +17 -0
- data/sig/pg_eventstore/subscriptions/events_processor_consumer/replica.rbs +17 -0
- data/sig/pg_eventstore/subscriptions/events_processor_consumer/single.rbs +17 -0
- data/sig/pg_eventstore/subscriptions/events_processor_consumer.rbs +14 -0
- data/sig/pg_eventstore/subscriptions/events_subscription_position_worker.rbs +28 -0
- data/sig/pg_eventstore/subscriptions/queries/subscription_queries.rbs +7 -39
- data/sig/pg_eventstore/subscriptions/replica_subscription_handler.rbs +33 -0
- data/sig/pg_eventstore/subscriptions/replica_subscription_runner.rbs +4 -0
- data/sig/pg_eventstore/subscriptions/subscription_feed_strategy/collection.rbs +14 -0
- data/sig/pg_eventstore/subscriptions/subscription_feed_strategy/index_read_strategy.rbs +23 -0
- data/sig/pg_eventstore/subscriptions/subscription_feed_strategy/replication_strategy.rbs +23 -0
- data/sig/pg_eventstore/subscriptions/subscription_feed_strategy.rbs +11 -0
- data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +13 -10
- data/sig/pg_eventstore/subscriptions/subscription_handler_performance.rbs +1 -1
- data/sig/pg_eventstore/subscriptions/subscription_runner.rbs +13 -2
- data/sig/pg_eventstore/subscriptions/subscription_runners_feeder.rbs +1 -8
- data/sig/pg_eventstore/subscriptions/subscriptions_lifecycle.rbs +4 -6
- data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +29 -43
- data/sig/pg_eventstore/synchronized_array.rbs +4 -0
- data/sig/pg_eventstore/utils.rbs +10 -11
- data/sig/pg_eventstore/web/application.rbs +13 -3
- data/sig/pg_eventstore/web/paginator/base_collection.rbs +2 -0
- data/sig/pg_eventstore/web/paginator/event_types_collection.rbs +4 -0
- data/sig/pg_eventstore/web/paginator/events_collection.rbs +11 -11
- data/sig/pg_eventstore/web/paginator/helpers.rbs +6 -15
- data/sig/pg_eventstore/web/paginator/markers_collection.rbs +13 -0
- data/sig/pg_eventstore/web/paginator/stream_contexts_collection.rbs +4 -0
- data/sig/pg_eventstore/web/paginator/stream_names_collection.rbs +4 -0
- data/sig/pg_eventstore/web/paginator/streams_collection.rbs +30 -0
- data/sig/pg_eventstore.rbs +8 -26
- metadata +175 -20
- data/Dockerfile +0 -3
- data/lib/pg_eventstore/commands/all_stream_read_grouped.rb +0 -69
- data/lib/pg_eventstore/commands/regular_stream_read_grouped.rb +0 -31
- data/lib/pg_eventstore/subscriptions/queries/subscription_service_queries.rb +0 -78
- data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +0 -15
- data/sig/pg_eventstore/commands/all_stream_read_grouped.rbs +0 -16
- data/sig/pg_eventstore/commands/regular_stream_read_grouped.rbs +0 -8
- data/sig/pg_eventstore/subscriptions/queries/subscription_service_queries.rbs +0 -19
- data/sig/pg_eventstore/subscriptions/synchronized_array.rbs +0 -4
- /data/sig/interfaces/{_raw_event_handler.rbs → raw_event_handler.rbs} +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Reading streams
|
|
2
|
+
|
|
3
|
+
`pg_eventstore` provides API to directly read a set of streams and detailed information of them.
|
|
4
|
+
|
|
5
|
+
## Stream revision
|
|
6
|
+
|
|
7
|
+
`Client#stream_revision` provides the best speed of retrieving current stream revision. Alternative way would be to read
|
|
8
|
+
last event in the stream, but it is much slower than this and may raise exception if stream does not exist. Example:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
stream = PgEventstore::Stream.new(context: 'FooCtx', stream_name: 'Foo', stream_id: '1')
|
|
12
|
+
PgEventstore.client.stream_revision(stream)
|
|
13
|
+
# => stream revision or Stream::NON_EXISTING_STREAM_REVISION (which is -1) if stream does not exist
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Streams list
|
|
17
|
+
|
|
18
|
+
You can directly retrieve a list of streams using `#read_streams`. The list is ordered by the stream starting position.
|
|
19
|
+
Starting position is a global position of the first event in the stream. Supported options are:
|
|
20
|
+
|
|
21
|
+
- `:direction`. Read direction. Default is `:asc`
|
|
22
|
+
- `:from_position`. Starting position to read from
|
|
23
|
+
- `:max_count`. Max number of objects to return
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
PgEventstore.client.read_streams
|
|
29
|
+
PgEventstore.client.read_streams(options: { direction: :desc })
|
|
30
|
+
PgEventstore.client.read_streams(options: { max_count: 10 })
|
|
31
|
+
PgEventstore.client.read_streams(options: { from_position: 123 })
|
|
32
|
+
PgEventstore.client.read_streams(options: { from_position: 123, direction: :desc })
|
|
33
|
+
PgEventstore.client.read_streams(options: { from_position: 123, max_count: 123 })
|
|
34
|
+
PgEventstore.client.read_streams(options: { from_position: 123, direction: :desc, max_count: 10 })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Unlike `Stream` object you can access via `Event#stream` of a persisted event - `Stream` objects from this API
|
|
38
|
+
additionally load values of `#starting_position` and `#stream_revision` attributes.
|
|
39
|
+
|
|
40
|
+
You can also paginate by streams. `#read_streams_paginated` yields an array of streams on each iteration. It accepts the
|
|
41
|
+
same list of options as `#read_streams`. Example:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
PgEventstore.client.read_streams_paginated(options: { max_count: 10 }).each do |streams|
|
|
45
|
+
# yields up to :max_count objects
|
|
46
|
+
streams.each do |stream|
|
|
47
|
+
# do something with your stream
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
```
|
data/docs/replication.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Database replication
|
|
2
|
+
|
|
3
|
+
pg_eventstore can be replicated, but for different purposes you should choose different replication strategies. This is
|
|
4
|
+
related to the complex architecture of pg_eventstore database(some tables are partitioned) and to the neediness to have
|
|
5
|
+
only certain tables list in your replica node. For example, such tables as `subscriptions`, `subscriptions_set`,
|
|
6
|
+
`event_subscription_positions_unprocessed`, `subscription_commands`, `subscriptions_set_commands` must not present in
|
|
7
|
+
the replica, because they are node-specific.
|
|
8
|
+
|
|
9
|
+
Let's review a simple example and, based on it, we build a replica setup. So, let's say we have distributed
|
|
10
|
+
architecture, and we target three regions - EU, US and Asia. We then want to have primary node along with two replica
|
|
11
|
+
nodes of two other regions per region. We want to have a hot standby replica per region in case of emergency as well. We
|
|
12
|
+
can summarize this description as follows:
|
|
13
|
+
|
|
14
|
+
## EU Region
|
|
15
|
+
|
|
16
|
+
- **EU primary**
|
|
17
|
+
- Replicates to: **EU hot standby**
|
|
18
|
+
- Replicates to: **US replica node**
|
|
19
|
+
- Replicates to: **Asia replica node**
|
|
20
|
+
|
|
21
|
+
- **US replica**
|
|
22
|
+
- Source: async replication from **US primary**
|
|
23
|
+
|
|
24
|
+
- **Asia replica**
|
|
25
|
+
- Source: async replication from **Asia primary**
|
|
26
|
+
|
|
27
|
+
- **EU read models** built from:
|
|
28
|
+
- EU primary
|
|
29
|
+
- US replica
|
|
30
|
+
- Asia replica
|
|
31
|
+
|
|
32
|
+
## US Region
|
|
33
|
+
|
|
34
|
+
- **US primary**
|
|
35
|
+
- Replicates to: **US hot standby**
|
|
36
|
+
- Replicates to: **EU replica node**
|
|
37
|
+
- Replicates to: **Asia replica node**
|
|
38
|
+
|
|
39
|
+
- **EU replica**
|
|
40
|
+
- Source: async replication from **EU primary**
|
|
41
|
+
|
|
42
|
+
- **Asia replica**
|
|
43
|
+
- Source: async replication from **Asia primary**
|
|
44
|
+
|
|
45
|
+
- **US read models** built from:
|
|
46
|
+
- US primary
|
|
47
|
+
- EU replica
|
|
48
|
+
- Asia replica
|
|
49
|
+
|
|
50
|
+
## Asia Region
|
|
51
|
+
|
|
52
|
+
- **Asia primary**
|
|
53
|
+
- Replicates to: **Asia hot standby**
|
|
54
|
+
- Replicates to: **US replica node**
|
|
55
|
+
- Replicates to: **EU replica node**
|
|
56
|
+
|
|
57
|
+
- **EU replica**
|
|
58
|
+
- Source: async replication from **EU primary**
|
|
59
|
+
|
|
60
|
+
- **US replica**
|
|
61
|
+
- Source: async replication from **US primary**
|
|
62
|
+
|
|
63
|
+
- **Asia read models** built from:
|
|
64
|
+
- Asia primary
|
|
65
|
+
- EU replica
|
|
66
|
+
- US replica
|
|
67
|
+
|
|
68
|
+
From the example above:
|
|
69
|
+
- applications, connecting to primary node should have `:primary` [role](configuration.md#eventstore-role)
|
|
70
|
+
- applications, connecting to replica node should have `:replica` [role](configuration.md#eventstore-role)
|
|
71
|
+
- hot standby replica should utilize [streaming WAL](https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION) replication. This will be a full copy of region's primary node
|
|
72
|
+
- region replicas should utilize pg_eventstore replication via `#create_replication`. Example (from the perspective of
|
|
73
|
+
US region primary node):
|
|
74
|
+
```ruby
|
|
75
|
+
# config.rb
|
|
76
|
+
PgEventstore.configure do |config|
|
|
77
|
+
config.pg_uri = 'postgresql://postgres:postgres@localhost:6432/eventstore_us'
|
|
78
|
+
config.eventstore_role = :primary
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
PgEventstore.configure(name: :eu_replica) do |config|
|
|
82
|
+
config.pg_uri = 'postgresql://postgres:postgres@localhost:6432/eventstore_us'
|
|
83
|
+
config.eventstore_role = :replica
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
PgEventstore.configure(name: :asia_replica) do |config|
|
|
87
|
+
config.pg_uri = 'postgresql://postgres:postgres@localhost:6432/eventstore_us'
|
|
88
|
+
config.eventstore_role = :replica
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# subscriptions
|
|
92
|
+
subscriptions_manager = PgEventstore.subscriptions_manager(subscription_set: "Replications")
|
|
93
|
+
# Replicates US pg_eventstore into EU region pg_eventstore
|
|
94
|
+
subscriptions_manager.create_replication('EU', :eu_replica)
|
|
95
|
+
# Replicates US pg_eventstore into Asia region pg_eventstore
|
|
96
|
+
subscriptions_manager.create_replication('Asia', :asia_replica)
|
|
97
|
+
|
|
98
|
+
subscriptions_manager.start
|
|
99
|
+
```
|
data/docs/subscriptions.md
CHANGED
|
@@ -1,27 +1,46 @@
|
|
|
1
1
|
# Subscriptions
|
|
2
2
|
|
|
3
|
-
In order to process new events in your microservices you have to have the ability to listen for them. `pg_eventstore`
|
|
3
|
+
In order to process new events in your microservices you have to have the ability to listen for them. `pg_eventstore`
|
|
4
|
+
implements a subscription feature for this matter. It is implemented as a background thread that pulls new events
|
|
5
|
+
according to your filters from time to time (see `subscription_pull_interval` setting option under [**Configuration
|
|
6
|
+
**](configuration.md) chapter).
|
|
4
7
|
|
|
5
8
|
## PgEventstore::Subscription
|
|
6
9
|
|
|
7
|
-
`pg_eventstore` stores various subscription information in the database. The corresponding object that describes the
|
|
10
|
+
`pg_eventstore` stores various subscription information in the database. The corresponding object that describes the
|
|
11
|
+
database records is the `PgEventstore::Subscription` object. It is used in the `config.subscription_restart_terminator`
|
|
12
|
+
setting for example. You can find its attributes
|
|
13
|
+
summary [here](https://rubydoc.info/gems/pg_eventstore/PgEventstore/Subscription).
|
|
8
14
|
|
|
9
15
|
## PgEventstore::SubscriptionsSet
|
|
10
16
|
|
|
11
|
-
`pg_eventstore` also stores information about which subscriptions are set. The corresponding object that describes the
|
|
17
|
+
`pg_eventstore` also stores information about which subscriptions are set. The corresponding object that describes the
|
|
18
|
+
database records is `PgEventstore::SubscriptionsSet`. You can find its attributes
|
|
19
|
+
summary [here](https://rubydoc.info/gems/pg_eventstore/PgEventstore/SubscriptionsSet).
|
|
12
20
|
|
|
13
|
-
This record is created when you start your subscriptions. All subscriptions created using a single subscriptions manager
|
|
21
|
+
This record is created when you start your subscriptions. All subscriptions created using a single subscriptions manager
|
|
22
|
+
instance are locked using a single `PgEventstore::SubscriptionsSet`. When subscriptions are locked, they can't be
|
|
23
|
+
managed anywhere else. When you stop your subscriptions, the `PgEventstore::SubscriptionsSet` is deleted, unlocking the
|
|
24
|
+
subscriptions. The `SubscriptionSet` also holds information about the state, number of restarts, the restart interval
|
|
25
|
+
and last error of the background runner which is responsible for pulling the subscription's events. You can set the max
|
|
26
|
+
number of restarts and the restarts interval of your subscriptions set via `config.subscriptions_set_max_retries` and
|
|
27
|
+
`config.subscriptions_set_retries_interval` settings. See [**Configuration**](configuration.md) chapter for more info.
|
|
14
28
|
|
|
15
29
|
## Creating a subscription
|
|
16
30
|
|
|
17
|
-
First step you need to do is to create a `PgEventstore::SubscriptionsManager` object and provide the `subscription_set`
|
|
31
|
+
First step you need to do is to create a `PgEventstore::SubscriptionsManager` object and provide the `subscription_set`
|
|
32
|
+
keyword argument. Optionally you can provide a config name to use, override the `config.subscriptions_set_max_retries`
|
|
33
|
+
and `config.subscriptions_set_retries_interval` settings:
|
|
18
34
|
|
|
19
35
|
```ruby
|
|
20
36
|
subscriptions_manager = PgEventstore.subscriptions_manager(subscription_set: 'SubscriptionsOfMyAwesomeMicroservice')
|
|
21
|
-
another_subscriptions_manager = PgEventstore.subscriptions_manager(
|
|
37
|
+
another_subscriptions_manager = PgEventstore.subscriptions_manager(
|
|
38
|
+
:my_custom_config, subscription_set: 'SubscriptionsOfMyAwesomeMicroservice', max_retries: 5, retries_interval: 2
|
|
39
|
+
)
|
|
22
40
|
```
|
|
23
41
|
|
|
24
|
-
The required `subscription_set` option groups your subscriptions into a set. For example, you could refer to your
|
|
42
|
+
The required `subscription_set` option groups your subscriptions into a set. For example, you could refer to your
|
|
43
|
+
service's name in the subscription set name.
|
|
25
44
|
|
|
26
45
|
Now we can use the `#subscribe` method to create the subscription:
|
|
27
46
|
|
|
@@ -29,19 +48,9 @@ Now we can use the `#subscribe` method to create the subscription:
|
|
|
29
48
|
subscriptions_manager.subscribe('MyAwesomeSubscription', handler: proc { |event| puts event })
|
|
30
49
|
```
|
|
31
50
|
|
|
32
|
-
First argument is the subscription's name. **It must be unique within the subscriptions set**. Second argument is your
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
51
|
+
First argument is the subscription's name. **It must be unique within the subscriptions set**. Second argument is your
|
|
52
|
+
subscription's handler where you will be processing your events as they arrive. The example shows the minimum set of
|
|
53
|
+
arguments required to create the subscription.
|
|
45
54
|
|
|
46
55
|
After you added all necessary subscriptions, it is time to start them:
|
|
47
56
|
|
|
@@ -50,12 +59,17 @@ subscriptions_manager.start
|
|
|
50
59
|
# => PgEventstore::BasicRunner
|
|
51
60
|
```
|
|
52
61
|
|
|
53
|
-
After calling `#start` all subscriptions are locked behind the given subscriptions set and can't be locked by any other
|
|
62
|
+
After calling `#start` all subscriptions are locked behind the given subscriptions set and can't be locked by any other
|
|
63
|
+
subscriptions set. This measure is needed to prevent running the same subscription under the same subscription set using
|
|
64
|
+
different processes/subscription managers. Such situation will lead to a malformed subscription state and will break its
|
|
65
|
+
position, meaning the same event will be processed several times.
|
|
54
66
|
|
|
55
67
|
If, for some reason, you want to lock already locked subscription - you can provide `force_lock: true`:
|
|
56
68
|
|
|
57
69
|
```ruby
|
|
58
|
-
subscriptions_manager = PgEventstore.subscriptions_manager(
|
|
70
|
+
subscriptions_manager = PgEventstore.subscriptions_manager(
|
|
71
|
+
subscription_set: 'SubscriptionsOfMyAwesomeMicroservice', force_lock: true
|
|
72
|
+
)
|
|
59
73
|
subscriptions_manager.start
|
|
60
74
|
```
|
|
61
75
|
|
|
@@ -63,27 +77,28 @@ A complete example of the subscription setup process looks like this:
|
|
|
63
77
|
|
|
64
78
|
```ruby
|
|
65
79
|
PgEventstore.configure do |config|
|
|
66
|
-
config.pg_uri = ENV.fetch('PG_EVENTSTORE_URI') { 'postgresql://postgres:postgres@localhost:5532/eventstore' }
|
|
80
|
+
config.pg_uri = ENV.fetch('PG_EVENTSTORE_URI') { 'postgresql://postgres:postgres@localhost:5532/eventstore' }
|
|
67
81
|
end
|
|
68
82
|
|
|
69
83
|
subscriptions_manager = PgEventstore.subscriptions_manager(
|
|
70
84
|
subscription_set: 'MyAwesomeSubscriptions'
|
|
71
85
|
)
|
|
72
86
|
subscriptions_manager.subscribe(
|
|
73
|
-
'Foo events Subscription',
|
|
74
|
-
handler: proc { |event| p "Foo events Subscription: #{event.inspect}" },
|
|
87
|
+
'Foo events Subscription',
|
|
88
|
+
handler: proc { |event| p "Foo events Subscription: #{event.inspect}" },
|
|
75
89
|
options: { filter: { event_types: ['Foo'] } }
|
|
76
90
|
)
|
|
77
91
|
subscriptions_manager.subscribe(
|
|
78
92
|
'"BarCtx" context Subscription',
|
|
79
|
-
handler: proc { |event| p "'BarCtx' context Subscription: #{event.inspect}" },
|
|
80
|
-
options: { filter: { streams: [{ context: 'BarCtx' }] }
|
|
93
|
+
handler: proc { |event| p "'BarCtx' context Subscription: #{event.inspect}" },
|
|
94
|
+
options: { filter: { streams: [{ context: 'BarCtx' }] }
|
|
81
95
|
}
|
|
82
96
|
)
|
|
83
97
|
subscriptions_manager.start
|
|
84
98
|
```
|
|
85
99
|
|
|
86
|
-
Persist this script into a file(let's say `subscriptions.rb`). Now it is time to start the process which will be
|
|
100
|
+
Persist this script into a file(let's say `subscriptions.rb`). Now it is time to start the process which will be
|
|
101
|
+
processing those subscriptions. `pg_eventstore` has CLI for that purpose:
|
|
87
102
|
|
|
88
103
|
```bash
|
|
89
104
|
# -r ./subscriptions.rb will load our subscriptions definitions
|
|
@@ -105,15 +120,93 @@ PgEventstore.client.append_to_stream(foo_stream, PgEventstore::Event.new(type: '
|
|
|
105
120
|
PgEventstore.client.append_to_stream(bar_stream, PgEventstore::Event.new(type: 'Foo', data: { foo: :bar }))
|
|
106
121
|
```
|
|
107
122
|
|
|
108
|
-
You will then see the output of your subscription handlers. To gracefully stop the subscriptions process, use
|
|
123
|
+
You will then see the output of your subscription handlers. To gracefully stop the subscriptions process, use
|
|
124
|
+
`kill -TERM <pid>` command.
|
|
125
|
+
|
|
126
|
+
### Filtering events
|
|
127
|
+
|
|
128
|
+
To listen to the specific events you have to supply `:filter` option. Example:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
subscriptions_manager.subscribe(
|
|
132
|
+
'MyAwesomeSubscription',
|
|
133
|
+
handler: proc { |event| puts event },
|
|
134
|
+
options: { filter: { streams: [{ context: 'FooCtx' }], event_types: %w[Foo Bar] } }
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Filtering events of a subscription is the same as filtering events when [reading from "all" stream](reading_events.md#all-stream-filtering),
|
|
139
|
+
but with some additions:
|
|
140
|
+
- there is no limitations on markers filter. The next is all allowed:
|
|
141
|
+
```ruby
|
|
142
|
+
# Listen to all events from 'FooCtx' context, marked with 'foo' marker
|
|
143
|
+
subscriptions_manager.subscribe(
|
|
144
|
+
'Sub1',
|
|
145
|
+
handler: proc { |event| puts event },
|
|
146
|
+
options: { filter: { streams: [{ context: 'FooCtx' }], event_types: [{ markers: ['foo'] }] } }
|
|
147
|
+
)
|
|
148
|
+
# Listen to all events from 'FooCtx' context & 'Foo' stream name, marked with 'foo' marker
|
|
149
|
+
subscriptions_manager.subscribe(
|
|
150
|
+
'Sub2',
|
|
151
|
+
handler: proc { |event| puts event },
|
|
152
|
+
options: { filter: { streams: [{ context: 'FooCtx', stream_name: 'Foo' }], event_types: [{ markers: ['foo'] }] } }
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
- you can filter event types by prefixes. In the next example the subscription will catch all events with event types
|
|
156
|
+
starting from `'Foo'`(e.g. `'FooBar'`, `'Foo1'`, `'Foosomethingelse'`, etc):
|
|
157
|
+
```ruby
|
|
158
|
+
# Listen to all events from 'FooCtx' context, marked with 'foo' marker
|
|
159
|
+
subscriptions_manager.subscribe(
|
|
160
|
+
'Sub1',
|
|
161
|
+
handler: proc { |event| puts event },
|
|
162
|
+
options: { filter: { streams: [{ context: 'FooCtx' }], event_types: [{ prefix: 'Foo' }] } }
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Subscription position
|
|
167
|
+
|
|
168
|
+
Internally, each event has its own subscription position assigned. This happens in the background process with
|
|
169
|
+
`config.events_subscription_position_update_interval` interval(in seconds) whenever you start any subscription. The
|
|
170
|
+
algorithm of assigning of event subscription position is deterministic and events processing by subscriptions based on
|
|
171
|
+
that value is idempotent. The event subscription position ordering may differ from `Event#global_position` ordering, but
|
|
172
|
+
it correlates with a stream revision ordering inside a single stream, meaning that an event with stream revision `0`
|
|
173
|
+
can't be processed earlier than event with stream revision `1` if they both belong to the same stream.
|
|
174
|
+
|
|
175
|
+
Let's say you never run subscriptions, and you have now plenty of events without an assigned subscription position. What
|
|
176
|
+
will happen if you start any subscription? The worker which is responsible for assigning those positions will need to
|
|
177
|
+
process every existing event. It does so in batches of 100k records(current implementation), each batch takes 1-2
|
|
178
|
+
seconds to finish(depending on your hardware). Thus, if your subscription targets some events farther from the beginning
|
|
179
|
+
of your events history - it will require some time to get there.
|
|
180
|
+
|
|
181
|
+
#### Setting starting position
|
|
182
|
+
|
|
183
|
+
You can set the initial position of new subscription to start with:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
subscriptions_manager.subscribe(
|
|
187
|
+
'MyAwesomeSubscription',
|
|
188
|
+
handler: proc { |event| puts event },
|
|
189
|
+
options: { from_position: 100 }
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
This allows to jump to the certain position to start with and skip unwanted events. Please note that **this is not** an
|
|
194
|
+
`Event#global_position` value, but rather subscription position of the event. You can find corresponding subscription
|
|
195
|
+
position of an event in admin web UI.
|
|
196
|
+
|
|
197
|
+
Unlike `:from_position` of Read API - `:from_position` in Subscriptions API is **exclusive**. For example, if you have
|
|
198
|
+
events with positions 100, 101 and 102, setting `:from_position` to 100 will result in processing event at position 101.
|
|
109
199
|
|
|
110
200
|
## Overriding Subscription config values
|
|
111
201
|
|
|
112
|
-
You can override `subscription_pull_interval`, `subscription_max_retries`, `subscription_retries_interval`,
|
|
202
|
+
You can override `subscription_pull_interval`, `subscription_max_retries`, `subscription_retries_interval`,
|
|
203
|
+
`subscription_restart_terminator`, `failed_subscription_notifier` and `subscription_graceful_shutdown_timeout` config
|
|
204
|
+
values (see [**Configuration**](configuration.md) chapter for details) for the specific subscription by providing the
|
|
205
|
+
corresponding arguments. Example:
|
|
113
206
|
|
|
114
207
|
```ruby
|
|
115
208
|
subscriptions_manager.subscribe(
|
|
116
|
-
'MyAwesomeSubscription',
|
|
209
|
+
'MyAwesomeSubscription',
|
|
117
210
|
handler: proc { |event| puts event },
|
|
118
211
|
# overrides config.subscription_pull_interval
|
|
119
212
|
pull_interval: 0.5,
|
|
@@ -126,13 +219,33 @@ subscriptions_manager.subscribe(
|
|
|
126
219
|
# overrides config.failed_subscription_notifier
|
|
127
220
|
failed_subscription_notifier: proc { |_subscription, err| p err },
|
|
128
221
|
# overrides config.subscription_graceful_shutdown_timeout
|
|
129
|
-
graceful_shutdown_timeout: 20
|
|
222
|
+
graceful_shutdown_timeout: 20,
|
|
223
|
+
# Yield array of events into your handler instead a single event. See example bellow.
|
|
224
|
+
in_batches: true
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Processing events in batches
|
|
229
|
+
|
|
230
|
+
There is an ability to tell the subscription to yield an array of events it pulled last time. The number of events is
|
|
231
|
+
the implementation specific(current implementation may yield up to 1k events) and can't be adjusted. Example:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
subscriptions_manager.subscribe(
|
|
235
|
+
'MyAwesomeSubscription',
|
|
236
|
+
handler: proc { |events| puts events }, # => outputs array of events
|
|
237
|
+
in_batches: true
|
|
130
238
|
)
|
|
131
239
|
```
|
|
132
240
|
|
|
241
|
+
In this case the subscription's position will be advanced to the position of the last event of the chunk. Please note
|
|
242
|
+
that in case of failure the whole chunk will be yielded again during subscription retries phase.
|
|
243
|
+
|
|
133
244
|
## Middlewares
|
|
134
245
|
|
|
135
|
-
If you would like to skip some of your registered middlewares from processing events after they are being pulled by the
|
|
246
|
+
If you would like to skip some of your registered middlewares from processing events after they are being pulled by the
|
|
247
|
+
subscription, you should use the `:middlewares` argument which allows you to override the list of middlewares you would
|
|
248
|
+
like to use.
|
|
136
249
|
|
|
137
250
|
Let's say you have these registered middlewares:
|
|
138
251
|
|
|
@@ -142,7 +255,8 @@ PgEventstore.configure do |config|
|
|
|
142
255
|
end
|
|
143
256
|
```
|
|
144
257
|
|
|
145
|
-
And you want to skip `FooMiddleware` and `BazMiddleware`. You simply have to provide an array of corresponding
|
|
258
|
+
And you want to skip `FooMiddleware` and `BazMiddleware`. You simply have to provide an array of corresponding
|
|
259
|
+
middleware keys you would like to use when creating the subscription:
|
|
146
260
|
|
|
147
261
|
```ruby
|
|
148
262
|
subscriptions_manager.subscribe('MyAwesomeSubscription', handler: proc { |event| puts event }, middlewares: %i[bar])
|
|
@@ -152,4 +266,20 @@ See the [Writing middleware](writing_middleware.md) chapter for info about what
|
|
|
152
266
|
|
|
153
267
|
## How many subscriptions I should put in one process?
|
|
154
268
|
|
|
155
|
-
It depends on the nature of your subscription handlers. If they spend more time on ruby code execution than on IO
|
|
269
|
+
It depends on the nature of your subscription handlers. If they spend more time on ruby code execution than on IO
|
|
270
|
+
operations, you should limit the number of subscriptions per single process. This can be especially noticed when you
|
|
271
|
+
rebuild the read models of your microservice, processing all events from the start.
|
|
272
|
+
|
|
273
|
+
## Few words about a subscription handler
|
|
274
|
+
|
|
275
|
+
It is recommended your subscription handler be idempotent. The subscription implementation tries hard not to process the
|
|
276
|
+
same event multiple times. The internal implementation is next:
|
|
277
|
+
|
|
278
|
+
- fetch an event from the database
|
|
279
|
+
- pass it into subscription's handler
|
|
280
|
+
- advance the subscription's position
|
|
281
|
+
|
|
282
|
+
These steps are not atomic, and anything can happen in between the second and the third steps. In this case the same
|
|
283
|
+
event may be processed multiple times until the whole process succeeds. In this case the implementation guarantees the
|
|
284
|
+
processed event will never be processed again after the subscription's position was persisted (unless you reset the
|
|
285
|
+
subscription position of course).
|
data/docs/writing_middleware.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Writing middleware
|
|
2
2
|
|
|
3
|
-
Middlewares are objects that modify events before they are appended to a stream, or right after they are read from a
|
|
3
|
+
Middlewares are objects that modify events before they are appended to a stream, or right after they are read from a
|
|
4
|
+
stream. Middleware object must respond to `#serialize` and `#deserialize` methods. The `#serialize` method is called
|
|
5
|
+
each time an event is going to be appended. The `#deserialize` method is called each time an event is read from a
|
|
6
|
+
stream. There are two ways how you can define a middleware:
|
|
4
7
|
|
|
5
|
-
- by defining your class and including `PgEventstore::Middleware` module in it. This way you can override only one of
|
|
8
|
+
- by defining your class and including `PgEventstore::Middleware` module in it. This way you can override only one of
|
|
9
|
+
its methods, or both of them. Example:
|
|
6
10
|
|
|
7
11
|
```ruby
|
|
8
12
|
# Override #serialize only to define your custom logic
|
|
@@ -38,11 +42,15 @@ end
|
|
|
38
42
|
|
|
39
43
|
# Configure your middlewares
|
|
40
44
|
PgEventstore.configure do |config|
|
|
41
|
-
config.middlewares = {
|
|
45
|
+
config.middlewares = {
|
|
46
|
+
my_awesome_serializer: MyAwesomeSerializer.new,
|
|
47
|
+
my_awesome_deserializer: MyAwesomeDeserializer.new,
|
|
48
|
+
my_awesome_middleware: MyAwesomeMiddleware.new
|
|
49
|
+
}
|
|
42
50
|
end
|
|
43
51
|
```
|
|
44
52
|
|
|
45
|
-
- implement your own object that implements `#serialize` and `#deserialize` methods. Example:
|
|
53
|
+
- implement your own object that implements `#serialize` and `#deserialize` methods. Example:
|
|
46
54
|
|
|
47
55
|
```ruby
|
|
48
56
|
require 'securerandom'
|
|
@@ -99,7 +107,7 @@ class ExtractLargePayload
|
|
|
99
107
|
def resolve_large_payload(payload_key)
|
|
100
108
|
JSON.parse(Faraday.get("https://my.awesome.api/api/large_payload", { payload_key: payload_key }).body)['value']
|
|
101
109
|
end
|
|
102
|
-
end
|
|
110
|
+
end
|
|
103
111
|
end
|
|
104
112
|
|
|
105
113
|
# Configure our middlewares
|
|
@@ -127,9 +135,13 @@ PgEventstore.client.read(stream).last
|
|
|
127
135
|
|
|
128
136
|
## Remarks
|
|
129
137
|
|
|
130
|
-
It is important to know that `pg_eventstore` may retry commands. In that case `#serialize` and `#deserialize` methods
|
|
138
|
+
It is important to know that `pg_eventstore` may retry commands. In that case `#serialize` and `#deserialize` methods
|
|
139
|
+
may also be retried. You have to make sure that the implementation of `#serialize` and `#deserialize` always returns the
|
|
140
|
+
same result for the same input, and it does not create duplications. Let's look at the `#serialize` implementation from
|
|
141
|
+
the example above:
|
|
131
142
|
|
|
132
143
|
```ruby
|
|
144
|
+
|
|
133
145
|
def serialize(event)
|
|
134
146
|
return if event.fields_with_large_payloads.empty?
|
|
135
147
|
|
|
@@ -153,8 +165,13 @@ def extract_large_payload_async(field_name, value)
|
|
|
153
165
|
end
|
|
154
166
|
```
|
|
155
167
|
|
|
156
|
-
Private method `#extract_large_payload_async` should return the same result when passing the same `field_name` and
|
|
168
|
+
Private method `#extract_large_payload_async` should return the same result when passing the same `field_name` and
|
|
169
|
+
`value` arguments values, and `POST https://my.awesome.api/api/extract_large_payload` may not want to produce duplicates
|
|
170
|
+
when called multiple times with the same payload.
|
|
157
171
|
|
|
158
172
|
## Async vs Sync implementation
|
|
159
173
|
|
|
160
|
-
You may notice that the extracting of a large payload is asynchronous in the example above. It is recommended approach
|
|
174
|
+
You may notice that the extracting of a large payload is asynchronous in the example above. It is recommended approach
|
|
175
|
+
of the implementation of `#serialize` method to increase overall performance. But if it hard for you to guarantee the
|
|
176
|
+
persistence of a payload value - you can go with sync approach, thus not allowing event to be persisted if a payload
|
|
177
|
+
extraction request fails.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#include "ruby.h"
|
|
2
|
+
#include "ruby/st.h"
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Loops through the given array of partition ids to find an index at which we have up to max_partitions_per_call unique
|
|
6
|
+
partitions. Ruby representation of this implementation looks like this:
|
|
7
|
+
def range_to_slice2(partition_ids, max_partitions_per_call)
|
|
8
|
+
return (0..) if partition_ids.size <= max_partitions_per_call
|
|
9
|
+
|
|
10
|
+
partitions_map = {}
|
|
11
|
+
latest_index = nil
|
|
12
|
+
partition_ids.each_with_index do |partition_id, index|
|
|
13
|
+
partitions_map[partition_id] = true
|
|
14
|
+
if partitions_map.size > max_partitions_per_call
|
|
15
|
+
latest_index = index - 1
|
|
16
|
+
break
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
0..latest_index
|
|
20
|
+
end
|
|
21
|
+
It was extracted into C extension because of performance of ruby loops - they are deadly slow comparing to C loops.
|
|
22
|
+
The performance gain is x5-x15 times faster comparing to ruby implementation. Because we need to do this operation on
|
|
23
|
+
each read command - it worth having it here.
|
|
24
|
+
*/
|
|
25
|
+
static VALUE
|
|
26
|
+
range_to_slice(VALUE self, VALUE partition_ids, VALUE max_partitions_per_call)
|
|
27
|
+
{
|
|
28
|
+
long partition_ids_size = RARRAY_LEN(partition_ids);
|
|
29
|
+
const VALUE *partition_id_values;
|
|
30
|
+
long max_partitions = NUM2LONG(max_partitions_per_call);
|
|
31
|
+
st_table *unique_partition_ids;
|
|
32
|
+
long unique_partitions = 0;
|
|
33
|
+
long index;
|
|
34
|
+
VALUE latest_index = Qnil;
|
|
35
|
+
|
|
36
|
+
if (partition_ids_size <= max_partitions) {
|
|
37
|
+
return rb_range_new(INT2FIX(0), Qnil, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
partition_id_values = RARRAY_CONST_PTR(partition_ids);
|
|
41
|
+
unique_partition_ids = st_init_numtable();
|
|
42
|
+
|
|
43
|
+
for (index = 0; index < partition_ids_size; index++) {
|
|
44
|
+
st_data_t partition_id = (st_data_t)partition_id_values[index];
|
|
45
|
+
|
|
46
|
+
if (!st_lookup(unique_partition_ids, partition_id, NULL)) {
|
|
47
|
+
st_insert(unique_partition_ids, partition_id, (st_data_t)1);
|
|
48
|
+
unique_partitions++;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (unique_partitions > max_partitions) {
|
|
52
|
+
latest_index = LONG2NUM(index - 1);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
st_free_table(unique_partition_ids);
|
|
58
|
+
|
|
59
|
+
return rb_range_new(INT2FIX(0), latest_index, 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
void
|
|
63
|
+
Init_pg_eventstore_ext(void)
|
|
64
|
+
{
|
|
65
|
+
VALUE pg_eventstore = rb_define_module("PgEventstore");
|
|
66
|
+
VALUE utils = rb_define_class_under(pg_eventstore, "Utils", rb_cObject);
|
|
67
|
+
|
|
68
|
+
rb_define_singleton_method(
|
|
69
|
+
utils,
|
|
70
|
+
"range_to_slice",
|
|
71
|
+
range_to_slice,
|
|
72
|
+
2
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgEventstore
|
|
4
|
+
class AsyncRunner
|
|
5
|
+
class Cancellation < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@jobs = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [Integer]
|
|
13
|
+
def jobs_size
|
|
14
|
+
@jobs.size
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [void]
|
|
18
|
+
def run
|
|
19
|
+
loop do
|
|
20
|
+
break if @jobs.empty?
|
|
21
|
+
|
|
22
|
+
run_once
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run_once
|
|
27
|
+
return if @jobs.empty?
|
|
28
|
+
|
|
29
|
+
@jobs.keys.each do |job|
|
|
30
|
+
@jobs[job] = true
|
|
31
|
+
job.resume
|
|
32
|
+
@jobs.delete(job) unless job.alive?
|
|
33
|
+
end
|
|
34
|
+
rescue
|
|
35
|
+
@jobs.each do |job, started|
|
|
36
|
+
next unless job.alive?
|
|
37
|
+
|
|
38
|
+
if started
|
|
39
|
+
begin
|
|
40
|
+
job.raise(Cancellation)
|
|
41
|
+
rescue StandardError
|
|
42
|
+
# Because the rescue mechanisms inside the terminating job can potentially raise - catch them here
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
# Kill those jobs which survived our Cancellation exception
|
|
48
|
+
job.kill if job.alive?
|
|
49
|
+
rescue StandardError
|
|
50
|
+
# Ensure we handle any errors inside an exception handler(e.g. ensure block) of the given job
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@jobs.clear
|
|
55
|
+
raise
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [void]
|
|
59
|
+
def async(&)
|
|
60
|
+
@jobs[Fiber.new(&)] = false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|