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.



