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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +4 -3
- data/lib/search_engine/postgres_outbox/database_queries.rb +17 -0
- data/lib/search_engine/postgres_outbox/materialization_cursor.rb +25 -11
- data/lib/search_engine/postgres_outbox/repository.rb +17 -13
- data/lib/search_engine/postgres_outbox.rb +1 -0
- data/lib/search_engine/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e2a45aebc5484036b192915f9de295af3efbce20c7831c3e301dd9608b68a26d
|
|
4
|
+
data.tar.gz: e7274a561a6f852f5daa8489f33349bbdf430da811f8e0db3556c36547a1098e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
#
|
|
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 =
|
|
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
|
-
|
|
38
|
-
@forward_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
|
-
|
|
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
|
-
|
|
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(
|
|
58
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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'
|
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.
|
|
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
|