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.