php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Request #57970 apc_lock() and apc_unlock()
Submitted: 2007-12-14 05:00 UTC Modified: 2007-12-17 03:17 UTC
From: Ninzya at inbox dot lv Assigned:
Status: Closed Package: APC (PECL)
PHP Version: Irrelevant OS: Any
Private report: No CVE-ID: None
View Add Comment Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
You can add a comment by following this link or if you reported this bug, you can edit this bug over here.
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: Ninzya at inbox dot lv
New email:
PHP Version: OS:

 

 [2007-12-14 05:00 UTC] Ninzya at inbox dot lv
Description:
------------
Hello.

I use apc as caching mechanism a lot. It's speed is perfect, performance is outstanding. But, there is one small problem that i can't synchronize accesses to APC cache between parallel requests. That is, when it comes to "cache generating", the code looks like following:

if( !($cachedData =apc_fetch( 'someKey'))) {
 $cacheData =generate_cache_data();
 apc_store( 'someKey', $cacheData, $ttl);
}
echo $cacheData;

The problem is that with this code (which is not synchronized at all), multiple parallel threads may enter the "cache generating" state, which i, obviously, don't want to happen. If the "generating" process is very expensive, the cache misses to me are more than twice expensive.

In a few PHP extensions, for example, msession (a handler for session storage for a web farm), they have implemented synchronization functions for preventing parallel access to session data, for example, msession_lock() and msession_unlock(). In APC, these two functions (apc_lock() and apc_unlock()) are the missing rocks of the giant cliff. apc_lock() and apc_unlock() could lock and unlock the whole cache, preventing apc_add, apc_fetch, apc_store functions of other threads to access the cache, which would result in a complete ellimination of potentially performance-waste situations, like i described above.

Reproduce code:
---------------
if( !($cachedData =apc_fetch( 'someKey'))) {
 $cacheData =generate_cache_data();
 apc_store( 'someKey', $cacheData, $ttl);
}
echo $cacheData;

Expected result:
----------------
apc_lock();
if( !($cachedData =apc_fetch( 'someKey'))) {
 $cacheData =generate_cache_data();
 apc_store( 'someKey', $cacheData, $ttl);
}
apc_unlock();
echo $cacheData;

Actual result:
--------------
A synchronized code

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2007-12-14 07:00 UTC] gopalv82 at yahoo dot com
Read

http://t3.dotgnu.info/blog/php/user-cache-timebomb

Also

http://php.net/apc_add
 [2007-12-14 07:42 UTC] Ninzya at inbox dot lv
Imagine the following situation.

The cache under key $key is empty. Two threads simultaneously hit cache by calling apc_fetch( $key). Both receive false and begin generating the cache, after what the first thread reaches apc_add state first, calls apc_add( $key, $generatedData, $ttl), adds a cache, then the second thread reaches the apc_add and gets false as return code, which indicates that the second cache generating was the waste of time, because the cache was already generated by the first thread. That's what i'm talking about. See the following code:

$key ='test';
$ttl =60;
if( !($generatedData =apc_fetch( $key))) {
  // we see that no cache exists, generate it
  /*
   lots of expensive tasks...
  */
  $generatedData =get_cache_generating_result();
  // store the results in the cache
  if( apc_add( $key, $generatedData) ===false) {
    // OMG, another thread already rendered the same cache
    // i was rendering right now. That wasn't very nice
    // from another's thread side. I gotta tell this admin.
    trigger_error( 'Omg, someone is doing my job!', E_USER_ERROR);
  }
}
// success, operate on obtained data

As i mentioned before, if two threads enter the cache generating state, which IS POSSBLE in APC right now because of synchronization lack at stage of generating, both threads generate the same cache, and after one of these generating results just being thrown away.
 [2007-12-16 15:22 UTC] t3 at freeshell dot in
What happens if PHP exits with a syntax error/segv inside the apc_lock() segment?
 [2007-12-16 19:54 UTC] rasmus@php.net
Generally the way I have handled this in the past for file-based caches is to touch() the previous cache file when it times out.  That is a fast operation which lets other processes use the old cache data while a single process is generating the new data.  Doing a fast touch() drastically reduces the size of the window for other processes to enter the expensive data generation section.  Depending on what you are actually doing, you should be able to adapt that sort of approach to your situation simply by checking an apc entry containing a timestamp or just a boolean flag.  I see no need for userspace locking for this.
 [2007-12-16 20:46 UTC] Ninzya at inbox dot lv
t3 at freeshell dot in: well, the consequences of unexpected errors can be avoided by adding an implicit apc_unlock() call if apc is 'locked' at the end of request (implicit!).

rasmus at php dot net:
this behavior:
 if( !apc_fetch( 'somekey')) {
  apc_store( 'somekey');
 }
is not atomic, and will never be, thus, i cannot use the method you described to achieve the synchronization i need.
 [2007-12-16 21:23 UTC] rasmus@php.net
apc_add() is atomic.

if(!$lck=apc_fetch('lock')) {
  if(apc_add('lock',$_SERVER['REQUEST_TIME'])) {
    // got lock, proceed...
  } else {
    // someone beat us to it
  }
} else {
  // Someone else has the lock - check it...
}
 [2007-12-17 02:57 UTC] Ninzya at inbox dot lv
Hmm, really... How i didn't think of abusing apc_add() before... Thank you very much rasmus.

People looking for solution to synchronize apc, use this code:

function apc_lock() {
 while( !apc_add( '_lock'))
  usleep( 500);
}

function apc_unlock() {
 apc_delete( '_lock');
}

Use these functions as i described above.
Once more, thanks rasmus :)
 [2007-12-17 03:17 UTC] Ninzya at inbox dot lv
There is a serious trouble you can run into using this code. If script fails in a segment between apc_lock() and apc_unlock(), there will be a deadlock for all other threads trying to lock apc. 

Couple solutions:

1) Except writing error-safe code, try to set timeout for the lock variable of apc, e.g. call apc_add( $lockName, true, 5), this will lock apc for a max. of 5 seconds. After 5 seconds elapse, the locked cache will be unlocked implicitly.

2)
 function apc_lock( $emergencyAfter =5) {
   global $_APCLockID, $_APCLocked;
   while( !apc_add( '_lock', $_APCLockID =microtime( true), $emergencyAfter))
     usleep( 500);
   $_APCLocked =true;
 }
 function apc_unlock() {
   global $_APCLockID, $_APCLocked;
   if( $_APCLocked)
     if( apc_fetch( '_lock') ==$_APCLockID)
       apc_delete( '_lock');
   $_APCLocked =false;
 }

 then add error handler

 function custom_errorhndlr( $errno, $errmsg) {
   apc_unlock();// implicitly unlock apc on error
 }

 and use:

 set_error_handler( 'custom_errorhndlr');

 apc_lock();
 // do your job here
 //  note, if error occurs, custom error handler will
 //  implicitly unlock locked cache. if a hard error occurs,
 //  and script skips custom_errorhndlr, lock will be released
 //  implicitly after 5 seconds
 apc_unlock();

Anyway, despite that, this is not the best solution that can be available.
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Sun Apr 28 13:01:29 2024 UTC