Skip to content

Commit 141da0c

Browse files
committed
start of interface rework, some bug fixes
1 parent 1564b78 commit 141da0c

File tree

11 files changed

+68
-82
lines changed

11 files changed

+68
-82
lines changed
Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* An airlock controls how many users can access a resource simultaneously,
1313
* queuing additional users until a slot becomes available.
1414
*/
15-
interface AirlockInterface
15+
interface Airlock
1616
{
1717
/**
1818
* Attempt to enter the airlock.
@@ -34,25 +34,6 @@ public function enter(string $identifier, int $priority = 0): EntryResult;
3434
*/
3535
public function leave(string $identifier): void;
3636

37-
/**
38-
* Release an acquired slot and notify the next queued user.
39-
*
40-
* Call this when the user finishes using the resource or their session ends.
41-
*
42-
* @param SealToken $token The access token received from enter()
43-
*/
44-
public function release(SealToken $token): void;
45-
46-
/**
47-
* Extend the lease on an acquired slot.
48-
*
49-
* @param SealToken $token The current access token
50-
* @param float|null $ttlInSeconds New TTL, or null to use the default
51-
*
52-
* @return SealToken|null New token if refreshed, null if the lease expired
53-
*/
54-
public function refresh(SealToken $token, ?float $ttlInSeconds = null): ?SealToken;
55-
5637
/**
5738
* Get the current queue position for a waiting user.
5839
*

src/Bridge/Symfony/Mercure/SymfonyMercureHubFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Clegginabox\Airlock\Bridge\Symfony\Mercure;
66

7-
use Clegginabox\Airlock\AirlockInterface;
7+
use Clegginabox\Airlock\Airlock;
88
use Symfony\Component\Mercure\Hub;
99
use Symfony\Component\Mercure\HubInterface;
1010
use Symfony\Component\Mercure\Jwt\FactoryTokenProvider;
@@ -29,7 +29,7 @@ public static function create(string $hubUrl, string $jwtSecret): HubInterface
2929
public static function createForAirlock(
3030
string $hubUrl,
3131
string $jwtSecret,
32-
AirlockInterface $airlock,
32+
Airlock $airlock,
3333
string $identifier,
3434
): HubInterface {
3535
$topic = $airlock->getTopic($identifier);

src/Bridge/Symfony/Seal/SymfonyLockSeal.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
namespace Clegginabox\Airlock\Bridge\Symfony\Seal;
66

77
use Clegginabox\Airlock\Exception\LeaseExpiredException;
8+
use Clegginabox\Airlock\Exception\SealReleasingException;
89
use Clegginabox\Airlock\Seal\RefreshableSeal;
910
use Clegginabox\Airlock\Seal\ReleasableSeal;
1011
use Clegginabox\Airlock\Seal\Seal;
1112
use Clegginabox\Airlock\Seal\SealToken;
13+
use Symfony\Component\Lock\Exception\LockReleasingException;
1214
use Symfony\Component\Lock\Key;
1315
use Symfony\Component\Lock\LockFactory;
1416
use Symfony\Component\Lock\LockInterface;
@@ -44,11 +46,21 @@ public function tryAcquire(): ?SealToken
4446
public function release(SealToken $token): void
4547
{
4648
if (!$token instanceof SymfonyLockToken) {
47-
return;
49+
throw new SealReleasingException(
50+
sprintf('Invalid token type: %s. Expected %s', $token::class, SymfonyLockToken::class)
51+
);
4852
}
4953

50-
$lock = $this->factory->createLockFromKey($token->getKey());
51-
$lock->release();
54+
try {
55+
$lock = $this->factory->createLockFromKey($token->getKey());
56+
$lock->release();
57+
} catch (LockReleasingException $e) {
58+
throw new SealReleasingException(
59+
sprintf('Unable to release lock: %s', $e->getMessage()),
60+
$e->getCode(),
61+
$e
62+
);
63+
}
5264
}
5365

5466
public function refresh(SealToken $token, ?float $ttlInSeconds = null): SealToken

src/OpportunisticAirlock.php

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,16 @@
44

55
namespace Clegginabox\Airlock;
66

7-
use Clegginabox\Airlock\Seal\RefreshableSeal;
8-
use Clegginabox\Airlock\Seal\ReleasableSeal;
97
use Clegginabox\Airlock\Seal\Seal;
10-
use Clegginabox\Airlock\Seal\SealToken;
118

129
/**
1310
* A waiting room implementation that uses polling to determine admission.
1411
* The first user to acquire the semaphore is admitted.
1512
*/
16-
final readonly class OpportunisticAirlock implements AirlockInterface
13+
final readonly class OpportunisticAirlock implements Airlock
1714
{
1815
public function __construct(
19-
private Seal&ReleasableSeal&RefreshableSeal $seal,
16+
private Seal $seal,
2017
) {
2118
}
2219

@@ -35,16 +32,6 @@ public function leave(string $identifier): void
3532
// Nothing to do here
3633
}
3734

38-
public function release(SealToken $token): void
39-
{
40-
$this->seal->release($token);
41-
}
42-
43-
public function refresh(SealToken $token, ?float $ttlInSeconds = null): SealToken
44-
{
45-
return $this->seal->refresh($token, $ttlInSeconds);
46-
}
47-
4835
public function getPosition(string $identifier): ?int
4936
{
5037
return null;

src/QueueAirlock.php

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66

77
use Clegginabox\Airlock\Notifier\AirlockNotifierInterface;
88
use Clegginabox\Airlock\Queue\QueueInterface;
9-
use Clegginabox\Airlock\Seal\RefreshableSeal;
109
use Clegginabox\Airlock\Seal\ReleasableSeal;
1110
use Clegginabox\Airlock\Seal\Seal;
1211
use Clegginabox\Airlock\Seal\SealToken;
1312

14-
final readonly class QueueAirlock implements AirlockInterface
13+
final readonly class QueueAirlock implements Airlock, ReleasingAirlock
1514
{
1615
public function __construct(
17-
private Seal&ReleasableSeal&RefreshableSeal $seal,
16+
private Seal&ReleasableSeal $seal,
1817
private QueueInterface $queue,
1918
private AirlockNotifierInterface $notifier,
2019
private string $topicPrefix = '/waiting-room',
@@ -29,7 +28,7 @@ public function enter(string $identifier, int $priority = 0): EntryResult
2928
$position = $this->queue->add($identifier, $priority);
3029

3130
// 2. Are we at the front of the line? (Position 1)
32-
if ($position > 1) {
31+
if ($position !== 1) {
3332
// No? Wait your turn.
3433
return EntryResult::queued($position, $this->topicFor($identifier));
3534
}
@@ -67,11 +66,6 @@ public function release(SealToken $token): void
6766
$this->notifier->notify($nextPassenger, $this->topicFor($nextPassenger));
6867
}
6968

70-
public function refresh(SealToken $token, ?float $ttlInSeconds = null): SealToken
71-
{
72-
return $this->seal->refresh($token, $ttlInSeconds);
73-
}
74-
7569
public function getPosition(string $identifier): ?int
7670
{
7771
return $this->queue->getPosition($identifier);

src/RefreshingAirlock.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Clegginabox\Airlock;
6+
7+
use Clegginabox\Airlock\Seal\SealToken;
8+
9+
interface RefreshingAirlock
10+
{
11+
/**
12+
* Extend the lease on an acquired slot.
13+
*
14+
* @param SealToken $token The current access token
15+
* @param float|null $ttlInSeconds New TTL, or null to use the default
16+
*
17+
* @return SealToken|null New token if refreshed, null if the lease expired
18+
*/
19+
public function refresh(SealToken $token, ?float $ttlInSeconds = null): ?SealToken;
20+
}

src/ReleasingAirlock.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Clegginabox\Airlock;
6+
7+
use Clegginabox\Airlock\Seal\SealToken;
8+
9+
interface ReleasingAirlock
10+
{
11+
/**
12+
* Release an acquired slot and notify the next queued user.
13+
*
14+
* Call this when the user finishes using the resource or their session ends.
15+
*
16+
* @param SealToken $token The access token received from enter()
17+
*/
18+
public function release(SealToken $token): void;
19+
}

tests/Unit/Bridge/Symfony/Mercure/SymfonyMercureHubFactoryTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Clegginabox\Airlock\Tests\Unit\Bridge\Symfony\Mercure;
66

7-
use Clegginabox\Airlock\AirlockInterface;
7+
use Clegginabox\Airlock\Airlock;
88
use Clegginabox\Airlock\Bridge\Symfony\Mercure\SymfonyMercureHubFactory;
99
use JsonException;
1010
use PHPUnit\Framework\TestCase;
@@ -23,7 +23,7 @@ public function testCreateReturnsHub(): void
2323

2424
public function testCreateForAirlockScopesJwtToTopic(): void
2525
{
26-
$airlock = $this->createMock(AirlockInterface::class);
26+
$airlock = $this->createMock(Airlock::class);
2727
$airlock->expects($this->once())
2828
->method('getTopic')
2929
->with('user-123')

tests/Unit/Bridge/Symfony/Seal/SymfonyLockSealTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Clegginabox\Airlock\Bridge\Symfony\Seal\SymfonyLockSeal;
88
use Clegginabox\Airlock\Bridge\Symfony\Seal\SymfonyLockToken;
99
use Clegginabox\Airlock\Exception\LeaseExpiredException;
10+
use Clegginabox\Airlock\Exception\SealReleasingException;
1011
use Clegginabox\Airlock\Seal\SealToken;
1112
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
1213
use PHPUnit\Framework\MockObject\MockObject;
@@ -98,13 +99,14 @@ public function testItReleasesLock(): void
9899
}
99100

100101
#[AllowMockObjectsWithoutExpectations]
101-
public function testReleaseWithInvalidTokenTypeDoesNothing(): void
102+
public function testReleaseWithInvalidTokenThrows(): void
102103
{
103104
$invalidToken = $this->createMock(SealToken::class);
104105

105106
$this->mockFactory->expects($this->never())
106107
->method('createLockFromKey');
107108

109+
$this->expectException(SealReleasingException::class);
108110
$this->seal->release($invalidToken);
109111
}
110112

tests/Unit/OpportunisticAirlockTest.php

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,24 +82,6 @@ public function testEnterWhenQueued(): void
8282
$this->assertEquals(-1, $entryResult->getPosition());
8383
}
8484

85-
public function testRelease(): void
86-
{
87-
$this->mockSeal->expects($this->once())
88-
->method('release')
89-
->with($this->mockSealToken);
90-
91-
$this->airlock->release($this->mockSealToken);
92-
}
93-
94-
public function testRefresh(): void
95-
{
96-
$this->mockSeal->expects($this->once())
97-
->method('refresh')
98-
->with($this->mockSealToken);
99-
100-
$this->airlock->refresh($this->mockSealToken);
101-
}
102-
10385
#[AllowMockObjectsWithoutExpectations]
10486
public function testLeave(): void
10587
{

0 commit comments

Comments
 (0)