diff options
author | Antonio Barone <syntonyze@gmail.com> | 2025-07-01 12:22:46 +0200 |
---|---|---|
committer | Antonio Barone <syntonyze@gmail.com> | 2025-07-02 15:24:03 +0200 |
commit | c3f354edc72f37cee1d8439c9e060d7f04eede6d (patch) | |
tree | febca7691a22b51619e46d0945dd97e9043d1efc | |
parent | 8d524e74d6c14d551b543e6e9d3ef62e9d676d68 (diff) | |
download | jgit-stable-7.2.tar.gz jgit-stable-7.2.zip |
Lock reftable auto-refresh to ensure consistencystable-7.2
Ensure that reftable auto-refresh operations, clearing the database
cache and reloading the reftable stack are executed in an exclusive
critical section under lock. Previously, these steps were performed
without an exclusive critical section, creating a window where
concurrent threads could interfere with each other.
In a race condition, one thread might clear the cache and before it had
a chance of reloading the stack, another thread could repopulate the
cache with stale data, keeping a reference to the open BlockSource
channel to the underlying tables that are subsequently removed when the
first thread reloads the stack.
The above race condition resulted in attempts to access closed resources
and lead to ClosedChannelException errors.
As an example, consider the following scenario:
* T0 - Thread-1 is executing auto-refresh and it clears the database
cache
* T1 - The master branch moves forward (for any reason):
- A new refTable (`R_new`) file is created
- An existing refTable (`R_old`) file is deleted due to
auto-compaction.
* T3 - Thread-2 repopulates the database cache before Thread-1 has had a
chance to reload the refTable stack.
* T4 - Thread-1 finally reloads the refTable stack, causing the closing
of the BlockSource wrapping the removed `R_old` refTable file.
* T5 - Thread-2 attempts to read from the already-closed `R_old`
BlockSource and the `j.n.c.ClosedChannelException` is thrown
To reproduce this problem, you can run a script created to craft this
racing condition: I1e78e175cff.
While such errors during concurrent execution might be expected and
tolerable in isolation, the situation becomes more severe when the
`RepositoryCache` is involved, as is the case with Gerrit.
The `FileReftableDatabase` instance is cached within the `Repository`
object. When a `BlockSource` is closed prematurely due to this race
condition, the dangling reference remains in memory until the cached
Repository expires, which is one hour by default.
This means that, once the race occurs, the repository may be unable to
perform any ref lookups for up to an hour, effectively causing a
repository outage.
By introducing a ReentrantLock around both operations, the refresh logic
now guarantees that concurrent readers and writers maintain a consistent
view of the reftable state, eliminating the race condition.
Verified by executing the script provided at I1e78e175cff and no
exceptions are raised anymore.
Bug: jgit-130
Change-Id: I6153528a7b2695115b670bda04d4d4228c1731e1
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java | 10 |
1 files changed, 8 insertions, 2 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java index e9782e2e18..d5a060f06c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java @@ -270,8 +270,14 @@ public class FileReftableDatabase extends RefDatabase { public void refresh() { try { if (!reftableStack.isUpToDate()) { - reftableDatabase.clearCache(); - reftableStack.reload(); + ReentrantLock lock = getLock(); + lock.lock(); + try { + reftableDatabase.clearCache(); + reftableStack.reload(); + } finally { + lock.unlock(); + } } } catch (IOException e) { throw new UncheckedIOException(e); |