You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@commons.apache.org by Artem Bilan via dev <de...@commons.apache.org> on 2026/08/27 18:39:01 UTC
[IO] Tailer leaves a stale/closed reader after a failed reopen-on-rotation, silently killing the tailing thread
Hi all,
While investigating an intermittent message-loss report in Spring
Integration's ApacheCommonsFileTailingMessageProducer (which wraps Tailer),
we traced it to a bug in Tailer.run()'s rotation-detection branch. Filing
this here first since it looks related to the still-open
https://issues.apache.org/jira/browse/IO-399, and wanted to share the
analysis in case it's useful, or points to a shared fix for both. Plus, it
looks like we cannot create JIRA accounts to open a new ticket or comment
on the opened one.
The code (Tailer.java, current run(), the length < position "file was
rotated" branch):
if (length < position) {
// File was rotated
listener.fileRotated();
// Reopen the reader after rotation ensuring that the old file is
closed iff we re-open it
// successfully
try (RandomAccessResourceBridge save = reader) {
reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
// At this point, we're sure that the old file is rotated
// Finish scanning the old file and then we'll start with the new
one
try {
readLines(save);
} catch (final IOException ioe) {
listener.handle(ioe);
}
position = 0;
} catch (final FileNotFoundException e) {
// in this case we continue to use the previous reader and
position values
listener.fileNotFound();
ThreadUtils.sleep(delayDuration);
}
continue;
}
The bug: if the reopen tailable.getRandomAccess(RAF_READ_ONLY_MODE)
throws FileNotFoundException — which happens naturally during ordinary
rotation, when the old file has just been moved/removed but the replacement
hasn't been created yet — the comment says the code "continues to use the
previous reader and position values." It doesn't: the enclosing try
RandomAccessResourceBridge save = reader) { ... } already closed that
reader (as save) before the reopen attempt failed, because the assignment
reader = ... never completed. reader still refers to that now-closed
object.
On the next iteration, once the replacement file grows past the stale
position, the loop takes the length > position branch and calls
readLines(reader) on the closed resource. That throws, and it's caught by
the outer catch (final Exception e) { listener.handle(e); } in run()'s
top-level try — which does not rethrow. run() proceeds to finally { ...;
close(); } and returns. The tailing thread is gone for good, with no
indication beyond whatever the TailerListener.handle(Exception) callback
happened to do with it (in our case, publishing an event nobody was
necessarily watching).
Net effect: a routine, transient "file briefly absent during rotation"
window can permanently and silently stop a Tailer, with no automatic
recovery, no exception surfaced to the caller, and no correction of the
stale reader/position state as the comment claims.
Relation to IO-399: that issue describes the same class of failure
(uncaught/unhandled FileNotFoundException during a rotation-triggered
reopen killing the thread) in the sibling reOpen=true periodic-reopen path,
which today still has no try/catch around its getRandomAccess() call at
all:
if (getRun() && reOpen) {
reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
reader.seek(position);
}
Both point at the same underlying gap: Tailer doesn't have a robust way
to say "the file is momentarily gone, keep the old state and retry" without
either (a) closing a resource it then tries to keep using, or (b) not
catching the failure at all.
Suggested direction: in the length < position branch, don't let the
try-with-resources close the old reader until a replacement has actually
been successfully opened — e.g. only swap save/reader after
getRandomAccess() succeeds, or restructure so the FileNotFoundException
catch reopens/reassigns a genuinely usable reader (or explicitly nulls it
out and lets the "reader == null" top-of-loop reopen logic take over)
rather than leaving a closed handle referenced by a live variable.
Workaround on our side:
https://github.com/spring-projects/spring-integration/pull/11381
Thanks for maintaining commons-io — let us know if a minimal standalone
reproducer would help, we can put one together!
Regards,
Artem Bilan
Spring Framework Team
Broadcom
Re: [IO] Tailer leaves a stale/closed reader after a failed reopen-on-rotation, silently killing the tailing thread
Posted by Gary Gregory <ga...@gmail.com>.
Hi Artem,
Thank you for your email.
The best path forward would be to create a PR on GitHub with a test that
fails without main changes.
You should be able to create a Jira account, it just need to be approved
manually due to the amount of spamming accounts we get otherwise.
Thank you!
Gary
On Thu, Aug 27, 2026, 18:12 Artem Bilan via dev <de...@commons.apache.org>
wrote:
> Hi all,
>
>
>
>
>
>
>
> While investigating an intermittent message-loss report in Spring
> Integration's ApacheCommonsFileTailingMessageProducer (which wraps Tailer),
> we traced it to a bug in Tailer.run()'s rotation-detection branch. Filing
> this here first since it looks related to the still-open
> https://issues.apache.org/jira/browse/IO-399, and wanted to share the
> analysis in case it's useful, or points to a shared fix for both. Plus, it
> looks like we cannot create JIRA accounts to open a new ticket or comment
> on the opened one.
>
>
>
>
>
>
> The code (Tailer.java, current run(), the length < position "file was
> rotated" branch):
>
>
>
>
>
>
> if (length < position) {
>
>
>
> // File was rotated
>
>
>
> listener.fileRotated();
>
>
>
> // Reopen the reader after rotation ensuring that the old file is
> closed iff we re-open it
>
>
> // successfully
>
>
>
> try (RandomAccessResourceBridge save = reader) {
>
>
>
> reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
>
>
>
> // At this point, we're sure that the old file is rotated
>
>
>
> // Finish scanning the old file and then we'll start with the
> new one
>
>
> try {
>
>
>
> readLines(save);
>
>
>
> } catch (final IOException ioe) {
>
>
>
> listener.handle(ioe);
>
>
>
> }
>
>
>
> position = 0;
>
>
>
> } catch (final FileNotFoundException e) {
>
>
>
> // in this case we continue to use the previous reader and
> position values
>
>
> listener.fileNotFound();
>
>
>
> ThreadUtils.sleep(delayDuration);
>
>
>
> }
>
>
>
> continue;
>
>
>
> }
>
>
>
>
>
>
>
> The bug: if the reopen tailable.getRandomAccess(RAF_READ_ONLY_MODE)
> throws FileNotFoundException — which happens naturally during ordinary
> rotation, when the old file has just been moved/removed but the replacement
> hasn't been created yet — the comment says the code "continues to use the
> previous reader and position values." It doesn't: the enclosing try
> RandomAccessResourceBridge save = reader) { ... } already closed that
> reader (as save) before the reopen attempt failed, because the assignment
> reader = ... never completed. reader still refers to that now-closed
> object.
>
>
>
>
>
>
>
> On the next iteration, once the replacement file grows past the stale
> position, the loop takes the length > position branch and calls
> readLines(reader) on the closed resource. That throws, and it's caught by
> the outer catch (final Exception e) { listener.handle(e); } in run()'s
> top-level try — which does not rethrow. run() proceeds to finally { ...;
> close(); } and returns. The tailing thread is gone for good, with no
> indication beyond whatever the TailerListener.handle(Exception) callback
> happened to do with it (in our case, publishing an event nobody was
> necessarily watching).
>
>
>
>
>
>
>
> Net effect: a routine, transient "file briefly absent during rotation"
> window can permanently and silently stop a Tailer, with no automatic
> recovery, no exception surfaced to the caller, and no correction of the
> stale reader/position state as the comment claims.
>
>
>
>
> Relation to IO-399: that issue describes the same class of failure
> (uncaught/unhandled FileNotFoundException during a rotation-triggered
> reopen killing the thread) in the sibling reOpen=true periodic-reopen path,
> which today still has no try/catch around its getRandomAccess() call at
> all:
>
>
>
>
>
>
>
> if (getRun() && reOpen) {
>
>
>
> reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
>
>
>
> reader.seek(position);
>
>
>
> }
>
>
>
>
>
>
>
> Both point at the same underlying gap: Tailer doesn't have a robust way
> to say "the file is momentarily gone, keep the old state and retry" without
> either (a) closing a resource it then tries to keep using, or (b) not
> catching the failure at all.
>
>
>
>
> Suggested direction: in the length < position branch, don't let the
> try-with-resources close the old reader until a replacement has actually
> been successfully opened — e.g. only swap save/reader after
> getRandomAccess() succeeds, or restructure so the FileNotFoundException
> catch reopens/reassigns a genuinely usable reader (or explicitly nulls it
> out and lets the "reader == null" top-of-loop reopen logic take over)
> rather than leaving a closed handle referenced by a live variable.
>
>
>
>
>
> Workaround on our side:
> https://github.com/spring-projects/spring-integration/pull/11381
>
>
>
>
>
>
> Thanks for maintaining commons-io — let us know if a minimal standalone
> reproducer would help, we can put one together!
>
> Regards,
> Artem Bilan
> Spring Framework Team
> Broadcom
>