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.