Skip to content

Commit 62c62da

Browse files
Limit the Number of Snapshots in a BlobStoreRepository (#64461) (#64911)
Adds a limit to the maximum number of snapshots that are allowed to be added to a snapshot repository as a safety measure of last resort against repositories that grow to an unmanageable size due to e.g. incorrect SLM settings. Co-authored-by: David Turner <[email protected]>
1 parent 7bc7783 commit 62c62da

File tree

4 files changed

+91
-19
lines changed

4 files changed

+91
-19
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.repositories.blobstore;
21+
22+
import org.elasticsearch.action.ActionFuture;
23+
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
24+
import org.elasticsearch.common.settings.Settings;
25+
import org.elasticsearch.repositories.RepositoryException;
26+
import org.elasticsearch.repositories.fs.FsRepository;
27+
import org.elasticsearch.snapshots.AbstractSnapshotIntegTestCase;
28+
import org.hamcrest.Matchers;
29+
30+
import java.util.List;
31+
32+
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
33+
34+
public class BlobStoreSizeLimitIT extends AbstractSnapshotIntegTestCase {
35+
36+
public void testBlobStoreSizeIsLimited() throws Exception {
37+
final String repoName = "test-repo";
38+
final int maxSnapshots = randomIntBetween(1, 10);
39+
createRepository(repoName, FsRepository.TYPE, Settings.builder()
40+
.put(BlobStoreRepository.MAX_SNAPSHOTS_SETTING.getKey(), maxSnapshots).put("location", randomRepoPath()));
41+
final List<String> snapshotNames = createNSnapshots(repoName, maxSnapshots);
42+
final ActionFuture<CreateSnapshotResponse> failingSnapshotFuture = startFullSnapshot(repoName, "failing-snapshot");
43+
final RepositoryException repositoryException = expectThrows(RepositoryException.class, failingSnapshotFuture::actionGet);
44+
assertThat(repositoryException.getMessage(), Matchers.endsWith(
45+
"Cannot add another snapshot to this repository as it already contains [" + maxSnapshots +
46+
"] snapshots and is configured to hold up to [" + maxSnapshots + "] snapshots only."));
47+
assertEquals(repositoryException.repository(), repoName);
48+
assertAcked(startDeleteSnapshot(repoName, randomFrom(snapshotNames)).get());
49+
createFullSnapshot(repoName, "last-snapshot");
50+
}
51+
}

server/src/internalClusterTest/java/org/elasticsearch/snapshots/ConcurrentSnapshotsIT.java

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import org.elasticsearch.cluster.SnapshotDeletionsInProgress;
3535
import org.elasticsearch.cluster.SnapshotsInProgress;
3636
import org.elasticsearch.common.Strings;
37-
import org.elasticsearch.common.UUIDs;
3837
import org.elasticsearch.common.settings.Settings;
3938
import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException;
4039
import org.elasticsearch.discovery.AbstractDisruptionTestCase;
@@ -56,7 +55,6 @@
5655
import java.util.Arrays;
5756
import java.util.Collection;
5857
import java.util.List;
59-
import java.util.Locale;
6058
import java.util.concurrent.TimeUnit;
6159

6260
import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_REPLICAS;
@@ -1272,23 +1270,6 @@ private static void assertSnapshotStatusCountOnRepo(String otherBlockedRepoName,
12721270
assertThat(snapshotStatuses, hasSize(count));
12731271
}
12741272

1275-
private List<String> createNSnapshots(String repoName, int count) throws Exception {
1276-
final PlainActionFuture<Collection<CreateSnapshotResponse>> allSnapshotsDone = PlainActionFuture.newFuture();
1277-
final ActionListener<CreateSnapshotResponse> snapshotsListener = new GroupedActionListener<>(allSnapshotsDone, count);
1278-
final List<String> snapshotNames = new ArrayList<>(count);
1279-
final String prefix = "snap-" + UUIDs.randomBase64UUID(random()).toLowerCase(Locale.ROOT) + "-";
1280-
for (int i = 0; i < count; i++) {
1281-
final String snapshot = prefix + i;
1282-
snapshotNames.add(snapshot);
1283-
client().admin().cluster().prepareCreateSnapshot(repoName, snapshot).setWaitForCompletion(true).execute(snapshotsListener);
1284-
}
1285-
for (CreateSnapshotResponse snapshotResponse : allSnapshotsDone.get()) {
1286-
assertThat(snapshotResponse.getSnapshotInfo().state(), is(SnapshotState.SUCCESS));
1287-
}
1288-
logger.info("--> created {} in [{}]", snapshotNames, repoName);
1289-
return snapshotNames;
1290-
}
1291-
12921273
private ActionFuture<AcknowledgedResponse> startDeleteFromNonMasterClient(String repoName, String snapshotName) {
12931274
logger.info("--> deleting snapshot [{}] from repo [{}] from non master client", snapshotName, repoName);
12941275
return internalCluster().nonMasterClient().admin().cluster().prepareDeleteSnapshot(repoName, snapshotName).execute();

server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
218218
*/
219219
public static final Setting<Boolean> SUPPORT_URL_REPO = Setting.boolSetting("support_url_repo", true, Setting.Property.NodeScope);
220220

221+
/**
222+
* Setting that defines the maximum number of snapshots to which the repository may grow. Trying to create a snapshot into the
223+
* repository that would move it above this size will throw an exception.
224+
*/
225+
public static final Setting<Integer> MAX_SNAPSHOTS_SETTING =
226+
Setting.intSetting("max_number_of_snapshots", Integer.MAX_VALUE, 1, Setting.Property.NodeScope);
227+
221228
protected final boolean supportURLRepo;
222229

223230
private final boolean compress;
@@ -298,6 +305,11 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
298305
*/
299306
protected final int bufferSize;
300307

308+
/**
309+
* Maximum number of snapshots that this repository can hold.
310+
*/
311+
private final int maxSnapshotCount;
312+
301313
/**
302314
* Constructs new BlobStoreRepository
303315
* @param metadata The metadata for this repository including name and settings
@@ -323,6 +335,7 @@ protected BlobStoreRepository(
323335
readOnly = metadata.settings().getAsBoolean("readonly", false);
324336
cacheRepositoryData = CACHE_REPOSITORY_DATA.get(metadata.settings());
325337
bufferSize = Math.toIntExact(BUFFER_SIZE_SETTING.get(metadata.settings()).getBytes());
338+
this.maxSnapshotCount = MAX_SNAPSHOTS_SETTING.get(metadata.settings());
326339
}
327340

328341
@Override
@@ -1097,6 +1110,13 @@ public void finalizeSnapshot(final ShardGenerations shardGenerations,
10971110
final StepListener<RepositoryData> repoDataListener = new StepListener<>();
10981111
getRepositoryData(repoDataListener);
10991112
repoDataListener.whenComplete(existingRepositoryData -> {
1113+
final int existingSnapshotCount = existingRepositoryData.getSnapshotIds().size();
1114+
if (existingSnapshotCount >= maxSnapshotCount) {
1115+
listener.onFailure(new RepositoryException(metadata.name(), "Cannot add another snapshot to this repository as it " +
1116+
"already contains [" + existingSnapshotCount + "] snapshots and is configured to hold up to [" + maxSnapshotCount +
1117+
"] snapshots only."));
1118+
return;
1119+
}
11001120

11011121
final Map<IndexId, String> indexMetas;
11021122
final Map<String, String> indexMetaIdentifiers;

test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020

2121
import org.elasticsearch.Version;
2222
import org.elasticsearch.action.ActionFuture;
23+
import org.elasticsearch.action.ActionListener;
2324
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
2425
import org.elasticsearch.action.index.IndexRequestBuilder;
2526
import org.elasticsearch.action.search.SearchRequest;
27+
import org.elasticsearch.action.support.GroupedActionListener;
2628
import org.elasticsearch.action.support.PlainActionFuture;
2729
import org.elasticsearch.action.support.master.AcknowledgedResponse;
2830
import org.elasticsearch.cluster.ClusterState;
@@ -75,6 +77,7 @@
7577
import java.util.Collection;
7678
import java.util.Collections;
7779
import java.util.List;
80+
import java.util.Locale;
7881
import java.util.Map;
7982
import java.util.concurrent.TimeUnit;
8083
import java.util.concurrent.TimeoutException;
@@ -549,6 +552,23 @@ protected void awaitMasterFinishRepoOperations() throws Exception {
549552
});
550553
}
551554

555+
protected List<String> createNSnapshots(String repoName, int count) throws Exception {
556+
final PlainActionFuture<Collection<CreateSnapshotResponse>> allSnapshotsDone = PlainActionFuture.newFuture();
557+
final ActionListener<CreateSnapshotResponse> snapshotsListener = new GroupedActionListener<>(allSnapshotsDone, count);
558+
final List<String> snapshotNames = new ArrayList<>(count);
559+
final String prefix = "snap-" + UUIDs.randomBase64UUID(random()).toLowerCase(Locale.ROOT) + "-";
560+
for (int i = 0; i < count; i++) {
561+
final String snapshot = prefix + i;
562+
snapshotNames.add(snapshot);
563+
client().admin().cluster().prepareCreateSnapshot(repoName, snapshot).setWaitForCompletion(true).execute(snapshotsListener);
564+
}
565+
for (CreateSnapshotResponse snapshotResponse : allSnapshotsDone.get()) {
566+
assertThat(snapshotResponse.getSnapshotInfo().state(), is(SnapshotState.SUCCESS));
567+
}
568+
logger.info("--> created {} in [{}]", snapshotNames, repoName);
569+
return snapshotNames;
570+
}
571+
552572
public static void forEachFileRecursively(Path path,
553573
CheckedBiConsumer<Path, BasicFileAttributes, IOException> forEach) throws IOException {
554574
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {

0 commit comments

Comments
 (0)