From 8cc6342d07e3b113929a3579bdd64de5666bfa5a Mon Sep 17 00:00:00 2001
From: Holger Nahrstaedt <holger@nahrstaedt.de>
Date: Fri, 27 Apr 2018 21:41:00 +0200
Subject: [PATCH] Memory consumption for graphenerpc reduced and other
 improvements

Memo
* make prefix changeble
Transationbuilder
* sign() return the signed struct now
Graphenerpc
* Session are used for requests
* Singleton for websocket instance added
* Both measures reduce ram consumption when more than one Steem object is created.
Add missing scrypt package to dependency
---
 beem/instance.py               |  5 ++
 beem/memo.py                   | 14 ++++--
 beem/transactionbuilder.py     |  1 +
 beem/wallet.py                 |  7 +--
 beemgrapheneapi/graphenerpc.py | 86 ++++++++++++++++++++++++++--------
 examples/benchmark_beem.py     |  2 +-
 setup.py                       |  1 +
 7 files changed, 86 insertions(+), 30 deletions(-)

diff --git a/beem/instance.py b/beem/instance.py
index 6cb1e139..2efe17fe 100644
--- a/beem/instance.py
+++ b/beem/instance.py
@@ -8,6 +8,7 @@ import beem as stm
 
 
 class SharedInstance(object):
+    """Singelton for the Steem Instance"""
     instance = None
     config = {}
 
@@ -58,3 +59,7 @@ def set_shared_config(config):
     if not isinstance(config, dict):
         raise AssertionError()
     SharedInstance.config = config
+    # if one is already set, delete
+    if SharedInstance.instance:
+        clear_cache()
+        SharedInstance.instance = None
diff --git a/beem/memo.py b/beem/memo.py
index 28a7ae89..df9342c5 100644
--- a/beem/memo.py
+++ b/beem/memo.py
@@ -177,12 +177,15 @@ class Memo(object):
         if not memo_wif:
             raise MissingKeyError("Memo key for %s missing!" % self.from_account["name"])
 
+        if not hasattr(self, 'chain_prefix'):
+            self.chain_prefix = self.steem.prefix
+
         if bts_encrypt:
             enc = BtsMemo.encode_memo_bts(
                 PrivateKey(memo_wif),
                 PublicKey(
                     self.to_account["memo_key"],
-                    prefix=self.steem.prefix
+                    prefix=self.chain_prefix
                 ),
                 nonce,
                 memo
@@ -199,11 +202,11 @@ class Memo(object):
                 PrivateKey(memo_wif),
                 PublicKey(
                     self.to_account["memo_key"],
-                    prefix=self.steem.prefix
+                    prefix=self.chain_prefix
                 ),
                 nonce,
                 memo,
-                prefix=self.steem.prefix
+                prefix=self.chain_prefix
             )
 
             return {
@@ -255,6 +258,9 @@ class Memo(object):
                     "Need any of {}".format(
                     [memo_to["name"], memo_from["name"]]))
 
+        if not hasattr(self, 'chain_prefix'):
+            self.chain_prefix = self.steem.prefix
+
         if message[0] == '#':
             return BtsMemo.decode_memo(
                 PrivateKey(memo_wif),
@@ -263,7 +269,7 @@ class Memo(object):
         else:
             return BtsMemo.decode_memo_bts(
                 PrivateKey(memo_wif),
-                PublicKey(pubkey, prefix=self.steem.prefix),
+                PublicKey(pubkey, prefix=self.chain_prefix),
                 nonce,
                 message
             )
diff --git a/beem/transactionbuilder.py b/beem/transactionbuilder.py
index fc52398c..083bac5b 100644
--- a/beem/transactionbuilder.py
+++ b/beem/transactionbuilder.py
@@ -276,6 +276,7 @@ class TransactionBuilder(dict):
 
         signedtx.sign(self.wifs, chain=self.steem.rpc.chain_params)
         self["signatures"].extend(signedtx.json().get("signatures"))
+        return signedtx
 
     def verify_authority(self):
         """ Verify the authority of the signed transaction
diff --git a/beem/wallet.py b/beem/wallet.py
index f2899596..4889e2c7 100644
--- a/beem/wallet.py
+++ b/beem/wallet.py
@@ -180,12 +180,7 @@ class Wallet(object):
                 self.masterpassword = self.masterpwd.decrypted_master
 
     def tryUnlockFromEnv(self):
-        """Try to fetch the unlock password first from 'UNLOCK' environment variable.
-            This is only done, when steem.config['password_storage'] == 'environment'.
-            and then from the keyring module keyring.get_password('beem', 'wallet'),
-            when steem.config['password_storage'] == 'keyring'
-            In order to use this, you have to store the password in the 'UNLOCK' variable
-            or in the keyring by python -m keyring set beem wallet
+        """ Try to fetch the unlock password from UNLOCK environment variable and keyring when no password is given.
         """
         password_storage = self.steem.config["password_storage"]
         if password_storage == "environment" and "UNLOCK" in os.environ:
diff --git a/beemgrapheneapi/graphenerpc.py b/beemgrapheneapi/graphenerpc.py
index 0df11163..3b9869f5 100644
--- a/beemgrapheneapi/graphenerpc.py
+++ b/beemgrapheneapi/graphenerpc.py
@@ -15,7 +15,6 @@ import threading
 import re
 import time
 import warnings
-from requests.exceptions import ConnectionError
 from .exceptions import (
     UnauthorizedError, RPCConnection, RPCError, RPCErrorDoRetry, NumRetriesReached
 )
@@ -38,6 +37,9 @@ REQUEST_MODULE = None
 if not REQUEST_MODULE:
     try:
         import requests
+        from requests.adapters import HTTPAdapter
+        from requests.packages.urllib3.util.retry import Retry
+        from requests.exceptions import ConnectionError
         REQUEST_MODULE = "requests"
     except ImportError:
         REQUEST_MODULE = None
@@ -46,6 +48,56 @@ if not REQUEST_MODULE:
 log = logging.getLogger(__name__)
 
 
+def requests_retry_session(
+    retries=3,
+    backoff_factor=0.3,
+    status_forcelist=(500, 502, 504),
+    session=None,
+):
+    if REQUEST_MODULE is None:
+        raise Exception()
+    session = session or requests.Session()
+    retry = Retry(total=retries,
+                  read=retries,
+                  connect=retries,
+                  backoff_factor=backoff_factor,
+                  status_forcelist=status_forcelist)
+    adapter = HTTPAdapter(max_retries=retry)
+    session.mount('http://', adapter)
+    session.mount('https://', adapter)
+    return session
+
+
+class WebsocketInstance(object):
+    """Singelton for the Websocket Instance"""
+    instance_ws = None
+    instance_wss = None
+
+
+def shared_ws_instance(use_ssl=True):
+    """Get websocket instance"""
+    if WEBSOCKET_MODULE is None:
+        raise Exception()
+    if use_ssl and not WebsocketInstance.instance_wss:
+        ssl_defaults = ssl.get_default_verify_paths()
+        sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile}
+        WebsocketInstance.instance_wss = websocket.WebSocket(sslopt=sslopt_ca_certs, enable_multithread=True)
+    elif not use_ssl and not WebsocketInstance.instance_ws:
+        WebsocketInstance.instance_ws = websocket.WebSocket(enable_multithread=True)
+    if use_ssl:
+        return WebsocketInstance.instance_wss
+    else:
+        return WebsocketInstance.instance_ws
+
+
+def set_ws_instance(ws_instance, use_ssl=True):
+    """Set websocket instance"""
+    if use_ssl:
+        WebsocketInstance.instance_wss = ws_instance
+    else:
+        WebsocketInstance.instance_ws = ws_instance
+
+
 class GrapheneRPC(object):
     """
     This class allows to call API methods synchronously, without callbacks.
@@ -105,6 +157,7 @@ class GrapheneRPC(object):
         self.user = user
         self.password = password
         self.ws = None
+        self.session = None
         self.rpc_queue = []
         self.timeout = kwargs.get('timeout', 60)
         self.num_retries = kwargs.get("num_retries", -1)
@@ -144,27 +197,23 @@ class GrapheneRPC(object):
                     self.error_cnt[self.url] = 0
                 log.debug("Trying to connect to node %s" % self.url)
                 if self.url[:3] == "wss":
-                    if WEBSOCKET_MODULE is None:
-                        raise Exception()
-                    ssl_defaults = ssl.get_default_verify_paths()
-                    sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile}
-                    self.ws = websocket.WebSocket(sslopt=sslopt_ca_certs, enable_multithread=True)
+                    self.ws = shared_ws_instance(use_ssl=True)
                     self.current_rpc = self.rpc_methods["ws"]
                 elif self.url[:2] == "ws":
-                    if WEBSOCKET_MODULE is None:
-                        raise Exception()
-                    self.ws = websocket.WebSocket(enable_multithread=True)
+                    self.ws = shared_ws_instance(use_ssl=False)
                     self.current_rpc = self.rpc_methods["ws"]
                 else:
-                    if REQUEST_MODULE is None:
-                        raise Exception()
                     self.ws = None
+                    self.session = requests_retry_session(retries=self.num_retries_call)
                     self.current_rpc = self.rpc_methods["jsonrpc"]
-                    self.headers = {'User-Agent': 'beem v%s' % (beem_version),
-                                    'content-type': 'application/json'}
+                    self.session.auth = (self.user, self.password)
+                    self.session.headers.update({'User-Agent': 'beem v%s' % (beem_version),
+                                                 'content-type': 'application/json'})
+
             try:
                 if self.ws:
                     self.ws.connect(self.url)
+                    self.rpclogin(self.user, self.password)
                 try:
                     props = None
                     props = self.get_config(api="database")
@@ -180,7 +229,6 @@ class GrapheneRPC(object):
                     else:
                         self.current_rpc = self.rpc_methods["appbase"]
                 self.chain_params = self.get_network(props)
-                self.rpclogin(self.user, self.password)
                 break
             except KeyboardInterrupt:
                 raise
@@ -199,13 +247,13 @@ class GrapheneRPC(object):
         """Close Websocket"""
         if self.ws:
             self.ws.close()
+        if self.session:
+            self.session.close()
 
     def request_send(self, payload):
-        response = requests.post(self.url,
-                                 data=payload,
-                                 headers=self.headers,
-                                 timeout=self.timeout,
-                                 auth=(self.user, self.password))
+        response = self.session.post(self.url,
+                                     data=payload,
+                                     timeout=self.timeout)
         if response.status_code == 401:
             raise UnauthorizedError
         return response.text
diff --git a/examples/benchmark_beem.py b/examples/benchmark_beem.py
index a5cfad6c..eea39133 100644
--- a/examples/benchmark_beem.py
+++ b/examples/benchmark_beem.py
@@ -17,7 +17,7 @@ logging.basicConfig(level=logging.INFO)
 
 
 if __name__ == "__main__":
-    node_setup = 2
+    node_setup = 0
     how_many_hours = 1
     if node_setup == 0:
         stm = Steem(node="https://api.steemit.com", num_retries=10)
diff --git a/setup.py b/setup.py
index ee8cb727..4218019c 100755
--- a/setup.py
+++ b/setup.py
@@ -27,6 +27,7 @@ requires = [
     "websocket-client",
     "appdirs",
     "Events",
+    "scrypt",
     "pylibscrypt",
     "pycryptodomex",
     "pytz",
-- 
GitLab