-
Notifications
You must be signed in to change notification settings - Fork 25.3k
Reserved cluster state service #88527
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
Pinging @elastic/es-core-infra (Team:Core/Infra) |
* @param errorListener a listener function that will be called with every unwrapped throwable we find | ||
* @param limit after how many encountered throwables should we stop unwrapping. Prevents stack overflows. 10 is reasonable max. | ||
*/ | ||
public static void unwrap(Throwable t, Consumer<Throwable> errorListener, int limit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this helper because I couldn't find a suitable one here that would return each throwable message. When we parse JSON, the messages of errors are reported on each throwable as it goes down in subsections of the JSON body.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good. I left some thoughts.
* @param errorListener a listener function that will be called with every unwrapped throwable we find | ||
* @param limit after how many encountered throwables should we stop unwrapping. Prevents stack overflows. 10 is reasonable max. | ||
*/ | ||
public static void unwrap(Throwable t, Consumer<Throwable> errorListener, int limit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are already a lot of random unwrap/tostring type helpers here. Could we avoid adding another?
This seems to be a helper for iterating through exception causes. Yet the calls I see later in this PR only print the message of the causes, not the stack trace. If we are going to preserve the exception chain, then preserving the entire stacktrace seems better, especially since the exception will only be consumed in exceptional cases by cloud (misconfiguration), or through the Elasticsearch log (plugin coding error for packaged entities). If that is the case, we already have a method for printing the stack trace of a Throwable to a String in ExceptionsHelper.
* @param handlerNames Names of handlers found in the {@link ReservedStateChunk} | ||
* @return | ||
*/ | ||
static LinkedHashSet<String> orderedStateHandlers(Map<String, ReservedClusterStateHandler<?>> handlers, Set<String> handlerNames) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be the only method exposed on this class, and it is static. Can this method be moved to the ReservedClusterStateController, which looks like the only caller?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I split it off so that I can reduce the size of the controller file. I can bring it back.
* <p> | ||
* This class contains the logic about validation, ordering and applying of | ||
* the cluster state specified in a file or through plugins/modules. Reserved cluster state | ||
* cannot be modified throught the REST APIs, only through this controller class. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: throught -> through
import java.util.Map; | ||
|
||
/** | ||
* Tuple class containing the cluster state to be saved and reserved and the version info |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a tuple? Maybe say "A holder for ..."?
} | ||
|
||
public void testHandlerOrdering() { | ||
ReservedClusterStateHandler<Map<String, Object>> oh1 = new ReservedClusterStateHandler<>() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe create a helper method to dynamically create these (taking name + varargs of deps) so the ordering tests can be much smaller?
|
||
@Override | ||
public void clusterStatePublished(ClusterState newClusterState) { | ||
logger.info("Wrote new error state in immutable metadata"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you anticipate this info message being useful in debugging? (also, immutable -> reserved cluster state?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's useful to know that we've recorded the error state in the cluster state. Updating the state can fail here, maybe we are not the master anymore, or ES terminated before we got to write the error state. By seeing the message in the logs, we definitely know we wrote what's wrong with the file settings. Thanks for catching the wording bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thoughts are centered on:
- it's an info level message. If it's important, and it's related to an error, shouldn't it be at least warn if not error level?
- It doesn't have any supporting information. The message alone doesn't convey anything useful, other than "something happened". If a user is looking through their logs and sees this, is that useful to them? Can they do anything about it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's a valid point, it's not useful to the end user as is the confirmation of new cluster state being set. I'll change this to a debug message instead, so we can use it while working on the feature.
* the cluster state specified in a file or through plugins/modules. Reserved cluster state | ||
* cannot be modified throught the REST APIs, only through this controller class. | ||
*/ | ||
public class ReservedClusterStateController { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we've typically used the "Service" suffix for other similar "things" in a node, any reason not to use that here in place of Controller?
try { | ||
Set<String> existingKeys = keysForHandler(existingMetadata, handlerName); | ||
TransformState transformState = handler.transform(reservedState.get(handlerName), new TransformState(state, existingKeys)); | ||
state = transformState.state(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each handler is create it's own ClusterState. Can we do this more efficiently? Building cluster state is not cheap. Could we instead create a ClusterState.Builder here (or perhaps just the customs and metadata builders), and that is passed to the transform method? I think the TransformState would not be needed, the method could just return the new set of keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't fully use ClusterState.Builder without more significant refactor of how the TransportActions we are wrapping for this work. All of the transport actions we use for the reserved state handlers take cluster state in and return a new state back. However, I really like the idea of using the builder instead of the TransformState. This way we can remove that TransformState class, we can start using the builder and then we can slowly improve the transport actions to maybe work with a builder in the future, to improve efficiency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I said that too quickly, I think using the builder at the moment without refactoring all of the transport actions will be more overhead. I can out transform state and use Tuple, but then I sprinkle those v1(), v2() everywhere instead of the named variables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ok with waiting on this optimization, but I do think it is something we should look into. Creating cluster state is relatively expensive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll follow-up with a PR to optimize this after I check that it's OK to refactor the Transport node actions to use a builder.
|
||
throw new ReservedStateVersion.IncompatibleVersionException( | ||
format( | ||
"Not updating error state because version [%s] is less or equal to the last state error version [%s]", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this an error? Isn't this part of the idempotent nature of the reserved state design?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this because of what might potentially happen when plugins/modules are using the controller. If there are bugs in how they call the controller and they repeatedly send the same erroneous content to be reserved, we'll be spamming the cluster with error cluster state updates. We simply check if the version metadata of the new erroneous content has previously been seen, and if we have seen it, we log an error in the logs, but don't send cluster state update.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about if the same operator settings file has an update triggered, but for the same version? Wouldn't this cause an internal error, when it should be a no-op?
Separate from this PR, I think we should consider how plugins can use this functionality without directly calling the reserved state service. As you point out, it can be error prone, so perhaps we can redesign the pluggable portion to declaratively specify their resources, and the server is actually what does the registration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point, I throw a specific exception here that would be caught and handled differently by the caller of the service, but it's not ideal, it should be a no-op and a log message only. I'll fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separate from this PR, I think we should consider how plugins can use this functionality without directly calling the reserved state service. As you point out, it can be error prone, so perhaps we can redesign the pluggable portion to declaratively specify their resources, and the server is actually what does the registration.
I like this idea, perhaps though SPI we can ask all plugins if they want to declare any state chunks that need to be applied and we call the update service internally. This way no plugins will have access to the update service and we'll apply it using our rules.
PARSER.declareString(ConstructingObjectParser.constructorArg(), COMPATIBILITY); | ||
} | ||
|
||
public static ReservedStateVersion parse(XContentParser parser, Void v) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the implementation specific Void here could be hidden from the signature? Remove from the method and pass null to PARSER?
Thanks again for the review @rjernst, I removed that extra exception and changed such that we return the original cluster state reference to avoid unnecessary update. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
) | ||
logger.warn( | ||
() -> format( | ||
"Cluster state version [%s] is not compatible with this Elasticsearch node", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this should mention the reserved state name?
@elasticmachine update branch |
* upstream/master: (2974 commits) Reserved cluster state service (elastic#88527) Add transport action immutable state checks (elastic#88491) Remove suggest flag from index stats docs (elastic#85479) Polling cluster formation state for master-is-stable health indicator (elastic#88397) Add test execution guide in yamlRestTest asciidoc (elastic#88490) Add troubleshooting guide for corrupt repository (elastic#88391) [Transform] Finetune Schedule to be less noisy on retry and retry slower (elastic#88531) Updatable API keys - auto-update legacy RDs (elastic#88514) Fix typo in TransportForceMergeAction and TransportClearIndicesCacheA… (elastic#88064) Fixed NullPointerException on bulk request (elastic#88358) Avoid needless index metadata builders during reroute (elastic#88506) Set metadata on request in API key noop test (elastic#88507) Fix passing positional args to ES in Docker (elastic#88502) Improve description for task api detailed param (elastic#88493) Support cartesian shape with doc values (elastic#88487) Promote usage of Subjects in Authentication class (elastic#88494) Add CCx 2.0 feature flag (elastic#88451) Reword the watcher 'always' and 'never' condition docs (elastic#86105) Simplify azure discovery installation docs (elastic#88404) Breakup FIPS CI testing jobs ... # Conflicts: # server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java # x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java
This PR adds the reserved cluster state controller, which part of the File based settings PR (#86224).
This is a reusable component that has number of Reserved State Handler components that know how to update parts of the Elasticsearch state. This component is only used by the File Settings Service at the moment, but the intent is for modules and plugins to be able to call it to save settings and entities in the cluster state that cannot be changed by the end user. For example work on using this component to update state from plugins/modules, please see #88341
Relates to #86224.