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

Separate out validation of groups of settings #34184

Merged
merged 44 commits into from
Jan 7, 2019
Merged

Separate out validation of groups of settings #34184

merged 44 commits into from
Jan 7, 2019

Conversation

cbismuth
Copy link
Contributor

@cbismuth cbismuth commented Oct 1, 2018

This pull request allows a setting to be validated against target settings to ensure runtime dependencies are met (e.g. disk watermarks low, high and flood_stage).

This pull request also includes a fix to not update settings when no value has changed.

@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-core-infra

Copy link
Contributor

@DaveCTurner DaveCTurner left a comment

Choose a reason for hiding this comment

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

This needs some tests adding. The one I suggested would be valuable, but I think it also needs something that focusses on this change specifically, so that this remains covered even if the disk watermark feature were changed and that test were lost.

@DaveCTurner
Copy link
Contributor

@elasticmachine test this please

@cbismuth
Copy link
Contributor Author

cbismuth commented Oct 4, 2018

Sure, I'll report the test you've suggested and add a new one on this change in particular, thank you @DaveCTurner.

@DaveCTurner
Copy link
Contributor

Note that the build that failed looks like it failed for good reason. Could you move this failure into a unit test and then fix it?

@cbismuth
Copy link
Contributor Author

cbismuth commented Oct 4, 2018

Yes 👍

@cbismuth
Copy link
Contributor Author

cbismuth commented Oct 4, 2018

Note that the build that failed looks like it failed for good reason. Could you move this failure into a unit test and then fix it?

Issue found and fixed in f77d1e7, thanks @DaveCTurner. I'm now focusing on implementing relevant test cases.

@DaveCTurner
Copy link
Contributor

Responding to #28309 (comment) here since this is the right place to discuss this PR.

The single test present in this PR (the current head that I'm looking at is f77d1e7) does not support the change to the production code, in the sense that it passes on today's master as well. To be worthwhile it needs to be a test that fails without the change in place.

The test failure that you found and fixed should also be captured in a test.

Additionally I think we should have a test that focusses on this change to the way that Settings work which is independent of the disk watermark feature.

@cbismuth
Copy link
Contributor Author

cbismuth commented Oct 4, 2018

I agree with you @DaveCTurner, tests are under development.

Let me please reword: in my previous comment, I meant I've modified the test you suggested and I wanted to know whether or not you agree with the changes I've made in your test. Do you see what I mean?

@DaveCTurner
Copy link
Contributor

I wanted to know whether or not you agree with the changes I've made in your test. Do you see what I mean?

I think so, and my feedback was that this test passes on master so doesn't demonstrate the bug that we're chasing, which means it's not the test we need for this change. I suppose it's a reasonable test in the sense that it's expected to pass, but I also think there are already test cases that state the same thing more generally so it's not really necessary.

If there is more work in progress then it's probably best to ping when the whole PR is ready to review - it'll be easier to see the whole picture.

@cbismuth
Copy link
Contributor Author

cbismuth commented Oct 4, 2018

I understand, thank you.

@cbismuth
Copy link
Contributor Author

cbismuth commented Oct 5, 2018

@DaveCTurner PR is complete and ready for review.

@cbismuth
Copy link
Contributor Author

cbismuth commented Oct 5, 2018

I've found another bug in the AbstractScopedSettings#updateSettings API: the return value was true even when no value has changed.

I've added commit 68044ba to this PR to fix it and I've added a test case to cover this change.

PR is ready for review, I've nothing more to add, thank you.

Copy link
Contributor

@DaveCTurner DaveCTurner left a comment

Choose a reason for hiding this comment

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

Thanks @cbismuth, this is a tricky area and you've done some good work here. I've left a few comments and suggested an alternative approach that I think would be better, although I haven't tried it so you're welcome to describe why it doesn't work :)

final ClusterSettings clusterSettings = new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
new DiskThresholdSettings(settings, clusterSettings); // this has the effect of registering the settings updater

settings = clusterSettings.applySettings(Settings.builder()
Copy link
Contributor

Choose a reason for hiding this comment

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

I see what's happened here, and I must apologise. The unit test I suggested was looking at applySettings but really I should have used updateSettings since this is where the update is calculated. My bad.

However, this test still passes on master without any changes so I don't think it should be added here. Could you make an appropriate test based around updateSettings instead, and check that it fails on master and passes on this branch?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries at all. Sure, I'll update the test and run it on this branch and on master.

updates,
"transient"
);
assertFalse(updated);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's ok to return true here. Claiming to have updated a setting when really it hasn't changed in value is a right-side failure: it means we call reroute() to reallocate any shards affected by the new settings, but this does nothing if the setting change was a no-op. On the other hand failing to call reroute() when a setting does change is very bad, so changing the behaviour here needs a lot more scrutiny.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand. I'll restore the previous behavior to not break the current data flow and leave this responsibility to downstream components, it's safer.

I wasn't very at ease with this change, thank you for catching it. I'll dig deeper into it with a debugger just for my own curiosity.

);
assertTrue(updated);
assertThat(target.get(SETTING_FOO_LOW.getKey()), equalTo("20"));
assertThat(updates.get(SETTING_FOO_LOW.getKey()), equalTo("20"));
Copy link
Contributor

Choose a reason for hiding this comment

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

We can write this with fewer lines and fewer mutable variables by putting each of these steps into its own block like this:

        {
            final Settings.Builder updates = Settings.builder();
            assertTrue(service.updateSettings(Settings.builder().put(SETTING_FOO_LOW.getKey(), 20).build(), target, updates, "transient"));
            assertThat(target.get(SETTING_FOO_LOW.getKey()), equalTo("20"));
            assertThat(updates.get(SETTING_FOO_LOW.getKey()), equalTo("20"));
        }

The line-width limit is 140 characters, and this fits into that very neatly. It's something of a matter of taste, but we seem to prefer denser code in this sort of situation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's fine for me. The more immutable code is, the more confident I feel.

Settings.builder(),
"transient"
));
assertThat(exception.getMessage(), equalTo("[high]=10 is lower than [low]=20"));
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we also have a case that shows that if low is set below 10 then high can also be set below 10, to verify that we're not validating against both the default and the actual values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I'll add these test cases.

import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.sameInstance;

public class ScopedSettingsTests extends ESTestCase {

private static class FooLowSettingValidator implements Setting.Validator<Integer> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think these settings can reasonably be moved right next to testUpdateOfValidationDependantSettings since that's the only place they're used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that would be better, I'll move them.

changed = true;
changed = hasChanged(toApply, target);
if (changed) {
validate(toApply, target.build());
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this isn't quite the right approach. As I understand it this change means we're validating the whole settings update here, but by my reading we already validate that later on in the process:

clusterSettings.validate(transientFinalSettings, true);
clusterSettings.validate(persistentFinalSettings, true);

I think it'd be better to try and do less validation here on settings that have dependent settings. Perhaps the Setting.Validator interface should have two validate() methods, one for this stage of validation (good for settings with no dependencies, i.e., most of them) and one for the full validation including dependencies.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I see what you mean. I'll try to move the full validation including dependencies in a new API in Setting.Validator. I also think it's a better location for this.


private boolean hasChanged(Settings toApply, Settings.Builder target) {
boolean changed = toApply.keySet().stream().anyMatch(k -> k.endsWith("*"));
if (!changed) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The house style is to avoid using the ! unary operator and to prefer saying changed == false here, because ! is small and easy to miss in a long expression, and == false is larger and therefore more visible. I think this code will go away due to other comments, but thought I'd raise it anyway for your info.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for sharing this guideline with me, it's indeed more readable without the unary operator.

@DaveCTurner DaveCTurner self-assigned this Oct 6, 2018
@cbismuth
Copy link
Contributor Author

cbismuth commented Oct 8, 2018

Thank you @DaveCTurner for the kind words and insightful review. I'll rework my changes and let you know when it's ready.

Copy link
Contributor

@s1monw s1monw left a comment

Choose a reason for hiding this comment

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

LGTM I left a suggestion regarding the interface

*
* @param value the value of this setting
* @param settings a map from the settings specified by {@link #settings()}} to their values
*/
void validate(T value, Map<Setting<T>, T> settings);
default void validate(T value, Map<Setting<T>, T> settings) {
Copy link
Contributor

Choose a reason for hiding this comment

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

does it make sense to delegate to validate(T value) here?

Copy link
Contributor

Choose a reason for hiding this comment

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

from an interface perspective I think we might be able to only have validate(T value, Optional<Map<Setting<T>, T>> settings) and instead check the optional if needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @s1monw.

I'd prefer to keep these two API as most clients will only be interested in validating a setting in isolation (i.e. not against dependencies).
Besides, we do a fail-fast validation here where we don't have resolved setting dependencies yet. Therefore, if we want to keep this fail-fast shortcut, any client will have to implement an if optional present/absent block even if he's interested in validating a setting against its dependencies.

What do you think about these two points?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I prefer the two methods to the optional parameter too. In practice there are very few dependencies between settings, so the one-argument lambda is less noisy in almost all cases. Settings with dependencies can't use a lambda anyway because they have to override the settings() function too.

I also think there's no need for the dependent validation to delegate to the other one, because we call both during settings validation. However I think this should be mentioned in the Javadocs as implementors shouldn't need to go to the implementation to check this (and I know I'd forget and have to check). Also the interface-level comment needs updating as it's no longer wholly accurate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @DaveCTurner, here is a additional commit 7c33bfe to improve the Validator class documentation, thank you.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't have strong feelings but if I see two methods I'd only implement one and don't know what I need to do with the other. The optional makes me think about the opportunity to validate against others. I don't know what you mean by clients?

Copy link
Contributor Author

@cbismuth cbismuth Nov 19, 2018

Choose a reason for hiding this comment

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

I meant developers who will implement this interface.

We tried to make javadoc more explicit, but I see what you mean: the API doesn't enforce the developer to ask himself whether or not he would like to implement dependencies validation. But as it's an edge case, I still hope the developer would read the javadoc or jump to the interface definition if he needs to do so.

Copy link
Contributor Author

@cbismuth cbismuth Nov 19, 2018

Choose a reason for hiding this comment

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

Another alternative would be to remove the validation shortcut and keep only the validate(T value, Map<Setting<T>, T> settings) interface API, I think that would make sense.

We would remove this validation shortcut here and keep only this validation call here (and remove this line).

What do you think about it?

Copy link
Contributor

@DaveCTurner DaveCTurner left a comment

Choose a reason for hiding this comment

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

I left a comment about Javadocs using the wrong button -> https://github.com/elastic/elasticsearch/pull/34184/files#r234140602

@cbismuth
Copy link
Contributor Author

cbismuth commented Nov 23, 2018

Hi @s1monw and @DaveCTurner, I'm sure everyone is quite busy, so here is a quick follow up below to ease PR validation and merge.

  • This PR separates out validation of groups of settings,
  • A fail-fast validation is done early in the code to check each updated setting without resolving its dependencies,
  • A full validation is done later to check dependencies of each updated setting,
  • We've introduced a new API to validate dependencies of a setting and it is a no-op default method in the Validator<T> interface,
  • @s1monw would prefer to have a single API to implement in the Validator<T> interface without the no-op default interface method to validate dependencies of a setting.

We have three options:

  1. Leave things as they are now and rely on the updated Java documentation to tell developers there's a no-op method to override to validate dependencies of a setting.
  2. Refactor the Validator<T> interface to have a single validate(T value, Optional<Map<Setting<T>, T>> settings) API and delegate to developers the task to check whether or not the optional parameter is present or absent (it will always be absent when the early fail-fast first check is done).
  3. Refactor the Validator<T> interface to have a single validate(T value, Map<Setting<T>, T> settings) API and remove the early fail-fast first check so that the map parameter will always be present (not empty when a setting has dependencies or empty when a setting has no dependency).

I would prefer option 3, and you?

@cbismuth
Copy link
Contributor Author

We have a green build 👍

@cbismuth
Copy link
Contributor Author

cbismuth commented Nov 30, 2018

Hi, I know it's Wednesday and there're probably many things to close before the weekend.

I hope next week we will be able state whether or not this PR can be merged as it is (with latest documentation fix included) or should we go for a new iteration to simplify the Validator<T> interface.

The more I look at it, the more I think this version is a good one and we should merge it as it is.

@cbismuth
Copy link
Contributor Author

Hi @DaveCTurner, what do you think we should do on this PR? Thanks.

@DaveCTurner
Copy link
Contributor

Hi @cbismuth, just to let you know that this PR is still in my queue. Bear with me, I have a few too many things in flight right now.

@cbismuth
Copy link
Contributor Author

cbismuth commented Dec 12, 2018

No worries, thank you @DaveCTurner, I'll be available when needed.

Copy link
Contributor

@DaveCTurner DaveCTurner left a comment

Choose a reason for hiding this comment

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

I made some minor changes to the Javadocs in 02a62fb and merged a recent master in, and now this LGTM. Thanks for your patience @cbismuth.

@cbismuth
Copy link
Contributor Author

cbismuth commented Jan 7, 2019

That's great! Thanks a lot @DaveCTurner for finely reviewing the code and no worries at all for the time it took.

@DaveCTurner DaveCTurner merged commit 9602d79 into elastic:master Jan 7, 2019
DaveCTurner pushed a commit that referenced this pull request Jan 7, 2019
Today, a setting can declare that its validity depends on the values of other
related settings. However, the validity of a setting is not always checked
against the correct values of its dependent settings because those settings'
correct values may not be available when the validator runs.

This commit separates the validation of a settings updates into two phases,
with separate methods on the `Setting.Validator` interface. In the first phase
the setting's validity is checked in isolation, and in the second phase it is
checked again against the values of its related settings. Most settings only
use the first phase, and only the few settings with dependencies make use of
the second phase.
jasontedor added a commit to jasontedor/elasticsearch that referenced this pull request Jan 7, 2019
…piration

* elastic/master:
  Removing unused methods in Numbers (elastic#37186)
  Fix setting by time unit (elastic#37192)
  [DOCS] Cleans up xpackml attributes
  ML: fix delayed data annotations on secured cluster (elastic#37193)
  Update version in SearchRequest and related test
  [DOCS] Adds overview and API ref for cluster voting configurations (elastic#36954)
  ML: changing JobResultsProvider.getForecastRequestStats to support > 1 index (elastic#37157)
  Separate out validation of groups of settings (elastic#34184)
@cbismuth cbismuth deleted the 28309_disk_watermark_validation_rejects_apparently_valid_sequence_of_updates branch April 3, 2019 21:12
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.

7 participants