"use strict"; /* * Copyright 2022 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RetryingCall = exports.MessageBufferTracker = exports.RetryThrottler = void 0; const constants_1 = require("./constants"); const metadata_1 = require("./metadata"); const logging = require("./logging"); const TRACER_NAME = 'retrying_call'; class RetryThrottler { constructor(maxTokens, tokenRatio, previousRetryThrottler) { this.maxTokens = maxTokens; this.tokenRatio = tokenRatio; if (previousRetryThrottler) { /* When carrying over tokens from a previous config, rescale them to the * new max value */ this.tokens = previousRetryThrottler.tokens * (maxTokens / previousRetryThrottler.maxTokens); } else { this.tokens = maxTokens; } } addCallSucceeded() { this.tokens = Math.max(this.tokens + this.tokenRatio, this.maxTokens); } addCallFailed() { this.tokens = Math.min(this.tokens - 1, 0); } canRetryCall() { return this.tokens > this.maxTokens / 2; } } exports.RetryThrottler = RetryThrottler; class MessageBufferTracker { constructor(totalLimit, limitPerCall) { this.totalLimit = totalLimit; this.limitPerCall = limitPerCall; this.totalAllocated = 0; this.allocatedPerCall = new Map(); } allocate(size, callId) { var _a; const currentPerCall = (_a = this.allocatedPerCall.get(callId)) !== null && _a !== void 0 ? _a : 0; if (this.limitPerCall - currentPerCall < size || this.totalLimit - this.totalAllocated < size) { return false; } this.allocatedPerCall.set(callId, currentPerCall + size); this.totalAllocated += size; return true; } free(size, callId) { var _a; if (this.totalAllocated < size) { throw new Error(`Invalid buffer allocation state: call ${callId} freed ${size} > total allocated ${this.totalAllocated}`); } this.totalAllocated -= size; const currentPerCall = (_a = this.allocatedPerCall.get(callId)) !== null && _a !== void 0 ? _a : 0; if (currentPerCall < size) { throw new Error(`Invalid buffer allocation state: call ${callId} freed ${size} > allocated for call ${currentPerCall}`); } this.allocatedPerCall.set(callId, currentPerCall - size); } freeAll(callId) { var _a; const currentPerCall = (_a = this.allocatedPerCall.get(callId)) !== null && _a !== void 0 ? _a : 0; if (this.totalAllocated < currentPerCall) { throw new Error(`Invalid buffer allocation state: call ${callId} allocated ${currentPerCall} > total allocated ${this.totalAllocated}`); } this.totalAllocated -= currentPerCall; this.allocatedPerCall.delete(callId); } } exports.MessageBufferTracker = MessageBufferTracker; const PREVIONS_RPC_ATTEMPTS_METADATA_KEY = 'grpc-previous-rpc-attempts'; class RetryingCall { constructor(channel, callConfig, methodName, host, credentials, deadline, callNumber, bufferTracker, retryThrottler) { this.channel = channel; this.callConfig = callConfig; this.methodName = methodName; this.host = host; this.credentials = credentials; this.deadline = deadline; this.callNumber = callNumber; this.bufferTracker = bufferTracker; this.retryThrottler = retryThrottler; this.listener = null; this.initialMetadata = null; this.underlyingCalls = []; this.writeBuffer = []; /** * The offset of message indices in the writeBuffer. For example, if * writeBufferOffset is 10, message 10 is in writeBuffer[0] and message 15 * is in writeBuffer[5]. */ this.writeBufferOffset = 0; /** * Tracks whether a read has been started, so that we know whether to start * reads on new child calls. This only matters for the first read, because * once a message comes in the child call becomes committed and there will * be no new child calls. */ this.readStarted = false; this.transparentRetryUsed = false; /** * Number of attempts so far */ this.attempts = 0; this.hedgingTimer = null; this.committedCallIndex = null; this.initialRetryBackoffSec = 0; this.nextRetryBackoffSec = 0; if (callConfig.methodConfig.retryPolicy) { this.state = 'RETRY'; const retryPolicy = callConfig.methodConfig.retryPolicy; this.nextRetryBackoffSec = this.initialRetryBackoffSec = Number(retryPolicy.initialBackoff.substring(0, retryPolicy.initialBackoff.length - 1)); } else if (callConfig.methodConfig.hedgingPolicy) { this.state = 'HEDGING'; } else { this.state = 'TRANSPARENT_ONLY'; } } getCallNumber() { return this.callNumber; } trace(text) { logging.trace(constants_1.LogVerbosity.DEBUG, TRACER_NAME, '[' + this.callNumber + '] ' + text); } reportStatus(statusObject) { this.trace('ended with status: code=' + statusObject.code + ' details="' + statusObject.details + '"'); this.bufferTracker.freeAll(this.callNumber); this.writeBufferOffset = this.writeBufferOffset + this.writeBuffer.length; this.writeBuffer = []; process.nextTick(() => { var _a; // Explicitly construct status object to remove progress field (_a = this.listener) === null || _a === void 0 ? void 0 : _a.onReceiveStatus({ code: statusObject.code, details: statusObject.details, metadata: statusObject.metadata, }); }); } cancelWithStatus(status, details) { this.trace('cancelWithStatus code: ' + status + ' details: "' + details + '"'); this.reportStatus({ code: status, details, metadata: new metadata_1.Metadata() }); for (const { call } of this.underlyingCalls) { call.cancelWithStatus(status, details); } } getPeer() { if (this.committedCallIndex !== null) { return this.underlyingCalls[this.committedCallIndex].call.getPeer(); } else { return 'unknown'; } } getBufferEntry(messageIndex) { var _a; return ((_a = this.writeBuffer[messageIndex - this.writeBufferOffset]) !== null && _a !== void 0 ? _a : { entryType: 'FREED', allocated: false, }); } getNextBufferIndex() { return this.writeBufferOffset + this.writeBuffer.length; } clearSentMessages() { if (this.state !== 'COMMITTED') { return; } const earliestNeededMessageIndex = this.underlyingCalls[this.committedCallIndex].nextMessageToSend; for (let messageIndex = this.writeBufferOffset; messageIndex < earliestNeededMessageIndex; messageIndex++) { const bufferEntry = this.getBufferEntry(messageIndex); if (bufferEntry.allocated) { this.bufferTracker.free(bufferEntry.message.message.length, this.callNumber); } } this.writeBuffer = this.writeBuffer.slice(earliestNeededMessageIndex - this.writeBufferOffset); this.writeBufferOffset = earliestNeededMessageIndex; } commitCall(index) { if (this.state === 'COMMITTED') { return; } if (this.underlyingCalls[index].state === 'COMPLETED') { return; } this.trace('Committing call [' + this.underlyingCalls[index].call.getCallNumber() + '] at index ' + index); this.state = 'COMMITTED'; this.committedCallIndex = index; for (let i = 0; i < this.underlyingCalls.length; i++) { if (i === index) { continue; } if (this.underlyingCalls[i].state === 'COMPLETED') { continue; } this.underlyingCalls[i].state = 'COMPLETED'; this.underlyingCalls[i].call.cancelWithStatus(constants_1.Status.CANCELLED, 'Discarded in favor of other hedged attempt'); } this.clearSentMessages(); } commitCallWithMostMessages() { if (this.state === 'COMMITTED') { return; } let mostMessages = -1; let callWithMostMessages = -1; for (const [index, childCall] of this.underlyingCalls.entries()) { if (childCall.state === 'ACTIVE' && childCall.nextMessageToSend > mostMessages) { mostMessages = childCall.nextMessageToSend; callWithMostMessages = index; } } if (callWithMostMessages === -1) { /* There are no active calls, disable retries to force the next call that * is started to be committed. */ this.state = 'TRANSPARENT_ONLY'; } else { this.commitCall(callWithMostMessages); } } isStatusCodeInList(list, code) { return list.some(value => value === code || value.toString().toLowerCase() === constants_1.Status[code].toLowerCase()); } getNextRetryBackoffMs() { var _a; const retryPolicy = (_a = this.callConfig) === null || _a === void 0 ? void 0 : _a.methodConfig.retryPolicy; if (!retryPolicy) { return 0; } const nextBackoffMs = Math.random() * this.nextRetryBackoffSec * 1000; const maxBackoffSec = Number(retryPolicy.maxBackoff.substring(0, retryPolicy.maxBackoff.length - 1)); this.nextRetryBackoffSec = Math.min(this.nextRetryBackoffSec * retryPolicy.backoffMultiplier, maxBackoffSec); return nextBackoffMs; } maybeRetryCall(pushback, callback) { if (this.state !== 'RETRY') { callback(false); return; } const retryPolicy = this.callConfig.methodConfig.retryPolicy; if (this.attempts >= Math.min(retryPolicy.maxAttempts, 5)) { callback(false); return; } let retryDelayMs; if (pushback === null) { retryDelayMs = this.getNextRetryBackoffMs(); } else if (pushback < 0) { this.state = 'TRANSPARENT_ONLY'; callback(false); return; } else { retryDelayMs = pushback; this.nextRetryBackoffSec = this.initialRetryBackoffSec; } setTimeout(() => { var _a, _b; if (this.state !== 'RETRY') { callback(false); return; } if ((_b = (_a = this.retryThrottler) === null || _a === void 0 ? void 0 : _a.canRetryCall()) !== null && _b !== void 0 ? _b : true) { callback(true); this.attempts += 1; this.startNewAttempt(); } }, retryDelayMs); } countActiveCalls() { let count = 0; for (const call of this.underlyingCalls) { if ((call === null || call === void 0 ? void 0 : call.state) === 'ACTIVE') { count += 1; } } return count; } handleProcessedStatus(status, callIndex, pushback) { var _a, _b, _c; switch (this.state) { case 'COMMITTED': case 'TRANSPARENT_ONLY': this.commitCall(callIndex); this.reportStatus(status); break; case 'HEDGING': if (this.isStatusCodeInList((_a = this.callConfig.methodConfig.hedgingPolicy.nonFatalStatusCodes) !== null && _a !== void 0 ? _a : [], status.code)) { (_b = this.retryThrottler) === null || _b === void 0 ? void 0 : _b.addCallFailed(); let delayMs; if (pushback === null) { delayMs = 0; } else if (pushback < 0) { this.state = 'TRANSPARENT_ONLY'; this.commitCall(callIndex); this.reportStatus(status); return; } else { delayMs = pushback; } setTimeout(() => { this.maybeStartHedgingAttempt(); // If after trying to start a call there are no active calls, this was the last one if (this.countActiveCalls() === 0) { this.commitCall(callIndex); this.reportStatus(status); } }, delayMs); } else { this.commitCall(callIndex); this.reportStatus(status); } break; case 'RETRY': if (this.isStatusCodeInList(this.callConfig.methodConfig.retryPolicy.retryableStatusCodes, status.code)) { (_c = this.retryThrottler) === null || _c === void 0 ? void 0 : _c.addCallFailed(); this.maybeRetryCall(pushback, retried => { if (!retried) { this.commitCall(callIndex); this.reportStatus(status); } }); } else { this.commitCall(callIndex); this.reportStatus(status); } break; } } getPushback(metadata) { const mdValue = metadata.get('grpc-retry-pushback-ms'); if (mdValue.length === 0) { return null; } try { return parseInt(mdValue[0]); } catch (e) { return -1; } } handleChildStatus(status, callIndex) { var _a; if (this.underlyingCalls[callIndex].state === 'COMPLETED') { return; } this.trace('state=' + this.state + ' handling status with progress ' + status.progress + ' from child [' + this.underlyingCalls[callIndex].call.getCallNumber() + '] in state ' + this.underlyingCalls[callIndex].state); this.underlyingCalls[callIndex].state = 'COMPLETED'; if (status.code === constants_1.Status.OK) { (_a = this.retryThrottler) === null || _a === void 0 ? void 0 : _a.addCallSucceeded(); this.commitCall(callIndex); this.reportStatus(status); return; } if (this.state === 'COMMITTED') { this.reportStatus(status); return; } const pushback = this.getPushback(status.metadata); switch (status.progress) { case 'NOT_STARTED': // RPC never leaves the client, always safe to retry this.startNewAttempt(); break; case 'REFUSED': // RPC reaches the server library, but not the server application logic if (this.transparentRetryUsed) { this.handleProcessedStatus(status, callIndex, pushback); } else { this.transparentRetryUsed = true; this.startNewAttempt(); } break; case 'DROP': this.commitCall(callIndex); this.reportStatus(status); break; case 'PROCESSED': this.handleProcessedStatus(status, callIndex, pushback); break; } } maybeStartHedgingAttempt() { if (this.state !== 'HEDGING') { return; } if (!this.callConfig.methodConfig.hedgingPolicy) { return; } const hedgingPolicy = this.callConfig.methodConfig.hedgingPolicy; if (this.attempts >= Math.min(hedgingPolicy.maxAttempts, 5)) { return; } this.attempts += 1; this.startNewAttempt(); this.maybeStartHedgingTimer(); } maybeStartHedgingTimer() { var _a, _b, _c; if (this.hedgingTimer) { clearTimeout(this.hedgingTimer); } if (this.state !== 'HEDGING') { return; } if (!this.callConfig.methodConfig.hedgingPolicy) { return; } const hedgingPolicy = this.callConfig.methodConfig.hedgingPolicy; if (this.attempts >= Math.min(hedgingPolicy.maxAttempts, 5)) { return; } const hedgingDelayString = (_a = hedgingPolicy.hedgingDelay) !== null && _a !== void 0 ? _a : '0s'; const hedgingDelaySec = Number(hedgingDelayString.substring(0, hedgingDelayString.length - 1)); this.hedgingTimer = setTimeout(() => { this.maybeStartHedgingAttempt(); }, hedgingDelaySec * 1000); (_c = (_b = this.hedgingTimer).unref) === null || _c === void 0 ? void 0 : _c.call(_b); } startNewAttempt() { const child = this.channel.createLoadBalancingCall(this.callConfig, this.methodName, this.host, this.credentials, this.deadline); this.trace('Created child call [' + child.getCallNumber() + '] for attempt ' + this.attempts); const index = this.underlyingCalls.length; this.underlyingCalls.push({ state: 'ACTIVE', call: child, nextMessageToSend: 0, }); const previousAttempts = this.attempts - 1; const initialMetadata = this.initialMetadata.clone(); if (previousAttempts > 0) { initialMetadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); } let receivedMetadata = false; child.start(initialMetadata, { onReceiveMetadata: metadata => { this.trace('Received metadata from child [' + child.getCallNumber() + ']'); this.commitCall(index); receivedMetadata = true; if (previousAttempts > 0) { metadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); } if (this.underlyingCalls[index].state === 'ACTIVE') { this.listener.onReceiveMetadata(metadata); } }, onReceiveMessage: message => { this.trace('Received message from child [' + child.getCallNumber() + ']'); this.commitCall(index); if (this.underlyingCalls[index].state === 'ACTIVE') { this.listener.onReceiveMessage(message); } }, onReceiveStatus: status => { this.trace('Received status from child [' + child.getCallNumber() + ']'); if (!receivedMetadata && previousAttempts > 0) { status.metadata.set(PREVIONS_RPC_ATTEMPTS_METADATA_KEY, `${previousAttempts}`); } this.handleChildStatus(status, index); }, }); this.sendNextChildMessage(index); if (this.readStarted) { child.startRead(); } } start(metadata, listener) { this.trace('start called'); this.listener = listener; this.initialMetadata = metadata; this.attempts += 1; this.startNewAttempt(); this.maybeStartHedgingTimer(); } handleChildWriteCompleted(childIndex) { var _a, _b; const childCall = this.underlyingCalls[childIndex]; const messageIndex = childCall.nextMessageToSend; (_b = (_a = this.getBufferEntry(messageIndex)).callback) === null || _b === void 0 ? void 0 : _b.call(_a); this.clearSentMessages(); childCall.nextMessageToSend += 1; this.sendNextChildMessage(childIndex); } sendNextChildMessage(childIndex) { const childCall = this.underlyingCalls[childIndex]; if (childCall.state === 'COMPLETED') { return; } if (this.getBufferEntry(childCall.nextMessageToSend)) { const bufferEntry = this.getBufferEntry(childCall.nextMessageToSend); switch (bufferEntry.entryType) { case 'MESSAGE': childCall.call.sendMessageWithContext({ callback: error => { // Ignore error this.handleChildWriteCompleted(childIndex); }, }, bufferEntry.message.message); break; case 'HALF_CLOSE': childCall.nextMessageToSend += 1; childCall.call.halfClose(); break; case 'FREED': // Should not be possible break; } } } sendMessageWithContext(context, message) { var _a; this.trace('write() called with message of length ' + message.length); const writeObj = { message, flags: context.flags, }; const messageIndex = this.getNextBufferIndex(); const bufferEntry = { entryType: 'MESSAGE', message: writeObj, allocated: this.bufferTracker.allocate(message.length, this.callNumber), }; this.writeBuffer.push(bufferEntry); if (bufferEntry.allocated) { (_a = context.callback) === null || _a === void 0 ? void 0 : _a.call(context); for (const [callIndex, call] of this.underlyingCalls.entries()) { if (call.state === 'ACTIVE' && call.nextMessageToSend === messageIndex) { call.call.sendMessageWithContext({ callback: error => { // Ignore error this.handleChildWriteCompleted(callIndex); }, }, message); } } } else { this.commitCallWithMostMessages(); // commitCallWithMostMessages can fail if we are between ping attempts if (this.committedCallIndex === null) { return; } const call = this.underlyingCalls[this.committedCallIndex]; bufferEntry.callback = context.callback; if (call.state === 'ACTIVE' && call.nextMessageToSend === messageIndex) { call.call.sendMessageWithContext({ callback: error => { // Ignore error this.handleChildWriteCompleted(this.committedCallIndex); }, }, message); } } } startRead() { this.trace('startRead called'); this.readStarted = true; for (const underlyingCall of this.underlyingCalls) { if ((underlyingCall === null || underlyingCall === void 0 ? void 0 : underlyingCall.state) === 'ACTIVE') { underlyingCall.call.startRead(); } } } halfClose() { this.trace('halfClose called'); const halfCloseIndex = this.getNextBufferIndex(); this.writeBuffer.push({ entryType: 'HALF_CLOSE', allocated: false, }); for (const call of this.underlyingCalls) { if ((call === null || call === void 0 ? void 0 : call.state) === 'ACTIVE' && call.nextMessageToSend === halfCloseIndex) { call.nextMessageToSend += 1; call.call.halfClose(); } } } setCredentials(newCredentials) { throw new Error('Method not implemented.'); } getMethod() { return this.methodName; } getHost() { return this.host; } } exports.RetryingCall = RetryingCall; //# sourceMappingURL=retrying-call.js.map