Repository navigation
Fix the parallel writer's flush and close ordering - #36
Merged
Merged
Conversation
Problem: with fstWriterSetParallelMode(ctx, 1), a file whose blocks are flushed before the close can be unreadable: the reader stops on a trace flushed every 5 time steps whose last time step has no changes. The writer copies its context for the next flush thread, and fstWriterClose2() reads the section state, without waiting for the previous flush thread: a lock and unlock of the mutex returns at once when the thread has not taken the mutex yet, and the loop on in_pthread came after the copy and read it without the mutex. The copy can then hold the previous section's start. The parallel path also cleared section_header_only, which the serial flush leaves set after it writes the next section's header, so the close never truncated a trailing header without data. And a flush of a block without value changes, which the serial flush skips, started a thread that wrote nothing but still counted a section and dropped the time steps of the block. Solution: fstWriterWaitFlushThread() waits for the flush thread by reading in_pthread under the mutex, which the thread holds while it flushes and while it clears in_pthread. The writer calls it before it copies the context, at the start of the close and after the close's last flush. The parallel path skips a block without value changes and records section_header_only as the serial flush does, and the flush thread decides about the next section header from its own copy of the context instead of reading the writer's bit-fields while the writer changes them. tests/write_parallel.c writes the same pseudo-random traces with the serial and the parallel writer, reads the parallel file back and compares it with the serial file byte for byte. It has no block without value changes: the serial writer has a bug of its own there, fixed separately. Signed-off-by: Marco Brambilla <marco@hairyotter.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Please update both this PR and #35 versus what is current in libfst, thanks! |
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem. In the parallel mode (
fstWriterSetParallelMode(ctx, 1)), a file that is flushed before it is closed can come out broken. Example: 64 variables, 400 time steps, a flush every 5 time steps, and a last time step without changes. Depending on how the threads happen to interleave, the reader either stops withfstReaderIterBlocks2(), fac: 2 clen: 48 (rc=-3), exiting., or reads values that differ from the file the serial writer writes for the same trace. The serial writer is always right.Cause. In the parallel mode, each flush copies the writer's context and hands the copy to a thread; the writer meanwhile goes on filling the next block. Four things go wrong:
fstWriterFlushContextPrivate()copies the context before the previous flush thread has finished. Its lock-and-unlock of the mutex returns at once if the thread has not taken the mutex yet, and its wait onin_pthreadcomes after the copy. So the copy can still hold the previous section's start.fstWriterClose2()reads the section state after the same lock-and-unlock, so it can also run before the thread has finished.section_header_only. The serial flush keeps it set after writing the next section's header, so that the close removes a header that no value change follows. In the parallel mode that header is never removed.ThreadSanitizer also reports a data race in the parallel case of
tests/write_error.c: the flush thread readsskip_writing_section_hdrin the writer's context while the writer setsflush_context_pending, a bit-field in the same word.Fix. A new helper,
fstWriterWaitFlushThread(), waits for the flush thread by readingin_pthreadunder the mutex. The thread holds the mutex while it flushes and while it clearsin_pthread, so once the wait returns, everything the thread wrote to the writer's context is visible, including the write error added in #34.fstWriterClose2(), and after the close's last flush. The spin loops onin_pthreadare removed.section_header_onlyas the serial flush does.The serial path is unchanged.
Test.
tests/write_parallel.cwrites the same pseudo-random trace (variables of 1 to 128 bits, some of them aliases, values 0, 1, x and z) with the serial and with the parallel writer. It reads the parallel file back withfstReaderIterBlocks(), compares every value change with what was written, and then compares the two files byte for byte (both set the same date withfstWriterSetDate()). It covers a flush every 5 time steps and no flush before the close. It avoids blocks without value changes, because the serial writer has its own bug there, fixed in #35. Meson registers the test only when threads are available.Without this change the new test fails in all 6 builds below. With it, the whole suite passes with gcc 13 and clang 18 on Linux x86-64, plain, with
-Db_sanitize=address,undefined, and with-Db_sanitize=thread; under ThreadSanitizer,write_parallelandwrite_erroralso passed 20 runs each. Two reports of gcc's UndefinedBehaviorSanitizer are already on main and are not changed here:empty_fileandmask_no_variablespass a null pointer with a length of 0 tofwrite()infstWriterEmitSectionHeader().AI-assisted (Claude Opus 5.5)