Merge lp:~robru/gwibber/async into lp:~barry/gwibber/py3
Status: | Merged |
---|---|
Merged at revision: | 1407 |
Proposed branch: | lp:~robru/gwibber/async |
Merge into: | lp:~barry/gwibber/py3 |
Diff against target: |
65 lines (+22/-5) 2 files modified
gwibber/gwibber/protocols/base.py (+7/-2) gwibber/gwibber/tests/test_plugins.py (+15/-3) |
To merge this branch: | bzr merge lp:~robru/gwibber/async |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw | Approve | ||
Review via email: mp+122430@code.launchpad.net |
Description of the change
Alright barry, sorry for spamming your inbox with that other merge proposal. What happened was I had implemented the threading code inside of a decorator that could be applied to any method, which was sexy in a general purpose kind of way, but then I realized that for the decorator to be effective, every single plugin was going to need to decorate every single method, which was going to be cumbersome and error prone (all it would take is one plugin author to forget to decorate one method and then suddenly all of gwibber is blocked on IO when that method gets called).
After that, I had the flash of brilliance that I could just decorate BasePlugin.__call__ and that would be enough to make all other methods invoked through __call__ to run in threads. That was a good start, but I was a little bit concerned that there were too many layers of misdirection happening (calling plugin('foo') meant invoking the decorator, which invoked a thread, which invoked a lambda, which invoked __call__, which invoked getattr, which invoked the correct operation). Although it wasn't anywhere near as bad, it was starting to look like the old dispatcher code, and I wanted to avoid that at all costs. So at that point I gave up on the decorator protocol and just merged the threading logic directly into the __call__ method. I was also able to factor out the lambda, which means __call__ invokes a thread, which invokes getattr, which invokes the desired operation directly.
The best part about this particular approach is that it means we can test all plugin operations in a very straightforward *synchronous* manner, without needing to write any tests against the threading module.
Hi Robert. I'm going to review the newly pushed update, which contains the
change of s/plugin/protocol/
=== modified file 'gwibber/ gwibber/ protocols/ base.py' gwibber/ protocols/ base.py 2012-09-04 20:39:25 +0000 gwibber/ protocols/ base.py 2012-09-04 21:09:51 +0000 target= getattr( self, operation), kwargs) .start( )
--- gwibber/
+++ gwibber/
> @@ -19,12 +19,17 @@
> ]
>
>
> +from threading import Thread
> +
> +
> class Base:
> def __init__(self, account):
> self.account = account
>
> - def __call__(self, operation, **kws):
> - return getattr(self, operation)(**kws)
> + def __call__(self, operation, *args, **kwargs):
> + """Use threads to avoid blocking on I/O."""
> + Thread(
> + args=args, kwargs=
You probably want to make this a daemon thread, so that if it doesn't
complete, it won't prevent exiting of the main process. Here's what I plan on
committing:
def __call__(self, operation, *args, **kwargs): target= method, args=args, kwargs=kwargs)
thread. daemon = True
thread. start()
"""Use threads to avoid blocking on I/O."""
# Let AttributeErrors propagate up.
method = getattr(self, operation)
thread = Thread(
Although it might be better to squirrel the thread away in an instance enumerate( ) as in your test.
variable, so that it can be directly joined, instead of having to loop through
threading.
>
> def _login(self):
> """Prevent redundant login attempts."""
=== modified file 'gwibber/ gwibber/ tests/test_ plugins. py' gwibber/ tests/test_ plugins. py 2012-09-04 20:39:25 +0000 gwibber/ tests/test_ plugins. py 2012-09-04 21:09:51 +0000
--- gwibber/
+++ gwibber/
> @@ -32,9 +32,10 @@
>
> class MyProtocol(Base):
> """Simplest possible protocol implementation to allow testing of Base."""
> + result = ''
Do you think it would be better to initialize the result to None in Base?
Also, what if you try to access result why the thread is still running?
Should you get an exception? Or just None? should trying to get the result
implicitly join the sub-thread, thus guaranteeing that the self.result is the
return value? Of course, that could block, but maybe that's better than
getting a potentially incorrect value.
Putting result in Base could be accompanied by a comment indicating the
required API. Thinking about this some more, I'm worried about race
conditions on self.result. What if two operations on the same plugin are
invoked? Let's say they both complete before the consumer reads self.result.
Won't this cause one result to get overwritten and lost?
I think we're now stumbling into the use cases for the defer package. :)