feat: introduce routing status tracking for approved roads and telemetry batch queue limits
This commit is contained in:
@@ -50,6 +50,11 @@ export class DriverTelemetryDto {
|
||||
return val != null ? Number(val) : 0;
|
||||
})
|
||||
elevation?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Client GPS capture time as ISO-8601', example: '2026-09-16T10:20:30.000Z' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export class DriverTelemetryBatchDto {
|
||||
|
||||
@@ -25,14 +25,17 @@ export class TelemetryController {
|
||||
|
||||
@Post('batch')
|
||||
@ApiOperation({
|
||||
summary: 'Batch ingest driver telemetry points 📦',
|
||||
description: 'Receives an array of telemetry points for offline-buffered sync or high-frequency traces.',
|
||||
summary: 'Queue navigation telemetry batch 📦',
|
||||
description: 'Acknowledges a navigation batch immediately; the server persists it from Redis in the background.',
|
||||
})
|
||||
async ingestBatch(@Body() body: DriverTelemetryBatchDto) {
|
||||
if (!body || !Array.isArray(body.points)) {
|
||||
throw new HttpException('Invalid payload: expected { points: [...] }', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return this.telemetryService.ingestBatch(body.points);
|
||||
if (body.points.length > 100) {
|
||||
throw new HttpException('A telemetry batch may contain at most 100 points', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return this.telemetryService.enqueueBatch(body.points);
|
||||
}
|
||||
|
||||
@Get('nearby')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { TelemetryLog } from './telemetry.entity';
|
||||
@@ -6,8 +6,11 @@ import { DriverTelemetryDto } from './dto/driver-telemetry.dto';
|
||||
import { RedisService } from '../common/redis.service';
|
||||
|
||||
@Injectable()
|
||||
export class TelemetryService implements OnModuleInit {
|
||||
export class TelemetryService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(TelemetryService.name);
|
||||
private readonly queueKey = 'telemetry:batch:queue';
|
||||
private draining = false;
|
||||
private queueTimer?: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(TelemetryLog)
|
||||
@@ -43,6 +46,50 @@ export class TelemetryService implements OnModuleInit {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Telemetry DB auto-migration check note: ${err.message}`);
|
||||
}
|
||||
// The request path only queues data. A small background worker persists it
|
||||
// in batches, keeping navigation uploads fast even when PostGIS is busy.
|
||||
this.queueTimer = setInterval(() => void this.drainQueue(), 5_000);
|
||||
void this.drainQueue();
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
if (this.queueTimer) clearInterval(this.queueTimer);
|
||||
}
|
||||
|
||||
async enqueueBatch(points: DriverTelemetryDto[]) {
|
||||
if (!points?.length) return { success: true, accepted: 0, queued: 0 };
|
||||
if (points.length > 100) {
|
||||
throw new Error('A telemetry batch may contain at most 100 points.');
|
||||
}
|
||||
await this.redisService.getClient().lPush(this.queueKey, JSON.stringify(points));
|
||||
const queuedBatches = await this.redisService.getClient().lLen(this.queueKey);
|
||||
void this.drainQueue();
|
||||
return { success: true, accepted: points.length, queuedBatches };
|
||||
}
|
||||
|
||||
private async drainQueue() {
|
||||
if (this.draining) return;
|
||||
this.draining = true;
|
||||
try {
|
||||
// Limit a turn so requests and other Redis work stay responsive.
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const raw = await this.redisService.getClient().rPop(this.queueKey);
|
||||
if (!raw) break;
|
||||
try {
|
||||
const points = JSON.parse(raw) as DriverTelemetryDto[];
|
||||
await this.ingestBatch(points);
|
||||
} catch (error: any) {
|
||||
// Put the original payload back at the head for a later retry.
|
||||
await this.redisService.getClient().rPush(this.queueKey, raw);
|
||||
this.logger.error(`Telemetry queue persistence failed: ${error.message}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Telemetry queue worker failed: ${error.message}`);
|
||||
} finally {
|
||||
this.draining = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +111,7 @@ export class TelemetryService implements OnModuleInit {
|
||||
heading,
|
||||
distance,
|
||||
elevation,
|
||||
timestamp: new Date(),
|
||||
timestamp: this.captureTime(data.timestamp),
|
||||
location: {
|
||||
type: 'Point',
|
||||
coordinates: [lng, lat],
|
||||
@@ -127,7 +174,7 @@ export class TelemetryService implements OnModuleInit {
|
||||
heading,
|
||||
distance,
|
||||
elevation,
|
||||
timestamp: new Date(),
|
||||
timestamp: this.captureTime(p.timestamp),
|
||||
location: {
|
||||
type: 'Point',
|
||||
coordinates: [lng, lat],
|
||||
@@ -168,6 +215,12 @@ export class TelemetryService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
private captureTime(value?: string): Date {
|
||||
if (!value) return new Date();
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? new Date() : date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find nearby active drivers using PostGIS spatial geography search
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user