Testing Race Conditions in WordPress

WordPress is not thread-safe.

I’ve spoken about this, and even started work on a plugin called WP_Lock that will aim to introduce some thread-safety into core to address the occasional TOCTOU bug under high load (and concurrency). For example ticket #44568 is an easy-to-reproduce complaint about concurrent REST API access 😉

Testing Concurrency Issues in WordPress with PHPUnit

If you thought writing thread-safe code in WordPress plugins is hard, unit testing the code for concurrency issues is even harder. One of the ways I found works best is by utilizing the PCNTL module in PHP to fork and test critical sections.

function test_in_child( callable $callback ) {
    global $wpdb;

    $wpdb->close();

    if ( ! $child = pcntl_fork() && define( 'CHILD_PROCESS', true ) ) {
        // Reinitialize your own connections (add persistent object caches, logs and whatnot)
        $wpdb->connect( false );

        exit $callback(); // Call the code in a child process
    }

    $wpdb->connect( false );

    return $child;
}

Call test_in_child as needed inside your test case, go on to do work in the parent, while the child is working, then check the data integrity.

For example:

public function test_counter() {
    global $wpdb;
    $wpdb->query( 'SET AUTOCOMMIT = 1;' ); // Shared data has to be committed

    $post = $this->create_post(); // Create a post

    $child = test_in_child( function() use ( $post, $ipc ) {
        foreach ( xrange( 1, 10 ) as $_ ) {
            $pageviews = get_post_meta( $post->ID, 'pageviews', true );
            usleep(200000); // 200ms
            update_post_meta( $post->ID, 'pageviews', $pageviews + 1 );
        }
    } );

    foreach ( xrange( 1, 5 ) as $_ ) {
        usleep(300000); // 300ms
        update_post_meta( $post->ID, 'pageviews', get_post_meta( $post->ID, 'pageviews', true ) + 1 );
    }

    pcntl_wait( $child ); // Wait for the child before moving on.

    $this->assertEquals( 15, get_post_meta( $post->ID, 'pageviews', true ) ); // Check data integrity

    $wpdb->query( 'SET AUTOCOMMIT = 0;' ); // Back to transaction-based tests
}

As expected, since there are large and small pauses between reads and writes the counter will be less than 15. Race conditions in action. Fixing it outside of the scope of this article. But, you know: locks. Let’s hope your bank account balance is not run on a WordPress installation 🙂

It is important to keep in mind that forking is very tricky when it comes to file handles and connections, make sure you know what you’re doing. Other than that happy testing and may the deadlock spare you!