Skip to content

Commit

Permalink
Add burn testing to CI
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Dec 5, 2024
1 parent 7b9b2d9 commit 3fd576b
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 61 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/test-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ jobs:
matrix:
php: ['7.4', '8.0', '8.1', '8.2', '8.3']
type: ['Phpunit', 'Phpunit Lowest']
include:
- php: 'latest'
type: 'Phpunit Burn'
env:
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')))] }}"
services:
Expand Down Expand Up @@ -108,12 +111,13 @@ jobs:
- name: Install PHP dependencies
run: |
if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "Phpunit Lowest" ]; then composer remove --no-interaction --no-update phpunit/phpunit ergebnis/phpunit-slow-test-detector --dev; fi
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
if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer ergebnis/composer-normalize --dev; fi
if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/\* --dev; fi
if [ -n "$LOG_COVERAGE" ]; then composer require --no-interaction --no-install phpunit/phpcov; fi
composer update --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader
if [ "${{ matrix.type }}" = "Phpunit Lowest" ]; then composer update --ansi --prefer-dist --prefer-lowest --prefer-stable --no-interaction --no-progress --optimize-autoloader; fi
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
- name: Init
run: |
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ continue to function as long as a majority of the servers still works.

Example:
```php
$redis = new Redis();
$redis = new \Redis();
$redis->connect('localhost');

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

Example:
```php
$redis = new Client('redis://localhost');
$redis = new \Predis\Client('redis://localhost');

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

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

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

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

$mutex = new PgAdvisoryLockMutex($pdo, 'balance');
$mutex->synchronized(function () use ($bankAccount, $amount) {
Expand Down
21 changes: 13 additions & 8 deletions tests/mutex/CASMutexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\mutex\CASMutex;
use phpmock\environment\SleepEnvironmentBuilder;
use phpmock\MockEnabledException;
use phpmock\phpunit\PHPMock;
use PHPUnit\Framework\TestCase;

Expand All @@ -19,14 +20,18 @@ protected function setUp(): void
{
parent::setUp();

$builder = new SleepEnvironmentBuilder();
$builder->addNamespace(__NAMESPACE__);
$builder->addNamespace('malkusch\lock\mutex');
$builder->addNamespace('malkusch\lock\util');
$sleep = $builder->build();
$sleep->enable();

$this->registerForTearDown($sleep);
$sleepBuilder = new SleepEnvironmentBuilder();
$sleepBuilder->addNamespace(__NAMESPACE__);
$sleepBuilder->addNamespace('malkusch\lock\mutex');
$sleepBuilder->addNamespace('malkusch\lock\util');
$sleep = $sleepBuilder->build();
try {
$sleep->enable();
$this->registerForTearDown($sleep);
} catch (MockEnabledException $e) {
// workaround for burn testing
\assert($e->getMessage() === 'microtime is already enabled.Call disable() on the existing mock.');
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions tests/mutex/MemcachedMutexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class MemcachedMutexTest extends TestCase
#[\Override]
protected function setUp(): void
{
parent::setUp();

$this->memcached = $this->createMock(\Memcached::class);
$this->mutex = new MemcachedMutex('test', $this->memcached, 1);
}
Expand Down
57 changes: 32 additions & 25 deletions tests/mutex/MutexConcurrencyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,17 @@ private function fork(int $concurrency, \Closure $code): void
*
* @param \Closure(0|1): int $code The counter code
* @param \Closure(float): Mutex $mutexFactory
* @param \Closure(): void $setUp
*
* @dataProvider provideHighContentionCases
*/
#[DataProvider('provideHighContentionCases')]
public function testHighContention(\Closure $code, \Closure $mutexFactory): void
public function testHighContention(\Closure $code, \Closure $mutexFactory, ?\Closure $setUp = null): void
{
if ($setUp !== null) {
$setUp();
}

$concurrency = 10;
$iterations = 1000 / $concurrency;
$timeout = $concurrency * 20;
Expand All @@ -110,14 +115,12 @@ public function testHighContention(\Closure $code, \Closure $mutexFactory): void
*/
public static function provideHighContentionCases(): iterable
{
foreach (static::provideExecutionIsSerializedWhenLockedCases() as [$mutexFactory]) {
foreach (static::provideExecutionIsSerializedWhenLockedCases() as $name => [$mutexFactory]) {
$filename = tempnam(sys_get_temp_dir(), 'php-lock-high-contention');

static::$temporaryFiles[] = $filename;

file_put_contents($filename, '0');

yield [
yield $name => [
static function (int $increment) use ($filename): int {
$counter = file_get_contents($filename);
$counter += $increment;
Expand All @@ -127,21 +130,19 @@ static function (int $increment) use ($filename): int {
return $counter;
},
$mutexFactory,
static function () use ($filename): void {
file_put_contents($filename, '0');
},
];
}

$addPDO = static function ($dsn, $user, $password, $vendor) {
$makePDOCase = static function (string $dsn, string $user, string $password, string $vendor) {
$pdo = self::getPDO($dsn, $user, $password);

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

$pdo->beginTransaction();
$pdo->exec('DELETE FROM counter');
$pdo->exec('INSERT INTO counter VALUES (1, 0)');
$pdo->commit();

self::$pdo = null;

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

return $counter;
},
static function ($timeout = 3) use ($dsn, $user, $password) {
static function ($timeout) use ($dsn, $user, $password) {
self::$pdo = null;
$pdo = self::getPDO($dsn, $user, $password);

return new TransactionalMutex($pdo, $timeout);
},
static function () use ($pdo): void {
$pdo->beginTransaction();
$pdo->exec('DELETE FROM counter');
$pdo->exec('INSERT INTO counter VALUES (1, 0)');
$pdo->commit();
},
];
};

if (getenv('MYSQL_DSN')) {
$dsn = getenv('MYSQL_DSN');
$user = getenv('MYSQL_USER');
$password = getenv('MYSQL_PASSWORD');
yield 'mysql' => $addPDO($dsn, $user, $password, 'mysql');
yield 'mysql' => $makePDOCase($dsn, $user, $password, 'mysql');
}

if (getenv('PGSQL_DSN')) {
$dsn = getenv('PGSQL_DSN');
$user = getenv('PGSQL_USER');
$password = getenv('PGSQL_PASSWORD');
yield 'postgres' => $addPDO($dsn, $user, $password, 'postgres');
yield 'postgres' => $makePDOCase($dsn, $user, $password, 'postgres');
}
}

/**
* Tests that five processes run sequentially.
*
* @param \Closure(): Mutex $mutexFactory
* @param \Closure(float): Mutex $mutexFactory
*
* @dataProvider provideExecutionIsSerializedWhenLockedCases
*/
Expand All @@ -200,7 +207,7 @@ public function testExecutionIsSerializedWhenLocked(\Closure $mutexFactory): voi
$time = \microtime(true);

$this->fork(6, static function () use ($mutexFactory): void {
$mutex = $mutexFactory();
$mutex = $mutexFactory(3);
$mutex->synchronized(static function (): void {
\usleep(200 * 1000);
});
Expand All @@ -221,29 +228,29 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable

self::$temporaryFiles[] = $filename;

yield 'flock' => [static function ($timeout = 3) use ($filename): Mutex {
yield 'flock' => [static function ($timeout) use ($filename): Mutex {
$file = fopen($filename, 'w');

return new FlockMutex($file);
return new FlockMutex($file, $timeout);
}];

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

return $lock->popsValue();
}];

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

return $lock->popsValue();
}];

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

if (getenv('MEMCACHE_HOST')) {
yield 'memcached' => [static function ($timeout = 3): Mutex {
yield 'memcached' => [static function ($timeout): Mutex {
$memcached = new \Memcached();
$memcached->addServer(getenv('MEMCACHE_HOST'), 11211);

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

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

if (class_exists(\Redis::class)) {
yield 'PHPRedisMutex' => [
static function ($timeout = 3) use ($uris): Mutex {
static function ($timeout) use ($uris): Mutex {
$apis = array_map(
static function (string $uri): \Redis {
$redis = new \Redis();
Expand All @@ -306,7 +313,7 @@ static function (string $uri): \Redis {
}

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

Expand Down
4 changes: 3 additions & 1 deletion tests/mutex/MutexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class MutexTest extends TestCase
#[\Override]
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();

vfsStream::setup('test');
}

Expand Down Expand Up @@ -71,7 +73,7 @@ public static function provideMutexFactoriesCases(): iterable
return $lock->popsValue();
}];

yield 'flockWithTimoutBusy' => [static function ($timeout = 3): Mutex {
yield 'flockWithTimoutBusy' => [static function (): Mutex {
$file = fopen(vfsStream::url('test/lock'), 'w');
$lock = Liberator::liberate(new FlockMutex($file, 3));
$lock->strategy = FlockMutex::STRATEGY_BUSY; // @phpstan-ignore property.notFound
Expand Down
11 changes: 10 additions & 1 deletion tests/mutex/PHPRedisMutexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ private function _eval(string $script, array $args = [], int $numKeys = 0)
$this->mutex = new PHPRedisMutex($this->connections, 'test');
}

#[\Override]
protected function assertPostConditions(): void
{
// workaround for burn testing
$this->connections = [];

parent::assertPostConditions();
}

private function closeMajorityConnections(): void
{
$numberToClose = (int) ceil(count($this->connections) / 2);
Expand Down Expand Up @@ -209,7 +218,7 @@ public function testEvalScriptFails(): void
* @dataProvider provideSerializersAndCompressorsCases
*/
#[DataProvider('provideSerializersAndCompressorsCases')]
public function testSerializersAndCompressors($serializer, $compressor): void
public function testSerializersAndCompressors(int $serializer, int $compressor): void
{
foreach ($this->connections as $connection) {
$connection->setOption(\Redis::OPT_SERIALIZER, $serializer);
Expand Down
13 changes: 9 additions & 4 deletions tests/mutex/RedisMutexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use malkusch\lock\exception\TimeoutException;
use malkusch\lock\mutex\RedisMutex;
use phpmock\environment\SleepEnvironmentBuilder;
use phpmock\MockEnabledException;
use phpmock\phpunit\PHPMock;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
Expand All @@ -34,9 +35,13 @@ protected function setUp(): void
$sleepBuilder->addNamespace('malkusch\lock\mutex');
$sleepBuilder->addNamespace('malkusch\lock\util');
$sleep = $sleepBuilder->build();

$sleep->enable();
$this->registerForTearDown($sleep);
try {
$sleep->enable();
$this->registerForTearDown($sleep);
} catch (MockEnabledException $e) {
// workaround for burn testing
\assert($e->getMessage() === 'microtime is already enabled.Call disable() on the existing mock.');
}
}

/**
Expand Down Expand Up @@ -136,7 +141,7 @@ static function () use (&$i, $available): bool {
* @dataProvider provideMinorityCases
*/
#[DataProvider('provideMinorityCases')]
public function testAcquireTooFewKeys($count, $available): void
public function testAcquireTooFewKeys(int $count, int $available): void
{
$this->expectException(TimeoutException::class);
$this->expectExceptionMessage('Timeout of 1.0 seconds exceeded');
Expand Down
Loading

0 comments on commit 3fd576b

Please sign in to comment.