Skip to content

Commit caa770c

Browse files
author
Alan Plum
committed
Handle 503
Fixes #710
1 parent a5fdbfd commit caa770c

File tree

3 files changed

+83
-86
lines changed

3 files changed

+83
-86
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ This driver uses semantic versioning:
100100
- Changed return values of `db.getUserAccessLevel` and `db.getUserDatabases`
101101
to match documented return types
102102

103+
- Retry requests resulting in status 503 `ArangoError` ([#710](https://github.com/arangodb/arangojs/issues/710))
104+
105+
Unless retries are explicitly disabled by setting `config.maxRetries` to
106+
`false`, requests will now also be retried if the server responded with a
107+
503 `ArangoError`, which ArangoDB uses to indicate the server is running in
108+
maintenance mode. Previously this would always result in an error.
109+
103110
### Added
104111

105112
- Added `toJSON` method to system errors

src/connection.ts

+75-86
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {
2020
isSystemError,
2121
} from "./error";
2222
import { base64Encode } from "./lib/btoa";
23-
import { ERROR_ARANGO_CONFLICT } from "./lib/codes";
23+
import {
24+
ERROR_ARANGO_CONFLICT,
25+
ERROR_ARANGO_MAINTENANCE_MODE,
26+
} from "./lib/codes";
2427
import { Errback } from "./lib/errback";
2528
import { normalizeUrl } from "./lib/normalizeUrl";
2629
import { querystringify } from "./lib/querystringify";
@@ -283,8 +286,9 @@ type Task = {
283286
stack?: () => string;
284287
allowDirtyRead: boolean;
285288
retryOnConflict: number;
286-
resolve: (res: ArangojsResponse) => void;
289+
resolve: (result: any) => void;
287290
reject: (error: Error) => void;
291+
transform?: (res: ArangojsResponse) => any;
288292
retries: number;
289293
options: {
290294
method: string;
@@ -495,9 +499,7 @@ export class Connection {
495499
protected _arangoVersion: number = 30900;
496500
protected _headers: Headers;
497501
protected _loadBalancingStrategy: LoadBalancingStrategy;
498-
protected _useFailOver: boolean;
499-
protected _shouldRetry: boolean;
500-
protected _maxRetries: number;
502+
protected _maxRetries: number | false;
501503
protected _maxTasks: number;
502504
protected _queue = new LinkedList<Task>();
503505
protected _databases = new Map<string, Database>();
@@ -507,8 +509,8 @@ export class Connection {
507509
protected _activeDirtyHost: number;
508510
protected _transactionId: string | null = null;
509511
protected _precaptureStackTraces: boolean;
510-
protected _responseQueueTimeSamples: number;
511512
protected _queueTimes = new LinkedList<[number, number]>();
513+
protected _responseQueueTimeSamples: number;
512514

513515
/**
514516
* @internal
@@ -543,18 +545,15 @@ export class Connection {
543545
this._maxTasks = this._agentOptions.maxSockets;
544546
this._headers = { ...config.headers };
545547
this._loadBalancingStrategy = config.loadBalancingStrategy ?? "NONE";
546-
this._useFailOver = this._loadBalancingStrategy !== "ROUND_ROBIN";
547548
this._precaptureStackTraces = Boolean(config.precaptureStackTraces);
548549
this._responseQueueTimeSamples = config.responseQueueTimeSamples ?? 10;
549550
if (this._responseQueueTimeSamples < 0) {
550551
this._responseQueueTimeSamples = Infinity;
551552
}
552553
if (config.maxRetries === false) {
553-
this._shouldRetry = false;
554-
this._maxRetries = 0;
554+
this._maxRetries = false;
555555
} else {
556-
this._shouldRetry = true;
557-
this._maxRetries = config.maxRetries ?? 0;
556+
this._maxRetries = Number(config.maxRetries ?? 0);
558557
}
559558

560559
this.addToHostList(URLS);
@@ -615,12 +614,66 @@ export class Connection {
615614
this._activeTasks += 1;
616615
const callback: Errback<ArangojsResponse> = (err, res) => {
617616
this._activeTasks -= 1;
617+
if (!err && res) {
618+
if (res.statusCode === 503 && res.headers[LEADER_ENDPOINT_HEADER]) {
619+
const url = res.headers[LEADER_ENDPOINT_HEADER]!;
620+
const [index] = this.addToHostList(url);
621+
task.host = index;
622+
if (this._activeHost === host) {
623+
this._activeHost = index;
624+
}
625+
this._queue.push(task);
626+
} else {
627+
res.arangojsHostId = host;
628+
const contentType = res.headers["content-type"];
629+
const queueTime = res.headers["x-arango-queue-time-seconds"];
630+
if (queueTime) {
631+
this._queueTimes.push([Date.now(), Number(queueTime)]);
632+
while (this._responseQueueTimeSamples < this._queueTimes.length) {
633+
this._queueTimes.shift();
634+
}
635+
}
636+
let parsedBody: any = undefined;
637+
if (res.body.length && contentType && contentType.match(MIME_JSON)) {
638+
try {
639+
parsedBody = res.body;
640+
parsedBody = JSON.parse(parsedBody);
641+
} catch (e: any) {
642+
if (!task.options.expectBinary) {
643+
if (typeof parsedBody !== "string") {
644+
parsedBody = res.body.toString("utf-8");
645+
}
646+
e.res = res;
647+
if (task.stack) {
648+
e.stack += task.stack();
649+
}
650+
callback(e);
651+
return;
652+
}
653+
}
654+
} else if (res.body && !task.options.expectBinary) {
655+
parsedBody = res.body.toString("utf-8");
656+
} else {
657+
parsedBody = res.body;
658+
}
659+
if (isArangoErrorResponse(parsedBody)) {
660+
res.body = parsedBody;
661+
err = new ArangoError(res);
662+
} else if (res.statusCode && res.statusCode >= 400) {
663+
res.body = parsedBody;
664+
err = new HttpError(res);
665+
} else {
666+
if (!task.options.expectBinary) res.body = parsedBody;
667+
task.resolve(task.transform ? task.transform(res) : (res as any));
668+
}
669+
}
670+
}
618671
if (err) {
619672
if (
620673
!task.allowDirtyRead &&
621674
this._hosts.length > 1 &&
622675
this._activeHost === host &&
623-
this._useFailOver
676+
this._loadBalancingStrategy !== "ROUND_ROBIN"
624677
) {
625678
this._activeHost = (this._activeHost + 1) % this._hosts.length;
626679
}
@@ -632,12 +685,14 @@ export class Connection {
632685
task.retryOnConflict -= 1;
633686
this._queue.push(task);
634687
} else if (
635-
!task.host &&
636-
this._shouldRetry &&
637-
task.retries < (this._maxRetries || this._hosts.length - 1) &&
638-
isSystemError(err) &&
639-
err.syscall === "connect" &&
640-
err.code === "ECONNREFUSED"
688+
((isSystemError(err) &&
689+
err.syscall === "connect" &&
690+
err.code === "ECONNREFUSED") ||
691+
(isArangoError(err) &&
692+
err.errorNum === ERROR_ARANGO_MAINTENANCE_MODE)) &&
693+
task.host === undefined &&
694+
this._maxRetries !== false &&
695+
task.retries < (this._maxRetries || this._hosts.length - 1)
641696
) {
642697
task.retries += 1;
643698
this._queue.push(task);
@@ -647,23 +702,6 @@ export class Connection {
647702
}
648703
task.reject(err);
649704
}
650-
} else {
651-
const response = res!;
652-
if (
653-
response.statusCode === 503 &&
654-
response.headers[LEADER_ENDPOINT_HEADER]
655-
) {
656-
const url = response.headers[LEADER_ENDPOINT_HEADER]!;
657-
const [index] = this.addToHostList(url);
658-
task.host = index;
659-
if (this._activeHost === host) {
660-
this._activeHost = index;
661-
}
662-
this._queue.push(task);
663-
} else {
664-
response.arangojsHostId = host;
665-
task.resolve(response);
666-
}
667705
}
668706
this._runQueue();
669707
};
@@ -922,57 +960,8 @@ export class Connection {
922960
body,
923961
},
924962
reject,
925-
resolve: (res: ArangojsResponse) => {
926-
const contentType = res.headers["content-type"];
927-
const queueTime = res.headers["x-arango-queue-time-seconds"];
928-
if (queueTime) {
929-
this._queueTimes.push([Date.now(), Number(queueTime)]);
930-
while (this._responseQueueTimeSamples < this._queueTimes.length) {
931-
this._queueTimes.shift();
932-
}
933-
}
934-
let parsedBody: any = undefined;
935-
if (res.body.length && contentType && contentType.match(MIME_JSON)) {
936-
try {
937-
parsedBody = res.body;
938-
parsedBody = JSON.parse(parsedBody);
939-
} catch (e: any) {
940-
if (!expectBinary) {
941-
if (typeof parsedBody !== "string") {
942-
parsedBody = res.body.toString("utf-8");
943-
}
944-
e.response = res;
945-
if (task.stack) {
946-
e.stack += task.stack();
947-
}
948-
reject(e);
949-
return;
950-
}
951-
}
952-
} else if (res.body && !expectBinary) {
953-
parsedBody = res.body.toString("utf-8");
954-
} else {
955-
parsedBody = res.body;
956-
}
957-
if (isArangoErrorResponse(parsedBody)) {
958-
res.body = parsedBody;
959-
const err = new ArangoError(res);
960-
if (task.stack) {
961-
err.stack += task.stack();
962-
}
963-
reject(err);
964-
} else if (res.statusCode && res.statusCode >= 400) {
965-
res.body = parsedBody;
966-
const err = new HttpError(res);
967-
if (task.stack) {
968-
err.stack += task.stack();
969-
}
970-
reject(err);
971-
} else {
972-
if (!expectBinary) res.body = parsedBody;
973-
resolve(transform ? transform(res) : (res as any));
974-
}
975-
},
963+
resolve,
964+
transform,
976965
};
977966

978967
if (this._precaptureStackTraces) {

src/lib/codes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
export const TRANSACTION_NOT_FOUND = 10;
11+
export const ERROR_ARANGO_MAINTENANCE_MODE = 503;
1112
export const ERROR_ARANGO_CONFLICT = 1200;
1213
export const ANALYZER_NOT_FOUND = 1202;
1314
export const DOCUMENT_NOT_FOUND = 1202;

0 commit comments

Comments
 (0)