Repository navigation
fix: delete only cache tiles that can touch the previous footprint (MAPCO-12035) #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
41f2d65
fd9d42f
ca7b2e7
3e3924a
81cfe44
19879f7
4838942
7ea6969
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| import type { Logger } from '@map-colonies/js-logger'; | ||
| import { footprintToTileRanges } from '@map-colonies/mc-utils'; | ||
| import type { Feature, MultiPolygon, Polygon } from 'geojson'; | ||
| import { bbox, bboxPolygon, booleanIntersects } from '@turf/turf'; | ||
| import { degreesPerTile, footprintToTileRanges } from '@map-colonies/mc-utils'; | ||
| import type { BBox2d, ITileRange } from '@map-colonies/mc-utils'; | ||
| import type { ICreateJobBody, ICreateTaskBody } from '@map-colonies/mc-priority-queue'; | ||
| import { OperationStatus, TaskHandler as QueueClient } from '@map-colonies/mc-priority-queue'; | ||
| import type { CacheDeletionJobParams, LayerName } from '@map-colonies/raster-shared'; | ||
|
|
@@ -30,6 +33,37 @@ interface CacheDeletionStrategy { | |
| * - swap-update: wipes entire prefix (layer redirects to new tiles path) | ||
| * - update: deletes specific tile ranges over ingested footprint across all zooms | ||
| */ | ||
| const GRID_MIN_LON = -180; | ||
| const GRID_MIN_LAT = -90; | ||
|
|
||
| /** | ||
| * The tiles whose squares touch the bbox, bounds included: an edge lying exactly on a grid line also claims the | ||
| * tile beyond it on either side (ceil - 1 and floor agree everywhere else), as footprintToTileRanges does for | ||
| * footprints. Indices may fall outside the grid at the world's edge; they are only ever intersected with ranges | ||
| * that lie within it | ||
| */ | ||
| const tilesTouchingBbox = ([minLon, minLat, maxLon, maxLat]: BBox2d, zoom: number): ITileRange => { | ||
| const side = degreesPerTile(zoom); | ||
| return { | ||
| zoom, | ||
| minX: Math.ceil((minLon - GRID_MIN_LON) / side) - 1, | ||
| minY: Math.ceil((minLat - GRID_MIN_LAT) / side) - 1, | ||
|
Comment on lines
+49
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| maxX: Math.floor((maxLon - GRID_MIN_LON) / side), | ||
| maxY: Math.floor((maxLat - GRID_MIN_LAT) / side), | ||
| }; | ||
| }; | ||
|
|
||
| /** the ground a tile range covers */ | ||
| const tileRangeToPolygon = (range: ITileRange): Feature<Polygon> => { | ||
| const side = degreesPerTile(range.zoom); | ||
| return bboxPolygon([ | ||
| GRID_MIN_LON + range.minX * side, | ||
| GRID_MIN_LAT + range.minY * side, | ||
| GRID_MIN_LON + (range.maxX + 1) * side, | ||
| GRID_MIN_LAT + (range.maxY + 1) * side, | ||
| ]); | ||
| }; | ||
|
|
||
| @injectable() | ||
| export class CacheDeletionJobCreator { | ||
| private readonly taskConfig: CacheDeletionTaskConfig; | ||
|
|
@@ -141,11 +175,22 @@ export class CacheDeletionJobCreator { | |
|
|
||
| private async *buildRangeTasks(job: IngestionFinalizeJob, prefix: string, logger: Logger): AsyncGenerator<CacheDeletionTask> { | ||
| const { maxZoom, tileBatchSize, maxRangesPerTask } = this.taskConfig; | ||
| const { previousFootprint } = job.parameters.additionalParams; | ||
|
|
||
| const updateArea = await this.readProductGeometry(job.parameters.inputFiles.productShapefilePath); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think it is better to query the catalog for the current footprint. files might be removed at this point in time.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not quite, cache deletion job is creating right after a completed ingestion update job which basically mean files won't be deleted, |
||
| logger.info({ | ||
| msg: 'Computing tile ranges over the updated footprint', | ||
| maxZoom, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its true for the update operations, not for cache deletion, cache may store zoom level up to 21 by our definition (upscale) that we have to delete. |
||
| tileBatchSize, | ||
| maxRangesPerTask, | ||
| restrictedToPreviousFootprint: previousFootprint !== undefined, | ||
| }); | ||
|
|
||
| const geometry = await this.readProductGeometry(job.parameters.inputFiles.productShapefilePath); | ||
| logger.info({ msg: 'Computing tile ranges over the updated footprint', maxZoom, tileBatchSize, maxRangesPerTask }); | ||
|
|
||
| const ranges = footprintToTileRanges(geometry, { minZoom: 0, maxZoom }); | ||
| // without the previous footprint (jobs created before init recorded it) every tile of the update area is covered | ||
| const ranges = | ||
| previousFootprint === undefined | ||
| ? footprintToTileRanges(updateArea, { minZoom: 0, maxZoom }) | ||
| : this.rangesTouchingPreviousFootprint(updateArea, previousFootprint, maxZoom, logger); | ||
|
|
||
| for await (const batch of limitedTileBatchGenerator(tileBatchSize, maxRangesPerTask, ranges)) { | ||
| yield { | ||
|
|
@@ -156,6 +201,44 @@ export class CacheDeletionJobCreator { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Per zoom, clips the update ranges to the tile box of the previous bbox and keeps those whose ground touches the | ||
| * previous footprint; stops at the first zoom with no shared tile, since deeper zooms cannot share any either. | ||
| */ | ||
| private *rangesTouchingPreviousFootprint( | ||
| updateArea: Polygon | MultiPolygon, | ||
| previousFootprint: Polygon | MultiPolygon, | ||
| maxZoom: number, | ||
| logger: Logger | ||
| ): Generator<ITileRange> { | ||
| const [minLon, minLat, maxLon, maxLat] = bbox(previousFootprint); | ||
| const previousBbox: BBox2d = [minLon, minLat, maxLon, maxLat]; | ||
|
Comment on lines
+214
to
+215
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. destructure constants are unused. simply assign |
||
|
|
||
| for (let zoom = 0; zoom <= maxZoom; zoom++) { | ||
| const previous = tilesTouchingBbox(previousBbox, zoom); | ||
| let shared = false; | ||
|
|
||
| for (const range of footprintToTileRanges(updateArea, { minZoom: zoom, maxZoom: zoom })) { | ||
| const clipped: ITileRange = { | ||
| zoom, | ||
| minX: Math.max(range.minX, previous.minX), | ||
| minY: Math.max(range.minY, previous.minY), | ||
| maxX: Math.min(range.maxX, previous.maxX), | ||
| maxY: Math.min(range.maxY, previous.maxY), | ||
| }; | ||
| if (clipped.minX <= clipped.maxX && clipped.minY <= clipped.maxY && booleanIntersects(tileRangeToPolygon(clipped), previousFootprint)) { | ||
| shared = true; | ||
| yield clipped; | ||
| } | ||
| } | ||
|
|
||
| if (!shared) { | ||
|
Comment on lines
+217
to
+235
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there are cases in which this algorithm is potentially too much eager, it depends on the outcome from |
||
| logger.info({ msg: 'Update area stops sharing tiles with the previous footprint, ending the range walk', lastZoom: zoom - 1, maxZoom }); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private async createJobWithStreamedTasks( | ||
| job: IngestionFinalizeJob, | ||
| jobType: string, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ import { | |
| taskBlockDuplicationParamSchema, | ||
| ingestionBaseJobParamsSchema, | ||
| ingestionUpdateJobParamsSchema, | ||
| multiPolygonSchema, | ||
| polygonSchema, | ||
| } from '@map-colonies/raster-shared'; | ||
| import { z } from 'zod'; | ||
| import { Grid } from '../../../common/interfaces'; | ||
|
|
@@ -39,7 +41,14 @@ export const internalIdSchema = z.object({ | |
| }); | ||
|
|
||
| //#region AdditionalParams | ||
| export const updateFinalizeAdditionalParamsSchema = updateAdditionalParamsSchema; | ||
| /** the layer's catalog footprint as it stood before the update, recorded by the update job's init step */ | ||
| export const previousFootprintSchema = polygonSchema.or(multiPolygonSchema); | ||
| export type PreviousFootprint = z.infer<typeof previousFootprintSchema>; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| export const updateFinalizeAdditionalParamsSchema = updateAdditionalParamsSchema.extend({ | ||
| // optional so jobs created before overseer recorded it still finalize; cache deletion then covers the whole update area | ||
| previousFootprint: previousFootprintSchema.optional(), | ||
| }); | ||
|
|
||
| export const swapUpdateFinalizeAdditionalParamsSchema = updateFinalizeAdditionalParamsSchema.extend({ | ||
| displayPath: updateAdditionalParamsSchema.shape.displayPath.optional(), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are the ranges here follow tms or wmts (y axis direction)?