This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 9ead3ac13a feat(mounter): let a deployment give geesefs a disk cache 
(#8666)
9ead3ac13a is described below

commit 9ead3ac13a3f11a70457f0159c8f7a11cb2c4535
Author: ali risheh <[email protected]>
AuthorDate: Fri Sep 25 18:02:48 2026 +0000

    feat(mounter): let a deployment give geesefs a disk cache (#8666)
    
    ### What changes were proposed in this PR?
    
    `bin/mounter/mounter.py` starts geesefs with `--memory-limit 512` and no
    `--cache`, whose default is off. Nothing a mount serves is written to
    local disk, so every read goes back to the object store through the file
    service and LakeFS. Reading the same 200 MiB three times through a mount
    measured 179, 137 and 124 MB/s — no warming at all — against 12.4 GB/s
    for the same file on the node's local disk.
    
    This adds `mounter.cacheRoot`, empty by default. When set, geesefs is
    given `--cache <dir>`.
    
    The cache is keyed by repository and commit, not by computing unit: a
    commit is immutable, so every unit on a node reads one cache instead of
    each new unit starting cold — which matters most for a shared public
    unit, where the first execution pays and the rest benefit.
    
    `cacheRoot` is a separate value from `hostMountRoot` rather than a
    directory inside it. `hostMountRoot` holds only mount points and usually
    sits on the OS disk, while a cache is sized by the data being cached, so
    the two generally belong on different volumes.
    
    Off by default, so no deployment changes behaviour until an operator
    opts in.
    
    ### Any related issues, documentation, discussions?
    
    Fixes #8665.
    
    ### How was this PR tested?
    
    Two cases added to `bin/mounter/tests/test_mounter.py`: that no
    `--cache` is passed by default, and that with a cache root set the
    directory handed to geesefs is `<cacheRoot>/<repo>/<commit>` and exists.
    Full mounter suite passes (95 tests).
    
    The chart was rendered both ways with `mounter.enabled=true`: without
    `cacheRoot` the DaemonSet has no `CACHE_ROOT` env, no extra volumeMount
    and no extra volume; with `cacheRoot` set it has all three, pointing at
    the configured path. `bin/k8s/tests/test_helm_values.sh` passes.
    
    Tested with a live server.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 5
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../templates/base/mounter/mounter-daemonset.yaml  | 14 +++++++++
 bin/k8s/values.yaml                                |  7 +++++
 bin/mounter/mounter.py                             | 20 +++++++++++++
 bin/mounter/tests/test_mounter.py                  | 35 ++++++++++++++++++++++
 4 files changed, 76 insertions(+)

diff --git a/bin/k8s/templates/base/mounter/mounter-daemonset.yaml 
b/bin/k8s/templates/base/mounter/mounter-daemonset.yaml
index 43927260b0..8c2fdecf66 100644
--- a/bin/k8s/templates/base/mounter/mounter-daemonset.yaml
+++ b/bin/k8s/templates/base/mounter/mounter-daemonset.yaml
@@ -55,6 +55,10 @@ spec:
               value: "{{ .Values.mounter.port }}"
             - name: MOUNT_ROOT
               value: "{{ .Values.mounter.hostMountRoot }}"
+            {{- if .Values.mounter.cacheRoot }}
+            - name: CACHE_ROOT
+              value: "{{ .Values.mounter.cacheRoot }}"
+            {{- end }}
             - name: POOL_NAMESPACE
               value: "{{ .Values.workflowComputingUnitPool.namespace }}"
             - name: CU_POD_NAME_PREFIX
@@ -76,9 +80,19 @@ spec:
             - name: texera-mounts
               mountPath: {{ .Values.mounter.hostMountRoot }}
               mountPropagation: Bidirectional
+            {{- if .Values.mounter.cacheRoot }}
+            - name: texera-mount-cache
+              mountPath: {{ .Values.mounter.cacheRoot }}
+            {{- end }}
       volumes:
         - name: texera-mounts
           hostPath:
             path: {{ .Values.mounter.hostMountRoot }}
             type: DirectoryOrCreate
+        {{- if .Values.mounter.cacheRoot }}
+        - name: texera-mount-cache
+          hostPath:
+            path: {{ .Values.mounter.cacheRoot }}
+            type: DirectoryOrCreate
+        {{- end }}
 {{- end }}
diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml
index 5b4a2bc1d7..a0ac347118 100644
--- a/bin/k8s/values.yaml
+++ b/bin/k8s/values.yaml
@@ -422,6 +422,13 @@ mounter:
   hostMountRoot: /var/lib/texera-mounts
   # Where a computing-unit pod sees its mounts.
   podMountRoot: /mnt/texera-mounts
+  # Host directory for geesefs's on-disk cache. Empty (the default) means no 
cache, so every
+  # read of a mounted repository is served from the object store. Set it when 
a mount is read
+  # repeatedly -- a reference dataset scanned on every execution -- and the 
node has room for
+  # the data being cached. Deliberately separate from hostMountRoot rather 
than a directory
+  # inside it: hostMountRoot holds only mount points and usually sits on the 
OS disk, while a
+  # cache is sized by the data, so the two generally belong on different 
volumes.
+  cacheRoot: ""
   serviceAccountName: texera-mounter-service-account
   # The mounter authenticates its callers: a caller presents a service-account 
token
   # projected for the mounter's audience, which the mounter verifies with 
TokenReview and
diff --git a/bin/mounter/mounter.py b/bin/mounter/mounter.py
index 3dc939679f..83c26d7608 100644
--- a/bin/mounter/mounter.py
+++ b/bin/mounter/mounter.py
@@ -50,6 +50,9 @@ from http.server import BaseHTTPRequestHandler, 
ThreadingHTTPServer
 from urllib.parse import parse_qs, urlparse
 
 MOUNT_ROOT = os.environ.get("MOUNT_ROOT", "/var/lib/texera-mounts")
+# Where geesefs keeps its on-disk page cache, if a deployment gives it one. 
Empty means
+# no cache, which is geesefs's own default: every read goes back to the object 
store.
+CACHE_ROOT = os.environ.get("CACHE_ROOT", "").strip()
 MOUNTER_PORT = int(os.environ.get("MOUNTER_PORT", "8100"))
 POOL_NAMESPACE = os.environ.get("POOL_NAMESPACE", 
"texera-workflow-computing-unit-pool")
 MOUNT_SECRET_PLACEHOLDER = "texera-jwt-mount"
@@ -237,6 +240,23 @@ def do_mount(cuid, repo, commit, jwt, file_service_base):
         "--endpoint", file_service_base,
         "--memory-limit", "512",
         "-o", "ro,allow_other",
+    ]
+    if CACHE_ROOT:
+        # CACHE_ROOT itself is never created here, only the two levels under 
it. The chart
+        # mounts it as a volume, so if it is missing the volume is missing -- 
and creating
+        # it would put the cache in the container's own writable layer, where 
it is lost on
+        # restart and can fill the node's disk with hundreds of GB nobody is 
watching.
+        if not os.path.isdir(CACHE_ROOT):
+            raise RuntimeError(
+                f"CACHE_ROOT {CACHE_ROOT!r} is not a directory; is the cache 
volume mounted?"
+            )
+        # Keyed by repository and commit, not by computing unit: a commit is 
immutable, so
+        # every unit on this node reads one cache rather than each new unit 
starting cold.
+        # Both are already validated as single path segments above.
+        cache_dir = os.path.join(CACHE_ROOT, repo, commit)
+        os.makedirs(cache_dir, exist_ok=True)
+        cmd += ["--cache", cache_dir]
+    cmd += [
         f"{repo}:{commit}",
         target,
     ]
diff --git a/bin/mounter/tests/test_mounter.py 
b/bin/mounter/tests/test_mounter.py
index 9e0a5321b8..c7b8447292 100644
--- a/bin/mounter/tests/test_mounter.py
+++ b/bin/mounter/tests/test_mounter.py
@@ -146,6 +146,41 @@ def 
test_do_mount_passes_the_jwt_as_the_s3_access_key(mounter):
     assert kwargs["env"]["AWS_ACCESS_KEY_ID"] == "the-user-jwt"
 
 
+def test_do_mount_asks_for_no_cache_by_default(mounter):
+    mounter.do_mount("7", "dataset-1", "abc", "t", "http://file-service:9092";)
+
+    cmd, _ = mounter.runs[0]
+    # geesefs's own default is no disk cache, and an unconfigured deployment 
keeps it.
+    assert "--cache" not in cmd
+
+
+def 
test_do_mount_caches_by_repository_and_commit_when_a_cache_root_is_set(mounter, 
tmp_path, monkeypatch):
+    # The chart mounts CACHE_ROOT as a volume, so it is already there.
+    (tmp_path / "cache").mkdir()
+    monkeypatch.setattr(mounter, "CACHE_ROOT", str(tmp_path / "cache"))
+
+    mounter.do_mount("7", "dataset-1", "abc", "t", "http://file-service:9092";)
+
+    cmd, _ = mounter.runs[0]
+    cache_dir = cmd[cmd.index("--cache") + 1]
+    # Keyed by repo and commit, never by cuid: a commit is immutable, so a 
second computing
+    # unit on this node reuses the first one's cache instead of starting cold.
+    assert cache_dir == str(tmp_path / "cache" / "dataset-1" / "abc")
+    assert os.path.isdir(cache_dir)
+
+
+def test_do_mount_refuses_a_cache_root_that_is_not_there(mounter, tmp_path, 
monkeypatch):
+    absent = tmp_path / "not-mounted"
+    monkeypatch.setattr(mounter, "CACHE_ROOT", str(absent))
+
+    # Creating it would cache into the container's own filesystem instead of 
the volume.
+    with pytest.raises(RuntimeError):
+        mounter.do_mount("7", "dataset-1", "abc", "t", 
"http://file-service:9092";)
+
+    assert not absent.exists()
+    assert mounter.runs == []
+
+
 def test_do_mount_is_idempotent_for_a_live_mount(mounter, cu_dir):
     _, target = cu_dir("7", commit="abc")
     mounter.set_mounts(target)

Reply via email to