Fix ThreadSanitizer data races between bundle processing and async background writer closing in FileBasedSink and WriteFiles - #39458
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Checks are failing. Will not request review until checks are succeeding. If you'd like to override that behavior, comment |
|
Assigning reviewers: R: @chamikaramj for label java. Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
|
R: @sjvanrossum |
|
Stopping reviewer notifications for this pull request: review requested by someone other than the bot, ceding control. If you'd like to restart, comment |
|
If I understand correctly, Alternatively, attempt a CAS on |
05f9167 to
32eea5d
Compare
|
@sjvanrossum It's not only open() and async close(), it is also racing between write() and async close. take a look at this approach. |
|
R: @sjvanrossum |
|
Ah, of course. We may expect any method to be called on the writer thread and Any sane implementation of
If you mark |
…ckground writer closing in FileBasedSink and WriteFiles
32eea5d to
49085f8
Compare
|
Something like this perhaps? /** The channel to write to. */
private volatile @Nullable WritableByteChannel channel;
/**
* Opens a uniquely named temporary file and initializes the writer using {@link #prepareWrite}.
*
* <p>The unique id that is given to open should be used to ensure that the writer's output does
* not interfere with the output of other Writers, as a bundle may be executed many times for
* fault tolerance.
*/
public final void open(String uId) throws Exception {
// Shadow members as local variables to ensure local consistency and force member access
// through this.
final @Nullable String id;
final @Nullable ResourceId outputFile;
final @Nullable WritableByteChannel channel;
try {
this.id = id = spreadUid(uId); // normal store
ResourceId tempDirectory = getWriteOperation().getTempDirectory();
this.outputFile =
outputFile =
tempDirectory.resolve(id, StandardResolveOptions.RESOLVE_FILE); // normal store
verifyNotNull(
outputFile,
"FileSystems are not allowed to return null from resolve: %s",
tempDirectory);
final WritableByteChannelFactory factory =
getWriteOperation().getSink().writableByteChannelFactory;
// The factory may force a MIME type or it may return null, indicating to use the sink's
// MIME.
String channelMimeType = firstNonNull(factory.getMimeType(), mimeType);
CreateOptions createOptions =
StandardCreateOptions.builder()
.setMimeType(channelMimeType)
// The file is based upon a uuid and thus we expect it to be unique and to not
// already
// exist. A new uuid is generated on each bundle processing and thus this also holds
// across bundle retries. Collisions of filenames would result in data loss as we
// would otherwise overwrite already finalized data.
.setExpectFileToNotExist(true)
.build();
WritableByteChannel tempChannel = FileSystems.create(outputFile, createOptions);
try {
this.channel =
channel =
factory.create(tempChannel); // volatile store (sequentially consistent release)
} catch (Exception e) {
// If we have opened the underlying channel but fail to open the compression channel,
// we should still close the underlying channel.
try (tempChannel) {
throw e;
}
}
} catch (Throwable t) {
// Ensure that memory visibility matches non-exception case before throwing.
this.channel = null; // volatile store (sequentially consistent release)
throw t;
}
// The caller shouldn't have to close() this Writer if it fails to open(), so close
// the channel if prepareWrite() or writeHeader() fails.
try {
LOG.debug("Preparing write to {}.", outputFile);
prepareWrite(channel);
LOG.debug("Writing header to {}.", outputFile);
writeHeader();
} catch (Exception e) {
LOG.error("Beginning write to {} failed, closing channel.", outputFile, e);
try (channel) {
throw e;
}
}
LOG.debug("Starting write of bundle {} to {}.", id, outputFile);
}
public final void cleanup() throws Exception {
final @Nullable WritableByteChannel channel = this.channel; // volatile load (acquire)
final @Nullable ResourceId outputFile = this.outputFile; // normal load
try (channel) {
if (channel != null && channel.isOpen()) {
LOG.warn("Channel to {} is still open after cleanup() was called.", outputFile);
}
// channel and outputFile may be null if open() was not called or failed.
if (outputFile != null) {
LOG.info("Deleting temporary file {}", outputFile);
FileSystems.delete(
Collections.singletonList(outputFile), StandardMoveOptions.IGNORE_MISSING_FILES);
}
}
}
/** Closes the channel and returns the bundle result. */
public final void close() throws Exception {
final @Nullable WritableByteChannel channel = this.channel; // volatile load (acquire)
final @Nullable ResourceId outputFile = this.outputFile; // normal load
try (channel) {
checkState(outputFile != null, "FileResult.close cannot be called with a null outputFile");
LOG.debug("Closing {}", outputFile);
writeFooter();
finishWrite();
LOG.debug("Closing channel to {}.", outputFile);
}
LOG.info("Successfully wrote temporary file {}", outputFile);
} |
Summary
Fixes ThreadSanitizer data races during asynchronous file closing in
WriteFiles/FileBasedSinkand an OS file-descriptor race inTFRecordSchemaTransformProviderTest.Root Cause
FileBasedSink/WriteFiles):FileBasedSink.Writerinstances are written to on the bundle processing thread and closed asynchronously viaMoreFutures.runAsync. Lacking a release-acquire memory barrier between threads, TSAN reported cross-thread data races on:FileBasedSink.Writer.close()V(FileBasedSink.java:1066readingchannel.isOpen()vs.Writer.open()atFileBasedSink.java:993writingchannel = factory.create(...)).CRC32.getValue()J(reading GZIP checksums duringclose()vs.crc.update(...)duringwrite(value)).TFRecordSchemaTransformProviderTest): An unclosedFileInputStreamcaused a syscall race when the JDK background Cleaner thread closed the unreferenced file descriptor during test execution.Solution
readyToClose): Added anAtomicBoolean readyToClosehandoff toFileBasedSink.Writer. CallingreleaseForBackgroundClose()(readyToClose.set(true)) before spawning the async task and callingreadyToClose.get()at the start ofclose()/cleanup()ensures thatopen(),write(), and all stream mutations happen-before background closing.FileInputStreaminTFRecordSchemaTransformProviderTest.runTestWritein a try-with-resources block for deterministic closure.Testing
TFRecordSchemaTransformProviderTest(--config=tsan-chlor) pass cleanly with zero data race reports.Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:
addresses #123), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, commentfixes #<ISSUE NUMBER>instead.CHANGES.mdwith noteworthy changes.See the Contributor Guide for more tips on how to make review process smoother.
To check the build health, please visit https://github.com/apache/beam/blob/master/.test-infra/BUILD_STATUS.md
GitHub Actions Tests Status (on master branch)
See CI.md for more information about GitHub Actions CI or the workflows README to see a list of phrases to trigger workflows.