|
22 | 22 | import com.azure.storage.blob.models.BlobProperties; |
23 | 23 | import com.azure.storage.blob.models.BlobStorageException; |
24 | 24 | import com.azure.storage.blob.specialized.BlockBlobClient; |
25 | | -import com.google.common.collect.AbstractIterator; |
26 | 25 | import com.google.common.collect.Iterators; |
27 | 26 | import java.io.IOException; |
28 | 27 | import java.io.InputStream; |
29 | 28 | import java.io.UncheckedIOException; |
30 | 29 | import java.time.OffsetDateTime; |
| 30 | +import java.util.ArrayList; |
31 | 31 | import java.util.Iterator; |
32 | 32 | import java.util.List; |
33 | 33 | import java.util.Map; |
34 | 34 | import java.util.Optional; |
35 | 35 | import java.util.Properties; |
36 | 36 | import java.util.concurrent.Callable; |
37 | 37 | import java.util.logging.Logger; |
| 38 | +import java.util.regex.Matcher; |
| 39 | +import java.util.regex.Pattern; |
38 | 40 | import java.util.stream.Collectors; |
39 | 41 | import java.util.stream.Stream; |
40 | 42 | import javax.annotation.Nullable; |
|
53 | 55 | import org.geowebcache.storage.StorageException; |
54 | 56 | import org.geowebcache.storage.TileObject; |
55 | 57 | import org.geowebcache.storage.TileRange; |
56 | | -import org.geowebcache.storage.TileRangeIterator; |
57 | 58 | import org.geowebcache.util.TMSKeyBuilder; |
58 | 59 | import org.springframework.http.HttpStatus; |
59 | 60 |
|
60 | 61 | public class AzureBlobStore implements BlobStore { |
61 | 62 |
|
62 | | - static Logger log = Logging.getLogger(AzureBlobStore.class.getName()); |
| 63 | + private static final Logger LOG = Logging.getLogger(AzureBlobStore.class.getName()); |
| 64 | + |
| 65 | + private static final Pattern TILE_BLOB_NAME_REGEXP = Pattern.compile("(?<z>\\d+)/(?<x>\\d+)/(?<y>\\d+)\\.\\w+$"); |
63 | 66 |
|
64 | 67 | private final TMSKeyBuilder keyBuilder; |
65 | 68 | private final BlobStoreListenerList listeners = new BlobStoreListenerList(); |
@@ -190,58 +193,105 @@ public boolean delete(TileRange tileRange) throws StorageException { |
190 | 193 | return false; |
191 | 194 | } |
192 | 195 |
|
193 | | - // open an iterator oer tile locations, to avoid memory accumulation |
194 | | - final Iterator<long[]> tileLocations = new AbstractIterator<>() { |
195 | | - |
196 | | - // TileRange iterator with 1x1 meta tiling factor |
197 | | - private TileRangeIterator trIter = new TileRangeIterator(tileRange, new int[] {1, 1}); |
198 | | - |
199 | | - @Override |
200 | | - protected long[] computeNext() { |
201 | | - long[] gridLoc = trIter.nextMetaGridLocation(new long[3]); |
202 | | - return gridLoc == null ? endOfData() : gridLoc; |
203 | | - } |
204 | | - }; |
| 196 | + List<BlobItem> blobsToDelete = findTileBlobsToDelete(tileRange, coordsPrefix); |
205 | 197 |
|
206 | | - // if no listeners, we don't need to gather extra tile info, use a dedicated fast path |
207 | 198 | if (listeners.isEmpty()) { |
208 | 199 | // if there are no listeners, don't bother requesting every tile |
209 | 200 | // metadata to notify the listeners |
210 | | - Iterator<String> keysIterator = Iterators.transform( |
211 | | - tileLocations, tl -> keyBuilder.forLocation(coordsPrefix, tl, tileRange.getMimeType())); |
212 | 201 | // split the iteration in parts to avoid memory accumulation |
213 | | - Iterator<List<String>> partition = Iterators.partition(keysIterator, DeleteManager.PAGE_SIZE); |
| 202 | + List<String> keysToDelete = |
| 203 | + blobsToDelete.stream().map(BlobItem::getName).collect(Collectors.toList()); |
| 204 | + |
| 205 | + Iterator<List<String>> partition = Iterators.partition(keysToDelete.iterator(), DeleteManager.PAGE_SIZE); |
214 | 206 |
|
215 | 207 | while (partition.hasNext() && !shutDown) { |
216 | | - List<String> locations = partition.next(); |
217 | | - deleteManager.deleteParallel(locations); |
| 208 | + deleteManager.deleteParallel(partition.next()); |
218 | 209 | } |
219 | 210 |
|
220 | 211 | } else { |
221 | 212 | // if we need to gather info, we'll end up just calling "delete" on each tile |
222 | 213 | // this is run here instead of inside the delete manager as we need high level info |
223 | 214 | // about tiles, e.g., TileObject, to inform the listeners |
224 | | - String layerName = tileRange.getLayerName(); |
225 | | - String gridSetId = tileRange.getGridSetId(); |
226 | | - String format = tileRange.getMimeType().getFormat(); |
227 | | - Map<String, String> parameters = tileRange.getParameters(); |
228 | | - |
229 | | - Iterator<Callable<?>> tilesIterator = Iterators.transform(tileLocations, xyz -> { |
230 | | - TileObject tile = TileObject.createQueryTileObject(layerName, xyz, gridSetId, format, parameters); |
231 | | - tile.setParametersId(tileRange.getParametersId()); |
232 | | - return (Callable<Object>) () -> delete(tile); |
233 | | - }); |
234 | | - Iterator<List<Callable<?>>> partition = Iterators.partition(tilesIterator, DeleteManager.PAGE_SIZE); |
235 | | - |
236 | | - // once a page of callables is ready, run them in parallel on the delete manager |
237 | | - while (partition.hasNext() && !shutDown) { |
238 | | - deleteManager.executeParallel(partition.next()); |
239 | | - } |
| 215 | + List<Callable<?>> tilesDeletions = blobsToDelete.stream() |
| 216 | + .map(blobItem -> { |
| 217 | + TileObject tile = createTileObject(blobItem, tileRange); |
| 218 | + tile.setParametersId(tileRange.getParametersId()); |
| 219 | + return (Callable<Object>) () -> delete(tile); |
| 220 | + }) |
| 221 | + .collect(Collectors.toList()); |
| 222 | + |
| 223 | + executeParallelDeletions(tilesDeletions); |
240 | 224 | } |
241 | 225 |
|
242 | 226 | return true; |
243 | 227 | } |
244 | 228 |
|
| 229 | + private List<BlobItem> findTileBlobsToDelete(TileRange tileRange, String coordsPrefix) { |
| 230 | + |
| 231 | + List<BlobItem> blobsToDelete = new ArrayList<>(); |
| 232 | + |
| 233 | + for (int zoom = tileRange.getZoomStart(); zoom <= tileRange.getZoomStop(); zoom++) { |
| 234 | + |
| 235 | + String zoomPrefix = coordsPrefix + "/" + zoom; |
| 236 | + |
| 237 | + if (!client.prefixExists(zoomPrefix)) { |
| 238 | + // empty level, skipping |
| 239 | + continue; |
| 240 | + } |
| 241 | + |
| 242 | + long[] rangeBoundsAtZoom = tileRange.rangeBounds(zoom); |
| 243 | + |
| 244 | + client.listBlobs(zoomPrefix) |
| 245 | + .filter(tb -> isTileBlobInBounds(tb, rangeBoundsAtZoom)) |
| 246 | + .forEach(blobsToDelete::add); |
| 247 | + } |
| 248 | + |
| 249 | + return blobsToDelete; |
| 250 | + } |
| 251 | + |
| 252 | + private boolean isTileBlobInBounds(BlobItem tileBlob, long[] bounds) { |
| 253 | + long minX = bounds[0]; |
| 254 | + long minY = bounds[1]; |
| 255 | + long maxX = bounds[2]; |
| 256 | + long maxY = bounds[3]; |
| 257 | + |
| 258 | + long[] index = extractTileIndex(tileBlob); |
| 259 | + long tileX = index[0]; |
| 260 | + long tileY = index[1]; |
| 261 | + |
| 262 | + return tileX >= minX && tileX <= maxX && tileY >= minY && tileY <= maxY; |
| 263 | + } |
| 264 | + |
| 265 | + private TileObject createTileObject(BlobItem blobItem, TileRange tileRange) { |
| 266 | + String layerName = tileRange.getLayerName(); |
| 267 | + String gridSetId = tileRange.getGridSetId(); |
| 268 | + String format = tileRange.getMimeType().getFormat(); |
| 269 | + Map<String, String> parameters = tileRange.getParameters(); |
| 270 | + return TileObject.createQueryTileObject(layerName, extractTileIndex(blobItem), gridSetId, format, parameters); |
| 271 | + } |
| 272 | + |
| 273 | + private long[] extractTileIndex(BlobItem blobItem) { |
| 274 | + Matcher matcher = TILE_BLOB_NAME_REGEXP.matcher(blobItem.getName()); |
| 275 | + |
| 276 | + if (!matcher.find()) { |
| 277 | + throw new IllegalArgumentException("Invalid tile blob name"); |
| 278 | + } |
| 279 | + |
| 280 | + return new long[] { |
| 281 | + Long.parseLong(matcher.group("x")), Long.parseLong(matcher.group("y")), Long.parseLong(matcher.group("z")) |
| 282 | + }; |
| 283 | + } |
| 284 | + |
| 285 | + private void executeParallelDeletions(List<Callable<?>> tilesDeletions) throws StorageException { |
| 286 | + Iterator<List<Callable<?>>> tilesDeletionsPartitions = |
| 287 | + Iterators.partition(tilesDeletions.iterator(), DeleteManager.PAGE_SIZE); |
| 288 | + |
| 289 | + // once a page of callables is ready, run them in parallel on the delete manager |
| 290 | + while (tilesDeletionsPartitions.hasNext() && !shutDown) { |
| 291 | + deleteManager.executeParallel(tilesDeletionsPartitions.next()); |
| 292 | + } |
| 293 | + } |
| 294 | + |
245 | 295 | @Override |
246 | 296 | public boolean get(TileObject obj) throws StorageException { |
247 | 297 | final String key = keyBuilder.forTile(obj); |
@@ -373,7 +423,7 @@ public boolean rename(String oldLayerName, String newLayerName) throws StorageEx |
373 | 423 | // revisit: this seems to hold true only for GeoServerTileLayer, "standalone" TileLayers |
374 | 424 | // return getName() from getId(), as in AbstractTileLayer. Unfortunately the only option |
375 | 425 | // for non-GeoServerTileLayers would be copy and delete. Expensive. |
376 | | - log.fine("No need to rename layers, AzureBlobStore uses layer id as key root"); |
| 426 | + LOG.fine("No need to rename layers, AzureBlobStore uses layer id as key root"); |
377 | 427 | if (client.prefixExists(oldLayerName)) { |
378 | 428 | listeners.sendLayerRenamed(oldLayerName, newLayerName); |
379 | 429 | } |
|
0 commit comments