0.1.3
5.6.2.3 Reach RPC Protocol Client Implementation Walkthrough

The Reach RPC Protocol is designed to be simple to implement in languages that support HTTP and JSON interaction. This document walks through the implementation of an RPC client in Python. An example use of this library is shown in the tutorial section on RPC-based frontends. The entire library is 80 lines of code.

The library uses a few standard Python libraries for interacting with JSON, HTTP servers, and networking:

 1    # flake8: noqa
 2    
 3    import json
 4    import os
 5    import requests
 6    import socket
 7    import time
 8    import urllib3
 9    
..    # ...

..    # ...
11    def mk_rpc(opts={}):
12        def opt_of(field, envvar, default=None, f=lambda x: x):
13            opt =  f(opts.get(field))        if opts.get(field)        is not None \
14              else f(os.environ.get(envvar)) if os.environ.get(envvar) is not None \
15              else default
16    
17            if opt is None:
18                raise RuntimeError('Mandatory configuration unset for: %s' % field)
19    
20            return opt
21    
22        host    = opt_of('host',    'REACH_RPC_SERVER')
23        port    = opt_of('port',    'REACH_RPC_PORT')
24        key     = opt_of('key',     'REACH_RPC_KEY')
25        timeout = opt_of('timeout', 'REACH_RPC_TIMEOUT',               f=int, default=5)
26        verify  = opt_of('verify',  'REACH_RPC_TLS_REJECT_UNVERIFIED', f=lambda x: x != '0')
27    
..    # ...

The library provides a single function, mk_rpc, that accepts the Reach RPC Client Standard Options.

..    # ...
28        if not verify:
29            urllib3.disable_warnings()
30            print('\n*** Warning! TLS verification disabled! ***\n')
31            print(' This is highly insecure in Real Life™ applications and must')
32            print(' only be permitted under controlled conditions (such as')
33            print(' during development).\n')
34    
..    # ...

It starts by observing the verify option and informing the Python library it uses for HTTPS interaction to turn off warnings. It displays a warning to users that they should be nervous about using this setting.

..    # ...
35        # From: https://gist.github.com/butla/2d9a4c0f35ea47b7452156c96a4e7b12
36        start_time = time.perf_counter()
37        while True:
38            try:
39                with socket.create_connection((host, port), timeout=timeout):
40                    break
41            except OSError as ex:
42                time.sleep(0.01)
43                if time.perf_counter() - start_time >= timeout:
44                    raise TimeoutError('Waited too long for the port {} '
45                                       'on host {} to accept connection.'
46                                       .format(port, host)) from ex
47    
..    # ...

Next, it attempts to connect to the Reach RPC Server and throws an error if it does not respond quickly enough.

..    # ...
52        def rpc(m, *args):
53            lab = 'RPC %s %s' % (m, json.dumps([*args]))
54            debug(lab)
55            ans = requests.post('https://%s:%s%s' % (host, port, m),
56                                json    = [*args],
57                                headers = {'X-API-Key': key},
58                                verify  = verify)
59            ans.raise_for_status()
60            debug('%s ==> %s' % (lab, json.dumps(ans.json())))
61            return ans.json()
62    
..    # ...

It defines a function, rpc, which will be returned later on, that implements the protocol for synchronous value RPC methods. It formats a given request, posts it, and then returns the deserialized result. It prints debugging information for convenience.

..    # ...
63        def rpc_callbacks(m, arg, cbacks):
64            vals  = {k: v    for k, v in cbacks.items() if not callable(v)}
65            meths = {k: True for k, v in cbacks.items() if     callable(v)}
66            p     = rpc(m, arg, vals, meths)
67    
68            while True:
69                if p['t'] == 'Done':
70                    return p
71    
72                elif p['t'] == 'Kont':
73                    cback = cbacks[p['m']]
74                    ans   = cback(*p['args'])
75                    p     = rpc('/kont', p['kid'], ans)
76    
77                else:
78                    raise Exception('Illegal callback return: %s' % json.dumps(p))
79    
..    # ...

It defines a function, rpc_callbacks, which will be returned later on, that implements the protocol for interactive RPC methods. On lines 64 and 65, this function inspects its third argument, cbacks, and separates the callable arguments from the values and creates the intermediate objects, vals and meths, to provide the RPC invocation. After it makes the call, in the while loop starting on line 68, it inspects the result to determine if it is a final answer or an interactive RPC callback. If it is a callback, as indicated by the test on line 72, then it extracts the name of the method, p['m'], and invokes it in the original third argument, cbacks, with the provided arguments. It replaces the p value with the result of that continuation invocation and continues.

..    # ...
80        return rpc, rpc_callbacks

Finally, it returns rpc and rpc_callbacks to the user.