yii2-queue-monitor icon indicating copy to clipboard operation
yii2-queue-monitor copied to clipboard

Job progress

Open loveorigami opened this issue 5 years ago • 25 comments

Роман, подскажите, пожалуйста, и возможно ли, реализовать следующую логику. Есть задача. Выполняется она длительное время (10-15 мин.).

В коде самой задачи я хочу проставить некие метки о ее готовности (прогресс исполнения). Нужно это, чтобы знать, в каком именно месте кода происходит "падение" воркера.

class MyJob
{
protected $progress;

public function execute()
{
   $this->progress = 0;
   // job start
  $this->progress = 25;
  // complex logic
  $this->progress = 75;
  // simple logic
  $this->progress = 100;
}

public function getProgress():int
{
  return $this->progress;
}

}

В yiisoft/yii2-queue/src/cli/Queue.php:113 есть такой участок кода с событием self::EVENT_WORKER_LOOP


    protected function runWorker(callable $handler)
    {
         //....

        $exitCode = null;
        try {
            call_user_func($handler, function () use ($loop, $event) {
                $this->trigger(self::EVENT_WORKER_LOOP, $event);
                return $event->exitCode === null && $loop->canContinue();
            });
        } finally {
            $this->trigger(self::EVENT_WORKER_STOP, $event);
            $this->_workerPid = null;
        }

        return $event->exitCode;
    }

Смогу ли я через воркер считать значение $progress задачи и записать его в монитор. И как именно?

Спасибо.

loveorigami avatar Apr 23 '19 07:04 loveorigami

Нет в этом модуле прогресс выполнения заданий не предусмотрен. Ну и EVENT_WORKER_LOOP не то, что стоило бы для этого использовать. Это событие происходит в основном процессе воркера в цикле опроса очереди. Т.е. после того, как выполнилась одна задача, но перед тем, как начнет выполняться следующая.

zhuravljov avatar Apr 23 '19 11:04 zhuravljov

технически это реализовать можно либо на уровне самого джоба, (или базового класса джобов)


class MyJob implements JoиInterface
   {
        private $progress;
        
        public function execute($queue)
        {
              try{
                   $this->executeInternal();
              }catch(\Throwable $e){
                    $this->saveProgress(..);
                     throw $e;
             }
        }
   }

либо поведением которое добавит свойство прогресса и будет записывать его по событиям завершения/ошибки или еще когда но главная проблема в том, что довольно проблематично добавить какие-то свои записи в таблицу текущего мониторинга - это надо перекрывать модель, свои миграции...
Де-факто кейсы с необходимостью записи какой-то текущей стадии и/или отчета выполнения очень частые, и было бы неплохо чтоб модуль поддерживал какое-то произвольное поле summary типа json/text в зависимости от бд куда можно было писать что-то в процессе выполнения без допольнительных шаманств с перекрытием или доп.таблицами.

Insolita avatar Apr 23 '19 16:04 Insolita

Спасибо за идею! Новые поля пришлось добавит, т.к. решал //-но еще одну задачу:

Определенный экшен генерирует около 100-800 задач, по которым необходимо знать их суммарное выполнение и блокировать повторный запуск. Например, есть некий список товаров от разных поставщиков - более 15000 наименований. На все эти товары необходимо сформировать документы приема, сгруппированные по поставщикам. Формирование 1 документа занимает ок. 10 сек. А их может быть от 100 до 800 (один документ на job - задачу).

С job-ом я пока размышляю на предмет событий. Чтоб обработку можно было добавить в сам монитор https://github.com/zhuravljov/yii2-queue-monitor/blob/master/src/JobMonitor.php#L72

loveorigami avatar Apr 23 '19 17:04 loveorigami

Де-факто кейсы с необходимостью записи какой-то текущей стадии и/или отчета выполнения очень частые, и было бы неплохо чтоб модуль поддерживал какое-то произвольное поле summary типа json/text в зависимости от бд куда можно было писать что-то в процессе выполнения без допольнительных шаманств с перекрытием или доп.таблицами.

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

Я обычное если нужен прогресс чего-либо решаю вопрос на уровне проекта.

zhuravljov avatar Apr 23 '19 18:04 zhuravljov

В моем случае, как раз таки это и нужно для отладки.

В коде самой задачи я хочу проставить некие метки о ее готовности (прогресс исполнения). Нужно это, чтобы знать, в каком именно месте кода происходит "падение" воркера.

Задачи пишут разные программисты в команде, иногда "тяжелые" задачи нужно дополнительно разбить на части и проанализировать каждую отдельно, чтоб узнать, где и по какой причине происходит задержка... (кривой запрос, долгая связь со сторонним сервисом, устаревшая библиотека... )

loveorigami avatar Apr 23 '19 18:04 loveorigami

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

  • добавлению поля для фиксации стадии/прогресса, иногда чтобы просто глянув -понимать долго ли еще будет шуршать задача, иногда на основе стадии делается умный перезапуск чтоб не повторять лишние этапы при повторе
  • добавлению поля для отчета, типа "обработано столько-то, загружено столько-то, найдены проблемы там-сям" или ссылки на сгенерированный файл и сотв.метода для сохранения из джоба
  • добавление полей для полиморфной связи с сущностями бизнес-логики, чтобы из других моделей можно было получить инфу о последней связанной задаче со статусом/отчетом/логом

Ну еще иногда для детальной отладки проброска контекста джоба в параметры приложения, и соответственно настроенный DbLogTarget который добавлет этот контекст в таблицу, для быстрого отсмотра по конкретной джобе, но это уже к модулю практически не относится, только во вьюху доп.ссылка добавляется

Insolita avatar Apr 23 '19 19:04 Insolita

Я у себя стараюсь проектировать архитектуру так, чтобы лонг-ранов не было. Лонг-ран, как процесс, превращается в лонг-ран, как перечень джобов, которые нужно выполнить. Каждая итерация - отдельный джоб. Тогда и прогрессе легко отследить по соотношению того, сколько джобов выполнено и сколько осталось. Тут отсылка к еще недоработанному в виде расширения yii2-queue-chain. И перезапуск фейлов легко организовать штатными средствами очереди.

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

zhuravljov avatar Apr 23 '19 19:04 zhuravljov

Про лог-ран как перечень джобов - да, но при этом это все равно остается одним логическим джобом и для этого как раз и нужен общий стейт и/или завязка на объект-инициатор чтоб пробрасывать стейт туда.

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

Insolita avatar Apr 23 '19 20:04 Insolita

Про лог-ран как перечень джобов - да, но при этом это все равно остается одним логическим джобом и для этого как раз и нужен общий стейт и/или завязка на объект-инициатор чтоб пробрасывать стейт туда.

Ну да, эта идея и лежит в основе queue-chain: https://github.com/zhuravljov/yii2-queue-chain/blob/master/src/StorageInterface.php Считаем сколько заданий группы поставлено в очередь и сколько уже выполнено. А прогресс - это соотношение этих двух счетчиков.

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

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

zhuravljov avatar Apr 23 '19 21:04 zhuravljov

Вот снова нарисовалось - надо при фейле таска юзеру аккуратную ошибочку вывести... куда удобнее ее было бы класть в кастомное поле лога джоба, чем вставлять еще одно поле в модель-инициатора для текста ошибки (у нас связано с моделями). Может, задумывалось оно как для отладки, но совсем с небольшим допилом оно приобретает дополнительный и весьма востребованный функционал, который заненадобностью можно и не использовать, нуллить значения по дефолту и все.. Может какой опрос на yii-шных ресурсах замутить? Мне почему-то кажется, что далеко не одна я такая извращенка и подобные возможности ожидают и другие... Или таки собраться и свой блек-джек мутить

Insolita avatar Apr 24 '19 01:04 Insolita

https://github.com/zhuravljov/yii2-queue-monitor/blob/master/src/JobMonitor.php#L179 seems we can got result only for success execution. What about support special Exception type with additional result message for storing it as result for failed execution?

Insolita avatar Apr 25 '19 07:04 Insolita

@Insolita how it could be used?

zhuravljov avatar Apr 25 '19 15:04 zhuravljov

class JobException extend \Exception
{
       public $result;
       public function __construct(string $message, array $result, int $code=0, $previous = null)
   {
          $this->result = $result;
          parent::__construct($message, $code, $previous);
     }
}
class MyJob implements JobInterface
   {    
        public function execute($queue)
        {
              try{
                   $this->executeInternal();
              }catch(\Throwable $e){
                     throw new JobException($e->getMessage(), ['some advanced data for result - like current step, user-friendly error, additional context.. '], 0, $e);
             }
        }
   }

Insolita avatar Apr 25 '19 16:04 Insolita

Если exception рассматривается как результат, то не проще ли обычным способом?

class MyJob implements JobInterface
{
    public function execute($queue)
    {
        try {
            return $this->executeInternal();
        } catch (\Throwable $e) {
            return ['some advanced data for result - like current step, user-friendly error, additional context.. '];
        }
    }
}

zhuravljov avatar Apr 25 '19 17:04 zhuravljov

да но онож на retry не пойдет

Insolita avatar Apr 25 '19 17:04 Insolita

Верно, не пойдет. Я же и говорю про тот кейс, где исключение = результат.

zhuravljov avatar Apr 25 '19 17:04 zhuravljov

What about support special Exception type with additional result message for storing it as result for failed execution?

А стоит ли тогда создавать структурированное исключенние, если данные, которые туда будут передаваться, автоматика далее использовать не будет? А если только для ручного контроля и анализа, то проще зашить их в message:

class JobException extends \Exception
{
    public function __construct(string $message, array $result, int $code=0, $previous = null)
    {
        $message .= "\n" . VarDumper::dumpAsString($result);
        parent::__construct($message, $code, $previous);
    }
}

zhuravljov avatar Apr 25 '19 17:04 zhuravljov

@Insolita кстати, на тему прогресса мысль пришла. Можно было бы JobInterface::execute() запускать как генератор. Тогда промежуточные этапы лонг-рана можно было бы во вне отправлять через yield.

zhuravljov avatar Apr 25 '19 18:04 zhuravljov

А стоит ли тогда создавать структурированное исключенние, если данные, которые туда будут передаваться, автоматика далее использовать не будет?

Его как раз тогда можно будет залоггировать

имеется в виду что вместо https://github.com/zhuravljov/yii2-queue-monitor/blob/master/src/JobMonitor.php#L164

    public function afterError(ExecEvent $event)
    {
        $push = static::$startedPush ?: $this->getPushRecord($event);
        if (!$push) {
            return;
        }
        if ($push->isStopped()) {
            // Breaks retry in case is stopped
            $event->retry = false;
        }
        if ($push->last_exec_id) {
            ExecRecord::updateAll([
                'finished_at' => time(),
                'memory_usage' => static::$startedPush ? memory_get_peak_usage() : null,
                'error' => $event->error,
                'result_data' => $event->error instanceof JobException? $event->error->result:null,
                'retry' => $event->retry,
            ], [
                'id' => $push->last_exec_id
            ]);
        }
    }

А сейчас мы при ошибке в error получаем полный exception с трейсом который трудно препарировать, он норм для внтренней отладки, но его никому не покажешь

Insolita avatar Apr 25 '19 18:04 Insolita

кстати, на тему прогресса мысль пришла.

Любопытно, но это на уровне queue надо разруливать...

Insolita avatar Apr 25 '19 18:04 Insolita

Его как раз тогда можно будет залоггировать

Смущает то, что JobException будет в неймспейсе мониторинга, и это спровоцирует пробрасывать связанность с мониторингом на уровень бизнес-логики. А хотелось бы оставаться в рамках концепции пассивного веб-интерфейса для наблюдения за очередями.

А сейчас мы при ошибке в error получаем полный exception с трейсом который трудно препарировать, он норм для внтренней отладки, но его никому не покажешь

Так его и не нужно препарировать. Если стоит такая задача, в которой нужна специфическая обработка отдельных типов исключений, лучше сделать отдельный обработчик для Queue::EVENT_AFTER_ERROR с сохранением промежуточных результатов в отдельное хранилище за пределами мониторинга.

А если протаскивать это на сторону мониторинга, то нужно проектировать плаганизацию уже самого мониторинга со своими событиями и обработчиками. Чтобы, как в водрдпрессе, через хуки можно было бы вклиниться на любом этапе: сохранение, рендеринг вьюх и прочее.

zhuravljov avatar Apr 25 '19 19:04 zhuravljov

Если бы php допускал концепцию приватных классов то по задумке за пределами неймспейса yii\queue\monitor видно было только те классы, которые нужны для конфигурирования: Env, JobMonitor, WorkerMonitor, Module, а остальное, как в черном ящике, для внешнего использования не предусмотрено.

zhuravljov avatar Apr 25 '19 19:04 zhuravljov

а если чекать исключение на предмет наличия property или метода getResultData ?

Вот сейчас я наконец поняла, что ты хочешь держать его с возможностью безболезненно отключить, или только в dev/staging подключать

Insolita avatar Apr 25 '19 20:04 Insolita

Вот сейчас я наконец поняла, что ты хочешь держать его с возможностью безболезненно отключить, или только в dev/staging подключать

Да, это ключевая идея. Что-то типа yii-debug, но для очередей.

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

zhuravljov avatar Apr 25 '19 20:04 zhuravljov

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

Insolita avatar Apr 25 '19 20:04 Insolita