Ondřej Mirtes

Detekce neuzavřených transakcí

Vezměte si následující kód:

public function processRow(Row $row)
{
	$this->databaseConnection->begin();
	try {
		// nějaké počáteční kontroly
		// $isProcessed = ...

		if ($isProcessed) {
			return;
		}

		// spousta práce se zpracováním row

		$this->databaseConnection->commit();
	} catch (\Exception $e) {
		$this->databaseConnection->rollback();
		throw $e;
	}
}

Spatřili jste tu chybu? V případě, že $isProcessed === true, dojde k opuštění metody bez commitu či rollbacku transakce, čímž zůstane v databázi otevřená až do uzavření spojení. A může zbytečně držet příliš dlouho zámky, o které mohou mít zájem ostatní vlákna aplikace.

Pokud by vám nevadily zamčené řádky, tak se vám stejně nebude líbit další důsledek tohoto bugu:

$this->databaseConnection->begin();
try {
	// volaná metoda vyhodnotí předaný řádek jako $isProcessed
	$this->processRow($fooRow);
	$this->databaseConnection->commit();
} catch (\Exception $e) {
	$this->databaseConnection->rollback();
	throw $e;
}

V tomto případě totiž commitujete či rollbackujete jinou transakci, než byste čekali!

S lepším transakčním API se nastalá situace dá detekovat:

$databaseTransaction = $this->databaseConnection->begin();
try {
	// volaná metoda vyhodnotí předaný řádek jako $isProcessed
	$this->processRow($fooRow);
	$databaseTransaction->commit();
} catch (\Exception $e) {
	$databaseTransaction->rollback();
	throw $e;
}

V tomto případě je databázová transakce reprezentovaná odděleným objektem, takže je vždy jednoznačně určeno, s jakou konkrétní transakcí pracujeme.

Jak ale detekovat, že nám v aplikaci zůstává viset nevyřešená transakce? Pomůžeme si destruktorem:

class DatabaseTransaction
{

	//...

	public function __destruct()
	{
		if (!$this->resolved) {
			// destruktor nemůže vyhazovat výjimky
			trigger_error('Unresolved transaction!', E_USER_NOTICE);
		}
	}

	//...

}

Kdy ale PHP destruktor objektu zavolá?

The destructor method will be called as soon as there are no other references to a particular object, or in any order during the shutdown sequence.

V závislosti na podobě implementace DatabaseConnection se destruktor zavolá jakmile PHP opustí metodu, ve které proběhlo přiřazení $databaseTransaction, a nebo taky ne, třeba v případě, že si všechny transakce ukládáme do interního pole pro pozdější odkazování.

Pokud se tedy destruktor transakce zavolá ihned po opuštění metody, tak v zalogované chybě dostaneme stack trace, ve které snadno dohledáme to místo, kde je špatně ošetřená transakce, a budeme ho moci opravit.

Pokud se destruktor zavolá až při shutdown sekvenci, tak už nám stack trace chyby o místu, kde vzniká neošetřená transakce, nic neřekne. V případě, že máte velkou mnohovrstevnatou aplikaci s mnoha transakcemi, se takové místo hledá velmi těžko.

Přišel jsem ale s funkčním řešením, které v této situaci debugging transakce usnadní. V PHP lze instanciovat výjimku, aniž bychom jí vyhazovali, a takto vytvořená výjimka si v sobě nese stack trace z místa svého vzniku!

class DatabaseTransaction
{

	/** @var UnresolvedTrasactionException */
	private $originException;

	public function __construct(DatabaseConnection $databaseConnection/*, ...*/)
	{
		// ...
		$this->originException = new UnresolvedTrasactionException();
	}

	public function __destruct()
	{
		if (!$this->resolved) {
			\Tracy\Debugger::log($this->originException);

			// destruktor nemůže vyhazovat výjimky
			trigger_error('Unresolved transaction!', E_USER_NOTICE);
		}
	}

	//...

}

Pro každou započatou transakci tedy vytvořím výjimku, kterou v případě nevyřešené transakce zaloguji a získám tak místo, kde chybná transakce vznikla. V destruktoru stále vyvolávám notice, aby si programátor této chyby všiml i během vývoje, kdy běžně složku s logy nesleduje.

Z hlediska čistoty a architektury kódu jde samozřejmě o dost neobvyklé až šílené řešení, které bych nikdy při code review nevpustil do běžné business logiky aplikace, kde si vývojář musí vystačit s běžnými vyjadřovacími prostředky a návrhovými vzory z OOP, ale v tomto případě jde o infrastrukturní pomocnou záležitost, která se v celé aplikaci vyskytuje právě jednou a nijak neovlivňuje architekturu zbytku aplikace. Pro usnadnění debuggingu a tedy ušetření času vývojáře jsem ochotný překročit hranici, ke které bych se běžně vůbec nepřiblížil.

‹ Slevomat Coding Standard What is Code? ›