Skip to content

Add Watch Files source with persistent state and optional runtime - #8788

Open
michaaels wants to merge 1 commit into
apache:mainfrom
michaaels:feat/watch-files-source
Open

michaaels wants to merge 1 commit into
apache:mainfrom
michaaels:feat/watch-files-source

Conversation

@michaaels

@michaaels michaaels commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Adds a Watch Files source for directory arrivals and changes, with AUTO/NATIVE/POLLING detection, stable-file checks, persistent checkpoints and reconciliation. It emits CREATED/MODIFIED/DELETED metadata for existing Hop readers. The GUI has General, Advanced and Maintenance tabs, wildcard filters, automatic state identity and an optional runtime in minutes/hours; blank keeps monitoring continuously.

Implemented with Codex (high reasoning setting). Extensive tests cover Windows/Linux, real pipeline restarts, overflow, checkpoint/crash recovery, SFTP, ENOSPC and GUI behavior. State represents source observations, independently of downstream completion; support is limited to the normal Local Hop Engine.

  • Latest review validation: Windows 94 passed (18 platform/environment skips); Linux 108 headless + 10 isolated GUI passed (11 platform/environment skips); 9 real SFTP tests passed separately. Checkstyle, Spotless and RAT passed.
  • Review regressions cover decoded local paths, Unicode/literal percent signs, Windows drive/UNC conversion, unusual Unix filenames, capped invalid-key cleanup and directory recreation. Watch Files is now ordered before Web services lookup.
  • Historical 24-hour Linux soak: 187,000 events, zero duplicates/unexpected rows. Later changes have separate regressions; the soak, ENOSPC and packaged HopRun smoke were not rerun for these review fixes.
  • Testing instructions. Full reactor CI remains the merge gate.

General (optional timeout):

Watch Files General

Maintenance:

Watch Files Maintenance

  • One commit; module clean install and license/format checks passed.
  • Full reactor CI for the updated commit: awaiting maintainer approval (code, documentation).
  • This contribution is licensed under the Apache License, Version 2.0.

@mattcasters mattcasters left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

// its raw path to contain UTF-8 percent escapes; Windows happens to accept the Unicode form.
// VFS can leave these filename characters unescaped; they are illegal in a URI path.
URI uri =
URI.create(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] localPath is on the listing hot path (isLink calls it for every local file and directory). Commons VFS 2.10 only percent-encodes #, space, and % in getURI(). This method then encodes []{}|^`` and still calls URI.create. A legal Unix name containing ?, ", <, >, `, or a newline is left raw. URI.create throws IllegalArgumentException for most of those, and Path.of throws for ? (URI has a query component) and for # if it were unescaped. tick only catches IOException, and processRow turns that runtime exception into a transform failure, so one such file stops the watcher. The same URI.create pattern is repeated in resolveFile for explicit file: paths.

Suggestion: Percent-encode every character that is illegal in a URI path (at least ?, ", <, >, \, and controls) before URI.create, or build the Path from the decoded VFS path instead of round-tripping the URI. Add a regression with a name such as report?.csv and a<b.txt.

// A key already removed from the service must be processed on the next scan.
if (key != null) {
key.pollEvents();
key.reset();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] When the hint cap stops the drain, this already-dequeued key is discarded with pollEvents() and reset() without using the return value. The in-loop path removes keys and registered when reset() is false; this path does not. If the directory is gone, it stays in registered, so register will not watch it again after it is recreated, and it still counts toward the directory limit. Reconciliation can still see the files, but native delivery for that directory stays off for the rest of the run.

Suggestion: Use the same reset() handling as the loop: on false, drop the key and its path from keys and registered before setting reconcile.

*** xref:pipeline/transforms/webservices.adoc[Web services lookup]
*** xref:pipeline/transforms/workflow-executor.adoc[Workflow Executor]
*** xref:pipeline/transforms/workflow-logging.adoc[Workflow logging]
*** xref:pipeline/transforms/watchfiles.adoc[Watch Files]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The transform list is alphabetical by title. Watch Files is inserted after Workflow logging and before Write to log, so it sits past Web services lookup and Workflow Executor.

Suggestion: Move this xref to just before Web services lookup.

@michaaels
michaaels force-pushed the feat/watch-files-source branch from 539f1d9 to 6fd8ccc Compare October 8, 2026 02:57
@michaaels
michaaels requested a review from mattcasters October 8, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants