Drop-in tiered file storage for apps on ephemeral nodes: redirect big/durable files to Databricks Volumes or S3, checkpoint hot local state for restart safety.
Reason this release was yanked:
Incorrect README
Project description
fractfs
Drop-in tiered file storage for apps on ephemeral nodes.
fractfs lets an app with limited, ephemeral local disk transparently push large
files to durable remote storage (Databricks Volumes primarily; S3 / any fsspec
backend by extension) while keeping small hot state local, with periodic
checkpoint/restore so a node stop/start doesn't lose data.
The only change to your application is one line at startup:
import fractfs
fractfs.init()
plus a .fractfs.toml in the repo. No I/O interception, no monkeypatching — it
works at the filesystem layer via symlinks, so it's library-agnostic (duckdb,
polars, pyarrow, raw C all just work).
Why
Apps moving from Posit Connect to Databricks Apps run on nodes with limited,
ephemeral local disk. Large files don't fit and shouldn't live on the node, and
anything written locally is lost when the node restarts. fractfs tags
directories and files into tiers and provisions the filesystem so the right data
lands in the right place — without intercepting I/O calls.
Install
pip install fractfs # core (Databricks Volumes mount / local backend)
pip install 'fractfs[s3]' # + S3 / fsspec backends
pip install 'fractfs[hashing]' # + xxhash content-based change detection
Configuration
A single .fractfs.toml at the app root:
[dirs]
# Directories whose contents live on the Volume (durable remote store).
# Directory-granular. New files created here later also land remote by default.
paths = ["data/blobs", "exports", "cache/parquet"]
[ignore]
# gitignore-syntax. Matching files are NEVER synced/checkpointed.
patterns = ["*.tmp", "__pycache__/", ".DS_Store"]
[local]
# gitignore-syntax. Matching files are "hot": they live on LOCAL disk
# (fast, atomic rename) but ARE checkpointed for restore.
patterns = ["*.meta.json", "manifest.json", "index.sqlite"]
Environment variables
| Var | Meaning | Example |
|---|---|---|
fractfs_BACKEND |
volumes | s3 | local |
volumes |
fractfs_VOLUME_ROOT |
Mount root (or fsspec URL) for the remote store | /Volumes/cat/schema/vol |
fractfs_SYNC_INTERVAL |
Checkpoint cadence (seconds) | 300 |
fractfs_SCRATCH |
Node-local scratch root for back-symlink targets | /tmp/fractfs |
fractfs_CHECKPOINT_SUBDIR |
Where checkpoints live under the Volume | _checkpoint |
fractfs_CONTENT_HASH |
Use content hashing for change detection | true |
fractfs_AUTO_IGNORE_BUNDLE |
Exclude the deploy bundle from the checkpoint | true |
fractfs_ROOT |
App root holding .fractfs.toml |
/app |
Env vars override the TOML scalar fields. (Both fractfs_ and FRACTFS_
prefixes are accepted.)
The three tiers
| Tier | Source | Lands | Checkpointed? | Mechanism |
|---|---|---|---|---|
dirs |
[dirs].paths |
Volume | No (already durable) | directory symlink → VOL/<dir> |
local |
[local].patterns |
Node | Yes | pre-created back-symlink when inside a Volume dir |
ignore |
[ignore].patterns |
Node | No | back-symlink (kept off Volume) + sync walker skips |
| (default) | everything else | Node | Yes | normal local disk, checkpointed |
Precedence (the load-bearing rule)
When a path matches more than one tier, highest priority wins:
ignore— never synced, stays local.local— stays local, synced.dirs— Volume redirect (the default for everything else under the dir).
Patterns use gitignore syntax (via pathspec)
matched against the full relative path: manifest.json matches at any depth;
data/blobs/manifest.json matches only there.
The local tier — what it is and is not
local is for small files co-written with large blobs (a manifest.json next to
x.parquet) that must survive restart but shouldn't take the FUSE cost of going
direct-to-Volume. Local ext4 gives proper atomic rename; a Volume FUSE mount
has real per-op overhead and weak atomicity, so for small mutable metadata
local-tier is both faster and safer.
The limit: local does not give blob↔metadata transactional
consistency. On cold restart you restore a checkpoint up to
fractfs_SYNC_INTERVAL seconds stale, while the blob on the Volume is current.
Use local only for independent / rebuildable small state. If the small file is
a pointer into the blob that must be exactly consistent, either put it in a
[dirs] directory too (shared fate, accept the FUSE cost) or make it
reconstructable from the blob on startup.
Keeping local files always-local (lock files, sqlite, manifests)
A [dirs] directory is a symlink to the Volume, so files created inside it
follow that link to the Volume by default. To keep a local/ignore file on
fast node-local disk instead, fractfs places a back-symlink on the Volume
that points at node-local scratch:
VOL/data/blobs/manifest.json -> $fractfs_SCRATCH/data/blobs/manifest.json
A symlink has to exist before the write to redirect it, so what fractfs can pin depends on whether it can predict the path:
-
Exact filenames (
manifest.json,index.sqlite, or anchoreddata/blobs/manifest.json) are pinned with a back-symlink pre-created atinit(), possibly dangling — so the file is local from its very first write, no restart needed. -
Directory patterns (a pattern ending in
/, e.g..locks/) pin a whole subtree. Any filename created inside it lands local — this is the escape hatch for lock files and other state with unpredictable names. Point the app at a pinned subdirectory:[ignore] patterns = [".locks/"] # data/blobs/.locks/<anything> stays node-local
-
Globs (
*.lock,*.tmp) cannot be pre-pinned — fractfs can't know the filename until the app creates it, and intercepting the write would mean monkeypatching I/O (explicitly rejected). A*.lockcreated directly inside a[dirs]dir therefore lands on the Volume.init()emits a warning (also instatus()["warnings"]) when a[local]glob could be affected.
The boundary, stated plainly: anything that spawns sidecar files with names
you don't control next to the data — a SQLite db emitting -wal/-shm, a
library dropping <name>.lock beside the file it locks — should not live
loose inside a [dirs] directory. Put that state in the default local tree (not
under any [dirs] path) where it's plain local disk + checkpointed with all
sidecars co-located, or confine it to a pinned foo/ subdirectory.
Public API
import fractfs
fractfs.init() # load config, provision symlinks, restore checkpoint, start sync
# ... app runs unchanged ...
fractfs.sync_now() # force a checkpoint (e.g. before graceful shutdown)
fractfs.status() # tier of each tracked path, last sync time, etc.
fractfs.shutdown() # stop the sync daemon (runs a final checkpoint)
init() blocks on restore before returning, so the app never reads cold state.
The provisioner refuses to replace a non-empty real local directory with a
symlink unless you pass fractfs.init(force=True) (which migrates its contents
to the Volume first).
The deploy bundle is auto-ignored
The platform re-supplies your deployed app bundle (code, assets, .fractfs.toml)
from the image on every cold start, so checkpointing it would copy your whole app
to durable storage every interval for nothing. fractfs detects the bundle and
excludes it automatically.
How: at each init() (after provisioning, before restore) fractfs takes the
set of files already present in the local tree and subtracts anything it already
knows is runtime state (everything in the checkpoint manifest). On a cold
ephemeral node the remainder is exactly the freshly-deployed bundle; on a warm /
persistent disk the subtraction keeps real runtime files out of the bundle so
they keep being checkpointed. It's recomputed every start, so it tracks redeploys
that add or remove files with no config changes. status() reports
bundle_file_count.
Turn it off with fractfs_AUTO_IGNORE_BUNDLE=false (or auto_ignore_bundle = false in the TOML) if you want every local file checkpointed regardless.
Caveat (persistent disks only): a runtime file created but never checkpointed before a restart that survives on a persistent local disk could be misclassified as bundle and then skipped. This can't happen on an ephemeral disk (uncheckpointed files are already gone), and a persistent local disk usually doesn't need the checkpoint anyway — but disable the feature if you rely on one.
Deploying on fast ephemeral disk (NVMe instance store)
When a node has local NVMe, the cleanest layout is to put the entire working directory on NVMe — fastest possible disk for all hot state — and let fractfs divert big files to durable storage and checkpoint the rest. NVMe being wiped on stop/replace is exactly what the checkpoint covers.
No new fractfs concept is needed: NVMe simply becomes the local disk. (NVMe is never a durable target — you never checkpoint to it; it's a fast, ephemeral source that gets checkpointed, the same role as the default local tree.)
# 1. NVMe instance store mounted at /mnt/nvme (instance/launch config).
# 2. Run the app from there so the bundle and all writes live on NVMe.
export fractfs_ROOT=/mnt/nvme/app
export fractfs_SCRATCH=/mnt/nvme/app/.fractfs-scratch # back-symlink targets on NVMe too
# 3. Durable store for big files + checkpoints (NOT on NVMe):
export fractfs_BACKEND=s3
export fractfs_VOLUME_ROOT=s3://my-bucket/my-app
export fractfs_SYNC_INTERVAL=300
# /mnt/nvme/app/.fractfs.toml
[dirs]
paths = ["data/blobs", "exports"] # big files -> S3, direct
Then:
- Big files (
[dirs]) go straight to S3 — never on NVMe, never checkpointed. - Runtime state (default tier) lives on fast NVMe and is checkpointed to S3.
- The bundle is auto-ignored (re-supplied by the image each start).
- On cold start, the platform re-extracts the bundle onto NVMe and fractfs restores runtime state from the S3 checkpoint before your app reads anything.
Two notes:
- Reads of the bundle stay local on NVMe; you don't need EBS for it. Keep EBS only if you genuinely want the bundle to persist (e.g. slow re-deploys) — and if so, an OverlayFS mount (EBS lower, NVMe upper) gives "reads fall through to EBS, all writes land on NVMe" transparently. That's an infra-level mount set up before the app starts; fractfs composes on top of it unchanged.
- Splitting hot dirs across two local disks (some on NVMe, some on EBS, at the same time) is the one case that would need a future "fast-local redirect target" tier. The single-disk layout above needs none of it.
Sharp edges
- Multi-replica. Back-symlink targets are node-local; the link itself lives on the Volume and is visible to other replicas. Fine for single-replica apps — document/guard before running multiple replicas against the same Volume.
- FUSE atomicity. Checkpoint writes use temp-file-then-
rename. If your mount doesn't honour atomic rename, the backend falls back to a plain copy. - Change detection. Default is size+mtime (cheap, can miss same-size edits).
Set
fractfs_CONTENT_HASH=true(and installfractfs[hashing]) for content hashing on correctness-sensitive trees.
License
See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fractfs-0.1.0.tar.gz.
File metadata
- Download URL: fractfs-0.1.0.tar.gz
- Upload date:
- Size: 27.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
47d567e6b65ee355f1a4fcdb1d46b59e7776a942bef02c3e9a6a84d9d2a899a2
|
|
| MD5 |
64ad90af93c03394c12759620ba97a5b
|
|
| BLAKE2b-256 |
2423234badca4e2d0d0c38e8ba47546348cd7a65a603a770fc4946c8eb59d04b
|
Provenance
The following attestation bundles were made for fractfs-0.1.0.tar.gz:
Publisher:
release.yml on isaac-harvey/fractfs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fractfs-0.1.0.tar.gz -
Subject digest:
47d567e6b65ee355f1a4fcdb1d46b59e7776a942bef02c3e9a6a84d9d2a899a2 - Sigstore transparency entry: 2029527164
- Sigstore integration time:
-
Permalink:
isaac-harvey/fractfs@1bd3418f50c4a78b190313c0b9d5197a81101537 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/isaac-harvey
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1bd3418f50c4a78b190313c0b9d5197a81101537 -
Trigger Event:
push
-
Statement type:
File details
Details for the file fractfs-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fractfs-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c7fef23059e7ac398135348c747b3e513d203bb5e63ee61385ce8b5bde6c095
|
|
| MD5 |
2e4f00899260589805f499842ce80305
|
|
| BLAKE2b-256 |
1eda038974d73373e29e33f4ca869a6afbbd6516b13a3895aa7306df54d62a08
|
Provenance
The following attestation bundles were made for fractfs-0.1.0-py3-none-any.whl:
Publisher:
release.yml on isaac-harvey/fractfs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fractfs-0.1.0-py3-none-any.whl -
Subject digest:
8c7fef23059e7ac398135348c747b3e513d203bb5e63ee61385ce8b5bde6c095 - Sigstore transparency entry: 2029527370
- Sigstore integration time:
-
Permalink:
isaac-harvey/fractfs@1bd3418f50c4a78b190313c0b9d5197a81101537 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/isaac-harvey
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1bd3418f50c4a78b190313c0b9d5197a81101537 -
Trigger Event:
push
-
Statement type: