Files
2026-07-13 13:17:40 +08:00

233 lines
8.4 KiB
Python

import os
from pathlib import Path
import pyarrow.fs
import pytest
import ray
from ray.train._checkpoint import (
_CHECKPOINT_TEMP_DIR_PREFIX,
_METADATA_FILE_NAME,
Checkpoint,
_get_del_lock_path,
_list_existing_del_locks,
)
from ray.train._internal.storage import _exists_at_fs_path, _upload_to_fs_path
from ray.train.tests.test_new_persistence import _create_mock_custom_fs
_CHECKPOINT_CONTENT_FILE = "dummy.txt"
@pytest.fixture(params=["local", "mock", "custom_fs"])
def checkpoint(request, tmp_path):
"""Fixture that sets up a checkpoint on different filesystems."""
checkpoint_fs_type = request.param
checkpoint_path = tmp_path / "ckpt_dir"
checkpoint_path.mkdir(exist_ok=True)
(checkpoint_path / _CHECKPOINT_CONTENT_FILE).write_text("dummy")
if checkpoint_fs_type == "local":
yield Checkpoint.from_directory(str(checkpoint_path))
elif checkpoint_fs_type == "mock":
_checkpoint = Checkpoint(path="mock:///mock_bucket/ckpt_dir")
_upload_to_fs_path(
local_path=str(checkpoint_path),
fs=_checkpoint.filesystem,
fs_path=_checkpoint.path,
)
# The "mock://" URI doesn't persist across different instances of
# the pyarrow.fs.MockFileSystem, so we must make sure to return
# the checkpoint with the same filesystem instance that we uploaded
# some mock content to.
yield _checkpoint
elif checkpoint_fs_type == "custom_fs":
custom_storage_fs = _create_mock_custom_fs(tmp_path / "custom_fs")
_upload_to_fs_path(
local_path=str(checkpoint_path),
fs=custom_storage_fs,
fs_path="mock_bucket/ckpt_dir",
)
yield Checkpoint(path="mock_bucket/ckpt_dir", filesystem=custom_storage_fs)
def test_to_directory(checkpoint: Checkpoint):
checkpoint_path = Path(checkpoint.to_directory())
assert (checkpoint_path / _CHECKPOINT_CONTENT_FILE).exists()
assert _CHECKPOINT_TEMP_DIR_PREFIX in checkpoint_path.name
def test_to_directory_with_user_specified_path(checkpoint: Checkpoint, tmp_path):
# Test with a string
checkpoint_path = Path(checkpoint.to_directory(str(tmp_path / "special_dir")))
assert (checkpoint_path / _CHECKPOINT_CONTENT_FILE).exists()
assert checkpoint_path.name == "special_dir"
# Test with a PathLike
checkpoint_path = Path(checkpoint.to_directory(tmp_path / "special_dir"))
assert (checkpoint_path / _CHECKPOINT_CONTENT_FILE).exists()
assert checkpoint_path.name == "special_dir"
def test_multiprocess_to_directory(checkpoint: Checkpoint):
"""Test the case where multiple processes are trying to checkpoint.
Only one process should download the checkpoint, and the others should
wait until it's finished. In the end, a single checkpoint dir should be
shared by all processes.
"""
if checkpoint.filesystem.type_name == "mock":
pytest.skip("Mock filesystem cannot be pickled for use with Ray.")
@ray.remote
def download_checkpoint(checkpoint: Checkpoint) -> str:
return checkpoint.to_directory()
paths = [ray.get(download_checkpoint.remote(checkpoint)) for _ in range(5)]
# Check that all the paths are the same (no duplicates).
assert len(set(paths)) == 1
def test_as_directory(checkpoint: Checkpoint):
with checkpoint.as_directory() as checkpoint_path:
checkpoint_path = Path(checkpoint_path)
assert (checkpoint_path / _CHECKPOINT_CONTENT_FILE).exists()
if isinstance(checkpoint.filesystem, pyarrow.fs.LocalFileSystem):
# We should have directly returned the local path
assert str(checkpoint_path) == checkpoint.path
else:
# We should have downloaded to a temp dir.
assert _CHECKPOINT_TEMP_DIR_PREFIX in checkpoint_path.name
if isinstance(checkpoint.filesystem, pyarrow.fs.LocalFileSystem):
# Checkpoint should not be deleted, if we directly gave the local path.
assert (checkpoint_path / _CHECKPOINT_CONTENT_FILE).exists()
else:
# Should have been deleted, if the checkpoint downloaded to a temp dir.
assert not checkpoint_path.exists()
def test_multiprocess_as_directory(checkpoint: Checkpoint, monkeypatch):
"""Tests that deletion lock files are created and deleted correctly,
when multiple processes are accessing a checkpoint with `as_directory`"""
# If this is pointing to a local path, no lockfiles will be created.
is_local_checkpoint = isinstance(checkpoint.filesystem, pyarrow.fs.LocalFileSystem)
monkeypatch.setattr(os, "getpid", lambda: 11111)
with checkpoint.as_directory() as checkpoint_dir_1:
lock_file_1 = Path(_get_del_lock_path(checkpoint_dir_1))
# Pretend that the 2nd `as_directory` is called from a different process.
monkeypatch.setattr(os, "getpid", lambda: 22222)
with checkpoint.as_directory() as checkpoint_dir_2:
lock_file_2 = Path(_get_del_lock_path(checkpoint_dir_2))
# Should point both processes to the same canonical temp directory.
assert checkpoint_dir_1 == checkpoint_dir_2
if is_local_checkpoint:
# Check that 2 different lock files were created.
assert not lock_file_1.exists()
assert not lock_file_2.exists()
else:
# Check that 2 different lock files were created.
assert lock_file_1.exists()
assert lock_file_2.exists()
assert len(_list_existing_del_locks(checkpoint_dir_1)) == 2
if not is_local_checkpoint:
# Check that the 2nd lock file was deleted.
assert len(_list_existing_del_locks(checkpoint_dir_1)) == 1
assert not lock_file_2.exists()
if not is_local_checkpoint:
# Check that the 1st lock file was deleted.
assert len(_list_existing_del_locks(checkpoint_dir_1)) == 0
assert not lock_file_1.exists()
# Check that the temp checkpoint directory was deleted.
assert not Path(checkpoint_dir_1).exists()
def test_as_directory_lock_cleanup(checkpoint: Checkpoint):
"""Errors when accessing a checkpoint with `as_directory`
shouldn't leave behind lock files.
"""
with pytest.raises(RuntimeError):
with checkpoint.as_directory() as checkpoint_dir:
raise RuntimeError
assert not _list_existing_del_locks(checkpoint_dir)
is_local_checkpoint = isinstance(checkpoint.filesystem, pyarrow.fs.LocalFileSystem)
if not is_local_checkpoint:
assert not Path(checkpoint_dir).exists()
def test_as_directory_download_error(checkpoint: Checkpoint, monkeypatch):
"""Errors during a checkpoint download should be raised directly when accessing
it with the `as_directory` context manager."""
if isinstance(checkpoint.filesystem, pyarrow.fs.LocalFileSystem):
pytest.skip(
"Local filesystem checkpoints don't download to a temp dir, so "
"there's no error handling to test."
)
error_text = "original error"
def to_directory_error(*args, **kwargs):
raise RuntimeError(error_text)
monkeypatch.setattr(checkpoint, "to_directory", to_directory_error)
with pytest.raises(RuntimeError, match=error_text):
with checkpoint.as_directory() as _:
pass
def test_metadata(checkpoint: Checkpoint):
assert checkpoint.get_metadata() == {}
# No metadata file by default.
assert not _exists_at_fs_path(
fs=checkpoint.filesystem,
fs_path=str(Path(checkpoint.path) / _METADATA_FILE_NAME),
)
checkpoint.update_metadata({"foo": "bar"})
assert checkpoint.get_metadata() == {"foo": "bar"}
checkpoint.update_metadata({"foo": "baz"})
assert checkpoint.get_metadata() == {"foo": "baz"}
checkpoint.update_metadata({"x": 1})
assert checkpoint.get_metadata() == {"foo": "baz", "x": 1}
# Set metadata completely resets the metadata.
checkpoint.set_metadata({"y": [1, 2, 3]})
assert checkpoint.get_metadata() == {"y": [1, 2, 3]}
# There should be a metadata file in the checkpoint directory.
assert _exists_at_fs_path(
fs=checkpoint.filesystem,
fs_path=str(Path(checkpoint.path) / _METADATA_FILE_NAME),
)
# Non JSON serializable metadata should raise an error.
class Test:
pass
with pytest.raises(TypeError):
checkpoint.set_metadata({"non_json_serializable": Test()})
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))