From db1623f43f2b2f22abb5bb9fee1b73ce49b3e828 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jason@rasm.me>
Date: Thu, 3 Oct 2024 15:28:36 -0400
Subject: [PATCH] refactor(server): worker env (#13160)

---
 server/bin/immich-healthcheck                 |  2 +-
 server/package.json                           |  1 -
 server/src/{utils => bin}/healthcheck.ts      |  8 +--
 server/src/enum.ts                            |  5 ++
 server/src/interfaces/config.interface.ts     |  4 +-
 server/src/main.ts                            | 52 +++++++++----------
 .../config.repository.spec.ts}                |  4 +-
 server/src/repositories/config.repository.ts  | 20 ++++++-
 server/src/utils/workers.ts                   | 21 --------
 .../repositories/config.repository.mock.ts    |  4 +-
 10 files changed, 63 insertions(+), 58 deletions(-)
 rename server/src/{utils => bin}/healthcheck.ts (69%)
 rename server/src/{utils/workers.spec.ts => repositories/config.repository.spec.ts} (92%)
 delete mode 100644 server/src/utils/workers.ts

diff --git a/server/bin/immich-healthcheck b/server/bin/immich-healthcheck
index 6043e526aa..cf0accb8dd 100755
--- a/server/bin/immich-healthcheck
+++ b/server/bin/immich-healthcheck
@@ -1,3 +1,3 @@
 #!/usr/bin/env bash
 
-node /usr/src/app/dist/utils/healthcheck.js
+node /usr/src/app/dist/bin/healthcheck.js
diff --git a/server/package.json b/server/package.json
index fbbc2c4892..5e0e0a6705 100644
--- a/server/package.json
+++ b/server/package.json
@@ -18,7 +18,6 @@
     "check": "tsc --noEmit",
     "check:code": "npm run format && npm run lint && npm run check",
     "check:all": "npm run check:code && npm run test:cov",
-    "healthcheck": "node ./dist/utils/healthcheck.js",
     "test": "vitest",
     "test:watch": "vitest --watch",
     "test:cov": "vitest --coverage",
diff --git a/server/src/utils/healthcheck.ts b/server/src/bin/healthcheck.ts
similarity index 69%
rename from server/src/utils/healthcheck.ts
rename to server/src/bin/healthcheck.ts
index 763fce81b4..b38d9d17df 100644
--- a/server/src/utils/healthcheck.ts
+++ b/server/src/bin/healthcheck.ts
@@ -1,12 +1,14 @@
 #!/usr/bin/env node
-const port = Number(process.env.IMMICH_PORT) || 3001;
-const controller = new AbortController();
+import { ImmichWorker } from 'src/enum';
+import { ConfigRepository } from 'src/repositories/config.repository';
 
 const main = async () => {
-  if (!process.env.IMMICH_WORKERS_INCLUDE?.includes('api')) {
+  const { workers, port } = new ConfigRepository().getEnv();
+  if (!workers.includes(ImmichWorker.API)) {
     process.exit();
   }
 
+  const controller = new AbortController();
   const timeout = setTimeout(() => controller.abort(), 2000);
   try {
     const response = await fetch(`http://localhost:${port}/api/server-info/ping`, {
diff --git a/server/src/enum.ts b/server/src/enum.ts
index d1a76573d1..109e9a90b7 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -334,3 +334,8 @@ export enum ImmichEnvironment {
   TESTING = 'testing',
   PRODUCTION = 'production',
 }
+
+export enum ImmichWorker {
+  API = 'api',
+  MICROSERVICES = 'microservices',
+}
diff --git a/server/src/interfaces/config.interface.ts b/server/src/interfaces/config.interface.ts
index fe0c809bf8..3787684b8d 100644
--- a/server/src/interfaces/config.interface.ts
+++ b/server/src/interfaces/config.interface.ts
@@ -1,4 +1,4 @@
-import { ImmichEnvironment, LogLevel } from 'src/enum';
+import { ImmichEnvironment, ImmichWorker, LogLevel } from 'src/enum';
 import { VectorExtension } from 'src/interfaces/database.interface';
 
 export const IConfigRepository = 'IConfigRepository';
@@ -18,6 +18,8 @@ export interface EnvData {
     ignoreMountCheckErrors: boolean;
   };
 
+  workers: ImmichWorker[];
+
   nodeVersion?: string;
 }
 
diff --git a/server/src/main.ts b/server/src/main.ts
index 48ce179e88..11cc44ec10 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -2,20 +2,15 @@ import { CommandFactory } from 'nest-commander';
 import { fork } from 'node:child_process';
 import { Worker } from 'node:worker_threads';
 import { ImmichAdminModule } from 'src/app.module';
-import { LogLevel } from 'src/enum';
-import { getWorkers } from 'src/utils/workers';
-const immichApp = process.argv[2] || process.env.IMMICH_APP;
+import { ImmichWorker, LogLevel } from 'src/enum';
+import { ConfigRepository } from 'src/repositories/config.repository';
 
-if (process.argv[2] === immichApp) {
+const immichApp = process.argv[2];
+if (immichApp) {
   process.argv.splice(2, 1);
 }
 
-async function bootstrapImmichAdmin() {
-  process.env.IMMICH_LOG_LEVEL = LogLevel.WARN;
-  await CommandFactory.run(ImmichAdminModule);
-}
-
-function bootstrapWorker(name: string) {
+function bootstrapWorker(name: ImmichWorker) {
   console.log(`Starting ${name} worker`);
 
   const execArgv = process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg));
@@ -35,26 +30,27 @@ function bootstrapWorker(name: string) {
 }
 
 function bootstrap() {
-  switch (immichApp) {
-    case 'immich-admin': {
-      process.title = 'immich_admin_cli';
-      return bootstrapImmichAdmin();
-    }
-    case 'immich': {
-      if (!process.env.IMMICH_WORKERS_INCLUDE) {
-        process.env.IMMICH_WORKERS_INCLUDE = 'api';
-      }
-      break;
-    }
-    case 'microservices': {
-      if (!process.env.IMMICH_WORKERS_INCLUDE) {
-        process.env.IMMICH_WORKERS_INCLUDE = 'microservices';
-      }
-      break;
-    }
+  if (immichApp === 'immich-admin') {
+    process.title = 'immich_admin_cli';
+    process.env.IMMICH_LOG_LEVEL = LogLevel.WARN;
+    return CommandFactory.run(ImmichAdminModule);
   }
+
+  if (immichApp === 'immich' || immichApp === 'microservices') {
+    console.error(
+      `Using "start.sh ${immichApp}" has been deprecated. See https://github.com/immich-app/immich/releases/tag/v1.118.0 for more information.`,
+    );
+    process.exit(1);
+  }
+
+  if (immichApp) {
+    console.error(`Unknown command: "${immichApp}"`);
+    process.exit(1);
+  }
+
   process.title = 'immich';
-  for (const worker of getWorkers()) {
+  const { workers } = new ConfigRepository().getEnv();
+  for (const worker of workers) {
     bootstrapWorker(worker);
   }
 }
diff --git a/server/src/utils/workers.spec.ts b/server/src/repositories/config.repository.spec.ts
similarity index 92%
rename from server/src/utils/workers.spec.ts
rename to server/src/repositories/config.repository.spec.ts
index 1e4ff5e2d3..2fbd76cefb 100644
--- a/server/src/utils/workers.spec.ts
+++ b/server/src/repositories/config.repository.spec.ts
@@ -1,4 +1,6 @@
-import { getWorkers } from 'src/utils/workers';
+import { ConfigRepository } from 'src/repositories/config.repository';
+
+const getWorkers = () => new ConfigRepository().getEnv().workers;
 
 describe('getWorkers', () => {
   beforeEach(() => {
diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts
index 9c65a5608b..df4e426886 100644
--- a/server/src/repositories/config.repository.ts
+++ b/server/src/repositories/config.repository.ts
@@ -1,13 +1,30 @@
 import { Injectable } from '@nestjs/common';
 import { getVectorExtension } from 'src/database.config';
-import { ImmichEnvironment, LogLevel } from 'src/enum';
+import { ImmichEnvironment, ImmichWorker, LogLevel } from 'src/enum';
 import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
+import { setDifference } from 'src/utils/set';
 
 // TODO replace src/config validation with class-validator, here
 
+const WORKER_TYPES = new Set(Object.values(ImmichWorker));
+
+const asSet = (value: string | undefined, defaults: ImmichWorker[]) => {
+  const values = (value || '').replaceAll(/\s/g, '').split(',').filter(Boolean);
+  return new Set(values.length === 0 ? defaults : (values as ImmichWorker[]));
+};
+
 @Injectable()
 export class ConfigRepository implements IConfigRepository {
   getEnv(): EnvData {
+    const included = asSet(process.env.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]);
+    const excluded = asSet(process.env.IMMICH_WORKERS_EXCLUDE, []);
+    const workers = [...setDifference(included, excluded)];
+    for (const worker of workers) {
+      if (!WORKER_TYPES.has(worker)) {
+        throw new Error(`Invalid worker(s) found: ${workers.join(',')}`);
+      }
+    }
+
     return {
       port: Number(process.env.IMMICH_PORT) || 3001,
       environment: process.env.IMMICH_ENV as ImmichEnvironment,
@@ -20,6 +37,7 @@ export class ConfigRepository implements IConfigRepository {
       storage: {
         ignoreMountCheckErrors: process.env.IMMICH_IGNORE_MOUNT_CHECK_ERRORS === 'true',
       },
+      workers,
     };
   }
 }
diff --git a/server/src/utils/workers.ts b/server/src/utils/workers.ts
deleted file mode 100644
index 14daa2620f..0000000000
--- a/server/src/utils/workers.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-const WORKER_TYPES = new Set(['api', 'microservices']);
-
-export const getWorkers = () => {
-  let workers = ['api', 'microservices'];
-  const includedWorkers = process.env.IMMICH_WORKERS_INCLUDE?.replaceAll(/\s/g, '');
-  const excludedWorkers = process.env.IMMICH_WORKERS_EXCLUDE?.replaceAll(/\s/g, '');
-
-  if (includedWorkers) {
-    workers = includedWorkers.split(',');
-  }
-
-  if (excludedWorkers) {
-    workers = workers.filter((worker) => !excludedWorkers.split(',').includes(worker));
-  }
-
-  if (workers.some((worker) => !WORKER_TYPES.has(worker))) {
-    throw new Error(`Invalid worker(s) found: ${workers}`);
-  }
-
-  return workers;
-};
diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts
index 41bb8208b9..daf002335f 100644
--- a/server/test/repositories/config.repository.mock.ts
+++ b/server/test/repositories/config.repository.mock.ts
@@ -1,4 +1,4 @@
-import { ImmichEnvironment } from 'src/enum';
+import { ImmichEnvironment, ImmichWorker } from 'src/enum';
 import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
 import { DatabaseExtension } from 'src/interfaces/database.interface';
 import { Mocked, vitest } from 'vitest';
@@ -15,6 +15,8 @@ const envData: EnvData = {
   storage: {
     ignoreMountCheckErrors: false,
   },
+
+  workers: [ImmichWorker.API, ImmichWorker.MICROSERVICES],
 };
 
 export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {