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.
Files changed (303) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -1
  3. data/CHANGELOG.md +186 -20
  4. data/README.md +25 -15
  5. data/db/migrations/10_setup_pg_cron.rb +1 -23
  6. data/db/migrations/15_create_streams_global_index.sql +17 -0
  7. data/db/migrations/16_populate_streams_global_index.rb +150 -0
  8. data/db/migrations/17_create_events_global_index.sql +62 -0
  9. data/db/migrations/18_populate_events_global_index.rb +124 -0
  10. data/db/migrations/19_populate_events_global_index_p2.rb +25 -0
  11. data/db/migrations/20_migrate_subscriptions_current_position.sql +19 -0
  12. data/db/migrations/21_create_partitions_stats.sql +2 -0
  13. data/db/migrations/22_drop_streams_sql_view.sql +2 -0
  14. data/db/migrations/23_adjust_partitions_indexes.sql +5 -0
  15. data/db/migrations/24_drop_legacy_events_indexes.sql +3 -0
  16. data/db/migrations/25_add_parent_partitions_info_to_partitions.sql +16 -0
  17. data/db/migrations/26_change_events_stream_revision_type.rb +35 -0
  18. data/db/migrations/27_drop_events_horizon.sql +3 -0
  19. data/db/migrations/28_create_maintenance_tasks.sql +8 -0
  20. data/db/migrations/29_create_event_markers.sql +35 -0
  21. data/db/migrations/30_improve_partitions_search_in_admin_ui.rb +24 -0
  22. data/db/migrations/31_drop_events_id_default_value.sql +1 -0
  23. data/db/migrations/32_create_parittions_triggers.sql +101 -0
  24. data/docs/AGENTS.fragment.md +696 -0
  25. data/docs/admin_ui.md +3 -1
  26. data/docs/appending_events.md +110 -49
  27. data/docs/configuration.md +69 -36
  28. data/docs/event_tracing.md +55 -0
  29. data/docs/events_and_streams.md +54 -30
  30. data/docs/how_it_works.md +57 -16
  31. data/docs/linking_events.md +2 -1
  32. data/docs/maintenance.md +14 -5
  33. data/docs/multiple_commands.md +17 -8
  34. data/docs/reading_events.md +208 -43
  35. data/docs/reading_streams.md +50 -0
  36. data/docs/replication.md +99 -0
  37. data/docs/subscriptions.md +165 -35
  38. data/docs/writing_middleware.md +25 -8
  39. data/ext/pg_eventstore_ext/extconf.rb +5 -0
  40. data/ext/pg_eventstore_ext/pg_eventstore_ext.c +74 -0
  41. data/lib/pg_eventstore/async_runner.rb +63 -0
  42. data/lib/pg_eventstore/basic_config.rb +15 -0
  43. data/lib/pg_eventstore/chunks/chunk.rb +31 -0
  44. data/lib/pg_eventstore/chunks/read_api_events_index_chunk.rb +83 -0
  45. data/lib/pg_eventstore/chunks/replica_events_index_chunk.rb +35 -0
  46. data/lib/pg_eventstore/chunks/repository.rb +73 -0
  47. data/lib/pg_eventstore/chunks/subscription_checkpoint_chunk.rb +48 -0
  48. data/lib/pg_eventstore/chunks/subscription_events_index_chunk.rb +99 -0
  49. data/lib/pg_eventstore/chunks.rb +14 -0
  50. data/lib/pg_eventstore/client.rb +163 -50
  51. data/lib/pg_eventstore/commands/append.rb +62 -43
  52. data/lib/pg_eventstore/commands/delete_event.rb +7 -8
  53. data/lib/pg_eventstore/commands/delete_stream.rb +3 -3
  54. data/lib/pg_eventstore/commands/event_modifiers/prepare_link_event.rb +9 -7
  55. data/lib/pg_eventstore/commands/event_modifiers/prepare_regular_event.rb +12 -7
  56. data/lib/pg_eventstore/commands/link_to.rb +5 -5
  57. data/lib/pg_eventstore/commands/read.rb +23 -5
  58. data/lib/pg_eventstore/commands/read_grouped.rb +46 -0
  59. data/lib/pg_eventstore/commands/read_streams.rb +18 -0
  60. data/lib/pg_eventstore/commands/read_streams_paginated.rb +56 -0
  61. data/lib/pg_eventstore/commands/regular_stream_read_paginated.rb +4 -4
  62. data/lib/pg_eventstore/commands/revision_check/current_revision.rb +51 -0
  63. data/lib/pg_eventstore/commands/revision_check/event_type_revisions_comparison.rb +46 -0
  64. data/lib/pg_eventstore/commands/revision_check/expected_revision.rb +135 -0
  65. data/lib/pg_eventstore/commands/revision_check/stream_revision_check.rb +98 -0
  66. data/lib/pg_eventstore/commands/revision_check/stream_revision_comparison.rb +36 -0
  67. data/lib/pg_eventstore/commands/stream_revision.rb +14 -0
  68. data/lib/pg_eventstore/commands/system_stream_read_paginated.rb +4 -4
  69. data/lib/pg_eventstore/commands.rb +4 -2
  70. data/lib/pg_eventstore/config.rb +30 -9
  71. data/lib/pg_eventstore/connection.rb +21 -2
  72. data/lib/pg_eventstore/errors.rb +136 -10
  73. data/lib/pg_eventstore/event.rb +34 -2
  74. data/lib/pg_eventstore/event_deserializer.rb +5 -4
  75. data/lib/pg_eventstore/event_global_index.rb +118 -0
  76. data/lib/pg_eventstore/event_marker.rb +16 -0
  77. data/lib/pg_eventstore/event_marker_index.rb +25 -0
  78. data/lib/pg_eventstore/event_serializer.rb +6 -0
  79. data/lib/pg_eventstore/extensions/acts_as_configurable.rb +88 -0
  80. data/lib/pg_eventstore/extensions/options_defaults.rb +25 -0
  81. data/lib/pg_eventstore/extensions/options_extension.rb +2 -2
  82. data/lib/pg_eventstore/feature_marker.rb +18 -0
  83. data/lib/pg_eventstore/maintenance.rb +11 -6
  84. data/lib/pg_eventstore/middleware/event_tracing.rb +53 -0
  85. data/lib/pg_eventstore/partition.rb +6 -0
  86. data/lib/pg_eventstore/pg_connection.rb +70 -14
  87. data/lib/pg_eventstore/queries/event_marker_queries.rb +74 -0
  88. data/lib/pg_eventstore/queries/event_queries.rb +7 -93
  89. data/lib/pg_eventstore/queries/events_global_index_queries.rb +131 -0
  90. data/lib/pg_eventstore/queries/index_filtering_queries.rb +179 -0
  91. data/lib/pg_eventstore/queries/links_resolver.rb +4 -4
  92. data/lib/pg_eventstore/queries/maintenance_queries.rb +159 -42
  93. data/lib/pg_eventstore/queries/partition_queries.rb +45 -103
  94. data/lib/pg_eventstore/queries/replica_queries.rb +110 -0
  95. data/lib/pg_eventstore/queries/streams_global_index_queries.rb +163 -0
  96. data/lib/pg_eventstore/queries/transaction_queries.rb +52 -10
  97. data/lib/pg_eventstore/queries.rb +29 -1
  98. data/lib/pg_eventstore/query_builders/basic_filtering.rb +38 -5
  99. data/lib/pg_eventstore/query_builders/event_markers_filtering.rb +47 -0
  100. data/lib/pg_eventstore/query_builders/event_markers_index_filtering.rb +275 -0
  101. data/lib/pg_eventstore/query_builders/event_subscription_positions_filtering.rb +37 -0
  102. data/lib/pg_eventstore/query_builders/events_filtering.rb +15 -164
  103. data/lib/pg_eventstore/query_builders/events_global_index_filtering.rb +227 -0
  104. data/lib/pg_eventstore/query_builders/filters/collection.rb +324 -0
  105. data/lib/pg_eventstore/query_builders/filters/event_type_filter.rb +37 -0
  106. data/lib/pg_eventstore/query_builders/filters/filter_row.rb +54 -0
  107. data/lib/pg_eventstore/query_builders/filters/marker_filter.rb +20 -0
  108. data/lib/pg_eventstore/query_builders/filters/marker_filter_row.rb +49 -0
  109. data/lib/pg_eventstore/query_builders/filters/stream_filter.rb +48 -0
  110. data/lib/pg_eventstore/query_builders/index_based_events_filtering.rb +260 -0
  111. data/lib/pg_eventstore/query_builders/partitions_filtering.rb +175 -51
  112. data/lib/pg_eventstore/query_builders/read_cursor/stream_cursor.rb +122 -0
  113. data/lib/pg_eventstore/query_builders/streams_global_index_filtering.rb +83 -0
  114. data/lib/pg_eventstore/query_builders/subscription_events_filtering.rb +79 -0
  115. data/lib/pg_eventstore/query_strategy/async.rb +64 -0
  116. data/lib/pg_eventstore/query_strategy/foreground.rb +25 -0
  117. data/lib/pg_eventstore/query_strategy.rb +19 -0
  118. data/lib/pg_eventstore/raw_event.rb +46 -0
  119. data/lib/pg_eventstore/rspec/test_helpers.rb +21 -8
  120. data/lib/pg_eventstore/sql_builder.rb +87 -8
  121. data/lib/pg_eventstore/stream.rb +13 -16
  122. data/lib/pg_eventstore/stream_global_index.rb +28 -0
  123. data/lib/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rb +5 -17
  124. data/lib/pg_eventstore/subscriptions/callback_handlers/events_subscription_position_worker_handlers.rb +36 -0
  125. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rb +12 -0
  126. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +21 -7
  127. data/lib/pg_eventstore/subscriptions/events_processor.rb +20 -18
  128. data/lib/pg_eventstore/subscriptions/events_processor_consumer/multiple.rb +63 -0
  129. data/lib/pg_eventstore/subscriptions/events_processor_consumer/replica.rb +68 -0
  130. data/lib/pg_eventstore/subscriptions/events_processor_consumer/single.rb +62 -0
  131. data/lib/pg_eventstore/subscriptions/events_processor_consumer.rb +44 -0
  132. data/lib/pg_eventstore/subscriptions/events_subscription_position_worker.rb +68 -0
  133. data/lib/pg_eventstore/subscriptions/queries/event_subscription_position_queries.rb +177 -0
  134. data/lib/pg_eventstore/subscriptions/queries/subscription_queries.rb +78 -54
  135. data/lib/pg_eventstore/subscriptions/replica_subscription_handler.rb +140 -0
  136. data/lib/pg_eventstore/subscriptions/replica_subscription_runner.rb +9 -0
  137. data/lib/pg_eventstore/subscriptions/subscription.rb +2 -1
  138. data/lib/pg_eventstore/subscriptions/subscription_feed_strategies/collection.rb +36 -0
  139. data/lib/pg_eventstore/subscriptions/subscription_feed_strategies/index_read_strategy.rb +82 -0
  140. data/lib/pg_eventstore/subscriptions/subscription_feed_strategies/replication_strategy.rb +75 -0
  141. data/lib/pg_eventstore/subscriptions/subscription_feed_strategy.rb +31 -0
  142. data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +24 -0
  143. data/lib/pg_eventstore/subscriptions/subscription_handler_performance.rb +4 -2
  144. data/lib/pg_eventstore/subscriptions/subscription_runner.rb +20 -9
  145. data/lib/pg_eventstore/subscriptions/subscription_runner_commands/reset_position.rb +1 -1
  146. data/lib/pg_eventstore/subscriptions/subscription_runners_feeder.rb +9 -18
  147. data/lib/pg_eventstore/subscriptions/subscriptions_lifecycle.rb +0 -12
  148. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +74 -14
  149. data/lib/pg_eventstore/{subscriptions/synchronized_array.rb → synchronized_array.rb} +15 -0
  150. data/lib/pg_eventstore/tasks/setup.rake +2 -2
  151. data/lib/pg_eventstore/utils.rb +31 -0
  152. data/lib/pg_eventstore/version.rb +1 -1
  153. data/lib/pg_eventstore/web/application.rb +100 -16
  154. data/lib/pg_eventstore/web/paginator/base_collection.rb +5 -1
  155. data/lib/pg_eventstore/web/paginator/event_types_collection.rb +14 -4
  156. data/lib/pg_eventstore/web/paginator/events_collection.rb +29 -33
  157. data/lib/pg_eventstore/web/paginator/helpers.rb +16 -9
  158. data/lib/pg_eventstore/web/paginator/markers_collection.rb +48 -0
  159. data/lib/pg_eventstore/web/paginator/stream_contexts_collection.rb +13 -2
  160. data/lib/pg_eventstore/web/paginator/stream_ids_collection.rb +21 -11
  161. data/lib/pg_eventstore/web/paginator/stream_names_collection.rb +18 -4
  162. data/lib/pg_eventstore/web/paginator/streams_collection.rb +95 -0
  163. data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +49 -22
  164. data/lib/pg_eventstore/web/subscriptions/set_collection.rb +1 -1
  165. data/lib/pg_eventstore/web/subscriptions/subscriptions.rb +1 -1
  166. data/lib/pg_eventstore/web/subscriptions/with_state/set_collection.rb +1 -1
  167. data/lib/pg_eventstore/web/subscriptions/with_state/subscriptions.rb +1 -1
  168. data/lib/pg_eventstore/web/views/home/dashboard.erb +11 -9
  169. data/lib/pg_eventstore/web/views/home/partials/event_filter.erb +18 -5
  170. data/lib/pg_eventstore/web/views/home/partials/events.erb +38 -1
  171. data/lib/pg_eventstore/web/views/home/partials/markers_filter.erb +18 -0
  172. data/lib/pg_eventstore/web/views/layouts/application.erb +6 -0
  173. data/lib/pg_eventstore/web/views/streams/index.erb +110 -0
  174. data/lib/pg_eventstore/web/views/streams/partials/streams.erb +11 -0
  175. data/lib/pg_eventstore/web/views/subscriptions/index.erb +4 -1
  176. data/lib/pg_eventstore/web.rb +2 -0
  177. data/lib/pg_eventstore.rb +23 -60
  178. data/pg_eventstore.gemspec +6 -4
  179. data/sig/interfaces/_raw_events_handler.rbs +3 -0
  180. data/sig/interfaces/subscription_handler.rbs +1 -1
  181. data/sig/pg/result.rbs +9 -0
  182. data/sig/pg_eventstore/async_runner.rbs +13 -0
  183. data/sig/pg_eventstore/basic_config.rbs +9 -0
  184. data/sig/pg_eventstore/chunks/chunk.rbs +13 -0
  185. data/sig/pg_eventstore/chunks/read_api_events_index_chunk.rbs +30 -0
  186. data/sig/pg_eventstore/chunks/replica_events_index_chunk.rbs +11 -0
  187. data/sig/pg_eventstore/chunks/repository.rbs +22 -0
  188. data/sig/pg_eventstore/chunks/subscription_checkpoint_chunk.rbs +18 -0
  189. data/sig/pg_eventstore/chunks/subscription_events_index_chunk.rbs +37 -0
  190. data/sig/pg_eventstore/client.rbs +40 -42
  191. data/sig/pg_eventstore/commands/append.rbs +11 -25
  192. data/sig/pg_eventstore/commands/event_modifiers/prepare_link_event.rbs +9 -12
  193. data/sig/pg_eventstore/commands/event_modifiers/prepare_regular_event.rbs +5 -4
  194. data/sig/pg_eventstore/commands/link_to.rbs +6 -11
  195. data/sig/pg_eventstore/commands/read.rbs +2 -5
  196. data/sig/pg_eventstore/commands/read_grouped.rbs +7 -0
  197. data/sig/pg_eventstore/commands/read_streams.rbs +7 -0
  198. data/sig/pg_eventstore/commands/read_streams_paginated.rbs +16 -0
  199. data/sig/pg_eventstore/commands/regular_stream_read_paginated.rbs +9 -17
  200. data/sig/pg_eventstore/commands/revision_check/current_revision.rbs +25 -0
  201. data/sig/pg_eventstore/commands/revision_check/event_type_revisions_comparison.rbs +14 -0
  202. data/sig/pg_eventstore/commands/revision_check/expected_revision.rbs +67 -0
  203. data/sig/pg_eventstore/commands/revision_check/stream_revision_check.rbs +20 -0
  204. data/sig/pg_eventstore/commands/revision_check/stream_revision_comparison.rbs +9 -0
  205. data/sig/pg_eventstore/commands/stream_revision.rbs +7 -0
  206. data/sig/pg_eventstore/commands/system_stream_read_paginated.rbs +9 -14
  207. data/sig/pg_eventstore/config.rbs +15 -6
  208. data/sig/pg_eventstore/connection.rbs +11 -18
  209. data/sig/pg_eventstore/errors.rbs +64 -34
  210. data/sig/pg_eventstore/event.rbs +16 -2
  211. data/sig/pg_eventstore/event_deserializer.rbs +5 -10
  212. data/sig/pg_eventstore/event_global_index.rbs +60 -0
  213. data/sig/pg_eventstore/event_marker.rbs +9 -0
  214. data/sig/pg_eventstore/event_marker_index.rbs +12 -0
  215. data/sig/pg_eventstore/extensions/acts_as_configurable.rbs +29 -0
  216. data/sig/pg_eventstore/extensions/options_defaults.rbs +7 -0
  217. data/sig/pg_eventstore/feature_marker.rbs +10 -0
  218. data/sig/pg_eventstore/maintenance.rbs +12 -8
  219. data/sig/pg_eventstore/middleware/event_trace.rbs +14 -0
  220. data/sig/pg_eventstore/partition.rbs +2 -4
  221. data/sig/pg_eventstore/pg_connection.rbs +8 -0
  222. data/sig/pg_eventstore/queries/event_marker_queries.rbs +21 -0
  223. data/sig/pg_eventstore/queries/event_queries.rbs +4 -36
  224. data/sig/pg_eventstore/queries/events_global_index_queries.rbs +37 -0
  225. data/sig/pg_eventstore/queries/index_filtering_queries.rbs +50 -0
  226. data/sig/pg_eventstore/queries/links_resolver.rbs +5 -9
  227. data/sig/pg_eventstore/queries/maintenance_queries.rbs +12 -9
  228. data/sig/pg_eventstore/queries/partition_queries.rbs +32 -67
  229. data/sig/pg_eventstore/queries/replica_queries.rbs +31 -0
  230. data/sig/pg_eventstore/queries/streams_global_index_queries.rbs +39 -0
  231. data/sig/pg_eventstore/queries/transaction_queries.rbs +2 -0
  232. data/sig/pg_eventstore/queries.rbs +11 -21
  233. data/sig/pg_eventstore/query_builders/basic_filtering.rbs +10 -3
  234. data/sig/pg_eventstore/query_builders/event_markers_filtering.rbs +17 -0
  235. data/sig/pg_eventstore/query_builders/event_markers_index_filtering.rbs +69 -0
  236. data/sig/pg_eventstore/query_builders/event_subscription_positions_filtering.rbs +15 -0
  237. data/sig/pg_eventstore/query_builders/events_filtering_query.rbs +9 -51
  238. data/sig/pg_eventstore/query_builders/events_global_index_filtering.rbs +68 -0
  239. data/sig/pg_eventstore/query_builders/filters/collection.rbs +84 -0
  240. data/sig/pg_eventstore/query_builders/filters/event_type_filter.rbs +19 -0
  241. data/sig/pg_eventstore/query_builders/filters/filter_row.rbs +21 -0
  242. data/sig/pg_eventstore/query_builders/filters/marker_filter.rbs +13 -0
  243. data/sig/pg_eventstore/query_builders/filters/marker_filter_row.rbs +19 -0
  244. data/sig/pg_eventstore/query_builders/filters/stream_filter.rbs +24 -0
  245. data/sig/pg_eventstore/query_builders/index_based_events_filtering.rbs +60 -0
  246. data/sig/pg_eventstore/query_builders/partitions_filtering.rbs +29 -10
  247. data/sig/pg_eventstore/query_builders/read_cursor/stream_cursor.rbs +47 -0
  248. data/sig/pg_eventstore/query_builders/streams_global_index_filtering.rbs +27 -0
  249. data/sig/pg_eventstore/query_builders/subscription_events_filtering.rbs +26 -0
  250. data/sig/pg_eventstore/query_strategy/async.rbs +17 -0
  251. data/sig/pg_eventstore/query_strategy/foreground.rbs +11 -0
  252. data/sig/pg_eventstore/query_strategy.rbs +7 -0
  253. data/sig/pg_eventstore/raw_event.rbs +20 -0
  254. data/sig/pg_eventstore/sql_builder.rbs +32 -10
  255. data/sig/pg_eventstore/stream.rbs +10 -18
  256. data/sig/pg_eventstore/stream_global_index.rbs +14 -0
  257. data/sig/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rbs +5 -4
  258. data/sig/pg_eventstore/subscriptions/callback_handlers/events_subscription_position_worker_handlers.rbs +9 -0
  259. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rbs +17 -12
  260. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +17 -6
  261. data/sig/pg_eventstore/subscriptions/event_subscription_position_queries.rbs +30 -0
  262. data/sig/pg_eventstore/subscriptions/events_processor.rbs +12 -5
  263. data/sig/pg_eventstore/subscriptions/events_processor_consumer/multiple.rbs +17 -0
  264. data/sig/pg_eventstore/subscriptions/events_processor_consumer/replica.rbs +17 -0
  265. data/sig/pg_eventstore/subscriptions/events_processor_consumer/single.rbs +17 -0
  266. data/sig/pg_eventstore/subscriptions/events_processor_consumer.rbs +14 -0
  267. data/sig/pg_eventstore/subscriptions/events_subscription_position_worker.rbs +28 -0
  268. data/sig/pg_eventstore/subscriptions/queries/subscription_queries.rbs +7 -39
  269. data/sig/pg_eventstore/subscriptions/replica_subscription_handler.rbs +33 -0
  270. data/sig/pg_eventstore/subscriptions/replica_subscription_runner.rbs +4 -0
  271. data/sig/pg_eventstore/subscriptions/subscription_feed_strategy/collection.rbs +14 -0
  272. data/sig/pg_eventstore/subscriptions/subscription_feed_strategy/index_read_strategy.rbs +23 -0
  273. data/sig/pg_eventstore/subscriptions/subscription_feed_strategy/replication_strategy.rbs +23 -0
  274. data/sig/pg_eventstore/subscriptions/subscription_feed_strategy.rbs +11 -0
  275. data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +13 -10
  276. data/sig/pg_eventstore/subscriptions/subscription_handler_performance.rbs +1 -1
  277. data/sig/pg_eventstore/subscriptions/subscription_runner.rbs +13 -2
  278. data/sig/pg_eventstore/subscriptions/subscription_runners_feeder.rbs +1 -8
  279. data/sig/pg_eventstore/subscriptions/subscriptions_lifecycle.rbs +4 -6
  280. data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +29 -43
  281. data/sig/pg_eventstore/synchronized_array.rbs +4 -0
  282. data/sig/pg_eventstore/utils.rbs +10 -11
  283. data/sig/pg_eventstore/web/application.rbs +13 -3
  284. data/sig/pg_eventstore/web/paginator/base_collection.rbs +2 -0
  285. data/sig/pg_eventstore/web/paginator/event_types_collection.rbs +4 -0
  286. data/sig/pg_eventstore/web/paginator/events_collection.rbs +11 -11
  287. data/sig/pg_eventstore/web/paginator/helpers.rbs +6 -15
  288. data/sig/pg_eventstore/web/paginator/markers_collection.rbs +13 -0
  289. data/sig/pg_eventstore/web/paginator/stream_contexts_collection.rbs +4 -0
  290. data/sig/pg_eventstore/web/paginator/stream_names_collection.rbs +4 -0
  291. data/sig/pg_eventstore/web/paginator/streams_collection.rbs +30 -0
  292. data/sig/pg_eventstore.rbs +8 -26
  293. metadata +175 -20
  294. data/Dockerfile +0 -3
  295. data/lib/pg_eventstore/commands/all_stream_read_grouped.rb +0 -69
  296. data/lib/pg_eventstore/commands/regular_stream_read_grouped.rb +0 -31
  297. data/lib/pg_eventstore/subscriptions/queries/subscription_service_queries.rb +0 -78
  298. data/lib/pg_eventstore/web/views/home/partials/system_stream_filter.erb +0 -15
  299. data/sig/pg_eventstore/commands/all_stream_read_grouped.rbs +0 -16
  300. data/sig/pg_eventstore/commands/regular_stream_read_grouped.rbs +0 -8
  301. data/sig/pg_eventstore/subscriptions/queries/subscription_service_queries.rbs +0 -19
  302. data/sig/pg_eventstore/subscriptions/synchronized_array.rbs +0 -4
  303. /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
+ ```
@@ -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
+ ```
@@ -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` 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).
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 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).
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 database records is `PgEventstore::SubscriptionsSet`. You can find its attributes summary [here](https://rubydoc.info/gems/pg_eventstore/PgEventstore/SubscriptionsSet).
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 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.
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` 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:
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(:my_custom_config, subscription_set: 'SubscriptionsOfMyAwesomeMicroservice', max_retries: 5, retries_interval: 2)
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 service's name in the subscription set name.
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 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.
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 subscriptions set. This measure is needed to prevent running the same subscription under the same subscription set using different processes/subscription managers. Such situation will lead to a malformed subscription state and will break its position, meaning the same event will be processed several times.
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(subscription_set: 'SubscriptionsOfMyAwesomeMicroservice', force_lock: true)
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 processing those subscriptions. `pg_eventstore` has CLI for that purpose:
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 `kill -TERM <pid>` command.
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`, `subscription_restart_terminator`, `failed_subscription_notifier` and `subscription_graceful_shutdown_timeout` config values (see [**Configuration**](configuration.md) chapter for details) for the specific subscription by providing the corresponding arguments. Example:
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 subscription, you should use the `:middlewares` argument which allows you to override the list of middlewares you would like to use.
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 middleware keys you would like to use when creating the subscription:
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 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.
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).
@@ -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 stream. Middleware object must respond to `#serialize` and `#deserialize` methods. The `#serialize` method is called each time an event is going to be appended. The `#deserialize` method is called each time an event is read from a stream. There are two ways how you can define a middleware:
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 its methods, or both of them. Example:
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 = { my_awesome_serializer: MyAwesomeSerializer.new, my_awesome_deserializer: MyAwesomeDeserializer.new, my_awesome_middleware: MyAwesomeMiddleware.new }
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 may also be retried. You have to make sure that the implementation of `#serialize` and `#deserialize` always returns the same result for the same input, and it does not create duplications. Let's look at the `#serialize` implementation from the example above:
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 `value` arguments values, and `POST https://my.awesome.api/api/extract_large_payload` may not want to produce duplicates when called multiple times with the same payload.
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 of the implementation of `#serialize` method to increase overall performance. But if it hard for you to guarantee the persistence of a payload value - you can go with sync approach, thus not allowing event to be persisted if a payload extraction request fails.
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ create_makefile('pg_eventstore_ext')
@@ -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