Repository navigation
Let every process write its own schema sample and keep the first one saved - #19
Conversation
dxnnystyles
left a comment
There was a problem hiding this comment.
there's a race here for regular files. os.rename(tmp_path, path) can replace an existing destination on POSIX, so if two processes finish writing the same file, the second one can overwrite the first instead of preserving it. the Zarr directory case behaves differently. might be worth testing two writers targeting a regular file and using an atomic no-replace operation if first-writer-wins is the intended behavior for both.
|
It is true that on POSIX renaming a file onto an existing file replaces it. This doesn't happen with the |
The problem
DatasetCacher.cached_empty_datasetwrites the.zarrschema sample of a request directly under its final name,abc.zarr. Every process that needs the same request uses the same name, so a process can open a sample that another one is still writing:abc.zarr.abc.zarr: first the group, then the coordinates, then the variable.abc.zarralready there and opens it, half-written.The error B gets depends on how far A has got:
IndexError: list index out of rangeatclient_cdsapi.py:156: no data variable written yet;KeyError: 't': a coordinate, such assurface, is taken as the variable;ContainsArrayError: An array exists in store ... at path 'step': B didn't find the folder either, and writes into it too.Nothing in the code prevents this:
get_write_lock(f"{HOSTNAME}-zarr")) is a thread lock in a hedp-data-access dask task (scheduler="threads"). It holds only among the threads of one process, so every dask worker has its own, even within one worker pod.Example 1: a hedp-data-access ingestion on the esa-hedp cluster
setup.do_setupandingestion.do_ingestion.--nthreads=1)./tmp, shared by the pod's 8 dask workers), withcache_file: true.retries: 1.Each dask task runs on its own dask worker and opens the dataset there. The dask workers can't see the sample the controller pod built during setup in its own
/tmp, so 5 dask workers build it at the same time, each with its own lock.request_chunks{"year": 10}(120 months){"year": 2}(24 months){"year": 1}(12 months)On main, 8 of the 9 runs failed on a half-written sample and 1 failed with
ContainsArrayError. With 1 dask task per batch, main completes in all three cases, because only one dask worker needs the sample at a time.Example 2: a process killed while writing the sample
abc.zarrabc.zarr.1a2b3c4d, which is never openedThe change
cached_empty_datasetnow saves the sample the way GRIB downloads are saved, throughrobust_save_to_file:abc.zarrabc.zarr.1a2b3c4d(random suffix)abc.zarr. If another process got there first, it deletes its own copy and keeps theirsSo every process writes its own copy, the first one renamed into place is the one everybody opens, and
abc.zarronly exists once it's complete.robust_save_to_filealso removes a temporary folder whose write failed. Before, it only removed files.Costs
abc.zarr.<random>folder behind. These can be removed withrm -r <cache_folder>/*.zarr.????????when no run is active.Not in this PR
A sample that is already broken under its final name is still used as it is. It may have been left by a process killed before this PR, or damaged when copied. A follow-up PR will handle it, introducing the possibility to overwrite the cached .zarr files.
Tests
New
tests/test_30_engine_ecmwf.py: