Skip to content
Open
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/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ jobs:
strategy:
matrix:
php-version: ["8.3", "8.4", "8.5"]
redis-image: ["redis:7-alpine", "redis:8-alpine", "valkey/valkey:9-alpine"]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Build and Start Services
# Pass the matrix value as an environment variable to Docker Compose
run: PHP_VERSION=${{ matrix.php-version }} docker compose up redis -d --build
run: PHP_VERSION=${{ matrix.php-version }} REDIS_IMAGE=${{ matrix.redis-image }} docker compose up redis -d --build

- name: Unit Tests
run: |
make run-tests

- name: Stop Services
if: always()
run: PHP_VERSION=${{ matrix.php-version }} docker compose down
run: PHP_VERSION=${{ matrix.php-version }} REDIS_IMAGE=${{ matrix.redis-image }} docker compose down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ mago-format: # Run code formatting via mago.

.PHONY: run-tests
run-tests: # Run unit tests via PHPUnit.
docker compose run --rm php ./vendor/bin/phpunit --colors=always --configuration ./tests/phpunit.xml ./tests/Unit/
docker compose run --rm php ./vendor/bin/phpunit --colors=always --configuration ./tests/phpunit.xml ./tests/
86 changes: 79 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,28 @@ composer require mimatus/locksmith

## Roadmap
- [x] Basic in-memory & Redis semaphore implementation
- [x] Redlock algorithm for Redis semaphore
- [x] Predis support for Redis semaphore
- [x] AMPHP Redis client support for Redis semaphore
- [x] First class support and tests for Redis 7 | Redis 8 | Valkey 9
- [ ] Feedback and API stabilization
- [ ] Redis Cluster support
- [ ] Documentation improvements
- [ ] Redlock algorithm for Redis semaphore
- [ ] Predis support for Redis semaphore
- [ ] AMPHP Redis client support for Redis semaphore
- [ ] MySQL/MariaDB/PostgreSQL semaphore implementation

## Usage

> [!NOTE]
> Project is still in early stages of development, so API is not stable yet and may change. Feedback is very welcome to help shape the API and make it more intuitive and easy to use.

### In-Memory semaphore

For single-process scenarios you can use in-memory semaphore implementation. It allows to limit concurrent access to resource within single process (e.g., number of concurrent HTTP requests, background jobs, or other tasks).

It's suitable mainly for concurrent PHP - AMPHP, Swoole, ReactPHP, etc.

It's not suitable for multi-process scenarios (e.g., multiple PHP-FPM workers, multiple servers) as each process/server will have its own instance of in-memory semaphore. For multi-process scenarios you should use Redis-based semaphore implementation.

```php

$locksmith = new Locksmith(
Expand All @@ -41,11 +53,11 @@ $locksmith = new Locksmith(
$resource = new Resource(
namespace: 'test-resource', // Namespace/identifier for resource
version: 1, // Optional resource version
ttlNanoseconds: 1_000_000_000, //How long should be resource locked
);

$locked = $locksmith->locked(
$resource,
lockTTLNs: 1_000_000_000, // How long should be resource locked
maxLockWaitNs: 500_000_000, // How long to wait for lock acquisition - error if exceeded
minSuspensionDelayNs: 10_000 // Minimum delay between retries when lock acquisition fails
);
Expand All @@ -60,13 +72,26 @@ $locked(function (Closure $suspension): void {
```

### Redis semaphore

For distributed scenarios you can use Redis-based semaphore implementation.

Supported Redis servers:
- Redis 7+
- Valkey 9+

Supported Redis clients:
- PhpRedis
- Predis
- AMPHP Redis client

```php

$redis = new Redis();
$redis->connect('redis');
$phpRedisCleint = new PhpRedisClient($redis);

$semaphore = new RedisSemaphore(
redisClient: $redis,
redisClient: $phpRedisCleint,
maxConcurrentLocks: 3, // Max concurrent locks
);

Expand All @@ -76,11 +101,11 @@ $locksmith = new Locksmith(semaphore: $semaphore);
$resource = new Resource(
namespace: 'test-resource', // Namespace/identifier for resource
version: 1, // Optional resouce version
ttlNanoseconds: 1_000_000_000, //How long should be resource locked
);

$locked = $locksmith->locked(
$resource,
lockTTLNs: 1_000_000_000, // How long should be resource locked
maxLockWaitNs: 500_000_000, // How long to wait for lock acquisition - error if exceeded
minSuspensionDelayNs: 10_000 // Minimum delay between retries when lock acquisition fails
);
Expand All @@ -93,6 +118,53 @@ $locked(function (Closure $suspension): void {
// Lock is released after callback execution
```

### Distributed semaphore
Distributed semaphore allows to use multiple semaphore instances (e.g., multiple Redis instances) to achieve higher availability and fault tolerance. It uses quorum-based approach - single lock is successful only if the defined quorum of semaphores is reached.

Implementation of distributed semaphore is based on [Redlock algorithm](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/#the-redlock-algorithm) with some adjustments to fit the `Semaphore` interface and allow cooperative suspension points.

> [!NOTE]
> It's important to note that while distributed semaphore can be used Redis instances, it does not have first class support for Redis Cluster or Sentinel. First class support for Redis Cluster is on the roadmap, but in the meantime you can use distributed semaphore with multiple independent Redis instances as a workaround.

```php
$semaphores = new SemaphoreCollection([
new RedisSemaphore(
redisClient: $redisClient1,
maxConcurrentLocks: 3,
),
new RedisSemaphore(
redisClient: $redisClient2,
maxConcurrentLocks: 3,
),
new RedisSemaphore(
redisClient: $redisClient3,
maxConcurrentLocks: 3,
),
]);

$locksmith = new Locksmith(
semaphore: new DistributedSemaphore(
semaphores: $semaphores,
quorum: 2,
),
);
$resource = new Resource(
namespace: 'test-resource', // Namespace/identifier for resource
version: 1, // Optional resource version
);
$locked = $locksmith->locked(
$resource,
lockTTLNs: 1_000_000_000, // How long should be resource locked
maxLockWaitNs: 500_000_000, // How long to wait for lock acquisition - error if exceeded
minSuspensionDelayNs: 10_000 // Minimum delay between retries when lock acquisition fails
);
$locked(function (Closure $suspension): void {
// Critical section - code executed under lock

$suspension(); // Optional - cooperative suspension point to allow other lock acquisition attempts or allow lock TTL checks for long running processes
});
// Lock is released after callback execution
```
## Development

### Commits
Expand All @@ -115,7 +187,7 @@ mago-analyze: Run static analysis via mago.
mago-format: Run code formatting via mago.
mago-lint-fix: Run linting with auto-fix via mago.
mago-lint: Run linting via mago.
run-tests: Run unit tests via PHPUnit.
run-tests: Run tests via PHPUnit.
```

## License
Expand Down
9 changes: 7 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"php": ">=8.3"
},
"suggest": {
"ext-redis": "To use this library with the PHP Redis extension."
"ext-redis": "To use this library with the PHP Redis extension.",
"predis/predis": "To use this library with the Predis client.",
"amphp/redis": "To use this library with the AMPHP Redis client."
},
"autoload": {
"psr-4": {
Expand All @@ -20,6 +22,9 @@
},
"require-dev": {
"ext-redis": "*",
"phpunit/phpunit": "^12.5"
"phpunit/phpunit": "^12.5",
"amphp/redis": "^2.0",
"predis/predis": "^3.3",
"revolt/event-loop": "^1.0"
}
}
Loading