Consider the following:
function doAtomicTask($dbh)
{
$dbh->beginTransaction();
// ...
$dbh->commit();
}
Is there anything wrong here? We begin a transaction and we have a matching commit. But what if there’s an early return?
function doAtomicTask($dbh)
{
$dbh->beginTransaction();
if (/* some condition */ true) {
return;
}
$dbh->commit();
}
Or what if an exception is thrown?
function doAtomicTask($dbh)
{
$dbh->beginTransaction();
mightThrow();
$dbh->commit();
}
In both cases, the transaction is left dangling, neither committed nor rolled back. And if later your application needs to perform another transaction, then you might encounter the error, “There is already an active transaction.”
In C++ jargon, a transaction would be considered a resource. A resource is something you must give back once you have finished using it. In C++, the obvious example of a resource is memory that you get from the free store (using new) and have to give back to the free store (using delete). But other examples of resources are files (if you open one, you also have to close it), locks, sockets… or transactions.
Hazardous knee-jerk solution
When people first encounter this problem, they tend to consider it a problem with exceptions rather than with resource management, and they come up with a solution by catching the exception.
function doAtomicTask($dbh)
{
$dbh->beginTransaction();
try {
mightThrow();
} catch (Exception $e) {
$dbh->rollBack();
}
$dbh->commit();
}
But this solution doesn’t generalize well. Consider acquiring more resources.
function doAtomicTask($dbh)
{
$dbh->beginTransaction();
$fileHandle = fopen('file.txt', 'r');
flock($fileHandle, LOCK_EX); // acquire an exclusive lock
try {
mightThrow();
} catch (Exception $e) {
flock($fileHandle, LOCK_UN); // release the lock
fclose($fileHandle);
$dbh->rollBack();
}
flock($fileHandle, LOCK_UN); // release the lock
fclose($fileHandle);
$dbh->commit();
}
This solves the problem, but it repeats the resource release code, and repetitive code is a maintenance hazard.
Resource acquisition is initialization
Fortunately, we don’t need to litter our code with try/catch statements to deal with resource leaks. A better solution is to acquire a resource in the constructor of some object and release it in the matching destructor. This approach is called Resource Acquisition Is Initialization (RAII), or alternatively “Constructor Acquires, Destructor Releases.”
function doAtomicTask($dbh)
{
$transaction = new Transaction($dbh);
mightThrow();
$dbh->commit();
}
// This class "owns" a transaction resource.
// A transaction is acquired in the constructor and released in the destructor.
class Transaction
{
private $dbh;
public function __construct($dbh)
{
$this->dbh = $dbh;
// Acquire
$this->dbh->beginTransaction();
}
public function __destruct()
{
// Release
$this->dbh->rollBack();
}
}
Now whichever way we leave doAtomicTask()
, whether an early return or an exception, the destructor for $transaction
will be invoked and the transaction released (rolled back).