Ruby IO#flock retry boundaries for duplicate write prevention
0 reputation · 24 Apr 2022, 10:45 UTC
0 reputation · 24 Apr 2022, 10:45 UTC
When multiple Ruby processes append records to a shared file, an exclusive File::LOCK_EX via IO#flock serializes access, but it does not make the guarded write itself atomic or idempotent. If a write raises an IOError or the process is interrupted after a partial write, a naive retry inside the same locked block can duplicate bytes. The lock is advisory and is released automatically when the descriptor closes or is garbage-collected, so the retry window must be carefully bounded.
On POSIX systems, attempting to flock a file already locked by the same process raises EDEADLK, which complicates retry loops that re-acquire the lock rather than reusing the existing descriptor. Using File::LOCK_NB returns false immediately instead of blocking, but that only changes scheduling, not write durability.
Given these constraints, what is the reliable boundary for retrying a failed write under an exclusive flock without duplicating data? How should partial-write state be tracked across retries in the same process, and is re-entrant locking ever a safe pattern for this?
35775 reputation · 24 Apr 2022, 20:31 UTC
The reliable retry boundary is a single File::LOCK_EX acquisition on one open file descriptor. Retry only inside that critical section using the same descriptor. Do not close, reopen, or re-acquire the lock between attempts. Release the lock explicitly with flock(File::LOCK_UN) or by closing the descriptor in an ensure block.
flock is released when any descriptor for that file is closed by the process. Closing and reopening creates a new critical section that cannot see prior partial-write state.flock is not reentrant. A second flock(File::LOCK_EX) on a different descriptor for the same file in the same process will block (self-deadlock) or raise EDEADLK depending on the OS. Never nest lock calls expecting mutex behavior.IO#syswrite (or IO#write with sync mode) and capture the returned byte count.IOError is raised, keep the descriptor open, advance a cursor into your record buffer by the bytes actually written, and retry only the remaining suffix from the current file position.File::APPEND (O_APPEND), each syswrite targets the current end of file. Record the file size (fstat.size) before the first attempt; on retry, truncate to that size if you need to overwrite a torn record, then write the full record again.File.open('shared.log', File::WRONLY | File::APPEND | File::CREAT) do |f|
f.flock(File::LOCK_EX)
begin
record = "#{Time.now.iso8601} | event data\n"
offset = 0
while offset < record.bytesize
written = f.syswrite(record.byteslice(offset..-1))
raise IOError, "zero-byte write" if written == 0
offset += written
end
ensure
f.flock(File::LOCK_UN)
end
end
flock alone cannot prevent duplicates after a process crash or power loss — the lock is gone, and the partial bytes remain on disk. To survive crashes:
File::LOCK_NB ChangesLOCK_NB makes acquisition non-blocking (flock returns false immediately when contended). It affects scheduling only — it does not change durability, atomicity, or the retry boundary.
flock is emulated via mandatory locking; semantics differ. Verify cross-process exclusion on your target OS.flock may be unreliable or unsupported. Use fcntl locks (File::LOCK_EX with File::LOCK_NB on some Rubies) or a dedicated coordination service (Redis, etcd, database row locks) instead.File::LOCK_EX in one; confirm the other blocks (or returns false with LOCK_NB) until the lock is released in an ensure block.syswrite returns fewer bytes than the record length. Capture the byte count, resume writing only the unsent suffix, and confirm the file contains exactly one complete record.flock(File::LOCK_EX) on a separate descriptor for the same file in the same process to observe whether it blocks or raises EDEADLK on your OS. Do not use this pattern in production.syswrite's return value and File.stat(path).size before and after each write to confirm partial-write tracking matches actual bytes on disk.What is the target deployment OS and filesystem (local ext4/xfs, NFS, Windows, container overlay)? The answer changes whether flock is sufficient or you need fcntl / external coordination.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.