Writes to a uniquely-named temporary file in the same directory as path,
then renames it into place. Rename-into-place is atomic on POSIX
filesystems, and server-side atomic on SMB/CIFS. So an interrupted write
leaves the destination either absent or complete. It is never a truncated
file that a later read would halt on. An interruption here means a
SIGKILL, a crash, or a dropped mount. ... is forwarded to
qs2::qs_save().
Arguments
- object
Object to serialize.
- path
Destination path. Its parent directory must already exist.
- ...
Passed to
qs2::qs_save()(e.g.nthreads).
Details
The parent directory of path must already exist: this function never
creates it.
What this does not promise, stated because the tempting reading is wrong:
It is not durability.
file.rename()is atomic with respect to other readers; it is not anfsync. A power loss can still lose a renamed file whose data has not reached the disk. This protects against a killed process, not a killed machine.It is not a lock. Two concurrent writers of the same
patheach produce a complete file and the last rename wins. No reader sees a torn file, but nothing here decides which writer should have won.It does not always clean up after itself. The partial temp file is removed on an R-level error. But
on.exit()cannot run after aSIGKILL, so a hard-killed worker leaves its randomly-named.tmpbehind. The destination is still absent-or-complete, which is the guarantee that matters; the litter is not.
The temporary file is created with tempfile() in the destination directory
rather than paste0(path, ".tmp", Sys.getpid()). A PID suffix is not
collision-proof. PIDs are unique only among live processes on one host.
Data of this kind commonly lives on a share that two hosts mount at once.
The same PID on two machines could then pick the same temp path for the
same target. Same directory is required: file.rename() is not atomic
across filesystems.
This is a standalone writer, independent of the
run_and_write_files_atomically() commit engine. Its temp carries no
dispatch attempt token, and that engine's failure cleanup does not sweep it.
See also
run_and_write_files_atomically() for the same guarantee applied
to every output of a dispatched item. That function's commit engine is
separate: this writer emits no marker, and its temporary file is not
swept by that engine's failure cleanup.
vignette("batchit") for where this sits relative to
the four dispatch functions.
Examples
path <- file.path(tempdir(), "cars.qs2")
write_qs2_atomically(mtcars, path)
nrow(qs2::qs_read(path))
#> [1] 32
# The destination is replaced only once the new file is complete, so a
# reader never sees a truncated file, and an interrupted write leaves the
# previous contents intact.
write_qs2_atomically(mtcars[1:5, ], path)
nrow(qs2::qs_read(path))
#> [1] 5
unlink(path)
