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:
- put( key, value ): Put a mapping between a
key and a value into the registry. This
method is an asynchronous event.
- get( key ): Retrieve the value associated with
key. If no value is present, then a
no_such_key exception is thrown.
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:
- Synchronous call with default timeout
- Synchronous call with explicit timeout
- Asynchronous call with default timeout
- 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:
- get: A version of
get that accepts
a second argument, the visited nodes list. It is the method used
in order to perform a get query internally in the network.
- get_all: Retrieve all values associated with
a key. There are two versions of the method, similarly to
get.
- add_peer: Adds a peer to a node. This is how
the overlay is constructed.
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:
- get_random( N ): Computes a random N-bit number.
- get_prime( N ): Computes a random N-bit prime.
- is_prime( x ): Checks whether x is a prime.
- GCD( x, y ): Computes the GCD of x and y.
- 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