Files
newznab-tmux/app/Services/Binaries/HeaderStorageTransaction.php
T
2026-04-29 09:22:43 +02:00

193 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Binaries;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Handles header storage transactions and rollback cleanup.
*/
final class HeaderStorageTransaction
{
private const MAX_SQL_ROWS_PER_STATEMENT = 500;
private CollectionHandler $collectionHandler;
private BinaryHandler $binaryHandler;
private string $batchNoise;
private bool $hadErrors = false;
public function __construct(
CollectionHandler $collectionHandler,
BinaryHandler $binaryHandler,
PartHandler $partHandler
) {
$this->collectionHandler = $collectionHandler;
$this->binaryHandler = $binaryHandler;
$this->batchNoise = bin2hex(random_bytes(8));
}
/**
* Get the batch noise marker for this transaction.
*/
public function getBatchNoise(): string
{
return $this->batchNoise;
}
/**
* Start a new database transaction.
*/
public function begin(): void
{
DB::beginTransaction();
$this->hadErrors = false;
}
/**
* Mark that an error occurred.
*/
public function markError(): void
{
$this->hadErrors = true;
}
/**
* Check if errors occurred.
*/
public function hasErrors(): bool
{
return $this->hadErrors;
}
/**
* Commit the transaction if no errors, rollback otherwise.
*/
public function finish(): bool
{
if ($this->hadErrors) {
$this->rollbackAndCleanup();
return false;
}
try {
DB::commit();
return true;
} catch (\Throwable $e) {
$this->rollbackAndCleanup();
if (config('app.debug') === true) {
Log::error('HeaderStorageTransaction commit failed: '.$e->getMessage());
}
return false;
}
}
/**
* Perform rollback and cleanup any orphaned data.
*/
private function rollbackAndCleanup(): void
{
try {
DB::rollBack();
} catch (\Throwable $e) {
// Already rolled back
}
// InnoDB rollback already reverts all rows written by this transaction.
// Manual cleanup is retained only for SQLite test/in-memory scenarios.
if (DB::getDriverName() === 'sqlite') {
$this->cleanup();
}
}
/**
* Cleanup rows that may have been inserted before rollback.
*/
private function cleanup(): void
{
try {
$this->cleanupParts();
$this->cleanupBinaries();
$this->cleanupCollections();
} catch (\Throwable $e) {
if (config('app.debug') === true) {
Log::warning('Post-rollback cleanup failed: '.$e->getMessage());
}
}
}
private function cleanupParts(): void
{
$ids = $this->binaryHandler->getInsertedIds();
foreach (array_chunk($ids, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) {
$placeholders = implode(',', array_fill(0, \count($chunk), '?'));
DB::statement("DELETE FROM parts WHERE binaries_id IN ({$placeholders})", $chunk);
}
}
private function cleanupBinaries(): void
{
$ids = $this->binaryHandler->getInsertedIds();
foreach (array_chunk($ids, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) {
$placeholders = implode(',', array_fill(0, \count($chunk), '?'));
DB::statement("DELETE FROM binaries WHERE id IN ({$placeholders})", $chunk);
}
}
private function cleanupCollections(): void
{
$insertedIds = $this->collectionHandler->getInsertedIds();
$allIds = $this->collectionHandler->getAllIds();
$hashes = $this->collectionHandler->getBatchHashes();
$ids = ! empty($insertedIds) ? $insertedIds : $allIds;
if (! empty($ids)) {
foreach (array_chunk($ids, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) {
$placeholders = implode(',', array_fill(0, \count($chunk), '?'));
DB::statement(
"DELETE FROM parts WHERE binaries_id IN (SELECT id FROM binaries WHERE collections_id IN ({$placeholders}))",
$chunk
);
DB::statement("DELETE FROM binaries WHERE collections_id IN ({$placeholders})", $chunk);
DB::statement("DELETE FROM collections WHERE id IN ({$placeholders})", $chunk);
}
} elseif (! empty($hashes)) {
foreach (array_chunk($hashes, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) {
$placeholders = implode(',', array_fill(0, \count($chunk), '?'));
DB::statement(
"DELETE FROM parts WHERE binaries_id IN (SELECT id FROM binaries WHERE collections_id IN (SELECT id FROM collections WHERE collectionhash IN ({$placeholders})))",
$chunk
);
DB::statement(
"DELETE FROM binaries WHERE collections_id IN (SELECT id FROM collections WHERE collectionhash IN ({$placeholders}))",
$chunk
);
DB::statement("DELETE FROM collections WHERE collectionhash IN ({$placeholders})", $chunk);
}
} else {
// Fallback by noise marker
DB::statement(
'DELETE FROM parts WHERE binaries_id IN (SELECT b.id FROM binaries b WHERE b.collections_id IN (SELECT c.id FROM collections c WHERE c.noise = ?))',
[$this->batchNoise]
);
DB::statement(
'DELETE FROM binaries WHERE collections_id IN (SELECT id FROM collections WHERE noise = ?)',
[$this->batchNoise]
);
DB::statement('DELETE FROM collections WHERE noise = ?', [$this->batchNoise]);
}
}
}