1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00
unleash.unleash/src/lib/server-impl.ts
Gastón Fournier 331de07e39
fix: double token initialization (#9783)
## About the changes
Initially at Unleash we started using `process.nextTick` inside
constructors to delay initialization of services.
Later we stared using a pattern where we instantiate services multiple
times.
The problem is the first pattern implies we have singleton services,
while the second pattern breaks the singleton.

There are reasons for both patterns, but we've decided that
`process.nextTick` inside constructors is not something we want to keep
as it creates side effects from creating objects. Instead this PR
proposes a more explicit approach.

Fixes #9775
2025-04-17 09:22:35 +02:00

244 lines
7.1 KiB
TypeScript

import stoppable, { type StoppableServer } from 'stoppable';
import { promisify } from 'util';
import version from './util/version';
import { migrateDb } from '../migrator';
import getApp from './app';
import { createMetricsMonitor } from './metrics';
import { createStores } from './db';
import { createServices } from './services';
import { createConfig } from './create-config';
import registerGracefulShutdown from './util/graceful-shutdown';
import { createDb } from './db/db-pool';
import sessionDb from './middleware/session-db';
// Types
import {
type CustomAuthHandler,
IAuthType,
type IUnleash,
type IUnleashConfig,
type IUnleashOptions,
type IUnleashServices,
RoleName,
} from './types';
import User, { type IAuditUser, type IUser } from './types/user';
import ApiUser, { type IApiUser } from './types/api-user';
import { type Logger, LogLevel } from './logger';
import AuthenticationRequired from './types/authentication-required';
import Controller from './routes/controller';
import type { IApiRequest, IAuthRequest } from './routes/unleash-types';
import type { SimpleAuthSettings } from './types/settings/simple-auth-settings';
import { Knex } from 'knex';
import * as permissions from './types/permissions';
import * as eventType from './types/events';
import { Db } from './db/db';
import { defaultLockKey, defaultTimeout, withDbLock } from './util/db-lock';
import { scheduleServices } from './features/scheduler/schedule-services';
import { compareAndLogPostgresVersion } from './util/postgres-version-checker';
export async function initialServiceSetup(
{ authentication }: Pick<IUnleashConfig, 'authentication'>,
{
userService,
apiTokenService,
}: Pick<IUnleashServices, 'userService' | 'apiTokenService'>,
) {
await userService.initAdminUser(authentication);
if (authentication.initApiTokens.length > 0) {
await apiTokenService.initApiTokens(authentication.initApiTokens);
}
}
async function createApp(
config: IUnleashConfig,
startApp: boolean,
): Promise<IUnleash> {
// Database dependencies (stateful)
const logger = config.getLogger('server-impl.js');
const serverVersion = config.enterpriseVersion ?? version;
const db = createDb(config);
const stores = createStores(config, db);
await compareAndLogPostgresVersion(config, stores.settingStore);
const services = createServices(stores, config, db);
await initialServiceSetup(config, services);
if (!config.disableScheduler) {
await scheduleServices(services, config);
}
const metricsMonitor = createMetricsMonitor();
const unleashSession = sessionDb(config, db);
const stopUnleash = async (server?: StoppableServer) => {
logger.info('Shutting down Unleash...');
if (server) {
const stopServer = promisify(server.stop);
await stopServer();
}
if (typeof config.shutdownHook === 'function') {
try {
await config.shutdownHook();
} catch (e) {
logger.error('Failure when executing shutdown hook', e);
}
}
services.schedulerService.stop();
services.addonService.destroy();
await db.destroy();
};
if (!config.server.secret) {
const secret = await stores.settingStore.get<string>('unleash.secret');
config.server.secret = secret!;
}
const app = await getApp(config, stores, services, unleashSession, db);
await metricsMonitor.startMonitoring(
config,
stores,
serverVersion,
config.eventBus,
services.instanceStatsService,
services.schedulerService,
db,
);
const unleash: Omit<IUnleash, 'stop'> = {
stores,
eventBus: config.eventBus,
services,
app,
config,
version: serverVersion,
};
if (config.import.file) {
await services.importService.importFromFile(
config.import.file,
config.import.project,
config.import.environment,
);
}
if (
config.environmentEnableOverrides &&
config.environmentEnableOverrides?.length > 0
) {
await services.environmentService.overrideEnabledProjects(
config.environmentEnableOverrides,
);
}
return new Promise((resolve, reject) => {
if (startApp) {
const server = stoppable(
app.listen(config.listen, () =>
logger.info('Unleash has started.', server.address()),
),
config.server.gracefulShutdownTimeout,
);
server.keepAliveTimeout = config.server.keepAliveTimeout;
server.headersTimeout = config.server.headersTimeout;
server.on('listening', () => {
resolve({
...unleash,
server,
stop: () => stopUnleash(server),
});
});
server.on('error', reject);
} else {
resolve({ ...unleash, stop: stopUnleash });
}
});
}
async function start(opts: IUnleashOptions = {}): Promise<IUnleash> {
const config = createConfig(opts);
const logger = config.getLogger('server-impl.js');
try {
if (config.db.disableMigration) {
logger.info('DB migration: disabled');
} else {
logger.info('DB migration: start');
if (config.flagResolver.isEnabled('migrationLock')) {
logger.info('Running migration with lock');
const lock = withDbLock(config.db, {
lockKey: defaultLockKey,
timeout: defaultTimeout,
logger,
});
await lock(migrateDb)(config);
} else {
logger.info('Running migration without lock');
await migrateDb(config);
}
logger.info('DB migration: end');
}
} catch (err) {
logger.error('Failed to migrate db', err);
throw err;
}
const unleash = await createApp(config, true);
if (config.server.gracefulShutdownEnable) {
registerGracefulShutdown(unleash, logger);
}
return unleash;
}
async function create(opts: IUnleashOptions): Promise<IUnleash> {
const config = createConfig(opts);
const logger = config.getLogger('server-impl.js');
try {
if (config.db.disableMigration) {
logger.info('DB migrations disabled');
} else {
await migrateDb(config);
}
} catch (err) {
logger.error('Failed to migrate db', err);
throw err;
}
return createApp(config, false);
}
export default {
start,
create,
};
export {
start,
create,
Controller,
AuthenticationRequired,
User,
ApiUser,
LogLevel,
RoleName,
IAuthType,
Knex,
Db,
permissions,
eventType,
};
export type {
Logger,
IUnleash,
IUnleashOptions,
IUnleashConfig,
IUser,
IApiUser,
IAuditUser,
IUnleashServices,
IAuthRequest,
IApiRequest,
SimpleAuthSettings,
CustomAuthHandler,
};