Metadata Cache
Besides the page cache for file data, the client ships a metadata cache aligned with the GooseFS Java client's goosefs.user.metadata.cache.* semantics. get_status, exists, open_file and non-recursive list_status (including their batch_* / *_grouped variants) share one process-local TTL-bounded LRU, so repeated metadata lookups of the same paths no longer hit the master. It is off by default, matching Java (see below).
It replaces the earlier FileInfo open cache (GOOSEFS_FILE_INFO_CACHE_TTL_MS / GOOSEFS_FILE_INFO_CACHE_CAPACITY), which has been removed.
Because the Python binding shares the Rust configuration core, the cache is available through env / properties with no binding change.
Behavior
- Off by default — matches the Java client (
goosefs.user.metadata.cache.enabled=false). Set the switch totrueto opt in. Every file open resolves its status through this cache, so with it off a workload of many small ranged reads pays one masterget_statusRPC per read. That RPC also hides the page cache: a page-cache hit over io_uring costs tens of microseconds, so a per-open master round-trip dwarfs it and the read waits on metadata rather than on disk. - Three entry kinds per path — a status slot, a directory listing, and a negative (
NotFound) marker. - Write-time TTL — entries expire
expirationafter insertion; status and listing under the same path share the insertion timestamp. open_file()reuses the cached status — a priorget_statushit means the open issues no extra master RPC.- Write paths self-invalidate —
mkdir/delete/renamedrop the path and its parent after the RPC succeeds. - Incomplete files never count as hits — a cached
INCOMPLETEfile falls through to the master. - Process-local — writes from other clients are not observed until the TTL elapses. Keep the TTL short (or leave the cache off) when out-of-band writers must be visible immediately.
When the cache is bypassed
| Situation | Effect |
|---|---|
list_status(..., recursive=True) | Listing never cached (client-side walk each time). |
goosefs.user.file.metadata.load.type=ALWAYS | Listing cache skipped (no read/write). Does not skip get_status / exists / open_file client cache. Master still re-loads from UFS on those RPCs. |
goosefs.user.file.metadata.sync.interval=0 | get_status: skip read, still write back. list_status: skip read and write. |
goosefs.user.metadata.cache.expiration.time<=0 | Cache is not constructed at all, even when enabled. |
ALWAYS skips the listing cache. To force every status lookup to skip the client cache, set GOOSEFS_FILE_METADATA_SYNC_INTERVAL=0. load_metadata_type is still sent on get_status (default ONCE) so the master can load missing UFS paths.
Tuning the Cache
The switch is false by default (the metadata-cache feature still compiles
the cache in). Set it to true to opt in.
export GOOSEFS_METADATA_CACHE_ENABLED=true
export GOOSEFS_METADATA_CACHE_EXPIRATION=1min # 10min default; also accepts raw ms
export GOOSEFS_METADATA_CACHE_MAX_SIZE=100000
export GOOSEFS_FILE_METADATA_SYNC_INTERVAL=-1 # default; 0 skips cache on every get/list
export GOOSEFS_FILE_METADATA_LOAD_TYPE=ONCE # ONCE / ALWAYS / NEVER (ALWAYS skips listing cache)
# Force every list to the master:
# export GOOSEFS_FILE_METADATA_LOAD_TYPE=ALWAYS
# goosefs-site.properties
goosefs.user.metadata.cache.enabled=true
goosefs.user.metadata.cache.expiration.time=1min
goosefs.user.metadata.cache.max.size=100000
goosefs.user.file.metadata.sync.interval=-1
goosefs.user.file.metadata.load.type=ONCE
Or inline via the Config builder:
from goosefs import Config
cfg = Config(
"127.0.0.1:9200",
properties={
"goosefs.user.metadata.cache.enabled": "true",
"goosefs.user.metadata.cache.expiration.time": "1min",
"goosefs.user.metadata.cache.max.size": "100000",
"goosefs.user.file.metadata.sync.interval": "-1",
"goosefs.user.file.metadata.load.type": "ONCE",
},
)
For Lance / OpenDAL callers the same knobs are exposed as storage options:
import lance
ds = lance.dataset(
"gfs://bucket/dataset.lance",
storage_options={
"goosefs_metadata_cache_enabled": "true",
"goosefs_metadata_cache_expiration": "1min",
"goosefs_metadata_cache_max_size": "100000",
"goosefs_file_metadata_sync_interval": "-1",
"goosefs_file_metadata_load_type": "ONCE",
},
)
Example
import asyncio
from goosefs import AsyncGoosefs, Config
async def main():
cfg = Config(
"127.0.0.1:9200",
properties={
"goosefs.user.metadata.cache.enabled": "true",
"goosefs.user.metadata.cache.expiration.time": "1min",
},
)
async with await AsyncGoosefs.connect(cfg) as fs:
await fs.get_status("/data/file.parquet") # miss → master RPC
await fs.get_status("/data/file.parquet") # hit → no RPC
# Batch metadata benefits too: repeated paths are served from the cache.
await fs.batch_get_status(["/data/a.parquet", "/data/b.parquet"])
# Non-recursive listings are cached; recursive ones are not.
await fs.list_status("/data", recursive=False)
# delete/rename/mkdir invalidate the path and its parent automatically.
await fs.delete("/data/file.parquet")
asyncio.run(main())
Configuration Reference
| Properties key | Env var | Storage option | Default |
|---|---|---|---|
goosefs.user.metadata.cache.enabled | GOOSEFS_METADATA_CACHE_ENABLED | goosefs_metadata_cache_enabled | false |
goosefs.user.metadata.cache.max.size | GOOSEFS_METADATA_CACHE_MAX_SIZE | goosefs_metadata_cache_max_size | 100000 |
goosefs.user.metadata.cache.expiration.time | GOOSEFS_METADATA_CACHE_EXPIRATION | goosefs_metadata_cache_expiration | 10min |
goosefs.user.file.metadata.sync.interval | GOOSEFS_FILE_METADATA_SYNC_INTERVAL | goosefs_file_metadata_sync_interval | -1 |
goosefs.user.file.metadata.load.type | GOOSEFS_FILE_METADATA_LOAD_TYPE | goosefs_file_metadata_load_type | ONCE |
Values below 1 for max.size are clamped to 1. Expiration and sync interval accept 10min / 30s / 2day or a raw millisecond number (-1 / 0 are milliseconds).
goosefs.user.file.metadata.load.type values
The value is sent on every get_status and list_status RPC, so it has two effects: one on the client listing cache, one on how the master treats the under file system (UFS). Default ONCE matches the Java SDK; an unset field is proto NEVER and COS-only files fail stat with NotFound.
| Value | Client listing cache | Master behaviour | Use when |
|---|---|---|---|
ONCE (default) | Used normally (read + write) | Loads a path's metadata from the UFS the first time it is accessed, then serves it from the master namespace | Default. GooseFS is the only writer, or a stale window of one TTL is acceptable |
ALWAYS | Skipped — every call goes to the master, and the result is not cached | Re-loads metadata from the UFS on every call, so files written out-of-band (directly to COS/HDFS) become visible immediately | Another system writes into the UFS behind GooseFS and the listing must be fresh |
NEVER | Used normally (read + write) | Never touches the UFS; only what is already in the master namespace is returned, so unloaded UFS files stay invisible | Pure GooseFS namespace, and you want to avoid UFS round-trips entirely |
ALWAYS costs a UFS round-trip and a master RPC per list, so it is the slowest option. Values are case-insensitive; an unrecognised value is ignored and the default ONCE is kept.
goosefs.user.file.metadata.sync.interval values
| Value | Effect |
|---|---|
-1 (default) | Does not skip the cache. get_status and non-recursive list_status may be served from the LRU. |
0 | Skips the cache on every call: get_status re-reads from the master but still writes the result back; list_status neither reads nor writes the listing cache. |
> 0 (e.g. 5s) | Parsed and stored, but the skip check only tests for 0, so it currently behaves like -1. |
Unlike load.type, this knob affects both get_status and list_status, and it does not change what the master does with the UFS.
Observability
| Metric | Type | Meaning |
|---|---|---|
Client.MetadataCacheHits | Counter | Status / listing hits |
Client.MetadataCacheMisses | Counter | Misses, including TTL expiry |
Client.MetadataCacheExpirations | Counter | Entries dropped because the TTL elapsed |
Client.MetadataCacheInvalidations | Counter | Explicit write-path invalidations |
Client.MetadataCacheNegativeHits | Counter | Negative-cache (NotFound) hits |
Client.MetadataCacheSize | Gauge | Current LRU entry count |
Client.MetadataCacheEnabled | Gauge | 1 when a cache was constructed |
Client.GetStatusOps counts master RPCs only — cache hits are not counted.
For hit/miss logs without a metrics backend:
RUST_LOG=goosefs_sdk::metadata_cache=debug python your_script.py
See the Rust Metadata Cache page and docs/CLIENT_CONFIGURATION.md for the full matrix.