Apache 2.0 · Java и Kotlin · Spring Boot 2/3/4 · PostgreSQL 15+

Устойчивое выполнение процессов в виде Java библиотеки

JES надёжно выполняет процессы (workflow) и задания: они сохраняются при сбоях, перезапусках и перемещении между машинами, не теряя своего состояния. Отдельный сервер не нужен. Если у вас уже есть Spring Boot и PostgreSQL, у вас уже есть все необходимое.

Одна зависимость. Три таблицы. Ноль инфраструктуры.

OrderWorkflow.java
@Service
public class OrderWorkflow {
    // Запускаем workflow обработки заказа
    public void start(String orderId) {
        JobOptions options =
            JobOptions.jobWithExternalId(orderId);
        jobSystem.startJob(options,
            this::processOrder, String.class, orderId);
    }

    // Сам переживёт сбои и повторные запуски
    public String processOrder(String orderId) {
        String receipt = jobSystem.execute(
            paymentService::chargePayment,
            String.class, orderId);
        try {
            jobSystem.execute(
                deliveryService::deliverGoods, orderId);
        } catch (JobFailure e) {
            jobSystem.execute(
                paymentService::rollbackPayment,
                orderId, receipt);
            throw e;
        }
        return receipt;
    }
}
30 000 / сзаданий в секунду на БД c8r16
1–2 мсот отправки задания до выполнения
3 таблицыPostgreSQL - всё состояние системы
∞длительность выполнения задания не ограничена

Всё, что должен уметь движок workflow, - внутри вашего приложения

Иерархия заданий

Неограниченная вложенность родительских и дочерних заданий, последовательно или параллельно. Дочерние идемпотентны по построению: тот же метод и те же аргументы → выполнится один раз, результат закеширован.

Отказоустойчивость по умолчанию

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

Состояние и контекст

Именованные типизированные слоты состояния переживают перезапуски задания и читаются извне. Типизированный контекст передаётся между заданиями - с поддержкой полиморфизма.

Сигналы и отмена

Отправляйте произвольные сообщения работающему заданию, отменяйте его извне, прерывайте при необходимости. Сигналы передаются вниз по иерархии.

Ограничения ресурсов

Лимиты на общее число заданий, на тип задания и на ресурс. Равномерное распределение по инстансам приложения - из коробки.

Наблюдаемость

Метрики Micrometer, распространение trace_id/span_id по иерархии, HealthIndicator для Actuator, встроенный HTTP API и поиск по истории заданий.

Тесты без базы данных

Лёгкий TestJobSystem выполняет настоящие задания в юнит-тестах - без PostgreSQL, с пропуском задержек и ограничением перезапусков.

Обновления без простоя

Задокументированные стратегии эволюции заданий на работающем приложении: версионирование методов, ветвление по времени первого запуска, drain-затем-update.

Четыре шага до запуска

Подключите зависимость

JES опубликована на Maven Central. Spring Boot 2.7+, Java 17+, PostgreSQL 15+.

pom.xml
<dependency>
  <groupId>dev.jesproject</groupId>
  <artifactId>job-execution-system</artifactId>
  <version>2.0.0</version>
</dependency>

Создайте схему БД

Три таблицы через Liquibase: скопируйте db/job_execution_system из репозитория в свои миграции и подключите changelog.

changelog-master.xml
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog">

  <!-- Таблицы JES идут в комплекте с библиотекой:
       скопируйте db/job_execution_system в свои миграции -->
  <include file="job_execution_system/init/v2.0.xml"
           relativeToChangelogFile="true"/>

</databaseChangeLog>

Включите JES

Одна аннотация на главном классе приложения.

Application.java
@SpringBootApplication
@EnableJobExecutionSystem
public class ExampleApp {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApp.class, args);
    }
}

Запустите первое задание

Любой публичный метод Spring-бина становится устойчивым заданием - повторяется до успеха.

HelloService.java
@Service
public class HelloService {
    private final JobSystem jobSystem;

    public void runHelloJobs() {
        jobSystem.startJob(this::hello, "World");
    }

    public void hello(String name) {
        // выполнится как минимум один раз, переживёт сбой
        log.info("Hello, {}!", name);
    }
}

Как это работает

Три таблицы - весь движок

Задание - это просто строка в таблице PostgreSQL: метод, аргументы, родитель, статус, расписание. Никакого скрытого состояния - сделайте SELECT из таблиц jobs, job_signals и job_data и увидите всё.

Инстансы разбирают задания из очереди

Экземпляры приложения опрашивают таблицы и забирают следующее задание короткой транзакцией с FOR UPDATE SKIP LOCKED - поэтому десятки инстансов масштабируются горизонтально, не мешая друг другу, а работа сама течёт к свободным.

Падение инстанса - не падение задания

Каждый исполнитель подтверждает активность heartbeat'ом. Если инстанс умер, другой забирает его задания и выполняет заново - хотя бы раз, с сохранённым состоянием. То же верно и для падения всего кластера.

Workflow - это дерево заданий, которое возобновляет само себя

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

Как это выглядит на практике

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

Запуск параллельных заданий

Одно дочернее задание на заказ - выполняются параллельно, повторяются независимо, результаты собираются после завершения всех.

OrderProcessor.java
// По дочернему заданию на заказ: параллельно, независимые повторы
public List<Report> processAll(List<Order> orders) {
    List<Job> jobs = jobSystem.startJobs(
        JobDefinition.ofList(this::processOrder, orders));

    // ждём все дочерние и собираем результаты
    return Job.getAll(jobs, Report.class).toList();
}

public Report processOrder(Order order) {
    // те же аргументы → выполнится один раз, результат закеширован
    return warehouse.reserve(order);
}

Гибкая настройка под вашу нагрузку

Из коробки JES работает с настройками по умолчанию - конфигурировать ничего не нужно. Но каждая ручка доступна: пул потоков, задержки повторов, heartbeat, автоочистка и встроенный API управления.

application.yml
job-execution-system:
  max-runner-count: 100    # потоки под задания
  jobs:
    new-check-interval: 200ms # старт новых заданий
    heartbeat-rate: 850ms     # обнаружение сбоев
    heartbeat-timeout: 5000ms # порог failover
    delay-on-error: 0ms       # пауза перед повтором
  signals:
    new-check-interval: 1000ms # доставка сигналов
  cleanup:
    enabled: false           # автоочистка старых заданий
    expiration-period: P1Y8M5D
    cron: "0 0 */20 * * *"   # расписание очистки
  api:
    enabled: false           # HTTP API управления
  • Дефолты, которые работают

    Все значения выше необязательны - без этого блока JES работает с теми же дефолтами.

  • Масштаб на инстанс

    Количество потоков, предварительная выборка и сигналы подтверждения активности устанавливаются для каждого экземпляра приложения; балансировка работы экземпляров осуществляется автоматически.

  • Бережно к БД

    Частоты опроса и размер пула Hikari настраиваются - число соединений строго ограничено.

Все настройки необязательны - в примере показаны значения по умолчанию.

Полный справочник в документации →

Создана для нетривиальных задач распределённых систем

Идемпотентное выполнение Выполнение кода как минимум один раз Отложенный запуск и расписание Обработка в стиле MapReduce Распределённые вычисления Transactional Outbox Распределённые повторы Оркестрация Saga

Ваш PostgreSQL - уже движок workflow