ReactionLikeSynchronizer каждый час повторно обрабатывает лайки PostAction, которые уже отправлены в корзину

Мы столкнулись с проблемой на самохостинговом сайте Discourse, в которой ReactionLikeSynchronizer, по-видимому, вызывает ошибки 500/504 при выполнении действий пользователя.

Задача, похоже, каждый час повторно помечает как удалённые одни и те же 26 766 лайков PostAction, хотя все они уже были помечены как удалённые. Во время этих длительных циклов синхронизации обычные запросы пользователей, похоже, блокируются в PostgreSQL и достигают 30-секундного тайм-аута Pitchfork.

Окружение

  • Самохостинговый Discourse версии v2026.10.0-latest-277-ge1f6bd561
  • PostgreSQL 18.4
  • discourse_reactions_like_sync_enabled = true
  • Это старый сайт, поэтому на нём много исторических данных о реакциях

Сообщения пользователей

  • Реакции иногда не срабатывают и выводят сообщение «Что-то пошло не так».
  • Темы, которые посещал пользователь, иногда остаются отмеченными как непрочитанные.
  • Сообщения о проблемах группируются примерно раз в час, в начале каждого часа.

Логи и данные

13:58:58 ReactionLikeSynchronizer запускается
13:59:50 Тайм-аут воркера Pitchfork / 500
14:00:02 Тайм-аут воркера Pitchfork / 500
14:00:18 Тайм-аут воркера Pitchfork / 500
14:00:48 ReactionLikeSynchronizer завершает работу

15:00:09 Синхронизация начинается
15:01:16 Тайм-аут запроса / 500
15:01:40 Тайм-аут запроса / 500
15:01:45 Синхронизация завершена

Вывод синхронизатора

Вставлено 0 лайков post action, восстановлено 0 удалённых лайков post action. (0 всего)
Удалено 26766 лайков post action.

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

Содержимое базы данных

SELECT
  COUNT(*) AS total,
  COUNT(*) FILTER (WHERE pa.deleted_at IS NULL) AS currently_active,
  COUNT(*) FILTER (WHERE pa.deleted_at IS NOT NULL) AS already_trashed
FROM post_actions pa
JOIN discourse_reactions_reaction_users ru
  ON ru.user_id = pa.user_id
 AND ru.post_id = pa.post_id
JOIN discourse_reactions_reactions r
  ON r.id = ru.reaction_id
WHERE pa.post_action_type_id = 2
  AND r.reaction_value IN (
    '-1','poop','face_with_symbols_on_mouth','enraged_face',
    'face_vomiting','nauseated_face','angry','kitchen_knife',
    'water_pistol','bomb','clown_face','unamused_face','roll_eyes',
    'confused','smirking_face','disappointed_face','pensive_face',
    'lying_face','weary_face','tired_face','confounded_face',
    'persevering_face','smiling_face_with_horns','angry_face_with_horns',
    'cigarette','person_gesturing_no','prohibited','no_entry',
    'dagger','cross_mark'
  );
 total | currently_active | already_trashed

-------+------------------+-----------------

 26766 |                0 |           26766

Примечания

Что примечательно, похоже, ReactionLikeSynchronizer#trash_excluded_post_actions сопоставляет лайки, которые были исключены, даже если они уже были удалены. Запрос выглядит так:

UPDATE post_actions
SET deleted_at = NOW()
FROM discourse_reactions_reaction_users ru
INNER JOIN discourse_reactions_reactions
  ON discourse_reactions_reactions.id = ru.reaction_id
WHERE post_actions.user_id = ru.user_id
  AND post_actions.post_id = ru.post_id
  AND post_actions.post_action_type_id = :like
  AND discourse_reactions_reactions.reaction_value IN (:excluded_from_like)
RETURNING post_actions.id

Примечательно, что здесь не исключаются удалённые действия с помощью условия вроде AND post_actions.deleted_at IS NULL.

Похоже, это приводит к тому, что один и тот же набор строк обрабатывается повторно каждый час. При необходимости я могу добавить трассировку стека.

В качестве временного решения я отключил discourse_reactions_like_sync_enabled и наблюдаю, прекратятся ли ошибки, возникающие каждый час.

Отредактировано: исправлены опечатки и уточнен запрос в разделе «Примечания».