node-opcua-client
Version:
pure nodejs OPCUA SDK - module client
218 lines • 11.5 kB
JavaScript
"use strict";
/**
* @module node-opcua-client
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClientSessionKeepAliveManager = void 0;
const chalk_1 = __importDefault(require("chalk"));
const events_1 = require("events");
const node_opcua_assert_1 = require("node-opcua-assert");
const node_opcua_basic_types_1 = require("node-opcua-basic-types");
const node_opcua_common_1 = require("node-opcua-common");
const node_opcua_constants_1 = require("node-opcua-constants");
const node_opcua_debug_1 = require("node-opcua-debug");
const node_opcua_nodeid_1 = require("node-opcua-nodeid");
const node_opcua_secure_channel_1 = require("node-opcua-secure-channel");
const node_opcua_status_code_1 = require("node-opcua-status-code");
const serverStatusStateNodeId = (0, node_opcua_nodeid_1.coerceNodeId)(node_opcua_constants_1.VariableIds.Server_ServerStatus_State);
const debugLog = (0, node_opcua_debug_1.make_debugLog)(__filename);
const doDebug = (0, node_opcua_debug_1.checkDebugFlag)(__filename);
const warningLog = (0, node_opcua_debug_1.make_warningLog)(__filename);
const maxBackoffInterval = 60_000;
class ClientSessionKeepAliveManager extends events_1.EventEmitter {
session;
timerId;
pingTimeout;
lastKnownState;
transactionInProgress = false;
consecutiveFailures = 0;
count = 0;
checkInterval;
constructor(session) {
super();
this.session = session;
this.timerId = undefined;
this.pingTimeout = 0;
this.checkInterval = 0;
this.count = 0;
}
start(keepAliveInterval) {
(0, node_opcua_assert_1.assert)(!this.timerId);
/* c8 ignore next*/
if (this.session.timeout < 600) {
warningLog(`[NODE-OPCUA-W13] ClientSessionKeepAliveManager detected that the session timeout (${this.session.timeout} ms) is really too small: please adjust it to a greater value ( at least 1000))`);
}
/* c8 ignore next*/
if (this.session.timeout < 100) {
throw new Error(`ClientSessionKeepAliveManager detected that the session timeout (${this.session.timeout} ms) is really too small: please adjust it to a greater value ( at least 1000))`);
}
const selectedCheckInterval = keepAliveInterval ||
Math.min(Math.floor(Math.min((this.session.timeout * 2) / 3, 20000)), node_opcua_secure_channel_1.ClientSecureChannelLayer.defaultTransportTimeout);
this.checkInterval = selectedCheckInterval;
this.pingTimeout = Math.floor(Math.min(Math.max(50, selectedCheckInterval / 2), 20000));
// make sure first one is almost immediate
this.timerId = setTimeout(() => this.ping_server(), this.pingTimeout);
}
stop() {
if (this.timerId) {
debugLog("ClientSessionKeepAliveManager#stop");
clearTimeout(this.timerId);
this.timerId = undefined;
}
else {
debugLog("warning ClientSessionKeepAliveManager#stop ignore (already stopped)");
}
}
ping_server() {
this._ping_server().then((delta) => {
if (!this.session || this.session.hasBeenClosed()) {
return; // stop here
}
if (this.timerId) {
// When delta exceeds checkInterval it is an explicit backoff requested by _ping_server;
// otherwise delta is the time already consumed this cycle.
const timeout = delta > this.checkInterval ? delta : Math.max(1, this.checkInterval - delta);
this.timerId = setTimeout(() => this.ping_server(), timeout);
}
});
}
/**
* @private
* when a session is opened on a server, the client shall send request on a regular basis otherwise the server
* session object might time out.
* start_ping make sure that ping_server is called on a regular basis to prevent session to timeout.
*
*/
async _ping_server() {
const session = this.session;
if (!session || session.isReconnecting) {
debugLog("ClientSessionKeepAliveManager#ping_server => no session available");
return 0;
}
if (!this.timerId) {
return 0; // keep-alive has been canceled ....
}
const now = Date.now();
const timeSinceLastServerContact = now - session.lastResponseReceivedTime.getTime();
if (timeSinceLastServerContact < this.pingTimeout) {
debugLog("ClientSessionKeepAliveManager#ping_server skipped because last communication with server was not that long ago ping timeout=", Math.round(this.pingTimeout), "timeSinceLastServerContact = ", timeSinceLastServerContact);
// no need to send a ping yet
return timeSinceLastServerContact - this.pingTimeout;
}
if (session.isReconnecting) {
debugLog("ClientSessionKeepAliveManager#ping_server skipped because client is reconnecting");
return 0;
}
if (session.hasBeenClosed()) {
debugLog("ClientSessionKeepAliveManager#ping_server skipped because client is reconnecting");
return 0;
}
debugLog("ClientSessionKeepAliveManager#ping_server timeSinceLastServerContact=", timeSinceLastServerContact, "timeout", this.session.timeout);
if (this.transactionInProgress) {
// readVariable already taking place ! Ignore
return 0;
}
this.transactionInProgress = true;
// Server_ServerStatus_State
return new Promise((resolve) => {
session.read({
nodeId: serverStatusStateNodeId,
attributeId: node_opcua_basic_types_1.AttributeIds.Value
}, (err, dataValue) => {
this.transactionInProgress = false;
if (err) {
warningLog(chalk_1.default.cyan(" warning : ClientSessionKeepAliveManager#ping_server "), chalk_1.default.yellow(err.message));
const serviceFaultResponse = err.response;
if (serviceFaultResponse) {
const sc = serviceFaultResponse.responseHeader?.serviceResult;
if (sc?.equals(node_opcua_status_code_1.StatusCodes.BadSessionIdInvalid) || sc?.equals(node_opcua_status_code_1.StatusCodes.BadSessionClosed)) {
this.emit("failure");
warningLog("Keep alive has failed, considering a network outage is in place, forcing a reconnection");
terminateConnection(session._client);
resolve(0);
}
else {
if (sc?.equals(node_opcua_status_code_1.StatusCodes.BadInvalidTimestamp)) {
// BadInvalidTimestamp (OPC UA Part 4 7.38.2, Table 178:
// "The timestamp is outside the range allowed by the Server")
// refers to the timestamp field of the RequestHeader
// (OPC UA Part 4 7.32), which the spec states is used
// "only for diagnostic and logging purposes in the Server".
//
// The server responded at the OPC UA application layer:
// the SecureChannel and Session are intact. The cause is
// clock skew between client and server; this is an
// infrastructure concern outside the scope of the keepalive
// manager.
//
// Treating this as a keepalive failure is semantically
// incorrect: the round-trip succeeded. Incrementing
// consecutiveFailures leads to unbounded exponential backoff
// and eventual session expiry server-side, triggering an
// unnecessary reconnect loop.
//
// See: https://reference.opcfoundation.org/Core/Part4/v105/docs/7.38.2
// https://reference.opcfoundation.org/Core/Part4/v105/docs/7.32
this.consecutiveFailures = 0;
debugLog("emit keepalive (BadInvalidTimestamp: session alive, clock skew on request timestamp)");
this.emit("keepalive", this.lastKnownState ?? node_opcua_common_1.ServerState.Unknown, this.count);
resolve(0);
return;
}
this.consecutiveFailures++;
warningLog("Keep alive received ServiceFault from server (session intact):", sc?.toString());
this.emit("keepalive_failure");
resolve(Math.min(this.checkInterval * 2 ** this.consecutiveFailures, maxBackoffInterval));
}
}
else {
this.emit("failure");
warningLog("Keep alive has failed, considering a network outage is in place, forcing a reconnection");
terminateConnection(session._client);
resolve(0);
}
return;
}
if (!dataValue || !dataValue.value) {
/**
* @event failure
* raised when the server is not responding or is responding with en error to
* the keep alive read Variable value transaction
*/
this.emit("failure");
warningLog("Keep alive has failed, considering a network outage is in place, forcing a reconnection");
terminateConnection(session._client);
resolve(0);
return;
}
if (dataValue.statusCode.isGood()) {
const newState = dataValue.value.value;
// c8 ignore next
if (newState !== this.lastKnownState && this.lastKnownState) {
warningLog("ClientSessionKeepAliveManager#Server state has changed = ", node_opcua_common_1.ServerState[newState], " was ", node_opcua_common_1.ServerState[this.lastKnownState]);
}
this.lastKnownState = newState;
this.count++; // increase successful counter
}
this.consecutiveFailures = 0;
debugLog("emit keepalive");
this.emit("keepalive", this.lastKnownState, this.count);
resolve(0);
});
});
}
}
exports.ClientSessionKeepAliveManager = ClientSessionKeepAliveManager;
function terminateConnection(client) {
if (!client)
return;
const channel = client._secureChannel;
if (!channel) {
return;
}
channel.forceConnectionBreak();
}
//# sourceMappingURL=client_session_keepalive_manager.js.map