Add exponential delay at reconnect
authorJérôme Benoit <jerome.benoit@sap.com>
Sun, 22 Nov 2020 22:07:52 +0000 (23:07 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sun, 22 Nov 2020 22:07:52 +0000 (23:07 +0100)
The feature is still buggy ...

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
docker/config.json
src/assets/config-template.json
src/charging-station/ChargingStation.ts
src/types/ChargingStationTemplate.ts
src/types/ConfigurationData.ts
src/utils/Configuration.ts
src/utils/Utils.ts

index aee90bc77ecc2498c9f0cb98c2a9981871c3bb07..7dcbd0b174bea90dc7079899570d501f50470cc2 100644 (file)
@@ -3,7 +3,7 @@
     "ws://server:8010/OCPP16/5c866e81a2d9593de43efdb4"
   ],
   "statisticsDisplayInterval": 60,
-  "autoReconnectTimeout": 10,
+  "connectionTimeout": 10,
   "autoReconnectMaxRetries": 10,
   "distributeStationsToTenantsEqually": true,
   "useWorkerPool": false,
index 7359a304a6a3a74ee7e1c9e3a149bf17b9145a52..7b3dc1482751fbd77357eb49d8aad924fceb5f53 100644 (file)
@@ -2,7 +2,7 @@
   "supervisionURLs": [
     "ws://localhost:8010/OCPP16/5be7fb271014d90008992f06"
   ],
-  "autoReconnectTimeout": 10,
+  "connectionTimeout": 10,
   "autoReconnectMaxRetries": 10,
   "statisticsDisplayInterval": 60,
   "distributeStationsToTenantsEqually": true,
index 85045f7eb0aec2b391f9f6d041cdfe6ee95b8174..b63bd2c20ec15d96eb15a6b86512522b2855cd6a 100644 (file)
@@ -42,9 +42,9 @@ export default class ChargingStation {
   private _wsConnection: WebSocket;
   private _hasStopped: boolean;
   private _hasSocketRestarted: boolean;
+  private _connectionTimeout: number;
   private _autoReconnectRetryCount: number;
   private _autoReconnectMaxRetries: number;
-  private _autoReconnectTimeout: number;
   private _requests: Requests;
   private _messageQueue: string[];
   private _automaticTransactionGeneration: AutomaticTransactionGenerator;
@@ -63,9 +63,9 @@ export default class ChargingStation {
 
     this._hasStopped = false;
     this._hasSocketRestarted = false;
+    this._connectionTimeout = Configuration.getConnectionTimeout() * 1000; // Ms, zero for disabling
     this._autoReconnectRetryCount = 0;
     this._autoReconnectMaxRetries = Configuration.getAutoReconnectMaxRetries(); // -1 for unlimited
-    this._autoReconnectTimeout = Configuration.getAutoReconnectTimeout() * 1000; // Ms, zero for disabling
 
     this._requests = {} as Requests;
     this._messageQueue = [] as string[];
@@ -295,7 +295,7 @@ export default class ChargingStation {
     return !Utils.isUndefined(this._stationInfo.voltageOut) ? Utils.convertToInt(this._stationInfo.voltageOut) : defaultVoltageOut;
   }
 
-  _getTransactionidTag(transactionId: number): string {
+  _getTransactionIdTag(transactionId: number): string {
     for (const connector in this._connectors) {
       if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
         return this.getConnector(Utils.convertToInt(connector)).idTag;
@@ -322,6 +322,10 @@ export default class ChargingStation {
     return supervisionUrls as string;
   }
 
+  _getReconnectExponentialDelay(): boolean {
+    return !Utils.isUndefined(this._stationInfo.reconnectExponentialDelay) ? this._stationInfo.reconnectExponentialDelay : false;
+  }
+
   _getAuthorizeRemoteTxRequests(): boolean {
     const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests');
     return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
@@ -499,8 +503,14 @@ export default class ChargingStation {
     }
   }
 
-  _openWSConnection(): void {
-    this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16);
+  _openWSConnection(options?: WebSocket.ClientOptions): void {
+    if (Utils.isUndefined(options)) {
+      options = {} as WebSocket.ClientOptions;
+    }
+    if (Utils.isUndefined(options.handshakeTimeout)) {
+      options.handshakeTimeout = this._connectionTimeout;
+    }
+    this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16, options);
     logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl);
   }
 
@@ -537,7 +547,7 @@ export default class ChargingStation {
     this._hasStopped = true;
   }
 
-  _reconnect(error): void {
+  async _reconnect(error): Promise<void> {
     logger.error(this._logPrefix() + ' Socket: abnormally closed: %j', error);
     // Stop heartbeat
     this._stopHeartbeat();
@@ -548,16 +558,15 @@ export default class ChargingStation {
       !this._automaticTransactionGeneration.timeToStop) {
       this._automaticTransactionGeneration.stop().catch(() => { });
     }
-    if (this._autoReconnectTimeout !== 0 &&
-      (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) {
-      logger.error(`${this._logPrefix()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`);
+    if (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1) {
       this._autoReconnectRetryCount++;
-      setTimeout(() => {
-        logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount.toString());
-        this._openWSConnection();
-      }, this._autoReconnectTimeout);
-    } else if (this._autoReconnectTimeout !== 0 || this._autoReconnectMaxRetries !== -1) {
-      logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectTimeout})`);
+      const reconnectDelay = (this._getReconnectExponentialDelay() ? Utils.exponentialDelay(this._autoReconnectRetryCount) : this._connectionTimeout);
+      logger.error(`${this._logPrefix()} Socket: connection retry in ${Utils.roundTo(reconnectDelay, 2)}ms, timeout ${reconnectDelay - 100}ms`);
+      await Utils.sleep(reconnectDelay);
+      logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount.toString());
+      this._openWSConnection({ handshakeTimeout : reconnectDelay - 100 });
+    } else if (this._autoReconnectMaxRetries !== -1) {
+      logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectMaxRetries})`);
     }
   }
 
@@ -578,15 +587,15 @@ export default class ChargingStation {
         });
       }
     }
-    this._hasSocketRestarted = false;
     this._autoReconnectRetryCount = 0;
+    this._hasSocketRestarted = false;
   }
 
-  onError(errorEvent): void {
+  async onError(errorEvent): Promise<void> {
     switch (errorEvent.code) {
       case 'ECONNREFUSED':
         this._hasSocketRestarted = true;
-        this._reconnect(errorEvent);
+        await this._reconnect(errorEvent);
         break;
       default:
         logger.error(this._logPrefix() + ' Socket error: %j', errorEvent);
@@ -594,7 +603,7 @@ export default class ChargingStation {
     }
   }
 
-  onClose(closeEvent): void {
+  async onClose(closeEvent): Promise<void> {
     switch (closeEvent) {
       case 1000: // Normal close
       case 1005:
@@ -603,7 +612,7 @@ export default class ChargingStation {
         break;
       default: // Abnormal close
         this._hasSocketRestarted = true;
-        this._reconnect(closeEvent);
+        await this._reconnect(closeEvent);
         break;
     }
   }
@@ -732,7 +741,7 @@ export default class ChargingStation {
   }
 
   async sendStopTransaction(transactionId: number, reason: StopTransactionReason = StopTransactionReason.NONE): Promise<StopTransactionResponse> {
-    const idTag = this._getTransactionidTag(transactionId);
+    const idTag = this._getTransactionIdTag(transactionId);
     try {
       const payload = {
         transactionId,
index 255fb8d0563fe83be9f1366af79745b73d4e01d2..5c2081db19589ee7449c8b3fe2bc7aee8ffe4a13 100644 (file)
@@ -40,6 +40,7 @@ export default interface ChargingStationTemplate {
   useConnectorId0?: boolean;
   randomConnectors?: boolean;
   resetTime?: number;
+  reconnectExponentialDelay?: boolean;
   enableStatistics?: boolean;
   voltageOut?: number;
   Configuration?: ChargingStationConfiguration;
index bf4420ec5f56b0b3f31718d69df0408dfefd0e2a..371e9c5d558587d137a031655f2dd4cc3c4757f9 100644 (file)
@@ -7,7 +7,7 @@ export default interface ConfigurationData {
   supervisionURLs?: string[];
   stationTemplateURLs: StationTemplateURL[];
   statisticsDisplayInterval?: number;
-  autoReconnectTimeout?: number;
+  connectionTimeout?: number;
   autoReconnectMaxRetries?: number;
   distributeStationsToTenantsEqually?: boolean;
   useWorkerPool?: boolean;
index ed5eb2d4234ab7477cb905d86b4519fcf737850c..fef4d93a5da06787a5bcfbc71fea74b0446b2a9a 100644 (file)
@@ -11,9 +11,10 @@ export default class Configuration {
     return Utils.objectHasOwnProperty(Configuration.getConfig(), 'statisticsDisplayInterval') ? Configuration.getConfig().statisticsDisplayInterval : 60;
   }
 
-  static getAutoReconnectTimeout(): number {
+  static getConnectionTimeout(): number {
+    Configuration.deprecateConfigurationKey('autoReconnectTimeout', 'Use \'connectionTimeout\' instead');
     // Read conf
-    return Utils.objectHasOwnProperty(Configuration.getConfig(), 'autoReconnectTimeout') ? Configuration.getConfig().autoReconnectTimeout : 10;
+    return Utils.objectHasOwnProperty(Configuration.getConfig(), 'connectionTimeout') ? Configuration.getConfig().connectionTimeout : 10;
   }
 
   static getAutoReconnectMaxRetries(): number {
index d6fe05d42c7c282d3fe5e2dcafa18eff5cb694e1..a1f28155f26b21460cf43beb141be2b34ba513d4 100644 (file)
@@ -176,4 +176,14 @@ export default class Utils {
   }
 
   static insertAt = (str: string, subStr: string, pos: number): string => `${str.slice(0, pos)}${subStr}${str.slice(pos)}`;
+
+  /**
+   * @param  {number} [retryNumber=0]
+   * @return {number} - delay in milliseconds
+   */
+  static exponentialDelay(retryNumber = 0): number {
+    const delay = Math.pow(2, retryNumber) * 100;
+    const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay
+    return delay + randomSum;
+  }
 }