Skip to content

[core][sandbox] Skip repeated work on each sandbox create - #67076

Open
xyuzh wants to merge 12 commits into
ray-project:masterfrom
xyuzh:sandbox-create-overhead
Open

xyuzh wants to merge 12 commits into
ray-project:masterfrom
xyuzh:sandbox-create-overhead

Conversation

@xyuzh

@xyuzh xyuzh commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Description

Each image lookup on a create scans the whole cache for eviction (sizing every cached image and live sandbox marker), then takes the image's exclusive lock, so creates of one image queue. Each bundle also runs runsc spec, a Go process, for the same default spec.

This PR:

Measured on 2026-10-14 against base 00a3adb, before #67025 and the floor check (16 cores, medians of 5 alternating runs; the other images were stand-in cache entries):

master this PR
Cache hit, 100 other images, 96 live sandboxes 3.32 ms 0.088 ms
48 creates at once, idle node 86/s 94/s
48 creates at once, 100 other images, 96 live 24/s 90/s

Related issues

Split out of #66821.

Additional information

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes sandbox creation and image caching performance. Key changes include caching the default OCI specification per runsc binary to avoid redundant process spawning, introducing a fast path with shared locks for image cache hits, and delegating image pulling and pinning entirely to the backend to prevent redundant lookups and clean up failed creations. Feedback on these changes suggests optimizing the _short_of_free_space helper to reduce statvfs system calls to at most one, and opening lock files in append mode ('a') instead of write mode ('w') to avoid unnecessary file truncation and metadata contention under high concurrency.

Comment on lines +720 to +727
def _short_of_free_space(images_dir: str) -> int:
"""The free-space floor if the cache's filesystem is below it, else 0.

At most two statvfs calls, cheap enough for every cache hit.
"""
min_free = image_cache_min_free_bytes(images_dir)
free = _free_bytes(images_dir) if min_free > 0 else None
return min_free if free is not None and free < min_free else 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _short_of_free_space function currently calls image_cache_min_free_bytes and _free_bytes, both of which may invoke shutil.disk_usage (which performs a statvfs system call). This results in up to two statvfs calls on every single cache hit. We can optimize this to at most one statvfs call by directly calling shutil.disk_usage once and using its returned total and free fields.

def _short_of_free_space(images_dir: str) -> int:
    """The free-space floor if the cache's filesystem is below it, else 0.

    At most one statvfs call, cheap enough for every cache hit.
    """
    raw = os.environ.get(_IMAGE_CACHE_MIN_FREE_BYTES_ENV)
    if raw is not None and raw.strip():
        try:
            min_free = max(int(raw), 0)
        except ValueError:
            min_free = None
    else:
        min_free = None

    try:
        if min_free is not None:
            if min_free == 0:
                return 0
            free = shutil.disk_usage(images_dir).free
            return min_free if free < min_free else 0
        else:
            usage = shutil.disk_usage(images_dir)
            min_free = usage.total // 10
            return min_free if usage.free < min_free else 0
    except OSError:
        return 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this: two statvfs calls cost a few microseconds against a 0.09 ms cache hit, and folding them into one would duplicate how image_cache_min_free_bytes reads RAY_SANDBOX_IMAGE_CACHE_MIN_FREE_BYTES.

# sandboxes starting on a node at once don't queue on one another. Pulls
# and eviction take the lock exclusively, so neither runs while a user
# registers here.
with open(lock_path, "w", encoding="utf-8") as f_lock:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Opening the lock file with mode "w" truncates the file. Under high concurrency (e.g., many sandboxes starting at once and hitting the cache), multiple processes concurrently truncating the same lock file causes unnecessary write I/O and metadata contention on the filesystem. Since the lock file is only used for flock and its content is irrelevant, opening it with mode "a" (append mode) is a safer and more efficient alternative that avoids truncation while still creating the file if it does not exist.

Suggested change
with open(lock_path, "w", encoding="utf-8") as f_lock:
with open(lock_path, "a", encoding="utf-8") as f_lock:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 12ec976: both opens in pull_and_extract_container_image use "a" now. flock doesn't care about the mode, and a cache hit no longer truncates the file.

except Exception:
pass

with open(lock_path, "w", encoding="utf-8") as f_lock:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly, opening the lock file with mode "w" here truncates the file. Using mode "a" (append mode) is preferred to avoid unnecessary write I/O and metadata updates on the filesystem.

Suggested change
with open(lock_path, "w", encoding="utf-8") as f_lock:
with open(lock_path, "a", encoding="utf-8") as f_lock:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same commit, 12ec976.

@ray-gardener ray-gardener Bot added the core Issues that should be addressed in Ray Core label Oct 15, 2026
@xyuzh
xyuzh force-pushed the sandbox-create-overhead branch from 12ec976 to 0ca0654 Compare October 22, 2026 19:34
@xyuzh

xyuzh commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

@pcmoritz Rebased onto master, with one change after your approval. This PR used to pull the image before making the sandbox's directory, but #67022 now has the create take the bundle's owner file first, so that a reclaim can release the image pin of a worker that dies mid-create. The pull now stays after the owner file, and a SandboxCreationError from it is raised as is instead of wrapped, which keeps the failed-pull test's behavior. On a CoreWeave pod it's 174 passed and 1 skipped against master's 168 and 1, plus 21 netns tests passed.

xyuzh added 2 commits October 22, 2026 14:55
Every create repeated work whose answer rarely changes:

- `SandboxRuntime.create` looked the image up in the cache, and then the
  backend's create looked it up again.
- Each lookup first scanned the whole image cache for eviction, sizing
  every cached image and the in-use marker of every live sandbox, and
  then took the image's exclusive lock, so creates of one image queued.
- Each bundle ran `runsc spec`, a Go process, for the same default spec.
- A read-only sandbox with a workdir logged the same warning per create.

Now the backend's lookup is the only one. It runs once the bundle's owner
file exists, so a reclaim can release the pin of a worker that dies, and a
failed pull still reaches the caller as raised. A cache hit registers its sandbox under a shared
lock and skips the eviction scan unless the disk is below the free-space
floor, which it checks with two statvfs calls: running sandboxes'
overlays can fill the disk without any pull. A pull scans under both the
size cap and the floor, under the image's exclusive lock. The default
spec is cached per runsc binary (path and mtime), and the warning is
logged once per process.

Signed-off-by: xyuzh <xinyzng@gmail.com>
Every cache hit opened the image's lock file with "w", which truncates it, only to flock it. Open it with "a" instead: the lock doesn't care about the mode, and a hit no longer writes to the file.

Signed-off-by: xyuzh <xinyzng@gmail.com>
@xyuzh
xyuzh force-pushed the sandbox-create-overhead branch from 0ca0654 to 82c1022 Compare October 22, 2026 22:05
- image_utils: master's ray-project#66469 finishes interrupted deletions at the top
  of every pull. That stays on every call, cache hits included (one
  listdir), so a node that only hits its cache still gets the space back.
  Eviction stays on the pull path and below the free-space floor.
- image_manager: ray-project#66470 boots a readonly sandbox with a workdir from a
  kernel overlay, so the warning this PR logged once per process is gone.
  The once-per-process rule moves to the warning that remains, for
  workers that can't mount an overlay and fall back to a writable root.
- gvisor: kept master's `image_dir = pull_image(...)` with this PR's
  comment on when the pin is taken.

Signed-off-by: xyuzh <xinyzng@gmail.com>
@xyuzh

xyuzh commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

@pcmoritz I merged master in ea07691. Two things changed after your approval, both where master moved code this PR touches:

  • [core][sandbox] Move an image aside before deleting it #66469 now finishes interrupted image deletions at the top of every pull. I kept that call on every pull, cache hits included. It's one listdir, and it means a node that only ever hits its cache still gets that space back. Eviction stays where this PR put it: on a pull, or on a hit below the free-space floor.
  • [core][sandbox] Boot sandboxes from an overlayfs rootfs if required #66470 now boots a readonly sandbox with a workdir from a kernel overlay, so the warning this PR rate-limited no longer exists. The once-per-process rule moved to the warning that remains, for workers that can't mount an overlay and fall back to a writable root. test_writable_root_fallback_warns_once_per_process covers it.

Real gVisor in a privileged container (aarch64, runsc release-20261026.0): test_{gvisor_backend,checkpoint_restore,image_manager,image_utils,sandbox_runtime,overlayfs,sandbox_config}.py 368 passed, 15 skipped (master: 361 passed, 15 skipped).

xyuzh added 2 commits November 3, 2026 07:01
…mpty directory

The sandbox directory can hold more than bundles (ray-project#67731 adds the owner
locks' directory), so look for bundle directories.

Signed-off-by: xyuzh <xinyzng@gmail.com>
Signed-off-by: xyuzh <xinyzng@gmail.com>
pcmoritz added a commit that referenced this pull request Nov 9, 2026
## Description

Every runsc CLI call the gVisor backend makes (`run`, `state`, `exec`,
`kill`, `delete`, `spec`) is a Go process, and Go sizes its runtime to
every CPU it can see. On a 64-core node without a CPU limit, 64
concurrent `runsc exec` calls cost 0.023 CPU-s each. gVisor's containerd
shim runs runsc with `GOMAXPROCS=2` for this reason.

This PR runs those calls with `GOMAXPROCS=2` too (`runsc_env()` in
`backend/gvisor.py`), unless the worker's environment sets it. runsc
doesn't pass the variable on to the sandbox or the gofer, so `nproc` in
a sandbox still reports every CPU.

Measured on 2026-10-14 against base 00a3adb, on a 64-core CoreWeave
node: 64 sandboxes each ran 20 `true` execs back to back, all at once;
medians of 5 alternating runs per side.

| CPUs runsc sees | master | this PR |
| --- | --- | --- |
| 64 (no CPU limit) | 1,855 execs/s, 0.0226 CPU-s each | 2,436 execs/s,
0.0129 CPU-s each |
| 16 (`taskset`) | 907 execs/s, 0.0162 CPU-s each | 990 execs/s, 0.0146
CPU-s each |

In pods with CPU limits, Go already sizes itself to the limit and the
gain is small: 2-4% more execs and creates through the gRPC facade on
four 30-CPU pods, with `GOMAXPROCS=2` set in the Ray workers'
environment (runsc inherits it).

## Related issues

Split out of #66821.

## Additional information

- Not a duplicate: no open PR sets `GOMAXPROCS` for runsc (I searched
for "sandbox GOMAXPROCS" and "runsc spec"). #66932 and #67022 add runsc
calls, which should pass `env=runsc_env()` if this lands first. #67074
and #67076 edit the same `runsc state` and `runsc spec` calls; one-line
merges.
- Tests: `test_runsc_env_caps_gomaxprocs_unless_set` checks the default
and an override; `test_runsc_run_starts_with_gomaxprocs` boots a sandbox
and reads its `runsc run`'s environment. Both fail on master.
`test_gpu_checks_run_runsc_with_runsc_env` covers the two GPU checks'
runsc calls that came in with #65898.
- Tested at 3cfe602, with master 6c61f9a (which has #67074)
merged in, in a privileged container (aarch64, runsc release-20261026.0,
erofs-utils 1.9.4): `TEST_SANDBOX=1 pytest
tests/test_{gvisor_backend,checkpoint_restore,image_manager,image_utils,sandbox_runtime,overlayfs,sandbox_config,cgroup,resource_limits}.py`:
480 passed, 25 skipped, and the same two GPU tests fail as on master
(473 passed), since that container's Ray predates `get_cdi_kind`.
pre-commit passes.
- I used Claude Code to help write and test this change.

---------

Signed-off-by: xyuzh <xinyzng@gmail.com>
Co-authored-by: Philipp Moritz <pcmoritz@gmail.com>
Signed-off-by: xyuzh <xinyzng@gmail.com>
…head

Signed-off-by: xyuzh <xinyzng@gmail.com>
Master's ray-project#67874 added test_layer_cache_reclaimed_before_images, which uses a
cache hit to start the eviction scan. With this PR a hit scans only when the
disk is below the free-space floor, and the size cap waits for the next pull,
as the docs say ("Before each pull"). The test's size case now pulls a second
image to start the scan; the free-space case still uses a hit.

Signed-off-by: xyuzh <xinyzng@gmail.com>
@xyuzh

xyuzh commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

Merged master in 5df6c08 and adapted one test in 67226c2. Master's #67874 added test_layer_cache_reclaimed_before_images, whose size case used a cache hit to start the eviction scan. With this PR a hit scans only when the disk is below the free-space floor, and the size cap waits for the next pull, which is what the docs on master say ("Before each pull"). That case now pulls a second image to start the scan, and the free-space case still uses a hit. Nothing else changed.

macOS: 468 passed, and the same 31 environment failures as master. Real gVisor (runsc release-20261026.0): test_image_utils.py, test_image_manager.py and test_sandbox_runtime.py, 138 passed.

@pcmoritz, this is the only change since your approval.

Comment on lines +1245 to +1266
min_free = _short_of_free_space(images_dir)
if min_free:
# Only the floor: a hit doesn't grow the cache, so the size
# cap waits for the next pull.
evict_least_recently_used_images(
images_dir, None, keep=safe_name, min_free_bytes=min_free
)
return _finish()

# A pull makes room first. A cache hit (every sandbox start after
# the first) skips this scan, which sizes every cached image,
# unless the disk is below the floor. Under this image's lock,
# which the scan never takes (keep).
max_cache = image_cache_max_bytes(images_dir)
min_free = image_cache_min_free_bytes(images_dir)
if max_cache > 0 or min_free > 0:
evict_least_recently_used_images(
images_dir,
max_cache if max_cache > 0 else None,
keep=safe_name,
min_free_bytes=min_free,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking RAY_SANDBOX_IMAGE_CACHE_MAX_BYTES only before a pull (while the new image contributes 0 bytes) and skipping it after build_erofs_image and on cache hits leaves the cache above its byte cap indefinitely once a pull or later unpin crosses it. Have you considered recording each EROFS image's size in .extracted and re-checking max_cache after build_erofs_image?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed for pulls in a7d3c36: a pull scans for the size cap again once its own image is in the cache, so a pull that crosses the cap evicts right away. Test: test_a_pull_that_takes_the_cache_over_its_size_cap_evicts_at_once. An image that was pinned when the cap was crossed still waits for the next pull after it's released, since a hit doesn't grow the cache. Recording sizes in .extracted would make a check on every hit affordable; I left that out of this PR.

Comment on lines +1190 to 1193
# Finish deleting what a killed worker moved aside. One listdir, so a
# cache hit pays it too, and a node that only ever hits its cache still
# gets that space back.
_remove_stale_deletions(images_dir)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running _remove_stale_deletions unlocked on every cache hit races active evictions that moved a directory to .deleting. and are running shutil.rmtree outside the image lock, making concurrent cache hits block on synchronous rmtree.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a7d3c36: on a cache hit the sweep skips a .deleting.* directory that changed in the last minute, which is one an eviction is still deleting, and takes the rest. A pull still sweeps everything after it releases the lock. Test: test_a_cache_hit_leaves_a_deletion_in_progress_to_its_owner.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: 25e7de9 replaces the one-minute rule with a lock. A deletion holds a lock on the directory it removes, and a cache hit skips a directory whose lock is held, so it's exact for a deletion of any length.

Comment on lines +751 to 756
# lives (its EROFS image is the sandbox's root filesystem). Pinned
# only once the owner file names the image, so whenever this
# process dies, a reclaim can release the pin.
image_dir = self._image_manager.pull_image(
config.image,
timeout_seconds=config.timeout_seconds,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered passing the already-pinned image_dir from _start_sandbox into prepare_oci_bundle / create_oci_spec so each create doesn't call pull_image twice on a cache hit?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this: with this PR a cache hit costs about 32 µs (5,000 hits on a cached busybox image in the test container), so the second lookup isn't worth threading the pinned directory through prepare_oci_bundle and create_oci_spec, which are part of BaseImageManager's interface.

Comment on lines +57 to +69
with _default_spec_lock:
cached = _default_spec_cache.get(key) if key else None
if cached is None:
from ray.experimental.sandbox.backend.gvisor import runsc_env

with tempfile.TemporaryDirectory() as temp_dir:
subprocess.run(["runsc", "spec"], cwd=temp_dir, check=True, env=runsc_env())
config_path = os.path.join(temp_dir, "config.json")
with open(config_path, "r", encoding="utf-8") as f:
cached = json.load(f)
if key:
with _default_spec_lock:
_default_spec_cache[key] = cached

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_default_spec_lock isn't held across subprocess.run(['runsc', 'spec']), so a cold multi-thread burst spawns concurrent runsc spec processes; also consider passing runsc from shutil.which('runsc') rather than re-searching PATH.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a7d3c36: the lock is held while runsc spec runs, so a burst of first creates runs it once, and it runs the binary that shutil.which found and the cache is keyed on. Test: test_a_burst_of_first_creates_runs_runsc_spec_once.

Comment on lines +175 to +176
# The backend pulls the image (a cache hit after the first create)
# and pins it for the sandbox's lifetime.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description nit: item 1 already landed on master (this diff only adds a comment here), and item 3's warning in image_manager.py now covers the writable-root overlayfs fallback rather than readonly workdir.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the description: the first bullet now says only what this diff does there, and the warning is described as the writable-root fallback.

Signed-off-by: xyuzh <xinyzng@gmail.com>
… hits off running deletions

- A pull scans for the size cap again once its own image counts. The
  scan before the pull can't see that image, and cache hits don't check
  the cap, so the cache stayed over it until the next pull.
- A cache hit finishes only the deletions that have sat untouched for a
  minute. One that an eviction started a moment ago is left to it, so
  the hit doesn't wait for another process's rmtree.
- The first creates of a burst run 'runsc spec' once: the lock is held
  while it runs, and it runs the binary that the cache is keyed on.

Signed-off-by: xyuzh <xinyzng@gmail.com>

@xyuzh xyuzh left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. a7d3c36 addresses the size cap, the stale-deletion sweep and the runsc spec lock, and the description is corrected.

@pcmoritz, the change since your approval is that commit: a pull checks the size cap a second time after its image is built, a cache hit only sweeps up moved-aside directories that have sat for a minute, and runsc spec runs under the cache's lock.

Comment on lines +1245 to +1266
min_free = _short_of_free_space(images_dir)
if min_free:
# Only the floor: a hit doesn't grow the cache, so the size
# cap waits for the next pull.
evict_least_recently_used_images(
images_dir, None, keep=safe_name, min_free_bytes=min_free
)
return _finish()

# A pull makes room first. A cache hit (every sandbox start after
# the first) skips this scan, which sizes every cached image,
# unless the disk is below the floor. Under this image's lock,
# which the scan never takes (keep).
max_cache = image_cache_max_bytes(images_dir)
min_free = image_cache_min_free_bytes(images_dir)
if max_cache > 0 or min_free > 0:
evict_least_recently_used_images(
images_dir,
max_cache if max_cache > 0 else None,
keep=safe_name,
min_free_bytes=min_free,
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed for pulls in a7d3c36: a pull scans for the size cap again once its own image is in the cache, so a pull that crosses the cap evicts right away. Test: test_a_pull_that_takes_the_cache_over_its_size_cap_evicts_at_once. An image that was pinned when the cap was crossed still waits for the next pull after it's released, since a hit doesn't grow the cache. Recording sizes in .extracted would make a check on every hit affordable; I left that out of this PR.

Comment on lines +1190 to 1193
# Finish deleting what a killed worker moved aside. One listdir, so a
# cache hit pays it too, and a node that only ever hits its cache still
# gets that space back.
_remove_stale_deletions(images_dir)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a7d3c36: on a cache hit the sweep skips a .deleting.* directory that changed in the last minute, which is one an eviction is still deleting, and takes the rest. A pull still sweeps everything after it releases the lock. Test: test_a_cache_hit_leaves_a_deletion_in_progress_to_its_owner.

Comment on lines +751 to 756
# lives (its EROFS image is the sandbox's root filesystem). Pinned
# only once the owner file names the image, so whenever this
# process dies, a reclaim can release the pin.
image_dir = self._image_manager.pull_image(
config.image,
timeout_seconds=config.timeout_seconds,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this: with this PR a cache hit costs about 32 µs (5,000 hits on a cached busybox image in the test container), so the second lookup isn't worth threading the pinned directory through prepare_oci_bundle and create_oci_spec, which are part of BaseImageManager's interface.

Comment on lines +57 to +69
with _default_spec_lock:
cached = _default_spec_cache.get(key) if key else None
if cached is None:
from ray.experimental.sandbox.backend.gvisor import runsc_env

with tempfile.TemporaryDirectory() as temp_dir:
subprocess.run(["runsc", "spec"], cwd=temp_dir, check=True, env=runsc_env())
config_path = os.path.join(temp_dir, "config.json")
with open(config_path, "r", encoding="utf-8") as f:
cached = json.load(f)
if key:
with _default_spec_lock:
_default_spec_cache[key] = cached

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a7d3c36: the lock is held while runsc spec runs, so a burst of first creates runs it once, and it runs the binary that shutil.which found and the cache is keyed on. Test: test_a_burst_of_first_creates_runs_runsc_spec_once.

Comment on lines +175 to +176
# The backend pulls the image (a cache hit after the first create)
# and pins it for the sandbox's lifetime.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the description: the first bullet now says only what this diff does there, and the warning is described as the writable-root fallback.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

… age

The one-minute rule missed a deletion that runs longer: unlinking files
deeper in a moved-aside directory doesn't change the directory itself.
Each deletion now holds a lock on its directory, and a sweep skips a
directory whose lock is held. The lock goes with its process, so what a
killed worker left is taken at once, as before the age rule.

Signed-off-by: xyuzh <xinyzng@gmail.com>

@xyuzh xyuzh left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pcmoritz, one more commit since your approval: 25e7de9 replaces the one-minute rule from a7d3c36 with a lock on each directory that's being deleted.

Comment on lines +1190 to 1193
# Finish deleting what a killed worker moved aside. One listdir, so a
# cache hit pays it too, and a node that only ever hits its cache still
# gets that space back.
_remove_stale_deletions(images_dir)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: 25e7de9 replaces the one-minute rule with a lock. A deletion holds a lock on the directory it removes, and a cache hit skips a directory whose lock is held, so it's exact for a deletion of any length.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 25e7de9. Configure here.

The deletion's lock was taken by the sweep that deletes the directory,
after the rename had already put it under its .deleting name. A cache hit
that swept in between found it unlocked and deleted it itself, which is
the wait the lock is there to prevent. The rename now takes the lock
first and hands it to the delete, so a directory is never under its new
name without a lock while its owner is alive.

Signed-off-by: xyuzh <xinyzng@gmail.com>

@xyuzh xyuzh left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pcmoritz, one more commit since your approval: aa07e86 closes a window Bugbot found in 25e7de9, where a cache hit could delete an image that another pull had just moved aside.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Issues that should be addressed in Ray Core sandbox

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants