Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions .github/workflows/phpunits.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
php-versions: [8.3, 8.4, 8.5]
databases: [testing, pgsql, mysql, mariadb]
caches: [array, redis, memcached, database]
locks: [redis, memcached]
locks: [redis, memcached, database]

services:
redis:
Expand Down Expand Up @@ -145,7 +145,8 @@ jobs:

- name: Check coveralls
id: coveralls-check
run: echo "execute=${{ matrix.php-versions == '8.3' && matrix.caches == 'array' && matrix.locks == 'redis' && matrix.databases == 'testing' }}" >> $GITHUB_OUTPUT
run: |
echo "execute=true" >> "$GITHUB_OUTPUT"

- name: Prepare run test suite
id: unit-prepare
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ build/
node_modules/
.deptrac.cache
.phpunit.cache/
PLAN.md
READMAP.md
12 changes: 12 additions & 0 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@
/**
* The driver for the cache.
*
* Note: When using PostgreSQL with 'database' lock driver, the package
* automatically forces 'array' cache driver. This is CRITICAL because:
* 1. Before locking, balance MUST be read from DB with FOR UPDATE
* 2. This balance is synced to StorageService (state transaction) via multiSync()
* 3. External cache (database, redis, memcached) would be redundant and could cause inconsistencies
* 4. Array cache ensures balance is always fresh from DB within transaction
*
* @var string
*/
'driver' => env('WALLET_CACHE_DRIVER', 'array'),
Expand Down Expand Up @@ -114,6 +121,11 @@
* - memcached
* - database
*
* When using 'database' driver with PostgreSQL, the package automatically
* uses PostgreSQL-specific row-level locks (SELECT ... FOR UPDATE) for
* better performance and consistency. For other databases, standard
* Laravel database locks are used.
*
* @var string
*/
'driver' => env('WALLET_LOCK_DRIVER', 'array'),
Expand Down
23 changes: 23 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
version: '3.8'

services:
postgres:
image: postgres:15-alpine
container_name: laravel-wallet-postgres
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: wallet
POSTGRES_DB: wallet
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U root"]
interval: 5s
timeout: 3s
retries: 5

volumes:
postgres_data:

2 changes: 1 addition & 1 deletion docs/guide/db/atomic-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ What's going on here?
We block the wallet and raise the ad in the transaction (yes, atomic immediately starts the transaction - this is the main difference from LockServiceInterface).
We raise the ad and deduct the amount from the wallet. If there are not enough funds to raise the ad, the error will complete the atomic operation and the transaction will roll back, and the lock on the wallet will be removed.

There is also an opportunity to block a lot of wallets. The operation is expensive, it generates N requests to the lock service. Maybe I'll optimize it in the future, but that's not for sure.
There is also an opportunity to block a lot of wallets. When using PostgreSQL with `lock.driver = 'database'`, the operation is optimized: all wallets are locked in a single database query (`SELECT ... FOR UPDATE`), significantly improving performance compared to multiple individual lock requests.

---

Expand Down
35 changes: 35 additions & 0 deletions docs/guide/db/race-condition.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,39 @@ 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.

## PostgreSQL Row-Level Locks

When using PostgreSQL with `lock.driver = 'database'`, the package automatically uses PostgreSQL-specific row-level locks (`SELECT ... FOR UPDATE`) for optimal performance and data consistency.

### Benefits

- **Database-level locking**: Locks are managed directly by PostgreSQL, ensuring true atomicity
- **Better performance**: Single query locks multiple wallets at once, reducing database round trips
- **Automatic cache management**: The package automatically forces `array` cache driver when using PostgreSQL locks, as database-level locks ensure consistency without external cache synchronization

### How It Works

When you configure:
```php
'lock' => [
'driver' => 'database',
],
```

And your database connection is PostgreSQL, the package automatically:
1. Uses `PostgresLockService` instead of standard `LockService`
2. Locks wallets using `SELECT ... FOR UPDATE` at the database level
3. Forces `array` cache driver for optimal performance (external cache becomes redundant)

### Important Notes

- **Automatic selection**: No additional configuration needed - works automatically when `lock.driver = 'database'` and database is PostgreSQL
- **Array cache**: When using PostgreSQL locks, the package automatically forces `array` cache driver. This is **CRITICAL** because:
- Before locking, balance **MUST** be read from DB with `FOR UPDATE`
- This balance is synced to StorageService (state transaction) via `multiSync()`
- External cache (database, redis, memcached) would be redundant and could cause inconsistencies
- Array cache ensures balance is always fresh from DB within transaction
- **Other databases**: For non-PostgreSQL databases, standard Laravel database locks are used
- **Backward compatible**: All existing code continues to work without changes

It's simple!
Loading
Loading