@temporalio/worker
Version:
Temporal.io SDK Worker sub-package
219 lines (205 loc) • 8.82 kB
text/typescript
import 'abort-controller/polyfill'; // eslint-disable-line import/no-unassigned-import
import { asyncLocalStorage, CompleteAsyncError, Context, Info } from '@temporalio/activity';
import {
ActivityFunction,
ApplicationFailure,
ApplicationFailureCategory,
CancelledFailure,
ensureApplicationFailure,
FAILURE_SOURCE,
IllegalStateError,
LoadedDataConverter,
MetricMeter,
MetricTags,
SdkComponent,
} from '@temporalio/common';
import { encodeErrorToFailure, encodeToPayload } from '@temporalio/common/lib/internal-non-workflow';
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
import { isAbortError } from '@temporalio/common/lib/type-helpers';
import { Logger, LoggerWithComposedMetadata } from '@temporalio/common/lib/logger';
import { MetricMeterWithComposedTags } from '@temporalio/common/lib/metrics';
import { coresdk } from '@temporalio/proto';
import {
ActivityExecuteInput,
ActivityInboundCallsInterceptor,
ActivityInterceptorsFactory,
ActivityOutboundCallsInterceptor,
} from './interceptors';
const UNINITIALIZED = Symbol('UNINITIALIZED');
export type CancelReason =
| keyof typeof coresdk.activity_task.ActivityCancelReason
| 'WORKER_SHUTDOWN'
| 'HEARTBEAT_DETAILS_CONVERSION_FAILED';
export class Activity {
protected cancelReason?: CancelReason;
public readonly context: Context;
public cancel: (reason: CancelReason) => void = () => undefined;
public readonly abortController: AbortController = new AbortController();
public readonly interceptors: {
inbound: ActivityInboundCallsInterceptor[];
outbound: ActivityOutboundCallsInterceptor[];
};
/**
* Logger bound to `sdkComponent: worker`, with metadata from this activity.
* This is the logger to use for all log messages emitted by the activity
* worker. Note this is not exactly the same thing as the activity context
* logger, which is bound to `sdkComponent: activity`.
*/
private readonly workerLogger;
/**
* Metric Meter with tags from this activity, including tags from interceptors.
*/
private readonly metricMeter;
constructor(
public readonly info: Info,
public readonly fn: ActivityFunction<any[], any> | undefined,
public readonly dataConverter: LoadedDataConverter,
public readonly heartbeatCallback: Context['heartbeat'],
workerLogger: Logger,
workerMetricMeter: MetricMeter,
interceptors: ActivityInterceptorsFactory[]
) {
this.workerLogger = LoggerWithComposedMetadata.compose(workerLogger, this.getLogAttributes.bind(this));
this.metricMeter = MetricMeterWithComposedTags.compose(workerMetricMeter, this.getMetricTags.bind(this));
const promise = new Promise<never>((_, reject) => {
this.cancel = (reason: CancelReason) => {
this.cancelReason = reason;
const err = new CancelledFailure(reason);
this.abortController.abort(err);
reject(err);
};
});
this.context = new Context(
info,
promise,
this.abortController.signal,
this.heartbeatCallback,
// This is the activity context logger, to be used exclusively from user code
LoggerWithComposedMetadata.compose(this.workerLogger, { sdkComponent: SdkComponent.activity }),
this.metricMeter
);
// Prevent unhandled rejection
promise.catch(() => undefined);
this.interceptors = { inbound: [], outbound: [] };
interceptors
.map((factory) => factory(this.context))
.forEach(({ inbound, outbound }) => {
if (inbound) this.interceptors.inbound.push(inbound);
if (outbound) this.interceptors.outbound.push(outbound);
});
}
protected getLogAttributes(): Record<string, unknown> {
const logAttributes = activityLogAttributes(this.info);
// In case some interceptor uses the logger while initializing...
if (this.interceptors == null) return logAttributes;
return composeInterceptors(this.interceptors.outbound, 'getLogAttributes', (a) => a)(logAttributes);
}
protected getMetricTags(): MetricTags {
const baseTags = {
namespace: this.info.workflowNamespace,
taskQueue: this.info.taskQueue,
activityType: this.info.activityType,
};
// In case some interceptors use the metric meter while initializing...
if (this.interceptors == null) return baseTags;
return composeInterceptors(this.interceptors.outbound, 'getMetricTags', (a) => a)(baseTags);
}
/**
* Actually executes the function.
*
* Any call up to this function and including this one will be trimmed out of stack traces.
*/
protected async execute(fn: ActivityFunction<any[], any>, input: ActivityExecuteInput): Promise<unknown> {
let error: any = UNINITIALIZED; // In case someone decides to throw undefined...
const startTime = process.hrtime.bigint();
this.workerLogger.debug('Activity started');
try {
const executeNextHandler = ({ args }: any) => fn(...args);
const executeWithInterceptors = composeInterceptors(this.interceptors.inbound, 'execute', executeNextHandler);
return await executeWithInterceptors(input);
} catch (err: any) {
error = err;
throw err;
} finally {
const durationNanos = process.hrtime.bigint() - startTime;
const durationMs = Number(durationNanos / 1_000_000n);
if (error === UNINITIALIZED) {
this.workerLogger.debug('Activity completed', { durationMs });
} else if (
(error instanceof CancelledFailure || isAbortError(error)) &&
this.context.cancellationSignal.aborted
) {
this.workerLogger.debug('Activity completed as cancelled', { durationMs });
} else if (error instanceof CompleteAsyncError) {
this.workerLogger.debug('Activity will complete asynchronously', { durationMs });
} else {
if (error instanceof ApplicationFailure && error.category === ApplicationFailureCategory.BENIGN) {
// Downgrade log level to DEBUG for benign application errors.
this.workerLogger.debug('Activity failed', { error, durationMs });
} else {
this.workerLogger.warn('Activity failed', { error, durationMs });
}
}
}
}
public run(input: ActivityExecuteInput): Promise<coresdk.activity_result.IActivityExecutionResult> {
return asyncLocalStorage.run(this.context, async (): Promise<coresdk.activity_result.IActivityExecutionResult> => {
try {
if (this.fn === undefined) throw new IllegalStateError('Activity function is not defined');
const result = await this.execute(this.fn, input);
return { completed: { result: await encodeToPayload(this.dataConverter, result) } };
} catch (err) {
if (err instanceof CompleteAsyncError) {
return { willCompleteAsync: {} };
}
if (this.cancelReason === 'HEARTBEAT_DETAILS_CONVERSION_FAILED') {
// Ignore actual failure, it is likely a CancelledFailure but server
// expects activity to only fail with ApplicationFailure
return {
failed: {
failure: await encodeErrorToFailure(
this.dataConverter,
ApplicationFailure.retryable(this.cancelReason, 'CancelledFailure')
),
},
};
} else if (this.cancelReason) {
// Either a CancelledFailure that we threw or AbortError from AbortController
if (err instanceof CancelledFailure) {
const failure = await encodeErrorToFailure(this.dataConverter, err);
failure.stackTrace = undefined;
return { cancelled: { failure } };
} else if (isAbortError(err)) {
return { cancelled: { failure: { source: FAILURE_SOURCE, canceledFailureInfo: {} } } };
}
}
return {
failed: {
failure: await encodeErrorToFailure(this.dataConverter, ensureApplicationFailure(err)),
},
};
}
});
}
public runNoEncoding(fn: ActivityFunction<any[], any>, input: ActivityExecuteInput): Promise<unknown> {
if (this.fn !== undefined) throw new IllegalStateError('Activity function is defined');
return asyncLocalStorage.run(this.context, () => this.execute(fn, input));
}
}
/**
* Returns a map of attributes to be set on log messages for a given Activity
*/
export function activityLogAttributes(info: Info): Record<string, unknown> {
return {
isLocal: info.isLocal,
attempt: info.attempt,
namespace: info.workflowNamespace,
taskToken: info.base64TaskToken,
workflowId: info.workflowExecution.workflowId,
workflowRunId: info.workflowExecution.runId,
workflowType: info.workflowType,
activityId: info.activityId,
activityType: info.activityType,
taskQueue: info.taskQueue,
};
}