[PEAK] Events and listeners
Phillip J. Eby
pje at telecommunity.com
Fri Jan 2 21:23:51 EST 2004
After letting the rough draft of peak.events sit for a while, I think I
know how I'd like to have events and listeners work in PEAK, in general.
The draft version of peak.events had callbacks that took no arguments. The
current draft of peak.running.timers has callbacks with several
arguments. I think I'd like to compromise on a format that takes two
arguments:
class IEventSink(protocols.Interface):
"""Accept an event from an event source"""
def __call__(source, event=None):
"""Accept 'event' from 'source'; return truth to "consume" event"""
Here's the design theory:
* Lots of useful event sources really need to provide some data as part of
the event. Although the draft mechanism for values, queues, etc. provides
a way to get the "current value" from certain kinds of sources, it would be
much more efficient to simply provide the value with the callback.
* We only need to provide *one* value, though, because it can be an
arbitrary object, tuple, dictionary, etc. We don't need a strongly-typed
event mechanism for this interface, because that's defined by the specific
thing you're getting called back by. And, we make it optional since some
event sources don't have any event data besides "I just happened."
* But, we keep the source as a separate parameter, so that the 'event' can
be a simple value (like a string or integer). Otherwise, each new event
source would have to come up with an event type or data structure
convention to holding the source as well as what other data was
needed. And, it makes it possible to quickly call back the source to e.g.
re-add the callback.
* Some event services and systems need a way to "consume" events. That is,
a way to stop their propagation through a sequence of alternate
handlers. For example, most GUI frameworks have a mechanism like this, and
I also want the PEAK logging framework to be able to have handlers that
process log events this way. So, if an event source works that way, the
convention will be "return truth to "consume" an event", since the natural
way for most handlers to end is by not returning a value... i.e., None, and
consuming an event should not be the default case.
I think this is simple enough of a signature to work for pretty much
anything we need across an enormous variety of events. Unfortunately,
callback parameters are pretty much wasted on threads, since there isn't
any way to get them back into the underlying iterator. Unless of course we
gave threads a "mailbox" that held incoming events, and supplied it to the
iterator, e.g.:
def doSomething(mbox,otherArg,...):
yield anEventSource; resume()
for source,event in mbox:
...
OTOH, if you need this, it could perhaps be expressed as:
def doSomething(otherArg,...):
mbox = EventQueue(anEventSource)
yield mbox; resume()
for source,event in mbox:
...
yield mbox; resume()
with some loss of efficiency.
This brings up another issue: circular references. Callbacks mean circular
references, if the thing that's being called has a reference to the thing
doing the calling. The 'source' parameter might allow mitigating this
issue in some circumstances, though not in the general case. For example,
consider the EventQueue above; if 'anEventSource' keeps a reference to one
of the queue's methods in order to call it back, then of necessity the
EventQueue can't be garbage collected, even if there are no references to
it remaining. Indeed, the queue would keep filling up with events from
anEventSource long after anybody was taking any of the events *out* of the
queue. Matters are made worse by the lack of try/finally in generators,
making it impossible to force a queue to be "closed".
We could let the EventQueue hold on to 'anEventSource', but only give
'anEventSource' a reference to the queue when there's at least one callback
(reader) present on the queue. This lets garbage collection take place,
but at the price of ignoring events while you're not actively listening to
them.
So the only way I see to deal with the circularity issue in general is to
have listener-adapters that hold a weak reference to their target, know
what attribute of the target to invoke, and can deal with the target going
away.
On the other hand, some kinds of listeners, like logging handlers, never
really go away, since they aren't a pipeline to some thread that may or may
not be paying attention. So they don't need weak reference
management. That probably means that we'll need to ensure that event
sources based on other event sources have a way to supply weakrefs to their
upstream subscription, rather than build weakref management into the
event-producer side of the equation. Thus, event "transformers" that
consume events via callbacks and then produce events of their own, need to
have weakref management for their input subscriptions.
Interestingly, this need would go away if there was a means for passing
values back into a running thread, since this would allow one to receive
the value that was being "waited on". Indeed, the idea of subscriptions
could go away entirely (except for "handler"-type event sinks), since it
would suffice to have a thread wait for, and directly receive, the values
it needed from an event source.
OTOH, the existence of a way for the data to "come back" into the thread
would effectively be a queue, so it's not clear that it'd solve the problem
in the general case. But, since callbacks would be of a one-shot nature, a
dead link would clear itself quickly. That is, even if the thread were no
longer runnable, the event source it was waiting on last would drop its
reference as soon as its next firing. So, that actually would break the
circularity as soon as the event fired.
This seems to make a strong argument for using only one-shot callbacks on
event sources that you "pull" from. If you needed a callback to happen
repeatedly, you just loop yielding to that event source, which re-adds the
callback each time you actually need it. Since nothing else happens while
a thread is between yields, you can't miss any events. Of course, if
you're yielding to one event, you could miss an occurrence of
another. But, that's what 'AnyOf' will be for, to monitor multiple event
sources at once.
[much rambling and many unworkable approaches deleted]
I've got to give some more thought to inter-thread communication in order
to be able to finish this up. I'll do that on the way home, though.
More information about the PEAK
mailing list