Skip to content

Commit 3fd576b

Browse files
committed
Add burn testing to CI
1 parent 7b9b2d9 commit 3fd576b

10 files changed

+103
-61
lines changed

.github/workflows/test-unit.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ jobs:
6666
matrix:
6767
php: ['7.4', '8.0', '8.1', '8.2', '8.3']
6868
type: ['Phpunit', 'Phpunit Lowest']
69+
include:
70+
- php: 'latest'
71+
type: 'Phpunit Burn'
6972
env:
7073
LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == '8.3' && matrix.type == 'Phpunit' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}"
7174
services:
@@ -108,12 +111,13 @@ jobs:
108111
109112
- name: Install PHP dependencies
110113
run: |
111-
if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "Phpunit Lowest" ]; then composer remove --no-interaction --no-update phpunit/phpunit ergebnis/phpunit-slow-test-detector --dev; fi
114+
if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "Phpunit Lowest" ] && [ "${{ matrix.type }}" != "Phpunit Burn" ]; then composer remove --no-interaction --no-update phpunit/phpunit ergebnis/phpunit-slow-test-detector --dev; fi
112115
if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer ergebnis/composer-normalize --dev; fi
113116
if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/\* --dev; fi
114117
if [ -n "$LOG_COVERAGE" ]; then composer require --no-interaction --no-install phpunit/phpcov; fi
115118
composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader
116119
if [ "${{ matrix.type }}" = "Phpunit Lowest" ]; then composer update --ansi --prefer-dist --prefer-lowest --prefer-stable --no-interaction --no-progress --optimize-autoloader; fi
120+
if [ "${{ matrix.type }}" = "Phpunit Burn" ]; then sed -i 's~public function runBare(): void~public function runBare(): void { gc_collect_cycles(); $memDiffs = array_fill(0, '"$(if [ \"$GITHUB_EVENT_NAME\" == \"schedule\" ]; then echo 64; else echo 16; fi)"', 0); $emitter = Event\\Facade::emitter(); for ($i = -1; $i < count($memDiffs); ++$i) { $this->_runBare(); if ($this->inIsolation) { $dispatcher = \\Closure::bind(static fn () => $emitter->dispatcher, null, Event\\DispatchingEmitter::class)(); if ($i === -1) { $dispatcherEvents = $dispatcher->flush()->asArray(); } else { $dispatcher->flush(); } foreach ($dispatcherEvents as $event) { $dispatcher->dispatch($event); } } gc_collect_cycles(); $mem = memory_get_usage(); if ($i !== -1) { $memDiffs[$i] = $mem - $memPrev; } $memPrev = $mem; rsort($memDiffs); if (array_sum($memDiffs) >= 4096 * 1024 || $memDiffs[2] > 0) { $e = new AssertionFailedError("Memory leak detected! (" . implode(" + ", array_map(static fn ($v) => number_format($v / 1024, 3, ".", " "), array_filter($memDiffs))) . " KB, " . ($i + 2) . " iterations)"); $this->status = TestStatus::failure($e->getMessage()); $emitter->testFailed($this->valueObjectForEvents(), Event\\Code\\ThrowableBuilder::from($e), Event\\Code\\ComparisonFailureBuilder::from($e)); $this->onNotSuccessfulTest($e); } } } private function _runBare(): void~' vendor/phpunit/phpunit/src/Framework/TestCase.php && cat vendor/phpunit/phpunit/src/Framework/TestCase.php | grep '_runBare('; fi
117121
118122
- name: Init
119123
run: |

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ continue to function as long as a majority of the servers still works.
253253

254254
Example:
255255
```php
256-
$redis = new Redis();
256+
$redis = new \Redis();
257257
$redis->connect('localhost');
258258

259259
$mutex = new PHPRedisMutex([$redis], 'balance');
@@ -275,7 +275,7 @@ The **PredisMutex** is the distributed lock implementation of
275275

276276
Example:
277277
```php
278-
$redis = new Client('redis://localhost');
278+
$redis = new \Predis\Client('redis://localhost');
279279

280280
$mutex = new PredisMutex([$redis], 'balance');
281281
$mutex->synchronized(function () use ($bankAccount, $amount) {
@@ -354,7 +354,7 @@ Also note that `GET_LOCK` function is server wide and the MySQL manual suggests
354354
you to namespace your locks like `dbname.lockname`.
355355

356356
```php
357-
$pdo = new PDO('mysql:host=localhost;dbname=test', 'username');
357+
$pdo = new \PDO('mysql:host=localhost;dbname=test', 'username');
358358

359359
$mutex = new MySQLMutex($pdo, 'balance', 15);
360360
$mutex->synchronized(function () use ($bankAccount, $amount) {
@@ -380,7 +380,7 @@ No time outs are supported. If the connection to the database server is lost or
380380
interrupted, the lock is automatically released.
381381

382382
```php
383-
$pdo = new PDO('pgsql:host=localhost;dbname=test', 'username');
383+
$pdo = new \PDO('pgsql:host=localhost;dbname=test', 'username');
384384

385385
$mutex = new PgAdvisoryLockMutex($pdo, 'balance');
386386
$mutex->synchronized(function () use ($bankAccount, $amount) {

tests/mutex/CASMutexTest.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use malkusch\lock\exception\LockAcquireException;
88
use malkusch\lock\mutex\CASMutex;
99
use phpmock\environment\SleepEnvironmentBuilder;
10+
use phpmock\MockEnabledException;
1011
use phpmock\phpunit\PHPMock;
1112
use PHPUnit\Framework\TestCase;
1213

@@ -19,14 +20,18 @@ protected function setUp(): void
1920
{
2021
parent::setUp();
2122

22-
$builder = new SleepEnvironmentBuilder();
23-
$builder->addNamespace(__NAMESPACE__);
24-
$builder->addNamespace('malkusch\lock\mutex');
25-
$builder->addNamespace('malkusch\lock\util');
26-
$sleep = $builder->build();
27-
$sleep->enable();
28-
29-
$this->registerForTearDown($sleep);
23+
$sleepBuilder = new SleepEnvironmentBuilder();
24+
$sleepBuilder->addNamespace(__NAMESPACE__);
25+
$sleepBuilder->addNamespace('malkusch\lock\mutex');
26+
$sleepBuilder->addNamespace('malkusch\lock\util');
27+
$sleep = $sleepBuilder->build();
28+
try {
29+
$sleep->enable();
30+
$this->registerForTearDown($sleep);
31+
} catch (MockEnabledException $e) {
32+
// workaround for burn testing
33+
\assert($e->getMessage() === 'microtime is already enabled.Call disable() on the existing mock.');
34+
}
3035
}
3136

3237
/**

tests/mutex/MemcachedMutexTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class MemcachedMutexTest extends TestCase
2828
#[\Override]
2929
protected function setUp(): void
3030
{
31+
parent::setUp();
32+
3133
$this->memcached = $this->createMock(\Memcached::class);
3234
$this->mutex = new MemcachedMutex('test', $this->memcached, 1);
3335
}

tests/mutex/MutexConcurrencyTest.php

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,17 @@ private function fork(int $concurrency, \Closure $code): void
8282
*
8383
* @param \Closure(0|1): int $code The counter code
8484
* @param \Closure(float): Mutex $mutexFactory
85+
* @param \Closure(): void $setUp
8586
*
8687
* @dataProvider provideHighContentionCases
8788
*/
8889
#[DataProvider('provideHighContentionCases')]
89-
public function testHighContention(\Closure $code, \Closure $mutexFactory): void
90+
public function testHighContention(\Closure $code, \Closure $mutexFactory, ?\Closure $setUp = null): void
9091
{
92+
if ($setUp !== null) {
93+
$setUp();
94+
}
95+
9196
$concurrency = 10;
9297
$iterations = 1000 / $concurrency;
9398
$timeout = $concurrency * 20;
@@ -110,14 +115,12 @@ public function testHighContention(\Closure $code, \Closure $mutexFactory): void
110115
*/
111116
public static function provideHighContentionCases(): iterable
112117
{
113-
foreach (static::provideExecutionIsSerializedWhenLockedCases() as [$mutexFactory]) {
118+
foreach (static::provideExecutionIsSerializedWhenLockedCases() as $name => [$mutexFactory]) {
114119
$filename = tempnam(sys_get_temp_dir(), 'php-lock-high-contention');
115120

116121
static::$temporaryFiles[] = $filename;
117122

118-
file_put_contents($filename, '0');
119-
120-
yield [
123+
yield $name => [
121124
static function (int $increment) use ($filename): int {
122125
$counter = file_get_contents($filename);
123126
$counter += $increment;
@@ -127,21 +130,19 @@ static function (int $increment) use ($filename): int {
127130
return $counter;
128131
},
129132
$mutexFactory,
133+
static function () use ($filename): void {
134+
file_put_contents($filename, '0');
135+
},
130136
];
131137
}
132138

133-
$addPDO = static function ($dsn, $user, $password, $vendor) {
139+
$makePDOCase = static function (string $dsn, string $user, string $password, string $vendor) {
134140
$pdo = self::getPDO($dsn, $user, $password);
135141

136142
$options = ['mysql' => 'engine=InnoDB'];
137143
$option = $options[$vendor] ?? '';
138144
$pdo->exec('CREATE TABLE IF NOT EXISTS counter(id INT PRIMARY KEY, counter INT) ' . $option);
139145

140-
$pdo->beginTransaction();
141-
$pdo->exec('DELETE FROM counter');
142-
$pdo->exec('INSERT INTO counter VALUES (1, 0)');
143-
$pdo->commit();
144-
145146
self::$pdo = null;
146147

147148
return [
@@ -163,34 +164,40 @@ static function (int $increment) use ($dsn, $user, $password) {
163164

164165
return $counter;
165166
},
166-
static function ($timeout = 3) use ($dsn, $user, $password) {
167+
static function ($timeout) use ($dsn, $user, $password) {
167168
self::$pdo = null;
168169
$pdo = self::getPDO($dsn, $user, $password);
169170

170171
return new TransactionalMutex($pdo, $timeout);
171172
},
173+
static function () use ($pdo): void {
174+
$pdo->beginTransaction();
175+
$pdo->exec('DELETE FROM counter');
176+
$pdo->exec('INSERT INTO counter VALUES (1, 0)');
177+
$pdo->commit();
178+
},
172179
];
173180
};
174181

175182
if (getenv('MYSQL_DSN')) {
176183
$dsn = getenv('MYSQL_DSN');
177184
$user = getenv('MYSQL_USER');
178185
$password = getenv('MYSQL_PASSWORD');
179-
yield 'mysql' => $addPDO($dsn, $user, $password, 'mysql');
186+
yield 'mysql' => $makePDOCase($dsn, $user, $password, 'mysql');
180187
}
181188

182189
if (getenv('PGSQL_DSN')) {
183190
$dsn = getenv('PGSQL_DSN');
184191
$user = getenv('PGSQL_USER');
185192
$password = getenv('PGSQL_PASSWORD');
186-
yield 'postgres' => $addPDO($dsn, $user, $password, 'postgres');
193+
yield 'postgres' => $makePDOCase($dsn, $user, $password, 'postgres');
187194
}
188195
}
189196

190197
/**
191198
* Tests that five processes run sequentially.
192199
*
193-
* @param \Closure(): Mutex $mutexFactory
200+
* @param \Closure(float): Mutex $mutexFactory
194201
*
195202
* @dataProvider provideExecutionIsSerializedWhenLockedCases
196203
*/
@@ -200,7 +207,7 @@ public function testExecutionIsSerializedWhenLocked(\Closure $mutexFactory): voi
200207
$time = \microtime(true);
201208

202209
$this->fork(6, static function () use ($mutexFactory): void {
203-
$mutex = $mutexFactory();
210+
$mutex = $mutexFactory(3);
204211
$mutex->synchronized(static function (): void {
205212
\usleep(200 * 1000);
206213
});
@@ -221,29 +228,29 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable
221228

222229
self::$temporaryFiles[] = $filename;
223230

224-
yield 'flock' => [static function ($timeout = 3) use ($filename): Mutex {
231+
yield 'flock' => [static function ($timeout) use ($filename): Mutex {
225232
$file = fopen($filename, 'w');
226233

227-
return new FlockMutex($file);
234+
return new FlockMutex($file, $timeout);
228235
}];
229236

230-
yield 'flockWithTimoutPcntl' => [static function ($timeout = 3) use ($filename): Mutex {
237+
yield 'flockWithTimoutPcntl' => [static function ($timeout) use ($filename): Mutex {
231238
$file = fopen($filename, 'w');
232239
$lock = Liberator::liberate(new FlockMutex($file, $timeout));
233240
$lock->strategy = FlockMutex::STRATEGY_PCNTL; // @phpstan-ignore property.notFound
234241

235242
return $lock->popsValue();
236243
}];
237244

238-
yield 'flockWithTimoutBusy' => [static function ($timeout = 3) use ($filename): Mutex {
245+
yield 'flockWithTimoutBusy' => [static function ($timeout) use ($filename): Mutex {
239246
$file = fopen($filename, 'w');
240247
$lock = Liberator::liberate(new FlockMutex($file, $timeout));
241248
$lock->strategy = FlockMutex::STRATEGY_BUSY; // @phpstan-ignore property.notFound
242249

243250
return $lock->popsValue();
244251
}];
245252

246-
yield 'semaphore' => [static function ($timeout = 3) use ($filename): Mutex {
253+
yield 'semaphore' => [static function () use ($filename): Mutex {
247254
$semaphore = sem_get(ftok($filename, 'b'));
248255
self::assertThat(
249256
$semaphore,
@@ -257,7 +264,7 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable
257264
}];
258265

259266
if (getenv('MEMCACHE_HOST')) {
260-
yield 'memcached' => [static function ($timeout = 3): Mutex {
267+
yield 'memcached' => [static function ($timeout): Mutex {
261268
$memcached = new \Memcached();
262269
$memcached->addServer(getenv('MEMCACHE_HOST'), 11211);
263270

@@ -268,7 +275,7 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable
268275
if (getenv('REDIS_URIS')) {
269276
$uris = explode(',', getenv('REDIS_URIS'));
270277

271-
yield 'PredisMutex' => [static function ($timeout = 3) use ($uris): Mutex {
278+
yield 'PredisMutex' => [static function ($timeout) use ($uris): Mutex {
272279
$clients = array_map(
273280
static fn ($uri) => new Client($uri),
274281
$uris
@@ -279,7 +286,7 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable
279286

280287
if (class_exists(\Redis::class)) {
281288
yield 'PHPRedisMutex' => [
282-
static function ($timeout = 3) use ($uris): Mutex {
289+
static function ($timeout) use ($uris): Mutex {
283290
$apis = array_map(
284291
static function (string $uri): \Redis {
285292
$redis = new \Redis();
@@ -306,7 +313,7 @@ static function (string $uri): \Redis {
306313
}
307314

308315
if (getenv('MYSQL_DSN')) {
309-
yield 'MySQLMutex' => [static function ($timeout = 3): Mutex {
316+
yield 'MySQLMutex' => [static function ($timeout): Mutex {
310317
$pdo = new \PDO(getenv('MYSQL_DSN'), getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'));
311318
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
312319

tests/mutex/MutexTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class MutexTest extends TestCase
3636
#[\Override]
3737
public static function setUpBeforeClass(): void
3838
{
39+
parent::setUpBeforeClass();
40+
3941
vfsStream::setup('test');
4042
}
4143

@@ -71,7 +73,7 @@ public static function provideMutexFactoriesCases(): iterable
7173
return $lock->popsValue();
7274
}];
7375

74-
yield 'flockWithTimoutBusy' => [static function ($timeout = 3): Mutex {
76+
yield 'flockWithTimoutBusy' => [static function (): Mutex {
7577
$file = fopen(vfsStream::url('test/lock'), 'w');
7678
$lock = Liberator::liberate(new FlockMutex($file, 3));
7779
$lock->strategy = FlockMutex::STRATEGY_BUSY; // @phpstan-ignore property.notFound

tests/mutex/PHPRedisMutexTest.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ private function _eval(string $script, array $args = [], int $numKeys = 0)
153153
$this->mutex = new PHPRedisMutex($this->connections, 'test');
154154
}
155155

156+
#[\Override]
157+
protected function assertPostConditions(): void
158+
{
159+
// workaround for burn testing
160+
$this->connections = [];
161+
162+
parent::assertPostConditions();
163+
}
164+
156165
private function closeMajorityConnections(): void
157166
{
158167
$numberToClose = (int) ceil(count($this->connections) / 2);
@@ -209,7 +218,7 @@ public function testEvalScriptFails(): void
209218
* @dataProvider provideSerializersAndCompressorsCases
210219
*/
211220
#[DataProvider('provideSerializersAndCompressorsCases')]
212-
public function testSerializersAndCompressors($serializer, $compressor): void
221+
public function testSerializersAndCompressors(int $serializer, int $compressor): void
213222
{
214223
foreach ($this->connections as $connection) {
215224
$connection->setOption(\Redis::OPT_SERIALIZER, $serializer);

tests/mutex/RedisMutexTest.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use malkusch\lock\exception\TimeoutException;
1111
use malkusch\lock\mutex\RedisMutex;
1212
use phpmock\environment\SleepEnvironmentBuilder;
13+
use phpmock\MockEnabledException;
1314
use phpmock\phpunit\PHPMock;
1415
use PHPUnit\Framework\Attributes\DataProvider;
1516
use PHPUnit\Framework\Attributes\Group;
@@ -34,9 +35,13 @@ protected function setUp(): void
3435
$sleepBuilder->addNamespace('malkusch\lock\mutex');
3536
$sleepBuilder->addNamespace('malkusch\lock\util');
3637
$sleep = $sleepBuilder->build();
37-
38-
$sleep->enable();
39-
$this->registerForTearDown($sleep);
38+
try {
39+
$sleep->enable();
40+
$this->registerForTearDown($sleep);
41+
} catch (MockEnabledException $e) {
42+
// workaround for burn testing
43+
\assert($e->getMessage() === 'microtime is already enabled.Call disable() on the existing mock.');
44+
}
4045
}
4146

4247
/**
@@ -136,7 +141,7 @@ static function () use (&$i, $available): bool {
136141
* @dataProvider provideMinorityCases
137142
*/
138143
#[DataProvider('provideMinorityCases')]
139-
public function testAcquireTooFewKeys($count, $available): void
144+
public function testAcquireTooFewKeys(int $count, int $available): void
140145
{
141146
$this->expectException(TimeoutException::class);
142147
$this->expectExceptionMessage('Timeout of 1.0 seconds exceeded');

0 commit comments

Comments
 (0)