Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,45 @@

package org.elasticsearch.cluster.metadata;

import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.ResourceAlreadyExistsException;
import org.elasticsearch.action.ActionFuture;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.admin.indices.create.CreateIndexClusterStateUpdateRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.create.TransportCreateIndexAction;
import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
import org.elasticsearch.action.admin.indices.get.GetIndexResponse;
import org.elasticsearch.action.support.master.ShardsAcknowledgedResponse;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.ClusterStateListener;
import org.elasticsearch.cluster.ClusterStateUpdateTask;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.indices.InvalidIndexNameException;
import org.elasticsearch.test.ESIntegTestCase;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;

public class MetadataCreateIndexServiceIT extends ESIntegTestCase {

public void testRequestTemplateIsRespected() throws InterruptedException {
/*
* This test passes a template in the CreateIndexClusterStateUpdateRequest, and makes sure that the settings from that template
* are used when creating the index.
* This test passes a template in the CreateIndexClusterStateUpdateRequest, and makes sure that the settings
* from that template are used when creating the index.
*/
MetadataCreateIndexService metadataCreateIndexService = internalCluster().getCurrentMasterNodeInstance(
MetadataCreateIndexService.class
Expand Down Expand Up @@ -75,4 +93,132 @@ public void onFailure(Exception e) {
Settings settings = response.getSettings().get(indexName);
assertThat(settings.get("index.number_of_replicas"), equalTo(Integer.toString(numberOfReplicas)));
}

public void testCreateIndexBatching() throws Exception {
final var masterClusterService = internalCluster().getCurrentMasterNodeInstance(ClusterService.class);

final int totalRequestCount = randomIntBetween(1, 20);
final int validRequestCount = randomIntBetween(0, totalRequestCount);
final int invalidRequestCount = totalRequestCount - validRequestCount;

final var allRequestNames = new ArrayList<String>(totalRequestCount);
final var validIndicesNames = new LinkedHashSet<String>();
final var invalidSettingsNames = new LinkedHashSet<String>();

for (int i = 0; i < validRequestCount; i++) {
final var indexName = randomIndexName();
validIndicesNames.add(indexName);
allRequestNames.add(indexName);
}
// No collisions
assertThat(validIndicesNames.size(), equalTo(validRequestCount));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Technically this could fail, with very low probability, but it's a little unclear to the reader what's happening here. We want N distinct strings, so I'd suggest adding a random string to a set repeatedly until the set's size is as desired.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed the possible collision in 3a5295


int duplicateCount = 0;
int invalidNameCount = 0;
int invalidSettingsCount = 0;
for (int i = 0; i < invalidRequestCount; i++) {
int failureType = validIndicesNames.isEmpty() ? randomIntBetween(0, 1) : randomIntBetween(0, 2);
switch (failureType) {
case 0 -> {
allRequestNames.add("INVALID_" + randomAlphaOfLength(6).toLowerCase(Locale.ROOT));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe randomIdentifier("INVALID_")? Or even randomIdentifier("INVALID_BECAUSE_UPPER_CASE_")?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 3a5295

invalidNameCount++;
}
case 1 -> {
final var indexName = randomIndexName();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Likewise here, we need this not to be one of the valid index names. But it can match an invalid one since we won't be creating those indices.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 3a5295

invalidSettingsNames.add(indexName);
allRequestNames.add(indexName);
invalidSettingsCount++;
}
default -> {
allRequestNames.add(randomFrom(validIndicesNames));
duplicateCount++;
}
}
}
Collections.shuffle(allRequestNames, random());

final ClusterStateListener listener = event -> {
final var projectMetadata = event.state().metadata().getProject(ProjectId.DEFAULT);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh yes also any chance we can test batching across several projects? Not sure quite what that might entail for this test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So it's possible, see 23eabb.
That said, it does require overriding the project resolver to use TestOnlyMultiProjectResolver (instead of the DefaultProjectResolver), which isn't used in production. So we're exercising a somewhat artificial code path to test a future feature. It also requires a new gradle import (MP IT testing does not appear to be leveraged other tests in this package). We could consider splitting into two test classes (one for single-project and one for multi-project) to keep coverage for both or we could drop the multi-project testing for now and revisit when multi-project is closer to production-ready. Let me know your thoughts on this.
Side note, I have not yet managed to make the multi project setup work without disabling routing via routing.allocation.enable: none and using waitForActiveShards(NONE). Still looking into it though

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see ok, thanks for investigating so deeply! The need for the new Gradle import tells us that nothing else is testing in this way today and there's nothing particularly special about these tests in terms of multi-project support so let's back this out and leave it for the multi-project team to strengthen all these tests when the time is right.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sounds good to me, thanks!

if (projectMetadata == null) {
return;
}
final var createdInState = validIndicesNames.stream().filter(projectMetadata::hasIndex).toList();
assertThat(
"expected either none or all valid indices to appear atomically, but found " + createdInState.size(),
createdInState,
anyOf(hasSize(0), hasSize(validIndicesNames.size()))
);
for (final var indexName : invalidSettingsNames) {
assertFalse("invalid index [" + indexName + "] should never be created", projectMetadata.hasIndex(indexName));
}
};
masterClusterService.addListener(listener);
try {
final var barrier = new CyclicBarrier(2);
masterClusterService.submitUnbatchedStateUpdateTask("block", new ClusterStateUpdateTask() {
@Override
public ClusterState execute(ClusterState currentState) {
safeAwait(barrier);
safeAwait(barrier);
return currentState;
}

@Override
public void onFailure(Exception e) {
throw new AssertionError("blocking task failed", e);
}
});
safeAwait(barrier);

final var futures = new ArrayList<ActionFuture<CreateIndexResponse>>(totalRequestCount);
for (final var indexName : allRequestNames) {
final var request = new CreateIndexRequest(indexName);
if (invalidSettingsNames.contains(indexName)) {
request.settings(Settings.builder().put("index.version.created", 1));
}
futures.add(client().execute(TransportCreateIndexAction.TYPE, request));
}

assertTrue(
"create-index tasks should be queued behind the blocking task",
waitUntil(
() -> masterClusterService.getMasterService()
.pendingTasks()
.stream()
.filter(pct -> pct.getSource().toString().startsWith("create-index"))
.count() == totalRequestCount
)
);
final var initialState = masterClusterService.state();
safeAwait(barrier);

int successCount = 0;
int invalidNameExceptionCount = 0;
int alreadyExistsExceptionCount = 0;
int indexCreationExceptionCount = 0;
for (final var future : futures) {
try {
assertTrue(future.get(30, TimeUnit.SECONDS).isAcknowledged());
successCount++;
} catch (ExecutionException e) {
final var cause = ExceptionsHelper.unwrapCause(e.getCause());
switch (cause) {
case InvalidIndexNameException ignored -> invalidNameExceptionCount++;
case ResourceAlreadyExistsException ignored -> alreadyExistsExceptionCount++;
case IllegalArgumentException ignored -> indexCreationExceptionCount++;
case null, default -> throw new AssertionError("Unexpected failure when creating indices in batch", e);
}
}
}
assertThat(successCount, equalTo(validIndicesNames.size()));
assertThat(invalidNameExceptionCount, equalTo(invalidNameCount));
assertThat(alreadyExistsExceptionCount, equalTo(duplicateCount));
assertThat(indexCreationExceptionCount, equalTo(invalidSettingsCount));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rather than just checking the counts, could we check that each request gets the outcome we expect? You can use ActionTestUtils#assertNoFailureListener for the expected successes, and ActionTestUtils#assertNoSuccessListener for the expected failures. Then we can wait on all the responses with a single CountDownLatch rather than multiple Future.get() calls each of which might take 30s to time out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks a lot for the tip! Refactored in 3a5295

if (validIndicesNames.isEmpty()) {
assertSame("cluster state should not change when all requests failed", masterClusterService.state(), initialState);
}
} finally {
masterClusterService.removeListener(listener);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,6 @@ yield new DataStreamAutoShardingEvent(
projectState.cluster(),
createIndexClusterStateRequest,
silent,
RerouteBehavior.SKIP_REROUTE,
(builder, indexMetadata) -> {
downgradeBrokenTsdbBackingIndices(dataStream, builder);
builder.put(
Expand All @@ -435,6 +434,7 @@ yield new DataStreamAutoShardingEvent(
)
);
},
RerouteBehavior.SKIP_REROUTE,
rerouteCompletionIsNotRequired()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,8 @@ public static ClusterState createFailureStoreIndex(
currentState,
createIndexRequest,
false,
RerouteBehavior.SKIP_REROUTE,
metadataTransformer,
RerouteBehavior.SKIP_REROUTE,
AllocationActionListener.rerouteCompletionIsNotRequired()
);
} catch (ResourceAlreadyExistsException e) {
Expand Down
Loading
Loading