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 час. Т.е. я сейчас спокойно могу сидеть дебажить, мне часа точно хватит, и другая нода в этот момент не начнёт параллельно выполнять джобу.


Ответы (0 шт):