My current Perl Ironman Challenge status is: My Ironman Badge

Sunday, May 24, 2009

Of the long road to usable POEx::Roles

This past week was punctuated by lots of frustration. But the good kind really. As of yesterday, I have a working POEx::Role::TCPServer. That is, the simple POE::Wheel::SocketFactory and POE::Wheel::ReadWrite bundle on top of POEx::Role::SessionInstantiation(pxrsi) works as expected without surprises. It took all week to get there really.

Pxrsi had problems. A lot of problems. While in the simple cases, it would work as designed, as soon as you threw in a Wheel that fiddles with the states of the instantiating session, it go sideways, off the cliff, into the valley of lava, inhabited by flesh eating zombies. The biggest problem was that my advised methods like _start were getting called in an infinite loop, especially if the first _start executed touch the states and kicked off a _clone_self. Chatting with Matt Trout about the problem had a startling revelation: when constructing an anonymous class, roles that you give to the method are applied. Which makes sense if you if you step away from my crack filled idea of what I was doing (anonymous class clones to enable per instance method level changes). So my methods were getting advised multiple times.

And so that led to lots of hilarity. in TCPServer, I am instantiating a SocketFactory on _start. Well if there are multple _starts, the second will fail because the first has already bound to the port. But of course, that isn't obvious at first. In the depths of the debugger, I was noticing that as roles were being applied that the instantiated SocketFactory was /going out of scope/. WTF? So yeah, I chased that one for a little while.

So I made a few changes to pxrsi: only clone once when needed, revert tracing from fancy advised methods to simple statement before invocation, etc. The only clone once thing made a lot of sense. It would only happen if an instance received a _register_state event, and from that point on, you are working with an anonymous clone class specific to that instance anyhow. So that in the end is a big performance gain not having to clone the class every time a state is registered or removed.

Awesome, but then I came across another problem. I was making use of Moosex::AttributeHelpers in POEx::Role::TCPServer for wheels management and I had defined a number of "provides" methods. Of course, the _invoke_state heuristic for determining events from non-events was failing. And that is the problem with implicitness, in this case.

But the day was saved when Cory Watson announced work to support method traits. Holy crap that is exactly what I needed. MooseX::POE (mxp) had added a declarative keyword for defining events. And it made sense because of how mxp was architected around POE::Session. I wanted something a little more magical. So it wasn't a day or so before Cory had the method traits stuff working. A couple of git remote updates later, and I had declarative events. So _invoke_state and _register_state were rewritten to handle checking for a POEx::Role::Event role and like magic, the simple cases were working.

So I scurried back to POEx::Role::TCP and it was still failing. The coderefs installed by the various wheels shipped with POE obviously expect a POE::Session API compatibility. Hrm. Then reworked part of the method responsible for wrapping coderefs delivered from wheels (a horrible hack, I might add).

Then mysteriously another failure cropped up in pxrsi that took me awhile to figure out. My test wouldn't even run. Of course, this was after a week of changes from Devel::Declare, to MooseX::Method::Signatures, to MooseX::Declare (basically the whole chain of dependencies under pxrsi). The message I was getting was rather cryptic. It (Moose) was complaining that it couldn't find one of the advised methods in the inheritance hierarchy for the Role. It kind of blew my mind. It was working before! But then I remember that I had made a small change to the test to advise one of the methods in the class being defined rather than in a class defined from an event handler.

The way MooseX::Declare works with roles definition is that it defers application of those roles until after the class has finished parsing. And with my changes, if you remember, method modifiers don't get executed until after roles have been applied. So I was a little perplexed. I don't know if this is a bug in Class::MOP/Moose or what, but the roles being applied weren't actually loaded, yet. But once you had an instance of the class, your role was ready to go. So I patched MooseX::Declare to call load_class as the roles were encountered to ensure they are loaded before being applied. And that fixed my advising.

So what is the result of all of this? POEx::Role::TCPServer works and is very simple. Scarily simple. And you can advise any portion of it. This means that you could around advise handle_on_connect to SSLify a socket before letting TCPServer do the mundane stuff, etc. It is pretty cool.

But ultimately, why am I reinventing the wheel, you may ask. I came across a tiny little problem when working on Voltron last weekend. I need a much, much simpler model for session proxying than what IKC provides. Basically, I want to be able to "subscribe" to a remote session and have it instantiate a local, persistent proxy session. So I need to write that. And well, if I am writing things like this, I might as well eat my own dog food and build up all of the toolchain to make that happen. So that includes things like TCP server and client roles, etc. It worries me somewhat because I only have a month left before YAPC and that means, I need to get this thing working as quickly as possible. Hell of a time to decide that I need to replace one of the core components.

I really feel that writing these roles will decrease complexity in developing POE applications. For me, it always felt like there was this tension in writing good, clean OO code and making use of POE. Oh sure you have object states and whatever, but the machinations of POE would shine through for every constructor that instantiated a POE::Session object. I want to make the encapsulation complete, eventually. Ricardo Signes is working on some MRO::Magic module that promises some awesomeness. It would be very cool if all your method invocations, magically, behind the scenes carried out the POE stuff without having to be too concerned about it. So posting, yielding, and calling would look like ordinary method invocations on an object, but the object itself would essentially be a proxy. Still a long way off from making that happen, but we're getting there.

No comments:

Post a Comment