Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -12,6 +12,7 @@
import org.apache.http.client.methods.HttpGet;
import org.elasticsearch.action.ActionFuture;
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
import org.elasticsearch.action.admin.cluster.snapshots.get.After;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
import org.elasticsearch.action.admin.cluster.snapshots.get.SnapshotSortKey;
Expand Down Expand Up @@ -386,7 +387,7 @@ private static void assertStablePagination(String repoName, Collection<String> a
final GetSnapshotsResponse getSnapshotsResponse = sortedWithLimit(
repoName,
sort,
sort.encodeAfterQueryParam(after),
After.fromSnapshotInfo(after, sort).toQueryParam(),
i,
order,
includeIndexNames
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
import org.elasticsearch.action.admin.cluster.snapshots.create.TransportCreateSnapshotAction;
import org.elasticsearch.action.admin.cluster.snapshots.get.After;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequestBuilder;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
Expand Down Expand Up @@ -826,7 +827,7 @@ private static void assertStablePagination(String[] repoNames, Collection<String
final GetSnapshotsResponse getSnapshotsResponse = sortedWithLimit(
repoNames,
sort,
sort.encodeAfterQueryParam(after),
After.fromSnapshotInfo(after, sort).toQueryParam(),
i,
order
);
Expand Down Expand Up @@ -1144,7 +1145,7 @@ public void testAllFeatures() {
.sort(sortKey)
.order(order)
.size(nextSize)
.after(SnapshotSortKey.decodeAfterQueryParam(nextRequestAfter))
.after(After.decodeAfterQueryParam(nextRequestAfter))
.states(requestedStates);
final GetSnapshotsResponse nextResponse = safeAwait(l -> client().execute(TransportGetSnapshotsAction.TYPE, nextRequest, l));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.action.admin.cluster.snapshots.get;

import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.snapshots.SnapshotInfo;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
* Cursor value for the {@code ?after} pagination parameter of the get-snapshots API. Encodes the sort key value plus snapshot and
* repository names for tiebreaking. Can be decoded from a query param and passed in a {@link GetSnapshotsRequest}, and used by
* {@link AfterPredicates} to filter out snapshots that are not after this cursor.
*/
public record After(String value, String repoName, String snapshotName) implements Writeable {

public After(StreamInput in) throws IOException {
this(in.readString(), in.readString(), in.readString());
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(value);
out.writeString(repoName);
out.writeString(snapshotName);
}

/**
* Build an {@link After} cursor from the last snapshot in a page, for use when building the {@code ?after} value for the next request.
*
* @param snapshotInfo the snapshot (typically the last in the current page)
* @param sortBy the sort key used for the request
* @return the cursor
*/
public static After fromSnapshotInfo(SnapshotInfo snapshotInfo, SnapshotSortKey sortBy) {
return new After(switch (sortBy) {
case START_TIME -> Long.toString(snapshotInfo.startTime());
case NAME -> snapshotInfo.snapshotId().getName();
case DURATION -> Long.toString(snapshotInfo.endTime() - snapshotInfo.startTime());
case INDICES -> Integer.toString(snapshotInfo.indices().size());
case SHARDS -> Integer.toString(snapshotInfo.totalShards());
case FAILED_SHARDS -> Integer.toString(snapshotInfo.failedShards());
case REPOSITORY -> snapshotInfo.repository();
}, snapshotInfo.repository(), snapshotInfo.snapshotId().getName());
}

/**
* Encode this cursor as the {@code ?after} query parameter value (base64url-encoded "value,repoName,snapshotName").
*
* @return the encoded string for use as the {@code after} query parameter
*/
public String toQueryParam() {
return Base64.getUrlEncoder().encodeToString((value + "," + repoName + "," + snapshotName).getBytes(StandardCharsets.UTF_8));
}

/**
* Decode the {@code ?after} query parameter into an {@link After} instance.
*
* @param param the value of the {@code after} query parameter (base64url-encoded "value,repoName,snapshotName")
* @return the decoded cursor
* @throws IllegalArgumentException if the parameter format is invalid
*/
public static After decodeAfterQueryParam(String param) {
final String[] parts = new String(Base64.getUrlDecoder().decode(param), StandardCharsets.UTF_8).split(",");
if (parts.length != 3) {
throw new IllegalArgumentException("invalid ?after parameter [" + param + "]");
}
return new After(parts[0], parts[1], parts[2]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.action.admin.cluster.snapshots.get;

import org.elasticsearch.core.Nullable;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.snapshots.SnapshotInfo;

import java.util.function.Predicate;
import java.util.function.ToLongFunction;

/**
* Predicate for the {@code ?after} filter of the get-snapshots action. The {@link #test(SnapshotInfo)} predicate is applied to
* {@link SnapshotInfo} instances to filter out those that sort before the cursor value (i.e. were returned on earlier pages of results).
*/
final class AfterPredicates {

private static final AfterPredicates MATCH_ALL = new AfterPredicates(null);

@Nullable // if MATCH_ALL
private final Predicate<SnapshotInfo> snapshotPredicate;

private AfterPredicates(@Nullable Predicate<SnapshotInfo> snapshotPredicate) {
this.snapshotPredicate = snapshotPredicate;
}

/**
* Test using the full {@link SnapshotInfo}. Returns true if the snapshot should be included (sorts after the cursor).
*/
boolean test(SnapshotInfo snapshotInfo) {
return snapshotPredicate == null || snapshotPredicate.test(snapshotInfo);
}

static AfterPredicates forAfter(@Nullable After after, SnapshotSortKey sortBy, SortOrder order) {
if (after == null) {
return MATCH_ALL;
}

return new AfterPredicates(switch (sortBy) {
case START_TIME -> longValuePredicate(after, SnapshotInfo::startTime, order);
case NAME -> namePredicate(after, order);
case DURATION -> longValuePredicate(after, info -> info.endTime() - info.startTime(), order);
case INDICES -> longValuePredicate(after, info -> info.indices().size(), order);
case SHARDS -> longValuePredicate(after, SnapshotInfo::totalShards, order);
case FAILED_SHARDS -> longValuePredicate(after, SnapshotInfo::failedShards, order);
case REPOSITORY -> repositoryPredicate(after, order);
});
}

private static Predicate<SnapshotInfo> longValuePredicate(After after, ToLongFunction<SnapshotInfo> extractor, SortOrder sortOrder) {
final long afterVal = Long.parseLong(after.value());
final String snapshotName = after.snapshotName();
final String repoName = after.repoName();
return sortOrder == SortOrder.ASC ? info -> {
final long val = extractor.applyAsLong(info);
return afterVal < val || (afterVal == val && compareName(snapshotName, repoName, info) < 0);
} : info -> {
final long val = extractor.applyAsLong(info);
return afterVal > val || (afterVal == val && compareName(snapshotName, repoName, info) > 0);
};
}

private static Predicate<SnapshotInfo> namePredicate(After after, SortOrder sortOrder) {
final String snapshotName = after.snapshotName();
final String repoName = after.repoName();
return sortOrder == SortOrder.ASC
? (info -> compareName(snapshotName, repoName, info) < 0)
: (info -> compareName(snapshotName, repoName, info) > 0);
}

private static Predicate<SnapshotInfo> repositoryPredicate(After after, SortOrder sortOrder) {
final String snapshotName = after.snapshotName();
final String repoName = after.repoName();
return sortOrder == SortOrder.ASC
? (info -> compareRepositoryName(snapshotName, repoName, info) < 0)
: (info -> compareRepositoryName(snapshotName, repoName, info) > 0);
}

private static int compareName(String name, String repoName, SnapshotInfo info) {
final int res = name.compareTo(info.snapshotId().getName());
if (res != 0) {
return res;
}
return repoName.compareTo(info.repository());
}

private static int compareRepositoryName(String name, String repoName, SnapshotInfo info) {
final int res = repoName.compareTo(info.repository());
if (res != 0) {
return res;
}
return name.compareTo(info.snapshotId().getName());
}
}
Loading