Skip to content

Special behaviour for temporal prefixes #1644

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
24 changes: 24 additions & 0 deletions packages/common/src/reserved.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const TEMPORAL_RESERVED_PREFIX = '__temporal_';
export const STACK_TRACE_RESERVED_PREFIX = '__stack_trace';
export const ENHANCED_STACK_TRACE_RESERVED_PREFIX = '__enhanced_stack_trace';

export const reservedPrefixes = [
TEMPORAL_RESERVED_PREFIX,
STACK_TRACE_RESERVED_PREFIX,
ENHANCED_STACK_TRACE_RESERVED_PREFIX,
];

export function throwIfReservedName(type: string, name: string): void {
const prefix = isReservedName(name);
if (prefix) {
throw Error(`Cannot register ${type} name: '${name}', with reserved prefix: '${prefix}'`);
}
}

export function isReservedName(name: string): string | undefined {
for (const prefix of reservedPrefixes) {
if (name.startsWith(prefix)) {
return prefix;
}
}
}
98 changes: 97 additions & 1 deletion packages/test/src/test-integration-split-two.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ import {
import { msToNumber, tsToMs } from '@temporalio/common/lib/time';
import { decode as payloadDecode, decodeFromPayloadsAtIndex } from '@temporalio/common/lib/internal-non-workflow';

import { condition, defineQuery, defineSignal, setDefaultQueryHandler, setHandler, sleep } from '@temporalio/workflow';
import {
condition,
defineQuery,
defineSignal,
defineUpdate,
setDefaultQueryHandler,
setHandler,
sleep,
} from '@temporalio/workflow';
import { reservedPrefixes } from '@temporalio/common/lib/reserved';
import { configurableHelpers, createTestWorkflowBundle } from './helpers-integration';
import * as activities from './activities';
import * as workflows from './workflows';
Expand Down Expand Up @@ -751,3 +760,90 @@ test('default query handler is not used if requested query exists', configMacro,
t.deepEqual(result, { name: definedQuery.name, args });
});
});

test('Cannot register activities using reserved prefixes', configMacro, async (t, config) => {
const { createWorkerWithDefaults } = config;

for (const prefix of reservedPrefixes) {
const activityName = prefix + '_test';
await t.throwsAsync(
createWorkerWithDefaults(t, {
activities: { [activityName]: () => {} },
}),
{
instanceOf: Error,
message: `Cannot register activity name: '${activityName}', with reserved prefix: '${prefix}'`,
}
);
}
});

test('Cannot register task queues using reserved prefixes', configMacro, async (t, config) => {
const { createWorkerWithDefaults } = config;

for (const prefix of reservedPrefixes) {
const taskQueue = prefix + '_test';

await t.throwsAsync(
createWorkerWithDefaults(t, {
taskQueue,
}),
{
instanceOf: Error,
message: `Cannot register task queue name: '${taskQueue}', with reserved prefix: '${prefix}'`,
}
);
}
});

interface HandlerError {
name: string;
message: string;
}

export async function workflowBadPrefixHandler(prefix: string): Promise<HandlerError[]> {
// Re-package errors, default payload converter has trouble converting native errors (no 'data' field).
const expectedErrors: HandlerError[] = [];
try {
setHandler(defineSignal(prefix + '_signal'), () => {});
} catch (e) {
if (e instanceof Error) {
expectedErrors.push({ name: e.name, message: e.message });
}
}
try {
setHandler(defineUpdate(prefix + '_update'), () => {});
} catch (e) {
if (e instanceof Error) {
expectedErrors.push({ name: e.name, message: e.message });
}
}
try {
setHandler(defineQuery(prefix + '_query'), () => {});
} catch (e) {
if (e instanceof Error) {
expectedErrors.push({ name: e.name, message: e.message });
}
}
return expectedErrors;
}

test('Workflow failure if define signals/updates/queries with reserved prefixes', configMacro, async (t, config) => {
const { env, createWorkerWithDefaults } = config;
const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env);
const worker = await createWorkerWithDefaults(t);
await worker.runUntil(async () => {
const prefix = reservedPrefixes[0];
// for (const prefix of reservedPrefixes) {
const result = await executeWorkflow(workflowBadPrefixHandler, {
args: [prefix],
});
console.log('result', result);
t.deepEqual(result, [
{ name: 'Error', message: `Cannot register signal name: '${prefix}_signal', with reserved prefix: '${prefix}'` },
{ name: 'Error', message: `Cannot register update name: '${prefix}_update', with reserved prefix: '${prefix}'` },
{ name: 'Error', message: `Cannot register query name: '${prefix}_query', with reserved prefix: '${prefix}'` },
]);
// }
});
});
86 changes: 86 additions & 0 deletions packages/test/src/test-workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ import { VMWorkflow, VMWorkflowCreator } from '@temporalio/worker/lib/workflow/v
import { SdkFlag, SdkFlags } from '@temporalio/workflow/lib/flags';
import { ReusableVMWorkflow, ReusableVMWorkflowCreator } from '@temporalio/worker/lib/workflow/reusable-vm';
import { parseWorkflowCode } from '@temporalio/worker/lib/worker';
import {
ENHANCED_STACK_TRACE_RESERVED_PREFIX,
reservedPrefixes,
STACK_TRACE_RESERVED_PREFIX,
TEMPORAL_RESERVED_PREFIX,
} from '@temporalio/common/lib/reserved';
import * as activityFunctions from './activities';
import { cleanStackTrace, REUSE_V8_CONTEXT, u8 } from './helpers';
import { ProcessedSignal } from './workflows';
Expand Down Expand Up @@ -2528,3 +2534,83 @@ test('Signals/Updates/Activities/Timers - Trace promises completion order - 1.11
);
}
});

test('Default query handler fail activations with reserved names - workflowWithDefaultHandlers', async (t) => {
const { workflowType } = t.context;

await activate(t, makeActivation(undefined, makeInitializeWorkflowJob(workflowType)));

for (const prefix of reservedPrefixes) {
const completion = await activate(t, makeActivation(undefined, makeQueryWorkflowJob('1', prefix + '_query')));

compareCompletion(t, completion, {
failed: {
failure: {
...completion.failed?.failure,
// We only care about the error message.
message: `Cannot register query name: '${prefix}_query', with reserved prefix: '${prefix}'`,
},
},
});
}
});

test('Default signal handler fail activations with temporal prefix - workflowWithDefaultHandlers', async (t) => {
const { workflowType } = t.context;

await activate(t, makeActivation(undefined, makeInitializeWorkflowJob(workflowType)));
const signalName = TEMPORAL_RESERVED_PREFIX + '_signal';
const job = makeSignalWorkflowJob(signalName, []);

const completion = await activate(t, makeActivation(undefined, job));

compareCompletion(t, completion, {
failed: {
failure: {
...completion.failed?.failure,
// We only care about the error message.
message: `Cannot register signal name: '${signalName}', with reserved prefix: '${TEMPORAL_RESERVED_PREFIX}'`,
},
},
});
});

test('Default signal handler fail activations with stack trace prefix - workflowWithDefaultHandlers', async (t) => {
const { workflowType } = t.context;

await activate(t, makeActivation(undefined, makeInitializeWorkflowJob(workflowType)));
const signalName = STACK_TRACE_RESERVED_PREFIX + '_signal';
const job = makeSignalWorkflowJob(signalName, []);

const completion = await activate(t, makeActivation(undefined, job));

compareCompletion(t, completion, {
failed: {
failure: {
...completion.failed?.failure,
// We only care about the error message.
message: `Cannot register signal name: '${signalName}', with reserved prefix: '${STACK_TRACE_RESERVED_PREFIX}'`,
},
},
});
});

test('Default signal handler fail activations with enhanced stack trace prefix - workflowWithDefaultHandlers', async (t) => {
const { workflowType } = t.context;

await activate(t, makeActivation(undefined, makeInitializeWorkflowJob(workflowType)));
const signalName = ENHANCED_STACK_TRACE_RESERVED_PREFIX + '_signal';
const job = makeSignalWorkflowJob(signalName, []);

const completion = await activate(t, makeActivation(undefined, job));

compareCompletion(t, completion, {
failed: {
failure: {
...completion.failed?.failure,
// We only care about the error message.
message: `Cannot register signal name: '${signalName}', with reserved prefix: '${ENHANCED_STACK_TRACE_RESERVED_PREFIX}'`,
},
},
});
});
1 change: 1 addition & 0 deletions packages/test/src/workflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ export * from './upsert-and-read-search-attributes';
export * from './wait-on-user';
export * from './workflow-cancellation-scenarios';
export * from './upsert-and-read-memo';
export * from './workflow-with-default-handlers';
16 changes: 16 additions & 0 deletions packages/test/src/workflows/workflow-with-default-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
condition,
defineSignal,
setDefaultQueryHandler,
setDefaultSignalHandler,
setHandler,
} from '@temporalio/workflow';

export async function workflowWithDefaultHandlers(): Promise<void> {
const complete = true;
setDefaultQueryHandler(() => {});
setDefaultSignalHandler(() => {});
setHandler(defineSignal('completeSignal'), () => {});

await condition(() => complete);
}
4 changes: 4 additions & 0 deletions packages/worker/src/worker-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { LoggerSinks } from '@temporalio/workflow';
import { Context } from '@temporalio/activity';
import { checkExtends } from '@temporalio/common/lib/type-helpers';
import { WorkerOptions as NativeWorkerOptions, WorkerTuner as NativeWorkerTuner } from '@temporalio/core-bridge';
import { throwIfReservedName } from '@temporalio/common/lib/reserved';
import { ActivityInboundLogInterceptor } from './activity-log-interceptor';
import { NativeConnection } from './connection';
import { CompiledWorkerInterceptors, WorkerInterceptors } from './interceptors';
Expand Down Expand Up @@ -822,6 +823,9 @@ export function compileWorkerOptions(rawOpts: WorkerOptions, logger: Logger): Co
}

const activities = new Map(Object.entries(opts.activities ?? {}).filter(([_, v]) => typeof v === 'function'));
for (const activityName of activities.keys()) {
throwIfReservedName('activity', activityName);
}
const tuner = asNativeTuner(opts.tuner, logger);

return {
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import * as native from '@temporalio/core-bridge';
import { ShutdownError, UnexpectedError } from '@temporalio/core-bridge';
import { coresdk, temporal } from '@temporalio/proto';
import { type SinkCall, type WorkflowInfo } from '@temporalio/workflow';
import { throwIfReservedName } from '@temporalio/common/lib/reserved';
import { Activity, CancelReason, activityLogAttributes } from './activity';
import { extractNativeClient, extractReferenceHolders, InternalNativeConnection, NativeConnection } from './connection';
import { ActivityExecuteInput } from './interceptors';
Expand Down Expand Up @@ -462,6 +463,7 @@ export class Worker {
* This method initiates a connection to the server and will throw (asynchronously) on connection failure.
*/
public static async create(options: WorkerOptions): Promise<Worker> {
throwIfReservedName('task queue', options.taskQueue);
const logger = withMetadata(Runtime.instance().logger, {
sdkComponent: SdkComponent.worker,
taskQueue: options.taskQueue ?? 'default',
Expand Down
37 changes: 29 additions & 8 deletions packages/workflow/src/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ import {
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
import type { coresdk, temporal } from '@temporalio/proto';
import {
ENHANCED_STACK_TRACE_RESERVED_PREFIX,
STACK_TRACE_RESERVED_PREFIX,
isReservedName,
throwIfReservedName,
} from '@temporalio/common/lib/reserved';
import { alea, RNG } from './alea';
import { RootCancellationScope } from './cancellation-scope';
import { UpdateScope } from './update-scope';
Expand Down Expand Up @@ -249,7 +255,7 @@ export class Activator implements ActivationHandler {
*/
public readonly queryHandlers = new Map<string, WorkflowQueryAnnotatedType>([
[
'__stack_trace',
STACK_TRACE_RESERVED_PREFIX,
{
handler: () => {
return this.getStackTraces()
Expand All @@ -260,7 +266,7 @@ export class Activator implements ActivationHandler {
},
],
[
'__enhanced_stack_trace',
ENHANCED_STACK_TRACE_RESERVED_PREFIX,
{
handler: (): EnhancedStackTrace => {
const { sourceMap } = this;
Expand Down Expand Up @@ -619,6 +625,8 @@ export class Activator implements ActivationHandler {
protected queryWorkflowNextHandler({ queryName, args }: QueryInput): Promise<unknown> {
let fn = this.queryHandlers.get(queryName)?.handler;
if (fn === undefined && this.defaultQueryHandler !== undefined) {
// Do not call default query handler with reserved query name.
throwIfReservedName('query', queryName);
fn = this.defaultQueryHandler.bind(this, queryName);
}
// No handler or default registered, fail.
Expand Down Expand Up @@ -649,17 +657,28 @@ export class Activator implements ActivationHandler {
throw new TypeError('Missing query activation attributes');
}

const queryInput = {
queryName: queryType,
args: arrayFromPayloads(this.payloadConverter, activation.arguments),
queryId,
headers: headers ?? {},
};

// Skip interceptors if this is an internal query.
if (isReservedName(queryType)) {
this.queryWorkflowNextHandler(queryInput).then(
(result) => this.completeQuery(queryId, result),
(reason) => this.failQuery(queryId, reason)
);
return;
}

const execute = composeInterceptors(
this.interceptors.inbound,
'handleQuery',
this.queryWorkflowNextHandler.bind(this)
);
execute({
queryName: queryType,
args: arrayFromPayloads(this.payloadConverter, activation.arguments),
queryId,
headers: headers ?? {},
}).then(
execute(queryInput).then(
(result) => this.completeQuery(queryId, result),
(reason) => this.failQuery(queryId, reason)
);
Expand Down Expand Up @@ -797,6 +816,8 @@ export class Activator implements ActivationHandler {
if (fn) {
return await fn(...args);
} else if (this.defaultSignalHandler) {
// Do not call default signal handler with reserved signal name.
throwIfReservedName('signal', signalName);
return await this.defaultSignalHandler(signalName, ...args);
} else {
throw new IllegalStateError(`No registered signal handler for signal: ${signalName}`);
Expand Down
3 changes: 3 additions & 0 deletions packages/workflow/src/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { versioningIntentToProto } from '@temporalio/common/lib/versioning-inten
import { Duration, msOptionalToTs, msToNumber, msToTs, requiredTsToMs } from '@temporalio/common/lib/time';
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
import { temporal } from '@temporalio/proto';
import { throwIfReservedName } from '@temporalio/common/lib/reserved';
import { CancellationScope, registerSleepImplementation } from './cancellation-scope';
import { UpdateScope } from './update-scope';
import {
Expand Down Expand Up @@ -1258,6 +1259,8 @@ export function setHandler<
options?: QueryHandlerOptions | SignalHandlerOptions | UpdateHandlerOptions<Args>
): void {
const activator = assertInWorkflowContext('Workflow.setHandler(...) may only be used from a Workflow Execution.');
// Cannot register handler for reserved names
throwIfReservedName(def.type, def.name);
const description = options?.description;
if (def.type === 'update') {
if (typeof handler === 'function') {
Expand Down
Loading