Hide low-level details behind an interface

Perl 5.16 makes the Perl special variable, $$, writeable, but with some magic. That’s the variable that holds the process ID. Why would you ever want to do that? There’s not much to write about with this new feature, but there’s plenty to write against it since it introduces more magic (see commit 9cdac2 on June 13, 2011).

Prior to Perl 5.6.1, you could write to $$ all you wanted, but after that it was almost a read-only variable. You could still change it through a backdoor; you could localize the typeglob *$, and, having done that, affect $$:

{
local *$;
$$ = 42;
...
}

Do you really need to do that? Sometimes the problem is making too many things possible, and I think that’s the case here. The answer might be to push the complexity back to the higher level. That is, make the people who want a writeable $$ come up with a different way to do it.

The change in Perl 5.15.0 motivated by a test failure in DBIx::Connector. The _seems_connected compares the value in $$ with the value it stored. It does this so it can seemingly maintain a database connection across a fork. When it realizes it is now in a different process, it performs some housecleaning to signal that it needs to reconnect:

# lib/DBIx/Connector.pm
sub _seems_connected {
    my $self = shift;
    my $dbh = $self->{_dbh} or return;
    if ( defined $self->{_tid} && $self->{_tid} != threads->tid ) {
        return;
    } elsif ( $self->{_pid} != $$ ) {
        # We've forked, so prevent the parent process handle from touching the
        # DB on DESTROY. Here in the child process, that could really screw
        # things up. This is superfluous when AutoInactiveDestroy is set, but
        # harmless. It's better to be proactive anyway.
        $dbh->STORE(InactiveDestroy => 1);
        return;
    }
    # Use FETCH() to avoid death when called from during global destruction.
    return $dbh->FETCH('Active') ? $dbh : undef;
}

To test this, you need to test the branch where the value in $$ is different than the stored one. The test handles this by local-izing $$ and giving it a temporary value:

local *$; $$ = -42;
ok !$dbh->{InactiveDestroy}, 'InactiveDestroy should be false';
ok my $new_dbh = $conn->dbh, 'Fetch with different PID';
isnt $new_dbh, $dbh, 'It should be a different handle';
ok $dbh->{InactiveDestroy}, 'InactiveDestroy should be true for old handle';

There’s a design problem here. The _seems_connected depends on a low-level detail and accesses it directly, and it’s tests have to know something about that implementation to test it. The fix is not to change perl, but to make the more natural fix in the code. Since the problem is faking a value to test a branch of code, the problem with the code is that it doesn’t provide an interface for that value. In most cases, difficulty in creating a test highlights a problem with your design.

So, how can you fix that? Don’t make anything aware of $$. Instead of accessing this magic value directly, create a method to fetch it. Now, the implementation of the code that returns the process ID doesn’t matter because it’s in _current_pid:

# lib/DBIx/Connector.pm
sub _seems_connected {
    my $self = shift;
    my $dbh = $self->{_dbh} or return;
    if ( defined $self->{_tid} && $self->{_tid} != threads->tid ) {
        return;
    } elsif ( $self->{_pid} != $self->_current_pid ) {
		...
    }
	...
}

sub _current_pid { $$ }

If you have to play various games to get the right value for a particular operating system or threading model, you hide it behind _current_pid. You don’t have to know that it comes from a Perl special variable, or a library call, or something else.

When it comes to testing it, you can do many things, including redefining the method to return the value that you want:

{
no strict 'refs';
no warnings 'redefine';

my $class = ref $self;
local *{ "${class}::_current_pid" } = sub { 42 };

...;
}

That looks complicated, and there are various testing modules that can help you with that, but it’s still what’s happening behind the scenes, eventually, no matter how you wrap it. The more important idea is how much you expose to the lower levels. It’s better to do a little more work than depend on low-level details that you shouldn’t know.

If DBIx::Connector has to do something different to fetch the process ID, this test doesn’t break. Although it relies on the interface of the module, it doesn’t rely on the implemention. You don’t have to know anything more than you should, and isolated levels of knowledge is one of the keys to keeping code from misbehaving.

There is one project, PPerl, that might need to actually change $$, but it hasn’t seen a release in several years and has mostly FAIL reports from CPAN Testers. That’s not sufficient motivation for a change in perl either.

This issue with $$ a simple example, but you should extend the same idea to any code that you need to test. If you have to change a low-level value in a test, that means your code has a low-level dependency. Isolate that dependency, and, having done that, use that isolation to create the test.

And, having done that, perl doesn’t have to change itself to support a few modules that didn’t actually need the change, and perl wouldn’t have this new feature. You might like to read XXX Raymond Chen’s essay on the development difficulties with Windows 95, which wanted to support all sorts of third-party shenanigans, turning it into a festering pile of unmaintainable garbage. Perl 5 has a similar problem, which was one of the main motivations for Perl 6.

If you want to follow this feature, simply search for $$ in the git history (using the right escapes for your shell):

$ git log --oneline | grep \\$\\$
9cdac2a Make $$ writable, but still magical
0e21945 Turn $$ into a magical readonly variable that always fetches getpid() instead of caching it
e10bb1e Win32 stuff:  A. Use Perl_my_socketpair()  B. Use PerlSock_xxxx() rather than raw xxxx() so we get to load winsock.  C. (In passing) work round fact that $$ is now SvREADONLY so we need to     take special measures to set it during pseudo-fork.
306196c $$ readonly, take two Message-ID: <20011124195618.A14614@blackrider>

Things to remember

  • You can probably find another way to do what you need without changing $$
  • An issue with testing often points to a deficiency in design
  • Isolate low-level dependencies so you don’t need low-level access to test them

Post to Twitter Post to Delicious Post to Digg Post to Facebook Post to Reddit

Leave a comment

4 Comments.

  1. +1!

    Also, your solution will work on just about any perl version out there, and this $$ magic will only work in 5.16.0+… So DBIx::Connector will still need to implement something for perl <= 5.14.x…

  2. You asked me to explain why I needed writable $$ in IPC::Messaging.

    Briefly, the module can spawn processes and send messages to them afterwards. A message send looks like a method call on a var representing a process: $proc1->message1(1,2,3). Sometimes one wants to send message to the current process. I chose to make $$ an object so that the same syntax applies: $$->messageBlah(42). It is by no means a hard requirement; a global var or a function call like this_proc()->messageBlah(42) would work just fine. But since $$ already has a strong association with “this process”, such syntactical cuteness seemed appropriate, in the module’s context.

Leave a Reply

You must be logged in to post a comment.