28a5c5e764a29eef3b52d2e4817d2106f8b34893
2 use lib
(split(/:/, $ENV{GITPERLLIB
}));
8 use Fcntl
qw(:DEFAULT);
17 use lib
$ENV{GITWEBLIBDIR
} || "$ENV{GIT_BUILD_DIR}/gitweb/lib";
20 # Test creating a cache
22 BEGIN { use_ok
('GitwebCache::FileCacheWithLocking'); }
23 diag
("Using lib '$INC[0]'");
24 diag
("Testing '$INC{'GitwebCache/FileCacheWithLocking.pm'}'");
26 my $cache = new_ok
('GitwebCache::FileCacheWithLocking', [ {
27 'max_lifetime' => 0, # turn it off
28 'background_cache' => 0,
30 isa_ok
($cache, 'GitwebCache::SimpleFileCache');
32 # compute can fork, don't generate zombies
33 #local $SIG{CHLD} = 'IGNORE';
35 # Test that default values are defined
37 ok
(defined $GitwebCache::SimpleFileCache
::DEFAULT_CACHE_ROOT
,
38 '$DEFAULT_CACHE_ROOT defined');
39 ok
(defined $GitwebCache::SimpleFileCache
::DEFAULT_CACHE_DEPTH
,
40 '$DEFAULT_CACHE_DEPTH defined');
42 # Test accessors and default values for cache
45 skip
'default values not defined', 3
46 unless ($GitwebCache::SimpleFileCache
::DEFAULT_CACHE_ROOT
&&
47 $GitwebCache::SimpleFileCache
::DEFAULT_CACHE_DEPTH
);
49 is
($cache->get_namespace(), '', "default namespace is ''");
50 cmp_ok
($cache->get_root(), 'eq', $GitwebCache::SimpleFileCache
::DEFAULT_CACHE_ROOT
,
51 "default cache root is '$GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT'");
52 cmp_ok
($cache->get_depth(), '==', $GitwebCache::SimpleFileCache
::DEFAULT_CACHE_DEPTH
,
53 "default cache depth is $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH");
56 # ----------------------------------------------------------------------
59 # Test the getting, setting, and removal of a cached value
60 # (Cache::Cache interface)
63 my $value = 'Test Value';
65 subtest
'Cache::Cache interface' => sub {
66 foreach my $method (qw(get set remove)) {
67 can_ok
($cache, $method);
70 $cache->set($key, $value);
71 cmp_ok
($cache->get_size($key), '>', 0, 'get_size after set, is greater than 0');
72 is
($cache->get($key), $value, 'get after set, returns cached value');
74 ok
(!defined($cache->get($key)), 'get after remove, is undefined');
76 eval { $cache->remove('Not-Existent Key'); };
77 ok
(!$@
, 'remove on non-existent key doesn\'t die');
83 # Test the getting and setting of a cached value
91 subtest
'CHI interface' => sub {
92 can_ok
($cache, qw(compute));
94 is
($cache->compute($key, \
&get_value
), $value, "compute 1st time (set) returns '$value'");
95 is
($cache->compute($key, \
&get_value
), $value, "compute 2nd time (get) returns '$value'");
96 is
($cache->compute($key, \
&get_value
), $value, "compute 3rd time (get) returns '$value'");
97 cmp_ok
($call_count, '==', 1, 'get_value() is called once from compute');
102 # Test the getting and setting of a cached value
103 # (compute_fh interface)
111 subtest
'compute_fh interface' => sub {
112 can_ok
($cache, qw(compute_fh));
114 $cache->remove($key);
115 is
(cache_compute_fh
($cache, $key, \
&write_value
), $value,
116 "compute_fh 1st time (set) returns '$value'");
117 is
(cache_compute_fh
($cache, $key, \
&write_value
), $value,
118 "compute_fh 2nd time (get) returns '$value'");
119 is
(cache_compute_fh
($cache, $key, \
&write_value
), $value,
120 "compute_fh 3rd time (get) returns '$value'");
121 cmp_ok
($call_count, '==', 1, 'write_value() is called once from compute_fh');
126 # Test cache expiration
128 subtest
'cache expiration' => sub {
129 $cache->set_expires_in(60*60*24); # set expire time to 1 day
130 cmp_ok
($cache->get_expires_in(), '>', 0, '"expires in" is greater than 0');
131 is
($cache->get($key), $value, 'get returns cached value (not expired in 1d)');
133 $cache->set_expires_in(-1); # set expire time to never expire
134 is
($cache->get_expires_in(), -1, '"expires in" is set to never (-1)');
135 is
($cache->get($key), $value, 'get returns cached value (not expired)');
137 $cache->set_expires_in(0);
138 is
($cache->get_expires_in(), 0, '"expires in" is set to now (0)');
139 $cache->set($key, $value);
140 ok
(!defined($cache->get($key)), 'cache is expired');
145 # Test assertions for adaptive cache expiration
148 sub load
{ return $load; }
149 my $expires_min = 10;
150 my $expires_max = 30;
151 $cache->set_expires_in(-1);
152 $cache->set_expires_min($expires_min);
153 $cache->set_expires_max($expires_max);
154 $cache->set_check_load(\
&load
);
155 subtest
'adaptive cache expiration' => sub {
156 cmp_ok
($cache->get_expires_min(), '==', $expires_min,
157 '"expires min" set correctly');
158 cmp_ok
($cache->get_expires_max(), '==', $expires_max,
159 '"expires max" set correctly');
162 cmp_ok
($cache->get_expires_in(), '>=', $expires_min,
163 '"expires in" bound from down for load=0');
164 cmp_ok
($cache->get_expires_in(), '<=', $expires_max,
165 '"expires in" bound from up for load=0');
168 cmp_ok
($cache->get_expires_in(), '>=', $expires_min,
169 '"expires in" bound from down for heavy load');
170 cmp_ok
($cache->get_expires_in(), '<=', $expires_max,
171 '"expires in" bound from up for heavy load');
176 $cache->set_expires_in(-1);
178 # ----------------------------------------------------------------------
180 sub parallel_run
(&); # forward declaration of prototype
182 # Test 'stampeding herd' / 'cache miss stampede' problem
185 my $slow_time = 1; # how many seconds to sleep in mockup of slow generation
191 sub get_value_slow_fh
{
201 die "get_value_die\n";
204 my $lock_file = "$0.$$.lock";
205 sub get_value_die_once
{
206 if (sysopen my $fh, $lock_file, (O_WRONLY
| O_CREAT
| O_EXCL
)) {
208 die "get_value_die_once\n";
219 note
("Following tests contain artifical delay of $slow_time seconds");
220 subtest
'parallel access' => sub {
221 $cache->remove($key);
222 @output = parallel_run
{
224 my $data = cache_get_set
($cache, $key, \
&get_value_slow
);
225 print "$data$sep$call_count";
229 my ($child_out, $child_count) = split(quotemeta $sep, $_);
230 #is($child_out, $value, "get/set (parallel) returns '$value'");
231 $total_count += $child_count;
233 cmp_ok
($total_count, '==', 2, 'parallel get/set: get_value_slow() called twice');
235 $cache->remove($key);
236 @output = parallel_run
{
238 my $data = cache_compute
($cache, $key, \
&get_value_slow
);
239 print "$data$sep$call_count";
243 my ($child_out, $child_count) = split(quotemeta $sep, $_);
244 #is($child_out, $value, "compute (parallel) returns '$value'");
245 $total_count += $child_count;
247 cmp_ok
($total_count, '==', 1, 'parallel compute: get_value_slow() called once');
249 $cache->remove($key);
250 @output = parallel_run
{
252 my $data = cache_compute_fh
($cache, $key, \
&get_value_slow_fh
);
253 print "$data$sep$call_count";
257 my ($child_out, $child_count) = split(quotemeta $sep, $_);
258 #is($child_out, $value, "compute_fh (parallel) returns '$value'");
259 $total_count += $child_count;
261 cmp_ok
($total_count, '==', 1, 'parallel compute_fh: get_value_slow_fh() called once');
264 local $SIG{ALRM
} = sub { die "alarm\n"; };
267 @output = parallel_run
{
269 my $data = eval { cache_compute
($cache, 'No Key', \
&get_value_die
); };
271 print "$data" if defined $data;
273 print "$eval_error" if defined $eval_error;
277 [ ( "${sep}get_value_die\n" ) x
2 ],
278 'parallel compute: get_value_die() died in both'
283 ok
(!$@
, 'parallel compute: no alarm call (neither process hung)');
286 $cache->remove($key);
288 @output = parallel_run
{
289 my $data = eval { cache_compute
($cache, $key, \
&get_value_die_once
); };
291 print "$data" if defined $data;
293 print "$eval_error" if defined $eval_error;
297 [sort ("$value$sep", "${sep}get_value_die_once\n")],
298 'parallel compute: return correct value even if other process died'
305 # Test that cache returns stale data in existing but expired cache situation
306 # (probably should be run only if GIT_TEST_LONG)
308 my $stale_value = 'Stale Value';
310 subtest
'serving stale data when (re)generating' => sub {
311 # without background generation
312 $cache->set_background_cache(0);
314 $cache->set($key, $stale_value);
316 $cache->set_expires_in(0); # expire now
317 $cache->set_max_lifetime(-1); # forever (always serve stale data)
319 @output = parallel_run
{
320 my $data = cache_compute
($cache, $key, \
&get_value_slow
);
323 # returning stale data works
326 [sort ($value, $stale_value)],
327 'no background: stale data returned by one process'
330 $cache->set_expires_in(-1); # never expire for next ->get
331 is
($cache->get($key), $value,
332 'no background: value got set correctly, even if stale data returned');
335 # with background generation
336 $cache->set_background_cache(1);
337 $cache->set_generating_info_is_safe(1);
339 $cache->set($key, $stale_value);
340 $cache->set_expires_in(0); # set value is now expired
341 @output = parallel_run
{
342 my $data = cache_compute
($cache, $key, \
&get_value_slow
);
345 # returning stale data works
348 [ ($stale_value) x
2 ],
349 'background: stale data returned by both process when expired'
352 $cache->set_expires_in(-1); # never expire for next ->get
353 note
('waiting for background process to have time to set data');
354 sleep $slow_time; # wait for background process to have chance to set data
355 is
($cache->get($key), $value,
356 'background: value got set correctly by background process');
359 # $cache->set($key, $stale_value);
360 # unlink($lock_file);
361 # @output = parallel_run {
362 # my $data = eval { cache_compute($cache, $key, \&get_value_die_once); };
363 # my $eval_error = $@;
364 # print "$data" if defined $data;
366 # print "$eval_error" if defined $eval_error;
369 # local $TODO = 'not implemented';
373 # [sort ("$value${sep}", "${sep}get_value_die_once\n")],
374 # 'return non-stale value, even if process regenerating it died'
377 # $cache->set_expires_in(-1); # never expire for next ->get
378 # is($cache->get($key), $value,
379 # 'value got regenerated, even if process regenerating it died');
381 # unlink($lock_file);
383 $cache->set($key, $stale_value);
384 $cache->set_expires_in(0); # expire now
385 $cache->set_max_lifetime(0); # don't serve stale data
387 @output = parallel_run
{
388 my $data = cache_compute
($cache, $key, \
&get_value_slow
);
391 # no returning stale data
392 ok
(!scalar(grep { $_ eq $stale_value } @output),
393 'no stale data if configured');
398 $cache->set_expires_in(-1);
401 # Test 'generating_info' feature
403 $cache->remove($key);
404 my $progress_info = "Generating...";
405 sub test_generating_info
{
407 print "$progress_info";
409 $cache->set_generating_info(\
&test_generating_info
);
411 subtest
'generating progress info' => sub {
414 # without background generation
415 $cache->set_background_cache(0);
416 $cache->remove($key);
418 @output = parallel_run
{
419 my $data = cache_compute
($cache, $key, \
&get_value_slow
);
422 @progress = map { s/^(.*)\Q${sep}\E//o && $1 } @output;
425 [sort ('', $progress_info)],
426 'no background: one process waiting for data prints progress info'
431 'no background: both processes return correct value'
435 # without background generation, with stale value
436 $cache->set($key, $stale_value);
437 $cache->set_expires_in(0); # set value is now expired
438 $cache->set_max_lifetime(-1); # stale data never expire
439 @output = parallel_run
{
440 my $data = cache_compute
($cache, $key, \
&get_value_slow
);
445 ## no progress for generating process without background generation;
446 # [sort ("$progress_info$sep$value", "$sep$stale_value")],
447 [sort ("$sep$value", "$sep$stale_value")],
448 'no background, stale data: generating gets data, other gets stale data'
449 ) or diag
('@output is ', join ", ", sort @output);
450 $cache->set_expires_in(-1);
453 # with background generation
454 $cache->set_background_cache(1);
455 $cache->remove($key);
457 @output = parallel_run
{
458 my $data = cache_compute
($cache, $key, \
&get_value_slow
);
461 @progress = map { s/^(.*)\Q${sep}\E//o && $1 } @output;
464 [ ($progress_info) x
2],
465 'background: both process print progress info'
470 'background: both processes return correct value'
476 $cache->set_expires_in(-1);
479 # ----------------------------------------------------------------------
482 # Test 'on_error' handler
485 die "test_handler\n"; # newline needed
488 # prepare error condition
490 $is_prepared &&= $cache->set($key, $value);
491 $is_prepared &&= chmod 0555, dirname
($cache->path_to_key($key));
493 my $ntests = 1; # in subtest
494 skip
"could't prepare error condition for 'on_error' tests", $ntests
496 skip
"cannot test reliably 'on_error' as root (id=$>)", $ntests
499 subtest
'error handler' => sub {
500 my ($result, $error);
502 # check that error handler works
503 $cache->set_on_error(\
&test_handler
);
505 $cache->remove($key);
507 ok
(!defined $result, 'on_error: died on error (via handler)');
508 diag
("result is $result") if defined $result;
509 is
($error, "test_handler\n", 'on_error: test_handler was used');
511 # check that "ignore" works
512 $cache->set_on_error('ignore');
514 $cache->remove($key);
516 ok
(defined $result, 'on_error: error ignored if requested');
519 chmod 0777, dirname
($cache->path_to_key($key));
525 #######################################################################
526 #######################################################################
527 #######################################################################
529 # use ->get($key) and ->set($key, $data) interface
531 my ($cache, $key, $code) = @_;
533 my $data = $cache->get($key);
534 if (!defined $data) {
536 $cache->set($key, $data);
542 # use ->compute($key, $code) interface
544 my ($cache, $key, $code) = @_;
546 my $data = $cache->compute($key, $code);
549 # use ->compute_fh($key, $code_fh) interface
550 sub cache_compute_fh
{
551 my ($cache, $key, $code_fh) = @_;
553 my ($fh, $filename) = $cache->compute_fh($key, $code_fh);
559 # from http://aaroncrane.co.uk/talks/pipes_and_processes/
561 my ($child_process_code) = @_;
564 die "Failed to fork: $!\n" if !defined $pid;
566 return $pid if $pid != 0;
568 # Now we're in the new child process
569 $child_process_code->();
573 sub parallel_run
(&) {
574 my $child_code = shift;
578 my (%pid_for_child, %fd_for_child);
579 my $sel = IO
::Select
->new();
580 foreach my $child_idx (1..$nchildren) {
581 my $pipe = IO
::Pipe
->new()
582 or die "Failed to create pipe: $!\n";
584 my $pid = fork_child
{
586 or die "$$: Child \$pipe->writer(): $!\n";
587 dup2
(fileno($pipe), fileno(STDOUT
))
588 or die "$$: Child $child_idx failed to reopen stdout to pipe: $!\n";
590 or die "$$: Child $child_idx failed to close pipe: $!\n";
592 # From Test-Simple-0.96/t/subtest/fork.t
594 # Force all T::B output into the pipe (redirected to STDOUT),
595 # for the parent builder as well as the current subtest builder.
597 no warnings
'redefine';
598 *Test
::Builder
::output
= sub { *STDOUT
};
599 *Test
::Builder
::failure_output
= sub { *STDOUT
};
600 *Test
::Builder
::todo_output
= sub { *STDOUT
};
609 $pid_for_child{$pid} = $child_idx;
611 or die "Failed to \$pipe->reader(): $!\n";
612 $fd_for_child{$pipe} = $child_idx;
615 $children{$child_idx} = {
622 while (my @ready = $sel->can_read()) {
623 foreach my $fh (@ready) {
625 my $nread = sysread($fh, $buf, 1024);
627 exists $fd_for_child{$fh}
628 or die "Cannot find child for fd: $fh\n";
631 $children{$fd_for_child{$fh}}{'output'} .= $buf;
638 while (%pid_for_child) {
639 my $pid = waitpid -1, 0;
640 warn "Child $pid_for_child{$pid} ($pid) failed with status: $?\n"
642 delete $pid_for_child{$pid};
645 return map { $children{$_}{'output'} } keys %children;