Skip to content
This repository has been archived by the owner on May 5, 2021. It is now read-only.

User config watcher #3

Closed
wants to merge 27 commits into from
Closed

Conversation

asymmetric
Copy link

This PR adds a watcher for the user.toml file.

It's based on @krnowak's still-unmerged work on the file watcher.

@asymmetric asymmetric force-pushed the asymmetric/user-toml-watcher branch 2 times, most recently from e156811 to 48f550a Compare September 29, 2017 15:53
@@ -660,6 +662,7 @@ impl Service {
}

/// Write service files from gossip data to disk.
/// For the location, check `svc_file_path()`.
Copy link
Author

Choose a reason for hiding this comment

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

Wasn't sure how to create a symlink for this. Also, the function is private, so I'm not sure if there's a point.

Copy link

Choose a reason for hiding this comment

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

I guess this is a note to yourself, because I don't understand what it is about. Not enough context.

Copy link
Author

Choose a reason for hiding this comment

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

The svc_file_path function could be rendered as a link, since /// comments are Markdown. I just don't know what the target should be, as it seems to be relative to something.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed this.

@asymmetric
Copy link
Author

NB: The config is not currently reloaded, so this doesn't work yet.

@asymmetric asymmetric changed the title User config watcher [WIP] User config watcher Oct 1, 2017
Copy link

@krnowak krnowak left a comment

Choose a reason for hiding this comment

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

Looks good overall, I had some nits.

fs_cfg: Arc::new(fs_cfg),
organization: cfg.organization,
service_states: HashMap::new(),
sys: Arc::new(sys),
peer_watcher: peer_watcher,
user_config_watcher: UserConfigWatcher::new(),
Copy link

Choose a reason for hiding this comment

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

If you changed the order of members in Manager struct, it would be good to reorder the initializations here too.

@@ -660,6 +662,7 @@ impl Service {
}

/// Write service files from gossip data to disk.
/// For the location, check `svc_file_path()`.
Copy link

Choose a reason for hiding this comment

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

I guess this is a note to yourself, because I don't understand what it is about. Not enough context.



pub struct UserConfigWatcher {
// TODO use a HashSet? we don't really need the boolean:
Copy link

Choose a reason for hiding this comment

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

This comment does not seem to be true - reset_events_for sets the boolean to false.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, this is a note to myself saying that we could change the implementation. True/false values can be represented by presence/absence in a set.

Copy link
Author

Choose a reason for hiding this comment

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

Tried using a HashSet and turns out it's more complicated than expected (if we put a HashSet in an Arc, that HashSet is immutable, unless I use mutexes), so not worth it.

Copy link

Choose a reason for hiding this comment

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

Yeah, you would need to do something similar to what some code in butterfly does - Arc<RwLock<HashSet<Whatever>>>. Anyway, I think that current solution is fine as well.

);
let callbacks = UserConfigCallbacks { have_events: have_events };
let mut file_watcher =
default_file_watcher(&path, callbacks).expect("creating user-config watcher");
Copy link

Choose a reason for hiding this comment

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

Instead of using expect, I'd use outputln! macro to report an error and maybe quit the thread. I'm not sure if we should panic in this case.

Copy link
Author

Choose a reason for hiding this comment

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

Good point, I've committed a change.

File::create(&file_path).expect("creating file");

// Allow the watcher to notice that a file was created.
thread::sleep(Duration::from_millis(100));
Copy link

Choose a reason for hiding this comment

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

Why sleeping instead of wait_for_events?

use hcore::service::ServiceGroup;
use manager::service::Service;

static LOGKEY: &'static str = "UCM";
Copy link

Choose a reason for hiding this comment

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

Consider adding this log key to the documentation here - www/source/docs/reference/log-keys.html.md.

Copy link

@krnowak krnowak left a comment

Choose a reason for hiding this comment

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

Another two things I noticed.

name: String::from("foo"),
path: TempDir::new("user-config-watcher")
.expect("creating temp dir")
.into_path(),
Copy link

Choose a reason for hiding this comment

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

One note about this - when you use the into_path() function, you also take over the responsibility of removing the created temporary directory. Which you probably don't do, no?

Copy link
Author

Choose a reason for hiding this comment

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

Good point, fixed.

// This trait exists to ease the testing of functions that receive a Service. Creating Services
// requires a lot of ceremony, so we work around this with this trait.
pub trait Serviceable {
fn name(&self) -> &String;
Copy link

Choose a reason for hiding this comment

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

Returning references makes the trait less flexible, because you need an existing, non-temporary thing in memory to return its address (like a member of a struct that implements this trait). In other words - you can't compute the return value. Not sure if it is really important, but in general I would just return plain values. Of course the downside of returning values is that you need to clone the stuff, but OTOH this won't likely be a bottleneck ever.

Copy link
Author

Choose a reason for hiding this comment

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

My rationale here is that the name is always a reference to its containing struct.

Isn't it enough for users of this trait to be able to call name.clone() if they want to own the String?

Can you show an example of where the current implementation creates issues? As I'm not sure I understand your comment.

Copy link

Choose a reason for hiding this comment

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

There are no problems with current implementation. It was just a general note about returning references in trait functions. So in this case it's likely ok, but in general I would avoid it.

Copy link
Author

Choose a reason for hiding this comment

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

OK, let me know if you feel strongly about changing this, otherwise I'm gonna leave it as is.

Copy link

Choose a reason for hiding this comment

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

Nah, leave it as is, please. If somebody else points out this thing, then you can change it.

@asymmetric asymmetric changed the title [WIP] User config watcher User config watcher Oct 5, 2017
@asymmetric
Copy link
Author

@krnowak addressed your comments, but I guess this will just wait until your PR gets merged.

I also rebased on yours.

I will clean up the commit messages next week.

Copy link

@krnowak krnowak left a comment

Choose a reason for hiding this comment

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

I had two questions, otherwise looks good.

let (reload, reconfigure) = {
let ctx = self.render_context(census_ring);
let reload = self.compile_hooks(&ctx);
let reload = self.compile_hooks(&ctx) || self.user_config_updated;
Copy link

Choose a reason for hiding this comment

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

Should this || self.user_config_updated be moved one line below, to the let reconfigure line?

Copy link
Author

Choose a reason for hiding this comment

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

No, the needs_reconfiguration flag is only used to trigger a reconfigure hook, which may not be defined. The reload flag is actually going to trigger a service reload, which is what we need here.

Self { states: HashMap::new() }
}

pub fn add<T: Serviceable>(&mut self, service: &T) {
Copy link

Choose a reason for hiding this comment

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

We have an add function. Do we need a remove function then?

@asymmetric asymmetric changed the base branch from krnowak/watch-peer-file to master October 10, 2017 13:06
@asymmetric
Copy link
Author

asymmetric commented Oct 10, 2017

The PR is in a weird state right now. It shows commit that are not in the PR's branch. What happened is that the base branch got merged into master, and after that happened, I rebased my branch on master.

I then changed the PR to use master as the base branch, but now it shows commits here that shouldn't be in this branch (and aren't there according to git log).
It seems like this is a problem with GitHub's UI.

Also, please don't be too picky about commit messages (and the commits themselves), as I am still testing this PR out.

@asymmetric asymmetric force-pushed the asymmetric/user-toml-watcher branch 6 times, most recently from 0f04074 to 1a5b686 Compare October 18, 2017 15:32
Lorenzo Manacorda added 6 commits October 18, 2017 17:32
The UserConfigWatcher watches each service's user.toml file.
It reacts on file creation, deletion and change, triggering a service
reload and configuration.

If a service has hooks defined, those are executed, otherwise the
default actions are performed instead.
Now that there are multiple watcher, it makes sense to make the SpecWatcher field more descriptive
Lorenzo Manacorda and others added 18 commits October 18, 2017 17:32
It's used in several places, so a constant makes sense.
The comment was introduced in a9c98dd
and was incorrect back then as well. In the meantime, the function has been
changed and now doesn't return any value.
The `cfg_updated` variable is used to determine whether to run the
following block, in which we rebuild the configuration.

We need this to happen if the user-config watcher has detected changes to the
user-config file.
After we have performed all the actions required on detecting that the
user-config has changed, we revert the field to `false`, so that we
don't perform them again unless there are more changes.
The `load_user` function is in charge of loading the user-config file.
We need this function to be called every time the user-config watcher
has detected change to that file.
`user_config_updated` is set in the manager's
`update_running_services_from_user_config_watcher` function, which
receives events from the user-config watcher.

This ensures that the service is then reloaded whenever the
configuration is updated.
By adding another channel, we allow the User Config Watcher to
communicate to any of the Workers that they should stop running.

This allows the manager to cleanly remove a service.
Using a sync channel with buffer size of 1 allows us to mimic the
behaviour of a boolean. We are only interested in the fact that there
were events, not how many (since the message on this channel is always
`()`).
We are not interested in the messages themselves, only in their presence
Signed-off-by: Krzesimir Nowak <krzesimir@kinvolk.io>
Before this refactor, the "Hooks recompiled" message was being printed
twice:

The first time, because the `user.toml` had been updated, which in turn meant the
service got restarted.
That counted as a change in the census ring, which in turn caused the
hooks to be recompiled and the message to be printed again.

With this refactor, we avoid running compile_hooks when the only thing
that's changed is the user config.

Signed-off-by: Lorenzo Manacorda <lorenzo@kinvolk.io>
Before this change, we were calling the `load_user` method every time
there was a change, even if it wasn't unrelated to user config.

Now, we only do it if the `user_config_updated` flag is set.

Signed-off-by: Lorenzo Manacorda <lorenzo@kinvolk.io>
This function mirrors `default_file_watcher`, but instead of calling
create it calls `create_with_no_initial_event`.

It is used to start a default file watcher, where the first event
triggered by the file already being there isn't emitted.

Signed-off-by: Lorenzo Manacorda <lorenzo@kinvolk.io>
Before this change, if the user.toml file was already present, the
service would be restarted immediately after being booted, because the
File Watcher would notice the file and emit an event.

With this function, the File Watcher doesn't emit the event anymore.

Signed-off-by: Lorenzo Manacorda <lorenzo@kinvolk.io>
@asymmetric asymmetric force-pushed the asymmetric/user-toml-watcher branch 2 times, most recently from 3b31707 to 648d99f Compare October 18, 2017 16:27
@asymmetric
Copy link
Author

@krnowak could you take a look again?

Copy link

@krnowak krnowak left a comment

Choose a reason for hiding this comment

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

Looks mostly fine, but there was one hairy piece of code I'd like to see if it can be "balder".

// The problem we're trying to work around here by adding this block is that `write`
// creates an immutable borrow on `self`, and `self.remove_service` needs `&mut self`.
// The solution is to introduce the block to drop the borrow before the call to
// `self.remove_service`, and use `mem::swap` to copy the services to a variable defined
Copy link

Choose a reason for hiding this comment

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

To move, not copy. After swapping, the vector under self.services lock will be empty.

let cfg_changed = cfg_updated_from_rumors || self.user_config_updated;
let something_to_do = cfg_changed || census_ring.changed();

if something_to_do {
Copy link

Choose a reason for hiding this comment

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

This became a bit hard to follow. Would pseudo-rust below be clearer? (cufr = cfg_updated_from_rumors, sucu = self.user_config_updated, crc = census_ring.changed())

if sucu {
    if let Err(e) = self.cfg.load_user(&self.pkg) {…}
}
let (cfg_changed, reload, reconfigure) = {
    let ctx = …;
    if cufr || crc {
        (cufr || sucu, self.compile_hooks(&ctx) || sucu, self.compile_configuration(&ctx))
    } else if sucu {
        (true, true, self.compile_configuration(&ctx))
    } else {
        (false, self.needs_reload, self.needs_reconfiguration)
    }
}
self.needs_reload = reload;
self.needs_reconfiguration = reconfigure;
self.user_config_updated = false;
/* return */ cfg_changed

Copy link

Choose a reason for hiding this comment

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

The something_to_do variable would be gone.

Copy link
Author

@asymmetric asymmetric Oct 23, 2017

Choose a reason for hiding this comment

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

I find the 3 value tuple even less clear tbh...

(cufr || sucu, self.compile_hooks(&ctx) || sucu, self.compile_configuration(&ctx))

what?

Copy link

Choose a reason for hiding this comment

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

Not sure what this "what?" is about, but to be clear - I don't actually want to have variables like cufr or sucu. I wrote it like that to avoid typing in the comment.

Copy link
Author

Choose a reason for hiding this comment

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

I think that even with more descriptive names, it's hard to parse logically. There's just too much happening on one line: returning a tuple, ORing two values, calling functions..

Not sure it's an improvement.

Copy link

@krnowak krnowak Oct 23, 2017

Choose a reason for hiding this comment

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

You don't need to put everything in one line. You could very well have:

let config_changed = config_updated_from_rumors || self.user_config_updated;
let needs_reload = self.compile_hooks(&ctx) || self.user_config_updated;
let needs_reconfigure = self.compile_configuration(&ctx);
(config_changed, needs_reload, needs_reconfigure)

In general, what I wanted to propose is to simplify the flow of the code, instead of using nested ifs and matches that are sometimes executed and sometimes not.

Copy link
Author

Choose a reason for hiding this comment

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

In the above implementation though, we're calling self.compile_hooks even if we don't need to, which will lead to "Hooks recompiled" being printed over and over (this function gets executed on every tick).

I'm definitely open to simplifying the code but obviously it has to have the same behaviour.

The problem is that some of these functions have side effects so we can't just call them willy-nilly.

Copy link

Choose a reason for hiding this comment

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

The self.compile_hooks(&ctx) should only be executed when either cfg_updated_from_rumors is true or if census_ring.changed() returns true. So if this happens everytime, then something is wrong.

stop_running: Sender<()>,
}

type ServiceName = String;
Copy link

Choose a reason for hiding this comment

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

That's nice.

Ok(())
}

/// Removes a service from the User Config Watcher, thereby stopping the watcher thread.
Copy link

Choose a reason for hiding this comment

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

I'd add "eventually" - "thereby eventually stopping …"

Copy link
Author

Choose a reason for hiding this comment

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

Why eventually? Do you mean that the operation is asynchronous?

If so, I think I can explain it more explicitly.

Ok(())
}

/// Checks whether the watcher for the specified service has observed any events, thereby
Copy link

Choose a reason for hiding this comment

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

thereby is your new favourite word, eh? :)

Copy link
Author

Choose a reason for hiding this comment

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

It's a pretty cool word, you gotta admit that

/// consuming them.
pub fn have_events_for<T: Serviceable>(&self, service: &T) -> bool {
if let Some(state) = self.states.get(service.name()) {

Copy link

Choose a reason for hiding this comment

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

Drop this empty line?

Lorenzo Manacorda added 3 commits October 25, 2017 16:27
Signed-off-by: Lorenzo Manacorda <lorenzo@kinvolk.io>
The thread is currently not restarted.

Signed-off-by: Lorenzo Manacorda <lorenzo@kinvolk.io>
This refactor aims at simplifying the flow of the code and reducing
conditionals.

Signed-off-by: Lorenzo Manacorda <lorenzo@kinvolk.io>
@asymmetric
Copy link
Author

Closing in favor of habitat-sh#3898.

@asymmetric asymmetric closed this Oct 25, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants