test(browser): keep the scratch on disk when /dev/shm is short, delete in place after cleanup (#384)
The largest scratch copy is about 8 MB; Docker's default /dev/shm is 64 MB and shared with the browsers. A reset that empties a folder after the server's cleanup closed its trash deletes in place instead of starting a thread nobody stops.
This commit is contained in:
@@ -36,6 +36,11 @@ SEED_ON_LIVE_ENV = "PM_TEST_SEED_ON_LIVE"
|
||||
SCRATCH_SCHEMA = "reset_seed"
|
||||
# Linux's shared-memory tmpfs (present in the Playwright image and on runners).
|
||||
MEMORY_ROOT = Path("/dev/shm")
|
||||
# The largest fixtures' scratch copy is about 8 MB (database plus a WAL that
|
||||
# autocheckpoints near 4 MB). Docker's default /dev/shm is 64 MB, shared with
|
||||
# the browsers and the other workers; with less free than this, the scratch
|
||||
# copy goes on disk instead.
|
||||
MEMORY_FREE_BYTES = 32 * 1024 * 1024
|
||||
|
||||
|
||||
def _quote(name: str) -> str:
|
||||
@@ -225,7 +230,12 @@ class ScratchSeeder:
|
||||
kernel is writing back the pages they rewrite.
|
||||
"""
|
||||
root = self._memory_root
|
||||
if root is not None and root.is_dir() and os.access(root, os.W_OK):
|
||||
if (
|
||||
root is not None
|
||||
and root.is_dir()
|
||||
and os.access(root, os.W_OK)
|
||||
and shutil.disk_usage(root).free >= MEMORY_FREE_BYTES
|
||||
):
|
||||
return Path(tempfile.mkdtemp(prefix="pm-reset-", dir=root))
|
||||
self._fallback.mkdir(parents=True, exist_ok=True)
|
||||
return self._fallback
|
||||
|
||||
@@ -46,6 +46,9 @@ class _Trash:
|
||||
|
||||
|
||||
_trashes: dict[Path, _Trash] = {}
|
||||
# Trash folders whose server has cleaned up: a reset still running then
|
||||
# deletes in place instead of starting a thread nobody would stop.
|
||||
_closed: set[Path] = set()
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
@@ -56,15 +59,20 @@ def replace_folder(folder: Path) -> None:
|
||||
directory = folder.parent / TRASH_NAME
|
||||
with _lock:
|
||||
trash = _trashes.get(directory)
|
||||
if trash is None:
|
||||
if trash is None and directory not in _closed:
|
||||
trash = _trashes[directory] = _Trash(directory)
|
||||
trash.discard(folder)
|
||||
if trash is not None:
|
||||
trash.discard(folder)
|
||||
if folder.exists():
|
||||
shutil.rmtree(folder)
|
||||
folder.mkdir()
|
||||
|
||||
|
||||
def close_trash(parent: Path) -> None:
|
||||
"""Stop deleting under ``parent``'s trash and remove it (blocking)."""
|
||||
directory = Path(parent) / TRASH_NAME
|
||||
with _lock:
|
||||
trash = _trashes.pop(Path(parent) / TRASH_NAME, None)
|
||||
_closed.add(directory)
|
||||
trash = _trashes.pop(directory, None)
|
||||
if trash is not None:
|
||||
trash.close()
|
||||
|
||||
@@ -255,6 +255,21 @@ class TestScratchSeeder(unittest.TestCase):
|
||||
seeder.close()
|
||||
self.assertEqual(list(memory.iterdir()), [])
|
||||
|
||||
def test_a_full_memory_root_puts_the_scratch_on_disk(self):
|
||||
memory = self.root / "memory"
|
||||
memory.mkdir()
|
||||
seeder = ScratchSeeder(self.api, self.root / "fallback", memory_root=memory)
|
||||
self.addCleanup(seeder.close)
|
||||
full = mock.patch.object(
|
||||
fast_reset.shutil,
|
||||
"disk_usage",
|
||||
return_value=types.SimpleNamespace(free=fast_reset.MEMORY_FREE_BYTES - 1),
|
||||
)
|
||||
with full:
|
||||
seeder.run(reseed)
|
||||
self.assertEqual(list(memory.iterdir()), [])
|
||||
self.assertTrue((self.root / "fallback" / "seed.db").exists())
|
||||
|
||||
def test_close_releases_the_scratch_files(self):
|
||||
self.seeder.run(reseed)
|
||||
self.seeder.close()
|
||||
|
||||
@@ -48,5 +48,20 @@ class TestFolderTrash(unittest.TestCase):
|
||||
close_trash(self.root) # idempotent
|
||||
|
||||
|
||||
class TestFolderTrashAfterClose(unittest.TestCase):
|
||||
def test_a_replacement_after_close_deletes_in_place_without_a_thread(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
close_trash(root)
|
||||
folder = root / "output"
|
||||
folder.mkdir()
|
||||
(folder / "late.png").write_bytes(b"png")
|
||||
replace_folder(folder)
|
||||
self.assertEqual(list(folder.iterdir()), [])
|
||||
self.assertFalse((root / TRASH_NAME).exists())
|
||||
names = [t.name for t in threading.enumerate()]
|
||||
self.assertNotIn("pm-reset-trash", names)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user