Fix: atomic write for last_run metadata to prevent corruption on SIGKILL - #199
Fix: atomic write for last_run metadata to prevent corruption on SIGKILL#199CDJ-happier wants to merge 2 commits into
Conversation
FileHandler#write used File.write (truncate-then-write), which is non-atomic.
If the process is SIGKILL'd (e.g. OOM killer) between open(O_TRUNC) and write,
the metadata file is left empty. On restart, YAML.load('') returns false/nil,
:sql_last_value falls back to default (0 / 1970-01-01), and the SQL becomes
'WHERE id >= 0' triggering a full-table scan.
Fix: write to a temp file (#{path}.tmp.#{pid}) then File.rename (atomic on
same filesystem). If killed mid-write, original file stays intact.
Added 4 rspec tests covering normal write, no temp residue, failure
preservation, and old-content-intact-until-rename.
|
❌ Author of the following commits did not sign a Contributor Agreement: Please, read and sign the above mentioned agreement if you want to contribute to this project |
FileHandler is defined at module Jdbc scope, not nested under ValueTracking. Use FileHandler directly (visible in module Jdbc context).
There was a problem hiding this comment.
Pull request overview
This PR addresses corruption of the JDBC input’s last_run metadata file by switching FileHandler#write from a truncate-then-write approach to an atomic write-temp-then-rename pattern, reducing the chance of an empty/invalid metadata file after a SIGKILL (e.g., OOM kill) and preventing unintended full-table scans on restart.
Changes:
- Implement atomic metadata persistence in
FileHandler#writeusing temp-file +rename. - Add RSpec coverage for the atomic-write behavior and failure modes.
- Bump plugin version to
5.6.4and document the fix in the changelog.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| version | Bumps plugin version to 5.6.4. |
| CHANGELOG.md | Adds a 5.6.4 entry describing the atomic last_run metadata persistence fix. |
| lib/logstash/plugin_mixins/jdbc/value_tracking.rb | Updates FileHandler#write to write to a temp file and atomically rename into place; adds cleanup. |
| spec/plugin_mixins/jdbc/value_tracking_spec.rb | Adds specs validating correctness, cleanup, and failure-preservation semantics for atomic writes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # triggering a full-table scan on next run. | ||
| # rename(2) is atomic on the same filesystem, so the target file is either | ||
| # fully updated or untouched. | ||
| tmp = "#{@path}.tmp.#{Process.pid}" |
|
|
||
| # simulate failure when writing the temp file | ||
| allow(::File).to receive(:write).and_call_original | ||
| allow(::File).to receive(:write).with(/\.tmp\.\d+$/, anything).and_raise(Errno::ENOSPC) |
|
@CDJ-happier Would you mind signing the CLA? Thanks! |
PR 描述
Why
renameis atomicrename(2)is guaranteed atomic by POSIX when source and destination are on the same filesystem. The destination file is either fully replaced or untouched—there is no intermediate state where it is empty or partially written. Since the temp file is created in the same directory as the target (same filesystem), this guarantee holds.Why PID in the temp filename
Defensive against (unlikely) concurrent writes to the same path from multiple plugin instances. Each process gets its own temp file (
<path>.tmp.<pid>), avoiding collision. Theensureblock cleans up any residual temp file.Testing
Added 4 rspec tests in
spec/plugin_mixins/jdbc/value_tracking_spec.rb:.tmp.*file left after successful writeErrno::ENOSPC), original file content is untouched and no temp file remainsFile.renameto verify target file still has old content at the moment just before renameReal-world incident
This fix addresses a production incident observed in a Logstash pipeline:
SIGKILL) mid-File.write→ metadata file truncated to 0 bytes:sql_last_valuefell back to0→ SQLWHERE id >= 0full-table scan loaded 100k historical rowscreate_timefrom 2022 → rejected by ES time-partition validation (must not write data whose timestamp has expired) → HTTP 500The
truncate-then-writeroot cause was confirmed via/var/log/messages(OOM kill at 01:57:38) andlogstash-plain.log(SQL changed fromid >= 1283605499467534336toid >= 0after restart at 01:58).