Tried reproducing same thing with cron jobs.
- In lib/pkp/classes/core/PKPContainer.php - Set queues.retry_after = 60 (only to shorten the window).
- Queue one real large daily file, e.g 140 MB. I queued 2 files because as claude said need multiple jobs or smth like that so this problem would be reproducable. But 2 files did not create 2 jobs, they were in one job record.
-
Added a handful of unrelated filler jobs to the queue (so a second consumer has something to pop and thus reaches pop()).
For that part as claude generated me dummy jobs slow running jobs. As I understand should work with any other unrelated job like emails or whatever jobs OJS have. Just slow ones.
<?php
/**
* @file tools/vuDispatchDummies.php
*
* VU THROWAWAY TEST TOOL — delete after the reclaim/overlap reproduction
*
* Dispatches N slow VuGateOpenerJob jobs onto the default queue so a second jobs.php run
* worker has something to chew on (keeping the gate open) long enough to reclaim a
* still-running, reservation-expired ProcessUsageStatsLogFile job.
*
* Usage (local Docker only):
* docker compose exec php php tools/vuDispatchDummies.php [count] [sleepSeconds]
* e.g. ... tools/vuDispatchDummies.php 15 12 -> 15 jobs, each sleeping 12s
*
* Not for commit.
*/
require_once dirname(__FILE__) . '/bootstrap.php';
use APP\jobs\VuGateOpenerJob;
use PKP\cliTool\CommandLineTool;
class vuDispatchDummies extends CommandLineTool
{
public int $count = 15;
public int $sleepSeconds = 12;
public function execute()
{
for ($i = 0; $i < $this->count; $i++) {
VuGateOpenerJob::dispatch($this->sleepSeconds);
}
fwrite(STDOUT, "Dispatched {$this->count} VuGateOpenerJob, each sleeping {$this->sleepSeconds}s.\n");
}
}
$tool = new vuDispatchDummies([$argv[0] ?? 'vuDispatchDummies.php']);
$tool->count = (int) ($argv[1] ?? 15);
$tool->sleepSeconds = (int) ($argv[2] ?? 12);
$tool->execute();
classes/jobs/VuGateOpenerJob.php
<?php
/**
* @file classes/jobs/VuGateOpenerJob.php
*
* VU THROWAWAY TEST JOB — delete after the reclaim/overlap reproduction
*
* A deliberately slow no-op job whose only purpose is to keep the queue "gate" non-empty:
* while one worker is busy with a long ProcessUsageStatsLogFile job, a second worker needs
* SOME available job to walk past the gate (jobs.php run's count() check) and reach pop().
* Once the long job's reservation expires (retry_after), that pop() reclaims it -> overlap.
*
* Not for commit.
*/
namespace APP\jobs;
use APP\vu\VULogger;
use PKP\jobs\BaseJob;
class VuGateOpenerJob extends BaseJob
{
public function __construct(private int $sleepSeconds = 12)
{
parent::__construct();
}
public function handle(): void
{
VULogger::log('VuGateOpenerJob start sleep=' . $this->sleepSeconds . 's pid=' . getmypid());
sleep($this->sleepSeconds);
VULogger::log('VuGateOpenerJob done pid=' . getmypid());
}
}
- dispatch those dummy jobs (writes into jobs table):
docker compose exec php php tools/vuDispatchDummies.php 15 12
- Ran two concurrent php lib/pkp/tools/jobs.php run processes — simulating two overlapping cron invocations / two concurrent web requests. In 2 terminal tabs ran those:
while true; do docker compose exec php php lib/pkp/tools/jobs.php run; echo "--- T1 retry $(date +%T) ---"; sleep 5; done
Its every 5 seconds I know its not a production thing, there was in doc for 30 seconds written I guess to set cron but still to have better chance of simulation of corruption, cause logic is the same I think.
Now logs show that both are inserting (claude generated)
Observed (pid / attempt fields below are our own instrumentation, not stock):
17:45:03 ProcessUsageStatsLogFile starting … pid=1184 attempt=1
17:45:08 ProcessUsageStatsLogFile deleteByLoadId() done pid=1184 attempt=1
… 1184 inserting …
17:46:05 ProcessUsageStatsLogFile starting … pid=1191 attempt=2 ← reclaim, ~60s (=retry_after) later
17:46:06 ProcessUsageStatsLogFile deleteByLoadId() done pid=1191 attempt=2 ← wipes 1184’s rows
… 1184 AND 1191 both inserting the same load_id concurrently for ~4.5 min …
17:50:40 ProcessUsageStatsLogFile loop done pid=1184 attempt=1
17:51:51 ProcessUsageStatsLogFile loop done pid=1191 attempt=2
And here are the logs of actual command executed:
grep -E '1191|1184' vu_debug.log
2026-06-22 17:45:03 ProcessUsageStatsLogFile starting. loadId=usage_events_20260406.log memStartMB=8 pid=1184 attempt=1
2026-06-22 17:45:03 ProcessUsageStatsLogFile file located. loadId=usage_events_20260406.log sizeKB=142903.1 pid=1184 attempt=1
2026-06-22 17:45:05 VuGateOpenerJob start sleep=12s pid=1191
2026-06-22 17:45:08 ProcessUsageStatsLogFile deleteByLoadId() done. loadId=usage_events_20260406.log elapsedSec=5.29 pid=1184 attempt=1
2026-06-22 17:45:17 VuGateOpenerJob done pid=1191
2026-06-22 17:45:17 VuGateOpenerJob start sleep=12s pid=1191
2026-06-22 17:45:29 VuGateOpenerJob done pid=1191
2026-06-22 17:45:29 VuGateOpenerJob start sleep=12s pid=1191
2026-06-22 17:45:34 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=20000 inserted=6865 bots=13135 invalid=0 loopSec=25.91 memMB=10 pid=1184 attempt=1
2026-06-22 17:45:41 VuGateOpenerJob done pid=1191
2026-06-22 17:45:41 VuGateOpenerJob start sleep=12s pid=1191
2026-06-22 17:45:43 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=25000 inserted=9227 bots=15773 invalid=0 loopSec=34.9 memMB=10 pid=1184 attempt=1
2026-06-22 17:45:53 VuGateOpenerJob done pid=1191
2026-06-22 17:45:53 VuGateOpenerJob start sleep=12s pid=1191
2026-06-22 17:46:05 VuGateOpenerJob done pid=1191
2026-06-22 17:46:05 ProcessUsageStatsLogFile starting. loadId=usage_events_20260406.log memStartMB=8 pid=1191 attempt=2
2026-06-22 17:46:05 ProcessUsageStatsLogFile file located. loadId=usage_events_20260406.log sizeKB=142903.1 pid=1191 attempt=2
2026-06-22 17:46:06 ProcessUsageStatsLogFile deleteByLoadId() done. loadId=usage_events_20260406.log elapsedSec=0.62 pid=1191 attempt=2
2026-06-22 17:46:19 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=55000 inserted=19423 bots=35577 invalid=0 loopSec=70.96 memMB=10 pid=1184 attempt=1
2026-06-22 17:46:24 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=60000 inserted=20809 bots=39191 invalid=0 loopSec=75.83 memMB=10 pid=1184 attempt=1
2026-06-22 17:46:29 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=65000 inserted=22199 bots=42801 invalid=0 loopSec=80.56 memMB=10 pid=1184 attempt=1
2026-06-22 17:46:31 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=20000 inserted=6865 bots=13135 invalid=0 loopSec=25.63 memMB=10 pid=1191 attempt=2
2026-06-22 17:46:40 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=25000 inserted=9227 bots=15773 invalid=0 loopSec=34.38 memMB=10 pid=1191 attempt=2
2026-06-22 17:46:45 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=80000 inserted=26771 bots=53229 invalid=0 loopSec=96.67 memMB=10 pid=1184 attempt=1
2026-06-22 17:46:57 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=90000 inserted=30206 bots=59794 invalid=0 loopSec=108.79 memMB=10 pid=1184 attempt=1
2026-06-22 17:47:15 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=55000 inserted=19423 bots=35577 invalid=0 loopSec=69.3 memMB=10 pid=1191 attempt=2
2026-06-22 17:47:20 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=60000 inserted=20809 bots=39191 invalid=0 loopSec=73.94 memMB=10 pid=1191 attempt=2
2026-06-22 17:47:24 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=65000 inserted=22199 bots=42801 invalid=0 loopSec=78.5 memMB=10 pid=1191 attempt=2
2026-06-22 17:47:34 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=120000 inserted=40473 bots=79527 invalid=0 loopSec=145.68 memMB=10 pid=1184 attempt=1
2026-06-22 17:47:40 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=80000 inserted=26771 bots=53229 invalid=0 loopSec=94.18 memMB=10 pid=1191 attempt=2
2026-06-22 17:47:40 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=125000 inserted=42121 bots=82879 invalid=0 loopSec=151.69 memMB=10 pid=1184 attempt=1
2026-06-22 17:47:52 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=90000 inserted=30206 bots=59794 invalid=0 loopSec=106.54 memMB=10 pid=1191 attempt=2
2026-06-22 17:48:29 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=120000 inserted=40473 bots=79527 invalid=0 loopSec=143.29 memMB=10 pid=1191 attempt=2
2026-06-22 17:48:35 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=125000 inserted=42121 bots=82879 invalid=0 loopSec=149.28 memMB=10 pid=1191 attempt=2
2026-06-22 17:49:03 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=205000 inserted=66826 bots=138174 invalid=0 loopSec=234.75 memMB=10 pid=1184 attempt=1
2026-06-22 17:49:09 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=210000 inserted=68409 bots=141591 invalid=0 loopSec=240.27 memMB=10 pid=1184 attempt=1
2026-06-22 17:49:16 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=215000 inserted=70370 bots=144630 invalid=0 loopSec=247.55 memMB=10 pid=1184 attempt=1
2026-06-22 17:49:24 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=220000 inserted=72498 bots=147502 invalid=0 loopSec=255.26 memMB=10 pid=1184 attempt=1
2026-06-22 17:49:48 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=235000 inserted=79040 bots=155960 invalid=0 loopSec=279.23 memMB=10 pid=1184 attempt=1
2026-06-22 17:49:56 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=240000 inserted=81210 bots=158790 invalid=0 loopSec=287.03 memMB=10 pid=1184 attempt=1
2026-06-22 17:50:05 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=205000 inserted=66826 bots=138174 invalid=0 loopSec=239.6 memMB=10 pid=1191 attempt=2
2026-06-22 17:50:12 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=210000 inserted=68409 bots=141591 invalid=0 loopSec=245.87 memMB=10 pid=1191 attempt=2
2026-06-22 17:50:18 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=215000 inserted=70370 bots=144630 invalid=0 loopSec=252.46 memMB=10 pid=1191 attempt=2
2026-06-22 17:50:25 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=220000 inserted=72498 bots=147502 invalid=0 loopSec=259.3 memMB=10 pid=1191 attempt=2
2026-06-22 17:50:40 ProcessUsageStatsLogFile loop done. loadId=usage_events_20260406.log totalLines=261409 inserted=89711 bots=171697 invalid=0 loopSec=331.93 pid=1184 attempt=1
2026-06-22 17:50:40 ProcessUsageStatsLogFile done. loadId=usage_events_20260406.log elapsedSec=337.23 memPeakMB=10 pid=1184 attempt=1
2026-06-22 17:50:40 VuGateOpenerJob start sleep=12s pid=1184
2026-06-22 17:50:52 VuGateOpenerJob done pid=1184
2026-06-22 17:50:52 VuGateOpenerJob start sleep=12s pid=1184
2026-06-22 17:50:56 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=235000 inserted=79040 bots=155960 invalid=0 loopSec=289.93 memMB=10 pid=1191 attempt=2
2026-06-22 17:51:04 VuGateOpenerJob done pid=1184
2026-06-22 17:51:04 VuGateOpenerJob start sleep=12s pid=1184
2026-06-22 17:51:10 ProcessUsageStatsLogFile progress. loadId=usage_events_20260406.log lines=240000 inserted=81210 bots=158790 invalid=0 loopSec=304.36 memMB=10 pid=1191 attempt=2
2026-06-22 17:51:16 VuGateOpenerJob done pid=1184
2026-06-22 17:51:16 VuGateOpenerJob start sleep=12s pid=1184
2026-06-22 17:51:28 VuGateOpenerJob done pid=1184
2026-06-22 17:51:28 VuGateOpenerJob start sleep=12s pid=1184
2026-06-22 17:51:40 VuGateOpenerJob done pid=1184
2026-06-22 17:51:40 VuGateOpenerJob start sleep=12s pid=1184
2026-06-22 17:51:51 ProcessUsageStatsLogFile loop done. loadId=usage_events_20260406.log totalLines=261409 inserted=89711 bots=171697 invalid=0 loopSec=345.25 pid=1191 attempt=2
2026-06-22 17:51:51 ProcessUsageStatsLogFile done. loadId=usage_events_20260406.log elapsedSec=345.88 memPeakMB=10 pid=1191 attempt=2
2026-06-22 17:51:51 VuGateOpenerJob start sleep=12s pid=1191
2026-06-22 17:51:52 VuGateOpenerJob done pid=1184
2026-06-22 17:51:52 VuGateOpenerJob start sleep=12s pid=1184
2026-06-22 17:52:03 VuGateOpenerJob done pid=1191
2026-06-22 17:52:03 VuGateOpenerJob start sleep=12s pid=1191
2026-06-22 17:52:04 VuGateOpenerJob done pid=1184
2026-06-22 17:52:05 VuGateOpenerJob start sleep=12s pid=1184
2026-06-22 17:52:15 VuGateOpenerJob done pid=1191
2026-06-22 17:52:17 VuGateOpenerJob done pid=1184
Btw class is updated to include pid lib/pkp/jobs/statistics/PKPProcessUsageStatsLogFile.php:
public function handle(): void
{
$vuStartTime = microtime(true);
$vuMemStart = memory_get_usage(true);
// VU: per-instance tag so concurrent runs of the SAME job/file are distinguishable in the log.
// pid = OS process (each concurrent web-runner worker is its own process); attempt = queue attempt
// number (1 = first pop, 2 = first reclaim, 3 = second reclaim, ...). Grep one pid= to follow an
// instance start->done; the order of 'done' lines + attempt= tells which finished 1st/2nd/3rd.
$vuTag = ' pid=' . getmypid() . ' attempt=' . $this->attempts();
VULogger::log('ProcessUsageStatsLogFile starting. loadId=' . $this->loadId . ' memStartMB=' . round($vuMemStart / 1048576, 1) . $vuTag);
$filename = $this->loadId;
$dispatchFilePath = StatisticsHelper::getUsageStatsDirPath() . '/' . FileLoader::FILE_LOADER_PATH_DISPATCH . '/' . $filename;
if (!file_exists($dispatchFilePath)) {
throw new JobException(__(
'admin.job.processLogFile.fileNotFound',
['file' => $dispatchFilePath]
));
}
$vuFileSize = filesize($dispatchFilePath);
VULogger::log('ProcessUsageStatsLogFile file located. loadId=' . $this->loadId . ' sizeKB=' . round($vuFileSize / 1024, 1) . $vuTag);
try {
$this->process($dispatchFilePath);
} catch (\Throwable $e) {
$vuElapsed = microtime(true) - $vuStartTime;
VULogger::log('ProcessUsageStatsLogFile THREW after ' . round($vuElapsed, 2) . 's. loadId=' . $this->loadId . ' memPeakMB=' . round(memory_get_peak_usage(true) / 1048576, 1) . ' msg=' . $e->getMessage() . $vuTag);
throw $e;
}
$vuElapsed = microtime(true) - $vuStartTime;
VULogger::log('ProcessUsageStatsLogFile done. loadId=' . $this->loadId . ' elapsedSec=' . round($vuElapsed, 2) . ' memPeakMB=' . round(memory_get_peak_usage(true) / 1048576, 1) . $vuTag);
}
protected function process(string $dispatchFilePath): void
{
$vuTag = ' pid=' . getmypid() . ' attempt=' . $this->attempts(); // VU: per-instance tag (see handle())
try {
$splFileObject = new SplFileObject($dispatchFilePath, 'r');
} catch (Exception $e) {
// reject file -- move the file from dispatch to reject folder
$filename = $this->loadId;
$rejectFilePath = StatisticsHelper::getUsageStatsDirPath() . '/' . FileLoader::FILE_LOADER_PATH_REJECT . '/' . $filename;
if (!rename($dispatchFilePath, $rejectFilePath)) {
error_log(__('admin.job.compileMetrics.returnToStaging.error', ['file' => $filename, 'dispatchFilePath' => $dispatchFilePath, 'rejectFilePath' => $rejectFilePath]));
}
throw new JobException(__('admin.job.processLogFile.openFileFailed', ['file' => $dispatchFilePath]), 0, $e);
}
// Make sure we don't have any temporary records associated
// with the current load ID in database.
$vuDeleteStart = microtime(true);
$this->deleteByLoadId();
VULogger::log('ProcessUsageStatsLogFile deleteByLoadId() done. loadId=' . $this->loadId . ' elapsedSec=' . round(microtime(true) - $vuDeleteStart, 2) . $vuTag);
$vuLoopStart = microtime(true);
$vuInsertedCount = 0;
$vuSkippedBots = 0;
$vuSkippedInvalid = 0;
$lineNumber = 0;
while (!$splFileObject->eof()) {
$lineNumber++;
$line = $splFileObject->fgets();
if (empty($line) || substr($line, 0, 1) === '#') {
continue;
} // Spacing or comment lines. This actually should not occur in the new format.
$entryData = json_decode($line);
if ($entryData === null) {
// This line is not in the right format.
$vuSkippedInvalid++;
$message = __(
'admin.job.processLogFile.wrongLoglineFormat',
['file' => $this->loadId, 'lineNumber' => $lineNumber]
);
error_log($message);
continue;
}
try {
$this->validateLogEntry($entryData);
} catch (Exception $e) {
$vuSkippedInvalid++;
$message = __(
'admin.job.processLogFile.invalidLogEntry',
['file' => $this->loadId, 'lineNumber' => $lineNumber, 'error' => $e->getMessage()]
);
error_log($message);
continue;
}
// Avoid bots.
if (Core::isUserAgentBot($entryData->userAgent)) {
$vuSkippedBots++;
continue;
}
$this->insertTemporaryUsageStatsData($entryData, $lineNumber);
$vuInsertedCount++;
// Periodic progress so we can see how far we got if the PHP process gets killed mid-job.
if ($lineNumber % 5000 === 0) {
VULogger::log('ProcessUsageStatsLogFile progress. loadId=' . $this->loadId . ' lines=' . $lineNumber . ' inserted=' . $vuInsertedCount . ' bots=' . $vuSkippedBots . ' invalid=' . $vuSkippedInvalid . ' loopSec=' . round(microtime(true) - $vuLoopStart, 2) . ' memMB=' . round(memory_get_usage(true) / 1048576, 1) . $vuTag);
}
}
VULogger::log('ProcessUsageStatsLogFile loop done. loadId=' . $this->loadId . ' totalLines=' . $lineNumber . ' inserted=' . $vuInsertedCount . ' bots=' . $vuSkippedBots . ' invalid=' . $vuSkippedInvalid . ' loopSec=' . round(microtime(true) - $vuLoopStart, 2) . $vuTag);
//explicitly assign null, so that the file can be deleted
$splFileObject = null;
}
Is this known issue with cron also and cannot be trusted if job time exceeds retry_after?