Skip to main content

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 on by default (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

  • Enabled by default — this diverges from the Java client, whose goosefs.user.metadata.cache.enabled default is false. Every file open resolves its status through this cache, so with it off a workload of many small ranged reads pays one master get_status RPC 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. Set the switch to false to opt out.
  • Three entry kinds per path — a status slot, a directory listing, and a negative (NotFound) marker.
  • Write-time TTL — entries expire expiration after insertion; status and listing under the same path share the insertion timestamp.
  • open_file() reuses the cached status — a prior get_status hit means the open issues no extra master RPC.
  • Write paths self-invalidatemkdir / delete / rename drop the path and its parent after the RPC succeeds.
  • Incomplete files never count as hits — a cached INCOMPLETE file 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

SituationEffect
list_status(..., recursive=True)Listing never cached (client-side walk each time).
goosefs.user.file.metadata.load.type=ALWAYSListing 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=0get_status: skip read, still write back. list_status: skip read and write.
goosefs.user.metadata.cache.expiration.time<=0Cache 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 already true by default; set it explicitly to be self-documenting, or to false to opt out.

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 keyEnv varStorage optionDefault
goosefs.user.metadata.cache.enabledGOOSEFS_METADATA_CACHE_ENABLEDgoosefs_metadata_cache_enabledtrue
goosefs.user.metadata.cache.max.sizeGOOSEFS_METADATA_CACHE_MAX_SIZEgoosefs_metadata_cache_max_size100000
goosefs.user.metadata.cache.expiration.timeGOOSEFS_METADATA_CACHE_EXPIRATIONgoosefs_metadata_cache_expiration10min
goosefs.user.file.metadata.sync.intervalGOOSEFS_FILE_METADATA_SYNC_INTERVALgoosefs_file_metadata_sync_interval-1
goosefs.user.file.metadata.load.typeGOOSEFS_FILE_METADATA_LOAD_TYPEgoosefs_file_metadata_load_typeONCE

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.

ValueClient listing cacheMaster behaviourUse 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 namespaceDefault. GooseFS is the only writer, or a stale window of one TTL is acceptable
ALWAYSSkipped — every call goes to the master, and the result is not cachedRe-loads metadata from the UFS on every call, so files written out-of-band (directly to COS/HDFS) become visible immediatelyAnother system writes into the UFS behind GooseFS and the listing must be fresh
NEVERUsed normally (read + write)Never touches the UFS; only what is already in the master namespace is returned, so unloaded UFS files stay invisiblePure 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

ValueEffect
-1 (default)Does not skip the cache. get_status and non-recursive list_status may be served from the LRU.
0Skips 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

MetricTypeMeaning
Client.MetadataCacheHitsCounterStatus / listing hits
Client.MetadataCacheMissesCounterMisses, including TTL expiry
Client.MetadataCacheExpirationsCounterEntries dropped because the TTL elapsed
Client.MetadataCacheInvalidationsCounterExplicit write-path invalidations
Client.MetadataCacheNegativeHitsCounterNegative-cache (NotFound) hits
Client.MetadataCacheSizeGaugeCurrent LRU entry count
Client.MetadataCacheEnabledGauge1 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.