Skip to content

Commit 7ecc390

Browse files
authored
Merge pull request #83 from bavix/lock
Race Condition [Atomic Locks]
2 parents ef7135e + 98a1918 commit 7ecc390

17 files changed

+512
-190
lines changed

.scrutinizer.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ build:
33
environment:
44
php:
55
version: 7.2
6-
6+
pecl_extensions:
7+
- memcached
8+
memcached: true
9+
710
nodes:
811
analysis:
912
project_setup:

.travis.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ php:
66
- '7.4snapshot'
77
- 'nightly'
88

9+
services:
10+
- memcached
11+
912
matrix:
1013
allow_failures:
1114
- php: '7.4snapshot'
@@ -14,7 +17,7 @@ matrix:
1417
before_script:
1518
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
1619
- chmod +x ./cc-test-reporter
17-
- ./cc-test-reporter before-build
20+
- if [ $(phpenv version-name) = "7.3" ]; then ./cc-test-reporter before-build; fi
1821
- composer install
1922

2023
script:
@@ -23,4 +26,4 @@ script:
2326

2427
after_script:
2528
- cp ./build/logs/clover.xml clover.xml
26-
- ./cc-test-reporter after-build --coverage-input-type clover --exit-code $TRAVIS_TEST_RESULT
29+
- if [ $(phpenv version-name) = "7.3" ]; then ./cc-test-reporter after-build --coverage-input-type clover --exit-code $TRAVIS_TEST_RESULT; fi

config/config.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use Bavix\Wallet\Services\CommonService;
55
use Bavix\Wallet\Services\ProxyService;
66
use Bavix\Wallet\Services\WalletService;
7+
use Bavix\Wallet\Services\LockService;
78
use Bavix\Wallet\Models\Transaction;
89
use Bavix\Wallet\Models\Transfer;
910
use Bavix\Wallet\Models\Wallet;
@@ -18,6 +19,14 @@
1819
'rateable' => Rate::class,
1920
],
2021

22+
/**
23+
* Lock settings for highload projects
24+
*/
25+
'lock' => [
26+
'enabled' => false,
27+
'seconds' => 1,
28+
],
29+
2130
/**
2231
* Sometimes a slug may not match the currency and you need the ability to add an exception.
2332
* The main thing is that there are not many exceptions)
@@ -39,6 +48,7 @@
3948
'common' => CommonService::class,
4049
'proxy' => ProxyService::class,
4150
'wallet' => WalletService::class,
51+
'lock' => LockService::class,
4252
],
4353

4454
/**

src/Objects/EmptyLock.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace Bavix\Wallet\Objects;
4+
5+
use Illuminate\Contracts\Cache\Lock;
6+
use Illuminate\Support\Str;
7+
8+
class EmptyLock implements Lock
9+
{
10+
11+
/**
12+
* @var string
13+
*/
14+
protected $ownerId;
15+
16+
/**
17+
* Attempt to acquire the lock.
18+
*
19+
* @param callable|null $callback
20+
* @return mixed
21+
*/
22+
public function get($callback = null)
23+
{
24+
if ($callback === null) {
25+
return null;
26+
}
27+
28+
return $callback();
29+
}
30+
31+
/**
32+
* Attempt to acquire the lock for the given number of seconds.
33+
*
34+
* @param int $seconds
35+
* @param callable|null $callback
36+
* @return bool
37+
*/
38+
public function block($seconds, $callback = null): bool
39+
{
40+
return true;
41+
}
42+
43+
/**
44+
* Release the lock.
45+
*
46+
* @return void
47+
* @codeCoverageIgnore
48+
*/
49+
public function release(): void
50+
{
51+
// lock release
52+
}
53+
54+
/**
55+
* Returns the current owner of the lock.
56+
*
57+
* @return string
58+
*/
59+
public function owner(): string
60+
{
61+
if (!$this->ownerId) {
62+
$this->ownerId = Str::random();
63+
}
64+
65+
return $this->ownerId;
66+
}
67+
68+
/**
69+
* Releases this lock in disregard of ownership.
70+
*
71+
* @return void
72+
* @codeCoverageIgnore
73+
*/
74+
public function forceRelease(): void
75+
{
76+
// force lock release
77+
}
78+
79+
}

src/Services/CommonService.php

Lines changed: 101 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ class CommonService
2828
*/
2929
public function transfer(Wallet $from, Wallet $to, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
3030
{
31-
$this->verifyWithdraw($from, $amount);
32-
return $this->forceTransfer($from, $to, $amount, $meta, $status);
31+
return app(LockService::class)->lock($this, __FUNCTION__, function () use ($from, $to, $amount, $meta, $status) {
32+
$this->verifyWithdraw($from, $amount);
33+
return $this->forceTransfer($from, $to, $amount, $meta, $status);
34+
});
3335
}
3436

3537
/**
@@ -42,23 +44,25 @@ public function transfer(Wallet $from, Wallet $to, int $amount, ?array $meta = n
4244
*/
4345
public function forceTransfer(Wallet $from, Wallet $to, int $amount, ?array $meta = null, string $status = Transfer::STATUS_TRANSFER): Transfer
4446
{
45-
$fee = app(WalletService::class)->fee($to, $amount);
46-
$withdraw = $this->forceWithdraw($from, $amount + $fee, $meta);
47-
$deposit = $this->deposit($to, $amount, $meta);
48-
49-
$from = app(WalletService::class)
50-
->getWallet($from);
51-
52-
$transfers = $this->multiBrings([
53-
(new Bring())
54-
->setStatus($status)
55-
->setDeposit($deposit)
56-
->setWithdraw($withdraw)
57-
->setFrom($from)
58-
->setTo($to)
59-
]);
60-
61-
return current($transfers);
47+
return app(LockService::class)->lock($this, __FUNCTION__, function () use ($from, $to, $amount, $meta, $status) {
48+
$fee = app(WalletService::class)->fee($to, $amount);
49+
$withdraw = $this->forceWithdraw($from, $amount + $fee, $meta);
50+
$deposit = $this->deposit($to, $amount, $meta);
51+
52+
$from = app(WalletService::class)
53+
->getWallet($from);
54+
55+
$transfers = $this->multiBrings([
56+
(new Bring())
57+
->setStatus($status)
58+
->setDeposit($deposit)
59+
->setWithdraw($withdraw)
60+
->setFrom($from)
61+
->setTo($to)
62+
]);
63+
64+
return current($transfers);
65+
});
6266
}
6367

6468
/**
@@ -70,23 +74,25 @@ public function forceTransfer(Wallet $from, Wallet $to, int $amount, ?array $met
7074
*/
7175
public function forceWithdraw(Wallet $wallet, int $amount, ?array $meta, bool $confirmed = true): Transaction
7276
{
73-
$walletService = app(WalletService::class);
74-
$walletService->checkAmount($amount);
75-
76-
/**
77-
* @var WalletModel $wallet
78-
*/
79-
$wallet = $walletService->getWallet($wallet);
80-
81-
$transactions = $this->multiOperation($wallet, [
82-
(new Operation())
83-
->setType(Transaction::TYPE_WITHDRAW)
84-
->setConfirmed($confirmed)
85-
->setAmount(-$amount)
86-
->setMeta($meta)
87-
]);
88-
89-
return current($transactions);
77+
return app(LockService::class)->lock($this, __FUNCTION__, function () use ($wallet, $amount, $meta, $confirmed) {
78+
$walletService = app(WalletService::class);
79+
$walletService->checkAmount($amount);
80+
81+
/**
82+
* @var WalletModel $wallet
83+
*/
84+
$wallet = $walletService->getWallet($wallet);
85+
86+
$transactions = $this->multiOperation($wallet, [
87+
(new Operation())
88+
->setType(Transaction::TYPE_WITHDRAW)
89+
->setConfirmed($confirmed)
90+
->setAmount(-$amount)
91+
->setMeta($meta)
92+
]);
93+
94+
return current($transactions);
95+
});
9096
}
9197

9298
/**
@@ -98,23 +104,25 @@ public function forceWithdraw(Wallet $wallet, int $amount, ?array $meta, bool $c
98104
*/
99105
public function deposit(Wallet $wallet, int $amount, ?array $meta, bool $confirmed = true): Transaction
100106
{
101-
$walletService = app(WalletService::class);
102-
$walletService->checkAmount($amount);
103-
104-
/**
105-
* @var WalletModel $wallet
106-
*/
107-
$wallet = $walletService->getWallet($wallet);
108-
109-
$transactions = $this->multiOperation($wallet, [
110-
(new Operation())
111-
->setType(Transaction::TYPE_DEPOSIT)
112-
->setConfirmed($confirmed)
113-
->setAmount($amount)
114-
->setMeta($meta)
115-
]);
116-
117-
return current($transactions);
107+
return app(LockService::class)->lock($this, __FUNCTION__, function () use ($wallet, $amount, $meta, $confirmed) {
108+
$walletService = app(WalletService::class);
109+
$walletService->checkAmount($amount);
110+
111+
/**
112+
* @var WalletModel $wallet
113+
*/
114+
$wallet = $walletService->getWallet($wallet);
115+
116+
$transactions = $this->multiOperation($wallet, [
117+
(new Operation())
118+
->setType(Transaction::TYPE_DEPOSIT)
119+
->setConfirmed($confirmed)
120+
->setAmount($amount)
121+
->setMeta($meta)
122+
]);
123+
124+
return current($transactions);
125+
});
118126
}
119127

120128
/**
@@ -148,20 +156,22 @@ public function verifyWithdraw(Wallet $wallet, int $amount, bool $allowZero = nu
148156
*/
149157
public function multiOperation(Wallet $self, array $operations): array
150158
{
151-
$amount = 0;
152-
$objects = [];
153-
foreach ($operations as $operation) {
154-
if ($operation->isConfirmed()) {
155-
$amount += $operation->getAmount();
159+
return app(LockService::class)->lock($this, __FUNCTION__, function () use ($self, $operations) {
160+
$amount = 0;
161+
$objects = [];
162+
foreach ($operations as $operation) {
163+
if ($operation->isConfirmed()) {
164+
$amount += $operation->getAmount();
165+
}
166+
167+
$objects[] = $operation
168+
->setWallet($self)
169+
->create();
156170
}
157171

158-
$objects[] = $operation
159-
->setWallet($self)
160-
->create();
161-
}
162-
163-
$this->addBalance($self, $amount);
164-
return $objects;
172+
$this->addBalance($self, $amount);
173+
return $objects;
174+
});
165175
}
166176

167177
/**
@@ -173,9 +183,11 @@ public function multiOperation(Wallet $self, array $operations): array
173183
*/
174184
public function assemble(array $brings): array
175185
{
176-
$self = $this;
177-
return DB::transaction(static function() use ($self, $brings) {
178-
return $self->multiBrings($brings);
186+
return app(LockService::class)->lock($this, __FUNCTION__, function () use ($brings) {
187+
$self = $this;
188+
return DB::transaction(static function () use ($self, $brings) {
189+
return $self->multiBrings($brings);
190+
});
179191
});
180192
}
181193

@@ -187,12 +199,14 @@ public function assemble(array $brings): array
187199
*/
188200
public function multiBrings(array $brings): array
189201
{
190-
$objects = [];
191-
foreach ($brings as $bring) {
192-
$objects[] = $bring->create();
193-
}
202+
return app(LockService::class)->lock($this, __FUNCTION__, function () use ($brings) {
203+
$objects = [];
204+
foreach ($brings as $bring) {
205+
$objects[] = $bring->create();
206+
}
194207

195-
return $objects;
208+
return $objects;
209+
});
196210
}
197211

198212
/**
@@ -203,20 +217,22 @@ public function multiBrings(array $brings): array
203217
*/
204218
public function addBalance(Wallet $wallet, int $amount): bool
205219
{
206-
/**
207-
* @var ProxyService $proxy
208-
* @var WalletModel $wallet
209-
*/
210-
$proxy = app(ProxyService::class);
211-
$balance = $wallet->balance + $amount;
212-
if ($proxy->has($wallet->getKey())) {
213-
$balance = $proxy->get($wallet->getKey()) + $amount;
214-
}
220+
return app(LockService::class)->lock($this, __FUNCTION__, static function () use ($wallet, $amount) {
221+
/**
222+
* @var ProxyService $proxy
223+
* @var WalletModel $wallet
224+
*/
225+
$proxy = app(ProxyService::class);
226+
$balance = $wallet->balance + $amount;
227+
if ($proxy->has($wallet->getKey())) {
228+
$balance = $proxy->get($wallet->getKey()) + $amount;
229+
}
215230

216-
$result = $wallet->update(compact('balance'));
217-
$proxy->set($wallet->getKey(), $balance);
231+
$result = $wallet->update(compact('balance'));
232+
$proxy->set($wallet->getKey(), $balance);
218233

219-
return $result;
234+
return $result;
235+
});
220236
}
221237

222238
}

0 commit comments

Comments
 (0)