Как обеспечить выполнение хрон-задания точно на каждой возможной 5-минутной отметке (0:00, 0:05, 0:10) независимо от времени начала хрон-задания?

У меня есть функция JavaScript расширения Chrome, которую мне нужно запускать с точными 5-минутными интервалами, синхронизируя с текущим временем. Например, он должен запускаться в 5:00, 5:05, 5:10 и т. д. независимо от того, когда запускается сценарий.

Вот подход, который я пытаюсь реализовать:

Calculate the delay until the next 5-minute mark.
Use setTimeout to call the function at the next 5-minute mark.
Set up a setInterval to call the function every 5 minutes after the initial timeout.
Here is a simplified version of the code to demonstrate the issue:

function myFunction() {
    console.info("Function executed at:", new Date().toLocaleTimeString());
}

function checkTimeAndRunMainFunction() {
    let now = new Date();
    let minutes = now.getMinutes();
    let seconds = now.getSeconds();

    if (seconds === 0 && minutes % 5 === 0) {
        // Run the main function immediately
        myFunction();
    }
}

function calculateDelayToNextFiveMinuteMark() {
    let now = new Date();
    let millisecondsUntilNextFiveMinuteMark = (5 - (now.getMinutes() % 5)) * 60 * 1000 - now.getSeconds() * 1000 - now.getMilliseconds();
    return millisecondsUntilNextFiveMinuteMark;
}

// Set an initial timeout to synchronize with the next 5-minute mark
setTimeout(function() {
    checkTimeAndRunMainFunction();
    // Set an interval to check the time every 5 minutes after the initial call
    setInterval(checkTimeAndRunMainFunction, 5 * 60 * 1000);
}, calculateDelayToNextFiveMinuteMark());

Проблема, с которой я столкнулся, заключается в том, что setInterval(checkTimeAndRunMainFunction, 1000); отсчитывает каждую секунду с момента запуска пользователем кода и не синхронизируется с фактическими секундами часов, в результате чего условие if (секунды === 0 && минуты % 5 === 0) не выполняется точно.

Как я могу гарантировать, что моя функция будет выполняться точно на 5-минутных отметках (например, 5:00, 5:05, 5:10) независимо от того, когда запускается скрипт?

В опубликованном коде нет setInterval(checkTimeAndRunMainFunction, 1000). Может быть, у вас на странице работает старая версия сценария содержимого? Перезагрузите расширение на странице chrome://extensions и перезагрузите вкладку. См. также Как удалить потерянный скрипт после обновления расширения Chrome

woxxom 31.05.2024 09:57

@woxxom использует setTimeout для определения первого запуска.

Christopher 31.05.2024 10:16

@Hamed, вы передаете неправильное количество миллисекунд (время, прошедшее с момента последнего запуска) на первоначальный вызов. Используйте 300000 - (Date.now() % 300000) для продолжительности setTimeout.

Christopher 31.05.2024 10:21

@Christopher, я хочу сказать, что в вопросе упоминается часть кода, которой не существует.

woxxom 31.05.2024 11:02

@woxxom, его задача заключается в том, что когда сценарий выполняется за 0,995 секунды (или первый запуск выполняется, когда Date.now() заканчивается через 99N миллисекунд), проверка (мин = 0 и секунды = 0) может завершиться неудачей. Потому что результаты getSeconds() и getMilli Seconds() внутри обратного вызова могут быть 1 и >=0 во время выполнения.

Christopher 31.05.2024 12:36

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

woxxom 31.05.2024 13:10
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
6
138
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вот простой скрипт, который запускается каждые 5 минут и корректирует время следующего запуска при каждом запуске.

(function loop() {
  const date = new Date();
  const wait = 300000 - date % 300000
  console.info(date, wait);

  setTimeout(() => {
    console.info(new Date())

    loop();
    // your code here
  }, wait);
})();

Ошибка, если оставшееся время превышает 1 минуту. - Максимум, который вы вычтете из 300 000, равен 59 999. Используйте timespan - (Date.now() % timespan) для определения продолжительности.

Christopher 31.05.2024 10:44

Вы правы, исправили - переместили «ваш код» в план следующего запуска. На самом деле вы вычитаете только миллисекунды - это корректировки для планирования следующего цикла через следующие 5 минут. И только после того, как вы спланируете следующий запуск, вы выполняете свой основной код.

Degger 31.05.2024 10:55

Я отредактировал ответ. Должно работать правильно.

Degger 03.06.2024 18:05

Я протестировал его, и он работает нормально каждые 5 минут, но проблема в том, что это влияет на работу основной функции. Вероятно, это связано с конфликтом параметра задержки внутри основной функции. Как я сказал в своем вопросе, основная функция имеет свой собственный период, и ваш код привел к уменьшению и неточному его периоду.

Hamed 03.06.2024 18:54

Вот более безопасный сценарий. Он будет выполняться каждый раз, когда интервал достигает 5-минутной отметки, а затем перемещает запись в конец массива. Это означает, что есть место для некоторого колебания

Вы можете добавить checkTimeAndRunMainFunction() перед setInterval, чтобы запустить его немедленно, если пользователь запустит его ровно через 5 минут.

const myFunction = () => {
  console.info("Function executed at:", new Date().toLocaleTimeString());
}

const getNearestFiveMinuteMark = () => {
  const now = new Date();
  const minutes = now.getMinutes();
  return Math.ceil(minutes / 5) * 5;
}

const initializeFiveMinuteArray = () => {
  const nearestFiveMinuteMark = getNearestFiveMinuteMark();
  const intervals = [];
  for (let i = 0; i < 12; i++) { // 12 entries for every 5 minutes in an hour
    intervals.push((nearestFiveMinuteMark + i * 5) % 60);
  }
  return intervals;
}

const checkTimeAndRunMainFunction = () => {
  const now = new Date();
  const currentMinute = now.getMinutes();

  if (currentMinute === fiveMinuteIntervals[0]) {
    // Run the main function and move the entry to the end
    myFunction();
    const executedEntry = fiveMinuteIntervals.shift();
    fiveMinuteIntervals.push(executedEntry);
  }
  time.textContent = `next run will be at :${String(fiveMinuteIntervals[0]).padStart(2,'0')}`;
}

const fiveMinuteIntervals = initializeFiveMinuteArray();
console.info("Initial 5-minute intervals array:", fiveMinuteIntervals);

// Set an interval to check the time every second
setInterval(checkTimeAndRunMainFunction, 1000);
<span id = "time"></span>

Проблема ОП должна быть решена...

  1. ... в общее решение для функции, которая вычисляет количество миллисекунд до следующего доступного слота для запуска интервала запуска/выполнения основной задачи OP.

  2. ... и полностью переписать функцию checkTimeAndRunMainFunction ОП.

Что касается (1), пример кода, который предоставляется после объяснения, переименовывает функцию calculateDelayToNextFiveMinuteMark OP в getTimeToNextAvailableSchedulerSlot и реализует расчет задержки общим способом на основе точности передаваемого слота в секундах и возвращает задержку. значение в миллисекундах, чтобы быть максимально точным.

Что касается (2), checkTimeAndRunMainFunction необходимо полностью переопределить как scheduleTask, где первый параметр новой функции — это любая функция, которую нужно запустить/выполнить в запланированном режиме на основе временного интервала; его второй параметр равен точности передаваемого слота в секундах, которая будет использоваться как для расчета задержки до запуска расписания, так и для установки интервала расписания/задачи.

scheduleTask дополнительно возвращает объект, который имеет два свойства timeout и interval, оба из которых являются идентификаторами таймера. Таким образом, можно по-прежнему предотвратить запуск расписания, очистив тайм-аут с помощью timeout, и можно завершить уже работающее расписание/интервал, очистив интервал с помощью interval.

function getTimeToNextAvailableSchedulerSlot(slotPrecisionInSeconds) {
  const current = new Date;

  const precisionInMilliseconds = Number
    .isFinite(slotPrecisionInSeconds = parseInt(slotPrecisionInSeconds, 10))
      ? (slotPrecisionInSeconds * 1000)
      : 60_000; // 1 minute slot default.

  const currentMilliseconds = (
    current.getHours() * 60 * 60 * 1000 +
    current.getMinutes() * 60 * 1000 +
    current.getSeconds() * 1000 +
    current.getMilliseconds()
  );

  return (
    precisionInMilliseconds -
    (currentMilliseconds % precisionInMilliseconds)
  );
}

function scheduleTask(task, slotPrecisionInSeconds) {
  const timerIds = {
    timeout: null, interval: null,
  };
  const delay = getTimeToNextAvailableSchedulerSlot(slotPrecisionInSeconds);

  timerIds.timeout = setTimeout(() => {

    timerIds.interval = setInterval(task, slotPrecisionInSeconds * 1000);
    task();

  }, delay);

  return timerIds;
}


function chron_job_5_seconds_slot() {
  console.info('5 seconds chron job executed at ...', new Date().toLocaleTimeString());
}
function chron_job_15_seconds_slot() {
  console.info('15 seconds chron job executed at ...', new Date().toLocaleTimeString());
}
function chron_job_1_minute_slot() {
  console.info('1 minute chron job executed at ...', new Date().toLocaleTimeString());
}
function chron_job_5_minutes_slot() {
  console.info('5 minutes chron job executed at ...', new Date().toLocaleTimeString());
}
console.info('initialization started at ... ', new Date);

// execution at the next available 5 seconds slot continued with a 5 seconds interval.
const timer_ids_5_seconds_slot = scheduleTask(chron_job_5_seconds_slot, 5);

// execution at the next available 15 seconds slot continued with a 15 seconds interval.
const timer_ids_15_seconds_slot = scheduleTask(chron_job_15_seconds_slot, 15);

// execution at the next available 1 minute slot continued with a 1 minute interval.
const timer_ids_1_minute_slot = scheduleTask(chron_job_1_minute_slot, 60);

// execution at the next available 5 minutes slot continued with a 5 minutes interval.
const timer_ids_5_minutes_slot = scheduleTask(chron_job_5_minutes_slot, 5 * 60);


// terminate the 5 seconds based task immediately.
clearTimeout(timer_ids_5_seconds_slot.timeout);

// terminate all other tasks after 15 minutes.
setTimeout(() => {

  clearInterval(timer_ids_15_seconds_slot.interval);
  clearInterval(timer_ids_1_minute_slot.interval);
  clearInterval(timer_ids_5_minutes_slot.interval);

}, 15 * 60 * 1000);
.as-console-wrapper { min-height: 100%!important; top: 0; }


Совет профессионала

Проблему синхронизируемых функций/методов можно решить с помощью общего подхода. Еще лучше реализовать общий подход как clocked метод, например. Function.prototype.

Применение/использование последнего для решения проблемы ФП было бы так же просто, как...

// the OP's function.
function myFunction() {
  console.info("Function executed at:", new Date().toLocaleTimeString());
}
// - create a variant of the OP's above function
//   with an interval clocked at (or set to) 5 minutes.
const clockedMyFct = myFunction.clocked(5 * 60 * 1000);

// // - whether to trigger an immediate or
// //   initial execution of `myFunction`.
// myFunction();

// - invoke/call the clocked variant of the OP's `myFunction`.
// - the latter is going to be invoked every 5 minutes and infinitely.
clockedMyFct();

Но реализация метода clocked способна на гораздо большее. Можно настроить поток управления любой тактируемой функцией, предоставив дополнительную функцию обратного вызова, которая действует как controller.

Предоставив такую ​​функцию контроллера, последняя вызывается вместо функции, которая будет синхронизироваться, но тактируется с тем же интервалом. Единственным параметром контроллера является (тактируемый) объект data, который содержит все необходимое для управления потоком управления синхронизируемой функции.

Например, можно получить доступ к значению count и функции terminate, которая позволяет завершить/очистить интервал после определенного количества выполненных интервалов. Простой пример демонстрирует сказанное только что...

// the OP's function.
function myFunction() {
  console.info("Function executed at:", new Date().toLocaleTimeString());
}
// the controller function with a custom implemented control flow.
function controller(data) {
  const { clock: { count }, proceed, terminate } = data;

  if (count > 5) {

    terminate();

  } else {

    // - calling the function that needs
    //   to be executed in a clocked way.

    proceed();
  }
}
// - create a variant of the OP's above function with
//   an interval clocked at (or set to) 2 seconds which
//   terminates itself after having been invoked 5 times.
const clockedMyFct = myFunction.clocked(2_000, null, controller);

// // - whether to trigger an immediate or
// //   initial execution of `myFunction`.
// myFunction();

// - invoke/call the clocked variant of the OP's `myFunction`.
// - the latter is going to be invoked every 2 seconds and
//   to be terminated after having been invoked 5 times.
clockedMyFct();

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

function loggerOne() {
  console.info(
    `\nlogger ONE executed at ... ${ new Date().toLocaleTimeString() }\n`
  );
}
function loggerTwo() {
  console.info(
    `logger TWO executed at ... ${ new Date().toLocaleTimeString() }`
  );
}


function controller(clockedData) {
  const {
    clock: { interval, startTime, timestamp, count },
    target, args, proceed, terminate,
  } =
    clockedData;

  const passedTime = timestamp - startTime;
  const passedSeconds = passedTime / 1000;

  // - implement controller logic as you like/need.

  if (count > 10) {

    terminate();

  } else {
    console.info({
      interval,
      startTime,
      timestamp,
      passedSeconds,
      count,
      target,
      args,
      proceed,
    });

    // - calling the function that needs
    //   to be executed in a clocked way.

    proceed.apply(target, args);
  }
}
const clocked_at_20_seconds_cycling_infinitely =
  loggerOne.clocked(20_000);

const clocked_at_07_seconds_limited_to_10_cycles =
  loggerTwo.clocked(7_000, null, controller);


console.info(
  `... test started at ... ${ new Date().toLocaleTimeString() }`
);
clocked_at_20_seconds_cycling_infinitely();
clocked_at_07_seconds_limited_to_10_cycles();
body { margin: 0; }
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
(function (Reflect, Math, Number, Array, Function) {

  'use strict';

  /**
   * @typedef {Object} clockedControllerClockData
   *  @param {number} interval  - interval-value in milliseconds.
   *  @param {number} startTime - time of first invocation in milliseconds.
   *  @param {number} timestamp - time of current invocation in milliseconds.
   *  @param {number} count     - current invocation-count since first invocation; `count` value of 1st invocation is `1`.
   */

  /**
   * A data object which holds all the necessary references
   * in order to properly run any controller logic.
   *
   * @typedef {Object} clockedControllerData
   *  @param {clockedControllerClockData} clock
   *  @param {(Object|null)} target
   *  @param {Array} args
   *  @param {Function} proceed
   *  @param {Function} terminate
   */

  /**
   * A callback function which acts like a pre-processor which e.g. can decide
   * whether to proceed with or terminate the original functions `clocked` version.
   *
   * @callback clockedController
   *  @param {clockedControllerData} data
   *   A data object which holds all the necessary references
   *   in order to properly run any controller logic.
   */

  const DEFAULT_INTERVAL = 200;

  const { isFinite, parseInt } = Number;

  function isFunction(value) {
    return (
      typeof value === 'function' &&
      typeof value.call === 'function' &&
      typeof value.apply === 'function'
    );
  }

  function getSanitizedInteger(value) {
    value = parseInt(value, 10);
    return isFinite(value) ? value : 0;
  }
  function getSanitizedPositiveInteger(value) {
    return Math.max(getSanitizedInteger(value), 0);
  }

  /**
   *  Returns a function, that, invoked initially once, will continuously auto-invoke
   *  the original function every amount of milliseconds, as it was parametrized by
   *  this methods `interval` argument. It does so until the additional `terminate`
   *  method of the returned wrapper function gets called.
   *  This methods second parameter - `target` - provides the object/context a `clocked`
   *  method can act upon.
   *  This methods third parameter - `controller` - enables the even more precise
   *  customized handling of how to run a `clocked` function/method.
   *
   * @param {number=} interval  - The optional interval value; expected to be grater than zero and assuming milliseconds.
   * @param {*=} target         - The function's/method's optional target object.
   * @param {clockedController=} controller
   *  The optional callback function which acts like a pre-processor where additionally
   *  provided program logic might e.g. decide whether to proceed with or terminate
   *  the original functions `clocked` version.
   * @returns {Function}
   *  The original function's/method's `clocked` version.
   */
  function createClockedFunction(interval/*, delay*/, target, controller) {
    const proceed = this;

    let thisArg;
    let argsArr;

    let clockCount = null;
    let clockStart = null;

    let timerId = null;

    target = target ?? null; // `null` instead of an `undefined` default.
    interval = getSanitizedPositiveInteger(interval) || DEFAULT_INTERVAL;

    function triggerController() {
      controller({
        clock: {
          interval,
          startTime: clockStart,
          timestamp: Date.now(),
          count: ++clockCount,
        },
        target: thisArg,
        args: [...argsArr],
        proceed,
        terminate,
      });
    }
    function triggerProceed() {
      proceed.apply(thisArg, argsArr);
    }

    function terminate() {
      clearInterval(timerId);
      timerId = null;

      clockStart = null;
      clockCount = null;
    }

    function isActive() {
      return (timerId !== null);
    }

    function clocked(...argumentsArray) {
      // - a `clocked` method's target can be delegated at call time, thus
      //   it overrules the target which was provided at composition time.
      thisArg = (this ?? null) ?? target;
      argsArr = argumentsArray;

      if (isActive()) {
        terminate();
      }
      clockCount = 0;
      clockStart = Date.now();

      const trigger = isFunction(controller)
        ? triggerController
        : triggerProceed;

      timerId = setInterval(trigger, interval);
    }
    clocked.terminate = terminate;
    clocked.isActive = isActive;

    return (isFunction(proceed) && clocked) || proceed;
  }

  Reflect.defineProperty(Function.prototype, 'clocked', {
    configurable: true,
    writable: true,
    value: createClockedFunction,
  });

}(Reflect, Math, Number, Array, Function));
</script>

@Hamed ... Я отредактировал свой (первый) ответ таким образом, чтобы он впервые предоставил решение вашей проблемы, заключающейся в том, как точно запускать задания хрона на основе временных интервалов и интервалов. Моим первым ответом, который не решил реальную проблему, стал раздел «Совет профессионала» в обоих приведенных выше решениях.

Peter Seliger 04.06.2024 17:47

Другие вопросы по теме