Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add RawValue support for non-converted Payloads #1664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion packages/common/src/converter/payload-converter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { decode, encode } from '../encoding';
import { PayloadConverterError, ValueError } from '../errors';
import { Payload } from '../interfaces';
import { encodingKeys, encodingTypes, METADATA_ENCODING_KEY } from './types';
import { encodingKeys, encodingTypes, METADATA_ENCODING_KEY, METADATA_RAW_VALUE_KEY } from './types';

/**
* Used by the framework to serialize/deserialize data like parameters and return values.
Expand Down Expand Up @@ -100,6 +100,60 @@ export function mapFromPayloads<K extends string, T = unknown>(
) as Record<K, T>;
}

/**
* RawValue is a wrapper over a payload.
* A payload that belongs to a RawValue is special in that it bypasses normal payload conversion,
* but still undergoes codec conversion.
*/
export class RawValue {
private readonly _payload: Payload;

/**
* Receives an incoming Payload and returns a RawValue wrapping it.
*
* Notably, this method strips any raw value metadata if it exists.
* This allows users to convert the payload back to its native representation.
*
* @param payload the incoming Payload
* @returns an instance of RawValue
*/
static receive(payload: Payload): RawValue {
if (payload.metadata == null) {
throw new ValueError('Missing payload metadata');
}
// Remove the raw value identifier key (if it exists).
delete payload.metadata[METADATA_RAW_VALUE_KEY];
return new RawValue(payload);
}

constructor(payload: Payload) {
this._payload = payload;
}

get payload(): Payload {
return this._payload;
}

/**
* Sends the Payload from the RawValue.
*
* Notably, this method add raw value metadata to identify that the Payload
* belongs to a RawValue when we {@link receive}.
*
* @param payload the incoming Payload
* @returns an instance of RawValue
*/
send(): Payload {
return {
metadata: {
...this.payload.metadata,
[METADATA_RAW_VALUE_KEY]: encode('true'),
},
data: this.payload.data,
};
}
}

export interface PayloadConverterWithEncoding {
/**
* Converts a value to a {@link Payload}.
Expand Down Expand Up @@ -143,6 +197,9 @@ export class CompositePayloadConverter implements PayloadConverter {
* Returns the first successful result, throws {@link ValueError} if there is no converter that can handle the value.
*/
public toPayload<T>(value: T): Payload {
if (value instanceof RawValue) {
return value.send();
}
for (const converter of this.converters) {
const result = converter.toPayload(value);
if (result !== undefined) {
Expand All @@ -160,6 +217,12 @@ export class CompositePayloadConverter implements PayloadConverter {
if (payload.metadata === undefined || payload.metadata === null) {
throw new ValueError('Missing payload metadata');
}
// Payload is intended to be a RawValue.
// Avoid payload conversion, return payload wrapped as RawValue.
if (payload.metadata[METADATA_RAW_VALUE_KEY]) {
return RawValue.receive(payload) as T;
}

const encoding = decode(payload.metadata[METADATA_ENCODING_KEY]);
const converter = this.converterByEncoding.get(encoding);
if (converter === undefined) {
Expand Down
6 changes: 6 additions & 0 deletions packages/common/src/converter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ export const encodingKeys = {
} as const;

export const METADATA_MESSAGE_TYPE_KEY = 'messageType';

/**
* Metadata key used to identify a RawValue payload.
* A RawValue payload is a payload intended to bypass normal payload conversion.
*/
export const METADATA_RAW_VALUE_KEY = 'rawValue';
45 changes: 44 additions & 1 deletion packages/test/src/test-integration-workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import {
ActivityCancellationType,
ApplicationFailure,
defineSearchAttributeKey,
METADATA_ENCODING_KEY,
RawValue,
SearchAttributePair,
SearchAttributeType,
TypedSearchAttributes,
WorkflowExecutionAlreadyStartedError,
} from '@temporalio/common';
import { temporal } from '@temporalio/proto';
import { signalSchedulingWorkflow } from './activities/helpers';
import { activityStartedSignal } from './workflows/definitions';
import * as workflows from './workflows';
Expand Down Expand Up @@ -384,7 +387,10 @@ test('Query workflow metadata returns handler descriptions', async (t) => {

await worker.runUntil(async () => {
const handle = await startWorkflow(queryWorkflowMetadata);
const meta = await handle.query(workflow.workflowMetadataQuery);
const rawValue = await handle.query(workflow.workflowMetadataQuery);
const meta = workflow.defaultPayloadConverter.fromPayload(
rawValue.payload
) as temporal.api.sdk.v1.IWorkflowMetadata;
t.is(meta.definition?.type, 'queryWorkflowMetadata');
const queryDefinitions = meta.definition?.queryDefinitions;
// Three built-in ones plus dummyQuery1 and dummyQuery2
Expand Down Expand Up @@ -1337,3 +1343,40 @@ test('can register search attributes to dev server', async (t) => {
t.deepEqual(desc.searchAttributes, { 'new-search-attr': [12] }); // eslint-disable-line deprecation/deprecation
await env.teardown();
});

export async function rawValueWorkflow(rawValue: RawValue): Promise<RawValue> {
const { rawValueActivity } = workflow.proxyActivities({ startToCloseTimeout: '10s' });
return await rawValueActivity(rawValue);
}

test('workflow and activity can receive/return RawValue', async (t) => {
const { executeWorkflow, createWorker } = helpers(t);
const worker = await createWorker({
activities: {
async rawValueActivity(rawValue: RawValue): Promise<RawValue> {
return rawValue;
},
},
});

await worker.runUntil(async () => {
const testValue = 'test';
const rawValueWithKey: RawValue = new RawValue(workflow.defaultPayloadConverter.toPayload(testValue));
const res = await executeWorkflow(rawValueWorkflow, {
args: [rawValueWithKey],
});
// Compare payloads. Explicitly convert to Uint8Array because the actual
// returned payload has Buffer types.
console.log('RETURNED', res);
const actualMetadata = res.payload.metadata![METADATA_ENCODING_KEY];
const expectedMetadata = rawValueWithKey.payload.metadata![METADATA_ENCODING_KEY];
t.deepEqual(new Uint8Array(actualMetadata), new Uint8Array(expectedMetadata));
const actualData = res.payload.data!;
const expectedData = rawValueWithKey.payload.data!;
t.deepEqual(new Uint8Array(actualData), new Uint8Array(expectedData));

// Compare value from wrapped payload.
const resValue = workflow.defaultPayloadConverter.fromPayload(res.payload);
t.deepEqual(resValue, testValue);
});
});
23 changes: 13 additions & 10 deletions packages/workflow/src/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import {
WorkflowUpdateValidatorType,
mapFromPayloads,
fromPayloadsAtIndex,
RawValue,
} from '@temporalio/common';
import {
decodeSearchAttributes,
decodeTypedSearchAttributes,
} from '@temporalio/common/lib/converter/payload-search-attributes';
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
import type { coresdk, temporal } from '@temporalio/proto';
import type { coresdk } from '@temporalio/proto';
import { alea, RNG } from './alea';
import { RootCancellationScope } from './cancellation-scope';
import { UpdateScope } from './update-scope';
Expand Down Expand Up @@ -292,7 +293,7 @@ export class Activator implements ActivationHandler {
[
'__temporal_workflow_metadata',
{
handler: (): temporal.api.sdk.v1.IWorkflowMetadata => {
handler: (): RawValue => {
const workflowType = this.info.workflowType;
const queryDefinitions = Array.from(this.queryHandlers.entries()).map(([name, value]) => ({
name,
Expand All @@ -306,14 +307,16 @@ export class Activator implements ActivationHandler {
name,
description: value.description,
}));
return {
definition: {
type: workflowType,
queryDefinitions,
signalDefinitions,
updateDefinitions,
},
};
return new RawValue(
this.payloadConverter.toPayload({
definition: {
type: workflowType,
queryDefinitions,
signalDefinitions,
updateDefinitions,
},
})
);
},
description: 'Returns metadata associated with this workflow.',
},
Expand Down
4 changes: 2 additions & 2 deletions packages/workflow/src/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
WorkflowReturnType,
WorkflowUpdateValidatorType,
SearchAttributeUpdatePair,
RawValue,
} from '@temporalio/common';
import {
encodeUnifiedSearchAttributes,
Expand All @@ -30,7 +31,6 @@ import {
import { versioningIntentToProto } from '@temporalio/common/lib/versioning-intent-enum';
import { Duration, msOptionalToTs, msToNumber, msToTs, requiredTsToMs } from '@temporalio/common/lib/time';
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
import { temporal } from '@temporalio/proto';
import { CancellationScope, registerSleepImplementation } from './cancellation-scope';
import { UpdateScope } from './update-scope';
import {
Expand Down Expand Up @@ -1589,4 +1589,4 @@ export function allHandlersFinished(): boolean {

export const stackTraceQuery = defineQuery<string>('__stack_trace');
export const enhancedStackTraceQuery = defineQuery<EnhancedStackTrace>('__enhanced_stack_trace');
export const workflowMetadataQuery = defineQuery<temporal.api.sdk.v1.IWorkflowMetadata>('__temporal_workflow_metadata');
export const workflowMetadataQuery = defineQuery<RawValue>('__temporal_workflow_metadata');
Loading