Document: Tutorial

A Tutorial on Writing Peers Applications

In this document we present a tutorial on writing Peers applications with a series of toy examples. The source code is contained in the examples directory. Before proceeding further, you should first build the peers code base, and then build the example modules by running make.

Registry: An Introductory Example

The first example of the tutorial illustrates how to implement and interact with distributed objects. We implement a registry object, which locally stores key-value pairs and allows remote clients to access them.

The first step in creating the registry is to define the interaction protocol. The protocol is defined by specifying the node interface in XX. The interface defines two methods:

Implementation

The implementation of the interface is a python class, node.node_imp. The class extends proto.local_node:
...
class node_imp( proto.local_node ):
   def __init__( self ):
       proto.local_node.__init__( self )
...

This class is generated by rpcc and provides the stub for node interface implementations. The node.node_imp class implements the two methods of the interface, as node_imp.put and node_imp.get.

The interesting parts in the implementation of the two methods is the first argument in the signature and the exception raised by get:

...
   def put( self, bd, k, v ):
       self.values[k] = v
...
   def get( self, bd, k ):
       if k in self.values:
           return self.values[k]
       else:
           raise peers.rpc_exception( proto.no_such_key, k )
...

The bd argument is an instance of peers.binding, provided by the Peers kernel. The binding can be used to perform higher level access control or to create a remote object proxy for performing callbacks to the peer. The callbacks will go through the reverse channel that is implicitly created when a connection is established by the kernel. Any interface can be bound in the reverse channel, but for P2P applications it usually is symmetric. Of course, usage of the binding is not limited to this, it can also be used as dictionary key for keeping track of long standing interactions, a token for keeping track of interaction history, etc.

The exception raised by get is a subclass of peers.rpc error. The class is auto-generated by rpcc from proto.no_such_key. Since the implementation is synchronous, the exception is raised as a normal python exception. The helper function peers.rpc_exception is used to initialize the stack trace and internal representation of the exception. Raising exceptions from asynchronous method implementations is discussed later on in this tutorial.

Interacting with a registry node

We can now illustrate how to program with distributed objects. First, we need to instantiate the implementation of the node interface. The node module can be executed as a python program, spawning a registry node implementation:
vyzo@erb registry $ python node.py inet:localhost:5000&
[1] 2134

The node implementation is now running in the background, accessible through port 5000 in the local host. We can fire up a python interpreter shell and interact with it in a programming environment. We will use the node.connect utility method, which creates a proxy.

The implementation of node.connect is trivial. It simply spares us a couple of lines of code when connecting to the node in the Python shell:

def connect( addr ):
   addr = peers.parse_address( addr )
   ep = peers.rpc.connect( peers.proto_entry.sock_stream( addr ))
   return proto.node( ep )

proto.node is the remote object proxy class for the node interface, automatically generated by rpcc. The proxy is a lightweight object, instantiated on a peers.endpoint. It simply exports the interface methods, which can be called as normal local methods:

vyzo@erb registry $ python
Python 2.4.1 (#1, Jun 21 2005, 00:26:59)
[GCC 3.3.5-20050130 (Gentoo Linux 3.3.5.20050130-r1, ssp-3.3.5.20050130-1, pie- on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import peers, node
>>> peers.events.spawn_loop()
<peers._peers.thread object at 0xb788a144>
>>> n = node.connect( 'inet:localhost:5000' )
>>> n.put( 'arrakis', 'dune' )
>>> n.get( 'arrakis' )
'dune'

In the code snippet above, first we import the necessary modules and then start the Peers event loop in a separate thread. Then we connect to the registry node running in the background and make some remote procedure calls.

Exception Handling

As you might expect, exceptions are propagated across process boundaries:
>>> n.get( "Muad'Dib" )
Traceback (most recent call last):
File "", line 1, in ?
_local.no_such_key: rpc error: [75d49e957679a9f338365db6c61b1c]@{{{AF_INET:127.0.0.1:47503}:{AF_INET:127.0.0.1:5000}}}: remote exception: [node.py:36] ::no_such_key: Muad'Dib

The exception can be handled as a normal python exception:

>>> import proto
>>> try:
...  n.get( "Muad'Dib" )
... except proto.no_such_key, e:
...  print "Oops: %s" % e
...
Oops: rpc error: [75d49e957679a9f338365db6c61b1c]@{{{AF_INET:127.0.0.1:47503}:{AF_INET:127.0.0.1:5000}}}: remote exception: [node.py:36] ::no_such_key: Muad'Dib

When the exception is asynchronously handled (when performing a call with continuation as described in the next section), the exception handler receives the actual instance of the rpc exception. We can check for the class by calling peers.is_rpc_error.

RPC conventions

The call convention we used above is a synchronous call. The call is based on the current stack and blocks the current thread. This is fine for simple client-centric interaction, but for complicated distributed interactions we need a more lightweight mechanism.

The Peers kernel is built with continuation passing style, so internally we already have a powerful and lightweight mechanism for handling this type of interactions. The only question is how it is exported to the programmer, and this is where rpcc helps. For each method in the interface that is not an event, there are four different signatures generated for matching the appropriate calling convention:

  1. Synchronous call with default timeout
  2. Synchronous call with explicit timeout
  3. Asynchronous call with default timeout
  4. Asynchronous call with explicit timeout
Events are by definition asynchronous, hence there is no notion of timeout or synchronous call convention. They never return a value, and therefore they look at the client-side as a void call that returns immediately. No explicit exceptions can be thrown from event implementations. Events are declared in the protocol specification with an :event directive. node.put is an example of an event.

We have only used the first calling convention so far. The timeout in this case is implicit: If the interface specification defines a timeout with a :timeout directive, then it is used. Otherwise, the default timeout defined at compile-time is used. The programmer can always override the default timeout by explicitly supplying a timeout as a peers.time_rep instance or an integer. If the timeout is zero or negative, then it is treated as infinity.

The asynchronous calling conventions are more interesting. The method signature gets two extra arguments, both functions, which are the continuation and the exception handler. When the call returns, the continuation function is evaluated with the result in a clean stack. Similarly, when an exception is thrown, either explicitly by the remote object or implicitly by the Peers kernel because of a locally detected error, it is passed to the exception handler in a clean stack. A word of caution however: if an error is imediately detected locally, when for example the endpoint for a remote proxy has become invalid, then an exception is synchronously thrown.

This calling convention is a remote call with continuation. Continuations in the Peers kernel are not stack-copying; rather they are implemented with a trampoline (the Peers event loop). By combining the lexical scoping features of python with the closure lifting facilities provided by peers.wrap and peers.wrap_left we obtain an explicit control mechanism for implementing protocols with fine-grained concurrency.

We show how to take advantage of this mechanism in subsequent examples. But first let's see how to actually make a remote call with continuation:

>>> def my_cc( k, v ): print "Got: %s -> %s" % (k, v)
...
>>> def my_error( e ):
...  if peers.is_rpc_error( e, proto.no_such_key ):
...   print "Oops: %s" % e
...
>>> n.get( 'arrakis', 0, peers.wrap_left( my_cc, 'arrakis' ), my_error )
>>> Got: arrakis -> dune
>>> bad_key = "Maud'Dib"
>>> n.get( bad_key, 0, peers.wrap_left( my_cc, bad_key ), my_error )
>>> Oops: rpc error: [75d49e957679a9f338365db6c61b1c]@{{{AF_INET:127.0.0.1:2289}:{AF_INET:127.0.0.1:5000}}}: remote exception: [node.py:36] remote exception: Maud'Dib

Here, we used the explicit timeout version of the call. The same rules for implicit timeouts apply as in the synchronous call convention. We defined my_cc as our continuation function and lifted a closure by wrapping its left argument to the key we use for the call. A lambda expression or an instance method could be used just as well. Or even a continuation for a remote call inside an asynchronous method implementation...

A P2P Registry

Interacting with an isolated registry node is not very interesting in its own right. It is far more interesting to consider a P2P registry, where the key->value mapping is distributed across multiple nodes. In this example we extend the registry protocol and implementation to operate in this fashion.

Structure of the P2P registry

The structure of the P2P registry is that of a simple P2P overlay. Each node maintains its own local store. In addition, each node is connected to a number of other peers in the network. Get queries directed to a node are first attempted to resolve locally, and if this fails they are forward to the peers of the node. The overlay is maintained by each node individually, no node has a global view of the network.

The basic P2P registry interface

Because the P2P node interface extends the isolated node interface, we can perform get queries and observe exceptions in the same way we did in the previous example. A program that uses the isolated registry node interface as a client can use the P2P registry as a drop-in replacement.

The extended interface defines the following additional methods:

Since our query method implementations are now recursive, we can no longer use synchronous implementation. Therefore, all methods in the P2P registry node implementation are asynchronous. The get method was defined as synchronous in the basic interface, but was overridden in the extended interface specification. Let's take a look at the actual implementation of get:
...
   @multimethod( peers.binding, str, object, object )
   def get( self, bd, key, cc, error ):
       self.get( bd, key, (), cc, error )
...
   @multimethod( peers.binding, str, tuple, object, object )
   def get( self, bd, key, visited, cc, error ):
       def _cc( v ):
           ...
       def _get( lst, visited ):
           ...
 
       if key in self.values:
           cc( self.values[key] )
       else:
           lst = list( (x, y) for (x, y) in self.peers.iteritems()
                       if x not in visited )
           random.shuffle( lst )
           _get( lst, tuple( set( x for (x, y) in self.peers.iteritems())\
                             .union( visited + (self.id,))))
...

Before discussing the details of the method implementation, let us clarify some preliminaries. Firstly, p2p_node.get is overloaded, hence we must use the multimethod decorator. There are two method signatures, one that is used by clients and one that is used by peer nodes. The client-version just calls the peer version locally, starting the query with an empty visited list. Secondly, the implementation is asynchronous, as specified by the :async directive in the protocol specification.

Asynchronous method implementations do not have a stack. They do not return results with a return statement and cannot explicitly throw exceptions. Rather, they receive two extra arguments by the Peers kernel on dispatch: the continuation function (cc) and the exception handler (error). Both are logically attached to the endpoint where the RPC came from. When invoked, they send the argument back to the caller. The argument to the exception handler must be an instance of rpc_error. All exceptions declared in the protocol specification are subclasses of rpc_error. Also note that the functions are one shot (thus it is meaningful to invoke them only once) and soft-bound.

The method first checks to see if there is value associated with the key in the local store. If so, it returns by invoking the continuation. If not, it performs a recursive search in the stores of the peers that have not already been visited in this query. The recursive search is performed by _get:

    ...
        def _get( lst, visited ):
            if lst:
                (id, peer) = lst[0]
                handle_error = wrap( self.peer_error, id,
                                     lambda : _get( lst[1:], visited ))
                with_error_handler( wrap( peer.get, key, visited,
                                          _cc, handle_error ),
                                    handle_error )
            else:
                error( peers.rpc_exception( bd, proto.no_such_key, key ))
...

The recursion is performed by checking a node at a time, performing an asynchronous get call. This is in effect a randomized depth-first search. The pattern is iteration by recursion with continuation-passing style. If the list of candidate nodes is exhausted without obtaining a result then an exception is asynchronously raised by calling error. Connection errors are handled by peer_error, which removes dead links in the overlay.

If a remote get succeeds, then the value is received by _cc:

    ...
        def _cc( v ):
            # perhaps we obtained a value in the meantime?
            if key not in self.values:
                self.message( "cached value: %s -> %s" % (key, v))
                self.values[key] = v
                cc( v )
            else:
                self.message( "ignored value: %s -> %s" % (key, v))
                cc( self.values[key] )
    ...

_cc locally caches the result obtained from the get. However, it does not do so blindly. The reason is that the remote call might take some time to complete. In the meantime, a new local mapping might have been created, either because of a second query running in parallel or because it has been created by a put in the local node. If this is the case, the result is discarded and the local value is returned instead.

As an added bonus for using the asynchronous call interface, the implementation is tail-recursive. The stack is not growing as we are recursing to perform the search.

Searching the P2P registry

The get query is a little limited nonetheless. We can only obtain a single value for the target key. Furthermore, after the first query, a node without a local mapping will cache the retrieved mapping (if any) until explicitly overwritten with a put.

This is where get_all is useful. It performs a global system query by recursively walking the overlay graph. In order to control the number of messages, the implementation uses a visited nodes list. The list contains the ids that have already been visited or will be visited from a different path in the query. Therefore, get_all performs a parallel search of the P2P network graph, while ensuring that for a graph of size N exactly N messages are sent.

Let's take a look at the implementation details:

...
    @multimethod( peers.binding, str, tuple, object, object )
    def get_all( self, bd, key, visited, cc, error ):
        def _cc( values ):
            ...
 
        lst = list( (x, y) for (x, y) in self.peers.iteritems()
                    if x not in visited )
        xv = set( x for (x, y) in self.peers.iteritems() )\
             .union( visited + (self.id,))
        if lst:
            collect = peers.collect_async( _cc, len( lst ))
            for (x, (id, peer)) in enumerate( lst ):
                handle_error = wrap( self.peer_error, id,
                                     lambda : collect( x, None ))
                with_error_handler( wrap( peer.get_all, key, tuple( xv ),
                                          wrap_left( collect, x ),
                                          handle_error ),
                                    handle_error )
        else:
            _cc( [] )
...

The method first obtains a list of peer nodes that have not been visited yet in the query. It also updates the visited list in order to prune the search. It then spawns a number of asynchronous computation threads by recursively calling get_all to its peers. The results are collected with an asynchronous collection barrier, created by peers.collect_async. The effect of this code is to launch a breadth-first search on the P2P network.

The implementation of _cc, which is the continuation of the asynchronous collection process is simple. The result sets from each peer are aggregated with the local value, by performing a set union:

    ...
        def _cc( values ):
            xv = set()
            for x in values:
                if x: xv = xv.union( x )
            if key in self.values:
                xv.add( self.values[key] )
            cc( tuple( xv ))
    ...

Interacting with the P2P registry

Having described the details of the interaction with the P2P registry, it is time to observe them in action. For demonstration purposes, we will create a tiny diamond-shaped P2P network:
vyzo@erb p2p_registry $ python node.py inet:localhost:5000&
[1] 4625
vyzo@erb p2p_registry $ python node.py inet:localhost:5001 inet:localhost:5000&
[2] 4626
4625 [inet:localhost:5000]: add peer inet:localhost:5001
vyzo@erb p2p_registry $ python node.py inet:localhost:5002 inet:localhost:5000&
[3] 4627
4625 [inet:localhost:5000]: add peer inet:localhost:5002
vyzo@erb p2p_registry $ python node.py inet:localhost:5003 inet:localhost:5001 i
net:localhost:5002&
[4] 4628
4626 [inet:localhost:5001]: add peer inet:localhost:5003
4627 [inet:localhost:5002]: add peer inet:localhost:5003

The nodes are spawned by using the utility node module. The only interesting details of the process is the creation of the overlay, handled by the implementation of bind:

    ...
    def bind( self, ep, others ):
        def _add_peer( id, peer ):
            self.peers[id] = peer
 
        proto.local_p2p_node.bind( self, ep )
        for x in others:
            xep = peers.rpc.connect( x )
            peers.rpc.set_keepalive( xep, 0 )
            peer = proto.p2p_node( xep )
            proto.local_p2p_node.bind( xep, self )
            peer.add_peer( self.id, wrap( _add_peer, peer ), peers.warn )
    ...
For each peer address supplied by executing the node module, the local node connects and binds itself on the reverse channel to enable reverse channel calls. The overlay is constructed by the implementation of add_peer and the _add_peer continuation. Also notice that we explicitly set the keep alive of the endpoint to infinity by calling peers.rpc.set_keepalive( xep, 0 ). This is necessary to avoid having the endpoint garbage collected after a period of inactivity.

In order to illustrate the details of the interaction, we create proxies connected to each one of the registry nodes:

vyzo@erb p2p_registry $ python
Python 2.4.1 (#1, Jun 21 2005, 00:26:59)
[GCC 3.3.5-20050130 (Gentoo Linux 3.3.5.20050130-r1, ssp-3.3.5.20050130-1, pie- on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import peers, node
>>> peers.events.spawn_loop()
<peers._peers.thread object at 0xb7c2e144>
>>> (n1, n2, n3, n4) = tuple( node.connect( "inet:localhost:%d" % x ) for x in xrange( 5000, 5004 ))
As a result of the connect generator expression, n1 is connected to process 4625, with identity inet:localhost:5000, n2 is connected to process 4626, with identity inet:localhost:5001 and so on in order of creation. In the P2P overlay, both n1 and n4 are connected with bidirectional links to n2 and n3, but they are not directly connected to each other. Similarly, n2 and n3 are not directly connected.

First, we verify that nothing is know in our little network about Arrakis. Then we initialize n1 with some knowledge about the desolate planet:

>>> n1.get_all( 'arrakis' )
()
>>> n1.put( 'arrakis', 'dune' )
>>> 4625 [inet:localhost:5000]: put arrakis -> dune

Next, we show how get operates. We ask n4, which is not directly connected to n1, about Arrakis and trace the query path and caching of the value:

>>> n4.get( 'arrakis' )
4626 [inet:localhost:5001]: cached value: arrakis -> dune
4628 [inet:localhost:5003]: cached value: arrakis -> dune
'dune'

The printed messages suggest that the value for 'arrakis' that was originally put to n1 is now cached at n2 and n4

Now let's add some competing values and trace the effect:

>>> n2.put( 'arrakis', 'atreides territory' )
>>> 4626 [inet:localhost:5001]: put arrakis -> atreides territory
>>> n3.put( 'arrakis', 'harkonnen territory' )
>>> 4627 [inet:localhost:5002]: put arrakis -> harkonnen territory
>>> n2.get( 'arrakis' )
'atreides territory'
>>> n3.get( 'arrakis' )
'harkonnen territory'
>>> n1.get( 'arrakis' )
'dune'
>>> n4.get( 'arrakis' )
'dune'

So now there are 3 different values mapped to Arrakis in the network. Let's retrieve them all by performing a get_all:

>>> n4.get_all( 'arrakis' )
('atreides territory', 'dune', 'harkonnen territory')

Finally, let's establish world order, get rid of the Harkonnens and the Atreides and notice the changes in the network:

>>> n4.put( 'arrakis', 'fremen territory' )
>>> 4628 [inet:localhost:5003]: put arrakis -> fremen territory
>>> import os
>>> os.kill( 4627, 15 )
>>> n1.get_all( 'arrakis' )
[2005/07/14 22:56:57] WARNING:  illegal_operation -- [rpc_kernel.C:415]: illegal_operation: Invalid endpoint [userland]
4625 [inet:localhost:5000]: removed dead peer: inet:localhost:5002
('atreides territory', 'fremen territory', 'dune')
>>> n4.get_all( 'arrakis' )
[2005/07/14 22:57:23] WARNING:  illegal_operation -- [rpc_kernel.C:415]: illegal_operation: Invalid endpoint [userland]
4628 [inet:localhost:5003]: removed dead peer: inet:localhost:5002
('atreides territory', 'fremen territory', 'dune')
>>> n2.put( 'arrakis', 'fremen territory' )
>>> 4626 [inet:localhost:5001]: put arrakis -> fremen territory
>>> n1.get_all( 'arrakis' )
('fremen territory', 'dune')

Notice how the death of n3 was detected: First n1 attempted to propagate the query through it, detected the failure, and removed it from its peer list. n4 did not attempt to contact n3 because the path was pruned. The failure was detected by n4 later, when we asked it to perform a get_all itself. Finally, when the Fremen conquered n2, the world knowledge about Arrakis converged to the facts: It is the planet also known as Dune, and it is ruled by the Fremen. The implication is that the Fremen control the universe...

Numbers: A Number Theoretic Computing Herd

As a third example, we construct a herd of number theoretic computing peers. We do not go into implementation details this time, since the key ideas have already been covered in the P2P registry discussion. Rather, we illustrate the interaction and provide you with pointers to the implementation. You will need pycrypto to run this example.

The basic node interface

The interface of the basic herd node, as defined in the protocol specification, exports procedures from the number module of pycrypto. The basic node interface defines 5 synchronous methods:
  1. get_random( N ): Computes a random N-bit number.
  2. get_prime( N ): Computes a random N-bit prime.
  3. is_prime( x ): Checks whether x is a prime.
  4. GCD( x, y ): Computes the GCD of x and y.
  5. inverse( u, v ): Computes the inverse of u modulo v.
All big numbers are passed as strings which contain compressed Python pickles. The numbers are encoded using peers.pickle and decoded with peers.unpickle.

The node module

In order to simplify launching nodes and interaction with the pickles, we define an additional node module, similar to what we did in the previous examples. The module defines an extension of the auto-generated remote object proxy: node.proxy. The extension is trivial, it converts python longs to and from the pickled representation for simpler interactive use. When the module is evaluated as a main program, then it runs a node implementation of the specified class.

Basic implementation

The basic interface is implemented trivially by proto_imp.sync_node. For each method, the big number arguments (if any) are unpickled and the respective pycrypto procedure is invoked. The result is returned synchronously with a returnstatement. If the result is a big number, then it is pickled before transmission. Exceptions from pycrypto are propagated as operation_failed.

We can now start to directly interact with an isolated numbers node. First, let us spawn a node with the basic implementation:

vyzo@erb numbers $ python node.py loner inet:localhost:5000&
[1] 21908

We can interact with the node directly through a python interpreter shell, just as we did in the registry examples:

vyzo@erb numbers $ python
Python 2.4.1 (#1, Jun 21 2005, 00:26:59)
[GCC 3.3.5-20050130 (Gentoo Linux 3.3.5.20050130-r1, ssp-3.3.5.20050130-1, pie- on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import peers, node
>>> peers.events.spawn_loop()
<peers._peers.thread object at 0xb7888144>
>>> n = node.connect( 'inet:localhost:5000' )
>>> x = n.get_prime( 512 )
>>> x
12068422012452744968096837547639050499594087024524176583093086680791894346605120917694374287010367045349810390926689908068593873835446912789114144316967359L
>>> n.is_prime( x )
True
>>> y = n.get_random( 512 )
>>> y
10172755187064037739448500743181605864161965005492925665166382699590434163638818363550123450365116104270303944864564324315158536436364807359880436581613473L
>>> n.GCD( x, y )
1L
>>> z = n.inverse( y, x )
>>> z
942362325943225370416552034194312248757903371826682469965310319026011172822534055304186507767388246541320777469968038746657763984432315922913161956294421L
>>> (z * y) % x
1L

Constructing the herd

The number herd is constructed by extending the basic node interface. The methods are now asynchronous, and we also add an add_peer method for managing the overlay. The difference from the basic implementation is that when a request arrives at a node, the node flips a coin to decide whether it should perform the computation locally or forward it to a peer. And since the computation tends to be time consuming, we also perform it in a background thread.

Here is the previous interaction, but this time performed by a herd of three numbers nodes:

vyzo@erb numbers $ python node.py herd inet:localhost:5000&
[1] 4741
vyzo@erb numbers $ python node.py herd inet:localhost:5001 inet:localhost:5000&
[2] 4742
4741: Added peer at [e3d3a2dd1645c8313f2c24bd71d10]@{{{AF_INET:127.0.0.1:2759}:{AF_INET:127.0.0.1:5000}}}
vyzo@erb numbers $ python node.py herd inet:localhost:5002 inet:localhost:5000&
[3] 4743
4741: Added peer at [e3d3a2dd1645c8313f2c24bd71d10]@{{{AF_INET:127.0.0.1:2760}:{AF_INET:127.0.0.1:5000}}}
Python 2.4.1 (#1, Jun 21 2005, 00:26:59)
[GCC 3.3.5-20050130 (Gentoo Linux 3.3.5.20050130-r1, ssp-3.3.5.20050130-1, pie- on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import peers, node
>>> peers.events.spawn_loop()
<peers._peers.thread object at 0xb78891b4>
>>> n = node.connect( 'inet:localhost:5000' )
>>> x = n.get_prime( 512 )
4741: get_prime
4741: Forward to [e3d3a2dd1645c8313f2c24bd71d10]@{{{AF_INET:127.0.0.1:2760}:{AF_INET:127.0.0.1:5000}}}
4743: get_prime
4743: Forward to [e3d3a2dd1645c8313f2c24bd71d10]@{{AF_INET:127.0.0.1:5000}}
4741: get_prime
4741: Forward to [e3d3a2dd1645c8313f2c24bd71d10]@{{{AF_INET:127.0.0.1:2760}:{AF_INET:127.0.0.1:5000}}}
4743: get_prime
>>> x
10853953617286470467569594467523706933280909558986424713789151746096643341929749658492544483416591962814591265070242229709700276777416652647195377763542557L
>>> n.is_prime( x )
4741: is_prime
4741: Forward to [e3d3a2dd1645c8313f2c24bd71d10]@{{{AF_INET:127.0.0.1:2760}:{AF_INET:127.0.0.1:5000}}}
4743: is_prime
4743: Forward to [e3d3a2dd1645c8313f2c24bd71d10]@{{AF_INET:127.0.0.1:5000}}
4741: is_prime
True
>>> y = n.get_random( 512 )
4741: get_random
>>> y
10197091234158423420892120848853851004472189026997424569111473008052749393433458441819269693787141639420540421834243361130855009463414655727596281875439852L
>>> n.GCD( x, y )
4741: GCD
1L
>>> z = n.inverse( y, x )
4741: inverse
4741: Forward to [e3d3a2dd1645c8313f2c24bd71d10]@{{{AF_INET:127.0.0.1:2759}:{AF_INET:127.0.0.1:5000}}}
4742: inverse
4742: Forward to [e3d3a2dd1645c8313f2c24bd71d10]@{{AF_INET:127.0.0.1:5000}}
4741: inverse
>>> z
1354799878906943213183576162210890254076247291409268705150089956477317466508528448763953659353497437939139883892953960404515802199097750140274195747810645L
>>> (z * y) % x
1L

Working with Data

TBA

Native Object Implementation

TBA

Low-Level Event-driven Programming

TBA