using Race::Condition to help track/test, well, race conditions ...

Knock Knock. Race::Condition. Who’s there?

Race conditions are a fact of life. What can we do when one knocks upon our door?

Often we mark race conditions that are not immediately solvable with a comment:

if (!-l $config && -f _ && -r _) {
    # RACE! config file could go away, change type, or become unreadable between file test and file read
    open my $fh, '<', $config …
    …

That is good so that future us can be easily reminded of the issue. It is really hard/impossible to debug or test the various possible race conditions though.

Race::Condition gives us a way to still mark the race condition but also adds in the ability to hook into it for debugging or testing purposes.

use Race::Condition ();
…
if (!-l $config && -f _ && -r _) {
    race::condition('config file could go away, change type, or become unreadable between file test and file read');
    open my $fh, '<', $config …
    …

We still have the advantage of the explanation that the comment contained (and now we have something consistent to grep for to find them, neat!).

The Real Hotness™ though, is that now we can test how our code handles it when a race condition does happen. For example, assume our code above is in a function called foo(), we might do something like this:

{
    note "Testing foo() race condition when config file turns into symlink";

    my $name;
    no warnings "redefine";
    local *race::condition = sub {
        ($name) = $_[-1]; # use -1 so that it can be called as a method and/or have additional meta info if that made sense in your context
        ok(1, "sanity: race::condition() called ok for symlink test: $name");
        … change $temp_file symlink here …
    };

    … call foo() w/ $temp_file and test that is it handled as expected here …

    ok(defined $name, sanity: race::condition() called ok for symlink test: $name");
}

Of course you could test other stuff too (what if it goes away, what if it becomes a directory, what if it becomes a fifo, what if permissions change, etc etc).

The idea is you can write those sorts of tests very easy this way.

Another, similar, use is to use it for debugging during development that contains not–immediately–resolvable race conditions:

no warnings "redefine";
local *race::condition = sub {
    my ($race_text) = $_[-1]; # use -1 so that it can be called as a method and/or have addition meta info if that made sense in your context

    # see what the state of the file is after our test and before acting on it, still could change after this but at least we might start seeing the pattern on a rare race
    d( {$race_text => [stat "foo/bar"]} ); 
};
⋮
…  foo() is called later …

or call it with more info and make your debug more flexible:

race::condition($config, 'config file could go away, change type, or become unreadable between file test and file read');

no warnings "redefine";
use Devel::Kit; # gives d() and a lot more!
local *race::condition = sub {
    my ($path, $race_text) = @_; # use -1 for race text so that it can be called as a method and/or have addition meta info if that made sense in your context

    # see what the state of the file is after our test and before acting on it, still could change after this but at least we might start seeing the pattern on a rare race
    d( {$race_text => [stat $path]} ); 
};

I hope you find this useful, enjoy!

Side Note: I made it a fully qualified function (which I feel should be used sparingly and only when there are specific valid reasons) for a few reasons that I may blog about later.

Leave a comment

About Dan Muey

user-pic I ♥ perl