Skip to content

Commit f44bf6e

Browse files
committed
fix(fly): warm rclone dir cache after activate to avoid first-readdir race
`rclone mount --daemon` returns immediately after fork; FUSE's first readdir on the mountpoint root then races the daemon's first remote listing fetch. The agent's first `ls` could observe an empty directory even though the mount is live. Add a single throw-away `ls` inside `_verify_mount_active` after `mountpoint -q` passes — its only purpose is the side effect of priming rclone's directory cache so subsequent listings see the bucket contents immediately. Verified end-to-end: previously the first listing would briefly show empty for ~half a second; with the warmup, the very first agent-side listing returns the populated mount.
1 parent 1517a66 commit f44bf6e

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

src/agents/extensions/sandbox/flyio/mounts.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ async def _verify_mount_active(session: BaseSandboxSession, mount_path: Path) ->
150150
context={"path": str(mount_path)},
151151
)
152152

153+
# Force rclone to materialize the root directory listing before we hand
154+
# control back to the caller. Without this, the next ``readdir`` from the
155+
# agent races the daemon's first listing fetch and can briefly observe an
156+
# empty directory. The exit code is irrelevant here — we just want the
157+
# side effect of priming rclone's dir cache.
158+
await session.exec("sh", "-lc", f"ls {quoted} >/dev/null 2>&1", shell=False, timeout=15)
159+
153160

154161
async def _default_user_ids(session: BaseSandboxSession) -> tuple[str, str] | None:
155162
result = await session.exec("sh", "-lc", "id -u; id -g", shell=False, timeout=30)

tests/extensions/test_sandbox_fly.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1694,12 +1694,17 @@ async def test_ensure_fuse_support_raises_when_post_install_fusermount_still_mis
16941694

16951695
@pytest.mark.asyncio
16961696
async def test_verify_mount_active_passes_when_mountpoint_reports_mounted() -> None:
1697-
session = _FakeMountSession([_mounted()])
1697+
session = _FakeMountSession([_mounted(), _ok()])
16981698

16991699
await _verify_mount_active(session, Path("/workspace/tigris"))
17001700

1701+
# Two calls: the mountpoint probe followed by a directory-listing warm-up
1702+
# that forces rclone to populate its root readdir cache before the caller
1703+
# uses the mount.
1704+
assert len(session.exec_calls) == 2
17011705
assert "mountpoint -q /workspace/tigris" in session.exec_calls[0]
17021706
assert _MOUNTED in session.exec_calls[0]
1707+
assert "ls /workspace/tigris >/dev/null 2>&1" in session.exec_calls[1]
17031708

17041709

17051710
@pytest.mark.asyncio
@@ -1711,6 +1716,22 @@ async def test_verify_mount_active_raises_when_path_is_not_a_mount() -> None:
17111716
assert excinfo.value.context.get("path") == "/workspace/tigris"
17121717

17131718

1719+
@pytest.mark.asyncio
1720+
async def test_verify_mount_active_warmup_runs_after_mountpoint_check() -> None:
1721+
"""The post-mountpoint listing warmup runs even if the directory is empty.
1722+
1723+
Confirms the warmup exec is fired regardless of what ``ls`` returns —
1724+
we only care about the side effect of priming rclone's dir cache.
1725+
"""
1726+
1727+
session = _FakeMountSession([_mounted(), _ok()])
1728+
1729+
await _verify_mount_active(session, Path("/workspace/some/nested/mount"))
1730+
1731+
assert "mountpoint -q /workspace/some/nested/mount" in session.exec_calls[0]
1732+
assert "ls /workspace/some/nested/mount" in session.exec_calls[1]
1733+
1734+
17141735
@pytest.mark.asyncio
17151736
async def test_rclone_pattern_appends_allow_other_and_user_ids() -> None:
17161737
session = _FakeMountSession([_ok(stdout=b"1001\n1001\n")])

0 commit comments

Comments
 (0)