search-engine-for-typesense 30.9.1 → 30.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f5d1b6315a75f241c80d16c1f69727fed0ceef2edabb8ce774d33c348756a59
4
- data.tar.gz: fbb2364ac8afc372ff0bbcb8f70174874ab190e226f6d08e1e747b018be8259b
3
+ metadata.gz: e2a45aebc5484036b192915f9de295af3efbce20c7831c3e301dd9608b68a26d
4
+ data.tar.gz: e7274a561a6f852f5daa8489f33349bbdf430da811f8e0db3556c36547a1098e
5
5
  SHA512:
6
- metadata.gz: 1db71818517bd5dd234a43ebfe63f1b9affa33b4fcb0f6a15914cb6bcd55a3a6844ba8a87e68ead1bbc81a3a87dbe3dba2d845f9ba8c4da6d3251ed877755c5b
7
- data.tar.gz: 56a54cdd5a7a0fdf3566c021788ba9f4d54aa9311f7df508b8b9e99c5d7010c9ecd26cf2d83cc219625a48d7738883e03c49360c9d7c4d256f4fc48d173e315b
6
+ metadata.gz: 38f920eb23837108385ad80e09607d6e8f221847660af0870de49552110305d5b1a8d8a4186d0f9635b609ccafdaf0817127fa911cf5f3a9f9d17f1d6ea3d032
7
+ data.tar.gz: c89b82cbb8575cc4608bbeab0536599aa0b5d37f26198117e6f519df40fd0b588919297c7fa26b0466efaa370fce9df56cd196e9a21cfcb15a7cebd679bd01a8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 30.9.2
4
+
5
+ - Check recent event IDs during catch-up so a returning delivery target can discover new work
6
+ while its saved position moves through older events. Collection quotas still apply.
7
+ - Read outbox checkpoints, delivery state and row locks directly from the database on every poll,
8
+ including when Active Record query caching is enabled on Rails 6.1 and 7.0.
9
+
3
10
  ## 30.9.1
4
11
 
5
12
  - Serialize PostgreSQL delivery discovery across schedulers and workers and check delivery membership
data/README.md CHANGED
@@ -572,9 +572,10 @@ For large histories, create a checkpoint table in a migration with
572
572
  `c.postgres_outbox.materialization_cursor_table_name = 'search_engine_outbox_materialization_cursors'`.
573
573
  Run the migration before starting consumers with this setting.
574
574
 
575
- With checkpoints, each discovery call checks at most 1,000 IDs around the forward position and
576
- 1,000 historical IDs against the requested targets. The forward scan overlaps its previous
577
- range to catch nearby out-of-order commits. Each target set maintains its own position. A
575
+ With checkpoints, each discovery call checks at most 2,000 event IDs. Each target set maintains
576
+ its own forward position and a historical sweep. The forward scan overlaps its previous range
577
+ to catch nearby out-of-order commits. When it falls behind, part of the historical scan budget
578
+ checks the newest IDs while the forward scan continues through the intervening events. A
578
579
  transaction that commits farther behind, or an older event that becomes due, can wait until
579
580
  the historical scan revisits its ID. Sweep time depends on retained history and polling rate.
580
581
  Collection batch quotas and the latest-event rule still govern which deliveries may be created.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SearchEngine
4
+ module PostgresOutbox
5
+ # Operational reads must observe current delivery state and acquire row locks on each poll.
6
+ module DatabaseQueries
7
+ private
8
+
9
+ def select_rows(sql)
10
+ result = connection.respond_to?(:exec_query) ? connection.exec_query(sql) : connection.select_all(sql)
11
+ return result.to_a if result.respond_to?(:to_a)
12
+
13
+ Array(result)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -5,11 +5,14 @@ require 'json'
5
5
 
6
6
  module SearchEngine
7
7
  module PostgresOutbox
8
- # Read under the outbox materialization lock. One page follows new event IDs;
9
- # the other sweeps backwards to revisit late commits, due retries and existing events.
8
+ # Scan under the materialization lock. Keep moving through history even when new IDs arrive faster.
10
9
  class MaterializationCursor
10
+ include DatabaseQueries
11
+
11
12
  PAGE_SIZE = 1000
12
13
 
14
+ attr_reader :checkpoint_ids
15
+
13
16
  def initialize(connection, events_table, cursor_table, target_keys)
14
17
  @connection = connection
15
18
  @events = events_table
@@ -21,7 +24,7 @@ module SearchEngine
21
24
  # @return [Array<Integer>] event IDs to check against the configured delivery targets
22
25
  def candidate_ids(limit:)
23
26
  @page_size = [PAGE_SIZE, limit].min
24
- state = @connection.select_all(<<~SQL).first
27
+ state = select_rows(<<~SQL).first
25
28
  SELECT forward_id, backfill_through_id FROM #{@table} WHERE target_scope = #{@scope}
26
29
  SQL
27
30
  if state
@@ -34,18 +37,27 @@ module SearchEngine
34
37
  # Recheck a recent ID range for concurrent inserts that commit behind the watermark.
35
38
  # Even for dense IDs, at least half of the page remains available for new events.
36
39
  @forward_page = event_ids("id > #{@forward_id - @page_size / 2} ORDER BY id ASC")
37
- @backfill_page = event_ids("id <= #{@backfill_through_id} ORDER BY id DESC")
38
- @forward_page | @backfill_page
40
+ # A returning target can be far behind. Reserve some of the second page for the current head.
41
+ @recent_page = if @forward_page.size == @page_size
42
+ event_ids("id > #{@forward_page.last} ORDER BY id DESC", [@page_size / 2, 1].max)
43
+ else
44
+ []
45
+ end
46
+ @backfill_limit = [@page_size - @recent_page.size, 1].max
47
+ @backfill_page = event_ids("id <= #{@backfill_through_id} ORDER BY id DESC", @backfill_limit)
48
+ @checkpoint_ids = @forward_page | @backfill_page
49
+ @checkpoint_ids | @recent_page
39
50
  end
40
51
 
41
52
  # Save progress in the same transaction as the new deliveries.
42
53
  def advance!
43
54
  forward = [@forward_page.last || @forward_id, @forward_id].max
44
55
  # Freeze the upper boundary until the backwards sweep reaches the beginning.
45
- backfill = @backfill_page.size < @page_size ? forward : @backfill_page.last - 1
56
+ head = [forward, @recent_page.first || forward].max
57
+ backfill = @backfill_page.size < @backfill_limit ? head : @backfill_page.last - 1
46
58
  return if forward == @forward_id && backfill == @backfill_through_id
47
59
 
48
- @connection.execute(<<~SQL)
60
+ connection.execute(<<~SQL)
49
61
  UPDATE #{@table} SET forward_id = #{forward}, backfill_through_id = #{backfill}
50
62
  WHERE target_scope = #{@scope}
51
63
  SQL
@@ -53,17 +65,19 @@ module SearchEngine
53
65
 
54
66
  private
55
67
 
68
+ attr_reader :connection
69
+
56
70
  def initialize_positions
57
- head = Integer(@connection.select_value("SELECT COALESCE(MAX(id), 0) FROM #{@events}"))
58
- @connection.execute(<<~SQL)
71
+ head = Integer(select_rows("SELECT COALESCE(MAX(id), 0) AS head FROM #{@events}").first.fetch('head'))
72
+ connection.execute(<<~SQL)
59
73
  INSERT INTO #{@table} (target_scope, forward_id, backfill_through_id)
60
74
  VALUES (#{@scope}, #{head}, #{head})
61
75
  SQL
62
76
  [head, head]
63
77
  end
64
78
 
65
- def event_ids(order)
66
- @connection.select_all("SELECT id FROM #{@events} WHERE #{order} LIMIT #{@page_size}").map do |row|
79
+ def event_ids(order, limit = @page_size)
80
+ select_rows("SELECT id FROM #{@events} WHERE #{order} LIMIT #{limit}").map do |row|
67
81
  Integer(row.fetch('id'))
68
82
  end
69
83
  end
@@ -7,6 +7,8 @@ module SearchEngine
7
7
  module PostgresOutbox
8
8
  # Raw SQL repository for host-managed PostgreSQL outbox rows.
9
9
  class Repository
10
+ include DatabaseQueries
11
+
10
12
  # Keep expensive duplicate-key ranking bounded while leaving headroom for hot keys.
11
13
  CLAIM_CANDIDATE_OVERSAMPLE = 4
12
14
  MATERIALIZATION_LOCK_NAMESPACE = 0x53454F42 # SEOB
@@ -198,7 +200,7 @@ module SearchEngine
198
200
  event_ids = cursor&.candidate_ids(limit: scan_limit)
199
201
  preview = select_rows(delivery_materialization_select_sql(limit, targets, event_ids: event_ids, lock: false))
200
202
  rows = preview.empty? ? [] : materialize_batch(limit, targets, preview)
201
- if cursor && rows.size == preview.size && materialization_page_drained?(rows, event_ids, limit)
203
+ if cursor && rows.size == preview.size && page_drained?(rows, event_ids, limit, cursor, targets)
202
204
  cursor.advance!
203
205
  end
204
206
  rows
@@ -414,14 +416,23 @@ module SearchEngine
414
416
  end
415
417
 
416
418
  # When a quota may have omitted matches, revisit the page after these deliveries commit.
417
- def materialization_page_drained?(rows, event_ids, limit)
419
+ def page_drained?(rows, event_ids, limit, cursor, targets)
418
420
  return true if rows.empty? || rows.size == event_ids.size
419
421
 
420
422
  batch_size = global_limit_for(limit)
421
- return rows.size < batch_size unless collection_limited_batch?(limit)
422
-
423
- per_collection = SearchEngine.config.postgres_outbox.normalized_batch_sizes.values
424
- rows.size < [batch_size, *per_collection].min
423
+ if collection_limited_batch?(limit)
424
+ quotas = SearchEngine.config.postgres_outbox.normalized_batch_sizes.values
425
+ batch_size = [batch_size, *quotas].min
426
+ end
427
+ return true if rows.size < batch_size
428
+ return false if event_ids.size == cursor.checkpoint_ids.size
429
+
430
+ # A moving head can fill a quota on every poll. Only work on the saved pages must hold progress.
431
+ select_rows(<<~SQL).empty?
432
+ SELECT 1 FROM #{quoted_table} outbox
433
+ WHERE #{materialization_filter_sql(targets, cursor.checkpoint_ids)}
434
+ LIMIT 1
435
+ SQL
425
436
  end
426
437
 
427
438
  # Revalidate the exact preview under parent locks before locking any deliveries.
@@ -1289,13 +1300,6 @@ module SearchEngine
1289
1300
  'THEN COALESCE(aggregate.terminal_processed_at, CURRENT_TIMESTAMP) ELSE NULL END'
1290
1301
  end
1291
1302
 
1292
- def select_rows(sql)
1293
- result = connection.select_all(sql)
1294
- return result.to_a if result.respond_to?(:to_a)
1295
-
1296
- Array(result)
1297
- end
1298
-
1299
1303
  def execute(sql)
1300
1304
  connection.execute(sql)
1301
1305
  end
@@ -3,6 +3,7 @@
3
3
  require 'search_engine/postgres_outbox/event'
4
4
  require 'search_engine/postgres_outbox/delivery_target'
5
5
  require 'search_engine/postgres_outbox/processor_result'
6
+ require 'search_engine/postgres_outbox/database_queries'
6
7
  require 'search_engine/postgres_outbox/materialization_cursor'
7
8
  require 'search_engine/postgres_outbox/repository'
8
9
  require 'search_engine/postgres_outbox/event_processor'
@@ -3,5 +3,5 @@
3
3
  module SearchEngine
4
4
  # Current gem version.
5
5
  # @return [String]
6
- VERSION = '30.9.1'
6
+ VERSION = '30.9.2'
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: search-engine-for-typesense
3
3
  version: !ruby/object:Gem::Version
4
- version: 30.9.1
4
+ version: 30.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikita Shkoda
@@ -183,6 +183,7 @@ files:
183
183
  - lib/search_engine/otel.rb
184
184
  - lib/search_engine/partitioner.rb
185
185
  - lib/search_engine/postgres_outbox.rb
186
+ - lib/search_engine/postgres_outbox/database_queries.rb
186
187
  - lib/search_engine/postgres_outbox/delivery_target.rb
187
188
  - lib/search_engine/postgres_outbox/drain_enqueuer.rb
188
189
  - lib/search_engine/postgres_outbox/drainer.rb