Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 87fbb34

Browse files
Release Locks from Internal Store on Postgres waitAndSave*
if something goes wrong talking with the database connection (like a disconnect or timeout) the parent `Lock` object will catch and simply say that there was an error acquring the lock. However, waitAndSave and waitAndSaveRead both store the lock in an internal store so trying to re-acquire the lock with the same connection would result in a lock confliected exception even though the lock was never acquired in the first place. This takes the fix from #44828 and applies it to the waitAndSave and waitAndSaveRead methods on both DoctrineDbalPostgreSqlStore and PostgreSqlStore.
1 parent 49b6d7f commit 87fbb34

File tree

4 files changed

+228
-21
lines changed

4 files changed

+228
-21
lines changed

src/Symfony/Component/Lock/Store/DoctrineDbalPostgreSqlStore.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,18 @@ public function waitAndSave(Key $key)
173173
// Internal store does not allow blocking mode, because there is no way to acquire one in a single process
174174
$this->getInternalStore()->save($key);
175175

176+
$lockAcquired = false;
176177
$sql = 'SELECT pg_advisory_lock(:key)';
177-
$this->conn->executeStatement($sql, [
178-
'key' => $this->getHashedKey($key),
179-
]);
178+
try {
179+
$this->conn->executeStatement($sql, [
180+
'key' => $this->getHashedKey($key),
181+
]);
182+
$lockAcquired = true;
183+
} finally {
184+
if (!$lockAcquired) {
185+
$this->getInternalStore()->delete($key);
186+
}
187+
}
180188

181189
// release lock in case of promotion
182190
$this->unlockShared($key);
@@ -188,10 +196,18 @@ public function waitAndSaveRead(Key $key)
188196
// Internal store does not allow blocking mode, because there is no way to acquire one in a single process
189197
$this->getInternalStore()->saveRead($key);
190198

199+
$lockAcquired = false;
191200
$sql = 'SELECT pg_advisory_lock_shared(:key)';
192-
$this->conn->executeStatement($sql, [
193-
'key' => $this->getHashedKey($key),
194-
]);
201+
try {
202+
$this->conn->executeStatement($sql, [
203+
'key' => $this->getHashedKey($key),
204+
]);
205+
$lockAcquired = true;
206+
} finally {
207+
if (!$lockAcquired) {
208+
$this->getInternalStore()->delete($key);
209+
}
210+
}
195211

196212
// release lock in case of demotion
197213
$this->unlock($key);

src/Symfony/Component/Lock/Store/PostgreSqlStore.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,18 @@ public function waitAndSave(Key $key)
235235
// Internal store does not allow blocking mode, because there is no way to acquire one in a single process
236236
$this->getInternalStore()->save($key);
237237

238+
$lockAcquired = false;
238239
$sql = 'SELECT pg_advisory_lock(:key)';
239-
$stmt = $this->getConnection()->prepare($sql);
240-
241-
$stmt->bindValue(':key', $this->getHashedKey($key));
242-
$stmt->execute();
240+
try {
241+
$stmt = $this->getConnection()->prepare($sql);
242+
$stmt->bindValue(':key', $this->getHashedKey($key));
243+
$stmt->execute();
244+
$lockAcquired = true;
245+
} finally {
246+
if (!$lockAcquired) {
247+
$this->getInternalStore()->delete($key);
248+
}
249+
}
243250

244251
// release lock in case of promotion
245252
$this->unlockShared($key);
@@ -257,11 +264,19 @@ public function waitAndSaveRead(Key $key)
257264
// Internal store does not allow blocking mode, because there is no way to acquire one in a single process
258265
$this->getInternalStore()->saveRead($key);
259266

267+
$lockAcquired = false;
260268
$sql = 'SELECT pg_advisory_lock_shared(:key)';
261-
$stmt = $this->getConnection()->prepare($sql);
262269

263-
$stmt->bindValue(':key', $this->getHashedKey($key));
264-
$stmt->execute();
270+
try {
271+
$stmt = $this->getConnection()->prepare($sql);
272+
$stmt->bindValue(':key', $this->getHashedKey($key));
273+
$stmt->execute();
274+
$lockAcquired = true;
275+
} finally {
276+
if (!$lockAcquired) {
277+
$this->getInternalStore()->delete($key);
278+
}
279+
}
265280

266281
// release lock in case of demotion
267282
$this->unlock($key);

src/Symfony/Component/Lock/Tests/Store/DoctrineDbalPostgreSqlStoreTest.php

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Symfony\Component\Lock\Tests\Store;
1313

14+
use Doctrine\DBAL\Connection;
1415
use Doctrine\DBAL\DriverManager;
16+
use Doctrine\DBAL\Exception as DBALException;
1517
use Symfony\Component\Lock\Exception\InvalidArgumentException;
1618
use Symfony\Component\Lock\Exception\LockConflictedException;
1719
use Symfony\Component\Lock\Key;
@@ -29,15 +31,21 @@ class DoctrineDbalPostgreSqlStoreTest extends AbstractStoreTest
2931
use BlockingStoreTestTrait;
3032
use SharedLockStoreTestTrait;
3133

34+
public function createPostgreSqlConnection(): Connection
35+
{
36+
if (!getenv('POSTGRES_HOST')) {
37+
$this->markTestSkipped('Missing POSTGRES_HOST env variable');
38+
}
39+
40+
return DriverManager::getConnection(['url' => 'pgsql://postgres:password@'.getenv('POSTGRES_HOST')]);
41+
}
42+
3243
/**
3344
* {@inheritdoc}
3445
*/
3546
public function getStore(): PersistingStoreInterface
3647
{
37-
if (!getenv('POSTGRES_HOST')) {
38-
$this->markTestSkipped('Missing POSTGRES_HOST env variable');
39-
}
40-
$conn = DriverManager::getConnection(['url' => 'pgsql://postgres:password@'.getenv('POSTGRES_HOST')]);
48+
$conn = $this->createPostgreSqlConnection();
4149

4250
return new DoctrineDbalPostgreSqlStore($conn);
4351
}
@@ -86,4 +94,82 @@ public function testSaveAfterConflict()
8694
$store2->save($key);
8795
$this->assertTrue($store2->exists($key));
8896
}
97+
98+
/**
99+
* @ticket https://github.com/symfony/symfony/issues/45505
100+
*/
101+
public function testWaitAndSaveAfterConflictReleasesLockFromInternalStore()
102+
{
103+
$store1 = $this->getStore();
104+
$conn = $this->createPostgreSqlConnection();
105+
$store2 = new DoctrineDbalPostgreSqlStore($conn);
106+
107+
$keyId = uniqid(__METHOD__, true);
108+
$store1Key = new Key($keyId);
109+
110+
$store1->save($store1Key);
111+
112+
// set a low time out then try to wait and save, which will fail
113+
// because the key is already set above.
114+
$conn->executeStatement('SET statement_timeout = 1');
115+
$waitSaveError = null;
116+
try {
117+
$store2->waitAndSave(new Key($keyId));
118+
} catch (DBALException $waitSaveError) {
119+
}
120+
$this->assertInstanceOf(DBALException::class, $waitSaveError, 'waitAndSave should have thrown');
121+
$conn->executeStatement('SET statement_timeout = 0');
122+
123+
$store1->delete($store1Key);
124+
$this->assertFalse($store1->exists($store1Key));
125+
126+
$store2Key = new Key($keyId);
127+
$lockConflicted = false;
128+
try {
129+
$store2->waitAndSave($store2Key);
130+
} catch (LockConflictedException $lockConflictedException) {
131+
$lockConflicted = true;
132+
}
133+
134+
$this->assertFalse($lockConflicted, 'lock should be available now that its been remove from $store1');
135+
$this->assertTrue($store2->exists($store2Key));
136+
}
137+
138+
/**
139+
* @ticket https://github.com/symfony/symfony/issues/45505
140+
*/
141+
public function testWaitAndSaveReadAfterConflictReleasesLockFromInternalStore()
142+
{
143+
$store1 = $this->getStore();
144+
$conn = $this->createPostgreSqlConnection();
145+
$store2 = new DoctrineDbalPostgreSqlStore($conn);
146+
147+
$keyId = uniqid(__METHOD__, true);
148+
$store1Key = new Key($keyId);
149+
150+
$store1->save($store1Key);
151+
152+
// set a low time out then try to wait and save, which will fail
153+
// because the key is already set above.
154+
$conn->executeStatement('SET statement_timeout = 1');
155+
$waitSaveError = null;
156+
try {
157+
$store2->waitAndSaveRead(new Key($keyId));
158+
} catch (DBALException $waitSaveError) {
159+
}
160+
$this->assertInstanceOf(DBALException::class, $waitSaveError, 'waitAndSaveRead should have thrown');
161+
162+
$store1->delete($store1Key);
163+
$this->assertFalse($store1->exists($store1Key));
164+
165+
$store2Key = new Key($keyId);
166+
// since the lock is going to be acquired in read mode and is not exclusive
167+
// this won't every throw a LockConflictedException as it would from
168+
// waitAndSave, but it will hang indefinitely as it waits for postgres
169+
// so set a time out of 2 seconds here so the test doesn't just sit forever
170+
$conn->executeStatement('SET statement_timeout = 2000');
171+
$store2->waitAndSaveRead($store2Key);
172+
173+
$this->assertTrue($store2->exists($store2Key));
174+
}
89175
}

src/Symfony/Component/Lock/Tests/Store/PostgreSqlStoreTest.php

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,24 @@ class PostgreSqlStoreTest extends AbstractStoreTest
2828
use BlockingStoreTestTrait;
2929
use SharedLockStoreTestTrait;
3030

31+
public function getPostgresHost(): string
32+
{
33+
$host = getenv('POSTGRES_HOST');
34+
if (!$host) {
35+
$this->markTestSkipped('Missing POSTGRES_HOST env variable');
36+
}
37+
38+
return $host;
39+
}
40+
3141
/**
3242
* {@inheritdoc}
3343
*/
3444
public function getStore(): PersistingStoreInterface
3545
{
36-
if (!getenv('POSTGRES_HOST')) {
37-
$this->markTestSkipped('Missing POSTGRES_HOST env variable');
38-
}
46+
$host = $this->getPostgresHost();
3947

40-
return new PostgreSqlStore('pgsql:host='.getenv('POSTGRES_HOST'), ['db_username' => 'postgres', 'db_password' => 'password']);
48+
return new PostgreSqlStore('pgsql:host='.$host, ['db_username' => 'postgres', 'db_password' => 'password']);
4149
}
4250

4351
/**
@@ -78,4 +86,86 @@ public function testSaveAfterConflict()
7886
$store2->save($key);
7987
$this->assertTrue($store2->exists($key));
8088
}
89+
90+
/**
91+
* @ticket https://github.com/symfony/symfony/issues/45505
92+
*/
93+
public function testWaitAndSaveAfterConflictReleasesLockFromInternalStore()
94+
{
95+
$store1 = $this->getStore();
96+
$postgresHost = $this->getPostgresHost();
97+
$pdo = new \PDO('pgsql:host='.$postgresHost, 'postgres', 'password');
98+
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
99+
$store2 = new PostgreSqlStore($pdo);
100+
101+
$keyId = uniqid(__METHOD__, true);
102+
$store1Key = new Key($keyId);
103+
104+
$store1->save($store1Key);
105+
106+
// set a low time out then try to wait and save, which will fail
107+
// because the key is already set above.
108+
$pdo->exec('SET statement_timeout = 1');
109+
$waitSaveError = null;
110+
try {
111+
$store2->waitAndSave(new Key($keyId));
112+
} catch (\PDOException $waitSaveError) {
113+
}
114+
$this->assertInstanceOf(\PDOException::class, $waitSaveError, 'waitAndSave should have thrown');
115+
$pdo->exec('SET statement_timeout = 0');
116+
117+
$store1->delete($store1Key);
118+
$this->assertFalse($store1->exists($store1Key));
119+
120+
$store2Key = new Key($keyId);
121+
$lockConflicted = false;
122+
try {
123+
$store2->waitAndSave($store2Key);
124+
} catch (LockConflictedException $lockConflictedException) {
125+
$lockConflicted = true;
126+
}
127+
128+
$this->assertFalse($lockConflicted, 'lock should be available now that its been remove from $store1');
129+
$this->assertTrue($store2->exists($store2Key));
130+
}
131+
132+
/**
133+
* @ticket https://github.com/symfony/symfony/issues/45505
134+
*/
135+
public function testWaitAndSaveReadAfterConflictReleasesLockFromInternalStore()
136+
{
137+
$store1 = $this->getStore();
138+
$postgresHost = $this->getPostgresHost();
139+
$pdo = new \PDO('pgsql:host='.$postgresHost, 'postgres', 'password');
140+
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
141+
$store2 = new PostgreSqlStore($pdo);
142+
143+
$keyId = uniqid(__METHOD__, true);
144+
$store1Key = new Key($keyId);
145+
146+
$store1->save($store1Key);
147+
148+
// set a low time out then try to wait and save, which will fail
149+
// because the key is already set above.
150+
$pdo->exec('SET statement_timeout = 1');
151+
$waitSaveError = null;
152+
try {
153+
$store2->waitAndSaveRead(new Key($keyId));
154+
} catch (\PDOException $waitSaveError) {
155+
}
156+
$this->assertInstanceOf(\PDOException::class, $waitSaveError, 'waitAndSave should have thrown');
157+
158+
$store1->delete($store1Key);
159+
$this->assertFalse($store1->exists($store1Key));
160+
161+
$store2Key = new Key($keyId);
162+
// since the lock is going to be acquired in read mode and is not exclusive
163+
// this won't every throw a LockConflictedException as it would from
164+
// waitAndSave, but it will hang indefinitely as it waits for postgres
165+
// so set a time out of 2 seconds here so the test doesn't just sit forever
166+
$pdo->exec('SET statement_timeout = 20000');
167+
$store2->waitAndSaveRead($store2Key);
168+
169+
$this->assertTrue($store2->exists($store2Key));
170+
}
81171
}

0 commit comments

Comments
 (0)