Skip to content

Commit 48a8c81

Browse files
authored
Merge pull request #8323 from kenjis/fix-redis-session
fix: [Session] Redis session race condition
2 parents 3c5a22e + 2fa55d4 commit 48a8c81

File tree

1 file changed

+44
-23
lines changed

1 file changed

+44
-23
lines changed

system/Session/Handlers/RedisHandler.php

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ protected function setSavePath(): void
115115
*
116116
* @param string $path The path where to store/retrieve the session
117117
* @param string $name The session name
118+
*
119+
* @throws RedisException
118120
*/
119121
public function open($path, $name): bool
120122
{
@@ -124,12 +126,20 @@ public function open($path, $name): bool
124126

125127
$redis = new Redis();
126128

127-
if (! $redis->connect($this->savePath['protocol'] . '://' . $this->savePath['host'], ($this->savePath['host'][0] === '/' ? 0 : $this->savePath['port']), $this->savePath['timeout'])) {
129+
if (
130+
! $redis->connect(
131+
$this->savePath['protocol'] . '://' . $this->savePath['host'],
132+
($this->savePath['host'][0] === '/' ? 0 : $this->savePath['port']),
133+
$this->savePath['timeout']
134+
)
135+
) {
128136
$this->logger->error('Session: Unable to connect to Redis with the configured settings.');
129137
} elseif (isset($this->savePath['password']) && ! $redis->auth($this->savePath['password'])) {
130138
$this->logger->error('Session: Unable to authenticate to Redis instance.');
131139
} elseif (isset($this->savePath['database']) && ! $redis->select($this->savePath['database'])) {
132-
$this->logger->error('Session: Unable to select Redis database with index ' . $this->savePath['database']);
140+
$this->logger->error(
141+
'Session: Unable to select Redis database with index ' . $this->savePath['database']
142+
);
133143
} else {
134144
$this->redis = $redis;
135145

@@ -146,6 +156,8 @@ public function open($path, $name): bool
146156
*
147157
* @return false|string Returns an encoded string of the read data.
148158
* If nothing was read, it must return false.
159+
*
160+
* @throws RedisException
149161
*/
150162
#[ReturnTypeWillChange]
151163
public function read($id)
@@ -168,14 +180,16 @@ public function read($id)
168180
return $data;
169181
}
170182

171-
return '';
183+
return false;
172184
}
173185

174186
/**
175187
* Writes the session data to the session storage.
176188
*
177189
* @param string $id The session ID
178190
* @param string $data The encoded session data
191+
*
192+
* @throws RedisException
179193
*/
180194
public function write($id, $data): bool
181195
{
@@ -222,8 +236,8 @@ public function close(): bool
222236
$pingReply = $this->redis->ping();
223237

224238
if (($pingReply === true) || ($pingReply === '+PONG')) {
225-
if (isset($this->lockKey)) {
226-
$this->releaseLock();
239+
if (isset($this->lockKey) && ! $this->releaseLock()) {
240+
return false;
227241
}
228242

229243
if (! $this->redis->close()) {
@@ -246,12 +260,16 @@ public function close(): bool
246260
* Destroys a session
247261
*
248262
* @param string $id The session ID being destroyed
263+
*
264+
* @throws RedisException
249265
*/
250266
public function destroy($id): bool
251267
{
252268
if (isset($this->redis, $this->lockKey)) {
253269
if (($result = $this->redis->del($this->keyPrefix . $id)) !== 1) {
254-
$this->logger->debug('Session: Redis::del() expected to return 1, got ' . var_export($result, true) . ' instead.');
270+
$this->logger->debug(
271+
'Session: Redis::del() expected to return 1, got ' . var_export($result, true) . ' instead.'
272+
);
255273
}
256274

257275
return $this->destroyCookie();
@@ -278,6 +296,8 @@ public function gc($max_lifetime)
278296
* Acquires an emulated lock.
279297
*
280298
* @param string $sessionID Session ID
299+
*
300+
* @throws RedisException
281301
*/
282302
protected function lockSession(string $sessionID): bool
283303
{
@@ -287,48 +307,49 @@ protected function lockSession(string $sessionID): bool
287307
// so we need to check here if the lock key is for the
288308
// correct session ID.
289309
if ($this->lockKey === $lockKey) {
310+
// If there is the lock, make the ttl longer.
290311
return $this->redis->expire($this->lockKey, 300);
291312
}
292313

293314
$attempt = 0;
294315

295316
do {
296-
$ttl = $this->redis->ttl($lockKey);
297-
assert(is_int($ttl));
317+
$result = $this->redis->set(
318+
$lockKey,
319+
(string) Time::now()->getTimestamp(),
320+
// NX -- Only set the key if it does not already exist.
321+
// EX seconds -- Set the specified expire time, in seconds.
322+
['nx', 'ex' => 300]
323+
);
298324

299-
if ($ttl > 0) {
300-
sleep(1);
325+
if (! $result) {
326+
usleep(100000);
301327

302328
continue;
303329
}
304330

305-
if (! $this->redis->setex($lockKey, 300, (string) Time::now()->getTimestamp())) {
306-
$this->logger->error('Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID);
307-
308-
return false;
309-
}
310-
311331
$this->lockKey = $lockKey;
312332
break;
313-
} while (++$attempt < 30);
333+
} while (++$attempt < 300);
314334

315-
if ($attempt === 30) {
316-
log_message('error', 'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.');
335+
if ($attempt === 300) {
336+
$this->logger->error(
337+
'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID
338+
. ' after 300 attempts, aborting.'
339+
);
317340

318341
return false;
319342
}
320343

321-
if ($ttl === -1) {
322-
log_message('debug', 'Session: Lock for ' . $this->keyPrefix . $sessionID . ' had no TTL, overriding.');
323-
}
324-
325344
$this->lock = true;
326345

327346
return true;
328347
}
329348

330349
/**
331350
* Releases a previously acquired lock
351+
*
352+
* @throws RedisException
332353
*/
333354
protected function releaseLock(): bool
334355
{

0 commit comments

Comments
 (0)