Форум dkLab и Denwer
Здесь общаются Web-разработчики.
Генеральный спонсор:
Хостинг «Джино»

Класс (в виде набора функций) для проведения операций с файлами с возможностью отмены изменений (Юрий Насретдинов)
Author Message
Юрий Насретдинов
Модератор



Joined: 13 Mar 2003
Posts: 8642
Карма: 198
   поощрить/наказать

Location: 007 495

PostPosted: Sat Jul 25, 2009 3:56 pm (написано за 14 минут 8 секунд)
   Post subject: Класс (в виде набора функций) для проведения операций с файлами с возможностью отмены изменений
Reply with quote

Для дальнейшей работы над своей БД я решил написать класс (который специально оформлен в виде набора функций, чтобы полностью заменить стандартные fopen/fread/fgets/fwrite/fputs/ftruncate/fclose), который бы добавлял поддержку отмены всех изменений, которые были внесены в файл. Файл при этом желательно открывать в режиме r+b (в основном только для этого режима отмена изменений имеет смысл). Поддержка режимов a* (append) не предусмотрена -- для этого режима в общем случае возвратить изменения не получится из-за его особенностей.

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

Синтаксис ничем не отличается от стандартных f*-функций, за исключением дополнительного параметра у fclose -- $rollback, который, будучи установленным в true, перед закрытием файла вернет обратно все изменения, сделанные в файле.

Также введена новая функция rfrollback($fp) -- она возвращает изменения, сделанные в файле, назад.

Все функции имеют название rf*, по аналогии с f*, встроенными в PHP (fopen -> rfopen, fread -> rfread, ...). Функции возвращают файловый указатель, также, как и обычные f*-функции, так что их можно свободно передавать в функции, которые читают содержимое из файла, или возвращают информацию о дескрипторе (к примеру, ftell() и fseek() должны корректно работать с возвращенным указателем). Первая буква r означает «reversable».

Вот код класса (в нём всё ещё могут быть ошибки (или неточости), несмотря на то, что я его много раз тестировал):
Code (php): скопировать код в буфер обмена
<?
// a set of functions to support reversable file operations

$__rfio_fps = array (www.php.net/array)( // file pointers (could not use $fp itself as index for hash, so we have an additional table)
    // index => fp,
);

$__rfio_idx = array (www.php.net/array)(
    // index => array(filename, filesize, /* previous data */ array( file_position => data_chunk, ... ))
);

function _rf_get_index($fp)
{
    global (www.php.net/global) $__rfio_fps;
   
    return array_search (www.php.net/array_search)($fp, $__rfio_fps);
}

// all the parameters match that of fopen, no matter which PHP version you have

function rfopen()
{
    global (www.php.net/global) $__rfio_fps, $__rfio_idx;
   
    $args = func_get_args (www.php.net/func_get_args)();
   
    list($filename, $mode) = $args;
   
    if($mode[0] /* mode */ == 'a') return call_user_func_array (www.php.net/call_user_func_array)('fopen', $args); // rollback is not supported
   
    if(!$fp = call_user_func_array (www.php.net/call_user_func_array)('fopen', $args)) return false;
   
    // this is the more correct way to determine filesize instead of using clearstatcache(); and filesize();
    fseek (www.php.net/fseek)($fp, 0, SEEK_END);
    $filesize = ftell (www.php.net/ftell)($fp);
    fseek (www.php.net/fseek)($fp, 0, SEEK_SET);
   
    $__rfio_fps[] = $fp;
    $__rfio_idx[] = array (www.php.net/array)( $filename, $filesize, array (www.php.net/array)() );
   
    //echo 'Opened fp -- '.$fp.'<br>';
   
    return $fp;
}

function rfputs($fp, $data, $length = null)
{
    global (www.php.net/global) $__rfio_idx;
   
    $args = func_get_args (www.php.net/func_get_args)();
   
    $idx = _rf_get_index($fp);
   
    if($length === null) $length = strlen (www.php.net/strlen)($data); // crappy fputs :).
   
    if($idx === false) return fputs (www.php.net/fputs)($fp, $data, $length); // 'a' is not supported
   
    if(strlen (www.php.net/strlen)($data) < $length) $data = substr (www.php.net/substr)($data, 0, $length);
   
    $old_pos = ftell (www.php.net/ftell)($fp);
   
    // read the chunk of data that is going to be overwritten and cache it
    $tmp = array (www.php.net/array)( $old_pos, fread (www.php.net/fread)($fp, $length) ); // it does not really matter if there is nothing to read, or you can read less, than $length, see rfrollback() for details
   
    $__rfio_idx[$idx][2][] = $tmp;
   
    fseek (www.php.net/fseek)($fp, $old_pos, SEEK_SET);
   
    return fputs (www.php.net/fputs)($fp, $data, $length);
}

function rftruncate($fp, $length)
{
    global (www.php.net/global) $__rfio_idx;
   
    $idx = _rf_get_index($fp);
   
    if($idx === false) return ftruncate (www.php.net/ftruncate)($fp, $length); // 'a' is not supported
   
    $old_pos = ftell (www.php.net/ftell)($fp);
   
    fseek (www.php.net/fseek)($fp, 0, SEEK_END);
    $cur_fsize = ftell (www.php.net/ftell)($fp);
   
    if($length < $cur_fsize) // in case file is going to shrink
    {
        fseek (www.php.net/fseek)($fp, $length, SEEK_SET);
       
        $tmp = array (www.php.net/array)( $length, fread (www.php.net/fread)($fp, $cur_fsize - $length) );
       
        $__rfio_idx[$idx][2][] = $tmp;
    }
   
    fseek (www.php.net/fseek)($fp, $old_pos);
   
    return ftruncate (www.php.net/ftruncate)($fp, $length);
}

function rfwrite($fp, $data, $length = null)
{
    return rfputs($fp, $data, $length);
}

function rfread($fp, $length)
{
    return fread (www.php.net/fread)($fp, $length);
}

function rfgets($fp, $length = null)
{
    return fgets (www.php.net/fgets)($fp, $length);
}

function rfclose($fp, $rollback = false) // rollback the made changes ?
{
    global (www.php.net/global) $__rfio_idx, $__rfio_fps;
   
    $idx = _rf_get_index($fp);
   
    if($idx === false)
    {
        $succ = fclose (www.php.net/fclose)($fp);
        if(!$rollback) return $succ;
        else           return false; // rollback is not supported for files, opened in 'a' mode and for usual file pointers
    }
   
    list($filename, $filesize, ) = $__rfio_idx[$idx];
   
    if($rollback) rfrollback($fp);
   
    unset (www.php.net/unset)($__rfio_idx[$idx]);
    unset (www.php.net/unset)($__rfio_fps[$idx]);
   
    return fclose (www.php.net/fclose)($fp);
}

function rfrollback($fp)
{
    global (www.php.net/global) $__rfio_idx;
   
    $idx = _rf_get_index($fp);
   
    if($idx === false) return false; // rollback is not supported for files, opened in 'a' mode and for usual file pointers
   
    $old_pos = ftell (www.php.net/ftell)($fp);
   
    list($filename, $filesize, $chunks) = $__rfio_idx[$idx];
   
    // We cancel stacked changes: we succeedingly revert changes, from the top of the stack. When stack is empty, we get to the initial state
   
    foreach(array_reverse (www.php.net/array_reverse)($chunks) as $v)
    {
        list($off, $data) = $v;
       
        fseek (www.php.net/fseek)($fp, $off, SEEK_SET);
        fputs (www.php.net/fputs)($fp, $data);
    }
   
    $__rfio_idx[$idx][2] = array (www.php.net/array)(); // clear the list of stacked changes, so we can rollback changes several times for a single open $fp
   
    fseek (www.php.net/fseek)($fp, $old_pos);
   
    ftruncate (www.php.net/ftruncate)($fp, $filesize); // in case filesize increased after write operations
   
    return true;
}
?>
RFC.

Last edited by Юрий Насретдинов on Sun Aug 09, 2009 11:55 pm; edited 2 times in total
Back to top
View user's profile Send private message Send e-mail
Юрий Насретдинов
Модератор



Joined: 13 Mar 2003
Posts: 8642
Карма: 198
   поощрить/наказать

Location: 007 495

PostPosted: Sat Jul 25, 2009 6:32 pm (спустя 2 часа 36 минут; написано за 2 минуты 9 секунд)
   Post subject:
Reply with quote

Хотелось бы отметить, что упреждающее чтение и обратная запись фрагментов сделана таким образом, чтобы даже если производилась запись перекрывающимися кусками, данные возвращались на место именно в том виде, в котором они были изначально. На самом деле, задача организовать правильное поведение в этом случае не такая простая, но имеет очень простое решение, которое Вы можете посмотреть в исходном коде.
Back to top
View user's profile Send private message Send e-mail
Юрий Насретдинов
Модератор



Joined: 13 Mar 2003
Posts: 8642
Карма: 198
   поощрить/наказать

Location: 007 495

PostPosted: Mon Sep 14, 2009 2:29 am (спустя 1 месяц 19 дней 7 часов 57 минут)
   Post subject:
Reply with quote


М

Ветка выделена в отдельную тему «Обсуждение библиотеки rfio»,
расположенную в форуме Разное :: PHP (14 Сентября 2009, 03:29).
Back to top
View user's profile Send private message Send e-mail
Юрий Насретдинов
Модератор



Joined: 13 Mar 2003
Posts: 8642
Карма: 198
   поощрить/наказать

Location: 007 495

PostPosted: Mon Sep 14, 2009 2:46 am (спустя 16 минут; написано за 5 минут 8 секунд)
   Post subject:
Reply with quote

Ещё одна небольшая функция, которая облегчает кэширование файловых дескрипторов:
Code (php): скопировать код в буфер обмена
// descriptor caching (can really improve speed in some cases)

function fopen_cached($name, $mode, $lock = false) // note, that arguments are not the same as fopen()!
{
        static (www.php.net/static) $fopen_cache = array (www.php.net/array)();
       
        if(isset (www.php.net/isset)($fopen_cache[$name]) && $fopen_cache[$name]['mode'] == $mode) return $fopen_cache[$name]['fp'];
       
        if(!($fp = fopen (www.php.net/fopen)($name, $mode)) && is_file (www.php.net/is_file)($name))
        {
                // check other stuff
                if((strpos (www.php.net/strpos)($mode,'r')!==false || strpos (www.php.net/strpos)($mode,'+')!==false) && !is_readable (www.php.net/is_readable)($name)) return false;
                if((strpos (www.php.net/strpos)($mode,'w')!==false || strpos (www.php.net/strpos)($mode,'a')!==false) && !is_writable (www.php.net/is_writable)($name)) return false;
               
                // if all is ok (file readable & writable and it is file)
                // the we just hit the limit of max. opened files and should
                // free a file that is stored in cache to get a room for a new entry
               
                $el=(array_shift (www.php.net/array_shift)($fopen_cache));
                fclose (www.php.net/fclose)($el['fp']); // fclose releases lock, if it was set
        }
       
        if($lock) @flock (www.php.net/flock)($fp, LOCK_EX);
       
        $fopen_cache[$name] = array (www.php.net/array)('fp'=>$fp, 'mode'=>$mode/*, 'locked'=>$lock*/);
       
        return $fp;
}
Использование такое же, как fopen() -- доступны, правда, лишь первые 2 аргумента. Введен третий аргумент, который нужно поставить в true, если Вы хотите заблокировать открывшийся дескриптор (она не делает повторного блокирования дескриптора, чтобы скрипт не «завис» в ожидании блокировки, которую сам и поставил). Также стоит отметить, что закрывать дескрипторы не нужно! Они сами будут закрыты, если закончится лимит открытых дескрипторов. Это принципиальный момент, поскольку именно отсутствие закрытия дескрипторов позволяет использовать дескрипторный кэш.

Бенчмарк:
Code (php): скопировать код в буфер обмена
define (www.php.net/define)('N', 10000);

echo (www.php.net/echo) 'Without descriptor cache:'."\n";

$start = microtime (www.php.net/microtime)(true);

for($i = 0; $i < N; $i++)
{
        $fp = fopen (www.php.net/fopen)('test.bin','rb');
       
        fread (www.php.net/fread)($fp, 2048);
       
        fclose (www.php.net/fclose)($fp);
}

echo (www.php.net/echo) round (www.php.net/round)(N/(microtime (www.php.net/microtime)(true)-$start)).' operations/sec'."\n\n";

echo (www.php.net/echo) 'With descriptor cache:'."\n";

$start = microtime (www.php.net/microtime)(true);

for($i = 0; $i < N; $i++)
{
        $fp = fopen_cached('test.bin','rb');
       
        fseek (www.php.net/fseek)($fp, 0);
        fread (www.php.net/fread)($fp, 2048);
}

echo (www.php.net/echo) round (www.php.net/round)(N/(microtime (www.php.net/microtime)(true)-$start)).' operations/sec'."\n\n";

echo (www.php.net/echo) 'With manual descriptor cache:'."\n";

$start = microtime (www.php.net/microtime)(true);

$fp = fopen (www.php.net/fopen)('test.bin','rb');

for($i = 0; $i < N; $i++)
{
        fseek (www.php.net/fseek)($fp, 0);
        fread (www.php.net/fread)($fp, 2048);
}

echo (www.php.net/echo) round (www.php.net/round)(N/(microtime (www.php.net/microtime)(true)-$start)).' operations/sec'."\n\n";
Результаты на моей системе:
Code (any language): скопировать код в буфер обмена
Without descriptor cache:
28947 operations/sec

With descriptor cache:
108981 operations/sec

With manual descriptor cache:
158509 operations/sec
На сервере под управлением FreeBSD:
Code (any language): скопировать код в буфер обмена
Without descriptor cache:
63201 operations/sec

With descriptor cache:
246993 operations/sec

With manual descriptor cache:
419955 operations/sec
Не очень впечатляет, но ускорение работы в 4 раза тем не менее имеется при разумных размерах блока чтения.
Back to top
View user's profile Send private message Send e-mail
Display posts from previous:   
Post new topic   Reply to topic All times are GMT + 3 Hours
Page 1 of 1    Email to a Friend.
You cannot post new topics in this forum. You cannot reply to topics in this forum. You cannot edit your posts in this forum. You cannot delete your posts in this forum. You cannot vote in polls in this forum. You cannot attach files in this forum. You can download files in this forum.
XML