search-engine-for-typesense 30.9.0 → 30.9.1
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 +6 -0
- data/README.md +16 -0
- data/lib/search_engine/config.rb +3 -0
- data/lib/search_engine/postgres_outbox/materialization_cursor.rb +72 -0
- data/lib/search_engine/postgres_outbox/migration_helpers.rb +9 -0
- data/lib/search_engine/postgres_outbox/repository.rb +94 -125
- data/lib/search_engine/postgres_outbox.rb +1 -0
- data/lib/search_engine/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f5d1b6315a75f241c80d16c1f69727fed0ceef2edabb8ce774d33c348756a59
|
|
4
|
+
data.tar.gz: fbb2364ac8afc372ff0bbcb8f70174874ab190e226f6d08e1e747b018be8259b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1db71818517bd5dd234a43ebfe63f1b9affa33b4fcb0f6a15914cb6bcd55a3a6844ba8a87e68ead1bbc81a3a87dbe3dba2d845f9ba8c4da6d3251ed877755c5b
|
|
7
|
+
data.tar.gz: 56a54cdd5a7a0fdf3566c021788ba9f4d54aa9311f7df508b8b9e99c5d7010c9ecd26cf2d83cc219625a48d7738883e03c49360c9d7c4d256f4fc48d173e315b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 30.9.1
|
|
4
|
+
|
|
5
|
+
- Serialize PostgreSQL delivery discovery across schedulers and workers and check delivery membership
|
|
6
|
+
by event and target. Hosts can enable checkpointed scans to bound discovery on large outboxes.
|
|
7
|
+
- Execute advisory locks and delivery cutover polls on the database when Rails query caching is enabled.
|
|
8
|
+
|
|
3
9
|
## 30.9.0
|
|
4
10
|
|
|
5
11
|
- Qualify geo/vector search and native caching against Typesense 30.2 while keeping the gem major
|
data/README.md
CHANGED
|
@@ -565,6 +565,22 @@ target-specific status, retry, lock, and queue state for each logical event. Add
|
|
|
565
565
|
`create_search_engine_outbox_drain_slots` in that delivery-target migration so the drain enqueuer can cap
|
|
566
566
|
queued and running drain jobs per target.
|
|
567
567
|
|
|
568
|
+
Concurrent listener, scheduler and worker calls share one nonblocking PostgreSQL transaction lock for
|
|
569
|
+
delivery discovery on each events table. Other callers can keep claiming existing deliveries.
|
|
570
|
+
For large histories, create a checkpoint table in a migration with
|
|
571
|
+
`create_search_engine_outbox_materialization_cursors`, then set
|
|
572
|
+
`c.postgres_outbox.materialization_cursor_table_name = 'search_engine_outbox_materialization_cursors'`.
|
|
573
|
+
Run the migration before starting consumers with this setting.
|
|
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
|
|
578
|
+
transaction that commits farther behind, or an older event that becomes due, can wait until
|
|
579
|
+
the historical scan revisits its ID. Sweep time depends on retained history and polling rate.
|
|
580
|
+
Collection batch quotas and the latest-event rule still govern which deliveries may be created.
|
|
581
|
+
Without a configured checkpoint table the existing full-history discovery uses indexed per-event
|
|
582
|
+
delivery checks and the same concurrency lock.
|
|
583
|
+
|
|
568
584
|
Drain slots are a generic backpressure mechanism for delivery-target mode. On each listener or polling
|
|
569
585
|
wakeup, the enqueuer materializes missing delivery rows, acquires idle slots from
|
|
570
586
|
`c.postgres_outbox.drain_slot_table_name`, and enqueues one drain job per acquired slot. If all slots for a
|
data/lib/search_engine/config.rb
CHANGED
|
@@ -278,6 +278,8 @@ module SearchEngine
|
|
|
278
278
|
attr_accessor :delivery_table_name
|
|
279
279
|
# @return [String] database table used by host-managed drain slots
|
|
280
280
|
attr_accessor :drain_slot_table_name
|
|
281
|
+
# @return [String, nil] optional checkpoint table for bounded delivery discovery
|
|
282
|
+
attr_accessor :materialization_cursor_table_name
|
|
281
283
|
# @return [String] PostgreSQL notification channel for wakeups
|
|
282
284
|
attr_accessor :channel
|
|
283
285
|
# @return [String] queue name used by host app job dispatch
|
|
@@ -936,6 +938,7 @@ module SearchEngine
|
|
|
936
938
|
table_name: postgres_outbox.table_name,
|
|
937
939
|
delivery_table_name: postgres_outbox.delivery_table_name,
|
|
938
940
|
drain_slot_table_name: postgres_outbox.drain_slot_table_name,
|
|
941
|
+
materialization_cursor_table_name: postgres_outbox.materialization_cursor_table_name,
|
|
939
942
|
channel: postgres_outbox.channel,
|
|
940
943
|
queue_name: postgres_outbox.queue_name,
|
|
941
944
|
batch_size: postgres_outbox.batch_size,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module SearchEngine
|
|
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.
|
|
10
|
+
class MaterializationCursor
|
|
11
|
+
PAGE_SIZE = 1000
|
|
12
|
+
|
|
13
|
+
def initialize(connection, events_table, cursor_table, target_keys)
|
|
14
|
+
@connection = connection
|
|
15
|
+
@events = events_table
|
|
16
|
+
@table = connection.quote_table_name(cursor_table)
|
|
17
|
+
@scope = connection.quote(Digest::SHA256.hexdigest(JSON.generate([events_table, target_keys.uniq.sort])))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param limit [Integer] upper bound on the number of event IDs to read in each direction
|
|
21
|
+
# @return [Array<Integer>] event IDs to check against the configured delivery targets
|
|
22
|
+
def candidate_ids(limit:)
|
|
23
|
+
@page_size = [PAGE_SIZE, limit].min
|
|
24
|
+
state = @connection.select_all(<<~SQL).first
|
|
25
|
+
SELECT forward_id, backfill_through_id FROM #{@table} WHERE target_scope = #{@scope}
|
|
26
|
+
SQL
|
|
27
|
+
if state
|
|
28
|
+
@forward_id = Integer(state.fetch('forward_id'))
|
|
29
|
+
@backfill_through_id = Integer(state.fetch('backfill_through_id'))
|
|
30
|
+
else
|
|
31
|
+
@forward_id, @backfill_through_id = initialize_positions
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Recheck a recent ID range for concurrent inserts that commit behind the watermark.
|
|
35
|
+
# Even for dense IDs, at least half of the page remains available for new events.
|
|
36
|
+
@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
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Save progress in the same transaction as the new deliveries.
|
|
42
|
+
def advance!
|
|
43
|
+
forward = [@forward_page.last || @forward_id, @forward_id].max
|
|
44
|
+
# Freeze the upper boundary until the backwards sweep reaches the beginning.
|
|
45
|
+
backfill = @backfill_page.size < @page_size ? forward : @backfill_page.last - 1
|
|
46
|
+
return if forward == @forward_id && backfill == @backfill_through_id
|
|
47
|
+
|
|
48
|
+
@connection.execute(<<~SQL)
|
|
49
|
+
UPDATE #{@table} SET forward_id = #{forward}, backfill_through_id = #{backfill}
|
|
50
|
+
WHERE target_scope = #{@scope}
|
|
51
|
+
SQL
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def initialize_positions
|
|
57
|
+
head = Integer(@connection.select_value("SELECT COALESCE(MAX(id), 0) FROM #{@events}"))
|
|
58
|
+
@connection.execute(<<~SQL)
|
|
59
|
+
INSERT INTO #{@table} (target_scope, forward_id, backfill_through_id)
|
|
60
|
+
VALUES (#{@scope}, #{head}, #{head})
|
|
61
|
+
SQL
|
|
62
|
+
[head, head]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def event_ids(order)
|
|
66
|
+
@connection.select_all("SELECT id FROM #{@events} WHERE #{order} LIMIT #{@page_size}").map do |row|
|
|
67
|
+
Integer(row.fetch('id'))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -102,6 +102,15 @@ module SearchEngine
|
|
|
102
102
|
on_delete: :cascade
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# Create the small checkpoint table used for bounded delivery discovery.
|
|
106
|
+
# @param table_name [String, Symbol] table configured as materialization_cursor_table_name
|
|
107
|
+
def create_search_engine_outbox_materialization_cursors(table_name: :search_engine_outbox_materialization_cursors)
|
|
108
|
+
create_table table_name, id: :string, primary_key: :target_scope do |t|
|
|
109
|
+
t.bigint :forward_id, null: false
|
|
110
|
+
t.bigint :backfill_through_id, null: false
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
105
114
|
# Create the SearchEngine outbox drain slots table.
|
|
106
115
|
#
|
|
107
116
|
# @param table_name [String, Symbol] destination drain slots table name
|
|
@@ -9,6 +9,7 @@ module SearchEngine
|
|
|
9
9
|
class Repository
|
|
10
10
|
# Keep expensive duplicate-key ranking bounded while leaving headroom for hot keys.
|
|
11
11
|
CLAIM_CANDIDATE_OVERSAMPLE = 4
|
|
12
|
+
MATERIALIZATION_LOCK_NAMESPACE = 0x53454F42 # SEOB
|
|
12
13
|
ERROR_LIMIT = 1000
|
|
13
14
|
PROCESSED_AT_ASSIGNMENT = 'processed_at = CURRENT_TIMESTAMP'
|
|
14
15
|
RETIREMENT_BATCH_SIZE = 1000
|
|
@@ -181,43 +182,27 @@ module SearchEngine
|
|
|
181
182
|
with_delivery_target_session_lock(target, mode: :shared, timing: timing, &block)
|
|
182
183
|
end
|
|
183
184
|
|
|
184
|
-
# Create missing
|
|
185
|
-
# @return [
|
|
185
|
+
# Create missing deliveries. Concurrent pollers leave discovery to the current owner.
|
|
186
|
+
# @return [Array<Hash>, nil] materialized events, or nil when no targets are configured
|
|
186
187
|
def materialize_deliveries!(limit: nil)
|
|
187
188
|
targets = materialization_delivery_targets
|
|
188
189
|
return if targets.empty?
|
|
189
190
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
preview_rows = select_rows(delivery_materialization_select_sql(limit, targets, lock: false))
|
|
193
|
-
return preview_rows if preview_rows.empty?
|
|
191
|
+
scan_limit = collection_limited_batch?(limit) ? collection_limited_candidate_limit : global_limit_for(limit)
|
|
192
|
+
return [] if scan_limit.zero?
|
|
194
193
|
|
|
195
|
-
supersede_parents = select_rows(supersede_parent_ids_sql(preview_rows))
|
|
196
|
-
rows = []
|
|
197
194
|
connection.transaction do
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
next if rows.empty?
|
|
210
|
-
|
|
211
|
-
supersede_candidates = select_rows(materialization_supersede_parent_candidates_sql(rows, targets))
|
|
212
|
-
older_event_ids = supersede_candidates.map { |candidate| row_value(candidate, :event_id) }
|
|
213
|
-
older_delivery_ids = supersede_candidates.map { |candidate| row_value(candidate, :delivery_id) }
|
|
214
|
-
lock_delivery_rows!(older_delivery_ids)
|
|
215
|
-
supersede_pending_events_by_id!(older_event_ids)
|
|
216
|
-
supersede_pending_deliveries!(older_delivery_ids, event_ids: older_event_ids)
|
|
217
|
-
execute(delivery_materialization_insert_sql(rows, targets))
|
|
195
|
+
next [] unless materialization_lock_acquired?
|
|
196
|
+
|
|
197
|
+
cursor = materialization_cursor(targets)
|
|
198
|
+
event_ids = cursor&.candidate_ids(limit: scan_limit)
|
|
199
|
+
preview = select_rows(delivery_materialization_select_sql(limit, targets, event_ids: event_ids, lock: false))
|
|
200
|
+
rows = preview.empty? ? [] : materialize_batch(limit, targets, preview)
|
|
201
|
+
if cursor && rows.size == preview.size && materialization_page_drained?(rows, event_ids, limit)
|
|
202
|
+
cursor.advance!
|
|
203
|
+
end
|
|
204
|
+
rows
|
|
218
205
|
end
|
|
219
|
-
|
|
220
|
-
rows
|
|
221
206
|
end
|
|
222
207
|
|
|
223
208
|
# Refresh cleanup-eligible parent event statuses from terminal delivery rows.
|
|
@@ -415,6 +400,49 @@ module SearchEngine
|
|
|
415
400
|
end
|
|
416
401
|
end
|
|
417
402
|
|
|
403
|
+
# PostgreSQL's two-integer lock space is separate from the single-key delivery cutover locks.
|
|
404
|
+
def materialization_lock_acquired?
|
|
405
|
+
sql = <<~SQL
|
|
406
|
+
SELECT pg_try_advisory_xact_lock(#{MATERIALIZATION_LOCK_NAMESPACE}, #{quote(quoted_table)}::regclass::integer)
|
|
407
|
+
SQL
|
|
408
|
+
advisory_lock_truthy?(select_scalar(sql))
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def materialization_cursor(targets)
|
|
412
|
+
table = SearchEngine.config.postgres_outbox.materialization_cursor_table_name
|
|
413
|
+
MaterializationCursor.new(connection, quoted_table, table, targets.map(&:key)) if table
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# When a quota may have omitted matches, revisit the page after these deliveries commit.
|
|
417
|
+
def materialization_page_drained?(rows, event_ids, limit)
|
|
418
|
+
return true if rows.empty? || rows.size == event_ids.size
|
|
419
|
+
|
|
420
|
+
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
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Revalidate the exact preview under parent locks before locking any deliveries.
|
|
428
|
+
def materialize_batch(limit, targets, preview_rows)
|
|
429
|
+
supersede_parents = select_rows(supersede_parent_ids_sql(preview_rows))
|
|
430
|
+
preview_ids = preview_rows.map { |row| row_value(row, :id) }
|
|
431
|
+
lock_event_rows!(preview_ids + supersede_parents.map { |row| row_value(row, :event_id) })
|
|
432
|
+
|
|
433
|
+
rows = select_rows(delivery_materialization_select_sql(limit, targets, event_ids: preview_ids))
|
|
434
|
+
return rows if rows.empty?
|
|
435
|
+
|
|
436
|
+
supersede_candidates = select_rows(materialization_supersede_parent_candidates_sql(rows, targets))
|
|
437
|
+
older_event_ids = supersede_candidates.map { |candidate| row_value(candidate, :event_id) }
|
|
438
|
+
older_delivery_ids = supersede_candidates.map { |candidate| row_value(candidate, :delivery_id) }
|
|
439
|
+
lock_delivery_rows!(older_delivery_ids)
|
|
440
|
+
supersede_pending_events_by_id!(older_event_ids)
|
|
441
|
+
supersede_pending_deliveries!(older_delivery_ids, event_ids: older_event_ids)
|
|
442
|
+
execute(delivery_materialization_insert_sql(rows, targets))
|
|
443
|
+
rows
|
|
444
|
+
end
|
|
445
|
+
|
|
418
446
|
def claim_pending_deliveries(limit:, worker_id:)
|
|
419
447
|
rows = claim_pending_delivery_rows(limit: limit, worker_id: worker_id)
|
|
420
448
|
|
|
@@ -431,51 +459,34 @@ module SearchEngine
|
|
|
431
459
|
return collection_limited_materialization_select_sql(targets, event_ids: event_ids, lock: lock)
|
|
432
460
|
end
|
|
433
461
|
|
|
434
|
-
limit = global_limit_for(limit)
|
|
435
462
|
<<~SQL
|
|
436
|
-
|
|
437
|
-
VALUES #{delivery_target_values_sql(targets)}
|
|
438
|
-
),
|
|
439
|
-
candidate_events AS MATERIALIZED (
|
|
440
|
-
SELECT outbox.id,
|
|
441
|
-
outbox.collection,
|
|
442
|
-
outbox.document_id
|
|
443
|
-
FROM #{quoted_table} outbox
|
|
444
|
-
WHERE outbox.status IN ('pending', 'processing', 'failed')
|
|
445
|
-
AND (outbox.next_attempt_at IS NULL OR outbox.next_attempt_at <= CURRENT_TIMESTAMP)
|
|
446
|
-
#{newer_event_exclusion_sql('outbox')}
|
|
447
|
-
#{event_id_filter_sql('outbox', event_ids)}
|
|
448
|
-
AND EXISTS (
|
|
449
|
-
SELECT 1
|
|
450
|
-
FROM target
|
|
451
|
-
WHERE NOT EXISTS (
|
|
452
|
-
SELECT 1
|
|
453
|
-
FROM #{quoted_delivery_table} deliveries
|
|
454
|
-
WHERE deliveries.event_id = outbox.id
|
|
455
|
-
AND deliveries.target_key = target.target_key
|
|
456
|
-
)
|
|
457
|
-
)
|
|
458
|
-
ORDER BY outbox.id ASC
|
|
459
|
-
LIMIT #{limit}
|
|
460
|
-
#{row_lock_sql(lock)}
|
|
461
|
-
),
|
|
462
|
-
latest_candidate_ids AS (
|
|
463
|
-
SELECT id
|
|
464
|
-
FROM (
|
|
465
|
-
SELECT id,
|
|
466
|
-
ROW_NUMBER() OVER (
|
|
467
|
-
PARTITION BY collection, document_id
|
|
468
|
-
ORDER BY id DESC
|
|
469
|
-
) AS row_number
|
|
470
|
-
FROM candidate_events
|
|
471
|
-
) ranked_candidate_events
|
|
472
|
-
WHERE row_number = 1
|
|
473
|
-
)
|
|
474
|
-
SELECT outbox.*
|
|
463
|
+
SELECT #{lock ? 'outbox.*' : 'outbox.id, outbox.collection, outbox.document_id'}
|
|
475
464
|
FROM #{quoted_table} outbox
|
|
476
|
-
|
|
477
|
-
ON latest_candidate_ids.id = outbox.id
|
|
465
|
+
WHERE #{materialization_filter_sql(targets, event_ids)}
|
|
478
466
|
ORDER BY outbox.id ASC
|
|
467
|
+
LIMIT #{global_limit_for(limit)}
|
|
468
|
+
#{row_lock_sql(lock)}
|
|
469
|
+
SQL
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Each membership check reads at most one unique delivery per requested key.
|
|
473
|
+
# The scalar newer-event probe also prevents a hash over the entire parent history.
|
|
474
|
+
def materialization_filter_sql(targets, event_ids)
|
|
475
|
+
keys = targets.map(&:key).uniq
|
|
476
|
+
<<~SQL.chomp
|
|
477
|
+
outbox.status IN ('pending', 'processing', 'failed')
|
|
478
|
+
AND (outbox.next_attempt_at IS NULL OR outbox.next_attempt_at <= CURRENT_TIMESTAMP)
|
|
479
|
+
#{event_id_filter_sql('outbox', event_ids)}
|
|
480
|
+
AND (
|
|
481
|
+
SELECT newer.id FROM #{quoted_table} newer
|
|
482
|
+
WHERE newer.collection = outbox.collection AND newer.document_id = outbox.document_id
|
|
483
|
+
AND newer.id > outbox.id
|
|
484
|
+
LIMIT 1
|
|
485
|
+
) IS NULL
|
|
486
|
+
AND (
|
|
487
|
+
SELECT COUNT(*) FROM #{quoted_delivery_table} deliveries
|
|
488
|
+
WHERE deliveries.event_id = outbox.id AND deliveries.target_key IN (#{ids_sql(keys)})
|
|
489
|
+
) < #{keys.size}
|
|
479
490
|
SQL
|
|
480
491
|
end
|
|
481
492
|
|
|
@@ -683,6 +694,8 @@ module SearchEngine
|
|
|
683
694
|
end
|
|
684
695
|
|
|
685
696
|
def select_scalar(sql)
|
|
697
|
+
# Advisory locks and quiescence polls must reach the current database session on every attempt.
|
|
698
|
+
return connection.exec_query(sql).rows.first&.first if connection.respond_to?(:exec_query)
|
|
686
699
|
return connection.select_value(sql) if connection.respond_to?(:select_value)
|
|
687
700
|
|
|
688
701
|
result = connection.execute(sql)
|
|
@@ -1594,64 +1607,32 @@ module SearchEngine
|
|
|
1594
1607
|
|
|
1595
1608
|
def collection_limited_materialization_select_sql(targets, event_ids: nil, lock: true)
|
|
1596
1609
|
<<~SQL
|
|
1597
|
-
WITH
|
|
1598
|
-
VALUES #{delivery_target_values_sql(targets)}
|
|
1599
|
-
),
|
|
1600
|
-
#{collection_limits_cte_sql},
|
|
1610
|
+
WITH #{collection_limits_cte_sql},
|
|
1601
1611
|
candidate_events AS MATERIALIZED (
|
|
1602
|
-
SELECT outbox.id,
|
|
1603
|
-
outbox.collection,
|
|
1604
|
-
outbox.document_id
|
|
1612
|
+
SELECT outbox.id, outbox.collection
|
|
1605
1613
|
FROM #{quoted_table} outbox
|
|
1606
|
-
WHERE
|
|
1607
|
-
AND (outbox.next_attempt_at IS NULL OR outbox.next_attempt_at <= CURRENT_TIMESTAMP)
|
|
1608
|
-
#{newer_event_exclusion_sql('outbox')}
|
|
1609
|
-
#{event_id_filter_sql('outbox', event_ids)}
|
|
1610
|
-
AND EXISTS (
|
|
1611
|
-
SELECT 1
|
|
1612
|
-
FROM target
|
|
1613
|
-
WHERE NOT EXISTS (
|
|
1614
|
-
SELECT 1
|
|
1615
|
-
FROM #{quoted_delivery_table} deliveries
|
|
1616
|
-
WHERE deliveries.event_id = outbox.id
|
|
1617
|
-
AND deliveries.target_key = target.target_key
|
|
1618
|
-
)
|
|
1619
|
-
)
|
|
1614
|
+
WHERE #{materialization_filter_sql(targets, event_ids)}
|
|
1620
1615
|
ORDER BY outbox.id ASC
|
|
1621
1616
|
LIMIT #{collection_limited_candidate_limit}
|
|
1622
1617
|
#{row_lock_sql(lock)}
|
|
1623
1618
|
),
|
|
1624
|
-
latest_candidate_ids AS (
|
|
1625
|
-
SELECT id,
|
|
1626
|
-
collection
|
|
1627
|
-
FROM (
|
|
1628
|
-
SELECT id,
|
|
1629
|
-
collection,
|
|
1630
|
-
ROW_NUMBER() OVER (
|
|
1631
|
-
PARTITION BY collection, document_id
|
|
1632
|
-
ORDER BY id DESC
|
|
1633
|
-
) AS row_number
|
|
1634
|
-
FROM candidate_events
|
|
1635
|
-
) ranked_candidate_events
|
|
1636
|
-
WHERE row_number = 1
|
|
1637
|
-
),
|
|
1638
1619
|
ranked_by_collection AS (
|
|
1639
|
-
SELECT
|
|
1620
|
+
SELECT candidate_events.id,
|
|
1640
1621
|
ROW_NUMBER() OVER (
|
|
1641
|
-
PARTITION BY
|
|
1642
|
-
ORDER BY
|
|
1622
|
+
PARTITION BY candidate_events.collection
|
|
1623
|
+
ORDER BY candidate_events.id ASC
|
|
1643
1624
|
) AS collection_row_number,
|
|
1644
1625
|
COALESCE(collection_limits.batch_size, #{global_limit_for(nil)}) AS collection_batch_size
|
|
1645
|
-
FROM
|
|
1626
|
+
FROM candidate_events
|
|
1646
1627
|
LEFT JOIN collection_limits
|
|
1647
|
-
ON collection_limits.collection =
|
|
1628
|
+
ON collection_limits.collection = candidate_events.collection
|
|
1648
1629
|
),
|
|
1649
1630
|
selected_candidate_ids AS (
|
|
1650
1631
|
SELECT id
|
|
1651
1632
|
FROM ranked_by_collection
|
|
1652
1633
|
WHERE collection_row_number <= collection_batch_size
|
|
1653
1634
|
)
|
|
1654
|
-
SELECT outbox.*
|
|
1635
|
+
SELECT #{lock ? 'outbox.*' : 'outbox.id, outbox.collection, outbox.document_id'}
|
|
1655
1636
|
FROM #{quoted_table} outbox
|
|
1656
1637
|
INNER JOIN selected_candidate_ids
|
|
1657
1638
|
ON selected_candidate_ids.id = outbox.id
|
|
@@ -1784,18 +1765,6 @@ module SearchEngine
|
|
|
1784
1765
|
Array(ids).compact.uniq(&:to_s)
|
|
1785
1766
|
end
|
|
1786
1767
|
|
|
1787
|
-
def newer_event_exclusion_sql(event_relation)
|
|
1788
|
-
<<~SQL.chomp
|
|
1789
|
-
AND NOT EXISTS (
|
|
1790
|
-
SELECT 1
|
|
1791
|
-
FROM #{quoted_table} newer_events
|
|
1792
|
-
WHERE newer_events.collection = #{event_relation}.collection
|
|
1793
|
-
AND newer_events.document_id = #{event_relation}.document_id
|
|
1794
|
-
AND newer_events.id > #{event_relation}.id
|
|
1795
|
-
)
|
|
1796
|
-
SQL
|
|
1797
|
-
end
|
|
1798
|
-
|
|
1799
1768
|
def processing_event_exclusion_sql(event_relation)
|
|
1800
1769
|
<<~SQL.chomp
|
|
1801
1770
|
AND NOT EXISTS (
|
|
@@ -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/materialization_cursor'
|
|
6
7
|
require 'search_engine/postgres_outbox/repository'
|
|
7
8
|
require 'search_engine/postgres_outbox/event_processor'
|
|
8
9
|
require 'search_engine/postgres_outbox/drainer'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nikita Shkoda
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -189,6 +189,7 @@ files:
|
|
|
189
189
|
- lib/search_engine/postgres_outbox/event.rb
|
|
190
190
|
- lib/search_engine/postgres_outbox/event_processor.rb
|
|
191
191
|
- lib/search_engine/postgres_outbox/listener.rb
|
|
192
|
+
- lib/search_engine/postgres_outbox/materialization_cursor.rb
|
|
192
193
|
- lib/search_engine/postgres_outbox/migration_helpers.rb
|
|
193
194
|
- lib/search_engine/postgres_outbox/processor_result.rb
|
|
194
195
|
- lib/search_engine/postgres_outbox/repository.rb
|