|
"use strict"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
exports.LoadBalancingCall = void 0; |
|
const connectivity_state_1 = require("./connectivity-state"); |
|
const constants_1 = require("./constants"); |
|
const deadline_1 = require("./deadline"); |
|
const metadata_1 = require("./metadata"); |
|
const picker_1 = require("./picker"); |
|
const uri_parser_1 = require("./uri-parser"); |
|
const logging = require("./logging"); |
|
const control_plane_status_1 = require("./control-plane-status"); |
|
const http2 = require("http2"); |
|
const TRACER_NAME = 'load_balancing_call'; |
|
class LoadBalancingCall { |
|
constructor(channel, callConfig, methodName, host, credentials, deadline, callNumber) { |
|
var _a, _b; |
|
this.channel = channel; |
|
this.callConfig = callConfig; |
|
this.methodName = methodName; |
|
this.host = host; |
|
this.credentials = credentials; |
|
this.deadline = deadline; |
|
this.callNumber = callNumber; |
|
this.child = null; |
|
this.readPending = false; |
|
this.pendingMessage = null; |
|
this.pendingHalfClose = false; |
|
this.ended = false; |
|
this.metadata = null; |
|
this.listener = null; |
|
this.onCallEnded = null; |
|
this.childStartTime = null; |
|
const splitPath = this.methodName.split('/'); |
|
let serviceName = ''; |
|
|
|
|
|
|
|
if (splitPath.length >= 2) { |
|
serviceName = splitPath[1]; |
|
} |
|
const hostname = (_b = (_a = (0, uri_parser_1.splitHostPort)(this.host)) === null || _a === void 0 ? void 0 : _a.host) !== null && _b !== void 0 ? _b : 'localhost'; |
|
|
|
|
|
this.serviceUrl = `https://${hostname}/${serviceName}`; |
|
this.startTime = new Date(); |
|
} |
|
getDeadlineInfo() { |
|
var _a, _b; |
|
const deadlineInfo = []; |
|
if (this.childStartTime) { |
|
if (this.childStartTime > this.startTime) { |
|
if ((_a = this.metadata) === null || _a === void 0 ? void 0 : _a.getOptions().waitForReady) { |
|
deadlineInfo.push('wait_for_ready'); |
|
} |
|
deadlineInfo.push(`LB pick: ${(0, deadline_1.formatDateDifference)(this.startTime, this.childStartTime)}`); |
|
} |
|
deadlineInfo.push(...this.child.getDeadlineInfo()); |
|
return deadlineInfo; |
|
} |
|
else { |
|
if ((_b = this.metadata) === null || _b === void 0 ? void 0 : _b.getOptions().waitForReady) { |
|
deadlineInfo.push('wait_for_ready'); |
|
} |
|
deadlineInfo.push('Waiting for LB pick'); |
|
} |
|
return deadlineInfo; |
|
} |
|
trace(text) { |
|
logging.trace(constants_1.LogVerbosity.DEBUG, TRACER_NAME, '[' + this.callNumber + '] ' + text); |
|
} |
|
outputStatus(status, progress) { |
|
var _a, _b; |
|
if (!this.ended) { |
|
this.ended = true; |
|
this.trace('ended with status: code=' + |
|
status.code + |
|
' details="' + |
|
status.details + |
|
'" start time=' + |
|
this.startTime.toISOString()); |
|
const finalStatus = Object.assign(Object.assign({}, status), { progress }); |
|
(_a = this.listener) === null || _a === void 0 ? void 0 : _a.onReceiveStatus(finalStatus); |
|
(_b = this.onCallEnded) === null || _b === void 0 ? void 0 : _b.call(this, finalStatus.code); |
|
} |
|
} |
|
doPick() { |
|
var _a, _b; |
|
if (this.ended) { |
|
return; |
|
} |
|
if (!this.metadata) { |
|
throw new Error('doPick called before start'); |
|
} |
|
this.trace('Pick called'); |
|
const finalMetadata = this.metadata.clone(); |
|
const pickResult = this.channel.doPick(finalMetadata, this.callConfig.pickInformation); |
|
const subchannelString = pickResult.subchannel |
|
? '(' + |
|
pickResult.subchannel.getChannelzRef().id + |
|
') ' + |
|
pickResult.subchannel.getAddress() |
|
: '' + pickResult.subchannel; |
|
this.trace('Pick result: ' + |
|
picker_1.PickResultType[pickResult.pickResultType] + |
|
' subchannel: ' + |
|
subchannelString + |
|
' status: ' + |
|
((_a = pickResult.status) === null || _a === void 0 ? void 0 : _a.code) + |
|
' ' + |
|
((_b = pickResult.status) === null || _b === void 0 ? void 0 : _b.details)); |
|
switch (pickResult.pickResultType) { |
|
case picker_1.PickResultType.COMPLETE: |
|
const combinedCallCredentials = this.credentials.compose(pickResult.subchannel.getCallCredentials()); |
|
combinedCallCredentials |
|
.generateMetadata({ method_name: this.methodName, service_url: this.serviceUrl }) |
|
.then(credsMetadata => { |
|
var _a; |
|
|
|
|
|
|
|
if (this.ended) { |
|
this.trace('Credentials metadata generation finished after call ended'); |
|
return; |
|
} |
|
finalMetadata.merge(credsMetadata); |
|
if (finalMetadata.get('authorization').length > 1) { |
|
this.outputStatus({ |
|
code: constants_1.Status.INTERNAL, |
|
details: '"authorization" metadata cannot have multiple values', |
|
metadata: new metadata_1.Metadata(), |
|
}, 'PROCESSED'); |
|
} |
|
if (pickResult.subchannel.getConnectivityState() !== |
|
connectivity_state_1.ConnectivityState.READY) { |
|
this.trace('Picked subchannel ' + |
|
subchannelString + |
|
' has state ' + |
|
connectivity_state_1.ConnectivityState[pickResult.subchannel.getConnectivityState()] + |
|
' after getting credentials metadata. Retrying pick'); |
|
this.doPick(); |
|
return; |
|
} |
|
if (this.deadline !== Infinity) { |
|
finalMetadata.set('grpc-timeout', (0, deadline_1.getDeadlineTimeoutString)(this.deadline)); |
|
} |
|
try { |
|
this.child = pickResult |
|
.subchannel.getRealSubchannel() |
|
.createCall(finalMetadata, this.host, this.methodName, { |
|
onReceiveMetadata: metadata => { |
|
this.trace('Received metadata'); |
|
this.listener.onReceiveMetadata(metadata); |
|
}, |
|
onReceiveMessage: message => { |
|
this.trace('Received message'); |
|
this.listener.onReceiveMessage(message); |
|
}, |
|
onReceiveStatus: status => { |
|
this.trace('Received status'); |
|
if (status.rstCode === |
|
http2.constants.NGHTTP2_REFUSED_STREAM) { |
|
this.outputStatus(status, 'REFUSED'); |
|
} |
|
else { |
|
this.outputStatus(status, 'PROCESSED'); |
|
} |
|
}, |
|
}); |
|
this.childStartTime = new Date(); |
|
} |
|
catch (error) { |
|
this.trace('Failed to start call on picked subchannel ' + |
|
subchannelString + |
|
' with error ' + |
|
error.message); |
|
this.outputStatus({ |
|
code: constants_1.Status.INTERNAL, |
|
details: 'Failed to start HTTP/2 stream with error ' + |
|
error.message, |
|
metadata: new metadata_1.Metadata(), |
|
}, 'NOT_STARTED'); |
|
return; |
|
} |
|
(_a = pickResult.onCallStarted) === null || _a === void 0 ? void 0 : _a.call(pickResult); |
|
this.onCallEnded = pickResult.onCallEnded; |
|
this.trace('Created child call [' + this.child.getCallNumber() + ']'); |
|
if (this.readPending) { |
|
this.child.startRead(); |
|
} |
|
if (this.pendingMessage) { |
|
this.child.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message); |
|
} |
|
if (this.pendingHalfClose) { |
|
this.child.halfClose(); |
|
} |
|
}, (error) => { |
|
|
|
const { code, details } = (0, control_plane_status_1.restrictControlPlaneStatusCode)(typeof error.code === 'number' ? error.code : constants_1.Status.UNKNOWN, `Getting metadata from plugin failed with error: ${error.message}`); |
|
this.outputStatus({ |
|
code: code, |
|
details: details, |
|
metadata: new metadata_1.Metadata(), |
|
}, 'PROCESSED'); |
|
}); |
|
break; |
|
case picker_1.PickResultType.DROP: |
|
const { code, details } = (0, control_plane_status_1.restrictControlPlaneStatusCode)(pickResult.status.code, pickResult.status.details); |
|
setImmediate(() => { |
|
this.outputStatus({ code, details, metadata: pickResult.status.metadata }, 'DROP'); |
|
}); |
|
break; |
|
case picker_1.PickResultType.TRANSIENT_FAILURE: |
|
if (this.metadata.getOptions().waitForReady) { |
|
this.channel.queueCallForPick(this); |
|
} |
|
else { |
|
const { code, details } = (0, control_plane_status_1.restrictControlPlaneStatusCode)(pickResult.status.code, pickResult.status.details); |
|
setImmediate(() => { |
|
this.outputStatus({ code, details, metadata: pickResult.status.metadata }, 'PROCESSED'); |
|
}); |
|
} |
|
break; |
|
case picker_1.PickResultType.QUEUE: |
|
this.channel.queueCallForPick(this); |
|
} |
|
} |
|
cancelWithStatus(status, details) { |
|
var _a; |
|
this.trace('cancelWithStatus code: ' + status + ' details: "' + details + '"'); |
|
(_a = this.child) === null || _a === void 0 ? void 0 : _a.cancelWithStatus(status, details); |
|
this.outputStatus({ code: status, details: details, metadata: new metadata_1.Metadata() }, 'PROCESSED'); |
|
} |
|
getPeer() { |
|
var _a, _b; |
|
return (_b = (_a = this.child) === null || _a === void 0 ? void 0 : _a.getPeer()) !== null && _b !== void 0 ? _b : this.channel.getTarget(); |
|
} |
|
start(metadata, listener) { |
|
this.trace('start called'); |
|
this.listener = listener; |
|
this.metadata = metadata; |
|
this.doPick(); |
|
} |
|
sendMessageWithContext(context, message) { |
|
this.trace('write() called with message of length ' + message.length); |
|
if (this.child) { |
|
this.child.sendMessageWithContext(context, message); |
|
} |
|
else { |
|
this.pendingMessage = { context, message }; |
|
} |
|
} |
|
startRead() { |
|
this.trace('startRead called'); |
|
if (this.child) { |
|
this.child.startRead(); |
|
} |
|
else { |
|
this.readPending = true; |
|
} |
|
} |
|
halfClose() { |
|
this.trace('halfClose called'); |
|
if (this.child) { |
|
this.child.halfClose(); |
|
} |
|
else { |
|
this.pendingHalfClose = true; |
|
} |
|
} |
|
setCredentials(credentials) { |
|
throw new Error('Method not implemented.'); |
|
} |
|
getCallNumber() { |
|
return this.callNumber; |
|
} |
|
} |
|
exports.LoadBalancingCall = LoadBalancingCall; |
|
|