Длительно выполняемая задача Sidekiq перезапускает внутренний код

Я в настоящее время создаю плагин, связанный с meritmoot.com (который находится в разработке и работает в режиме rolling releases). Из-за использования внешних данных я использую длительную фоновую задачу Sidekiq. К сожалению, по какой-то причине задача постоянно перезапускает внутренний код, не завершаясь с ошибкой и не выдавая сообщений об ошибках. Не упустил ли я что-то в Sidekiq, что может вызывать перезапуск задачи?

В моей последней задаче (которая должна выполняться только раз в сутки), связанной с проверкой присутствия (Roll Calls), задача перезапускалась в следующие интервалы без ошибок:

0 ч 58 мин 42 с (время начала первой итерации)
1 ч 30 мин 12 с (первый перезапуск)
2 ч 1 мин 1 с (второй перезапуск)
3 ч 46 мин 49 с
4 ч 17 мин 11 с
4 ч 47 мин 33 с

Задача не завершилась, и весь прогресс был сброшен. Стоит отметить, что у меня есть собственный процесс логирования, который перенаправляет stderr и stdout для моего внутреннего кода, хотя я сомневаюсь, что это может влиять на Sidekiq. (Спросите меня, если хотите взглянуть — это очень полезно для разработки!)

Я мог бы сохранять прогресс внутри кода, но предпочёл бы более простой процесс, так как это создаёт дополнительные накладные расходы. Не упустил ли я что-то в Sidekiq, что может вызывать перезапуск моего кода?

Итак, вы хотите, чтобы задача Sidekiq намеренно выполнялась более часа за один запуск? Можете немного подробнее объяснить, зачем это нужно?

Я обрабатываю большой объем внешних данных и храню их на своем сайте. Эти данные касаются информации о Конгрессе США.

Редактирование: например, публично доступные законопроекты, голосование поименно и данные о членах Конгресса.

У вас заканчивается память? На нашем хостинге Sidekiq настроен на автоматический перезапуск, если он начинает использовать слишком много памяти.

Вот несколько идей:

  • Отправляйте данные через API (но в этом случае вы упрётесь в ограничения скорости запросов)
  • Запустите внешний скрипт, особенно если нужно лишь извлечь начальную порцию данных, а обновления будут занимать меньше времени

Речь о памяти базы данных или жёсткого диска? Да, я использую её довольно много. Сейчас рассматриваю слегка модифицированный вариант предложения от @pfaffman: я форкаю процесс, позволяя основному потоку завершиться, но создавая дочерний процесс с тем же контекстом (по сути, внешний скрипт по отношению к Sidekiq).

Проверяю паттерн из https://stackoverflow.com/questions/806267/how-to-fire-and-forget-a-subprocess#806326, чтобы решить проблему:

pid = Process.fork
if pid.nil? then
  # В дочернем процессе
  exec "whatever --take-very-long"
else
  # В родительском процессе
  Process.detach(pid)
end

Это немного странная ситуация, но у API, к которому я подключаюсь, нет функции обновления, поэтому я просто обновляю данные, ежедневно скачивая заново большие части API :expressionless: :man_shrugging:

Через пару часов сообщу, как продвигается дело.

edit: снова остановилось — думаю, буду периодически сохранять прогресс и поищу способы сделать процесс более эффективным.

Вместо этого, почему бы не научить вашу задачу работать небольшими порциями? Действительно ли ей нужно занимать 4 часа? Синхронизируйте 10 тем, затем еще 10… и так далее.

Sidekiq не имеет механизмов для прерывания длительных задач; это могут сделать пересборки приложения или даже обновления через веб-интерфейс.

Спасибо за всю помощь.

В итоге я удалил код процесса — он оказался неэффективным. Перезапуск задачи лишь маскировал фундаментальную проблему: крайне низкую производительность :sweat_smile:. Вместо этого я:

  • Пишу пакетные SQL-запросы (что намного быстрее последовательной обработки), определяю, когда обновление действительно необходимо, и позволяю себе пропустить использование класса PostRevisor для повторного обновления неизменённых элементов.
  • Повышаю эффективность извлечения данных по HTTP, используя постоянные соединения и другие оптимизации, включая сжатые данные (где это возможно).

Я обнаружил, что выполнение пакетных SQL-команд даёт огромный прирост скорости. То, что я обновляю:

таблица post: поля cooked, last_updated
таблица topic: поля title, last_updated

Моя следующая идея — полностью отказаться от PostRevisor, действуя примерно так:

  1. Переместить данные во временную таблицу.

  2. Выполнить:
    UPDATE topics FROM temp_table
    SET topics.title = temp_table.title, topics.last_updated = temp_table.last_updated
    WHERE topics.id = temp_table.id AND topics.title != temp_table.title

  3. Выполнить:
    UPDATE posts FROM temp_table
    SET posts.raw = temp_table.raw, posts.last_updated = temp_table.last_updated
    WHERE posts.id = temp_table.id AND posts.raw != temp_table.raw

  4. После этого запустить задачу повторного индексирования поиска, так как изменились заголовок и содержимое.

Не упускаю ли я что-то? Discourse — сложная система, и, отказавшись от PostRevisor, я боюсь задеть таблицы, с которыми не работал (например, post_stats, post_timings, post_uploads, quoted_posts — это лишь некоторые из тех, что я вижу в базе данных). Однако мне не нужна вся валидация, которую предоставляет PostRevisor, поскольку система получает эти изменения из доверенного и предсказуемого источника. Решение кажется довольно рискованным.

Что вы думаете?

Обновление — я проводил проверку кода, так как наблюдалось необычное количество обновлений со временем, и обнаружил, что есть причина для необоснованных обновлений элементов данных, которые на самом деле не изменились в своём сыром формате JSON. Как только эта ошибка будет исправлена, вышеупомянутое, вероятно, станет не нужно :tipping_hand_man: следовало бы провести тестирование… это сэкономило бы мне много времени. Думаю, я всё же попробую реализовать описанное выше, но это не будет приоритетом. Это поможет при быстрых обновлениях, когда я изменю формат представления данных. Кроме того, код уже написан, просто не протестирован.

Готов код массового обновления — хотели бы вы, чтобы я выложил его в конкретную ветку, когда он станет более стабильным? Он довольно специфичен по области применения, но выполняет свою задачу быстро: обновляет тысячи записей, включая теги. Он разработан как расширение для TopicsBulkAction. Вот написанное мной README, если хотите получить более подробную информацию:

  # входные данные
  #   - список хешей, содержащих cooked, post_id, topic_id, title, updated_at, tags (raw будет указывать на cooked)
  #   [{post_id: #, cooked: "", topic_id: #, title: "", updated_at: date_time, tags: [{tag: "", tagGroup: ""}, ... ] } ,  ... ]
  #   - category_name, название обновляемой категории. Используется для индексации поиска.

  # необязательные атрибуты хеша для включения в элементы списка:
  #   - raw, если не указан, будет равен cooked.
  #   - fancy_title, если не указан, будет равен title.
  #   - slug, если не указан, будет сгенерирован из title (это связано с его URL).

  # сценарий использования: регулярное обновление тем из изменяющегося внешнего источника данных (не Discourse) эффективным способом
  # для синхронизации обновляемой информации. Обратите внимание: этот код не предназначен для общей публикации постов или тем, а только для обновления
  # заголовка темы и ОСНОВНОГО поста темы. Для общего редактирования постов обратитесь к PostRevisor в lib/post_revisor.rb.

  # - Предполагает, что данные уже обработаны (pre-cooked), имеют кастомную обработку (custom cooked) или используются как есть. Данные не проверяются.
  # - при создании постов, если raw == cooked, необходимо установить (cook_methods: Post.cook_methods[:raw_html]).
  #     Это нужно, если вы вставляете собственный HTML внутрь поста.
  #     В противном случае Discourse может переобработать его в будущем, что приведёт к проблемам. Убедитесь, что источник информации
  #     надёжен, а содержимое экранировано.
  # - Если описанное выше не подходит, обязательно включите raw, укажите правильный метод обработки при создании поста
  #     (на случай повторной обработки системой), пропустите raw через выбранный метод обработки и включите как raw, так и полученный cooked
  #     в ваши хеши.
  # - Отслеживает word_count, вычисляя разницу между количеством слов до и после обновления поста, и передаёт это значение теме.
  # - Аналогичным образом отслеживает количество тегов.