Dawno nie było nic z o Magento i postanowiłam się poprawić w tym obszarze. Dość niedawno udostępniłam na FB zagadkę związana z tematem, który chciałabym dziś poruszyć. Na ogół uruchomienie sklepu nie kończy się na zainstalowaniu silnika i wgraniu skórki, mimo iż znaczna część małych e-commerce’ów tak działa. Najczęściej jest to dopiero początek pracy nad budową sprawnie działających mechanizmów sprzedaży. Mimo iż klienci na ogół tego nie widzą, w tle pracuje wiele procesów, które w trybie niejednokrotnie ciągłym dbają o to, by nasze zamówienie zostało prawidłowo przeprocesowane.
Dlaczego o tym wspominam na wstępie? Zapewne jeśli czytasz ten artykuł, będziesz chciał zbudować taki mechanizm, który będzie miał za zadanie coś ważnego wykonać. Być może prześlesz swoje zamówienia do ERP’a, a może będziesz chciał wysłać maile z przypomnieniem o płatności. Może Twoim celem będzie anulowanie zamówień, które zostały złożone ponad miesiąc temu i jeszcze nie zostały opłacone. To wszystko wymaga pracy z bazą danych i ustawienia tak poszczególnych procesów, aby te na siebie nie nachodziły. W artykule tym pokażę Wam jak można zaprojektować taki ekosystem by móc nad nim całkowicie panować i nie narazić się na niechciane sytuacje jak w zagadce poniżej, kiedy to oczekiwalibyśmy wartości w bazie A=5, B=1, a otrzymaliśmy A=4, B=1. Taka pomyłka może nam bowiem rozsypać całą logikę procesowania zamówień.
Harmonogram procesów w Magento 2
Zacznijmy może od tego, że Magento posiada swój własny crontab, w ramach którego realizowane są core’owe zadania Magento takie jak np:
- reindeksacja danych
- wysyłanie powiadomień e-mail
- aktualizowanie kursów walut
- generowanie Google Sitemaps
- generowanie i wysyłanie powiadomień
Instalacja cron’a w Magento 2
Aby jednak mogło się to wydarzyć, mimo wszystko potrzebujemy wsparcia ze strony serwera, a mianowicie potrzebujemy zainstalować w crontab’ie systemu zadania, które będą uruchamiać mechanizm magentowy. Służy do tego polecenie:
bin/magento cron:install
Po jego wykonaniu, jeśli użyjemy polecenia w konsoli na naszym serwerze:
crontab -l
powinniśmy zobaczyć:
#~ MAGENTO START c5f9e5ed71cceaabc4d4fd9b3e827a2b * * * * * /usr/bin/php /var/www/html/magento2/bin/magento cron:run 2>&1 | grep -v "Ran jobs by schedule" >> /var/www/html/magento2/var/log/magento.cron.log * * * * * /usr/bin/php /var/www/html/magento2/update/cron.php >> /var/www/html/magento2/var/log/update.cron.log * * * * * /usr/bin/php /var/www/html/magento2/bin/magento setup:cron:run >> /var/www/html/magento2/var/log/setup.cron.log #~ MAGENTO END c5f9e5ed71cceaabc4d4fd9b3e827a2b
Wraz z poleceniem bin/magento cron:install
można również użyć parametru –force:
bin/magento cron:install --force
Magento 2 posiada swój wbudowany system zarządzania zadaniami. W odróżnieniu od Magento 1 mamy możliwość stworzenia dowolnej ilości kolejek dla zaplanowanych zadań, dlatego też w momencie gdy nasze zadanie musi wykonać się w konkretnej godzinie i nie może zostać zablokowane przez inny proces, należy dla niego przygotować osobną.
#~ MAGENTO START c5f9e5ed71cceaabc4d4fd9b3e827a2b [...] #~ MAGENTO END c5f9e5ed71cceaabc4d4fd9b3e827a2b
Zadania cron w Magento 2
Zadania cron w Magento 2 definiowane są w pliku crontab.xml
znajdującym się w katalogu etc
modułu:
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd"> <group id="<group_name>"> <job name="<job_name>" instance="<classpath>" method="<method>"> <schedule><time></schedule> </job> </group> </config>
Parametr | Opis |
---|---|
<group_name> |
Kolejka w ramach którego będzie wykonywane zadanie |
<job_name> |
Unikalna nazwa zadania |
<classpath> |
Nazwa klasy, w jakiej ulokowana jest logika zadania |
<method> |
Metoda klasy, która zostanie uruchomiona |
<time> |
Harmonogram wykonywania wyrażony w formacie:
<Minute> <Hour> <Day_of_the_Month> <Month_of_the_Year> <Day_of_the_Week> |
Status wszystkich wykonanych zadań można przejrzeć w bazie danych w tabeli cron_schedule
:
SELECT * FROM cron_schedule;
Poniżej znajduje się przykład najprostszego mechanizmu, który pozwoli nam potwierdzić, że nasze zadanie cron działa:
<?php namespace Hello\World\Cron; class Test { public function execute() { $writer = new ZendLogWriterStream(BP . '/var/log/cron.log'); $logger = new ZendLogLogger(); $logger->addWriter($writer); $logger->info(__METHOD__); return $this; } }
Tworzenie dodatkowych kolejek/grup zadań cron w Magento 2
Zadania w danej kolejce wykonywane są kolejno wg harmonogramu. W związku z tym może dojść do sytuacji, w której zaplanowane zadanie oczekuje wciąż na wykonanie, mimo iż czas jego wykonania już minął. Jak już wspomniałam wcześniej, jeśli mamy takie zadania, które muszą się wykonać o zaplanowanych godzinach, dobrze jest stworzyć dla nich osobną kolejkę. W tym celu wykorzystuje się plik cron_groups.xml
:
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/cron_groups.xsd"> <group id="<group_name>"> <schedule_generate_every>1</schedule_generate_every> <schedule_ahead_for>4</schedule_ahead_for> <schedule_lifetime>2</schedule_lifetime> <history_cleanup_every>10</history_cleanup_every> <history_success_lifetime>60</history_success_lifetime> <history_failure_lifetime>600</history_failure_lifetime> <use_separate_process>1</use_separate_process> </group> </config>
Parametr | Opis |
---|---|
schedule_generate_every |
Częstotliwość zapisywania zadań w tabeli cron_schedule , wyrażona w minutach |
schedule_ahead_for |
Planowanie zadań w tabeli cron_schedule do przodu. Jeśli nasz cron wykonuje się co godzinę, a nasz parametr będzie miał wartość 240, to podczas pierwszego uruchomienia się mechanizmu zostaną zaplanowane 4 zadania, następnie zapisane do cron_schedule i będą oczekiwały na przetworzenie |
schedule_lifetime |
Czas wyrażony w minutach, liczony od planowanej daty przetwarzania, w jakim zadanie powinno zostać zdjęte z kolejki. Jeśli czas ten zostanie przekroczony zadanie otrzyma status missed i nie będzie przetwarzane. |
history_cleanup_every |
Czas wyrażony w minutach, przez jaki będą trzymane w bazie pominięte, bądź podjęte procesy |
history_success_lifetime |
Czas wyrażony w minutach, przez jaki będą trzymane w bazie procesy, które wykonały się z sukcesem |
history_failure_lifetime |
Czas wyrażony w minutach, przez jaki będą trzymane w bazie procesy, które nie wykonały się z sukcesem |
use_separate_process |
Przyjmuje wartości 1 i 0 . Decyduje o tym, czy kolejka ma być przetwarzana w osobnym procesie PHP |
Polecenie bin/magento cron:run
uruchamiania wszystkie kolejki, aby uruchomić ręcznie konkretną należy dodać parametr --group
:
bin/magento cron:run --group index
Polecenia CLI w Magento
Crontab Magento jest najbardziej rekomendowanym sposobem definiowania procesów. Jednak czasem zdarzają się takie, które możemy chcieć uruchomić ręcznie. Mamy możliwość uruchomienia konkretnego lejka i to się sprawdza, jeśli w ramach niego wykonujemy tylko jedną operację, gorzej jeśli jest ich kilka. Wówczas z pomocą przychodzą komendy CLI. Możemy wstrzyknąć do nich klasę naszego procesu i wyzwolić ją dzięki temu.
Aby zdefiniować nową komendę CLI użyjemy pliku di.xml
:
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="MagentoFrameworkConsoleCommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> <item name="<name>" xsi:type="object"><classpath></item> </argument> </arguments> </type> </config>
Parametr | Opis |
---|---|
<name> |
Unikalna nazwa naszej komendy |
<classpath> |
Nazwa klasy, w jakiej ulokowana jest logika zadania |
Poniżej znajduje się logika naszej klasy:
<?php namespace Hello\World\Console\Command; use Hello\World\Cron\Test; use SymfonyComponentConsoleCommandCommand; use SymfonyComponentConsoleInputInputInterface; use SymfonyComponentConsoleInputInputOption; use SymfonyComponentConsoleOutputOutputInterface; /** * Class SomeCommand */ class SomeCommand extends Command { /** @var string **/ const NAME = 'helloworld:testcronjob'; /** @var Test **/ protected $testCronJob; public function __construct(Test $testCronJob) { $this->testCronJob = $testCronJob; } /** * @inheritDoc */ protected function configure() { $options = []; $this->setName(self::NAME) ->setDescription('Test cron job runner') ->setDefinition($options); parent::configure(); } /** * Execute the command * * @param InputInterface $input * @param OutputInterface $output * * @return null|int */ protected function execute(InputInterface $input, OutputInterface $output) { try { $this->testCronJob->execute(); } catch (Exception $e) { $output->writeln(sprintf('<error>%s</error>', $e->getMessage())); } } }
Zastosowanie takiego mechanizmu CLI, jest też dobrym i najszybszym sposobem wykonania testu, czy nasz mechanizm wciąż działa, np mamy połączenie z ERP.
Czasami zdarza się również, że potrzebujemy stworzyć proces totalnie odseparowany od mechanizmów cron’owych Magento. Załóżmy, że nasze zadanie wykonuje się co 5 minut. W ciągu dnia wykona się więc 288 razy, gdyby wykonywał się co minutę wpisów w tabeli cron_schedule
byłoby 5 razy więcej, a więc 1440, natomiast gdybyśmy mieli takich procesów 10 – byłoby ich 14400. Zdarza się również, że takich małych procesów może być o wiele wiele więcej. Korzystanie wówczas ze standardowych mechanizmów Magento może sprawić, że ilość procesów read
i write
na tabeli spowoduje nie tylko niesamowity przyrost w ramach auto_increment
’a, co ostatecznie doprowadzi do zamknięcia zakresu typu int
, ale i zwyczajnie w świecie zaczną pojawiać się DEAD LOCK
’i. W takich przypadkach lepszym rozwiązaniem jest wykorzystywanie crontab’a systemu i polecenia CLI.
Polecenia CLI w Magento 2 – parametry
W ramach poleceń CLI możemy wykorzystywać również parametry, dzięki którym z zewnątrz będziemy w stanie przekazać potrzebne wartości. Możemy tego dokonać na dwa sposoby za pomocą metody setDefiniction()
:
$options = [ new InputOption( self::NAME, null, InputOption::VALUE_REQUIRED, 'Name' ) ]; $this->setName(self::NAME) ->setDescription('Test cron job runner') ->setDefinition($options);
bądź używając metody setArgument()
:
$this->addArgument(self::NAME, InputArgument::OPTIONAL, 'Name');
Planowanie, podgląd i zabezpieczenie procesów w Magento 2
Wspomniałam już trochę o tym, co powinniśmy wziąć pod uwagę pod kątem decyzji, który z mechanizmów cron’owych wybrać. Nie możemy jednak zapomnieć o tym, że jeśli mamy wiele procesów działających w tle, część z nich może zacząć na siebie nachodzić. Mogą wówczas wydarzyć się sytuacje, które totalnie sparaliżują prawidłowe procesowanie zamówień.
Aby zabezpieczyć zadania cron, które mamy zdefiniowane w crontabie systemowym, można użyć polecenia flock
np.:
*/5 * * * * /usr/bin/flock -n /tmp/ms.lockfile /usr/bin/php /path/to/magento/bin helloworld:testcronjob
Jednak nie zabezpieczy nas to przed dodatkowymi niechcianymi zmianami w bazie. W momencie, kiedy wykonujemy save()
na modelu, nie zmienia się w bazie tylko ta jedna wartość, którą chcieliśmy zmienić. Magento pobiera i zapisuje cały wiersz. Oznacza to, że jeśli dwa procesy w tym samym czasie pobiorą rekord z bazy i zmienią dwie różne wartości, po czym dokonają zapisu, końcowy stan wiersza nie będzie zawierał wszystkich aktualnych wartości. Dlatego też dla pojedynczych zmian w przypadku wielu procesów zalecam wykorzystywanie Magento\Framework\App\ResourceConnection
. Wstrzyknięcie tej klasy do konstruktora pozwoli nam na wykonanie:
$connection = $this->connection->getConnection(); $connection->query( sprintf("UPDATE sales_order SET somecolumn = 1 WHERE entity_id = %s", $order->getId()) );
Dzięki temu nasz proces będzie zmieniał tylko jedną konkretną wartość w przeciwieństwie do update’u na wszystkich kolumnach podczas wykonywania save()
na modelu.
To nie uchroni nas jednak przez niespodziankami w 100%. Czasami, któryś z procesów pracuje zbyt długo i dane wynikowe potrzebne do kolejnego mogą jeszcze nie zostać wprowadzone do bazy.
Megaplaza Cron Schedule
Jeśli korzystamy z crontab’a Magentowego możemy sprawdzić jakie procesy wykonują się za pomocą modułu Cron Schedule dostarczonego przez Megaplaza. Znajdziemy w nim nie tylko diagram prezentujący wszystkie procesy, ale i możemy sprawdzić ile każdy z nich trwał:
CRONV
W przypadku zadań zdefiniowanych w systemowym crontabie możemy wykorzystać narzędzie cronv, które zostało napisane w języku GO
. Jego wykorzystanie jest na prawdę proste – polecenie:
$ crontab -l | cronv [Cron Tasks] 1 tasks. [Cron Tasks] './crontab.html' generated.
zamieni nam:
$ crontab -l 1 * * * * /home/ijozwiak/public/project/bin/magneto c:f
na plik crontab.html
:
Niestety bez dodatkowego wsparcia nie uzyskamy za jego pomocą informacji o tym, ile konkretny proces trwał. Bez problemu możemy jednak to zmierzyć dodając przed naszą komendą CLI dodatkowe polecenie time
, np.:
time bin/magento helloworld:testcronjob
wówczas po wykonaniu procesu pojawią nam się dodatkowe dane:
real 0m0,610s - całkowity czas wykonywania procesu user 0m0,549s - czas jaki przypadł na użytkownika systemu sys 0m0,062s - czas jaki przypadł na system
Jeśli te dane będą zbyt mało precyzyjne można również wesprzeć się innymi narzędziami profilująco-monitorującymi takimi jak xdebug, xhprof czy new relic.
Niektórym może nie odpowiadać sposób prezentacji wyników na timeline’ie tj. urwane polecenia cron, jeśli są zbyt długie. Poradziłam sobie z tym w ten sposób, że przed każdym procesem wprowadzam komentarz jak poniżej:
$ crontab -l # czyszczenie cache 1 * * * * /home/ijozwiak/public/project/bin/magneto c:f
Następnie zapisuję listę zadań do pliku:
crontab -l > crontab.txt
I przepuszczam to przez skrypt sh
:
#!/bin/bash input=$1 tmp='tmp.txt' if [[ -n "$input" ]]; then touch $tmp while read -r line; do lineB=$(($line-1)) lineBefore='' job=`cat $input | sed -n "${line}p" | sed s/|//g` command=`echo "$job" | cut -d " " -f 6- | sed s/|//g` if [ $lineB -gt 0 ]; then lineBefore=`cat $input | sed -n "${lineB}p" | sed s/#//g` fi if [ -n "$lineBefore" ] ; then output=`echo "$job" | sed "s|$command|$lineBefore|g"` echo "$output" >> $tmp else echo "$job" >> $tmp fi done < <(cat $input | egrep -vn ^# | awk '{print $1}' | cut -d ":" -f 1); cat $tmp | cronv -d 1d --from-time=0:00 -t Crontab rm $tmp else echo "Podaj plik źródłowy" fi
za pomocą polecenia:
./cronv.sh crontab.txt
W outpucie dostaje dużo czytelniejszy wynik:
Kolejność wykonywania się procesów
W przypadku crontab’a Magento 2 jeśli wykorzystujemy jedną kolejkę nie powinna wydarzyć się sytuacja, w której szybciej wykona się proces niż powinien – oczywiście o ile prawidłowo skonfigurujemy czasy i nie wystąpi po drodze żaden błąd.
Natomiast, gdy stosujemy crontab’a systemowego, może dojść do sytuacji, że np będziemy próbować przetwarzać dane z tabel tymczasowych, nim zdążymy je jeszcze pobrać. Może się też przydarzyć, że przetworzymy przestarzałe dane, ponieważ z powodu błędu nie pobraliśmy nowych i np włączymy dostępność produktów, których nie ma już na stanie. Aby się przed tym zabezpieczyć, niezależnie od miejsca zdefiniowania zadania, dobrze jest zawsze mieć dostęp do informacji:
- kiedy ostatnio dany proces się wykonał
- jakie procesy muszą się wykonać przed nim i jak świeże powinny być to dane aby proces mógł je wykorzystać
- czy proces przetworzył już dane, które zamierzamy przeprocesować, jeśli tak to nie powinien robić tego ponownie, chyba że użyjemy parametru –force
Podsumowanie
Mam nadzieję, że materiał zebrany w ramach tego artykułu pomoże Wam w codziennej pracy z integracjami, bowiem to właśnie podczas ich tworzenia i aktualizacji najczęściej spotykamy się z opisanymi sytuacjami.