mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
refactor: config init event for first config load (#13930)
This commit is contained in:
parent
c383e115af
commit
d456d35510
18 changed files with 160 additions and 146 deletions
|
@ -78,11 +78,11 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.eventRepository.setup({ services });
|
this.eventRepository.setup({ services });
|
||||||
await this.eventRepository.emit('app.bootstrap', this.worker);
|
await this.eventRepository.emit('app.bootstrap');
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
await this.eventRepository.emit('app.shutdown', this.worker);
|
await this.eventRepository.emit('app.shutdown');
|
||||||
await teardownTelemetry();
|
await teardownTelemetry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,22 +2,21 @@ import { ClassConstructor } from 'class-transformer';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { ImmichWorker } from 'src/enum';
|
|
||||||
import { JobItem, QueueName } from 'src/interfaces/job.interface';
|
import { JobItem, QueueName } from 'src/interfaces/job.interface';
|
||||||
|
|
||||||
export const IEventRepository = 'IEventRepository';
|
export const IEventRepository = 'IEventRepository';
|
||||||
|
|
||||||
type EventMap = {
|
type EventMap = {
|
||||||
// app events
|
// app events
|
||||||
'app.bootstrap': [ImmichWorker];
|
'app.bootstrap': [];
|
||||||
'app.shutdown': [ImmichWorker];
|
'app.shutdown': [];
|
||||||
|
|
||||||
|
'config.init': [{ newConfig: SystemConfig }];
|
||||||
// config events
|
// config events
|
||||||
'config.update': [
|
'config.update': [
|
||||||
{
|
{
|
||||||
newConfig: SystemConfig;
|
newConfig: SystemConfig;
|
||||||
/** When the server starts, `oldConfig` is `undefined` */
|
oldConfig: SystemConfig;
|
||||||
oldConfig?: SystemConfig;
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
|
'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
|
||||||
|
@ -89,6 +88,13 @@ export type EventItem<T extends EmitEvent> = {
|
||||||
server: boolean;
|
server: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum BootstrapEventPriority {
|
||||||
|
// Database service should be initialized before anything else, most other services need database access
|
||||||
|
DatabaseService = -200,
|
||||||
|
// Initialise config after other bootstrap services, stop other services from using config on bootstrap
|
||||||
|
SystemConfig = 100,
|
||||||
|
}
|
||||||
|
|
||||||
export interface IEventRepository {
|
export interface IEventRepository {
|
||||||
setup(options: { services: ClassConstructor<unknown>[] }): void;
|
setup(options: { services: ClassConstructor<unknown>[] }): void;
|
||||||
emit<T extends keyof EventMap>(event: T, ...args: ArgsOf<T>): Promise<void>;
|
emit<T extends keyof EventMap>(event: T, ...args: ArgsOf<T>): Promise<void>;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { PassThrough } from 'node:stream';
|
||||||
import { defaults, SystemConfig } from 'src/config';
|
import { defaults, SystemConfig } from 'src/config';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { ImmichWorker, StorageFolder } from 'src/enum';
|
import { ImmichWorker, StorageFolder } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IJobRepository, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { IProcessRepository } from 'src/interfaces/process.interface';
|
import { IProcessRepository } from 'src/interfaces/process.interface';
|
||||||
|
@ -16,13 +17,14 @@ describe(BackupService.name, () => {
|
||||||
let sut: BackupService;
|
let sut: BackupService;
|
||||||
|
|
||||||
let databaseMock: Mocked<IDatabaseRepository>;
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let processMock: Mocked<IProcessRepository>;
|
let processMock: Mocked<IProcessRepository>;
|
||||||
let storageMock: Mocked<IStorageRepository>;
|
let storageMock: Mocked<IStorageRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
({ sut, databaseMock, jobMock, processMock, storageMock, systemMock } = newTestService(BackupService));
|
({ sut, configMock, databaseMock, jobMock, processMock, storageMock, systemMock } = newTestService(BackupService));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -32,25 +34,23 @@ describe(BackupService.name, () => {
|
||||||
describe('onBootstrapEvent', () => {
|
describe('onBootstrapEvent', () => {
|
||||||
it('should init cron job and handle config changes', async () => {
|
it('should init cron job and handle config changes', async () => {
|
||||||
databaseMock.tryLock.mockResolvedValue(true);
|
databaseMock.tryLock.mockResolvedValue(true);
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.API);
|
await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(jobMock.addCronJob).toHaveBeenCalled();
|
expect(jobMock.addCronJob).toHaveBeenCalled();
|
||||||
expect(systemMock.get).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialize backup database cron job when lock is taken', async () => {
|
it('should not initialize backup database cron job when lock is taken', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
|
||||||
databaseMock.tryLock.mockResolvedValue(false);
|
databaseMock.tryLock.mockResolvedValue(false);
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.API);
|
await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialise backup database job when running on microservices', async () => {
|
it('should not initialise backup database job when running on microservices', async () => {
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||||
|
await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -58,9 +58,8 @@ describe(BackupService.name, () => {
|
||||||
|
|
||||||
describe('onConfigUpdateEvent', () => {
|
describe('onConfigUpdateEvent', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
systemMock.get.mockResolvedValue(defaults);
|
|
||||||
databaseMock.tryLock.mockResolvedValue(true);
|
databaseMock.tryLock.mockResolvedValue(true);
|
||||||
await sut.onBootstrap(ImmichWorker.API);
|
await sut.onConfigInit({ newConfig: defaults });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update cron job if backup is enabled', () => {
|
it('should update cron job if backup is enabled', () => {
|
||||||
|
@ -80,14 +79,9 @@ describe(BackupService.name, () => {
|
||||||
expect(jobMock.updateCronJob).toHaveBeenCalled();
|
expect(jobMock.updateCronJob).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing if oldConfig is not provided', () => {
|
|
||||||
sut.onConfigUpdate({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
|
|
||||||
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing if instance does not have the backup database lock', async () => {
|
it('should do nothing if instance does not have the backup database lock', async () => {
|
||||||
databaseMock.tryLock.mockResolvedValue(false);
|
databaseMock.tryLock.mockResolvedValue(false);
|
||||||
await sut.onBootstrap(ImmichWorker.API);
|
await sut.onConfigInit({ newConfig: defaults });
|
||||||
sut.onConfigUpdate({ newConfig: systemConfigStub.backupEnabled as SystemConfig, oldConfig: defaults });
|
sut.onConfigUpdate({ newConfig: systemConfigStub.backupEnabled as SystemConfig, oldConfig: defaults });
|
||||||
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
|
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,14 +14,15 @@ import { validateCronExpression } from 'src/validation';
|
||||||
export class BackupService extends BaseService {
|
export class BackupService extends BaseService {
|
||||||
private backupLock = false;
|
private backupLock = false;
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'config.init' })
|
||||||
async onBootstrap(workerType: ImmichWorker) {
|
async onConfigInit({
|
||||||
if (workerType !== ImmichWorker.API) {
|
newConfig: {
|
||||||
|
backup: { database },
|
||||||
|
},
|
||||||
|
}: ArgOf<'config.init'>) {
|
||||||
|
if (this.worker !== ImmichWorker.API) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {
|
|
||||||
backup: { database },
|
|
||||||
} = await this.getConfig({ withCache: true });
|
|
||||||
|
|
||||||
this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase);
|
this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase);
|
||||||
|
|
||||||
|
@ -36,8 +37,8 @@ export class BackupService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'config.update', server: true })
|
@OnEvent({ name: 'config.update', server: true })
|
||||||
onConfigUpdate({ newConfig: { backup }, oldConfig }: ArgOf<'config.update'>) {
|
onConfigUpdate({ newConfig: { backup } }: ArgOf<'config.update'>) {
|
||||||
if (!oldConfig || !this.backupLock) {
|
if (!this.backupLock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
VectorExtension,
|
VectorExtension,
|
||||||
VectorIndex,
|
VectorIndex,
|
||||||
} from 'src/interfaces/database.interface';
|
} from 'src/interfaces/database.interface';
|
||||||
|
import { BootstrapEventPriority } from 'src/interfaces/event.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
type CreateFailedArgs = { name: string; extension: string; otherName: string };
|
type CreateFailedArgs = { name: string; extension: string; otherName: string };
|
||||||
|
@ -64,7 +65,7 @@ const RETRY_DURATION = Duration.fromObject({ seconds: 5 });
|
||||||
export class DatabaseService extends BaseService {
|
export class DatabaseService extends BaseService {
|
||||||
private reconnection?: NodeJS.Timeout;
|
private reconnection?: NodeJS.Timeout;
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap', priority: -200 })
|
@OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.DatabaseService })
|
||||||
async onBootstrap() {
|
async onBootstrap() {
|
||||||
const version = await this.databaseRepository.getPostgresVersion();
|
const version = await this.databaseRepository.getPostgresVersion();
|
||||||
const current = semver.coerce(version);
|
const current = semver.coerce(version);
|
||||||
|
|
|
@ -31,7 +31,7 @@ describe(JobService.name, () => {
|
||||||
|
|
||||||
describe('onConfigUpdate', () => {
|
describe('onConfigUpdate', () => {
|
||||||
it('should update concurrency', () => {
|
it('should update concurrency', () => {
|
||||||
sut.onConfigUpdate({ oldConfig: defaults, newConfig: defaults });
|
sut.onConfigInitOrUpdate({ newConfig: defaults });
|
||||||
|
|
||||||
expect(jobMock.setConcurrency).toHaveBeenCalledTimes(15);
|
expect(jobMock.setConcurrency).toHaveBeenCalledTimes(15);
|
||||||
expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FACIAL_RECOGNITION, 1);
|
expect(jobMock.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FACIAL_RECOGNITION, 1);
|
||||||
|
|
|
@ -38,8 +38,9 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JobService extends BaseService {
|
export class JobService extends BaseService {
|
||||||
|
@OnEvent({ name: 'config.init' })
|
||||||
@OnEvent({ name: 'config.update', server: true })
|
@OnEvent({ name: 'config.update', server: true })
|
||||||
onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) {
|
onConfigInitOrUpdate({ newConfig: config }: ArgOf<'config.init'>) {
|
||||||
if (this.worker !== ImmichWorker.MICROSERVICES) {
|
if (this.worker !== ImmichWorker.MICROSERVICES) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { mapLibrary } from 'src/dtos/library.dto';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { AssetType, ImmichWorker } from 'src/enum';
|
import { AssetType, ImmichWorker } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import {
|
import {
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
|
@ -16,7 +17,6 @@ import {
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
|
||||||
import { LibraryService } from 'src/services/library.service';
|
import { LibraryService } from 'src/services/library.service';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
@ -35,30 +35,28 @@ describe(LibraryService.name, () => {
|
||||||
let sut: LibraryService;
|
let sut: LibraryService;
|
||||||
|
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let databaseMock: Mocked<IDatabaseRepository>;
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let libraryMock: Mocked<ILibraryRepository>;
|
let libraryMock: Mocked<ILibraryRepository>;
|
||||||
let storageMock: Mocked<IStorageRepository>;
|
let storageMock: Mocked<IStorageRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
({ sut, assetMock, databaseMock, jobMock, libraryMock, storageMock, systemMock } = newTestService(LibraryService));
|
({ sut, assetMock, configMock, databaseMock, jobMock, libraryMock, storageMock } = newTestService(LibraryService));
|
||||||
|
|
||||||
databaseMock.tryLock.mockResolvedValue(true);
|
databaseMock.tryLock.mockResolvedValue(true);
|
||||||
|
configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(sut).toBeDefined();
|
expect(sut).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onBootstrapEvent', () => {
|
describe('onConfigInit', () => {
|
||||||
it('should init cron job and handle config changes', async () => {
|
it('should init cron job and handle config changes', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryScan);
|
await sut.onConfigInit({ newConfig: defaults });
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
|
||||||
|
|
||||||
expect(jobMock.addCronJob).toHaveBeenCalled();
|
expect(jobMock.addCronJob).toHaveBeenCalled();
|
||||||
expect(systemMock.get).toHaveBeenCalled();
|
|
||||||
|
|
||||||
await sut.onConfigUpdate({
|
await sut.onConfigUpdate({
|
||||||
oldConfig: defaults,
|
oldConfig: defaults,
|
||||||
|
@ -82,7 +80,6 @@ describe(LibraryService.name, () => {
|
||||||
libraryStub.externalLibraryWithImportPaths2,
|
libraryStub.externalLibraryWithImportPaths2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
||||||
libraryMock.get.mockImplementation((id) =>
|
libraryMock.get.mockImplementation((id) =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
[libraryStub.externalLibraryWithImportPaths1, libraryStub.externalLibraryWithImportPaths2].find(
|
[libraryStub.externalLibraryWithImportPaths1, libraryStub.externalLibraryWithImportPaths2].find(
|
||||||
|
@ -91,7 +88,7 @@ describe(LibraryService.name, () => {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(storageMock.watch.mock.calls).toEqual(
|
expect(storageMock.watch.mock.calls).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
|
@ -102,33 +99,30 @@ describe(LibraryService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialize watcher when watching is disabled', async () => {
|
it('should not initialize watcher when watching is disabled', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchDisabled as SystemConfig });
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
|
||||||
|
|
||||||
expect(storageMock.watch).not.toHaveBeenCalled();
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialize watcher when lock is taken', async () => {
|
it('should not initialize watcher when lock is taken', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
||||||
databaseMock.tryLock.mockResolvedValue(false);
|
databaseMock.tryLock.mockResolvedValue(false);
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(storageMock.watch).not.toHaveBeenCalled();
|
expect(storageMock.watch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialize library scan cron job when lock is taken', async () => {
|
it('should not initialize library scan cron job when lock is taken', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
||||||
databaseMock.tryLock.mockResolvedValue(false);
|
databaseMock.tryLock.mockResolvedValue(false);
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialize watcher or library scan job when running on api', async () => {
|
it('should not initialize watcher or library scan job when running on api', async () => {
|
||||||
await sut.onBootstrap(ImmichWorker.API);
|
configMock.getWorker.mockReturnValue(ImmichWorker.API);
|
||||||
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryScan as SystemConfig });
|
||||||
|
|
||||||
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -136,19 +130,13 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
describe('onConfigUpdateEvent', () => {
|
describe('onConfigUpdateEvent', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
systemMock.get.mockResolvedValue(defaults);
|
|
||||||
databaseMock.tryLock.mockResolvedValue(true);
|
databaseMock.tryLock.mockResolvedValue(true);
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: defaults });
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing if oldConfig is not provided', async () => {
|
|
||||||
await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScan as SystemConfig });
|
|
||||||
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing if instance does not have the watch lock', async () => {
|
it('should do nothing if instance does not have the watch lock', async () => {
|
||||||
databaseMock.tryLock.mockResolvedValue(false);
|
databaseMock.tryLock.mockResolvedValue(false);
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: defaults });
|
||||||
await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScan as SystemConfig, oldConfig: defaults });
|
await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScan as SystemConfig, oldConfig: defaults });
|
||||||
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
|
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -156,9 +144,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should update cron job and enable watching', async () => {
|
it('should update cron job and enable watching', async () => {
|
||||||
libraryMock.getAll.mockResolvedValue([]);
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
await sut.onConfigUpdate({
|
await sut.onConfigUpdate({
|
||||||
newConfig: {
|
newConfig: systemConfigStub.libraryScanAndWatch as SystemConfig,
|
||||||
library: { ...systemConfigStub.libraryScan.library, ...systemConfigStub.libraryWatchEnabled.library },
|
|
||||||
} as SystemConfig,
|
|
||||||
oldConfig: defaults,
|
oldConfig: defaults,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -172,15 +158,11 @@ describe(LibraryService.name, () => {
|
||||||
it('should update cron job and disable watching', async () => {
|
it('should update cron job and disable watching', async () => {
|
||||||
libraryMock.getAll.mockResolvedValue([]);
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
await sut.onConfigUpdate({
|
await sut.onConfigUpdate({
|
||||||
newConfig: {
|
newConfig: systemConfigStub.libraryScanAndWatch as SystemConfig,
|
||||||
library: { ...systemConfigStub.libraryScan.library, ...systemConfigStub.libraryWatchEnabled.library },
|
|
||||||
} as SystemConfig,
|
|
||||||
oldConfig: defaults,
|
oldConfig: defaults,
|
||||||
});
|
});
|
||||||
await sut.onConfigUpdate({
|
await sut.onConfigUpdate({
|
||||||
newConfig: {
|
newConfig: systemConfigStub.libraryScan as SystemConfig,
|
||||||
library: { ...systemConfigStub.libraryScan.library, ...systemConfigStub.libraryWatchDisabled.library },
|
|
||||||
} as SystemConfig,
|
|
||||||
oldConfig: defaults,
|
oldConfig: defaults,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -703,12 +685,10 @@ describe(LibraryService.name, () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||||
|
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
||||||
|
|
||||||
const mockClose = vitest.fn();
|
const mockClose = vitest.fn();
|
||||||
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
|
||||||
await sut.delete(libraryStub.externalLibraryWithImportPaths1.id);
|
await sut.delete(libraryStub.externalLibraryWithImportPaths1.id);
|
||||||
|
|
||||||
expect(mockClose).toHaveBeenCalled();
|
expect(mockClose).toHaveBeenCalled();
|
||||||
|
@ -837,12 +817,11 @@ describe(LibraryService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create watched with import paths', async () => {
|
it('should create watched with import paths', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
||||||
libraryMock.create.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.create.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
libraryMock.getAll.mockResolvedValue([]);
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
|
||||||
await sut.create({
|
await sut.create({
|
||||||
ownerId: authStub.admin.user.id,
|
ownerId: authStub.admin.user.id,
|
||||||
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||||
|
@ -902,10 +881,9 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
||||||
libraryMock.getAll.mockResolvedValue([]);
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if an import path is invalid', async () => {
|
it('should throw an error if an import path is invalid', async () => {
|
||||||
|
@ -944,9 +922,7 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
describe('watching disabled', () => {
|
describe('watching disabled', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchDisabled as SystemConfig });
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not watch library', async () => {
|
it('should not watch library', async () => {
|
||||||
|
@ -960,9 +936,8 @@ describe(LibraryService.name, () => {
|
||||||
|
|
||||||
describe('watching enabled', () => {
|
describe('watching enabled', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
||||||
libraryMock.getAll.mockResolvedValue([]);
|
libraryMock.getAll.mockResolvedValue([]);
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should watch library', async () => {
|
it('should watch library', async () => {
|
||||||
|
@ -1114,7 +1089,6 @@ describe(LibraryService.name, () => {
|
||||||
libraryStub.externalLibraryWithImportPaths2,
|
libraryStub.externalLibraryWithImportPaths2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||||
|
|
||||||
libraryMock.get.mockImplementation((id) =>
|
libraryMock.get.mockImplementation((id) =>
|
||||||
|
@ -1128,7 +1102,7 @@ describe(LibraryService.name, () => {
|
||||||
const mockClose = vitest.fn();
|
const mockClose = vitest.fn();
|
||||||
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
|
||||||
await sut.onShutdown();
|
await sut.onShutdown();
|
||||||
|
|
||||||
expect(mockClose).toHaveBeenCalledTimes(2);
|
expect(mockClose).toHaveBeenCalledTimes(2);
|
||||||
|
|
|
@ -32,16 +32,16 @@ export class LibraryService extends BaseService {
|
||||||
private lock = false;
|
private lock = false;
|
||||||
private watchers: Record<string, () => Promise<void>> = {};
|
private watchers: Record<string, () => Promise<void>> = {};
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'config.init' })
|
||||||
async onBootstrap(workerType: ImmichWorker) {
|
async onConfigInit({
|
||||||
if (workerType !== ImmichWorker.MICROSERVICES) {
|
newConfig: {
|
||||||
|
library: { watch, scan },
|
||||||
|
},
|
||||||
|
}: ArgOf<'config.init'>) {
|
||||||
|
if (this.worker !== ImmichWorker.MICROSERVICES) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await this.getConfig({ withCache: false });
|
|
||||||
|
|
||||||
const { watch, scan } = config.library;
|
|
||||||
|
|
||||||
// This ensures that library watching only occurs in one microservice
|
// This ensures that library watching only occurs in one microservice
|
||||||
this.lock = await this.databaseRepository.tryLock(DatabaseLock.Library);
|
this.lock = await this.databaseRepository.tryLock(DatabaseLock.Library);
|
||||||
|
|
||||||
|
@ -62,8 +62,8 @@ export class LibraryService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'config.update', server: true })
|
@OnEvent({ name: 'config.update', server: true })
|
||||||
async onConfigUpdate({ newConfig: { library }, oldConfig }: ArgOf<'config.update'>) {
|
async onConfigUpdate({ newConfig: { library } }: ArgOf<'config.update'>) {
|
||||||
if (!oldConfig || !this.lock) {
|
if (!this.lock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { AssetType, ImmichWorker, SourceType } from 'src/enum';
|
import { AssetType, ImmichWorker, SourceType } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
|
@ -32,6 +33,7 @@ describe(MetadataService.name, () => {
|
||||||
|
|
||||||
let albumMock: Mocked<IAlbumRepository>;
|
let albumMock: Mocked<IAlbumRepository>;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let cryptoMock: Mocked<ICryptoRepository>;
|
let cryptoMock: Mocked<ICryptoRepository>;
|
||||||
let eventMock: Mocked<IEventRepository>;
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
|
@ -55,6 +57,7 @@ describe(MetadataService.name, () => {
|
||||||
sut,
|
sut,
|
||||||
albumMock,
|
albumMock,
|
||||||
assetMock,
|
assetMock,
|
||||||
|
configMock,
|
||||||
cryptoMock,
|
cryptoMock,
|
||||||
eventMock,
|
eventMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
|
@ -70,6 +73,8 @@ describe(MetadataService.name, () => {
|
||||||
|
|
||||||
mockReadTags();
|
mockReadTags();
|
||||||
|
|
||||||
|
configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||||
|
|
||||||
delete process.env.TZ;
|
delete process.env.TZ;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -83,17 +88,16 @@ describe(MetadataService.name, () => {
|
||||||
|
|
||||||
describe('onBootstrapEvent', () => {
|
describe('onBootstrapEvent', () => {
|
||||||
it('should pause and resume queue during init', async () => {
|
it('should pause and resume queue during init', async () => {
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onBootstrap();
|
||||||
|
|
||||||
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
||||||
expect(mapMock.init).toHaveBeenCalledTimes(1);
|
expect(mapMock.init).toHaveBeenCalledTimes(1);
|
||||||
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return if reverse geocoding is disabled', async () => {
|
it('should return if running on api', async () => {
|
||||||
systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
configMock.getWorker.mockReturnValue(ImmichWorker.API);
|
||||||
|
await sut.onBootstrap();
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
|
||||||
|
|
||||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||||
expect(mapMock.init).not.toHaveBeenCalled();
|
expect(mapMock.init).not.toHaveBeenCalled();
|
||||||
|
|
|
@ -80,12 +80,12 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetadataService extends BaseService {
|
export class MetadataService extends BaseService {
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'app.bootstrap' })
|
||||||
async onBootstrap(app: ArgOf<'app.bootstrap'>) {
|
async onBootstrap() {
|
||||||
if (app !== ImmichWorker.MICROSERVICES) {
|
if (this.worker !== ImmichWorker.MICROSERVICES) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const config = await this.getConfig({ withCache: false });
|
this.logger.log('Bootstrapping metadata service');
|
||||||
await this.init(config);
|
await this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'app.shutdown' })
|
@OnEvent({ name: 'app.shutdown' })
|
||||||
|
@ -93,17 +93,8 @@ export class MetadataService extends BaseService {
|
||||||
await this.metadataRepository.teardown();
|
await this.metadataRepository.teardown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'config.update' })
|
private async init() {
|
||||||
async onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
|
this.logger.log('Initializing metadata service');
|
||||||
await this.init(newConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async init({ reverseGeocoding }: SystemConfig) {
|
|
||||||
const { enabled } = reverseGeocoding;
|
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
|
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
|
||||||
|
|
|
@ -77,7 +77,7 @@ describe(NotificationService.name, () => {
|
||||||
|
|
||||||
describe('onConfigUpdate', () => {
|
describe('onConfigUpdate', () => {
|
||||||
it('should emit client and server events', () => {
|
it('should emit client and server events', () => {
|
||||||
const update = { newConfig: defaults };
|
const update = { oldConfig: defaults, newConfig: defaults };
|
||||||
expect(sut.onConfigUpdate(update)).toBeUndefined();
|
expect(sut.onConfigUpdate(update)).toBeUndefined();
|
||||||
expect(eventMock.clientBroadcast).toHaveBeenCalledWith('on_config_update');
|
expect(eventMock.clientBroadcast).toHaveBeenCalledWith('on_config_update');
|
||||||
expect(eventMock.serverSend).toHaveBeenCalledWith('config.update', update);
|
expect(eventMock.serverSend).toHaveBeenCalledWith('config.update', update);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { ImmichWorker } from 'src/enum';
|
import { ImmichWorker } from 'src/enum';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||||
|
@ -22,12 +23,14 @@ describe(SmartInfoService.name, () => {
|
||||||
let machineLearningMock: Mocked<IMachineLearningRepository>;
|
let machineLearningMock: Mocked<IMachineLearningRepository>;
|
||||||
let searchMock: Mocked<ISearchRepository>;
|
let searchMock: Mocked<ISearchRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
({ sut, assetMock, databaseMock, jobMock, machineLearningMock, searchMock, systemMock } =
|
({ sut, assetMock, databaseMock, jobMock, machineLearningMock, searchMock, systemMock, configMock } =
|
||||||
newTestService(SmartInfoService));
|
newTestService(SmartInfoService));
|
||||||
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
configMock.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -63,11 +66,11 @@ describe(SmartInfoService.name, () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onBootstrapEvent', () => {
|
describe('onConfigInit', () => {
|
||||||
it('should return if not microservices', async () => {
|
it('should return if not microservices', async () => {
|
||||||
await sut.onBootstrap(ImmichWorker.API);
|
configMock.getWorker.mockReturnValue(ImmichWorker.API);
|
||||||
|
await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(systemMock.get).not.toHaveBeenCalled();
|
|
||||||
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
||||||
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||||
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||||
|
@ -78,11 +81,8 @@ describe(SmartInfoService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return if machine learning is disabled', async () => {
|
it('should return if machine learning is disabled', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningDisabled as SystemConfig });
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
|
||||||
|
|
||||||
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
|
||||||
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
||||||
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||||
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||||
|
@ -95,9 +95,8 @@ describe(SmartInfoService.name, () => {
|
||||||
it('should return if model and DB dimension size are equal', async () => {
|
it('should return if model and DB dimension size are equal', async () => {
|
||||||
searchMock.getDimensionSize.mockResolvedValue(512);
|
searchMock.getDimensionSize.mockResolvedValue(512);
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
|
||||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||||
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||||
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||||
|
@ -111,9 +110,8 @@ describe(SmartInfoService.name, () => {
|
||||||
searchMock.getDimensionSize.mockResolvedValue(768);
|
searchMock.getDimensionSize.mockResolvedValue(768);
|
||||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
|
||||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||||
expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512);
|
expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512);
|
||||||
expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
|
expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||||
|
@ -126,9 +124,8 @@ describe(SmartInfoService.name, () => {
|
||||||
searchMock.getDimensionSize.mockResolvedValue(768);
|
searchMock.getDimensionSize.mockResolvedValue(768);
|
||||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true });
|
||||||
|
|
||||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
await sut.onConfigInit({ newConfig: systemConfigStub.machineLearningEnabled as SystemConfig });
|
||||||
|
|
||||||
expect(systemMock.get).toHaveBeenCalledTimes(1);
|
|
||||||
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1);
|
||||||
expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512);
|
expect(searchMock.setDimensionSize).toHaveBeenCalledWith(512);
|
||||||
expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
|
expect(jobMock.getQueueStatus).toHaveBeenCalledTimes(1);
|
||||||
|
@ -139,6 +136,22 @@ describe(SmartInfoService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onConfigUpdateEvent', () => {
|
describe('onConfigUpdateEvent', () => {
|
||||||
|
it('should return if not microservices', async () => {
|
||||||
|
configMock.getWorker.mockReturnValue(ImmichWorker.API);
|
||||||
|
await sut.onConfigUpdate({
|
||||||
|
newConfig: systemConfigStub.machineLearningEnabled as SystemConfig,
|
||||||
|
oldConfig: systemConfigStub.machineLearningEnabled as SystemConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(searchMock.getDimensionSize).not.toHaveBeenCalled();
|
||||||
|
expect(searchMock.setDimensionSize).not.toHaveBeenCalled();
|
||||||
|
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
||||||
|
expect(jobMock.getQueueStatus).not.toHaveBeenCalled();
|
||||||
|
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||||
|
expect(jobMock.waitForQueueCompletion).not.toHaveBeenCalled();
|
||||||
|
expect(jobMock.resume).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should return if machine learning is disabled', async () => {
|
it('should return if machine learning is disabled', async () => {
|
||||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||||
|
|
||||||
|
|
|
@ -13,17 +13,12 @@ import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SmartInfoService extends BaseService {
|
export class SmartInfoService extends BaseService {
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'config.init' })
|
||||||
async onBootstrap(app: ArgOf<'app.bootstrap'>) {
|
async onConfigInit({ newConfig }: ArgOf<'config.init'>) {
|
||||||
if (app !== ImmichWorker.MICROSERVICES) {
|
await this.init(newConfig);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = await this.getConfig({ withCache: false });
|
|
||||||
await this.init(config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'config.update' })
|
@OnEvent({ name: 'config.update', server: true })
|
||||||
async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
|
async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
|
||||||
await this.init(newConfig, oldConfig);
|
await this.init(newConfig, oldConfig);
|
||||||
}
|
}
|
||||||
|
@ -40,7 +35,7 @@ export class SmartInfoService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init(newConfig: SystemConfig, oldConfig?: SystemConfig) {
|
private async init(newConfig: SystemConfig, oldConfig?: SystemConfig) {
|
||||||
if (!isSmartSearchEnabled(newConfig.machineLearning)) {
|
if (this.worker !== ImmichWorker.MICROSERVICES || !isSmartSearchEnabled(newConfig.machineLearning)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe(StorageTemplateService.name, () => {
|
||||||
|
|
||||||
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } });
|
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } });
|
||||||
|
|
||||||
sut.onConfigUpdate({ newConfig: defaults });
|
sut.onConfigInitOrUpdate({ newConfig: defaults });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onConfigValidate', () => {
|
describe('onConfigValidate', () => {
|
||||||
|
@ -171,7 +171,7 @@ describe(StorageTemplateService.name, () => {
|
||||||
const config = structuredClone(defaults);
|
const config = structuredClone(defaults);
|
||||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}';
|
||||||
|
|
||||||
sut.onConfigUpdate({ oldConfig: defaults, newConfig: config });
|
sut.onConfigInitOrUpdate({ newConfig: config });
|
||||||
|
|
||||||
userMock.get.mockResolvedValue(user);
|
userMock.get.mockResolvedValue(user);
|
||||||
assetMock.getByIds.mockResolvedValueOnce([asset]);
|
assetMock.getByIds.mockResolvedValueOnce([asset]);
|
||||||
|
@ -192,7 +192,7 @@ describe(StorageTemplateService.name, () => {
|
||||||
const user = userStub.user1;
|
const user = userStub.user1;
|
||||||
const config = structuredClone(defaults);
|
const config = structuredClone(defaults);
|
||||||
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}';
|
config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}';
|
||||||
sut.onConfigUpdate({ oldConfig: defaults, newConfig: config });
|
sut.onConfigInitOrUpdate({ newConfig: config });
|
||||||
|
|
||||||
userMock.get.mockResolvedValue(user);
|
userMock.get.mockResolvedValue(user);
|
||||||
assetMock.getByIds.mockResolvedValueOnce([asset]);
|
assetMock.getByIds.mockResolvedValueOnce([asset]);
|
||||||
|
|
|
@ -74,8 +74,9 @@ export class StorageTemplateService extends BaseService {
|
||||||
return this._template;
|
return this._template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'config.init' })
|
||||||
@OnEvent({ name: 'config.update', server: true })
|
@OnEvent({ name: 'config.update', server: true })
|
||||||
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
|
onConfigInitOrUpdate({ newConfig }: ArgOf<'config.init'>) {
|
||||||
const template = newConfig.storageTemplate.template;
|
const template = newConfig.storageTemplate.template;
|
||||||
if (!this._template || template !== this.template.raw) {
|
if (!this._template || template !== this.template.raw) {
|
||||||
this.logger.debug(`Compiling new storage template: ${template}`);
|
this.logger.debug(`Compiling new storage template: ${template}`);
|
||||||
|
|
|
@ -4,17 +4,17 @@ import _ from 'lodash';
|
||||||
import { defaults } from 'src/config';
|
import { defaults } from 'src/config';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { SystemConfigDto, mapConfig } from 'src/dtos/system-config.dto';
|
import { SystemConfigDto, mapConfig } from 'src/dtos/system-config.dto';
|
||||||
import { ArgOf } from 'src/interfaces/event.interface';
|
import { ArgOf, BootstrapEventPriority } from 'src/interfaces/event.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { clearConfigCache } from 'src/utils/config';
|
import { clearConfigCache } from 'src/utils/config';
|
||||||
import { toPlainObject } from 'src/utils/object';
|
import { toPlainObject } from 'src/utils/object';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SystemConfigService extends BaseService {
|
export class SystemConfigService extends BaseService {
|
||||||
@OnEvent({ name: 'app.bootstrap', priority: -100 })
|
@OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.SystemConfig })
|
||||||
async onBootstrap() {
|
async onBootstrap() {
|
||||||
const config = await this.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
await this.eventRepository.emit('config.update', { newConfig: config });
|
await this.eventRepository.emit('config.init', { newConfig: config });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSystemConfig(): Promise<SystemConfigDto> {
|
async getSystemConfig(): Promise<SystemConfigDto> {
|
||||||
|
@ -26,14 +26,18 @@ export class SystemConfigService extends BaseService {
|
||||||
return mapConfig(defaults);
|
return mapConfig(defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'config.update', server: true })
|
@OnEvent({ name: 'config.init' })
|
||||||
onConfigUpdate({ newConfig: { logging } }: ArgOf<'config.update'>) {
|
onConfigInit({ newConfig: { logging } }: ArgOf<'config.init'>) {
|
||||||
const { logLevel: envLevel } = this.configRepository.getEnv();
|
const { logLevel: envLevel } = this.configRepository.getEnv();
|
||||||
const configLevel = logging.enabled ? logging.level : false;
|
const configLevel = logging.enabled ? logging.level : false;
|
||||||
const level = envLevel ?? configLevel;
|
const level = envLevel ?? configLevel;
|
||||||
this.logger.setLogLevel(level);
|
this.logger.setLogLevel(level);
|
||||||
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
|
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
|
||||||
// TODO only do this if the event is a socket.io event
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'config.update', server: true })
|
||||||
|
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
|
||||||
|
this.onConfigInit({ newConfig });
|
||||||
clearConfigCache();
|
clearConfigCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
29
server/test/fixtures/system-config.stub.ts
vendored
29
server/test/fixtures/system-config.stub.ts
vendored
|
@ -54,6 +54,9 @@ export const systemConfigStub = {
|
||||||
},
|
},
|
||||||
libraryWatchEnabled: {
|
libraryWatchEnabled: {
|
||||||
library: {
|
library: {
|
||||||
|
scan: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
@ -61,6 +64,9 @@ export const systemConfigStub = {
|
||||||
},
|
},
|
||||||
libraryWatchDisabled: {
|
libraryWatchDisabled: {
|
||||||
library: {
|
library: {
|
||||||
|
scan: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
@ -72,6 +78,20 @@ export const systemConfigStub = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
cronExpression: '0 0 * * *',
|
cronExpression: '0 0 * * *',
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
libraryScanAndWatch: {
|
||||||
|
library: {
|
||||||
|
scan: {
|
||||||
|
enabled: true,
|
||||||
|
cronExpression: '0 0 * * *',
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
backupEnabled: {
|
backupEnabled: {
|
||||||
|
@ -88,4 +108,13 @@ export const systemConfigStub = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
machineLearningEnabled: {
|
||||||
|
machineLearning: {
|
||||||
|
enabled: true,
|
||||||
|
clip: {
|
||||||
|
modelName: 'ViT-B-16__openai',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} satisfies Record<string, DeepPartial<SystemConfig>>;
|
} satisfies Record<string, DeepPartial<SystemConfig>>;
|
||||||
|
|
Loading…
Reference in a new issue