summaryrefslogtreecommitdiff
path: root/tools/dev/stress.pl
blob: 5b76be3931ddf729749e7fec26599cb1f7e813b6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
#!/usr/bin/perl -w
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
# ====================================================================

# A script that allows some simple testing of Subversion, in
# particular concurrent read, write and read-write access by the 'svn'
# client. It can also create working copy trees containing a large
# number of files and directories. All repository access is via the
# 'svnadmin' and 'svn' commands.
#
# This script constructs a repository, and populates it with
# files. Then it loops making changes to a subset of the files and
# committing the tree. Thus when two, or more, instances are run in
# parallel there is concurrent read and write access. Sometimes a
# commit will fail due to a commit conflict. This is expected, and is
# automatically resolved by updating the working copy.
#
# Each file starts off containing:
#    A0
#    0
#    A1
#    1
#    A2
#    .
#    .
#    A9
#    9
#
# The script runs with an ID in the range 0-9, and when it modifies a
# file it modifes the line that starts with its ID. Thus scripts with
# different IDs will make changes that can be merged automatically.
#
# The main loop is then:
#
#   step 1: modify a random selection of files
#
#   step 2: optional sleep or wait for RETURN keypress
#
#   step 3: update the working copy automatically merging out-of-date files
#
#   step 4: try to commit, if not successful go to step 3 otherwise go to step 1
#
# To allow break-out of potentially infinite loops, the script will
# terminate if it detects the presence of a "stop file", the path to
# which is specified with the -S option (default ./stop). This allows
# the script to be stopped without any danger of interrupting an 'svn'
# command, which experiment shows may require Berkeley db_recover to
# be used on the repository.
#
#  Running the Script
#  ==================
#
# Use three xterms all with shells on the same directory.  In the
# first xterm run (note, this will remove anything called repostress
# in the current directory)
#
#         % stress.pl -c -s1
#
# When the message "Committed revision 1." scrolls pass use the second
# xterm to run
#
#         % stress.pl -s1
#
# Both xterms will modify, update and commit separate working copies to
# the same repository.
#
# Use the third xterm to touch a file 'stop' to cause the scripts to
# exit cleanly, i.e. without interrupting an svn command.
#
# To run a third, fourth, etc. instance of the script use -i
#
#         % stress.pl -s1 -i2
#         % stress.pl -s1 -i3
#
# Running several instances at once will cause a *lot* of disk
# activity. I have run ten instances simultaneously on a Linux tmpfs
# (RAM based) filesystem -- watching ten xterms scroll irregularly
# can be quite hypnotic!

use strict;
use IPC::Open3;
use Getopt::Std;
use File::Find;
use File::Path;
use File::Spec::Functions;
use Cwd;

# The name of this script, for error messages.
my $stress = 'stress.pl';

# When testing BDB 4.4 and later with DB_RECOVER enabled, the criteria
# for a failed update and commit are a bit looser than otherwise.
my $dbrecover = undef;

# Repository check/create
sub init_repo
  {
    my ( $repo, $create, $no_sync, $fsfs ) = @_;
    if ( $create )
      {
        rmtree([$repo]) if -e $repo;
        my $svnadmin_cmd = "svnadmin create $repo";
        $svnadmin_cmd .= " --fs-type bdb" if not $fsfs;
        $svnadmin_cmd .= " --bdb-txn-nosync" if $no_sync;
        system( $svnadmin_cmd) and die "$stress: $svnadmin_cmd: failed: $?\n";
        open ( CONF, ">>$repo/conf/svnserve.conf")
          or die "$stress: open svnserve.conf: $!\n";
        print CONF "[general]\nanon-access = write\n";
        close CONF or die "$stress: close svnserve.conf: $!\n";
      }
    $repo = getcwd . "/$repo" if not file_name_is_absolute $repo;
    $dbrecover = 1 if -e "$repo/db/__db.register";
    print "$stress: BDB automatic database recovery enabled\n" if $dbrecover;
    return $repo;
  }

# Check-out a working copy
sub check_out
  {
    my ( $url, $options ) = @_;
    my $wc_dir = "wcstress.$$";
    mkdir "$wc_dir", 0755 or die "$stress: mkdir wcstress.$$: $!\n";
    my $svn_cmd = "svn co $url $wc_dir $options";
    system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
    return $wc_dir;
  }

# Print status and update. The update is to do any required merges.
sub status_update
  {
    my ( $options, $wc_dir, $wait_for_key, $disable_status,
         $resolve_conflicts ) = @_;
    my $svn_cmd = "svn st -u $options $wc_dir";
    if ( not $disable_status ) {
      print "Status:\n";
      system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
    }
    print "Press return to update/commit\n" if $wait_for_key;
    read STDIN, $wait_for_key, 1 if $wait_for_key;
    print "Updating:\n";
    $svn_cmd = "svn up --non-interactive $options $wc_dir";

    # Check for conflicts during the update.  If any exist, we resolve them.
    my $pid = open3(\*UPDATE_WRITE, \*UPDATE_READ, \*UPDATE_ERR_READ,
                    $svn_cmd);
    my @conflicts = ();
    while ( <UPDATE_READ> )
      {
        print;
        s/\r*$//;               # [Windows compat] Remove trailing \r's
        if ( /^C  (.*)$/ )
          {
            push(@conflicts, ($1))
          }
      }

    # Print any errors.
    my $acceptable_error = 0;
    while ( <UPDATE_ERR_READ> )
      {
        print;
        if ($dbrecover)
          {
            s/\r*$//;          # [Windows compat] Remove trailing \r's
            $acceptable_error = 1 if ( /^svn:[ ]
                                       (
                                        bdb:[ ]PANIC
                                        |
                                        DB_RUNRECOVERY
                                       )
                                       /x );
          }
      }

    # Close up the streams.
    close UPDATE_ERR_READ or die "$stress: close UPDATE_ERR_READ: $!\n";
    close UPDATE_WRITE or die "$stress: close UPDATE_WRITE: $!\n";
    close UPDATE_READ or die "$stress: close UPDATE_READ: $!\n";

    # Get commit subprocess exit status
    die "$stress: waitpid: $!\n" if $pid != waitpid $pid, 0;
    die "$stress: unexpected update fail: exit status: $?\n"
      unless $? == 0 or ( $? == 256 and $acceptable_error );

    if ($resolve_conflicts)
      {
        foreach my $conflict (@conflicts)
          {
            $svn_cmd = "svn resolved $conflict";
            system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
          }
      }
  }

# Print status, update and commit. The update is to do any required
# merges.  Returns 0 if the commit succeeds and 1 if it fails due to a
# conflict.
sub status_update_commit
  {
    my ( $options, $wc_dir, $wait_for_key, $disable_status,
         $resolve_conflicts ) = @_;
    status_update $options, $wc_dir, $wait_for_key, $disable_status, \
                  $resolve_conflicts;
    print "Committing:\n";
    # Use current time as log message
    my $now_time = localtime;
    # [Windows compat] Must use double quotes for the log message.
    my $svn_cmd = "svn ci $options $wc_dir -m \"$now_time\"";

    # Need to handle the commit carefully. It could fail for all sorts
    # of reasons, but errors that indicate a conflict are "acceptable"
    # while other errors are not.  Thus there is a need to check the
    # return value and parse the error text.
    my $pid = open3(\*COMMIT_WRITE, \*COMMIT_READ, \*COMMIT_ERR_READ,
                    $svn_cmd);
    print while ( <COMMIT_READ> );

    # Look for acceptable errors, ones we expect to occur due to conflicts
    my $acceptable_error = 0;
    while ( <COMMIT_ERR_READ> )
      {
        print;
        s/\r*$//;               # [Windows compat] Remove trailing \r's
        $acceptable_error = 1 if ( /^svn:[ ]
                                   (
                                    .*out[ ]of[ ]date
                                    |
                                    Conflict[ ]at
                                    |
                                    Baseline[ ]incorrect
                                    |
                                   )
                                   /ix )
            or ( $dbrecover and  ( /^svn:[ ]
                                   (
                                    bdb:[ ]PANIC
                                    |
                                    DB_RUNRECOVERY
                                   )
                                   /x ));


      }
    close COMMIT_ERR_READ or die "$stress: close COMMIT_ERR_READ: $!\n";
    close COMMIT_WRITE or die "$stress: close COMMIT_WRITE: $!\n";
    close COMMIT_READ or die "$stress: close COMMIT_READ: $!\n";

    # Get commit subprocess exit status
    die "$stress: waitpid: $!\n" if $pid != waitpid $pid, 0;
    die "$stress: unexpected commit fail: exit status: $?\n"
      if ( $? != 0 and $? != 256 ) or ( $? == 256 and $acceptable_error != 1 );

    return $? == 256 ? 1 : 0;
  }

# Get a list of all versioned files in the working copy
{
  my @get_list_of_files_helper_array;
  sub GetListOfFilesHelper
    {
      $File::Find::prune = 1 if $File::Find::name =~ m[/.svn];
      return if $File::Find::prune or -d;
      push @get_list_of_files_helper_array, $File::Find::name;
    }
  sub GetListOfFiles
    {
      my ( $wc_dir ) = @_;
      @get_list_of_files_helper_array = ();
      find( \&GetListOfFilesHelper, $wc_dir);
      return @get_list_of_files_helper_array;
    }
}

# Populate a working copy
sub populate
  {
    my ( $dir, $dir_width, $file_width, $depth, $pad, $props ) = @_;
    return if not $depth--;

    for my $nfile ( 1..$file_width )
      {
        my $filename = "$dir/foo$nfile";
        open( FOO, ">$filename" ) or die "$stress: open $filename: $!\n";

        for my $line ( 0..9 )
          {
            print FOO "A$line\n$line\n"
                or die "$stress: write to $filename: $!\n";
            map { print FOO $_ x 255, "\n"; } ("a", "b", "c", "d")
              foreach (1..$pad);
          }
        print FOO "\$HeadURL: \$\n"
            or die "$stress: write to $filename: $!\n" if $props;
        close FOO or die "$stress: close $filename: $!\n";

        my $svn_cmd = "svn add $filename";
        system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";

        if ( $props )
          {
            $svn_cmd = "svn propset svn:eol-style native $filename";
            system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";

            $svn_cmd = "svn propset svn:keywords HeadURL $filename";
            system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
          }
      }

    if ( $depth )
      {
        for my $ndir ( 1..$dir_width )
          {
            my $dirname = "$dir/bar$ndir";
            my $svn_cmd = "svn mkdir $dirname";
            system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";

            populate( "$dirname", $dir_width, $file_width, $depth, $pad,
                      $props );
          }
      }
  }

# Modify a versioned file in the working copy
sub ModFile
  {
    my ( $filename, $mod_number, $id ) = @_;

    # Read file into memory replacing the line that starts with our ID
    open( FOO, "<$filename" ) or die "$stress: open $filename: $!\n";
    my @lines = map { s[(^$id.*)][$1,$mod_number]; $_ } <FOO>;
    close FOO or die "$stress: close $filename: $!\n";

    # Write the memory back to the file
    open( FOO, ">$filename" ) or die "$stress: open $filename: $!\n";
    print FOO or die "$stress: print $filename: $!\n" foreach @lines;
    close FOO or die "$stress: close $filename: $!\n";
  }

sub ParseCommandLine
  {
    my %cmd_opts;
    my $usage = "
usage: stress.pl [-cdfhprW] [-i num] [-n num] [-s secs] [-x num] [-o options]
                 [-D num] [-F num] [-N num] [-P num] [-R path] [-S path]
                 [-U url]

where
  -c cause repository creation
  -d don't make the status calls
  -f use --fs-type fsfs during repository creation
  -h show this help information (other options will be ignored)
  -i the ID (valid IDs are 0 to 9, default is 0 if -c given, 1 otherwise)
  -n the number of sets of changes to commit
  -p add svn:eol-style and svn:keywords properties to the files
  -r perform update-time conflict resolution
  -s the sleep delay (-1 wait for key, 0 none)
  -x the number of files to modify in each commit
  -o options to pass for subversion client
  -D the number of sub-directories per directory in the tree
  -F the number of files per directory in the tree
  -N the depth of the tree
  -P the number of 10K blocks with which to pad the file
  -R the path to the repository
  -S the path to the file whose presence stops this script
  -U the URL to the repository (file:///<-R path> by default)
  -W use --bdb-txn-nosync during repository creation
";

    # defaults
    $cmd_opts{'D'} = 2;            # number of subdirs per dir
    $cmd_opts{'F'} = 2;            # number of files per dir
    $cmd_opts{'N'} = 2;            # depth
    $cmd_opts{'P'} = 0;            # padding blocks
    $cmd_opts{'R'} = "repostress"; # repository name
    $cmd_opts{'S'} = "stop";       # path of file to stop the script
    $cmd_opts{'U'} = "none";       # URL
    $cmd_opts{'W'} = 0;            # create with --bdb-txn-nosync
    $cmd_opts{'c'} = 0;            # create repository
    $cmd_opts{'d'} = 0;            # disable status
    $cmd_opts{'f'} = 0;            # create with --fs-type fsfs
    $cmd_opts{'h'} = 0;            # help
    $cmd_opts{'i'} = 0;            # ID
    $cmd_opts{'n'} = 200;          # sets of changes
    $cmd_opts{'p'} = 0;            # add file properties
    $cmd_opts{'r'} = 0;            # conflict resolution
    $cmd_opts{'s'} = -1;           # sleep interval
    $cmd_opts{'x'} = 4;            # files to modify
    $cmd_opts{'o'} = "";           # no options passed

    getopts( 'cdfhi:n:prs:x:o:D:F:N:P:R:S:U:W', \%cmd_opts ) or die $usage;

    # print help info (and exit nicely) if requested
    if ( $cmd_opts{'h'} )
      {
        print( $usage );
        exit 0;
      }

    # default ID if not set
    $cmd_opts{'i'} = 1 - $cmd_opts{'c'} if not $cmd_opts{'i'};
    die $usage if $cmd_opts{'i'} !~ /^[0-9]$/;

    return %cmd_opts;
  }

############################################################################
# Main

# Why the fixed seed?  I use this script for more than stress testing,
# I also use it to create test repositories.  When creating a test
# repository, while I don't care exactly which files get modified, I
# find it useful for the repositories to be reproducible, i.e. to have
# the same files modified each time.  When using this script for
# stress testing one could remove this fixed seed and Perl will
# automatically use a pseudo-random seed.  However it doesn't much
# matter, the stress testing really depends on the real-time timing
# differences between mutiple instances of the script, rather than the
# randomness of the chosen files.
srand 123456789;

my %cmd_opts = ParseCommandLine();

my $repo = init_repo( $cmd_opts{'R'}, $cmd_opts{'c'}, $cmd_opts{'W'},
                      $cmd_opts{'f'} );

# [Windows compat]
# Replace backslashes in the path, and tweak the number of slashes
# in the scheme separator to make the URL always correct.
my $urlsep = ($repo =~ m/^\// ? '//' : '///');
$repo =~ s/\\/\//g;

# Make URL from path if URL not explicitly specified
$cmd_opts{'U'} = "file:$urlsep$repo" if $cmd_opts{'U'} eq "none";

my $wc_dir = check_out $cmd_opts{'U'}, $cmd_opts{'o'};

if ( $cmd_opts{'c'} )
  {
    my $svn_cmd = "svn mkdir $wc_dir/trunk";
    system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
    populate( "$wc_dir/trunk", $cmd_opts{'D'}, $cmd_opts{'F'}, $cmd_opts{'N'},
              $cmd_opts{'P'}, $cmd_opts{'p'} );
    status_update_commit $cmd_opts{'o'}, $wc_dir, 0, 1
        and die "$stress: populate checkin failed\n";
  }

my @wc_files = GetListOfFiles $wc_dir;
die "$stress: not enough files in repository\n"
    if $#wc_files + 1 < $cmd_opts{'x'};

my $wait_for_key = $cmd_opts{'s'} < 0;

my $stop_file = $cmd_opts{'S'};

for my $mod_number ( 1..$cmd_opts{'n'} )
  {
    my @chosen;
    for ( 1..$cmd_opts{'x'} )
      {
        # Extract random file from list and modify it
        my $mod_file = splice @wc_files, int rand $#wc_files, 1;
        ModFile $mod_file, $mod_number, $cmd_opts{'i'};
        push @chosen, $mod_file;
      }
    # Reinstate list of files, the order doesn't matter
    push @wc_files, @chosen;

    if ( $cmd_opts{'x'} > 0 ) {
      # Loop committing until successful or the stop file is created
      1 while not -e $stop_file
        and status_update_commit $cmd_opts{'o'}, $wc_dir, $wait_for_key, \
                                 $cmd_opts{'d'}, $cmd_opts{'r'};
    } else {
      status_update $cmd_opts{'o'}, $wc_dir, $wait_for_key, $cmd_opts{'d'}, \
                    $cmd_opts{'r'};
    }

    # Break out of loop, or sleep, if required
    print( "stop file '$stop_file' detected\n" ), last if -e $stop_file;
    sleep $cmd_opts{'s'} if $cmd_opts{'s'} > 0;
  }