From 2a6dbbd16942e18fc4b92241aa0c312bf554cd55 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 16:42:15 +0100 Subject: [PATCH 1/7] test: support non-indexed stored fixture fields --- test/integration/fixtures.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index 2bceb86..12235f9 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -38,7 +38,7 @@ export interface SolrIntegrationFixture { addDocuments(documents: readonly (SolrDocument & { id: string })[]): Promise; commit(): Promise; createCore(prefix?: string): Promise; - addSchemaField(name: string, type?: string, docValues?: boolean): Promise; + addSchemaField(name: string, type?: string, docValues?: boolean, indexed?: boolean): Promise; deleteSchemaField(name: string): Promise; addSchemaFieldType(fieldType: SolrSchemaFieldType): Promise; addSchemaDynamicField(field: SolrSchemaDynamicField): Promise; @@ -101,8 +101,8 @@ export function integrationTest( createdCores.add(name); return name; }, - async addSchemaField(name, type = "string", docValues = false) { - const response = await client.schema({ "add-field": { name, type, stored: true, indexed: true, docValues } }); + async addSchemaField(name, type = "string", docValues = false, indexed = true) { + const response = await client.schema({ "add-field": { name, type, stored: true, indexed, docValues } }); schemaFields.add(name); return response; }, From 1574fb67ac6eaa365577d8ff855a370f8c5f1ac0 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 16:42:18 +0100 Subject: [PATCH 2/7] test: verify ReplicationHandler abort lifecycle --- .../solr-replication-lifecycle.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index cc7eda8..bbff42e 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { randomBytes } from "node:crypto"; import { SolrClient } from "../../src/index.js"; import { integrationTest } from "./fixtures.js"; @@ -77,3 +78,87 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso const followerGeneration = followerVersion.generation ?? Number(followerVersion.indexversion); assert.ok(Number.isFinite(followerGeneration) && followerGeneration > 0, "follower should publish its fetched index"); }); + +integrationTest("ReplicationHandler aborts an in-progress fetch and reports its state", async ({ + createCore, + createId, + addSchemaField, + addDocuments, + commit +}) => { + const leaderCore = await createCore("replication_abort_leader"); + const followerCore = await createCore("replication_abort_follower"); + const baseUrl = process.env.SOLR_BASE_URL!; + const username = process.env.SOLR_USERNAME; + const password = process.env.SOLR_PASSWORD; + const makeClient = (core: string) => new SolrClient({ + baseUrl, + core, + timeoutMs: 120_000, + ...(username && password ? { auth: { type: "basic" as const, username, password } } : {}) + }); + const leader = makeClient(leaderCore); + const follower = makeClient(followerCore); + const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}`; + const documentId = createId("replication_abort"); + const storedField = createId("replication_payload"); + const payload = randomBytes(12 * 1024 * 1024).toString("base64"); + + await addSchemaField(storedField, "string", false, false); + await leader.add([{ id: documentId, [storedField]: payload }]); + await leader.commit({ waitSearcher: true }); + + await leader.config({ + "update-requesthandler": { + name: "/replication", + class: "solr.ReplicationHandler", + customOptions: { leader: { replicateAfter: ["commit"] } } + } + }); + await follower.config({ + "update-requesthandler": { + name: "/replication", + class: "solr.ReplicationHandler", + customOptions: { follower: { leaderUrl } } + } + }); + + const fetchResponse = await follower.replication("fetchindex", { leaderUrl }); + assert.equal(fetchResponse.status, "OK", "fetchindex should start asynchronously"); + + const isReplicating = async (): Promise => { + const response = await follower.replication("details"); + const details = response.details as { follower?: { isReplicating?: string | boolean } } | undefined; + return String(details?.follower?.isReplicating).toLowerCase() === "true"; + }; + + let observedActiveFetch = false; + for (let attempt = 0; attempt < 200; attempt += 1) { + if (await isReplicating()) { + observedActiveFetch = true; + break; + } + await new Promise(resolve => setTimeout(resolve, 25)); + } + assert.ok(observedActiveFetch, "details should expose isReplicating=true before the large fetch completes"); + + const abortResponse = await follower.replication("abortfetch"); + assert.equal(abortResponse.status, "OK", "abortfetch should acknowledge the cancellation request"); + + let fetchStopped = false; + for (let attempt = 0; attempt < 200; attempt += 1) { + if (!(await isReplicating())) { + fetchStopped = true; + break; + } + await new Promise(resolve => setTimeout(resolve, 25)); + } + assert.ok(fetchStopped, "details should report that the fetch stopped after abortfetch"); + + const result = await follower.query<{ response?: { numFound?: number } }>({ + query: `id:"${documentId}"`, + fields: ["id"], + limit: 1 + }); + assert.equal(result.response?.numFound ?? 0, 0, "an aborted fetch must not install the leader document"); +}); From 37684d33030057bb34fa129118686f37b1cd8946 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 16:42:28 +0100 Subject: [PATCH 3/7] test: keep integration fixture API unchanged --- test/integration/fixtures.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index 12235f9..2bceb86 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -38,7 +38,7 @@ export interface SolrIntegrationFixture { addDocuments(documents: readonly (SolrDocument & { id: string })[]): Promise; commit(): Promise; createCore(prefix?: string): Promise; - addSchemaField(name: string, type?: string, docValues?: boolean, indexed?: boolean): Promise; + addSchemaField(name: string, type?: string, docValues?: boolean): Promise; deleteSchemaField(name: string): Promise; addSchemaFieldType(fieldType: SolrSchemaFieldType): Promise; addSchemaDynamicField(field: SolrSchemaDynamicField): Promise; @@ -101,8 +101,8 @@ export function integrationTest( createdCores.add(name); return name; }, - async addSchemaField(name, type = "string", docValues = false, indexed = true) { - const response = await client.schema({ "add-field": { name, type, stored: true, indexed, docValues } }); + async addSchemaField(name, type = "string", docValues = false) { + const response = await client.schema({ "add-field": { name, type, stored: true, indexed: true, docValues } }); schemaFields.add(name); return response; }, From d862167d2104c30c9b1eed235be51e798c8b5e2b Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 16:42:31 +0100 Subject: [PATCH 4/7] test: create stored-only payload on isolated leader core --- test/integration/solr-replication-lifecycle.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index bbff42e..ccd8aa3 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -82,9 +82,6 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso integrationTest("ReplicationHandler aborts an in-progress fetch and reports its state", async ({ createCore, createId, - addSchemaField, - addDocuments, - commit }) => { const leaderCore = await createCore("replication_abort_leader"); const followerCore = await createCore("replication_abort_follower"); @@ -104,7 +101,7 @@ integrationTest("ReplicationHandler aborts an in-progress fetch and reports its const storedField = createId("replication_payload"); const payload = randomBytes(12 * 1024 * 1024).toString("base64"); - await addSchemaField(storedField, "string", false, false); + await leader.schema({ "add-field": { name: storedField, type: "string", stored: true, indexed: false, docValues: false } }); await leader.add([{ id: documentId, [storedField]: payload }]); await leader.commit({ waitSearcher: true }); From d7e190c20a8c022ab383ea053aa446f8d07eead5 Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 16:42:49 +0100 Subject: [PATCH 5/7] test: support authenticated ReplicationHandler abort fixture --- test/integration/solr-replication-lifecycle.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index ccd8aa3..96ec900 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -116,11 +116,14 @@ integrationTest("ReplicationHandler aborts an in-progress fetch and reports its "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", - customOptions: { follower: { leaderUrl } } + customOptions: { follower: { leaderUrl, ...(username && password ? { httpBasicAuthUser: username, httpBasicAuthPassword: password } : {}) } } } }); - const fetchResponse = await follower.replication("fetchindex", { leaderUrl }); + const fetchResponse = await follower.replication("fetchindex", { + leaderUrl, + ...(username && password ? { customParams: { httpBasicAuthUser: username, httpBasicAuthPassword: password } } : {}) + }); assert.equal(fetchResponse.status, "OK", "fetchindex should start asynchronously"); const isReplicating = async (): Promise => { From b55dca6fcbc9757f0b9f506bb5feaaef261ff4ec Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 16:48:27 +0100 Subject: [PATCH 6/7] test: throttle leader transfer for deterministic abort coverage --- test/integration/solr-replication-lifecycle.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 96ec900..3cbd440 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -27,7 +27,11 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", - customOptions: { leader: { replicateAfter: ["commit"] } } + customOptions: { + leader: { replicateAfter: ["commit"] }, + // Throttle leader file streaming so the follower remains in a real in-progress fetch. + defaults: { maxWriteMBPerSec: 0.05 } + } } }); assert.equal(leaderConfig.responseHeader?.status, 0); From 2caaa4b335ebda04edd1f0d2fefefaf75a63affb Mon Sep 17 00:00:00 2001 From: Viliam Simko Date: Wed, 4 Nov 2026 16:48:36 +0100 Subject: [PATCH 7/7] test: throttle leader transfer for deterministic abort coverage --- test/integration/solr-replication-lifecycle.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/integration/solr-replication-lifecycle.test.ts b/test/integration/solr-replication-lifecycle.test.ts index 3cbd440..8a2b96f 100644 --- a/test/integration/solr-replication-lifecycle.test.ts +++ b/test/integration/solr-replication-lifecycle.test.ts @@ -27,11 +27,7 @@ integrationTest("ReplicationHandler fetches a committed leader index into an iso "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", - customOptions: { - leader: { replicateAfter: ["commit"] }, - // Throttle leader file streaming so the follower remains in a real in-progress fetch. - defaults: { maxWriteMBPerSec: 0.05 } - } + customOptions: { leader: { replicateAfter: ["commit"] } } } }); assert.equal(leaderConfig.responseHeader?.status, 0); @@ -113,7 +109,11 @@ integrationTest("ReplicationHandler aborts an in-progress fetch and reports its "update-requesthandler": { name: "/replication", class: "solr.ReplicationHandler", - customOptions: { leader: { replicateAfter: ["commit"] } } + customOptions: { + leader: { replicateAfter: ["commit"] }, + // Throttle leader file streaming so the follower remains in a real in-progress fetch. + defaults: { maxWriteMBPerSec: 0.05 } + } } }); await follower.config({