From c08b4c049fe2b28049fd96ee328ad9a5cfcd099e Mon Sep 17 00:00:00 2001 From: igalshilman Date: Wed, 20 Mar 2024 12:23:37 +0100 Subject: [PATCH] Switch to HTTP based error codes This commit replaces the gRPC based ErrorCode enum, with the HTTP based (integer) status code. --- src/context_impl.ts | 19 +++--- src/public_api.ts | 7 +- src/types/errors.ts | 161 ++++---------------------------------------- src/utils/rand.ts | 4 +- test/protoutils.ts | 14 ++-- 5 files changed, 34 insertions(+), 171 deletions(-) diff --git a/src/context_impl.ts b/src/context_impl.ts index 8dbe1548..266606c6 100644 --- a/src/context_impl.ts +++ b/src/context_impl.ts @@ -39,7 +39,6 @@ import { import { SideEffectEntryMessage } from "./generated/proto/javascript"; import { AsyncLocalStorage } from "async_hooks"; import { - ErrorCodes, RestateErrorCodes, RestateError, RetryableError, @@ -47,6 +46,8 @@ import { ensureError, errorToFailureWithTerminal, TimeoutError, + INTERNAL_ERROR_CODE, + UNKNOWN_ERROR_CODE, } from "./types/errors"; import { jsonSerialize, jsonDeserialize } from "./utils/utils"; import { Empty } from "./generated/google/protobuf/empty"; @@ -365,14 +366,14 @@ export class ContextImpl implements ObjectContext { if (this.isInSideEffect()) { throw new TerminalError( "You cannot do sideEffect calls from within a side effect.", - { errorCode: ErrorCodes.INTERNAL } + { errorCode: INTERNAL_ERROR_CODE } ); } else if (this.isInOneWayCall()) { throw new TerminalError( "Cannot do a side effect from within ctx.oneWayCall(...). " + "Context method ctx.oneWayCall() can only be used to invoke other services unidirectionally. " + "e.g. ctx.oneWayCall(() => client.greet(my_request))", - { errorCode: ErrorCodes.INTERNAL } + { errorCode: INTERNAL_ERROR_CODE } ); } this.checkNotExecutingSideEffect(); @@ -521,7 +522,7 @@ export class ContextImpl implements ObjectContext { public rejectAwakeable(id: string, reason: string): void { this.checkState("rejectAwakeable"); this.completeAwakeable(id, { - failure: { code: ErrorCodes.UNKNOWN, message: reason }, + failure: { code: UNKNOWN_ERROR_CODE, message: reason }, }); } @@ -589,7 +590,7 @@ export class ContextImpl implements ObjectContext { throw new TerminalError( `Invoked a RestateContext method while a side effect is still executing. Make sure you await the ctx.sideEffect call before using any other RestateContext method.`, - { errorCode: ErrorCodes.INTERNAL } + { errorCode: INTERNAL_ERROR_CODE } ); } } @@ -604,7 +605,7 @@ export class ContextImpl implements ObjectContext { if (context.type === CallContexType.SideEffect) { throw new TerminalError( `You cannot do ${callType} calls from within a side effect.`, - { errorCode: ErrorCodes.INTERNAL } + { errorCode: INTERNAL_ERROR_CODE } ); } @@ -613,7 +614,7 @@ export class ContextImpl implements ObjectContext { `Cannot do a ${callType} from within ctx.oneWayCall(...). Context method oneWayCall() can only be used to invoke other services in the background. e.g. ctx.oneWayCall(() => client.greet(my_request))`, - { errorCode: ErrorCodes.INTERNAL } + { errorCode: INTERNAL_ERROR_CODE } ); } } @@ -622,7 +623,7 @@ export class ContextImpl implements ObjectContext { if (!this.keyedContext) { throw new TerminalError( `You can do ${callType} calls only from a virtual object`, - { errorCode: ErrorCodes.INTERNAL } + { errorCode: INTERNAL_ERROR_CODE } ); } } @@ -723,7 +724,7 @@ async function executeWithRetries( throw new TerminalError( `Retries exhausted for ${name}. Last error: ${error.name}: ${error.message}`, { - errorCode: ErrorCodes.INTERNAL, + errorCode: INTERNAL_ERROR_CODE, } ); } diff --git a/src/public_api.ts b/src/public_api.ts index 0f171aba..c0ab1bbf 100644 --- a/src/public_api.ts +++ b/src/public_api.ts @@ -28,11 +28,6 @@ export { } from "./types/rpc"; export { endpoint, ServiceBundle, RestateEndpoint } from "./endpoint"; export * as RestateUtils from "./utils/public_utils"; -export { - ErrorCodes, - RestateError, - TerminalError, - TimeoutError, -} from "./types/errors"; +export { RestateError, TerminalError, TimeoutError } from "./types/errors"; export * as workflow from "./workflows/workflow"; export * as clients from "./clients/workflow_client"; diff --git a/src/types/errors.ts b/src/types/errors.ts index 4cd2b791..03a3acbd 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -16,150 +16,13 @@ import { formatMessageAsJson } from "../utils/utils"; import { FailureWithTerminal } from "../generated/proto/javascript"; import * as p from "./protocol"; -export enum ErrorCodes { - /** - * Not an error; returned on success. - * HTTP 200 - */ - OK = 0, - /** - * The operation was cancelled, typically by the caller. - * HTTP 408 - */ - CANCELLED = 1, - /** - * Unknown error. For example, this error may be returned when a - * Status value received from another address space belongs to an error - * space that is not known in this address space. Also errors raised by APIs - * that do not return enough error information may be converted to this - * error. - * HTTP 500 - */ - UNKNOWN = 2, - /** - * The client specified an invalid argument. Note that - * this differs from FAILED_PRECONDITION. INVALID_ARGUMENT indicates - * arguments that are problematic regardless of the state of the system - * (e.g., a malformed file name). - * HTTP 400 - */ - INVALID_ARGUMENT = 3, - /** - * The deadline expired before the operation could - * complete. For operations that change the state of the system, this error - * may be returned even if the operation has completed successfully. For - * example, a successful response from a server could have been delayed - * long. - * HTTP 408 - */ - DEADLINE_EXCEEDED = 4, - /** - * Some requested entity (e.g., file or directory) was not - * found. Note to server developers: if a request is denied for an entire - * class of users, such as gradual feature rollout or undocumented - * allowlist, NOT_FOUND may be used. If a request is denied for some users - * within a class of users, such as user-based access control, - * PERMISSION_DENIED must be used. - * HTTP 404 - */ - NOT_FOUND = 5, - /** - * The entity that a client attempted to create (e.g., file - * or directory) already exists. - * HTTP 409 - */ - ALREADY_EXISTS = 6, - /** - * The caller does not have permission to execute the - * specified operation. PERMISSION_DENIED must not be used for rejections - * caused by exhausting some resource (use RESOURCE_EXHAUSTED instead for - * those errors). PERMISSION_DENIED must not be used if the caller can not - * be identified (use UNAUTHENTICATED instead for those errors). This error - * code does not imply the request is valid or the requested entity exists - * or satisfies other pre-conditions. - * HTTP 403 - */ - PERMISSION_DENIED = 7, - /** - * Some resource has been exhausted, perhaps a per-user - * quota, or perhaps the entire file system is out of space. - * HTTP 413 - */ - RESOURCE_EXHAUSTED = 8, - /** - * The operation was rejected because the system is - * not in a state required for the operation's execution. For example, the - * directory to be deleted is non-empty, an rmdir operation is applied to a - * non-directory, etc. Service implementors can use the following guidelines - * to decide between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: (a) Use - * UNAVAILABLE if the client can retry just the failing call. (b) Use - * ABORTED if the client should retry at a higher level (e.g., when a - * client-specified test-and-set fails, indicating the client should restart - * a read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client - * should not retry until the system state has been explicitly fixed. E.g., - * if an "rmdir" fails because the directory is non-empty, - * FAILED_PRECONDITION should be returned since the client should not retry - * unless the files are deleted from the directory. - * HTTP 412 - */ - FAILED_PRECONDITION = 9, - /** - * The operation was aborted, typically due to a concurrency issue - * such as a sequencer check failure or transaction abort. See the - * guidelines above for deciding between FAILED_PRECONDITION, ABORTED, and - * UNAVAILABLE. - * HTTP 409 - */ - ABORTED = 10, - /** - * The operation was attempted past the valid range. E.g., - * seeking or reading past end-of-file. Unlike INVALID_ARGUMENT, this error - * indicates a problem that may be fixed if the system state changes. For - * example, a 32-bit file system will generate INVALID_ARGUMENT if asked to - * read at an offset that is not in the range [0,2^32-1], but it will - * generate OUT_OF_RANGE if asked to read from an offset past the current - * file size. There is a fair bit of overlap between FAILED_PRECONDITION and - * OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error) - * when it applies so that callers who are iterating through a space can - * easily look for an OUT_OF_RANGE error to detect when they are done. - * HTTP 400 - */ - OUT_OF_RANGE = 11, - /** - * The operation is not implemented or is not - * supported/enabled in this service. - * HTTP 501 - */ - UNIMPLEMENTED = 12, - /** - * Internal errors. This means that some invariants expected by - * the underlying system have been broken. This error code is reserved for - * serious errors. - * HTTP 500 - */ - INTERNAL = 13, - /** - * The service is currently unavailable. This is most likely a - * transient condition, which can be corrected by retrying with a backoff. - * Note that it is not always safe to retry non-idempotent operations. - * HTTP 503 - */ - UNAVAILABLE = 14, - /** - * Unrecoverable data loss or corruption. - * HTTP 500 - */ - DATA_LOSS = 15, - /** - * The request does not have valid authentication - * credentials for the operation. - * HTTP 401 - */ - UNAUTHENTICATED = 16, -} +export const INTERNAL_ERROR_CODE = 500; +export const TIMEOUT_ERROR_CODE = 408; +export const UNKNOWN_ERROR_CODE = 500; + export enum RestateErrorCodes { - JOURNAL_MISMATCH = 32, - PROTOCOL_VIOLATION = 33, + JOURNAL_MISMATCH = 570, + PROTOCOL_VIOLATION = 571, } export function ensureError(e: unknown): Error { @@ -182,7 +45,7 @@ export class RestateError extends Error { constructor(message: string, options?: { errorCode?: number; cause?: any }) { super(message, { cause: options?.cause }); - this.code = options?.errorCode ?? ErrorCodes.INTERNAL; + this.code = options?.errorCode ?? INTERNAL_ERROR_CODE; } public toFailure(): Failure { @@ -203,7 +66,7 @@ export class TerminalError extends RestateError { export class TimeoutError extends TerminalError { constructor() { - super("Timeout occurred", { errorCode: ErrorCodes.DEADLINE_EXCEEDED }); + super("Timeout occurred", { errorCode: TIMEOUT_ERROR_CODE }); } } @@ -249,7 +112,7 @@ export class RetryableError extends RestateError { public static apiViolation(message: string) { return new RetryableError(`API violation: ${message}`, { - errorCode: ErrorCodes.INTERNAL, + errorCode: INTERNAL_ERROR_CODE, }); } } @@ -258,7 +121,7 @@ export function errorToFailure(err: Error): Failure { return err instanceof RestateError ? err.toFailure() : Failure.create({ - code: ErrorCodes.INTERNAL, + code: INTERNAL_ERROR_CODE, message: err.message, }); } @@ -280,7 +143,7 @@ export function failureToError( terminalError: boolean ): Error { const errorMessage = failure.message ?? "(missing error message)"; - const errorCode = failure.code ?? ErrorCodes.INTERNAL; + const errorCode = failure.code ?? INTERNAL_ERROR_CODE; return terminalError ? new TerminalError(errorMessage, { errorCode }) @@ -288,7 +151,7 @@ export function failureToError( } export function errorToErrorMessage(err: Error): ErrorMessage { - const code = err instanceof RestateError ? err.code : ErrorCodes.INTERNAL; + const code = err instanceof RestateError ? err.code : INTERNAL_ERROR_CODE; return ErrorMessage.create({ code: code, diff --git a/src/utils/rand.ts b/src/utils/rand.ts index 8208e08c..3c95153d 100644 --- a/src/utils/rand.ts +++ b/src/utils/rand.ts @@ -13,7 +13,7 @@ //! License MIT import { Rand } from "../context"; -import { ErrorCodes, TerminalError } from "../types/errors"; +import { INTERNAL_ERROR_CODE, TerminalError } from "../types/errors"; import { CallContexType, ContextImpl } from "../context_impl"; import { createHash } from "crypto"; @@ -72,7 +72,7 @@ export class RandImpl implements Rand { if (context && context.type === CallContexType.SideEffect) { throw new TerminalError( `You may not call methods on Rand from within a side effect.`, - { errorCode: ErrorCodes.INTERNAL } + { errorCode: INTERNAL_ERROR_CODE } ); } } diff --git a/test/protoutils.ts b/test/protoutils.ts index d088bf99..eec74e19 100644 --- a/test/protoutils.ts +++ b/test/protoutils.ts @@ -65,7 +65,11 @@ import { import { expect } from "@jest/globals"; import { jsonSerialize, formatMessageAsJson } from "../src/utils/utils"; import { rlog } from "../src/logger"; -import { ErrorCodes, RestateErrorCodes } from "../src/types/errors"; +import { + INTERNAL_ERROR_CODE, + RestateErrorCodes, + UNKNOWN_ERROR_CODE, +} from "../src/types/errors"; import { SUPPORTED_PROTOCOL_VERSION } from "../src/io/decoder"; export type StartMessageOpts = { @@ -476,7 +480,7 @@ export function rejectAwakeableMessage(id: string, reason: string): Message { COMPLETE_AWAKEABLE_ENTRY_MESSAGE_TYPE, CompleteAwakeableEntryMessage.create({ id: id, - failure: { code: ErrorCodes.UNKNOWN, message: reason }, + failure: { code: UNKNOWN_ERROR_CODE, message: reason }, }) ); } @@ -508,7 +512,7 @@ export function combinatorEntryMessage( export function failure( msg: string, - code: number = ErrorCodes.INTERNAL + code: number = INTERNAL_ERROR_CODE ): Failure { return Failure.create({ code: code, message: msg }); } @@ -516,7 +520,7 @@ export function failure( export function failureWithTerminal( terminal: boolean, msg: string, - code: number = ErrorCodes.INTERNAL + code: number = INTERNAL_ERROR_CODE ): FailureWithTerminal { return FailureWithTerminal.create({ terminal, @@ -537,7 +541,7 @@ export function greetResponse(myGreeting: string): Uint8Array { export function checkError( outputMsg: Message, errorMessage: string, - code: number = ErrorCodes.INTERNAL + code: number = INTERNAL_ERROR_CODE ) { expect(outputMsg.messageType).toEqual(ERROR_MESSAGE_TYPE); expect((outputMsg.message as ErrorMessage).code).toStrictEqual(code);