Skip to content

Commit 4e6b2e4

Browse files
committed
Cache locks.
This commit adds atomic lock support to the Memcached and Redis drivers of the cache system, allowing for simple manipulation of “locks” without worrying about race conditions.
1 parent fcbc277 commit 4e6b2e4

File tree

9 files changed

+415
-1
lines changed

9 files changed

+415
-1
lines changed

src/Illuminate/Cache/Lock.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace Illuminate\Cache;
4+
5+
use Carbon\Carbon;
6+
use Illuminate\Contracts\Cache\LockTimeoutException;
7+
8+
abstract class Lock
9+
{
10+
/**
11+
* Attempt to acquire the lock.
12+
*
13+
* @return bool
14+
*/
15+
abstract public function acquire();
16+
17+
/**
18+
* Attempt to acquire the lock.
19+
*
20+
* @param callable|null $callback
21+
* @return bool
22+
*/
23+
public function get($callback = null)
24+
{
25+
$result = $this->acquire();
26+
27+
if ($result && is_callable($callback)) {
28+
return tap($callback(), function () {
29+
$this->release();
30+
});
31+
}
32+
33+
return $result;
34+
}
35+
36+
/**
37+
* Attempt to acquire the lock while blocking indefinitely.
38+
*
39+
* @param callable|null $calback
40+
* @return bool
41+
*/
42+
public function block($callback = null)
43+
{
44+
while (! $this->acquire()) {
45+
usleep(250 * 1000);
46+
}
47+
48+
if (is_callable($callback)) {
49+
return tap($callback(), function () {
50+
$this->release();
51+
});
52+
}
53+
54+
return true;
55+
}
56+
57+
/**
58+
* Attempt to acquire the lock for the given number of seconds.
59+
*
60+
* @param int $seconds
61+
* @param callable|null $callback
62+
* @return bool
63+
*/
64+
public function blockFor($seconds, $callback = null)
65+
{
66+
$starting = Carbon::now();
67+
68+
while (! $this->acquire()) {
69+
usleep(250 * 1000);
70+
71+
if (Carbon::now()->subSeconds($seconds)->gte($starting)) {
72+
throw new LockTimeoutException;
73+
}
74+
}
75+
76+
if (is_callable($callback)) {
77+
return tap($callback(), function () {
78+
$this->release();
79+
});
80+
}
81+
82+
return true;
83+
}
84+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Illuminate\Cache;
4+
5+
use Carbon\Carbon;
6+
use Illuminate\Contracts\Cache\Lock as LockContract;
7+
8+
class MemcachedLock extends Lock implements LockContract
9+
{
10+
/**
11+
* The Memcached instance.
12+
*
13+
* @var \Memcached
14+
*/
15+
protected $memcached;
16+
17+
/**
18+
* The name of the lock.
19+
*
20+
* @var string
21+
*/
22+
protected $name;
23+
24+
/**
25+
* The number of seconds the lock should be maintained.
26+
*
27+
* @var int
28+
*/
29+
protected $seconds;
30+
31+
/**
32+
* Create a new lock instance.
33+
*
34+
* @param \Memcached $memcached
35+
* @param string $name
36+
* @param int $seconds
37+
* @return void
38+
*/
39+
public function __construct($memcached, $name, $seconds)
40+
{
41+
$this->name = $name;
42+
$this->seconds = $seconds;
43+
$this->memcached = $memcached;
44+
}
45+
46+
/**
47+
* Attempt to acquire the lock.
48+
*
49+
* @return bool
50+
*/
51+
public function acquire()
52+
{
53+
return $this->memcached->add(
54+
$this->name, 1, $this->seconds
55+
);
56+
}
57+
58+
/**
59+
* Release the lock.
60+
*
61+
* @return void
62+
*/
63+
public function release()
64+
{
65+
$this->memcached->delete($this->name);
66+
}
67+
}

src/Illuminate/Cache/MemcachedStore.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
use Carbon\Carbon;
77
use ReflectionMethod;
88
use Illuminate\Contracts\Cache\Store;
9+
use Illuminate\Contracts\Cache\LockProvider;
910

10-
class MemcachedStore extends TaggableStore implements Store
11+
class MemcachedStore extends TaggableStore implements LockProvider, Store
1112
{
1213
/**
1314
* The Memcached instance.
@@ -170,6 +171,18 @@ public function forever($key, $value)
170171
$this->put($key, $value, 0);
171172
}
172173

174+
/**
175+
* Get a lock instance.
176+
*
177+
* @param string $name
178+
* @param int $seconds
179+
* @return \Illuminate\Contracts\Cache\Lock
180+
*/
181+
public function lock($name, $seconds = 0)
182+
{
183+
return new MemcachedLock($this->memcached, $this->prefix.$name, $seconds);
184+
}
185+
173186
/**
174187
* Remove an item from the cache.
175188
*

src/Illuminate/Cache/RedisLock.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Illuminate\Cache;
4+
5+
use Carbon\Carbon;
6+
use Illuminate\Contracts\Cache\Lock as LockContract;
7+
8+
class RedisLock extends Lock implements LockContract
9+
{
10+
/**
11+
* The Redis factory implementation.
12+
*
13+
* @var \Illuminate\Redis\Connections\Connection
14+
*/
15+
protected $redis;
16+
17+
/**
18+
* The name of the lock.
19+
*
20+
* @var string
21+
*/
22+
protected $name;
23+
24+
/**
25+
* The number of seconds the lock should be maintained.
26+
*
27+
* @var int
28+
*/
29+
protected $seconds;
30+
31+
/**
32+
* Create a new lock instance.
33+
*
34+
* @param \Illuminate\Redis\Connections\Connection $redis
35+
* @param string $name
36+
* @param int $seconds
37+
* @return void
38+
*/
39+
public function __construct($redis, $name, $seconds)
40+
{
41+
$this->name = $name;
42+
$this->redis = $redis;
43+
$this->seconds = $seconds;
44+
}
45+
46+
/**
47+
* Attempt to acquire the lock.
48+
*
49+
* @return bool
50+
*/
51+
public function acquire()
52+
{
53+
$result = $this->redis->setnx($this->name, 1);
54+
55+
if ($result === 1) {
56+
$this->redis->expire($this->name, $this->seconds);
57+
}
58+
59+
return $result === 1;
60+
}
61+
62+
/**
63+
* Release the lock.
64+
*
65+
* @return void
66+
*/
67+
public function release()
68+
{
69+
$this->redis->del($this->name);
70+
}
71+
}

src/Illuminate/Cache/RedisStore.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ public function forever($key, $value)
165165
$this->connection()->set($this->prefix.$key, $this->serialize($value));
166166
}
167167

168+
/**
169+
* Get a lock instance.
170+
*
171+
* @param string $name
172+
* @param int $seconds
173+
* @return \Illuminate\Contracts\Cache\Lock
174+
*/
175+
public function lock($name, $seconds = 0)
176+
{
177+
return new RedisLock($this->connection(), $this->prefix.$name, $seconds);
178+
}
179+
168180
/**
169181
* Remove an item from the cache.
170182
*
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Illuminate\Contracts\Cache;
4+
5+
interface Lock
6+
{
7+
/**
8+
* Attempt to acquire the lock.
9+
*
10+
* @param callable|null $callback
11+
* @return bool
12+
*/
13+
public function get($callback = null);
14+
15+
/**
16+
* Attempt to acquire the lock while blocking indefinitely.
17+
*
18+
* @param callable|null $calback
19+
* @return bool
20+
*/
21+
public function block($callback = null);
22+
23+
/**
24+
* Attempt to acquire the lock for the given number of seconds.
25+
*
26+
* @param int $seconds
27+
* @param callable|null $callback
28+
* @return bool
29+
*/
30+
public function blockFor($seconds, $callback = null);
31+
32+
/**
33+
* Release the lock.
34+
*
35+
* @return void
36+
*/
37+
public function release();
38+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Illuminate\Contracts\Cache;
4+
5+
interface LockProvider
6+
{
7+
/**
8+
* Get a lock instance.
9+
*
10+
* @param string $name
11+
* @param int $seconds
12+
* @return \Illuminate\Contracts\Cache\Lock
13+
*/
14+
public function lock($name, $seconds = 0);
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Illuminate\Contracts\Cache;
4+
5+
use Exception;
6+
7+
class LockTimeoutException extends Exception
8+
{
9+
//
10+
}

0 commit comments

Comments
 (0)