Skip to content

Instantly share code, notes, and snippets.

@samoshkin
Created November 14, 2022 21:34
Show Gist options
  • Save samoshkin/9333b148721ecec45c47a08531d12221 to your computer and use it in GitHub Desktop.
Save samoshkin/9333b148721ecec45c47a08531d12221 to your computer and use it in GitHub Desktop.
Node application entry point and runner based on "redux-saga" library
const Sentry = require('@sentry/node');
const {
fork,
} = require('redux-saga/effects');
async function init() {
// create any services and run all startup activites (e.g start HTTP server, connect to Db, etc)
const dbConn = await mongoDb.connect(config.services.mongo);
Basic.db = dbConn;
// connect to Redis
const redis = new RedisClient(config.services.redis);
await redis.connect();
// init cache
const cache = new Cache(config.services.redis);
await cache.connect();
User.cache = cache;
// etc
const stop = async (reason) => {
logger.info(`Gracefully stopping process. Reason: '${reason}'`);
// gracefully stop all your services
await server.stop();
await mongoDb.disconnect();
await redis.disconnect();
await eventBus.stop();
};
return {
// tell your services, will be used for DI purposes
services: {
config,
eventBus,
cache,
redis,
dbConn,
pycoreClient,
pycoreClientNew,
userService,
server,
io: server.io,
},
// graceful shutdown callback and shutdown timeout
stop,
stopTimeout: 10000,
// last chance to handle unhandled error before the process will be terminated
async onError(err) {
Sentry.captureException(err, { level: 'fatal' });
await Sentry.close(2000);
},
// application root process
appSaga,
};
}
function* appSaga() {
// fork any number of application-specific sagas
// application logic comes here
yield fork(...);
yield fork(...);
// resolve any services from saga context (sort of service registry pattern)
const conn = yield getContext('mongoDb');
}
module.exports = {
init,
};
const { run } = require('./app-runner');
const app = require('./app');
// application entry point
// $ node ./main.js
run(app);
const {
call,
take,
race,
delay,
} = require('redux-saga/effects');
const {
runSaga,
} = require('redux-saga');
const noop = () => {};
const { logger } = require('../../logger');
const {
fromEvent,
createMulticastChannel,
} = require('../utils/saga');
const {
openScope,
} = require('./effects');
async function run(app) {
const channel = createMulticastChannel();
const options = {
channel,
dispatch: (output) => {
channel.put(output);
},
};
// run root application process (aka PID=1)
const task = runSaga(options, rootSaga, app);
await task.toPromise();
// root process ran to the end, terminate process with zero exit code
// eslint-disable-next-line no-process-exit
process.exit(0);
}
function* rootSaga({ init: appInit }) {
let onErrorHandler;
try {
const {
services,
appSaga,
stop,
stopTimeout,
onError,
} = yield call(appInit);
onErrorHandler = onError;
// run app until signaled to stop, or fails due to error
const [, shutdownSignal] = yield race([
openScope({ context: services, saga: appSaga }),
call(waitForGracefulShutdown),
call(waitForUnhandledError),
]);
// gracefully stop the app (respects stopTimeout)
yield race([
call(stop, shutdownSignal || 'finished'),
delay(stopTimeout),
]);
} catch (err) {
// terminate process immediately in case of unhandler error
yield call(terminateProcess, err, onErrorHandler);
}
}
function* waitForGracefulShutdown() {
const [sigint, sigterm] = yield race([
take(fromEvent(process, 'SIGINT')),
take(fromEvent(process, 'SIGTERM')),
]);
return (sigint || sigterm);
}
function* waitForUnhandledError() {
const [uncaughtExceptionErr, unhandledRejectionErr] = yield race([
take(fromEvent(process, 'uncaughtException')),
take(fromEvent(process, 'unhandledRejection')),
]);
throw (uncaughtExceptionErr || unhandledRejectionErr);
}
async function terminateProcess(err, onError = noop) {
logger.error('Terminate process:', err);
try {
await onError(err);
} finally {
// eslint-disable-next-line no-process-exit
process.exit(1);
}
}
module.exports = {
run,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment