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 <[email protected]>
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
>

Reply via email to