-
Notifications
You must be signed in to change notification settings - Fork 26k
Batch index creation #144074
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
Batch index creation #144074
Changes from 11 commits
0010211
3cc912d
afad4a3
bcdfc8c
4fddbce
0e8d371
04e9043
da1a6a2
0a237dc
e23f2f1
cf274f7
3a52952
23eabb5
8a60cc3
5a74bc6
d1e63eb
79a14af
216bebe
4128e56
1eef9fe
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 |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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)); | ||
|
|
||
| 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)); | ||
|
Member
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. Maybe
Contributor
Author
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. Done in 3a5295 |
||
| invalidNameCount++; | ||
| } | ||
| case 1 -> { | ||
| final var indexName = randomIndexName(); | ||
|
Member
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. 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.
Contributor
Author
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. 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); | ||
|
Member
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. Oh yes also any chance we can test batching across several projects? Not sure quite what that might entail for this test.
Contributor
Author
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. So it's possible, see 23eabb.
Member
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. 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.
Contributor
Author
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. 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)); | ||
|
Member
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. Rather than just checking the counts, could we check that each request gets the outcome we expect? You can use
Contributor
Author
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. 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); | ||
| } | ||
| } | ||
| } | ||
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.
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.
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.
Addressed the possible collision in 3a5295