Files
XC_VM/tests/Unit/OnDemandStartLockTest.php
T
rootandClaude Opus 5 b8eb1fa881 fix(live): viewers arriving together start a stopped on-demand stream once
A new monitor takes a few hundred milliseconds to write its _.monitor file.
Every viewer that reached a stopped on-demand stream inside that window found
it unwatched and started it again: the later viewer deleted the _.monitor and
_.pid the earlier monitor had just written, each monitor's checkRunning ran
before the other had set its process title, and both launched a producer —
two connections to a source that often allows one, kicking each other off.

The check-and-start in live.php now runs under a per-stream flock
(<id>_.start on the streams tmpfs). The viewers behind the first wait on it,
re-read the monitor pid, and find the stream started. A viewer that dies
releases it with its process. The test runs four simultaneous viewers in
separate processes: one start with the lock, four without.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuZvjFSdodqgpyXtaoH1Xt
2026-09-12 21:59:27 +00:00

56 lines
1.8 KiB
PHP

<?php
namespace XcVm\Tests\Unit;
use PHPUnit\Framework\TestCase;
use XcVm\Domain\Stream\StreamProcess;
/**
* Viewers who reach a stopped on-demand stream at the same moment must start
* it once. Each simulated viewer below runs live.php's shape in its own
* process: take the start lock, look for a monitor, and — finding none — spend
* the monitor's start-up window starting one.
*/
class OnDemandStartLockTest extends TestCase {
private string $rDir;
protected function setUp(): void {
if (!defined('STREAMS_PATH')) {
define('STREAMS_PATH', sys_get_temp_dir() . '/xcvm-ondemand-lock/');
}
$this->rDir = STREAMS_PATH;
@mkdir($this->rDir, 0777, true);
@unlink($this->rDir . '77_.started');
@unlink($this->rDir . '77_.start');
}
private function viewer(): mixed {
$rCode = 'require ' . var_export(dirname(__DIR__) . '/bootstrap.php', true) . ';'
. 'define("STREAMS_PATH", ' . var_export($this->rDir, true) . ');'
. '$l = \XcVm\Domain\Stream\StreamProcess::lockOnDemandStart(77);'
. '$m = STREAMS_PATH . "77_.started";'
. 'if (!file_exists($m)) { usleep(300000); file_put_contents($m, getmypid() . "\n", FILE_APPEND); }'
. '\XcVm\Domain\Stream\StreamProcess::unlockOnDemandStart($l);';
return proc_open([PHP_BINARY, '-r', $rCode], [], $rPipes);
}
public function testViewersArrivingTogetherStartTheStreamOnce(): void {
$rViewers = [];
for ($i = 0; $i < 4; $i++) {
$rViewers[] = $this->viewer();
}
foreach ($rViewers as $rProc) {
proc_close($rProc);
}
$rStarts = array_filter(explode("\n", (string) @file_get_contents($this->rDir . '77_.started')));
$this->assertCount(1, $rStarts, 'monitors started: ' . count($rStarts));
}
public function testUnlockToleratesNoLock(): void {
StreamProcess::unlockOnDemandStart(null);
$this->assertTrue(true);
}
}