Zadania cron [KO3.2]

Bardziej rozbudowane serwisy mogą wymagać uruchamiania czynności powtarzanych okresowo. Są to zadania crona – uniksowego daemona zajmującego się okresowym wywoływaniem innych programów. Załóżmy, że mamy serwis społecznościowy i chcielibyśmy wysyłać powiadomienia o aktywności, ale nie tak aby każda aktywność była w osobnym mailu. Moglibyśmy sprawdzać np. co pół godziny czy są jakieś powiadomienia i wysyłać do użytkownika w jednym mailu.

Wysyłanie maili będzie odbywało się tak jakby po wywołaniu danej strony w przeglądarce, np. /cron/email, ale nie do końca…
Trzeba także wspomnieć, że czas wykonywania skryptów php jest ustalany przez administratora i po jego przekroczeniu skrypt przerwie swoją działalność! Zatem jeśli sprawdzalibyśmy w pętli wszystkich użytkowników czy mają coś do wysłania i dla każdego wysyłali email, to raczej pewne, że czas, który zwykle wynosi 30-60sek zostałby przekroczony. Należy więc wydać takie zapytanie, które pobierze tylko tych użytkowników, dla których jest coś do zrobienia.

Dla zaawansowanych »

Jeśli mamy własny serwer, lub dostęp na hostingu do ssh, to moglibyśmy stworzyć skrypt bash, który połączy się z bazą, pobierze tylko te wyniki, dla których jest coś do zrobienia i w pętli dla każdego „otworzy stronę” kohany, która np. wyśle maila z powiadomieniami dla danego użytkownika. Takie rozwiązanie ma przewagę nad tym w php, gdyż skrypt bashowy nie ma limitu czasowego, a skrypt php odpowiada za bardzo krótką akcję (np. wysłanie jednego maila). Wymagana jest jednak znajomość linuxa i podstawy programowania w bashu.

Stwórzmy więc plik bashowy:
/home/user/cron_kohany.sh

#!/bin/sh

DB_USER="user"
DB_PWD="password"
DB_NAME="database"
DIR="/sciezka/do/katalogu/strony/z/kohana"       #bez / na koncu
HASH="fabd1cc647247f42960ce27a6b3a2f33"          #hash zabezpieczajacy md5('blog.kohany.com')
QUERY='SELECT ...;'

result=($(mysql --user ${DB_USER} -p${DB_PWD} ${DB_NAME} -Bse "${QUERY}"))
cnt=${#result[@]}

for (( i=0 ; i<${cnt} ; i++ ))
    do
        #`php $DIR/index.php --uri="cron/email/$HASH/${result[$i]}"`
        echo ${result[$i]}
    done

Skrypt ten łączy się z bazą, wydaje odpowiednie zapytanie i dla każdego wyniku (wynikiem może być tylko 1 kolumna z bazy, najlepiej id!) otwiera stronę dzięki uri, np. dla każdego użytkownika dla którego jest coś do zrobienia wywoła się kontroler cron, akcja email, 1paramter to hash, a drugi id użytkownika:
cron/email/fabd1cc647247f42960ce27a6b3a2f33/12

Przed właściwym wykonaniem zalecam przetestować (komenda bash /home/user/cron_kohany.sh) wyświetlić echo, tak jak jest to teraz, a jeśli wszystko pójdzie OK, to wtedy zahashować i wykonywać w pętli właściwą komendę:

`php $DIR/index.php --uri="cron/email/$HASH/${result[$i]}"`
#echo ${result[$i]}

Stwórzmy kontroler odpowiedzialny za crona:
/aplication/classes/controller/cron.php

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Cron extends Controller {
    private $hash='fabd1cc647247f42960ce27a6b3a2f33';      //md5('blog.kohany.com');
    
    public function before(){
        if(!Kohana::$is_cli){
            throw new HTTP_Exception_404('The requested page does not exist!');
        }else{
            if(!isset($_SERVER['HTTP_HOST'])){
                $_SERVER['HTTP_HOST'] = 'www.example.com';
            }
        }
    }
    public function action_email(){
        $hash=$this->request->param('id');
        if($hash==$this->hash){
            //zadanie crona
        }
    }
}?>

Akcja email dla zaawansowanych »

public function action_email(){
    $hash=$this->request->param('id');
    if($hash==$this->hash){
        $user_id=$this->request->param('id2');
        $user=ORM::factory('user',$user_id);
        if($user->id){
            //pobranie powiadomien dla konkretnego uzytkownika
            //wyslanie jednego maila
        }
    }
}

Najpierw deklarujemy nasz tajny hash, który służy do zabezpieczenia, przed niepożądanym wywołaniem i u mnie jest to nic innego jak skrót md5 z ciągu znaków blog.kohany.com.

private $hash='fabd1cc647247f42960ce27a6b3a2f33';

Potem sprawdzamy czy dana strona została otwarta przez cli, jeśli nie została (otwarto ją w przeglądarce) wyrzucamy błąd, gdyż nie chcemy przypadkowego dostępu przez przeglądarkę, lecz tylko z lini komend (is_cli):

if(!Kohana::$is_cli){
    throw new HTTP_Exception_404('The requested page does not exist!');
}

Jeśli wywołano prawidłowo przez crona to sprawdzamy czy jest nazwa hosta (na niektórych serwerach może jej nie być przy uruchamianiu strony z linii komend), a jest to potrzebne jeśli np. w mailu chcielibyśmy użyć do stworzenia linku Url::base(‚http’,true):

else{
    if(!isset($_SERVER['HTTP_HOST'])){
        $_SERVER['HTTP_HOST'] = 'www.example.com';
    }
}

W akcji porównujemy hash przesłany parametrem z tym zadeklarowanym u góry w klasie

$hash=$this->request->param('id');
if($hash==$this->hash){
    //zadanie crona
}

W tym momencie możemy wykonywać jakieś zadanie crona.

Dla zaawansowanych »

Plik bashowy przesyła dodatkowo jako drugi parametr id usera, dla którego mamy coś wykonać, więc pobieramy jego id, tworzymy usera i dla niego pobieramy powiadomienia i wysyłamy w jednym mailu.

$user_id=$this->request->param('id2');
$user=ORM::factory('user',$user_id);
if($user->id){
    //pobranie powiadomien dla konkretnego uzytkownika
    //wyslanie jednego maila
}

Skrypt basha powtarza tą czynność dla wszystkich userów, dla których jest coś do wysłania.

To już prawie wszystko. Należy jeszcze tylko dodać kiedy cron ma się uruchamiać. Zwykle w panelach hostingowych jest specjalny panel do tego, w którym należy podać np. taki wpis:

*/30 * * * * php /sciezka/do/katalogu/z/kohana/index.php --uri="cron/email/fabd1cc647247f42960ce27a6b3a2f33"

Spowoduje to uruchomienie akcji email kontrolera cron co 30min.

Dla zaawansowanych »

Jeśli mamy własny serwer, musimy stworzyć plik, np. cron_kohany w odpowiednim katalogu: /etc/cron.d, a będzie on odpowiedzialny za uruchamianie naszego skryptu basha.

/etc/cron.d/cron_kohany

*/30 * * * * bash /home/user/cron_kohany.sh

Odpali to co 30 min skrypt, który pobierze userów dla których jest coś do wysłania i dla każdego odpali akcję kohany, w której wyślemy jednego maila.

Co możemy robić w cronie?
W cronie możemy tworzyć kopie zapasowe plików, bazy, możemy wysyłać ponownie maila użytkownikom, którzy nie potwierdzili konta, usuwać użytkowników, którzy po miesiącu nie potwierdzili konta, możemy tworzyć mapę strony, tworzyć statystyki i wiele innych rzeczy.

Co mogą oznaczać te gwiazdki?
Pierwsze pięć określa czas uruchomienia zadania, szósta definiuje użytkownika (jeśli wymagane są prawa) i komendę, która ma zostać wykonana:

*     *     *     *     * użytkownik komenda_do_wykonania
-     -     -     -     -
|     |     |     |     |
|     |     |     |     +----- dzień tygodnia (0 - 7) (Niedziela=0, Poniedziałek=1, Wtorek=2,..., Niedziela=7)
|     |     |     |     
|     |     |     +------- miesiąc (1 - 12)
|     |     |     
|     |     +--------- dzień miesiąca (1 - 31)
|     |     
|     +----------- godzina (0 - 23)
|     
+------------- minuta (0 - 59)

Dostępne możliwości:
1-3 – czyli wartości 1,2,3
0-10/2 – czyli wartość 0,2,4,6,8 i 10 (co druga wartość ze zbioru od 0 do 10)
1,2,5 – czyli wartości kolejno 1,2,5
*/2 – co 2 dozwolona wartość (np. w pierwszej kolumnie będzie to 0,2,4,6…56,58)
1-3,5,6 – czyli 1,2,3 oraz 5 i 6

6 Odpowiedzi :“Zadania cron [KO3.2]”

  1. 1. Trzymanie loginu i hasła plaintekstem w skrypcie bashowym to proszenie się o kłopoty. Niby robimy to samo przy plikach .php, ale skrypty bashowe jakoś ‚luźniej’ podróżują po systemie plików i mimo wszystko nie ryzykowałbym

    2. Zabezpieczenie skryptu hashem jest zbędne. Od sprawdzenia czy skrypt wywoływany jest z poziomu powłoki jest Kohana::is_cli, a hash w przypadku shella nie zabezpiecza przed niczym ponieważ

    a) widać go na liście procesów jeśli uprawnienia serwera są spieprzone,
    b) skrypt i tak powinien być wykonywalny jedynie dla użytkownika (jeśli nie jest to masz w tej chwili dużo większy problem)

    3.

    Trzeba także wspomnieć, że czas wykonywania skryptów php jest ustalany przez administratora i po jego przekroczeniu skrypt przerwie swoją działalność!

    Bzdura. set_time_limit. Chyba, że siedzimy na gównianym serwerze opanowanym przez domorosłych „administratorów”, wtedy mamy w tej chwili dużo większy problem.

    4.

    $hash=$this->request->param('id');
    $user_id=$this->request->param('id2');

    Kompletnie pomijając bezzasadność hasha – po pierwsze: od tego jest Route, żeby go używać. Nie korzystaj z domyślnej ścieżki za każdym razem tylko ustal sobie osobną tylko dla crona (wtedy zamiast takich potworków jak ‚id2′ będziesz mógł korzystać z normalnie nazwanych argumentów). Przykład:

    Route::set('cron', 'cron/<hash>(/<action>)')
    	->defaults(array(
    		'controller' => 'cron',
    		'action'     => 'index',
    	));

    Tutaj przy okazji można rozwiązać dwa problemy równocześnie i hash wrzucić bezpośrednio w routing. Wszystko zależy od tego co chcesz uzyskać.

    Po drugie: na litość boską, używaj białych znaków. Porównaj sobie na wikipedii jaka jest różnica w czytelności – twój kod wygląda jak blok losowych znaków.

    ——

    A na sam koniec:

    5. Cały ten wpis można będzie niedługo wrzucić do kosza, ponieważ od tego typu zadań Kohana ma specjalny moduł, który od wersji 3.3 będzie dołączany jako oficjalny i wspierany przez Kohana Team.

    • Mariusz napisał:

      Ad. 1. Trzeba oczywiście nadać odpowiednie prawa.
      Ad. 2. A co jeśli mamy konto na hostingu? Ktoś mógłby wpaść na to jak mamy zrobione i wywołać z cli.
      Ad. 3. Żaden szanujący się admin nie udostępni opcji unlimited, czas może być odpowiednio długi np. 450sek, ale set_time_limit 0 mogłoby zabić serwer, na którym uruchomiono by odpowiednią ilość „długich skryptów”.
      Ad. 4. Można oczywiście udoskonalić. Postaram się zacząć używać odstępów ;)
      Ad. 5. Póki co może komuś się przyda.

      • Jeśli mamy konto na hostingu i ktoś może wywoływać nasze skrypty z cli to znaczy, że może też przeczytać nasze pliki czyli ma dostęp do wszystkich naszych haseł (!). W tym momencie wywołanie skryptu to najmniejszy problem ;)

        Co do unlimited – szczerze mówiąc po ustawieniu set_time_limit na 0 jeszcze nigdy nie miałem problemu z execution time na różnorakich serwerach, z których korzystałem (większym problemem było zużycie pamięci przez skrypt, Kohana ma kilka naprawdę brzydkich memory leaków). Jeśli jednak mamy tak gigantyczny skrypt to faktycznie podzielenie go na mniejsze części i wywoływanie jednej po drugiej ma więcej sensu, tutaj się zgodzę ;)

        A co do „możę komuś się przyda”, Minion ma również wersję dla Kohany 3.2, jedyne co się zmieni to będzie nieco uproszczony (wcześniej był lekki bałagan w strukturze katalogów) i wrzucony domyślnie. Niestety przez podejście devów Minion to nadal nie Oil (narzędzie do CLI z Fuel PHP), ale zawsze to jakiś start.

    • Kristof napisał:

      Podoba mi się to zacięcie i walka między Wami. Jeden probuje udowodnić drugiemu że jest lepszy hehe. Słowa uznania dla obu programistów!
      Chciałbym tylko zaznaczyć, że z tego co pamietam Pan Mariusz to administrator sieci :) więc nie był bym tak krytyczny na temat jego pracy! – robi kawał dobrej roboty!

  2. labamba napisał:

    Będzie coś o paginacji?

    • Mariusz napisał:

      Ostatnio pracuję nad skryptem katalogu SEO i nie pojawiały się żadne wpisy, postaram się dziś, lub na dniach coś napisać ;)

Dodaj komentarz

Dodając kod PHP używaj tagów: [php][/php]

*