From df799c4bc7fbc1773790721dc0b7d0a78919a3f3 Mon Sep 17 00:00:00 2001 From: khj1222 Date: Mon, 31 Aug 2026 08:59:09 +0900 Subject: [PATCH] prepare_9um_isotropic_input: publish the staged zarr in a way Windows survives The script writes every tile into .zarr.partial and then renames it. On Windows that rename raises PermissionError WinError 5 while any file inside the staging directory is still open, and one handle is enough -- measured: renaming succeeds with nothing held, and fails with exactly WinError 5 with a single file inside opened for read. After writing a multi-gigabyte zarr, something holding a handle briefly is ordinary. The failure cost more than the rename. Every tile was already on disk, but the output was a bare traceback pointing at pathlib, so the obvious reading was that hours of conversion had been lost and had to be repeated. Release the store's handles before renaming, retry the rename with backoff, and if it still fails say what is true: the conversion finished, nothing needs recomputing, and a manual rename completes it. Verified on Windows: publishes immediately with nothing held; retries and succeeds when a handle is released after 0.8 s; and with a handle never released, exits with that message and leaves the staging directory intact. End to end on a synthetic 84x300x260 volume the output is byte-identical to the expected pooling. --- .../scripts/prepare_9um_isotropic_input.py | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/ink-detection/scripts/prepare_9um_isotropic_input.py b/ink-detection/scripts/prepare_9um_isotropic_input.py index 45d8e24e30..8b3c9ad597 100644 --- a/ink-detection/scripts/prepare_9um_isotropic_input.py +++ b/ink-detection/scripts/prepare_9um_isotropic_input.py @@ -13,6 +13,7 @@ from concurrent.futures import ThreadPoolExecutor import math from pathlib import Path +import time import numpy as np from numcodecs import Blosc @@ -42,6 +43,39 @@ def open_source_array(path: str, level: str) -> zarr.Array: return node[level] + +def publish_partial(staged: Path, output: Path, *, attempts: int = 6, delay: float = 0.5) -> None: + """Rename a finished staging directory onto the output path. + + POSIX renames a directory whose files are open. Windows refuses with + WinError 5 while any file inside is still open, and one handle is enough -- + measured on this failure: renaming succeeds with nothing held, and fails with + exactly WinError 5 with a single file inside opened for read. After writing a + multi-gigabyte zarr, something holding a handle for a moment (an indexer, a + scanner) is ordinary, so retry briefly before giving up. + + If it still fails, say what is true: every tile was written, so nothing needs + recomputing, and a manual rename finishes the job. The bare traceback this + replaces looked like a lost conversion. + """ + for attempt in range(attempts): + try: + staged.replace(output) + return + except PermissionError as exc: + if attempt + 1 == attempts: + raise SystemExit( + f"The conversion finished -- every tile was written -- but the result " + f"could not be published:\n" + f" {staged}\n" + f" -> {output}\n" + f" {exc}\n" + f"On Windows a directory cannot be renamed while any file inside it is " + f"open. Nothing needs recomputing: rename the staging directory by hand." + ) from None + time.sleep(delay * (2 ** attempt)) + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("input_zarr", help="Surface-volume OME-Zarr group path or URL (or a bare 3D array).") @@ -104,8 +138,10 @@ def process(tile: tuple[int, int, int, int]) -> int: completed += count if completed % 50 == 0 or completed == len(tiles): print(f"tiles={completed}/{len(tiles)}", flush=True) - partial.replace(args.output_zarr) - print(f"wrote {args.output_zarr} shape={tuple(target.shape)}") + output_shape = tuple(target.shape) + del target, group # release the store's handles before renaming its directory + publish_partial(partial, args.output_zarr) + print(f"wrote {args.output_zarr} shape={output_shape}") return 0