Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[PHPers Summit 2024] Beyond cronjobs: adopting ...

[PHPers Summit 2024] Beyond cronjobs: adopting long running business processes in practice

W każdym systemie są zadania które muszą być wykonywane co pewien czas. Klasycznym rozwiązaniem takiego problemu jest oczywiście użycie narzędzia o nazwie cronjob. Czy zastanawiałeś się kiedyś jak rozwiązać taki problem inaczej? Czy słyszałeś kiedyś o długo działających procesach biznesowych? Jeśli słyszałeś, to czy używałeś ich w praktyce?
Pozwól że pokaże Ci nasze podejście do tematu. Dwa, kompletnie różne rozwiązania. Zaprezentuje co poszło nie tak, a z czego jesteśmy dumni. Opowiem również o tym co jeszcze można usprawnić.
Moim celem jest pokazanie alternatywy dla cronjob i zachęcić Cię do eksplorowania i implementacji. Gwarantuje praktyczną wiedzę którą możesz wykorzystać w swoich projektach.

Damian Dziaduch

July 05, 2024
Tweet

More Decks by Damian Dziaduch

Other Decks in Programming

Transcript

  1. • Proces jest inicjowany poprzez wysyłkę zamówienia • Pobiera stan

    zamówienia z zewnątrz i propaguje go w systemie • Trwa on do momentu f inalizacji, zazwyczaj kilka dnia
  2. public function execute(): void { $now = $this -> clock

    -> now(); $orders = $this -> repository -> findByStatusCreatedAtEnqueuedAt( status: OrderStatus :: SENT_TO_PRODUCTION(), createdAtAfter: $now -> modify(sprintf('-%u days', $this -> daysToProcess)), lastUpdateBefore: $now -> modify(sprintf('-%u minutes', $this -> updateFrequencyMinutes)), limit: self :: LIMIT, ); foreach ($orders as $order) { $this -> enqueueOrderForStatusUpdating($order); $this -> updateLastStatusRequestedAt($order); $this -> repository -> save($order); } }
  3. • Menadżer procesu który będzie reagować na zdarzenia • Zdarzeniem

    inicjującym jest zlecenie produkcji • Zadania są kolejkowanie po każdym zdarzeniu • Wyjątkiem są zdarzenia f inalne jak np anulacja zamówienia • Początkowo nowe rozwiązanie będzie działać równolegle ze starym
  4. final class OrderTrackingProcessManager { public function start(OrderSentToProductionEvent $event): void {

    $orderId = new OrderId((string) $event -> getOrderId()); $order = $this -> orders -> get((string) $orderId); $delayMinutes = $this -> delayCalculator -> forStart(); $delayedCommand = DelayedCommandFactory :: createWithDelayInMinutes( new UpdateOrderState($order -> printProviderId(), $orderId), $delayMinutes, ); $order -> synchronizationPending($this -> clock -> now()); $this -> dispatcher -> enqueue($delayedCommand); $this -> orders -> save($order); $this -> logInfo( ‘start', (string) $event -> getOrderId(), $event -> getPrintProviderId() -> getValue(), $delayMinutes, ); }
  5. final class OrderTrackingProcessManager { public function continue(AbstractOrderLineEvent $event): void {

    $orderId = new OrderId((string) $event -> getOrderId()); $order = $this -> getOrder($orderId); $delayMinutes = $this -> delayCalculator -> forContinue( $event -> getPrintProviderId(), $event -> getDecoratorOrderId(), ); $delayedCommand = DelayedCommandFactory :: createWithDelayInMinutes( new UpdateOrderState($order -> printProviderId(), $orderId), $delayMinutes, ); $order -> synchronizationPending($this -> clock -> now()); $this -> dispatcher -> enqueue($delayedCommand); $this -> orders -> save($order); $this -> logInfo( ‘continue', (string) $event -> getOrderId(), $event -> getPrintProviderId() -> getValue(), $delayMinutes ); }
  6. final class OrderTrackingProcessManager { public function stop(AbstractOrderLineEvent $event): void {

    $this -> logInfo( 'stop', (string) $event -> getOrderId(), $event -> getPrintProviderId() -> getValue(), ); }
  7. if ($this -> shouldResumeOrderTrackingProcess($order)) { $this -> scheduleNextTrackingSynchronisation($order); } private

    function shouldResumeOrderTrackingProcess(Order $order): bool { return !$order - > isSynchronizationPending() / / next check is not schedule, it means that && !$order - > isInTerminalState(); // order is not in terminal state }
  8. final class OrderLineNoStatusTransitionEvent if ([] === $newOrderEvents) { $this ->

    eventDispatcher -> dispatch( new OrderLineNoStatusTransitionEvent( new OrderId((string) $order -> id()), $order -> decoratorOrderId(), $order -> printProviderId(), $remoteOrderEventId, ), ); } final class OrderTrackingProcessManager { public function continue( AbstractOrderLineEvent | OrderLineNoStatusTransitionEvent $event ): void {
  9. • Kluczowa jest obserwowalność - dzięki temu mamy pewność że

    na produkcji kod działa jak należy • Z pozoru łatwa implementacja sprawiła nam pewne trudności • Warto znać limity infrastruktury - u nas okazało się że brakuje indeksów w bazie Lessons learned
  10. • Baza danych zapewne będzie nadal ograniczeniem • Osobna kolekcja

    per kolejka - obecnie jedna kolekcja • Osobna instancja bazy dla celów kolejek • Inne rozwiązanie - Rabbit? Co dalej?
  11. • Cykliczne odpytywanie o stan zdrowia zewnętrznych urządzeń • Urządzenie

    niedostępne = wstrzymanie komunikacji • Urządzenie ponownie dostępne = wznowienie komunikacji
  12. final readonly class PerformHealthCheckHandler { public function __ invoke(PerformHealthCheck $command):

    void { $device = $this -> deviceFactory -> create($command -> printProviderId, $command -> facilityId); try { $this -> checkDeviceCondition -> check($device); if ($device -> isUnavailable()) { $device -> markAsAvailable(); } } catch (DeviceIsUnavailable) { $device -> markAsUnavailable(); } $this -> storeDeviceCondition -> store($device); $this -> bus -> dispatch($command); }
  13. final readonly class PerformHealthCheck implements AsyncDelayedCommand { public function __

    construct(public int $printProviderId, public ?int $facilityId) { } public function delaySeconds(): int { return 600; } }
  14. final readonly class SqsDelayMiddleware implements MiddlewareInterface { public function handle(Envelope

    $envelope, StackInterface $stack): Envelope { if ($envelope -> getMessage() instanceof AsyncDelayedCommand) { $delaySeconds = $envelope -> getMessage() -> delaySeconds(); $envelope = $envelope -> with(new DelayStamp($delaySeconds * 1000)); } return $stack -> next() -> handle($envelope, $stack); } }
  15. framework: messenger: default_bus: command.bus buses: command.bus: middleware: - 'Printify\XXX\SharedKernel\Framework\Symfony\Messenger\SqsDelayMiddleware' transports:

    sync: 'sync: // ' async: '%env(SQS_INTERNAL_DSN)%' device_health_check: '%env(SQS_DEVICE_HEALTH_CHECK_DSN)%' routing: 'Printify\XXX\YYY\Application\Commands\PerformHealthCheck': device_health_check 'Printify\XXX\SharedKernel\CommandBus\Command': sync 'Printify\XXX\SharedKernel\CommandBus\AsyncCommand': async
  16. final class PerformHealthCheck implements AsyncDelayedCommand interface AsyncDelayedCommand extends AsyncCommand routing:

    'Printify\XXX\YYY\Application\Commands\PerformHealthCheck': device_health_check 'Printify\XXX\SharedKernel\CommandBus\Command': sync 'Printify\XXX\SharedKernel\CommandBus\AsyncCommand': async
  17. module "device_health_check_fifo_queue" { source = "[email protected]:printify/xxx.git?ref=vX.Y.Z" service_account_role_name = module.service_account.iam_role_name name

    = "device-health-check-fifo" service = local.service environment = local.environment fifo_queue = true content_based_deduplication = true delay_seconds = 300 visibility_timeout = 120 }
  18. Podsumowanie • Bardziej ambitne rozwiązania niż zwykły cronjob • Samodzielna

    implementacja daje dużą wiedzę jak takie procesy działają • Rozwiązania same w sobie są trywialne, ale detale implementacji już nie • Kluczowe elementy • Obserwowalność • Infrastruktura • Znajomość narzędzi