Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move to standard tools for in-memory contract object cache #489

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion build/python_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ importlib_resources>=6.0.0
lmdb>=1.4.0
loguru>=0.6.0
mergedeep>=1.3.4
pyparsing>=3.0.9
requests>=2.28.2
requests-toolbelt>=0.10.1
secp256k1==0.13.2
Expand Down
59 changes: 34 additions & 25 deletions client/pdo/client/commands/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import cachetools.func
import logging
import os
import random

import pdo.client.builder.shell as pshell
import pdo.client.builder.script as pscript
import pdo.client.commands.sservice as sservice
Expand All @@ -26,42 +26,56 @@
import pdo.common.crypto as pcrypto
import pdo.contract as pcontract
from pdo.common.keys import ServiceKeys
from pdo.common.utility import valid_service_url
from pdo.submitter.create import create_submitter

logger = logging.getLogger(__name__)

__all__ = [
'flush_contract_cache',
'get_contract',
'get_contract_from_context',
'create_contract',
'send_to_contract',
'do_contract',
'load_commands',
]
]

## -----------------------------------------------------------------
## get_contract
## -----------------------------------------------------------------
__contract_cache__ = {}
class __hashabledict__(dict) :
"""Hashable dictionary

def get_contract(state, save_file) :
"""Get contract object using the save_file.
This is a relatively simple hack to make a dictionary hashable
so that the cache can work. It assumes that the dictionary does
not change. This is sufficent for the ledger configuration.
"""
def __hash__(self) :
return hash(frozenset(self))

global __contract_cache__
@cachetools.func.ttl_cache(maxsize=32, ttl=30)
def __get_contract__(ledger_config, save_file, data_directory) :
logger.debug(f'load contract from file: {save_file}')
return pcontract.Contract.read_from_file(ledger_config, save_file, data_dir=data_directory)

def get_contract(state, save_file) :
"""Retrieve a contract object associated with the named save file
"""
data_directory = state.get(['Contract', 'DataDirectory'])
ledger_config = __hashabledict__(state.get(['Ledger']))

if save_file not in __contract_cache__ :
try :
data_directory = state.get(['Contract', 'DataDirectory'])
ledger_config = state.get(['Ledger'])
# expiration from the cache really means that we want the
# contract re-read, so force any expired contracts out of
# the cache before we read it
__get_contract__.cache.expire()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this effectively force cache expiration on each get, so you don't really get the benefits of having a cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no. if you read the documentation on cache tools the behavior is more or less what i described. expire only drops those entries that have timed out. all other entries will remain. clear is the operation to drop all cache entries no matter the timing.


__contract_cache__[save_file] = pcontract.Contract.read_from_file(
ledger_config, save_file, data_dir=data_directory)
except Exception as e :
raise Exception('unable to load the contract; {0}'.format(str(e)))
return __get_contract__(ledger_config, save_file, data_directory)

return __contract_cache__[save_file]
def flush_contract_cache() :
"""Explicitly flush the contract cache
"""
__get_contract__.cache.expire()
__get_contract__.cache.clear()

# -----------------------------------------------------------------
# -----------------------------------------------------------------
Expand All @@ -81,7 +95,7 @@ def get_contract_from_context(state, context) :
# will also cache the contract object which we will probably be
# using again later
try :
contract = get_contract(state, save_file)
_ = get_contract(state, save_file)
logger.debug('contract found in file {}'.format(save_file))
except Exception as e :
logger.info("contract save file specified in context, but load failed; {}".format(e))
Expand All @@ -97,7 +111,6 @@ def __add_enclave_secrets__(ledger_config, contract_id, client_keys, enclaveclie
enclaves that will be provisioned for this contract.
"""

secrets = {}
encrypted_state_encryption_keys = {}
for enclaveclient in enclaveclients:
psecrets = []
Expand Down Expand Up @@ -327,7 +340,7 @@ def send_to_contract(state, message, save_file, **kwargs) :
try :
contract = get_contract(state, save_file)
except Exception as e :
raise Exception('unable to load the contract')
raise Exception('unable to load the contract; {}', str(e))

# ---------- set up the enclave service ----------
eservice_client = eservice.get_eservice_from_contract(state, save_file, eservice_url)
Expand All @@ -342,7 +355,6 @@ def send_to_contract(state, message, save_file, **kwargs) :
if not update_response.status :
raise ValueError(update_response.invocation_response)

data_directory = state.get(['Contract', 'DataDirectory'])
ledger_config = state.get(['Ledger'])

if update_response.state_changed and commit :
Expand Down Expand Up @@ -503,8 +515,6 @@ def add_arguments(cls, subparser) :

@classmethod
def invoke(cls, state, bindings, method, save_file, **kwargs) :
waitflag = kwargs.get('wait', False)

pparams = kwargs.get('positional') or []

kparams = dict()
Expand All @@ -526,13 +536,12 @@ def invoke(cls, state, bindings, method, save_file, **kwargs) :
# parameters must not contain an '='
if kwargs.get('params') :
for p in kwargs.get('params') :
kvsplit = p.split('=',1)
kvsplit = p.split('=', 1)
if len(kvsplit) == 1 :
pparams += kvsplit[0]
elif len(kvsplit) == 2 :
kparams[kvsplit[0]] = kvsplit[1]


message = pcontract.invocation_request(method, *pparams, **kparams)
result = send_to_contract(state, message, save_file, **kwargs)

Expand Down
3 changes: 2 additions & 1 deletion client/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
packages = find_packages(),
namespace_packages=['pdo'],
install_requires = [
'pyparsing',
'cachetools',
'colorama',
'toml',
],
data_files = data_files,
Expand Down
Loading