Skip to content

Commit 7382629

Browse files
alanmcdadeaaime
authored andcommitted
[1.27.x backport] Limit number of S3 requests when truncating based on a tile range
Manual backport
1 parent f33057a commit 7382629

File tree

14 files changed

+865
-204
lines changed

14 files changed

+865
-204
lines changed

geowebcache/core/src/main/java/org/geowebcache/util/TMSKeyBuilder.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ public String layerId(String layerName) {
5959
return layer.getId();
6060
}
6161

62+
public String layerNameFromId(String layerId) {
63+
for (TileLayer tileLayer : layers.getLayerList()) {
64+
if (layerId.equals(tileLayer.getId())) {
65+
return tileLayer.getName();
66+
}
67+
}
68+
return null;
69+
}
70+
6271
public Set<String> layerGridsets(String layerName) {
6372
TileLayer layer;
6473
try {
@@ -222,4 +231,27 @@ private static String join(boolean closing, Object... elements) {
222231
}
223232
return joiner.toString();
224233
}
234+
235+
private static String parametersFromTileRange(TileRange obj) {
236+
String parametersId = obj.getParametersId();
237+
if (parametersId == null) {
238+
Map<String, String> parameters = obj.getParameters();
239+
parametersId = ParametersUtils.getId(parameters);
240+
if (parametersId == null) {
241+
parametersId = "default";
242+
} else {
243+
obj.setParametersId(parametersId);
244+
}
245+
}
246+
return parametersId;
247+
}
248+
249+
public String forZoomLevel(TileRange tileRange, int level) {
250+
String layerId = layerId(tileRange.getLayerName());
251+
String gridsetId = tileRange.getGridSetId();
252+
String format = tileRange.getMimeType().getFileExtension();
253+
String parametersId = parametersFromTileRange(tileRange);
254+
255+
return join(true, prefix, layerId, gridsetId, format, parametersId, String.valueOf(level));
256+
}
225257
}

geowebcache/s3storage/Readme.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Tidy up aws after working with tests
2+
===
3+
4+
```
5+
aws s3 ls s3://<bucket>/ | grep tmp_ | awk '{print $2}' | while read obj; do
6+
echo "Object: $obj"
7+
aws s3 rm s3://gwc-s3-test/$obj --recursive
8+
done
9+
</code>
10+
```
11+
12+
Replace the `<bucket>` with the value configured in your system.
13+
This will delete all the temporary object that have been created
14+
15+
16+
Config file
17+
====
18+
Add a `.gwc_s3_tests.properties` to your home directory to get the integration tests to run.
19+
20+
```
21+
cat .gwc_s3_tests.properties
22+
```
23+
_contents of file_
24+
25+
```
26+
bucket=gwc-s3-test
27+
secretKey=lxL*****************************
28+
accessKey=AK***************```
29+
30+
```

geowebcache/s3storage/src/main/java/org/geowebcache/s3/S3BlobStore.java

Lines changed: 90 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,22 @@
1313
*/
1414
package org.geowebcache.s3;
1515

16+
import static com.google.common.base.Preconditions.checkArgument;
1617
import static com.google.common.base.Preconditions.checkNotNull;
18+
import static java.lang.String.format;
1719
import static java.util.Objects.isNull;
1820

1921
import com.amazonaws.AmazonServiceException;
2022
import com.amazonaws.services.s3.AmazonS3Client;
2123
import com.amazonaws.services.s3.model.AccessControlList;
2224
import com.amazonaws.services.s3.model.BucketPolicy;
2325
import com.amazonaws.services.s3.model.CannedAccessControlList;
24-
import com.amazonaws.services.s3.model.DeleteObjectsRequest;
25-
import com.amazonaws.services.s3.model.DeleteObjectsRequest.KeyVersion;
2626
import com.amazonaws.services.s3.model.Grant;
2727
import com.amazonaws.services.s3.model.ObjectMetadata;
2828
import com.amazonaws.services.s3.model.PutObjectRequest;
2929
import com.amazonaws.services.s3.model.S3Object;
3030
import com.amazonaws.services.s3.model.S3ObjectInputStream;
3131
import com.amazonaws.services.s3.model.S3ObjectSummary;
32-
import com.google.common.base.Function;
33-
import com.google.common.collect.AbstractIterator;
34-
import com.google.common.collect.Iterators;
35-
import com.google.common.collect.Lists;
3632
import com.google.common.io.ByteStreams;
3733
import java.io.ByteArrayInputStream;
3834
import java.io.ByteArrayOutputStream;
@@ -41,7 +37,6 @@
4137
import java.nio.channels.WritableByteChannel;
4238
import java.util.ArrayList;
4339
import java.util.Arrays;
44-
import java.util.Iterator;
4540
import java.util.List;
4641
import java.util.Map;
4742
import java.util.Objects;
@@ -50,7 +45,10 @@
5045
import java.util.Set;
5146
import java.util.logging.Level;
5247
import java.util.logging.Logger;
48+
import java.util.regex.Matcher;
49+
import java.util.regex.Pattern;
5350
import java.util.stream.Collectors;
51+
import java.util.stream.IntStream;
5452
import javax.annotation.Nullable;
5553
import org.geotools.util.logging.Logging;
5654
import org.geowebcache.GeoWebCacheException;
@@ -61,14 +59,14 @@
6159
import org.geowebcache.locks.LockProvider;
6260
import org.geowebcache.mime.MimeException;
6361
import org.geowebcache.mime.MimeType;
62+
import org.geowebcache.s3.streams.TileDeletionListenerNotifier;
6463
import org.geowebcache.storage.BlobStore;
6564
import org.geowebcache.storage.BlobStoreListener;
6665
import org.geowebcache.storage.BlobStoreListenerList;
6766
import org.geowebcache.storage.CompositeBlobStore;
6867
import org.geowebcache.storage.StorageException;
6968
import org.geowebcache.storage.TileObject;
7069
import org.geowebcache.storage.TileRange;
71-
import org.geowebcache.storage.TileRangeIterator;
7270
import org.geowebcache.util.TMSKeyBuilder;
7371

7472
public class S3BlobStore implements BlobStore {
@@ -83,8 +81,6 @@ public class S3BlobStore implements BlobStore {
8381

8482
private String bucketName;
8583

86-
private volatile boolean shutDown;
87-
8884
private final S3Ops s3Ops;
8985

9086
private CannedAccessControlList acl;
@@ -100,7 +96,7 @@ public S3BlobStore(S3BlobStoreInfo config, TileLayerDispatcher layers, LockProvi
10096
conn = validateClient(config.buildClient(), bucketName);
10197
acl = config.getAccessControlList();
10298

103-
this.s3Ops = new S3Ops(conn, bucketName, keyBuilder, lockProvider);
99+
this.s3Ops = new S3Ops(conn, bucketName, keyBuilder, lockProvider, listeners);
104100

105101
boolean empty = !s3Ops.prefixExists(prefix);
106102
boolean existing = Objects.nonNull(s3Ops.getObjectMetadata(keyBuilder.storeMetadata()));
@@ -172,7 +168,6 @@ private void checkBucketPolicy(AmazonS3Client client, String bucketName) throws
172168

173169
@Override
174170
public void destroy() {
175-
this.shutDown = true;
176171
AmazonS3Client conn = this.conn;
177172
this.conn = null;
178173
if (conn != null) {
@@ -279,80 +274,40 @@ public boolean get(TileObject obj) throws StorageException {
279274
return true;
280275
}
281276

282-
private class TileToKey implements Function<long[], KeyVersion> {
283-
284-
private final String coordsPrefix;
285-
286-
private final String extension;
287-
288-
public TileToKey(String coordsPrefix, MimeType mimeType) {
289-
this.coordsPrefix = coordsPrefix;
290-
this.extension = mimeType.getInternalName();
291-
}
292-
293-
@Override
294-
public KeyVersion apply(long[] loc) {
295-
long z = loc[2];
296-
long x = loc[0];
297-
long y = loc[1];
298-
StringBuilder sb = new StringBuilder(coordsPrefix);
299-
sb.append(z).append('/').append(x).append('/').append(y).append('.').append(extension);
300-
return new KeyVersion(sb.toString());
301-
}
302-
}
303-
304277
@Override
305278
public boolean delete(final TileRange tileRange) throws StorageException {
279+
checkNotNull(tileRange, "tile range must not be null");
280+
checkArgument(tileRange.getZoomStart() >= 0, "zoom start must be greater or equal than zero");
281+
checkArgument(
282+
tileRange.getZoomStop() >= tileRange.getZoomStart(),
283+
"zoom stop must be greater or equal than start zoom");
306284

307285
final String coordsPrefix = keyBuilder.coordinatesPrefix(tileRange, true);
308286
if (!s3Ops.prefixExists(coordsPrefix)) {
309287
return false;
310288
}
311289

312-
final Iterator<long[]> tileLocations = new AbstractIterator<>() {
313-
314-
// TileRange iterator with 1x1 meta tiling factor
315-
private TileRangeIterator trIter = new TileRangeIterator(tileRange, new int[] {1, 1});
290+
// Create a prefix for each zoom level
291+
long count = IntStream.range(tileRange.getZoomStart(), tileRange.getZoomStop() + 1)
292+
.mapToObj(level -> scheduleDeleteForZoomLevel(tileRange, level))
293+
.filter(Objects::nonNull)
294+
.count();
316295

317-
@Override
318-
protected long[] computeNext() {
319-
long[] gridLoc = trIter.nextMetaGridLocation(new long[3]);
320-
return gridLoc == null ? endOfData() : gridLoc;
321-
}
322-
};
323-
324-
if (listeners.isEmpty()) {
325-
// if there are no listeners, don't bother requesting every tile
326-
// metadata to notify the listeners
327-
Iterator<List<long[]>> partition = Iterators.partition(tileLocations, 1000);
328-
final TileToKey tileToKey = new TileToKey(coordsPrefix, tileRange.getMimeType());
329-
330-
while (partition.hasNext() && !shutDown) {
331-
List<long[]> locations = partition.next();
332-
List<KeyVersion> keys = Lists.transform(locations, tileToKey);
333-
334-
DeleteObjectsRequest req = new DeleteObjectsRequest(bucketName);
335-
req.setQuiet(true);
336-
req.setKeys(keys);
337-
conn.deleteObjects(req);
338-
}
296+
// Check all ranges where scheduled
297+
return count == (tileRange.getZoomStop() - tileRange.getZoomStart() + 1);
298+
}
339299

340-
} else {
341-
long[] xyz;
342-
String layerName = tileRange.getLayerName();
343-
String gridSetId = tileRange.getGridSetId();
344-
String format = tileRange.getMimeType().getFormat();
345-
Map<String, String> parameters = tileRange.getParameters();
346-
347-
while (tileLocations.hasNext()) {
348-
xyz = tileLocations.next();
349-
TileObject tile = TileObject.createQueryTileObject(layerName, xyz, gridSetId, format, parameters);
350-
tile.setParametersId(tileRange.getParametersId());
351-
delete(tile);
352-
}
300+
private String scheduleDeleteForZoomLevel(TileRange tileRange, int level) {
301+
String zoomPath = keyBuilder.forZoomLevel(tileRange, level);
302+
Bounds bounds = new Bounds(tileRange.rangeBounds(level));
303+
String prefix = format("%s?%s", zoomPath, bounds);
304+
try {
305+
s3Ops.scheduleAsyncDelete(prefix);
306+
return prefix;
307+
} catch (GeoWebCacheException e) {
308+
log.warning("Cannot schedule delete for prefix " + prefix);
309+
return null;
353310
}
354-
355-
return true;
356311
}
357312

358313
@Override
@@ -457,8 +412,7 @@ private Properties getLayerMetadata(String layerName) {
457412
}
458413

459414
private void putParametersMetadata(String layerName, String parametersId, Map<String, String> parameters) {
460-
assert (isNull(parametersId) == isNull(parameters));
461-
if (isNull(parametersId)) {
415+
if (isNull(parameters)) {
462416
return;
463417
}
464418
Properties properties = new Properties();
@@ -519,4 +473,63 @@ public Map<String, Optional<Map<String, String>>> getParametersMapping(String la
519473
.map(props -> (Map<String, String>) (Map<?, ?>) props)
520474
.collect(Collectors.toMap(ParametersUtils::getId, Optional::of));
521475
}
476+
477+
public static class Bounds {
478+
private static final Pattern boundsRegex =
479+
Pattern.compile("^(?<prefix>.*/)\\?bounds=(?<minx>\\d+),(?<miny>\\d+),(?<maxx>\\d+),(?<maxy>\\d+)$");
480+
private final long minX, minY, maxX, maxY;
481+
482+
public Bounds(long[] bound) {
483+
minX = Math.min(bound[0], bound[2]);
484+
minY = Math.min(bound[1], bound[3]);
485+
maxX = Math.max(bound[0], bound[2]);
486+
maxY = Math.max(bound[1], bound[3]);
487+
}
488+
489+
public long getMinX() {
490+
return minX;
491+
}
492+
493+
public long getMaxX() {
494+
return maxX;
495+
}
496+
497+
static Optional<Bounds> createBounds(String prefix) {
498+
Matcher matcher = boundsRegex.matcher(prefix);
499+
if (!matcher.matches()) {
500+
return Optional.empty();
501+
}
502+
503+
Bounds bounds = new Bounds(new long[] {
504+
Long.parseLong(matcher.group("minx")),
505+
Long.parseLong(matcher.group("miny")),
506+
Long.parseLong(matcher.group("maxx")),
507+
Long.parseLong(matcher.group("maxy"))
508+
});
509+
return Optional.of(bounds);
510+
}
511+
512+
static String prefixWithoutBounds(String prefix) {
513+
Matcher matcher = boundsRegex.matcher(prefix);
514+
if (matcher.matches()) {
515+
return matcher.group("prefix");
516+
}
517+
return prefix;
518+
}
519+
520+
@Override
521+
public String toString() {
522+
return format("bounds=%d,%d,%d,%d", minX, minY, maxX, maxY);
523+
}
524+
525+
public boolean predicate(S3ObjectSummary s3ObjectSummary) {
526+
var matcher = TileDeletionListenerNotifier.keyRegex.matcher(s3ObjectSummary.getKey());
527+
if (!matcher.matches()) {
528+
return false;
529+
}
530+
long x = Long.parseLong(matcher.group(TileDeletionListenerNotifier.X_GROUP_POS));
531+
long y = Long.parseLong(matcher.group(TileDeletionListenerNotifier.Y_GROUP_POS));
532+
return x >= minX && x <= maxX && y >= minY && y <= maxY;
533+
}
534+
}
522535
}

geowebcache/s3storage/src/main/java/org/geowebcache/s3/S3BlobStoreInfo.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,8 @@ public AmazonS3Client buildClient() {
423423
clientConfig.setUseGzip(useGzip);
424424
}
425425
log.fine("Initializing AWS S3 connection");
426-
AmazonS3Client client = new AmazonS3Client(getCredentialsProvider(), clientConfig);
426+
AWSCredentialsProvider credentialsProvider = getCredentialsProvider();
427+
AmazonS3Client client = new AmazonS3Client(credentialsProvider, clientConfig);
427428
if (endpoint != null && !"".equals(endpoint)) {
428429
S3ClientOptions s3ClientOptions = new S3ClientOptions();
429430
s3ClientOptions.setPathStyleAccess(true);

0 commit comments

Comments
 (0)