Skip to content

Commit 644b798

Browse files
authored
Merge pull request #6430 from joewiz/bugfix/4334-filelock-buffer-race
2 parents 10430be + 6742079 commit 644b798

1 file changed

Lines changed: 40 additions & 20 deletions

File tree

exist-core/src/main/java/org/exist/storage/lock/FileLock.java

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,15 @@ public class FileLock {
8686
/** An open channel to the lock file */
8787
private SeekableByteChannel channel = null;
8888

89-
/** Temporary buffer used for writing */
90-
private final ByteBuffer buf = ByteBuffer.allocate(MAGIC.length + 8);
91-
92-
/** The time (in milliseconds) of the last heartbeat written to the lock file */
93-
private long lastHeartbeat = -1L;
89+
/**
90+
* The time (in milliseconds) of the last heartbeat written to the lock file.
91+
* Marked {@code volatile} so the Quartz scheduler thread that calls
92+
* {@link #save()} every {@link #HEARTBEAT} ms publishes its writes
93+
* visibly to the main thread, which reads the value during
94+
* {@link #checkHeartbeat()} when deciding whether to take over a stale
95+
* lock file.
96+
*/
97+
private volatile long lastHeartbeat = -1L;
9498

9599
public FileLock(final BrokerPool pool, final Path path) {
96100
this.pool = pool;
@@ -229,23 +233,38 @@ private void open() throws IOException {
229233
this.channel = Files.newByteChannel(lockFile, READ, WRITE, SYNC);
230234
}
231235

236+
@SuppressWarnings("PMD.AvoidThrowingNullPointerException")
232237
protected void save() throws IOException {
233238
try {
234239
if (channel == null) {
235240
open();
236241
}
237-
238-
long now = System.currentTimeMillis();
239-
buf.clear();
242+
243+
// Allocate per-call rather than reusing a shared instance field:
244+
// save() and read() ran concurrently from the Quartz heartbeat
245+
// worker thread and the main startup thread without
246+
// synchronization, so a shared ByteBuffer's position/limit could
247+
// be clobbered mid-write -- the source of the
248+
// BufferOverflowException / IllegalArgumentException reported in
249+
// issue #4334.
250+
final ByteBuffer buf = ByteBuffer.allocate(MAGIC.length + 8);
251+
final long now = System.currentTimeMillis();
240252
buf.put(MAGIC);
241253
buf.putLong(now);
242254
buf.flip();
243255
channel.position(0);
244256
channel.write(buf);
245257
//channel.force(true); //handled by SYNC on open option
246258
lastHeartbeat = now;
247-
259+
248260
} catch(final NullPointerException npe) {
261+
// PMD: this catch-and-rethrow preserves the original NPE rather
262+
// than constructing a new one — the rule that flagged the throw
263+
// is meant for new NullPointerException() construction. The NPE
264+
// here is the symptom of release() nullifying `channel` between
265+
// our null-check and the channel.write() call; if the pool is
266+
// shutting down that race is expected and silenced, otherwise
267+
// the original NPE is propagated for diagnosis.
249268
if(pool.isShuttingDown()) {
250269
LOG.info("No need to save FileLock, database is shutting down");
251270
} else {
@@ -258,39 +277,40 @@ private void read() throws IOException {
258277
if (channel == null) {
259278
open();
260279
}
261-
280+
281+
// See save() for why this is a per-call local rather than a
282+
// shared instance field (issue #4334).
283+
final ByteBuffer buf = ByteBuffer.allocate(MAGIC.length + 8);
262284
channel.read(buf);
263285
buf.flip();
264286
if (buf.limit() < 16) {
265-
buf.clear();
266287
throw new IOException(message("Could not read file lock.", null));
267288
}
268-
289+
269290
final byte[] magic = new byte[8];
270291
buf.get(magic);
271292
if (!Arrays.equals(magic, MAGIC)) {
272293
throw new IOException(message("Bad signature in lock file. It does not seem to be an eXist lock file", null));
273294
}
274-
295+
275296
lastHeartbeat = buf.getLong();
276-
buf.clear();
277-
297+
278298
final DateFormat df = DateFormat.getDateInstance();
279299
message("File lock last access timestamp: " + df.format(getLastHeartbeat()), null);
280300
}
281301

282-
protected String message(String message, final Exception e) {
302+
protected String message(final String message, final Exception e) {
283303
final StringBuilder str = new StringBuilder(message);
284304
str.append(' ').append(lockFile.toAbsolutePath());
285305
if (e != null) {
286306
str.append(": ").append(e.getMessage());
287307
}
288-
289-
message = str.toString();
308+
309+
final String formatted = str.toString();
290310
if (LOG.isInfoEnabled()) {
291-
LOG.info(message);
311+
LOG.info(formatted);
292312
}
293313

294-
return message;
314+
return formatted;
295315
}
296316
}

0 commit comments

Comments
 (0)