Tuesday 26 January 2010

Code reloading and event notifications

A problem with the use of code reloading is reconciling the changes made to code with how existing and new uses of that code are affected. I've been experimenting with removing this problem for users of my code reloading framework, in this case in the implementation of an event subscription and notification system for my MUD framework.

Without code reloading

The use case is notifying instances of a class when an event they are interested in occurs. If code reloading is not in use, the following code might be one way for a user to indicate interest in, and to receive events.

Manual event registration:

class Object(BaseObject):
__events__ = BaseObject.__events__ + [ "OnSomethingHappened" ]

def __init__(self):
BaseObject.__init__(self)
events.Register(self)

def OnSomethingHappened(self):
pass
But in taking this approach, the user needs to keep several things in mind. That they do not clobber the events the base class may register for. That they list the function names of new events in the __events__ list on the class. That the base class may do the registration as well, so either they need to check and only register locally, or just register a second time if they cannot be bothered with it.

With code reloading

If code reloading is used, then the manual registration becomes a potential problem. Should an event registration call get added to the class sometime during the development process, existing instances will not receive the new events. And as changes are made over time and further events are added, which instances receive them will depend on whether they were created after the appropriate change. This is bad because it complicates the model of possible behaviours and problems that the user has to keep in their head. The way it should work, is such that any changes made to code are automatically applied to existing uses of that code, where it is reasonable to do so.

By having the framework react to the live addition of a class in a namespace, further live updates to that class and subsequent instantiations of it, it should be possible to build on these things to make event subscription a lot simpler for the user.

Automatic event registration:
class Object(BaseObject):
def event_SomethingHappened(self):
pass
Here the user simply needs to know that if they prefix a function name on their class with event_, any instances whether already existing or yet to be created, will be guaranteed to receive it.

Implementation

The main goal is that event delivery should be direct and not require any additional work to identify valid subscribers. All the work would instead be done when code reloading takes place. This would be achieved by processing classes that are created or updated and determining what events they implicitly require notification of.

Code reloading event registration:
    def OnClassCreation(namespace, className, class_):
pass

def OnClassUpdate(class_):
pass

cr = reloader.CodeReloader()
cr.SetClassCreationCallback(OnClassCreation)
cr.SetClassUpdateCallback(OnClassUpdate)
Existing instances would be unsubscribed from events where methods are removed, and subscribed to events where methods are added. This is a little complicated. It should be a lot easier to react to creation of new instances. But getting notified about the creation of new instances without requiring boilerplate on the part of the user is not straightforward.

One way would be to replace a method involved in the instantiation of instances of the class, like __new__ or __init__. __new__ can be ruled out, as it only works for new-style classes. __init__ seems to be the likeliest choice. With the events notifying of class creation and updates, injecting a method into a given class is a straightforward extension.

Monkey-patched __init__ method:
    def __init__(self, *args, **kwargs):
self.__real_init__(self, *args, **kwargs)
events.TrackAndRegisterInstance(self)
But maybe there is an easier way to hook into instance creation that I am not aware of.

Conclusion

It is very possible for the user to change code in a way that is incompatible with existing use of that code. But I think that it is a valuable goal not to try and increase the number of ways that this might happen.

Some of the problems are caused by the way Python is implemented, like where functions in mid-execution (particularly when using Stackless) use the code that was in place when they started executing, and continue using it until they exit. In this case a user needs to add boilerplate to ensure that changed code is adopted.

Loop boilerplate:
    def Loop(self):
while self.running:
self._Loop()
Sleep(1.0)

def _Loop(self):
pass # Actual loop logic
Other problems are caused by the user making changes that are incompatible with existing use, like modifying functions on a class to use a new variable that only gets initialised for new instances in the accompanying new version of __init__. To illustrate this, if the counter variable is added to __init__ and _Loop, then existing instances will cause an AttributeError on their next call to _Loop.

Problem case:
class SomeObject(BaseObject):
def __init__(self):
self.counter = 1
...

...

def _Loop(self):
self.counter += 1
...
I don't have a good solution for this. It is one of those things the user just has to remain aware of. And in around eight years of using another code reloading solution, I cannot say that I really recall making this mistake myself. The only way I can conceive of dealing with it, is by analysing code changes using the AST or some other method of introspection.

In fact, given the direction I am heading, this might be worth exploring. If it can be done in a way where it does not become a further burden on the user, then definitely so.

2 comments:

  1. Sounds interesting. Feel free to borrow and share anything from my 'reimport' python module.
    http://code.google.com/p/reimport/

    This handles the 'events' slightly differently, by allowing each module being reloaded to define a callback. This gets a chance to examine the old and new contents. I've been thinking about adding that for classes also, but haven't needed it, so haven't had a clear direction.

    ReplyDelete
  2. My code reloading framework does not handle standard Python modules, so high level behaviour reconcilation approaches do not necessarily translate over.

    However, it is interesting to look at frameworks which work with the standard module system for inspiration. Also, I need a framework that works with the standard module system for my MUD framework, as it also relies on standard modules in addition to the custom script directories managed by my code reloading framework. So hearing about more, is useful for reference, in case I do not choose to write my own.

    ReplyDelete