Skip to content
This repository was archived by the owner on Dec 14, 2025. It is now read-only.

Commit 25bc116

Browse files
author
Benjamin E. Coe
authored
feat!: isAvailable now tries both DNS and IP, choosing whichever responds first (#239)
1 parent 871a2cb commit 25bc116

File tree

3 files changed

+111
-14
lines changed

3 files changed

+111
-14
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"samples-test": "npm link && cd samples/ && npm link ../ && npm test && cd ../",
1818
"presystem-test": "npm run compile",
1919
"system-test": "mocha build/system-test --timeout 600000",
20-
"test": "c8 mocha build/test",
20+
"test": "c8 mocha --timeout=5000 build/test",
2121
"docs": "compodoc src/",
2222
"lint": "gts check && eslint '**/*.js'",
2323
"docs-test": "linkinator docs",

src/index.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
* See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
66
*/
77

8-
import {request} from 'gaxios';
8+
import {GaxiosOptions, GaxiosResponse, request} from 'gaxios';
99
import {OutgoingHttpHeaders} from 'http';
1010
const jsonBigint = require('json-bigint');
1111

1212
export const HOST_ADDRESS = 'http://169.254.169.254';
1313
export const BASE_PATH = '/computeMetadata/v1';
1414
export const BASE_URL = HOST_ADDRESS + BASE_PATH;
15+
export const SECONDARY_HOST_ADDRESS = 'http://metadata.google.internal.';
16+
export const SECONDARY_BASE_URL = SECONDARY_HOST_ADDRESS + BASE_PATH;
1517
export const HEADER_NAME = 'Metadata-Flavor';
1618
export const HEADER_VALUE = 'Google';
1719
export const HEADERS = Object.freeze({[HEADER_NAME]: HEADER_VALUE});
@@ -47,7 +49,8 @@ function validate(options: Options) {
4749
async function metadataAccessor<T>(
4850
type: string,
4951
options?: string | Options,
50-
noResponseRetries = 3
52+
noResponseRetries = 3,
53+
fastFail = false
5154
): Promise<T> {
5255
options = options || {};
5356
if (typeof options === 'string') {
@@ -59,7 +62,8 @@ async function metadataAccessor<T>(
5962
}
6063
validate(options);
6164
try {
62-
const res = await request<T>({
65+
const requestMethod = fastFail ? fastFailMetadataRequest : request;
66+
const res = await requestMethod<T>({
6367
url: `${BASE_URL}/${type}${property}`,
6468
headers: Object.assign({}, HEADERS, options.headers),
6569
retryConfig: {noResponseRetries},
@@ -91,6 +95,16 @@ async function metadataAccessor<T>(
9195
}
9296
}
9397

98+
async function fastFailMetadataRequest<T>(
99+
options: GaxiosOptions
100+
): Promise<GaxiosResponse> {
101+
const secondaryOptions = {
102+
...options,
103+
url: options.url!.replace(BASE_URL, SECONDARY_BASE_URL),
104+
};
105+
return Promise.race([request<T>(options), request<T>(secondaryOptions)]);
106+
}
107+
94108
// tslint:disable-next-line no-any
95109
export function instance<T = any>(options?: string | Options) {
96110
return metadataAccessor<T>('instance', options);
@@ -109,11 +123,18 @@ export async function isAvailable() {
109123
// Attempt to read instance metadata. As configured, this will
110124
// retry 3 times if there is a valid response, and fail fast
111125
// if there is an ETIMEDOUT or ENOTFOUND error.
112-
await metadataAccessor('instance', undefined, 0);
126+
await metadataAccessor('instance', undefined, 0, true);
113127
return true;
114128
} catch (err) {
115-
// Failure to resolve the metadata service means that it is not available.
116-
if (err.code && (err.code === 'ENOTFOUND' || err.code === 'ENOENT')) {
129+
if (err.type === 'request-timeout') {
130+
// If running in a GCP environment, metadata endpoint should return
131+
// within ms.
132+
return false;
133+
} else if (
134+
err.code &&
135+
(err.code === 'ENOTFOUND' || err.code === 'ENOENT')
136+
) {
137+
// Failure to resolve the metadata service means that it is not available.
117138
return false;
118139
}
119140
// Throw unexpected errors.

test/index.test.ts

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import * as gcp from '../src';
1111

1212
const assertRejects = require('assert-rejects');
1313

14+
// the metadata IP entry:
1415
const HOST = gcp.HOST_ADDRESS;
16+
// the metadata DNS entry:
17+
const SECONDARY_HOST = gcp.SECONDARY_HOST_ADDRESS;
1518
const PATH = gcp.BASE_PATH;
1619
const TYPE = 'instance';
1720
const PROPERTY = 'property';
@@ -174,33 +177,106 @@ it('should retry on DNS errors', async () => {
174177
assert(data);
175178
});
176179

177-
it('should report isGCE if the server returns a 500 first', async () => {
178-
const scope = nock(HOST)
180+
async function secondaryHostRequest(
181+
delay: number,
182+
responseType = 'success'
183+
): Promise<void> {
184+
let secondary: nock.Scope;
185+
if (responseType === 'success') {
186+
secondary = nock(SECONDARY_HOST)
187+
.get(`${PATH}/${TYPE}`)
188+
.delayConnection(delay)
189+
.reply(200, {}, HEADERS);
190+
} else {
191+
secondary = nock(SECONDARY_HOST)
192+
.get(`${PATH}/${TYPE}`)
193+
.delayConnection(delay)
194+
.replyWithError({code: responseType});
195+
}
196+
return new Promise((resolve, reject) => {
197+
setTimeout(() => {
198+
try {
199+
secondary.done();
200+
return resolve();
201+
} catch (err) {
202+
return reject(err);
203+
}
204+
}, delay + 50);
205+
});
206+
}
207+
208+
it('should report isGCE if primary server returns 500 followed by 200', async () => {
209+
const secondary = secondaryHostRequest(500);
210+
const primary = nock(HOST)
179211
.get(`${PATH}/${TYPE}`)
180212
.twice()
181213
.reply(500)
182214
.get(`${PATH}/${TYPE}`)
183215
.reply(200, {}, HEADERS);
184216
const isGCE = await gcp.isAvailable();
185-
scope.done();
217+
await secondary;
218+
primary.done();
186219
assert.strictEqual(isGCE, true);
187220
});
188221

189222
it('should fail fast on isAvailable if ENOTFOUND is returned', async () => {
190-
const scope = nock(HOST)
223+
const secondary = secondaryHostRequest(500);
224+
const primary = nock(HOST)
191225
.get(`${PATH}/${TYPE}`)
192226
.replyWithError({code: 'ENOTFOUND'});
193227
const isGCE = await gcp.isAvailable();
194-
scope.done();
228+
await secondary;
229+
primary.done();
195230
assert.strictEqual(false, isGCE);
196231
});
197232

198233
it('should fail fast on isAvailable if ENOENT is returned', async () => {
199-
const scope = nock(HOST)
234+
const secondary = secondaryHostRequest(500);
235+
const primary = nock(HOST)
200236
.get(`${PATH}/${TYPE}`)
201237
.replyWithError({code: 'ENOENT'});
202238
const isGCE = await gcp.isAvailable();
203-
scope.done();
239+
await secondary;
240+
primary.done();
241+
assert.strictEqual(false, isGCE);
242+
});
243+
244+
it('should fail on isAvailable if request times out', async () => {
245+
const secondary = secondaryHostRequest(5000);
246+
const primary = nock(HOST)
247+
.get(`${PATH}/${TYPE}`)
248+
.delayConnection(3500)
249+
// this should never get called, as the 3000 timeout will trigger.
250+
.reply(200, {}, HEADERS);
251+
const isGCE = await gcp.isAvailable();
252+
// secondary is allowed to simply timeout in the aether, to avoid
253+
// having a test that waits 5000 ms.
254+
primary.done();
255+
assert.strictEqual(false, isGCE);
256+
});
257+
258+
it('should report isGCE if secondary responds before primary', async () => {
259+
const secondary = secondaryHostRequest(10);
260+
const primary = nock(HOST)
261+
.get(`${PATH}/${TYPE}`)
262+
.delayConnection(3500)
263+
// this should never get called, as the 3000 timeout will trigger.
264+
.reply(200, {}, HEADERS);
265+
const isGCE = await gcp.isAvailable();
266+
await secondary;
267+
primary.done();
268+
assert.strictEqual(isGCE, true);
269+
});
270+
271+
it('should fail fast on isAvailable if ENOENT is returned by secondary', async () => {
272+
const secondary = secondaryHostRequest(10, 'ENOENT');
273+
const primary = nock(HOST)
274+
.get(`${PATH}/${TYPE}`)
275+
.delayConnection(250)
276+
.replyWithError({code: 'ENOENT'});
277+
const isGCE = await gcp.isAvailable();
278+
await secondary;
279+
primary.done();
204280
assert.strictEqual(false, isGCE);
205281
});
206282

0 commit comments

Comments
 (0)