Monday 11 January 2010

Sorrows mudlib: An event notification/subscription model, part 2

Previous post: Sorrows mudlib: An event notification/subscription model, part 1

At this time, my preferred approach is to identify event subscriptions by how functions are named within a given object.

    def event_OnServicesStarted(self):
However, when I think about functions simply receiving an event when it happens, using a simple decorator has some appeal.
    @event
def OnServicesStarted(self):
But I am not sure how extensive my event model is going to get. Do I want to allow a subscription to specify when it gets received, or how it gets received?

When events might get received

For some events, it is useful to know that you are receiving a notification before an event has had side-effects through its subscribers, when it is appropriate to act on it and cause side-effects or when all direct side-effects have happened.

A simple way to structure this is to allow subscription for one of three different partial broadcasts. The first, a pre-broadcast. The second, the actual broadcast. And the third, a post-broadcast.

Using the decorator syntax, subscription might happen in the following way.
    @event.pre
def OnServicesStarted(self): pass

@event
def OnServicesStarted(self): pass

@event.post
def OnServicesStarted(self): pass
Each decorated function will have the same name, and will overwrite any previous attribute on the class, specifically the event function before it. This means that this specific approach using decorators will not allow an object to register for more only one stage in the broadcast of a given event.

Using the name-based syntax, subscription might happen in the following way.
    def event_OnServicesStarted_pre(self): pass

def event_OnServicesStarted(self): pass

def event_OnServicesStarted_post(self): pass
This is a little more versatile than the decorator approach, as it allows an object to register for every stage in the broadcast of an event. Of course, to address this the function naming could be brought partially into the decorator approach, where the use of the _pre or _post suffix would have the same effect.

How events might get received

Using Stackless Python, my MUD framework cooperatively schedules the microthreads it is built upon. Cooperative scheduling is a lot simpler to program than preemptive scheduling, because you know where your code is going to block. You do not always want your code to block when you want to send an event, although at other times it might be acceptable.

So the broadcaster needs to be in control of how events get sent. They need to be able specify that a broadcast can block or not.

It might be done through a global function.
    event("OnServicesStarted")
Or avoiding the clunky string used for an event name.
    event.OnServicesStarted()
It is reasonable to assume this is how blocking event broadcasts are made, given that the natural expectation for a function call is for it to do processing and return. So, that leaves the question of how to do non-blocking event broadcasts.

One possibility, is a special attribute that injects differing behaviour.
    event.noblock.OnServicesStarted()
Here, the noblock attribute would simply return a secondary event object that starts event broadcasts in a new microthread.

Does this effect subscribers? Not unless a subscriber has the ability to interfere or interrupt a broadcast, which is not a desired part of this model. Event subscription does not need to know about, or cater for this. It is a broadcast related detail.

To decorate, or not

Not. There is no compelling reason, beyond appreciation of how it looks, to choose to use decorators. The function naming approach is fine for now, and does not cost an extra line of source code.

When are objects registered

It is all very well for objects to specify functions named in such a way that it is recognised they should be called when events happen. However, the object still needs to be registered for those events. What is the best way to go about this? How is the use of code reloading affected by this?

It is possible within the code reloading system, to set things up in such a way that when an object is instantiated, it automatically gets registered for events. The way this would work, is that when a class is first loaded, or subsequently reloaded, it would have event related post-processing applied. The code reloading system does not currently support this, but it is an easy feature to add to it.

A simpler approach, is to have any object which defines events, make an explicit call to the event system to register it for any events that it may handle. This would have to happen within the constructor for that object, the __init__ method. The problem is that this method is called when the object is instantiated, and if the registration call is added within a subsequent code change, existing instances of the object will never get registered. It complicates the model a programmer has to hold in their head about how effective code reloading is.

Taking care of registration with the aid of the code reloading system looks like the way to go.

Conclusion

I am pretty sure I have gone over what I need out of an event system. I have a preferred approach to how event subscriptions are declared. I have a preferred approach to how event broadcasts are made. At this point, implementation looks like the next step.

Next post: Sorrows mudlib: An event notification/subscription model, part 3
Source code: events.py

No comments:

Post a Comment