Skip to content

[Bug] Suppressed exception in worfklow child #636

@root-aza

Description

@root-aza

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions