From 85aca2bb54e79d32704164a2c6bf06382f61d6ae Mon Sep 17 00:00:00 2001 From: Zack Pollard <zackpollard@ymail.com> Date: Fri, 17 May 2024 14:44:30 +0100 Subject: [PATCH] feat: microservices be gone (#9551) * feat: microservices be gone and api is a worker now too * chore: remove very old startup scripts, surely nobody is using these anymore, right? right?.... --- docker/docker-compose.dev.yml | 56 +++++++---------- docker/docker-compose.prod.yml | 36 ++++------- docker/docker-compose.yml | 18 ------ e2e/docker-compose.yml | 44 ++++++-------- server/Dockerfile | 1 + server/src/main.ts | 93 ++++++----------------------- server/src/utils/workers.spec.ts | 49 +++++++++++++++ server/src/utils/workers.ts | 21 +++++++ server/src/workers/api.ts | 68 +++++++++++++++++++++ server/src/workers/microservices.ts | 4 +- server/start-microservices.sh | 3 - server/start-server.sh | 3 - 12 files changed, 206 insertions(+), 190 deletions(-) create mode 100644 server/src/utils/workers.spec.ts create mode 100644 server/src/utils/workers.ts create mode 100644 server/src/workers/api.ts delete mode 100755 server/start-microservices.sh delete mode 100755 server/start-server.sh diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 5b3685ac8d..f18a563b34 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -4,32 +4,29 @@ name: immich-dev -x-server-build: &server-common - image: immich-server-dev:latest - build: - context: ../ - dockerfile: server/Dockerfile - target: dev - restart: always - volumes: - - ../server:/usr/src/app - - ../open-api:/usr/src/open-api - - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - - ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload - - /usr/src/app/node_modules - - /etc/localtime:/etc/localtime:ro - env_file: - - .env - ulimits: - nofile: - soft: 1048576 - hard: 1048576 - services: immich-server: container_name: immich_server - command: ['/usr/src/app/bin/immich-dev', 'immich'] - <<: *server-common + command: ['/usr/src/app/bin/immich-dev'] + image: immich-server-dev:latest + build: + context: ../ + dockerfile: server/Dockerfile + target: dev + restart: always + volumes: + - ../server:/usr/src/app + - ../open-api:/usr/src/open-api + - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload + - ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload + - /usr/src/app/node_modules + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + ulimits: + nofile: + soft: 1048576 + hard: 1048576 ports: - 3001:3001 - 9230:9230 @@ -37,19 +34,6 @@ services: - redis - database - immich-microservices: - container_name: immich_microservices - command: ['/usr/src/app/bin/immich-dev', 'microservices'] - <<: *server-common - # extends: - # file: hwaccel.transcoding.yml - # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding - ports: - - 9231:9230 - depends_on: - - database - - immich-server - immich-web: container_name: immich_web image: immich-web-dev:latest diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 7901f58ef6..fc62ef0c5b 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -1,40 +1,24 @@ name: immich-prod -x-server-build: &server-common - image: immich-server:latest - build: - context: ../ - dockerfile: server/Dockerfile - volumes: - - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - - /etc/localtime:/etc/localtime:ro - env_file: - - .env - restart: always - services: immich-server: container_name: immich_server - command: ['start.sh', 'immich'] - <<: *server-common + image: immich-server:latest + build: + context: ../ + dockerfile: server/Dockerfile + volumes: + - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + restart: always ports: - 2283:3001 depends_on: - redis - database - immich-microservices: - container_name: immich_microservices - command: ['start.sh', 'microservices'] - <<: *server-common - # extends: - # file: hwaccel.transcoding.yml - # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding - depends_on: - - redis - - database - - immich-server - immich-machine-learning: container_name: immich_machine_learning image: immich-machine-learning:latest diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 11a2a626f0..16d628258e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -12,7 +12,6 @@ services: immich-server: container_name: immich_server image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} - command: ['start.sh', 'immich'] volumes: - ${UPLOAD_LOCATION}:/usr/src/app/upload - /etc/localtime:/etc/localtime:ro @@ -25,23 +24,6 @@ services: - database restart: always - immich-microservices: - container_name: immich_microservices - image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} - # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding - # file: hwaccel.transcoding.yml - # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding - command: ['start.sh', 'microservices'] - volumes: - - ${UPLOAD_LOCATION}:/usr/src/app/upload - - /etc/localtime:/etc/localtime:ro - env_file: - - .env - depends_on: - - redis - - database - restart: always - immich-machine-learning: container_name: immich_machine_learning # For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag. diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 703a07254e..5d86112389 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -2,38 +2,30 @@ version: '3.8' name: immich-e2e -x-server-build: &server-common - image: immich-server:latest - build: - context: ../ - dockerfile: server/Dockerfile - environment: - - DB_HOSTNAME=database - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - DB_DATABASE_NAME=immich - - IMMICH_MACHINE_LEARNING_ENABLED=false - - IMMICH_METRICS=true - volumes: - - upload:/usr/src/app/upload - - ./test-assets:/test-assets - depends_on: - - redis - - database - services: immich-server: container_name: immich-e2e-server - command: ['./start.sh', 'immich'] - <<: *server-common + command: ['./start.sh'] + image: immich-server:latest + build: + context: ../ + dockerfile: server/Dockerfile + environment: + - DB_HOSTNAME=database + - DB_USERNAME=postgres + - DB_PASSWORD=postgres + - DB_DATABASE_NAME=immich + - IMMICH_MACHINE_LEARNING_ENABLED=false + - IMMICH_METRICS=true + volumes: + - upload:/usr/src/app/upload + - ./test-assets:/test-assets + depends_on: + - redis + - database ports: - 2283:3001 - immich-microservices: - container_name: immich-e2e-microservices - command: ['./start.sh', 'microservices'] - <<: *server-common - redis: image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672 diff --git a/server/Dockerfile b/server/Dockerfile index 20bad262f5..8acdf91d05 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -61,3 +61,4 @@ ENV PATH="${PATH}:/usr/src/app/bin" VOLUME /usr/src/app/upload EXPOSE 3001 ENTRYPOINT ["tini", "--", "/bin/bash"] +CMD ["start.sh"] diff --git a/server/src/main.ts b/server/src/main.ts index 0d30fe7832..68cf73dd13 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,67 +1,8 @@ -import { NestFactory } from '@nestjs/core'; -import { NestExpressApplication } from '@nestjs/platform-express'; -import { json } from 'body-parser'; -import cookieParser from 'cookie-parser'; import { CommandFactory } from 'nest-commander'; -import { existsSync } from 'node:fs'; import { Worker } from 'node:worker_threads'; -import sirv from 'sirv'; -import { ApiModule, ImmichAdminModule } from 'src/app.module'; +import { ImmichAdminModule } from 'src/app.module'; import { LogLevel } from 'src/config'; -import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; -import { ApiService } from 'src/services/api.service'; -import { otelSDK } from 'src/utils/instrumentation'; -import { useSwagger } from 'src/utils/misc'; - -const host = process.env.HOST; - -async function bootstrapApi() { - otelSDK.start(); - - const port = Number(process.env.SERVER_PORT) || 3001; - const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true }); - const logger = await app.resolve(ILoggerRepository); - - logger.setAppName('ImmichServer'); - logger.setContext('ImmichServer'); - app.useLogger(logger); - app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); - app.set('etag', 'strong'); - app.use(cookieParser()); - app.use(json({ limit: '10mb' })); - if (isDev) { - app.enableCors(); - } - app.useWebSocketAdapter(new WebSocketAdapter(app)); - useSwagger(app, isDev); - - app.setGlobalPrefix('api', { exclude: excludePaths }); - if (existsSync(WEB_ROOT)) { - // copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46 - // provides serving of precompressed assets and caching of immutable assets - app.use( - sirv(WEB_ROOT, { - etag: true, - gzip: true, - brotli: true, - setHeaders: (res, pathname) => { - if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) { - res.setHeader('cache-control', 'public,max-age=31536000,immutable'); - } - }, - }), - ); - } - app.use(app.get(ApiService).ssr(excludePaths)); - - const server = await (host ? app.listen(port, host) : app.listen(port)); - server.requestTimeout = 30 * 60 * 1000; - - logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); -} - +import { getWorkers } from 'src/utils/workers'; const immichApp = process.argv[2] || process.env.IMMICH_APP; if (process.argv[2] === immichApp) { @@ -73,11 +14,12 @@ async function bootstrapImmichAdmin() { await CommandFactory.run(ImmichAdminModule); } -function bootstrapMicroservicesWorker() { - const worker = new Worker('./dist/workers/microservices.js'); +function bootstrapWorker(name: string) { + console.log(`Starting ${name} worker`); + const worker = new Worker(`./dist/workers/${name}.js`); worker.on('exit', (exitCode) => { if (exitCode !== 0) { - console.error(`Microservices worker exited with code ${exitCode}`); + console.error(`${name} worker exited with code ${exitCode}`); process.exit(exitCode); } }); @@ -85,23 +27,22 @@ function bootstrapMicroservicesWorker() { function bootstrap() { switch (immichApp) { - case 'immich': { - process.title = 'immich_server'; - if (process.env.INTERNAL_MICROSERVICES === 'true') { - bootstrapMicroservicesWorker(); - } - return bootstrapApi(); - } - case 'microservices': { - process.title = 'immich_microservices'; - return bootstrapMicroservicesWorker(); - } case 'immich-admin': { process.title = 'immich_admin_cli'; return bootstrapImmichAdmin(); } + case 'immich': { + process.title = 'immich_server'; + return bootstrapWorker('api'); + } + case 'microservices': { + process.title = 'immich_microservices'; + return bootstrapWorker('microservices'); + } default: { - throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|immich-admin`); + for (const worker of getWorkers()) { + bootstrapWorker(worker); + } } } } diff --git a/server/src/utils/workers.spec.ts b/server/src/utils/workers.spec.ts new file mode 100644 index 0000000000..1e4ff5e2d3 --- /dev/null +++ b/server/src/utils/workers.spec.ts @@ -0,0 +1,49 @@ +import { getWorkers } from 'src/utils/workers'; + +describe('getWorkers', () => { + beforeEach(() => { + process.env.IMMICH_WORKERS_INCLUDE = ''; + process.env.IMMICH_WORKERS_EXCLUDE = ''; + }); + + it('should return default workers', () => { + expect(getWorkers()).toEqual(['api', 'microservices']); + }); + + it('should return included workers', () => { + process.env.IMMICH_WORKERS_INCLUDE = 'api'; + expect(getWorkers()).toEqual(['api']); + }); + + it('should excluded workers from defaults', () => { + process.env.IMMICH_WORKERS_EXCLUDE = 'api'; + expect(getWorkers()).toEqual(['microservices']); + }); + + it('should exclude workers from include list', () => { + process.env.IMMICH_WORKERS_INCLUDE = 'api,microservices,randomservice'; + process.env.IMMICH_WORKERS_EXCLUDE = 'randomservice,microservices'; + expect(getWorkers()).toEqual(['api']); + }); + + it('should remove whitespace from included workers before parsing', () => { + process.env.IMMICH_WORKERS_INCLUDE = 'api, microservices'; + expect(getWorkers()).toEqual(['api', 'microservices']); + }); + + it('should remove whitespace from excluded workers before parsing', () => { + process.env.IMMICH_WORKERS_EXCLUDE = 'api, microservices'; + expect(getWorkers()).toEqual([]); + }); + + it('should remove whitespace from included and excluded workers before parsing', () => { + process.env.IMMICH_WORKERS_INCLUDE = 'api, microservices, randomservice,randomservice2'; + process.env.IMMICH_WORKERS_EXCLUDE = 'randomservice,microservices, randomservice2'; + expect(getWorkers()).toEqual(['api']); + }); + + it('should throw error for invalid workers', () => { + process.env.IMMICH_WORKERS_INCLUDE = 'api,microservices,randomservice'; + expect(getWorkers).toThrowError('Invalid worker(s) found: api,microservices,randomservice'); + }); +}); diff --git a/server/src/utils/workers.ts b/server/src/utils/workers.ts new file mode 100644 index 0000000000..14daa2620f --- /dev/null +++ b/server/src/utils/workers.ts @@ -0,0 +1,21 @@ +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/src/workers/api.ts b/server/src/workers/api.ts new file mode 100644 index 0000000000..2423eac072 --- /dev/null +++ b/server/src/workers/api.ts @@ -0,0 +1,68 @@ +import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import { json } from 'body-parser'; +import cookieParser from 'cookie-parser'; +import { existsSync } from 'node:fs'; +import { isMainThread } from 'node:worker_threads'; +import sirv from 'sirv'; +import { ApiModule } from 'src/app.module'; +import { envName, excludePaths, isDev, serverVersion, WEB_ROOT } from 'src/constants'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; +import { ApiService } from 'src/services/api.service'; +import { otelSDK } from 'src/utils/instrumentation'; +import { useSwagger } from 'src/utils/misc'; + +const host = process.env.HOST; + +async function bootstrap() { + otelSDK.start(); + + const port = Number(process.env.SERVER_PORT) || 3001; + const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true }); + const logger = await app.resolve(ILoggerRepository); + + logger.setAppName('ImmichServer'); + logger.setContext('ImmichServer'); + app.useLogger(logger); + app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); + app.set('etag', 'strong'); + app.use(cookieParser()); + app.use(json({ limit: '10mb' })); + if (isDev) { + app.enableCors(); + } + app.useWebSocketAdapter(new WebSocketAdapter(app)); + useSwagger(app, isDev); + + app.setGlobalPrefix('api', { exclude: excludePaths }); + if (existsSync(WEB_ROOT)) { + // copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46 + // provides serving of precompressed assets and caching of immutable assets + app.use( + sirv(WEB_ROOT, { + etag: true, + gzip: true, + brotli: true, + setHeaders: (res, pathname) => { + if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) { + res.setHeader('cache-control', 'public,max-age=31536000,immutable'); + } + }, + }), + ); + } + app.use(app.get(ApiService).ssr(excludePaths)); + + const server = await (host ? app.listen(port, host) : app.listen(port)); + server.requestTimeout = 30 * 60 * 1000; + + logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); +} + +if (!isMainThread) { + bootstrap().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/server/src/workers/microservices.ts b/server/src/workers/microservices.ts index cd339af13d..2400e62fc8 100644 --- a/server/src/workers/microservices.ts +++ b/server/src/workers/microservices.ts @@ -8,7 +8,7 @@ import { otelSDK } from 'src/utils/instrumentation'; const host = process.env.HOST; -export async function bootstrapMicroservices() { +export async function bootstrap() { otelSDK.start(); const port = Number(process.env.MICROSERVICES_PORT) || 3002; @@ -25,7 +25,7 @@ export async function bootstrapMicroservices() { } if (!isMainThread) { - bootstrapMicroservices().catch((error) => { + bootstrap().catch((error) => { console.error(error); process.exit(1); }); diff --git a/server/start-microservices.sh b/server/start-microservices.sh deleted file mode 100755 index c9e2cb42fb..0000000000 --- a/server/start-microservices.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -./start.sh microservices diff --git a/server/start-server.sh b/server/start-server.sh deleted file mode 100755 index 7ef959f63c..0000000000 --- a/server/start-server.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -./start.sh immich