gitweb: Show appropriate "Generating..." page when regenerating cache
[git/jnareb-git.git] / t / t9503 / test_cache_interface.pl
blob81f0b76527ff595b716cc5714ab5442eb381c7df
1 #!/usr/bin/perl
2 use lib (split(/:/, $ENV{GITPERLLIB}));
4 use warnings;
5 use strict;
7 use POSIX qw(dup2);
8 use Fcntl qw(:DEFAULT);
9 use IO::Handle;
10 use IO::Select;
11 use IO::Pipe;
13 use Test::More;
15 # test source version
16 use lib $ENV{GITWEBLIBDIR} || "$ENV{GIT_BUILD_DIR}/gitweb/lib";
19 # Test creating a cache
21 BEGIN { use_ok('GitwebCache::FileCacheWithLocking'); }
22 diag("Using lib '$INC[0]'");
23 diag("Testing '$INC{'GitwebCache/FileCacheWithLocking.pm'}'");
25 my $cache = new_ok('GitwebCache::FileCacheWithLocking', [ {
26 'max_lifetime' => 0, # turn it off
27 'background_cache' => 0,
28 } ]);
29 isa_ok($cache, 'GitwebCache::SimpleFileCache');
31 # compute can fork, don't generate zombies
32 #local $SIG{CHLD} = 'IGNORE';
34 # Test that default values are defined
36 ok(defined $GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT,
37 '$DEFAULT_CACHE_ROOT defined');
38 ok(defined $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH,
39 '$DEFAULT_CACHE_DEPTH defined');
41 # Test accessors and default values for cache
43 SKIP: {
44 skip 'default values not defined', 3
45 unless ($GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT &&
46 $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH);
48 is($cache->get_namespace(), '', "default namespace is ''");
49 cmp_ok($cache->get_root(), 'eq', $GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT,
50 "default cache root is '$GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT'");
51 cmp_ok($cache->get_depth(), '==', $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH,
52 "default cache depth is $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH");
55 # ----------------------------------------------------------------------
56 # CACHE API
58 # Test the getting, setting, and removal of a cached value
59 # (Cache::Cache interface)
61 my $key = 'Test Key';
62 my $value = 'Test Value';
64 subtest 'Cache::Cache interface' => sub {
65 foreach my $method (qw(get set remove)) {
66 can_ok($cache, $method);
69 $cache->set($key, $value);
70 cmp_ok($cache->get_size($key), '>', 0, 'get_size after set, is greater than 0');
71 is($cache->get($key), $value, 'get after set, returns cached value');
72 $cache->remove($key);
73 ok(!defined($cache->get($key)), 'get after remove, is undefined');
75 eval { $cache->remove('Not-Existent Key'); };
76 ok(!$@, 'remove on non-existent key doesn\'t die');
77 diag($@) if $@;
79 done_testing();
82 # Test the getting and setting of a cached value
83 # (CHI interface)
85 my $call_count = 0;
86 sub get_value {
87 $call_count++;
88 return $value;
90 subtest 'CHI interface' => sub {
91 can_ok($cache, qw(compute));
93 is($cache->compute($key, \&get_value), $value, "compute 1st time (set) returns '$value'");
94 is($cache->compute($key, \&get_value), $value, "compute 2nd time (get) returns '$value'");
95 is($cache->compute($key, \&get_value), $value, "compute 3rd time (get) returns '$value'");
96 cmp_ok($call_count, '==', 1, 'get_value() is called once from compute');
98 done_testing();
101 # Test the getting and setting of a cached value
102 # (compute_fh interface)
104 $call_count = 0;
105 sub write_value {
106 my $fh = shift;
107 $call_count++;
108 print {$fh} $value;
110 subtest 'compute_fh interface' => sub {
111 can_ok($cache, qw(compute_fh));
113 $cache->remove($key);
114 is(cache_compute_fh($cache, $key, \&write_value), $value,
115 "compute_fh 1st time (set) returns '$value'");
116 is(cache_compute_fh($cache, $key, \&write_value), $value,
117 "compute_fh 2nd time (get) returns '$value'");
118 is(cache_compute_fh($cache, $key, \&write_value), $value,
119 "compute_fh 3rd time (get) returns '$value'");
120 cmp_ok($call_count, '==', 1, 'write_value() is called once from compute_fh');
122 done_testing();
125 # Test cache expiration
127 subtest 'cache expiration' => sub {
128 $cache->set_expires_in(60*60*24); # set expire time to 1 day
129 cmp_ok($cache->get_expires_in(), '>', 0, '"expires in" is greater than 0');
130 is($cache->get($key), $value, 'get returns cached value (not expired in 1d)');
132 $cache->set_expires_in(-1); # set expire time to never expire
133 is($cache->get_expires_in(), -1, '"expires in" is set to never (-1)');
134 is($cache->get($key), $value, 'get returns cached value (not expired)');
136 $cache->set_expires_in(0);
137 is($cache->get_expires_in(), 0, '"expires in" is set to now (0)');
138 $cache->set($key, $value);
139 ok(!defined($cache->get($key)), 'cache is expired');
141 done_testing();
144 # Test assertions for adaptive cache expiration
146 my $load = 0.0;
147 sub load { return $load; }
148 my $expires_min = 10;
149 my $expires_max = 30;
150 $cache->set_expires_in(-1);
151 $cache->set_expires_min($expires_min);
152 $cache->set_expires_max($expires_max);
153 $cache->set_check_load(\&load);
154 subtest 'adaptive cache expiration' => sub {
155 cmp_ok($cache->get_expires_min(), '==', $expires_min,
156 '"expires min" set correctly');
157 cmp_ok($cache->get_expires_max(), '==', $expires_max,
158 '"expires max" set correctly');
160 $load = 0.0;
161 cmp_ok($cache->get_expires_in(), '>=', $expires_min,
162 '"expires in" bound from down for load=0');
163 cmp_ok($cache->get_expires_in(), '<=', $expires_max,
164 '"expires in" bound from up for load=0');
166 $load = 1_000;
167 cmp_ok($cache->get_expires_in(), '>=', $expires_min,
168 '"expires in" bound from down for heavy load');
169 cmp_ok($cache->get_expires_in(), '<=', $expires_max,
170 '"expires in" bound from up for heavy load');
172 done_testing();
175 $cache->set_expires_in(-1);
177 # ----------------------------------------------------------------------
178 # CONCURRENT ACCESS
179 sub parallel_run (&); # forward declaration of prototype
181 # Test 'stampeding herd' / 'cache miss stampede' problem
184 my $slow_time = 1; # how many seconds to sleep in mockup of slow generation
185 sub get_value_slow {
186 $call_count++;
187 sleep $slow_time;
188 return $value;
190 sub get_value_slow_fh {
191 my $fh = shift;
193 $call_count++;
194 sleep $slow_time;
195 print {$fh} $value;
198 sub get_value_die {
199 $call_count++;
200 die "get_value_die\n";
203 my $lock_file = "$0.$$.lock";
204 sub get_value_die_once {
205 if (sysopen my $fh, $lock_file, (O_WRONLY | O_CREAT | O_EXCL)) {
206 close $fh;
207 die "get_value_die_once\n";
208 } else {
209 sleep $slow_time;
210 return $value;
214 my @output;
215 my $sep = '|';
216 my $total_count = 0;
218 note("Following tests contain artifical delay of $slow_time seconds");
219 subtest 'parallel access' => sub {
220 $cache->remove($key);
221 @output = parallel_run {
222 $call_count = 0;
223 my $data = cache_get_set($cache, $key, \&get_value_slow);
224 print "$data$sep$call_count";
226 $total_count = 0;
227 foreach (@output) {
228 my ($child_out, $child_count) = split(quotemeta $sep, $_);
229 #is($child_out, $value, "get/set (parallel) returns '$value'");
230 $total_count += $child_count;
232 cmp_ok($total_count, '==', 2, 'parallel get/set: get_value_slow() called twice');
234 $cache->remove($key);
235 @output = parallel_run {
236 $call_count = 0;
237 my $data = cache_compute($cache, $key, \&get_value_slow);
238 print "$data$sep$call_count";
240 $total_count = 0;
241 foreach (@output) {
242 my ($child_out, $child_count) = split(quotemeta $sep, $_);
243 #is($child_out, $value, "compute (parallel) returns '$value'");
244 $total_count += $child_count;
246 cmp_ok($total_count, '==', 1, 'parallel compute: get_value_slow() called once');
248 $cache->remove($key);
249 @output = parallel_run {
250 $call_count = 0;
251 my $data = cache_compute_fh($cache, $key, \&get_value_slow_fh);
252 print "$data$sep$call_count";
254 $total_count = 0;
255 foreach (@output) {
256 my ($child_out, $child_count) = split(quotemeta $sep, $_);
257 #is($child_out, $value, "compute_fh (parallel) returns '$value'");
258 $total_count += $child_count;
260 cmp_ok($total_count, '==', 1, 'parallel compute_fh: get_value_slow_fh() called once');
262 eval {
263 local $SIG{ALRM} = sub { die "alarm\n"; };
264 alarm 4*$slow_time;
266 @output = parallel_run {
267 $call_count = 0;
268 my $data = eval { cache_compute($cache, 'No Key', \&get_value_die); };
269 my $eval_error = $@;
270 print "$data" if defined $data;
271 print "$sep";
272 print "$eval_error" if defined $eval_error;
274 is_deeply(
275 \@output,
276 [ ( "${sep}get_value_die\n" ) x 2 ],
277 'parallel compute: get_value_die() died in both'
280 alarm 0;
282 ok(!$@, 'parallel compute: no alarm call (neither process hung)');
283 diag($@) if $@;
285 $cache->remove($key);
286 unlink($lock_file);
287 @output = parallel_run {
288 my $data = eval { cache_compute($cache, $key, \&get_value_die_once); };
289 my $eval_error = $@;
290 print "$data" if defined $data;
291 print "$sep";
292 print "$eval_error" if defined $eval_error;
294 is_deeply(
295 [sort @output],
296 [sort ("$value$sep", "${sep}get_value_die_once\n")],
297 'parallel compute: return correct value even if other process died'
299 unlink($lock_file);
301 done_testing();
304 # Test that cache returns stale data in existing but expired cache situation
305 # (probably should be run only if GIT_TEST_LONG)
307 my $stale_value = 'Stale Value';
309 subtest 'serving stale data when (re)generating' => sub {
310 # without background generation
311 $cache->set_background_cache(0);
313 $cache->set($key, $stale_value);
314 $call_count = 0;
315 $cache->set_expires_in(0); # expire now
316 $cache->set_max_lifetime(-1); # forever (always serve stale data)
318 @output = parallel_run {
319 my $data = cache_compute($cache, $key, \&get_value_slow);
320 print $data;
322 # returning stale data works
323 is_deeply(
324 [sort @output],
325 [sort ($value, $stale_value)],
326 'no background: stale data returned by one process'
329 $cache->set_expires_in(-1); # never expire for next ->get
330 is($cache->get($key), $value,
331 'no background: value got set correctly, even if stale data returned');
334 # with background generation
335 $cache->set_background_cache(1);
337 $cache->set($key, $stale_value);
338 $cache->set_expires_in(0); # set value is now expired
339 @output = parallel_run {
340 my $data = cache_compute($cache, $key, \&get_value_slow);
341 print $data;
343 # returning stale data works
344 is_deeply(
345 \@output,
346 [ ($stale_value) x 2 ],
347 'background: stale data returned by both process when expired'
350 $cache->set_expires_in(-1); # never expire for next ->get
351 note('waiting for background process to have time to set data');
352 sleep $slow_time; # wait for background process to have chance to set data
353 is($cache->get($key), $value,
354 'background: value got set correctly by background process');
357 # $cache->set($key, $stale_value);
358 # unlink($lock_file);
359 # @output = parallel_run {
360 # my $data = eval { cache_compute($cache, $key, \&get_value_die_once); };
361 # my $eval_error = $@;
362 # print "$data" if defined $data;
363 # print "$sep";
364 # print "$eval_error" if defined $eval_error;
365 # };
366 # TODO: {
367 # local $TODO = 'not implemented';
369 # is_deeply(
370 # [sort @output],
371 # [sort ("$value${sep}", "${sep}get_value_die_once\n")],
372 # 'return non-stale value, even if process regenerating it died'
373 # );
375 # $cache->set_expires_in(-1); # never expire for next ->get
376 # is($cache->get($key), $value,
377 # 'value got regenerated, even if process regenerating it died');
378 # };
379 # unlink($lock_file);
381 $cache->set($key, $stale_value);
382 $cache->set_expires_in(0); # expire now
383 $cache->set_max_lifetime(0); # don't serve stale data
385 @output = parallel_run {
386 my $data = cache_compute($cache, $key, \&get_value_slow);
387 print $data;
389 # no returning stale data
390 ok(!scalar(grep { $_ eq $stale_value } @output),
391 'no stale data if configured');
394 done_testing();
396 $cache->set_expires_in(-1);
399 # Test 'generating_info' feature
401 $cache->remove($key);
402 my $progress_info = "Generating...";
403 sub test_generating_info {
404 local $| = 1;
405 print "$progress_info";
407 $cache->set_generating_info(\&test_generating_info);
409 subtest 'generating progress info' => sub {
410 my @progress;
412 # without background generation
413 $cache->set_background_cache(0);
414 $cache->remove($key);
416 @output = parallel_run {
417 my $data = cache_compute($cache, $key, \&get_value_slow);
418 print "$sep$data";
420 @progress = map { s/^(.*)\Q${sep}\E//o && $1 } @output;
421 is_deeply(
422 [sort @progress],
423 [sort ('', $progress_info)],
424 'no background: one process waiting for data prints progress info'
426 is_deeply(
427 \@output,
428 [ ($value) x 2 ],
429 'no background: both processes return correct value'
433 # without background generation, with stale value
434 $cache->set($key, $stale_value);
435 $cache->set_expires_in(0); # set value is now expired
436 $cache->set_max_lifetime(-1); # stale data never expire
437 @output = parallel_run {
438 my $data = cache_compute($cache, $key, \&get_value_slow);
439 print "$sep$data";
441 is_deeply(
442 [sort @output],
443 ## no progress for generating process without background generation;
444 # [sort ("$progress_info$sep$value", "$sep$stale_value")],
445 [sort ("$sep$value", "$sep$stale_value")],
446 'no background, stale data: generating gets data, other gets stale data'
447 ) or diag('@output is ', join ", ", sort @output);
448 $cache->set_expires_in(-1);
451 # with background generation
452 $cache->set_background_cache(1);
453 $cache->remove($key);
455 @output = parallel_run {
456 my $data = cache_compute($cache, $key, \&get_value_slow);
457 print "$sep$data";
459 @progress = map { s/^(.*)\Q${sep}\E//o && $1 } @output;
460 is_deeply(
461 \@progress,
462 [ ($progress_info) x 2],
463 'background: both process print progress info'
465 is_deeply(
466 \@output,
467 [ ($value) x 2 ],
468 'background: both processes return correct value'
472 done_testing();
474 $cache->set_expires_in(-1);
477 done_testing();
480 #######################################################################
481 #######################################################################
482 #######################################################################
484 # use ->get($key) and ->set($key, $data) interface
485 sub cache_get_set {
486 my ($cache, $key, $code) = @_;
488 my $data = $cache->get($key);
489 if (!defined $data) {
490 $data = $code->();
491 $cache->set($key, $data);
494 return $data;
497 # use ->compute($key, $code) interface
498 sub cache_compute {
499 my ($cache, $key, $code) = @_;
501 my $data = $cache->compute($key, $code);
502 return $data;
504 # use ->compute_fh($key, $code_fh) interface
505 sub cache_compute_fh {
506 my ($cache, $key, $code_fh) = @_;
508 my ($fh, $filename) = $cache->compute_fh($key, $code_fh);
510 local $/ = undef;
511 return <$fh>;
514 # from http://aaroncrane.co.uk/talks/pipes_and_processes/
515 sub fork_child (&) {
516 my ($child_process_code) = @_;
518 my $pid = fork();
519 die "Failed to fork: $!\n" if !defined $pid;
521 return $pid if $pid != 0;
523 # Now we're in the new child process
524 $child_process_code->();
525 exit;
528 sub parallel_run (&) {
529 my $child_code = shift;
530 my $nchildren = 2;
532 my %children;
533 my (%pid_for_child, %fd_for_child);
534 my $sel = IO::Select->new();
535 foreach my $child_idx (1..$nchildren) {
536 my $pipe = IO::Pipe->new()
537 or die "Failed to create pipe: $!\n";
539 my $pid = fork_child {
540 $pipe->writer()
541 or die "$$: Child \$pipe->writer(): $!\n";
542 dup2(fileno($pipe), fileno(STDOUT))
543 or die "$$: Child $child_idx failed to reopen stdout to pipe: $!\n";
544 close $pipe
545 or die "$$: Child $child_idx failed to close pipe: $!\n";
547 # From Test-Simple-0.96/t/subtest/fork.t
549 # Force all T::B output into the pipe (redirected to STDOUT),
550 # for the parent builder as well as the current subtest builder.
552 no warnings 'redefine';
553 *Test::Builder::output = sub { *STDOUT };
554 *Test::Builder::failure_output = sub { *STDOUT };
555 *Test::Builder::todo_output = sub { *STDOUT };
558 $child_code->();
560 *STDOUT->flush();
561 close(STDOUT);
564 $pid_for_child{$pid} = $child_idx;
565 $pipe->reader()
566 or die "Failed to \$pipe->reader(): $!\n";
567 $fd_for_child{$pipe} = $child_idx;
568 $sel->add($pipe);
570 $children{$child_idx} = {
571 'pid' => $pid,
572 'stdout' => $pipe,
573 'output' => '',
577 while (my @ready = $sel->can_read()) {
578 foreach my $fh (@ready) {
579 my $buf = '';
580 my $nread = sysread($fh, $buf, 1024);
582 exists $fd_for_child{$fh}
583 or die "Cannot find child for fd: $fh\n";
585 if ($nread > 0) {
586 $children{$fd_for_child{$fh}}{'output'} .= $buf;
587 } else {
588 $sel->remove($fh);
593 while (%pid_for_child) {
594 my $pid = waitpid -1, 0;
595 warn "Child $pid_for_child{$pid} ($pid) failed with status: $?\n"
596 if $? != 0;
597 delete $pid_for_child{$pid};
600 return map { $children{$_}{'output'} } keys %children;
603 __END__