PHP: Semafor dla skryptu wywoływanego cyklicznie

2012-09-07 PHP

Semafor to prosty plik, którego zadaniem jest blokowanie wywołania n-tej instancji skryptu jeśli instancja n-1 nie zakończyła jeszcze swojego działania.

Załóżmy, że napisaliśmy skrypt, którego zadaniem jest praca nad dużą ilością danych w bardzo krótkim odstępie czasu. Plik ze skryptem jest wywoływany cyklicznie co minutę przez cron'a. Jeśli dane wywołanie skryptu zdąży w minutę wykonać wszystkie operację to teoretycznie problemu nie ma. Co jeśli jednak do obróbki są gigabajty danych, tysiące plików a czas wykonania musi się zmieścić pomiędzy kolejnymi wywołaniami (w tym wypadku jednej minuty)? Skrypt może się w prosty sposób zapętlić gdyż cron nie wie czy poprzednie wywołanie programu zakończyło już swoje działanie i po minucie wywoła je ponownie.

Opiszę problem na przykładzie. Mamy katalog na serwerze (/var/www/upload) do którego użytkownik wrzuca fotografie. Każda fotografia ma trafić do specjalnego katalogu na zdjęcia. Dodatkowo musi zostać utworzona miniaturka oraz musi zostać dodany odpowiedni rekord w bazie. Skrypt skanujący katalog "upload" jet wywoływany co minutę w poszukiwaniu nowych plików. Co jeśli wrzucimy do katalogu "upload" sto tysięcy zdjęć? Czy zdążą się przetworzyć (wszystkie) w minutę. Oczywiście, że nie a pamiętajmy, że czas upływa i za 60 sekund skrypt zostanie ponownie wywołany. Zróbmy zatem prosty semafor, który powie skryptowy czy może pracować w danej chwili czy nie ponieważ poprzednie wywołanie skryptu jeszcze pracuje. Nasz semafor to będzie pusty plik, który nazwiemy:

.lock

Kiedy skrypt zostanie wywołany to pierwszym jego zadaniem będzie sprawdzenie czy w katalogu /var/www/uploads nie istnieje plik .lock. Jeśli istnieje to będzie oznaczać tyle, że katalog "jest zablokowany, ponieważ ktoś w nim pracuje", po czym skrypt zostanie momentalnie przerwany. Jeśli pliku nie ma to pierwszą czynnością będzie jego utworzenie a w dalszej kolejności dalsza obróbka danych. Cały problem można zobrazować kolejką do pracy.

Ludzie cierpliwie czekają w kolejce. Pierwsza osoba, która ma pracować wchodzi do pomieszczenia, zamyka za sobą drzwi i pracuje do czasu aż nie skończy. Osoba za nią ciągnie za klamkę i jeśli drzwi są zamknięte to wie, że w środku ktoś jest (i coś robi) i czeka cierpliwie na swoją kolej. Unikamy problemu kiedy do pomieszczenia wejdzie na raz tłum ludzi, który będzie chciał usiąść na jednym fotelu przy biurku.

Do dzieła, oto cały kod:

<?php
ini_set('display_errors', 1);
error_reporting(E_ALL ^ E_NOTICE);

require_once ('/var/www/includes/functions/functions_main.php');
require_once ('/var/www/includes/functions/functions_connection.php');

$dir		= '/var/www/upload/'; /** tutaj wrzucamy nasze pliki, np. przez FTP */

/** sprawdźmy czy jest blokada */
if ( file_exists($dir.'.lock') )
{
  echo $_SERVER['SCRIPT_FILENAME'].' zablokowany przez plik .lock w '.$dir;
  exit; /** jest blokada, nic tu po nas... */
}

/** zróbmy zabezpieczenie przed zapętlonym wywołaniem skryptu - stwórzmy plik .lock */
file_put_contents($dir.'.lock', 'zablokowano na czas wywolania skryptu', FILE_APPEND | LOCK_EX);

$files 		= scandir($dir);
$files		= array_diff($files, array('.', '..', '.lock')); /** plik .lock musi zostać gdyż to nasze zabezpieczenie */

$dirImg 	= '/var/www/data/images/'; /** tu wylądują zdjęcia */
$dirTh		= '/var/www/data/thumb/'; /** tu wylądują utworzone miniaturki */
$count 		= 0;

foreach ($files as $file)
{
  if ( file_exists($dir.$file) )
  {
    $fileInfo 		= pathinfo($file);
    $fileName 		= $fileInfo['filename'];
    $fileExtension	= strtolower($fileInfo['extension']);

    if ($fileExtension == 'jpg')
    {
      /** obrabiajmy zdjęcia partiami po 150 plików ponieważ co za dużo to nie zdrowo */
      if ($count >= 150)
      {
        unlink($dir.'.lock');
        break;
      }

      $count++;

      copy($dir.$file, $dirImg.$file);
      resizeImage($file, $dirImg, $dirTh, 470, 70); /** nasza własna funkcja */

      /** wrzućmy informację do bazy danych */
      dbConnect();
      mysql_query('insert katalog_images (image_name) values (''.$fileName.'');');
      dbDisconnect();

      unlink($dir.$file);
    }
    else
    {
      /** usuń każdy plik z rozszerzeniem innym niż "jpg" - interesują nas tylko zdjęcia */
      unlink($dir.$file);
    }
  }
}

/** koniec pracy - zdejmijmy blokadę (otwórzmy za sobą drzwi dla następnego pracownika, który czeka w kolejce) */
unlink($dir.'.lock');

if ($count > 0)
{
  logEvent('~cron', $_SERVER['REMOTE_ADDR'], 'dodał '.$count.' nowych zdjęć przez skrypt '.$_SERVER['SCRIPT_FILENAME']);
}

$now = date('c');
echo($now.' '.$_SERVER['SCRIPT_FILENAME'].' dodano '.$count.' plik(ów)'."n");
?>

Teraz dodajmy informację do cron'a:

*/1 * * * * php /var/www/cron/cron_upload_imgs.php >> /var/www/log/cron.log

Wszystko co wyświetlamy na ekranie poprzez funkcję echo zostanie zapisane w naszych logach. Teraz kiedy dane wywołanie skryptu będzie pracowało nad obrobieniem partii 150 zdjęć po minucie kolejne wywołanie tego skryptu zobaczy, że w katalogu jest plik .lock i zakończy swoje działanie.

PS. Uprzedzając komentarze – wiemy, że w trakcie pętli "foreach" może zabraknąć prądu i .lock zostanie na serwerze blokując wszystko. Alternatywą może być semafor w formie wpisu do bazy danych z datą wstawienia – jeśli zainsertowano blokadę np. 24 godziny temu i jest ona nadal to można przypuszczać, że wystąpiła jakaś awaria. Blokadę należy zdjąć z bazy tym samym skryptem i działać dalej.

Do NOT follow this link or you will be banned from the site!