1
0
mirror of https://github.com/danog/parallel.git synced 2024-12-02 09:37:57 +01:00

Expand context & task exceptions

This commit is contained in:
Aaron Piotrowski 2020-02-11 11:06:30 -06:00
parent 1e8062c702
commit 53a97cbd92
No known key found for this signature in database
GPG Key ID: ADD1EF783EDE9EEB
14 changed files with 428 additions and 69 deletions

View File

@ -0,0 +1,83 @@
<?php
namespace Amp\Parallel\Sync;
final class ContextPanicError extends PanicError
{
/** @var string */
private $originalMessage;
/** @var int|string */
private $originalCode;
/** @var string[] */
private $originalTrace;
/**
* @param string $className Original exception class name.
* @param string $message Original exception message.
* @param int|string $code Original exception code.
* @param array $trace Backtrace generated by {@see formatFlattenedBacktrace()}.
* @param self|null $previous Instance representing any previous exception thrown in the child process or thread.
*/
public function __construct(string $className, string $message, $code, array $trace, ?self $previous = null)
{
$format = 'Uncaught %s in child process or thread with message "%s" and code "%s"; use %s::getOriginalTrace() '
. 'for the stack trace in the child process or thread';
parent::__construct(
$className,
\sprintf($format, $className, $message, $code, self::class),
formatFlattenedBacktrace($trace),
$previous
);
$this->originalMessage = $message;
$this->originalCode = $code;
$this->originalTrace = $trace;
}
/**
* @return string Original exception class name.
*/
public function getOriginalClassName(): string
{
return $this->getName();
}
/**
* @return string Original exception message.
*/
public function getOriginalMessage(): string
{
return $this->originalMessage;
}
/**
* @return int|string Original exception code.
*/
public function getOriginalCode()
{
return $this->originalCode;
}
/**
* Original exception stack trace.
*
* @return array Same as {@see Throwable::getTrace()}, except all function arguments are formatted as strings.
*/
public function getOriginalTrace(): array
{
return $this->originalTrace;
}
/**
* Original backtrace flattened to a human-readable string.
*
* @return string
*/
public function getOriginalTraceAsString(): string
{
return $this->getPanicTrace();
}
}

View File

@ -39,22 +39,10 @@ final class ExitFailure implements ExitResult
throw $this->createException();
}
private function createException(): PanicError
private function createException(): ContextPanicError
{
$previous = $this->previous ? $this->previous->createException() : null;
return new PanicError(
$this->type,
\sprintf(
'Uncaught %s in worker with message "%s" and code "%s"; use %s::getPanicTrace() '
. 'for the stack trace in the context',
$this->type,
$this->message,
$this->code,
PanicError::class
),
\implode("\n", $this->trace),
$previous
);
return new ContextPanicError($this->type, $this->message, $this->code, $this->trace, $previous);
}
}

View File

@ -2,7 +2,11 @@
namespace Amp\Parallel\Sync;
final class PanicError extends \Error
/**
* @deprecated ContextPanicError will be thrown from uncaught exceptions in child processes and threads instead of
* this class.
*/
class PanicError extends \Error
{
/** @var string Class name of uncaught exception. */
private $name;
@ -18,7 +22,7 @@ final class PanicError extends \Error
* @param string $trace The panic stack trace.
* @param \Throwable|null $previous Previous exception.
*/
public function __construct(string $name, string $message = '', string $trace = '', \Throwable $previous = null)
public function __construct(string $name, string $message = '', string $trace = '', ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
@ -27,6 +31,8 @@ final class PanicError extends \Error
}
/**
* @deprecated Use ContextPanicError::getOriginalClassName() instead.
*
* Returns the class name of the uncaught exception.
*
* @return string
@ -37,6 +43,8 @@ final class PanicError extends \Error
}
/**
* @deprecated Use ContextPanicError::getOriginalTraceAsString() instead.
*
* Gets the stack trace at the point the panic occurred.
*
* @return string

View File

@ -5,34 +5,47 @@ namespace Amp\Parallel\Sync;
/**
* @param \Throwable $exception
*
* @return string[] Serializable array of strings representing the exception backtrace including function arguments.
* @return array Serializable exception backtrace, with all function arguments flattened to strings.
*/
function flattenThrowableBacktrace(\Throwable $exception): array
{
$output = [];
$counter = 0;
$trace = $exception->getTrace();
foreach ($trace as $call) {
foreach ($trace as &$call) {
unset($call['object']);
$call['args'] = \array_map(__NAMESPACE__ . '\\flattenArgument', $call['args']);
}
return $trace;
}
/**
* @param array $trace Backtrace produced by {@see formatFlattenedBacktrace()}.
*
* @return string
*/
function formatFlattenedBacktrace(array $trace): string
{
$output = [];
foreach ($trace as $index => $call) {
if (isset($call['class'])) {
$name = $call['class'] . $call['type'] . $call['function'];
} else {
$name = $call['function'];
}
$args = \implode(', ', \array_map(__NAMESPACE__ . '\\flattenArgument', $call['args']));
$output[] = \sprintf(
'#%d %s(%d): %s(%s)',
$counter++,
$index,
$call['file'] ?? '[internal function]',
$call['line'] ?? 0,
$name,
$args
\implode(', ', $call['args'])
);
}
return $output;
return \implode("\n", $output);
}
/**

View File

@ -4,8 +4,8 @@ namespace Amp\Parallel\Worker\Internal;
use Amp\Failure;
use Amp\Parallel\Sync;
use Amp\Parallel\Worker\TaskError;
use Amp\Parallel\Worker\TaskException;
use Amp\Parallel\Worker\TaskFailureError;
use Amp\Parallel\Worker\TaskFailureException;
use Amp\Promise;
/** @internal */
@ -55,23 +55,10 @@ final class TaskFailure extends TaskResult
{
$previous = $this->previous ? $this->previous->createException() : null;
$format = 'Uncaught %s in worker with message "%s" and code "%s"; use %s::getWorkerTrace() '
. 'for the stack trace in the worker';
if ($this->parent === self::PARENT_ERROR) {
return new TaskError(
$this->type,
\sprintf($format, $this->type, $this->message, $this->code, TaskError::class),
\implode("\n", $this->trace),
$previous
);
return new TaskFailureError($this->type, $this->message, $this->code, $this->trace, $previous);
}
return new TaskException(
$this->type,
\sprintf($format, $this->type, $this->message, $this->code, TaskException::class),
\implode("\n", $this->trace),
$previous
);
return new TaskFailureException($this->type, $this->message, $this->code, $this->trace, $previous);
}
}

View File

@ -2,7 +2,10 @@
namespace Amp\Parallel\Worker;
final class TaskError extends \Error
/**
* @deprecated TaskFailureError will be thrown from failed Tasks instead of this class.
*/
class TaskError extends \Error
{
/** @var string Class name of error thrown from task. */
private $name;
@ -16,7 +19,7 @@ final class TaskError extends \Error
* @param string $trace The panic stack trace.
* @param \Throwable|null $previous Previous exception.
*/
public function __construct(string $name, string $message = '', string $trace = '', \Throwable $previous = null)
public function __construct(string $name, string $message = '', string $trace = '', ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
@ -25,6 +28,8 @@ final class TaskError extends \Error
}
/**
* @deprecated Use TaskFailureThrowable::getOriginalClassName() instead.
*
* Returns the class name of the error thrown from the task.
*
* @return string
@ -35,6 +40,8 @@ final class TaskError extends \Error
}
/**
* @deprecated Use TaskFailureThrowable::getOriginalTraceAsString() instead.
*
* Gets the stack trace at the point the error was thrown in the task.
*
* @return string

View File

@ -2,7 +2,10 @@
namespace Amp\Parallel\Worker;
final class TaskException extends \Exception
/**
* @deprecated TaskFailureException will be thrown from failed Tasks instead of this class.
*/
class TaskException extends \Exception
{
/** @var string Class name of exception thrown from task. */
private $name;
@ -16,7 +19,7 @@ final class TaskException extends \Exception
* @param string $trace The panic stack trace.
* @param \Throwable|null $previous Previous exception.
*/
public function __construct(string $name, string $message = '', string $trace = '', \Throwable $previous = null)
public function __construct(string $name, string $message = '', string $trace = '', ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
@ -25,6 +28,8 @@ final class TaskException extends \Exception
}
/**
* @deprecated Use TaskFailureThrowable::getOriginalClassName() instead.
*
* Returns the class name of the exception thrown from the task.
*
* @return string
@ -35,6 +40,8 @@ final class TaskException extends \Exception
}
/**
* @deprecated Use TaskFailureThrowable::getOriginalTraceAsString() instead.
*
* Gets the stack trace at the point the exception was thrown in the task.
*
* @return string

View File

@ -0,0 +1,86 @@
<?php
namespace Amp\Parallel\Worker;
use function Amp\Parallel\Sync\formatFlattenedBacktrace;
final class TaskFailureError extends TaskError implements TaskFailureThrowable
{
/** @var string */
private $originalMessage;
/** @var int|string */
private $originalCode;
/** @var string[] */
private $originalTrace;
/**
* @param string $className Original exception class name.
* @param string $message Original exception message.
* @param int|string $code Original exception code.
* @param array $trace Backtrace generated by
* {@see \Amp\Parallel\Sync\flattenThrowableBacktrace()}.
* @param TaskFailureThrowable|null $previous Instance representing any previous exception thrown in the Task.
*/
public function __construct(string $className, string $message, $code, array $trace, ?TaskFailureThrowable $previous = null)
{
$format = 'Uncaught %s in worker with message "%s" and code "%s"; use %s::getOriginalTrace() '
. 'for the stack trace in the worker';
parent::__construct(
$className,
\sprintf($format, $className, $message, $code, self::class),
formatFlattenedBacktrace($trace),
$previous
);
$this->originalMessage = $message;
$this->originalCode = $code;
$this->originalTrace = $trace;
}
/**
* @return string Original exception class name.
*/
public function getOriginalClassName(): string
{
return $this->getName();
}
/**
* @return string Original exception message.
*/
public function getOriginalMessage(): string
{
return $this->originalMessage;
}
/**
* @return int|string Original exception code.
*/
public function getOriginalCode()
{
return $this->originalCode;
}
/**
* Returns the original exception stack trace.
*
* @return array Same as {@see Throwable::getTrace()}, except all function arguments are formatted as strings.
*/
public function getOriginalTrace(): array
{
return $this->originalTrace;
}
/**
* Original backtrace flattened to a human-readable string.
*
* @return string
*/
public function getOriginalTraceAsString(): string
{
return $this->getWorkerTrace();
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace Amp\Parallel\Worker;
use function Amp\Parallel\Sync\formatFlattenedBacktrace;
final class TaskFailureException extends TaskException implements TaskFailureThrowable
{
/** @var string */
private $originalMessage;
/** @var int|string */
private $originalCode;
/** @var string[] */
private $originalTrace;
/**
* @param string $className Original exception class name.
* @param string $message Original exception message.
* @param int|string $code Original exception code.
* @param array $trace Backtrace generated by
* {@see \Amp\Parallel\Sync\flattenThrowableBacktrace()}.
* @param TaskFailureThrowable|null $previous Instance representing any previous exception thrown in the Task.
*/
public function __construct(string $className, string $message, $code, array $trace, ?TaskFailureThrowable $previous = null)
{
$format = 'Uncaught %s in worker with message "%s" and code "%s"; use %s::getOriginalTrace() '
. 'for the stack trace in the worker';
parent::__construct(
$className,
\sprintf($format, $className, $message, $code, self::class),
formatFlattenedBacktrace($trace),
$previous
);
$this->originalMessage = $message;
$this->originalCode = $code;
$this->originalTrace = $trace;
}
/**
* @return string Original exception class name.
*/
public function getOriginalClassName(): string
{
return $this->getName();
}
/**
* @return string Original exception message.
*/
public function getOriginalMessage(): string
{
return $this->originalMessage;
}
/**
* @return int|string Original exception code.
*/
public function getOriginalCode()
{
return $this->originalCode;
}
/**
* Returns the original exception stack trace.
*
* @return array Same as {@see Throwable::getTrace()}, except all function arguments are formatted as strings.
*/
public function getOriginalTrace(): array
{
return $this->originalTrace;
}
/**
* Original backtrace flattened to a human-readable string.
*
* @return string
*/
public function getOriginalTraceAsString(): string
{
return $this->getWorkerTrace();
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Amp\Parallel\Worker;
/**
* Common interface for exceptions thrown when Task::run() throws an exception when being executed in a worker.
*/
interface TaskFailureThrowable extends \Throwable
{
/**
* @return string Original exception class name.
*/
public function getOriginalClassName(): string;
/**
* @return string Original exception message.
*/
public function getOriginalMessage(): string;
/**
* @return int|string Original exception code.
*/
public function getOriginalCode();
/**
* Returns the original exception stack trace.
*
* @return array Same as {@see Throwable::getTrace()}, except all function arguments are formatted as strings.
*/
public function getOriginalTrace(): array;
/**
* Original backtrace flattened to a human-readable string.
*
* @return string
*/
public function getOriginalTraceAsString(): string;
}

View File

@ -5,7 +5,7 @@ namespace Amp\Parallel\Test\Context;
use Amp\Delayed;
use Amp\Parallel\Context\Context;
use Amp\Parallel\Context\ContextException;
use Amp\Parallel\Sync\PanicError;
use Amp\Parallel\Sync\ContextPanicError;
use Amp\PHPUnit\AsyncTestCase;
abstract class AbstractContextTest extends AsyncTestCase
@ -24,7 +24,7 @@ abstract class AbstractContextTest extends AsyncTestCase
public function testFailingProcess()
{
$this->expectException(PanicError::class);
$this->expectException(ContextPanicError::class);
$this->expectExceptionMessage('No string provided');
$context = $this->createContext(__DIR__ . "/Fixtures/test-process.php");
@ -34,7 +34,7 @@ abstract class AbstractContextTest extends AsyncTestCase
public function testThrowingProcessOnReceive()
{
$this->expectException(PanicError::class);
$this->expectException(ContextPanicError::class);
$this->expectExceptionMessage('Test message');
$context = $this->createContext(__DIR__ . "/Fixtures/throwing-process.php");
@ -44,7 +44,7 @@ abstract class AbstractContextTest extends AsyncTestCase
public function testThrowingProcessOnSend()
{
$this->expectException(PanicError::class);
$this->expectException(ContextPanicError::class);
$this->expectExceptionMessage('Test message');
$context = $this->createContext(__DIR__ . "/Fixtures/throwing-process.php");
@ -55,7 +55,7 @@ abstract class AbstractContextTest extends AsyncTestCase
public function testInvalidScriptPath()
{
$this->expectException(PanicError::class);
$this->expectException(ContextPanicError::class);
$this->expectExceptionMessage("No script found at '../test-process.php'");
$context = $this->createContext("../test-process.php");
@ -65,7 +65,7 @@ abstract class AbstractContextTest extends AsyncTestCase
public function testInvalidResult()
{
$this->expectException(PanicError::class);
$this->expectException(ContextPanicError::class);
$this->expectExceptionMessage('The given data cannot be sent because it is not serializable');
$context = $this->createContext(__DIR__ . "/Fixtures/invalid-result-process.php");
@ -75,7 +75,7 @@ abstract class AbstractContextTest extends AsyncTestCase
public function testNoCallbackReturned()
{
$this->expectException(PanicError::class);
$this->expectException(ContextPanicError::class);
$this->expectExceptionMessage('did not return a callable function');
$context = $this->createContext(__DIR__ . "/Fixtures/no-callback-process.php");
@ -85,7 +85,7 @@ abstract class AbstractContextTest extends AsyncTestCase
public function testParseError()
{
$this->expectException(PanicError::class);
$this->expectException(ContextPanicError::class);
$this->expectExceptionMessage('contains a parse error');
$context = $this->createContext(__DIR__ . "/Fixtures/parse-error-process.inc");

View File

@ -3,13 +3,13 @@
namespace Amp\Parallel\Test\Worker;
use Amp\Parallel\Context\StatusError;
use Amp\Parallel\Sync\PanicError;
use Amp\Parallel\Sync\ContextPanicError;
use Amp\Parallel\Sync\SerializationException;
use Amp\Parallel\Worker\BasicEnvironment;
use Amp\Parallel\Worker\Environment;
use Amp\Parallel\Worker\Task;
use Amp\Parallel\Worker\TaskError;
use Amp\Parallel\Worker\TaskException;
use Amp\Parallel\Worker\TaskFailureError;
use Amp\Parallel\Worker\TaskFailureException;
use Amp\Parallel\Worker\WorkerException;
use Amp\PHPUnit\AsyncTestCase;
@ -172,8 +172,8 @@ abstract class AbstractWorkerTest extends AsyncTestCase
try {
yield $worker->enqueue(new Fixtures\FailingTask(\Exception::class));
} catch (TaskException $exception) {
$this->assertSame(\Exception::class, $exception->getName());
} catch (TaskFailureException $exception) {
$this->assertSame(\Exception::class, $exception->getOriginalClassName());
}
yield $worker->shutdown();
@ -185,8 +185,8 @@ abstract class AbstractWorkerTest extends AsyncTestCase
try {
yield $worker->enqueue(new Fixtures\FailingTask(\Error::class));
} catch (TaskError $exception) {
$this->assertSame(\Error::class, $exception->getName());
} catch (TaskFailureError $exception) {
$this->assertSame(\Error::class, $exception->getOriginalClassName());
}
yield $worker->shutdown();
@ -198,11 +198,11 @@ abstract class AbstractWorkerTest extends AsyncTestCase
try {
yield $worker->enqueue(new Fixtures\FailingTask(\Error::class, \Exception::class));
} catch (TaskError $exception) {
$this->assertSame(\Error::class, $exception->getName());
} catch (TaskFailureError $exception) {
$this->assertSame(\Error::class, $exception->getOriginalClassName());
$previous = $exception->getPrevious();
$this->assertInstanceOf(TaskException::class, $previous);
$this->assertSame(\Exception::class, $previous->getName());
$this->assertInstanceOf(TaskFailureException::class, $previous);
$this->assertSame(\Exception::class, $previous->getOriginalClassName());
}
yield $worker->shutdown();
@ -215,8 +215,8 @@ abstract class AbstractWorkerTest extends AsyncTestCase
try {
yield $worker->enqueue(new NonAutoloadableTask);
$this->fail("Tasks that cannot be autoloaded should throw an exception");
} catch (TaskError $exception) {
$this->assertSame("Error", $exception->getName());
} catch (TaskFailureError $exception) {
$this->assertSame("Error", $exception->getOriginalClassName());
$this->assertGreaterThan(0, \strpos($exception->getMessage(), \sprintf("Classes implementing %s", Task::class)));
}
@ -248,7 +248,7 @@ abstract class AbstractWorkerTest extends AsyncTestCase
try {
yield $worker->enqueue(new Fixtures\UnserializableResultTask);
$this->fail("Tasks results that cannot be serialized should throw an exception");
} catch (TaskException $exception) {
} catch (TaskFailureException $exception) {
$this->assertSame(0, \strpos($exception->getMessage(), "Uncaught Amp\Parallel\Sync\SerializationException in worker"));
}
@ -296,7 +296,7 @@ abstract class AbstractWorkerTest extends AsyncTestCase
public function testInvalidCustomAutoloader()
{
$this->expectException(PanicError::class);
$this->expectException(ContextPanicError::class);
$this->expectExceptionMessage('No file found at bootstrap file path given');
$worker = $this->createWorker(BasicEnvironment::class, __DIR__ . '/Fixtures/not-found.php');

View File

@ -0,0 +1,28 @@
<?php
namespace Amp\Parallel\Test\Worker;
use Amp\Parallel\Worker\TaskFailureError;
use Amp\PHPUnit\AsyncTestCase;
class TaskFailureErrorTest extends AsyncTestCase
{
public function testOriginalMethods(): void
{
$trace = [
[
'function' => 'error_message_trace',
'file' => 'file-name.php',
'line' => 1,
'args' => [],
]
];
$exception = new TaskFailureError('name', 'error_message', 0, $trace);
$this->assertSame('name', $exception->getOriginalClassName());
$this->assertSame('error_message', $exception->getOriginalMessage());
$this->assertSame(0, $exception->getOriginalCode());
$this->assertSame($trace, $exception->getOriginalTrace());
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Amp\Parallel\Test\Worker;
use Amp\Parallel\Worker\TaskFailureException;
use Amp\PHPUnit\AsyncTestCase;
class TaskFailureExceptionTest extends AsyncTestCase
{
public function testOriginalMethods(): void
{
$trace = [
[
'function' => 'error_message_trace',
'file' => 'file-name.php',
'line' => 1,
'args' => [],
]
];
$exception = new TaskFailureException('name', 'error_message', 0, $trace);
$this->assertSame('name', $exception->getOriginalClassName());
$this->assertSame('error_message', $exception->getOriginalMessage());
$this->assertSame(0, $exception->getOriginalCode());
$this->assertSame($trace, $exception->getOriginalTrace());
}
}