Workery w CodeIgniter

Workery to bardzo poręczne rozwiązanie, które pozwala nam na przyspieszenie działania naszego serwisu. Wszystko przez to, że niektóre zadania mogą być przez nas oddelegowane do procesu, który działa w tle. Dzięki temu, użytkownik nie będzie musiał czekać np. aż faktycznie jakiś email zostanie wysłany. Wystarczy, że taki email zostanie zakolejkowany do wysłania. W ten sposób, nasze aplikacje mogą działać bardziej płynne – bez niepotrzebnego oczekiwania na dłuższe procesy.

Jakie czynności możemy oddelegować do Workera? Jest tego sporo. Najbardziej oczywiste, to wysyłanie maili lub prace związane z grafiką. Weźmy może na warsztat ten ostatni przykład. Kiedy użytkownik doda do naszego serwisu zdjęcie, chcemy wykonać szereg manipulacji: wykonać miniaturę, dodać znaki wodne i przeskalować główne zdjęcie. Sporo pracy i na pewno odbije się to na czasie oczekiwania. Właśnie w takich sytuacjach workery sprawdzają się idealnie.

Jeszcze innym przykładem, gdzie można wykorzystać workery, jest komunikacja pomiędzy rożnymi platformami. Załóżmy, że mamy aplikację w PHP, która ma wygenerować jakiś raport. Do tego raportu potrzebujemy zrzutu ekranu. W PHP wykonanie zrzutu ekranu jest raczej trudne, za to w PhantomJS wręcz przeciwnie. Możemy więc skorzystać z NodeJS i modułu dla PhantomJS. A do komunikacji (przekazywania i zwracania wyników), możemy posłużyć się właśnie workerem.

No dobra, jeśli wiemy już do jakich zadań mogą się nam przydać workery, to warto przejść do konkretów. Do dyspozycji mamy wiele rozwiązań m.in.:

Dla naszych potrzeb wykorzystamy Beanstalkd, bo… czemu nie – to bardzo lekkie, szybkie i mało zasobożerne rozwiązanie ;) Przejdziemy teraz do napisania prostej aplikacji, która pozwoli nam na przekazywanie i odczytywanie zadań dodanych do workera.

Tworzymy testowy projekt

Na początek musimy pobrać Beanstalkd, a później go uruchomić.

./beanstalkd -l 127.0.0.1 -p 11300

Jeśli serwer działa, to pora na kod, który pozwoli nam na korzystanie z tego wszystkiego. Zakładam, że w pliku konfiguracyjnym CodeIgniter’a mamy aktywowaną obsługę Composer’a (application/config/config.php). Jeśli tak, to teraz musimy się upewnić, że mamy do dyspozycji bibliotekę, która pomoże nam obsługiwać Beanstalkd. W tym celu wykonujemy polecenie z linii komend (będąc w głównym katalogu naszej aplikacji):

composer require pda/pheanstalk

Skoro mamy już wymaganą bibliotekę, to możemy przejść dalej. Napiszemy teraz klasę (kontroler), która będzie odpowiedzialna za nasłuchiwanie zadań, które spływają do kolejki i dalej, na uruchamianiu określonych procesów.

class Worker extends CI_Controller
{
	/**
	 * Konstruktor
	 */
	public function __construct()
	{
		parent::__construct();

		// Upewniamy się że nasz plik można uruchomić tylko z poziomu linii komend
		is_cli() OR show_404();
	}

	/**
	 * Obsługa kolejki zadań
	 *
	 * @param string $tube Nazwa kolejki
	 *
	 * @return void;
	 */
	public function queue($tube = 'default')
	{
		// Ładujemy klasę Pheanstalk
		$pheanstalk = new \Pheanstalk\Pheanstalk('127.0.0.1');

		// Sprawdzamy status serwera beanstalkd
		$status = $pheanstalk->getConnection()->isServiceListening();

		// Jeśli status serwera jest poprawny to kontynuujemy pracę
		while ($status)
		{
			// Ustawiamy jaką kolejkę obserwujemy
			$pheanstalk->watch($tube);

			// Jeśli to inna kolejka niż "default" to ją pomijamy
			if ($tube !== 'default')
			{
				$pheanstalk->ignore('default');
			}

			// Pobieramy zadanie z kolejki, jeśli nic nie ma, to czekamy na zadanie
			$job = $pheanstalk->reserve();

			// Odczytujemy zadanie i rozbijamy je na odpowiednie elemnty
			list($class, $method, $args) = explode('||', $job->getData());
			// Wyświetlamy w konsoli, że mamy zadanie do wykonania
			echo "Mam zadanie: ". $job->getData() . "\n";

			// Przetwarzamy argumenty na tablicę
			$args = unserialize($args);

			// Ładujemy wymagany model
			// Model jest tutaj zastosowany tylko "umownie"
			// Równie dobrze moglibyśmy ładować bibliotekę lub zwykła klasę (za pośrednictwem Composer'a)
			$this->load->model($class);
			// Wywyołyjemy odpowiednią metodę razem z argumentami
			$output = $this->{$class}->{$method}($args);

			// Sprawdzamy status wykonanej metody
			if ($output)
			{
				echo "Gotowe \n";
				$pheanstalk->delete($job);
			}
			else
			{
				echo "Blad \n";
				$pheanstalk->bury($job);
			}

			// Sprawdzamy status serwera beanstalkd
			$status = $pheanstalk->getConnection()->isServiceListening();
		}
	}
}

Skoro mamy już klasę odpowiedzialną za nasłuchiwanie zadań, to pora na dodawanie zadań do kolejki. Jak dodawać zadania do kolejki? Zbudujemy sobie bardzo prostą klasę, żeby to sobie ułatwić (chociaż i bez tego można by się obejść).

class Queue
{
	/**
	 * Obiekt pheanstalk.
	 *
	 * @var object
	 */
	protected $pheanstalk;

	/**
	 * Nazwa kolejki.
	 *
	 * @var string
	 */
	protected $tube = 'default';

	/**
	 * Konstruktor
	 */
	public function __construct()
	{
		$this->pheanstalk = new \Pheanstalk\Pheanstalk('127.0.0.1');
	}

	/**
	 * Wybieranie kolejki do której powędruje zadanie
	 *
	 * @param string $name Nazwa kolejki
	 *
	 * @return Queue
	 */
	public function tube($name = 'default')
	{
		$this->pheanstalk->useTube($name);

		return $this;
	}

	/**
	 * Dodawanie zadania do kolejki
	 *
	 * @param string $class  Nazwa klasy
	 * @param string $method Nazwa metody (metoda klasy)
	 * @param array  $args   Dodatkowe parametry (parametry dla metody)
	 *
	 * @return void;
	 */
	public function job($class = NULL, $method = NULL, $args = [])
	{
		if (is_null($class))
		{
			show_error('Queue::job. Parametr $class nie moze byc pusty');
		}

		if (is_null($method))
		{
			show_error('Queue::job. Parametr $method nie moze byc pusty');
		}

		// Kluczową sprawą jest serializacja parametrów, które przekazujemy
		// jest to niezbędne, ponieważ zadanie które przekazujemy do kolejki może mieć wyłącznie postać ciągu znaków
		// nie możemy więc przekazać tablicy w "czystej" postaci i tutaj do akcji wchodzi właśnie funkcja serialize
		$args = serialize($args);
		// Przekazujemy zadanie do workera
		$this->pheanstalk->put("{$class}||{$method}||{$args}");
	}
}

Gotowe. Możemy więc spróbować przygotować jakiś przykładowy model, który będzie uruchamiany za pośrednictwem workera.

class Sample_model extends CI_Model
{
	/**
	 * Zapisz plik w katalogu cache
	 *
	 * @param array $args Argumenty
	 *
	 * @return bool
	 */
	public function save_file(array $args)
	{
		$file    = APPPATH.'cache/'.date('YmdHis').'.txt';
		$content = 'Pozdrowienia od '. $args['name'];
		return file_put_contents($file, $content);
	}
}

Stworzyliśmy więc model, który będzie tworzył plik tekstowy w katalogu cache. Ostatnim kropiem będzie stworzenie metody kontrolera, której uruchomienie spowoduje przekazanie zadania do kolejki.

class Sample extends CI_Controller
{
	/**
	 * Wyślij zadanie do workera
	 *
	 * @param string $name Nazwa
	 *
	 * @return void;
	 */
	public function send(string $name = 'Beanstalkd')
	{
		// Ładujemy bibliotekę Queue
		$this->load->library('queue');

		// Wysyłamy konkretne zadanie do kolejki 'sample'
		// W tym zadaniu:
		// 'simple_model' - to nazwa naszego modelu
		// 'save_file' - to nazwa metody
		// ostatni argument to tablica z parametrami dla metody 'save_file'
		$this->queue->tube('sample')->job('sample_model', 'save_file', ['name' => $name]);
	}
}

Ok, mamy już wszystkie potrzebne elementy, aby przejść do testów. Pierwszą rzeczą jaką zrobimy, będzie uruchomienie naszego workera, tak aby mógł nasłuchiwać czy są jakieś zadania do wykonania. W tym celu z linii komend (będąc w głównym katalogu naszego projektu) wykonujemy polecenie:

php index.php worker/queue/sample

Tym samym uruchomiliśmy Worker, który będzie nasłuchiwał zadań w kolejce „sample”. Następnym krokiem będzie uruchomienie metody send z kontrolera Sample. W tym celu możemy przejść do przeglądarki i wywołać ten adres, lub zrobić to za pośrednictwem linii komend (w nowym oknie). Ja skorzystam z tej ostatniej opcji:

php index.php sample/send

Jeśli wrócimy do okna konsoli, w którym został uruchomiony worker, to zobaczymy, że zostało tam wykonane nasze zadanie, a w katalogu cache powinniśmy znaleźć nowo utworzony plik.

Mam zadanie: sample_model||save_file||a:1:{s:4:"name";s:10:"Beanstalkd";}
Gotowe

Podsumowanie

Na koniec kilka ważnych uwaga. Jeśli będziemy korzystać z bazy danych, to powinniśmy wewnątrz naszego workera dodać opcję ponownego połączenia się z bazą, ponieważ długo działający proces workera, na pewno takie połączenie straci przy dłuższej bezczynności. Tak więc, gdzieś przed załadowaniem modelu (ale w pętli while), musimy dodać
$this->db->reconnect();

Kolejna sprawa, to zmiany które nanosimy do naszego kodu. Jeśli wprowadzimy jakieś zmiany w pliku Sample_model, to zostaną one uwzględnione dopiero jeśli zresetujemy nasz worker (uruchomimy go ponownie). Do tego momentu wszystko będzie działać w oparciu o nasz „stary” kod. Warto o tym pamiętać, bo może to nam przysporzyć niezłego bólu głowy :)

Jeśli chcemy mieć wiele kolejek, np. jedna do obsługi maili, a druga do grafiki, to oczywiście dla każdej z nich musimy odpalić osobny worker.

I jeszcze jedno. Możemy sobie umilić pracę z Beanstald instalując Beanstalk Console – jest to interfejs w przeglądarce, który pozwala na podgląd całej kolejki zadań i ogólnie jej zarządzaniem. Czasami jest to bardzo przydatne – szczególnie w środowisku developerskim, pozwala na wygodniejsze testowanie tego co się dzieje.

I to by było na tyle. Mam nadzieję, że temat Wam się spodobał i przy którymś z projektów, będziecie mogli skorzystać z zawartych tu informacji.

Dodaj komentarz

This site uses Akismet to reduce spam. Learn how your comment data is processed.