|
10 | 10 |
|
11 | 11 | import org.elasticsearch.action.ActionFuture;
|
12 | 12 | import org.elasticsearch.action.ActionRequestValidationException;
|
| 13 | +import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; |
| 14 | +import org.elasticsearch.action.admin.cluster.repositories.put.TransportPutRepositoryAction; |
| 15 | +import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; |
13 | 16 | import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
|
| 17 | +import org.elasticsearch.action.admin.cluster.snapshots.create.TransportCreateSnapshotAction; |
14 | 18 | import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
|
15 | 19 | import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequestBuilder;
|
16 | 20 | import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
|
17 | 21 | import org.elasticsearch.action.admin.cluster.snapshots.get.SnapshotSortKey;
|
| 22 | +import org.elasticsearch.action.admin.cluster.snapshots.get.TransportGetSnapshotsAction; |
| 23 | +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; |
| 24 | +import org.elasticsearch.action.admin.indices.create.TransportCreateIndexAction; |
| 25 | +import org.elasticsearch.action.support.RefCountingListener; |
18 | 26 | import org.elasticsearch.cluster.SnapshotsInProgress;
|
| 27 | +import org.elasticsearch.common.Strings; |
19 | 28 | import org.elasticsearch.common.settings.Settings;
|
| 29 | +import org.elasticsearch.core.Predicates; |
20 | 30 | import org.elasticsearch.repositories.RepositoryMissingException;
|
| 31 | +import org.elasticsearch.repositories.fs.FsRepository; |
21 | 32 | import org.elasticsearch.search.sort.SortOrder;
|
| 33 | +import org.elasticsearch.test.ESTestCase; |
| 34 | +import org.elasticsearch.test.hamcrest.ElasticsearchAssertions; |
22 | 35 | import org.elasticsearch.threadpool.ThreadPool;
|
23 | 36 |
|
24 | 37 | import java.nio.file.Path;
|
25 | 38 | import java.util.ArrayList;
|
26 | 39 | import java.util.Collection;
|
| 40 | +import java.util.Collections; |
27 | 41 | import java.util.HashSet;
|
28 | 42 | import java.util.List;
|
29 | 43 | import java.util.Map;
|
| 44 | +import java.util.Objects; |
30 | 45 | import java.util.Set;
|
| 46 | +import java.util.function.Predicate; |
| 47 | +import java.util.stream.Collectors; |
31 | 48 |
|
32 | 49 | import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
|
33 | 50 | import static org.hamcrest.Matchers.empty;
|
@@ -745,4 +762,242 @@ private static GetSnapshotsRequestBuilder baseGetSnapshotsRequest(String[] repoN
|
745 | 762 | return clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoNames)
|
746 | 763 | .setSnapshots("*", "-" + AbstractSnapshotIntegTestCase.OLD_VERSION_SNAPSHOT_PREFIX + "*");
|
747 | 764 | }
|
| 765 | + |
| 766 | + public void testAllFeatures() { |
| 767 | + // A test that uses (potentially) as many of the features of the get-snapshots API at once as possible, to verify that they interact |
| 768 | + // in the expected order etc. |
| 769 | + |
| 770 | + // Create a few repositories and a few indices |
| 771 | + final var repositories = randomList(1, 4, ESTestCase::randomIdentifier); |
| 772 | + final var indices = randomList(1, 4, ESTestCase::randomIdentifier); |
| 773 | + final var slmPolicies = randomList(1, 4, ESTestCase::randomIdentifier); |
| 774 | + |
| 775 | + safeAwait(l -> { |
| 776 | + try (var listeners = new RefCountingListener(l.map(v -> null))) { |
| 777 | + for (final var repository : repositories) { |
| 778 | + client().execute( |
| 779 | + TransportPutRepositoryAction.TYPE, |
| 780 | + new PutRepositoryRequest(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, repository).type(FsRepository.TYPE) |
| 781 | + .settings(Settings.builder().put("location", randomRepoPath()).build()), |
| 782 | + listeners.acquire(ElasticsearchAssertions::assertAcked) |
| 783 | + ); |
| 784 | + } |
| 785 | + |
| 786 | + for (final var index : indices) { |
| 787 | + client().execute( |
| 788 | + TransportCreateIndexAction.TYPE, |
| 789 | + new CreateIndexRequest(index, indexSettings(1, 0).build()), |
| 790 | + listeners.acquire(ElasticsearchAssertions::assertAcked) |
| 791 | + ); |
| 792 | + } |
| 793 | + } |
| 794 | + }); |
| 795 | + ensureGreen(); |
| 796 | + |
| 797 | + // Create a few snapshots |
| 798 | + final var snapshotInfos = Collections.synchronizedList(new ArrayList<SnapshotInfo>()); |
| 799 | + safeAwait(l -> { |
| 800 | + try (var listeners = new RefCountingListener(l.map(v -> null))) { |
| 801 | + for (int i = 0; i < 10; i++) { |
| 802 | + client().execute( |
| 803 | + TransportCreateSnapshotAction.TYPE, |
| 804 | + new CreateSnapshotRequest( |
| 805 | + TEST_REQUEST_TIMEOUT, |
| 806 | + // at least one snapshot per repository to satisfy consistency checks |
| 807 | + i < repositories.size() ? repositories.get(i) : randomFrom(repositories), |
| 808 | + randomIdentifier() |
| 809 | + ).indices(randomNonEmptySubsetOf(indices)) |
| 810 | + .userMetadata( |
| 811 | + randomBoolean() ? Map.of() : Map.of(SnapshotsService.POLICY_ID_METADATA_FIELD, randomFrom(slmPolicies)) |
| 812 | + ) |
| 813 | + .waitForCompletion(true), |
| 814 | + listeners.acquire( |
| 815 | + createSnapshotResponse -> snapshotInfos.add(Objects.requireNonNull(createSnapshotResponse.getSnapshotInfo())) |
| 816 | + ) |
| 817 | + ); |
| 818 | + } |
| 819 | + } |
| 820 | + }); |
| 821 | + |
| 822 | + Predicate<SnapshotInfo> snapshotInfoPredicate = Predicates.always(); |
| 823 | + |
| 824 | + // {repository} path parameter |
| 825 | + final String[] requestedRepositories; |
| 826 | + if (randomBoolean()) { |
| 827 | + requestedRepositories = new String[] { randomFrom("_all", "*") }; |
| 828 | + } else { |
| 829 | + final var selectedRepositories = Set.copyOf(randomNonEmptySubsetOf(repositories)); |
| 830 | + snapshotInfoPredicate = snapshotInfoPredicate.and(si -> selectedRepositories.contains(si.repository())); |
| 831 | + requestedRepositories = selectedRepositories.toArray(new String[0]); |
| 832 | + } |
| 833 | + |
| 834 | + // {snapshot} path parameter |
| 835 | + final String[] requestedSnapshots; |
| 836 | + if (randomBoolean()) { |
| 837 | + requestedSnapshots = randomBoolean() ? Strings.EMPTY_ARRAY : new String[] { randomFrom("_all", "*") }; |
| 838 | + } else { |
| 839 | + final var selectedSnapshots = randomNonEmptySubsetOf(snapshotInfos).stream() |
| 840 | + .map(si -> si.snapshotId().getName()) |
| 841 | + .collect(Collectors.toSet()); |
| 842 | + snapshotInfoPredicate = snapshotInfoPredicate.and(si -> selectedSnapshots.contains(si.snapshotId().getName())); |
| 843 | + requestedSnapshots = selectedSnapshots.stream() |
| 844 | + // if we have multiple repositories, add a trailing wildcard to each requested snapshot name, because if we specify exact |
| 845 | + // names then there must be a snapshot with that name in every requested repository |
| 846 | + .map(n -> repositories.size() == 1 && randomBoolean() ? n : n + "*") |
| 847 | + .toArray(String[]::new); |
| 848 | + } |
| 849 | + |
| 850 | + // ?slm_policy_filter parameter |
| 851 | + final String[] requestedSlmPolicies; |
| 852 | + switch (between(0, 3)) { |
| 853 | + default -> requestedSlmPolicies = Strings.EMPTY_ARRAY; |
| 854 | + case 1 -> { |
| 855 | + requestedSlmPolicies = new String[] { "*" }; |
| 856 | + snapshotInfoPredicate = snapshotInfoPredicate.and( |
| 857 | + si -> si.userMetadata().get(SnapshotsService.POLICY_ID_METADATA_FIELD) != null |
| 858 | + ); |
| 859 | + } |
| 860 | + case 2 -> { |
| 861 | + requestedSlmPolicies = new String[] { "_none" }; |
| 862 | + snapshotInfoPredicate = snapshotInfoPredicate.and( |
| 863 | + si -> si.userMetadata().get(SnapshotsService.POLICY_ID_METADATA_FIELD) == null |
| 864 | + ); |
| 865 | + } |
| 866 | + case 3 -> { |
| 867 | + final var selectedPolicies = Set.copyOf(randomNonEmptySubsetOf(slmPolicies)); |
| 868 | + requestedSlmPolicies = selectedPolicies.stream() |
| 869 | + .map(policy -> randomBoolean() ? policy : policy + "*") |
| 870 | + .toArray(String[]::new); |
| 871 | + snapshotInfoPredicate = snapshotInfoPredicate.and( |
| 872 | + si -> si.userMetadata().get(SnapshotsService.POLICY_ID_METADATA_FIELD) instanceof String policy |
| 873 | + && selectedPolicies.contains(policy) |
| 874 | + ); |
| 875 | + } |
| 876 | + } |
| 877 | + |
| 878 | + // ?sort and ?order parameters |
| 879 | + final var sortKey = randomFrom(SnapshotSortKey.values()); |
| 880 | + final var order = randomFrom(SortOrder.values()); |
| 881 | + // NB we sometimes choose to sort by FAILED_SHARDS, but there are no failed shards in these snapshots. We're still testing the |
| 882 | + // fallback sorting by snapshot ID in this case. We also have no multi-shard indices so there's no difference between sorting by |
| 883 | + // INDICES and by SHARDS. The actual sorting behaviour for these cases is tested elsewhere, here we're just checking that sorting |
| 884 | + // interacts correctly with the other parameters to the API. |
| 885 | + |
| 886 | + // compute the ordered sequence of snapshots which match the repository/snapshot name filters and SLM policy filter |
| 887 | + final var selectedSnapshots = snapshotInfos.stream() |
| 888 | + .filter(snapshotInfoPredicate) |
| 889 | + .sorted(sortKey.getSnapshotInfoComparator(order)) |
| 890 | + .toList(); |
| 891 | + |
| 892 | + final var getSnapshotsRequest = new GetSnapshotsRequest(TEST_REQUEST_TIMEOUT, requestedRepositories, requestedSnapshots).policies( |
| 893 | + requestedSlmPolicies |
| 894 | + ) |
| 895 | + // apply sorting params |
| 896 | + .sort(sortKey) |
| 897 | + .order(order); |
| 898 | + |
| 899 | + // sometimes use ?from_sort_value to skip some items; note that snapshots skipped in this way are subtracted from |
| 900 | + // GetSnapshotsResponse.totalCount whereas snapshots skipped by ?after and ?offset are not |
| 901 | + final int skippedByFromSortValue; |
| 902 | + if (randomBoolean()) { |
| 903 | + final var startingSnapshot = randomFrom(snapshotInfos); |
| 904 | + getSnapshotsRequest.fromSortValue(switch (sortKey) { |
| 905 | + case START_TIME -> Long.toString(startingSnapshot.startTime()); |
| 906 | + case NAME -> startingSnapshot.snapshotId().getName(); |
| 907 | + case DURATION -> Long.toString(startingSnapshot.endTime() - startingSnapshot.startTime()); |
| 908 | + case INDICES, SHARDS -> Integer.toString(startingSnapshot.indices().size()); |
| 909 | + case FAILED_SHARDS -> "0"; |
| 910 | + case REPOSITORY -> startingSnapshot.repository(); |
| 911 | + }); |
| 912 | + final Predicate<SnapshotInfo> fromSortValuePredicate = snapshotInfo -> { |
| 913 | + final var comparison = switch (sortKey) { |
| 914 | + case START_TIME -> Long.compare(snapshotInfo.startTime(), startingSnapshot.startTime()); |
| 915 | + case NAME -> snapshotInfo.snapshotId().getName().compareTo(startingSnapshot.snapshotId().getName()); |
| 916 | + case DURATION -> Long.compare( |
| 917 | + snapshotInfo.endTime() - snapshotInfo.startTime(), |
| 918 | + startingSnapshot.endTime() - startingSnapshot.startTime() |
| 919 | + ); |
| 920 | + case INDICES, SHARDS -> Integer.compare(snapshotInfo.indices().size(), startingSnapshot.indices().size()); |
| 921 | + case FAILED_SHARDS -> 0; |
| 922 | + case REPOSITORY -> snapshotInfo.repository().compareTo(startingSnapshot.repository()); |
| 923 | + }; |
| 924 | + return order == SortOrder.ASC ? comparison < 0 : comparison > 0; |
| 925 | + }; |
| 926 | + |
| 927 | + int skipCount = 0; |
| 928 | + for (final var snapshotInfo : selectedSnapshots) { |
| 929 | + if (fromSortValuePredicate.test(snapshotInfo)) { |
| 930 | + skipCount += 1; |
| 931 | + } else { |
| 932 | + break; |
| 933 | + } |
| 934 | + } |
| 935 | + skippedByFromSortValue = skipCount; |
| 936 | + } else { |
| 937 | + skippedByFromSortValue = 0; |
| 938 | + } |
| 939 | + |
| 940 | + // ?offset parameter |
| 941 | + if (randomBoolean()) { |
| 942 | + getSnapshotsRequest.offset(between(0, selectedSnapshots.size() + 1)); |
| 943 | + } |
| 944 | + |
| 945 | + // ?size parameter |
| 946 | + if (randomBoolean()) { |
| 947 | + getSnapshotsRequest.size(between(1, selectedSnapshots.size() + 1)); |
| 948 | + } |
| 949 | + |
| 950 | + // compute the expected offset and size of the returned snapshots as indices in selectedSnapshots: |
| 951 | + final var expectedOffset = Math.min(selectedSnapshots.size(), skippedByFromSortValue + getSnapshotsRequest.offset()); |
| 952 | + final var expectedSize = Math.min( |
| 953 | + selectedSnapshots.size() - expectedOffset, |
| 954 | + getSnapshotsRequest.size() == GetSnapshotsRequest.NO_LIMIT ? Integer.MAX_VALUE : getSnapshotsRequest.size() |
| 955 | + ); |
| 956 | + |
| 957 | + // get the actual response |
| 958 | + final GetSnapshotsResponse getSnapshotsResponse = safeAwait( |
| 959 | + l -> client().execute(TransportGetSnapshotsAction.TYPE, getSnapshotsRequest, l) |
| 960 | + ); |
| 961 | + |
| 962 | + // verify it returns the expected results |
| 963 | + assertEquals( |
| 964 | + selectedSnapshots.stream().skip(expectedOffset).limit(expectedSize).map(SnapshotInfo::snapshotId).toList(), |
| 965 | + getSnapshotsResponse.getSnapshots().stream().map(SnapshotInfo::snapshotId).toList() |
| 966 | + ); |
| 967 | + assertEquals(expectedSize, getSnapshotsResponse.getSnapshots().size()); |
| 968 | + assertEquals(selectedSnapshots.size() - skippedByFromSortValue, getSnapshotsResponse.totalCount()); |
| 969 | + assertEquals(selectedSnapshots.size() - expectedOffset - expectedSize, getSnapshotsResponse.remaining()); |
| 970 | + assertEquals(getSnapshotsResponse.remaining() > 0, getSnapshotsResponse.next() != null); |
| 971 | + |
| 972 | + // now use ?after to page through the rest of the results |
| 973 | + var nextRequestAfter = getSnapshotsResponse.next(); |
| 974 | + var nextExpectedOffset = expectedOffset + expectedSize; |
| 975 | + var remaining = getSnapshotsResponse.remaining(); |
| 976 | + while (nextRequestAfter != null) { |
| 977 | + final var nextSize = between(1, remaining); |
| 978 | + final var nextRequest = new GetSnapshotsRequest(TEST_REQUEST_TIMEOUT, requestedRepositories, requestedSnapshots) |
| 979 | + // same name/policy filters, same ?sort and ?order params, new ?size, but no ?offset or ?from_sort_value because of ?after |
| 980 | + .policies(requestedSlmPolicies) |
| 981 | + .sort(sortKey) |
| 982 | + .order(order) |
| 983 | + .size(nextSize) |
| 984 | + .after(SnapshotSortKey.decodeAfterQueryParam(nextRequestAfter)); |
| 985 | + final GetSnapshotsResponse nextResponse = safeAwait(l -> client().execute(TransportGetSnapshotsAction.TYPE, nextRequest, l)); |
| 986 | + |
| 987 | + assertEquals( |
| 988 | + selectedSnapshots.stream().skip(nextExpectedOffset).limit(nextSize).map(SnapshotInfo::snapshotId).toList(), |
| 989 | + nextResponse.getSnapshots().stream().map(SnapshotInfo::snapshotId).toList() |
| 990 | + ); |
| 991 | + assertEquals(nextSize, nextResponse.getSnapshots().size()); |
| 992 | + assertEquals(selectedSnapshots.size(), nextResponse.totalCount()); |
| 993 | + assertEquals(remaining - nextSize, nextResponse.remaining()); |
| 994 | + assertEquals(nextResponse.remaining() > 0, nextResponse.next() != null); |
| 995 | + |
| 996 | + nextRequestAfter = nextResponse.next(); |
| 997 | + nextExpectedOffset += nextSize; |
| 998 | + remaining -= nextSize; |
| 999 | + } |
| 1000 | + |
| 1001 | + assertEquals(0, remaining); |
| 1002 | + } |
748 | 1003 | }
|
0 commit comments