Stop making things better
September 14th, 2008Sometimes I think we should never improve anything.
Every time we add goodness to an app or library someone gets hurt in the fall-out. Some unexpected side effect causes pain in userland (which is a place of many wails at the best of times). I swear it’s not our fault. OK, let me rephrase that. I suggest that occasionally we’re not to blame. Users have a way of finding a product’s one unsupported feature (which we maintain is actually a minor unfixed bug) and using it to power their entire business model. So when we fix the bug at last we’re surprised to get a call from an irate person called Bob enumerating a number of interesting and anatomically implausible plans he has to improve us, should we decide not to roll things right back.
Sorry Bob, but by then it’s too late. You can never take it back. At least four users will have launched new enterprises that depend utterly on the fix. And so the cycle of pain and recrimination rolls on.
This week I played Bob and spent some time in a userland hell of my own making. It’s a long story, actually, that starts here.
Back in the days of PHP 5.1 the language had a slightly odd feature. PHP provided a magic __toString() method that looks promisingly like the Java equivalent. The promise here is clear enough. Where a class wraps a string, it looks as if you should be able to implement __toString() and have the string value substituted when an object instance is used in a string context. And, yes, for this:
class wrapper {
function __toString() { return "I'm a wrapper\n"; }
}
$wrapper = new wrapper();
Then
print $wrapper;
outputs
I'm a wrapper
Trouble is, if you place the object reference in a string, then you get something else entirely. This:
print "explain: $wrapper\n";
outputs:
explain Object id #46
Now, its obvious that this is one of those ‘features’ that will just go away one day. It’s just a loophole waiting to be closed. So of course I made significant parts of a project dependent upon it. Because I’m a user. I realized that you could use stringified object references as array keys:
$list["$wrapper"]
resolves to
$list["Object id #46"]
Neat.
But also quite stupid. Because when PHP 5.2 came along the loophole was closed, and suddenly
$list["$wrapper"]
resolved to
$list["I'm a wrapper\n"]
which is no longer guaranteed unique (not that the original behavior was in all circumstances, but that’s a different story). Much fixing of bugs ensued.
Fast forward a year. The team saw that this nifty string behaviour worked as expected, and it was good. Looking through our codebase our aesthetic sensibilities were offended by clumsy constructions like this:
print "adding value ".$mywrapper->toString()." here";
So over time we implemented __toString() methods and cleaned up clumsiness thus:
print "adding value {$mywrapper} here";
All was well. All unit tests passed. All regression tests passed. QA were happy. The release went out. With three layers of testing like this, surely little can go wrong. My laugh is bitter and hollow.
Someone ran our code against PHP 5.1. Files named Object id #45.db started appearing on the filesystem. Users were greeted: Hello Object id #36. Fatal errors appeared, some immediate, others as bad values were pulled from a database and slotted into confused and panicking code.
In fixing this, my first approach was a slack-jawed drool of an idea. I tried to chase down each error. This fly-fishing strategy failed utterly, mainly because the errors were not always obvious. There’s nothing about
print "adding value {$mywrapper} here";
that’s obviously wrong unless you know what kind of value $mywrapper holds. This statement produces garbage, but it does not generate a nice easy exception. After a full day of bug hunting I went home smelling of fail. The next morning, though, I switched from fly fisherman to trawler. Actually the best platform to catch these errors is PHP 5.2, because it’s here that you know that __toString() will be invoked. It was simply a matter of adding a stack trace and a die() statement to each of these methods, in order to catch all the invoking code. Every time the system died, I followed the stack trace and replaced the old inelegant explicit toString() call. When my unit tests all finally passed, I switched back to PHP 5.1 to confirm that all worked as expected.
So what have I learned? If I were smart I’d take this away:
1. Test against every major version and platform you purport to support. Virtualization can help here.
2. Don’t exploit side effects and undocumented features in a language, library or app. They are voodoo and will eat your soul sooner or later.
3. Don’t use new features if you want to support platforms on which they won’t work. Or if you do, then degrade gracefully.
If you find this bloody obvious, then woohoo for you. Personally I need the occasional reminder.
Alternatively, of course, I could choose blame the PHP team for my own shortcomings. The problem with open source products though is that there’s often no one person you can to shout at. Anyway if you lurk on php-internals you’ll find that they’re far too busy shouting at each other to tolerate any userland bolshiness.