From 3584c5a59836135ab3119853a315693e192bdbd5 Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Sat, 15 Oct 2022 16:30:03 +0300 Subject: [PATCH 1/5] Subscribe to laravel events --- composer.json | 2 +- config/config.php | 2 + ecs.php | 5 +- rector.php | 5 ++ .../Exceptions/ExceptionInterface.php | 2 - .../TransactionRollbackException.php | 21 ++++++ .../Exceptions/TransactionStartException.php | 11 --- .../TransactionBeginningListener.php | 24 +++++++ .../TransactionCommittedListener.php | 24 +++++++ .../TransactionCommittingListener.php | 28 ++++++++ .../TransactionRolledBackListener.php | 24 +++++++ src/Internal/Service/ConnectionService.php | 26 +++++++ .../Service/ConnectionServiceInterface.php | 12 ++++ src/Internal/Service/DatabaseService.php | 50 +++---------- .../Service/DatabaseServiceInterface.php | 2 - src/Internal/Service/LockService.php | 25 +++++++ src/Internal/Service/LockServiceInterface.php | 5 ++ src/Services/RegulatorService.php | 70 +++++++++++++------ src/Services/RegulatorServiceInterface.php | 10 +++ src/WalletServiceProvider.php | 13 ++++ tests/Units/Domain/EventTest.php | 1 - tests/Units/Service/DatabaseTest.php | 27 ------- tests/Units/Service/LockServiceTest.php | 42 +++++++++++ 23 files changed, 322 insertions(+), 109 deletions(-) create mode 100644 src/Internal/Exceptions/TransactionRollbackException.php delete mode 100644 src/Internal/Exceptions/TransactionStartException.php create mode 100644 src/Internal/Listeners/TransactionBeginningListener.php create mode 100644 src/Internal/Listeners/TransactionCommittedListener.php create mode 100644 src/Internal/Listeners/TransactionCommittingListener.php create mode 100644 src/Internal/Listeners/TransactionRolledBackListener.php create mode 100644 src/Internal/Service/ConnectionService.php create mode 100644 src/Internal/Service/ConnectionServiceInterface.php diff --git a/composer.json b/composer.json index 65d1bf50b..795781cc6 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "brick/math": "~0.8", "doctrine/dbal": "^2.8|^3.0", "illuminate/contracts": "^9.0", - "illuminate/database": "^9.0", + "illuminate/database": "^9.37", "ramsey/uuid": "^4.0" }, "require-dev": { diff --git a/config/config.php b/config/config.php index 4c75d58d6..0e24030da 100644 --- a/config/config.php +++ b/config/config.php @@ -19,6 +19,7 @@ use Bavix\Wallet\Internal\Repository\TransferRepository; use Bavix\Wallet\Internal\Repository\WalletRepository; use Bavix\Wallet\Internal\Service\ClockService; +use Bavix\Wallet\Internal\Service\ConnectionService; use Bavix\Wallet\Internal\Service\DatabaseService; use Bavix\Wallet\Internal\Service\DispatcherService; use Bavix\Wallet\Internal\Service\JsonService; @@ -82,6 +83,7 @@ */ 'internal' => [ 'clock' => ClockService::class, + 'connection' => ConnectionService::class, 'database' => DatabaseService::class, 'dispatcher' => DispatcherService::class, 'json' => JsonService::class, diff --git a/ecs.php b/ecs.php index c0d1c6e33..274fbc90f 100644 --- a/ecs.php +++ b/ecs.php @@ -9,10 +9,10 @@ use Symplify\CodingStandard\Fixer\LineLength\DocBlockLineLengthFixer; use Symplify\CodingStandard\Fixer\LineLength\LineLengthFixer; use Symplify\EasyCodingStandard\Config\ECSConfig; -use Symplify\EasyCodingStandard\ValueObject\Option; use Symplify\EasyCodingStandard\ValueObject\Set\SetList; return static function (ECSConfig $containerConfigurator): void { + $containerConfigurator->parallel(); $services = $containerConfigurator->services(); $services->set(ArraySyntaxFixer::class) ->call('configure', [[ @@ -22,9 +22,6 @@ $services->set(DeclareStrictTypesFixer::class); $services->set(LineLengthFixer::class); - $parameters = $containerConfigurator->parameters(); - $parameters->set(Option::PARALLEL, true); - $containerConfigurator->paths([ __DIR__ . '/config', __DIR__ . '/database', diff --git a/rector.php b/rector.php index 18b319a46..bb721e85f 100644 --- a/rector.php +++ b/rector.php @@ -4,12 +4,15 @@ use Rector\CodeQuality\Rector\PropertyFetch\ExplicitMethodCallOverMagicGetSetRector; use Rector\Config\RectorConfig; +use Rector\Laravel\Rector\Assign\CallOnAppArrayAccessToStandaloneAssignRector; +use Rector\Laravel\Rector\ClassMethod\AddParentRegisterToEventServiceProviderRector; use Rector\Laravel\Set\LaravelSetList; use Rector\Php74\Rector\Property\TypedPropertyRector; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\SetList; return static function (RectorConfig $containerConfigurator): void { + $containerConfigurator->parallel(); $containerConfigurator->paths([ __DIR__ . '/src', __DIR__ . '/tests', @@ -29,4 +32,6 @@ // register a single rule $services->set(TypedPropertyRector::class); + $services->set(CallOnAppArrayAccessToStandaloneAssignRector::class); + $services->set(AddParentRegisterToEventServiceProviderRector::class); }; diff --git a/src/Internal/Exceptions/ExceptionInterface.php b/src/Internal/Exceptions/ExceptionInterface.php index f4774fb8b..f2c5fb4b0 100644 --- a/src/Internal/Exceptions/ExceptionInterface.php +++ b/src/Internal/Exceptions/ExceptionInterface.php @@ -31,6 +31,4 @@ interface ExceptionInterface extends Throwable public const TRANSACTION_FAILED = 1 << 10; public const MODEL_NOT_FOUND = 1 << 11; - - public const TRANSACTION_START = 1 << 12; } diff --git a/src/Internal/Exceptions/TransactionRollbackException.php b/src/Internal/Exceptions/TransactionRollbackException.php new file mode 100644 index 000000000..83f885fe8 --- /dev/null +++ b/src/Internal/Exceptions/TransactionRollbackException.php @@ -0,0 +1,21 @@ +result; + } +} diff --git a/src/Internal/Exceptions/TransactionStartException.php b/src/Internal/Exceptions/TransactionStartException.php deleted file mode 100644 index deb30f370..000000000 --- a/src/Internal/Exceptions/TransactionStartException.php +++ /dev/null @@ -1,11 +0,0 @@ -connectionService->get()->transactionLevel() === 1) { + $this->regulatorService->purge(); + } + } +} diff --git a/src/Internal/Listeners/TransactionCommittedListener.php b/src/Internal/Listeners/TransactionCommittedListener.php new file mode 100644 index 000000000..59478a110 --- /dev/null +++ b/src/Internal/Listeners/TransactionCommittedListener.php @@ -0,0 +1,24 @@ +connectionService->get()->transactionLevel() === 0) { + $this->regulatorService->committed(); + } + } +} diff --git a/src/Internal/Listeners/TransactionCommittingListener.php b/src/Internal/Listeners/TransactionCommittingListener.php new file mode 100644 index 000000000..b10482ec7 --- /dev/null +++ b/src/Internal/Listeners/TransactionCommittingListener.php @@ -0,0 +1,28 @@ +connectionService->get()->transactionLevel() === 1) { + $this->regulatorService->committing(); + } + } +} diff --git a/src/Internal/Listeners/TransactionRolledBackListener.php b/src/Internal/Listeners/TransactionRolledBackListener.php new file mode 100644 index 000000000..6c0312029 --- /dev/null +++ b/src/Internal/Listeners/TransactionRolledBackListener.php @@ -0,0 +1,24 @@ +connectionService->get()->transactionLevel() === 0) { + $this->regulatorService->purge(); + } + } +} diff --git a/src/Internal/Service/ConnectionService.php b/src/Internal/Service/ConnectionService.php new file mode 100644 index 000000000..f91f5fa02 --- /dev/null +++ b/src/Internal/Service/ConnectionService.php @@ -0,0 +1,26 @@ +connection = $connectionResolver->connection(config('wallet.database.connection')); + } + + public function get(): ConnectionInterface + { + return $this->connection; + } +} diff --git a/src/Internal/Service/ConnectionServiceInterface.php b/src/Internal/Service/ConnectionServiceInterface.php new file mode 100644 index 000000000..b8e96f81c --- /dev/null +++ b/src/Internal/Service/ConnectionServiceInterface.php @@ -0,0 +1,12 @@ +connection = $connectionResolver->connection(config('wallet.database.connection')); } /** @@ -33,39 +24,23 @@ public function __construct( */ public function transaction(callable $callback): mixed { - $level = $this->connection->transactionLevel(); - if ($level > 0 && ! $this->init) { - throw new TransactionStartException( - 'Working inside an embedded transaction is not possible. https://bavix.github.io/laravel-wallet/#/transaction', - ExceptionInterface::TRANSACTION_START, - ); - } - - if ($level > 0) { - return $callback(); - } - - $this->init = true; - try { - $this->regulatorService->purge(); + $connection = $this->connectionService->get(); + if ($connection->transactionLevel() > 0) { + return $callback(); + } - return $this->connection->transaction(function () use ($callback) { + return $connection->transaction(function () use ($callback) { $result = $callback(); - $this->init = false; - if ($result === false) { - return false; + if ($result === false || (is_countable($result) && count($result) === 0)) { + throw new TransactionRollbackException($result); } - if (is_countable($result) && count($result) === 0) { - return $result; - } - - $this->regulatorService->approve(); - return $result; }); + } catch (TransactionRollbackException $rollbackException) { + return $rollbackException->getResult(); } catch (RecordsNotFoundException|ExceptionInterface $exception) { throw $exception; } catch (Throwable $throwable) { @@ -74,9 +49,6 @@ public function transaction(callable $callback): mixed ExceptionInterface::TRANSACTION_FAILED, $throwable ); - } finally { - $this->regulatorService->purge(); - $this->init = false; } } } diff --git a/src/Internal/Service/DatabaseServiceInterface.php b/src/Internal/Service/DatabaseServiceInterface.php index 37c00d959..b844366c9 100644 --- a/src/Internal/Service/DatabaseServiceInterface.php +++ b/src/Internal/Service/DatabaseServiceInterface.php @@ -6,7 +6,6 @@ use Bavix\Wallet\Internal\Exceptions\ExceptionInterface; use Bavix\Wallet\Internal\Exceptions\TransactionFailedException; -use Bavix\Wallet\Internal\Exceptions\TransactionStartException; use Illuminate\Database\RecordsNotFoundException; interface DatabaseServiceInterface @@ -17,7 +16,6 @@ interface DatabaseServiceInterface * @return T * * @throws RecordsNotFoundException - * @throws TransactionStartException * @throws TransactionFailedException * @throws ExceptionInterface */ diff --git a/src/Internal/Service/LockService.php b/src/Internal/Service/LockService.php index 8ef9e9384..a4fffe651 100644 --- a/src/Internal/Service/LockService.php +++ b/src/Internal/Service/LockService.php @@ -23,6 +23,7 @@ final class LockService implements LockServiceInterface private CacheRepository $cache; public function __construct( + private ConnectionServiceInterface $connectionService, CacheFactory $cacheFactory, private int $seconds ) { @@ -43,6 +44,13 @@ public function block(string $key, callable $callback): mixed ->lock(self::LOCK_KEY . $key, $this->seconds); $this->lockedKeys->put(self::INNER_KEYS . $key, true, $this->seconds); + // let's release the lock after the transaction, the fight against the race + if ($this->connectionService->get()->transactionLevel() > 0) { + $lock->block($this->seconds); + + return $callback(); + } + try { return $lock->block($this->seconds, $callback); } finally { @@ -71,6 +79,23 @@ public function blocks(array $keys, callable $callback): mixed return $callable(); } + public function releases(array $keys): void + { + $lockProvider = $this->getLockProvider(); + + foreach ($keys as $key) { + if (! $this->isBlocked($key)) { + continue; + } + + $lockProvider + ->lock(self::LOCK_KEY . $key, $this->seconds) + ->forceRelease(); + + $this->lockedKeys->delete(self::INNER_KEYS . $key); + } + } + public function isBlocked(string $key): bool { return $this->lockedKeys->get(self::INNER_KEYS . $key) === true; diff --git a/src/Internal/Service/LockServiceInterface.php b/src/Internal/Service/LockServiceInterface.php index 37a7622aa..373f51f61 100644 --- a/src/Internal/Service/LockServiceInterface.php +++ b/src/Internal/Service/LockServiceInterface.php @@ -29,5 +29,10 @@ public function block(string $key, callable $callback): mixed; */ public function blocks(array $keys, callable $callback): mixed; + /** + * @param string[] $keys + */ + public function releases(array $keys): void; + public function isBlocked(string $key): bool; } diff --git a/src/Services/RegulatorService.php b/src/Services/RegulatorService.php index ddfabdea5..ff815618a 100644 --- a/src/Services/RegulatorService.php +++ b/src/Services/RegulatorService.php @@ -8,6 +8,7 @@ use Bavix\Wallet\Internal\Exceptions\RecordNotFoundException; use Bavix\Wallet\Internal\Repository\WalletRepositoryInterface; use Bavix\Wallet\Internal\Service\DispatcherServiceInterface; +use Bavix\Wallet\Internal\Service\LockServiceInterface; use Bavix\Wallet\Internal\Service\MathServiceInterface; use Bavix\Wallet\Internal\Service\StorageServiceInterface; use Bavix\Wallet\Models\Wallet; @@ -22,12 +23,18 @@ final class RegulatorService implements RegulatorServiceInterface */ private array $wallets = []; + /** + * @var array + */ + private array $multiIncrease = []; + public function __construct( private BalanceUpdatedEventAssemblerInterface $balanceUpdatedEventAssembler, private BookkeeperServiceInterface $bookkeeperService, private DispatcherServiceInterface $dispatcherService, private StorageServiceInterface $storageService, private MathServiceInterface $mathService, + private LockServiceInterface $lockService, private WalletRepositoryInterface $walletRepository ) { } @@ -86,29 +93,32 @@ public function decrease(Wallet $wallet, float|int|string $value): string return $this->increase($wallet, $this->mathService->negative($value)); } - public function approve(): void + public function committing(): void { - try { - $balances = []; - $incrementValues = []; - foreach ($this->wallets as $wallet) { - $diffValue = $this->diff($wallet); - if ($this->mathService->compare($diffValue, 0) === 0) { - continue; - } - - $incrementValues[$wallet->uuid] = $diffValue; - $balances[$wallet->getKey()] = $this->amount($wallet); + $balances = []; + $incrementValues = []; + foreach ($this->wallets as $wallet) { + $diffValue = $this->diff($wallet); + if ($this->mathService->compare($diffValue, 0) === 0) { + continue; } - if ($balances === [] || $incrementValues === [] || $this->wallets === []) { - return; - } + $balances[$wallet->getKey()] = $this->amount($wallet); + $incrementValues[$wallet->uuid] = $this->diff($wallet); + } - $this->walletRepository->updateBalances($balances); - $multiIncrease = $this->bookkeeperService->multiIncrease($this->wallets, $incrementValues); + if ($balances === [] || $incrementValues === [] || $this->wallets === []) { + return; + } - foreach ($multiIncrease as $uuid => $balance) { + $this->walletRepository->updateBalances($balances); + $this->multiIncrease = $this->bookkeeperService->multiIncrease($this->wallets, $incrementValues); + } + + public function committed(): void + { + try { + foreach ($this->multiIncrease as $uuid => $balance) { $wallet = $this->wallets[$uuid]; $wallet->fill([ @@ -124,13 +134,29 @@ public function approve(): void } } - public function purge(): void + /** + * @codeCoverageIgnore + */ + public function approve(): void { - foreach ($this->wallets as $wallet) { - $this->missing($wallet); + try { + $this->committing(); + } finally { + $this->committed(); } + } - $this->dispatcherService->forgot(); + public function purge(): void + { + try { + $this->lockService->releases(array_keys($this->wallets)); + $this->multiIncrease = []; + foreach ($this->wallets as $wallet) { + $this->missing($wallet); + } + } finally { + $this->dispatcherService->forgot(); + } } private function persist(Wallet $wallet): void diff --git a/src/Services/RegulatorServiceInterface.php b/src/Services/RegulatorServiceInterface.php index 77e8f0c6b..0bd16c387 100644 --- a/src/Services/RegulatorServiceInterface.php +++ b/src/Services/RegulatorServiceInterface.php @@ -20,6 +20,16 @@ public function increase(Wallet $wallet, float|int|string $value): string; public function decrease(Wallet $wallet, float|int|string $value): string; + public function committing(): void; + + public function committed(): void; + + /** + * @deprecated + * + * @see committing + * @see committed + */ public function approve(): void; public function purge(): void; diff --git a/src/WalletServiceProvider.php b/src/WalletServiceProvider.php index 60b97952a..dcc07ab8f 100644 --- a/src/WalletServiceProvider.php +++ b/src/WalletServiceProvider.php @@ -46,6 +46,8 @@ use Bavix\Wallet\Internal\Repository\WalletRepositoryInterface; use Bavix\Wallet\Internal\Service\ClockService; use Bavix\Wallet\Internal\Service\ClockServiceInterface; +use Bavix\Wallet\Internal\Service\ConnectionService; +use Bavix\Wallet\Internal\Service\ConnectionServiceInterface; use Bavix\Wallet\Internal\Service\DatabaseService; use Bavix\Wallet\Internal\Service\DatabaseServiceInterface; use Bavix\Wallet\Internal\Service\DispatcherService; @@ -109,6 +111,11 @@ use function dirname; use function function_exists; use Illuminate\Contracts\Cache\Factory as CacheFactory; +use Illuminate\Database\Events\TransactionBeginning; +use Illuminate\Database\Events\TransactionCommitted; +use Illuminate\Database\Events\TransactionCommitting; +use Illuminate\Database\Events\TransactionRolledBack; +use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; final class WalletServiceProvider extends ServiceProvider @@ -122,6 +129,11 @@ public function boot(): void { $this->loadTranslationsFrom(dirname(__DIR__) . '/resources/lang', 'wallet'); + Event::listen(TransactionBeginning::class, Internal\Listeners\TransactionBeginningListener::class); + Event::listen(TransactionCommitting::class, Internal\Listeners\TransactionCommittingListener::class); + Event::listen(TransactionCommitted::class, Internal\Listeners\TransactionCommittedListener::class); + Event::listen(TransactionRolledBack::class, Internal\Listeners\TransactionRolledBackListener::class); + if (! $this->app->runningInConsole()) { return; } @@ -213,6 +225,7 @@ private function internal(array $configure): void ->giveConfig('wallet.cache.ttl'); $this->app->singleton(ClockServiceInterface::class, $configure['clock'] ?? ClockService::class); + $this->app->singleton(ConnectionServiceInterface::class, $configure['connection'] ?? ConnectionService::class); $this->app->singleton(DatabaseServiceInterface::class, $configure['database'] ?? DatabaseService::class); $this->app->singleton(DispatcherServiceInterface::class, $configure['dispatcher'] ?? DispatcherService::class); $this->app->singleton(JsonServiceInterface::class, $configure['json'] ?? JsonService::class); diff --git a/tests/Units/Domain/EventTest.php b/tests/Units/Domain/EventTest.php index c0a981272..984be31bc 100644 --- a/tests/Units/Domain/EventTest.php +++ b/tests/Units/Domain/EventTest.php @@ -77,7 +77,6 @@ public function testBalanceUpdatedThrowDateListener(): void // unit $this->expectException(UnknownEventException::class); - $this->expectExceptionMessage(ClockFakeService::FAKE_DATETIME); $this->expectExceptionCode(789); $buyer->deposit(789); diff --git a/tests/Units/Service/DatabaseTest.php b/tests/Units/Service/DatabaseTest.php index 480dcb29a..991fadea9 100644 --- a/tests/Units/Service/DatabaseTest.php +++ b/tests/Units/Service/DatabaseTest.php @@ -6,10 +6,8 @@ use Bavix\Wallet\Internal\Exceptions\ExceptionInterface; use Bavix\Wallet\Internal\Exceptions\TransactionFailedException; -use Bavix\Wallet\Internal\Exceptions\TransactionStartException; use Bavix\Wallet\Internal\Service\DatabaseServiceInterface; use Bavix\Wallet\Test\Infra\TestCase; -use Illuminate\Support\Facades\DB; /** * @internal @@ -29,29 +27,4 @@ public function testCheckCode(): void throw new \RuntimeException('hello'); }); } - - /** - * @throws ExceptionInterface - */ - public function testCheckInTransaction(): void - { - $this->expectException(TransactionStartException::class); - $this->expectExceptionCode(ExceptionInterface::TRANSACTION_START); - - DB::beginTransaction(); - app(DatabaseServiceInterface::class)->transaction(static fn (): int => 42); - } - - public function testInterceptionErrorStartTransaction(): void - { - try { - DB::beginTransaction(); - app(DatabaseServiceInterface::class)->transaction(static fn (): int => 42); - self::fail(__METHOD__); - } catch (TransactionStartException) { - DB::rollBack(0); // for success - $result = app(DatabaseServiceInterface::class)->transaction(static fn (): int => 42); - self::assertSame(42, $result); - } - } } diff --git a/tests/Units/Service/LockServiceTest.php b/tests/Units/Service/LockServiceTest.php index 06d011861..2413c5edf 100644 --- a/tests/Units/Service/LockServiceTest.php +++ b/tests/Units/Service/LockServiceTest.php @@ -6,6 +6,7 @@ use Bavix\Wallet\Internal\Service\LockServiceInterface; use Bavix\Wallet\Test\Infra\TestCase; +use Illuminate\Support\Facades\DB; /** * @internal @@ -63,4 +64,45 @@ public function testLockDeep(): void self::assertTrue($checkIsBlock); self::assertFalse($lock->isBlocked($blockKey)); } + + public function testInTransactionLockable(): void + { + $blockKey1 = __METHOD__ . '1'; + $blockKey2 = __METHOD__ . '2'; + $lock = app(LockServiceInterface::class); + self::assertFalse($lock->isBlocked($blockKey1)); + self::assertFalse($lock->isBlocked($blockKey2)); + + $checkIsBlock1 = $lock->block($blockKey1, static fn () => $lock->isBlocked($blockKey1)); + self::assertTrue($checkIsBlock1); + self::assertFalse($lock->isBlocked($blockKey1)); + self::assertFalse($lock->isBlocked($blockKey2)); + + $checkIsBlock2 = $lock->block($blockKey2, static fn () => $lock->isBlocked($blockKey2)); + self::assertTrue($checkIsBlock2); + self::assertFalse($lock->isBlocked($blockKey1)); + self::assertFalse($lock->isBlocked($blockKey2)); + + DB::beginTransaction(); + + $checkIsBlock1 = $lock->block($blockKey1, static fn () => $lock->isBlocked($blockKey1)); + self::assertTrue($checkIsBlock1); + self::assertTrue($lock->isBlocked($blockKey1)); + self::assertFalse($lock->isBlocked($blockKey2)); + + $checkIsBlock2 = $lock->block($blockKey2, static fn () => $lock->isBlocked($blockKey2)); + self::assertTrue($checkIsBlock2); + self::assertTrue($lock->isBlocked($blockKey1)); + self::assertTrue($lock->isBlocked($blockKey2)); + + DB::commit(); + + self::assertTrue($lock->isBlocked($blockKey1)); + self::assertTrue($lock->isBlocked($blockKey2)); + + $lock->releases([$blockKey1, $blockKey2]); + + self::assertFalse($lock->isBlocked($blockKey1)); + self::assertFalse($lock->isBlocked($blockKey2)); + } } From 16433792096a68658d54c8efafac29f76e809768 Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Thu, 27 Oct 2022 18:22:27 +0300 Subject: [PATCH 2/5] fixed for laravel octane --- deptrac.yaml | 8 ++++++++ .../Listeners/TransactionBeginningListener.php | 10 ++-------- .../Listeners/TransactionCommittedListener.php | 10 ++-------- .../Listeners/TransactionCommittingListener.php | 10 ++-------- .../Listeners/TransactionRolledBackListener.php | 10 ++-------- src/Internal/Repository/WalletRepository.php | 9 +++++++++ 6 files changed, 25 insertions(+), 32 deletions(-) diff --git a/deptrac.yaml b/deptrac.yaml index f4a7e779f..10c17c18c 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -50,6 +50,11 @@ parameters: - type: className regex: ^Bavix\\.*\\Internal\\Exceptions\\.* + - name: InternalListener + collectors: + - type: className + regex: ^Bavix\\.*\\Internal\\Listeners\\.* + - name: Event collectors: - type: className @@ -250,6 +255,9 @@ parameters: - UIException - Model + InternalListener: + - ServiceInterface + ServiceInterface: - InternalException - EventInterface diff --git a/src/Internal/Listeners/TransactionBeginningListener.php b/src/Internal/Listeners/TransactionBeginningListener.php index 695f7ae42..4c928f5e5 100644 --- a/src/Internal/Listeners/TransactionBeginningListener.php +++ b/src/Internal/Listeners/TransactionBeginningListener.php @@ -9,16 +9,10 @@ final class TransactionBeginningListener { - public function __construct( - private ConnectionServiceInterface $connectionService, - private RegulatorServiceInterface $regulatorService - ) { - } - public function __invoke(): void { - if ($this->connectionService->get()->transactionLevel() === 1) { - $this->regulatorService->purge(); + if (app(ConnectionServiceInterface::class)->get()->transactionLevel() === 1) { + app(RegulatorServiceInterface::class)->purge(); } } } diff --git a/src/Internal/Listeners/TransactionCommittedListener.php b/src/Internal/Listeners/TransactionCommittedListener.php index 59478a110..10029d332 100644 --- a/src/Internal/Listeners/TransactionCommittedListener.php +++ b/src/Internal/Listeners/TransactionCommittedListener.php @@ -9,16 +9,10 @@ final class TransactionCommittedListener { - public function __construct( - private ConnectionServiceInterface $connectionService, - private RegulatorServiceInterface $regulatorService - ) { - } - public function __invoke(): void { - if ($this->connectionService->get()->transactionLevel() === 0) { - $this->regulatorService->committed(); + if (app(ConnectionServiceInterface::class)->get()->transactionLevel() === 0) { + app(RegulatorServiceInterface::class)->committed(); } } } diff --git a/src/Internal/Listeners/TransactionCommittingListener.php b/src/Internal/Listeners/TransactionCommittingListener.php index b10482ec7..12b7ad872 100644 --- a/src/Internal/Listeners/TransactionCommittingListener.php +++ b/src/Internal/Listeners/TransactionCommittingListener.php @@ -9,20 +9,14 @@ final class TransactionCommittingListener { - public function __construct( - private ConnectionServiceInterface $connectionService, - private RegulatorServiceInterface $regulatorService - ) { - } - public function __invoke(): void { /** * In fact, this if is not needed here. * But in order to protect the code from changes in the framework, I added a check here. */ - if ($this->connectionService->get()->transactionLevel() === 1) { - $this->regulatorService->committing(); + if (app(ConnectionServiceInterface::class)->get()->transactionLevel() === 1) { + app(RegulatorServiceInterface::class)->committing(); } } } diff --git a/src/Internal/Listeners/TransactionRolledBackListener.php b/src/Internal/Listeners/TransactionRolledBackListener.php index 6c0312029..9983ea88a 100644 --- a/src/Internal/Listeners/TransactionRolledBackListener.php +++ b/src/Internal/Listeners/TransactionRolledBackListener.php @@ -9,16 +9,10 @@ final class TransactionRolledBackListener { - public function __construct( - private ConnectionServiceInterface $connectionService, - private RegulatorServiceInterface $regulatorService - ) { - } - public function __invoke(): void { - if ($this->connectionService->get()->transactionLevel() === 0) { - $this->regulatorService->purge(); + if (app(ConnectionServiceInterface::class)->get()->transactionLevel() === 0) { + app(RegulatorServiceInterface::class)->purge(); } } } diff --git a/src/Internal/Repository/WalletRepository.php b/src/Internal/Repository/WalletRepository.php index bc1a04c9e..2b885725a 100644 --- a/src/Internal/Repository/WalletRepository.php +++ b/src/Internal/Repository/WalletRepository.php @@ -29,6 +29,15 @@ public function create(array $attributes): Wallet */ public function updateBalances(array $data): int { + // One element gives x10 speedup, on some data + if (count($data) === 1) { + return $this->wallet->newQuery() + ->whereKey(key($data)) + ->update([ + 'balance' => current($data), + ]); + } + $cases = []; foreach ($data as $walletId => $balance) { $cases[] = 'WHEN id = ' . $walletId . ' THEN ' . $balance; From 7ef013f74fb143489974a59994b198d4b9ec0ec8 Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Thu, 27 Oct 2022 19:16:25 +0300 Subject: [PATCH 3/5] update docs --- docs/transaction.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/transaction.md b/docs/transaction.md index 255daa0a4..df482002c 100644 --- a/docs/transaction.md +++ b/docs/transaction.md @@ -1,5 +1,28 @@ ## Transaction +> Starting from version 9.6 you can safely use standard framework transactions. + +You definitely need to know the feature of transactions. The wallet is automatically blocked from the moment it is used until the end of the transaction. Therefore, it is necessary to use the wallet closer to the end of the transaction. + +Very important! Almost all wallet transactions are blocking. + +```php +use Illuminate\Support\Facades\DB; + +DB::beginTransaction(); +$wallet->balanceInt; // now the wallet is blocked +doingMagic(); // running for a long time. +DB::commit(); // here will unlock the wallet +``` + +The point is that you need to minimize operations within transactions as much as possible. The longer the transaction, the longer the wallet lock. +The maximum wallet blocking time is set in the configuration. The longer the transaction takes, the more likely it is to get a race for the wallet. + +--- + +If you are using a version below 9.6, then the following is relevant for you: +-- + > Since version 9.2 it is safer to use AtomicServiceInterface. > Is it possible to use laravel's inline transactions? No, It is Immpossible. This limitation is due to the internal architecture of the package. To achieve the maximum speed of work, work with an internal state of balance was needed. Starting with version 8.2, a special error has appeared that will inform you about incorrect work with the `TransactionStartException` package. From 57c2d9585f01051a145ba4df109290547ea65fab Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Thu, 27 Oct 2022 19:27:58 +0300 Subject: [PATCH 4/5] fix nova-action.md --- docs/nova-action.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/nova-action.md b/docs/nova-action.md index 37902bf1e..6c8d55381 100644 --- a/docs/nova-action.md +++ b/docs/nova-action.md @@ -1,5 +1,7 @@ ## Nova Action +> Use only if you have a package version below 9.6 + As you know, the package works with internal state. You can read more [here](https://github.com/bavix/laravel-wallet/pull/412) and [here](https://github.com/bavix/laravel-wallet/issues/455). The action runs inside a transaction, which means you need to reset the transaction manually. From 96ed176acdf654951815d1a20a330f4ebe8c939b Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Thu, 27 Oct 2022 19:59:23 +0300 Subject: [PATCH 5/5] v9.6 --- changelog.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 7c2c9bde7..3185dec19 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [9.6.0] - 2022-10-27 +### Added +* Full support for standard transactions and laravel nova #589 + ## [9.5.0] - 2022-10-06 ### Added * Improved performance api handles #576 @@ -944,7 +948,8 @@ The operation is now executed in the transaction and updates the new `refund` fi - Exceptions: AmountInvalid, BalanceIsEmpty. - Models: Transfer, Transaction. -[Unreleased]: https://github.com/bavix/laravel-wallet/compare/9.5.0...master +[Unreleased]: https://github.com/bavix/laravel-wallet/compare/9.6.0...master +[9.6.0]: https://github.com/bavix/laravel-wallet/compare/9.5.0...9.6.0 [9.5.0]: https://github.com/bavix/laravel-wallet/compare/9.4.0...9.5.0 [9.4.0]: https://github.com/bavix/laravel-wallet/compare/9.3.0...9.4.0 [9.3.0]: https://github.com/bavix/laravel-wallet/compare/9.2.0...9.3.0