Skip to content

Commit 38c4932

Browse files
authored
Merge pull request #24 from tharropoulos/alias-swap-fix
fix: preserve live collection during alias-based reindex
2 parents a6ed99e + ebcab47 commit 38c4932

2 files changed

Lines changed: 79 additions & 2 deletions

File tree

lib/typesense-rails.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ class << base
270270
end
271271

272272
def collection_name_with_timestamp(options)
273-
"#{typesense_collection_name(options)}_#{Time.now.to_i}"
273+
"#{typesense_collection_name(options)}_#{Time.now.to_i}_#{SecureRandom.hex(4)}"
274274
end
275275

276276
def typesense_create_collection(collection_name, settings = nil, existing_collection: nil)
@@ -635,10 +635,11 @@ def typesense_reindex(batch_size = Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
635635
next if typesense_indexing_disabled?(options)
636636

637637
existing_collection_resources = {}
638+
old_collection_name = nil
638639
begin
639640
master_index = typesense_ensure_init(options, settings, false)
640641
existing_collection_resources = typesense_collection_resources(master_index[:alias_name])
641-
delete_collection(master_index[:alias_name])
642+
old_collection_name = master_index[:collection_name]
642643
rescue ArgumentError
643644
@typesense_indexes[settings] = { collection_name: "", alias_name: typesense_collection_name(options) }
644645
master_index = @typesense_indexes[settings]
@@ -665,6 +666,7 @@ def typesense_reindex(batch_size = Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
665666
end
666667

667668
upsert_alias(src_index_name, master_index[:alias_name])
669+
delete_collection(old_collection_name) if old_collection_name.present? && old_collection_name != src_index_name
668670
master_index[:collection_name] = src_index_name
669671
end
670672
nil

spec/integration_spec.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@
142142
create_table :misconfigured_blocks do |t|
143143
t.string :name
144144
end
145+
create_table :reindex_alias_probes do |t|
146+
t.string :name
147+
end
145148
if defined?(ActiveModel::Serializer)
146149
create_table :serialized_objects do |t|
147150
t.string :name
@@ -248,6 +251,14 @@ def self.truth
248251
end
249252
end
250253

254+
class ReindexAliasProbe < ActiveRecord::Base
255+
include Typesense
256+
257+
typesense auto_index: false, index_name: safe_index_name("ReindexAliasProbe") do
258+
attribute :name
259+
end
260+
end
261+
251262
module Namespaced
252263
def self.table_name_prefix
253264
"namespaced_"
@@ -991,6 +1002,33 @@ class SerializedObject < ActiveRecord::Base
9911002
expect(Product.search("*", "", { "per_page" => Typesense::IndexSettings::DEFAULT_BATCH_SIZE }).size).to eq(n)
9921003
end
9931004

1005+
it "does not delete the live alias target before swapping during reindex" do
1006+
alias_name = Product.index_name
1007+
old_collection_name = "#{alias_name}_old"
1008+
new_collection_name = "#{alias_name}_new"
1009+
previous_indexes = Product.instance_variable_get(:@typesense_indexes)
1010+
1011+
Product.instance_variable_set(:@typesense_indexes, {})
1012+
1013+
allow(Product).to receive(:get_collection).with(alias_name).and_return({ "name" => old_collection_name })
1014+
allow(Product).to receive(:typesense_collection_resources).with(alias_name).and_return({})
1015+
allow(Product).to receive(:collection_name_with_timestamp).and_return(new_collection_name)
1016+
allow(Product).to receive(:typesense_find_in_batches)
1017+
1018+
expect(Product).not_to receive(:delete_collection).with(alias_name)
1019+
expect(Product).to receive(:create_collection).with(
1020+
new_collection_name,
1021+
Product.typesense_settings,
1022+
existing_collection: {}
1023+
).ordered
1024+
expect(Product).to receive(:upsert_alias).with(new_collection_name, alias_name).ordered
1025+
expect(Product).to receive(:delete_collection).with(old_collection_name).ordered
1026+
1027+
Product.reindex(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
1028+
ensure
1029+
Product.instance_variable_set(:@typesense_indexes, previous_indexes)
1030+
end
1031+
9941032
it "should not return products that are not indexable" do
9951033
@sekrit.index!
9961034
@no_href.index!
@@ -1071,6 +1109,43 @@ class SerializedObject < ActiveRecord::Base
10711109
end
10721110
end
10731111

1112+
describe "ReindexAliasProbe" do
1113+
before(:each) do
1114+
ReindexAliasProbe.delete_all
1115+
ReindexAliasProbe.clear_index!
1116+
rescue StandardError
1117+
ArgumentError
1118+
ensure
1119+
ReindexAliasProbe.create!(name: "alpha")
1120+
ReindexAliasProbe.create!(name: "beta")
1121+
ReindexAliasProbe.reindex(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
1122+
end
1123+
1124+
after(:each) do
1125+
ReindexAliasProbe.delete_all
1126+
ReindexAliasProbe.clear_index!
1127+
rescue StandardError
1128+
ArgumentError
1129+
end
1130+
1131+
it "keeps the alias searchable while reindex builds the replacement collection" do
1132+
alias_name = ReindexAliasProbe.index_name
1133+
search_params = { q: "alpha", query_by: "name" }
1134+
1135+
expect(ReindexAliasProbe.typesense_client.collections[alias_name].documents.search(search_params)["found"]).to eq(1)
1136+
1137+
expect(ReindexAliasProbe).to receive(:create_collection).and_wrap_original do |original, *args, **kwargs|
1138+
expect(
1139+
ReindexAliasProbe.typesense_client.collections[alias_name].documents.search(search_params)["found"]
1140+
).to eq(1)
1141+
original.call(*args, **kwargs)
1142+
end
1143+
1144+
ReindexAliasProbe.reindex(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
1145+
expect(ReindexAliasProbe.typesense_client.collections[alias_name].documents.search(search_params)["found"]).to eq(1)
1146+
end
1147+
end
1148+
10741149
describe "Book" do
10751150
before(:all) do
10761151
Book.clear_index!

0 commit comments

Comments
 (0)