Skip to content

Commit 05d798b

Browse files
committed
Add support for different weight
This drop the workflow with the counter key. Since we use a lua script - the atomicity is guarantee
1 parent ea06e59 commit 05d798b

File tree

6 files changed

+135
-76
lines changed

6 files changed

+135
-76
lines changed

src/Symfony/Component/Semaphore/Key.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,24 @@ final class Key
2525
{
2626
private $resource;
2727
private $limit;
28+
private $weight;
2829
private $expiringTime;
2930
private $state = [];
3031

31-
public function __construct(string $resource, int $limit)
32+
public function __construct(string $resource, int $limit, int $weight = 1)
3233
{
3334
if (1 > $limit) {
3435
throw new InvalidArgumentException("The limit ($limit) should be greater than 0.");
3536
}
37+
if (1 > $weight) {
38+
throw new InvalidArgumentException("The weight ($weight) should be greater than 0.");
39+
}
40+
if ($weight > $limit) {
41+
throw new InvalidArgumentException("The weight ($weight) should be lower than the limit ($limit).");
42+
}
3643
$this->resource = $resource;
3744
$this->limit = $limit;
45+
$this->weight = $weight;
3846
}
3947

4048
public function __toString(): string
@@ -47,6 +55,11 @@ public function getLimit(): int
4755
return $this->limit;
4856
}
4957

58+
public function getWeight(): int
59+
{
60+
return $this->weight;
61+
}
62+
5063
public function hasState(string $stateKey): bool
5164
{
5265
return isset($this->state[$stateKey]);

src/Symfony/Component/Semaphore/Store/RedisStore.php

Lines changed: 6 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -52,47 +52,14 @@ public function save(Key $key, float $ttlInSecond)
5252
throw new InvalidArgumentException("The TTL should be greater than 0, '$ttlInSecond' given.");
5353
}
5454

55-
$script = '
56-
local key = KEYS[1]
57-
local counterKey = key .. ":counter"
58-
local ownerKey = key .. ":owner"
59-
local timeKey = key .. ":time"
60-
local identifier = ARGV[1]
61-
local now = tonumber(ARGV[2])
62-
local ttlInSecond = tonumber(ARGV[3])
63-
local limit = tonumber(ARGV[4])
64-
65-
-- Remove expired values
66-
redis.call("ZREMRANGEBYSCORE", timeKey, "-inf", now)
67-
redis.call("ZINTERSTORE", ownerKey, 2, ownerKey, timeKey, "WEIGHTS", 1, 0)
68-
69-
-- Try to get a semaphore
70-
local counter = redis.call("INCR", counterKey)
71-
redis.call("ZADD", timeKey, now + ttlInSecond, identifier)
72-
redis.call("ZADD", ownerKey, counter, identifier)
73-
local rank = redis.call("ZRANK", ownerKey, identifier)
74-
75-
if rank >= limit then
76-
redis.call("ZREM", timeKey, identifier)
77-
redis.call("ZREM", ownerKey, identifier)
78-
79-
return false
80-
end
81-
82-
-- Extends the TTL
83-
local maxExpiration = redis.call("ZREVRANGE", timeKey, 0, 0, "WITHSCORES")[2]
84-
redis.call("EXPIREAT", counterKey, maxExpiration + 10)
85-
redis.call("EXPIREAT", ownerKey, maxExpiration + 10)
86-
redis.call("EXPIREAT", timeKey, maxExpiration + 10)
87-
88-
return true
89-
';
55+
$script = file_get_contents(__DIR__.'/scripts/redis_save.lua');
9056

9157
$args = [
9258
$this->getUniqueToken($key),
9359
time(),
9460
$ttlInSecond,
9561
$key->getLimit(),
62+
$key->getWeight(),
9663
];
9764

9865
if (!$this->evaluate($script, sprintf('{%s}', $key), $args)) {
@@ -109,27 +76,7 @@ public function putOffExpiration(Key $key, float $ttlInSecond)
10976
throw new InvalidArgumentException("The TTL should be greater than 0, '$ttlInSecond' given.");
11077
}
11178

112-
$script = '
113-
local key = KEYS[1]
114-
local counterKey = key .. ":counter"
115-
local ownerKey = key .. ":owner"
116-
local timeKey = key .. ":time"
117-
118-
local added = redis.call("ZADD", timeKey, ARGV[1], ARGV[2])
119-
if added == 1 then
120-
redis.call("ZREM", timeKey, ARGV[2])
121-
end
122-
123-
-- Extends the TTL
124-
local maxExpiration = redis.call("ZREVRANGE", timeKey, 0, 0, "WITHSCORES")[2]
125-
if maxExpiration then
126-
redis.call("EXPIREAT", counterKey, maxExpiration + 10)
127-
redis.call("EXPIREAT", ownerKey, maxExpiration + 10)
128-
redis.call("EXPIREAT", timeKey, maxExpiration + 10)
129-
end
130-
131-
return added
132-
';
79+
$script = file_get_contents(__DIR__.'/scripts/redis_put_off_expiration.lua');
13380

13481
if ($this->evaluate($script, sprintf('{%s}', $key), [time() + $ttlInSecond, $this->getUniqueToken($key)])) {
13582
throw new SemaphoreExpiredException($key, 'the script returns false');
@@ -141,32 +88,17 @@ public function putOffExpiration(Key $key, float $ttlInSecond)
14188
*/
14289
public function delete(Key $key)
14390
{
144-
$script = '
145-
local key = KEYS[1]
146-
local counterKey = key .. ":counter"
147-
local ownerKey = key .. ":owner"
148-
local timeKey = key .. ":time"
149-
local identifier = ARGV[1]
150-
151-
redis.call("ZREM", timeKey, identifier)
152-
local ret = redis.call("ZREM", ownerKey, identifier)
153-
154-
if redis.call("ZCARD", ownerKey) == 0 then
155-
redis.call("DEL", counterKey)
156-
end
157-
158-
return ret
159-
';
91+
$script = file_get_contents(__DIR__.'/scripts/redis_delete.lua');
16092

161-
$this->evaluate($script, sprintf('{%s}', $key),[$this->getUniqueToken($key)]);
93+
$this->evaluate($script, sprintf('{%s}', $key), [$this->getUniqueToken($key)]);
16294
}
16395

16496
/**
16597
* {@inheritdoc}
16698
*/
16799
public function exists(Key $key): bool
168100
{
169-
return (bool) $this->redis->zScore(sprintf("{%s}:owner", $key), $this->getUniqueToken($key));
101+
return (bool) $this->redis->zScore(sprintf('{%s}:weight', $key), $this->getUniqueToken($key));
170102
}
171103

172104
/**
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
local key = KEYS[1]
2+
local weightKey = key .. ":weight"
3+
local timeKey = key .. ":time"
4+
local identifier = ARGV[1]
5+
6+
redis.call("ZREM", timeKey, identifier)
7+
return redis.call("ZREM", weightKey, identifier)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
local key = KEYS[1]
2+
local counterKey = key .. ":counter"
3+
local weightKey = key .. ":weight"
4+
local timeKey = key .. ":time"
5+
6+
local added = redis.call("ZADD", timeKey, ARGV[1], ARGV[2])
7+
if added == 1 then
8+
redis.call("ZREM", timeKey, ARGV[2])
9+
redis.call("ZREM", weightKey, ARGV[2])
10+
end
11+
12+
-- Extends the TTL
13+
local maxExpiration = redis.call("ZREVRANGE", timeKey, 0, 0, "WITHSCORES")[2]
14+
if maxExpiration then
15+
redis.call("EXPIREAT", counterKey, maxExpiration + 10)
16+
redis.call("EXPIREAT", weightKey, maxExpiration + 10)
17+
redis.call("EXPIREAT", timeKey, maxExpiration + 10)
18+
end
19+
20+
return added
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
local key = KEYS[1]
2+
local weightKey = key .. ":weight"
3+
local timeKey = key .. ":time"
4+
local identifier = ARGV[1]
5+
local now = tonumber(ARGV[2])
6+
local ttlInSecond = tonumber(ARGV[3])
7+
local limit = tonumber(ARGV[4])
8+
local weight = tonumber(ARGV[5])
9+
10+
-- Remove expired values
11+
redis.call("ZREMRANGEBYSCORE", timeKey, "-inf", now)
12+
redis.call("ZINTERSTORE", weightKey, 2, weightKey, timeKey, "WEIGHTS", 1, 0)
13+
14+
-- Sempahore already acquired?
15+
if redis.call("ZSCORE", timeKey, identifier) then
16+
return true
17+
end
18+
19+
-- Try to get a semaphore
20+
local semaphores = redis.call("ZRANGE", weightKey, 0, -1, "WITHSCORES")
21+
local count = 0
22+
23+
for i = 1, #semaphores, 2 do
24+
count = count + semaphores[i+1]
25+
end
26+
27+
redis.log(2, count)
28+
29+
-- Could we get the sempahore ?
30+
if count + weight > limit then
31+
return false
32+
end
33+
34+
-- Acquire the semaphore
35+
redis.call("ZADD", timeKey, now + ttlInSecond, identifier)
36+
redis.call("ZADD", weightKey, weight, identifier)
37+
38+
-- Extend the TTL
39+
local maxExpiration = redis.call("ZREVRANGE", timeKey, 0, 0, "WITHSCORES")[2]
40+
redis.call("EXPIREAT", weightKey, maxExpiration + 10)
41+
redis.call("EXPIREAT", timeKey, maxExpiration + 10)
42+
43+
return true

src/Symfony/Component/Semaphore/Tests/Store/AbstractStoreTest.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,51 @@ public function testSaveWithLimitAt2()
117117

118118
try {
119119
$store->save($key3, 10);
120-
$this->fail('The store shouldn\'t save the second key');
120+
$this->fail('The store shouldn\'t save the third key');
121+
} catch (SemaphoreAcquiringException $e) {
122+
}
123+
124+
// The failure of previous attempt should not impact the state of current semaphores
125+
$this->assertTrue($store->exists($key1));
126+
$this->assertTrue($store->exists($key2));
127+
$this->assertFalse($store->exists($key3));
128+
129+
$store->delete($key1);
130+
$this->assertFalse($store->exists($key1));
131+
$this->assertTrue($store->exists($key2));
132+
$this->assertFalse($store->exists($key3));
133+
134+
$store->save($key3, 10);
135+
$this->assertFalse($store->exists($key1));
136+
$this->assertTrue($store->exists($key2));
137+
$this->assertTrue($store->exists($key3));
138+
139+
$store->delete($key2);
140+
$store->delete($key3);
141+
}
142+
143+
public function testSaveWithWeightAndLimitAt3()
144+
{
145+
$store = $this->getStore();
146+
147+
$resource = 'resource';
148+
$key1 = new Key($resource, 4, 2);
149+
$key2 = new Key($resource, 4, 2);
150+
$key3 = new Key($resource, 4, 2);
151+
152+
$store->save($key1, 10);
153+
$this->assertTrue($store->exists($key1));
154+
$this->assertFalse($store->exists($key2));
155+
$this->assertFalse($store->exists($key3));
156+
157+
$store->save($key2, 10);
158+
$this->assertTrue($store->exists($key1));
159+
$this->assertTrue($store->exists($key2));
160+
$this->assertFalse($store->exists($key3));
161+
162+
try {
163+
$store->save($key3, 10);
164+
$this->fail('The store shouldn\'t save the third key');
121165
} catch (SemaphoreAcquiringException $e) {
122166
}
123167

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy