@@ -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