Local and self-hosted¶
Filesystem backend¶
The filesystem backend stores the bucket layout in a directory. It needs no extra dependency:
db = CairnDB.configure({"storage": {"type": "filesystem", "path": "/var/lib/myapp/cairndb"}})
CAIRNDB_STORAGE_TYPE=filesystem
CAIRNDB_STORAGE_PATH=/var/lib/myapp/cairndb
Put-if-absent is a write to a temp file followed by an atomic os.link.
Compare-and-swap takes an exclusive flock on a sibling .{name}.lock
file. The kernel releases the lock if its holder dies, so crashes never
leave stale locks. As a result, many processes on one machine can
share a directory safely. The test suite relies on exactly this to
simulate real contention.
Limits:
POSIX only (Linux, macOS). No Windows.
One machine. Do not share the directory over NFS, SMB, or similar unless you have verified
flockandlinksemantics on that filesystem.Durability is your disk’s durability. Back up the directory. Its contents are a plain file tree and copy well with
rsyncor restic.
The filesystem backend has no prefix option. To keep several
independent engines apart, give each its own directory.
MinIO (S3-compatible, self-hosted)¶
Use a MinIO release with conditional-write support (late 2024 or newer):
docker run -d --name minio -p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data --console-address :9001
# create the bucket (via the console on :9001, or the mc client)
docker run --rm --network host --entrypoint sh minio/mc -c \
"mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb local/myapp"
export CAIRNDB_STORAGE_TYPE=s3 \
CAIRNDB_S3_BUCKET=myapp \
CAIRNDB_S3_ENDPOINT_URL=http://localhost:9000 \
AWS_ACCESS_KEY_ID=minioadmin AWS_SECRET_ACCESS_KEY=minioadmin \
AWS_DEFAULT_REGION=us-east-1
python -m cairndb.client.run # the demo, now against MinIO
Before relying on any S3-compatible store, run a quick contention check:
import asyncio
from cairndb import CairnDB
async def check():
db = CairnDB.configure({"storage": {"type": "s3", "bucket": "myapp",
"endpoint_url": "http://localhost:9000"}})
await db.objects.delete("probe/key")
results = await asyncio.gather(*[
db.objects.put("probe/key", str(i).encode(), if_absent=True) for i in range(20)
])
wins = sum(r is not None for r in results)
assert wins == 1, f"{wins} writers won put-if-absent: conditional writes NOT enforced"
print("conditional writes enforced")
asyncio.run(check())
Running the jobs locally¶
With cron¶
# m h dom mon dow command
0 3 * * * cd /srv/myapp && CAIRNDB_STORAGE_TYPE=filesystem CAIRNDB_STORAGE_PATH=/var/lib/myapp/cairndb \
.venv/bin/cairndb snapshot --handlers myapp.projections:registry
0 4 * * 0 cd /srv/myapp && CAIRNDB_STORAGE_TYPE=filesystem CAIRNDB_STORAGE_PATH=/var/lib/myapp/cairndb \
.venv/bin/cairndb gc --keep-snapshots 3
With systemd timers¶
# /etc/systemd/system/cairndb-snapshot.service
[Service]
Type=oneshot
User=myapp
WorkingDirectory=/srv/myapp
Environment=CAIRNDB_STORAGE_TYPE=filesystem
Environment=CAIRNDB_STORAGE_PATH=/var/lib/myapp/cairndb
ExecStart=/srv/myapp/.venv/bin/cairndb snapshot --handlers myapp.projections:registry
# /etc/systemd/system/cairndb-snapshot.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
Enable it with systemctl enable --now cairndb-snapshot.timer. Create a
cairndb-gc pair the same way.
With Docker¶
docker build -t cairndb-jobs . # from the cairndb repository
docker build -t myapp-jobs -f Dockerfile.jobs . # your image, FROM cairndb-jobs (see Deployment overview)
docker run --rm \
-e CAIRNDB_STORAGE_TYPE=filesystem -e CAIRNDB_STORAGE_PATH=/data \
-v /var/lib/myapp/cairndb:/data \
myapp-jobs snapshot --handlers myapp.projections:registry
The image runs as UID 1000 (cairndb). Make sure that user can write to
the mounted directory.
Local development tips¶
Every test can get its own
tmp_pathbucket. Concurrency tests on the filesystem backend exercise real races, with no mocks needed.Delete projection files freely. They are rebuilt from the log.
Inspect state with
lsandcat: documents are JSON, and commits are msgpack (python -c "import msgpack,sys; print(msgpack.unpackb(open(sys.argv[1],'rb').read()))" data/log/000000000001.msgpack).