|
24 | 24 | import org.elasticsearch.cluster.routing.RerouteService; |
25 | 25 | import org.elasticsearch.cluster.service.ClusterService; |
26 | 26 | import org.elasticsearch.cluster.service.MasterServiceTaskQueue; |
| 27 | +import org.elasticsearch.common.Strings; |
27 | 28 | import org.elasticsearch.common.settings.ClusterSettings; |
28 | 29 | import org.elasticsearch.common.settings.Settings; |
29 | 30 | import org.elasticsearch.core.Releasable; |
|
41 | 42 | import org.mockito.ArgumentMatchers; |
42 | 43 |
|
43 | 44 | import java.io.IOException; |
| 45 | +import java.util.ArrayList; |
44 | 46 | import java.util.Collection; |
45 | 47 | import java.util.LinkedHashSet; |
46 | 48 | import java.util.List; |
|
60 | 62 | import static org.hamcrest.Matchers.is; |
61 | 63 | import static org.hamcrest.Matchers.not; |
62 | 64 | import static org.hamcrest.Matchers.notNullValue; |
| 65 | +import static org.hamcrest.Matchers.nullValue; |
63 | 66 | import static org.hamcrest.Matchers.sameInstance; |
64 | 67 | import static org.hamcrest.Matchers.startsWith; |
65 | 68 | import static org.mockito.ArgumentMatchers.any; |
66 | 69 | import static org.mockito.ArgumentMatchers.anyString; |
67 | 70 | import static org.mockito.Mockito.doNothing; |
68 | 71 | import static org.mockito.Mockito.doReturn; |
69 | 72 | import static org.mockito.Mockito.mock; |
| 73 | +import static org.mockito.Mockito.never; |
70 | 74 | import static org.mockito.Mockito.spy; |
71 | 75 | import static org.mockito.Mockito.times; |
72 | 76 | import static org.mockito.Mockito.verify; |
|
76 | 80 |
|
77 | 81 | public class ReservedClusterStateServiceTests extends ESTestCase { |
78 | 82 |
|
| 83 | + private static final String TEST_CHUNK_TEMPLATE = """ |
| 84 | + { |
| 85 | + "metadata": { |
| 86 | + "version": "%s", |
| 87 | + "compatibility": "8.4.0" |
| 88 | + }, |
| 89 | + "state": { |
| 90 | + "%s": { |
| 91 | + "nothing": "useful" |
| 92 | + } |
| 93 | + } |
| 94 | + } |
| 95 | + """; |
| 96 | + |
79 | 97 | @SuppressWarnings("unchecked") |
80 | 98 | private static <T extends ClusterStateTaskListener> MasterServiceTaskQueue<T> mockTaskQueue() { |
81 | 99 | return (MasterServiceTaskQueue<T>) mock(MasterServiceTaskQueue.class); |
@@ -349,6 +367,178 @@ public void success(Runnable onPublicationSuccess) { |
349 | 367 | verify(rerouteService, times(1)).reroute(anyString(), any(), any()); |
350 | 368 | } |
351 | 369 |
|
| 370 | + public void testUpdateErrorStateNonExistingProject() { |
| 371 | + ClusterService clusterService = mock(ClusterService.class); |
| 372 | + ClusterState state = ClusterState.builder(new ClusterName("test")).build(); |
| 373 | + when(clusterService.state()).thenReturn(state); |
| 374 | + |
| 375 | + ReservedClusterStateService service = new ReservedClusterStateService( |
| 376 | + clusterService, |
| 377 | + mock(RerouteService.class), |
| 378 | + List.of(), |
| 379 | + List.of() |
| 380 | + ); |
| 381 | + |
| 382 | + ErrorState error = new ErrorState( |
| 383 | + Optional.of(randomUniqueProjectId()), |
| 384 | + "namespace", |
| 385 | + 2L, |
| 386 | + ReservedStateVersionCheck.HIGHER_VERSION_ONLY, |
| 387 | + List.of("error"), |
| 388 | + ReservedStateErrorMetadata.ErrorKind.TRANSIENT |
| 389 | + ); |
| 390 | + service.updateErrorState(error); |
| 391 | + verify(clusterService, never()).createTaskQueue(any(), any(), any()); |
| 392 | + } |
| 393 | + |
| 394 | + public void testProcessMultipleChunks() throws Exception { |
| 395 | + ClusterService clusterService = mock(ClusterService.class); |
| 396 | + when(clusterService.createTaskQueue(any(), any(), any())).thenReturn(mockTaskQueue()); |
| 397 | + final ClusterName clusterName = new ClusterName("elasticsearch"); |
| 398 | + |
| 399 | + ClusterState state = ClusterState.builder(clusterName).build(); |
| 400 | + ProjectId projectId = randomProjectIdOrDefault(); |
| 401 | + state = setupProject(state, Optional.of(projectId)); |
| 402 | + when(clusterService.state()).thenReturn(state); |
| 403 | + |
| 404 | + AtomicReference<Exception> exceptionRef = new AtomicReference<>(); |
| 405 | + List<ReservedStateChunk> chunks = new ArrayList<>(); |
| 406 | + |
| 407 | + String[] randomStateKeys = generateRandomStringArray(randomIntBetween(5, 10), randomIntBetween(10, 15), false); |
| 408 | + |
| 409 | + List<ReservedClusterStateHandler<ProjectMetadata, ?>> projectHandlers = new ArrayList<>(); |
| 410 | + for (var key : randomStateKeys) { |
| 411 | + projectHandlers.add(spy(new TestProjectStateHandler(key))); |
| 412 | + } |
| 413 | + |
| 414 | + ReservedClusterStateService controller = new ReservedClusterStateService( |
| 415 | + clusterService, |
| 416 | + mock(RerouteService.class), |
| 417 | + List.of(), |
| 418 | + projectHandlers |
| 419 | + ); |
| 420 | + |
| 421 | + for (var testHandler : randomStateKeys) { |
| 422 | + String testChunkJSON = Strings.format(TEST_CHUNK_TEMPLATE, 1, testHandler); |
| 423 | + try ( |
| 424 | + XContentParser chunkParser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testChunkJSON); |
| 425 | + ) { |
| 426 | + chunks.add(controller.parse(projectId, "test", chunkParser)); |
| 427 | + } |
| 428 | + } |
| 429 | + |
| 430 | + controller.process( |
| 431 | + projectId, |
| 432 | + "test", |
| 433 | + chunks, |
| 434 | + randomFrom(ReservedStateVersionCheck.HIGHER_VERSION_ONLY, ReservedStateVersionCheck.HIGHER_OR_SAME_VERSION), |
| 435 | + exceptionRef::set |
| 436 | + ); |
| 437 | + |
| 438 | + assertThat(exceptionRef.get(), nullValue()); |
| 439 | + |
| 440 | + for (var projectHandler : projectHandlers) { |
| 441 | + verify(projectHandler, times(1)).transform(any(), any()); |
| 442 | + } |
| 443 | + } |
| 444 | + |
| 445 | + public void testProcessMultipleChunksVersionMismatch() throws Exception { |
| 446 | + ClusterService clusterService = mock(ClusterService.class); |
| 447 | + when(clusterService.createTaskQueue(any(), any(), any())).thenReturn(mockTaskQueue()); |
| 448 | + final ClusterName clusterName = new ClusterName("elasticsearch"); |
| 449 | + |
| 450 | + ClusterState state = ClusterState.builder(clusterName).build(); |
| 451 | + ProjectId projectId = randomProjectIdOrDefault(); |
| 452 | + state = setupProject(state, Optional.of(projectId)); |
| 453 | + when(clusterService.state()).thenReturn(state); |
| 454 | + |
| 455 | + String testJSON1 = Strings.format(TEST_CHUNK_TEMPLATE, 1, "test1"); |
| 456 | + String testJSON2 = Strings.format(TEST_CHUNK_TEMPLATE, 2, "test2"); |
| 457 | + |
| 458 | + AtomicReference<Exception> exceptionRef = new AtomicReference<>(); |
| 459 | + List<ReservedStateChunk> chunks = new ArrayList<>(); |
| 460 | + |
| 461 | + TestProjectStateHandler projectStateHandler1 = spy(new TestProjectStateHandler("test1")); |
| 462 | + TestProjectStateHandler projectStateHandler2 = spy(new TestProjectStateHandler("test2")); |
| 463 | + |
| 464 | + ReservedClusterStateService controller = new ReservedClusterStateService( |
| 465 | + clusterService, |
| 466 | + mock(RerouteService.class), |
| 467 | + List.of(), |
| 468 | + List.of(projectStateHandler1, projectStateHandler2) |
| 469 | + ); |
| 470 | + |
| 471 | + try ( |
| 472 | + XContentParser chunkParser1 = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON1); |
| 473 | + XContentParser chunkParser2 = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON2) |
| 474 | + ) { |
| 475 | + chunks.add(controller.parse(projectId, "test", chunkParser1)); |
| 476 | + chunks.add(controller.parse(projectId, "test", chunkParser2)); |
| 477 | + } |
| 478 | + |
| 479 | + controller.process( |
| 480 | + projectId, |
| 481 | + "test", |
| 482 | + chunks, |
| 483 | + randomFrom(ReservedStateVersionCheck.HIGHER_VERSION_ONLY, ReservedStateVersionCheck.HIGHER_OR_SAME_VERSION), |
| 484 | + exceptionRef::set |
| 485 | + ); |
| 486 | + |
| 487 | + assertThat(exceptionRef.get(), notNullValue()); |
| 488 | + assertThat(exceptionRef.get().getMessage(), containsString("Failed to merge reserved state chunks because of version mismatch: [")); |
| 489 | + verify(projectStateHandler1, times(0)).transform(any(), any()); |
| 490 | + verify(projectStateHandler2, times(0)).transform(any(), any()); |
| 491 | + } |
| 492 | + |
| 493 | + public void testProcessMultipleChunksDuplicateKeys() throws Exception { |
| 494 | + ClusterService clusterService = mock(ClusterService.class); |
| 495 | + when(clusterService.createTaskQueue(any(), any(), any())).thenReturn(mockTaskQueue()); |
| 496 | + final ClusterName clusterName = new ClusterName("elasticsearch"); |
| 497 | + |
| 498 | + ClusterState state = ClusterState.builder(clusterName).build(); |
| 499 | + ProjectId projectId = randomProjectIdOrDefault(); |
| 500 | + state = setupProject(state, Optional.of(projectId)); |
| 501 | + when(clusterService.state()).thenReturn(state); |
| 502 | + |
| 503 | + String testJSON1 = Strings.format(TEST_CHUNK_TEMPLATE, 1, "test"); |
| 504 | + String testJSON2 = Strings.format(TEST_CHUNK_TEMPLATE, 1, "test"); |
| 505 | + |
| 506 | + AtomicReference<Exception> exceptionRef = new AtomicReference<>(); |
| 507 | + List<ReservedStateChunk> chunks = new ArrayList<>(); |
| 508 | + |
| 509 | + TestProjectStateHandler projectStateHandler1 = spy(new TestProjectStateHandler("test")); |
| 510 | + |
| 511 | + ReservedClusterStateService controller = new ReservedClusterStateService( |
| 512 | + clusterService, |
| 513 | + mock(RerouteService.class), |
| 514 | + List.of(), |
| 515 | + List.of(projectStateHandler1) |
| 516 | + ); |
| 517 | + |
| 518 | + try ( |
| 519 | + XContentParser chunkParser1 = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON1); |
| 520 | + XContentParser chunkParser2 = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON2) |
| 521 | + ) { |
| 522 | + chunks.add(controller.parse(projectId, "test", chunkParser1)); |
| 523 | + chunks.add(controller.parse(projectId, "test", chunkParser2)); |
| 524 | + } |
| 525 | + |
| 526 | + controller.process( |
| 527 | + projectId, |
| 528 | + "test", |
| 529 | + chunks, |
| 530 | + randomFrom(ReservedStateVersionCheck.HIGHER_VERSION_ONLY, ReservedStateVersionCheck.HIGHER_OR_SAME_VERSION), |
| 531 | + exceptionRef::set |
| 532 | + ); |
| 533 | + |
| 534 | + assertThat(exceptionRef.get(), notNullValue()); |
| 535 | + assertThat( |
| 536 | + exceptionRef.get().getMessage(), |
| 537 | + containsString("Failed to merge reserved state chunks because of duplicate keys: [test]") |
| 538 | + ); |
| 539 | + verify(projectStateHandler1, times(0)).transform(any(), any()); |
| 540 | + } |
| 541 | + |
352 | 542 | public void testUpdateErrorState() { |
353 | 543 | ClusterService clusterService = mock(ClusterService.class); |
354 | 544 | ClusterState state = ClusterState.builder(new ClusterName("test")).build(); |
|
0 commit comments