[Rt-commit] rt branch, 4.0/parallel-test-race, created. rt-4.0.1rc1-8-ge3d23b8

Alex Vandiver alexmv at bestpractical.com
Tue Jun 7 18:36:43 EDT 2011


The branch, 4.0/parallel-test-race has been created
        at  e3d23b8cd0be091f9e27909ee89a70bf5d2a9142 (commit)

- Log -----------------------------------------------------------------
commit e3d23b8cd0be091f9e27909ee89a70bf5d2a9142
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Jun 7 15:45:23 2011 -0400

    Use a locked file to eliminate race conditions when picking a port and db name
    
    The previous method of picking a random port and testing if it was
    unused failed under high concurrency, as there is a large gap between
    when the port is checked for availability and when the server is started
    to claim it.  This is particularly important because (in parallel
    testing) the database name is based on the port, so failures show up
    even if there is no server started in the test, as the computed database
    names conflict.
    
    Thus, provide a flock()-protected file which provides a list of ports
    which we know to have claimed, as _well_ as checking the port for being
    already bound.  This provides a stronger requirement for the race
    condition to be met.

diff --git a/lib/RT/Test.pm b/lib/RT/Test.pm
index b468aae..e5d18bd 100644
--- a/lib/RT/Test.pm
+++ b/lib/RT/Test.pm
@@ -60,8 +60,6 @@ use File::Path qw(mkpath);
 use File::Spec;
 
 our @EXPORT = qw(is_empty diag parse_mail works fails);
-our ($port, $dbname);
-our @SERVERS;
 
 my %tmp = (
     directory => undef,
@@ -93,26 +91,8 @@ problem in Perl that hides the top-level optree from L<Devel::Cover>.
 
 =cut
 
-sub generate_port {
-    my $self = shift;
-    my $port = 1024 + int rand(10000) + $$ % 1024;
-
-    my $paddr = sockaddr_in( $port, inet_aton('localhost') );
-    socket( SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp') )
-      or die "socket: $!";
-    if ( connect( SOCK, $paddr ) ) {
-        close(SOCK);
-        return generate_port();
-    }
-    close(SOCK);
-
-    return $port;
-}
-
-BEGIN {
-    $port   = generate_port();
-    $dbname = $ENV{RT_TEST_PARALLEL}? "rt4test_$port" : "rt4test";
-};
+our $port;
+our @SERVERS;
 
 sub import {
     my $class = shift;
@@ -137,6 +117,8 @@ sub import {
 
     $class->bootstrap_tempdir;
 
+    $class->bootstrap_port;
+
     $class->bootstrap_plugins_paths( %args );
 
     $class->bootstrap_config( %args );
@@ -208,6 +190,49 @@ sub db_requires_no_dba {
     return 1 if $db_type eq 'SQLite';
 }
 
+sub bootstrap_port {
+    my $class = shift;
+
+    my %ports;
+
+    # Determine which ports are in use
+    use Fcntl qw(:DEFAULT :flock);
+    sysopen(PORTS, "t/tmp/ports", O_RDWR|O_CREAT)
+        or die "Can't write to ports file: $!";
+    flock(PORTS, LOCK_EX)
+        or die "Can't write-lock ports file: $!";
+    $ports{$_}++ for split ' ', join("",<PORTS>);
+
+    # Pick a random port, checking that the port isn't in our in-use
+    # list, and that something isn't already listening there.
+    {
+        $port = 1024 + int rand(10_000) + $$ % 1024;
+        redo if $ports{$port};
+
+        # There is a race condition in here, where some non-RT::Test
+        # process claims the port after we check here but before our
+        # server binds.  However, since we mostly care about race
+        # conditions with ourselves under high concurrency, this is
+        # generally good enough.
+        my $paddr = sockaddr_in( $port, inet_aton('localhost') );
+        socket( SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp') )
+            or die "socket: $!";
+        if ( connect( SOCK, $paddr ) ) {
+            close(SOCK);
+            redo;
+        }
+        close(SOCK);
+    }
+
+    $ports{$port}++;
+
+    # Write back out the in-use ports
+    seek(PORTS, 0, 0);
+    truncate(PORTS, 0);
+    print PORTS "$_\n" for sort {$a <=> $b} keys %ports;
+    close(PORTS) or die "Can't close ports file: $!";
+}
+
 sub bootstrap_tempdir {
     my $self = shift;
     my $test_file = (
@@ -231,6 +256,7 @@ sub bootstrap_config {
     open( my $config, '>', $tmp{'config'}{'RT'} )
         or die "Couldn't open $tmp{'config'}{'RT'}: $!";
 
+    my $dbname = $ENV{RT_TEST_PARALLEL}? "rt4test_$port" : "rt4test";
     print $config qq{
 Set( \$WebDomain, "localhost");
 Set( \$WebPort,   $port);
@@ -1446,6 +1472,22 @@ END {
     if ( $ENV{RT_TEST_PARALLEL} && $created_new_db ) {
         __drop_database();
     }
+
+    # Drop our port from t/tmp/ports; do this after dropping the
+    # database, as our port lock is also a lock on the database name.
+    if ($port) {
+        my %ports;
+        sysopen(PORTS, "t/tmp/ports", O_RDWR|O_CREAT)
+            or die "Can't write to ports file: $!";
+        flock(PORTS, LOCK_EX)
+            or die "Can't write-lock ports file: $!";
+        $ports{$_}++ for split ' ', join("",<PORTS>);
+        delete $ports{$port};
+        seek(PORTS, 0, 0);
+        truncate(PORTS, 0);
+        print PORTS "$_\n" for sort {$a <=> $b} keys %ports;
+        close(PORTS) or die "Can't close ports file: $!";
+    }
 }
 
 { 
diff --git a/t/fts/indexed_mysql.t b/t/fts/indexed_mysql.t
index f0395a5..8966f1c 100644
--- a/t/fts/indexed_mysql.t
+++ b/t/fts/indexed_mysql.t
@@ -25,7 +25,9 @@ ok $q && $q->id, 'loaded or created queue';
 my $queue = $q->Name;
 
 sub setup_indexing {
-    my $port = RT::Test->generate_port;
+    # Since we're not running a webserver in this test, use the
+    # known-safe port we determined at test setup
+    my $port = $RT::Test::port;
     my ($exit_code, $output) = RT::Test->run_and_capture(
         'no-ask'       => 1,
         command        => $RT::SbinPath .'/rt-setup-fulltext-index',

-----------------------------------------------------------------------


More information about the Rt-commit mailing list