Skip to content

Commit e04a74e

Browse files
committed
added timeouts for multi-call methods
1 parent 3ef9a24 commit e04a74e

39 files changed

+341
-143
lines changed

src/api/data-api-http-client.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,43 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { BaseOptions } from '@/src/data-api/types';
1615
import { DEFAULT_NAMESPACE, DEFAULT_TIMEOUT, HTTP_METHODS, HttpClient, RawDataApiResponse } from '@/src/api';
1716
import { DataAPIResponseError, DataAPITimeout, mkRespErrorFromResponse, ObjectId, UUID } from '@/src/data-api';
1817
import { logger } from '@/src/logger';
18+
import {
19+
MkTimeoutError,
20+
MultiCallTimeoutManager,
21+
SingleCallTimeoutManager,
22+
TimeoutManager, TimeoutOptions,
23+
} from '@/src/api/timeout-managers';
1924

2025
interface DataApiRequestInfo {
2126
url: string;
22-
timeout?: number;
2327
collection?: string;
2428
namespace?: string;
2529
command: Record<string, any>;
30+
timeoutManager: TimeoutManager;
31+
}
32+
33+
type ExecuteCommandOptions = TimeoutOptions & {
34+
collection?: string;
35+
namespace?: string;
2636
}
2737

2838
export class DataApiHttpClient extends HttpClient {
2939
public collection?: string;
3040
public namespace?: string;
3141

32-
public async executeCommand(command: Record<string, any>, options?: BaseOptions & { collection?: string, namespace?: string }) {
42+
public multiCallTimeoutManager(timeoutMs: number | undefined) {
43+
return mkTimeoutManager(MultiCallTimeoutManager, timeoutMs);
44+
}
45+
46+
public async executeCommand(command: Record<string, any>, options: ExecuteCommandOptions | undefined) {
47+
const timeoutManager = options?.timeoutManager ?? mkTimeoutManager(SingleCallTimeoutManager, options?.maxTimeMS);
48+
3349
const response = await this._requestDataApi({
3450
url: this.baseUrl,
35-
timeout: options?.maxTimeMS,
51+
timeoutManager: timeoutManager,
3652
collection: options?.collection,
3753
namespace: options?.namespace,
3854
command: command,
@@ -54,16 +70,13 @@ export class DataApiHttpClient extends HttpClient {
5470
const response = await this._request({
5571
url: url,
5672
data: JSON.stringify(info.command, replacer),
57-
timeout: info.timeout,
73+
timeoutManager: info.timeoutManager,
5874
method: HTTP_METHODS.Post,
59-
timeoutError() {
60-
return new DataAPITimeout(info.command, info.timeout || DEFAULT_TIMEOUT);
61-
},
6275
reviver: reviver,
6376
});
6477

6578
if (response.status === 401 || (response.data?.errors?.length > 0 && response.data?.errors[0]?.message === 'UNAUTHENTICATED: Invalid token')) {
66-
return this._mkError('Authentication failed; is your token valid?');
79+
return mkFauxErroredResponse('Authentication failed; is your token valid?');
6780
}
6881

6982
if (response.status === 200) {
@@ -75,7 +88,7 @@ export class DataApiHttpClient extends HttpClient {
7588
} else {
7689
logger.error(info.url + ": " + response.status);
7790
logger.error("Data: " + JSON.stringify(info.command));
78-
return this._mkError(`Some non-200 status code was returned. Check the logs for more information. ${response.status}, ${JSON.stringify(response.data)}`);
91+
return mkFauxErroredResponse(`Some non-200 status code was returned. Check the logs for more information. ${response.status}, ${JSON.stringify(response.data)}`);
7992
}
8093
} catch (e: any) {
8194
logger.error(info.url + ": " + e.message);
@@ -88,10 +101,19 @@ export class DataApiHttpClient extends HttpClient {
88101
throw e;
89102
}
90103
}
104+
}
91105

92-
private _mkError(message: string): RawDataApiResponse {
93-
return { errors: [{ message }] };
94-
}
106+
const mkTimeoutManager = (constructor: new (maxMs: number, mkTimeoutError: MkTimeoutError) => TimeoutManager, maxMs: number | undefined) => {
107+
const timeout = maxMs ?? DEFAULT_TIMEOUT;
108+
return new constructor(timeout, mkTimeoutErrorMaker(timeout));
109+
}
110+
111+
const mkTimeoutErrorMaker = (timeout: number): MkTimeoutError => {
112+
return (info) => new DataAPITimeout(info.data!, timeout)
113+
}
114+
115+
const mkFauxErroredResponse = (message: string): RawDataApiResponse => {
116+
return { errors: [{ message }] };
95117
}
96118

97119
export function replacer(this: any, key: string, value: any): any {

src/api/devops-api-http-client.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,50 @@ import { HTTPClientOptions } from '@/src/api/types';
1919
import { HTTP1AuthHeaderFactories, HTTP1Strategy } from '@/src/api/http1';
2020
import { DevopsApiResponseError, DevopsApiTimeout, DevopsUnexpectedStateError } from '@/src/devops/errors';
2121
import { AdminBlockingOptions } from '@/src/devops/types';
22+
import {
23+
MkTimeoutError,
24+
MultiCallTimeoutManager,
25+
SingleCallTimeoutManager,
26+
TimeoutManager,
27+
TimeoutOptions,
28+
} from '@/src/api/timeout-managers';
2229

2330
interface DevopsApiRequestInfo {
2431
path: string,
25-
timeout?: number,
2632
method: HTTP_METHODS,
2733
data?: Record<string, any>,
2834
params?: Record<string, any>,
2935
}
3036

37+
interface LongRunningRequestInfo {
38+
id: string | ((resp: AxiosResponse) => string),
39+
target: string,
40+
legalStates: string[],
41+
defaultPollInterval: number,
42+
options: AdminBlockingOptions | undefined,
43+
}
44+
3145
export class DevopsApiHttpClient extends HttpClient {
3246
constructor(props: HTTPClientOptions) {
3347
super(props);
3448
this.requestStrategy = new HTTP1Strategy(HTTP1AuthHeaderFactories.DevopsApi);
3549
}
3650

37-
public async request(info: DevopsApiRequestInfo): Promise<AxiosResponse> {
51+
public multiCallTimeoutManager(timeoutMs: number | undefined) {
52+
return mkTimeoutManager(MultiCallTimeoutManager, timeoutMs);
53+
}
54+
55+
public async request(info: DevopsApiRequestInfo, options: TimeoutOptions | undefined): Promise<AxiosResponse> {
3856
try {
57+
const timeoutManager = options?.timeoutManager ?? mkTimeoutManager(SingleCallTimeoutManager, options?.maxTimeMS);
3958
const url = this.baseUrl + info.path;
4059

4160
return await this._request({
4261
url: url,
4362
method: info.method,
44-
timeout: info.timeout || DEFAULT_TIMEOUT,
45-
timeoutError: () => new DevopsApiTimeout(url, info.timeout || DEFAULT_TIMEOUT),
4663
params: info.params,
4764
data: info.data,
65+
timeoutManager,
4866
}) as any;
4967
} catch (e) {
5068
if (!(e instanceof AxiosError)) {
@@ -54,15 +72,29 @@ export class DevopsApiHttpClient extends HttpClient {
5472
}
5573
}
5674

57-
public async awaitStatus(idRef: { id: string }, target: string, legalStates: string[], options: AdminBlockingOptions | undefined, defaultPollInterval: number): Promise<void> {
75+
public async requestLongRunning(req: DevopsApiRequestInfo, info: LongRunningRequestInfo): Promise<AxiosResponse> {
76+
const timeoutManager = this.multiCallTimeoutManager(info.options?.maxTimeMS);
77+
const resp = await this.request(req, { timeoutManager });
78+
79+
const id = (typeof info.id === 'function')
80+
? info.id(resp)
81+
: info.id;
82+
83+
await this._awaitStatus(id, 'ACTIVE', ['MAINTENANCE'], info.options, info.defaultPollInterval, timeoutManager);
84+
return resp;
85+
}
86+
87+
private async _awaitStatus(id: string, target: string, legalStates: string[], options: AdminBlockingOptions | undefined, defaultPollInterval: number, timeoutManager: TimeoutManager): Promise<void> {
5888
if (options?.blocking === false) {
5989
return;
6090
}
6191

6292
for (;;) {
6393
const resp = await this.request({
6494
method: HTTP_METHODS.Get,
65-
path: `/databases/${idRef.id}`,
95+
path: `/databases/${id}`,
96+
}, {
97+
timeoutManager: timeoutManager,
6698
});
6799

68100
if (resp.data?.status === target) {
@@ -79,3 +111,12 @@ export class DevopsApiHttpClient extends HttpClient {
79111
}
80112
}
81113
}
114+
115+
const mkTimeoutManager = (constructor: new (maxMs: number, mkTimeoutError: MkTimeoutError) => TimeoutManager, maxMs: number | undefined) => {
116+
const timeout = maxMs ?? DEFAULT_TIMEOUT;
117+
return new constructor(timeout, mkTimeoutErrorMaker(timeout));
118+
}
119+
120+
const mkTimeoutErrorMaker = (timeout: number): MkTimeoutError => {
121+
return (info) => new DevopsApiTimeout(info.url, timeout);
122+
}

src/api/http-client.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { CLIENT_USER_AGENT, DEFAULT_TIMEOUT } from '@/src/api/constants';
16-
import {
17-
GuaranteedAPIResponse,
18-
HTTPClientOptions,
19-
HTTPRequestInfo,
20-
HTTPRequestStrategy
21-
} from '@/src/api/types';
15+
import { CLIENT_USER_AGENT } from '@/src/api/constants';
16+
import { GuaranteedAPIResponse, HTTPClientOptions, HTTPRequestInfo, HTTPRequestStrategy } from '@/src/api/types';
2217
import { HTTP1AuthHeaderFactories, HTTP1Strategy } from '@/src/api/http1';
2318
import { HTTP2Strategy } from '@/src/api/http2';
2419
import { Mutable } from '@/src/data-api/types/utils';
@@ -96,12 +91,11 @@ export class HttpClient {
9691
return await this.requestStrategy.request({
9792
url: info.url,
9893
data: info.data,
99-
timeout: info.timeout || DEFAULT_TIMEOUT,
10094
method: info.method,
10195
params: info.params ?? {},
10296
token: this.#applicationToken,
10397
userAgent: this.userAgent,
104-
timeoutError: info.timeoutError,
98+
timeoutManager: info.timeoutManager,
10599
reviver: info.reviver,
106100
});
107101
}

src/api/http1.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class HTTP1Strategy implements HTTPRequestStrategy {
6868
data: info.data,
6969
params: info.params,
7070
method: info.method,
71-
timeout: info.timeout,
71+
timeout: info.timeoutManager.msRemaining,
7272
headers: {
7373
...this._authHeaderFactory(info.token),
7474
'User-Agent': info.userAgent,
@@ -80,7 +80,7 @@ export class HTTP1Strategy implements HTTPRequestStrategy {
8080
});
8181
} catch (e: any) {
8282
if (e.code === 'ECONNABORTED') {
83-
throw info.timeoutError();
83+
throw info.timeoutManager.mkTimeoutError(info);
8484
}
8585
throw e;
8686
}

src/api/http2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class HTTP2Strategy implements HTTPRequestStrategy {
3838
this.#session = this._reviveSession();
3939
}
4040

41-
const timer = setTimeout(() => reject(info.timeoutError()), info.timeout);
41+
const timer = setTimeout(() => reject(info.timeoutManager.mkTimeoutError(info)), info.timeoutManager.msRemaining);
4242

4343
const path = info.url.replace(this.origin, '');
4444
const params = info.params ? `?${new URLSearchParams(info.params).toString()}` : '';

src/api/timeout-managers.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright DataStax, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { InternalHTTPRequestInfo } from '@/src/api/types';
16+
17+
export type TimeoutOptions = {
18+
maxTimeMS?: number,
19+
timeoutManager?: never,
20+
} | {
21+
timeoutManager: TimeoutManager,
22+
maxTimeMS?: never,
23+
}
24+
25+
export type MkTimeoutError = (ctx: InternalHTTPRequestInfo) => Error;
26+
27+
export interface TimeoutManager {
28+
msRemaining: number,
29+
mkTimeoutError: MkTimeoutError,
30+
}
31+
32+
export class SingleCallTimeoutManager implements TimeoutManager {
33+
constructor(readonly msRemaining: number, readonly mkTimeoutError: MkTimeoutError) {}
34+
}
35+
36+
export class MultiCallTimeoutManager implements TimeoutManager {
37+
private _deadline!: number;
38+
private _started: boolean;
39+
40+
constructor(maxMs: number, readonly mkTimeoutError: MkTimeoutError) {
41+
this._deadline = maxMs;
42+
this._started = false;
43+
}
44+
45+
get msRemaining() {
46+
if (!this._started) {
47+
this._started = true;
48+
this._deadline = Date.now() + this._deadline;
49+
}
50+
return this._deadline - Date.now();
51+
}
52+
}

src/api/types.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import type { HTTP_METHODS } from '@/src/api/index';
1616
import { Caller } from '@/src/client';
17+
import { TimeoutManager } from '@/src/api/timeout-managers';
1718

1819
/**
1920
* @internal
@@ -51,9 +52,8 @@ export interface HTTPRequestInfo {
5152
data?: unknown,
5253
params?: Record<string, string>,
5354
method: HTTP_METHODS,
54-
timeout?: number,
55-
timeoutError: () => Error,
5655
reviver?: (key: string, value: any) => any,
56+
timeoutManager: TimeoutManager,
5757
}
5858

5959
/**
@@ -62,7 +62,6 @@ export interface HTTPRequestInfo {
6262
export interface InternalHTTPRequestInfo extends HTTPRequestInfo {
6363
token: string,
6464
method: HTTP_METHODS,
65-
timeout: number,
6665
userAgent: string,
6766
}
6867

src/common/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright DataStax, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
export * from './types';

src/common/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright DataStax, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
export interface WithTimeout {
16+
maxTimeMS?: number;
17+
}

0 commit comments

Comments
 (0)