Part of the EllisLab Network
   
1 of 4
1
Cache Functions Suggestions
Posted: 01 February 2008 06:35 PM   [ Ignore ]  
Grad Student
Avatar
Rank
Total Posts:  42
Joined  11-20-2007

I have a few suggestions for 2 of the cache functions in the output library.
NOTE: I’m still new to CI.

I will discuss the following function in the Output.php Library.
_display_cache() and _write_cache()

===============================================================================

_display_cache() in Output.php

Replace

if ( ! @file_exists($filepath))
{
    
return FALSE;
}
if ( ! $fp = @fopen($filepath'rb'))
{
    
return FALSE;
}
    
flock
($fpLOCK_SH);

$cache '';
if (
filesize($filepath) > 0)
{
    $cache 
fread($fpfilesize($filepath));
}

flock
($fpLOCK_UN);
fclose($fp); 

With

if (!@file_exists($filepath) || !($cache = @file_get_contents($filepath)){
    
return false;

Why
file_get_contents According to the php manual is much more efficient and is the preferred way of reading contents of a file into a string.  Under my tests I noticed a 50-100KB in memory savings. 

===============================================================================

_write_cache() in Output.php

Replace

$cache_path .= md5($uri);

if ( ! 
$fp = @fopen($cache_path'wb'))
{
    log_message
('error'"Unable to write cache file: ".$cache_path);
    return;
}

$expire 
time() + ($this->cache_expiration 60);

flock($fpLOCK_EX);
fwrite($fp$expire.'TS--->'.$output);
flock($fpLOCK_UN);
fclose($fp);
@
chmod($cache_path0777); 

With

$cache_path .= md5($uri);
$tmp_file $cache_path md5(uniqid(rand(), true));

if ( ! 
$fp = @fopen($tmp_file'wb'))
{
    log_message
('error'"Unable to write cache file: ".$tmp_file);
    return;
}

$expire 
time() + ($this->cache_expiration 60);
fwrite($fp$expire.'TS--->'.$output);
fclose($fp);
rename($tmp_file$cache_path);
@
chmod($cache_path0777); 

Why
For a web site that gets hit a lot, file locks will restrict other processes from reading the cache because it’s locked by a single process. 
We can get around this by writing to a temporary file and once complete, we rename the file to the cache filename.
rename() acts on the file inode so it’s fast and efficient

===============================================================================

What do you all think?

Profile
 
 
Posted: 08 February 2008 05:11 AM   [ Ignore ]   [ # 1 ]  
Grad Student
Avatar
Rank
Total Posts:  49
Joined  12-22-2006

The locks are there for a reason smile If you have a high traffic then you might get multiple read/write attempts on a file. The locking might slow you down a bit but will prevent corrupted cache files and php errors.

Profile
 
 
Posted: 08 February 2008 09:45 AM   [ Ignore ]   [ # 2 ]  
Grad Student
Avatar
Rank
Total Posts:  42
Joined  11-20-2007

Lock isn’t necessary using my methods.

Profile
 
 
Posted: 11 February 2008 05:26 PM   [ Ignore ]   [ # 3 ]  
Summer Student
Total Posts:  17
Joined  06-29-2007

Looks good section31, I’m going to play around with it and see how it compares to the unmodified CI cache functions.

Profile
 
 
Posted: 11 February 2008 06:24 PM   [ Ignore ]   [ # 4 ]  
Research Assistant
Avatar
RankRankRank
Total Posts:  770
Joined  02-06-2007
section31 - 01 February 2008 11:35 PM

For a web site that gets hit a lot, file locks will restrict other processes from reading the cache because it’s locked by a single process.


Other processes will be momentarily delayed from reading the cache while it’s being written, but we’re talking milliseconds aren’t we?

section31 - 01 February 2008 11:35 PM

We can get around this by writing to a temporary file and once complete, we rename the file to the cache filename.

What happens when the file is overwritten by rename when another process is in the middle of reading it? I’m thinking that the reader will get corrupted data, but I really don’t know.

 Signature 

“I am the terror that flaps in the night”

Profile
 
 
Posted: 12 February 2008 11:56 AM   [ Ignore ]   [ # 5 ]  
Lab Assistant
Avatar
RankRank
Total Posts:  196
Joined  03-26-2006

Due to the nature of caching… one assumes that you will have a high traffic site to warrant using it.

Therefore, if you have a lot of traffic, and you’re caching a page for an hour…

Over the course of one hour, you may read the file possibly 200 times, and write just once…
The time taken to write is minimal, around 10ms? - So, out of one hour, you have 10ms ‘downtime’ for your cache reads.

Which is 0.00027% downtime… Extremely minimal, and not really worth worrying about.
Locks will only cause this fractional locking time, and, if it only effects 2 people trying to write at the same time.

The correct way to work around it is explained here:

* 1 page is cached for an hour.

* When that cache expires, 2 people visit the site with 10ms of each other (the only time this will cause an ‘issue’, because otherwise, the first will have written the cache before the second requests the page).

* The first user ‘reads’ the cache file, sees it’s expired so begins rendering the page normally with PHP/MySQL
* The second user has the same thing going on… the cache hasn’t been updated by the second user.

* The first user finishes building the page, and begins writing the file, locking it.
* The second user also finishes building the page, and attempts to write it.

* Because the first user has locked the file, the second user can’t write it…
* This is a problem for the second user, so you simply build a little fix to make it all work like magic.

If the second user can’t write the the cache (because it is locked), simply echo the contents of the rendered page, not saving it to the cache.
The first user is dealing with the cache, so just render the output.

With CI, this is how it works…

Line 299 of Output.php:

if ( ! $fp = @fopen($cache_path'wb'))
        
{
            log_message
('error'"Unable to write cache file: ".$cache_path);
            return;
        

The cache file is only written if it’s ‘really_writable’ (not locked)

Line 59 of Output.php:

elseif (($fp = @fopen($file'ab')) === FALSE)
    
{
        
return FALSE

So… if we cant write it, just render the second users’ request.

Simple, and it works, beautifully.

The guys at Ellislabs have a very very good caching system built for us here, and it doesn’t need any work done to it.

 Signature 

On the first day, God created CodeIgniter… Then he could really get some work done!

Elliot Haughin CodeIgniter Blog |
Twitter

Profile
 
 
Posted: 12 February 2008 12:49 PM   [ Ignore ]   [ # 6 ]  
Summer Student
Total Posts:  17
Joined  06-29-2007

Thanks for the explanation Elliot Haughin, locking the file the way CI does it makes perfect sense now!

Would it still be better to use file_get_contents() (not fread) when reading the cache files?

Profile
 
 
Posted: 12 February 2008 01:00 PM   [ Ignore ]   [ # 7 ]  
Summer Student
Total Posts:  17
Joined  06-29-2007

Ah, never mind my last post about file_get_contents(). You can’t (as far as I know) lock the file using file_get_contents(), so fread is required for reading the cache also.

Profile
 
 
Posted: 12 February 2008 11:51 PM   [ Ignore ]   [ # 8 ]  
Sr. Research Associate
Avatar
RankRankRankRankRank
Total Posts:  4777
Joined  03-23-2006

Elliot.  You rock.  Thanks sir.

 Signature 

DerekAllard.com - CodeIgniter, ExpressionEngine, and the World of Web Design

Profile
MSG
 
 
Posted: 13 February 2008 05:54 PM   [ Ignore ]   [ # 9 ]  
Research Assistant
Avatar
RankRankRank
Total Posts:  770
Joined  02-06-2007
Elliot Haughin - 12 February 2008 04:56 PM

...
* The first user finishes building the page, and begins writing the file, locking it.
* The second user also finishes building the page, and attempts to write it.

* Because the first user has locked the file, the second user can’t write it…
* This is a problem for the second user, so you simply build a little fix to make it all work like magic.

If the second user can’t write the the cache (because it is locked), simply echo the contents of the rendered page, not saving it to the cache.
The first user is dealing with the cache, so just render the output.

With CI, this is how it works…

Line 299 of Output.php:

if ( ! $fp = @fopen($cache_path'wb'))
        {
            log_message
('error'"Unable to write cache file: ".$cache_path);
            return;
        

The cache file is only written if it’s ‘really_writable’ (not locked)

Line 59 of Output.php:

elseif (($fp = @fopen($file'ab')) === FALSE)
    {
        
return FALSE

So… if we cant write it, just render the second users’ request.

Untrue.

The second user will wait for the first user to finish writing, and then the second user will overwrite the cache. In fact, on excessively busy sites, many users will stack up waiting to write to the cache. The way to prevent waits and make sure that the second user doesn’t write to cache is to use a non-blocking lock in combination with the exclusive lock, but it doesn’t work on windows. fopen() won’t return false when a file is locked for writing.

Here is some code to demonstrate. Put this method in a controller and open a few browsers each with a different id as a url parameter.

// Create a file "myfile.txt" in the controllers directory and call 
// this method from a few browsers like so:
// http://localhost/controller/test/1, http://localhost/controller/test/2, etc.
function test($id)
{
   $file_name 
APPPATH 'controllers/myfile.txt';

   if ( ! 
$fp = @fopen($file_name'wb'))
   {
       
echo('Cannot write to file. Segment id: ' $id);
   
}
   
else
   

      flock
$fpLOCK_EX );
      
sleep(10);
      
fwrite$fp'Writing to file. Segment id: ' $id);
      echo(
'Successfully wrote file. Segment id: ' $id);
      
flock($fpLOCK_UN);
      
fclose($fp);
   
}
 Signature 

“I am the terror that flaps in the night”

Profile
 
 
Posted: 13 February 2008 06:35 PM   [ Ignore ]   [ # 10 ]  
Lab Assistant
Avatar
RankRank
Total Posts:  196
Joined  03-26-2006

Ah, good catch… I’ve done some work on this, and it turns out that the way around this is to test on the flock with an extra LOCK_NB passed to it…

Output.php line 299:

Replace:

if ( ! $fp = @fopen($cache_path'wb'))
        
{
            log_message
('error'"Unable to write cache file: ".$cache_path);
            return;
        
}
        
        $expire 
time() + ($this->cache_expiration 60);
        
        
flock($fpLOCK_EX);
        
fwrite($fp$expire.'TS--->'.$output);
        
flock($fpLOCK_UN);
        
fclose($fp);
        @
chmod($cache_path0777);

        
log_message('debug'"Cache file written: ".$cache_path); 
if ( ! $fp = @fopen($cache_path'wb'))
        
{
            log_message
('error'"Unable to write cache file: ".$cache_path);
            return;
        
}
        
else
        
{
            $expire 
time() + ($this->cache_expiration 60);

            if ( 
flock($fpLOCK_EX LOCK_NB) )
            
{
                fwrite
($fp$expire.'TS--->'.$output);
                
flock($fpLOCK_UN);

                
log_message('debug'"Cache file written: ".$cache_path);
            
}
            
else
            
{
                log_message
('debug'"Cache file not written (due to locking): ".$cache_path);
                return;
            
}

            fclose
($fp);
            @
chmod($cache_path0777);
        

I’ll forward this little gem on to Derek Allard… I’m sure he’ll notice this anyway, but nice catch Rick, hopefully this solution should solve this situation.

 Signature 

On the first day, God created CodeIgniter… Then he could really get some work done!

Elliot Haughin CodeIgniter Blog |
Twitter

Profile
 
 
   
1 of 4
1