Skip to content
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

fix(inotify): watch arbitrary depth #5410

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

rgrinberg
Copy link
Member

This PR fixes the inotify backend to allow watching x/y/z even if y (or x) do not exist. The fix works by watching the topmost directory that exists and then adding more watches as descendants are created. After adding a watch, we rescan the directory (as per the manual) because some events might have already occurred in that directory.

@rgrinberg rgrinberg added this to the 3.0.0 milestone Feb 3, 2022
@rgrinberg rgrinberg requested a review from snowleopard February 3, 2022 17:35
@rgrinberg
Copy link
Member Author

friendly ping @snowleopard

@snowleopard
Copy link
Collaborator

@rgrinberg There is some logic in fs_memo.ml that (partially) addresses this issue:

(* CR-someday aalekseyev: For [watch_path] to work correctly we need to ensure
that the parent directory of [path] exists. That's certainly not guaranteed
by the [Fs_memo] API, so we should do something to make it more robust, but
I believe that is masked by the fact that we usually (always?) look at the
source directory before looking for files in that directory.
It might seem that the [`Does_not_exist] "fall back to the containing dir"
trick used below can be extended to fall back all the way to the root, but
it can't be because watching the root is not sufficient to receive events
for creation of "root/a/b/c" -- for that we need to watch "root/a/b". *)
let watch_path watcher path =
match Dune_file_watcher.add_watch watcher path with
| Ok () -> ()
| Error `Does_not_exist -> (
(* If we're at the root of the workspace (or the Unix root) then we can't
get [`Does_not_exist] because Dune can't start without a workspace and
the Unix root always exists. Hence, the [_exn] below can't raise,
except if the user deletes the workspace directory under our feet, in
which case all bets are off. *)
let containing_dir = Path.parent_exn path in
(* If the [path] is absent, we need to wait for it to be created by
watching the parent. We still try to add a watch for the [path] itself
after that succeeds, in case the [path] was created already before we
started watching its parent. *)
(match Dune_file_watcher.add_watch watcher containing_dir with
| Ok () -> ()
| Error `Does_not_exist ->
Log.info
[ Pp.textf "Attempted to add watch to non-existent directory %s."
(Path.to_string containing_dir)
]);

Is this not sufficient for you, i.e. are you hitting the Attempted to add watch to non-existent directory path?

@rgrinberg
Copy link
Member Author

Thanks for the pointer, we should get rid of that workaround now I suppose.

Is this not sufficient for you, i.e. are you hitting the Attempted to add watch to non-existent directory path?

Yes, this is easy to hit. But also, note that the macos does not have this limitation at all. So this PR just addresses the inconsistency between the two backends.

@snowleopard
Copy link
Collaborator

@rgrinberg Right, makes sense. Let's drop this workaround then.

@rgrinberg rgrinberg force-pushed the ps/rr/fix_inotify___watch_arbitrary_depth branch from 20f8ea0 to dba7c8f Compare February 8, 2022 01:07
@rgrinberg
Copy link
Member Author

The workaround is gone.

@rgrinberg rgrinberg force-pushed the ps/rr/fix_inotify___watch_arbitrary_depth branch from dba7c8f to a4879e3 Compare February 8, 2022 15:30
@rgrinberg
Copy link
Member Author

ping @snowleopard

@snowleopard
Copy link
Collaborator

ping @snowleopard

Sorry, I'm too out of breath for reviewing subtle stuff like this. Need to find some calm time -- will try to review by end of week!

@rgrinberg rgrinberg removed this from the 3.0.0 milestone Feb 10, 2022
@rgrinberg
Copy link
Member Author

@snowleopard this week perhaps?

in
Mutex.unlock inotify.mutex;
( [ Event.Fs_memo_event (Fs_memo_event.create ~kind ~path) ]
, Option.to_list to_scan )
Copy link
Collaborator

Choose a reason for hiding this comment

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

to_scan might as well be a list to avoid this to_list.

match Table.mem inotify.awaiting_creation path with
| false -> None
| true ->
Inotify_lib.add inotify.inotify (Path.to_string path);
Copy link
Collaborator

Choose a reason for hiding this comment

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

The assumption here seems to be that the path got created. Maybe worth adding an assert about this?

Copy link
Collaborator

@snowleopard snowleopard Feb 15, 2022

Choose a reason for hiding this comment

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

Actually, it doesn't seem great that we're doing all this for every event, not just for events that correspond to path creations. I think we shoud refactor this logic so that inotify.awaiting_creation and the associated mutex is only touched when necessary. (There should probably be a separate function that processes path creation events and returns a list of paths that need to be scanned.)

let processed, to_scan =
process_inotify_event ~inotify event
in
loop (acc @ processed) (events @ scan_dirs to_scan)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The order of arguments in @ seems weird, almost guaranteeing quadratic complexity if we assume that acc grows from a large number of small processed lists. Should this be swapped?

Copy link
Collaborator

Choose a reason for hiding this comment

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

(Same for events.)

let inotify =
create_inotifylib_watcher ~inotify:inotify_decl ~sync_table ~scheduler
in
Fdecl.set inotify_decl
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add a brief comment why this Fdecl is needed?

| Unix.Unix_error (ENOENT, _, _) -> Error `Does_not_exist)
()
| Inotify { inotify; awaiting_creation; mutex } ->
Mutex.lock mutex;
Copy link
Collaborator

@snowleopard snowleopard Feb 15, 2022

Choose a reason for hiding this comment

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

Same architectural concern here: messing around with mutex for every watch, even though most watches don't care about the awaiting_creation logic, seems wrong.

let (_ : (_, _) result) = Table.add awaiting_creation p () in
match Path.parent p with
| None ->
User_warning.emit
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should print something more informative here. My understanding is that we can only end up here if p is the workspace root and it doesn't exist? I guess this can only mean that the user deleted the directory while Dune was running. If you agree that that's the only situation where we can reach this code path, let's just say this.

{|
{ path = In_source_tree "e"; kind = "Created" }
{ path = In_source_tree "e/1"; kind = "Created" }
{ path = In_source_tree "e/1/2"; kind = "Created" } |}]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you also add a test where we move a whole tree (say, with 2 levels and 2 subdirectories in each node) into a watched place? I'd like to make sure we can correctly deal with moves and trees (in addition to creating sequences of nested paths as tested above).

Copy link
Collaborator

@snowleopard snowleopard left a comment

Choose a reason for hiding this comment

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

Thanks, it's a good improvement but I'm a bit unhappy that we are polluting happy/typical code paths with unhappy logic -- can we structure the code in a way that makes it obvious that the mutex and friends are only needed for watching paths that don't exist?

Signed-off-by: Rudi Grinberg <[email protected]>

ps-id: 38F4A728-5530-4539-AECD-259E16F44D40
Signed-off-by: Rudi Grinberg <[email protected]>
and also make sure that we unlock on finalize to avoid dead locks

Signed-off-by: Rudi Grinberg <[email protected]>
Signed-off-by: Rudi Grinberg <[email protected]>
@rgrinberg rgrinberg force-pushed the ps/rr/fix_inotify___watch_arbitrary_depth branch from a4879e3 to 8018fab Compare March 4, 2022 17:55
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