Как разбить серию Async на SIGTERM? - PullRequest
3 голосов
/ 20 апреля 2019

Предположим, у меня есть следующий сценарий -

async.series(
  [
    function (cbi) {
      students.getAll('student', function (err, response) {
        if (err) {
          logger.error(err);
        }
        cbi(err, response);
      });
    },
    function (cbi) {
      students.deleteAll('student', function (err, response) {
        if (err) {
          logger.error(err);
        }
        cbi(err, response);
      });
    },
    function (cbi) {
      teachers.getAll('teacher', function (err, response) {
        if (err) {
          logger.error(err);
        }
        cbi(err, response);
      });
    },
    function (cbi) {
      teachers.deleteAll('teacher', function (err, response) {
        if (err) {
          logger.error(err);
        }
        cbi(err, response);
      });
    };
  ]
);

И я хочу изящную очистку при отправке SIGTERM. Это очистка всех учеников или всех учителей, в зависимости от того, когда идет процесс, когда сигнал был отправлен, а следующий не должен начинаться.

function (cbi) {
  students.getAll('student', function (err, response) {
    if (err || GLOBAL_VAR_SIGTERM === true) {
      logger.error(err);
    }
    cbi(err, response);
  });
}

Я думал, что должен установить глобальную переменную для отслеживания сигнала SIGTERM.

process.on('SIGTERM', function onSigterm () {
  GLOBAL_VAR_SIGTERM = true;
}

Есть ли лучший способ разорвать асинхронную серию, чтобы разорвать сигнал SIGTERM?

Ответы [ 3 ]

3 голосов
/ 27 апреля 2019

Как @adamrights указывает в своем ответе , основная проблема в вашем коде состоит в том, что вы не позвонили cbi(err, response) с правдивым err 1-м параметром, который необходим для остановки async.seriesпродолжение следующей задачи в очереди.

Теперь ваш код должен работать, но в вашем коде есть повторяющийся шаблон:

function (cbi) {
  students.getAll('student', function (err, response) {
    // these 3 lines appear in every callback function
    if (GLOBAL_VAR_SIGTERM) err = new Error("SIGTERM: Aborting remaining tasks");
    if (err) logger.error(err);
    cbi(err, response);
    // end of repeat pattern
  });
}

Ваш обратный вызов, переданный каждой асинхронной задаче, всегда выполняетта же самая 3-х линейная вещь.Мы знаем правило СУХОЙ, всегда полезно извлечь повторяющийся шаблон в другую функцию, чтобы максимально использовать его.

Таким образом, вместо того, чтобы неоднократно объявлять анонимные функции, вы должны объявить фабричную функцию.

function callbackFactory(cbi) {
  return function(err, response) {
    if (GLOBAL_VAR_SIGTERM) err = new Error("SIGTERM: Aborting remaining tasks");   
    if (err) logger.error(err);
    cbi(err, response);
  }
}

// use arrow function to write more concise code
async.series(
  [
    cbi => students.getAll('student', callbackFactory(cbi)),
    cbi => students.deleteAll('student', callbackFactory(cbi)),
    cbi => teachers.getAll('teacher', callbackFactory(cbi)),
    cbi => teachers.deleteAll('teacher', callbackFactory(cbi)),
  ]
);

Предварительная тема: использование декоратора для решения сквозных задач

Давайте рассмотрим эту тему немного подробнее.Очевидно, прерывание на раннем этапе при получении SIGTERM является сквозной проблемой, которую следует отделить от бизнес-логики.Предположим, что ваша бизнес-логика варьируется от задачи к задаче:

async.series(
  [
    cbi => students.getAll('student', (err, response) => {
      if (err) {
        logger.error(err);
        return cbi(err);
      }
      updateStudentCount(response.data.length)  // <- extra work
      cbi(err, response);
    }),
    cbi => teachers.getAll('student', (err, response) => {
      if (err) {
        logger.error(err);
        return cbi(err);
      }
      updateTeacherCount(response.data.length)  // <- different extra work
      cbi(err, response);
    })
  ]
);

Поскольку обратный вызов варьируется, его может быть трудно извлечь из заводской функции, как раньше.С этой точки зрения нам лучше внедрить поведение на раннем этапе прерывания для каждой задачи, чтобы было проще писать нормальную бизнес-логику.

Здесь шаблон декоратора пригодитсяНо глобальная переменная - не лучший инструмент для ее реализации, мы будем использовать прослушиватель событий.

Базовый интерфейс декоратора выглядит следующим образом:

// `task` will be things like `cbi => students.getAll('student', ... )`
function decorateTaskAbortEarly(task) {
  return (originalCbi) => {
    ...
    task(originalCbi)
  }
}

Ниже приведен наш контрольный список реализации:

  • мы собираемся позвонить originalCbi, если мы получим SIGTERM
  • , но когда мы не получим SIGTERM, originalCbi все еще вызывается внутри обратного вызовалюбой асинхронной задачи, такой как обычная
  • , если originalCbi когда-либо вызывается один раз, мы должны отписаться от SIGTERM, чтобы предотвратить утечку памяти

Реализация:

function decorateTaskAbortEarly(task) {
  return (originalCbi) => {
    // subscribe to `SIGTERM`
    var listener = () => originalCbi(new Error("SIGTERM: Aborting remaining tasks"));
    process.once('SIGTERM', listener);

    var wrappedCbi = (err, response) => {
      // unsubscribe if `cbi` is called once
      process.off('SIGTERM', listener);
      return originalCbi(err, response);
    };
    // pass `cbi` through to `task`
    task(wrappedCbi);
  }
}

// Usage:
async.series(
  [
    cbi => students.getAll('student', (err, response) => {
      if (err) {
        logger.error(err);
        return cbi(err);
      }
      updateStudentCount(response.data.length)
      cbi(err, response);
    }),
    cbi => teachers.getAll('student', (err, response) => {
      if (err) {
        logger.error(err);
        return cbi(err);
      }
      updateTeacherCount(response.data.length)
      cbi(err, response);
    })
  ].map(decorateTaskAbortEarly)  // <--- nice API
);
3 голосов
/ 27 апреля 2019

Если вы хотите ответить на событие SIGTERM из async.series(), то вы правы, самый простой способ - отслеживать глобальную переменную.

Но вам нужно установить первый параметр (обратный вызов с ошибкой) в функции cbi(err, response) на true, чтобы разорвать ряд.

так:

if (err || GLOBAL_VAR_SIGTERM === true) {
  logger.error(err);
}

должно быть больше похоже на:

if (err) logger.error(err);
if (GLOBAL_VAR_SIGTERM) err = new Error("SIGTERM: Aborting remaining tasks");
// You could just do err = true
// But best practice is to use an Error instance.

Тогда, поскольку cbi(err, response) будет вызываться со значением err, равным true, остальные задачи не будут выполняться.

1 голос
/ 30 апреля 2019

Мне понравились другие ответы.Это еще один способ добиться того же.Я использую свой собственный пример:

var async = require('async');
var ifAsync = require('if-async')
var GLOBAL_VAR_SIGTERM = false;

async.series({
    one: ifAsync(notsigterm).then(function (callback) {
        setTimeout(function () {
            console.log('one');
            callback(null, 1);
        }, 1000);
    }),
    two: ifAsync(notsigterm).then(function (callback) {
        setTimeout(function () {
            console.log('two');
            callback(null, 2);
        }, 1000);
    }),
    three: ifAsync(notsigterm).then(function (callback) {
        setTimeout(function () {
            console.log('three');
            callback(null, 3);
        }, 1000);
    }),
    four: ifAsync(notsigterm).then(function (callback) {
        setTimeout(function () {
            console.log('four');
            callback(null, 4);
        }, 1000);
    }),
}, function (err, results) {
    if (err) {
        //Handle the error in some way. Here we simply throw it
        //Other options: pass it on to an outer callback, log it etc.
        throw err;
    }
    console.log('Results are ' + JSON.stringify(results));
});

process.on('SIGTERM', function onSigterm () {
    console.log('SIGTERM caught');

  GLOBAL_VAR_SIGTERM = true;
});

function notsigterm(callback) {
    if (!GLOBAL_VAR_SIGTERM) return callback(null, true)
    else return callback(null, false)
}

Я использую пакет под названием ifAsync, который позволяет вам использовать предикат notsigterm, чтобы решить, должен ли быть вызван обратный вызов или нет.Если notsigterm вернет true, обратный вызов будет вызван, иначе он будет пропущен.Это аналогичный ответ для других, но как-то я нахожу это чище.Дайте мне знать, если у вас есть вопросы.

...