САМОУЧИТЕЛЬ PHP 4

       

Разделенные вычисления


Большинство хостинг-провайдеров ставят ограничения на то время, в течение которого могут выполняться сценарии пользователя. Иными словами, если выполнение программы занимает более определенного времени (например, 10 секунд), она прерывается принудительным образом. Минимальный квант времени задается в файле конфигурации php.ini. Как правило, его хватает для большинства программ, но все же существуют Web-приложения, требующие длительной работы.

Одним из таких приложений является автоматически генерируемая карта сервера. Она может представлять собой обычный сценарий на PHP, который рекурсивно обходит все каталоги сервера и собирает информацию о файлах, которые в них находятся. Конечно, если сайт велик, кванта времени, отведенного хостинг-провайдером, может и не хватить. Кроме того, не очень вежливо заставлять пользователя ждать загрузки страницы карты сервера дольше нескольких секунд.

Как же быть, если описанный сценарий нужен для вашего сайта? Для этого следует формировать карту не при каждом запросе, а лишь изредка, — ведь новые страницы добавляются на сервер довольно редко. Гораздо реже, чем, например, их загружают пользователи. Кроме того, наверное, пользователь не будет особенно недоволен, если изменение на карте сервера проявится не сразу же, а спустя некоторое время — например, час. Главное для него, чтобы карта была всегда перед глазами, а значит, отображалась быстро.

Мы можем хранить уже "просчитанную" карту сервера в файле, быстро выдавая его пользователю при запросе. Но даже если мы собираемся обновлять этот файл всего лишь один раз в час (при очередном запросе карты пользователем), мы наталкиваемся на проблему нехватки кванта времени, выделенного хостинг-провайдером.

Чтобы решить и эту проблему, придется разбить построение большой карты на множество мелких этапов, каждый из которых занимает, скажем, не более 2-х секунд. Каждый такой этап должен запускаться при очередном обращении пользователя к карте сервера, но уже после того, как содержимое временного файла с "просчитанной" картой будет отправлено пользователю. Таким образом, мы постепенно будем накапливать сведения и, как только весь сайт обработан, перестроим карту во временном файле. В ближайший час будет отображаться именно она.


Напишем функцию WalkSite(), которая будет заниматься поиском и обработкой файлов на каждом этапе обхода сайта. Листинг 33.1 содержит код библиотеки,  в которой описана эта функция. Чтобы не "привязываться" к специфике конкретной задачи, сделаем функцию универсальной. Будем передавать ей имя процедуры-обработчика, умеющего "вытаскивать" из указанного файла всю информацию, необходимую для построения карты (например, название страницы, ее размер и т. д.),

сама же WalkSite() будет просто вызывать этот обработчик в нужный момент времени, следя за тем, чтобы квант времени, отведенный на данный этап построения карты, не истек. Если это произойдет, текущее состояние обхода сервера (включая всю собранную информацию) будет сохранено в специальном файле, а при следующем запуске — восстановлено, с тем чтобы обход продолжился с того же места, где он завершился в прошлый раз.

Листинг 33.1. Библиотека для обхода дерева сайта: SiteWalker.phl

<?

// Функция выполняет один этап обхода всех каталогов и файлов сайта.

// Если обход нужно продолжить, загружается предыдущее состояние

// из файла $cache. Если этого файла не существует, значит,



// необходимо начать новый обход, начиная с каталога $Root.

// Этап будет длиться не более $time секунд (если 0, то за один

// раз обрабатывается ровно один файл или каталог).

// Для каждого обнаруженного файла или каталога вызывается функция,

// имя которой передано в $Func.

// Формат функции: function FWalker(string $fname, array &$Result)

// Эта функция должна обрабатывать найденный файл $fname

// соответствующим образом и добавлять данные в массив $Result

// (в любом формате). Состояние массива $Result будет автоматически

// сохранено сразу по истечении кванта времени и восстановлено

// перед началом нового этапа.

// Возвращает true, если процесс не был закончен на этом этапе,

// и false, если только что были обработаны последние файлы на сервере.

function WalkSite($Root,$Func,$cache,$time,&$Result)



{ $Start=time();

  // Состояние в самом начале работы. Нужно обработать

  // корневой каталог $Root.

  $Prg=array(

    "Todo" => array($Root), // для накопления путей необработанных файлов

    "Res"  => array()       // результат обработки всех файлов

  );

  // Пытаемся загрузить текущее состояние. Если не получается,

  // значит, обход только что начался.

  if($f=@fopen($cache,"rb")) {

    if(@flock($f,LOCK_SH)) {

      $Prg=Unserialize(fread($f,filesize($cache)));

      fclose($f);

    }

  }

  // Обходим сайт — по одной итерации цикла на каждый файл или

  // каталог. Найденные файлы добавляются в конец массива

  // $Prg['Res'], а подвергающиеся обработке — извлекаются из его

  // начала. Таким образом, мы продолжаем процесс до тех пор,

  // пока не будут "пройдены" все файлы на сервере.

  do {

    // очередное полное имя файла

    $fname=array_shift($Prg['Todo']);

    // если это не файл и не каталог, пропускаем

    if(!@is_file($fname) && !@is_dir($fname)) continue;

    // если это каталог, добавляем все его содержимое

    if(@is_dir($fname)) {

      $Files=array();

      for($d=openDir($fname); $e=readDir($d); ) {

        if($e=="."||$e=="..") continue;

        $Files[]="$fname/$e";

      }

      closeDir($d);

      // вставляем в начало массива, чтобы на следующей итерации

      // цикла обрабатывались именно эти файлы

      $Prg['Todo']=array_merge($Files,$Prg['Todo']);

    }

    // вызываем функцию для обработки очередного файла или каталога

    $Func($fname,$Prg['Res']);

    // выходим, если время истекло, или же необработанных

    // файлов не осталось.

  } while(time()-$Start<$time && count($Prg['Todo']));

  // Вернуть текущий результат в $Result.

  $Result=$Prg['Res'];

  // Если еще есть файлы для обработки, сохранить состояние.

  if(count($Prg['Todo'])) {

    // Сохраняем текущее состояние. В следующий раз мы начнем с него.



    $f=fopen($cache,"a+b");

    flock($f,LOCK_EX);

    ftruncate($f,0);

    fwrite($f,Serialize($Prg));

    fflush($f); fclose($f);

    return true; // процесс продолжается

  }

  // Иначе процесс закончился. Удалить файл состояния.

  @unlink($cache);

  return false;

}

?>

Я не буду приводить здесь реальный сценарий для построения карты сервера, потому что он слишком велик и, к тому же, довольно однообразен и неинтересен. Вся "изюминка" заключена именно в функции WalkSite(). Листинг 33.2 содержит небольшую "демонстрацию" ее возможностей. Сценарий собирает сведения о размере каждого файла сайта, печатая на каждом этапе имена обработанных объектов, а затем выводит сводную информацию.

Листинг 33.2. Демонстрация возможностей функции WalkSite(): demo.php

<?

// Подключаем библиотекаря "прямым" способом.

include "$DOCUMENT_ROOT/php/Librarian.phl";

// Подключаем модуль с функцией WalkSite().

Uses("SiteWalker");

// Эта функция будет вызываться для каждого файла на сервере.

// Ее задача — добавить обработанные данные из этого файла

// в массив $Result (формат определяется назначением этих данных).

function Walk($fname,&$Result)

{ // для диагностики выводим имя файла

  print ">$fname<br>";

  // в качестве примера — просто добавляем имя файла в массив

  $Result[]="$fname: <b>".filesize($fname)."</b>";

}

// Если WalkSite() вернула false, значит, процесс закончился.

if(!WalkSite($DOCUMENT_ROOT,"Walk","map",0,$Result)) {

  // В качестве примера просто выводим содержимое массива,

  // сформированного вызовами функции Walk(). Реальный код

  // должен был бы вырабатывать HTML-представление карты,

  // данные которой накоплены в $Result.

  print "<hr>";

  print join("<br>\n",$Result); 

} else {

  // для примера заставляем страницу обновить саму себя,

  // имитируя многократные посещения пользователей.



  print "<meta http-equiv=refresh content='0; url=$SCRIPT_NAME'>";

}

?>

В этом сценарии функции WalkSite() передается 0

как значение размера кванта времени, в течение которого можно собирать данные о сайте. Это означает, что файлы будут обрабатываться по одному при каждом запросе.

В реальном коде карты сервера, конечно, это не так — нужно указывать приемлемый промежуток времени, чтобы в него "уложилась" обработка сразу нескольких страниц. Чем меньше будет этот промежуток, тем менее заметным для пользователя станет замедление, связанное с работой сценария, но тем значительнее будут "накладные расходы", вызванные работой функций сериализации. Так что тут нужно выбирать некоторый "средний" вариант. Проще всего это сделать опытным путем — например, так, чтобы примерно за час при известной посещаемости успевала перестроиться вся карта сервера.



Функция WalkSite() из листинга 33.2 работает с файлами, устанавливая на них рекомендательные блокировки. Этот процесс хоть и позволяет обойти проблемы с разделением доступа к файлам, немного сложен для понимания. Он подробно описан в главе 15 части IV.


Содержание раздела