1
0
Эх сурвалжийг харах

feat(core): Implement health check server for worker

Relates to #994
Michael Bromley 4 жил өмнө
parent
commit
fd374b3fa4

+ 11 - 11
packages/core/src/bootstrap.ts

@@ -15,10 +15,10 @@ import { coreEntitiesMap } from './entity/entities';
 import { registerCustomEntityFields } from './entity/register-custom-entity-fields';
 import { setEntityIdStrategy } from './entity/set-entity-id-strategy';
 import { validateCustomFieldsConfig } from './entity/validate-custom-fields-config';
-import { JobQueueService } from './job-queue/job-queue.service';
 import { getConfigurationFunction, getEntitiesFromPlugins } from './plugin/plugin-metadata';
 import { getPluginStartupMessages } from './plugin/plugin-utils';
 import { setProcessContext } from './process-context/process-context';
+import { VendureWorker } from './worker/vendure-worker';
 
 export type VendureBootstrapFunction = (config: VendureConfig) => Promise<INestApplication>;
 
@@ -74,9 +74,9 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
 
 /**
  * @description
- * Bootstraps the Vendure worker. Resolves to an object containing a reference to the underlying
- * NestJs [standalone application](https://docs.nestjs.com/standalone-applications) as well as
- * a function used to start listening for and processing jobs in the job queue.
+ * Bootstraps a Vendure worker. Resolves to a {@link VendureWorker} object containing a reference to the underlying
+ * NestJs [standalone application](https://docs.nestjs.com/standalone-applications) as well as convenience
+ * methods for starting the job queue and health check server.
  *
  * Read more about the [Vendure Worker]({{< relref "vendure-worker" >}}).
  *
@@ -87,15 +87,14 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
  *
  * bootstrapWorker(config)
  *   .then(worker => worker.startJobQueue())
+ *   .then(worker => worker.startHealthCheckServer({ port: 3020 }))
  *   .catch(err => {
  *     console.log(err);
  *   });
  * ```
  * @docsCategory worker
  * */
-export async function bootstrapWorker(
-    userConfig: Partial<VendureConfig>,
-): Promise<{ app: INestApplicationContext; startJobQueue: () => Promise<void> }> {
+export async function bootstrapWorker(userConfig: Partial<VendureConfig>): Promise<VendureWorker> {
     const vendureConfig = await preBootstrapConfig(userConfig);
     const config = disableSynchronize(vendureConfig);
     if (config.logger instanceof DefaultLogger) {
@@ -104,18 +103,19 @@ export async function bootstrapWorker(
     Logger.useLogger(config.logger);
     Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`);
 
-    const appModule = await import('./app.module');
     setProcessContext('worker');
     DefaultLogger.hideNestBoostrapLogs();
-    const workerApp = await NestFactory.createApplicationContext(appModule.AppModule, {
+
+    const WorkerModule = await import('./worker/worker.module').then(m => m.WorkerModule);
+    const workerApp = await NestFactory.createApplicationContext(WorkerModule, {
         logger: new Logger(),
     });
     DefaultLogger.restoreOriginalLogLevel();
     workerApp.useLogger(new Logger());
     workerApp.enableShutdownHooks();
     await validateDbTablesForWorker(workerApp);
-    const startJobQueue = () => workerApp.get(JobQueueService).start();
-    return { app: workerApp, startJobQueue };
+    Logger.info('Vendure Worker is ready');
+    return new VendureWorker(workerApp);
 }
 
 /**

+ 1 - 0
packages/core/src/index.ts

@@ -12,6 +12,7 @@ export * from './entity/index';
 export * from './data-import/index';
 export * from './service/index';
 export * from './i18n/index';
+export * from './worker/index';
 export * from '@vendure/common/lib/shared-types';
 export {
     Permission,

+ 2 - 0
packages/core/src/worker/index.ts

@@ -0,0 +1,2 @@
+export * from './vendure-worker';
+export * from './worker-health.service';

+ 46 - 0
packages/core/src/worker/vendure-worker.ts

@@ -0,0 +1,46 @@
+import { INestApplicationContext } from '@nestjs/common';
+
+import { JobQueueService } from '../job-queue/job-queue.service';
+
+import { WorkerHealthCheckConfig, WorkerHealthService } from './worker-health.service';
+
+/**
+ * @description
+ * This object is created by calling the {@link bootstrapWorker} function.
+ *
+ * @docsCategory worker
+ */
+export class VendureWorker {
+    /**
+     * @description
+     * A reference to the `INestApplicationContext` object, which represents
+     * the NestJS [standalone application](https://docs.nestjs.com/standalone-applications) instance.
+     */
+    public app: INestApplicationContext;
+
+    constructor(app: INestApplicationContext) {
+        this.app = app;
+    }
+
+    /**
+     * @description
+     * Starts the job queues running so that the worker can handle background jobs.
+     */
+    async startJobQueue(): Promise<VendureWorker> {
+        await this.app.get(JobQueueService).start();
+        return this;
+    }
+
+    /**
+     * @description
+     * Starts a simple http server which can be used as a health check on the worker instance.
+     * This endpoint can be used by container orchestration services such as Kubernetes to
+     * verify whether the worker is running.
+     *
+     * @since 1.2.0
+     */
+    async startHealthCheckServer(healthCheckConfig: WorkerHealthCheckConfig): Promise<VendureWorker> {
+        await this.app.get(WorkerHealthService).initializeHealthCheckEndpoint(healthCheckConfig);
+        return this;
+    }
+}

+ 74 - 0
packages/core/src/worker/worker-health.service.ts

@@ -0,0 +1,74 @@
+import { Injectable, OnModuleDestroy } from '@nestjs/common';
+import express from 'express';
+import http from 'http';
+
+import { Logger } from '../config/logger/vendure-logger';
+
+/**
+ * @description
+ * Specifies the configuration for the Worker's HTTP health check endpoint.
+ *
+ * @since 1.2.0
+ * @docsCategory worker
+ */
+export interface WorkerHealthCheckConfig {
+    /**
+     * @description
+     * The port on which the worker will listen
+     */
+    port: number;
+    /**
+     * @description
+     * The hostname
+     *
+     * @default 'localhost'
+     */
+    hostname?: string;
+    /**
+     * @description
+     * The route at which the health check is available.
+     *
+     * @default '/health'
+     */
+    route?: string;
+}
+
+@Injectable()
+export class WorkerHealthService implements OnModuleDestroy {
+    private server: http.Server | undefined;
+
+    initializeHealthCheckEndpoint(config: WorkerHealthCheckConfig): Promise<void> {
+        const { port, hostname, route } = config;
+        const healthRoute = route || '/health';
+        const app = express();
+        const server = http.createServer(app);
+        app.get(healthRoute, (req, res) => {
+            res.send({
+                status: 'ok',
+            });
+        });
+        this.server = server;
+        return new Promise((resolve, reject) => {
+            server.on('error', err => {
+                Logger.error(`Failed to start worker health endpoint server (${err.toString()})`);
+                reject(err);
+            });
+            server.on('listening', () => {
+                const endpointUrl = `http://${hostname || 'localhost'}:${port}${healthRoute}`;
+                Logger.info(`Worker health check endpoint: ${endpointUrl}`);
+                resolve();
+            });
+            server.listen(port, hostname || '');
+        });
+    }
+
+    onModuleDestroy(): any {
+        return new Promise<void>(resolve => {
+            if (this.server) {
+                this.server.close(() => resolve());
+            } else {
+                resolve();
+            }
+        });
+    }
+}

+ 34 - 0
packages/core/src/worker/worker.module.ts

@@ -0,0 +1,34 @@
+import { Module, OnApplicationShutdown } from '@nestjs/common';
+
+import { ConfigModule } from '../config/config.module';
+import { Logger } from '../config/logger/vendure-logger';
+import { I18nModule } from '../i18n/i18n.module';
+import { PluginModule } from '../plugin/plugin.module';
+import { ProcessContextModule } from '../process-context/process-context.module';
+import { ServiceModule } from '../service/service.module';
+
+import { WorkerHealthService } from './worker-health.service';
+
+/**
+ * This is the main module used when bootstrapping the worker process via
+ * `bootstrapWorker()`. It contains the same imports as the AppModule except
+ * for the ApiModule, which is not needed for the worker. Omitting the ApiModule
+ * greatly increases startup time (about 4x in testing).
+ */
+@Module({
+    imports: [
+        ProcessContextModule,
+        ConfigModule,
+        I18nModule,
+        PluginModule.forRoot(),
+        ServiceModule.forRoot(),
+    ],
+    providers: [WorkerHealthService],
+})
+export class WorkerModule implements OnApplicationShutdown {
+    async onApplicationShutdown(signal?: string) {
+        if (signal) {
+            Logger.info('Received shutdown signal:' + signal);
+        }
+    }
+}