Skip to content

Commit 78e5684

Browse files
committed
Add tryLock method
1 parent c2839b5 commit 78e5684

File tree

9 files changed

+154
-36
lines changed

9 files changed

+154
-36
lines changed

src/Driver/BlockingFile.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ public function lock(LockType $type, ?Cancellation $cancellation = null): void
7373
$this->lockMode = $type;
7474
}
7575

76+
public function tryLock(LockType $type): bool
77+
{
78+
$locked = Internal\tryLock($this->path, $this->getFileHandle(), $type);
79+
if ($locked) {
80+
$this->lockMode = $type;
81+
}
82+
83+
return $locked;
84+
}
85+
7686
public function unlock(): void
7787
{
7888
Internal\unlock($this->path, $this->getFileHandle());

src/Driver/ParallelFile.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,16 @@ public function lock(LockType $type, ?Cancellation $cancellation = null): void
152152
$this->lockMode = $type;
153153
}
154154

155+
public function tryLock(LockType $type): bool
156+
{
157+
$locked = $this->flock('try-lock', $type);
158+
if ($locked) {
159+
$this->lockMode = $type;
160+
}
161+
162+
return $locked;
163+
}
164+
155165
public function unlock(): void
156166
{
157167
$this->flock('unlock');
@@ -163,7 +173,7 @@ public function getLockType(): ?LockType
163173
return $this->lockMode;
164174
}
165175

166-
private function flock(string $action, ?LockType $type = null, ?Cancellation $cancellation = null): void
176+
private function flock(string $action, ?LockType $type = null, ?Cancellation $cancellation = null): bool
167177
{
168178
if ($this->id === null) {
169179
throw new ClosedException("The file has been closed");
@@ -172,7 +182,7 @@ private function flock(string $action, ?LockType $type = null, ?Cancellation $ca
172182
$this->busy = true;
173183

174184
try {
175-
$this->worker->execute(new Internal\FileTask('flock', [$type, $action], $this->id), $cancellation);
185+
return $this->worker->execute(new Internal\FileTask('flock', [$type, $action], $this->id), $cancellation);
176186
} catch (TaskFailureException $exception) {
177187
throw new StreamException("Attempting to lock the file failed", 0, $exception);
178188
} catch (WorkerException $exception) {

src/Driver/StatusCachingFile.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ public function lock(LockType $type, ?Cancellation $cancellation = null): void
5959
$this->file->lock($type, $cancellation);
6060
}
6161

62+
public function tryLock(LockType $type): bool
63+
{
64+
return $this->file->tryLock($type);
65+
}
66+
6267
public function unlock(): void
6368
{
6469
$this->file->unlock();

src/File.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Amp\ByteStream\ReadableStream;
77
use Amp\ByteStream\WritableStream;
88
use Amp\Cancellation;
9-
use Amp\Sync\Lock;
109

1110
interface File extends ReadableStream, WritableStream
1211
{
@@ -58,13 +57,23 @@ public function getMode(): string;
5857
public function truncate(int $size): void;
5958

6059
/**
61-
* Non-blocking method to obtain a shared or exclusive lock on the file.
60+
* Non-blocking method to obtain a shared or exclusive lock on the file. This method must only return once
61+
* the lock has been obtained. Use {@see tryLock()} to make a single attempt to get the lock.
6262
*
6363
* @throws FilesystemException If there is an error when attempting to lock the file.
6464
* @throws ClosedException If the file has been closed.
6565
*/
6666
public function lock(LockType $type, ?Cancellation $cancellation = null): void;
6767

68+
/**
69+
* Make a single non-blocking attempt to obtain a shared or exclusive lock on the file. Returns true if the lock
70+
* was obtained, otherwise false. Use {@see lock()} to return only once the lock is obtained.
71+
*
72+
* @throws FilesystemException If there is an error when attempting to lock the file.
73+
* @throws ClosedException If the file has been closed.
74+
*/
75+
public function tryLock(LockType $type): bool;
76+
6877
/**
6978
* @throws FilesystemException If there is an error when attempting to unlock the file.
7079
* @throws ClosedException If the file has been closed.

src/Internal/FileTask.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,20 @@ public function run(Channel $channel, Cancellation $cancellation): mixed
106106
switch ($action) {
107107
case 'lock':
108108
$file->lock($type, $cancellation);
109-
break;
109+
return true;
110+
111+
case 'try-lock':
112+
return $file->tryLock($type);
110113

111114
case 'unlock':
112115
$file->unlock();
113-
break;
116+
return true;
114117

115118
default:
116119
throw new \Error("Invalid lock action - " . $action);
117120
}
118121

119-
return null;
122+
return false; // CS fixer fails without this return.
120123

121124
default:
122125
throw new \Error('Invalid operation');

src/Internal/QueuedWritesFile.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,23 @@ abstract protected function getFileHandle();
133133

134134
public function lock(LockType $type, ?Cancellation $cancellation = null): void
135135
{
136-
lock($this->getPath(), $this->getFileHandle(), $type, $cancellation);
136+
lock($this->path, $this->getFileHandle(), $type, $cancellation);
137137
$this->lockMode = $type;
138138
}
139139

140+
public function tryLock(LockType $type): bool
141+
{
142+
$locked = tryLock($this->path, $this->getFileHandle(), $type);
143+
if ($locked) {
144+
$this->lockMode = $type;
145+
}
146+
147+
return $locked;
148+
}
149+
140150
public function unlock(): void
141151
{
142-
unlock($this->getPath(), $this->getFileHandle());
152+
unlock($this->path, $this->getFileHandle());
143153
$this->lockMode = null;
144154
}
145155

src/Internal/functions.php

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,57 @@
1616
*/
1717
function lock(string $path, $handle, LockType $type, ?Cancellation $cancellation): void
1818
{
19-
static $latencyTimeout = 0.01;
20-
static $delayLimit = 1;
19+
for ($attempt = 0; true; ++$attempt) {
20+
if (tryLock($path, $handle, $type)) {
21+
return;
22+
}
2123

22-
$error = null;
23-
$errorHandler = static function (int $type, string $message) use (&$error): bool {
24-
$error = $message;
25-
return true;
26-
};
24+
// Exponential back-off with a maximum delay of 1 second.
25+
delay(\min(1, 0.01 * (2 ** $attempt)), cancellation: $cancellation);
26+
}
27+
}
2728

29+
/**
30+
* @internal
31+
*
32+
* @param resource $handle
33+
*
34+
* @throws FilesystemException
35+
*/
36+
function tryLock(string $path, $handle, LockType $type): bool
37+
{
2838
$flags = \LOCK_NB | match ($type) {
2939
LockType::Shared => \LOCK_SH,
3040
LockType::Exclusive => \LOCK_EX,
3141
};
3242

33-
for ($attempt = 0; true; ++$attempt) {
34-
\set_error_handler($errorHandler);
35-
try {
36-
$lock = \flock($handle, $flags, $wouldBlock);
37-
} finally {
38-
\restore_error_handler();
39-
}
43+
$error = null;
44+
\set_error_handler(static function (int $type, string $message) use (&$error): bool {
45+
$error = $message;
46+
return true;
47+
});
4048

41-
if ($lock) {
42-
return;
43-
}
49+
try {
50+
$lock = \flock($handle, $flags, $wouldBlock);
51+
} finally {
52+
\restore_error_handler();
53+
}
4454

45-
if (!$wouldBlock) {
46-
throw new FilesystemException(
47-
\sprintf(
48-
'Error attempting to lock file at "%s": %s',
49-
$path,
50-
$error ?? 'Unknown error',
51-
)
52-
);
53-
}
55+
if ($lock) {
56+
return true;
57+
}
5458

55-
delay(\min($delayLimit, $latencyTimeout * (2 ** $attempt)), cancellation: $cancellation);
59+
if (!$wouldBlock) {
60+
throw new FilesystemException(
61+
\sprintf(
62+
'Error attempting to lock file at "%s": %s',
63+
$path,
64+
$error ?? 'Unknown error',
65+
)
66+
);
5667
}
68+
69+
return false;
5770
}
5871

5972
/**

test/AsyncFileTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Amp\File\PendingOperationError;
1010
use Revolt\EventLoop;
1111
use function Amp\async;
12+
use function Amp\delay;
1213

1314
abstract class AsyncFileTest extends FileTest
1415
{
@@ -128,4 +129,30 @@ public function testSimultaneousLock(): void
128129
$handle1->close();
129130
$handle2->close();
130131
}
132+
133+
public function testTryLockLoop(): void
134+
{
135+
$this->setMinimumRuntime(0.1);
136+
$this->setTimeout(0.3);
137+
138+
$path = Fixture::path() . "/lock";
139+
$handle1 = $this->driver->openFile($path, "c+");
140+
$handle2 = $this->driver->openFile($path, "c+");
141+
142+
self::assertTrue($handle1->tryLock(LockType::Exclusive));
143+
self::assertSame(LockType::Exclusive, $handle1->getLockType());
144+
145+
EventLoop::delay(0.1, $handle1->unlock(...));
146+
147+
$future = async(function () use ($handle2): void {
148+
while (!$handle2->tryLock(LockType::Exclusive)) {
149+
delay(0.1);
150+
}
151+
});
152+
153+
$future->await();
154+
155+
$handle1->close();
156+
$handle2->close();
157+
}
131158
}

test/FileTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,37 @@ public function testUnlockAfterClose(): void
354354
$handle->unlock();
355355
}
356356

357+
public function testTryLock(): void
358+
{
359+
$path = Fixture::path() . "/lock";
360+
$handle1 = $this->driver->openFile($path, "c+");
361+
$handle2 = $this->driver->openFile($path, "c+");
362+
363+
self::assertTrue($handle1->tryLock(LockType::Exclusive));
364+
self::assertSame(LockType::Exclusive, $handle1->getLockType());
365+
366+
self::assertFalse($handle2->tryLock(LockType::Exclusive));
367+
self::assertNull($handle2->getLockType());
368+
369+
$handle1->unlock();
370+
self::assertNull($handle1->getLockType());
371+
372+
self::assertTrue($handle2->tryLock(LockType::Shared));
373+
self::assertSame(LockType::Shared, $handle2->getLockType());
374+
375+
self::assertTrue($handle1->tryLock(LockType::Shared));
376+
self::assertSame(LockType::Shared, $handle1->getLockType());
377+
378+
self::assertFalse($handle1->tryLock(LockType::Exclusive));
379+
self::assertSame(LockType::Shared, $handle1->getLockType());
380+
381+
$handle2->unlock();
382+
self::assertNull($handle2->getLockType());
383+
384+
self::assertTrue($handle1->tryLock(LockType::Exclusive));
385+
self::assertSame(LockType::Exclusive, $handle1->getLockType());
386+
}
387+
357388
abstract protected function createDriver(): File\FilesystemDriver;
358389

359390
protected function setUp(): void

0 commit comments

Comments
 (0)