commit fd9d32deb9d08da41b4098bee9289e9600380917
parent 196e1452bb3f36469c7b55c4b68d1788027e0ca5
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date: Fri, 26 Apr 2019 13:37:49 +0100
Make JsonRPC use persistent connections
Now JsonRPC will try to reuse connections instead to creating
a new connection for each RPC call. This solves a problem on
windows where Electrum downloading all the block headers resulted
in crashes because of all the sockets being created and closed.
Diffstat:
2 files changed, 68 insertions(+), 28 deletions(-)
diff --git a/electrumpersonalserver/server/common.py b/electrumpersonalserver/server/common.py
@@ -500,7 +500,8 @@ def run_electrum_server(rpc, txmonitor, config):
else:
logger.error("IOError: " + repr(e))
try:
- sock.close()
+ if sock != None:
+ sock.close()
except IOError:
pass
sock = None
@@ -712,7 +713,8 @@ def main():
rpc = JsonRpc(host = config.get("bitcoin-rpc", "host"),
port = int(config.get("bitcoin-rpc", "port")),
user = rpc_u, password = rpc_p,
- wallet_filename=config.get("bitcoin-rpc", "wallet_filename").strip())
+ wallet_filename=config.get("bitcoin-rpc", "wallet_filename").strip(),
+ logger=logger)
#TODO somewhere here loop until rpc works and fully sync'd, to allow
# people to run this script without waiting for their node to fully
diff --git a/electrumpersonalserver/server/jsonrpc.py b/electrumpersonalserver/server/jsonrpc.py
@@ -1,5 +1,7 @@
-#jsonrpc.py from https://github.com/JoinMarket-Org/joinmarket/blob/master/joinmarket/jsonrpc.py
-#copyright # Copyright (C) 2013,2015 by Daniel Kraft <d@domob.eu> and phelix / blockchained.com
+# Copyright (C) 2013,2015 by Daniel Kraft <d@domob.eu>
+# Copyright (C) 2014 by phelix / blockchained.com
+
+#jsonrpc.py from https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/jmclient/jmclient/jsonrpc.py
import base64
import http.client
@@ -9,49 +11,85 @@ class JsonRpcError(Exception): pass
class JsonRpcConnectionError(JsonRpcError): pass
class JsonRpc(object):
- def __init__(self, host, port, user, password, wallet_filename=""):
+ """
+ Simple implementation of a JSON-RPC client that is used
+ to connect to Bitcoin.
+ """
+ def __init__(self, host, port, user, password, wallet_filename="",
+ logger=None):
self.host = host
self.port = port
+ self.conn = http.client.HTTPConnection(self.host, self.port)
self.authstr = "%s:%s" % (user, password)
if len(wallet_filename) > 0:
self.url = "/wallet/" + wallet_filename
else:
self.url = ""
+ self.logger = logger
self.queryId = 1
def queryHTTP(self, obj):
+ """
+ Send an appropriate HTTP query to the server. The JSON-RPC
+ request should be (as object) in 'obj'. If the call succeeds,
+ the resulting JSON object is returned. In case of an error
+ with the connection (not JSON-RPC itself), an exception is raised.
+ """
headers = {"User-Agent": "electrum-personal-server",
"Content-Type": "application/json",
"Accept": "application/json"}
- headers["Authorization"] = "Basic %s" % base64.b64encode(
- self.authstr.encode()).decode()
+ headers["Authorization"] = (b"Basic " +
+ base64.b64encode(self.authstr.encode('utf-8')))
body = json.dumps(obj)
- try:
- conn = http.client.HTTPConnection(self.host, self.port)
- conn.request("POST", self.url, body, headers)
- response = conn.getresponse()
- if response.status == 401:
- conn.close()
- raise JsonRpcConnectionError(
- "authentication for JSON-RPC failed")
- # All of the codes below are 'fine' from a JSON-RPC point of view.
- if response.status not in [200, 404, 500]:
- conn.close()
- raise JsonRpcConnectionError("unknown error in JSON-RPC")
- data = response.read()
- conn.close()
- return json.loads(data.decode())
- except JsonRpcConnectionError as exc:
- raise exc
- except Exception as exc:
- raise JsonRpcConnectionError("JSON-RPC connection failed. Err:" +
- repr(exc))
+ for i in range(20):
+ try:
+ self.conn.request("POST", self.url, body, headers)
+ response = self.conn.getresponse()
+ if response.status == 401:
+ self.conn.close()
+ raise JsonRpcConnectionError(
+ "authentication for JSON-RPC failed")
+ #All the codes below are 'fine' from a JSON-RPC point of view.
+ if response.status not in [200, 404, 500]:
+ self.conn.close()
+ raise JsonRpcConnectionError("unknown error in JSON-RPC")
+ data = response.read()
+ return json.loads(data.decode('utf-8'))
+ except JsonRpcConnectionError as exc:
+ raise exc
+ except http.client.BadStatusLine:
+ return "CONNFAILURE"
+ except OSError as e:
+ self.logger.debug('Reconnecting RPC after error: ' + repr(e))
+ self.conn.close()
+ self.conn.connect()
+ continue
+ except Exception as exc:
+ raise JsonRpcConnectionError("JSON-RPC connection failed. Err:"
+ + repr(exc))
+ break
+ return None
def call(self, method, params):
currentId = self.queryId
self.queryId += 1
+
request = {"method": method, "params": params, "id": currentId}
- response = self.queryHTTP(request)
+ #query can fail from keepalive timeout; keep retrying if it does, up
+ #to a reasonable limit, then raise (failure to access blockchain
+ #is a critical failure). Note that a real failure to connect (e.g.
+ #wrong port) is raised in queryHTTP directly.
+ response_received = False
+ for i in range(100):
+ response = self.queryHTTP(request)
+ if response != "CONNFAILURE":
+ response_received = True
+ break
+ #Failure means keepalive timed out, just make a new one
+ self.conn = http.client.HTTPConnection(self.host, self.port)
+ self.logger.debug("Creating new jsonrpc HTTPConnection")
+ if not response_received:
+ raise JsonRpcConnectionError("Unable to connect over RPC")
if response["id"] != currentId:
raise JsonRpcConnectionError("invalid id returned by query")
if response["error"] is not None: