PHP: Výsledky srovnání funkcí file_(get)put_contents & f(read/write/flush)

Kit

  • *****
  • 657
    • Zobrazit profil
    • E-mail
Na endoru jsem se kdysi díval a byla omezená trafikem. Pokud se nepletu. Už jsem se před časem díval ale to bylo mnoho lez zpátky a narážel jsem na samé limity. Například limit odeslaných požadavků na server databáze.

Tak si najdi nějaké žebříčky freehostingů a vyber si takový, který ti bude vyhovovat.

Narazil jsem na tohle: https://infinityfree.net/


Tak asi to zkusím.
« Poslední změna: 15. 10. 2019, 11:31:02 od exkalibr »

přidávání dat do souboru pomocí fopen(filename, "a") je taky neatomicitní?

Kit

  • *****
  • 657
    • Zobrazit profil
    • E-mail
přidávání dat do souboru pomocí fopen(filename, "a") je taky neatomicitní?

Zkus raději podle manuálu
Kód: [Vybrat]
file_put_contents($filename, $data, FILE_APPEND | LOCK_EX);

přidávání dat do souboru pomocí fopen(filename, "a") je taky neatomicitní?

Zkus raději podle manuálu
Kód: [Vybrat]
file_put_contents($filename, $data, FILE_APPEND | LOCK_EX);

To mohu použít jen v případě, že ve scriptu nejedou dva procesy paralelně, já myslel právě pro to použití dvou skriptů. Dám příklad:

Co chci zapsat do logu:
"Byl jsem tady Skript 2 čas 1:00:11.000"
"Byl jsem tady Skript 1 čas 1:00:11.002"
"Byl jsem tady Skript 3 čas 1:00:11.001"

Jestli je zapíše za sebou - jedno v jakém pořadí,
nebo se může stát, že vznikne kolize

např

Kód: [Vybrat]
Byl jsem tady Skript 2 čas 1:00:11.00
Byl jsem tady Skript 1 čas 21:00:11.002
Byl jsem tady Skript 3 čas 1:00:11.001

(Ubral nulu na konci prvního řádku)


Dokončil jsem skript a prakticky mi to už nehlásí chyby.

Tento test je blízký testu 7 - vycházel jsem z jeho modifikovaných verzí po opravách, které jste mi doporučili.

Hlavním problémem byl jak už tu psal Lin, zjišťování velikosti souboru. To nemohlo správně fungovat. Když jsem vynechal test velikosti souboru, tak vše jede. Původně jsem ten test přidal proto, že to celé nejelo: velikost souboru se měnila až k nule.

V tomto testu jsem přidal funkci na test existence zámku pro zálohu souboru.


Zámek na atomicitu čtení a zápisu jsem nepřidával. Zřejmě už samotný fakt, že probíhá kopírování, to celé časově nějak rozhodí a nedojde k té kolizi jako před tím. Tím kopírováním je to realističtější, jenže já to paradoxně stejně nemohu obnovit, protože v té části, kde bych to obnovoval není nastaven atomicitní zámek. A kdybych ho tam dal, tak zase může dojít k chybě atomicity fail.

Jediná chyba, která tak občas nastane je "atomicity fail" v prvním zámku, který spravuje zálohování souboru. Ten mě ale fakticky nezajímá.

Přikládám celý kód, protože už mi to fakt moc nemyslí a třeba v něm najdete nějaké věci, které stojí za zmínku.

Kód: [Vybrat]
clearstatcache();
$_DEBUG_ = false;

/*
folder.t1 - directory lock for backup copy
folder.t2 - directory lock for restore copy
*/

echo "Lock and flush tester.".time()."<br>";
$time_constant = 1571149526;
//die; // Remove this line when you set time_constant

while ( time()<$time_constant )
 {
 usleep(500);
 }

/*
Creates directory lock for atomicity or waits
$dirPrefix = "lock"; // dir. lock name
$lockN = 1; // first lock
$lockTimes = 7; test 7 times
*/
function AtomicFuseTestTimes($dirPrefix, $n, $lockN, $lockTimes, $printError = true){
  $start = atomicFuse($dirPrefix, $n,$lockN,0);
  for ($i=1; $i <= $lockTimes; $i++ )
    if (!$start)
      $start = atomicFuse($dirPrefix, $n,$lockN,$i);
    else
      break;
  if (!$start) $start = atomicFuse($dirPrefix, $n, $lockN, null, false);
  if ( $printError==true && !$start) echo "<br><b>Atomicity failed for lock $lockN.</b><br> ";
  return $start;
}
/*
Before or after, you call AtomicityTestLockTimes,
you need to create a directory lock.

$n = file size in kb
$lockN = 1; // first lock
$lockTimes = 7; test 7 times
*/
function AtomicityTestLockTimes($dirPrefix, $n, $lockN, $lockTimes, $printError = true){
  $start = TestAtomicLock($dirPrefix, $n,$lockN,0);
  for ($i=1; $i <= $lockTimes; $i++ )
    if (!$start)
      $start = TestAtomicLock($dirPrefix, $n, $lockN,$i);
    else
      break;
  if (!$start) $start = TestAtomicLock($dirPrefix, $n, $lockN, null);
  if ( $printError==true && !$start) echo "<br><b>Atomicity failed for lock $lockN.</b><br> ";
  return $start;
}
/*
This makes a test of atomic lock but does not
create it.
c is counter for optimalization
first call must have c = 0;
*/
function TestAtomicLock($dirPrefix, $n, $lockNumber, $c){
  if ( $c<0 )
    die("<h3>TestAtomicLock: Error - c is less than 0</h3>");
  $start = true;
 
  echo "<br>'$dirPrefix.t$lockNumber'<br>";
 
  if ( file_exists("$dirPrefix.t$lockNumber") )
   {
   clearstatcache();
   $st = @stat("$dirPrefix.t$lockNumber");
   echo "dir lock time: ".(time()-$st['ctime']);
   if ( time()-$st['ctime'] > 10 )
     {
     @rmdir("$dirPrefix.t$lockNumber"); // remove directory lock
     echo "<h1>Old lock removed</h1>";
     }
   $start = false;
   }
  else echo " not found; ";
  if ( isset($c) ){
    if ( $start == false )
     {
     $n = $n*30;
     switch($c):      // Delay example increase:
       case 0: break; // 0,01569 total
       case 1: break; // 0,03138 total
       case 2: $n = $n*2; break; // 0,06276 total
       case 3: $n = $n*4; break; // 0,12552 total
       // case 4: You need at least *6 or *8 to get out of problems with extrem times
       case 4: $n = $n*8; break; // 0,25104 t.(upper limit)
       // In case of heavy traffic:
       case 5: $n = $n*8; break; // 0,36087 total extrem
       case 6: $n = $n*10; break; // 0,51777 total extrem
       case 7: $n = $n*20; break; // 1,03554 total extrem
       default: $n = $n*8; break;
     endswitch;
     usleep($n);
     echo "($n)<br>";
     }
    }
  return $start;
}
/*
dirPrefix - should be equal to name of the file you're trying to lock
lockNumber for adding more locks */
function atomicFuse($dirPrefix, $n, $lockNumber, $c){
  if ( $c<0 )
    die("<h3>TestAtomicLock: Error - c is less than 0</h3>");
  $start = false; 
  if ( @mkdir("$dirPrefix.t$lockNumber") )
   $start = true;
  if ( isset($c) ){
    if ( $start == false )
     {
     $n = $n*30;
     switch($c):      // Delay example increase:
       case 0: break; // 0,01569 total
       case 1: break; // 0,03138 total
       case 2: $n = $n*2; break; // 0,06276 total
       case 3: $n = $n*4; break; // 0,12552 total
       // case 4: You need at least *6 or *8 to get out of problems with extrem times
       case 4: $n = $n*8; break; // 0,25104 t.(upper limit)
       // In case of heavy traffic:
       case 5: $n = $n*8; break; // 0,36087 total extrem
       case 6: $n = $n*10; break; // 0,51777 total extrem
       case 7: $n = $n*20; break; // 1,03554 total extrem
       default: $n = $n*8; break;
     endswitch;
     usleep($n);
     echo "!($n)<br>";
     }
    }
  return $start;
}

function test($n, $p, $_DEBUG_){
//  $delay_multiplier = $n*2.5;
  $original_fname = "$n";
  $cname = "$n.b";    // copy to restore
  $working_filename = "$n.txt";    // source & target
  echo "<h4>$n at ".time()."</h4>";
  for ($i = 0; $i<50; $i++ ){
    $start_time = microtime(true);
    clearstatcache(); // needed for filesize and touch   
   
    //////////////////////
    // TEST ATOMIC LOCK //
    //////////////////////

    // Test 1 - "backup" lock
    // MAKE BACKUP
    // Test 2 - "restoring" lock
    $start = AtomicityTestLockTimes($n,$n,1,7);
    if ( $start )
      {
      $success = @mkdir("$n.t1");
      if ( $success == false )
         echo "<b>Failed to lock;</b><br>";
      $result = copy($original_fname, $cname);
      if ( $result == false )
        echo "<h4>BACKUP failed.</h4>";
      $result = @rmdir("$n.t1");
      if ( $result == false )
        echo "<h4>Directory lock was not removed</h4>";
      }

    // Test 2 - "restoring" lock
    // $start = AtomicityTestLockTimes($n,$n,2,7);
    if ( true )
    {
    $st = stat("$working_filename");
    $original_size = $st['size'];
    if ( $_DEBUG_ )
      echo "; 1) prevAccess by ".$st['mtime']." fsize ".$st['size']."; ";
    $fsize = filesize($working_filename);
    if ( $original_size <> $fsize )
      die("; fsize total FAILTURE; ");
    if ($fsize === 0)
     die ("! <b>The fsize is 0</b>: fstat(): $original_size <>".$st['size']." ;");   
   
    // READ OPERATION AND LOCK FOR SHARE
    $fp = fopen($working_filename, "r+");
    $locked = flock($fp, LOCK_SH);
    if ( $locked == false)
      die("failed to get LOCK_SH;<br>");
    $s = fread( $fp, $fsize );
    // CHANGE LOCK
    $success = flock($fp, LOCK_UN);
    $locked = flock($fp, LOCK_EX);
    if ( $success === false  )
      die("; r flock release failed; ");
    if ( $locked == false )
        die("failed to get LOCK_EX;<br>");
    clearstatcache();
    $st = stat("$working_filename");                               
    if ( $_DEBUG_ )
      echo "; 2) prevAccess by ".$st['mtime']." fsize is ".$fsize."; ";

    // WRITE OPERATION WITH LOCK_EX
    $locked = flock($fp, LOCK_EX);
    if ( $locked ) {  // acquire an exclusive lock
        $success = rewind($fp);
        if ($success == false)
          echo " rewind failture; ";
        $success = fwrite($fp, $s);
        if ( $success === false)
          echo "; w FAILED;";
        else
          if ( $_DEBUG_ )
                echo " $success B written; ";
        $success = fflush($fp);// flush output before releasing the lock
        if ( $success === false )
          echo "; flush FAILED; ";
        // Test 2 - "restoring" lock
        // $start = AtomicityTestLockTimes($n,$n,2,7);

        $success = flock($fp, LOCK_UN);
        $success = fclose($fp);

        if (false)//$start)
          {
          $original_fname = "$n";
          // Create directory lock
          $success = @mkdir("$n.t2");
          clearstatcache(); // Check file size
          $st = fstat($fp);
          if ($original_size>$st['size'])
              {
              echo "; <b>WRITE FAILED, restoring</b>;";
              if ($success) {
                 $success = flock($fp, LOCK_UN);
                 $success = fclose($fp);
                 $result = copy($cname, $working_filename);
                 if ( $result == false )
                   echo "<h4>Restoring failed.</h4>";
                }
              if ($result == false )
                die(" <b>TOTAL FAILTURE: copy failed.</b>");
              else
                echo " <b>RESTORED</b>;";
              }
          else
            {   // FINISH SUCCESSFUL WRITE OPERATION
            if ( $success )
                touch("$working_filename",$fsize,$p);
            $success = flock($fp, LOCK_UN);    // release the lock
            if ( $success === false )
              echo "; release FAILED; ";
            $success = fclose($fp);
            if ( $success === false )
              echo "; fclose FAILED; ";
            // 10 - data načtená , $p - prohlížeč
            if ( $success )
                {
                $result = touch("$working_filename",strlen($s),$p);
                if ( $_DEBUG_ )
                   echo "; TOUCH: $result;";
                }
            else
              die("fclose FAIL.");
            }
         
         // RELEASE DIRECTORY LOCK: (Test 2)
         /*
         $result = rmdir("$n.t2");
         if ( $result == false )
           echo "<h4>Directory lock was not removed</h4>";
         */
         } // END OF ATOMIC LOCK (mkdir) Test 2         
      } else echo "Couldn't get the lock!";
    }
     
    /////////////////////////
    // END OF TIME MEASURE //
    /////////////////////////

    $time_elapsed_secs = microtime(true) - $start_time;
    //usleep( $delay_multiplier + $n*rand(2,6) );
    if ( $time_elapsed_secs === 0 )
      echo " FAILED ";
    echo "time: $time_elapsed_secs s<br>";
  } // for
}

copy("523","523.txt");
copy("948","948.txt");
copy("1371","1371.txt");
copy("1913","1913.txt");
copy("2701","2701.txt");
copy("4495","4495.txt");
copy("6758","6758.txt");

test("523",$p,$_DEBUG_);
test("948",$p,$_DEBUG_);
test("1371",$p,$_DEBUG_);
test("1913",$p,$_DEBUG_);
test("2701",$p,$_DEBUG_);
test("4495",$p,$_DEBUG_);
test("6758",$p,$_DEBUG_);

Původní blok 7-8 testů a prodlev jsem přesunul do funkce:
AtomicityTestLockTimes

Která vlastně dělá jen
Kód: [Vybrat]
function AtomicityTestLockTimes($dirPrefix, $n, $lockN, $lockTimes, $printError = true){
  $start = TestAtomicLock($dirPrefix, $n,$lockN,0);
  for ($i=1; $i <= $lockTimes; $i++ )
    if (!$start)
      $start = TestAtomicLock($dirPrefix, $n, $lockN,$i);
    else
      break;
...
return $start;
}

Aplikace prvního zámku:
Kód: [Vybrat]
    $start = AtomicityTestLockTimes($n,$n,1,7);
    if ( $start )
      {
      $success = @mkdir("$n.t1");
      if ( $success == false )
         echo "<b>Failed to lock;</b><br>";
      $result = copy($original_fname, $cname);
      if ( $result == false )
        echo "<h4>BACKUP failed.</h4>";
      $result = @rmdir("$n.t1");
      if ( $result == false )
        echo "<h4>Directory lock was not removed</h4>";
      }

A druhý zámek jsem neaplikoval, jak jsem už napsal.
« Poslední změna: 15. 10. 2019, 16:48:42 od exkalibr »

Kit

  • *****
  • 657
    • Zobrazit profil
    • E-mail
přidávání dat do souboru pomocí fopen(filename, "a") je taky neatomicitní?

Zkus raději podle manuálu
Kód: [Vybrat]
file_put_contents($filename, $data, FILE_APPEND | LOCK_EX);

To mohu použít jen v případě, že ve scriptu nejedou dva procesy paralelně, já myslel právě pro to použití dvou skriptů. Dám příklad:

Co chci zapsat do logu:
"Byl jsem tady Skript 2 čas 1:00:11.000"
"Byl jsem tady Skript 1 čas 1:00:11.002"
"Byl jsem tady Skript 3 čas 1:00:11.001"

Jestli je zapíše za sebou - jedno v jakém pořadí,
nebo se může stát, že vznikne kolize

např

Kód: [Vybrat]
Byl jsem tady Skript 2 čas 1:00:11.00
Byl jsem tady Skript 1 čas 21:00:11.002
Byl jsem tady Skript 3 čas 1:00:11.001

(Ubral nulu na konci prvního řádku)

To jsem zkusil už kdysi dávno a fungovalo to spolehlivě, k promíchání dat z osmi procesů nedošlo ani jednou.

Kit

  • *****
  • 657
    • Zobrazit profil
    • E-mail
Aplikace prvního zámku:
Kód: [Vybrat]
    $start = AtomicityTestLockTimes($n,$n,1,7);
    if ( $start )
      {
      $success = @mkdir("$n.t1");
      if ( $success == false )
         echo "<b>Failed to lock;</b><br>";
      $result = copy($original_fname, $cname);
      if ( $result == false )
        echo "<h4>BACKUP failed.</h4>";
      $result = @rmdir("$n.t1");
      if ( $result == false )
        echo "<h4>Directory lock was not removed</h4>";
      }

A druhý zámek jsem neaplikoval, jak jsem už napsal.

Zkusil jsem tuto část kódu přepsat podle sebe, tak jen pro inspiraci:

if (AtomicityTestLockTimes($n,$n,1,7) === false) {
    throw new Exception("Selhalo");
}
if (@mkdir("$n.t1") === false) {
    throw new Exception("Failed to lock;");
}
if (copy($original_fname, $cname) === false) {
    throw new Exception("BACKUP failed.");
}
if (@rmdir("$n.t1") === false) {
    throw new Exception("Directory lock was not removed");
}

A teď už konečně výsledky z posledního testu. A doufám, že tento byl už opravdu poslední.

Jak vidíte, zkopírování 2MB souboru + čtení a zápis v originále už začíná být znát. Nejvyšší čas (v poměru k velikosti souboru) je 2.7 MB.

První časy pro jednotlivé bloky a procesy:
Kód: [Vybrat]
523kB 0,848779202 0,257663012 0,257443905 0,001293182
948kB 0,001987934 1,540060997 0,002835035 1,537259817
1371kB 0,004508018 0,818147898 0,005446196 2,225590944
1913kB 1,965152025 3,108391047 0,008336067 3,122910023
2701kB 0,679250002 4,378324986 0,013814211 0,009026051
4495kB 3,45074296 3,259652853 0,037675858 0,021569967
6758kB 0,275954008 8,090615034 0,042881966 6,911487103

Chybovost považuju za 0, i když je fakt, že na začátku jsou prodlevy kvůli čekání na zámek pro copy().
« Poslední změna: 15. 10. 2019, 17:49:29 od exkalibr »

Návštěvníkům, kteří zde zabloudí se asi nebude chtít vše pročítat, tak mohl bys k tomu grafu stručně napsat jaké z něj plyne tvé doporučení nebo závěr pro ostatní programátory PHP? Podle názvu této diskuze bude návštěvníky hlavně zajímat, kterou funkci (zámek) použít pro čtení/zápis souborů.

Teď to čtu v manuálu, že append mode pro fopen je atomický.

"If handle was fopen()ed in append mode, fwrite()s are atomic (unless the size of string exceeds the filesystem's block size, on some platforms, and as long as the file is on a local filesystem). That is, there is no need to flock() a resource before calling fwrite(); all of the data will be written without interruption."

Pokud to tu čte někdo kdo má práva k editaci prvního příspěvku, bylo by možné tam editací připojit ten poslední graf a odkaz na příspěvek #140 se závěrečným funkčním kódem?

Návštěvníkům, kteří zde zabloudí se asi nebude chtít vše pročítat, tak mohl bys k tomu grafu stručně napsat jaké z něj plyne tvé doporučení nebo závěr pro ostatní programátory PHP? Podle názvu této diskuze bude návštěvníky hlavně zajímat, kterou funkci (zámek) použít pro čtení/zápis souborů.

Závěr jsem chtěl napsat, ale pak jsem ho na poslední chvíli smazal. Lze použít script co jsem tu vložil - test10 - si upravit. Každý si ale musí doladit jaký chce použít mód. Já teď například píšu univerzální funkci pro zápis, která bude přidávat data. V praxi přece nikdo nechce jen načíst data. Musíš to udělat tak, že načteš data, pak provedeš operace a pak je uložíš. Což znamená znovu zkontrolovat velikost souboru, porovnat jestli velikost sedí nebo případně načíst celý soubor v modu r+, porovnat data, dát rewind a zapsat. Jen tak si člověk může ověřit že mezi zpracováváním dat nedošlo ke změně. Člověk se může rozhodnout jestli tu kontrolu dat chce udělat. Prostě záleží to na tom jak důležitý je ten souboru a jak velký provoz je na stránce. Kdo to porovnávat nechce, tak to dá do modu w (write) nebo a (append).
« Poslední změna: 16. 10. 2019, 16:34:41 od exkalibr »

Kit

  • *****
  • 657
    • Zobrazit profil
    • E-mail
Teď to čtu v manuálu, že append mode pro fopen je atomický.

"If handle was fopen()ed in append mode, fwrite()s are atomic (unless the size of string exceeds the filesystem's block size, on some platforms, and as long as the file is on a local filesystem). That is, there is no need to flock() a resource before calling fwrite(); all of the data will be written without interruption."

Obvykle je ten block size nastaven na 8 KiB, takže na běžné logování je to vyhovující. Ovšem pokud napíšeš
Kód: [Vybrat]
fwrite($f, "Hello, ");
fwrite($f, "World");
tak musíš počítat s tím, že se ti mezi ta dvě slova může vecpat něco jiného. Na to je však snadný recept: Naskládáš si vše do jednoho stringu a ten atomicky zapíšeš jedním vrzem. Je to i rychlejší.

Kit

  • *****
  • 657
    • Zobrazit profil
    • E-mail
V praxi přece nikdo nechce jen načíst data. Musíš to udělat tak, že načteš data, pak provedeš operace a pak je uložíš. Což znamená znovu zkontrolovat velikost souboru, porovnat jestli velikost sedí nebo případně načíst celý soubor v modu r+, porovnat data, dát rewind a zapsat. Jen tak si člověk může ověřit že mezi zpracováváním dat nedošlo ke změně.

Naopak. Data chci buď jen načítat (konfigurace, šablony, data z cizích webů) anebo je chci zapisovat (výstup, logy, PDF, GIF, ...) Nevzpomínám si, kdy jsem naposledy potřeboval obojí s jedním souborem, navíc s rizikem kolize.

@Exkalibr: Pokud k tomu grafu nemáš žádné vyhodnocení, tak proč si nám ho sem dal? K čemu nám je užitečný?