Skip to content

Commit 7b647fd

Browse files
alan-geosolutionsalanmcdadegroldanpeter-afrigisaaime
authored
Optimize S3 cache truncation
* On delete tile cache workflows minimize the opertunity for 404 to be returned when deleting tile cash. Use list-objects and delete-objects batch method to operate on 1000 sized batches where possible. Ensure that tile caches are informed of changes through listeners Fix Integration tests that exercise delete file paths * On delete tile cache workflows minimize the opertunity for 404 to be returned when deleting tile cash. Use list-objects and delete-objects batch method to operate on 1000 sized batches where possible. Ensure that tile caches are informed of changes through listeners Fix Integration tests that exercise delete file paths * On delete tile cache workflows minimize the opertunity for 404 to be returned when deleting tile cash. Use list-objects and delete-objects batch method to operate on 1000 sized batches where possible. Ensure that tile caches are informed of changes through listeners Fix Integration tests that exercise delete file paths * Fixed multiple calls to listener. Added simplest bounded delete. Added test for bounded delete. Added test to check if TileDeleted events are received when a layer is deleted. There is a race in this test, so it can pass even though tileDeleted is sent. * Fixed multiple calls to listener. Added simplest bounded delete. Added test for bounded delete. Added test to check if TileDeleted events are received when a layer is deleted. There is a race in this test, so it can pass even though tileDeleted is sent. * Restored check in putParametersMetadata, tests have been refactored to supply correct data. Added Copyright to BoundedS3KeySupplier Removed redundant test from integration tests Added an await to the AbstractBlobStoreTest as testDeleteRangeSingleLevel was failing with an aborted BulkDelete without it * Retained 1.26.0 config for compatibility testing * Updated version to 1.27-SNAPSHOT * Fix gt.version = 33-SNAPSHOT-SNAPSHOT/33-SNAPSHOT, there is a problem with the release script * Updated release notes for 1.28-SNAPSHOT * Updated version to 1.28-SNAPSHOT * Fix missing and extra spaces in log messages * Update Ubuntu to 22.04, 20.04 is not longer supported * Revert formatting on AbstractBlobStoreTest * Use a FakeListener rather than a mock Downgrade log messages to warnings as they only have a big impact in tests. The Delete will run again later. Disable testTruncateRespectsLevels in windows environment. * Removed global windows test skip in AbstractS3BlobStoreIntegrationTest * Disable all window S3Blobstore tests * Disable all window S3Blobstore tests --------- Co-authored-by: Alan McDade <[email protected]> Co-authored-by: groldan <[email protected]> Co-authored-by: Gabriel Roldan <[email protected]> Co-authored-by: Peter Smythe <[email protected]> Co-authored-by: Andrea Aime <[email protected]>
1 parent 00e1df1 commit 7b647fd

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)