Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

sungshik
Copy link
Contributor

@sungshik sungshik commented Apr 21, 2025

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.)

@sungshik sungshik mentioned this pull request Apr 21, 2025
3 tasks
Copy link

codecov bot commented Apr 21, 2025

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 lines Patch % Lines
...a/engineering/swat/watch/impl/mac/MacWatchKey.java 80.6% 4 Missing and 8 partials ⚠️
.../engineering/swat/watch/impl/mac/MacWatchable.java 56.5% 8 Missing and 2 partials ⚠️
...ava/engineering/swat/watch/impl/jdk/JDKPoller.java 63.6% 6 Missing and 2 partials ⚠️
...gineering/swat/watch/impl/mac/MacWatchService.java 53.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.

Copy link
Contributor Author

@sungshik sungshik left a comment

Choose a reason for hiding this comment

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

Comments

@sungshik sungshik marked this pull request as ready for review April 23, 2025 08:38
@sungshik sungshik requested a review from DavyLandman April 23, 2025 08:38
Copy link
Member

@DavyLandman DavyLandman left a comment

Choose a reason for hiding this comment

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

Some small comments

public MacWatchKey(MacWatchable watchable, MacWatchService service) throws IOException {
this.watchable = watchable;
this.service = service;
this.pendingEvents = new LinkedBlockingQueue<>();
Copy link
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
Contributor Author

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
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
Contributor Author

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
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
Contributor Author

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.

Copy link
Contributor Author

@sungshik sungshik left a comment

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.


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

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
Contributor Author

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
Contributor Author

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
Contributor Author

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-main Apr 30, 2025
33 of 35 checks passed
@sungshik sungshik deleted the improved-macos-support/nio-file-watch-service branch April 30, 2025 17:20
Sign up for free to 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