Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Adding some syntactic sugar to RPC #2

Closed
wants to merge 1 commit into from

3 participants

@charrea6
Owner

I've added 2 new attributes to the channel class to make calling rpc function more like calling normal python functions.
The attributes, sync and async, allow calling rpc functions via normal attribute access.
sync - All calls are synchronous (ie they wait and then return the result of the RPC call)
async - All calls return an inprogress object (as rpc method does)

I've also added a NotConnectedException that is raised when attempting to make a call on a channel that isn't connected.

Example:
try:
channel.sync.getFavorites()
except NotConnectedException:
pass
or
channel.async.getScheduledRecordings().wait()

@jtackaberry
Owner

I think I'm philosophically opposed to this approach. Once upon a time, we used to have kaa.ipc. kaa.ipc's goal was to provide a very transparent remote method invocation mechanism. It was fairly implicit and magical, and that made it tricky and difficult to read. We deprecated it with kaa.rpc and had a much more explicit syntax quite intentionally.

That is, we intentionally preferred channel.rpc('foo', 42) over channel.foo(42) because although it's more verbose, it was more obvious to read: it is an rpc call which means there is a network cost, and 'foo' is an arbitrary string exposed by the server.

Moreover, the Kaa philosophy is such that everything that blocks is asynchronous via InProgress. Adding syntactic sugar for synchronous calls will encourage callers to use it. Synchronous invocation is really discouraged, especially when it is implicit. IMO, wait() should be the only allowed form of synchronous invocation in kaa, specifically because it is explicit and requires extra work.

(For this reason, I consider the async parameter of @kaa.threaded() to be a bit of a wart and I'm tempted to remove it. The only reason I haven't so far is because at least there is a weak excuse in that it allows for an optimization. It's probably not a good enough reason and ultimately I'll probably remove it.)

@Dischi may disagree with all this. I'll hear arguments. :)

NotConnectedException seems reasonable for when channel.rpc('foo') is called on an unconnected channel. But this should be called NotConnectedError to be in line with other Python exceptions, and actually I'd prefer if it was in kaa.socket instead so that the Socket class in general can benefit from it. I was thinking that kaa.Socket exceptions needed an overhaul.

@Dischi
Owner

I agree with Jason. We had a long discussion about this years ago. Calling wait() is always a bad hack and should be avoided since it forks a new main loop and things can get messy. Calling wait() should only be used when you have no main loop running (yet) and otherwise, rethink your code. :) If you need to wait, maybe the function calling it also has to be a coroutine. In that case, you wait using yield. IMHO this is the better way.

The exception sounds like a good addition. About the naming, I do not care. :)

@charrea6
Owner

Good to know the development procedures work! :-)

I'll rework so that rpc() throws a NotConnectedError when the status is DISCONNECTED.

On the wait() versus coroutines, I'm yet to understand how coroutines are a good thing, perhaps I just need to see a good example. But, at the moment they appear to me to be magic (I don't understand when the code will re-enter the function) and I prefer the wait() for a lot of case where the caller doesn't want to know about the underlying operations (or about kaa). This may be because freevo 1 had kaa back ported into it and doesn't have the knowledge of kaa spread through the code.

@charrea6 charrea6 closed this
@jtackaberry
Owner

In general, coroutines will be reentered at the earliest opportunity: on the next mainloop iteration if kaa.NotFinished is yielded, immediately when a yielded InProgress has finished. wait() is problematic because it nests main loops, and you might find yourself getting into sticky corner cases if you use it liberally. You're really better off using coroutines and yielding.

We can talk on the mailing list if you have questions or need clarifications about coroutines. They only feel magical, they aren't really magic. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 10, 2012
  1. @charrea6
This page is out of date. Refresh to see the latest.
Showing with 51 additions and 0 deletions.
  1. +51 −0 src/rpc.py
View
51 src/rpc.py
@@ -144,6 +144,12 @@ class RemoteException(AsyncExceptionBase):
def _kaa_get_header(self):
return "Exception during RPC call '%s'; remote traceback follows:" % self._kaa_exc_args[0]
+class NotConnectedException(Exception):
+ """
+ Raised when an attempt is made to call a method on a channel that is not connected.
+ """
+ pass
+
class Server(Object):
"""
@@ -260,6 +266,10 @@ def __init__(self, sock, auth_secret):
self._auth_secret = py3_b(auth_secret)
self._pending_challenge = None
+ # Some syntactic sugar to make calling RPC functions more like calling normal python functions
+ self.async = RPCCallable(self, True)
+ self.sync = RPCCallable(self, False)
+
# Creates a circular reference so that RPC channels survive even when
# there is no reference to them. (Servers may not hold references to
# clients channels.) As long as the socket is connected, the channel
@@ -762,6 +772,47 @@ def __repr__(self):
return '<kaa.rpc.Channel (%s) - disconnected>' % tp
return '<kaa.rpc.Channel (%s) %s>' % (tp, self._socket.fileno)
+ def __getattr__(self, item):
+ # By default accessing a member on the channel will return an asynchronous callable.
+ return getattr(self.async, item)
+
+
+class RPCCallable(object):
+ """
+ Class to add syntactic sugar to the RPC channel.
+ This class allows access to RPC functions in a python like manner, via normal attribute access.
+ """
+
+ def __init__(self, channel, async):
+ self.__channel = channel
+ self.__async = async
+
+ def __rpc_call(self, name, args, kwargs):
+ if self.__channel.status == DISCONNECTED:
+ raise NotConnectedException()
+ in_progress = self.__rpc(name, *args, **kwargs)
+ if self.__async:
+ return in_progress
+ in_progress.wait()
+ return in_progress.result
+
+ def __getattr__(self, item):
+
+ class RPCPathElement(object):
+ def __init__(self, name, rpc_call):
+ self.__name = name
+ self.__parent_rpc_call = rpc_call
+
+ def __rpc_call(self, name, args, kwargs):
+ return self.__parent_rpc_call('%s.%s' % (self.__name, name), args, kwargs)
+
+ def __call__(self, *args, **kwargs):
+ return self.__parent_rpc_call(self.__name, args, kwargs)
+
+ def __getattr__(self, item):
+ return RPCPathElement(item, self.__rpc_call)
+
+ return RPCPathElement(item, self.__rpc_call)
DISCONNECTED = 'DISCONNECTED'
Something went wrong with that request. Please try again.