From b2f6adb39884265a70c62de54cc65ecb170357f9 Mon Sep 17 00:00:00 2001 From: "W. J. van der Laan" Date: Mon, 21 Jun 2021 15:50:20 +0200 Subject: [PATCH] Merge bitcoin/bitcoin#21056: rpc: Add a `-rpcwaittimeout` parameter to limit time spent waiting b9e76f1bf08c52fcd402b2314e00db4ad247ebc8 rpc: Add test for -rpcwaittimeout (Christian Decker) f76cb10d7dc9a7b0c55d28011161606399417664 rpc: Prefix rpcwaittimeout error with details on its nature (Christian Decker) c490e17ef698a1695050f82ef6567b3b87a21861 doc: Add release notes for the `-rpcwaittimeout` cli parameter (Christian Decker) a7fcc8eb59fe51473571661316214156fbdbdcae rpc: Add a `-rpcwaittimeout` parameter to limit time spent waiting (Christian Decker) Pull request description: Adds a new numeric `-rpcwaittimeout` that can be used to limit the time we spend waiting on the RPC server to appear. This is used by downstream projects to provide a bit of slack when `bitcoind`s RPC interface is not available right away. This makes the `-rpcwait` argument more useful, since we can now limit how long we'll ultimately wait, before potentially giving up and reporting an error to the caller. It was discussed in the context of the BTCPayServer wanting to have c-lightning wait for the RPC interface to become available but still have the option of giving up eventually ([4355]). I checked with laanwj whether this is already possible ([comment]), and whether this would be a welcome change. Initially I intended to repurpose the (optional) argument to `-rpcwait`, however I decided against it since it would potentially break existing configurations, using things like `rpcwait=1`, or `rpcwait=true` (the former would have an unintended short timeout, when old behavior was to wait indefinitely). ~Due to its simplicity I didn't implement a test for it yet, but if that's desired I can provide one.~ Test was added during reviews. [4355]: https://github.com/ElementsProject/lightning/issues/4355 [comment]: https://github.com/ElementsProject/lightning/issues/4355#issuecomment-768288261 ACKs for top commit: laanwj: Code review ACK b9e76f1bf08c52fcd402b2314e00db4ad247ebc8 promag: ACK b9e76f1bf08c52fcd402b2314e00db4ad247ebc8. Tree-SHA512: 3cd6728038ec7ca7c35c2e7ccb213bfbe963f99a49bb48bbc1e511c4dd23d9957c04f9af1f8ec57120e47b26eaf580b46817b099d5fc5083c98da7aa92db8638 --- doc/release-notes-21056.md | 6 ++++++ src/bitcoin-cli.cpp | 14 ++++++++++---- test/functional/interface_bitcoin_cli.py | 8 ++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 doc/release-notes-21056.md diff --git a/doc/release-notes-21056.md b/doc/release-notes-21056.md new file mode 100644 index 00000000000000..2201a8cdaefd7a --- /dev/null +++ b/doc/release-notes-21056.md @@ -0,0 +1,6 @@ +New bitcoin-cli settings +------------------------ + +- A new `-rpcwaittimeout` argument to `bitcoin-cli` sets the timeout + in seconds to use with `-rpcwait`. If the timeout expires, + `bitcoin-cli` will report a failure. (#21056) diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 9c2e0518b290e6..91a06ea7a199f6 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -40,6 +40,7 @@ UrlDecodeFn* const URL_DECODE = urlDecode; static const char DEFAULT_RPCCONNECT[] = "127.0.0.1"; static const int DEFAULT_HTTP_CLIENT_TIMEOUT=900; +static constexpr int DEFAULT_WAIT_CLIENT_TIMEOUT = 0; static const bool DEFAULT_NAMED=false; static const int CONTINUE_EXECUTION=-1; @@ -69,6 +70,7 @@ static void SetupCliArgs(ArgsManager& argsman) argsman.AddArg("-rpcuser=", "Username for JSON-RPC connections", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-rpcwait", "Wait for RPC server to start", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-rpcwallet=", "Send RPC for non-default wallet on RPC server (needs to exactly match corresponding -wallet option passed to dashd). This changes the RPC endpoint used, e.g. http://127.0.0.1:9998/wallet/", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-rpcwaittimeout=", strprintf("Timeout in seconds to wait for the RPC server to start, or 0 for no timeout. (default: %d)", DEFAULT_WAIT_CLIENT_TIMEOUT), ArgsManager::ALLOW_INT, OptionsCategory::OPTIONS); argsman.AddArg("-stdin", "Read extra arguments from standard input, one per line until EOF/Ctrl-D (recommended for sensitive information such as passphrases). When combined with -stdinrpcpass, the first line from standard input is used for the RPC password.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-stdinrpcpass", "Read RPC password from standard input as a single line. When combined with -stdin, the first line from standard input is used for the RPC password. When combined with -stdinwalletpassphrase, -stdinrpcpass consumes the first line, and -stdinwalletpassphrase consumes the second.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-stdinwalletpassphrase", "Read wallet passphrase from standard input as a single line. When combined with -stdin, the first line from standard input is used for the wallet passphrase.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); @@ -705,6 +707,9 @@ static UniValue ConnectAndCallRPC(BaseRequestHandler* rh, const std::string& str UniValue response(UniValue::VOBJ); // Execute and handle connection failures with -rpcwait. const bool fWait = gArgs.GetBoolArg("-rpcwait", false); + const int timeout = gArgs.GetArg("-rpcwaittimeout", DEFAULT_WAIT_CLIENT_TIMEOUT); + const int64_t deadline = GetTime().count() + timeout; + do { try { response = CallRPC(rh, strMethod, args, rpcwallet); @@ -715,11 +720,12 @@ static UniValue ConnectAndCallRPC(BaseRequestHandler* rh, const std::string& str } } break; // Connection succeeded, no need to retry. - } catch (const CConnectionFailed&) { - if (fWait) { - UninterruptibleSleep(std::chrono::milliseconds{1000}); + } catch (const CConnectionFailed& e) { + const int64_t now = GetTime().count(); + if (fWait && (timeout <= 0 || now < deadline)) { + UninterruptibleSleep(std::chrono::seconds{1}); } else { - throw; + throw CConnectionFailed(strprintf("timeout on transient error: %s", e.what())); } } } while (fWait); diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 19488b6fc126bf..a957d8f501b9af 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -10,10 +10,12 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than_or_equal, assert_raises_process_error, assert_raises_rpc_error, get_auth_cookie, ) +import time # The block reward of coinbaseoutput.nValue (500) DASH/block matures after # COINBASE_MATURITY (100) blocks. Therefore, after mining 101 blocks we expect @@ -247,6 +249,12 @@ def run_test(self): self.nodes[0].wait_for_rpc_connection() assert_equal(blocks, BLOCKS + 25) + self.log.info("Test -rpcwait option waits at most -rpcwaittimeout seconds for startup") + self.stop_node(0) # stop the node so we time out + start_time = time.time() + assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcwait', '-rpcwaittimeout=5').echo) + assert_greater_than_or_equal(time.time(), start_time + 5) + if __name__ == '__main__': TestBitcoinCli().main()