From 7a73ec7c063d7813d9349d9051c503f7076f64e2 Mon Sep 17 00:00:00 2001 From: Benjamin Sergeant Date: Fri, 30 Aug 2019 12:46:35 -0700 Subject: [PATCH] New option to cap the max wait between reconnection attempts. Still default to 10s. (setMaxWaitBetweenReconnectionRetries) (#108) --- .gitignore | 1 + CMakeLists.txt | 2 ++ CHANGELOG.md => docs/CHANGELOG.md | 8 ++++++ docs/usage.md | 42 ++++++++++++++++++++++++++++ ixwebsocket/IXExponentialBackoff.cpp | 26 +++++++++++++++++ ixwebsocket/IXExponentialBackoff.h | 16 +++++++++++ ixwebsocket/IXWebSocket.cpp | 37 +++++++++++------------- ixwebsocket/IXWebSocket.h | 8 +++++- makefile | 6 ++++ ws/ws.cpp | 5 +++- ws/ws.h | 3 +- ws/ws_connect.cpp | 13 ++++++--- 12 files changed, 139 insertions(+), 28 deletions(-) rename CHANGELOG.md => docs/CHANGELOG.md (92%) create mode 100644 ixwebsocket/IXExponentialBackoff.cpp create mode 100644 ixwebsocket/IXExponentialBackoff.h diff --git a/.gitignore b/.gitignore index 6f97ca1a..1e363192 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build *.pyc +venv diff --git a/CMakeLists.txt b/CMakeLists.txt index b654112e..82a0235c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ set( IXWEBSOCKET_SOURCES ixwebsocket/IXCancellationRequest.cpp ixwebsocket/IXConnectionState.cpp ixwebsocket/IXDNSLookup.cpp + ixwebsocket/IXExponentialBackoff.cpp ixwebsocket/IXHttp.cpp ixwebsocket/IXHttpClient.cpp ixwebsocket/IXHttpServer.cpp @@ -53,6 +54,7 @@ set( IXWEBSOCKET_HEADERS ixwebsocket/IXCancellationRequest.h ixwebsocket/IXConnectionState.h ixwebsocket/IXDNSLookup.h + ixwebsocket/IXExponentialBackoff.h ixwebsocket/IXHttp.h ixwebsocket/IXHttpClient.h ixwebsocket/IXHttpServer.h diff --git a/CHANGELOG.md b/docs/CHANGELOG.md similarity index 92% rename from CHANGELOG.md rename to docs/CHANGELOG.md index 41e5338d..90ebb51f 100644 --- a/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog All notable changes to this project will be documented in this file. +## [5.0.9] - 2019-08-30 + +New option to cap the max wait between reconnection attempts. Still default to 10s. (setMaxWaitBetweenReconnectionRetries). + +``` +ws connect --max_wait 5000 ws://example.com # will only wait 5 seconds max between reconnection attempts +``` + ## [5.0.7] - 2019-08-23 - WebSocket: add new option to pass in extra HTTP headers when connecting. - `ws connect` add new option (-H, works like [curl](https://stackoverflow.com/questions/356705/how-to-send-a-header-using-a-http-request-through-a-curl-call)) to pass in extra HTTP headers when connecting diff --git a/docs/usage.md b/docs/usage.md index b28fa68e..c648f585 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -187,6 +187,48 @@ headers["foo"] = "bar"; webSocket.setExtraHeaders(headers); ``` +### Automatic reconnection + +Automatic reconnection kicks in when the connection is disconnected without the user consent. This feature is on by default and can be turned off. + +``` +webSocket.enableAutomaticReconnection(); // turn on +webSocket.disableAutomaticReconnection(); // turn off +bool enabled = webSocket.isAutomaticReconnectionEnabled(); // query state +``` + +The technique to calculate wait time is called [exponential +backoff](https://docs.aws.amazon.com/general/latest/gr/api-retries.html). Here +are the default waiting times between attempts (from connecting with `ws connect ws://foo.com`) + +``` +> Connection error: Got bad status connecting to foo.com, status: 301, HTTP Status line: HTTP/1.1 301 Moved Permanently + +#retries: 1 +Wait time(ms): 100 +#retries: 2 +Wait time(ms): 200 +#retries: 3 +Wait time(ms): 400 +#retries: 4 +Wait time(ms): 800 +#retries: 5 +Wait time(ms): 1600 +#retries: 6 +Wait time(ms): 3200 +#retries: 7 +Wait time(ms): 6400 +#retries: 8 +Wait time(ms): 10000 +``` + +The waiting time is capped by default at 10s between 2 attempts, but that value can be changed and queried. + +``` +webSocket.setMaxWaitBetweenReconnectionRetries(5 * 1000); // 5000ms = 5s +uint32_t m = webSocket.getMaxWaitBetweenReconnectionRetries(); +``` + ## WebSocket server API ``` diff --git a/ixwebsocket/IXExponentialBackoff.cpp b/ixwebsocket/IXExponentialBackoff.cpp new file mode 100644 index 00000000..8ec0ab06 --- /dev/null +++ b/ixwebsocket/IXExponentialBackoff.cpp @@ -0,0 +1,26 @@ +/* + * IXExponentialBackoff.h + * Author: Benjamin Sergeant + * Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved. + */ + +#include "IXExponentialBackoff.h" + +#include + +namespace ix +{ + uint32_t calculateRetryWaitMilliseconds( + uint32_t retry_count, + uint32_t maxWaitBetweenReconnectionRetries) + { + uint32_t wait_time = std::pow(2, retry_count) * 100; + + if (wait_time > maxWaitBetweenReconnectionRetries || wait_time == 0) + { + wait_time = maxWaitBetweenReconnectionRetries; + } + + return wait_time; + } +} diff --git a/ixwebsocket/IXExponentialBackoff.h b/ixwebsocket/IXExponentialBackoff.h new file mode 100644 index 00000000..7c946c21 --- /dev/null +++ b/ixwebsocket/IXExponentialBackoff.h @@ -0,0 +1,16 @@ +/* + * IXExponentialBackoff.h + * Author: Benjamin Sergeant + * Copyright (c) 2017-2019 Machine Zone, Inc. All rights reserved. + */ + +#pragma once + +#include + +namespace ix +{ + uint32_t calculateRetryWaitMilliseconds( + uint32_t retry_count, + uint32_t maxWaitBetweenReconnectionRetries); +} // namespace ix diff --git a/ixwebsocket/IXWebSocket.cpp b/ixwebsocket/IXWebSocket.cpp index d176c2a6..62d782ba 100644 --- a/ixwebsocket/IXWebSocket.cpp +++ b/ixwebsocket/IXWebSocket.cpp @@ -7,30 +7,11 @@ #include "IXWebSocket.h" #include "IXSetThreadName.h" #include "IXWebSocketHandshake.h" +#include "IXExponentialBackoff.h" #include #include -namespace -{ - uint64_t calculateRetryWaitMilliseconds(uint32_t retry_count) - { - uint64_t wait_time; - - if (retry_count <= 6) - { - // max wait_time is 6400 ms (2 ^ 6 = 64) - wait_time = ((uint64_t)std::pow(2, retry_count) * 100L); - } - else - { - wait_time = 10 * 1000; // 10 sec - } - - return wait_time; - } -} - namespace ix { OnTrafficTrackerCallback WebSocket::_onTrafficTrackerCallback = nullptr; @@ -38,11 +19,13 @@ namespace ix const int WebSocket::kDefaultPingIntervalSecs(-1); const int WebSocket::kDefaultPingTimeoutSecs(-1); const bool WebSocket::kDefaultEnablePong(true); + const uint32_t WebSocket::kDefaultMaxWaitBetweenReconnectionRetries(10 * 1000); // 10s WebSocket::WebSocket() : _onMessageCallback(OnMessageCallback()), _stop(false), _automaticReconnection(true), + _maxWaitBetweenReconnectionRetries(kDefaultMaxWaitBetweenReconnectionRetries), _handshakeTimeoutSecs(kDefaultHandShakeTimeoutSecs), _enablePong(kDefaultEnablePong), _pingIntervalSecs(kDefaultPingIntervalSecs), @@ -149,6 +132,18 @@ namespace ix _perMessageDeflateOptions = perMessageDeflateOptions; } + void WebSocket::setMaxWaitBetweenReconnectionRetries(uint32_t maxWaitBetweenReconnectionRetries) + { + std::lock_guard lock(_configMutex); + _maxWaitBetweenReconnectionRetries = maxWaitBetweenReconnectionRetries; + } + + uint32_t WebSocket::getMaxWaitBetweenReconnectionRetries() const + { + std::lock_guard lock(_configMutex); + return _maxWaitBetweenReconnectionRetries; + } + void WebSocket::start() { if (_thread.joinable()) return; // we've already been started @@ -276,7 +271,7 @@ namespace ix if (_automaticReconnection) { - duration = millis(calculateRetryWaitMilliseconds(retries++)); + duration = millis(calculateRetryWaitMilliseconds(retries++, _maxWaitBetweenReconnectionRetries)); connectErr.wait_time = duration.count(); connectErr.retries = retries; diff --git a/ixwebsocket/IXWebSocket.h b/ixwebsocket/IXWebSocket.h index c7ffb2d7..c2bf898d 100644 --- a/ixwebsocket/IXWebSocket.h +++ b/ixwebsocket/IXWebSocket.h @@ -97,6 +97,8 @@ namespace ix void enableAutomaticReconnection(); void disableAutomaticReconnection(); bool isAutomaticReconnectionEnabled() const; + void setMaxWaitBetweenReconnectionRetries(uint32_t maxWaitBetweenReconnectionRetries); + uint32_t getMaxWaitBetweenReconnectionRetries() const; private: WebSocketSendInfo sendMessage(const std::string& text, @@ -123,10 +125,14 @@ namespace ix static OnTrafficTrackerCallback _onTrafficTrackerCallback; std::atomic _stop; - std::atomic _automaticReconnection; std::thread _thread; std::mutex _writeMutex; + // Automatic reconnection + std::atomic _automaticReconnection; + static const uint32_t kDefaultMaxWaitBetweenReconnectionRetries; + uint32_t _maxWaitBetweenReconnectionRetries; + std::atomic _handshakeTimeoutSecs; static const int kDefaultHandShakeTimeoutSecs; diff --git a/makefile b/makefile index 88676229..47fac67e 100644 --- a/makefile +++ b/makefile @@ -74,6 +74,12 @@ install_cmake_for_linux: mkdir -p /tmp/cmake (cd /tmp/cmake ; curl -L -O https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0-Linux-x86_64.tar.gz ; tar zxf cmake-3.14.0-Linux-x86_64.tar.gz) +# python -m venv venv +# source venv/bin/activate +# pip install mkdocs +doc: + ./venv/bin/mkdocs build -d ../bsergean.github.io/IXWebSocket/site + .PHONY: test .PHONY: build .PHONY: ws diff --git a/ws/ws.cpp b/ws/ws.cpp index e2958a74..b0a92ecf 100644 --- a/ws/ws.cpp +++ b/ws/ws.cpp @@ -89,6 +89,7 @@ int main(int argc, char** argv) int delayMs = -1; int count = 1; int jobs = 4; + uint32_t maxWaitBetweenReconnectionRetries; CLI::App* sendApp = app.add_subcommand("send", "Send a file"); sendApp->add_option("url", url, "Connection url")->required(); @@ -113,6 +114,7 @@ int main(int argc, char** argv) connectApp->add_flag("-d", disableAutomaticReconnection, "Disable Automatic Reconnection"); connectApp->add_flag("-x", disablePerMessageDeflate, "Disable per message deflate"); connectApp->add_flag("-b", binaryMode, "Send in binary mode"); + connectApp->add_option("--max_wait", maxWaitBetweenReconnectionRetries, "Max Wait Time between reconnection retries"); CLI::App* chatApp = app.add_subcommand("chat", "Group chat"); chatApp->add_option("url", url, "Connection url")->required(); @@ -254,7 +256,8 @@ int main(int argc, char** argv) else if (app.got_subcommand("connect")) { ret = ix::ws_connect_main(url, headers, disableAutomaticReconnection, - disablePerMessageDeflate, binaryMode); + disablePerMessageDeflate, binaryMode, + maxWaitBetweenReconnectionRetries); } else if (app.got_subcommand("chat")) { diff --git a/ws/ws.h b/ws/ws.h index d9539f93..1eca8ec3 100644 --- a/ws/ws.h +++ b/ws/ws.h @@ -34,7 +34,8 @@ namespace ix const std::string& headers, bool disableAutomaticReconnection, bool disablePerMessageDeflate, - bool binaryMode); + bool binaryMode, + uint32_t maxWaitBetweenReconnectionRetries); int ws_receive_main(const std::string& url, bool enablePerMessageDeflate, int delayMs); diff --git a/ws/ws_connect.cpp b/ws/ws_connect.cpp index 160f7574..37716048 100644 --- a/ws/ws_connect.cpp +++ b/ws/ws_connect.cpp @@ -21,7 +21,8 @@ namespace ix const std::string& headers, bool disableAutomaticReconnection, bool disablePerMessageDeflate, - bool binaryMode); + bool binaryMode, + uint32_t maxWaitBetweenReconnectionRetries); void subscribe(const std::string& channel); void start(); @@ -44,7 +45,8 @@ namespace ix const std::string& headers, bool disableAutomaticReconnection, bool disablePerMessageDeflate, - bool binaryMode) : + bool binaryMode, + uint32_t maxWaitBetweenReconnectionRetries) : _url(url), _disablePerMessageDeflate(disablePerMessageDeflate), _binaryMode(binaryMode) @@ -53,6 +55,7 @@ namespace ix { _webSocket.disableAutomaticReconnection(); } + _webSocket.setMaxWaitBetweenReconnectionRetries(maxWaitBetweenReconnectionRetries); _headers = parseHeaders(headers); } @@ -186,14 +189,16 @@ namespace ix const std::string& headers, bool disableAutomaticReconnection, bool disablePerMessageDeflate, - bool binaryMode) + bool binaryMode, + uint32_t maxWaitBetweenReconnectionRetries) { std::cout << "Type Ctrl-D to exit prompt..." << std::endl; WebSocketConnect webSocketChat(url, headers, disableAutomaticReconnection, disablePerMessageDeflate, - binaryMode); + binaryMode, + maxWaitBetweenReconnectionRetries); webSocketChat.start(); while (true)