search-engine-for-typesense 30.1.8.17 → 30.1.8.19
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20f330c7ec29b4f4ea6366c48d23f05d5a089e47eaeba79f0ad741c2a83b09ec
|
|
4
|
+
data.tar.gz: 1359ad4c68d304d334c238bae7beddeee57a6350565b8b8e5b6b8d3f97c75e82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55191671cd8fb5dc620d25b605715b7608db8f8de48c220e7fe3d59727c239a9dd386574a421fd4477b6170f71c010575acf8b5c3e961a4fe2a455a568fe3e1c
|
|
7
|
+
data.tar.gz: f74ac6499069ed09918eac51273bcfcd5e82e0d91295001a9f3c5505b1ad69159f3c92f75c796d0cef43af29ee3159ecc79438c285fd15b2a4b0cb666d4618e1
|
data/README.md
CHANGED
|
@@ -405,6 +405,11 @@ inspected before deletion because they contain the last error and retry state. A
|
|
|
405
405
|
only rows with `status IN ('processed', 'superseded')` and `processed_at` older than
|
|
406
406
|
`c.postgres_outbox.retention_s`.
|
|
407
407
|
|
|
408
|
+
In delivery-target mode, cleanup can first call
|
|
409
|
+
`SearchEngine::PostgresOutbox::Repository#refresh_terminal_delivery_event_statuses!`. This bounded helper
|
|
410
|
+
repairs parent events whose delivery rows are all terminal but whose parent status is still non-terminal,
|
|
411
|
+
then normal retention cleanup can delete the repaired parent rows and cascade their deliveries.
|
|
412
|
+
|
|
408
413
|
## Example app
|
|
409
414
|
|
|
410
415
|
See `examples/demo_shop` — demonstrates single/multi search, JOINs, grouping, presets/curation, and DX/observability. Supports offline mode via the stub client (see [Testing](https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/testing)).
|
|
@@ -42,6 +42,9 @@ module SearchEngine
|
|
|
42
42
|
add_index table_name,
|
|
43
43
|
%i[collection document_id status id],
|
|
44
44
|
name: 'idx_search_engine_outbox_coalescing'
|
|
45
|
+
add_index table_name,
|
|
46
|
+
%i[collection document_id id],
|
|
47
|
+
name: 'idx_search_engine_outbox_coalesce_lookup'
|
|
45
48
|
add_index table_name,
|
|
46
49
|
:locked_at,
|
|
47
50
|
name: 'idx_search_engine_outbox_processing',
|
|
@@ -128,6 +128,24 @@ module SearchEngine
|
|
|
128
128
|
rows
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
# Refresh cleanup-eligible parent event statuses from terminal delivery rows.
|
|
132
|
+
#
|
|
133
|
+
# This repairs historical residue where every delivery row for an event is
|
|
134
|
+
# terminal but the parent event still has a non-terminal status. The
|
|
135
|
+
# candidate set is bounded before aggregate work to keep cleanup safe on
|
|
136
|
+
# large outbox tables.
|
|
137
|
+
#
|
|
138
|
+
# @param retention_s [Integer] retention window in seconds
|
|
139
|
+
# @param limit [Integer, nil] maximum parent events to refresh
|
|
140
|
+
# @return [Integer] refreshed parent event count
|
|
141
|
+
def refresh_terminal_delivery_event_statuses!(retention_s:, limit: nil)
|
|
142
|
+
return 0 unless delivery_table_exists?
|
|
143
|
+
|
|
144
|
+
rows = select_rows(terminal_delivery_event_status_refresh_sql(retention_s: retention_s, limit: limit))
|
|
145
|
+
|
|
146
|
+
rows.size
|
|
147
|
+
end
|
|
148
|
+
|
|
131
149
|
# Check whether the optional drain slot table exists.
|
|
132
150
|
#
|
|
133
151
|
# @return [Boolean]
|
|
@@ -360,14 +378,27 @@ module SearchEngine
|
|
|
360
378
|
|
|
361
379
|
def reset_stale_delivery_processing!
|
|
362
380
|
execute(<<~SQL)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
381
|
+
WITH reset_deliveries AS (
|
|
382
|
+
UPDATE #{quoted_delivery_table}
|
|
383
|
+
SET status = 'pending',
|
|
384
|
+
locked_at = NULL,
|
|
385
|
+
locked_by = NULL,
|
|
386
|
+
updated_at = CURRENT_TIMESTAMP
|
|
387
|
+
WHERE target_key = #{quote(target_key)}
|
|
388
|
+
AND status = 'processing'
|
|
389
|
+
AND locked_at < (CURRENT_TIMESTAMP - interval '#{processing_timeout_s} seconds')
|
|
390
|
+
RETURNING event_id
|
|
391
|
+
),
|
|
392
|
+
aggregate AS (
|
|
393
|
+
#{event_status_aggregate_sql('SELECT event_id FROM reset_deliveries')}
|
|
394
|
+
)
|
|
395
|
+
UPDATE #{quoted_table} events
|
|
396
|
+
SET status = aggregate.status,
|
|
397
|
+
processed_at = #{aggregate_processed_at_sql},
|
|
398
|
+
last_error = aggregate.last_error,
|
|
367
399
|
updated_at = CURRENT_TIMESTAMP
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
AND locked_at < (CURRENT_TIMESTAMP - interval '#{processing_timeout_s} seconds')
|
|
400
|
+
FROM aggregate
|
|
401
|
+
WHERE events.id = aggregate.event_id
|
|
371
402
|
SQL
|
|
372
403
|
end
|
|
373
404
|
|
|
@@ -470,26 +501,33 @@ module SearchEngine
|
|
|
470
501
|
|
|
471
502
|
def materialization_supersede_older_deliveries_sql(rows, targets)
|
|
472
503
|
<<~SQL
|
|
473
|
-
WITH
|
|
504
|
+
WITH latest(collection, document_id, id) AS (
|
|
505
|
+
VALUES #{coalesce_values_sql(rows)}
|
|
506
|
+
),
|
|
507
|
+
target(target_key, queue_name) AS (
|
|
508
|
+
VALUES #{delivery_target_values_sql(targets)}
|
|
509
|
+
),
|
|
510
|
+
older_event_targets AS MATERIALIZED (
|
|
511
|
+
SELECT older_events.id AS event_id,
|
|
512
|
+
target.target_key
|
|
513
|
+
FROM latest
|
|
514
|
+
CROSS JOIN target
|
|
515
|
+
INNER JOIN #{quoted_table} older_events
|
|
516
|
+
ON older_events.collection = latest.collection
|
|
517
|
+
AND older_events.document_id = latest.document_id
|
|
518
|
+
AND older_events.id < latest.id
|
|
519
|
+
),
|
|
520
|
+
updated_deliveries AS (
|
|
474
521
|
UPDATE #{quoted_delivery_table} older_deliveries
|
|
475
522
|
SET status = 'superseded',
|
|
476
523
|
processed_at = CURRENT_TIMESTAMP,
|
|
477
524
|
locked_at = NULL,
|
|
478
525
|
locked_by = NULL,
|
|
479
526
|
updated_at = CURRENT_TIMESTAMP
|
|
480
|
-
FROM
|
|
481
|
-
|
|
482
|
-
VALUES #{coalesce_values_sql(rows)}
|
|
483
|
-
) AS latest(collection, document_id, id),
|
|
484
|
-
(
|
|
485
|
-
VALUES #{delivery_target_values_sql(targets)}
|
|
486
|
-
) AS target(target_key, queue_name)
|
|
487
|
-
WHERE older_deliveries.event_id = older_events.id
|
|
527
|
+
FROM older_event_targets
|
|
528
|
+
WHERE older_deliveries.event_id = older_event_targets.event_id
|
|
488
529
|
AND older_deliveries.status = 'pending'
|
|
489
|
-
AND older_deliveries.target_key =
|
|
490
|
-
AND older_events.collection = latest.collection
|
|
491
|
-
AND older_events.document_id = latest.document_id
|
|
492
|
-
AND older_events.id < latest.id
|
|
530
|
+
AND older_deliveries.target_key = older_event_targets.target_key
|
|
493
531
|
RETURNING older_deliveries.event_id
|
|
494
532
|
),
|
|
495
533
|
aggregate AS (
|
|
@@ -497,10 +535,7 @@ module SearchEngine
|
|
|
497
535
|
)
|
|
498
536
|
UPDATE #{quoted_table} events
|
|
499
537
|
SET status = aggregate.status,
|
|
500
|
-
processed_at =
|
|
501
|
-
WHEN aggregate.status IN ('processed', 'superseded') THEN CURRENT_TIMESTAMP
|
|
502
|
-
ELSE NULL
|
|
503
|
-
END,
|
|
538
|
+
processed_at = #{aggregate_processed_at_sql},
|
|
504
539
|
last_error = aggregate.last_error,
|
|
505
540
|
updated_at = CURRENT_TIMESTAMP
|
|
506
541
|
FROM aggregate
|
|
@@ -528,23 +563,29 @@ module SearchEngine
|
|
|
528
563
|
|
|
529
564
|
def delivery_supersede_older_pending_sql(rows)
|
|
530
565
|
<<~SQL
|
|
531
|
-
WITH
|
|
566
|
+
WITH latest(target_key, collection, document_id, event_id, delivery_id) AS (
|
|
567
|
+
VALUES #{delivery_coalesce_values_sql(rows)}
|
|
568
|
+
),
|
|
569
|
+
older_event_targets AS MATERIALIZED (
|
|
570
|
+
SELECT older_events.id AS event_id,
|
|
571
|
+
latest.target_key
|
|
572
|
+
FROM latest
|
|
573
|
+
INNER JOIN #{quoted_table} older_events
|
|
574
|
+
ON older_events.collection = latest.collection
|
|
575
|
+
AND older_events.document_id = latest.document_id
|
|
576
|
+
AND older_events.id < latest.event_id
|
|
577
|
+
),
|
|
578
|
+
updated_deliveries AS (
|
|
532
579
|
UPDATE #{quoted_delivery_table} older_deliveries
|
|
533
580
|
SET status = 'superseded',
|
|
534
581
|
processed_at = CURRENT_TIMESTAMP,
|
|
535
582
|
locked_at = NULL,
|
|
536
583
|
locked_by = NULL,
|
|
537
584
|
updated_at = CURRENT_TIMESTAMP
|
|
538
|
-
FROM
|
|
539
|
-
|
|
540
|
-
VALUES #{delivery_coalesce_values_sql(rows)}
|
|
541
|
-
) AS latest(target_key, collection, document_id, event_id, delivery_id)
|
|
542
|
-
WHERE older_deliveries.event_id = older_events.id
|
|
585
|
+
FROM older_event_targets
|
|
586
|
+
WHERE older_deliveries.event_id = older_event_targets.event_id
|
|
543
587
|
AND older_deliveries.status = 'pending'
|
|
544
|
-
AND older_deliveries.target_key =
|
|
545
|
-
AND older_events.collection = latest.collection
|
|
546
|
-
AND older_events.document_id = latest.document_id
|
|
547
|
-
AND older_events.id < latest.event_id
|
|
588
|
+
AND older_deliveries.target_key = older_event_targets.target_key
|
|
548
589
|
RETURNING older_deliveries.event_id
|
|
549
590
|
),
|
|
550
591
|
aggregate AS (
|
|
@@ -552,10 +593,7 @@ module SearchEngine
|
|
|
552
593
|
)
|
|
553
594
|
UPDATE #{quoted_table} events
|
|
554
595
|
SET status = aggregate.status,
|
|
555
|
-
processed_at =
|
|
556
|
-
WHEN aggregate.status IN ('processed', 'superseded') THEN CURRENT_TIMESTAMP
|
|
557
|
-
ELSE NULL
|
|
558
|
-
END,
|
|
596
|
+
processed_at = #{aggregate_processed_at_sql},
|
|
559
597
|
last_error = aggregate.last_error,
|
|
560
598
|
updated_at = CURRENT_TIMESTAMP
|
|
561
599
|
FROM aggregate
|
|
@@ -582,47 +620,60 @@ module SearchEngine
|
|
|
582
620
|
ids = Array(event_ids).compact
|
|
583
621
|
return if ids.empty?
|
|
584
622
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
623
|
+
with_delivery_status_refresh(ids) do
|
|
624
|
+
execute(<<~SQL)
|
|
625
|
+
UPDATE #{quoted_delivery_table}
|
|
626
|
+
SET status = #{quote(status)},
|
|
627
|
+
#{extra},
|
|
628
|
+
locked_at = NULL,
|
|
629
|
+
locked_by = NULL,
|
|
630
|
+
updated_at = CURRENT_TIMESTAMP
|
|
631
|
+
WHERE target_key = #{quote(target_key)}
|
|
632
|
+
AND event_id IN (#{ids_sql(ids)})
|
|
633
|
+
SQL
|
|
634
|
+
end
|
|
596
635
|
end
|
|
597
636
|
|
|
598
637
|
def mark_delivery_retryable!(event_ids, error:)
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
638
|
+
with_delivery_status_refresh(event_ids) do
|
|
639
|
+
execute(<<~SQL)
|
|
640
|
+
UPDATE #{quoted_delivery_table}
|
|
641
|
+
SET attempts = attempts + 1,
|
|
642
|
+
status = CASE WHEN attempts + 1 >= #{max_attempts} THEN 'failed' ELSE 'pending' END,
|
|
643
|
+
next_attempt_at = CURRENT_TIMESTAMP + #{retry_interval_case_sql},
|
|
644
|
+
locked_at = NULL,
|
|
645
|
+
locked_by = NULL,
|
|
646
|
+
last_error = #{quote(truncate_error(error))},
|
|
647
|
+
updated_at = CURRENT_TIMESTAMP
|
|
648
|
+
WHERE target_key = #{quote(target_key)}
|
|
649
|
+
AND event_id IN (#{ids_sql(event_ids)})
|
|
650
|
+
SQL
|
|
651
|
+
end
|
|
612
652
|
end
|
|
613
653
|
|
|
614
654
|
def mark_delivery_failed!(event_ids, error:)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
655
|
+
with_delivery_status_refresh(event_ids) do
|
|
656
|
+
execute(<<~SQL)
|
|
657
|
+
UPDATE #{quoted_delivery_table}
|
|
658
|
+
SET status = 'failed',
|
|
659
|
+
locked_at = NULL,
|
|
660
|
+
locked_by = NULL,
|
|
661
|
+
last_error = #{quote(truncate_error(error))},
|
|
662
|
+
updated_at = CURRENT_TIMESTAMP
|
|
663
|
+
WHERE target_key = #{quote(target_key)}
|
|
664
|
+
AND event_id IN (#{ids_sql(event_ids)})
|
|
665
|
+
SQL
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def with_delivery_status_refresh(event_ids)
|
|
670
|
+
ids = Array(event_ids).compact
|
|
671
|
+
return if ids.empty?
|
|
672
|
+
|
|
673
|
+
connection.transaction do
|
|
674
|
+
yield
|
|
675
|
+
refresh_event_statuses!(ids)
|
|
676
|
+
end
|
|
626
677
|
end
|
|
627
678
|
|
|
628
679
|
def refresh_event_statuses!(event_ids)
|
|
@@ -632,10 +683,7 @@ module SearchEngine
|
|
|
632
683
|
execute(<<~SQL)
|
|
633
684
|
UPDATE #{quoted_table} events
|
|
634
685
|
SET status = aggregate.status,
|
|
635
|
-
processed_at =
|
|
636
|
-
WHEN aggregate.status IN ('processed', 'superseded') THEN CURRENT_TIMESTAMP
|
|
637
|
-
ELSE NULL
|
|
638
|
-
END,
|
|
686
|
+
processed_at = #{aggregate_processed_at_sql},
|
|
639
687
|
last_error = aggregate.last_error,
|
|
640
688
|
updated_at = CURRENT_TIMESTAMP
|
|
641
689
|
FROM (
|
|
@@ -655,6 +703,7 @@ module SearchEngine
|
|
|
655
703
|
WHEN COUNT(*) FILTER (WHERE status = 'processed') > 0 THEN 'processed'
|
|
656
704
|
ELSE 'pending'
|
|
657
705
|
END AS status,
|
|
706
|
+
MAX(processed_at) FILTER (WHERE status IN ('processed', 'superseded')) AS terminal_processed_at,
|
|
658
707
|
(ARRAY_AGG(last_error ORDER BY updated_at DESC) FILTER (WHERE last_error IS NOT NULL))[1] AS last_error
|
|
659
708
|
FROM #{quoted_delivery_table}
|
|
660
709
|
WHERE event_id IN (#{event_ids_sql})
|
|
@@ -662,6 +711,63 @@ module SearchEngine
|
|
|
662
711
|
SQL
|
|
663
712
|
end
|
|
664
713
|
|
|
714
|
+
def terminal_delivery_event_status_refresh_sql(retention_s:, limit:)
|
|
715
|
+
retention_seconds = [retention_s.to_i, 0].max
|
|
716
|
+
batch_limit = global_limit_for(limit)
|
|
717
|
+
|
|
718
|
+
<<~SQL
|
|
719
|
+
WITH candidate_events AS MATERIALIZED (
|
|
720
|
+
SELECT events.id
|
|
721
|
+
FROM #{quoted_table} events
|
|
722
|
+
WHERE events.status NOT IN ('processed', 'superseded')
|
|
723
|
+
AND EXISTS (
|
|
724
|
+
SELECT 1
|
|
725
|
+
FROM #{quoted_delivery_table} deliveries
|
|
726
|
+
WHERE deliveries.event_id = events.id
|
|
727
|
+
)
|
|
728
|
+
AND NOT EXISTS (
|
|
729
|
+
SELECT 1
|
|
730
|
+
FROM #{quoted_delivery_table} deliveries
|
|
731
|
+
WHERE deliveries.event_id = events.id
|
|
732
|
+
AND deliveries.status IN ('pending', 'processing', 'failed')
|
|
733
|
+
)
|
|
734
|
+
ORDER BY events.id ASC
|
|
735
|
+
LIMIT #{batch_limit}
|
|
736
|
+
FOR UPDATE SKIP LOCKED
|
|
737
|
+
),
|
|
738
|
+
eligible_events AS (
|
|
739
|
+
SELECT candidate_events.id
|
|
740
|
+
FROM candidate_events
|
|
741
|
+
WHERE (
|
|
742
|
+
SELECT MAX(deliveries.processed_at)
|
|
743
|
+
FROM #{quoted_delivery_table} deliveries
|
|
744
|
+
WHERE deliveries.event_id = candidate_events.id
|
|
745
|
+
AND deliveries.status IN ('processed', 'superseded')
|
|
746
|
+
) < (CURRENT_TIMESTAMP - interval '#{retention_seconds} seconds')
|
|
747
|
+
),
|
|
748
|
+
aggregate AS (
|
|
749
|
+
#{event_status_aggregate_sql('SELECT id FROM eligible_events')}
|
|
750
|
+
),
|
|
751
|
+
updated_events AS (
|
|
752
|
+
UPDATE #{quoted_table} events
|
|
753
|
+
SET status = aggregate.status,
|
|
754
|
+
processed_at = #{aggregate_processed_at_sql},
|
|
755
|
+
last_error = aggregate.last_error,
|
|
756
|
+
updated_at = CURRENT_TIMESTAMP
|
|
757
|
+
FROM aggregate
|
|
758
|
+
WHERE events.id = aggregate.event_id
|
|
759
|
+
RETURNING events.id
|
|
760
|
+
)
|
|
761
|
+
SELECT id
|
|
762
|
+
FROM updated_events
|
|
763
|
+
SQL
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def aggregate_processed_at_sql
|
|
767
|
+
"CASE WHEN aggregate.status IN ('processed', 'superseded') " \
|
|
768
|
+
'THEN COALESCE(aggregate.terminal_processed_at, CURRENT_TIMESTAMP) ELSE NULL END'
|
|
769
|
+
end
|
|
770
|
+
|
|
665
771
|
def select_rows(sql)
|
|
666
772
|
result = connection.select_all(sql)
|
|
667
773
|
return result.to_a if result.respond_to?(:to_a)
|
|
@@ -819,6 +925,15 @@ module SearchEngine
|
|
|
819
925
|
SearchEngine.config.postgres_outbox.drain_slot_table_name
|
|
820
926
|
end
|
|
821
927
|
|
|
928
|
+
def delivery_table_exists?
|
|
929
|
+
table_name = SearchEngine.config.postgres_outbox.delivery_table_name
|
|
930
|
+
if connection.respond_to?(:data_source_exists?)
|
|
931
|
+
connection.data_source_exists?(table_name)
|
|
932
|
+
else
|
|
933
|
+
connection.table_exists?(table_name)
|
|
934
|
+
end
|
|
935
|
+
end
|
|
936
|
+
|
|
822
937
|
def quote(value)
|
|
823
938
|
connection.quote(value)
|
|
824
939
|
end
|
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.1.8.
|
|
4
|
+
version: 30.1.8.19
|
|
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
|
+
date: 2026-07-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|