Follow-up to r29036: Now that the "mergeinfo" transaction file is no
[svn.git] / tools / dev / stress.pl
blob5b822edfcab03088af22c0d6bb82c50dbeea4104
1 #!/usr/bin/perl -w
3 # A script that allows some simple testing of Subversion, in
4 # particular concurrent read, write and read-write access by the 'svn'
5 # client. It can also create working copy trees containing a large
6 # number of files and directories. All repository access is via the
7 # 'svnadmin' and 'svn' commands.
9 # This script constructs a repository, and populates it with
10 # files. Then it loops making changes to a subset of the files and
11 # committing the tree. Thus when two, or more, instances are run in
12 # parallel there is concurrent read and write access. Sometimes a
13 # commit will fail due to a commit conflict. This is expected, and is
14 # automatically resolved by updating the working copy.
16 # Each file starts off containing:
17 # A0
18 # 0
19 # A1
20 # 1
21 # A2
22 # .
23 # .
24 # A9
25 # 9
27 # The script runs with an ID in the range 0-9, and when it modifies a
28 # file it modifes the line that starts with its ID. Thus scripts with
29 # different IDs will make changes that can be merged automatically.
31 # The main loop is then:
33 # step 1: modify a random selection of files
35 # step 2: optional sleep or wait for RETURN keypress
37 # step 3: update the working copy automatically merging out-of-date files
39 # step 4: try to commit, if not successful go to step 3 otherwise go to step 1
41 # To allow break-out of potentially infinite loops, the script will
42 # terminate if it detects the presence of a "stop file", the path to
43 # which is specified with the -S option (default ./stop). This allows
44 # the script to be stopped without any danger of interrupting an 'svn'
45 # command, which experiment shows may require Berkeley db_recover to
46 # be used on the repository.
48 # Running the Script
49 # ==================
51 # Use three xterms all with shells on the same directory. In the
52 # first xterm run (note, this will remove anything called repostress
53 # in the current directory)
55 # % stress.pl -c -s1
57 # When the message "Committed revision 1." scrolls pass use the second
58 # xterm to run
60 # % stress.pl -s1
62 # Both xterms will modify, update and commit separate working copies to
63 # the same repository.
65 # Use the third xterm to touch a file 'stop' to cause the scripts to
66 # exit cleanly, i.e. without interrupting an svn command.
68 # To run a third, fourth, etc. instance of the script use -i
70 # % stress.pl -s1 -i2
71 # % stress.pl -s1 -i3
73 # Running several instances at once will cause a *lot* of disk
74 # activity. I have run ten instances simultaneously on a Linux tmpfs
75 # (RAM based) filesystem -- watching ten xterms scroll irregularly
76 # can be quite hypnotic!
78 use strict;
79 use IPC::Open3;
80 use Getopt::Std;
81 use File::Find;
82 use File::Path;
83 use File::Spec::Functions;
84 use Cwd;
86 # The name of this script, for error messages.
87 my $stress = 'stress.pl';
89 # When testing BDB 4.4 and later with DB_RECOVER enabled, the criteria
90 # for a failed update and commit are a bit looser than otherwise.
91 my $dbrecover = undef;
93 # Repository check/create
94 sub init_repo
96 my ( $repo, $create, $no_sync, $fsfs ) = @_;
97 if ( $create )
99 rmtree([$repo]) if -e $repo;
100 my $svnadmin_cmd = "svnadmin create $repo";
101 $svnadmin_cmd .= " --fs-type bdb" if not $fsfs;
102 $svnadmin_cmd .= " --bdb-txn-nosync" if $no_sync;
103 system( $svnadmin_cmd) and die "$stress: $svnadmin_cmd: failed: $?\n";
104 open ( CONF, ">>$repo/conf/svnserve.conf")
105 or die "$stress: open svnserve.conf: $!\n";
106 print CONF "[general]\nanon-access = write\n";
107 close CONF or die "$stress: close svnserve.conf: $!\n";
109 $repo = getcwd . "/$repo" if not file_name_is_absolute $repo;
110 $dbrecover = 1 if -e "$repo/db/__db.register";
111 print "$stress: BDB automatic database recovery enabled\n" if $dbrecover;
112 return $repo;
115 # Check-out a working copy
116 sub check_out
118 my ( $url ) = @_;
119 my $wc_dir = "wcstress.$$";
120 mkdir "$wc_dir", 0755 or die "$stress: mkdir wcstress.$$: $!\n";
121 my $svn_cmd = "svn co $url $wc_dir";
122 system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
123 return $wc_dir;
126 # Print status and update. The update is to do any required merges.
127 sub status_update
129 my ( $wc_dir, $wait_for_key, $disable_status, $resolve_conflicts ) = @_;
130 my $svn_cmd = "svn st -u $wc_dir";
131 if ( not $disable_status ) {
132 print "Status:\n";
133 system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
135 print "Press return to update/commit\n" if $wait_for_key;
136 read STDIN, $wait_for_key, 1 if $wait_for_key;
137 print "Updating:\n";
138 $svn_cmd = "svn up $wc_dir";
140 # Check for conflicts during the update. If any exist, we resolve them.
141 my $pid = open3(\*UPDATE_WRITE, \*UPDATE_READ, \*UPDATE_ERR_READ,
142 $svn_cmd);
143 my @conflicts = ();
144 while ( <UPDATE_READ> )
146 print;
147 s/\r*$//; # [Windows compat] Remove trailing \r's
148 if ( /^C (.*)$/ )
150 push(@conflicts, ($1))
154 # Print any errors.
155 my $acceptable_error = 0;
156 while ( <UPDATE_ERR_READ> )
158 print;
159 if ($dbrecover)
161 s/\r*$//; # [Windows compat] Remove trailing \r's
162 $acceptable_error = 1 if ( /^svn:[ ]
164 bdb:[ ]PANIC
166 DB_RUNRECOVERY
168 /x );
172 # Close up the streams.
173 close UPDATE_ERR_READ or die "$stress: close UPDATE_ERR_READ: $!\n";
174 close UPDATE_WRITE or die "$stress: close UPDATE_WRITE: $!\n";
175 close UPDATE_READ or die "$stress: close UPDATE_READ: $!\n";
177 # Get commit subprocess exit status
178 die "$stress: waitpid: $!\n" if $pid != waitpid $pid, 0;
179 die "$stress: unexpected update fail: exit status: $?\n"
180 unless $? == 0 or ( $? == 256 and $acceptable_error );
182 if ($resolve_conflicts)
184 foreach my $conflict (@conflicts)
186 $svn_cmd = "svn resolved $conflict";
187 system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
192 # Print status, update and commit. The update is to do any required
193 # merges. Returns 0 if the commit succeeds and 1 if it fails due to a
194 # conflict.
195 sub status_update_commit
197 my ( $wc_dir, $wait_for_key, $disable_status, $resolve_conflicts ) = @_;
198 status_update $wc_dir, $wait_for_key, $disable_status, $resolve_conflicts;
199 print "Committing:\n";
200 # Use current time as log message
201 my $now_time = localtime;
202 # [Windows compat] Must use double quotes for the log message.
203 my $svn_cmd = "svn ci $wc_dir -m \"$now_time\"";
205 # Need to handle the commit carefully. It could fail for all sorts
206 # of reasons, but errors that indicate a conflict are "acceptable"
207 # while other errors are not. Thus there is a need to check the
208 # return value and parse the error text.
209 my $pid = open3(\*COMMIT_WRITE, \*COMMIT_READ, \*COMMIT_ERR_READ,
210 $svn_cmd);
211 print while ( <COMMIT_READ> );
213 # Look for acceptable errors, ones we expect to occur due to conflicts
214 my $acceptable_error = 0;
215 while ( <COMMIT_ERR_READ> )
217 print;
218 s/\r*$//; # [Windows compat] Remove trailing \r's
219 $acceptable_error = 1 if ( /^svn:[ ]
221 Out[ ]of[ ]date
223 Conflict[ ]at
225 Baseline[ ]incorrect
227 Your[ ]file[ ]or[ ]directory[ ]
228 \'[^\']+\'
229 [ ]is[ ]probably[ ]out-of-date
231 /x )
232 or ( $dbrecover and ( /^svn:[ ]
234 bdb:[ ]PANIC
236 DB_RUNRECOVERY
238 /x ));
242 close COMMIT_ERR_READ or die "$stress: close COMMIT_ERR_READ: $!\n";
243 close COMMIT_WRITE or die "$stress: close COMMIT_WRITE: $!\n";
244 close COMMIT_READ or die "$stress: close COMMIT_READ: $!\n";
246 # Get commit subprocess exit status
247 die "$stress: waitpid: $!\n" if $pid != waitpid $pid, 0;
248 die "$stress: unexpected commit fail: exit status: $?\n"
249 if ( $? != 0 and $? != 256 ) or ( $? == 256 and $acceptable_error != 1 );
251 return $? == 256 ? 1 : 0;
254 # Get a list of all versioned files in the working copy
256 my @get_list_of_files_helper_array;
257 sub GetListOfFilesHelper
259 $File::Find::prune = 1 if $File::Find::name =~ m[/.svn];
260 return if $File::Find::prune or -d;
261 push @get_list_of_files_helper_array, $File::Find::name;
263 sub GetListOfFiles
265 my ( $wc_dir ) = @_;
266 @get_list_of_files_helper_array = ();
267 find( \&GetListOfFilesHelper, $wc_dir);
268 return @get_list_of_files_helper_array;
272 # Populate a working copy
273 sub populate
275 my ( $dir, $dir_width, $file_width, $depth, $pad, $props ) = @_;
276 return if not $depth--;
278 for my $nfile ( 1..$file_width )
280 my $filename = "$dir/foo$nfile";
281 open( FOO, ">$filename" ) or die "$stress: open $filename: $!\n";
283 for my $line ( 0..9 )
285 print FOO "A$line\n$line\n"
286 or die "$stress: write to $filename: $!\n";
287 map { print FOO $_ x 255, "\n"; } ("a", "b", "c", "d")
288 foreach (1..$pad);
290 print FOO "\$HeadURL: \$\n"
291 or die "$stress: write to $filename: $!\n" if $props;
292 close FOO or die "$stress: close $filename: $!\n";
294 my $svn_cmd = "svn add $filename";
295 system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
297 if ( $props )
299 $svn_cmd = "svn propset svn:eol-style native $filename";
300 system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
302 $svn_cmd = "svn propset svn:keywords HeadURL $filename";
303 system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
307 if ( $depth )
309 for my $ndir ( 1..$dir_width )
311 my $dirname = "$dir/bar$ndir";
312 my $svn_cmd = "svn mkdir $dirname";
313 system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
315 populate( "$dirname", $dir_width, $file_width, $depth, $pad,
316 $props );
321 # Modify a versioned file in the working copy
322 sub ModFile
324 my ( $filename, $mod_number, $id ) = @_;
326 # Read file into memory replacing the line that starts with our ID
327 open( FOO, "<$filename" ) or die "$stress: open $filename: $!\n";
328 my @lines = map { s[(^$id.*)][$1,$mod_number]; $_ } <FOO>;
329 close FOO or die "$stress: close $filename: $!\n";
331 # Write the memory back to the file
332 open( FOO, ">$filename" ) or die "$stress: open $filename: $!\n";
333 print FOO or die "$stress: print $filename: $!\n" foreach @lines;
334 close FOO or die "$stress: close $filename: $!\n";
337 sub ParseCommandLine
339 my %cmd_opts;
340 my $usage = "
341 usage: stress.pl [-cdfhprW] [-i num] [-n num] [-s secs] [-x num] [-D num]
342 [-F num] [-N num] [-P num] [-R path] [-S path] [-U url]
344 where
345 -c cause repository creation
346 -d don't make the status calls
347 -f use --fs-type fsfs during repository creation
348 -h show this help information (other options will be ignored)
349 -i the ID (valid IDs are 0 to 9, default is 0 if -c given, 1 otherwise)
350 -n the number of sets of changes to commit
351 -p add svn:eol-style and svn:keywords properties to the files
352 -r perform update-time conflict resolution
353 -s the sleep delay (-1 wait for key, 0 none)
354 -x the number of files to modify in each commit
355 -D the number of sub-directories per directory in the tree
356 -F the number of files per directory in the tree
357 -N the depth of the tree
358 -P the number of 10K blocks with which to pad the file
359 -R the path to the repository
360 -S the path to the file whose presence stops this script
361 -U the URL to the repository (file:///<-R path> by default)
362 -W use --bdb-txn-nosync during repository creation
365 # defaults
366 $cmd_opts{'D'} = 2; # number of subdirs per dir
367 $cmd_opts{'F'} = 2; # number of files per dir
368 $cmd_opts{'N'} = 2; # depth
369 $cmd_opts{'P'} = 0; # padding blocks
370 $cmd_opts{'R'} = "repostress"; # repository name
371 $cmd_opts{'S'} = "stop"; # path of file to stop the script
372 $cmd_opts{'U'} = "none"; # URL
373 $cmd_opts{'W'} = 0; # create with --bdb-txn-nosync
374 $cmd_opts{'c'} = 0; # create repository
375 $cmd_opts{'d'} = 0; # disable status
376 $cmd_opts{'f'} = 0; # create with --fs-type fsfs
377 $cmd_opts{'h'} = 0; # help
378 $cmd_opts{'i'} = 0; # ID
379 $cmd_opts{'n'} = 200; # sets of changes
380 $cmd_opts{'p'} = 0; # add file properties
381 $cmd_opts{'r'} = 0; # conflict resolution
382 $cmd_opts{'s'} = -1; # sleep interval
383 $cmd_opts{'x'} = 4; # files to modify
385 getopts( 'cdfhi:n:prs:x:D:F:N:P:R:U:W', \%cmd_opts ) or die $usage;
387 # print help info (and exit nicely) if requested
388 if ( $cmd_opts{'h'} )
390 print( $usage );
391 exit 0;
394 # default ID if not set
395 $cmd_opts{'i'} = 1 - $cmd_opts{'c'} if not $cmd_opts{'i'};
396 die $usage if $cmd_opts{'i'} !~ /^[0-9]$/;
398 return %cmd_opts;
401 ############################################################################
402 # Main
404 # Why the fixed seed? I use this script for more than stress testing,
405 # I also use it to create test repositories. When creating a test
406 # repository, while I don't care exactly which files get modified, I
407 # find it useful for the repositories to be reproducible, i.e. to have
408 # the same files modified each time. When using this script for
409 # stress testing one could remove this fixed seed and Perl will
410 # automatically use a pseudo-random seed. However it doesn't much
411 # matter, the stress testing really depends on the real-time timing
412 # differences between mutiple instances of the script, rather than the
413 # randomness of the chosen files.
414 srand 123456789;
416 my %cmd_opts = ParseCommandLine();
418 my $repo = init_repo( $cmd_opts{'R'}, $cmd_opts{'c'}, $cmd_opts{'W'},
419 $cmd_opts{'f'} );
421 # [Windows compat]
422 # Replace backslashes in the path, and tweak the number of slashes
423 # in the scheme separator to make the URL always correct.
424 my $urlsep = ($repo =~ m/^\// ? '//' : '///');
425 $repo =~ s/\\/\//g;
427 # Make URL from path if URL not explicitly specified
428 $cmd_opts{'U'} = "file:$urlsep$repo" if $cmd_opts{'U'} eq "none";
430 my $wc_dir = check_out $cmd_opts{'U'};
432 if ( $cmd_opts{'c'} )
434 my $svn_cmd = "svn mkdir $wc_dir/trunk";
435 system( $svn_cmd ) and die "$stress: $svn_cmd: failed: $?\n";
436 populate( "$wc_dir/trunk", $cmd_opts{'D'}, $cmd_opts{'F'}, $cmd_opts{'N'},
437 $cmd_opts{'P'}, $cmd_opts{'p'} );
438 status_update_commit $wc_dir, 0, 1
439 and die "$stress: populate checkin failed\n";
442 my @wc_files = GetListOfFiles $wc_dir;
443 die "$stress: not enough files in repository\n"
444 if $#wc_files + 1 < $cmd_opts{'x'};
446 my $wait_for_key = $cmd_opts{'s'} < 0;
448 my $stop_file = $cmd_opts{'S'};
450 for my $mod_number ( 1..$cmd_opts{'n'} )
452 my @chosen;
453 for ( 1..$cmd_opts{'x'} )
455 # Extract random file from list and modify it
456 my $mod_file = splice @wc_files, int rand $#wc_files, 1;
457 ModFile $mod_file, $mod_number, $cmd_opts{'i'};
458 push @chosen, $mod_file;
460 # Reinstate list of files, the order doesn't matter
461 push @wc_files, @chosen;
463 if ( $cmd_opts{'x'} > 0 ) {
464 # Loop committing until successful or the stop file is created
465 1 while not -e $stop_file
466 and status_update_commit $wc_dir, $wait_for_key, \
467 $cmd_opts{'d'}, $cmd_opts{'r'};
468 } else {
469 status_update $wc_dir, $wait_for_key, $cmd_opts{'d'}, $cmd_opts{'r'};
472 # Break out of loop, or sleep, if required
473 print( "stop file '$stop_file' detected\n" ), last if -e $stop_file;
474 sleep $cmd_opts{'s'} if $cmd_opts{'s'} > 0;