-
Notifications
You must be signed in to change notification settings - Fork 43.7k
Fix/gh 51463 nested jar locking #51580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
25f0577
18b4bca
2ecc8ab
aa9fe8e
64c3d03
a10e0f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ | |
| * support for slicing. | ||
| * | ||
| * @author Phillip Webb | ||
| * @author Ian Kettle | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if i should add this - changes to the concurrency feel significant enough to meet threshold in the contribution doc. If its added here it should add to the NestedJarFile change too. |
||
| */ | ||
| class FileDataBlock implements CloseableDataBlock { | ||
|
|
||
|
|
@@ -81,7 +82,7 @@ public int read(ByteBuffer dst, long pos) throws IOException { | |
| long updatedLimit = dst.position() + remaining; | ||
| dst.limit((updatedLimit > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) updatedLimit); | ||
| } | ||
| int result = this.fileAccess.read(dst, this.offset + pos); | ||
| int result = this.fileAccess.read(dst, this.offset + pos, ClosedChannelException::new); | ||
| if (originalDestinationLimit != -1) { | ||
| dst.limit(originalDestinationLimit); | ||
| } | ||
|
|
@@ -160,7 +161,7 @@ static class FileAccess { | |
|
|
||
| private final Path path; | ||
|
|
||
| private int referenceCount; | ||
| private volatile int referenceCount; | ||
|
|
||
| private FileChannel fileChannel; | ||
|
|
||
|
|
@@ -183,8 +184,12 @@ static class FileAccess { | |
| this.path = path; | ||
| } | ||
|
|
||
| int read(ByteBuffer dst, long position) throws IOException { | ||
| int read(ByteBuffer dst, long position, Supplier<? extends IOException> closedExceptionSupplier) | ||
| throws IOException { | ||
| synchronized (this.lock) { | ||
| if (this.referenceCount == 0) { | ||
| throw closedExceptionSupplier.get(); | ||
| } | ||
| if (position < this.bufferPosition || position >= this.bufferPosition + this.bufferSize) { | ||
| fillBuffer(position); | ||
| } | ||
|
|
@@ -243,24 +248,26 @@ private void repairFileChannel() throws IOException { | |
|
|
||
| void open() throws IOException { | ||
| synchronized (this.lock) { | ||
| if (this.referenceCount == 0) { | ||
| int localReferenceCount = this.referenceCount; | ||
| if (localReferenceCount == 0) { | ||
| debug.log("Opening '%s'", this.path); | ||
| this.fileChannel = FileChannel.open(this.path, StandardOpenOption.READ); | ||
| this.buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); | ||
| tracker.openedFileChannel(this.path); | ||
| } | ||
| this.referenceCount++; | ||
| debug.log("Reference count for '%s' incremented to %s", this.path, this.referenceCount); | ||
| this.referenceCount = ++localReferenceCount; | ||
| debug.log("Reference count for '%s' incremented to %s", this.path, localReferenceCount); | ||
| } | ||
| } | ||
|
|
||
| void close() throws IOException { | ||
| synchronized (this.lock) { | ||
| if (this.referenceCount == 0) { | ||
| int localReferenceCount = this.referenceCount; | ||
| if (localReferenceCount == 0) { | ||
| return; | ||
| } | ||
| this.referenceCount--; | ||
| if (this.referenceCount == 0) { | ||
| this.referenceCount = --localReferenceCount; | ||
| if (localReferenceCount == 0) { | ||
| debug.log("Closing '%s'", this.path); | ||
| this.buffer = null; | ||
| this.bufferPosition = -1; | ||
|
|
@@ -274,15 +281,13 @@ void close() throws IOException { | |
| this.randomAccessFile = null; | ||
| } | ||
| } | ||
| debug.log("Reference count for '%s' decremented to %s", this.path, this.referenceCount); | ||
| debug.log("Reference count for '%s' decremented to %s", this.path, localReferenceCount); | ||
| } | ||
| } | ||
|
|
||
| <E extends Exception> void ensureOpen(Supplier<E> exceptionSupplier) throws E { | ||
| synchronized (this.lock) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will need to find or reproduce the thread dump to confirm but I believe this was where the blocking moved to after sorting the NestedJarFile concurrency. There were 4 places inside the class competing for the same lock. The open, close, read and ensureOpen. The only usage of ensureOpen is in the same method call as the read. This change removes the synchronisation entirely from ensureOpen by using an AtomicInteger for the reference tracking which reduces internal contention. Previously FileDataBlock#read was needing to synchronize on the lock twice for each call.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed approach - reverted my original change as i didn't want to include it but then found that there was a gap in the read method meaning that the ensureOpen result could be stale by the time the read actually occurred and the read didn't recheck that it was still open in the sync block. Have moved referenceCount to be volatil so the ensureOpen no longer needs a sync block as it is informative only and the read now also checks the referenceCount within the same sync block that actually does the read |
||
| if (this.referenceCount == 0) { | ||
| throw exceptionSupplier.get(); | ||
| } | ||
| if (this.referenceCount == 0) { | ||
| throw exceptionSupplier.get(); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While validating this approach I realised that the zipContent variable being returned from the zipContent() method isn't a volatile field so there is a small gap here. Adding volatile to this field in NestedJarFileResources would close the gap. Without changing it to volatile though the code should still be safe as the reference counting in the FileDataBlock would catch it and throw a consistent error (no corruption or deadlock).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added additional concurrency tests to verify the safety of this change as it stands. These tests are what found the bug in NestedJarFileResources. The test (NestedJarFileConcurrencyTest) was intended to confirm that the reference count check in FileDataBlock is sufficient to make a stale read of the non-volatile zipContent field fail cleanly.
I have been unable to reproduce the scenario of a stale read, but the possible difference is that a ClosedChannelException is thrown where an IllegalStateException was previously thrown. I did consider catching it and rethrowing as IllegalStateException, but consider the scenario unlikely enough that I have not.