Подтвердить что ты не робот

Как вы разделите период времени на равные промежутки времени и найдите текущий?

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

Интервал относительно момента времени, который является одним и тем же для всех пользователей. Учитывая такой момент времени, например Instant#EPOCH или какое-либо другое постоянное значение, как найти дату начала текущего интервала?

Я могу сделать

Instant now = Instant.now();
Instant origin = Instant.EPOCH;
Duration interval = Duration.ofDays(4);

Duration duration = Duration.between(origin, now);
long sinceOrigin = duration.toMillis();
long millisPerInterval = interval.toMillis();

long intervalsSince = sinceOrigin / millisPerInterval;
Instant startNext = origin.plus(interval.multipliedBy(intervalsSince));

int cursor = distributionStrategy.distribute(hashCode, millisPerInterval);

Затем я могу использовать cursor для планирования задания на Instant относительно начала текущего интервала.

Здесь много математики, и я не уверен, что преобразование в миллисекунды везде будет поддерживать фактические даты. Есть ли более точный способ деления времени между двумя моментами и нахождение одного (подразделения), в котором мы находимся в настоящее время?

4b9b3361

Ответ 1

Если вы хотите уменьшить математику здесь, вы можете использовать остаток вместо деления и умножения.

long millisSinceIntervalStart = sinceOrigin % millisPerInterval;
Instant startNext = now.minusMillis(millisSinceIntervalStart);

Здесь вам не нужно вычислять количество интервалов, прошедших с момента возникновения. Просто получите время, прошедшее с интервального старта, и вычтите его из текущего времени.

Кроме того, ваш startNext, кажется, указывает начало текущего интервала, а не следующий интервал. Правильно?

Ответ 2

Предполагая, что вы действительно заинтересованы в моментах и ​​продолжительности (т.е. не связаны с периодами, датами, часовыми поясами и т.д.), тогда ваш код должен быть в порядке. В этом случае я бы пошел в миллисекунды раньше... математика здесь просты.

Interval getInterval(Instant epoch, Duration duration, Instant now) {
    long epochMillis = epoch.getMillis();
    long durationMillis = duration.getMillis();

    long millisSinceEpoch = now.getMillis() - epochMillis;        
    long periodNumber = millisSinceEpoch / durationMillis;
    long start = epochMillis + periodNumber * durationMillis;
    return new Interval(start, start + durationMillis);
}

Это предполагает, что вам не нужно беспокоиться о now до epoch - в этот момент вам нужно будет немного поработать, так как вы хотите пол операции деления, а не усечение в направлении 0.

(Если вы хотите только начать, вы можете просто вернуть new Instant(start).)

Ответ 3

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

Вам нужно только ответить "когда этот следующий объект будет запущен?", так что ответ будет статистически равномерно распределен по интервалу и согласован (не зависит от "сейчас", за исключением того, что следующий прогон всегда после "сейчас",).

Этот метод делает это:

public static long nextRun(long origin, long interval, Object obj) {
    long nextRunTime = origin + (System.currentTimeMillis() - origin)
       / interval * interval + Math.abs(obj.hashCode() % interval);
    return nextRunTime > System.currentTimeMillis() ? nextRunTime : nextRunTime + interval;
}

Этот метод возвращает следующий раз, когда объект должен запускаться с использованием его hashCode(), чтобы определить, где в течение продолжительности его следует запланировать, а затем возвращает следующее фактическое время, которое произойдет.

Небольшая заметка о реализации: Math.abs(obj.hashCode() % interval) используется вместо Math.abs(obj.hashCode()) % interval для защиты от hashCode() возвращаемого Integer.MIN_VALUE и зная, что Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE


Если вам требуется, чтобы в вашем API были использованы классы java.time, здесь тот же код, но с параметрами java.time и возвращаемым типом:

public static Instant nextRun(Instant origin, Duration interval, Object target) {
    long start = origin.toEpochMilli();
    long width = interval.toMillis();
    long nextRunTime = start + (System.currentTimeMillis() - start)
       / width * width + Math.abs(target.hashCode() % width);
    nextRunTime = nextRunTime > System.currentTimeMillis() ? nextRunTime : nextRunTime + width;
    return Instant.ofEpochMilli(nextRunTime);
}

Чтобы помочь понять математику, здесь более длинная версия с разбивкой компонентов и назначением значимых имен переменных:

public static Instant nextRun(Instant origin, Duration duration, Object target) {
    long now = System.currentTimeMillis();
    long start = origin.toEpochMilli();
    long intervalWidth = duration.toMillis();
    long ageSinceOrigin = now - start;
    long totalCompleteDurations = ageSinceOrigin / intervalWidth * intervalWidth;
    long mostRecentIntervalStart = start + totalCompleteDurations;
    long offsetInDuration = Math.abs(target.hashCode() % intervalWidth);
    long nextRun = mostRecentIntervalStart + offsetInDuration;
    // schedule for next duration if this duration time has already passed
    if (nextRun < now) { 
        nextRun += intervalWidth;
    }
    return Instant.ofEpochMilli(nextRun);
}

Ответ 4

Я попытался бы определить каждый период времени как объект с датой начала и окончания. Затем используйте дерево RB для хранения объектов Period. Затем вы можете перемещаться по дереву на определенную дату:

Если дата находится в течение первого периода, вы ее нашли. если дата предшествует дате начала периода, перейдите влево node и проверьте этот период если дата указана после даты окончания периода, перейдите вправо node и проверьте, что период

Ответ 5

Хорошо, как уже было сказано, найти этот содержащий Duration интервал, который вы уже делаете или используя миллис, напрямую для этого варианта использования, и математика, участвующая в этом, проста. Однако, если у вас был вариант использования, который гарантировал интервал Period с часами, вот как это можно было бы обработать:

  • Переведите Period в приблизительную продолжительность часов и используйте это, чтобы определить, сколько интервалов от цели происходит от начала.
  • Оцените Period по вашей оценке. Обработайте скопления 24 часа в качестве дополнительных дней.
  • Переместить интервал происхождения в масштабе. Если перемещенный интервал содержит вашу цель, все готово. Если нет, пересчитайте сумму, которую вы пропустили.

Важно отметить, что поиск содержащего интервала состоит в том, что все сложение Period должно происходить непосредственно из источника, а не из промежуточных интервалов для согласованности. Например, если у вас есть Period одного месяца с началом 31 января, интервалы сразу после интервала начала должны начинаться 28 и 31 марта. Добавление двух месяцев к 31 января будет корректно дано 31 марта, но добавление одного месяца к 28 февраля неверно даст 28 марта.

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

public static final int NUM_HOURS_IN_DAY = 24;
public static final int NUM_HOURS_IN_MONTH = 730;  // approximate

public ZonedDateTime startOfContainingInterval(ZonedDateTime origin, Period period, int hours, ZonedDateTime target) {
    return intervalStart(origin, period, hours, containingIntervalNum(origin, period, hours, target));
}

public int containingIntervalNum(ZonedDateTime origin, Period period, int hours, ZonedDateTime target) {
    int intervalNum = 0;
    ZonedDateTime intervalStart = origin, intervalFinish;
    long approximatePeriodHours = period.toTotalMonths() * NUM_HOURS_IN_MONTH + period.getDays() * NUM_HOURS_IN_DAY + hours;
    do {
        long gap = ChronoUnit.HOURS.between(intervalStart, target);
        long estimatedIntervalsAway = Math.floorDiv(gap, approximatePeriodHours);
        intervalNum += estimatedIntervalsAway;
        intervalStart = intervalStart(origin, period, hours, intervalNum);
        intervalFinish = intervalStart(origin, period, hours, intervalNum + 1);
    } while (!(target.isAfter(intervalStart) && target.isBefore(intervalFinish) || target.equals(intervalStart)));
    return intervalNum;
}

public ZonedDateTime intervalStart(ZonedDateTime origin, Period period, int hours, int intervalNum) {
    Period scaledPeriod = period.multipliedBy(intervalNum).plusDays(hours * intervalNum / NUM_HOURS_IN_DAY);
    long leftoverHours = hours * intervalNum % NUM_HOURS_IN_DAY;
    return origin.plus(scaledPeriod).plusHours(leftoverHours);
}