sdk-php icon indicating copy to clipboard operation
sdk-php copied to clipboard

[Bug] Suppressed exception in worfklow child

Open root-aza opened this issue 6 months ago • 0 comments

What are you really trying to do?

I am starting the workflow

Describe the bug

If an exception is thrown from PayloadConverter, it cannot be processed and is not visible in the logs or in ui temporal itself. I would like to be able to send this exception to sentry/logs

Minimal Reproduction

ChildWorkflow Case
<?php

final class BRequest
{
    public function __construct(
        public string $id,
    ) {
    }
}


#[WorkflowInterface]
#[AssignWorker('mdm.client_verification.workflow')]
final class BWorkflow
{
    #[WorkflowMethod('BWorkflow')]
    public function start(BRequest $request): Generator
    {
        yield Workflow::timer(CarbonInterval::minutes(1));
    }
}


#[WorkflowInterface]
#[AssignWorker('mdm.client_verification.workflow')]
final class AWorkflow
{
    #[WorkflowMethod('AWorkflow')]
    public function start(): Generator
    {
        $workflow = Workflow::newChildWorkflowStub(
            BWorkflow::class,
            Workflow\ChildWorkflowOptions::new()
                ->withTaskQueue('mdm.client_verification.workflow')
                ->withNamespace(Workflow::getInfo()->namespace)
        );

        yield $workflow->start(new BRequest('123'));
    }
}
DataConverter
<?php

/**
 * Temporal Bundle
 *
 * @author Vlad Shashkov <[email protected]>
 * @copyright Copyright (c) 2023, The Vanta
 */

declare(strict_types=1);

namespace Vanta\Integration\Symfony\Temporal\DataConverter;

use App\ClientVerification\Workflow\Join\BRequest;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer as ObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface as Serializer;
use Temporal\Api\Common\V1\Payload;
use Temporal\DataConverter\EncodingKeys;
use Temporal\DataConverter\JsonConverter;
use Temporal\DataConverter\PayloadConverterInterface as PayloadConverter;
use Temporal\DataConverter\Type;
use Temporal\Exception\DataConverterException;
use Throwable;

final readonly class SymfonySerializerDataConverter implements PayloadConverter
{
    private const INPUT_TYPE = 'symfony.serializer.type';


    public function __construct(
        private Serializer $serializer,
        private PayloadConverter $payloadConverter = new JsonConverter(),
    ) {
    }


    public function getEncodingType(): string
    {
        return EncodingKeys::METADATA_ENCODING_JSON;
    }

    public function toPayload($value): Payload
    {
        $metadata = [
            EncodingKeys::METADATA_ENCODING_KEY => $this->getEncodingType(),
        ];

        $context = [ObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true];

        if (is_object($value)) {
            $metadata[self::INPUT_TYPE] = $value::class;
        }

        try {
            $data = $this->serializer->serialize($value, 'json', $context);
        } catch (Throwable $e) {
            throw new DataConverterException($e->getMessage(), $e->getCode(), $e);
        }

        $payload = new Payload();
        $payload->setMetadata($metadata);
        $payload->setData($data);

        return $payload;
    }

    public function fromPayload(Payload $payload, Type $type): mixed
    {
        if ("null" == $payload->getData() && $type->allowsNull()) {
            return null;
        }

        /** @var string|null $inputType */
        $inputType = $payload->getMetadata()[self::INPUT_TYPE] ?? null;


        if ($inputType == BRequest::class) {
            // dump
            throw new DataConverterException('BAM', 3, null);
        }

        if (!$type->isClass() && $inputType == null) {
            return $this->payloadConverter->fromPayload($payload, $type);
        }

        try {
            return $this->serializer->deserialize($payload->getData(), $inputType ?? $type->getName(), 'json');
        } catch (Throwable $e) {
            throw new DataConverterException($e->getMessage(), $e->getCode(), $e);
        }
    }
}
Temporal UI Image
Event history

594e549a-bb6d-4d0d-8415-6172ee9e4d56_events.json

ContinueAsNew Case
<?php

#[WorkflowInterface]
#[AssignWorker('mdm.client_verification.workflow')]
final class AWorkflow
{
    #[WorkflowMethod('AWorkflow')]
    public function start(?stdClass $foo = null): Generator
    {

        yield Workflow::timer(CarbonInterval::second(30));


        $workflow = Workflow::newContinueAsNewStub(
            self::class,
            ContinueAsNewOptions::new()
                 ->withTaskQueue('mdm.client_verification.workflow')
        );

        yield $workflow->start(new stdClass());
    }
}

DataConverter
<?php

/**
 * Temporal Bundle
 *
 * @author Vlad Shashkov <[email protected]>
 * @copyright Copyright (c) 2023, The Vanta
 */

declare(strict_types=1);

namespace Vanta\Integration\Symfony\Temporal\DataConverter;

use App\ClientVerification\Workflow\Join\BRequest;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer as ObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface as Serializer;
use Temporal\Api\Common\V1\Payload;
use Temporal\DataConverter\EncodingKeys;
use Temporal\DataConverter\JsonConverter;
use Temporal\DataConverter\PayloadConverterInterface as PayloadConverter;
use Temporal\DataConverter\Type;
use Temporal\Exception\DataConverterException;
use Throwable;

final readonly class SymfonySerializerDataConverter implements PayloadConverter
{
    private const INPUT_TYPE = 'symfony.serializer.type';


    public function __construct(
        private Serializer $serializer,
        private PayloadConverter $payloadConverter = new JsonConverter(),
    ) {
    }


    public function getEncodingType(): string
    {
        return EncodingKeys::METADATA_ENCODING_JSON;
    }

    public function toPayload($value): Payload
    {
        $metadata = [
            EncodingKeys::METADATA_ENCODING_KEY => $this->getEncodingType(),
        ];

        $context = [ObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true];

        if (is_object($value)) {
            $metadata[self::INPUT_TYPE] = $value::class;
        }

        try {
            $data = $this->serializer->serialize($value, 'json', $context);
        } catch (Throwable $e) {
            throw new DataConverterException($e->getMessage(), $e->getCode(), $e);
        }

        $payload = new Payload();
        $payload->setMetadata($metadata);
        $payload->setData($data);

        return $payload;
    }

    public function fromPayload(Payload $payload, Type $type): mixed
    {
        if ("null" == $payload->getData() && $type->allowsNull()) {
            return null;
        }

        /** @var string|null $inputType */
        $inputType = $payload->getMetadata()[self::INPUT_TYPE] ?? null;


        if ($inputType == \stdClass::class) {
            // workflow broken 💥
            throw new DataConverterException('BAM', 3, null);
        }

        if (!$type->isClass() && $inputType == null) {
            return $this->payloadConverter->fromPayload($payload, $type);
        }

        try {
            return $this->serializer->deserialize($payload->getData(), $inputType ?? $type->getName(), 'json');
        } catch (Throwable $e) {
            throw new DataConverterException($e->getMessage(), $e->getCode(), $e);
        }
    }
}
Temporal UI Image Image
Event history

dc5c2461-ab3e-4db6-9b3d-b9975f590159_events.json

ce120686-533e-4f9a-b7fd-c6f164ebf23c_events.json

Environment/Versions

  • Temporal sdk 2.15.1
  • RoadRunner 2025.1.2
  • Temporal server 1.24.2

root-aza avatar Jul 13 '25 06:07 root-aza