[PEAK] What do you use 'offerAs' for?

Phillip J. Eby pje at telecommunity.com
Tue Sep 20 03:24:13 EDT 2005


No, don't worry, I'm not planning to remove it.  :)  At least, not any time 
soon.

Here's the thing.  I've been mulling over one of the big aspects of what 
PEAK does - providing context for components - and realizing that it looks 
a lot like a giant kludge to work around the absence of Lisp-style dynamic 
variables.  If this is true, it would mean that PEAK's API could be 
dramatically simplified for certain types of tasks, to the point of seeming 
to disappear altogether.  In essence, many aspects of PEAK would look a lot 
more like a library and a lot less like a framework.

So, the thing that we found a couple of years back was that most PEAK 
applications tend to concentrate their configuration at some "global" 
level.  We dubbed this a "service area", because you tend to have a bunch 
of singleton services in it, that are shared by virtually every component 
that needs a service of the given type.  An application can have more than 
one service area, but this is usually for special reasons, like the 
supervisor tool that maintains a pre-populated service area for processes 
that it's going to fork off from itself, and a separate service area for 
managing the child processes.  Similarly, an application server hosting 
multiple applications would want separate service areas.

So, our implementation shifted a wee bit to make it easier to do this sort 
of thing.  We added an explicit ServiceArea type and interface, facilities 
to automatically create singletons when you ask for a general-purpose 
component, and so on.

There are some gotchas with the existing setup, however.  One use case for 
creating an alternate service area is when you want to say, create a second 
transaction independent of your current transaction.  For example, if 
you're in an error handler rolling back a primary transaction, you might 
need a separate transaction in which to send out an email notice about the 
error.  This is rather awkward in PEAK at the moment because you can't just 
create a separate transaction and share the remaining components in your 
existing service area, because the coupling between components is via the 
service area.  In other words, all your existing components that use a 
transaction will be tied to your original service area.  This is a bit of a 
pain, to say the least.

Similarly -- and this is the use case that got me thinking about all this 
-- peak.web right now jumps through a lot of hoops with its Context object 
to keep track of a whole bunch of theoretically-orthogonal but practically 
linked variables.  The base URL, current user, current skin, and a bunch of 
other stuff.  Not only does this induce a bunch of slow context lookups to 
find common values, it has no clean way of being extended by multiple 
components.  You can't just go and add a 'shopping_cart' request variable 
cleanly, for example.  Also, the way the peak.web InteractionPolicy works, 
it's hard to replace any of the things it configures at lower levels of 
your object hierarchy.  As a result, peak.web isn't really geared to host 
multiple applications in the same site very well.

What I realized at some point was that peak.web's Context variables really 
want to be like Lisp dynamic variables, that can be set in a function, and 
retrieved by any code called from that function.  Whenever you return from 
a function that sets such a variable, its old value would be 
restored.  Thus, you could for example track the current user by creating a 
web.user variable, something like:

     import context
     user = context.Variable()

You could then in a publishing routine do something like:

     web.user.push(auth_svc.getUser(request_data))
     try:
         print "the current user is", web.user.get()
     finally:
         web.user.pop()

Or in Python 2.5:

     with web.user(auth_svc.getUser(request_data)):
         print "the current user is", web.user.get()

Looks simple enough, yes?  Under the hood, Variable objects would be 
thread-local.  And, to support peak.events and Twisted, there would be a 
way to take a snapshot of all the current context variables, and restore 
the snapshot later, so that e.g. events.Task objects could swap their 
current context in and out.

I've actually prototyped data structures that do all this, and they are 
amazingly efficient - much more so than multi-level parent component 
walking to find things.  So I got to thinking about other uses.

One thing that occurred to me fairly quickly is that a *lot* of the edge 
cases in the usefulness of PEAK's component model can be fixed by using 
dynamic variables in place of hierarchy-based lookups.  For example, the 
idea of the "current transaction" is really time-bound, not 
space-bound.  It really should be "the transaction I'm in right now", not 
"the transaction service for my service area".  The latter just happens to 
be a useful approximation to the former, but if you can actually time-bind 
instead of space-bind, then why bother with the approximation?

So I tried writing some code samples using the idea of making 
'storage.transaction' a dynamic variable, and I quickly found that 
'transaction.get().begin()' was a pain.  So, I came up with the idea of a 
'context.Service', which would just be a proxy that delegated all its 
attributes and methods to the get() of a specified Variable.  I haven't 
worked out the precise API for defining and initially configuring one, but 
it would allow you to do stuff like 'storage.transaction.begin()' and 
'storage.transaction.commit()', and automatically forward the method calls 
to the value of the hidden Variable, so that they go to the right object 
for the current context.

Now, here's something interesting.  If you look closely, you'll see that I 
just wiped out the need to Obtain(ITransaction).  In fact, I wiped out the 
need for ITransaction itself, except for documentation or adaptation.  I 
just say, "storage.transaction", and there it is.  How simple can you get?

Really, the only way to get simpler is with a global variable.  But global 
variables are subject to unrestricted manipulation, and they don't play 
well with threads or events.Task pseudothreads.   But all context.Variables 
can be swapped out with just one call:

     old_vars = context.swap(new_vars)

This operation takes around 2 microseconds on my PC, so it doesn't slow 
down inter-Task switching.  And, each thread has its own current "state of 
all variables" mapping, so there's no inter-thread pollution.  Finally, the 
API is designed so that push() and pop() have to be paired, so manipulation 
is constrained to relatively-comprehensible hierarchies based on calling 
contexts.

All this sounds really great in theory.  But how often would you need to 
change context variables?  If it's done a lot, it would probably be a pain 
to push and pop all those variables.  So, I decided to grep the PEAK source 
for uses of 'offerAs', because 'offerAs' is a way of saying, "I'd like to 
set a new value for this configuration key in the context beneath me."

What I discovered shocked me.  Of the 38 uses of offerAs in the PEAK source 
tree, *18* were in tests, mostly to override a default service with a mock 
one for the test.  What's more, virtually all of the remaining uses were 
ones that would be unequivocally easier to use, understand, and extend if 
they used dynamic variables instead.  Some were transaction-related, but 
there was another edge case I hadn't thought of before the grep: commands.

You see, command objects use component context to get their stdin, stdout, 
argv, envrion, etc.  As soon as I saw that in the grep, I recalled all of 
the pain I went through getting that part of the running.commands framework 
to work correctly, and instantly saw that it would have been a piece of 
cake with dynamic variables, because they really "want" to be 
calling-context based.  It's only because the "real" (Python-supplied) 
versions of those variables aren't dynamic (thread/task-local) that any of 
the fancy footwork was needed in the first place!

Another new use case was the current IEventLoop or IMainLoop a task is 
running under.  There are aspects of that one that have bothered me for 
years!  And yet, the dynamic variable version of those is so simple and 
elegant and clean it makes me amazed I never thought of it before.  My 
prototype implementation doesn't use any Python features that weren't 
around in 2.2, so in principle I could have done this years ago.  (Granted, 
the 'with' statement coming in Python 2.5 provided some inspiration for the 
idea, and it will really be the easiest way to use dynamic variables, all 
things considered.)

So, it's gotten me quite curious, because apart from testing, *every* 
nontrivial use of offerAs in the PEAK core was a part of some sort of cruft 
that would've been incredibly better with dynamic variables.

So what uses do *you* have for offerAs?  Would they be clearer or less 
clear by using dynamic variables?  I'm intrigued by the possibility of 
simplifying the PEAK core quite a bit with this notion.  Most 
binding.Obtain() bindings would be replaced by simple direct reference to 
services, although component-local Obtain's would still be useful.  All the 
crufty bits of PEAK I found would become non-crufty.  All of the colorful 
configmaps and eigenvalues and all that would mostly melt away, with the 
.ini files just being used to set dynamic variables instead.

There are a lot of things that would have to be worked out to do that, of 
course.  Especially since the API, although improved, would likely be quite 
different.  I haven't even worked out the destination, let alone how the 
existing codebase would transition.  And the "when" of all this is quite 
questionable.  I have no idea when I'd even really start.

But in the meantime, I am quite curious.  If you have any uses for offerAs, 
please post about them.  I'm interested in whether there are any uses that 
really are better than using a dynamic variable for the same thing.




More information about the PEAK mailing list