Database¶
Arbiter stores its data (batches, documents, spans, users, the audit log, and
settings) in one of two databases. Choose one with arbiter.store (or the
ARBITER_STORE environment variable):
arbiter.store |
Database | Use it for |
|---|---|---|
mongodb (default) |
A MongoDB server | Deployments that run more than one Arbiter instance, or that already operate MongoDB. |
sqlite |
A single file on the Arbiter host | Single-node deployments, including air-gapped ones, that would rather not run a database server. |
Both stores support the same features. Arbiter refuses to start if
arbiter.store is set to anything else.
The bundled docker-compose.yaml uses SQLite, with the database in the
arbiter-data volume. To run it on MongoDB instead, start the MongoDB service
with its profile:
ARBITER_STORE=mongodb docker compose --profile mongodb up
docker-compose.prod.yaml runs Arbiter and Philter on one host, each with
its own SQLite database in a volume (arbiter-data and philter-data). The
comments at the top of the file list the secrets it needs in .env.
Valkey is still required for sessions with either store. See Configuration.
MongoDB¶
Set spring.data.mongodb.uri to the server's connection URI. Arbiter creates
its collections and indexes on first use. See
Configuration for the related settings.
SQLite¶
With arbiter.store=sqlite, Arbiter keeps everything in one SQLite database
file and creates its tables and indexes on startup. No other database software
is needed.
| Property | Default | Purpose |
|---|---|---|
arbiter.sqlite.path |
./data/arbiter.db |
The database file. Arbiter creates the directory if it is missing. |
arbiter.sqlite.pool-size |
8 |
Connections to the file. Reads run in parallel; writes take turns. |
arbiter.sqlite.busy-timeout-ms |
30000 |
How long a write waits for another write to finish before it fails. |
arbiter.sqlite.synchronous |
FULL |
SQLite's synchronous setting. FULL writes each commit to disk before it returns, so a committed change survives a power loss. NORMAL is faster, but the most recent commits can be lost on power loss. |
Each property can also be set as an environment variable, for example
ARBITER_SQLITE_PATH.
Running one instance¶
A SQLite database belongs to one Arbiter process. Run a single Arbiter instance against a given file, and keep the file on a local disk: SQLite's locking is not reliable on network file systems such as NFS or SMB. To run several Arbiter instances behind a load balancer, use MongoDB.
Arbiter uses SQLite's write-ahead log, so the database is three files: the
arbiter.db file and, while Arbiter is running, arbiter.db-wal and
arbiter.db-shm next to it. Keep all three together.
In a container¶
The container's working directory is /app, so the default file is
/app/data/arbiter.db. Mount a volume at /app/data so the database
outlives the container:
docker run -e ARBITER_STORE=sqlite -v arbiter-data:/app/data ...
Backups¶
Back up a running instance with SQLite's online backup, which produces a consistent copy while Arbiter keeps working:
sqlite3 /app/data/arbiter.db ".backup '/backups/arbiter-$(date +%F).db'"
VACUUM INTO '/backups/arbiter.db' from the sqlite3 shell does the same and
also compacts the copy. Copying arbiter.db with ordinary file tools is safe
only while Arbiter is stopped, and must include any -wal file.
Restore by stopping Arbiter and putting the backup in place of arbiter.db
(and removing any leftover -wal and -shm files).
Encryption at rest¶
Document text, detected PII, comments, and the other fields listed in Security are encrypted by Arbiter before they are written, exactly as with MongoDB. The rest of the file (names, statuses, timestamps, email addresses of users) is not. Place the file on an encrypted volume to protect it as a whole, and restrict file permissions to the account Arbiter runs as.
Inspecting the data¶
Each collection is a table with id, version, and doc columns, where
doc is the record as JSON. SQLite's JSON functions read it, for example:
SELECT json_extract(doc, '$.status'), COUNT(*) FROM documents GROUP BY 1;
Change data through Arbiter rather than by editing the file. Arbiter relies on
the version column to detect conflicting edits.
Upgrading¶
One running import per batch¶
Arbiter runs at most one data import per batch at a time, enforced by a
unique index on running import jobs. Earlier versions left S3 imports out of
that index (uniq_running_data_import_per_batch). The corrected index is named
uniq_running_data_import_per_batch_v2 (ix_background_jobs_uniq_running_data_import_per_batch_v2
in SQLite), and both stores create it on startup.
Arbiter cannot create the new index, and does not start, if a batch already has more than one running import, which the old index allowed when one of them was an S3 import. Before upgrading, stop every Arbiter instance and look for such batches.
MongoDB:
db.background_jobs.aggregate([
{ $match: { status: "RUNNING", type: { $in: ["OPENSEARCH_INGEST", "ELASTICSEARCH_INGEST",
"LOCAL_DIRECTORY_INGEST", "S3_INGEST", "RDB_INGEST"] } } },
{ $group: { _id: "$batchId", jobs: { $push: "$_id" }, count: { $sum: 1 } } },
{ $match: { count: { $gt: 1 } } }
])
SQLite:
SELECT json_extract(doc, '$.batchId') AS batch, group_concat(id) AS jobs
FROM background_jobs
WHERE json_extract(doc, '$.status') = 'RUNNING'
AND json_extract(doc, '$.type') IN ('OPENSEARCH_INGEST', 'ELASTICSEARCH_INGEST',
'LOCAL_DIRECTORY_INGEST', 'S3_INGEST', 'RDB_INGEST')
GROUP BY batch HAVING COUNT(*) > 1;
With every instance stopped, none of these jobs is still running, and a job
left marked running would keep blocking its batch's imports. Mark each listed
job failed, replacing <job id>:
db.background_jobs.updateOne({ _id: "<job id>" },
{ $set: { status: "FAILED", errorMessage: "Interrupted; marked failed before upgrading." } })
UPDATE background_jobs
SET doc = json_set(doc, '$.status', 'FAILED', '$.errorMessage', 'Interrupted; marked failed before upgrading.')
WHERE id = '<job id>';
Run the import again from Add Documents if it is still needed (see Adding from a data source).
The old index stays in place after the upgrade. It enforces a subset of the new one, so it is harmless, and you can drop it:
db.background_jobs.dropIndex("uniq_running_data_import_per_batch")
DROP INDEX IF EXISTS "ix_background_jobs_uniq_running_data_import_per_batch";
Switching stores¶
Changing arbiter.store does not move data. An instance switched from MongoDB
to SQLite (or back) starts with an empty database.