NovinkaZverejnili sme 50+ Laravel balíkov ako open source
Custom AI apps, agents and automation — Roundly ConsultingRoundly
Všetky balíky
Testing for Laravel

Súbehy na reálnej databáze

Záznamník zámkov dokazuje, že zámok bol vyžiadaný; že naozaj drží, dokáže až druhá relácia. RoundlyConsulting\Testing\Concurrency\Racer forkne testovací proces a v potomkovi spustí jednu operáciu na vlastnom pripojení, kým rodič drží zámok:

use Illuminate\Support\Facades\DB;
use RoundlyConsulting\Testing\Concurrency\Racer;

it('serialises two redemptions of the last coupon', function (): void {
    DB::beginTransaction();
    Coupon::query()->lockForUpdate()->find($coupon->id);

    $racer = (new Racer)
        ->run(fn (): string => Coupons::redeem($coupon->code) ? 'redeemed' : 'refused')
        ->waitUntilBlockedOrDone();

    Coupons::redeem($coupon->code);
    DB::commit();

    expect($racer->outcome())->toBe('refused');
})->skip(fn (): bool => ! Racer::available(), 'needs a real engine and pcntl + posix');

waitUntilBlockedOrDone() sa vráti, keď racer čaká za zámkom rodiča, takže rodič môže pred commitom dokončiť svoju stranu súbehu. Ten istý mechanizmus bez fasády v ceste:

it('waits until the racer blocks on a row the parent holds, then lets it through', function (): void {
    DB::table('race_rows')->insert(['id' => 1, 'value' => 1]);

    DB::beginTransaction();
    DB::table('race_rows')->where('id', 1)->lockForUpdate()->first();

    $racer = (new Racer)->run(function (): string {
        DB::table('race_rows')->where('id', 1)->update(['value' => 2]);

        return 'updated';
    })->waitUntilBlockedOrDone();

    // Blocked, not finished: the row is still the parent's.
    expect(DB::table('race_rows')->value('value'))->toBe(1);

    DB::commit();

    expect($racer->outcome())->toBe('updated')
        ->and(DB::table('race_rows')->value('value'))->toBe(2);
})->skip(fn (): bool => ! Racer::available(), 'needs a real engine (pgsql/mysql) and pcntl + posix');

API

ČlenSprávanie
Racer::available()true na vetve pgsql alebo mysql (DriverMatrix::driver()), ak sú k dispozícii pcntl_fork() a posix_kill(). Použite ho v ->skip(), aby vetva, ktorá súbeh spustiť nevie, to viditeľne oznámila.
run(Closure $operation)Forkne proces a v rodičovi sa hneď vráti. Operácia vracia výsledok ako reťazec. Racer beží raz: druhé run() vyhodí LogicException. Bez pcntl alebo posix vyhodí RuntimeException s názvami rozšírení.
waitUntilBlockedOrDone(float $timeoutSeconds = 30.0)Vráti sa, keď racer čaká na zámok v databáze tohto testu (Postgres pg_stat_activity, MySQL performance_schema.data_lock_waits) alebo skončil, pretože ho nič nezablokovalo. Racer, ktorý v časovom limite neurobí ani jedno, sa ukončí cez SIGKILL a volanie vyhodí RuntimeException — bez osirelého procesu.
outcome()Počká na koniec racera a vráti, čo nahlásil.
Racer::CONNECTIONracer — predvolené pripojenie potomka.

outcome() a waitUntilBlockedOrDone() zavolané pred run() vyhodia LogicException.

Výsledky

(new Racer)->run(fn (): string => 'won')->outcome();
// 'won'

(new Racer)->run(fn (): string => throw new RuntimeException('lost the race'))->outcome();
// 'error: RuntimeException: lost the race'

(new Racer)->run(function (): string { exit(0); })->outcome();
// 'error: the racer exited without reporting an outcome'

(new Racer)->run(fn (): string => '')->outcome();
// '' — an empty outcome stays apart from a racer that never reported

$racer = (new Racer)->run(function (): string {
    sleep(30);

    return 'too late';
});
$racer->waitUntilBlockedOrDone(timeoutSeconds: 0.3);
// throws RuntimeException: The racer neither blocked nor finished within 0.3 s.
// $racer->outcome() then starts with 'error: the racer was killed'

Čo robí potomok a prečo

  • Vlastné pripojenie. Zaregistruje racer z DriverMatrix::connectionConfig() — pri --parallel databázu workera — a nastaví ho ako predvolené. Pripojenia rodiča sa nikdy nedotkne: zdieľa s ním relácie na serveri a zatvorenie jednej by rodičovi ukončilo reláciu uprostred testu.
  • Končí cez posix_kill(SIGKILL), nikdy cez exit() ani návrat: žiadne shutdown handlery, žiadne deštruktory, takže nič nezatvorí zdieľané relácie — ani reláciu ponechanej schémy. Operácia, ktorá sama zavolá exit(), sa aj tak ukončí cez SIGKILL zo shutdown funkcie.
  • Výsledky sú reťazce. Výnimka sa zmení na error: <trieda>: <správa>; operácia, ktorá zavolá exit(), nahlási, že skončila bez výsledku; potomok, ktorý bol ukončený zvonka alebo zlyhal skôr, než čokoľvek zapísal, nahlási, že zanikol bez výsledku. Prázdny reťazec zostane prázdnym reťazcom.
  • Kontrola čakania na zámok sa obmedzuje na databázu tohto testu, takže čakanie iného paralelného workera tento test predčasne neuvoľní.

Rozšírenia a enginy

  • ext-pcntl a ext-posix sú odporúčané, nie povinné: bez nich sa balík nainštaluje ako zvyčajne, Racer::available() vráti false a testy súbehov sa viditeľne preskočia. Obe existujú len na Unixe.
  • run() funguje aj na SQLite, no potomok s databázou v pamäti začína s vlastnou prázdnou databázou, takže súbeh tam nič nedokazuje — available() je false. Ani MariaDB tu nie je engine pre súbehy: jej tabuľky čakania na zámky sa líšia od MySQL.
  • Racer nepotrebuje Testbench: použije ho každá Laravel aplikácia, ktorej testovacie pripojenie je cez maticu driverov pgsql alebo mysql.

Prejavte lásku k open source

Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.

Ďalšie spôsoby podpory vrátane kryptomien

Odoslaním daru súhlasíte s našimi podmienkami prijímania darov.

Chcete to zabudovať do svojho produktu?

Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.