mirror of
https://github.com/immich-app/immich.git
synced 2025-12-26 01:11:47 +03:00
feat: queues (#24142)
This commit is contained in:
@@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum';
|
||||
import { QueueService } from 'src/services/queue.service';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(QueueService.name, () => {
|
||||
@@ -52,80 +53,64 @@ describe(QueueService.name, () => {
|
||||
|
||||
describe('getAllJobStatus', () => {
|
||||
it('should get all job statuses', async () => {
|
||||
mocks.job.getJobCounts.mockResolvedValue({
|
||||
active: 1,
|
||||
completed: 1,
|
||||
failed: 1,
|
||||
delayed: 1,
|
||||
waiting: 1,
|
||||
paused: 1,
|
||||
});
|
||||
mocks.job.getQueueStatus.mockResolvedValue({
|
||||
isActive: true,
|
||||
isPaused: true,
|
||||
});
|
||||
const stats = factory.queueStatistics({ active: 1 });
|
||||
const expected = { jobCounts: stats, queueStatus: { isActive: true, isPaused: true } };
|
||||
|
||||
const expectedJobStatus = {
|
||||
jobCounts: {
|
||||
active: 1,
|
||||
completed: 1,
|
||||
delayed: 1,
|
||||
failed: 1,
|
||||
waiting: 1,
|
||||
paused: 1,
|
||||
},
|
||||
queueStatus: {
|
||||
isActive: true,
|
||||
isPaused: true,
|
||||
},
|
||||
};
|
||||
mocks.job.getJobCounts.mockResolvedValue(stats);
|
||||
mocks.job.isPaused.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.getAll()).resolves.toEqual({
|
||||
[QueueName.BackgroundTask]: expectedJobStatus,
|
||||
[QueueName.DuplicateDetection]: expectedJobStatus,
|
||||
[QueueName.SmartSearch]: expectedJobStatus,
|
||||
[QueueName.MetadataExtraction]: expectedJobStatus,
|
||||
[QueueName.Search]: expectedJobStatus,
|
||||
[QueueName.StorageTemplateMigration]: expectedJobStatus,
|
||||
[QueueName.Migration]: expectedJobStatus,
|
||||
[QueueName.ThumbnailGeneration]: expectedJobStatus,
|
||||
[QueueName.VideoConversion]: expectedJobStatus,
|
||||
[QueueName.FaceDetection]: expectedJobStatus,
|
||||
[QueueName.FacialRecognition]: expectedJobStatus,
|
||||
[QueueName.Sidecar]: expectedJobStatus,
|
||||
[QueueName.Library]: expectedJobStatus,
|
||||
[QueueName.Notification]: expectedJobStatus,
|
||||
[QueueName.BackupDatabase]: expectedJobStatus,
|
||||
[QueueName.Ocr]: expectedJobStatus,
|
||||
[QueueName.Workflow]: expectedJobStatus,
|
||||
await expect(sut.getAllLegacy(factory.auth())).resolves.toEqual({
|
||||
[QueueName.BackgroundTask]: expected,
|
||||
[QueueName.DuplicateDetection]: expected,
|
||||
[QueueName.SmartSearch]: expected,
|
||||
[QueueName.MetadataExtraction]: expected,
|
||||
[QueueName.Search]: expected,
|
||||
[QueueName.StorageTemplateMigration]: expected,
|
||||
[QueueName.Migration]: expected,
|
||||
[QueueName.ThumbnailGeneration]: expected,
|
||||
[QueueName.VideoConversion]: expected,
|
||||
[QueueName.FaceDetection]: expected,
|
||||
[QueueName.FacialRecognition]: expected,
|
||||
[QueueName.Sidecar]: expected,
|
||||
[QueueName.Library]: expected,
|
||||
[QueueName.Notification]: expected,
|
||||
[QueueName.BackupDatabase]: expected,
|
||||
[QueueName.Ocr]: expected,
|
||||
[QueueName.Workflow]: expected,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCommand', () => {
|
||||
it('should handle a pause command', async () => {
|
||||
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Pause, force: false });
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Pause, force: false });
|
||||
|
||||
expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||
});
|
||||
|
||||
it('should handle a resume command', async () => {
|
||||
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Resume, force: false });
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Resume, force: false });
|
||||
|
||||
expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||
});
|
||||
|
||||
it('should handle an empty command', async () => {
|
||||
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Empty, force: false });
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Empty, force: false });
|
||||
|
||||
expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||
});
|
||||
|
||||
it('should not start a job that is already running', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }),
|
||||
sut.runCommandLegacy(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
@@ -133,33 +118,37 @@ describe(QueueService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle a start video conversion command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false });
|
||||
await sut.runCommandLegacy(QueueName.VideoConversion, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start storage template migration command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommand(QueueName.StorageTemplateMigration, { command: QueueCommand.Start, force: false });
|
||||
await sut.runCommandLegacy(QueueName.StorageTemplateMigration, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration });
|
||||
});
|
||||
|
||||
it('should handle a start smart search command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommand(QueueName.SmartSearch, { command: QueueCommand.Start, force: false });
|
||||
await sut.runCommandLegacy(QueueName.SmartSearch, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start metadata extraction command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false });
|
||||
await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.AssetExtractMetadataQueueAll,
|
||||
@@ -168,17 +157,19 @@ describe(QueueService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle a start sidecar command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommand(QueueName.Sidecar, { command: QueueCommand.Start, force: false });
|
||||
await sut.runCommandLegacy(QueueName.Sidecar, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start thumbnail generation command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommand(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false });
|
||||
await sut.runCommandLegacy(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.AssetGenerateThumbnailsQueueAll,
|
||||
@@ -187,34 +178,37 @@ describe(QueueService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle a start face detection command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommand(QueueName.FaceDetection, { command: QueueCommand.Start, force: false });
|
||||
await sut.runCommandLegacy(QueueName.FaceDetection, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start facial recognition command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommand(QueueName.FacialRecognition, { command: QueueCommand.Start, force: false });
|
||||
await sut.runCommandLegacy(QueueName.FacialRecognition, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start backup database command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics());
|
||||
|
||||
await sut.runCommand(QueueName.BackupDatabase, { command: QueueCommand.Start, force: false });
|
||||
await sut.runCommandLegacy(QueueName.BackupDatabase, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should throw a bad request when an invalid queue is used', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
mocks.job.isActive.mockResolvedValue(false);
|
||||
|
||||
await expect(
|
||||
sut.runCommand(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }),
|
||||
sut.runCommandLegacy(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user