Жизненный цикл задачи
Предпосылки: архитектура Sidekiq.
← Архитектура | Гарантии и идемпотентность →
Архитектура — карта: три роли, структуры данных, компоненты процесса. Но что конкретно происходит, когда код вызывает SendEmailJob.perform_async(user_id)?
Client: от вызова до Redis
Вызов perform_async запускает цепочку на стороне Rails-приложения:
SendEmailJob.perform_async(42)- Sidekiq::Client создаёт JSON-хеш с описанием задачи. Это не фиксированный «формат на все времена», а базовый payload, который потом может обрасти полями для retry, schedule или ActiveJob:
{
"class": "SendEmailJob",
"args": [42],
"queue": "default",
"jid": "b4a577edbccf1d805744efa9",
"created_at": 1712560000123,
"enqueued_at": 1712560000123,
"retry": true
}jid (job ID) — уникальный идентификатор конкретного вызова, 12 случайных байт в hex (24 символа). class указывает, какой Ruby-класс выполнит работу. args — аргументы для метода perform.
created_at фиксирует момент создания job. enqueued_at показывает момент попадания в рабочую очередь. Для scheduled job это поле появится позже, когда Poller переместит задачу из schedule в обычную queue. Формат *_at зависит от версии: в Sidekiq 7.x это epoch seconds с дробной частью, в 8.0+ — integer milliseconds.
-
JSON-хеш проходит через client middleware chain — цепочку обработчиков, каждый из которых может модифицировать или отклонить задачу. Middleware-обработчик получает хеш, может добавить поля (request_id, tenant_id), вызывает
yieldдля передачи дальше по цепочке. -
После middleware Sidekiq выполняет
LPUSH queue:default '<json>'— задача в Redis.
Ограничения аргументов: почему только примитивы
Sidekiq сериализует задачу через JSON.dump и десериализует через JSON.parse. JSON — текстовый формат, который поддерживает только примитивные типы: String, Integer, Float, Boolean, nil, Array и Hash со строковыми ключами.
Что происходит с типами, которые JSON не знает:
# ActiveRecord-объект → бесполезная строка
SendEmailJob.perform_async(User.find(1))
# В Redis: {"args":["#<User:0x000055f1a2b3c4d5>"]}
# perform получит строку "#<User:0x...>", а не объект
# Symbol (:active — легковесный идентификатор Ruby) → строка, ключи тоже строки
SendEmailJob.perform_async(status: :active)
# В Redis: {"args":[{"status":"active"}]}
# perform получит {"status" => "active"}, не {status: :active}
# Time → строка
SendEmailJob.perform_async(Time.now)
# В Redis: {"args":["2024-01-15 10:30:00 +0000"]}
# perform получит строку, не Time-объектПравильный подход — передавать ID и загружать объект из базы внутри perform:
# Вместо perform_async(user) — передать ID
SendEmailJob.perform_async(user.id)
def perform(user_id)
user = User.find(user_id) # свежие данные из БД
UserMailer.welcome(user).deliver_now
endПобочный бонус: между постановкой задачи и выполнением может пройти время (очередь забита). Загрузка из базы даёт актуальное состояние объекта, а не устаревший снимок.
Server: от Redis до perform
На стороне серверного процесса Processor выполняет зеркальную цепочку:
-
BRPOP queue:default <timeout>— Processor ждёт задачу из Redis. Если очередь пуста, после короткого timeout он повторяет попытку. Если задача есть —BRPOPатомарно извлекает её из списка. -
JSON.parse— десериализация JSON обратно в хеш. -
Хеш проходит через server middleware chain. Server middleware оборачивает выполнение задачи: типичные примеры — логирование (записать начало/конец), метрики (замерить время), восстановление контекста (установить
Current.request_idиз поля задачи). -
Sidekiq создаёт экземпляр класса (в данном примере
SendEmailJob.new) и вызываетperform(42)с аргументами из JSON.
Полная цепочка:
sequenceDiagram participant C as Client (Rails) participant R as Redis participant S as Server (Sidekiq) C->>C: perform_async(42)<br/>→ Sidekiq::Client<br/>→ JSON: {class, args, jid...}<br/>→ client middleware chain C->>R: LPUSH queue:default R->>S: BRPOP S->>S: JSON.parse<br/>→ server middleware chain<br/>→ SendEmailJob.new.perform(42)
Middleware
Client и server middleware работают по одному принципу: каждый обработчик вызывает yield, чтобы передать управление следующему в цепочке (или основному действию), и может выполнить код до и после:
class LoggingMiddleware
include Sidekiq::ServerMiddleware
def call(job_instance, job_payload, queue)
# CLOCK_MONOTONIC — монотонные часы ОС, не подвержены переводу системного времени
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield # следующий middleware или perform
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
logger.info "#{job_payload['class']} completed in #{duration.round(3)}s"
end
endРегистрация middleware — в инициализаторе Rails:
Sidekiq.configure_server do |config|
config.server_middleware do |chain|
chain.add LoggingMiddleware
end
endClient middleware настраивается и на стороне client (configure_client), и на стороне server (configure_server) — потому что серверные задачи тоже могут ставить новые задачи (fan-out).
Отложенные задачи
perform_in и perform_at не кладут задачу в рабочую очередь. Вместо этого задача попадает в sorted set schedule:
SendEmailJob.perform_in(5.minutes, user_id)
# → ZADD schedule <Time.now + 300> '<json>'Задача лежит в schedule, пока не наступит её время. Poller-поток периодически проверяет sorted set: выбирает задачи со score ≤ текущему времени и перемещает их в рабочую очередь (LPUSH). После этого задача проходит обычный путь: BRPOP → middleware → perform.
Интервал проверки Poller масштабируется с количеством процессов: чем больше процессов в кластере, тем реже каждый проверяет, чтобы не создавать лишнюю нагрузку на Redis.
Задача прошла полный путь и выполнена. Через два часа Sidekiq-процесс убит OOM-killer (механизм ядра Linux, который принудительно завершает процесс при нехватке памяти). Десять задач были в работе. В стандартном fetch-цикле BRPOP уже удалил их из Redis — все десять потеряны навсегда.
← Архитектура | Гарантии и идемпотентность →