Skip to content
Closed
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
env:
SOLR_BASE_URL: http://localhost:8983/solr
SOLR_CORE: integration
SOLR_VERSION: "${{ matrix.solr-version }}"
SOLR_BACKUP_LOCATION: /var/solr/backups
- name: Print Solr logs
if: always()
Expand Down
2 changes: 1 addition & 1 deletion docs/api-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ A generic request method does not mean that every endpoint has a typed wrapper,
| Security | `security(area, command)` | Security-enabled standalone matrix covers anonymous 401 responses for both authentication and authorization, authenticated reads, authentication property mutation/read-back, and malformed JSON rejection on Solr 8.11.4/9.11.0/10.1.0. Unit tests verify Basic/Bearer credentials and Authorization headers are not exposed through client HTTP errors. See Issues #86–#88. |
| System | `systemInfo()`, `systemProperties()`, `systemThreads()`, `systemLogging()` | System info, non-empty JVM/process properties, a non-empty thread dump, and logger metadata are exercised on pinned Solr 8.11.4/9.11.0/10.1.0. Optional fields remain version-dependent; response models preserve unknown fields. See Issues #89 and #91. |\n| Metrics | `metrics()` with typed group/registry/prefix/regex filters; exported `SolrMetricsJsonResponse`, `SolrMetricsRegistry`, and open `SolrMetricValue`; JSON/Prometheus response union | JVM response-format coverage remains distinct (observed JSON on Solr 8/9 and Prometheus exposition on 10.1). A `node` group integration check runs on all pinned versions, asserting a non-empty JSON registry on Solr 8/9 and Prometheus samples on 10.1; registry, prefix, and regex filters are asserted against JSON responses on Solr 8/9 (Solr 10.1 supports Prometheus/OpenMetrics, not the JSON writer). Consumer fixtures verify the extensible registry/value envelope; plugin-specific metrics remain open and unenumerated. See Issues #90, #91, and #236. |
| Backup and restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Typed core and SolrCloud backup/restore parameters and response models, including async status polling. Full core restore and SolrCloud collection backup/restore cycles are integration-tested on Solr 8.11.4/9.11.0/10.1.0 using local filesystem repositories and shared SolrCloud storage. See [backup and restore docs](backup-restore.md). |
| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 10 fetchindex/abortfetch; Solr 8/9 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles on Solr 8.11.4/9.11.0/10.1.0. An end-to-end leader/follower fetch cycle is not yet certified. See Issue #95 and [replication docs](replication.md). |
| Replication | `replication(command, params)`, `replicationFile(params)`; exported command-specific parameter and response models | V1 ReplicationHandler commands for index version, details, file list, leader/follower control (Solr 10 fetchindex/abortfetch; Solr 8/9 snappull/abort), core backup/restore/status/delete, and raw file-content streaming. Integration coverage includes core backup/restore cycles and a disposable two-core leader/follower fetch/read-back cycle on Solr 8.11.4/9.11.0/10.1.0. The test uses `snappull` on Solr 8/9 and `fetchindex` on Solr 10; abort and polling lifecycle behavior remains unit-only. See Issue #95 and [replication docs](replication.md). |
| Streaming Expressions | `stream()` | Sends `expr` with `wt=json` and parses the complete JSON response through the shared transport. It is not an incremental tuple-stream parser: no chunk-boundary protocol or memory-bound/backpressure guarantee is claimed. SolrCloud search is integration-tested on pinned Solr 8.11.4/9.11.0/10.1.0; unit tests cover JSON parsing, caller cancellation during body consumption, and cleanup after body-stream errors. See [Streaming Expressions](streaming.md) and Issue #93. |
| Export | `export()`, `exportStream()`; exported `SolrExportParams`, `SolrExportSort`, and `SolrExportFieldType` | Export requests require typed `fl` and `sort` parameters, support repeated filter queries, structured or raw sort expressions, batch sizing, JSON/JavaBin writers, and an explicit custom-parameter escape hatch. The raw Response preserves backpressure-aware streaming; `exportStream()` exposes its byte body directly. Sort fields must be single-valued docValues fields (int, long, float, double, or string); field lists additionally support date and boolean and may be multi-valued. Solr 10.1 `includeStoredFields` support is exposed but requires version-specific verification. Existing raw Export response tests pass on pinned Solr 8.11.4/9.11.0/10.1.0; typed parameter serialization and streamed chunk consumption have unit coverage. See [Export docs](export.md) and Issues #92–#94. |
| Advanced / Raw | `requestRaw(path, options)` with typed method/query/header/body/control options and a raw Fetch `Response` | Relative paths and same-origin absolute URLs use the shared transport. Cross-origin URLs, embedded credentials, and URL fragments are rejected before Fetch, preventing configured credentials from being sent to unrelated hosts. Generic request()/requestRaw() transport behavior has unit coverage only; arbitrary custom endpoint behavior remains unverified until endpoint-specific integration tests exist. See [raw requests](raw-requests.md) and Issue #98. |
Expand Down
3 changes: 2 additions & 1 deletion docs/api-inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,12 @@
"test/replication.test.ts",
"test/replication-api-types.ts",
"test/integration/solr-replication.test.ts",
"test/integration/solr-replication-lifecycle.test.ts",
"test/backup-restore.test.ts",
"test/backup-restore-api-types.ts",
"test/integration/solr-core-backup-restore.test.ts"
],
"notes": "Typed V1 ReplicationHandler commands for enablereplication, disablereplication, indexversion, fetchindex, abortfetch, snappull, abort, enablepoll, disablepoll, details, filelist, backup, restore, restorestatus, and deletebackup; replicationFile() returns raw filecontent streams. Live integration coverage on Solr 8.11.4, 9.11.0, and 10.1.0 covers indexversion, details, filelist, and filecontent. enablereplication/disablereplication are integration-tested on a configured leader; fetchindex/abortfetch (Solr 10), snappull/abort (Solr 8/9), enablepoll, and disablepoll have unit serialization coverage, but a multi-core leader/follower fetch cycle is not yet certified. Core-level backup, restore, restorestatus, and deletebackup are integration-tested on all three pinned versions. Filelist uses the generation returned by newer Solr indexversion responses and falls back to the legacy indexversion parameter when that field is absent. Additional operation evidence: Core-level ReplicationHandler backup, restore, restorestatus, and deletebackup are typed. Backup defaults to the core dataDir; restore status is polled with restorestatus. Full backup/restore/read-back/delete cycle is integration-tested on Solr 8.11.4, 9.11.0, and 10.1.0. Custom filesystem locations must be permitted by server allowPaths."
"notes": "Typed V1 ReplicationHandler commands for enablereplication, disablereplication, indexversion, fetchindex, abortfetch, snappull, abort, enablepoll, disablepoll, details, filelist, backup, restore, restorestatus, and deletebackup; replicationFile() returns raw filecontent streams. Live integration coverage on Solr 8.11.4, 9.11.0, and 10.1.0 covers indexversion, details, filelist, filecontent, leader enable/disable, and a disposable two-core leader/follower fetch with follower query read-back. The integration job supplies SOLR_VERSION so the test uses fetchindex on Solr 10 and snappull on Solr 8/9. abortfetch/abort, enablepoll, and disablepoll retain unit serialization coverage only; an in-progress abort lifecycle is not certified. Core-level backup, restore, restorestatus, and deletebackup are integration-tested on all three pinned versions. Filelist uses the generation returned by newer Solr indexversion responses and falls back to the legacy indexversion parameter when that field is absent. Additional operation evidence: Core-level ReplicationHandler backup, restore, restorestatus, and deletebackup are typed. Backup defaults to the core dataDir; restore status is polled with restorestatus. Full backup/restore/read-back/delete cycle is integration-tested on Solr 8.11.4, 9.11.0, and 10.1.0. Custom filesystem locations must be permitted by server allowPaths."
},
{
"id": "replication.file",
Expand Down
2 changes: 1 addition & 1 deletion docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The 8.11, 9.11, and 10.1 guides describe the API families below. The Replica Man
| `security()` — authentication and authorization handlers | Documented | Documented | Documented | Tested: authenticated BasicAuth/RuleBasedAuthorization reads, anonymous 401 on both handlers, authentication `set-property` read-back, and malformed JSON rejection on pinned 8.11.4 / 9.11.0 / 10.1.0 images. Unit tests verify Basic/Bearer credentials are not exposed by client HTTP errors |
| `stream()` — `/stream` | Documented | Documented | Documented | Tested: Streaming Expressions search against a disposable SolrCloud collection on pinned 8.11.4 / 9.11.0 / 10.1.0 images (CI #145) |
| `export()` — `/export` | Documented | Documented | Documented | Tested: raw response with docValues sort field on pinned 8.11.4 / 9.11.0 / 10.1.0 images |
| `replication()` / `replicationFile()` — `/solr/{core}/replication` | Documented | Documented | Documented | Tested: indexversion, details, filelist (generation on newer versions / legacy indexversion on older versions), streamed filecontent, enablereplication/disablereplication, core backup, restorestatus, restore, and deletebackup on pinned 8.11.4 / 9.11.0 / 10.1.0 images. fetchindex/abortfetch (Solr 10), snappull/abort (Solr 8/9), enablepoll, and disablepoll have unit serialization coverage; an end-to-end multi-core leader/follower fetch cycle is not yet certified |
| `replication()` / `replicationFile()` — `/solr/{core}/replication` | Documented | Documented | Documented | Tested: indexversion, details, filelist (generation on newer versions / legacy indexversion on older versions), streamed filecontent, enablereplication/disablereplication, core backup, restorestatus, restore, deletebackup, and a disposable two-core leader/follower fetch with follower query read-back on pinned 8.11.4 / 9.11.0 / 10.1.0 images. The fetch uses snappull on Solr 8/9 and fetchindex on Solr 10. Abort and polling lifecycle behavior remains unit-only |
| Core and collection backup/restore | `replication("backup" | "restore" | "restorestatus" | "deletebackup")`, `collections("BACKUP" | "RESTORE" | "LISTBACKUP" | "DELETEBACKUP" | "REQUESTSTATUS" | "DELETESTATUS")` | Documented | Documented | Documented | Tested: full core backup/restore/read-back and SolrCloud collection backup/list/restore/read-back/delete with asynchronous status polling on pinned 8.11.4 / 9.11.0 / 10.1.0 images using local filesystem repositories and shared SolrCloud storage |
| `request()` / `requestRaw()` — generic caller-defined transport | Depends on the selected endpoint | Depends on the selected endpoint | Depends on the selected endpoint | Generic transport behavior has unit tests only; no endpoint-independent live Solr integration claim is made. Each custom endpoint remains unverified until tested individually |

Expand Down
2 changes: 2 additions & 0 deletions docs/replication.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,7 @@ Supported directory types are `file` (Lucene index files), `cf` (configuration f
- Core-level `backup`, `restore`, `restorestatus`, and `deletebackup` are exposed through `replication()`; see [backup and restore docs](backup-restore.md).
- The client currently exposes the V1 handler path. Solr 10.1 also documents V2 equivalents for selected file and index-version operations; no dedicated V2 wrapper is provided.
- The handler may not be configured for user-managed leader/follower replication on every core. The server must have the appropriate ReplicationHandler configuration before control commands can be used.
- The pinned integration matrix creates disposable leader and follower cores, commits a unique leader document, triggers `snappull` on Solr 8/9 or `fetchindex` on Solr 10, and verifies that the document becomes searchable on the follower. This verifies a complete fetch/read-back cycle, not polling or abort of an in-progress transfer.
- `abort` (Solr 8/9) and `abortfetch` (Solr 10), plus `enablepoll`/`disablepoll`, have request-serialization unit coverage; cancellation of a live transfer remains unverified.

See the [Solr 8.11](https://solr.apache.org/__root/docs.solr.apache.org/guide/8_11/deployment-guide/user-managed-index-replication.html), [Solr 9.11](https://solr.apache.org/guide/solr/9_11/deployment-guide/user-managed-index-replication.html), and [Solr 10.1](https://solr.apache.org/guide/solr/10_1/deployment-guide/user-managed-index-replication.html) guides for server configuration and command semantics.
1 change: 1 addition & 0 deletions test/integration/all.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import "./solr-cursor.test.js";
import "./solr-schema.test.js";
import "./solr-security.test.js";
import "./solr-replication.test.js";
import "./solr-replication-lifecycle.test.js";
import "./solr-core-backup-restore.test.js";
import "./solrcloud-backup-restore.test.js";
import "./solr-smoke.test.js";
Expand Down
70 changes: 70 additions & 0 deletions test/integration/solr-replication-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import assert from "node:assert/strict";
import { SolrClient } from "../../src/index.js";
import { integrationTest } from "./fixtures.js";

integrationTest("ReplicationHandler fetches a committed leader index into an isolated follower core", async ({
createCore,
createId
}) => {
const leaderCore = await createCore("replication_lifecycle_leader");
const followerCore = await createCore("replication_lifecycle_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: 30_000,
...(username && password ? { auth: { type: "basic" as const, username, password } } : {})
});
const leader = makeClient(leaderCore);
const follower = makeClient(followerCore);
const leaderUrl = `${baseUrl.replace(/\/$/, "")}/${leaderCore}/replication`;
const documentId = createId("replication_lifecycle");

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 } }
}
});

await leader.add([{ id: documentId, title: "leader follower lifecycle" }]);
await leader.commit({ waitSearcher: true });

const leaderVersion = await leader.replication("indexversion");
assert.equal(leaderVersion.responseHeader?.status, 0);
const leaderGeneration = leaderVersion.generation ?? Number(leaderVersion.indexversion);
assert.ok(Number.isFinite(leaderGeneration) && leaderGeneration > 0, "leader must publish a replicable index");

const solrMajor = process.env.SOLR_VERSION?.split(".")[0];
const fetchCommand = solrMajor === "10" ? "fetchindex" : "snappull";
const fetchResponse = await follower.replication(fetchCommand, { leaderUrl });
assert.equal(fetchResponse.responseHeader?.status, 0, `${fetchCommand} should complete successfully`);

// Replication copies Lucene index files asynchronously; assert the resulting
// searchable state rather than treating the command's HTTP response as proof.
let found = false;
for (let attempt = 0; attempt < 60; attempt += 1) {
const result = await follower.query({ query: `id:"${documentId}"`, fields: ["id"], limit: 1 });
if ((result.response?.numFound ?? 0) === 1) {

Check failure on line 58 in test/integration/solr-replication-lifecycle.test.ts

View workflow job for this annotation

GitHub Actions / test (20)

'result' is of type 'unknown'.

Check failure on line 58 in test/integration/solr-replication-lifecycle.test.ts

View workflow job for this annotation

GitHub Actions / test (22)

'result' is of type 'unknown'.

Check failure on line 58 in test/integration/solr-replication-lifecycle.test.ts

View workflow job for this annotation

GitHub Actions / test (24)

'result' is of type 'unknown'.
found = true;
break;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
assert.ok(found, "follower should expose the leader document after the fetch completes");

const followerVersion = await follower.replication("indexversion");
assert.equal(followerVersion.responseHeader?.status, 0);
const followerGeneration = followerVersion.generation ?? Number(followerVersion.indexversion);
assert.ok(Number.isFinite(followerGeneration) && followerGeneration > 0, "follower should publish its fetched index");
});
Loading