mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
fix(server): add rate limit and deduplication to version check (#27747)
This commit is contained in:
@@ -707,6 +707,7 @@ export enum DatabaseLock {
|
|||||||
BackupDatabase = 42,
|
BackupDatabase = 42,
|
||||||
MaintenanceOperation = 621,
|
MaintenanceOperation = 621,
|
||||||
MemoryCreation = 777,
|
MemoryCreation = 777,
|
||||||
|
VersionCheck = 800,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MaintenanceAction {
|
export enum MaintenanceAction {
|
||||||
|
|||||||
@@ -233,6 +233,9 @@ export class JobRepository {
|
|||||||
case JobName.FacialRecognitionQueueAll: {
|
case JobName.FacialRecognitionQueueAll: {
|
||||||
return { jobId: JobName.FacialRecognitionQueueAll };
|
return { jobId: JobName.FacialRecognitionQueueAll };
|
||||||
}
|
}
|
||||||
|
case JobName.VersionCheck: {
|
||||||
|
return { jobId: JobName.VersionCheck };
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ describe(VersionService.name, () => {
|
|||||||
expect(mocks.versionHistory.create).not.toHaveBeenCalled();
|
expect(mocks.versionHistory.create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a version check cron job', async () => {
|
it('should create a version check cron job when the database lock is acquired', async () => {
|
||||||
|
mocks.database.tryLock.mockResolvedValue(true);
|
||||||
mocks.versionHistory.getLatest.mockResolvedValue({
|
mocks.versionHistory.getLatest.mockResolvedValue({
|
||||||
id: 'version-1',
|
id: 'version-1',
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -93,6 +94,25 @@ describe(VersionService.name, () => {
|
|||||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Skipped);
|
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Skipped);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should skip if the last check was less than 50 seconds ago', async () => {
|
||||||
|
mocks.systemMetadata.get.mockResolvedValueOnce(null).mockResolvedValueOnce({
|
||||||
|
checkedAt: DateTime.utc().minus({ seconds: 30 }).toISO(),
|
||||||
|
releaseVersion: '1.0.0',
|
||||||
|
});
|
||||||
|
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Skipped);
|
||||||
|
expect(mocks.serverInfo.getLatestRelease).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run if the last check was more than 50 seconds ago', async () => {
|
||||||
|
mocks.systemMetadata.get.mockResolvedValueOnce(null).mockResolvedValueOnce({
|
||||||
|
checkedAt: DateTime.utc().minus({ seconds: 60 }).toISO(),
|
||||||
|
releaseVersion: '1.0.0',
|
||||||
|
});
|
||||||
|
mocks.serverInfo.getLatestRelease.mockResolvedValue(mockVersionResponse(serverVersion.toString()));
|
||||||
|
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Success);
|
||||||
|
expect(mocks.serverInfo.getLatestRelease).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should run and notify if a new version is available', async () => {
|
it('should run and notify if a new version is available', async () => {
|
||||||
mocks.serverInfo.getLatestRelease.mockResolvedValue(mockVersionResponse('v100.0.0'));
|
mocks.serverInfo.getLatestRelease.mockResolvedValue(mockVersionResponse('v100.0.0'));
|
||||||
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Success);
|
await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.Success);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import semver, { SemVer } from 'semver';
|
|||||||
import { serverVersion } from 'src/constants';
|
import { serverVersion } from 'src/constants';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnEvent, OnJob } from 'src/decorators';
|
||||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { CronJob, DatabaseLock, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
|
import { CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
|
||||||
import { ArgOf } from 'src/repositories/event.repository';
|
import { ArgOf } from 'src/repositories/event.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { VersionCheckMetadata } from 'src/types';
|
import { VersionCheckMetadata } from 'src/types';
|
||||||
@@ -21,18 +21,21 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VersionService extends BaseService {
|
export class VersionService extends BaseService {
|
||||||
@OnEvent({ name: 'AppBootstrap' })
|
@OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Microservices] })
|
||||||
async onBootstrap(): Promise<void> {
|
async onBootstrap(): Promise<void> {
|
||||||
await this.handleVersionCheck();
|
const hasLock = await this.databaseRepository.tryLock(DatabaseLock.VersionCheck);
|
||||||
|
if (hasLock) {
|
||||||
|
await this.handleVersionCheck();
|
||||||
|
|
||||||
const randomMinute = Math.floor(Math.random() * 60);
|
const randomMinute = Math.floor(Math.random() * 60);
|
||||||
const expression = `${randomMinute} * * * *`;
|
const expression = `${randomMinute} * * * *`;
|
||||||
this.logger.debug(`Scheduling version check for cron ${expression}`);
|
this.logger.debug(`Scheduling version check for cron ${expression}`);
|
||||||
this.cronRepository.create({
|
this.cronRepository.create({
|
||||||
name: CronJob.VersionCheck,
|
name: CronJob.VersionCheck,
|
||||||
expression,
|
expression,
|
||||||
onTick: () => handlePromiseError(this.handleQueueVersionCheck(), this.logger),
|
onTick: () => handlePromiseError(this.handleQueueVersionCheck(), this.logger),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await this.databaseRepository.withLock(DatabaseLock.VersionHistory, async () => {
|
await this.databaseRepository.withLock(DatabaseLock.VersionHistory, async () => {
|
||||||
const previous = await this.versionRepository.getLatest();
|
const previous = await this.versionRepository.getLatest();
|
||||||
@@ -86,6 +89,15 @@ export class VersionService extends BaseService {
|
|||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const versionCheck = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState);
|
||||||
|
if (versionCheck?.checkedAt) {
|
||||||
|
const lastUpdate = DateTime.fromISO(versionCheck.checkedAt);
|
||||||
|
const elapsedTime = DateTime.now().diff(lastUpdate).as('seconds');
|
||||||
|
if (elapsedTime < 50) {
|
||||||
|
return JobStatus.Skipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease();
|
const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease();
|
||||||
const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
|
const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user