-
Notifications
You must be signed in to change notification settings - Fork 24.9k
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 security index auto-create and state recovery race #39582
Changes from 6 commits
9c2e1de
4db7e68
99a1a05
ac82dd4
f734038
1cd74ec
6363365
b358996
ea90cc1
0851725
9146fdb
974fc47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,11 +45,13 @@ | |
|
||
import java.nio.charset.StandardCharsets; | ||
import java.util.HashSet; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
import java.util.concurrent.CopyOnWriteArrayList; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
import java.util.function.BiConsumer; | ||
import java.util.function.Consumer; | ||
import java.util.function.Predicate; | ||
|
@@ -105,6 +107,11 @@ public boolean indexExists() { | |
return this.indexState.indexExists; | ||
} | ||
|
||
public boolean isStateRecovered() { | ||
// as soon as state recovers we know the name of the .security index | ||
return this.indexState.concreteIndexName != null; | ||
} | ||
|
||
/** | ||
* Returns whether the index is on the current format if it exists. If the index does not exist | ||
* we treat the index as up to date as we expect it to be created with the current format. | ||
|
@@ -161,14 +168,17 @@ public void clusterChanged(ClusterChangedEvent event) { | |
final Version mappingVersion = oldestIndexMappingVersion(event.state()); | ||
final ClusterHealthStatus indexStatus = indexMetaData == null ? null : | ||
new ClusterIndexHealth(indexMetaData, event.state().getRoutingTable().index(indexMetaData.getIndex())).getStatus(); | ||
// index name non-null iff state recovered | ||
final String concreteIndexName = indexMetaData == null ? INTERNAL_SECURITY_INDEX : indexMetaData.getIndex().getName(); | ||
final State newState = new State(indexExists, isIndexUpToDate, indexAvailable, mappingIsUpToDate, mappingVersion, concreteIndexName, | ||
indexStatus); | ||
this.indexState = newState; | ||
|
||
if (newState.equals(previousState) == false) { | ||
for (BiConsumer<State, State> listener : stateChangeListeners) { | ||
listener.accept(previousState, newState); | ||
// point in time iterator | ||
final Iterator<BiConsumer<State, State>> stateListenerIterator = stateChangeListeners.iterator(); | ||
while (stateListenerIterator.hasNext()) { | ||
stateListenerIterator.next().accept(previousState, newState); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I can tell this change doesn't actually do anything. Assuming that's the problem you're trying to solve, then the options come down to:
(2) is probably the best option. I don't think we add listeners very often, so making a list copy each time is probably OK, but you'd need to audit the places we modify the list. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is a rewrite that unwraps the syntactic sugar because I find it confusing to use that when the list is concurrently modified. The list is already of the |
||
} | ||
} | ||
} | ||
|
@@ -281,7 +291,9 @@ private static Version readMappingVersion(String indexName, MappingMetaData mapp | |
*/ | ||
public void checkIndexVersionThenExecute(final Consumer<Exception> consumer, final Runnable andThen) { | ||
final State indexState = this.indexState; // use a local copy so all checks execute against the same state! | ||
if (indexState.indexExists && indexState.isIndexUpToDate == false) { | ||
if (false == isStateRecovered()) { | ||
delayUntilStateRecovered(consumer, () -> checkIndexVersionThenExecute(consumer, andThen)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This worries me. We don't do that now. We don't queue if the security index is red, we currently fail if the index is not recovered. From a usability point of view it's a nice idea, but it feels like we're jumping in without thinking it through. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, understood! I just wonder if there is some alternative for "delaying requests" that would be acceptable beyond feature freeze? |
||
} else if (indexState.indexExists && indexState.isIndexUpToDate == false) { | ||
consumer.accept(new IllegalStateException( | ||
"Security index is not on the current version. Security features relying on the index will not be available until " + | ||
"the upgrade API is run on the security index")); | ||
|
@@ -297,14 +309,17 @@ public void checkIndexVersionThenExecute(final Consumer<Exception> consumer, fin | |
public void prepareIndexIfNeededThenExecute(final Consumer<Exception> consumer, final Runnable andThen) { | ||
final State indexState = this.indexState; // use a local copy so all checks execute against the same state! | ||
// TODO we should improve this so we don't fire off a bunch of requests to do the same thing (create or update mappings) | ||
if (indexState.indexExists && indexState.isIndexUpToDate == false) { | ||
if (false == isStateRecovered()) { | ||
delayUntilStateRecovered(consumer, () -> prepareIndexIfNeededThenExecute(consumer, andThen)); | ||
} else if (indexState.indexExists && indexState.isIndexUpToDate == false) { | ||
consumer.accept(new IllegalStateException( | ||
"Security index is not on the current version. Security features relying on the index will not be available until " + | ||
"the upgrade API is run on the security index")); | ||
} else if (indexState.indexExists == false) { | ||
LOGGER.info("security index does not exist. Creating [{}] with alias [{}]", INTERNAL_SECURITY_INDEX, SECURITY_INDEX_NAME); | ||
assert INTERNAL_SECURITY_INDEX.equals(indexState.concreteIndexName); | ||
LOGGER.info("security index does not exist. Creating [{}] with alias [{}]", indexState.concreteIndexName, SECURITY_INDEX_NAME); | ||
Tuple<String, Settings> mappingAndSettings = loadMappingAndSettingsSourceFromTemplate(); | ||
CreateIndexRequest request = new CreateIndexRequest(INTERNAL_SECURITY_INDEX) | ||
CreateIndexRequest request = new CreateIndexRequest(indexState.concreteIndexName) | ||
.alias(new Alias(SECURITY_INDEX_NAME)) | ||
.mapping("doc", mappingAndSettings.v1(), XContentType.JSON) | ||
.waitForActiveShards(ActiveShardCount.ALL) | ||
|
@@ -373,6 +388,41 @@ public static boolean isIndexDeleted(State previousState, State currentState) { | |
return previousState.indexStatus != null && currentState.indexStatus == null; | ||
} | ||
|
||
/** | ||
* Delay the {@code runnable} invocation until cluster state recovered. | ||
*/ | ||
private void delayUntilStateRecovered(final Consumer<Exception> consumer, final Runnable runnable) { | ||
final AtomicBoolean done = new AtomicBoolean(false); | ||
// context preserving one-shot runnable | ||
final Runnable delayedRunnable = client.threadPool().getThreadContext().preserveContext(() -> { | ||
if (done.compareAndSet(false, true)) { | ||
runnable.run(); | ||
} | ||
}); | ||
final BiConsumer<State, State> gatewayRecoveryListener = new BiConsumer<State, State>() { | ||
@Override | ||
public void accept(State prevState, State newState) { | ||
assert isStateRecovered() : "State listener is notified for updates only after state recovered."; | ||
assert newState.concreteIndexName != null : "The newly applied state following a recovery should name the .security index"; | ||
if (newState.concreteIndexName != null) { | ||
stateChangeListeners.remove(this); | ||
client.threadPool().generic().execute(delayedRunnable); | ||
} else { | ||
// any cluster state update is an indication that the state recovered | ||
consumer.accept(new IllegalStateException("State has been recovered, but the security index name is unknown.")); | ||
} | ||
} | ||
}; | ||
// enqueue and wait for the first cluster state update | ||
stateChangeListeners.add(gatewayRecoveryListener); | ||
// maybe state recovered in the meantime since we last checked | ||
if (isStateRecovered()) { | ||
// state indeed recovered and we _might_ have lost the notification | ||
stateChangeListeners.remove(gatewayRecoveryListener); | ||
delayedRunnable.run(); | ||
} | ||
} | ||
|
||
/** | ||
* State of the security index. | ||
*/ | ||
|
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.
Normally, I would've opted for a new state flag
isStateRecovered
, but given the amount of flags already existing I thinkconcreteIndexName
is a fitting substitute because the index name is also semantically (not only practically) linked to the state recovery status - it's easy to reason that we can't name the security index until the state containing all the index names has been recovered.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 would prefer a new flag.
I think it's too easy to miss that the
concreteIndexName
variable has additional, non-obvious semantics, and change the default constructor to assignINTERNAL_SECURITY_INDEX
to that field.It's really random that I didn't do that when I introduced the index name field.
As the code stands, you hanging this protection off the fact that we
null
at construction,STATE_NOT_RECOVERED_BLOCK
I don't think you can be sure that we will never change this implementation to behave differently and break one of those pre-conditions.
If we're worried about the number of fields here, we can switch all the booleans to a
BitSet
, but I don't think it's actually necessary to re-use fields for a purpose other than what they were intended.