Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
apc
Manage
Activity
Members
Labels
Plan
Wiki
Custom issue tracker
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Model registry
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
project
apc
Commits
240e38e5
Verified
Commit
240e38e5
authored
6 months ago
by
Alberto Paderno
Browse files
Options
Downloads
Patches
Plain Diff
Issue
#2332725
: Implement a lock backend
parent
81bd15a3
No related branches found
No related tags found
No related merge requests found
Pipeline
#263501
passed
6 months ago
Stage: build
Stage: validate
Stage: test
Changes
3
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
apc.info
+2
-1
2 additions, 1 deletion
apc.info
apc.test
+33
-24
33 additions, 24 deletions
apc.test
drupal_apc_lock.inc
+290
-0
290 additions, 0 deletions
drupal_apc_lock.inc
with
325 additions
and
25 deletions
apc.info
+
2
−
1
View file @
240e38e5
...
...
@@ -2,5 +2,6 @@ name = Alternative PHP Cache
description = Integrates the APCu extension with Drupal.
package = Performance and scalability
core = 7.x
files[] = drupal_apc_cache.inc
files[] = apc.test
files[] = drupal_apc_cache.inc
files[] = drupal_apc_lock.inc
This diff is collapsed.
Click to expand it.
apc.test
+
33
−
24
View file @
240e38e5
...
...
@@ -5,15 +5,17 @@
* Test classes for the Alternative PHP Cache module.
*/
// phpcs:disable SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator.NullCoalesceOperatorNotUsed
/**
* Methods and properties for all the test classes that write directly to APCu.
*/
trait
ApcTestTrait
{
trait
Apc
Apcu
TestTrait
{
/**
* Whether the test should be skipped.
*
* ApcTestTrait::checkRequirements() will set this property to TRUE if the
* Apc
Apcu
TestTrait::checkRequirements() will set this property to TRUE if the
* requirements for the test to run are not met.
*
* @var bool
...
...
@@ -295,14 +297,18 @@ trait ApcTestTrait {
* @param string $key
* The prefix to search for in APCu keys, excluding the regular expression
* delimiters and the ^ anchor.
* @param array $options
* An array of options, which can contain the following keys:
* - message: A custom message to display upon assertion.
*
* @return bool
* TRUE if there are APCu keys matching the prefix, FALSE otherwise.
*/
protected
function
assertApcuKeysByPrefix
(
$key
)
{
protected
function
assertApcuKeysByPrefix
(
$key
,
$options
=
array
()
)
{
$escaped_key
=
preg_quote
(
$key
,
'/'
);
$iterator
=
new
APCUIterator
(
"/^
$escaped_key
/"
,
APC_ITER_KEY
);
$message
=
format_string
(
'There are APCu keys matching @key.'
,
array
(
'@key'
=>
$this
->
varExport
(
"/^
$escaped_key
/"
)));
$message
=
isset
(
$options
[
'message'
])
?
$options
[
'message'
]
:
'There are APCu keys matching @key.'
;
$message
=
format_string
(
$message
,
array
(
'@key'
=>
$this
->
varExport
(
"/^
$escaped_key
/"
)));
return
$this
->
assertTrue
(
$iterator
->
getTotalCount
()
>
0
,
$message
);
}
...
...
@@ -313,14 +319,18 @@ trait ApcTestTrait {
* @param string $key
* The prefix to search for in APCu keys, excluding the regular expression
* delimiters and the ^ anchor.
* @param array $options
* An array of options, which can contain the following keys:
* - message: A custom message to display upon assertion.
*
* @return bool
* TRUE if there are no APCu keys matching the prefix, FALSE otherwise.
*/
protected
function
assertNoApcuKeysByPrefix
(
$key
)
{
protected
function
assertNoApcuKeysByPrefix
(
$key
,
$options
=
array
()
)
{
$escaped_key
=
preg_quote
(
$key
,
'/'
);
$iterator
=
new
APCUIterator
(
"/^
$escaped_key
/"
,
APC_ITER_KEY
);
$message
=
format_string
(
'There are no APCu keys matching @key.'
,
array
(
'@key'
=>
$this
->
varExport
(
"/^
$escaped_key
/"
)));
$message
=
isset
(
$options
[
'message'
])
?
$options
[
'message'
]
:
'There are no APCu keys matching @key.'
;
$message
=
format_string
(
$message
,
array
(
'@key'
=>
$this
->
varExport
(
"/^
$escaped_key
/"
)));
return
$this
->
assertTrue
(
$iterator
->
getTotalCount
()
===
0
,
$message
);
}
...
...
@@ -330,9 +340,9 @@ trait ApcTestTrait {
/**
* Base class for the test classes that write directly to APCu.
*/
class
ApcBaseTestCase
extends
DrupalUnitTestCase
{
class
Apc
Apcu
BaseTestCase
extends
DrupalUnitTestCase
{
use
ApcTestTrait
;
use
Apc
Apcu
TestTrait
;
/**
* {@inheritdoc}
...
...
@@ -355,7 +365,7 @@ class ApcBaseTestCase extends DrupalUnitTestCase {
/**
* Unit tests for reading and writing the APCu storage.
*/
class
ApcApcuStoreAndRetrieveTestCase
extends
ApcBaseTestCase
{
class
ApcApcuStoreAndRetrieveTestCase
extends
Apc
Apcu
BaseTestCase
{
/**
* {@inheritdoc}
...
...
@@ -419,9 +429,8 @@ class ApcApcuStoreAndRetrieveTestCase extends ApcBaseTestCase {
$message
=
'New items were stored in APCu.'
;
if
(
$this
->
assertTrue
(
!
empty
(
$stored
),
$message
))
{
// Increase the TTL set by ApcTestTrait::storageData() by 2 seconds to
// be sure that non-permanent values are purged when the APCU storage is
// read.
// Halt the tests for 10 seconds, which is 2 seconds more than the
// maximum TTL set by ApcApcuTestTrait::storageData().
$message
=
format_string
(
'The test will be halted for @delay seconds.'
,
array
(
'@delay'
=>
10
));
$persistent_key_message
=
'@key persists and it was found.'
;
$purgeable_key_message
=
'@key was purgeable and it was not found.'
;
...
...
@@ -454,6 +463,8 @@ class ApcApcuStoreAndRetrieveTestCase extends ApcBaseTestCase {
*/
trait
ApcCacheTestTrait
{
use
ApcApcuTestTrait
;
/**
* The cache bins to store on APCu.
*
...
...
@@ -692,7 +703,6 @@ trait ApcCacheTestTrait {
*/
class
ApcCacheBaseTestCase
extends
DrupalUnitTestCase
{
use
ApcTestTrait
;
use
ApcCacheTestTrait
;
/**
...
...
@@ -703,7 +713,7 @@ class ApcCacheBaseTestCase extends DrupalUnitTestCase {
parent
::
setUp
();
$conf
[
'cache_backends'
][]
=
drupal_get_path
(
'module'
,
'apc'
)
.
'/
'
.
'
drupal_apc_cache.inc'
;
$conf
[
'cache_backends'
][]
=
drupal_get_path
(
'module'
,
'apc'
)
.
'/drupal_apc_cache.inc'
;
foreach
(
$this
->
getCacheBins
()
as
$cache_bin
)
{
$conf
[
"cache_class_
$cache_bin
"
]
=
'DrupalApcCache'
;
...
...
@@ -739,7 +749,6 @@ class ApcCacheBaseTestCase extends DrupalUnitTestCase {
*/
class
ApcCacheClearTestCase
extends
ApcCacheBaseTestCase
{
use
ApcTestTrait
;
use
ApcCacheTestTrait
;
/**
...
...
@@ -900,7 +909,6 @@ class ApcCacheClearTestCase extends ApcCacheBaseTestCase {
*/
class
ApcCacheConflictingKeysTestCase
extends
ApcCacheBaseTestCase
{
use
ApcTestTrait
;
use
ApcCacheTestTrait
;
/**
...
...
@@ -1150,8 +1158,9 @@ class ApcCacheStoreAndRetrieveTestCase extends ApcCacheBaseTestCase {
$message
=
'New items were stored in the cache.'
;
if
(
$this
->
assertTrue
(
!
empty
(
$stored
),
$message
))
{
// Add 2 seconds to the maximum TTL ApcTestTrait::storageData() sets to
// be sure that non-permanent values are purged when the cache is read.
// Add 2 seconds to the maximum TTL ApcApcuTestTrait::storageData() sets
// to be sure that non-permanent values are purged when the cache is
// read.
$message
=
format_string
(
'The test will be halted for @delay seconds.'
,
array
(
'@delay'
=>
10
));
$this
->
pass
(
$message
);
...
...
@@ -1186,8 +1195,8 @@ class ApcCacheStoreAndRetrieveTestCase extends ApcCacheBaseTestCase {
foreach
(
$this
->
storageData
()
as
$id
=>
$data
)
{
$cid
=
"store_retrieve_cache_item_
$id
"
;
// The TTL set by ApcTestTrait::storageData() is ignored. All the
cache
// items are permanent.
// The TTL set by Apc
Apcu
TestTrait::storageData() is ignored. All the
//
cache
items are permanent.
if
(
$this
->
assertCacheItemSaved
(
$bin
,
$cid
,
$data
[
'value'
]))
{
$stored
[]
=
$cid
;
}
...
...
@@ -1227,10 +1236,10 @@ class ApcCacheStoreAndRetrieveTestCase extends ApcCacheBaseTestCase {
$message
=
'New items were stored in the cache.'
;
if
(
$this
->
assertTrue
(
!
empty
(
$stored
),
$message
))
{
// 12 is the maximum TTL set by ApcTestTrait::storageData(), plus 2
to
//
change the 0 TTL (which means a permanent cache item), plus 2 to be
// sure that all the cached items are
purged when cache_get_multiple()
// is called.
// 12 is the maximum TTL set by Apc
Apcu
TestTrait::storageData(), plus 2
//
to make the permanent cache items ApcApcuTestTrait::storageData()
//
returns purgeable, plus 2 to be
sure that all the cached items are
//
purged when cache_get_multiple()
is called.
$message
=
format_string
(
'The test will be halted for @delay seconds.'
,
array
(
'@delay'
=>
12
));
$this
->
pass
(
$message
);
...
...
This diff is collapsed.
Click to expand it.
drupal_apc_lock.inc
0 → 100644
+
290
−
0
View file @
240e38e5
<?php
/**
* @file
* An APCU-based implementation of a locking mechanism.
*
* @see includes/lock.inc
*/
/**
* Class for the APCu-based locking mechanism implementation.
*
* @internal
*/
class
ApcLock
{
/**
* The locks acquired in the current request.
*
* @var array
*/
protected
static
$locks
=
array
();
/**
* The unique ID used to generate the APCu keys for locks.
*
* @var string
*/
protected
static
$uniqueId
=
''
;
/**
* Initializes the locking system.
*/
public
static
function
initialize
()
{
self
::
$uniqueId
=
drupal_random_bytes
(
32
);
}
/**
* Acquires (or renews) a lock, but it does not block if it fails.
*
* @param string $name
* The name of the lock.
* @param float $timeout
* The number of seconds before the lock expires. The minimum timeout is 1.
*
* @return bool
* TRUE if the lock was acquired, FALSE otherwise.
*/
public
static
function
acquire
(
$name
,
$timeout
=
30.0
)
{
// Ensure that the timeout is at least one second. APCu works with integer
// TTLs where 0 means the value is persistent.
$timeout
=
(
int
)
round
(
max
(
$timeout
,
1.0
));
$key
=
self
::
getLockKey
(
$name
);
$expire
=
microtime
(
TRUE
)
+
$timeout
;
if
(
isset
(
self
::
$locks
[
$name
]))
{
// Try to extend the expiration of a lock we already acquired.
$success
=
apcu_store
(
$key
,
array
(
'expire'
=>
$expire
),
$timeout
);
if
(
!
$success
)
{
// The lock was broken.
unset
(
self
::
$locks
[
$name
]);
apcu_delete
(
$key
);
}
return
$success
;
}
else
{
// Try to acquire the lock.
$retry
=
FALSE
;
do
{
$success
=
apcu_store
(
$key
,
array
(
'expire'
=>
$expire
),
$timeout
);
if
(
$success
)
{
self
::
$locks
[
$name
]
=
TRUE
;
}
else
{
$retry
=
!
$retry
&&
self
::
maybeAvailable
(
$name
);
}
}
while
(
$retry
);
}
return
isset
(
self
::
$locks
[
$name
]);
}
/**
* Checks if a lock acquired by a different process is available.
*
* If an existing lock has expired, it is removed.
*
* @param string $name
* The name of the lock.
*
* @return bool
* TRUE if there is no lock, or it was removed, FALSE otherwise.
*/
public
static
function
maybeAvailable
(
$name
)
{
$key
=
self
::
getLockKey
(
$name
);
if
(
!
apcu_exists
(
$key
))
{
return
TRUE
;
}
$lock
=
apcu_fetch
(
$key
,
$success
);
if
(
!
$success
)
{
return
TRUE
;
}
$expire
=
(
float
)
$lock
[
'expire'
];
$now
=
microtime
(
TRUE
);
if
(
$now
>=
$expire
)
{
apcu_delete
(
$key
);
return
TRUE
;
}
return
FALSE
;
}
/**
* Waits for a lock to be available.
*
* @param string $name
* The name of the lock.
* @param float $delay
* The maximum number of seconds to wait, as an integer.
*
* @return bool
* FALSE if the lock is available, TRUE otherwise.
*/
public
static
function
waitForLock
(
$name
,
$delay
=
30.0
)
{
// Pause the process for short periods between calling
// lock_may_be_available(). However, if the wait period is too long, there
// is the potential for a large number of processes to be blocked waiting
// for a lock, especially if the item being rebuilt is commonly requested.
// To address both of these concerns, begin waiting for 25ms, then add 25
// ms to the wait period each time until it reaches 500 ms. After this
// point, polling will continue every 500 ms until $delay is reached.
// $delay is passed in seconds, but we will be using usleep(), which takes
// microseconds as a parameter. Multiply it by 1 million so that all
// further numbers are equivalent.
$delay
=
(
int
)
round
(
$delay
*
1000000
);
// Begin sleeping at 25ms.
$sleep
=
25000
;
while
(
$delay
>
0
)
{
// This function should only be called by a request that failed to get a
// lock, so we sleep first to give the parallel request a chance to finish
// and release the lock.
usleep
(
$sleep
);
// After each sleep, increase the value of $sleep until it reaches 500 ms,
// to reduce the potential for a lock stampede.
$delay
=
$delay
-
$sleep
;
$sleep
=
min
(
500000
,
$sleep
+
25000
,
$delay
);
if
(
!
apcu_exists
(
self
::
getLockKey
(
$name
)))
{
// No longer need to wait.
return
FALSE
;
}
}
// The caller must still wait longer to get the lock.
return
TRUE
;
}
/**
* Releases a lock previously acquired by lock_acquire().
*
* This will release the named lock if it is still held by the current
* request.
*
* @param string $name
* The name of the lock.
*/
public
static
function
release
(
$name
)
{
// The lock is unconditionally removed, since the caller assumes the lock is
// anyway released.
unset
(
self
::
$locks
[
$name
]);
apcu_delete
(
self
::
getLockKey
(
$name
));
}
/**
* Gets the name of the APCu key used to store a lock.
*
* @param string $name
* The name of the lock.
*
* @return string
* The APCu key name.
*/
public
static
function
getLockKey
(
$name
)
{
// self::$uniqueId is already initialized from ApcLock::initialize(). The
// following lines are added as safeguard, just in case
// ApcLock::initialize() is not called as expected. This should never happen
// because Drupal core calls lock_initialize() in
// _drupal_bootstrap_variables() and lock_initialize() implemented in this
// file is a wrapper for ApcLock::initialize().
if
(
empty
(
self
::
$uniqueId
))
{
self
::
$uniqueId
=
drupal_random_bytes
(
32
);
}
$hash
=
hash
(
'sha256'
,
self
::
$uniqueId
.
$name
,
TRUE
);
return
'apc_lock::'
.
drupal_base64_encode
(
$hash
);
}
}
/**
* Initializes the locking system.
*
* @see ApcLock::initialize()
*/
function
lock_initialize
()
{
ApcLock
::
initialize
();
}
/**
* Acquires (or renews) a lock, but it does not block if it fails.
*
* @param string $name
* The name of the lock.
* @param float $timeout
* The number of seconds before the lock expires. The minimum timeout is 1.
*
* @return bool
* TRUE if the lock was acquired, FALSE otherwise.
*
* @see ApcLock::acquire()
*/
function
lock_acquire
(
$name
,
$timeout
=
30.0
)
{
return
ApcLock
::
acquire
(
$name
,
$timeout
);
}
/**
* Checks if a lock acquired by a different process is available.
*
* If an existing lock has expired, it is removed.
*
* @param string $name
* The name of the lock.
*
* @return bool
* TRUE if there is no lock, or it was removed, FALSE otherwise.
*
* @see ApcLock::maybeAvailable()
*/
function
lock_may_be_available
(
$name
)
{
return
ApcLock
::
maybeAvailable
(
$name
);
}
/**
* Waits for a lock to be available.
*
* This function may be called in a request that fails to acquire a desired
* lock. This will block further execution until the lock is available or the
* specified delay in seconds is reached. This should not be used with locks
* that are acquired very frequently, since the lock is likely to be acquired
* again by a different request while waiting.
*
* @param string $name
* The name of the lock.
* @param float $delay
* The maximum number of seconds to wait, as an integer.
*
* @return bool
* FALSE if the lock is available, TRUE otherwise.
*
* @see ApcLock::waitForLock()
*/
function
lock_wait
(
$name
,
$delay
=
30.0
)
{
return
ApcLock
::
waitForLock
(
$name
,
$delay
);
}
/**
* Releases a lock previously acquired by lock_acquire().
*
* This will release the named lock if it is still held by the current request.
*
* @param string $name
* The name of the lock.
*
* @see ApcLock::release()
*/
function
lock_release
(
$name
)
{
ApcLock
::
release
(
$name
);
}
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment