Slide 1

Slide 1 text

Beyond cronjobs Adopting long running processes in practice

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Długodziałający oznacza po prostu czekanie!

Slide 4

Slide 4 text

Śledzenie produkcji zamówienia Przypadek #1

Slide 5

Slide 5 text

• 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

Slide 6

Slide 6 text

Stan zastany

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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); } }

Slide 9

Slide 9 text

Częste zapytania o zamówienia mocno obciążały bazę danych

Slide 10

Slide 10 text

Plan działania

Slide 11

Slide 11 text

class OrderSentToProductionEvent class OrderLinePickedEvent class OrderLinePrintedEvent class OrderLinePackagedEvent class OrderLineShippedEvent class OrderLineDeclinedEvent class OrderLineCanceledEvent

Slide 12

Slide 12 text

Menadżer procesu

Slide 13

Slide 13 text

• Menadżer procesu który będzie reagować na zdarzenia • Zdarzeniem inicjującym jest zlecenie produkcji • Zadania będą kolejkowane po zdarzeniu pośrednim jak np wydrukowano • Każde zadanie będzie wykonane w przyszłości • Proces ma się skończyć po zdarzeniu końcowym jak np anulowano • Początkowo nowe rozwiązanie będzie działać równolegle ze starym

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

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, ); }

Slide 16

Slide 16 text

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 ); }

Slide 17

Slide 17 text

final class OrderTrackingProcessManager { public function stop(AbstractOrderLineEvent $event): void { $this -> logInfo( 'stop', (string) $event -> getOrderId(), $event -> getPrintProviderId() -> getValue(), ); }

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

Brakujący puzzel Status zamówienia się nie zmienił

Slide 20

Slide 20 text

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 }

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

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 {

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

• 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 Nauczki przypadek #1

Slide 26

Slide 26 text

• 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 przypadek #1?

Slide 27

Slide 27 text

Sprawdzanie stanu zdrowia Przypadek #2

Slide 28

Slide 28 text

• Cykliczne odpytywanie o stan zdrowia zewnętrznych urządzeń • Urządzenie niedostępne = wstrzymanie komunikacji • Urządzenie ponownie dostępne = wznowienie komunikacji

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

Pierwsza implementacja

Slide 33

Slide 33 text

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); }

Slide 34

Slide 34 text

final readonly class PerformHealthCheck implements AsyncDelayedCommand { public function __ construct(public int $printProviderId, public ?int $facilityId) { } public function delaySeconds(): int { return 600; } }

Slide 35

Slide 35 text

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); } }

Slide 36

Slide 36 text

Pierwsze problemy Współdzielona kolejka

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

SQS content based deduplication

Slide 42

Slide 42 text

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 }

Slide 43

Slide 43 text

Nareszcie Działa!

Slide 44

Slide 44 text

Nauczki przypadek #2 • Znajomość infrastruktury jest kluczowa • Znajomość narzędzi jest również bardzo ważna

Slide 45

Slide 45 text

Podsumowanie • Warto pomyśleć "out of the box" by znaleźć inne rozwiązanie • Implementacja daje dużą wiedzę o wielu małych detalach • Rozwiązania same w sobie są trywialne, ale detale implementacji już nie • Kluczowe elementy • Obserwowalność • Infrastruktura • Znajomość narzędzi

Slide 46

Slide 46 text

KONIEC Znajdź mnie: @ddziaduch Printify szuka developerów! https://jobs.printify.com Oceń mnie:

Slide 47

Slide 47 text

No content