Files

121 lines
4.1 KiB
TypeScript

import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import * as path from 'path';
import { ONE_MINUTE_MS } from '../../src/core/constants';
import { initDbDataSourceAsync } from './initDbDataSourceAsync';
// Docker compose file name used for testing. Ports are picked to avoid collision with other workspaces
const dockerComposeFilename = 'docker-compose-test.yml';
/**
* Returned by `setupDependenciesAsync`. Call to shutdown the
* dependencies spun up by `setupDependenciesAsync`. Returns
* `true` if the teardown is successful.
*/
export type TeardownDependenciesFunctionHandle = () => boolean;
type Service = 'sqs' | 'postgres' | 'redis' | 'ganache';
/**
* Sets up 0x-api's dependencies
*
* @param services An array of services to start
* @returns A function handle which will tear down the dependencies when called
*/
export async function setupDependenciesAsync(services: Service[]): Promise<TeardownDependenciesFunctionHandle> {
if (services.length === 0) {
throw new Error('Pick at least one service to start');
}
const configFilePath = path.resolve(__dirname, '../../', dockerComposeFilename);
/**
* Only starts the services specified in `services`.
*/
const up = spawn(`docker-compose`, ['-f', configFilePath, 'up', ...services], {});
await waitForDependencyStartupAsync(up, services);
if (services.includes('postgres')) {
await confirmPostgresConnectivityAsync();
}
// Return the function handle which will shutdown the services
return function closeFunction(): boolean {
const wasSuccessfulKill = up.kill();
return wasSuccessfulKill;
};
}
/**
* Monitor the logs being emitted from the docker containers to detect
* when services have started up. Postgres startup is managed with
* `confirmPostgresConnectivityAsync`
*/
async function waitForDependencyStartupAsync(
logStream: ChildProcessWithoutNullStreams,
services: Service[],
): Promise<void> {
return new Promise<void>((resolve, reject) => {
const startupTimeout = ONE_MINUTE_MS * 3;
const timeoutHandle = setTimeout(() => {
reject(new Error(`Timed out waiting for dependency logs\n${JSON.stringify(isServiceStarted)}`));
}, startupTimeout);
const startupRegexSqs = /.*sqs.*listening on port \d{4}/;
const startupRegexRedis = /.*redis.*Ready to accept connections/;
const startupRegexGananche = /.*ganache.*Listening on 0.0.0.0:\d{4}/;
const isServiceStarted: Record<Service, boolean> = {
sqs: !services.includes('sqs'),
postgres: true, // managed by confirmPostgresConnectivityAsync
redis: !services.includes('redis'),
ganache: !services.includes('ganache'),
};
logStream.on('error', (error) => {
reject(`Stream closed with error: ${error}`);
});
logStream.stdout.on('data', (data) => {
const log = data.toString();
if (startupRegexRedis.test(log)) {
isServiceStarted.redis = true;
}
if (startupRegexSqs.test(log)) {
isServiceStarted.sqs = true;
}
if (startupRegexGananche.test(log)) {
isServiceStarted.ganache = true;
}
// Once all the services are started, resolve the promise
if (Object.values(isServiceStarted).every((v) => v)) {
// logStream.stdout.removeAllListeners('data');
// logStream.removeAllListeners('error');
clearTimeout(timeoutHandle);
resolve();
}
});
});
}
async function confirmPostgresConnectivityAsync(maxTries = 5): Promise<void> {
try {
await Promise.all([
// delay before retrying
new Promise<void>((resolve) => setTimeout(resolve, 2000)),
async () => {
await initDbDataSourceAsync();
},
]);
return;
} catch (e) {
if (maxTries > 0) {
await confirmPostgresConnectivityAsync(maxTries - 1);
} else {
throw e;
}
}
}