Skip to content

Improved macOS support: java.nio.file Watch Service - #41

Merged
sungshik merged 18 commits into
improved-macos-support-mainfrom
improved-macos-support/nio-file-watch-service
Apr 30, 2025
Merged

Improved macOS support: java.nio.file Watch Service#41
sungshik merged 18 commits into
improved-macos-support-mainfrom
improved-macos-support/nio-file-watch-service

Conversation

@sungshik

@sungshiksungshik commented Apr 21, 2025

Copy link
Copy Markdown
Contributor

This PR adds an implementation of java.nio.file's Watch Service API that uses NativeEventStreams (added in #40) to access the native APIs on macOS. It's an alternative to JDK's FileSystems.getDefault().newWatchService().

(The additional comments below contain copy/pastes of the JDK docs for convenience.)

@sungshiksungshik mentioned this pull request Apr 21, 2025
3 tasks
@codecov

codecovBot commented Apr 21, 2025

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 70.00000% with 36 lines in your changes missing coverage. Please review.

Project coverage is 80.6%. Comparing base (73b0238) to head (5b04d16).
Report is 19 commits behind head on improved-macos-support-main.

Files with missing linesPatch %Lines
...a/engineering/swat/watch/impl/mac/MacWatchKey.java80.6%4 Missing and 8 partials ⚠️
.../engineering/swat/watch/impl/mac/MacWatchable.java56.5%8 Missing and 2 partials ⚠️
...ava/engineering/swat/watch/impl/jdk/JDKPoller.java63.6%6 Missing and 2 partials ⚠️
...gineering/swat/watch/impl/mac/MacWatchService.java53.8%5 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## improved-macos-support-main #41 +/- ##
================================================================
+ Coverage 65.3% 80.6% +15.2% - Complexity 136 163 +27 
================================================================
Files 20 23 +3 Lines 705 822 +117 Branches 80 95 +15 ================================================================
+ Hits 461 663 +202 + Misses 205 97 -108 - Partials 39 62 +23 

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sungshiksungshik left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Comments

Comment threadsrc/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java Outdated
Comment threadsrc/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java Outdated
Comment threadsrc/main/java/engineering/swat/watch/impl/mac/MacWatchKey.java Outdated
Comment threadsrc/test/java/engineering/swat/watch/SmokeTests.java
@sungshik
sungshik marked this pull request as ready for review April 23, 2025 08:38

@DavyLandmanDavyLandman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some small comments

Comment threadsrc/main/java/engineering/swat/watch/impl/jdk/JDKPoller.java Outdated
Comment threadsrc/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java Outdated
Comment threadsrc/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java Outdated
Comment threadsrc/main/java/engineering/swat/watch/impl/mac/MacWatchKey.java Outdated
public MacWatchKey(MacWatchable watchable, MacWatchService service) throws IOException {
this.watchable = watchable;
this.service = service;
this.pendingEvents = new LinkedBlockingQueue<>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe we should batch add a whole range of events? Such that the queue contains lists of events? To avoid filling up the queue a lot and then cleaning it out again?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

For the sake of finishing this feature in a "good enough" state asap, for now, I'll add this as a possible performance optimization to the list of tasks in the feature PR (#39). Let's decide later if we want/need to do this as part of that PR, or move it to a separate issue.

private final NativeEventStream stream;

private volatile Configuration config = new Configuration();
private volatile boolean signalled = false; // `!signalled` means "ready"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need the signal variable? Is it not just : whenever the queue is not empty?

As I see all kinds of subtle races between clearing it and setting it in this class.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

According to the WatchKey spec, a watch key remains signalled until it's reset, even if the queue becomes (temporarily) nonempty, so a separate field is needed to do this bookkeeping.

The interaction between signalled and pendingEvents is quite tricky, indeed. To make it easier to understand what happens, I've isolated those fields and all code that accesses them in a separate helper inner class (including comments to argue that no harmful races arise).

}

public void unregister(MacWatchService watcher) {
registrations.remove(watcher);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't this also cause some kind of call to the wachter?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This method is used only as part of WatchKey.cancel(), which is called by the poller when the watcher for the path of that watch key is closed. I.e., the watcher triggers the unregistration, so it already knows about it. I'll make it package-private, though, because it's just an internal bookkeeping method that shouldn't be used directly by third parties.

Comment threadsrc/main/java/engineering/swat/watch/impl/mac/MacWatchable.java Outdated

@sungshiksungshik left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks! Addressed the comments and added a few clarifying responses.

Comment threadsrc/main/java/engineering/swat/watch/impl/mac/MacWatchKey.java Outdated

@Override
public List<WatchEvent<?>> pollEvents() {
var list = new ArrayList<WatchEvent<?>>(pendingEvents.size());

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Nope. (Well, to be fair, yes, computing the length of a linked list is indeed O(n) 😎, but LinkedBlockingQueue has an extra field to track it.)

}

public void unregister(MacWatchService watcher) {
registrations.remove(watcher);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This method is used only as part of WatchKey.cancel(), which is called by the poller when the watcher for the path of that watch key is closed. I.e., the watcher triggers the unregistration, so it already knows about it. I'll make it package-private, though, because it's just an internal bookkeeping method that shouldn't be used directly by third parties.

public MacWatchKey(MacWatchable watchable, MacWatchService service) throws IOException {
this.watchable = watchable;
this.service = service;
this.pendingEvents = new LinkedBlockingQueue<>();

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

For the sake of finishing this feature in a "good enough" state asap, for now, I'll add this as a possible performance optimization to the list of tasks in the feature PR (#39). Let's decide later if we want/need to do this as part of that PR, or move it to a separate issue.

private final NativeEventStream stream;

private volatile Configuration config = new Configuration();
private volatile boolean signalled = false; // `!signalled` means "ready"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

According to the WatchKey spec, a watch key remains signalled until it's reset, even if the queue becomes (temporarily) nonempty, so a separate field is needed to do this bookkeeping.

The interaction between signalled and pendingEvents is quite tricky, indeed. To make it easier to understand what happens, I've isolated those fields and all code that accesses them in a separate helper inner class (including comments to argue that no harmful races arise).

@sungshik
sungshik merged commit e21382d into improved-macos-support-mainApr 30, 2025
@sungshik
sungshik deleted the improved-macos-support/nio-file-watch-service branch April 30, 2025 17:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@sungshik@DavyLandman