mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2025-01-03 14:32:40 +00:00
Fix savepoint handling and locking in PostgreSQL driver
This commit is contained in:
parent
8a49202036
commit
1414f8979c
6 changed files with 47 additions and 13 deletions
|
@ -78,50 +78,63 @@ abstract class AbstractDriver implements Driver {
|
|||
}
|
||||
|
||||
public function savepointCreate(bool $lock = false): int {
|
||||
// if no transaction is active and a lock was requested, lock the database using a backend-specific routine
|
||||
if ($lock && !$this->transDepth) {
|
||||
$this->lock();
|
||||
$this->locked = true;
|
||||
}
|
||||
// create a savepoint, incrementing the transaction depth
|
||||
$this->exec("SAVEPOINT arsse_".(++$this->transDepth));
|
||||
// set the state of the newly created savepoint to pending
|
||||
$this->transStatus[$this->transDepth] = self::TR_PEND;
|
||||
// return the depth number
|
||||
return $this->transDepth;
|
||||
}
|
||||
|
||||
public function savepointRelease(int $index = null): bool {
|
||||
// assume the most recent savepoint if none was specified
|
||||
$index = $index ?? $this->transDepth;
|
||||
if (array_key_exists($index, $this->transStatus)) {
|
||||
switch ($this->transStatus[$index]) {
|
||||
case self::TR_PEND:
|
||||
// release the requested savepoint and set its state to committed
|
||||
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
||||
$this->transStatus[$index] = self::TR_COMMIT;
|
||||
// for any later pending savepoints, set their state to implicitly committed
|
||||
$a = $index;
|
||||
while (++$a && $a <= $this->transDepth) {
|
||||
if ($this->transStatus[$a] <= self::TR_PEND) {
|
||||
$this->transStatus[$a] = self::TR_PEND_COMMIT;
|
||||
}
|
||||
}
|
||||
// return success
|
||||
$out = true;
|
||||
break;
|
||||
case self::TR_PEND_COMMIT:
|
||||
// set the state to explicitly committed
|
||||
$this->transStatus[$index] = self::TR_COMMIT;
|
||||
$out = true;
|
||||
break;
|
||||
case self::TR_PEND_ROLLBACK:
|
||||
// set the state to explicitly committed
|
||||
$this->transStatus[$index] = self::TR_COMMIT;
|
||||
$out = false;
|
||||
break;
|
||||
case self::TR_COMMIT:
|
||||
case self::TR_ROLLBACK: //@codeCoverageIgnore
|
||||
// savepoint has already been released or rolled back; this is an error
|
||||
throw new Exception("savepointStale", ['action' => "commit", 'index' => $index]);
|
||||
default:
|
||||
throw new Exception("savepointStatusUnknown", $this->transStatus[$index]); // @codeCoverageIgnore
|
||||
}
|
||||
if ($index==$this->transDepth) {
|
||||
// if we've released the topmost savepoint, clean up all prior savepoints which have already been explicitly committed (or rolled back), if any
|
||||
while ($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
|
||||
array_pop($this->transStatus);
|
||||
$this->transDepth--;
|
||||
}
|
||||
}
|
||||
// if no savepoints are pending and the database was locked, unlock it
|
||||
if (!$this->transDepth && $this->locked) {
|
||||
$this->unlock();
|
||||
$this->locked = false;
|
||||
|
|
|
@ -13,6 +13,8 @@ use JKingWeb\Arsse\Db\ExceptionInput;
|
|||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||
|
||||
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||
protected $transStart = 0;
|
||||
|
||||
public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null, string $service = null) {
|
||||
// check to make sure required extension is loaded
|
||||
if (!static::requirementsMet()) {
|
||||
|
@ -104,37 +106,44 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
}
|
||||
|
||||
public function savepointCreate(bool $lock = false): int {
|
||||
if (!$this->transDepth) {
|
||||
if (!$this->transStart) {
|
||||
$this->exec("BEGIN TRANSACTION");
|
||||
}
|
||||
$this->transStart = parent::savepointCreate($lock);
|
||||
return $this->transStart;
|
||||
} else {
|
||||
return parent::savepointCreate($lock);
|
||||
}
|
||||
}
|
||||
|
||||
public function savepointRelease(int $index = null): bool {
|
||||
$out = parent::savepointUndo($index);
|
||||
if ($out && !$this->transDepth) {
|
||||
$this->exec("COMMIT TRANSACTION");
|
||||
$index = $index ?? $this->transDepth;
|
||||
$out = parent::savepointRelease($index);
|
||||
if ($index == $this->transStart) {
|
||||
$this->exec("COMMIT");
|
||||
$this->transStart = 0;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function savepointUndo(int $index = null): bool {
|
||||
$index = $index ?? $this->transDepth;
|
||||
$out = parent::savepointUndo($index);
|
||||
if ($out && !$this->transDepth) {
|
||||
$this->exec("ROLLBACK TRANSACTION");
|
||||
if ($index == $this->transStart) {
|
||||
$this->exec("ROLLBACK");
|
||||
$this->transStart = 0;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
protected function lock(): bool {
|
||||
if ($this->schemaVersion()) {
|
||||
if ($this->query("SELECT count(*) from information_schema.tables where table_schema = current_schema() and table_name = 'arsse_meta'")->getValue()) {
|
||||
$this->exec("LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function unlock(bool $rollback = false): bool {
|
||||
$this->exec((!$rollback) ? "COMMIT" : "ROLLBACK");
|
||||
// do nothing; transaction is committed or rolled back later
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -375,7 +375,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
// so the effect is usually the same
|
||||
$this->drv->savepointCreate(true);
|
||||
$this->assertException();
|
||||
$this->exec(str_replace("#", "3", $this->setVersion));
|
||||
$this->exec($this->lock);
|
||||
}
|
||||
|
||||
public function testUnlockTheDatabase() {
|
||||
|
|
|
@ -13,6 +13,14 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
|||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
protected static $implementation = "PDO PostgreSQL";
|
||||
protected $create = "CREATE TABLE arsse_test(id bigserial primary key)";
|
||||
protected $lock = ["BEGIN", "LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT"];
|
||||
protected $lock = ["BEGIN", "LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"];
|
||||
protected $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'";
|
||||
|
||||
public function tearDown() {
|
||||
try {
|
||||
$this->drv->exec("ROLLBACK");
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -197,9 +197,13 @@ class DatabaseInformation {
|
|||
}
|
||||
},
|
||||
'razeFunction' => function($db, array $afterStatements = []) use ($pgObjectList) {
|
||||
// rollback any pending transaction
|
||||
try {
|
||||
$db->exec("ROLLBACK");
|
||||
} catch(\Throwable $e) {
|
||||
}
|
||||
foreach ($pgObjectList($db) as $obj) {
|
||||
$db->exec("DROP {$obj['type']} IF EXISTS {$obj['name']} cascade");
|
||||
|
||||
}
|
||||
foreach ($afterStatements as $st) {
|
||||
$db->exec($st);
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
|
||||
<file>cases/Db/PostgreSQL/TestStatement.php</file>
|
||||
<file>cases/Db/PostgreSQL/TestCreation.php</file>
|
||||
<!--<file>cases/Db/PostgreSQL/TestDriver.php</file>-->
|
||||
<file>cases/Db/PostgreSQL/TestDriver.php</file>
|
||||
<!--<file>cases/Db/PostgreSQL/TestUpdate.php</file>-->
|
||||
</testsuite>
|
||||
<testsuite name="Database functions">
|
||||
|
|
Loading…
Reference in a new issue