-
Notifications
You must be signed in to change notification settings - Fork 2
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
Throw Errors from AWS SDK or Expose Custom Errors in TypeScript API #9
Comments
I had a similar issue, and agree with harmonizing the error handling. As a workaround, I ultimately built a system where it fails over to DDB client if DAX client fails. Then I log the DAX errors and deal with it in the morning. This covers every manor of potential DAX failure including db service, cluster availability, VPC network nightmares, capacity etc. This DAX client is a baby step for me to get my feet wet and offload some high use access patterns for cost and UX speed.
|
Thanks for the quick reply. I'm really glad I was not the only one experiencing VPC network nightmares. I'm working on a simple parser to reconstruct the SDK errors on my end anyway. I'll share it here once I have it working in case it's of any use. |
Okay here is my rough error parser. In case you want to harmonize the error parsing with the DB SDK, this may serve has a helpful starting point. Some Notes
DaxServiceError -> Serializable AWS SDK Error( import { CancellationReason } from "@aws-sdk/client-dynamodb";
import {
SerializableAwsDbError,
doesDbErrorNeedData,
getAwsDbErrorNames,
} from "@yaws/dynamodb";
import { assertUnreachable } from "yoomi-shared-js";
/**
* The DAX client that we use
* @see https://github.com/kwojcicki/amazon-dax-client-v3
* throws custom errors. However, sometimes these errors are caught by AWS
* middleware and converted to a new error object (which loses the fields from
* the original error). This happens in:
* @see node_modules/@aws-sdk/client-dynamodb/node_modules/@smithy/middleware-retry/dist-cjs/index.js
*
* Thus, this function attempts to parse the error object from the message
* string. This function is certainly hacky, but we rigorously test it.
*/
export function parseDaxServiceError(
e: Error,
): SerializableAwsDbError | undefined {
// The error message is in the format: `DaxServiceError: <aws-sdk-error-name>: ${message}`
const names = getAwsDbErrorNames();
type Name = (typeof names)[number];
const constructOutput = (name: Name): SerializableAwsDbError => {
if (doesDbErrorNeedData(name)) {
switch (name) {
// handle special cases individually
case "TransactionCanceledException":
return {
type: "AwsDbError",
name,
$metadata: {},
stack: e.stack,
message: e.message,
data: {
CancellationReasons:
_transactCanceledExtractReasonsFromErrorMsg(
e.message,
),
},
} satisfies SerializableAwsDbError;
default:
return assertUnreachable(name);
}
}
return {
type: "AwsDbError",
name,
$metadata: {},
stack: e.stack,
message: e.message,
data: undefined,
} satisfies SerializableAwsDbError;
};
for (const name of names) {
const re = new RegExp(`DaxServiceError: ${name}`);
if (re.test(e.message)) {
return constructOutput(name);
}
}
// if there was no match, then we return `undefined` to indicate that we
// could not parse the error
return undefined;
}
/**
* We only export this for unit testing purposes.
*/
export function _transactCanceledExtractReasonsFromErrorMsg(
msg: string,
): CancellationReason[] {
const regex = /for specific reasons \[([^\]]*)\]/g;
// get the first match
const match = regex.exec(msg)?.[1];
if (!match) {
console.warn(`No match found for ${msg}`);
return [];
}
return match
.split(",")
.map((e) => e.trim())
.filter((e) => e.length > 0)
.map((e) => ({ Code: e }));
} AWS SDK Error Encoder/Decoder (stringify/parse)import type * as DDB_IMPL from "@aws-sdk/client-dynamodb";
import {
BackupInUseException,
BackupNotFoundException,
ConditionalCheckFailedException,
ContinuousBackupsUnavailableException,
DuplicateItemException,
DynamoDBServiceException,
ExportConflictException,
ExportNotFoundException,
GlobalTableAlreadyExistsException,
GlobalTableNotFoundException,
IdempotentParameterMismatchException,
ImportConflictException,
ImportNotFoundException,
IndexNotFoundException,
InternalServerError,
InvalidEndpointException,
InvalidExportTimeException,
InvalidRestoreTimeException,
ItemCollectionSizeLimitExceededException,
LimitExceededException,
PointInTimeRecoveryUnavailableException,
PolicyNotFoundException,
ProvisionedThroughputExceededException,
ReplicaAlreadyExistsException,
ReplicaNotFoundException,
RequestLimitExceeded,
ResourceInUseException,
ResourceNotFoundException,
TableAlreadyExistsException,
TableInUseException,
TableNotFoundException,
TransactionCanceledException,
TransactionConflictException,
TransactionInProgressException,
} from "@aws-sdk/client-dynamodb";
import {
Constructor,
ExcludeStrict,
ExtractStrict,
assertUnreachable,
isObjectGeneral,
lazyFactoryFn,
safeObjectKeys,
} from "yoomi-shared-js";
type DDB = typeof DDB_IMPL;
type AllAwsDbErrors = {
[K in keyof DDB]: DDB[K] extends Constructor<unknown>
? InstanceType<DDB[K]> extends DynamoDBServiceException
? // we exclude the DynamoDBServiceException itself
DynamoDBServiceException extends InstanceType<DDB[K]>
? never
: InstanceType<DDB[K]>
: never
: never;
}[keyof DDB];
type AwsDbErrorNames = AllAwsDbErrors["name"];
/**
* Type mapping between some of the error names and the additional information
* that they contain.
*/
interface AdditionalErrorInfo {
TransactionCanceledException: {
CancellationReasons: DDB_IMPL.CancellationReason[] | undefined;
};
}
// the error names with additional information (strict exclude enforces that
// the keys of `AdditionalErrorInfo` are actually error names)
type WithData = ExtractStrict<AwsDbErrorNames, keyof AdditionalErrorInfo>;
type WithoutData = ExcludeStrict<AwsDbErrorNames, WithData>;
export interface SerializableAwsDbErrorBase<N extends AwsDbErrorNames, Data> {
type: "AwsDbError";
name: N;
$metadata: DynamoDBServiceException["$metadata"];
stack: string | undefined;
message: string;
/**
* We intentionally set this to `undefined` as well as `Data` in case that
* it is missing for some reason. We want this parser to be as robust as
* possible.
*/
data: Data | undefined;
}
// we intentionally distribute this union
type SerializableWithData<N extends WithData = WithData> = N extends N
? SerializableAwsDbErrorBase<N, AdditionalErrorInfo[N]>
: never;
// we intentionally do not distribute this union
type SerializableWithoutData<N extends WithoutData = WithoutData> =
SerializableAwsDbErrorBase<N, undefined>;
export type SerializableAwsDbError =
| SerializableWithData
| SerializableWithoutData;
// TODO: ensure that we allow for `DynamoDBServiceException` objects that
// are not in the named error list. however, make sure to prioritize the named
// list.
export function isAwsDbError(e: unknown): e is AllAwsDbErrors {
return e instanceof DynamoDBServiceException && e.name in ERROR_MAP;
}
export function isSerializableAwsDbError(
e: unknown,
): e is SerializableAwsDbError {
return isObjectGeneral<Pick<SerializableAwsDbError, "type">>(e, {
type: (v, r): v is typeof r => v === ("AwsDbError" satisfies typeof r),
});
}
export function toSerializableAwsDbError(
e: AllAwsDbErrors,
): SerializableAwsDbError {
const base: SerializableAwsDbErrorBase<AwsDbErrorNames, undefined> = {
type: "AwsDbError",
name: e.name,
$metadata: e.$metadata,
stack: e.stack,
message: e.message,
data: undefined,
};
if (doesDbErrorNeedData(e.name)) {
switch (e.name) {
case "TransactionCanceledException":
return {
...base,
name: e.name,
data: {
CancellationReasons: e.CancellationReasons,
},
} satisfies SerializableAwsDbError;
/* istanbul ignore next */
default:
return assertUnreachable(e.name);
}
}
return {
...base,
name: e.name,
data: undefined,
} satisfies SerializableAwsDbError;
}
export function reconstructAwsDbError(
s: SerializableAwsDbError,
): AllAwsDbErrors {
const constructError = <N extends AwsDbErrorNames>(
name: N,
): InstanceType<DDB[N]> => {
const C = ERROR_MAP[name];
const output = new C({
$metadata: s.$metadata,
message: s.message,
});
output.stack = s.stack;
return output satisfies AllAwsDbErrors as InstanceType<DDB[N]>;
};
if (doesDbErrorNeedData(s.name)) {
switch (s.name) {
case "TransactionCanceledException":
return (() => {
const e = constructError("TransactionCanceledException");
e.CancellationReasons = s.data?.CancellationReasons;
return e;
})();
/* istanbul ignore next */
default:
return assertUnreachable(s.name);
}
}
return constructError(s.name);
}
export const getAwsDbErrorNames: () => AwsDbErrorNames[] = lazyFactoryFn(() => {
const keys = safeObjectKeys(ERROR_MAP);
return () => keys;
});
const getAwsDbErrorNamesWithData: () => WithData[] = lazyFactoryFn(() => {
const keys = safeObjectKeys<AdditionalErrorInfo>({
TransactionCanceledException: null,
});
return () => keys;
});
export function doesDbErrorNeedData(name: AwsDbErrorNames): name is WithData {
return getAwsDbErrorNamesWithData().includes(name as WithData);
}
// mapping from error name to error constructor
const ERROR_MAP: {
[E in AllAwsDbErrors as E["name"]]: DDB[E["name"]];
} = {
ConditionalCheckFailedException,
BackupInUseException,
BackupNotFoundException,
InternalServerError,
RequestLimitExceeded,
InvalidEndpointException,
ProvisionedThroughputExceededException,
ResourceNotFoundException,
ItemCollectionSizeLimitExceededException,
ContinuousBackupsUnavailableException,
LimitExceededException,
TableInUseException,
TableNotFoundException,
GlobalTableAlreadyExistsException,
ResourceInUseException,
TransactionConflictException,
PolicyNotFoundException,
ExportNotFoundException,
GlobalTableNotFoundException,
ImportNotFoundException,
DuplicateItemException,
IdempotentParameterMismatchException,
TransactionInProgressException,
ExportConflictException,
InvalidExportTimeException,
PointInTimeRecoveryUnavailableException,
ImportConflictException,
TableAlreadyExistsException,
InvalidRestoreTimeException,
ReplicaAlreadyExistsException,
ReplicaNotFoundException,
IndexNotFoundException,
TransactionCanceledException,
}; Some Utilities from Custom Library (
|
@daveharig I do also have a few questions as I am wrapping up my Transact WritesI can't seem to get My question: Does your client currently support TransactWrites using a QueriesThis is my DAX client setup: export function createDaxClient(p: {
isTestEnv: boolean;
}): DbGeneralInterface<DaxSupportedTable> {
const dbBase = {
awsClient: new AWS_DynamoDbClient({
region: DYNAMO_DB_REGION,
endpoint: getDaxClusterEndpoint(p),
}),
};
const daxClient = new AmazonDaxClient({
client: dbBase.awsClient,
config: {
connectTimeout: 5000,
},
});
const db = new DynamoDbStandardInterface({
type: "instance",
client: daxClient,
// applying the xray middleware seems to be causing an error (i think
// that the dax client is not correctly instrumenting the command
// middleware when it makes the request to the dax cluster)
noGlobalMiddleware: true,
isTableSupported: isDaxSupportedTable,
});
return db;
} where
Under the hood, I noticed that you have My question: Am I using the |
Firstly, I want to preface this by saying that I'm shocked (but also at the same time not shocked) that AWS hasn't released an official DAX client with V3 support by now, but you guys are awesome for putting this library together.
I run a production code base and have been trying to start using DAX for some of our APIs. In our code base, I have built an abstraction layer to swap between the standard AWS SDK DB client and your DAX client. For the most part, your DAX client has worked well as a drop-in replacement.
The only part that prevents your client from being a complete drop-in replacement (at least for our code base) is that when your client encounters a service error, you throw a custom DaxServiceError instance. There are a lot of places in our code base where we use the
instanceof
operator to handle certain errors from@aws-sdk/client-dynamodb
(e.g.ConditionalCheckFailedException
) which obviously wouldn't work with your custom error.After digging into your source code, I do see that you have already done the hard part in terms of parsing the response to figure out the correct error. Ideally, it would be great if you could reconstruct the SDK errors from
@aws-sdk/client-dynamodb
. Otherwise, I would be grateful if you guys could add a quick typescript declaration for your custom DaxServiceError and expose the fields that are necessary to reconstruct the AWS SDK errors.The text was updated successfully, but these errors were encountered: