Skip to content

Commit

Permalink
ProcessJobExecutor: handles stdout and stderr separately
Browse files Browse the repository at this point in the history
  • Loading branch information
mabar committed Jan 12, 2024
1 parent 2d6b9d7 commit 267ae14
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 16 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `ProcessJobExecutor`
- uses microseconds instead of milliseconds for start and end times
- better exception message in case subprocess call failed
- handles stdout and stderr separately
- stderr output does not make the job processing fail
- if stderr output is produced, an exception is still thrown (explaining unexpected stderr instead of a job failure)
- `ManagedScheduler`
- acquired job locks are scoped just to their id - changing run frequency or job name will not make process loose
the lock
Expand Down
64 changes: 49 additions & 15 deletions src/Executor/ProcessJobExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use function is_array;
use function json_decode;
use function json_encode;
use function trim;
use const JSON_THROW_ON_ERROR;
use const PHP_BINARY;

Expand Down Expand Up @@ -86,26 +87,36 @@ public function runJobs(
// Check running jobs
foreach ($jobExecutions as $i => [$execution, $cronExpression]) {
assert($execution instanceof Process);
if (!$execution->isRunning()) {
unset($jobExecutions[$i]);
if ($execution->isRunning()) {
continue;
}

$output = $execution->getOutput() . $execution->getErrorOutput();
unset($jobExecutions[$i]);

try {
$decoded = json_decode($output, true, 512, JSON_THROW_ON_ERROR);
assert(is_array($decoded));
$output = $execution->getOutput();
$errorOutput = trim($execution->getErrorOutput());

yield $jobSummaries[] = $this->createSummary($decoded, $cronExpression);
} catch (JsonException $e) {
$message = Message::create()
->withContext("Running job via command {$execution->getCommandLine()}")
->withProblem('Job subprocess failed.')
->with('stdout + stderr (standard + error output)', $output);
try {
$decoded = json_decode($output, true, 512, JSON_THROW_ON_ERROR);
assert(is_array($decoded));
} catch (JsonException $e) {
$suppressedExceptions[] = $this->createSubprocessFail(
$execution,
$output,
$errorOutput,
);

$suppressedExceptions[] = JobProcessFailure::create()
->withMessage($message);
}
continue;
}

if ($errorOutput !== '') {
$suppressedExceptions[] = $this->createStderrFail(
$execution,
$errorOutput,
);
}

yield $jobSummaries[] = $this->createSummary($decoded, $cronExpression);
}

// Nothing to do, wait
Expand Down Expand Up @@ -170,4 +181,27 @@ private function createSummary(array $raw, CronExpression $cronExpression): JobS
);
}

private function createSubprocessFail(Process $execution, string $output, string $errorOutput): JobProcessFailure
{
$message = Message::create()
->withContext("Running job via command {$execution->getCommandLine()}")
->withProblem('Job subprocess failed.')
->with('stdout', trim($output))
->with('stderr', $errorOutput);

return JobProcessFailure::create()
->withMessage($message);
}

private function createStderrFail(Process $execution, string $errorOutput): JobProcessFailure
{
$message = Message::create()
->withContext("Running job via command {$execution->getCommandLine()}")
->withProblem('Job subprocess produced stderr output.')
->with('stderr', $errorOutput);

return JobProcessFailure::create()
->withMessage($message);
}

}
36 changes: 36 additions & 0 deletions tests/Unit/SchedulerProcessSetup.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use Orisai\Scheduler\Status\JobResult;
use Tests\Orisai\Scheduler\Doubles\CallbackList;
use Throwable;
use function fwrite;
use const STDERR;

final class SchedulerProcessSetup
{
Expand Down Expand Up @@ -65,6 +67,40 @@ public static function createWithThrowingJob(): ManagedScheduler
return new ManagedScheduler($jobManager, null, null, $executor, $clock);
}

public static function createWithStderr(): ManagedScheduler
{
$jobManager = new SimpleJobManager();
$clock = new FrozenClock(1);
$executor = new ProcessJobExecutor($clock);
$executor->setExecutable(__DIR__ . '/scheduler-process-binary-sdterr.php');

$jobManager->addJob(
new CallbackJob(static function (): void {
// Just forces executable to run
}),
new CronExpression('* * * * *'),
);

return new ManagedScheduler($jobManager, null, null, $executor, $clock);
}

public static function createWithStderrJob(): ManagedScheduler
{
$jobManager = new SimpleJobManager();
$clock = new FrozenClock(1);
$executor = new ProcessJobExecutor($clock);
$executor->setExecutable(__DIR__ . '/scheduler-process-binary-with-stderr-job.php');

$jobManager->addJob(
new CallbackJob(static function (): void {
fwrite(STDERR, ' job error ');
}),
new CronExpression('* * * * *'),
);

return new ManagedScheduler($jobManager, null, null, $executor, $clock);
}

/**
* @param Closure(Throwable, JobInfo, JobResult): (void)|null $errorHandler
*/
Expand Down
72 changes: 71 additions & 1 deletion tests/Unit/SimpleSchedulerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,77 @@ public function testProcessExecutorWithDefaultExecutable(): void
Context: Running job via command '/usr/bin/php%f' 'bin/console'
'scheduler:run-job' '%a' '--json' '--parameters' '{"second":%d}'
Problem: Job subprocess failed.
stdout + stderr (standard + error output): Could not open input file: bin/console
stdout: Could not open input file: bin/console
stderr:
MSG,
rtrim($suppressed->getMessage()),

Check failure on line 997 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 7.4, false)

Failed asserting that string matches format description.

Check failure on line 997 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 8.0, false)

Failed asserting that string matches format description.

Check failure on line 997 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 8.1, false)

Failed asserting that string matches format description.
);
}
}

public function testProcessStderr(): void
{
$scheduler = SchedulerProcessSetup::createWithStderr();

$e = null;
try {
$scheduler->run();
} catch (RunFailure $e) {
// Handled bellow
}

self::assertNotNull($e);
self::assertStringStartsWith(
<<<'MSG'
Run failed
Suppressed errors:
MSG,
$e->getMessage(),
);

self::assertNotSame([], $e->getSuppressed());
foreach ($e->getSuppressed() as $suppressed) {
self::assertInstanceOf(JobProcessFailure::class, $suppressed);
self::assertStringMatchesFormat(
<<<'MSG'
Context: Running job via command '/usr/bin/php%f'%c%w'%a/tests/Unit/scheduler-process-binary-sdterr.php'%c%w'scheduler:run-job' '%a' '--json' '--parameters' '{"second":%d}'

Check failure on line 1027 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Coding standard (ubuntu-latest, 8.1)

Line exceeds maximum limit of 150 characters, contains 172 characters.
Problem: Job subprocess failed.
stdout:%c
stderr: error
MSG,
rtrim($suppressed->getMessage()),

Check failure on line 1032 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 7.4, false)

Failed asserting that string matches format description.

Check failure on line 1032 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 8.0, false)

Failed asserting that string matches format description.

Check failure on line 1032 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 8.1, false)

Failed asserting that string matches format description.
);
}
}

public function testProcessJobStderr(): void
{
$scheduler = SchedulerProcessSetup::createWithStderrJob();

$e = null;
try {
$scheduler->run();
} catch (RunFailure $e) {
// Handled bellow
}

self::assertNotNull($e);
self::assertStringStartsWith(
<<<'MSG'
Run failed
Suppressed errors:
MSG,
$e->getMessage(),
);

self::assertNotSame([], $e->getSuppressed());
foreach ($e->getSuppressed() as $suppressed) {
self::assertInstanceOf(JobProcessFailure::class, $suppressed);
self::assertStringMatchesFormat(
<<<'MSG'
Context: Running job via command '/usr/bin/php%f'%c%w'%a/tests/Unit/scheduler-process-binary-with-stderr-job.php'%c%w'scheduler:run-job' '%a' '--json' '--parameters' '{"second":%d}'

Check failure on line 1062 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Coding standard (ubuntu-latest, 8.1)

Line exceeds maximum limit of 150 characters, contains 181 characters.
Problem: Job subprocess produced stderr output.
stderr: job error
MSG,
rtrim($suppressed->getMessage()),

Check failure on line 1066 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 7.4, false)

Failed asserting that string matches format description.

Check failure on line 1066 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 8.0, false)

Failed asserting that string matches format description.

Check failure on line 1066 in tests/Unit/SimpleSchedulerTest.php

View workflow job for this annotation

GitHub Actions / Tests (windows-latest, 8.1, false)

Failed asserting that string matches format description.
);
Expand Down
3 changes: 3 additions & 0 deletions tests/Unit/scheduler-process-binary-sdterr.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php declare(strict_types = 1);

fwrite(STDERR, ' error ');
17 changes: 17 additions & 0 deletions tests/Unit/scheduler-process-binary-with-stderr-job.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

use Orisai\Scheduler\Command\RunJobCommand;
use Symfony\Component\Console\Application;
use Tests\Orisai\Scheduler\Unit\SchedulerProcessSetup;

require_once __DIR__ . '/../../vendor/autoload.php';

$application = new Application();
$scheduler = SchedulerProcessSetup::createWithStderrJob();

$command = new RunJobCommand($scheduler);

$application = new Application();
$application->addCommands([$command]);

$application->run();

0 comments on commit 267ae14

Please sign in to comment.