Files
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

147 lines
6.2 KiB
Python

# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from typing import Any
from mirage.cache.file import io as cache_io
from mirage.cache.manager import CacheManager
from mirage.io import IOResult
from mirage.observe.record import OpRecord
from mirage.types import ConsistencyPolicy, FileStat, PathSpec
from mirage.workspace.mount import MountEntry
from mirage.workspace.mount.namespace import Namespace
from mirage.workspace.session import assert_mount_allowed
_DISPATCH_READ_OPS = frozenset({"read", "read_bytes"})
_DISPATCH_WRITE_OPS = frozenset(
{"write", "write_bytes", "append", "unlink", "create", "truncate"})
# Ops that act on the path entry itself (lstat semantics); every other op
# follows symlinks before mount lookup, so reads/writes go to the target
# and the cache keys under the real path.
_NO_FOLLOW_OPS = frozenset({"unlink", "rename", "rmdir"})
class Dispatcher:
"""Route a single VFS op to its mount and keep the file cache + index
consistent.
Owns the cache/IO coordination that used to live on Workspace: cache
lookups for read-caching backends, post-write file-cache eviction,
and parent index invalidation. Constructed with the namespace (for
addressing), cache store, and consistency policy; holds no other
workspace state. Drift checking stays on Workspace (it reads/writes
snapshot-owned state), which guards its own dispatch wrapper before
delegating here.
"""
def __init__(self, namespace: Namespace, cache,
consistency: ConsistencyPolicy) -> None:
self._namespace = namespace
self._cache = cache
self._consistency = consistency
async def dispatch(self, op: str, path: PathSpec,
**kwargs: Any) -> tuple[Any, IOResult]:
if op not in _NO_FOLLOW_OPS:
followed = self._namespace.follow(path.virtual)
if followed != path.virtual:
path = PathSpec.from_str_path(followed)
mount = self._namespace.mount_for(path.virtual)
assert_mount_allowed(mount.prefix)
caches_reads = mount.resource.caches_reads
if caches_reads and op in _DISPATCH_READ_OPS:
cached = await self._cache.get(path.virtual)
if cached is not None:
if self._consistency == ConsistencyPolicy.ALWAYS:
try:
remote_stat = await mount.execute_op(
"stat", path.virtual)
except FileNotFoundError:
await self._cache.remove(path.virtual)
raise
if (remote_stat is not None
and remote_stat.fingerprint is not None):
fresh = await self._cache.is_fresh(
path.virtual, remote_stat.fingerprint)
if not fresh:
await self._cache.remove(path.virtual)
cached = None
if cached is not None:
return cached, IOResult(reads={path.virtual: cached})
result = await mount.execute_op(op, path.virtual, **kwargs)
if op in _DISPATCH_WRITE_OPS:
await self.invalidate_after_write(mount, path.virtual)
return result, IOResult()
async def stat(self, path: str) -> FileStat:
scope = PathSpec(virtual=path,
directory=path,
resource_path="",
resolved=True)
result, _ = await self.dispatch("stat", scope)
return result
async def readdir(self, path: str) -> list[str]:
scope = PathSpec(virtual=path,
directory=path,
resource_path="",
resolved=False)
raw, _ = await self.dispatch("readdir", scope)
return raw
async def apply_io(self,
io: IOResult,
records: list[OpRecord] | None = None) -> None:
await cache_io.apply_io(self._cache,
io,
self.is_cacheable_path,
records=records)
def is_cacheable_path(self, path: str) -> bool:
try:
mount = self._namespace.mount_for(path)
except ValueError:
return False
return mount.resource.caches_reads
async def invalidate_after_write_by_path(self, path: str) -> None:
"""Drop file-cache + stale parent index after a write to `path`.
Single source of truth for post-write invalidation. Called from
both `Workspace.dispatch()` and `Ops._call(write=True)` so a
write through any code path sees the same invalidation rules:
file cache is dropped only for read-caching mounts, and the
parent directory index is dirtied for any mount that maintains
an index. No-op for paths that resolve to no known mount.
Args:
path (str): absolute mount path that was written.
"""
try:
mount = self._namespace.mount_for(path)
except ValueError:
return
await self.invalidate_after_write(mount, path)
async def invalidate_after_write(self, mount: MountEntry,
path: str) -> None:
manager = mount.cache_manager
if manager is None:
manager = CacheManager(self._cache,
getattr(mount.resource, "index", None),
mount.prefix, mount.resource.caches_reads)
await manager.invalidate_after_write(path)