ShedLock - второй инстанс начинает выполнять джобу, хотя первый ещё не окончил
Есть библиотека ShedLock
: используем её для организации блокировок в тех случаях, когда приложение развёрнуто в нескольких инстансах. Написал такую простенькую джобу:
@Component
@Slf4j
@ConditionalOnProperty(name = "jobs.testJob.enable", matchIfMissing = true)
public class TestJob {
@Value("${app.nodeName}")
private String nodeName;
@Scheduled(cron = "${jobs.testJob.cron}")
@SchedulerLock(name = "testJob", lockAtMostFor = "30s")
public void doSmth() throws InterruptedException {
log.info("testJob");
log.info("Текущее время: {}", LocalDateTime.now());
log.info("Выполнено на ноде: {}", nodeName);
Thread.sleep(10000); // пауза 10 секунд
log.info("Конец джобы testJob");
}
}
В файле application.yml
задаю соответствующие настройки:
app:
nodeName: ${NODE_NAME}
jobs:
testJob:
enable: true
# Каждые 10 секунд
cron: '*/10 * * * * *'
Тут логика такая: параметр lockAtMostFor
задаёт дефолтное значение блокировки, на случай если исполняющая нода вдруг сломается. Т.е. нода берет джобу в работу, и в этот момент проставляет в таблицу дефолтное время блокировки (в данном случае 30 секунд):
|name | lock_until | locked_at | locked_by |
--------------------------------------------------------------------------
|testJob | 2024-09-10 10:30:00.130 | 2024-09-10 10:29:30.130 | node_1 |
И если сейчас сервер упадёт, то в базе это дефолтное время и останется. Но если ничего не сломалось, джоба отработала примерно за 10 секунд с копейками, и в конце время lock_until
обновилось в таблице на реальное:
|name | lock_until | locked_at | locked_by |
--------------------------------------------------------------------------
|testJob | 2024-09-10 10:29:40.157 | 2024-09-10 10:29:30.130 | node_1 |
Ну вроде бы всё нормально работает, так как и ожидалось.
Вопрос вот в чём: я решил проверить, а что будет, если джоба будет работать дольше чем указано в параметре lockAtMostFor
. Поставлю теперь паузу 60 секунд
и запущу 2 экземпляра приложения:
@Scheduled(cron = "${jobs.testJob.cron}")
@SchedulerLock(name = "testJob", lockAtMostFor = "30s")
public void doSmth() throws InterruptedException {
log.info("testJob");
log.info("Текущее время: {}", LocalDateTime.now());
log.info("Выполнено на ноде: {}", nodeName);
Thread.sleep(60000); // пауза 60 секунд
log.info("Конец джобы testJob");
}
В итоге 1-я нода начала выполнять джобу и я вижу соответствующую запись в таблице:
|name | lock_until | locked_at | locked_by |
--------------------------------------------------------------------------
|testJob | 2024-09-10 10:43:20.121 | 2024-09-10 10:42:50.121 | node_1 |
т.е. блокировка по умолчанию выставилась на 30 секунд
. Я тут ожидал, что пока одна нода выполняет джобу, то другая не сможет её начать выполнять, даже если время истекло. Т.е. я думал она минуту будет работать, закончит, обновит в таблице поле lock_until на финальное значение 2024-09-10 10:43:50.121
(+ 1 минута), и потом уже другая нода сможет схватить её в работу.
Но на деле оказалось так: прошло 30 секунд, затем ещё 10 секунд (cron настроен раз в 10 секунд), и далее 2-ая нода начинает выполнять эту же джобу и обновляет запись в таблице:
|name | lock_until | locked_at | locked_by |
--------------------------------------------------------------------------
|testJob | 2024-09-10 10:44:00.079 | 2024-09-10 10:43:30.079 | node_2 |
ну и в консоли я вижу, что 1-ая нода ещё не успела закончить (не выведена надпись Конец джобы testJob
) а 2-ая нода уже начала выполнять.
Далее попробую тоже самое, но в режиме дебага и без Thread.sleep
@Scheduled(cron = "${jobs.testJob.cron}")
@SchedulerLock(name = "testJob", lockAtMostFor = "30s")
public void doSmth() throws InterruptedException {
log.info("testJob");
log.info("Текущее время: {}", LocalDateTime.now());
log.info("Выполнено на ноде: {}", nodeName); // точка остановки тут
// Thread.sleep(60000); // пауза 60 секунд
log.info("Конец джобы testJob");
}
Запускаю обе ноды в дебаге. 1-ая нода начинает выполнять джобу и останавливается на указанной строчке. В итоге в консоли 1-ой ноды вижу:
testJob
Текущее время .....
Далее проходят 30 секунд и 2-ая нода начинает выполнять джобу и обновляет таблицу lock. После этого я во 2-ой ноде убираю точку остановки и она просто продолжает работать, и каждые 10 выполняет джобу, обновляет таблицу и пишет в консоль. И всё это время 1-ая нода так и висит в дебаге.
В общем, я что-то немного не понял. Мне казалось, что по-логике должно быть так: не зависимо от настройки времени лока, если одна нода ещё выполняет джобу, то другие не должны её перехватывать. Но в тех 3-ёх статьях по ShedLock что я читал ничего про это не сказано.
В общем, можно ли настроить как то так, как я сейчас описал? ))) Чтобы пока одна нода ещё выполняет джобу, то другие не могли её перехватывать ?
И если нельзя, то что тогда делать? Я пока только одну идею придумал: я в своих джобах выставил параметр с большим запасом: lockAtMostFor = "60m"
- 1 час. Т.е. я сейчас спокойно могу сидеть дебажить, мне часа точно хватит, и другая нода в этот момент не начнёт параллельно выполнять джобу.