-
Notifications
You must be signed in to change notification settings - Fork 0
Add precompute tables for uploads and pending posts #3
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
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
e58906f
added upload tiling
clragon 18b2f43
added pending posts tiling
clragon f8378d7
increased tile update rate
clragon f3f400e
added todos for better tile deletion logic
clragon 252a4a0
fixed tiling intersection calculation
clragon 67668a3
added job queues
clragon 110f85f
added deleting tiles in date range
clragon 1247754
fixed splitting tile health
clragon c700c5e
removed health controller todo
clragon e23ffb8
fixed tile deletion response doc
clragon f2c31b6
added tiling table migrations
clragon b2dcef3
added post lifecycle table
clragon 2a72003
replaced pending tiles with direct lifecycle query
clragon c4a0d31
added migration to drop pending tiles
clragon bfd4936
fixed date calculations in lifecycle metrics
clragon 1275d0d
added todos for computed refresh dates
clragon 011eb82
renamed tile slice parameter for clarity
clragon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export enum FunkyCronExpression { | ||
| EVERY_3_MINUTES = '*/3 * * * *', | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| import { add, isEqual, min } from 'date-fns'; | ||
| import { Repository } from 'typeorm'; | ||
|
|
||
| import { DateRange, PartialDateRange, TimeScale, startOf } from './date'; | ||
|
|
||
| export enum TileType { | ||
| uploadHourly = 'upload_hourly', | ||
| postPendingHourly = 'post_pending_hourly', | ||
| } | ||
|
|
||
| export interface TileService { | ||
| /** | ||
| * Interval in hours between tiles of this type. | ||
| */ | ||
| interval: number; | ||
|
|
||
| /** | ||
| * Get the available tiling ranges for this tile type. | ||
| */ | ||
| getRanges: () => Promise<TilingRange[]>; | ||
|
|
||
| /** | ||
| * Find missing tiles in the given date range. | ||
| */ | ||
| findMissing: (range: TilingRange) => Promise<Date[]>; | ||
|
|
||
| /** | ||
| * Wipe all tiles of this type, or tiles within a date range if specified. | ||
| */ | ||
| wipe: (range?: PartialDateRange) => Promise<void>; | ||
| } | ||
|
|
||
| export interface TilingRange { | ||
| /** | ||
| * The date range for the tiles. | ||
| */ | ||
| dateRange: DateRange; | ||
|
|
||
| /** | ||
| * The minimum updatedAt timestamp for the tiles in this range. | ||
| */ | ||
| updatedAt?: Date; | ||
|
|
||
| /** | ||
| * Type identifier for this range. Undefined when not applicable. | ||
| */ | ||
| type?: string; | ||
| } | ||
|
|
||
| export const getTilingRanges = ( | ||
| manifests: Array<TilingRange>, | ||
| types: string[], | ||
| ): TilingRange[] => { | ||
| const events: Array<{ | ||
| time: Date; | ||
| type: string; | ||
| delta: number; | ||
| updatedAt?: Date; | ||
| }> = []; | ||
|
|
||
| for (const manifest of manifests) { | ||
| const type = manifest.type ?? 'default'; | ||
| const start = startOf(TimeScale.Hour, manifest.dateRange.startDate); | ||
| const endFloor = startOf(TimeScale.Hour, manifest.dateRange.endDate); | ||
| const end = | ||
| endFloor.getTime() === manifest.dateRange.endDate.getTime() | ||
| ? endFloor | ||
| : add(endFloor, { hours: 1 }); | ||
|
|
||
| events.push({ time: start, type, delta: 1, updatedAt: manifest.updatedAt }); | ||
| events.push({ time: end, type, delta: -1, updatedAt: manifest.updatedAt }); | ||
| } | ||
|
|
||
| events.sort((a, b) => { | ||
| const diff = a.time.getTime() - b.time.getTime(); | ||
| if (diff !== 0) return diff; | ||
| // We want splits in ranges, to make them manageable. | ||
| // To return one continuous range, this could be flipped. | ||
| return a.delta - b.delta; | ||
| }); | ||
|
|
||
| const depths = new Map<string, number>(); | ||
|
|
||
| const ranges: TilingRange[] = []; | ||
| let start: Date | null = null; | ||
| let updated: Date | undefined = undefined; | ||
|
|
||
| for (const event of events) { | ||
| const prevDepth = depths.get(event.type) ?? 0; | ||
| const newDepth = prevDepth + event.delta; | ||
| depths.set(event.type, newDepth); | ||
|
|
||
| const active = types.every((type) => (depths.get(type) ?? 0) > 0); | ||
|
|
||
| if (active) { | ||
| if (start === null) { | ||
| start = event.time; | ||
| } | ||
| if (event.updatedAt && (!updated || event.updatedAt > updated)) { | ||
| updated = event.updatedAt; | ||
| } | ||
| } else if (start !== null) { | ||
| ranges.push({ | ||
| dateRange: new DateRange({ | ||
| startDate: start, | ||
| endDate: event.time, | ||
| scale: TimeScale.Hour, | ||
| }), | ||
| updatedAt: updated, | ||
| }); | ||
| start = null; | ||
| updated = undefined; | ||
| } | ||
| } | ||
|
|
||
| return ranges; | ||
| }; | ||
|
|
||
| export const groupTimesIntoRanges = (times: Date[]): DateRange[] => { | ||
| if (times.length === 0) return []; | ||
|
|
||
| const sorted = [...times].sort((a, b) => a.getTime() - b.getTime()); | ||
| const ranges: DateRange[] = []; | ||
|
|
||
| let start = sorted[0]!; | ||
| let end = add(sorted[0]!, { hours: 1 }); | ||
|
|
||
| for (const current of sorted.slice(1)) { | ||
| if (isEqual(current, end)) { | ||
| end = add(current, { hours: 1 }); | ||
| } else { | ||
| ranges.push( | ||
| new DateRange({ | ||
| startDate: start, | ||
| endDate: end, | ||
| scale: TimeScale.Hour, | ||
| }), | ||
| ); | ||
| start = current; | ||
| end = add(current, { hours: 1 }); | ||
| } | ||
| } | ||
|
|
||
| ranges.push( | ||
| new DateRange({ | ||
| startDate: start, | ||
| endDate: end, | ||
| scale: TimeScale.Hour, | ||
| }), | ||
| ); | ||
|
|
||
| return ranges; | ||
| }; | ||
|
|
||
| export const chunkDateRange = ( | ||
| range: DateRange, | ||
| maxHours: number, | ||
| ): DateRange[] => { | ||
| const chunks: DateRange[] = []; | ||
| let current = range.startDate; | ||
|
|
||
| while (current < range.endDate) { | ||
| const chunkEnd = add(current, { hours: maxHours }); | ||
| const actualEnd = min([chunkEnd, range.endDate]); | ||
|
|
||
| chunks.push( | ||
| new DateRange({ | ||
| startDate: current, | ||
| endDate: actualEnd, | ||
| scale: TimeScale.Hour, | ||
| }), | ||
| ); | ||
|
|
||
| current = actualEnd; | ||
| } | ||
|
|
||
| return chunks; | ||
| }; | ||
|
|
||
| export interface WithTileTime { | ||
| time: Date; | ||
| updatedAt: Date; | ||
| } | ||
|
|
||
| export async function findMissingOrStaleTiles<T extends WithTileTime>( | ||
| repository: Repository<T>, | ||
| range: TilingRange, | ||
| ): Promise<Date[]> { | ||
| const startTime = startOf(TimeScale.Hour, range.dateRange.startDate); | ||
| const endTime = startOf(TimeScale.Hour, range.dateRange.endDate); | ||
| const tableName = repository.metadata.tableName; | ||
|
|
||
| const query = ` | ||
| SELECT series.hour AS time | ||
| FROM generate_series( | ||
| $1::timestamptz, | ||
| $2::timestamptz - interval '1 hour', | ||
| interval '1 hour' | ||
| ) AS series(hour) | ||
| LEFT JOIN ${tableName} tile ON tile.time = series.hour | ||
| WHERE tile.time IS NULL | ||
| OR ($3::timestamptz IS NOT NULL AND tile.updated_at < $3::timestamptz) | ||
| `; | ||
|
|
||
| const params = [startTime, endTime, range.updatedAt || null]; | ||
| const result = await repository.query(query, params); | ||
|
|
||
| return result.map((row: { time: Date }) => row.time); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export * from './tile-health.controller'; | ||
| export * from './tile-health.dto'; | ||
| export * from './tile-health.module'; | ||
| export * from './tile-health.service'; | ||
| export * from './tile-health.utils'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { | ||
| Controller, | ||
| Delete, | ||
| Get, | ||
| Param, | ||
| Query, | ||
| UseGuards, | ||
| } from '@nestjs/common'; | ||
| import { | ||
| ApiBearerAuth, | ||
| ApiOperation, | ||
| ApiParam, | ||
| ApiResponse, | ||
| ApiTags, | ||
| } from '@nestjs/swagger'; | ||
| import { ServerAdminGuard } from 'src/auth/auth.guard'; | ||
| import { PartialDateRange, TileType } from 'src/common'; | ||
| import { PaginationParams } from 'src/common/pagination.dto'; | ||
|
|
||
| import { TileHealth } from './tile-health.dto'; | ||
| import { TileHealthService } from './tile-health.service'; | ||
|
|
||
| @ApiTags('Health') | ||
| @Controller('health/tiles') | ||
| export class TileHealthController { | ||
| constructor(private readonly tileHealthService: TileHealthService) {} | ||
|
|
||
| @Get() | ||
| @ApiOperation({ | ||
| summary: 'Retrieve tile health', | ||
| description: 'Retrieve tile health and coverage information', | ||
| operationId: 'getTileHealth', | ||
| }) | ||
| @ApiResponse({ | ||
| status: 200, | ||
| description: 'Tile health information', | ||
| type: [TileHealth], | ||
| }) | ||
| @UseGuards(ServerAdminGuard) | ||
| @ApiBearerAuth() | ||
| async getTileHealth( | ||
| @Query() pages?: PaginationParams, | ||
| ): Promise<TileHealth[]> { | ||
| return this.tileHealthService.tiles(pages); | ||
| } | ||
|
|
||
| // This is kind of awkward, being handled in the health controller. | ||
| @Delete(':type') | ||
| @ApiOperation({ | ||
| summary: 'Delete all tiles of a type', | ||
| description: 'Delete all tiles of the specified type', | ||
| operationId: 'deleteTilesByType', | ||
| }) | ||
| @ApiParam({ | ||
| name: 'type', | ||
| enum: TileType, | ||
| description: 'The type of tiles to delete', | ||
| }) | ||
| @ApiResponse({ | ||
| status: 204, | ||
| description: 'Tiles deleted successfully', | ||
| }) | ||
| @UseGuards(ServerAdminGuard) | ||
| @ApiBearerAuth() | ||
| async deleteTilesByType( | ||
| @Param('type') type: TileType, | ||
| @Query() range?: PartialDateRange, | ||
| ): Promise<void> { | ||
| return this.tileHealthService.deleteTilesByType(type, range); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { ApiProperty } from '@nestjs/swagger'; | ||
| import { Raw, TileType } from 'src/common'; | ||
|
|
||
| export class TileSlice { | ||
| constructor(value: Raw<TileSlice>) { | ||
| Object.assign(this, value); | ||
| } | ||
|
|
||
| startDate: Date; | ||
| endDate: Date; | ||
|
|
||
| available: number; | ||
| unavailable: number; | ||
| none: number; | ||
| } | ||
|
|
||
| export class TileHealth { | ||
| constructor(value: Raw<TileHealth>) { | ||
| Object.assign(this, value); | ||
| } | ||
|
|
||
| @ApiProperty({ enum: TileType, enumName: 'TileType' }) | ||
| type: TileType; | ||
| startDate: Date; | ||
| endDate: Date; | ||
| expected: number; | ||
| actual: number; | ||
| slices: TileSlice[]; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { Module } from '@nestjs/common'; | ||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||
| import { ManifestEntity } from 'src/manifest/manifest.entity'; | ||
| import { UploadTilesModule } from 'src/upload/tiles/upload-tiles.module'; | ||
|
|
||
| import { TileHealthController } from './tile-health.controller'; | ||
| import { TileHealthService } from './tile-health.service'; | ||
|
|
||
| @Module({ | ||
| imports: [TypeOrmModule.forFeature([ManifestEntity]), UploadTilesModule], | ||
| controllers: [TileHealthController], | ||
| providers: [TileHealthService], | ||
| exports: [TileHealthService], | ||
| }) | ||
| export class TileHealthModule {} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.