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,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ class EventSubscriptionPositionQueries
6
+ # Max number of events_global_index records to update
7
+ # @return [Integer]
8
+ MAX_INDEX_RECORDS_TO_UPDATE_SUBSCRIPTION_POSITION = 100_000
9
+ # @return [Integer]
10
+ ASSIGN_SUBSCRIPTION_POSITIONS_LOCK_ID = 462_315_339_855_922
11
+ # @return [Integer]
12
+ REINDEX_PERIOD = 24 * 60 * 60 # 1 day
13
+ # Do not reindex if index size is under this value
14
+ # @return [Integer]
15
+ UNPROCESSED_POSITIONS_INDEX_SIZE_THRESHOLD = 500 * 1_024 * 1_024 # 500 MB
16
+ # @return [String]
17
+ UNPROCESSED_POSITIONS_INDEX_NAME = 'idx_event_subscription_positions_unprocessed_gposition'
18
+ # @return [Integer]
19
+ UNPROCESSED_POSITIONS_LOCK_EXPIRES_IN = 5 * 60 # 5 minutes
20
+
21
+ # @!attribute connection
22
+ # @return [PgEventstore::Connection]
23
+ attr_reader :connection
24
+ private :connection
25
+
26
+ # @param connection [PgEventstore::Connection]
27
+ def initialize(connection)
28
+ @connection = connection
29
+ end
30
+
31
+ # @param raw_events [Array<Hash>]
32
+ # @return [void]
33
+ def create_unprocessed_positions(raw_events)
34
+ values = raw_events.map { "(#{_1['global_position']})" }.join(', ')
35
+ connection.with do |conn|
36
+ conn.exec(<<~SQL)
37
+ INSERT INTO event_subscription_positions_unprocessed ("global_position") VALUES #{values};
38
+ SQL
39
+ end
40
+ end
41
+
42
+ # @param events [Array<PgEventstore::Event>]
43
+ # @return [Hash<Integer, Integer>]
44
+ def subscription_positions_from_db(events)
45
+ filtering = QueryBuilders::EventSubscriptionPositionsFiltering.new
46
+ filtering.by_global_positions(events.map(&:global_position))
47
+ connection.with do |conn|
48
+ conn.exec_params(*filtering.to_exec_params).to_h do |attrs|
49
+ [attrs['global_position'], attrs['subscription_position']]
50
+ end
51
+ end
52
+ end
53
+
54
+ # @return [Integer, nil]
55
+ def max_subscription_position
56
+ filtering = QueryBuilders::EventSubscriptionPositionsFiltering.new
57
+ filtering.max_subscription_position
58
+ connection.with do |conn|
59
+ conn.exec_params(*filtering.to_exec_params).first['max_subscription_position']
60
+ end
61
+ end
62
+
63
+ # @return [Integer, nil] number of updated records
64
+ def assign_subscription_position
65
+ transaction_queries.transaction(:read_committed) do
66
+ connection.with do |conn|
67
+ locked = conn.exec_params(
68
+ 'select pg_try_advisory_xact_lock($1::bigint) as locked',
69
+ [ASSIGN_SUBSCRIPTION_POSITIONS_LOCK_ID]
70
+ ).first['locked']
71
+ return unless locked
72
+
73
+ new_positions = conn.exec_params(<<~SQL, [MAX_INDEX_RECORDS_TO_UPDATE_SUBSCRIPTION_POSITION])
74
+ select global_position
75
+ from event_subscription_positions_unprocessed
76
+ order by global_position
77
+ limit $1
78
+ SQL
79
+ return 0 if new_positions.ntuples == 0
80
+
81
+ affected_positions = new_positions.map { _1['global_position'] }
82
+ new_positions_values = affected_positions.map { "(#{_1})" }.join(', ')
83
+
84
+ conn.exec(<<~SQL)
85
+ insert into event_subscription_positions (global_position) values #{new_positions_values}
86
+ SQL
87
+ affected_positions = new_positions.map { _1['global_position'] }
88
+ conn.exec(<<~SQL).cmd_tuples
89
+ delete from event_subscription_positions_unprocessed
90
+ where global_position in (#{affected_positions.join(',')})
91
+ SQL
92
+ end
93
+ end
94
+ end
95
+
96
+ # There is no concept of periodic maintenance tasks yet. Simply hardcode it for now and abstract when needed.
97
+ # @return [Time, nil]
98
+ def reindex_unprocessed_positions
99
+ task_name = 'reindex_unprocessed_positions'
100
+ task = connection.with do |conn|
101
+ conn.exec_params(
102
+ 'select * from maintenance_tasks where task_name = $1',
103
+ [task_name]
104
+ ).first
105
+ end
106
+ if task && task['performed_at'] && task['performed_at'] + REINDEX_PERIOD > Time.now
107
+ return task['performed_at'] + REINDEX_PERIOD
108
+ end
109
+
110
+ if task
111
+ locked =
112
+ if task['locked_at']
113
+ if task['locked_at'] + UNPROCESSED_POSITIONS_LOCK_EXPIRES_IN > Time.now
114
+ # the task is locked by someone else. Retry later
115
+ return task['locked_at'] + UNPROCESSED_POSITIONS_LOCK_EXPIRES_IN
116
+ end
117
+
118
+ connection.with do |conn|
119
+ conn.exec_params(
120
+ 'update maintenance_tasks set locked_at = $1 where task_name = $2 and locked_at = $3',
121
+ [Time.now.utc, task_name, task['locked_at']]
122
+ )
123
+ end.cmd_tuples == 1
124
+ else
125
+ connection.with do |conn|
126
+ conn.exec_params(
127
+ 'update maintenance_tasks set locked_at = $1 where task_name = $2 and locked_at is null',
128
+ [Time.now.utc, task_name]
129
+ )
130
+ end.cmd_tuples == 1
131
+ end
132
+ return unless locked
133
+ else
134
+ connection.with do |conn|
135
+ conn.exec_params(
136
+ 'insert into maintenance_tasks (task_name, locked_at) values ($1, $2) returning *',
137
+ [task_name, Time.now.utc]
138
+ ).first
139
+ end
140
+ end
141
+
142
+ index_name = UNPROCESSED_POSITIONS_INDEX_NAME
143
+ if index_size(index_name) > UNPROCESSED_POSITIONS_INDEX_SIZE_THRESHOLD
144
+ connection.with do |conn|
145
+ conn.exec("reindex index concurrently #{index_name}")
146
+ end
147
+ end
148
+ connection.with do |conn|
149
+ conn.exec_params(
150
+ 'update maintenance_tasks set performed_at = $1, locked_at = null where task_name = $2',
151
+ [Time.now.utc, task_name]
152
+ )
153
+ end
154
+ Time.now.utc + REINDEX_PERIOD
155
+ rescue PG::UniqueViolation
156
+ # concurrent process has created the record before us
157
+ end
158
+
159
+ # @param index_name [String]
160
+ # @return [Integer]
161
+ def index_size(index_name)
162
+ connection.with do |conn|
163
+ conn.exec_params(
164
+ 'select pg_relation_size(c.oid) as size from pg_class c where c.relname = $1',
165
+ [index_name]
166
+ ).first['size'] || 0
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ # @return [PgEventstore::TransactionQueries]
173
+ def transaction_queries
174
+ TransactionQueries.new(connection)
175
+ end
176
+ end
177
+ end
@@ -9,8 +9,10 @@ module PgEventstore
9
9
  private :connection
10
10
 
11
11
  # @param connection [PgEventstore::Connection]
12
- def initialize(connection)
12
+ # @param query_strategy [PgEventstore::QueryStrategy]
13
+ def initialize(connection, query_strategy)
13
14
  @connection = connection
15
+ @query_strategy = query_strategy
14
16
  end
15
17
 
16
18
  # @param attrs [Hash]
@@ -25,9 +27,7 @@ module PgEventstore
25
27
  # @return [Hash, nil]
26
28
  def find_by(attrs)
27
29
  builder = find_by_attrs_builder(attrs).limit(1)
28
- pg_result = connection.with do |conn|
29
- conn.exec_params(*builder.to_exec_params)
30
- end
30
+ pg_result = @query_strategy.exec_params(*builder.to_exec_params)
31
31
  return if pg_result.ntuples == 0
32
32
 
33
33
  deserialize(pg_result.to_a.first)
@@ -37,9 +37,7 @@ module PgEventstore
37
37
  # @return [Array<Hash>]
38
38
  def find_all(attrs)
39
39
  builder = find_by_attrs_builder(attrs)
40
- pg_result = connection.with do |conn|
41
- conn.exec_params(*builder.to_exec_params)
42
- end
40
+ pg_result = @query_strategy.exec_params(*builder.to_exec_params)
43
41
  return [] if pg_result.ntuples == 0
44
42
 
45
43
  pg_result.map(&method(:deserialize))
@@ -50,9 +48,7 @@ module PgEventstore
50
48
  def set_collection(state = nil)
51
49
  builder = SQLBuilder.new.from('subscriptions').select('set').group('set').order('set ASC')
52
50
  builder.where('state = ?', state) if state
53
- raw_subscriptions = connection.with do |conn|
54
- conn.exec_params(*builder.to_exec_params)
55
- end
51
+ raw_subscriptions = @query_strategy.exec_params(*builder.to_exec_params)
56
52
  raw_subscriptions.map { |attrs| attrs['set'] }
57
53
  end
58
54
 
@@ -71,12 +67,67 @@ module PgEventstore
71
67
  VALUES (#{Utils.positional_vars(attrs.values)})
72
68
  RETURNING *
73
69
  SQL
74
- pg_result = connection.with do |conn|
75
- conn.exec_params(sql, attrs.values)
76
- end
70
+ pg_result = @query_strategy.exec_params(sql, attrs.values)
77
71
  deserialize(pg_result.to_a.first)
78
72
  end
79
73
 
74
+ # @param id [Integer] Subscription#id
75
+ # @param options [Hash]
76
+ # @option options [Hash] :filter
77
+ # @param locked_by [Integer] SubscriptionSet#id
78
+ # @return [void]
79
+ def create_or_replace_table_function(id, options, locked_by)
80
+ filter_collection = QueryBuilders::Filters::Collection.from_options(options)
81
+ builder = QueryBuilders::IndexBasedEventsFiltering.sql_builder_for_subscriptions(filter_collection.collection)
82
+ function_name = QueryBuilders::SubscriptionEventsFiltering.new(id).to_table_name
83
+ transaction_queries.transaction(:read_committed) do
84
+ connection.with do |conn|
85
+ attrs = conn.exec_params('select * from subscriptions where id = $1 for update', [id]).to_a.first
86
+ unless attrs['locked_by'] == locked_by
87
+ # Subscription is force-locked by someone else. We have to roll back such transaction
88
+ raise(WrongLockIdError.new(attrs['set'], attrs['name'], attrs['locked_by']))
89
+ end
90
+
91
+ compiled = conn.compile(*builder.to_exec_params)
92
+ compiled = <<~SQL
93
+ create or replace function #{function_name}(
94
+ from_position bigint,
95
+ to_position bigint,
96
+ max_count int
97
+ )
98
+ returns table (
99
+ global_position bigint,
100
+ event_type_partition_id bigint,
101
+ subscription_position bigint
102
+ )
103
+ language plpgsql
104
+ stable
105
+ parallel safe
106
+ as $$
107
+ declare
108
+ from_gpos bigint;
109
+ to_gpos bigint;
110
+ begin
111
+ with candidates as (
112
+ select esp.global_position
113
+ from event_subscription_positions esp
114
+ where esp.subscription_position >= from_position
115
+ and esp.subscription_position <= to_position
116
+ )
117
+ /* + 0 here is to keep PostgreSQL from trying to optimize the query by picking wrong index */
118
+ select coalesce(min(candidates.global_position + 0), 0), coalesce(max(candidates.global_position + 0), 0)
119
+ into from_gpos, to_gpos
120
+ from candidates;
121
+
122
+ return query #{compiled};
123
+ end;
124
+ $$;
125
+ SQL
126
+ conn.exec(compiled)
127
+ end
128
+ end
129
+ end
130
+
80
131
  # @param id [Integer]
81
132
  # @param attrs [Hash]
82
133
  # @param locked_by [Integer, nil]
@@ -91,9 +142,7 @@ module PgEventstore
91
142
  sql =
92
143
  "UPDATE subscriptions SET #{attrs_sql} WHERE id = $#{attrs.keys.size + 1} RETURNING *"
93
144
  updated_attrs = transaction_queries.transaction(:read_committed) do
94
- pg_result = connection.with do |conn|
95
- conn.exec_params(sql, [*attrs.values, id])
96
- end
145
+ pg_result = @query_strategy.exec_params(sql, [*attrs.values, id])
97
146
  raise(RecordNotFound.new('subscriptions', id)) if pg_result.ntuples == 0
98
147
 
99
148
  updated_attrs = pg_result.to_a.first
@@ -112,34 +161,16 @@ module PgEventstore
112
161
  # @param subscriptions_ids [Array<Integer>] Array of Subscription#id
113
162
  # @return [Hash<Integer => Time>]
114
163
  def ping_all(subscriptions_set_id, subscriptions_ids)
115
- pg_result = connection.with do |conn|
116
- sql = <<~SQL
117
- UPDATE subscriptions SET updated_at = $1 WHERE locked_by = $2 AND id = ANY($3::int[])
118
- RETURNING id, updated_at
119
- SQL
120
- conn.exec_params(sql, [Time.now.utc, subscriptions_set_id, subscriptions_ids])
121
- end
164
+ sql = <<~SQL
165
+ UPDATE subscriptions SET updated_at = $1 WHERE locked_by = $2 AND id = ANY($3::int[])
166
+ RETURNING id, updated_at
167
+ SQL
168
+ pg_result = @query_strategy.exec_params(sql, [Time.now.utc, subscriptions_set_id, subscriptions_ids])
122
169
  pg_result.to_h do |attrs|
123
170
  [attrs['id'], attrs['updated_at']]
124
171
  end
125
172
  end
126
173
 
127
- # @param query_options [Hash{Integer => Hash}] runner_id/query options association
128
- # @return [Hash{Integer => Array<Hash>}] runner_id/events association
129
- def subscriptions_events(query_options)
130
- return {} if query_options.empty?
131
-
132
- final_builder = SQLBuilder.union_builders(query_options.map { |id, opts| query_builder(id, opts) })
133
- raw_events = connection.with do |conn|
134
- conn.exec_params(*final_builder.to_exec_params)
135
- end.to_a
136
- raw_events.group_by { _1['runner_id'] }.to_h do |runner_id, runner_raw_events|
137
- next [runner_id, runner_raw_events] unless query_options[runner_id][:resolve_link_tos]
138
-
139
- [runner_id, links_resolver.resolve(runner_raw_events)]
140
- end
141
- end
142
-
143
174
  # @param id [Integer] subscription's id
144
175
  # @param lock_id [Integer] id of the subscriptions set which reserves the subscription
145
176
  # @param force [Boolean] whether to lock the subscription despite on #locked_by value
@@ -155,9 +186,7 @@ module PgEventstore
155
186
  raise SubscriptionAlreadyLockedError.new(attrs[:set], attrs[:name], attrs[:locked_by])
156
187
  end
157
188
 
158
- connection.with do |conn|
159
- conn.exec_params('UPDATE subscriptions SET locked_by = $1 WHERE id = $2', [lock_id, id])
160
- end
189
+ @query_strategy.exec_params('UPDATE subscriptions SET locked_by = $1 WHERE id = $2', [lock_id, id])
161
190
  end
162
191
  lock_id
163
192
  end
@@ -165,22 +194,17 @@ module PgEventstore
165
194
  # @param id [Integer]
166
195
  # @return [void]
167
196
  def delete(id)
168
- connection.with do |conn|
169
- conn.exec_params('DELETE FROM subscriptions WHERE id = $1', [id])
197
+ function_name = QueryBuilders::SubscriptionEventsFiltering.new(id).to_table_name
198
+ transaction_queries.transaction(:read_committed) do
199
+ connection.with do |conn|
200
+ conn.exec_params('delete from subscriptions where id = $1', [id])
201
+ conn.exec("drop function if exists #{function_name}")
202
+ end
170
203
  end
171
204
  end
172
205
 
173
206
  private
174
207
 
175
- # @param id [Integer] runner id
176
- # @param options [Hash] query options
177
- # @return [PgEventstore::SQLBuilder]
178
- def query_builder(id, options)
179
- builder = PgEventstore::QueryBuilders::EventsFiltering.subscriptions_events_filtering(options).to_sql_builder
180
- builder.where('global_position <= ?', options[:to_position]) if options[:to_position]
181
- builder.select("#{id} as runner_id")
182
- end
183
-
184
208
  # @return [PgEventstore::TransactionQueries]
185
209
  def transaction_queries
186
210
  TransactionQueries.new(connection)
@@ -188,7 +212,7 @@ module PgEventstore
188
212
 
189
213
  # @return [PgEventstore::LinksResolver]
190
214
  def links_resolver
191
- LinksResolver.new(connection)
215
+ LinksResolver.new(connection, @query_strategy)
192
216
  end
193
217
 
194
218
  # @param hash [Hash]
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ class ReplicaSubscriptionHandler
5
+ def initialize(config_name, replica_config_name)
6
+ @replica_config_name = replica_config_name
7
+ @config_name = config_name
8
+ end
9
+
10
+ # @param indexes [Array<PgEventstore::EventGlobalIndex::SubscriptionRepr>]
11
+ # @return [void]
12
+ def call(indexes)
13
+ indexes = reject_already_processed(indexes)
14
+ runner = AsyncRunner.new
15
+ objects_to_migrate = {}
16
+ runner.async do
17
+ objects_to_migrate[:raw_events] = source_replica_queries.load_events(indexes)
18
+ end
19
+ runner.async do
20
+ objects_to_migrate[:markers_index] = source_replica_queries.load_event_markers_index(indexes)
21
+ objects_to_migrate[:markers] = source_replica_queries.load_markers(objects_to_migrate[:markers_index])
22
+ end
23
+ runner.async do
24
+ objects_to_migrate[:events_index] = source_replica_queries.load_events_global_index(indexes)
25
+ objects_to_migrate[:streams_index] = source_replica_queries.load_streams_global_index(
26
+ objects_to_migrate[:events_index]
27
+ )
28
+ objects_to_migrate[:partitions] = source_replica_queries.load_partitions(objects_to_migrate[:events_index])
29
+ end
30
+ runner.run
31
+
32
+ sql = []
33
+ sql << records_to_sql(
34
+ QueryBuilders::PartitionsFiltering::TABLE_NAME,
35
+ Partition.options.map(&:name),
36
+ objects_to_migrate[:partitions].map(&:options_hash),
37
+ on_conflict: 'on conflict do nothing'
38
+ )
39
+ sql << records_to_sql(
40
+ Event::PRIMARY_TABLE_NAME,
41
+ RawEvent.options.map(&:name),
42
+ objects_to_migrate[:raw_events].map(&:options_hash)
43
+ )
44
+ sql << records_to_sql(
45
+ QueryBuilders::EventsGlobalIndexFiltering::PRIMARY_TABLE_NAME,
46
+ EventGlobalIndex.options.map(&:name),
47
+ objects_to_migrate[:events_index].map(&:options_hash)
48
+ )
49
+ sql << records_to_sql(
50
+ QueryBuilders::StreamsGlobalIndexFiltering::PRIMARY_TABLE_NAME,
51
+ StreamGlobalIndex.options.map(&:name),
52
+ objects_to_migrate[:streams_index].map(&:options_hash),
53
+ on_conflict: 'on conflict (id) do update set stream_revision = EXCLUDED.stream_revision'
54
+ )
55
+ sql << records_to_sql(
56
+ QueryBuilders::EventSubscriptionPositionsFiltering::PRIMARY_TABLE_NAME,
57
+ %i[global_position subscription_position],
58
+ indexes.map(&:to_subscription_position_attrs)
59
+ )
60
+ if objects_to_migrate[:markers_index].any?
61
+ sql << records_to_sql(
62
+ QueryBuilders::EventMarkersIndexFiltering::PRIMARY_TABLE_NAME,
63
+ EventMarkerIndex.options.map(&:name),
64
+ objects_to_migrate[:markers_index].map(&:options_hash)
65
+ )
66
+ end
67
+ if objects_to_migrate[:markers].any?
68
+ sql << records_to_sql(
69
+ QueryBuilders::EventMarkersFiltering::PRIMARY_TABLE_NAME,
70
+ EventMarker.options.map(&:name),
71
+ objects_to_migrate[:markers].map(&:options_hash),
72
+ on_conflict: 'on conflict do nothing'
73
+ )
74
+ end
75
+ sql = sql.join("\n")
76
+ replica_transaction_queries.transaction(:read_committed) do
77
+ replica_connection.with do |conn|
78
+ conn.exec(sql)
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # @param indexes [Array<PgEventstore::EventGlobalIndex::SubscriptionRepr>]
86
+ # @return [Array<PgEventstore::EventGlobalIndex::SubscriptionRepr>]
87
+ def reject_already_processed(indexes)
88
+ existing_positions = destination_replica_queries.load_subscription_positions(indexes)
89
+ return indexes if existing_positions.empty?
90
+
91
+ indexes.reject do |index|
92
+ existing_positions.include?(index.subscription_position)
93
+ end
94
+ end
95
+
96
+ # @param table_name [String]
97
+ # @param attribute_names [Array<Symbol>]
98
+ # @param attributes_collection [Array<Hash<Symbol, Object>>]
99
+ # @param on_conflict [String, nil]
100
+ # @return [String]
101
+ def records_to_sql(table_name, attribute_names, attributes_collection, on_conflict: nil)
102
+ sql_values = attributes_collection.map do |attributes|
103
+ values = attribute_names.map do |attribute_name|
104
+ connection.with do |conn|
105
+ conn.prepared_value(attributes[attribute_name])
106
+ end
107
+ end
108
+ "(#{values.join(', ')})"
109
+ end
110
+ sql_values = sql_values.join(', ')
111
+ attributes = attribute_names.join(', ')
112
+ "insert into #{table_name} (#{attributes}) values #{sql_values} #{on_conflict if on_conflict};"
113
+ end
114
+
115
+ # @return [PgEventstore::Connection]
116
+ def connection
117
+ PgEventstore.connection(@config_name)
118
+ end
119
+
120
+ # @return [PgEventstore::Connection]
121
+ def replica_connection
122
+ PgEventstore.connection(@replica_config_name)
123
+ end
124
+
125
+ # @return [PgEventstore::TransactionQueries]
126
+ def replica_transaction_queries
127
+ TransactionQueries.new(replica_connection)
128
+ end
129
+
130
+ # @return [PgEventstore::ReplicaQueries]
131
+ def source_replica_queries
132
+ ReplicaQueries.new(connection, QueryStrategy::Async.new(connection))
133
+ end
134
+
135
+ # @return [PgEventstore::ReplicaQueries]
136
+ def destination_replica_queries
137
+ ReplicaQueries.new(replica_connection, QueryStrategy::Foreground.new(replica_connection))
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ # @!visibility private
5
+ # We need to differentiate between replica subscription runner and regular subscription runner. They both have the
6
+ # same functional though
7
+ class ReplicaSubscriptionRunner < SubscriptionRunner
8
+ end
9
+ end
@@ -102,7 +102,7 @@ module PgEventstore
102
102
 
103
103
  # @return [PgEventstore::SubscriptionQueries]
104
104
  def subscription_queries
105
- SubscriptionQueries.new(connection)
105
+ SubscriptionQueries.new(connection, QueryStrategy::Foreground.new(connection))
106
106
  end
107
107
  end
108
108
 
@@ -185,6 +185,7 @@ module PgEventstore
185
185
  time_between_restarts:,
186
186
  state: RunnerState::STATES[:initial]
187
187
  )
188
+ subscription_queries.create_or_replace_table_function(id, options, locked_by)
188
189
  reload
189
190
  end
190
191
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module SubscriptionFeedStrategy
5
+ # @!visibility private
6
+ class Collection < Array
7
+ # This number is used to determine how many subscriptions we fetch per SQL query.
8
+ # @return [Integer]
9
+ DEFAULT_SUBSCRIPTIONS_NUM_PER_QUERY = 10
10
+
11
+ class << self
12
+ # @param runners [Array<PgEventstore::SubscriptionRunner>]
13
+ # @param connection [PgEventstore::Connection]
14
+ # @param query_strategy [PgEventstore::QueryStrategy]
15
+ # @param subscriptions_per_query [Integer]
16
+ # @return [PgEventstore::SubscriptionFeedStrategy::Collection]
17
+ def create(runners, connection, query_strategy, subscriptions_per_query: DEFAULT_SUBSCRIPTIONS_NUM_PER_QUERY)
18
+ instance = new
19
+ replica_runners = runners.grep(ReplicaSubscriptionRunner)
20
+ common_runners = runners - replica_runners
21
+ common_runners.each_slice(subscriptions_per_query).each do |runners_slice|
22
+ strategy = IndexReadStrategy.new(connection, query_strategy)
23
+ strategy.add(*runners_slice)
24
+ instance.push(strategy)
25
+ end
26
+ replica_runners.each_slice(subscriptions_per_query) do |runners_slice|
27
+ strategy = ReplicationStrategy.new(connection, query_strategy)
28
+ strategy.add(*runners_slice)
29
+ instance.push(strategy)
30
+ end
31
+ instance
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module SubscriptionFeedStrategy
5
+ # @!visibility private
6
+ class IndexReadStrategy
7
+ include SubscriptionFeedStrategy
8
+
9
+ # Allow subscriptions to scan through up to this amount of events per a single query. This allows to make query
10
+ # plan more predictable. Downside: let's say subscription1 targets "Foo" event type, but between
11
+ # SubscriptionRunnersFeeder#feed runs more than this amount of events other than "Foo" event type are published -
12
+ # it will require at least one more loop to pick that event.
13
+ # @return [Integer]
14
+ INDEX_LOOK_UP_DISTANCE = 100_000
15
+
16
+ # @param connection [PgEventstore::Connection]
17
+ # @param query_strategy [PgEventstore::QueryStrategy]
18
+ def initialize(connection, query_strategy)
19
+ @connection = connection
20
+ @query_strategy = query_strategy
21
+ @runners = []
22
+ end
23
+
24
+ # @param runners [Array<PgEventstore::SubscriptionRunner>]
25
+ # @return [Array<PgEventstore::SubscriptionRunner>]
26
+ def add(*runners)
27
+ @runners.push(*runners)
28
+ end
29
+
30
+ # @return [Integer]
31
+ def size
32
+ @runners.size
33
+ end
34
+
35
+ # @return [Boolean]
36
+ def any?
37
+ @runners.any?
38
+ end
39
+
40
+ # @return [void]
41
+ def feed
42
+ runners_query_options = @runners.to_h do |runner|
43
+ next_chunk_query_opts = runner.next_chunk_query_opts
44
+ next_chunk_query_opts[:to_position] =
45
+ [next_chunk_query_opts[:from_position] + INDEX_LOOK_UP_DISTANCE, safe_position].min
46
+ [runner.id, next_chunk_query_opts]
47
+ end
48
+ grouped_indexes = events_global_index_queries.fetch_indexes_for_subscriptions(runners_query_options)
49
+ @runners.each do |runner|
50
+ if grouped_indexes[runner.id]
51
+ chunk = Chunks::SubscriptionEventsIndexChunk.new(
52
+ grouped_indexes[runner.id],
53
+ @connection,
54
+ QueryStrategy::Foreground.new(@connection),
55
+ runners_query_options[runner.id][:resolve_link_tos]
56
+ )
57
+ runner.feed(chunk)
58
+ else
59
+ runner.feed(Chunks::SubscriptionCheckpointChunk.new(runners_query_options[runner.id][:to_position]))
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # @return [Integer]
67
+ def safe_position
68
+ event_subscription_position_queries.max_subscription_position || 0
69
+ end
70
+
71
+ # @return [PgEventstore::EventsGlobalIndexQueries]
72
+ def events_global_index_queries
73
+ EventsGlobalIndexQueries.new(@connection, @query_strategy)
74
+ end
75
+
76
+ # @return [PgEventstore::EventSubscriptionPositionQueries]
77
+ def event_subscription_position_queries
78
+ EventSubscriptionPositionQueries.new(@connection)
79
+ end
80
+ end
81
+ end
82
+ end