Skip to content
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

feat: add momento-local-test-config-middleware #1517

Merged
merged 8 commits into from
Jan 31, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {Metadata} from '@grpc/grpc-js';
import {
Middleware,
MiddlewareMessage,
MiddlewareMetadata,
MiddlewareRequestHandler,
MiddlewareStatus,
} from './middleware';
import {MomentoErrorCodeMetadataConverter} from '../retry/momento-error-code-metadata-converter';
import {MomentoRPCMethodMetadataConverter} from '../retry/momento-rpc-method';

class ExperimentalMomentoLocalMiddlewareRequestHandler
implements MiddlewareRequestHandler
{
constructor(
private readonly metadata: ExperimentalMomentoLocalTestConfigMetadata
) {}

onRequestBody(request: MiddlewareMessage): Promise<MiddlewareMessage> {
return Promise.resolve(request);
}

onRequestMetadata(metadata: MiddlewareMetadata): Promise<MiddlewareMetadata> {
const grpcMetadata = metadata._grpcMetadata;
for (const [key, value] of Object.entries(this.metadata)) {
this.setGrpcMetadata(grpcMetadata, key, value as string);
}
return Promise.resolve(metadata);
}

onResponseMetadata(
metadata: MiddlewareMetadata
): Promise<MiddlewareMetadata> {
return Promise.resolve(metadata);
}

onResponseBody(
response: MiddlewareMessage | null
): Promise<MiddlewareMessage | null> {
return Promise.resolve(response);
}

onResponseStatus(status: MiddlewareStatus): Promise<MiddlewareStatus> {
return Promise.resolve(status);
}

private setGrpcMetadata(
metadata: Metadata,
key: string,
value?: unknown
): void {
if (value === undefined) return;

let convertedKey: string;
let convertedValue: string;

switch (key) {
case 'requestId':
convertedKey = 'request-id';
convertedValue = value as string;
break;
case 'returnError':
convertedKey = 'return-error';
convertedValue = MomentoErrorCodeMetadataConverter.convert(
value as string
);
break;
case 'errorRpcs':
convertedKey = 'error-rpcs';
convertedValue = (value as string[])
.map(rpcMethod =>
MomentoRPCMethodMetadataConverter.convert(rpcMethod)
)
.join(' ');
break;
case 'errorCount':
convertedKey = 'error-count';
convertedValue = (value as number).toString();
break;
case 'delayRpcs':
convertedKey = 'delay-rpcs';
convertedValue = (value as string[])
.map(rpcMethod =>
MomentoRPCMethodMetadataConverter.convert(rpcMethod)
)
.join(' ');
break;
case 'delayMs':
convertedKey = 'delay-ms';
convertedValue = (value as number).toString();
break;
case 'delayCount':
convertedKey = 'delay-count';
convertedValue = (value as number).toString();
break;
default:
convertedKey = key;
convertedValue = value as string;
}

metadata.set(convertedKey, convertedValue);
}
}

interface ExperimentalMomentoLocalTestConfigMetadata {
requestId: string;
returnError?: string;
errorRpcs?: string[];
errorCount?: number;
delayRpcs?: string[];
delayMs?: number;
delayCount?: number;
}

class ExperimentalMomentoLocalTestConfigMiddleware implements Middleware {
shouldLoadLate: boolean;
constructor(
private readonly metadata: ExperimentalMomentoLocalTestConfigMetadata
) {
this.shouldLoadLate = true;
}

onNewRequest(): MiddlewareRequestHandler {
return new ExperimentalMomentoLocalMiddlewareRequestHandler(this.metadata);
}
}

export {
ExperimentalMomentoLocalTestConfigMiddleware,
ExperimentalMomentoLocalTestConfigMetadata,
ExperimentalMomentoLocalMiddlewareRequestHandler,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {MomentoErrorCode} from '@gomomento/sdk-core';

class MomentoErrorCodeMetadataConverter {
private static readonly momentoErrorCodeToMetadataMap: Record<
string,
string
> = {
[MomentoErrorCode.INVALID_ARGUMENT_ERROR]: 'invalid-argument',
[MomentoErrorCode.UNKNOWN_SERVICE_ERROR]: 'unknown',
[MomentoErrorCode.CACHE_ALREADY_EXISTS_ERROR]: 'already-exists',
[MomentoErrorCode.ALREADY_EXISTS_ERROR]: 'already-exists',
[MomentoErrorCode.STORE_ALREADY_EXISTS_ERROR]: 'already-exists',
[MomentoErrorCode.CACHE_NOT_FOUND_ERROR]: 'not-found',
[MomentoErrorCode.NOT_FOUND_ERROR]: 'not-found',
[MomentoErrorCode.STORE_NOT_FOUND_ERROR]: 'not-found',
[MomentoErrorCode.STORE_ITEM_NOT_FOUND_ERROR]: 'not-found',
[MomentoErrorCode.INTERNAL_SERVER_ERROR]: 'internal',
[MomentoErrorCode.PERMISSION_ERROR]: 'permission-denied',
[MomentoErrorCode.AUTHENTICATION_ERROR]: 'unauthenticated',
[MomentoErrorCode.CANCELLED_ERROR]: 'cancelled',
[MomentoErrorCode.CONNECTION_ERROR]: 'unavailable',
[MomentoErrorCode.LIMIT_EXCEEDED_ERROR]: 'deadline-exceeded',
[MomentoErrorCode.BAD_REQUEST_ERROR]: 'invalid-argument',
[MomentoErrorCode.TIMEOUT_ERROR]: 'deadline-exceeded',
[MomentoErrorCode.SERVER_UNAVAILABLE]: 'unavailable',
[MomentoErrorCode.CLIENT_RESOURCE_EXHAUSTED]: 'resource-exhausted',
[MomentoErrorCode.FAILED_PRECONDITION_ERROR]: 'failed-precondition',
[MomentoErrorCode.UNKNOWN_ERROR]: 'unknown',
};

/**
* Converts a MomentoErrorCode enum value to its corresponding metadata type.
* @param errorCode - The error code to convert.
* @returns The corresponding metadata type.
*/
public static convert(errorCode: string | undefined): string {
if (!errorCode) {
return '';
}
return this.momentoErrorCodeToMetadataMap[errorCode] ?? '';
}
}

export {MomentoErrorCodeMetadataConverter};
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ enum MomentoRPCMethod {
SortedSetLengthByScore = '_SortedSetLengthByScoreRequest',
}

class MomentoRPCMethodConverter {
class MomentoRPCMethodMetadataConverter {
private static readonly rpcMethodToMetadataMap: Record<string, string> = {
[MomentoRPCMethod.Get]: 'get',
[MomentoRPCMethod.Set]: 'set',
Expand Down Expand Up @@ -100,10 +100,10 @@ class MomentoRPCMethodConverter {

/**
* Converts a MomentoRPCMethod enum value to its corresponding metadata type.
* @param rpcMethod - The MomentoRPCMethod enum value.
* @param rpcMethod - The rpc method to convert.
* @returns The corresponding metadata type.
*/
public static convert(rpcMethod: MomentoRPCMethod): string {
public static convert(rpcMethod: string): string {
const metadataType = this.rpcMethodToMetadataMap[rpcMethod];
if (!metadataType) {
throw new Error(`Unsupported MomentoRPCMethod: ${rpcMethod}`);
Expand All @@ -112,4 +112,4 @@ class MomentoRPCMethodConverter {
}
}

export {MomentoRPCMethod, MomentoRPCMethodConverter};
export {MomentoRPCMethod, MomentoRPCMethodMetadataConverter};
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import {
CacheIncrementResponse,
DefaultMomentoLoggerFactory,
ExponentialBackoffRetryStrategy,
MomentoErrorCode,
MomentoLogger,
} from '../../../src';
import {TestRetryMetricsCollector} from '../../test-retry-metrics-collector';
import {
MomentoRPCMethod,
MomentoRPCMethodConverter,
} from '../../../src/config/retry/momento-rpc-method';
import {MomentoRPCMethod} from '../../../src/config/retry/momento-rpc-method';
import {WithCacheAndCacheClient} from '../integration-setup';
import {TestRetryMetricsMiddlewareArgs} from '../../test-retry-metrics-middleware';
import {v4} from 'uuid';
Expand Down Expand Up @@ -43,8 +41,8 @@ describe('ExponentialBackoffRetryStrategy integration tests', () => {
logger: momentoLogger,
testMetricsCollector,
requestId: v4(),
returnError: 'unavailable',
errorRpcList: [MomentoRPCMethodConverter.convert(MomentoRPCMethod.Get)],
returnError: MomentoErrorCode.SERVER_UNAVAILABLE,
errorRpcList: [MomentoRPCMethod.Get],
};

await WithCacheAndCacheClient(
Expand Down Expand Up @@ -85,10 +83,8 @@ describe('ExponentialBackoffRetryStrategy integration tests', () => {
logger: momentoLogger,
testMetricsCollector,
requestId: v4(),
returnError: 'unavailable',
errorRpcList: [
MomentoRPCMethodConverter.convert(MomentoRPCMethod.Increment),
],
returnError: MomentoErrorCode.SERVER_UNAVAILABLE,
errorRpcList: [MomentoRPCMethod.Increment],
};

await WithCacheAndCacheClient(
Expand Down Expand Up @@ -126,8 +122,8 @@ describe('ExponentialBackoffRetryStrategy integration tests', () => {
logger: momentoLogger,
testMetricsCollector,
requestId: v4(),
returnError: 'unavailable',
errorRpcList: [MomentoRPCMethodConverter.convert(MomentoRPCMethod.Get)],
returnError: MomentoErrorCode.SERVER_UNAVAILABLE,
errorRpcList: [MomentoRPCMethod.Get],
errorCount: 2, // after 2 errors, subsequent requests succeed
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import {
CacheGetResponse,
CacheIncrementResponse,
DefaultMomentoLoggerFactory,
MomentoErrorCode,
MomentoLogger,
} from '../../../src';
import {TestRetryMetricsCollector} from '../../test-retry-metrics-collector';
import {
MomentoRPCMethod,
MomentoRPCMethodConverter,
} from '../../../src/config/retry/momento-rpc-method';
import {MomentoRPCMethod} from '../../../src/config/retry/momento-rpc-method';
import {WithCacheAndCacheClient} from '../integration-setup';
import {TestRetryMetricsMiddlewareArgs} from '../../test-retry-metrics-middleware';
import {v4} from 'uuid';
Expand All @@ -29,8 +27,8 @@ describe('Fixed count retry strategy with full network outage', () => {
logger: momentoLogger,
testMetricsCollector: testMetricsCollector,
requestId: v4(),
returnError: 'unavailable',
errorRpcList: [MomentoRPCMethodConverter.convert(MomentoRPCMethod.Get)],
returnError: MomentoErrorCode.SERVER_UNAVAILABLE,
errorRpcList: [MomentoRPCMethod.Get],
};

await WithCacheAndCacheClient(
Expand All @@ -56,10 +54,8 @@ describe('Fixed count retry strategy with full network outage', () => {
logger: momentoLogger,
testMetricsCollector: testMetricsCollector,
requestId: v4(),
returnError: 'unavailable',
errorRpcList: [
MomentoRPCMethodConverter.convert(MomentoRPCMethod.Increment),
],
returnError: MomentoErrorCode.SERVER_UNAVAILABLE,
errorRpcList: [MomentoRPCMethod.Increment],
};
await WithCacheAndCacheClient(
config => config,
Expand Down Expand Up @@ -97,8 +93,8 @@ describe('Fixed count retry strategy with temporary network outage', () => {
logger: momentoLogger,
testMetricsCollector: testMetricsCollector,
requestId: v4(),
returnError: 'unavailable',
errorRpcList: [MomentoRPCMethodConverter.convert(MomentoRPCMethod.Get)],
returnError: MomentoErrorCode.SERVER_UNAVAILABLE,
errorRpcList: [MomentoRPCMethod.Get],
errorCount: 2,
};
await WithCacheAndCacheClient(
Expand Down
Loading
Loading