Skip to content

[7.1] Transaction State #412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
477ba8d
[7.1] Transaction State
rez1dent3 Nov 28, 2021
7ba8a55
fix psalm.xml
rez1dent3 Nov 28, 2021
a9d8c06
removed an extra request to the storage
rez1dent3 Nov 28, 2021
fc78cbd
speed up the test by 15x for external storage
rez1dent3 Nov 28, 2021
e8bac3b
optimize addBalance
rez1dent3 Nov 28, 2021
f067822
fix units
rez1dent3 Nov 28, 2021
323eb7a
fix memory leak
rez1dent3 Nov 28, 2021
9a0505a
revert config.php
rez1dent3 Nov 28, 2021
388b9ba
replaced uniqueness with flags
rez1dent3 Nov 28, 2021
caf38b7
coverage up
rez1dent3 Nov 28, 2021
932c5bd
update docs
rez1dent3 Nov 28, 2021
2cf1567
add unit-test
rez1dent3 Nov 28, 2021
45868cb
update race-condition.md
rez1dent3 Nov 28, 2021
0e0b5dd
update units
rez1dent3 Nov 28, 2021
f4d6dca
ecs-fix
rez1dent3 Nov 28, 2021
d6e7a8d
update psalm/phpstan rules
rez1dent3 Nov 28, 2021
ac493e9
micro-optimize
rez1dent3 Nov 28, 2021
a99f2ad
big performance optimization for transactions
rez1dent3 Nov 28, 2021
7336321
drop addBalance
rez1dent3 Nov 28, 2021
60105a9
update unit-test
rez1dent3 Nov 28, 2021
8537c44
optimize refreshBalance
rez1dent3 Nov 28, 2021
f6cbed5
bug-fix
rez1dent3 Nov 29, 2021
8ecf1b6
Redesigned service regulator & drop WalletServiceLegacy
rez1dent3 Nov 29, 2021
1c4cd46
update units
rez1dent3 Nov 29, 2021
e898178
update units
rez1dent3 Nov 29, 2021
c582c4b
add unit
rez1dent3 Nov 29, 2021
976e03c
update units
rez1dent3 Nov 29, 2021
ad51d84
fix phpstan
rez1dent3 Nov 29, 2021
ad955c5
drop uniq from migrations
rez1dent3 Nov 29, 2021
03de829
update name test
rez1dent3 Nov 29, 2021
e2eca59
update changelog.md
rez1dent3 Dec 3, 2021
b59ffa6
composer.suggest
rez1dent3 Dec 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .phpstorm.meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
use Bavix\Wallet\Services\CommonServiceLegacy;
use Bavix\Wallet\Services\MetaServiceLegacy;
use Bavix\Wallet\Services\TaxServiceInterface;
use Bavix\Wallet\Services\WalletServiceLegacy;

override(\app(0), map([
// internal.assembler
Expand Down Expand Up @@ -93,7 +92,6 @@
// lagacy.services
CommonServiceLegacy::class => CommonServiceLegacy::class,
MetaServiceLegacy::class => MetaServiceLegacy::class,
WalletServiceLegacy::class => WalletServiceLegacy::class,
]));

}
10 changes: 10 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Transaction support.
- Now, within the transaction, the wallet has its own balance state.

### Updated
- Due to the state within transactions, I was able to speed up the computation up to 25 times for complex transfers.

### Removed
- class `WalletServiceLegacy`

## [7.0.0] - 2021-11-25
### Updated
- Optimization of the `payFreeCart` and `payFree` request. Now the package does not update the repository. But there is no point in updating it, because the client does not pay anything.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"vimeo/psalm": "^4.12"
},
"suggest": {
"bavix/laravel-wallet-swap": "Addition to the laravel-wallet library for quick setting of exchange rates"
"bavix/laravel-wallet-swap": "Addition to the laravel-wallet library for quick setting of exchange rates",
"bavix/laravel-wallet-warmup": "Addition to the laravel-wallet library for refresh balance wallets"
},
"autoload": {
"psr-4": {
Expand Down
2 changes: 2 additions & 0 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Bavix\Wallet\Services\ExchangeService;
use Bavix\Wallet\Services\PrepareService;
use Bavix\Wallet\Services\PurchaseService;
use Bavix\Wallet\Services\RegulatorService;
use Bavix\Wallet\Services\TaxService;

return [
Expand Down Expand Up @@ -78,6 +79,7 @@
'atomic' => AtomicService::class,
'basket' => BasketService::class,
'bookkeeper' => BookkeeperService::class,
'regulator' => RegulatorService::class,
'cast' => CastService::class,
'consistency' => ConsistencyService::class,
'discount' => DiscountService::class,
Expand Down
7 changes: 6 additions & 1 deletion docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@
- [Refund](refund)
- [Gift](gift)
- [Cart](cart)


- Transactions

- [Transaction](transaction)
- [Race condition](race-condition)

- Additions

- [Wallet Swap](laravel-wallet-swap)
37 changes: 37 additions & 0 deletions docs/race-condition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Race Condition

A common issue in the issue is about race conditions.

If you have not yet imported the config into the project, then you need to do this.
```bash
php artisan vendor:publish --tag=laravel-wallet-config
```

Previously, there was a vacuum package, but now it is a part of the core. You just need to configure the lock service and the cache service in the package configuration `wallet.php`.

```php
/**
* A system for dealing with race conditions.
*/
'lock' => [
'driver' => 'array',
'seconds' => 1,
],
```

To enable the fight against race conditions, you need to select a provider that supports work with locks. I recommend `redis`.

There is a setting for storing the state of the wallet, I recommend choosing `redis` here too.

```php
/**
* Storage of the state of the balance of wallets.
*/
'cache' => ['driver' => 'array'],
```

You need `redis-server` and `php-redis`.

Redis is recommended but not required. You can choose whatever the [framework](https://laravel.com/docs/8.x/cache#introduction) offers you.

It worked!
37 changes: 37 additions & 0 deletions docs/transaction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Transaction

Sometimes you need to execute many simple queries. You want to keep the data atomic. To do this, you need `laravel-wallet` v7.1+.

It is necessary to write off the amount from the balance and raise the ad in the search. What happens if the service for raising an ad fails? We wrote off the money, but did not raise the ad. Received reputational losses. We can imagine the opposite situation, we first raise the ad in the search, but it does not work to write off the money. There are not enough funds. This functionality will help to solve all this. We monitor ONLY the state of the wallet, the rest falls on the developer. Let's take an unsuccessful lift, for example.

```php
use Bavix\Wallet\Internal\Service\DatabaseServiceInterface;

/** @var object $businessLogicService */
/** @var \Bavix\Wallet\Models\Wallet $payer */
$payer->balanceInt; // 9999
app(DatabaseServiceInterface::class)->transaction(statuc function () use ($payer) {
$payer->withdraw(1000); // 8999
$businessLogicService->doingMagic($payer); // throws an exception
}); // rollback payer balance

$payer->balanceInt; // 9999
```

Now let's look at the successful raising of the ad.

```php
use Bavix\Wallet\Internal\Service\DatabaseServiceInterface;

/** @var object $businessLogicService */
/** @var \Bavix\Wallet\Models\Wallet $payer */
$payer->balanceInt; // 9999
app(DatabaseServiceInterface::class)->transaction(statuc function () use ($payer) {
$payer->withdraw(1000); // 8999
$businessLogicService->doingMagic($payer); // successfully
}); // commit payer balance

$payer->balanceInt; // 8999
```

It worked!
2 changes: 2 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Rector\Core\Configuration\Option;
use Rector\Laravel\Set\LaravelSetList;
use Rector\Php74\Rector\Property\TypedPropertyRector;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Set\ValueObject\SetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

Expand All @@ -17,6 +18,7 @@
]);

// Define what rule sets will be applied
$containerConfigurator->import(PHPUnitSetList::PHPUNIT_91);
$containerConfigurator->import(LaravelSetList::LARAVEL_60);
$containerConfigurator->import(SetList::DEAD_CODE);
$containerConfigurator->import(SetList::PHP_74);
Expand Down
22 changes: 20 additions & 2 deletions src/Internal/Service/DatabaseService.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Bavix\Wallet\Internal\Exceptions\ExceptionInterface;
use Bavix\Wallet\Internal\Exceptions\TransactionFailedException;
use Closure;
use Bavix\Wallet\Services\RegulatorServiceInterface;
use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\ConnectionResolverInterface;
Expand All @@ -15,12 +15,15 @@

final class DatabaseService implements DatabaseServiceInterface
{
private RegulatorServiceInterface $regulatorService;
private ConnectionInterface $connection;

public function __construct(
ConnectionResolverInterface $connectionResolver,
RegulatorServiceInterface $regulatorService,
ConfigRepository $config
) {
$this->regulatorService = $regulatorService;
$this->connection = $connectionResolver->connection(
$config->get('wallet.database.connection')
);
Expand All @@ -40,10 +43,25 @@ public function transaction(callable $callback)
return $callback();
}

return $this->connection->transaction(Closure::fromCallable($callback));
$this->regulatorService->purge();

return $this->connection->transaction(function () use ($callback) {
$result = $callback();
if ($result === false || (is_countable($result) && count($result) === 0)) {
$this->regulatorService->purge();
} else {
$this->regulatorService->approve();
}

return $result;
});
} catch (RecordsNotFoundException|ExceptionInterface $exception) {
$this->regulatorService->purge();

throw $exception;
} catch (Throwable $throwable) {
$this->regulatorService->purge();

throw new TransactionFailedException(
'Transaction failed',
ExceptionInterface::TRANSACTION_FAILED,
Expand Down
22 changes: 9 additions & 13 deletions src/Internal/Service/StorageService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,38 @@
use Bavix\Wallet\Internal\Exceptions\ExceptionInterface;
use Bavix\Wallet\Internal\Exceptions\LockProviderNotFoundException;
use Bavix\Wallet\Internal\Exceptions\RecordNotFoundException;
use Illuminate\Cache\CacheManager;
use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Cache\Repository as CacheRepository;

final class StorageService implements StorageServiceInterface
{
private LockServiceInterface $lockService;
private MathServiceInterface $mathService;
private CacheRepository $cache;
private CacheRepository $cacheRepository;

public function __construct(
CacheManager $cacheManager,
ConfigRepository $config,
LockServiceInterface $lockService,
MathServiceInterface $mathService
MathServiceInterface $mathService,
CacheRepository $cacheRepository
) {
$this->cacheRepository = $cacheRepository;
$this->mathService = $mathService;
$this->lockService = $lockService;
$this->cache = $cacheManager->driver(
$config->get('wallet.cache.driver', 'array')
);
}

public function flush(): bool
{
return $this->cache->clear();
return $this->cacheRepository->clear();
}

public function missing(string $key): bool
{
return $this->cache->forget($key);
return $this->cacheRepository->forget($key);
}

/** @throws RecordNotFoundException */
public function get(string $key): string
{
$value = $this->cache->get($key);
$value = $this->cacheRepository->get($key);
if ($value === null) {
throw new RecordNotFoundException(
'The repository did not find the object',
Expand All @@ -54,9 +49,10 @@ public function get(string $key): string
return $this->mathService->round($value);
}

/** @param float|int|string $value */
public function sync(string $key, $value): bool
{
return $this->cache->set($key, $value);
return $this->cacheRepository->set($key, $value);
}

/**
Expand Down
31 changes: 25 additions & 6 deletions src/Models/Wallet.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
use Bavix\Wallet\Internal\Exceptions\ExceptionInterface;
use Bavix\Wallet\Internal\Exceptions\LockProviderNotFoundException;
use Bavix\Wallet\Internal\Exceptions\TransactionFailedException;
use Bavix\Wallet\Internal\Service\DatabaseServiceInterface;
use Bavix\Wallet\Services\WalletServiceLegacy;
use Bavix\Wallet\Internal\Service\MathServiceInterface;
use Bavix\Wallet\Services\AtomicServiceInterface;
use Bavix\Wallet\Services\RegulatorServiceInterface;
use Bavix\Wallet\Traits\CanConfirm;
use Bavix\Wallet\Traits\CanExchange;
use Bavix\Wallet\Traits\CanPayFloat;
Expand Down Expand Up @@ -107,9 +108,15 @@ public function setNameAttribute(string $name): void
*/
public function refreshBalance(): bool
{
return app(DatabaseServiceInterface::class)->transaction(
fn () => app(WalletServiceLegacy::class)->refresh($this)
);
return app(AtomicServiceInterface::class)->block($this, function () {
$whatIs = $this->balance;
$balance = $this->getAvailableBalanceAttribute();
if (app(MathServiceInterface::class)->compare($whatIs, $balance) === 0) {
return true;
}

return app(RegulatorServiceInterface::class)->sync($this, $balance);
});
}

/** @codeCoverageIgnore */
Expand All @@ -125,7 +132,7 @@ public function getOriginalBalanceAttribute(): string
/**
* @return float|int
*/
public function getAvailableBalance()
public function getAvailableBalanceAttribute()
{
return $this->transactions()
->where('wallet_id', $this->getKey())
Expand All @@ -134,6 +141,18 @@ public function getAvailableBalance()
;
}

/**
* @deprecated
* @see getAvailableBalanceAttribute
* @codeCoverageIgnore
*
* @return float|int
*/
public function getAvailableBalance()
{
return $this->getAvailableBalanceAttribute();
}

public function holder(): MorphTo
{
return $this->morphTo();
Expand Down
4 changes: 2 additions & 2 deletions src/Services/CastService.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public function getWallet(Wallet $object, bool $save = true): WalletModel
assert($wallet instanceof WalletModel);
}

if ($save) {
$wallet->exists or $wallet->save();
if ($save && !$wallet->exists) {
$wallet->save();
}

return $wallet;
Expand Down
Loading