{"thread":{"id":"26127","subject":"[RFC PATCH v7 0/9] gitweb: Output caching, with eval/die based error handling","startedAt":"2010-12-22T23:54:32Z","lastAt":"2011-01-05T02:26:59Z","messageCount":34,"participants":["Jakub Narebski","Jonathan Nieder","Junio C Hamano","Jeff King","J.H."],"isPatch":true,"patchVersion":7,"patchTotal":9},"messages":[{"id":"158502","messageId":"20101222234843.7998.87068.stgit@localhost.localdomain","threadId":"26127","inReplyTo":null,"subject":"[RFC PATCH v7 0/9] gitweb: Output caching, with eval/die based error handling","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:54:32Z","receivedAt":"2010-12-22T23:54:32Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This is preliminary (proof of concept) version of shortened series\nintended as replacement (rewrite) of \"Gitweb caching v8\" series from\nJohn 'Warthog9' Hawley (J.H.).\n\nThis series shows how one can manage exception handling using\ndie_error like die, even in the presence of output caching.  The\noutput caching engine has an option that allows to turn off (default)\nor on caching of error pages.\n\nThis series is unfinished; it does not include adaptive cache\nlifetime, nor support for other caching engines than the one provided\n(like Cache::Cache or CHI), nor does it support background cache\ngeneration or progress info indicator.\n\nThis is just intended as proof of concept.\n\n---\nJakub Narebski (9):\n      gitweb: Add optional output caching\n      gitweb/lib - Cache captured output (using compute_fh)\n      gitweb/lib - Very simple file based cache\n      gitweb/lib - Simple output capture by redirecting STDOUT to file\n      t/test-lib.sh: Export also GIT_BUILD_DIR in test_external\n      gitweb: Prepare for splitting gitweb\n      gitweb: Introduce %actions_info, gathering information about actions\n      gitweb: use eval + die for error (exception) handling\n      gitweb: Go to DONE_REQUEST rather than DONE_GITWEB in die_error\n\n gitweb/Makefile                                |   22 +\n gitweb/README                                  |   46 ++\n gitweb/gitweb.perl                             |  280 +++++++++++++--\n gitweb/lib/GitwebCache/CacheOutput.pm          |   84 ++++\n gitweb/lib/GitwebCache/Capture/ToFile.pm       |  109 ++++++\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |  452 ++++++++++++++++++++++++\n t/gitweb-lib.sh                                |   11 +\n t/t9500-gitweb-standalone-no-errors.sh         |   20 +\n t/t9501-gitweb-standalone-http-status.sh       |   13 +\n t/t9502-gitweb-standalone-parse-output.sh      |   33 ++\n t/t9510-gitweb-capture-interface.sh            |   34 ++\n t/t9510/test_capture_interface.pl              |  132 +++++++\n t/t9511-gitweb-caching-interface.sh            |   34 ++\n t/t9511/test_cache_interface.pl                |  381 ++++++++++++++++++++\n t/t9512-gitweb-cache-output-interface.sh       |   34 ++\n t/t9512/test_cache_output.pl                   |  162 +++++++++\n t/test-lib.sh                                  |    4 \n 17 files changed, 1806 insertions(+), 45 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/CacheOutput.pm\n create mode 100644 gitweb/lib/GitwebCache/Capture/ToFile.pm\n create mode 100644 gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n create mode 100755 t/t9510-gitweb-capture-interface.sh\n create mode 100755 t/t9510/test_capture_interface.pl\n create mode 100755 t/t9511-gitweb-caching-interface.sh\n create mode 100755 t/t9511/test_cache_interface.pl\n create mode 100755 t/t9512-gitweb-cache-output-interface.sh\n create mode 100755 t/t9512/test_cache_output.pl\n\n-- \nJakub Narebski\nShadeHawk on #git\nPoland\n"},{"id":"158503","messageId":"20101222235459.7998.43333.stgit@localhost.localdomain","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 1/9] gitweb: Go to DONE_REQUEST rather than DONE_GITWEB in die_error","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:55:00Z","receivedAt":"2010-12-22T23:55:00Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"\n\nEnd the request after die_error finishes, rather than exiting gitweb\ninstance (perhaps wrapped like in ModPerl::Registry or gitweb.psgi\ncase).\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\n gitweb/gitweb.perl |    3 ++-\n 1 files changed, 2 insertions(+), 1 deletions(-)\n\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 4779618..724287b 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -1169,6 +1169,7 @@ sub run {\n \n \t\trun_request();\n \n+\tDONE_REQUEST:\n \t\t$post_dispatch_hook->()\n \t\t\tif $post_dispatch_hook;\n \t\t$first_request = 0;\n@@ -3767,7 +3768,7 @@ EOF\n \tprint \"</div>\\n\";\n \n \tgit_footer_html();\n-\tgoto DONE_GITWEB\n+\tgoto DONE_REQUEST\n \t\tunless ($opts{'-error_handler'});\n }\n \n"},{"id":"158504","messageId":"20101222235525.7998.99816.stgit@localhost.localdomain","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 2/9] gitweb: use eval + die for error (exception) handling","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:55:26Z","receivedAt":"2010-12-22T23:55:26Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"\n\nIn gitweb code it is assumed that calling die_error() subroutine would\nend request, just like running \"die\" would.  Up till now it was done by\nhaving die_error() jump to DONE_REQUEST (earlier DONE_GITWEB), or in\nearlier version just 'exit' (for mod_perl via ModPerl::Registry it ends\nrequest instead of exiting worker).\n\nInstead of using 'goto DONE_REQUEST' for longjmp-like nonlocal jump, or\nusing 'exit', gitweb uses now native for Perl exception handlingin the\nform of eval / die pair (\"eval BLOCK\" to trap exceptions, \"die LIST\" to\nraise/throw them).\n\nUp till now the \"goto DONE_REQUEST\" solution was enough, but with the\ncoming output caching support and it adding modular structure to gitweb,\nit would be difficult to continue to keep using this solution\n(e.g. interaction with capturing output).\n\n\nBecause gitweb now traps all exceptions occuring run_request(), the\nhandle_errors_html handler (set via set_message from CGI::Carp) is not\nneeded; gitweb can call die_error in -error_handler mode itself.  This\nhas the advantage that we can now set correct HTTP header (handler from\nCGI::Carp::set_message was run after HTTP headers were already sent).\n\nGitweb assumes here that exceptions thrown by Perl would be simple\nstrings; die_error() throws hash reference (if not for minimal\nextrenal dependencies, it would be probable object of Class::Exception\nor Throwable class thrown).\n\nNote: in newer versions of CGI::Carp there is set_die_handler(), where\nhandler have to set HTTP headers to the browser itself, but we cannot\nrely on new enough CGI::Carp version to have been installed.  Also\nset_die_handler interferes with fatalsToBrowser.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\n gitweb/gitweb.perl |   26 ++++++++------------------\n 1 files changed, 8 insertions(+), 18 deletions(-)\n\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 724287b..c7a1892 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -12,7 +12,7 @@ use strict;\n use warnings;\n use CGI qw(:standard :escapeHTML -nosticky);\n use CGI::Util qw(unescape);\n-use CGI::Carp qw(fatalsToBrowser set_message);\n+use CGI::Carp qw(fatalsToBrowser);\n use Encode;\n use Fcntl ':mode';\n use File::Find qw();\n@@ -1045,21 +1045,6 @@ sub configure_gitweb_features {\n \t}\n }\n \n-# custom error handler: 'die <message>' is Internal Server Error\n-sub handle_errors_html {\n-\tmy $msg = shift; # it is already HTML escaped\n-\n-\t# to avoid infinite loop where error occurs in die_error,\n-\t# change handler to default handler, disabling handle_errors_html\n-\tset_message(\"Error occured when inside die_error:\\n$msg\");\n-\n-\t# you cannot jump out of die_error when called as error handler;\n-\t# the subroutine set via CGI::Carp::set_message is called _after_\n-\t# HTTP headers are already written, so it cannot write them itself\n-\tdie_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);\n-}\n-set_message(\\&handle_errors_html);\n-\n # dispatch\n sub dispatch {\n \tif (!defined $action) {\n@@ -1167,7 +1152,11 @@ sub run {\n \t\t$pre_dispatch_hook->()\n \t\t\tif $pre_dispatch_hook;\n \n-\t\trun_request();\n+\t\teval { run_request() };\n+\t\tif (defined $@ && !ref($@)) {\n+\t\t\t# some Perl error, but not one thrown by die_error\n+\t\t\tdie_error(undef, undef, $@, -error_handler => 1);\n+\t\t}\n \n \tDONE_REQUEST:\n \t\t$post_dispatch_hook->()\n@@ -3768,7 +3757,8 @@ EOF\n \tprint \"</div>\\n\";\n \n \tgit_footer_html();\n-\tgoto DONE_REQUEST\n+\n+\tdie {'status' => $status, 'error' => $error}\n \t\tunless ($opts{'-error_handler'});\n }\n \n"},{"id":"158505","messageId":"20101222235552.7998.76918.stgit@localhost.localdomain","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 3/9] gitweb: Introduce %actions_info, gathering information about actions","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:55:52Z","receivedAt":"2010-12-22T23:55:52Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"\n\nCurrently it only contains information about output format, and is not\nused anywhere.  It will be used to check whether current action\nproduces HTML output, and therefore is displaying HTML-based progress\ninfo about (re)generating cache makes sense.\n\nIt can contain information about allowed extra options, whether to\ndisplay link to feed (Atom or RSS), etc. in easier and faster way than\nlisting all matching or all non-matching actions at appropriate place.\n\n\nCurrently not used; will be used in next commit, to check if action\nproduces HTML output and therefore we can use HTML-specific progress\nindicator.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\n gitweb/gitweb.perl |   57 ++++++++++++++++++++++++++++++++++++++++++++++++----\n 1 files changed, 53 insertions(+), 4 deletions(-)\n\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex c7a1892..e50654b 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -749,6 +749,54 @@ our %allowed_options = (\n \t\"--no-merges\" => [ qw(rss atom log shortlog history) ],\n );\n \n+# action => {\n+# \t# what is the output format (content-type) of action\n+# \t'output_format' => ('html' | 'text' | 'feed' | 'binary' | undef),\n+# \t# does action require $project parameter to work\n+# \t'needs_project' => (boolean | undef),\n+# \t# log-like action, can start with arbitrary ref or revision\n+# \t'log_like' => (boolean | undef),\n+# \t# has no specific feed, or should lik to OPML / generic project feed\n+# \t'no_feed' => (boolean | undef),\n+# \t# allowed options to be passed ussing 'opt' parameter\n+# \t'allowed_options' => { 'option_1' => 1 [, ... ] },\n+# }\n+our %actions_info = ();\n+sub evaluate_actions_info {\n+\tour %actions_info;\n+\tour (%actions);\n+\n+\t# unless explicitely stated otherwise, default output format is html\n+\t# most actions needs $project parameter\n+\tforeach my $action (keys %actions) {\n+\t\t$actions_info{$action}{'output_format'} = 'html';\n+\t\t$actions_info{$action}{'needs_project'} = 1;\n+\t}\n+\t# list all exceptions; undef means variable format (no definite format)\n+\t$actions_info{$_}{'output_format'} = 'text'\n+\t\tforeach qw(commitdiff_plain patch patches project_index blame_data);\n+\t$actions_info{$_}{'output_format'} = 'feed'\n+\t\tforeach qw(rss atom opml); # there are different types (document formats) of XML\n+\t$actions_info{$_}{'output_format'} = undef\n+\t\tforeach qw(blob_plain object);\n+\t$actions_info{'snapshot'}{'output_format'} = 'binary';\n+\n+\t$actions_info{$_}{'needs_project'} = 0\n+\t\tforeach qw(opml project_list project_index);\n+\n+\t$actions_info{$_}{'log_like'} = 1\n+\t\tforeach qw(log shortlog history);\n+\n+\t$actions_info{$_}{'no_feed'} = 1\n+\t\tforeach qw(tags heads forks tag search);\n+\n+\tforeach my $opt (keys %allowed_options) {\n+\t\tforeach my $act (@{$allowed_options{$opt}}) {\n+\t\t\t$actions_info{$act}{'allowed_options'}{$opt} = 1;\n+\t\t}\n+\t}\n+}\n+\n # fill %input_params with the CGI parameters. All values except for 'opt'\n # should be single values, but opt can be an array. We should probably\n # build an array of parameters that can be multi-valued, but since for the time\n@@ -980,7 +1028,7 @@ sub evaluate_and_validate_params {\n \t\tif (not exists $allowed_options{$opt}) {\n \t\t\tdie_error(400, \"Invalid option parameter\");\n \t\t}\n-\t\tif (not grep(/^$action$/, @{$allowed_options{$opt}})) {\n+\t\tif (!$actions_info{$action}{'allowed_options'}{$opt}) {\n \t\t\tdie_error(400, \"Invalid option parameter for this action\");\n \t\t}\n \t}\n@@ -1061,7 +1109,7 @@ sub dispatch {\n \tif (!defined($actions{$action})) {\n \t\tdie_error(400, \"Unknown action\");\n \t}\n-\tif ($action !~ m/^(?:opml|project_list|project_index)$/ &&\n+\tif ($actions_info{$action}{'needs_project'} &&\n \t    !$project) {\n \t\tdie_error(400, \"Project needed\");\n \t}\n@@ -1142,6 +1190,7 @@ sub evaluate_argv {\n \n sub run {\n \tevaluate_argv();\n+\tevaluate_actions_info();\n \n \t$first_request = 1;\n \t$pre_listen_hook->()\n@@ -1803,7 +1852,7 @@ sub format_ref_marker {\n \n \t\t\tif ($indirect) {\n \t\t\t\t$dest_action = \"tag\" unless $action eq \"tag\";\n-\t\t\t} elsif ($action =~ /^(history|(short)?log)$/) {\n+\t\t\t} elsif ($actions_info{$action}{'log_like'}) {\n \t\t\t\t$dest_action = $action;\n \t\t\t}\n \n@@ -2277,7 +2326,7 @@ sub get_feed_info {\n \treturn unless (defined $project);\n \t# some views should link to OPML, or to generic project feed,\n \t# or don't have specific feed yet (so they should use generic)\n-\treturn if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);\n+\treturn if ($actions_info{$action}{'no_feed'});\n \n \tmy $branch;\n \t# branches refs uses 'refs/heads/' prefix (fullname) to differentiate\n"},{"id":"158506","messageId":"20101222235618.7998.17447.stgit@localhost.localdomain","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 4/9] gitweb: Prepare for splitting gitweb","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:56:18Z","receivedAt":"2010-12-22T23:56:18Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"\n\nPrepare gitweb for having been split into modules that are to be\ninstalled alongside gitweb in 'lib/' subdirectory, by adding\n\n  use lib __DIR__.'/lib';\n\nto gitweb.perl (to main gitweb script), and preparing for putting\nmodules (relative path) in $(GITWEB_MODULES) in gitweb/Makefile.\n\nThis preparatory work allows to add new module to gitweb by simply\nadding\n\n  GITWEB_MODULES += <module>\n\nto gitweb/Makefile (assuming that the module is in 'gitweb/lib/'\ndirectory).\n\nWhile at it pass GITWEBLIBDIR in addition to GITWEB_TEST_INSTALLED to\nallow testing installed version of gitweb and installed version of\nmodules (for future tests which would check individual (sub)modules).\n\n\nUsing __DIR__ from Dir::Self module (not in core, that's why currently\ngitweb includes excerpt of code from Dir::Self defining __DIR__) was\nchosen over using FindBin-based solution (in core since perl 5.00307,\nwhile gitweb itself requires at least perl 5.8.0) because FindBin uses\nBEGIN block, which is a problem under mod_perl and other persistent\nPerl environments (thought there are workarounds).\n\nAt Pavan Kumar Sankara suggestion gitweb/Makefile uses\n\n  install [OPTION]... SOURCE... DIRECTORY\n\nformat (2nd format) with single SOURCE rather than\n\n  install [OPTION]... SOURCE DEST\n\nformat (1st format) because of security reasons (race conditions).\nModern GNU install has `-T' / `--no-target-directory' option, but we\ncannot rely that the $(INSTALL) we are using supports this option.\n\nThe install-modules target in gitweb/Makefile uses shell 'for' loop,\ninstead of make's $(foreach) function, to avoid possible problem with\ngenerating a command line that exceeded the maximum argument list\nlength.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\n gitweb/Makefile    |   17 +++++++++++++++--\n gitweb/gitweb.perl |    8 ++++++++\n 2 files changed, 23 insertions(+), 2 deletions(-)\n\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex 0a6ac00..e6029e1 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -57,6 +57,7 @@ PERL_PATH  ?= /usr/bin/perl\n bindir_SQ = $(subst ','\\'',$(bindir))#'\n gitwebdir_SQ = $(subst ','\\'',$(gitwebdir))#'\n gitwebstaticdir_SQ = $(subst ','\\'',$(gitwebdir)/static)#'\n+gitweblibdir_SQ = $(subst ','\\'',$(gitwebdir)/lib)#'\n SHELL_PATH_SQ = $(subst ','\\'',$(SHELL_PATH))#'\n PERL_PATH_SQ  = $(subst ','\\'',$(PERL_PATH))#'\n DESTDIR_SQ    = $(subst ','\\'',$(DESTDIR))#'\n@@ -153,20 +154,32 @@ test:\n \n test-installed:\n \tGITWEB_TEST_INSTALLED='$(DESTDIR_SQ)$(gitwebdir_SQ)' \\\n+\tGITWEBLIBDIR='$(DESTDIR_SQ)$(gitweblibdir_SQ)' \\\n \t\t$(MAKE) -C ../t gitweb-test\n \n ### Installation rules\n \n-install: all\n+install: all install-modules\n \t$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitwebdir_SQ)'\n \t$(INSTALL) -m 755 $(GITWEB_PROGRAMS) '$(DESTDIR_SQ)$(gitwebdir_SQ)'\n \t$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitwebstaticdir_SQ)'\n \t$(INSTALL) -m 644 $(GITWEB_FILES) '$(DESTDIR_SQ)$(gitwebstaticdir_SQ)'\n \n+install-modules:\n+\tinstall_dirs=\"$(sort $(dir $(GITWEB_MODULES)))\" && \\\n+\tfor dir in $$install_dirs; do \\\n+\t\ttest -d '$(DESTDIR_SQ)$(gitweblibdir_SQ)'/\"$$dir\" || \\\n+\t\t$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitweblibdir_SQ)'/\"$$dir\"; \\\n+\tdone\n+\tgitweb_modules=\"$(GITWEB_MODULES)\" && \\\n+\tfor mod in $$gitweb_modules; do \\\n+\t\t$(INSTALL) -m 644 \"lib/$$mod\" '$(DESTDIR_SQ)$(gitweblibdir_SQ)'/\"$$(dirname $$mod)\"; \\\n+\tdone\n+\n ### Cleaning rules\n \n clean:\n \t$(RM) gitweb.cgi static/gitweb.min.js static/gitweb.min.css GITWEB-BUILD-OPTIONS\n \n-.PHONY: all clean install test test-installed .FORCE-GIT-VERSION-FILE FORCE\n+.PHONY: all clean install install-modules test test-installed .FORCE-GIT-VERSION-FILE FORCE\n \ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex e50654b..880fdf2 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -10,6 +10,14 @@\n use 5.008;\n use strict;\n use warnings;\n+\n+use File::Spec;\n+# __DIR__ is taken from Dir::Self __DIR__ fragment\n+sub __DIR__ () {\n+\tFile::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);\n+}\n+use lib __DIR__ . '/lib';\n+\n use CGI qw(:standard :escapeHTML -nosticky);\n use CGI::Util qw(unescape);\n use CGI::Carp qw(fatalsToBrowser);\n"},{"id":"158507","messageId":"20101222235639.7998.10673.stgit@localhost.localdomain","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 5/9] t/test-lib.sh: Export also GIT_BUILD_DIR in test_external","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:56:39Z","receivedAt":"2010-12-22T23:56:39Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"\n\nThis way we can use it in test scripts written in other languages\n(e.g. in Perl), and not rely on \"$TEST_DIRECTORY/..\"\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\n t/test-lib.sh |    4 ++--\n 1 files changed, 2 insertions(+), 2 deletions(-)\n\n\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 48fa516..c077fa4 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -552,9 +552,9 @@ test_external () {\n \t\t# Announce the script to reduce confusion about the\n \t\t# test output that follows.\n \t\tsay_color \"\" \"# run $test_count: $descr ($*)\"\n-\t\t# Export TEST_DIRECTORY, TRASH_DIRECTORY and GIT_TEST_LONG\n+\t\t# Export TEST_DIRECTORY, GIT_BUILD_DIR, TRASH_DIRECTORY and GIT_TEST_LONG\n \t\t# to be able to use them in script\n-\t\texport TEST_DIRECTORY TRASH_DIRECTORY GIT_TEST_LONG\n+\t\texport TEST_DIRECTORY GIT_BUILD_DIR TRASH_DIRECTORY GIT_TEST_LONG\n \t\t# Run command; redirect its stderr to &4 as in\n \t\t# test_run_, but keep its stdout on our stdout even in\n \t\t# non-verbose mode.\n"},{"id":"158508","messageId":"20101222235705.7998.76695.stgit@localhost.localdomain","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 6/9] gitweb/lib - Simple output capture by redirecting STDOUT to file","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:57:05Z","receivedAt":"2010-12-22T23:57:05Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"\n\nAdd GitwebCache::Capture::ToFile package, which captures output by\nredirecting STDOUT to given file (specified by filename, or given opened\nfilehandle), earlier saving original STDOUT to restore it when finished\ncapturing.\n\nGitwebCache::Capture::ToFile preserves PerlIO layers, both those set\nbefore started capturing output, and those set during capture.\n\nNo care was taken to handle the following special cases (prior to\nstarting capture): closed STDOUT, STDOUT reopened to scalar reference,\ntied STDOUT.  You shouldn't modify STDOUT during capture.\n\nIncludes separate tests for capturing output in\nt9510/test_capture_interface.pl which is run as external test from\nt9510-gitweb-capture-interface.sh.  It tests capturing of utf8 data\nprinted in :utf8 mode, and of binary data (containing invalid utf8) in\n:raw mode.\n\n\nThis patch was based on \"gitweb: add output buffering and associated\nfunctions\" patch by John 'Warthog9' Hawley (J.H.) in \"Gitweb caching v7\"\nseries, and on code of Capture::Tiny by David Golden (Apache License 2.0).\n\nBased-on-work-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\n gitweb/lib/GitwebCache/Capture/ToFile.pm |  109 +++++++++++++++++++++++++\n t/t9510-gitweb-capture-interface.sh      |   34 ++++++++\n t/t9510/test_capture_interface.pl        |  132 ++++++++++++++++++++++++++++++\n 3 files changed, 275 insertions(+), 0 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/Capture/ToFile.pm\n create mode 100755 t/t9510-gitweb-capture-interface.sh\n create mode 100755 t/t9510/test_capture_interface.pl\n\n\ndiff --git a/gitweb/lib/GitwebCache/Capture/ToFile.pm b/gitweb/lib/GitwebCache/Capture/ToFile.pm\nnew file mode 100644\nindex 0000000..d2dbf0f\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/Capture/ToFile.pm\n@@ -0,0 +1,109 @@\n+# gitweb - simple web interface to track changes in git repositories\n+#\n+# (C) 2010, Jakub Narebski <jnareb@gmail.com>\n+#\n+# This program is licensed under the GPLv2\n+\n+#\n+# Simple output capturing via redirecting STDOUT to given file.\n+#\n+\n+# This is the same mechanism that Capture::Tiny uses, only simpler;\n+# we don't capture STDERR at all, we don't tee, we capture to\n+# explicitely provided file (or filehandle).\n+\n+package GitwebCache::Capture::ToFile;\n+\n+use strict;\n+use warnings;\n+\n+use PerlIO;\n+use Symbol qw(qualify_to_ref);\n+\n+# Constructor\n+sub new {\n+\tmy $class = shift;\n+\n+\tmy $self = {};\n+\t$self = bless($self, $class);\n+\n+\treturn $self;\n+}\n+\n+sub capture {\n+\tmy $self = shift;\n+\tmy $code = shift;\n+\n+\t$self->capture_start(@_); # pass rest of params\n+\teval { $code->(); 1; };\n+\tmy $exit_code = $?; # save this for later\n+\tmy $error = $@;     # save this for later\n+\n+\tmy $got_out = $self->capture_stop();\n+\t$? = $exit_code;\n+\tdie $error if $error;\n+\n+\treturn $got_out;\n+}\n+\n+# ----------------------------------------------------------------------\n+\n+# Start capturing data (STDOUT)\n+sub capture_start {\n+\tmy $self = shift;\n+\tmy $to   = shift;\n+\n+\t# save copy of real STDOUT via duplicating it\n+\tmy @layers = PerlIO::get_layers(\\*STDOUT);\n+\topen $self->{'orig_stdout'}, \">&\", \\*STDOUT\n+\t\tor die \"Couldn't dup STDOUT for capture: $!\";\n+\n+\t# close STDOUT, so that it isn't used anymode (to have it fd0)\n+\tclose STDOUT;\n+\n+\t$self->{'to'} = $to;\n+\tmy $fileno = fileno(qualify_to_ref($to)); \n+\tif (defined $fileno) {\n+\t\t# if $to is filehandle, redirect\n+\t\topen STDOUT, '>&', $fileno;\n+\t} elsif (! ref($to)) {\n+\t\t# if $to is name of file, open it\n+\t\topen STDOUT, '>',  $to;\n+\t}\n+\t_relayer(\\*STDOUT, \\@layers);\n+\n+\t# started capturing\n+\t$self->{'capturing'} = 1;\n+}\n+\n+# Stop capturing data (required for die_error)\n+sub capture_stop {\n+\tmy $self = shift;\n+\n+\t# return if we didn't start capturing\n+\treturn unless delete $self->{'capturing'};\n+\n+\t# close capture file, and restore original STDOUT\n+\tmy @layers = PerlIO::get_layers(\\*STDOUT);\n+\tclose STDOUT;\n+\topen STDOUT, '>&', fileno($self->{'orig_stdout'});\n+\t_relayer(\\*STDOUT, \\@layers);\n+\n+\treturn exists $self->{'to'} ? $self->{'to'} : $self->{'data'};\n+}\n+\n+# taken from Capture::Tiny by David Golden, Apache License 2.0\n+# with debugging stripped out\n+sub _relayer {\n+\tmy ($fh, $layers) = @_;\n+\n+\tmy %seen = ( unix => 1, perlio => 1); # filter these out\n+\tmy @unique = grep { !$seen{$_}++ } @$layers;\n+\n+\tbinmode($fh, join(\":\", \":raw\", @unique));\n+}\n+\n+\n+1;\n+__END__\n+# end of package GitwebCache::Capture::ToFile\ndiff --git a/t/t9510-gitweb-capture-interface.sh b/t/t9510-gitweb-capture-interface.sh\nnew file mode 100755\nindex 0000000..9151454\n--- /dev/null\n+++ b/t/t9510-gitweb-capture-interface.sh\n@@ -0,0 +1,34 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2010 Jakub Narebski\n+#\n+\n+test_description='gitweb capturing interface\n+\n+This test checks capturing interface used for capturing gitweb output\n+in gitweb caching (GitwebCache::Capture* modules).'\n+\n+# for now we are running only cache interface tests\n+. ./test-lib.sh\n+\n+# this test is present in gitweb-lib.sh\n+if ! test_have_prereq PERL; then\n+\tskip_all='perl not available, skipping test'\n+\ttest_done\n+fi\n+\n+\"$PERL_PATH\" -MTest::More -e 0 >/dev/null 2>&1 || {\n+\tskip_all='perl module Test::More unavailable, skipping test'\n+\ttest_done\n+}\n+\n+# ----------------------------------------------------------------------\n+\n+# The external test will outputs its own plan\n+test_external_has_tap=1\n+\n+test_external \\\n+\t'GitwebCache::Capture* Perl API (in gitweb/lib/)' \\\n+\t\"$PERL_PATH\" \"$TEST_DIRECTORY\"/t9510/test_capture_interface.pl\n+\n+test_done\ndiff --git a/t/t9510/test_capture_interface.pl b/t/t9510/test_capture_interface.pl\nnew file mode 100755\nindex 0000000..6d90497\n--- /dev/null\n+++ b/t/t9510/test_capture_interface.pl\n@@ -0,0 +1,132 @@\n+#!/usr/bin/perl\n+use lib (split(/:/, $ENV{GITPERLLIB}));\n+\n+use warnings;\n+use strict;\n+use utf8;\n+\n+use Test::More;\n+\n+# test source version\n+use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n+\n+# ....................................................................\n+\n+use_ok('GitwebCache::Capture::ToFile');\n+note(\"Using lib '$INC[0]'\");\n+note(\"Testing '$INC{'GitwebCache/Capture/ToFile.pm'}'\");\n+\n+# Test setting up capture\n+#\n+my $capture = new_ok('GitwebCache::Capture::ToFile' => [], 'The $capture');\n+\n+\n+# Test capturing to file (given by filename) and to filehandle\n+#\n+sub capture_block (&;$) {\n+\t$capture->capture(shift, shift || 'actual');\n+\n+\topen my $fh, '<', 'actual' or return;\n+\tlocal $/ = undef;\n+\tmy $result = <$fh>;\n+\tclose $fh;\n+\treturn $result;\n+}\n+\n+diag('Should not print anything except test results and diagnostic');\n+my $test_data = 'Capture this';\n+my $captured = capture_block {\n+\tprint $test_data;\n+};\n+is($captured, $test_data, 'capture simple data: filename');\n+\n+open my $fh, '>', 'actual';\n+$captured = capture_block(sub {\n+\tprint $test_data;\n+}, $fh);\n+close $fh;\n+is($captured, $test_data, 'capture simple data: filehandle');\n+\n+\n+# Test capturing :utf8 and :raw data\n+#\n+binmode STDOUT, ':utf8';\n+$test_data = <<'EOF';\n+Zażółć gęsią jaźń\n+EOF\n+utf8::decode($test_data);\n+$captured = capture_block {\n+\tbinmode STDOUT, ':utf8';\n+\n+\tprint $test_data;\n+};\n+utf8::decode($captured);\n+is($captured, $test_data, 'capture utf8 data');\n+\n+$test_data = '|\\x{fe}\\x{ff}|\\x{9F}|\\000|'; # invalid utf-8\n+$captured = capture_block {\n+\tbinmode STDOUT, ':raw';\n+\n+\tprint $test_data;\n+};\n+is($captured, $test_data, 'capture raw data');\n+\n+\n+# Test nested capturing, useful for future GitwebCache::CacheOutput tests\n+#\n+sub read_file {\n+\tmy $filename = shift;\n+\n+\topen my $fh, '<', $filename or return;\n+\tlocal $/ = undef;\n+\tmy $result = <$fh>;\n+\tclose $fh;\n+\n+\treturn $result;\n+}\n+\n+my $outer_capture = GitwebCache::Capture::ToFile->new();\n+$captured = $outer_capture->capture(sub {\n+\tprint \"pre|\";\n+\tmy $captured = $capture->capture(sub {\n+\t\tprint \"INNER\";\n+\t}, 'inner_actual');\n+\tprint \"|post\";\n+}, 'outer_actual');\n+\n+my $inner = read_file('inner_actual');\n+my $outer = read_file('outer_actual');\n+\n+is($inner, \"INNER\",     'nested capture: inner');\n+is($outer, \"pre||post\", 'nested capture: outer');\n+\n+\n+# Testing capture when code dies\n+#\n+$captured = $outer_capture->capture(sub {\n+\tprint \"pre|\";\n+\teval {\n+\t\tmy $captured = $capture->capture(sub {\n+\t\t\tprint \"INNER:pre|\";\n+\t\t\tdie \"die from inner\\n\";\n+\t\t\tprint \"INNER:post|\"\n+\t\t}, 'inner_actual');\n+\t};\n+\tprint \"@=$@\" if $@;\n+\tprint \"|post\";\n+}, 'outer_actual');\n+\n+my $inner = read_file('inner_actual');\n+my $outer = read_file('outer_actual');\n+\n+is($inner, \"INNER:pre|\",\n+   'nested capture with die: inner output captured up to die');\n+is($outer, \"pre|@=die from inner\\n|post\",\n+   'nested capture with die: outer caught rethrown exception from inner');\n+\n+\n+done_testing();\n+\n+# Local Variables:\n+# coding: utf-8\n+# End:\n"},{"id":"158509","messageId":"20101222235731.7998.3214.stgit@localhost.localdomain","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 7/9] gitweb/lib - Very simple file based cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:57:32Z","receivedAt":"2010-12-22T23:57:32Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"\n\nThis is first step towards implementing file based output (response)\ncaching layer that is used on such large sites as kernel.org.\n\nThis patch introduces GitwebCaching::SimpleFileCache package, which\nfollows Cache::Cache / CHI interface, although do not implement it\nfully.  The intent of following established convention for cache\ninterface is to be able to replace our simple file based cache,\ne.g. by the one using memcached.\n\nThe data is stored in the cache as-is, without adding metadata (like\nexpiration date), and without serialization (which means that one can\nstore only scalar data).  At this point there is no support for\nexpiring cache entries.\n\n\nThe code of GitwebCaching::SimpleFileCache package in gitweb/lib\nwas heavily based on file-based cache in Cache::Cache package, i.e.\non Cache::FileCache, Cache::FileBackend and Cache::BaseCache, and on\nfile-based cache in CHI, i.e. on CHI::Driver::File and CHI::Driver\n(including implementing atomic write, something that original patch\nlacks).  It tries to follow more modern CHI architecture, but without\nrequiring Moose.  It is much simplified compared to both interfaces\nand their file-based drivers.\n\nThis patch does not yet enable output caching in gitweb (it doesn't\nhave all required features yet); on the other hand it includes tests\nof cache Perl API in t/t9503-gitweb-caching-interface.sh.\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |  452 ++++++++++++++++++++++++\n t/t9511-gitweb-caching-interface.sh            |   34 ++\n t/t9511/test_cache_interface.pl                |  381 ++++++++++++++++++++\n 3 files changed, 867 insertions(+), 0 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n create mode 100755 t/t9511-gitweb-caching-interface.sh\n create mode 100755 t/t9511/test_cache_interface.pl\n\n\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nnew file mode 100644\nindex 0000000..ecd0e18\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -0,0 +1,452 @@\n+# gitweb - simple web interface to track changes in git repositories\n+#\n+# (C) 2006, John 'Warthog9' Hawley <warthog19@eaglescrag.net>\n+# (C) 2010, Jakub Narebski <jnareb@gmail.com>\n+#\n+# This program is licensed under the GPLv2\n+\n+#\n+# Gitweb caching engine, file-based cache with flock-based entry locking\n+#\n+\n+# Minimalistic cache that stores data in the filesystem, without serialization.\n+# It uses file locks (flock) to have only one process generating data and\n+# writing to cache, when using CHI-like interface ->compute_fh() method.\n+\n+package GitwebCache::FileCacheWithLocking;\n+\n+use strict;\n+use warnings;\n+\n+use Carp;\n+use File::Path qw(mkpath);\n+use Digest::MD5 qw(md5_hex);\n+use Fcntl qw(:flock);\n+use POSIX qw(setsid);\n+\n+# by default, the cache nests all entries on the filesystem single\n+# directory deep, i.e. '60/b725f10c9c85c70d97880dfe8191b3' for\n+# key name (key digest) 60b725f10c9c85c70d97880dfe8191b3.\n+#\n+our $DEFAULT_CACHE_DEPTH = 1;\n+\n+# by default, the root of the cache is located in 'cache'.\n+#\n+our $DEFAULT_CACHE_ROOT = \"cache\";\n+\n+# by default we don't use cache namespace (empty namespace);\n+# empty namespace does not allow for simple implementation of clear() method.\n+#\n+our $DEFAULT_NAMESPACE = '';\n+\n+# anything less than 0 means to not expire\n+#\n+our $NEVER_EXPIRE = -1;\n+\n+# cache expiration of 0 means that entry is expired\n+#\n+our $EXPIRE_NOW = 0;\n+\n+# ......................................................................\n+# constructor\n+\n+# The options are set by passing in hash or a reference to a hash containing\n+# any of the following keys:\n+#  * 'namespace'\n+#    The namespace associated with this cache.  This allows easy separation of\n+#    multiple, distinct caches without worrying about key collision.  Defaults\n+#    to $DEFAULT_NAMESPACE.  Might be empty string.\n+#  * 'cache_root' (Cache::FileCache compatibile),\n+#    'root_dir' (CHI::Driver::File compatibile),\n+#    The location in the filesystem that will hold the root of the cache.\n+#    Defaults to $DEFAULT_CACHE_ROOT.\n+#  * 'cache_depth' (Cache::FileCache compatibile),\n+#    'depth' (CHI::Driver::File compatibile),\n+#    The number of subdirectories deep to cache object item.  This should be\n+#    large enough that no cache directory has more than a few hundred objects.\n+#    Defaults to $DEFAULT_CACHE_DEPTH unless explicitly set.\n+#  * 'default_expires_in' (Cache::Cache compatibile),\n+#    'expires_in' (CHI compatibile) [seconds]\n+#    The expiration time for objects place in the cache.\n+#    Defaults to -1 (never expire) if not explicitly set.\n+#  * 'max_lifetime' [seconds]\n+#    If it is greater than 0, and cache entry is expired but not older\n+#    than it, serve stale data when waiting for cache entry to be \n+#    regenerated (refreshed).  Non-adaptive.\n+#  * 'on_error' (similar to CHI 'on_get_error'/'on_set_error')\n+#    How to handle runtime errors occurring during cache gets and cache\n+#    sets, which may or may not be considered fatal in your application.\n+#    Options are:\n+#    * \"die\" (the default) - call die() with an appropriate message\n+#    * \"warn\" - call warn() with an appropriate message\n+#    * \"ignore\" - do nothing\n+#    * <coderef> - call this code reference with an appropriate message\n+sub new {\n+\tmy $class = shift;\n+\tmy %opts = ref $_[0] ? %{ $_[0] } : @_;\n+\n+\tmy $self = {};\n+\t$self = bless($self, $class);\n+\n+\t$self->{'root'} =\n+\t\texists $opts{'cache_root'} ? $opts{'cache_root'} :\n+\t\texists $opts{'root_dir'}   ? $opts{'root_dir'} :\n+\t\t$DEFAULT_CACHE_ROOT;\n+\t$self->{'depth'} =\n+\t\texists $opts{'cache_depth'} ? $opts{'cache_depth'} :\n+\t\texists $opts{'depth'}       ? $opts{'depth'} :\n+\t\t$DEFAULT_CACHE_DEPTH;\n+\t$self->{'namespace'} =\n+\t\texists $opts{'namespace'} ? $opts{'namespace'} :\n+\t\t$DEFAULT_NAMESPACE;\n+\t$self->{'expires_in'} =\n+\t\texists $opts{'default_expires_in'} ? $opts{'default_expires_in'} :\n+\t\texists $opts{'expires_in'}         ? $opts{'expires_in'} :\n+\t\t$NEVER_EXPIRE;\n+\t$self->{'max_lifetime'} =\n+\t\texists $opts{'max_lifetime'}       ? $opts{'max_lifetime'} :\n+\t\texists $opts{'max_cache_lifetime'} ? $opts{'max_cache_lifetime'} :\n+\t\t$NEVER_EXPIRE;\n+\t$self->{'on_error'} =\n+\t\texists $opts{'on_error'}      ? $opts{'on_error'} :\n+\t\texists $opts{'on_get_error'}  ? $opts{'on_get_error'} :\n+\t\texists $opts{'on_set_error'}  ? $opts{'on_set_error'} :\n+\t\texists $opts{'error_handler'} ? $opts{'error_handler'} :\n+\t\t'die';\n+\n+\t# validation could be put here\n+\n+\treturn $self;\n+}\n+\n+\n+# ......................................................................\n+# accessors\n+\n+# http://perldesignpatterns.com/perldesignpatterns.html#AccessorPattern\n+\n+# creates get_depth() and set_depth($depth) etc. methods\n+foreach my $i (qw(depth root namespace expires_in max_lifetime\n+                  on_error)) {\n+\tmy $field = $i;\n+\tno strict 'refs';\n+\t*{\"get_$field\"} = sub {\n+\t\tmy $self = shift;\n+\t\treturn $self->{$field};\n+\t};\n+\t*{\"set_$field\"} = sub {\n+\t\tmy ($self, $value) = @_;\n+\t\t$self->{$field} = $value;\n+\t};\n+}\n+\n+\n+# ----------------------------------------------------------------------\n+# utility functions and methods\n+\n+# $path = $self->path_to_namespace();\n+#\n+# Return root dir for namespace (lazily built, cached)\n+sub path_to_namespace {\n+\tmy ($self) = @_;\n+\n+\tif (!exists $self->{'path_to_namespace'}) {\n+\t\tif (defined $self->{'namespace'} &&\n+\t\t    $self->{'namespace'} ne '') {\n+\t\t\t$self->{'path_to_namespace'} = \"$self->{'root'}/$self->{'namespace'}\";\n+\t\t} else {\n+\t\t\t$self->{'path_to_namespace'} =  $self->{'root'};\n+\t\t}\n+\t}\n+\treturn $self->{'path_to_namespace'};\n+}\n+\n+# $path = $cache->path_to_key($key);\n+# $path = $cache->path_to_key($key, \\$dir);\n+#\n+# Take an human readable key, and return file path.\n+# Puts dirname of file path in second argument, if it is provided.\n+sub path_to_key {\n+\tmy ($self, $key, $dir_ref) = @_;\n+\n+\tmy @paths = ( $self->path_to_namespace() );\n+\n+\t# Create a unique (hashed) key from human readable key\n+\tmy $filename = md5_hex($key); # or $digester->add($key)->hexdigest();\n+\n+\t# Split filename so that it have DEPTH subdirectories,\n+\t# where each subdirectory has a two-letter name\n+\tpush @paths, unpack(\"(a2)[$self->{'depth'}] a*\", $filename);\n+\t$filename = pop @paths;\n+\n+\t# Join paths together, computing dir separately if $dir_ref was passed.\n+\tmy $filepath;\n+\tif (defined $dir_ref && ref($dir_ref)) {\n+\t\tmy $dir = join('/', @paths);\n+\t\t$filepath = \"$dir/$filename\";\n+\t\t$$dir_ref = $dir;\n+\t} else {\n+\t\t$filepath = join('/', @paths, $filename);\n+\t}\n+\n+\treturn $filepath;\n+}\n+\n+# $self->ensure_path($dir);\n+#\n+# create $dir (directory) if it not exists, thus ensuring that path exists\n+sub ensure_path {\n+\tmy $self = shift;\n+\tmy $dir = shift || return;\n+\n+\tif (!-d $dir) {\n+\t\t# mkpath will croak()/die() if there is an error\n+\t\tmkpath($dir, 0, 0777);\n+\t}\n+}\n+\n+# $filename = $self->get_lockname($key);\n+#\n+# Take an human readable key, and return path to be used for lockfile\n+# Ensures that file can be created, if needed.\n+sub get_lockname {\n+\tmy ($self, $key) = @_;\n+\n+\tmy $lockfile = $self->path_to_key($key, \\my $dir) . '.lock';\n+\n+\t# ensure that directory leading to lockfile exists\n+\t$self->ensure_path($dir);\n+\n+\treturn $lockfile;\n+}\n+\n+# ----------------------------------------------------------------------\n+# \"private\" utility functions and methods\n+\n+# ($fh, $filename) = $self->_tempfile_to_path($path_for_key, $dir_for_key);\n+#\n+# take a file path to cache entry, and its directory\n+# return filehandle and filename of open temporary file,\n+# like File::Temp::tempfile\n+sub _tempfile_to_path {\n+\tmy ($self, $file, $dir) = @_;\n+\n+\tmy $tempname = \"$file.tmp\";\n+\topen my $temp_fh, '>', $tempname\n+\t\tor die \"Couldn't open temporary file '$tempname' for writing: $!\";\n+\n+\treturn ($temp_fh, $tempname);\n+}\n+\n+# ($fh, $filename) = $self->_wait_for_data($key, $code);\n+#\n+# Wait for data to be available using (blocking) $code,\n+# then return filehandle and filename to read from for $key.\n+sub _wait_for_data {\n+\tmy ($self, $key, $sync_coderef) = @_;\n+\tmy @result;\n+\n+\t# wait for data to be available\n+\t$sync_coderef->();\n+\t# fetch data\n+\t@result = $self->fetch_fh($key);\n+\n+\treturn @result;\n+}\n+\n+# $self->_handle_error($raw_error)\n+#\n+# based on _handle_get_error and _dispatch_error_msg from CHI::Driver\n+sub _handle_error {\n+\tmy ($self, $error) = @_;\n+\n+\tfor ($self->get_on_error()) {\n+\t\t(ref($_) eq 'CODE') && do { $_->($error) };\n+\t\t/^ignore$/ && do { };\n+\t\t/^warn$/   && do { carp $error };\n+\t\t/^die$/    && do { croak $error };\n+\t}\n+}\n+\n+# ----------------------------------------------------------------------\n+# nonstandard worker and semi-interface methods\n+\n+# ($fh, $filename) = $self->fetch_fh($key);\n+#\n+# Get filehandle to read from for given $key, and filename of cache file.\n+# Doesn't check if entry expired.\n+sub fetch_fh {\n+\tmy ($self, $key) = @_;\n+\n+\tmy $path = $self->path_to_key($key);\n+\treturn unless (defined $path);\n+\n+\topen my $fh, '<', $path or return;\n+\treturn ($fh, $path);\n+}\n+\n+# ($fh, $filename) = $self->get_fh($key, [option => value, ...])\n+#\n+# Returns filehandle to read from for given $key, and filename of cache file.\n+# Returns empty list if entry expired.\n+#\n+# $key may be followed by one or more name/value parameters:\n+# * expires_in [DURATION] - override global expiration time\n+sub get_fh {\n+\tmy ($self, $key, %opts) = @_;\n+\n+\treturn unless ($self->is_valid($key, $opts{'expires_in'}));\n+\n+\treturn $self->fetch_fh($key);\n+}\n+\n+# [($fh, $filename) =] $self->set_coderef_fh($key, $code_fh);\n+#\n+# Runs $code_fh, passing to it $fh and $filename of file to write to;\n+# the contents of this file would be contents of cache entry.\n+# Returns what $self->fetch_fh($key) would return.\n+sub set_coderef_fh {\n+\tmy ($self, $key, $code) = @_;\n+\n+\tmy $path = $self->path_to_key($key, \\my $dir);\n+\treturn unless (defined $path && defined $dir);\n+\n+\t# ensure that directory leading to cache file exists\n+\t$self->ensure_path($dir);\n+\n+\t# generate a temporary file / file to write to\n+\tmy ($fh, $tempfile) = $self->_tempfile_to_path($path, $dir);\n+\n+\t# code writes to filehandle or file\n+\t$code->($fh, $tempfile);\n+\n+\tclose $fh;\n+\trename($tempfile, $path)\n+\t\tor die \"Couldn't rename temporary file '$tempfile' to '$path': $!\";\n+\n+\topen $fh, '<', $path or return;\n+\treturn ($fh, $path);\n+}\n+\n+# ======================================================================\n+# ......................................................................\n+# interface methods\n+#\n+# note that only those methods use 'on_error' handler;\n+# all the rest just use \"die\"\n+\n+# Removing and expiring\n+\n+# $cache->remove($key)\n+#\n+# Remove the data associated with the $key from the cache.\n+sub remove {\n+\tmy ($self, $key) = @_;\n+\n+\tmy $file = $self->path_to_key($key)\n+\t\tor return;\n+\treturn unless -f $file;\n+\tunlink($file)\n+\t\tor $self->_handle_error(\"Couldn't remove cache entry file '$file' for key '$key': $!\");\n+}\n+\n+# $cache->is_valid($key[, $expires_in])\n+#\n+# Returns a boolean indicating whether $key exists in the cache\n+# and has not expired.  Uses global per-cache expires time, unless\n+# passed optional $expires_in argument.\n+sub is_valid {\n+\tmy ($self, $key, $expires_in) = @_;\n+\n+\tmy $path = $self->path_to_key($key);\n+\n+\t# does file exists in cache?\n+\treturn 0 unless -f $path;\n+\t# get its modification time\n+\tmy $mtime = (stat(_))[9] # _ to reuse stat structure used in -f test\n+\t\tor $self->_handle_error(\"Couldn't stat file '$path' for key '$key': $!\");\n+\n+\t# expire time can be set to never\n+\t$expires_in = defined $expires_in ? $expires_in : $self->get_expires_in();\n+\treturn 1 unless (defined $expires_in && $expires_in >= 0);\n+\n+\t# is file expired?\n+\tmy $now = time();\n+\n+\treturn (($now - $mtime) < $expires_in);\n+}\n+\n+# Getting and setting\n+\n+# ($fh, $filename) = $cache->compute_fh($key, $code);\n+#\n+# Combines the get and set operations in a single call.  Attempts to\n+# get $key; if successful, returns the filehandle it can be read from.\n+# Otherwise, calls $code passing filehandle to write to as a\n+# parameter; contents of this file is then used as the new value for\n+# $key; returns filehandle from which one can read newly generated data.\n+#\n+# Uses file locking to have only one process updating value for $key\n+# to avoid 'cache miss stampede' (aka 'stampeding herd') problem.\n+sub compute_fh {\n+\tmy ($self, $key, $code_fh) = @_;\n+\n+\tmy @result = eval { $self->get_fh($key) };\n+\treturn @result if @result;\n+\t$self->_handle_error($@) if $@;\n+\n+\tmy $lockfile = $self->get_lockname($key);\n+\n+\t# this loop is to protect against situation where process that\n+\t# acquired exclusive lock (writer) dies or exits\n+\t# before writing data to cache\n+\tmy $lock_state; # needed for loop condition\n+\tdo {\n+\t\topen my $lock_fh, '+>', $lockfile\n+\t\t\tor $self->_handle_error(\"Could't open lockfile '$lockfile': $!\");\n+\n+\t\t$lock_state = flock($lock_fh, LOCK_EX | LOCK_NB);\n+\t\tif ($lock_state) {\n+\t\t\t## acquired writers lock, have to generate data\n+\t\t\t@result = eval { $self->set_coderef_fh($key, $code_fh) };\n+\t\t\t$self->_handle_error($@) if $@;\n+\n+\t\t\t# closing lockfile releases writer lock\n+\t\t\tflock($lock_fh, LOCK_UN);\n+\t\t\tclose $lock_fh\n+\t\t\t\tor $self->_handle_error(\"Could't close lockfile '$lockfile': $!\");\n+\n+\t\t} else {\n+\t\t\t## didn't acquire writers lock, get stale data or wait for regeneration\n+\n+\t\t\t# try to retrieve stale data\n+\t\t\teval {\n+\t\t\t\t@result = $self->get_fh($key,\n+\t\t\t\t\t'expires_in' => $self->get_max_lifetime());\n+\t\t\t};\n+\t\t\treturn @result if @result;\n+\t\t\t$self->_handle_error($@) if $@;\n+\n+\t\t\t# wait for regeneration if no stale data to serve,\n+\t\t\t# using shared / readers lock to sync (wait for data)\n+\t\t\t@result = eval {\n+\t\t\t\t$self->_wait_for_data($key, sub {\n+\t\t\t\t\tflock($lock_fh, LOCK_SH);\n+\t\t\t\t});\n+\t\t\t};\n+\t\t\t$self->_handle_error($@) if $@;\n+\t\t\t# closing lockfile releases readers lock\n+\t\t\tflock($lock_fh, LOCK_UN);\n+\t\t\tclose $lock_fh\n+\t\t\t\tor $self->_handle_error(\"Could't close lockfile '$lockfile': $!\");\n+\n+\t\t}\n+\t} until (@result || $lock_state);\n+\t# repeat until we have data, or we tried generating data oneself and failed\n+\treturn @result;\n+}\n+\n+\n+1;\n+__END__\n+# end of package GitwebCache::FileCacheWithLocking;\ndiff --git a/t/t9511-gitweb-caching-interface.sh b/t/t9511-gitweb-caching-interface.sh\nnew file mode 100755\nindex 0000000..d8fc946\n--- /dev/null\n+++ b/t/t9511-gitweb-caching-interface.sh\n@@ -0,0 +1,34 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2010 Jakub Narebski\n+#\n+\n+test_description='gitweb caching interface\n+\n+This test checks caching interface used in gitweb caching, and caching\n+infrastructure (GitwebCache::* modules).'\n+\n+# for now we are running only cache interface tests\n+. ./test-lib.sh\n+\n+# this test is present in gitweb-lib.sh\n+if ! test_have_prereq PERL; then\n+\tskip_all='perl not available, skipping test'\n+\ttest_done\n+fi\n+\n+\"$PERL_PATH\" -MTest::More -e 0 >/dev/null 2>&1 || {\n+\tskip_all='perl module Test::More unavailable, skipping test'\n+\ttest_done\n+}\n+\n+# ----------------------------------------------------------------------\n+\n+# The external test will outputs its own plan\n+test_external_has_tap=1\n+\n+test_external \\\n+\t'GitwebCache::*Cache* Perl API (in gitweb/lib/)' \\\n+\t\"$PERL_PATH\" \"$TEST_DIRECTORY\"/t9511/test_cache_interface.pl\n+\n+test_done\ndiff --git a/t/t9511/test_cache_interface.pl b/t/t9511/test_cache_interface.pl\nnew file mode 100755\nindex 0000000..a2b006c\n--- /dev/null\n+++ b/t/t9511/test_cache_interface.pl\n@@ -0,0 +1,381 @@\n+#!/usr/bin/perl\n+use lib (split(/:/, $ENV{GITPERLLIB}));\n+\n+use warnings;\n+use strict;\n+\n+use POSIX qw(dup2);\n+use Fcntl qw(:DEFAULT);\n+use IO::Handle;\n+use IO::Select;\n+use IO::Pipe;\n+use File::Basename;\n+\n+use Test::More;\n+\n+# test installed version or source version\n+use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n+\n+\n+# Test creating a cache\n+#\n+BEGIN { use_ok('GitwebCache::FileCacheWithLocking'); }\n+note(\"Using lib '$INC[0]'\");\n+note(\"Testing '$INC{'GitwebCache/FileCacheWithLocking.pm'}'\");\n+\n+my $cache = new_ok('GitwebCache::FileCacheWithLocking');\n+\n+# Test that default values are defined\n+#\n+ok(defined $GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_ROOT,\n+\t'$GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_ROOT defined');\n+ok(defined $GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_DEPTH,\n+\t'$GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_DEPTH defined');\n+\n+# Test some accessors and some default values for cache\n+#\n+SKIP: {\n+\tskip 'default values not defined', 2\n+\t\tunless ($GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_ROOT &&\n+\t\t        $GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_DEPTH);\n+\n+\tcmp_ok($cache->get_root(),  'eq', $GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_ROOT,\n+\t\t\"default cache root is '$GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_ROOT'\");\n+\tcmp_ok($cache->get_depth(), '==', $GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_DEPTH,\n+\t\t\"default cache depth is $GitwebCache::FileCacheWithLocking::DEFAULT_CACHE_DEPTH\");\n+}\n+\n+# Test the getting and setting of a cached value,\n+# and removal of a cached value\n+#\n+my $key   = 'Test Key';\n+my $value = 'Test Value';\n+\n+my $call_count = 0;\n+sub get_value_fh {\n+\tmy $fh = shift;\n+\t$call_count++;\n+\tprint {$fh} $value;\n+}\n+\n+# use ->compute_fh($key, $code_fh) interface\n+sub cache_compute_fh {\n+\tmy ($cache, $key, $code_fh) = @_;\n+\n+\tmy ($fh, $filename) = $cache->compute_fh($key, $code_fh);\n+\treturn unless $fh;\n+\n+\tlocal $/ = undef;\n+\treturn <$fh>;\n+}\n+\n+# use ->get_fh($key) interface\n+sub cache_get_fh {\n+\tmy ($cache, $key) = @_;\n+\n+\tmy ($fh, $filename) = $cache->get_fh($key);\n+\treturn unless $fh;\n+\n+\tlocal $/ = undef;\n+\treturn <$fh>;\n+}\n+\n+# use ->set_coderef_fh($key, $code_fh) to set $key to $value\n+sub cache_set_fh {\n+\tmy ($cache, $key, $value) = @_;\n+\n+\t$cache->set_coderef_fh($key, sub { print {$_[0]} $value });\n+\treturn $value;\n+}\n+\n+subtest 'compute_fh interface' => sub {\n+\tforeach my $method (qw(remove compute_fh)) {\n+\t\tcan_ok($cache, $method);\n+\t}\n+\n+\teval { $cache->remove('Not-Existent Key'); };\n+\tok(!$@, 'remove on non-existent key doesn\\'t die');\n+\tdiag($@) if $@;\n+\n+\t$cache->remove($key); # just in case\n+\tis(cache_compute_fh($cache, $key, \\&get_value_fh), $value,\n+\t   \"compute_fh 1st time (set) returns '$value'\");\n+\tis(cache_compute_fh($cache, $key, \\&get_value_fh), $value,\n+\t   \"compute_fh 2nd time (get) returns '$value'\");\n+\tis(cache_compute_fh($cache, $key, \\&get_value_fh), $value,\n+\t   \"compute_fh 3rd time (get) returns '$value'\");\n+\tcmp_ok($call_count, '==', 1, 'get_value_fh() is called once from compute_fh');\n+\n+\tdone_testing();\n+};\n+\n+\n+# Test cache expiration\n+#\n+subtest 'cache expiration' => sub {\n+\t$cache->set_expires_in(60*60*24); # set expire time to 1 day\n+\tcmp_ok($cache->get_expires_in(), '>', 0, '\"expires in\" is greater than 0 (set to 1d)');\n+\t$call_count = 0;\n+\tcache_compute_fh($cache, $key, \\&get_value_fh);\n+\tcmp_ok($call_count, '==', 0, 'compute_fh didn\\'t need to compute data (not expired in 1d)');\n+\tis(cache_get_fh($cache, $key), $value, 'get_fh returns cached value (not expired in 1d)');\n+\n+\t$cache->set_expires_in(-1); # set expire time to never expire\n+\tis($cache->get_expires_in(), -1,         '\"expires in\" is set to never (-1)');\n+\tis(cache_get_fh($cache, $key), $value,   'get returns cached value (not expired)');\n+\n+\t$cache->set_expires_in(0);\n+\tis($cache->get_expires_in(),  0,         '\"expires in\" is set to now (0)');\n+\tok(!defined(cache_get_fh($cache, $key)), 'cache is expired, get_fh returns undef');\n+\tcache_compute_fh($cache, $key, \\&get_value_fh);\n+\tcmp_ok($call_count, '==', 1,             'compute_fh computed and set data');\n+\n+\tdone_testing();\n+};\n+\n+\n+# ----------------------------------------------------------------------\n+# CONCURRENT ACCESS\n+sub parallel_run (&); # forward declaration of prototype\n+\n+# Test 'stampeding herd' / 'cache miss stampede' problem\n+#\n+my $slow_time = 1; # how many seconds to sleep in mockup of slow generation\n+sub get_value_slow_fh {\n+\tmy $fh = shift;\n+\n+\t$call_count++;\n+\tsleep $slow_time;\n+\tprint {$fh} $value;\n+}\n+sub get_value_die {\n+\t$call_count++;\n+\tdie \"get_value_die\\n\";\n+}\n+my $lock_file = \"$0.$$.lock\"; # if exists then get_value_die_once_fh was already called\n+sub get_value_die_once_fh {\n+\tif (sysopen my $lock_fh, $lock_file, (O_WRONLY | O_CREAT | O_EXCL)) {\n+\t\tclose $lock_fh;\n+\t\tdie \"get_value_die_once_fh\\n\";\n+\t} else {\n+\t\tget_value_slow_fh(@_);\n+\t}\n+}\n+\n+my @output;    # gathers output from concurrent invocations\n+my $sep = '|'; # separate different parts of data for tests\n+my $total_count = 0; # number of calls around all concurrent invocations\n+\n+note(\"Following tests contain artifical delay of $slow_time seconds\");\n+subtest 'parallel access' => sub {\n+\n+\t$cache->remove($key);\n+\t@output = parallel_run {\n+\t\t$call_count = 0;\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n+\t\tprint $data if defined $data;\n+\t\tprint \"$sep$call_count\";\n+\t};\n+\t$total_count = 0;\n+\tforeach (@output) {\n+\t\tmy ($child_out, $child_count) = split(quotemeta $sep, $_);\n+\t\t$total_count += $child_count;\n+\t}\n+\tcmp_ok($total_count, '==', 1, 'parallel compute_fh: get_value_slow_fh() called only once');\n+\t# extract only data, without child count\n+\t@output = map { s/\\Q$sep\\E.*$//; $_ } @output;\n+\tis_deeply(\n+\t\t\\@output,\n+\t\t[ ($value) x 2 ],\n+\t\t\"parallel compute_fh: both returned '$value'\"\n+\t);\n+\n+\t$cache->set_on_error(sub { die @_; });\n+\teval {\n+\t\tlocal $SIG{ALRM} = sub { die \"alarm\\n\"; };\n+\t\talarm 4*$slow_time;\n+\n+\t\t@output = parallel_run {\n+\t\t\t$call_count = 0;\n+\t\t\tmy $data = eval { cache_compute_fh($cache, 'No Key', \\&get_value_die); };\n+\t\t\tmy $eval_error = $@;\n+\t\t\tprint \"$data\" if defined $data;\n+\t\t\tprint \"$sep\";\n+\t\t\tprint \"$eval_error\" if $eval_error;\n+\t\t};\n+\t\tis_deeply(\n+\t\t\t\\@output,\n+\t\t\t[ ( \"${sep}get_value_die\\n\" ) x 2 ],\n+\t\t\t'parallel compute_fh: get_value_die() died in both'\n+\t\t);\n+\n+\t\talarm 0;\n+\t};\n+\tok(!$@, 'parallel compute_fh: no alarm call (neither process hung)');\n+\tdiag($@) if $@;\n+\n+\t$cache->remove($key);\n+\tunlink($lock_file);\n+\t@output = parallel_run {\n+\t\tmy $data = eval { cache_compute_fh($cache, $key, \\&get_value_die_once_fh); };\n+\t\tmy $eval_error = $@;\n+\t\tprint \"$data\" if defined $data;\n+\t\tprint \"$sep\";\n+\t\tprint \"$eval_error\" if $eval_error;\n+\t};\n+\tis_deeply(\n+\t\t[sort @output],\n+\t\t[sort (\"$value$sep\", \"${sep}get_value_die_once_fh\\n\")],\n+\t\t'parallel compute_fh: return correct value even if other process died'\n+\t);\n+\tunlink($lock_file);\n+\n+\tdone_testing();\n+};\n+\n+\n+# Test that cache returns stale data in existing but expired cache situation\n+#\n+my $stale_value = 'Stale Value';\n+\n+subtest 'serving stale data when regenerating' => sub {\n+\tcache_set_fh($cache, $key, $stale_value);\n+\t$cache->set_expires_in(-1);   # never expire, for next check\n+\tis(cache_get_fh($cache, $key), $stale_value,\n+\t   'stale value set (prepared) correctly');\n+\n+\t$call_count = 0;\n+\t$cache->set_expires_in(0);    # expire now (so there are no fresh data)\n+\t$cache->set_max_lifetime(-1); # forever (always serve stale data)\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n+\t\tprint \"$call_count$sep\";\n+\t\tprint $data if defined $data;\n+\t};\n+\t# returning stale data works\n+\tis_deeply(\n+\t\t[sort @output],\n+\t\t[sort (\"0$sep$stale_value\", \"1$sep$value\")],\n+\t\t'no background: stale data returned by one process (the one not generating data)'\n+\t);\n+\t$cache->set_expires_in(-1); # never expire for next ->get\n+\tis(cache_get_fh($cache, $key), $value,\n+\t   'no background: value got set correctly, even if stale data returned');\n+\n+\n+\tcache_set_fh($cache, $key, $stale_value);\n+\t$cache->set_expires_in(0);   # expire now\n+\t$cache->set_max_lifetime(0); # don't serve stale data\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n+\t\tprint $data;\n+\t};\n+\t# no returning stale data\n+\tok(!scalar(grep { $_ eq $stale_value } @output),\n+\t   'no stale data if configured');\n+\n+\n+\tdone_testing();\n+};\n+$cache->set_expires_in(-1);\n+\n+\n+done_testing();\n+\n+\n+#######################################################################\n+#######################################################################\n+#######################################################################\n+\n+# from http://aaroncrane.co.uk/talks/pipes_and_processes/\n+sub fork_child (&) {\n+\tmy ($child_process_code) = @_;\n+\n+\tmy $pid = fork();\n+\tdie \"Failed to fork: $!\\n\" if !defined $pid;\n+\n+\treturn $pid if $pid != 0;\n+\n+\t# Now we're in the new child process\n+\t$child_process_code->();\n+\texit;\n+}\n+\n+sub parallel_run (&) {\n+\tmy $child_code = shift;\n+\tmy $nchildren = 2;\n+\n+\tmy %children;\n+\tmy (%pid_for_child, %fd_for_child);\n+\tmy $sel = IO::Select->new();\n+\tforeach my $child_idx (1..$nchildren) {\n+\t\tmy $pipe = IO::Pipe->new()\n+\t\t\tor die \"Failed to create pipe: $!\\n\";\n+\n+\t\tmy $pid = fork_child {\n+\t\t\t$pipe->writer()\n+\t\t\t\tor die \"$$: Child \\$pipe->writer(): $!\\n\";\n+\t\t\tdup2(fileno($pipe), fileno(STDOUT))\n+\t\t\t\tor die \"$$: Child $child_idx failed to reopen stdout to pipe: $!\\n\";\n+\t\t\tclose $pipe\n+\t\t\t\tor die \"$$: Child $child_idx failed to close pipe: $!\\n\";\n+\n+\t\t\t# From Test-Simple-0.96/t/subtest/fork.t\n+\t\t\t#\n+\t\t\t# Force all T::B output into the pipe (redirected to STDOUT),\n+\t\t\t# for the parent builder as well as the current subtest builder.\n+\t\t\t{\n+\t\t\t\tno warnings 'redefine';\n+\t\t\t\t*Test::Builder::output         = sub { *STDOUT };\n+\t\t\t\t*Test::Builder::failure_output = sub { *STDOUT };\n+\t\t\t\t*Test::Builder::todo_output    = sub { *STDOUT };\n+\t\t\t}\n+\n+\t\t\t$child_code->();\n+\n+\t\t\t*STDOUT->flush();\n+\t\t\tclose(STDOUT);\n+\t\t};\n+\n+\t\t$pid_for_child{$pid} = $child_idx;\n+\t\t$pipe->reader()\n+\t\t\tor die \"Failed to \\$pipe->reader(): $!\\n\";\n+\t\t$fd_for_child{$pipe} = $child_idx;\n+\t\t$sel->add($pipe);\n+\n+\t\t$children{$child_idx} = {\n+\t\t\t'pid'    => $pid,\n+\t\t\t'stdout' => $pipe,\n+\t\t\t'output' => '',\n+\t\t};\n+\t}\n+\n+\twhile (my @ready = $sel->can_read()) {\n+\t\tforeach my $fh (@ready) {\n+\t\t\tmy $buf = '';\n+\t\t\tmy $nread = sysread($fh, $buf, 1024);\n+\n+\t\t\texists $fd_for_child{$fh}\n+\t\t\t\tor die \"Cannot find child for fd: $fh\\n\";\n+\n+\t\t\tif ($nread > 0) {\n+\t\t\t\t$children{$fd_for_child{$fh}}{'output'} .= $buf;\n+\t\t\t} else {\n+\t\t\t\t$sel->remove($fh);\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\twhile (%pid_for_child) {\n+\t\tmy $pid = waitpid -1, 0;\n+\t\twarn \"Child $pid_for_child{$pid} ($pid) failed with status: $?\\n\"\n+\t\t\tif $? != 0;\n+\t\tdelete $pid_for_child{$pid};\n+\t}\n+\n+\treturn map { $children{$_}{'output'} } keys %children;\n+}\n+\n+__END__\n"},{"id":"158510","messageId":"20101222235757.7998.65738.stgit@localhost.localdomain","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 8/9] gitweb/lib - Cache captured output (using compute_fh)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:57:58Z","receivedAt":"2010-12-22T23:57:58Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"\n\nAdd GitwebCache::CacheOutput package, which introduces cache_output\nsubroutine.  If data for given key is present in cache, then\ncache_output gets data from cache and prints it.  If data is not present\nin cache, then cache_output runs provided subroutine (code reference),\ncaptures its output, saves this output in cache, and prints it.\n\nIt requires that provided $cache supports ->capture_fh method, like\nGitwebCache::FileCacheWithLocking introduced in earlier commit, and that\nprovided $capture supports capturing to file or filehandle via\n->capture($code, $file) method, like GitwebCache::Capture::ToFile\nintroduced in some earlier commit.\n\nExceptions in $code should be thrown using 'die' (Perl exception\nmechanism); one can choose whether error output (output printed when\nexception is raised, before raising it) should be saved to cache or not.\nBy default error output is not cached.\n\nGitweb would use cache_output to get page from cache, or to generate\npage and save it to cache.  The die_error subroutine throws exception,\nwhich will be caught and by default rethrown; error pages would not be\ncached.\n\nIt is assumed that data is saved to cache _converted_, and should\ntherefore be read from cache and printed to STDOUT in ':raw' (binary)\nmode.\n\n\nAdd t9512/test_cache_output.pl test, run as external test in\nt9512-gitweb-cache.  It checks that cache_output behaves correctly,\nnamely that it saves and restores action output in cache, and that it\nprints generated output or cached output, depending on whether there\nexist data in cache.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\n gitweb/lib/GitwebCache/CacheOutput.pm    |   84 ++++++++++++++++\n t/t9512-gitweb-cache-output-interface.sh |   34 ++++++\n t/t9512/test_cache_output.pl             |  162 ++++++++++++++++++++++++++++++\n 3 files changed, 280 insertions(+), 0 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/CacheOutput.pm\n create mode 100755 t/t9512-gitweb-cache-output-interface.sh\n create mode 100755 t/t9512/test_cache_output.pl\n\n\ndiff --git a/gitweb/lib/GitwebCache/CacheOutput.pm b/gitweb/lib/GitwebCache/CacheOutput.pm\nnew file mode 100644\nindex 0000000..4a75a7f\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -0,0 +1,84 @@\n+# gitweb - simple web interface to track changes in git repositories\n+#\n+# (C) 2010, Jakub Narebski <jnareb@gmail.com>\n+# (C) 2006, John 'Warthog9' Hawley <warthog19@eaglescrag.net>\n+#\n+# This program is licensed under the GPLv2\n+\n+#\n+# Capturing and caching (gitweb) output\n+#\n+\n+# Capture output, save it in cache and print it, or retrieve it from\n+# cache and print it.\n+\n+package GitwebCache::CacheOutput;\n+\n+use strict;\n+use warnings;\n+\n+use File::Copy qw();\n+use Symbol qw(qualify_to_ref);\n+\n+use Exporter qw(import);\n+our @EXPORT      = qw(cache_output);\n+our %EXPORT_TAGS = (all => [ @EXPORT ]);\n+\n+# cache_output($cache, $capture, $key, $action_code, [ option => value ]);\n+#\n+# Attempts to get $key from $cache; if successful, prints the value.\n+# Otherwise, calls $action_code, capture its output using $capture,\n+# and use the captured output as the new value for $key in $cache,\n+# then print captured output.\n+#\n+# It is assumed that captured data is already converted and it is\n+# in ':raw' format (and thus restored in ':raw' from cache)\n+#\n+# Supported options:\n+# * -cache_errors => 0|1  - whether error output should be cached\n+sub cache_output {\n+\tmy ($cache, $capture, $key, $code, %opts) = @_;\n+\n+\tmy ($fh, $filename);\n+\tmy ($capture_fh, $capture_filename);\n+\teval { # this `eval` is to catch rethrown error, so we can print captured output\n+\t\t($fh, $filename) = $cache->compute_fh($key, sub {\n+\t\t\t($capture_fh, $capture_filename) = @_;\n+\n+\t\t\t# this `eval` is to be able to cache error output (up till 'die')\n+\t\t\teval { $capture->capture($code, $capture_fh); };\n+\n+\t\t\t# note that $cache can catch this error itself (like e.g. CHI);\n+\t\t\t# use \"die\"-ing error handler to rethrow this exception to outside\n+\t\t\tdie $@ if ($@ && ! $opts{'-cache_errors'});\n+\t\t});\n+\t};\n+\tmy $error = $@;\n+\n+\t# if an exception was rethrown, and not caught by caching engine (by $cache)\n+\t# then ->compute_fh will not set $fh nor $filename; use those used for capture\n+\tif (!defined $fh) {\n+\t\t$filename ||= $capture_filename;\n+\t}\n+\n+\tif (defined $fh || defined $filename) {\n+\t\t# set binmode only if $fh is defined (is a filehandle)\n+\t\t# File::Copy::copy opens files given by filename in binary mode\n+\t\tbinmode $fh,    ':raw' if (defined $h);\n+\t\tbinmode STDOUT, ':raw';\n+\t\tFile::Copy::copy($fh || $filename, \\*STDOUT);\n+\t}\n+\n+\t# rethrow error if captured in outer `eval` (i.e. no -cache_errors),\n+\t# removing temporary file (exception thrown out of cache)\n+\tif ($error) {\n+\t\tunlink $capture_filename\n+\t\t\tif (defined $capture_filename && -e $capture_filename);\n+\t\tdie $error;\n+\t}\n+\treturn;\n+}\n+\n+1;\n+__END__\n+# end of package GitwebCache::CacheOutput\ndiff --git a/t/t9512-gitweb-cache-output-interface.sh b/t/t9512-gitweb-cache-output-interface.sh\nnew file mode 100755\nindex 0000000..fb9525a\n--- /dev/null\n+++ b/t/t9512-gitweb-cache-output-interface.sh\n@@ -0,0 +1,34 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2010 Jakub Narebski\n+#\n+\n+test_description='gitweb cache\n+\n+This test checks GitwebCache::CacheOutput Perl module that is\n+responsible for capturing and caching gitweb output.'\n+\n+# for now we are running only cache interface tests\n+. ./test-lib.sh\n+\n+# this test is present in gitweb-lib.sh\n+if ! test_have_prereq PERL; then\n+\tskip_all='perl not available, skipping test'\n+\ttest_done\n+fi\n+\n+\"$PERL_PATH\" -MTest::More -e 0 >/dev/null 2>&1 || {\n+\tskip_all='perl module Test::More unavailable, skipping test'\n+\ttest_done\n+}\n+\n+# ----------------------------------------------------------------------\n+\n+# The external test will outputs its own plan\n+test_external_has_tap=1\n+\n+test_external \\\n+\t'GitwebCache::CacheOutput Perl API (in gitweb/lib/)' \\\n+\t\"$PERL_PATH\" \"$TEST_DIRECTORY\"/t9512/test_cache_output.pl\n+\n+test_done\ndiff --git a/t/t9512/test_cache_output.pl b/t/t9512/test_cache_output.pl\nnew file mode 100755\nindex 0000000..758848c\n--- /dev/null\n+++ b/t/t9512/test_cache_output.pl\n@@ -0,0 +1,162 @@\n+#!/usr/bin/perl\n+use lib (split(/:/, $ENV{GITPERLLIB}));\n+\n+use warnings;\n+use strict;\n+\n+use Test::More;\n+\n+# test source version\n+use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n+\n+# ....................................................................\n+\n+# prototypes must be known at compile time, otherwise they do not work\n+BEGIN { use_ok('GitwebCache::CacheOutput'); }\n+\n+require_ok('GitwebCache::FileCacheWithLocking');\n+require_ok('GitwebCache::Capture::ToFile');\n+\n+note(\"Using lib '$INC[0]'\");\n+note(\"Testing '$INC{'GitwebCache/CacheOutput.pm'}'\");\n+note(\"Testing '$INC{'GitwebCache/FileCacheWithLocking.pm'}'\");\n+note(\"Testing '$INC{'GitwebCache/Capture/ToFile.pm'}'\");\n+\n+\n+# Test setting up $cache and $capture\n+my ($cache, $capture);\n+subtest 'setup' => sub {\n+\t$cache   = new_ok('GitwebCache::FileCacheWithLocking' => [], 'The $cache  ');\n+\t$capture = new_ok('GitwebCache::Capture::ToFile'      => [], 'The $capture');\n+\n+\tdone_testing();\n+};\n+\n+# ......................................................................\n+\n+# Prepare for testing cache_output\n+my $key = 'Key';\n+my $action_output = <<'EOF';\n+# This is data to be cached and shown\n+EOF\n+my $cached_output = <<\"EOF\";\n+$action_output# (version recovered from cache)\n+EOF\n+my $call_count = 0;\n+sub action {\n+\t$call_count++;\n+\tprint $action_output;\n+}\n+\n+my $die_output = <<\"EOF\";\n+$action_output# (died)\n+EOF\n+sub die_action {\n+\tprint $die_output;\n+\tdie \"die_action\\n\";\n+}\n+\n+# Catch output printed by cache_output\n+sub capture_output_of_cache_output {\n+\tmy ($code, @args) = @_;\n+\n+\tGitwebCache::Capture::ToFile->new()->capture(sub {\n+\t\tcache_output($cache, $capture, $key, $code, @args);\n+\t}, 'actual');\n+\n+\treturn get_actual();\n+}\n+\n+sub get_actual {\n+\topen my $fh, '<', 'actual' or return;\n+\tlocal $/ = undef;\n+\tmy $result = <$fh>;\n+\tclose $fh;\n+\treturn $result;\n+}\n+\n+# use ->get_fh($key) interface\n+sub cache_get_fh {\n+\tmy ($cache, $key) = @_;\n+\n+\tmy ($fh, $filename) = $cache->get_fh($key);\n+\treturn unless $fh;\n+\n+\tlocal $/ = undef;\n+\treturn <$fh>;\n+}\n+\n+# use ->set_coderef_fh($key, $code_fh) to set $key to $value\n+sub cache_set_fh {\n+\tmy ($cache, $key, $value) = @_;\n+\n+\t$cache->set_coderef_fh($key, sub { print {$_[0]} $value });\n+\treturn $value;\n+}\n+\n+\n+# ......................................................................\n+\n+# clean state\n+$cache->set_expires_in(-1);\n+$cache->remove($key);\n+my $test_data;\n+\n+# first time (if there is no cache) generates cache entry\n+subtest '1st time (generate data)' => sub {\n+\t$call_count = 0;\n+\t$test_data = capture_output_of_cache_output(\\&action);\n+\tis($test_data,                 $action_output, 'action() output is printed');\n+\tis(cache_get_fh($cache, $key), $action_output, 'action() output is saved in cache');\n+\tcmp_ok($call_count, '==', 1, 'action() was called to generate data');\n+\n+\tdone_testing();\n+};\n+\n+# second time (if cache is set/valid) reads from cache\n+subtest '2nd time (retreve from cache)' => sub {\n+\tcache_set_fh($cache, $key, $cached_output);\n+\t$call_count = 0;\n+\t$test_data = capture_output_of_cache_output(\\&action);\n+\tis(cache_get_fh($cache, $key), $cached_output, 'correct value is prepared in cache');\n+\tis($test_data,                 $cached_output, 'output is printed from cache');\n+\tcmp_ok($call_count, '==', 0, 'action() was not called');\n+\n+\tdone_testing();\n+};\n+\n+# caching output and error handling\n+subtest 'errors (exceptions) are not cached by default' => sub {\n+\t$cache->remove($key);\n+\tok(!defined cache_get_fh($cache, $key), 'cache is prepared correctly (no data in cache)');\n+\teval {\n+\t\t$test_data = capture_output_of_cache_output(\\&die_action);\n+\t};\n+\tmy $error = $@;\n+\t$test_data = get_actual();\n+\tis($test_data, $die_output,             'output of an error is printed');\n+\tok(!defined cache_get_fh($cache, $key), 'output is not captured and not cached');\n+\tlike($error, qr/^die_action\\n/m,        'exception made it to outside, correctly');\n+\n+\tdone_testing();\n+};\n+\n+subtest 'errors are cached with -cache_errors => 1' => sub {\n+\t$cache->remove($key);\n+\tok(!defined cache_get_fh($cache, $key), 'cache is prepared correctly (no data in cache)');\n+\teval {\n+\t\t$test_data = capture_output_of_cache_output(\\&die_action, -cache_errors => 1);\n+\t};\n+\tmy $error = $@;\n+\t$test_data = get_actual();\n+\tis($test_data,                 $die_output, 'output of an error is printed');\n+\tis(cache_get_fh($cache, $key), $die_output, 'output is captured and cached');\n+\tok(! $error, 'exception didn\\'t made it to outside');\n+\tdiag($error) if $error;\n+\n+\tdone_testing();\n+};\n+\n+\n+done_testing();\n+__END__\n"},{"id":"158511","messageId":"20101222235823.7998.15358.stgit@localhost.localdomain","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 9/9] gitweb: Add optional output caching","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-22T23:58:24Z","receivedAt":"2010-12-22T23:58:24Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"\n\nThis commit actually adds output caching to gitweb, as we have now\nminimal features required for it in GitwebCache::FileCacheWithLocking\n(a 'dumb' but fast file-based cache engine).  To enable cache you need\n(at least) set $caching_enabled to true in gitweb config, and copy\nrequired modules alongside generated gitweb.cgi - this is described\nin more detail in the new \"Gitweb caching\" section in gitweb/README.\n\"make install-gitweb\" would install all modules alongside gitweb\nitself.\n\nCapturing and caching is designed in such way that there is no\nbehaviour change if $caching_enabled is false.  If caching is not\nenabled, then capturing is also turned off.\n\nEnabling caching causes the following additional changes to gitweb\noutput:\n* Disables content-type negotiation (choosing between 'text/html'\n  mimetype and 'application/xhtml+xml') when caching, as there is no\n  content-type negotiation done when retrieving page from cache.\n  Use lowest common denominator of 'text/html' mimetype which can\n  be used by all browsers.  This may change in the future.\n* Disable optional timing info (how much time it took to generate the\n  original page, and how many git commands it took), and in its place show\n  unconditionally when page was originally generated (in GMT / UTC\n  timezone).\n* Disable 'blame_incremental' view, as it doesn't make sense without\n  printing data as soon as it is generated (which would require tee-ing\n  when capturing output for caching)... and it doesn't work currently\n  anyway.  Alternate solution would be to run 'blame_incremental' view\n  with caching disabled.\n\n\nAdd basic tests of caching support to t9500-gitweb-standalone-no-errors\ntest: set $caching_enabled to true and check for errors for first time\nrun (generating cache) and second time run (retrieving from cache) for a\nsingle view - summary view for a project.\n\nCheck in the t9501-gitweb-standalone-http-status test that gitweb at\nleast correctly handles \"404 Not Found\" error pages also in the case\nwhen gitweb caching is enabled.\n\nCheck in the t9502-gitweb-standalone-parse-output test that gitweb\nproduces the same output with and without caching, for first and\nsecond run, with binary or text output.\n\nAll those tests make use of new gitweb_enable_caching subroutine added\nto gitweb-lib.sh\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\n\n gitweb/Makefile                           |    5 +\n gitweb/README                             |   46 +++++++\n gitweb/gitweb.perl                        |  190 ++++++++++++++++++++++++++---\n gitweb/lib/GitwebCache/CacheOutput.pm     |    2 \n t/gitweb-lib.sh                           |   11 ++\n t/t9500-gitweb-standalone-no-errors.sh    |   20 +++\n t/t9501-gitweb-standalone-http-status.sh  |   13 ++\n t/t9502-gitweb-standalone-parse-output.sh |   33 +++++\n 8 files changed, 299 insertions(+), 21 deletions(-)\n mode change 100644 => 100755 t/gitweb-lib.sh\n\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex e6029e1..d67c138 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -113,6 +113,11 @@ endif\n \n GITWEB_FILES += static/git-logo.png static/git-favicon.png\n \n+# gitweb output caching\n+GITWEB_MODULES += GitwebCache/CacheOutput.pm\n+GITWEB_MODULES += GitwebCache/SimpleFileCache.pm\n+GITWEB_MODULES += GitwebCache/Capture/Simple.pm\n+\n GITWEB_REPLACE = \\\n \t-e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \\\n \t-e 's|++GIT_BINDIR++|$(bindir)|g' \\\ndiff --git a/gitweb/README b/gitweb/README\nindex 4a67393..efe3b2c 100644\n--- a/gitweb/README\n+++ b/gitweb/README\n@@ -258,6 +258,12 @@ not include variables usually directly set during build):\n    their default values before every request, so if you want to change\n    them, be sure to set this variable to true or a code reference effecting\n    the desired changes.  The default is true.\n+ * $caching_enabled\n+   If true, gitweb would use caching to speed up generating response.\n+   Currently supported is only output (response) caching.  See \"Gitweb caching\"\n+   section below for details on how to configure and customize caching.\n+   The default is false (caching is disabled).\n+\n \n Projects list file format\n ~~~~~~~~~~~~~~~~~~~~~~~~~\n@@ -329,6 +335,46 @@ You can use the following files in repository:\n    descriptions.\n \n \n+Gitweb caching\n+~~~~~~~~~~~~~~\n+\n+Currently gitweb supports only output (HTTP response) caching, similar\n+to the one used on http://git.kernel.org.  To turn it on, set \n+$caching_enabled variable to true value in gitweb config file, i.e.:\n+\n+   our $caching_enabled = 1;\n+\n+You can choose which caching engine should gitweb use by setting\n+$cache variable to _initialized_ instance of cache interface, or to\n+the name of cache class.\n+\n+Currenly though only cache which implements non-standard ->compute_fh()\n+method is supported.  Provided GitwebCache::FileCacheWithLocking implements\n+this method; it is the default caching engine used if $cache is not defined.\n+\n+The GitwebCache::FileCacheWithLocking is 'dumb' (but fast) file based\n+caching engine, currently without any support for cache size limiting, or\n+even removing expired / grossly expired entries.  It has therefore the\n+downside of requiring a huge amount of disk space if there are a number of\n+repositories involved.  It is not uncommon for git.kernel.org to have on the\n+order of 80G - 120G accumulate over the course of a few months.  It is\n+therefore recommended that the cache directory be periodically completely\n+deleted; this operation is safe to perform.  Suggested mechanism (substitute\n+$cachedir for actual path to gitweb cache):\n+\n+   # mv $cachedir $cachedir.flush && mkdir $cachedir && rm -rf $cachedir.flush\n+\n+Site-wide cache options are defined in %cache_options hash.  Those options\n+apply only when $cache is unset (GitwebCache::FileCacheWithLocking is used),\n+or if $cache is name of cache class.  You can override cache options in\n+gitweb config, e.g.:\n+\n+   $cache_options{'expires_in'} = 60; # 60 seconds = 1 minute\n+\n+Please read comments for %cache_options entries in gitweb/gitweb.perl for\n+description of available cache options.\n+\n+\n Webserver configuration\n -----------------------\n \ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 880fdf2..eb02b6b 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -268,6 +268,71 @@ our %highlight_ext = (\n \tmap { $_ => 'xml' } qw(xhtml html htm),\n );\n \n+\n+# This enables/disables the caching layer in gitweb.  Currently supported\n+# is only output (response) caching, similar to the one used on git.kernel.org.\n+our $caching_enabled = 0;\n+# Set to _initialized_ instance of cache interface implementing (for now)\n+# compute_fh($key, $code) method (non-standard CHI-inspired interface),\n+# or to name of class of cache interface implementing said method.\n+# If unset, GitwebCache::FileCacheWithLocking would be used, which is 'dumb'\n+# (but fast) file based caching layer, currently without any support for\n+# cache size limiting.  It is therefore recommended that the cache directory\n+# be periodically completely deleted; this operation is safe to perform.\n+#\n+# Suggested mechanism to clear cache:\n+#   mv $cachedir $cachedir.flush && mkdir $cachedir && rm -rf $cachedir.flush\n+# where $cachedir is directory where cache is, i.e. $cache_options{'cache_root'}\n+our $cache;\n+# You define site-wide cache options defaults here; override them with\n+# $GITWEB_CONFIG as necessary.\n+our %cache_options = (\n+\t# The location in the filesystem that will hold the root of the cache.\n+\t# This directory will be created as needed (if possible) on the first\n+\t# cache set.  Note that either this directory must exists and web server\n+\t# has to have write permissions to it, or web server must be able to\n+\t# create this directory.\n+\t# Possible values:\n+\t# * 'cache' (relative to gitweb),\n+\t# * File::Spec->catdir(File::Spec->tmpdir(), 'gitweb-cache'),\n+\t# * '/var/cache/gitweb' (FHS compliant, requires being set up),\n+\t'cache_root' => 'cache',\n+\n+\t# The number of subdirectories deep to cache object item.  This should be\n+\t# large enough that no cache directory has more than a few hundred\n+\t# objects.  Each non-leaf directory contains up to 256 subdirectories\n+\t# (00-ff).  Must be larger than 0.\n+\t'cache_depth' => 1,\n+\n+\t# The (global) expiration time for objects placed in the cache, in seconds.\n+\t'expires_in' => 20,\n+\n+\t# How to handle runtime errors occurring during cache gets and cache\n+\t# sets.  Options are:\n+\t#  * \"die\" (the default) - call die() with an appropriate message\n+\t#  * \"warn\" - call warn() with an appropriate message\n+\t#  * \"ignore\" - do nothing\n+\t#  * <coderef> - call this code reference with an appropriate message\n+\t# Note that gitweb catches 'die <message>' via custom handle_errors_html\n+\t# handler, set via set_message() from CGI::Carp.  'warn <message>' are\n+\t# written to web server logs.\n+\t#\n+\t# The default is to use cache_error_handler, which wraps die_error.\n+\t# Only first argument passed to cache_error_handler is used (c.f. CHI)\n+\t'on_error' => \\&cache_error_handler,\n+\n+\t# Extra options passed to GitwebCache::CacheOutput::cache_output subroutine\n+\t'cache_output' => {\n+\t\t# Enable caching of error pages (boolean).  Default is false.\n+\t\t'-cache_errors' => 0,\n+\t},\n+);\n+# Set to _initialized_ instance of GitwebCache::Capture::ToFile\n+# compatibile capturing engine, i.e. one implementing ->new()\n+# constructor, and ->capture($code, $file) method.  If unset\n+# (default), the GitwebCache::Capture::ToFile would be used.\n+our $capture;\n+\n # You define site-wide feature defaults here; override them with\n # $GITWEB_CONFIG as necessary.\n our %feature = (\n@@ -1121,7 +1186,16 @@ sub dispatch {\n \t    !$project) {\n \t\tdie_error(400, \"Project needed\");\n \t}\n-\t$actions{$action}->();\n+\n+\tif ($caching_enabled) {\n+\t\t# human readable key identifying gitweb output\n+\t\tmy $output_key = href(-replay => 1, -full => 1, -path_info => 0);\n+\n+\t\tcache_output($cache, $capture, $output_key, $actions{$action},\n+\t\t\t%{$cache_options{'cache_output'}});\n+\t} else {\n+\t\t$actions{$action}->();\n+\t}\n }\n \n sub reset_timer {\n@@ -1147,6 +1221,8 @@ sub run_request {\n \t\t}\n \t}\n \tcheck_loadavg();\n+\tconfigure_caching()\n+\t\tif ($caching_enabled);\n \n \t# $projectroot and $projects_list might be set in gitweb config file\n \t$projects_list ||= $projectroot;\n@@ -1210,7 +1286,7 @@ sub run {\n \t\t\tif $pre_dispatch_hook;\n \n \t\teval { run_request() };\n-\t\tif (defined $@ && !ref($@)) {\n+\t\tif ($@ && !ref($@)) {\n \t\t\t# some Perl error, but not one thrown by die_error\n \t\t\tdie_error(undef, undef, $@, -error_handler => 1);\n \t\t}\n@@ -1227,6 +1303,49 @@ sub run {\n \t1;\n }\n \n+sub configure_caching {\n+\tif (!eval { require GitwebCache::CacheOutput; 1; }) {\n+\t\tdie_error(500,\n+\t\t\t\"Caching enabled and error loading GitwebCache::CacheOutput\",\n+\t\t\tesc_html($@));\n+\n+\t\t# turn off caching and warn instead\n+\t\t#$caching_enabled = 0;\n+\t\t#warn \"Caching enabled and GitwebCache::CacheOutput not found\";\n+\t}\n+\tGitwebCache::CacheOutput->import();\n+\n+\t# $cache might be initialized (instantiated) cache, i.e. cache object,\n+\t# or it might be name of class, or it might be undefined\n+\tunless (defined $cache && ref($cache)) {\n+\t\t$cache ||= 'GitwebCache::FileCacheWithLocking';\n+\t\teval \"require $cache\";\n+\t\tif ($@) {\n+\t\t\tdie_error(500,\n+\t\t\t\t\"Error loading $cache\",\n+\t\t\t\tesc_html($@));\n+\t\t}\n+\n+\t\t$cache = $cache->new({\n+\t\t\t%cache_options,\n+\t\t\t#'cache_root' => '/tmp/cache',\n+\t\t\t#'cache_depth' => 2,\n+\t\t\t#'expires_in' => 20, # in seconds (CHI compatibile)\n+\t\t\t# (Cache::Cache compatibile initialization)\n+\t\t\t'default_expires_in' => $cache_options{'expires_in'},\n+\t\t\t# (CHI compatibile initialization)\n+\t\t\t'root_dir' => $cache_options{'cache_root'},\n+\t\t\t'depth' => $cache_options{'cache_depth'},\n+\t\t\t'on_get_error' => $cache_options{'on_error'},\n+\t\t\t'on_set_error' => $cache_options{'on_error'},\n+\t\t});\n+\t}\n+\tunless (defined $capture && ref($capture)) {\n+\t\trequire GitwebCache::Capture::ToFile;\n+\t\t$capture = GitwebCache::Capture::ToFile->new();\n+\t}\n+}\n+\n run();\n \n if (defined caller) {\n@@ -3597,7 +3716,9 @@ sub git_header_html {\n \t# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.\n \t# we have to do this because MSIE sometimes globs '*/*', pretending to\n \t# support xhtml+xml but choking when it gets what it asked for.\n-\tif (defined $cgi->http('HTTP_ACCEPT') &&\n+\t# Disable content-type negotiation when caching (use mimetype good for all).\n+\tif (!$caching_enabled &&\n+\t    defined $cgi->http('HTTP_ACCEPT') &&\n \t    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\\s|^)application\\/xhtml\\+xml(,|;|\\s|$)/ &&\n \t    $cgi->Accept('application/xhtml+xml') != 0) {\n \t\t$content_type = 'application/xhtml+xml';\n@@ -3622,7 +3743,9 @@ sub git_header_html {\n EOF\n \t# the stylesheet, favicon etc urls won't work correctly with path_info\n \t# unless we set the appropriate base URL\n-\tif ($ENV{'PATH_INFO'}) {\n+\t# if caching is enabled we can get it from cache for path_info when it\n+\t# is generated without path_info\n+\tif ($ENV{'PATH_INFO'} || $caching_enabled) {\n \t\tprint \"<base href=\\\"\".esc_url($base_url).\"\\\" />\\n\";\n \t}\n \t# print out each stylesheet that exist, providing backwards capability\n@@ -3739,17 +3862,25 @@ sub git_footer_html {\n \t}\n \tprint \"</div>\\n\"; # class=\"page_footer\"\n \n-\tif (defined $t0 && gitweb_check_feature('timed')) {\n+\t# timing info doesn't make much sense with output (response) caching,\n+\t# so when caching is enabled gitweb prints the time of page generation\n+\tif ((defined $t0 || $caching_enabled) &&\n+\t    gitweb_check_feature('timed')) {\n \t\tprint \"<div id=\\\"generating_info\\\">\\n\";\n-\t\tprint 'This page took '.\n-\t\t      '<span id=\"generating_time\" class=\"time_span\">'.\n-\t\t      tv_interval($t0, [ gettimeofday() ]).\n-\t\t      ' seconds </span>'.\n-\t\t      ' and '.\n-\t\t      '<span id=\"generating_cmd\">'.\n-\t\t      $number_of_git_cmds.\n-\t\t      '</span> git commands '.\n-\t\t      \" to generate.\\n\";\n+\t\tif ($caching_enabled) {\n+\t\t\tprint 'This page was generated at '.\n+\t\t\t      gmtime( time() ).\" GMT\\n\";\n+\t\t} else {\n+\t\t\tprint 'This page took '.\n+\t\t\t      '<span id=\"generating_time\" class=\"time_span\">'.\n+\t\t\t      tv_interval($t0, [ gettimeofday() ]).\n+\t\t\t      ' seconds </span>'.\n+\t\t\t      ' and '.\n+\t\t\t      '<span id=\"generating_cmd\">'.\n+\t\t\t      $number_of_git_cmds.\n+\t\t\t      '</span> git commands '.\n+\t\t\t      \" to generate.\\n\";\n+\t\t}\n \t\tprint \"</div>\\n\"; # class=\"page_footer\"\n \t}\n \n@@ -3758,8 +3889,8 @@ sub git_footer_html {\n \t}\n \n \tprint qq!<script type=\"text/javascript\" src=\"!.esc_url($javascript).qq!\"></script>\\n!;\n-\tif (defined $action &&\n-\t    $action eq 'blame_incremental') {\n+\tif (!$caching_enabled &&\n+\t    defined $action && $action eq 'blame_incremental') {\n \t\tprint qq!<script type=\"text/javascript\">\\n!.\n \t\t      qq!startBlame(\"!. href(action=>\"blame_data\", -replay=>1) .qq!\",\\n!.\n \t\t      qq!           \"!. href() .qq!\");\\n!.\n@@ -3800,6 +3931,7 @@ sub die_error {\n \t\t500 => '500 Internal Server Error',\n \t\t503 => '503 Service Unavailable',\n \t);\n+\n \tgit_header_html($http_responses{$status}, undef, %opts);\n \tprint <<EOF;\n <div class=\"page_body\">\n@@ -3819,6 +3951,22 @@ EOF\n \t\tunless ($opts{'-error_handler'});\n }\n \n+# custom error handler for caching engine (Internal Server Error)\n+sub cache_error_handler {\n+\tmy $error = shift;\n+\n+\t# just rethrow error that came from die_error\n+\t# thrown from $actions{$action}->()\n+\tdie $error if (ref $error);\n+\n+\t$error = to_utf8($error);\n+\t$error =\n+\t\t\"Error in caching layer: <i>\".ref($cache).\"</i><br>\\n\".\n+\t\tCGI::escapeHTML($error);\n+\t# die_error() would exit\n+\tdie_error(undef, undef, $error);\n+}\n+\n ## ----------------------------------------------------------------------\n ## functions printing or outputting HTML: navigation\n \n@@ -5554,7 +5702,8 @@ sub git_tag {\n \n sub git_blame_common {\n \tmy $format = shift || 'porcelain';\n-\tif ($format eq 'porcelain' && $cgi->param('js')) {\n+\tif ($format eq 'porcelain' && $cgi->param('js') &&\n+\t    !$caching_enabled) {\n \t\t$format = 'incremental';\n \t\t$action = 'blame_incremental'; # for page title etc\n \t}\n@@ -5608,7 +5757,8 @@ sub git_blame_common {\n \t\t\tor print \"ERROR $!\\n\";\n \n \t\tprint 'END';\n-\t\tif (defined $t0 && gitweb_check_feature('timed')) {\n+\t\tif (!$caching_enabled &&\n+\t\t    defined $t0 && gitweb_check_feature('timed')) {\n \t\t\tprint ' '.\n \t\t\t      tv_interval($t0, [ gettimeofday() ]).\n \t\t\t      ' '.$number_of_git_cmds;\n@@ -5628,7 +5778,7 @@ sub git_blame_common {\n \t\t$formats_nav .=\n \t\t\t$cgi->a({-href => href(action=>\"blame\", javascript=>0, -replay=>1)},\n \t\t\t        \"blame\") . \" (non-incremental)\";\n-\t} else {\n+\t} elsif (!$caching_enabled) {\n \t\t$formats_nav .=\n \t\t\t$cgi->a({-href => href(action=>\"blame_incremental\", -replay=>1)},\n \t\t\t        \"blame\") . \" (incremental)\";\n@@ -5787,7 +5937,7 @@ sub git_blame {\n }\n \n sub git_blame_incremental {\n-\tgit_blame_common('incremental');\n+\tgit_blame_common(!$caching_enabled ? 'incremental' : undef);\n }\n \n sub git_blame_data {\ndiff --git a/gitweb/lib/GitwebCache/CacheOutput.pm b/gitweb/lib/GitwebCache/CacheOutput.pm\nindex 4a75a7f..792ddb7 100644\n--- a/gitweb/lib/GitwebCache/CacheOutput.pm\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -64,7 +64,7 @@ sub cache_output {\n \tif (defined $fh || defined $filename) {\n \t\t# set binmode only if $fh is defined (is a filehandle)\n \t\t# File::Copy::copy opens files given by filename in binary mode\n-\t\tbinmode $fh,    ':raw' if (defined $h);\n+\t\tbinmode $fh,    ':raw' if (defined $fh);\n \t\tbinmode STDOUT, ':raw';\n \t\tFile::Copy::copy($fh || $filename, \\*STDOUT);\n \t}\ndiff --git a/t/gitweb-lib.sh b/t/gitweb-lib.sh\nold mode 100644\nnew mode 100755\nindex b9bb95f..4ce067f\n--- a/t/gitweb-lib.sh\n+++ b/t/gitweb-lib.sh\n@@ -52,6 +52,17 @@ EOF\n \texport SCRIPT_NAME\n }\n \n+gitweb_enable_caching () {\n+\ttest_expect_success 'enable caching' '\n+\t\tcat >>gitweb_config.perl <<-\\EOF &&\n+\t\t$caching_enabled = 1;\n+\t\t$cache_options{\"expires_in\"} = -1;      # never expire cache for tests\n+\t\t$cache_options{\"cache_root\"} = \"cache\"; # to clear the right thing\n+\t\tEOF\n+\t\trm -rf cache/\n+\t'\n+}\n+\n gitweb_run () {\n \tGATEWAY_INTERFACE='CGI/1.1'\n \tHTTP_ACCEPT='*/*'\ndiff --git a/t/t9500-gitweb-standalone-no-errors.sh b/t/t9500-gitweb-standalone-no-errors.sh\nindex 21cd286..cc9cee5 100755\n--- a/t/t9500-gitweb-standalone-no-errors.sh\n+++ b/t/t9500-gitweb-standalone-no-errors.sh\n@@ -677,4 +677,24 @@ test_expect_success HIGHLIGHT \\\n \t gitweb_run \"p=.git;a=blob;f=test.sh\"'\n test_debug 'cat gitweb.log'\n \n+# ----------------------------------------------------------------------\n+# caching\n+\n+gitweb_enable_caching\n+\n+test_expect_success \\\n+\t'caching enabled (project summary, first run, generating cache)' \\\n+\t'gitweb_run \"p=.git;a=summary\"'\n+test_debug 'cat gitweb.log'\n+\n+test_expect_success \\\n+\t'caching enabled (project summary, second run, cached version)' \\\n+\t'gitweb_run \"p=.git;a=summary\"'\n+test_debug 'cat gitweb.log'\n+\n+test_expect_success \\\n+\t'caching enabled (non-existent commit, not cached error page)' \\\n+\t'gitweb_run \"p=.git;a=commit;h=non-existent\"'\n+test_debug 'cat gitweb.log'\n+\n test_done\ndiff --git a/t/t9501-gitweb-standalone-http-status.sh b/t/t9501-gitweb-standalone-http-status.sh\nindex 2487da1..168e494 100755\n--- a/t/t9501-gitweb-standalone-http-status.sh\n+++ b/t/t9501-gitweb-standalone-http-status.sh\n@@ -134,5 +134,18 @@ cat >>gitweb_config.perl <<\\EOF\n our $maxload = undef;\n EOF\n \n+# ----------------------------------------------------------------------\n+# output caching\n+\n+gitweb_enable_caching\n+\n+test_expect_success 'caching enabled (non-existent commit, 404 error)' '\n+\tgitweb_run \"p=.git;a=commit;h=non-existent\" &&\n+\tgrep \"Status: 404 Not Found\" gitweb.headers &&\n+\tgrep \"404 - Unknown commit object\" gitweb.body\n+'\n+test_debug 'echo \"headers\" && cat gitweb.headers'\n+test_debug 'echo \"body\"    && cat gitweb.body'\n+\n \n test_done\ndiff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh\nindex dd83890..bc8cb92 100755\n--- a/t/t9502-gitweb-standalone-parse-output.sh\n+++ b/t/t9502-gitweb-standalone-parse-output.sh\n@@ -112,4 +112,37 @@ test_expect_success 'snapshot: hierarchical branch name (xx/test)' '\n '\n test_debug 'cat gitweb.headers'\n \n+\n+# ----------------------------------------------------------------------\n+# whether gitweb with caching enabled produces the same output\n+\n+test_expect_success 'setup for caching tests (utf8 commit, binary file)' '\n+\t. \"$TEST_DIRECTORY\"/t3901-utf8.txt &&\n+\tcp \"$TEST_DIRECTORY\"/test9200a.png image.png &&\n+\tgit add image.png &&\n+\tgit commit -F \"$TEST_DIRECTORY\"/t3900/1-UTF-8.txt &&\n+\tgitweb_run \"p=.git;a=patch\" &&\n+\tmv gitweb.body no_cache.html &&\n+\tgitweb_run \"p=.git;a=blob_plain;f=image.png\" &&\n+\tmv gitweb.body no_cache.png\n+'\n+\n+gitweb_enable_caching\n+\n+for desc in 'generating cache' 'cached version'; do\n+\ttest_expect_success \"caching enabled, HTML output, $desc\" '\n+\t\tgitweb_run \"p=.git;a=patch\" &&\n+\t\tmv gitweb.body cache.html &&\n+\t\ttest_cmp no_cache.html cache.html\n+\t'\n+done\n+\n+for desc in 'generating cache' 'cached version'; do\n+\ttest_expect_success \"caching enabled, binary output, $desc\" '\n+\t\tgitweb_run \"p=.git;a=blob_plain;f=image.png\" &&\n+\t\tmv gitweb.body cache.png &&\n+\t\tcmp no_cache.png cache.png\n+\t'\n+done\n+\n test_done\n"},{"id":"158517","messageId":"20101223015540.GA14585@burratino","threadId":"26127","inReplyTo":"20101222235459.7998.43333.stgit@localhost.localdomain","subject":"Re: [RFC PATCH v7 1/9] gitweb: Go to DONE_REQUEST rather than DONE_GITWEB in die_error","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-12-23T01:55:40Z","receivedAt":"2010-12-23T01:55:40Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Jakub Narebski wrote:\n\n> End the request after die_error finishes, rather than exiting gitweb\n> instance\n[...]\n> --- a/gitweb/gitweb.perl\n> +++ b/gitweb/gitweb.perl\n> @@ -1169,6 +1169,7 @@ sub run {\n>  \n>  \t\trun_request();\n>  \n> +\tDONE_REQUEST:\n>  \t\t$post_dispatch_hook->()\n>  \t\t\tif $post_dispatch_hook;\n>  \t\t$first_request = 0;\n> @@ -3767,7 +3768,7 @@ EOF\n\n[side note: the \"@@ EOF\" line above would say \"@@ sub die_error {\" if\nuserdiff.c had perl support and gitattributes used it.]\n\n>  \tprint \"</div>\\n\";\n>  \n>  \tgit_footer_html();\n> -\tgoto DONE_GITWEB\n> +\tgoto DONE_REQUEST\n>  \t\tunless ($opts{'-error_handler'});\n\nThis seems to remove the last user of the DONE_GITWEB label.  Why not\ndelete the label, too?\n\nWhen die_error is called by CGI::Carp (via handle_errors_html), it\ndoes not rearm the error handler afaict.  Previously that did not\nmatter because die_error kills gitweb; now should it be set up\nagain?\n\ndie_error gets called when server load is too high; I wonder whether\nit is right to go back for another request in that case.\n\nA broken per-request (or other) configuration could potentially leave\na gitweb process in a broken state, and until now the state would be\nreset on the first error.  I wonder if escape valve would be needed\n--- e.g., does the CGI harness take care of starting a new gitweb\nprocess after every couple hundred requests or so?\n\nAside from those (minor) worries, this patch seems like a good idea.\n"},{"id":"158518","messageId":"20101223020801.GB14585@burratino","threadId":"26127","inReplyTo":"20101222235525.7998.99816.stgit@localhost.localdomain","subject":"Re: [RFC PATCH v7 2/9] gitweb: use eval + die for error (exception) handling","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-12-23T02:08:01Z","receivedAt":"2010-12-23T02:08:01Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Jakub Narebski wrote:\n\n> Gitweb assumes here that exceptions thrown by Perl would be simple\n> strings; die_error() throws hash reference (if not for minimal\n> extrenal dependencies, it would be probable object of Class::Exception\n> or Throwable class thrown).\n\nHmm, why not throw an object of new type Gitweb::Exception?\n\n> --- a/gitweb/gitweb.perl\n> +++ b/gitweb/gitweb.perl\n> @@ -1045,21 +1045,6 @@ sub configure_gitweb_features {\n>  \t}\n>  }\n>  \n> -# custom error handler: 'die <message>' is Internal Server Error\n> -sub handle_errors_html {\n> -\tmy $msg = shift; # it is already HTML escaped\n> -\n> -\t# to avoid infinite loop where error occurs in die_error,\n> -\t# change handler to default handler, disabling handle_errors_html\n> -\tset_message(\"Error occured when inside die_error:\\n$msg\");\n> -\n> -\t# you cannot jump out of die_error when called as error handler;\n> -\t# the subroutine set via CGI::Carp::set_message is called _after_\n> -\t# HTTP headers are already written, so it cannot write them itself\n> -\tdie_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);\n> -}\n> -set_message(\\&handle_errors_html);\n> -\n\nHoorah!\n\n>  # dispatch\n>  sub dispatch {\n>  \tif (!defined $action) {\n> @@ -1167,7 +1152,11 @@ sub run {\n>  \t\t$pre_dispatch_hook->()\n>  \t\t\tif $pre_dispatch_hook;\n>  \n> -\t\trun_request();\n> +\t\teval { run_request() };\n> +\t\tif (defined $@ && !ref($@)) {\n> +\t\t\t# some Perl error, but not one thrown by die_error\n> +\t\t\tdie_error(undef, undef, $@, -error_handler => 1);\n> +\t\t}\n\nThe !ref($@) seems overzealous, which is why I am wondering if it\nwould be possible to use bless() for a finer-grained check.\n\n>  \n>  \tDONE_REQUEST:\n>  \t\t$post_dispatch_hook->()\n> @@ -3768,7 +3757,8 @@ EOF\n>  \tprint \"</div>\\n\";\n>  \n>  \tgit_footer_html();\n> -\tgoto DONE_REQUEST\n> +\n> +\tdie {'status' => $status, 'error' => $error}\n>  \t\tunless ($opts{'-error_handler'});\n\nIs the DONE_REQUEST label still needed?\n\nThanks, I am happy to see the semantics becoming less thorny.\nJonathan\n"},{"id":"158550","messageId":"20101224092932.GA31537@burratino","threadId":"26127","inReplyTo":"20101222235618.7998.17447.stgit@localhost.localdomain","subject":"Re: [RFC PATCH v7 4/9] gitweb: Prepare for splitting gitweb","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-12-24T09:29:32Z","receivedAt":"2010-12-24T09:29:32Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Jakub Narebski wrote:\n\n> Prepare gitweb for having been split into modules that are to be\n> installed alongside gitweb in 'lib/' subdirectory, by adding\n> \n>   use lib __DIR__.'/lib';\n> \n> to gitweb.perl (to main gitweb script), and preparing for putting\n> modules (relative path) in $(GITWEB_MODULES) in gitweb/Makefile.\n\nSpelled out, this means modules would typically go in\n\n\t/usr/share/gitweb/lib\n\nIs that the right place?  I suspect something like\n\n\t/usr/lib/gitweb/\n\ncould make sense in some installations for two reasons:\n\n - even braindamaged webserver configurations would not serve lib/\n   as static files in that case;\n\n - if some modules are implemented in C for speed, they would need\n   to go in /usr/lib anyway to follow usual filesystem conventions.\n\nDoes the Makefile let us override the directory with such a setting?\n\n> While at it pass GITWEBLIBDIR in addition to GITWEB_TEST_INSTALLED to\n> allow testing installed version of gitweb and installed version of\n> modules (for future tests which would check individual (sub)modules).\n> \n> Using __DIR__ from Dir::Self module (not in core, that's why currently\n> gitweb includes excerpt of code from Dir::Self defining __DIR__) was\n> chosen over using FindBin-based solution (in core since perl 5.00307,\n> while gitweb itself requires at least perl 5.8.0) because FindBin uses\n> BEGIN block\n\nThis explanation and the code below leave me nervous that the answer\nmight be \"no\". ;-)\n\n[...]\n> --- a/gitweb/gitweb.perl\n> +++ b/gitweb/gitweb.perl\n> @@ -10,6 +10,14 @@\n>  use 5.008;\n>  use strict;\n>  use warnings;\n> +\n> +use File::Spec;\n> +# __DIR__ is taken from Dir::Self __DIR__ fragment\n> +sub __DIR__ () {\n> +\tFile::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);\n> +}\n> +use lib __DIR__ . '/lib';\n> +\n>  use CGI qw(:standard :escapeHTML -nosticky);\n>  use CGI::Util qw(unescape);\n>  use CGI::Carp qw(fatalsToBrowser);\n"},{"id":"158551","messageId":"20101224094934.GA952@burratino","threadId":"26127","inReplyTo":"20101222235705.7998.76695.stgit@localhost.localdomain","subject":"Re: [RFC PATCH v7 6/9] gitweb/lib - Simple output capture by redirecting STDOUT to file","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-12-24T09:49:34Z","receivedAt":"2010-12-24T09:49:34Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Jakub Narebski wrote:\n\n> This patch was based on \"gitweb: add output buffering and associated\n> functions\" patch by John 'Warthog9' Hawley (J.H.) in \"Gitweb caching v7\"\n> series, and on code of Capture::Tiny by David Golden (Apache License 2.0).\n\nMicronit: if the license of Capture::Tiny were relevant then we would be\nin trouble, I think.  (Apache-2.0 and GPLv2 aren't compatible licenses.)\nLuckily\n\n[...]\n> +# taken from Capture::Tiny by David Golden, Apache License 2.0\n> +# with debugging stripped out\n> +sub _relayer {\n> +\tmy ($fh, $layers) = @_;\n> +\n> +\tmy %seen = ( unix => 1, perlio => 1); # filter these out\n> +\tmy @unique = grep { !$seen{$_}++ } @$layers;\n> +\n> +\tbinmode($fh, join(\":\", \":raw\", @unique));\n> +}\n\nlooks trivial enough.  Maybe either avoiding mention of the license or\nclarifying that that is not intended to be the sole license for the\nstripped-down code would help?\n"},{"id":"158581","messageId":"201012252314.22541.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101223015540.GA14585@burratino","subject":"Re: [RFC PATCH v7 1/9] gitweb: Go to DONE_REQUEST rather than DONE_GITWEB in die_error","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-25T22:14:21Z","receivedAt":"2010-12-25T22:14:21Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Thu, 23 Dec 2010, Jonathan Nieder wrote:\n> Jakub Narebski wrote:\n> \n> > End the request after die_error finishes, rather than exiting gitweb\n> > instance\n> [...]\n> > --- a/gitweb/gitweb.perl\n> > +++ b/gitweb/gitweb.perl\n> > @@ -1169,6 +1169,7 @@ sub run {\n> >  \n> >  \t\trun_request();\n> >  \n> > +\tDONE_REQUEST:\n> >  \t\t$post_dispatch_hook->()\n> >  \t\t\tif $post_dispatch_hook;\n> >  \t\t$first_request = 0;\n> > @@ -3767,7 +3768,7 @@ EOF\n> \n> [side note: the \"@@ EOF\" line above would say \"@@ sub die_error {\" if\n> userdiff.c had perl support and gitattributes used it.]\n\nHmmm, I thought that git has Perl-specific diff driver (xfuncname), but\nI see that it doesn't.  The default funcname works quite well for Perl\ncode... with exception of here-documents (or rather their ending).\n\nBTW. do you know how such perl support should look like?\n\n> >  \tprint \"</div>\\n\";\n> >  \n> >  \tgit_footer_html();\n> > -\tgoto DONE_GITWEB\n> > +\tgoto DONE_REQUEST\n> >  \t\tunless ($opts{'-error_handler'});\n> \n> This seems to remove the last user of the DONE_GITWEB label.  Why not\n> delete the label, too?\n\nWell, actually this patch is in this series only for the label ;-)\n\nAnyway, I can simply drop this patch, and have next one in series\n(adding exception-based error handling, making die_error work like\n'die') delete DONE_GITWEB label...\n\n> When die_error is called by CGI::Carp (via handle_errors_html), it\n> does not rearm the error handler afaict.  Previously that did not\n> matter because die_error kills gitweb; now should it be set up\n> again?\n\nThanks, I missed this (but after examining it turns out to be a \nnon-issue).  That will teach me to leave code outside of run() \nsubroutine; one of reasons behind creating c2394fe (gitweb: Put all \nper-connection code in run() subroutine, 2010-05-07) was to clarify \ncode flow.\n\nA note: using set_message inside handle_errors_html was necessary \nbecause if there was a fatal error in die_error, then \nhandle_errors_html would be called recursively - this was fixed in \nCGI.pm 3.45, but we cannot rely on this; we cannot rely on having new \nenough version of CGI::Carp that supports set_die_handler either.\n\nBut actually handle_errors_html gets called only from fatalsToBrowser,\nwhich in turn gets called from CGI::Carp::die... which ends calling\nCODE::die (aka realdie), which ends CGI process anyway.\n\nThat is why die_error ends with\n\n\tgoto DONE_GITWEB\n\t\tunless ($opts{'-error_handler'});\n\ni.e. it doesn't goto DONE_GITWEB nor DONE_REQUEST if called from\nhandle_errors_html anyway.\n\n> die_error gets called when server load is too high; I wonder whether\n> it is right to go back for another request in that case.\n\nIf client (web browser) are requesting connection, we have to tell it\nsomething anyway.  Note that each request might serve different client.\nBut when the die_error(503, \"The load average on the server is too \nhigh\") doesn't generate load by itself, all should be all right.\n\n> \n> A broken per-request (or other) configuration could potentially leave\n> a gitweb process in a broken state, and until now the state would be\n> reset on the first error.  I wonder if escape valve would be needed\n> --- e.g., does the CGI harness take care of starting a new gitweb\n> process after every couple hundred requests or so?\n\n'die $@ if $@' would call CORE::die, which means it would end gitweb\nprocess.\n\nFor CGI server it doesn't matter anyway, as for each request the process\nis respawned anyway (together with respawning Perl interpreter), and I\nthink that ModPerl::Registry and FastCGI servers monitor process that it\nis to serve requests, and respawn it if/when it dies.\n \n> Aside from those (minor) worries, this patch seems like a good idea.\n \nThanks a lot for your comments.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"158582","messageId":"201012260017.56306.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101223020801.GB14585@burratino","subject":"Re: [RFC PATCH v7 2/9] gitweb: use eval + die for error (exception) handling","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-25T23:17:55Z","receivedAt":"2010-12-25T23:17:55Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Thu, 23 Dec 2010, Jonathan Nieder wrote:\n> Jakub Narebski wrote:\n> \n> > Gitweb assumes here that exceptions thrown by Perl would be simple\n> > strings; die_error() throws hash reference (if not for minimal\n> > external dependencies, it would be probable object of Class::Exception\n> > or Throwable class thrown).\n> \n> Hmm, why not throw an object of new type Gitweb::Exception?\n\nFirst, 'gitweb: Prepare for splitting gitweb' commit is only later in\nseries... ;-) but that of course is not a serious issue.\n\nSecond, more important is that I'd rather gitweb doesn't go \"reinvent\nthe wheel\" route.  I'd rather (re)use Exception::Class (like e.g. \nSVN::Web does it) if we go the OO exception handling route.\n\nBut if we are going to use Exception::Class, then we can also use\nTry::Tiny, I think.\n\n> > --- a/gitweb/gitweb.perl\n> > +++ b/gitweb/gitweb.perl\n> > @@ -1045,21 +1045,6 @@ sub configure_gitweb_features {\n> >  \t}\n> >  }\n> >  \n> > -# custom error handler: 'die <message>' is Internal Server Error\n> > -sub handle_errors_html {\n> > -\tmy $msg = shift; # it is already HTML escaped\n> > -\n> > -\t# to avoid infinite loop where error occurs in die_error,\n> > -\t# change handler to default handler, disabling handle_errors_html\n> > -\tset_message(\"Error occured when inside die_error:\\n$msg\");\n> > -\n> > -\t# you cannot jump out of die_error when called as error handler;\n> > -\t# the subroutine set via CGI::Carp::set_message is called _after_\n> > -\t# HTTP headers are already written, so it cannot write them itself\n> > -\tdie_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);\n> > -}\n> > -set_message(\\&handle_errors_html);\n> > -\n> \n> Hoorah!\n\nYeah, that is very nice.\n\n> >  # dispatch\n> >  sub dispatch {\n> >  \tif (!defined $action) {\n> > @@ -1167,7 +1152,11 @@ sub run {\n> >  \t\t$pre_dispatch_hook->()\n> >  \t\t\tif $pre_dispatch_hook;\n> >  \n> > -\t\trun_request();\n> > +\t\teval { run_request() };\n> > +\t\tif (defined $@ && !ref($@)) {\n\nOoops, it should be 'if ($@ ...)', not 'if (defined $@ ...)'.\n\n> > +\t\t\t# some Perl error, but not one thrown by die_error\n> > +\t\t\tdie_error(undef, undef, $@, -error_handler => 1);\n> > +\t\t}\n> \n> The !ref($@) seems overzealous, which is why I am wondering if it\n> would be possible to use bless() for a finer-grained check.\n\nYou meant Scalar::Util::blessed here, isn't it? Fortunately Scalar::Util\nis core Perl module.\n\nBy 'overzealous' do you mean here possibility of catching what we \nshouldn't, i.e. non-gitweb error (not thrown by die_error)?  We can\nnarrow it to \"ref($@) eq 'HASH'\", but I don't think it would be ever\nnecessary: Perl throws string exceptions.\n\n> >  \n> >  \tDONE_REQUEST:\n> >  \t\t$post_dispatch_hook->()\n> > @@ -3768,7 +3757,8 @@ EOF\n> >  \tprint \"</div>\\n\";\n> >  \n> >  \tgit_footer_html();\n> > -\tgoto DONE_REQUEST\n> > +\n> > +\tdie {'status' => $status, 'error' => $error}\n> >  \t\tunless ($opts{'-error_handler'});\n> \n> Is the DONE_REQUEST label still needed?\n\nNo it isn't.\n\n> Thanks, I am happy to see the semantics becoming less thorny.\n\nNow I should check if this doesn't affect gitweb performance too badly.\nIIRC I have chosen 'goto DONE_GITWEB' because I didn't know about \nModPerl::Registry redefining 'exit' (why it was done), and because of\nsome microbenchmark showing that it performs better than die/eval (why\nthis specific solution)...\n\nBut I think that the performance hit would be negligible in practice;\nmaking gitweb more maintainable is I think worth the cost.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"158583","messageId":"20101226090731.GA21588@burratino","threadId":"26127","inReplyTo":"201012252314.22541.jnareb@gmail.com","subject":"[RFC/PATCH] diff: funcname and word patterns for perl","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-12-26T09:07:31Z","receivedAt":"2010-12-26T09:07:31Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"The default function name discovery already works quite well for Perl\ncode... with the exception of here-documents (or rather their ending).\n\n sub foo {\n\tprint <<END\n here-document\n END\n\treturn 1;\n }\n\nThe default funcname pattern treats the unindented END line as a\nfunction declaration and puts it in the @@ line of diff and \"grep\n--show-function\" output.\n\nWith a little knowledge of perl syntax, we can do better.  You can\ntry it out by adding \"*.perl diff=perl\" to the gitattributes file.\n\nSigned-off-by: Jonathan Nieder <jrnieder@gmail.com>\n---\nJakub Narebski wrote:\n\n> BTW. do you know how such perl support should look like?\n\nMaybe something like this?\n\n Documentation/gitattributes.txt |    2 ++\n t/t4018-diff-funcname.sh        |    2 +-\n userdiff.c                      |   15 +++++++++++++++\n 3 files changed, 18 insertions(+), 1 deletions(-)\n\ndiff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt\nindex 5a7f936..e59b878 100644\n--- a/Documentation/gitattributes.txt\n+++ b/Documentation/gitattributes.txt\n@@ -494,6 +494,8 @@ patterns are available:\n \n - `pascal` suitable for source code in the Pascal/Delphi language.\n \n+- `perl` suitable for source code in the Perl language.\n+\n - `php` suitable for source code in the PHP language.\n \n - `python` suitable for source code in the Python language.\ndiff --git a/t/t4018-diff-funcname.sh b/t/t4018-diff-funcname.sh\nindex 0a61b57..3646930 100755\n--- a/t/t4018-diff-funcname.sh\n+++ b/t/t4018-diff-funcname.sh\n@@ -32,7 +32,7 @@ EOF\n \n sed 's/beer\\\\/beer,\\\\/' < Beer.java > Beer-correct.java\n \n-builtin_patterns=\"bibtex cpp csharp fortran html java objc pascal php python ruby tex\"\n+builtin_patterns=\"bibtex cpp csharp fortran html java objc pascal perl php python ruby tex\"\n for p in $builtin_patterns\n do\n \ttest_expect_success \"builtin $p pattern compiles\" '\ndiff --git a/userdiff.c b/userdiff.c\nindex 2d54536..fc2afe3 100644\n--- a/userdiff.c\n+++ b/userdiff.c\n@@ -61,6 +61,21 @@ PATTERNS(\"pascal\",\n \t \"|[-+0-9.e]+|0[xXbB]?[0-9a-fA-F]+\"\n \t \"|<>|<=|>=|:=|\\\\.\\\\.\"\n \t \"|[^[:space:]]|[\\x80-\\xff]+\"),\n+PATTERNS(\"perl\",\n+\t \"^[ \\t]*package .*;\\n\"\n+\t \"^[ \\t]*sub .* \\\\{\",\n+\t /* -- */\n+\t \"[[:alpha:]_'][[:alnum:]_']*\"\n+\t \"|0[xb]?[0-9a-fA-F_]*\"\n+\t /* taking care not to interpret 3..5 as (3.)(.5) */\n+\t \"|[0-9a-fA-F_]+(\\\\.[0-9a-fA-F_]+)?([eE][-+]?[0-9_]+)?\"\n+\t \"|=>|-[rwxoRWXOezsfdlpSugkbctTBMAC>]|~~|::\"\n+\t \"|&&=|\\\\|\\\\|=|//=|\\\\*\\\\*=\"\n+\t \"|&&|\\\\|\\\\||//|\\\\+\\\\+|--|\\\\*\\\\*|\\\\.\\\\.\\\\.?\"\n+\t \"|[-+*/%.^&<>=!|]=\"\n+\t \"|=~|!~\"\n+\t \"|<<|<>|<=>|>>\"\n+\t \"|[^[:space:]]\"),\n PATTERNS(\"php\",\n \t \"^[\\t ]*(((public|protected|private|static)[\\t ]+)*function.*)$\\n\"\n \t \"^[\\t ]*(class.*)$\",\n-- \n1.7.2.3.554.gc9b5c.dirty\n"},{"id":"158584","messageId":"20101226095054.GB21588@burratino","threadId":"26127","inReplyTo":"201012252314.22541.jnareb@gmail.com","subject":"Re: [RFC PATCH v7 1/9] gitweb: Go to DONE_REQUEST rather than DONE_GITWEB in die_error","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-12-26T09:50:54Z","receivedAt":"2010-12-26T09:50:54Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Jakub Narebski wrote:\n> On Thu, 23 Dec 2010, Jonathan Nieder wrote:\n\n>> This seems to remove the last user of the DONE_GITWEB label.  Why not\n>> delete the label, too?\n>\n> Well, actually this patch is in this series only for the label ;-)\n>\n> Anyway, I can simply drop this patch, and have next one in series\n> (adding exception-based error handling, making die_error work like\n> 'die') delete DONE_GITWEB label...\n\nI like the current order (first the brief patch to change the\nsemantics, then the more ambitious change to an eval {} based error\nhandling implementation), but it doesn't matter so much.\n\n>> die_error gets called when server load is too high; I wonder whether\n>> it is right to go back for another request in that case.\n>\n> If client (web browser) are requesting connection, we have to tell it\n> something anyway.\n\nRight, I should have thought a few seconds more.  Respawning\ngitweb.perl would generate _more_ load[1].\n\n>> A broken per-request (or other) configuration could potentially leave\n>> a gitweb process in a broken state,\n[...]\n> 'die $@ if $@' would call CORE::die, which means it would end gitweb\n> process.\n\nThis is referring to a later patch?\n\n> For CGI server it doesn't matter anyway, as for each request the process\n> is respawned anyway (together with respawning Perl interpreter), and I\n> think that ModPerl::Registry and FastCGI servers monitor process that it\n> is to serve requests, and respawn it if/when it dies.\n\nSorry, that was unclear of me.  I meant that buggy configuration could\nleave a gitweb process in buggy but alive state and frequent failing\nrequests might be a way to notice that.  Contrived example (just to\nillustrate what I mean):\n\n\tour $version .= \".custom\";\n\tif (length $version >= 1000) {\t# untested, buggy code goes here.\n\t\t@diff_opts = (\"--nonsense\");\n\t}\n\nI think I was not right to worry about this, either.  It is better to\nmake such unusual and buggy configurations as noticeable as possible\nso they can be fixed.\n\n[...]\n> But actually handle_errors_html gets called only from fatalsToBrowser,\n> which in turn gets called from CGI::Carp::die... which ends calling\n> CODE::die (aka realdie), which ends CGI process anyway.\n>\n> That is why die_error ends with\n> \n>\tgoto DONE_GITWEB\n>\t\tunless ($opts{'-error_handler'});\n> \n> i.e. it doesn't goto DONE_GITWEB nor DONE_REQUEST if called from\n> handle_errors_html anyway.\n[...]\n> Thanks a lot for your comments.\n\nThanks for a thorough explanation.  For what it's worth, with or\nwithout removal of the DONE_GITWEB: label,\n\nReviewed-by: Jonathan Nieder <jrnieder@gmail.com>\n\n[1] I can imagine scenarios in which exiting gitweb would help\nalleviate the load, involving:\n\n - large memory footprint for each gitweb process forcing the system\n   into swapping (e.g., from a memory leak), or\n - FastCGI-like server noticing the load and choosing to decrease the\n   number of gitweb instances.\n\nIn the usual case, presumably gitweb memory footprint is small and\nFastCGI-like servers limit the number of gitweb instances to a modest\nfixed number.\n"},{"id":"158587","messageId":"20101226105441.GA27039@burratino","threadId":"26127","inReplyTo":"201012261143.33190.trast@student.ethz.ch","subject":"Re: [RFC/PATCH] diff: funcname and word patterns for perl","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-12-26T10:54:41Z","receivedAt":"2010-12-26T10:54:41Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Thomas Rast wrote:\n> Jonathan Nieder wrote:\n\n>> +\t \"|[^[:space:]]\"),\n>\n> I think it should get the |[\\x80-\\xff]+ arm, too.  That one was\n> designed to avoid splitting UTF-8 characters.  At the risk of gluing\n> together too many of them, of course, but I think confusing the\n> terminal would be worse.\n\nHmm.  Should it be\n\n\t|([\\x80-\\xff]+[\\x00-\\x7f])\n\nthen, to match exactly one multibyte UTF-8 character?\n"},{"id":"158588","messageId":"20101226112204.GA27124@burratino","threadId":"26127","inReplyTo":"201012261206.11942.trast@student.ethz.ch","subject":"Re: [RFC/PATCH] diff: funcname and word patterns for perl","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-12-26T11:22:04Z","receivedAt":"2010-12-26T11:22:04Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Thomas Rast wrote:\n\n> I just took the laziest (and most obvious) approach possible when I\n> wrote the original patterns.  I think the second most laziest one\n> would be to observe that bit patterns for leading characters are\n> always 11.., while those for continuation chars are 10..\n> \n> So that gives\n> \n>   |[\\xc0-\\xff][\\x80-\\xbf]+\n\nYes, that's what I was thinking of.  v2 will be a two-part series\nstarting with that.\n\nBTW, the perl token matcher is pretty half-hearted.  In part this is\nbecause \"only perl can parse perl\" [1] terrifies me and in part it is\nbecause I am too lazy to write down the state machine implied by\nPPI/Token/*.pm.\n\nIf some tokenization wizard would like to work on it, something like\nthe following might produce more pleasant word diffs:\n\n\t\"[%&$][[:space:]]*[0-9]+\"\t/* $1 */\n\t\"|[%&$][[:space:]]*([[:alpha:]_']|::)([[:alnum:]_']|::)*\"\t/* $var1 */\n\t\"|[%&$][[:space:]]*\\\\$([[:alnum:]_]|::)([[:alnum:]_']|::)*\"\t/* $$var1 */\n\t\"|[%&$][[:space:]]*\\\\$\\\\{\"     /* $${ introducing complicated expression */\n\t\"|[%&$][[:space:]]*\\\\$\\\\$\"     /* $$$ introducing complicated expression */\n\t\"|[%&$][[:space:]]*[^[:alnum:]_:'^$]\"\t/* $! */\n\t\"|[%&$][[:space:]]*\\\\^[][A-Z\\\\^_?]\"\t/* $^A */\n\t\"|[%&$][[:space:]]*\\\\{\\\\^[][A-Z\\\\^_?]\\\\}\"\t/* ${^A} */\n\t\"|[%&$][[:space:]]*\\\\{\\\\^[][A-Z\\\\^_?][[:alnum:]_]*\\\\}\" /* ${^Foo} */\n\t/* ${var} */\n\t\"|[%&$][[:space:]]*\\\\{[[:space:]]*([[:alpha:]_']|::)[[:alnum:]_:]*[[:space:]]\\\\}\"\n\t\"|[%&$][[:space:]]*\\\\{\"\t/* ${ introducing complicated expression */\n\t...\n\nthough it is an unmaintainable mess. :)\n\n[1] perl::toke.c and http://www.perlmonks.org/?node_id=44722\n"},{"id":"158600","messageId":"201012262325.51585.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101226095054.GB21588@burratino","subject":"Re: [RFC PATCH v7 1/9] gitweb: Go to DONE_REQUEST rather than DONE_GITWEB in die_error","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-26T22:25:50Z","receivedAt":"2010-12-26T22:25:50Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Sun, 26 Dec 2010, Jonathan Nieder wrote:\n> Jakub Narebski wrote:\n>> On Thu, 23 Dec 2010, Jonathan Nieder wrote:\n \n>>> die_error gets called when server load is too high; I wonder whether\n>>> it is right to go back for another request in that case.\n>>\n>> If client (web browser) are requesting connection, we have to tell it\n>> something anyway.\n> \n> Right, I should have thought a few seconds more.  Respawning\n> gitweb.perl would generate _more_ load[1].\n\n> [1] I can imagine scenarios in which exiting gitweb would help\n> alleviate the load, involving:\n> \n>  - large memory footprint for each gitweb process forcing the system\n>    into swapping (e.g., from a memory leak), or\n>  - FastCGI-like server noticing the load and choosing to decrease the\n>    number of gitweb instances.\n> \n> In the usual case, presumably gitweb memory footprint is small and\n> FastCGI-like servers limit the number of gitweb instances to a modest\n> fixed number.\n\nI assume that CGI / FastCGI / mod_perl (+ ModPerl::Registry) web server\nwould know how to regulate number of workers according to the server\nload.\n \n>>> A broken per-request (or other) configuration could potentially leave\n>>> a gitweb process in a broken state,\n> [...]\n>> 'die $@ if $@' would call CORE::die, which means it would end gitweb\n>> process.\n> \n> This is referring to a later patch?\n\nI'm sorry I haven't made myself clear.\n\nWhat I meant here is that gitweb includes the following code\n\n\tif (-e $GITWEB_CONFIG) {\n\t\tdo $GITWEB_CONFIG;\n\t\tdie $@ if $@;\n\t}\n\nwhich means that CGI::Carp::die is called, which might call \nhandle_errors_html, and which ends in CORE::die, which ends gitweb\nprocess.  So if there is no way for broken configuration to leave\ngitweb in a rboken state _at this point in series_.\n\nThank you for thinking about this, because it could cause problems\n(could because I have not checked if it does or if it doesn't) in the\nfollowing patch, when gitweb uses eval / die for error handling.\nThen it might happen when $per_request_config is false or CODE that\ninstead of trying to reread broken config on subsequent requests, we\nwill run with broken config.  It depends if \"die\"-ing in \nevaluate_gitweb_config would prevent setting $first_request to false.\nI'd have to check that.\n\n>> For CGI server it doesn't matter anyway, as for each request the process\n>> is respawned anyway (together with respawning Perl interpreter), and I\n>> think that ModPerl::Registry and FastCGI servers monitor process that it\n>> is to serve requests, and respawn it if/when it dies.\n> \n> Sorry, that was unclear of me.  I meant that buggy configuration could\n> leave a gitweb process in buggy but alive state and frequent failing\n> requests might be a way to notice that.  Contrived example (just to\n> illustrate what I mean):\n> \n> \tour $version .= \".custom\";\n> \tif (length $version>= 1000) {\t# untested, buggy code goes here.\n> \t\t@diff_opts = (\"--nonsense\");\n> \t}\n> \n> I think I was not right to worry about this, either.  It is better to\n> make such unusual and buggy configurations as noticeable as possible\n> so they can be fixed.\n\nSee above.\n\n> [...]\n>> But actually handle_errors_html gets called only from fatalsToBrowser,\n>> which in turn gets called from CGI::Carp::die... which ends calling\n>> CODE::die (aka realdie), which ends CGI process anyway.\n>>\n>> That is why die_error ends with\n>> \n>>\tgoto DONE_GITWEB\n>>\t\tunless ($opts{'-error_handler'});\n>> \n>> i.e. it doesn't goto DONE_GITWEB nor DONE_REQUEST if called from\n>> handle_errors_html anyway.\n> [...]\n>> Thanks a lot for your comments.\n\nWhich should make it in either commit message, or comments, I guess.\n \n> Thanks for a thorough explanation.  For what it's worth, with or\n> without removal of the DONE_GITWEB: label,\n> \n> Reviewed-by: Jonathan Nieder <jrnieder@gmail.com>\n\nThanks.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"158602","messageId":"201012262354.16955.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101224092932.GA31537@burratino","subject":"Re: [RFC PATCH v7 4/9] gitweb: Prepare for splitting gitweb","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-26T22:54:16Z","receivedAt":"2010-12-26T22:54:16Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Fri, 24 Dec 2010 10:29, Jonathan Nieder wrote:\n> Jakub Narebski wrote:\n> \n> > Prepare gitweb for having been split into modules that are to be\n> > installed alongside gitweb in 'lib/' subdirectory, by adding\n> > \n> >   use lib __DIR__.'/lib';\n> > \n> > to gitweb.perl (to main gitweb script), and preparing for putting\n> > modules (relative path) in $(GITWEB_MODULES) in gitweb/Makefile.\n> \n> Spelled out, this means modules would typically go in\n> \n> \t/usr/share/gitweb/lib\n\nYes, it's true.  It is mainly to support situation where one can install\nfiles in (subdirectory of) cgi-bin, but nowehere else.  That is why the\ndefault is to install modules alongside with gitweb.\n\nThe additional advantage is that t/gitweb-lib.sh used by gitweb tests\ncan very simply test source version of gitweb, with gitweb finding\nsource version of modules.  But it is not a very large obstacle to\nchange this.\n \n> Is that the right place?  I suspect something like\n> \n> \t/usr/lib/gitweb/\n> \n> could make sense in some installations for two reasons:\n> \n>  - even braindamaged webserver configurations would not serve lib/\n>    as static files in that case;\n\nActually it doesn't matter what web server does with those files when\naccessed directly, except for the client (user) confusion if he/she\ngoes where not invited.  Modules are used by Perl (by gitweb), not by\nweb server.\n\n> \n>  - if some modules are implemented in C for speed, they would need\n>    to go in /usr/lib anyway to follow usual filesystem conventions.\n\nUgh, XS!  I sincerely hope that when there would be decision to implement\nsome features in C for speed, we would be able to use Perl version of\nctypes for C-to-Perl interface, not XS.\n\nAnyway most probable to be implemented in C would be Git.pm, or rather\nPerl interface to libgit2.  It is probable that at some point gitweb\nwould be converted to use Git.pm or its successor.  But I guess that\nGit Perl module would be installed somewhere in PERL5LIB, so it would\nbe found even without  \"use lib __DIR__ . '/lib';\"  or its replacement.\n\n> Does the Makefile let us override the directory with such a setting?\n> \n\nI have thought that I did provide 'gitweblibdir' as configurable knob,\nbut I see that in the version I have send I don't do this:\n\n  # Shell quote;\n  bindir_SQ = $(subst ','\\'',$(bindir))#'\n  gitwebdir_SQ = $(subst ','\\'',$(gitwebdir))#'\n  gitwebstaticdir_SQ = $(subst ','\\'',$(gitwebdir)/static)#'\n  gitweblibdir_SQ = $(subst ','\\'',$(gitwebdir)/lib)#'\n\nBut if we are to allow custom gitweblibdir, we would have to change the\nway gitweb is to find its modules.  One solution would be inetad of\ncurrent\n\n  # __DIR__ is taken from Dir::Self __DIR__ fragment\n  sub __DIR__ () {\n  \tFile::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);\n  }\n  use lib __DIR__ . '/lib';\n\nuse simply\n\n  use lib $ENV{GITWEBLIBDIR} || \"++GITWEBLIBDIR++\";\n\nOf course both gitweb/Makefile and t/gitweb-lib.sh would have to be \nupdated: gitweb/Makefile to include replacement rule for '++GITWEBLIBDIR++'\nin GITWEB_REPLACE, and t/gitweb-lib.sh to declare and export GITWEBLIBDIR\nenvironmental variable so that gitweb/gitweb.perl would be able to find\nits modules when used for gitweb tests (see comment earlier).\n\n> > While at it pass GITWEBLIBDIR in addition to GITWEB_TEST_INSTALLED to\n> > allow testing installed version of gitweb and installed version of\n> > modules (for future tests which would check individual (sub)modules).\n> > \n> > Using __DIR__ from Dir::Self module (not in core, that's why currently\n> > gitweb includes excerpt of code from Dir::Self defining __DIR__) was\n> > chosen over using FindBin-based solution (in core since perl 5.00307,\n> > while gitweb itself requires at least perl 5.8.0) because FindBin uses\n> > BEGIN block\n> \n> This explanation and the code below leave me nervous that the answer\n> might be \"no\". ;-)\n\nNo it doesn't, but yes it could (see above).\n\n> [...]\n> > --- a/gitweb/gitweb.perl\n> > +++ b/gitweb/gitweb.perl\n> > @@ -10,6 +10,14 @@\n> >  use 5.008;\n> >  use strict;\n> >  use warnings;\n> > +\n> > +use File::Spec;\n> > +# __DIR__ is taken from Dir::Self __DIR__ fragment\n> > +sub __DIR__ () {\n> > +\tFile::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);\n> > +}\n> > +use lib __DIR__ . '/lib';\n> > +\n> >  use CGI qw(:standard :escapeHTML -nosticky);\n> >  use CGI::Util qw(unescape);\n> >  use CGI::Carp qw(fatalsToBrowser);\n\n\n-- \nJakub Narebski\nPoland\n"},{"id":"158603","messageId":"201012270003.23379.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101224094934.GA952@burratino","subject":"Re: [RFC PATCH v7 6/9] gitweb/lib - Simple output capture by redirecting STDOUT to file","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-26T23:03:22Z","receivedAt":"2010-12-26T23:03:22Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Fri, 24 Dec 2010, Jonathan Nieder wrote:\n> Jakub Narebski wrote:\n> \n> > This patch was based on \"gitweb: add output buffering and associated\n> > functions\" patch by John 'Warthog9' Hawley (J.H.) in \"Gitweb caching v7\"\n> > series, and on code of Capture::Tiny by David Golden (Apache License 2.0).\n> \n> Micronit: if the license of Capture::Tiny were relevant then we would be\n> in trouble, I think.  (Apache-2.0 and GPLv2 aren't compatible licenses.)\n\nDamn, I have thought that Apache-2.0 and GPLv2 are compatibile.  This is\nthe only reason that I explicitely mentioned the license (that and it is\nnot usual \"licensed like Perl\", i.e. dual Artistic Perl License / GPL \nlicensed).  I should have checked that Apache and GPLv2 are compatibile.\n\n> Luckily\n> \n> [...]\n> > +# taken from Capture::Tiny by David Golden, Apache License 2.0\n> > +# with debugging stripped out\n> > +sub _relayer {\n> > +\tmy ($fh, $layers) = @_;\n> > +\n> > +\tmy %seen = ( unix => 1, perlio => 1); # filter these out\n> > +\tmy @unique = grep { !$seen{$_}++ } @$layers;\n> > +\n> > +\tbinmode($fh, join(\":\", \":raw\", @unique));\n> > +}\n> \n> looks trivial enough.  Maybe either avoiding mention of the license or\n> clarifying that that is not intended to be the sole license for the\n> stripped-down code would help?\n\nYou are right.  I have done similar thing for PerlIO::Util based capture,\nthough I didn't know about the 'binmode($fh, join(\":\", \":raw\", @unique));'\ntrick.\n\nSo I think we would be in the clear by changing the comment to read:\n\n  +# see also _relayer in Capture::Tiny by David Golden\n\nor something like that.\n\n\nOr we can try to change gitweb license to GPLv3 / AGPLv3, which is\ncompatibile (one way only) with Apache-2.0... just kidding :-)\n-- \nJakub Narebski\nPoland\n"},{"id":"158604","messageId":"201012270014.09962.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101226090731.GA21588@burratino","subject":"Re: [RFC/PATCH] diff: funcname and word patterns for perl","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-26T23:14:09Z","receivedAt":"2010-12-26T23:14:09Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Sun, 26 Dec 2010 10:07, Jonathan Nieder wrote:\n> The default function name discovery already works quite well for Perl\n> code... with the exception of here-documents (or rather their ending).\n> \n>  sub foo {\n> \tprint <<END\n>  here-document\n>  END\n> \treturn 1;\n>  }\n> \n> The default funcname pattern treats the unindented END line as a\n> function declaration and puts it in the @@ line of diff and \"grep\n> --show-function\" output.\n> \n> With a little knowledge of perl syntax, we can do better.  You can\n> try it out by adding \"*.perl diff=perl\" to the gitattributes file.\n> \n> Signed-off-by: Jonathan Nieder <jrnieder@gmail.com>\n> ---\n> Jakub Narebski wrote:\n> \n> > BTW. do you know how such perl support should look like?\n> \n> Maybe something like this?\n\nThanks a lot.\n\n\nBesides here-doc, there are some tricky things that such code should\nbe aware about.\n\n1. BEGIN {\n   \t...\n   }\n\n   and similar code blocks (END, CHECK, INIT, ...) which I think should\n   be marked as 'BEGIN' in diff chunk.\n\n2. sub foo {\n    FOO: while (1) {\n   \t\t...\n   \t}\n   }\n\n   which should be marked with 'sub foo {', I think\n\n3. =head1 NAME\n\n   Git - Perl interface to the Git version control system\n\n   =cut\n\n   i.e. POD... which I don't know what to do about.\n\n\nI have not checked what your code does wrt those.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"158620","messageId":"7vmxnrxhgm.fsf@alter.siamese.dyndns.org","threadId":"26127","inReplyTo":"201012270014.09962.jnareb@gmail.com","subject":"Re: [RFC/PATCH] diff: funcname and word patterns for perl","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2010-12-27T17:18:17Z","receivedAt":"2010-12-27T17:18:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jakub Narebski <jnareb@gmail.com> writes:\n\n> 2. sub foo {\n>     FOO: while (1) {\n>    \t\t...\n>    \t}\n>    }\n>\n>    which should be marked with 'sub foo {', I think\n\nI do not think Jonathan's patterns would be fooled by this; it wants to\ncatch only \"package <anything>;\" and \"sub <anything> {\".\n\nJonathan's pattern set allows them to be indented, and followed by some\ngarbage at the end., which we might want to tighten.  How many people\nstart 'package' and the outermost 'sub' indented?\n\n userdiff.c |    6 ++++--\n 1 files changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/userdiff.c b/userdiff.c\nindex fc2afe3..79569c4 100644\n--- a/userdiff.c\n+++ b/userdiff.c\n@@ -62,8 +62,10 @@ PATTERNS(\"pascal\",\n \t \"|<>|<=|>=|:=|\\\\.\\\\.\"\n \t \"|[^[:space:]]|[\\x80-\\xff]+\"),\n PATTERNS(\"perl\",\n-\t \"^[ \\t]*package .*;\\n\"\n-\t \"^[ \\t]*sub .* \\\\{\",\n+\t \"^package .*;\\n\"\n+\t \"^sub .* \\\\{\\n\"\n+\t \"^[A-Z]+ \\\\{\\n\"\t/* BEGIN, END, ... */\n+\t \"^=head[0-9] \",\t/* POD */\n \t /* -- */\n \t \"[[:alpha:]_'][[:alnum:]_']*\"\n \t \"|0[xb]?[0-9a-fA-F_]*\"\n"},{"id":"158628","messageId":"201012272344.42657.jnareb@gmail.com","threadId":"26127","inReplyTo":"7vmxnrxhgm.fsf@alter.siamese.dyndns.org","subject":"Re: [RFC/PATCH] diff: funcname and word patterns for perl","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-27T22:44:41Z","receivedAt":"2010-12-27T22:44:41Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Mon, 27 Dec 2010, Junio C Hamano wrote:\n> Jakub Narebski <jnareb@gmail.com> writes:\n> \n> > 2. sub foo {\n> >     FOO: while (1) {\n> >    \t\t...\n> >    \t}\n> >    }\n> >\n> >    which should be marked with 'sub foo {', I think\n> \n> I do not think Jonathan's patterns would be fooled by this; it wants to\n> catch only \"package <anything>;\" and \"sub <anything> {\".\n\nAll right.\n\n> Jonathan's pattern set allows them to be indented, and followed by some\n> garbage at the end., which we might want to tighten.  How many people\n> start 'package' and the outermost 'sub' indented?\n> \n>  userdiff.c |    6 ++++--\n>  1 files changed, 4 insertions(+), 2 deletions(-)\n> \n> diff --git a/userdiff.c b/userdiff.c\n> index fc2afe3..79569c4 100644\n> --- a/userdiff.c\n> +++ b/userdiff.c\n> @@ -62,8 +62,10 @@ PATTERNS(\"pascal\",\n>  \t \"|<>|<=|>=|:=|\\\\.\\\\.\"\n>  \t \"|[^[:space:]]|[\\x80-\\xff]+\"),\n>  PATTERNS(\"perl\",\n> -\t \"^[ \\t]*package .*;\\n\"\n> -\t \"^[ \\t]*sub .* \\\\{\",\n> +\t \"^package .*;\\n\"\n\nNote that in future Perl 5.14 there would be 'package NAME {' form,\nso perhaps it would be better to future-proof and use\n\n  +\t \"^package .*[;{]\\n\"\n\n> +\t \"^sub .* \\\\{\\n\"\n\nUsing \"sub foo {\" is just a recommended programming convention (like e.g.\nGNU convention or K&R convention for C code).  I think it would be better\nto relax it a bit, either\n\n  +\t \"^sub \"\n\nor\n\n  +\t \"^sub .*( \\\\{)?\\n\"\n\n> +\t \"^[A-Z]+ \\\\{\\n\"\t/* BEGIN, END, ... */\n\nWe won't list possible block here?\n\n> +\t \"^=head[0-9] \",\t/* POD */\n>  \t /* -- */\n>  \t \"[[:alpha:]_'][[:alnum:]_']*\"\n>  \t \"|0[xb]?[0-9a-fA-F_]*\"\n\n-- \nJakub Narebski\nPoland\n"},{"id":"158629","messageId":"20101228035210.GA11735@sigill.intra.peff.net","threadId":"26127","inReplyTo":"7vmxnrxhgm.fsf@alter.siamese.dyndns.org","subject":"Re: [RFC/PATCH] diff: funcname and word patterns for perl","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2010-12-28T03:52:11Z","receivedAt":"2010-12-28T03:52:11Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Dec 27, 2010 at 09:18:17AM -0800, Junio C Hamano wrote:\n\n> Jonathan's pattern set allows them to be indented, and followed by some\n> garbage at the end., which we might want to tighten.  How many people\n> start 'package' and the outermost 'sub' indented?\n\nFWIW, I sometimes do:\n\n{\n  my $static;\n  sub foo {\n    ... use $static ...\n  }\n}\n\nso perhaps allowing whitespace in front of the keyword is worthwhile\nthere. I have never indented \"package\", though.\n\n-Peff\n"},{"id":"158792","messageId":"201012311903.15333.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 10/9] gitweb: Background cache generation and progress indicator","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-31T18:03:14Z","receivedAt":"2010-12-31T18:03:14Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This commit removes asymmetry in serving stale data (if stale data exists)\nwhen regenerating cache in GitwebCache::FileCacheWithLocking.  The process\nthat acquired exclusive (writers) lock, and is therefore selected to\nbe the one that (re)generates data to fill the cache, can now generate\ndata in background, while serving stale data.\n\nThose background processes are daemonized, i.e. detached from the main\nprocess (the one returning data or stale data).  Otherwise there might be a\nproblem when gitweb is running as (part of) long-lived process, for example\nfrom mod_perl or from FastCGI: it would leave unreaped children as zombies\n(entries in process table).  We don't want to wait for background process,\nand we can't set $SIG{CHLD} to 'IGNORE' in gitweb to automatically reap\nchild processes, because this interferes with using\n  open my $fd, '-|', git_cmd(), 'param', ...\n    or die_error(...)\n  # read from <$fd>\n  close $fd\n    or die_error(...)\nIn the above code \"close\" for magic \"-|\" open calls waitpid...  and we\nwould would die with \"No child processes\".  Removing 'or die' would\npossibly remove ability to react to other errors.\n\nThis feature can be enabled or disabled on demand via 'background_cache'\ncache parameter.  It is turned on by default.\n\n\nWhen there is no stale version suitable to serve the client, currently\nwe have to wait for the data to be generated in full before showing it.\nAdd to GitwebCache::FileCacheWithLocking, via 'generating_info' callback,\nthe ability to show user some activity indicator / progress bar, to\nshow that we are working on generating data.\n\nNote that without generating data in background, process generating\ndata wouldn't print progress info, because 'generating_info' can exit\n(and in the case of gitweb's git_generating_data_html does exit).\n\nWe don't need to daemonize background process in this case, where\nthere is no stale data to serve, but progress info is on.  This is\nbecause we have to wait for the background process to finish\ngenerating data anyway.\n\nGitweb itself uses \"Generating...\" page as activity indicator, which\nredirects (via <meta http-equiv=\"Refresh\" ...>) to refreshed version\nof the page after the cache is filled (via trick of not closing page\nand therefore not closing connection till data is available in cache).\nThe git_generating_data_html() subroutine, which is used by gitweb to\nimplement this feature, is highly configurable: you can choose\nfrequency of writing some data so that connection won't get closed,\nand maximum time to wait for data in \"Generating...\" page (see\ncomments in %generating_options hash definition), and initial delay\nbefore starting progress indicator page.\n\nThe git_generating_data_html() subroutine would return early (not showing\nHTML-base progress indicator) if action does not return HTML output, or\nif web browser / user agent is a robot / web crawler (or gitweb is run as\nstandalone script).  In such cases HTML \"Generating...\" page does not make\nmuch sense.\n\nFor this purpose new subroutine browser_is_robot() (which uses\nHTTP::BrowserDetect if possible, and fall backs on simple check of\nUser-Agent string) was added.\n\n\nThe default behavior of cache_output() from GitwebCache::CacheOutput was\nchanged so that it would cache error pages if they were generated in\ndetached background process.  This together with default initial delay in\ngit_generating_data_html progress_info subroutine should ensure that there\nare no problems with error pages and progress info interaction.\n\n\nThe t9511 test got updated to test both case with background generation\nenabled and case with background generation disabled.  Also adde test for\nsimple (not exiting) 'generating_info' subroutine, for both case with\nbackground generation disabled and enabled.\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThere are few changes included in this commit which should be fixed in\noriginal commits/patches earlier in the series.  Namely:\n\n* fix to gitweb/Makefile to use newer names for gitweb caching\n  modules, i.e. GitwebCache/FileCacheWithLocking.pm instead of\n  GitwebCache/SimpleFileCache.pm, and GitwebCache/Capture/ToFile.pm\n  instead of GitwebCache/Capture/Simple.pm\n\n* 'max_lifetime' was introduced in previous commit, so its use in\n  %cache_options (setting default value for gitweb) should also be\n  done in previous commit.\n\n* gitweb_enable_caching function in tgitweb-lib.sh should use\n  \"$TRASH_DIRECTORY/cache\" as 'cache_root' from beginning, just in\n  case\n\n\nIt is worth mentioning that git_generating_data_html does not need to\nend with 'die'; it could as well end with 'goto DONE_REQUEST'.  The\n'generating_info' subroutine is outside capture anyway.\n\n\nThe issue with error pages should be solved, even in the case when\nthey are not cached.  There are three layers of defense:\n\n1. git_generating_data_html has initial delay of 1 second, by default.\n   This means that if die_error finishes within this initial delay,\n   then redirection (and ending the request) wouldn't take place.  The\n   error page would be printed by parent process and not cached.\n\n2. In the case where there is no stale data to serve, but there is\n   'generating_info' subroutine and it would exit / end request before\n   error page is fully generated, background process would be not\n   detached, and it would print error page.  The error page would not\n   be cached.\n\n   Though I wonder if exit from git_generating_data_html should be\n   trapped, so that we can wait for background process; other solution\n   would be to use ripper SIGCHLD signal handler for this process...\n\n   Huh, something still to think about...\n\n3. In the case where there is stale data for what is now an error\n   condition (e.g. deleted branch or deleted project), and background\n   process would generate data being detached from originating\n   project, the error page would be captured and cached.\n\n gitweb/Makefile                                |    4 +-\n gitweb/gitweb.perl                             |  171 +++++++++++++++++++++++-\n gitweb/lib/GitwebCache/CacheOutput.pm          |   10 ++-\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |  113 +++++++++++++++-\n t/gitweb-lib.sh                                |    4 +-\n t/t9511/test_cache_interface.pl                |  149 ++++++++++++++++++++-\n 6 files changed, 439 insertions(+), 12 deletions(-)\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex d67c138..7a5c85f 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -115,8 +115,8 @@ GITWEB_FILES += static/git-logo.png static/git-favicon.png\n \n # gitweb output caching\n GITWEB_MODULES += GitwebCache/CacheOutput.pm\n-GITWEB_MODULES += GitwebCache/SimpleFileCache.pm\n-GITWEB_MODULES += GitwebCache/Capture/Simple.pm\n+GITWEB_MODULES += GitwebCache/FileCacheWithLocking.pm\n+GITWEB_MODULES += GitwebCache/Capture/ToFile.pm\n \n GITWEB_REPLACE = \\\n \t-e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \\\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex eb02b6b..5ef668d 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -307,6 +307,38 @@ our %cache_options = (\n \t# The (global) expiration time for objects placed in the cache, in seconds.\n \t'expires_in' => 20,\n \n+\t# Maximum cache file life, in seconds.  If cache entry lifetime exceeds\n+\t# this value, it wouldn't be served as being too stale when waiting for\n+\t# cache to be regenerated/refreshed, instead of trying to display\n+\t# existing cache date.\n+\t#\n+\t# Set it to -1 to always serve existing data if it exists.\n+\t# Set it to  0 to turn off serving stale data, i.e. always wait.\n+\t'max_lifetime' => 5*60*60, # 5 hours\n+\n+\t# This enables/disables background caching.  If it is set to true value,\n+\t# caching engine would return stale data (if it is not older than\n+\t# 'max_lifetime' seconds) if it exists, and launch process if regenerating\n+\t# (refreshing) cache into the background.  If it is set to false value,\n+\t# the process that fills cache must always wait for data to be generated.\n+\t# In theory this will make gitweb seem more responsive at the price of\n+\t# serving possibly stale data.\n+\t'background_cache' => 1,\n+\n+\t# Subroutine which would be called when gitweb has to wait for data to\n+\t# be generated (it can't serve stale data because there isn't any,\n+\t# or if it exists it is older than 'max_lifetime').  The default\n+\t# is to use git_generating_data_html(), which creates \"Generating...\"\n+\t# page, which would then redirect or redraw/rewrite the page when\n+\t# data is ready.\n+\t# Set it to `undef' to disable this feature.\n+\t#\n+\t# Such subroutine (if invoked from GitwebCache::FileCacheWithLocking)\n+\t# is passed the following parameters: $cache instance, human-readable\n+\t# $key to current page, and $sync_coderef subroutine to invoke to wait\n+\t# (in a blocking way) for data.\n+\t'generating_info' => \\&git_generating_data_html,\n+\n \t# How to handle runtime errors occurring during cache gets and cache\n \t# sets.  Options are:\n \t#  * \"die\" (the default) - call die() with an appropriate message\n@@ -323,10 +355,27 @@ our %cache_options = (\n \n \t# Extra options passed to GitwebCache::CacheOutput::cache_output subroutine\n \t'cache_output' => {\n-\t\t# Enable caching of error pages (boolean).  Default is false.\n-\t\t'-cache_errors' => 0,\n+\t\t# Enable caching of error pages (tristate, with undef meaning that error\n+\t\t# pages will be cached if were generated in detached process).\n+\t\t# Default is undef.\n+\t\t'-cache_errors' => undef,\n \t},\n );\n+# You define site-wide options for \"Generating...\" page (if enabled) here\n+# (which means that $cache_options{'generating_info'} is set to coderef);\n+# override them with $GITWEB_CONFIG as necessary.\n+our %generating_options = (\n+\t# The delay before displaying \"Generating...\" page, in seconds.  It is\n+\t# intended for \"Generating...\" page to be shown only when really needed.\n+\t'startup_delay' => 1,\n+\t# The time between generating new piece of output to prevent from\n+\t# redirection before data is ready, i.e. time between printing each\n+\t# dot in activity indicator / progress info, in seconds.\n+\t'print_interval' => 2,\n+\t# Maximum time \"Generating...\" page would be present, waiting for data,\n+\t# before unconditional redirect, in seconds.\n+\t'timeout' => $cache_options{'expires_min'},\n+);\n # Set to _initialized_ instance of GitwebCache::Capture::ToFile\n # compatibile capturing engine, i.e. one implementing ->new()\n # constructor, and ->capture($code, $file) method.  If unset\n@@ -870,6 +919,18 @@ sub evaluate_actions_info {\n \t}\n }\n \n+sub browser_is_robot {\n+\treturn 1 if !exists $ENV{'HTTP_USER_AGENT'}; # gitweb run as script\n+\tif (eval { require HTTP::BrowserDetect; }) {\n+\t\tmy $browser = HTTP::BrowserDetect->new();\n+\t\treturn $browser->robot();\n+\t}\n+\t# fallback on detecting known web browsers\n+\treturn 0 if ($ENV{'HTTP_USER_AGENT'} =~ /\\b(?:Mozilla|Opera|Safari|IE)\\b/);\n+\t# be conservative; if not sure, assume non-interactive\n+\treturn 1;\n+}\n+\n # fill %input_params with the CGI parameters. All values except for 'opt'\n # should be single values, but opt can be an array. We should probably\n # build an array of parameters that can be multi-valued, but since for the time\n@@ -3660,6 +3721,112 @@ sub get_page_title {\n \treturn $title;\n }\n \n+# creates \"Generating...\" page when caching enabled and not in cache\n+sub git_generating_data_html {\n+\tmy ($cache, $key, $sync_coderef) = @_;\n+\n+\t# when should gitweb show \"Generating...\" page\n+\tif ((defined $actions_info{$action}{'output_format'} &&\n+\t     $actions_info{$action}{'output_format'} eq 'feed') ||\n+\t    browser_is_robot()) {\n+\t\treturn;\n+\t}\n+\n+\t# Initial delay\n+\tif ($generating_options{'startup_delay'} > 0) {\n+\t\teval {\n+\t\t\tlocal $SIG{ALRM} = sub { die \"alarm clock restart\\n\" }; # NB: \\n required\n+\t\t\talarm $generating_options{'startup_delay'};\n+\t\t\t$sync_coderef->(); # wait for data\n+\t\t\talarm 0;           # turn off the alarm\n+\t\t};\n+\t\tif ($@) {\n+\t\t\t# propagate unexpected errors\n+\t\t\tdie $@ if $@ !~ /alarm clock restart/;\n+\t\t} else {\n+\t\t\t# we got response within 'startup_delay' timeout\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\tmy $title = \"[Generating...] \" . get_page_title();\n+\t# TODO: the following line of code duplicates the one\n+\t# in git_header_html, and it should probably be refactored.\n+\tmy $mod_perl_version = $ENV{'MOD_PERL'} ? \" $ENV{'MOD_PERL'}\" : '';\n+\n+\t# Use the trick that 'refresh' HTTP header equivalent (set via http-equiv)\n+\t# with timeout of 0 seconds would redirect as soon as page is finished.\n+\t# It assumes that browser would display partially received page.\n+\t# This \"Generating...\" redirect page should not be cached (externally).\n+\tmy %no_cache = (\n+\t\t# HTTP/1.0\n+\t\t-Pragma => 'no-cache',\n+\t\t# HTTP/1.1\n+\t\t-Cache_Control => join(', ', qw(private no-cache no-store must-revalidate\n+\t\t                                max-age=0 pre-check=0 post-check=0)),\n+\t);\n+\tprint STDOUT $cgi->header(-type => 'text/html', -charset => 'utf-8',\n+\t                          -status=> '200 OK', -expires => 'now',\n+\t                          %no_cache);\n+\tprint STDOUT <<\"EOF\";\n+<?xml version=\"1.0\" encoding=\"utf-8\"?>\n+<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n+                      \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n+<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en-US\" lang=\"en-US\">\n+<!-- git web interface version $version -->\n+<!-- git core binaries version $git_version -->\n+<head>\n+<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n+<meta http-equiv=\"refresh\" content=\"0\" />\n+<meta name=\"generator\" content=\"gitweb/$version git/$git_version$mod_perl_version\" />\n+<meta name=\"robots\" content=\"noindex, nofollow\" />\n+<title>$title</title>\n+</head>\n+<body>\n+EOF\n+\n+\tlocal $| = 1; # autoflush\n+\tprint STDOUT 'Generating...';\n+\n+\tmy $total_time = 0;\n+\tmy $interval = $generating_options{'print_interval'} || 1;\n+\tmy $timeout  = $generating_options{'timeout'};\n+\tmy $alarm_handler = sub {\n+\t\tlocal $! = 1;\n+\t\tprint STDOUT '.';\n+\t\t$total_time += $interval;\n+\t\tif ($total_time > $timeout) {\n+\t\t\tdie \"timeout\\n\";\n+\t\t}\n+\t};\n+\teval {\n+\t\tlocal $SIG{ALRM} = $alarm_handler;\n+\t\tTime::HiRes::alarm($interval, $interval);\n+\t\tmy $sync_ok;\n+\t\tdo {\n+\t\t\t# loop is needed here because SIGALRM (from 'alarm')\n+\t\t\t# can interrupt waiting (process of acquiring lock)\n+\t\t\t$sync_ok = $sync_coderef->(); # blocking wait for data\n+\t\t} until ($sync_ok);\n+\t\talarm 0;\n+\t};\n+\t# It doesn't really matter if we got lock, or timed-out\n+\t# but we should re-throw unknown (unexpected) errors\n+\tdie $@ if ($@ and $@ !~ /timeout/);\n+\n+\tprint STDOUT <<\"EOF\";\n+\n+</body>\n+</html>\n+EOF\n+\n+\t# after refresh web browser would reload page and send new request\n+\tdie { 'status' => 200 }; # to end request\n+\t#goto DONE_REQUEST;\n+\t#exit 0;\n+\t#return;\n+}\n+\n sub print_feed_meta {\n \tif (defined $project) {\n \t\tmy %href_params = get_feed_info();\ndiff --git a/gitweb/lib/GitwebCache/CacheOutput.pm b/gitweb/lib/GitwebCache/CacheOutput.pm\nindex 792ddb7..188d4ab 100644\n--- a/gitweb/lib/GitwebCache/CacheOutput.pm\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -35,16 +35,24 @@ our %EXPORT_TAGS = (all => [ @EXPORT ]);\n # in ':raw' format (and thus restored in ':raw' from cache)\n #\n # Supported options:\n-# * -cache_errors => 0|1  - whether error output should be cached\n+# * -cache_errors => undef|0|1  - whether error output should be cached,\n+#                                 undef means cache if we are in detached process\n sub cache_output {\n \tmy ($cache, $capture, $key, $code, %opts) = @_;\n \n+\n+\tmy $pid = $$;\n \tmy ($fh, $filename);\n \tmy ($capture_fh, $capture_filename);\n \teval { # this `eval` is to catch rethrown error, so we can print captured output\n \t\t($fh, $filename) = $cache->compute_fh($key, sub {\n \t\t\t($capture_fh, $capture_filename) = @_;\n \n+\t\t\tif (!defined $opts{'-cache_errors'}) {\n+\t\t\t\t# cache errors if we are in detached process\n+\t\t\t\t$opts{'-cache_errors'} = ($$ != $pid && getppid() != $pid);\n+\t\t\t}\n+\n \t\t\t# this `eval` is to be able to cache error output (up till 'die')\n \t\t\teval { $capture->capture($code, $capture_fh); };\n \ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex ecd0e18..291526e 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -73,6 +73,16 @@ our $EXPIRE_NOW = 0;\n #    If it is greater than 0, and cache entry is expired but not older\n #    than it, serve stale data when waiting for cache entry to be \n #    regenerated (refreshed).  Non-adaptive.\n+#  * 'background_cache' (boolean)\n+#    This enables/disables regenerating cache in background process.\n+#    Defaults to true.\n+#  * 'generating_info'\n+#    Subroutine (code) called when process has to wait for cache entry\n+#    to be (re)generated (when there is no not-too-stale data to serve\n+#    instead), for other process (or bacground process).  It is passed\n+#    $cache instance, $key, and $wait_code subroutine (code reference)\n+#    to invoke (to call) to wait for cache entry to be ready.\n+#    Unset by default (which means no activity indicator).\n #  * 'on_error' (similar to CHI 'on_get_error'/'on_set_error')\n #    How to handle runtime errors occurring during cache gets and cache\n #    sets, which may or may not be considered fatal in your application.\n@@ -107,6 +117,11 @@ sub new {\n \t\texists $opts{'max_lifetime'}       ? $opts{'max_lifetime'} :\n \t\texists $opts{'max_cache_lifetime'} ? $opts{'max_cache_lifetime'} :\n \t\t$NEVER_EXPIRE;\n+\t$self->{'background_cache'} =\n+\t\texists $opts{'background_cache'} ? $opts{'background_cache'} :\n+\t\t1;\n+\t$self->{'generating_info'} = $opts{'generating_info'}\n+\t\tif exists $opts{'generating_info'};\n \t$self->{'on_error'} =\n \t\texists $opts{'on_error'}      ? $opts{'on_error'} :\n \t\texists $opts{'on_get_error'}  ? $opts{'on_get_error'} :\n@@ -127,6 +142,7 @@ sub new {\n \n # creates get_depth() and set_depth($depth) etc. methods\n foreach my $i (qw(depth root namespace expires_in max_lifetime\n+                  background_cache generating_info\n                   on_error)) {\n \tmy $field = $i;\n \tno strict 'refs';\n@@ -140,6 +156,16 @@ foreach my $i (qw(depth root namespace expires_in max_lifetime\n \t};\n }\n \n+# $cache->generating_info($wait_code);\n+# runs 'generating_info' subroutine, for activity indicator,\n+# checking if it is defined first.\n+sub generating_info {\n+\tmy $self = shift;\n+\n+\tif (defined $self->{'generating_info'}) {\n+\t\t$self->{'generating_info'}->($self, @_);\n+\t}\n+}\n \n # ----------------------------------------------------------------------\n # utility functions and methods\n@@ -246,6 +272,10 @@ sub _wait_for_data {\n \tmy ($self, $key, $sync_coderef) = @_;\n \tmy @result;\n \n+\t# provide \"generating page...\" info, if exists\n+\t$self->generating_info($key, $sync_coderef);\n+\t# generating info may exit, so we can not get there\n+\n \t# wait for data to be available\n \t$sync_coderef->();\n \t# fetch data\n@@ -254,6 +284,57 @@ sub _wait_for_data {\n \treturn @result;\n }\n \n+sub _set_maybe_background {\n+\tmy ($self, $key, $code) = @_;\n+\n+\tmy ($pid, $detach);\n+\tmy (@result, @stale_result);\n+\n+\tif ($self->{'background_cache'}) {\n+\t\t# try to retrieve stale data\n+\t\t@stale_result = $self->get_fh($key,\n+\t\t\t'expires_in' => $self->get_max_lifetime());\n+\n+\t\t# fork if there is stale data, for background process\n+\t\t# to regenerate/refresh the cache (generate data),\n+\t\t# or if main process would show progress indicator\n+\t\t$detach = @stale_result;\n+\t\t$pid = fork()\n+\t\t\tif (@stale_result || $self->{'generating_info'});\n+\t}\n+\n+\tif ($pid) {\n+\t\t## forked and are in parent process\n+\t\t# reap child, which spawned grandchild process (detaching it)\n+\t\twaitpid $pid, 0\n+\t\t\tif $detach;\n+\n+\t} else {\n+\t\t## didn't fork, or are in background process\n+\n+\t\t# daemonize background process, detaching it from parent\n+\t\t# see also Proc::Daemonize, Apache2::SubProcess\n+\t\tif (defined $pid && $detach) {\n+\t\t\t## in background process\n+\t\t\tPOSIX::setsid(); # or setpgrp(0, 0);\n+\t\t\tfork() && CORE::exit(0);\n+\t\t}\n+\n+\t\t@result = $self->set_coderef_fh($key, $code);\n+\n+\t\tif (defined $pid) { # && !$pid\n+\t\t\t## in background process; parent or grandparent\n+\t\t\t## will serve stale data, or just generated data\n+\n+\t\t\t# lockfile will be automatically closed on exit,\n+\t\t\t# and therefore lockfile would be unlocked\n+\t\t\tCORE::exit(0);\n+\t\t}\n+\t}\n+\n+\treturn @result > 0 ? @result : @stale_result;\n+}\n+\n # $self->_handle_error($raw_error)\n #\n # based on _handle_get_error and _dispatch_error_msg from CHI::Driver\n@@ -408,14 +489,37 @@ sub compute_fh {\n \t\t$lock_state = flock($lock_fh, LOCK_EX | LOCK_NB);\n \t\tif ($lock_state) {\n \t\t\t## acquired writers lock, have to generate data\n-\t\t\t@result = eval { $self->set_coderef_fh($key, $code_fh) };\n+\t\t\teval { @result = $self->_set_maybe_background($key, $code_fh) };\n \t\t\t$self->_handle_error($@) if $@;\n \n \t\t\t# closing lockfile releases writer lock\n-\t\t\tflock($lock_fh, LOCK_UN);\n+\t\t\t#flock($lock_fh, LOCK_UN); # it would unlock here and in background process\n \t\t\tclose $lock_fh\n \t\t\t\tor $self->_handle_error(\"Could't close lockfile '$lockfile': $!\");\n \n+\t\t\tif (!@result) {\n+\t\t\t\t# wait for background process to finish generating data\n+\t\t\t\topen $lock_fh, '<', $lockfile\n+\t\t\t\t\tor $self->_handle_error(\"Couldn't reopen (for reading) lockfile '$lockfile': $!\");\n+\n+\t\t\t\teval {\n+\t\t\t\t\t@result = $self->_wait_for_data($key, sub {\n+\t\t\t\t\t\tflock($lock_fh, LOCK_SH);\n+\t\t\t\t\t\t# or 'waitpid -1, 0;', or 'wait;', as we don't detach now in this situation\n+\t\t\t\t\t});\n+\t\t\t\t};\n+\t\t\t\t$self->_handle_error($@) if $@;\n+\n+\t\t\t\t# closing lockfile releases readers lock used to wait for data\n+\t\t\t\tflock($lock_fh, LOCK_UN);\n+\t\t\t\tclose $lock_fh\n+\t\t\t\t\tor $self->_handle_error(\"Could't close reopened lockfile '$lockfile': $!\");\n+\n+\t\t\t\t# we didn't detach, so wait for the child to reap it\n+\t\t\t\t# (it should finish working, according to lock status)\n+\t\t\t\twait;\n+\t\t\t}\n+\n \t\t} else {\n \t\t\t## didn't acquire writers lock, get stale data or wait for regeneration\n \n@@ -429,12 +533,13 @@ sub compute_fh {\n \n \t\t\t# wait for regeneration if no stale data to serve,\n \t\t\t# using shared / readers lock to sync (wait for data)\n-\t\t\t@result = eval {\n-\t\t\t\t$self->_wait_for_data($key, sub {\n+\t\t\teval {\n+\t\t\t\t@result = $self->_wait_for_data($key, sub {\n \t\t\t\t\tflock($lock_fh, LOCK_SH);\n \t\t\t\t});\n \t\t\t};\n \t\t\t$self->_handle_error($@) if $@;\n+\n \t\t\t# closing lockfile releases readers lock\n \t\t\tflock($lock_fh, LOCK_UN);\n \t\t\tclose $lock_fh\ndiff --git a/t/gitweb-lib.sh b/t/gitweb-lib.sh\nindex 4ce067f..8652c91 100755\n--- a/t/gitweb-lib.sh\n+++ b/t/gitweb-lib.sh\n@@ -57,7 +57,9 @@ gitweb_enable_caching () {\n \t\tcat >>gitweb_config.perl <<-\\EOF &&\n \t\t$caching_enabled = 1;\n \t\t$cache_options{\"expires_in\"} = -1;      # never expire cache for tests\n-\t\t$cache_options{\"cache_root\"} = \"cache\"; # to clear the right thing\n+\t\t$cache_options{\"cache_root\"} = \"$TRASH_DIRECTORY/cache\"; # to clear the right thing\n+\t\t$cache_options{\"background_cache\"} = 0; # no background processes in test suite\n+\t\t$cache_options{\"generating_info\"} = undef; # tests do not use web browser\n \t\tEOF\n \t\trm -rf cache/\n \t'\ndiff --git a/t/t9511/test_cache_interface.pl b/t/t9511/test_cache_interface.pl\nindex a2b006c..1e8feb3 100755\n--- a/t/t9511/test_cache_interface.pl\n+++ b/t/t9511/test_cache_interface.pl\n@@ -23,7 +23,13 @@ BEGIN { use_ok('GitwebCache::FileCacheWithLocking'); }\n note(\"Using lib '$INC[0]'\");\n note(\"Testing '$INC{'GitwebCache/FileCacheWithLocking.pm'}'\");\n \n-my $cache = new_ok('GitwebCache::FileCacheWithLocking');\n+my $cache = new_ok('GitwebCache::FileCacheWithLocking', [\n+\t'max_lifetime' => 0, # turn it off\n+\t'background_cache' => 0,\n+]);\n+\n+# ->compute_fh() can fork, don't generate zombies\n+#local $SIG{CHLD} = 'IGNORE';\n \n # Test that default values are defined\n #\n@@ -239,6 +245,7 @@ subtest 'parallel access' => sub {\n my $stale_value = 'Stale Value';\n \n subtest 'serving stale data when regenerating' => sub {\n+\t$cache->remove($key);\n \tcache_set_fh($cache, $key, $stale_value);\n \t$cache->set_expires_in(-1);   # never expire, for next check\n \tis(cache_get_fh($cache, $key), $stale_value,\n@@ -246,7 +253,10 @@ subtest 'serving stale data when regenerating' => sub {\n \n \t$call_count = 0;\n \t$cache->set_expires_in(0);    # expire now (so there are no fresh data)\n-\t$cache->set_max_lifetime(-1); # forever (always serve stale data)\n+\t$cache->set_max_lifetime(-1); # stale data is valid forever\n+\n+\t# without background generation\n+\t$cache->set_background_cache(0);\n \n \t@output = parallel_run {\n \t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n@@ -264,6 +274,31 @@ subtest 'serving stale data when regenerating' => sub {\n \t   'no background: value got set correctly, even if stale data returned');\n \n \n+\t# with background generation\n+\t$cache->set_background_cache(1);\n+\t$call_count = 0;\n+\tcache_set_fh($cache, $key, $stale_value);\n+\t$cache->set_expires_in(0);  # expire now (so there are no fresh data)\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n+\t\tprint \"$call_count$sep\";\n+\t\tprint $data if defined $data;\n+\t};\n+\t# returning stale data works\n+\tis_deeply(\n+\t\t[sort @output],\n+\t\t[sort (\"0$sep$stale_value\", \"0$sep$stale_value\")],\n+\t\t'background: stale data returned by both processes'\n+\t);\n+\t$cache->set_expires_in(-1); # never expire for next ->get\n+\tnote(\"waiting $slow_time sec. for background process to have time to set data\");\n+\tsleep $slow_time; # wait for background process to have chance to set data\n+\tis(cache_get_fh($cache, $key), $value,\n+\t   'background: value got set correctly by background process');\n+\t$cache->set_expires_in(0);  # expire now (so there are no fresh data)\n+\n+\n \tcache_set_fh($cache, $key, $stale_value);\n \t$cache->set_expires_in(0);   # expire now\n \t$cache->set_max_lifetime(0); # don't serve stale data\n@@ -282,6 +317,116 @@ subtest 'serving stale data when regenerating' => sub {\n $cache->set_expires_in(-1);\n \n \n+# Test 'generating_info' feature\n+#\n+$cache->remove($key);\n+my $progress_info = \"Generating...\";\n+sub test_generating_info {\n+\tlocal $| = 1;\n+\tprint \"$progress_info\";\n+}\n+$cache->set_generating_info(\\&test_generating_info);\n+\n+subtest 'generating progress info' => sub {\n+\tmy @progress;\n+\n+\t# without background generation, and without stale value\n+\t$cache->set_background_cache(0);\n+\t$cache->remove($key); # no data and no stale data\n+\t$call_count = 0;\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n+\t\tprint \"$sep$call_count$sep\";\n+\t\tprint $data if defined $data;\n+\t};\n+\t# split progress and output\n+\t@progress = map { s/^(.*)\\Q${sep}\\E//o && $1 } @output;\n+\tis_deeply(\n+\t\t[sort @progress],\n+\t\t[sort (\"${sep}1\", \"$progress_info${sep}0\")],\n+\t\t'no background, no stale data: the process waiting for data prints progress info'\n+\t);\n+\tis_deeply(\n+\t\t\\@output,\n+\t\t[ ($value) x 2 ],\n+\t\t'no background, no stale data: both processes return correct value'\n+\t);\n+\n+\n+\t# without background generation, with stale value\n+\tcache_set_fh($cache, $key, $stale_value);\n+\t$cache->set_expires_in(0);    # set value is now expired\n+\t$cache->set_max_lifetime(-1); # stale data never expire\n+\t$call_count = 0;\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n+\t\tprint \"$sep$call_count$sep\";\n+\t\tprint $data if defined $data;\n+\t};\n+\t@progress = map { s/^(.*?)\\Q${sep}\\E//o && $1 } @output;\n+\tis_deeply(\n+\t\t\\@progress,\n+\t\t[ ('') x 2],\n+\t\t'no background, stale data: neither process prints progress info'\n+\t);\n+\tis_deeply(\n+\t\t[sort @output],\n+\t\t[sort (\"1$sep$value\", \"0$sep$stale_value\")],\n+\t\t'no background, stale data: generating gets data, other gets stale data'\n+\t);\n+\t$cache->set_expires_in(-1);\n+\n+\n+\t# with background generation\n+\t$cache->set_background_cache(1);\n+\t$cache->remove($key); # no data and no stale value\n+\t$call_count = 0;\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n+\t\tprint $sep;\n+\t\tprint $data if defined $data;\n+\t};\n+\t@progress = map { s/^(.*)\\Q${sep}\\E//o && $1 } @output;\n+\tis_deeply(\n+\t\t\\@progress,\n+\t\t[ ($progress_info) x 2],\n+\t\t'background, no stale data: both process print progress info'\n+\t);\n+\tis_deeply(\n+\t\t\\@output,\n+\t\t[ ($value) x 2 ],\n+\t\t'background, no stale data: both processes return correct value'\n+\t);\n+\n+\n+\t# with background generation, with stale value\n+\tcache_set_fh($cache, $key, $stale_value);\n+\t$cache->set_expires_in(0);    # set value is now expired\n+\t$cache->set_max_lifetime(-1); # stale data never expire\n+\t$call_count = 0;\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n+\t\tprint $sep;\n+\t\tprint $data if defined $data;\n+\t};\n+\t@progress = map { s/^(.*)\\Q${sep}\\E//o && $1 } @output;\n+\tis_deeply(\n+\t\t\\@progress,\n+\t\t[ ('') x 2],\n+\t\t'background, stale data: neither process prints progress info'\n+\t);\n+\tnote(\"waiting $slow_time sec. for background process to have time to set data\");\n+\tsleep $slow_time; # wait for background process to have chance to set data\n+\n+\n+\tdone_testing();\n+};\n+$cache->set_expires_in(-1);\n+\n done_testing();\n \n \n-- \n1.7.3\n"},{"id":"158855","messageId":"201101032233.16174.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH v7 11/9] [PoC] gitweb/lib - tee, i.e. print and capture during cache entry generation","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-01-03T21:33:02Z","receivedAt":"2011-01-03T21:33:02Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Instead of having gitweb use progress info indicator / throbber to\nnotify user that data is being generated by current process, gitweb\ncan now (provided that PerlIO::tee from PerlIO::Util is available)\nsend page to web browser while simultaneously saving it to cache\n(print and capture, i.e. tee), thus having incremental generating of\npage serve as a progress indicator.\n\n\nTo do this, the GitwebCache::Capture::ToFile module acquired ->tee()\nsubroutine, similar to ->capture(), but it prints while capturing\noutput.  The ->tee() method (and its worker methods ->tee_start() and\n->tee_end()) are available only if PerlIO::tee from PerlIO::Util\ndistribution is present.  Tests checking if this feature works as\nexpected were added to t9510 test.\n\nAn alternative would be to provide two versions of\nGitwebCache::Capture::ToFile.  Note also that PerlIO::tee is not\nstrictly necessary, as Capture::Tiny shows, but in most generic case\n(like done in Capture::Tiny) one needs separate process functioning as\nmultplexer.\n\n\nBecause tee-ing (printing while capturing) can function as a kind of\nprogress indicator only for process generating the data for cache\nentry, and not for the processes waiting for data to be generated,\ntherefore 'generating_info' got splitinto 'get_progress_info' and\n'set_progress_info'.  You can set now in GitwebCache::FIleCacheWithLocking\nthose two separately.  You are expected to unset 'set_progress_info'\nwhen using tee-ing capturing engine.  Some tests added to t9511 with\ntee-like situation.\n\nAs a proof of concept gitweb now uses two slightly different versions\nof \"Generating...\" page; if you worry about interaction between progress\nindicator and non-cacheable error pages, you can set 'set_progress_info'\nseparately to undef.\n\n\nThe cache_output subroutine from GitwebCache::CacheOutput got updated\nto use ->tee() subroutine if $capture supports it.  If ->tee() is\nused, then of course generated data doesn't need to and shouldn't be\nprinted; also cache_output unsets 'set_progress_info' locally.  Note\nthat ->tee() is used only if we are not in background process; if we\nare in background process, simple ->capture() is used.  No new tests\nfor now.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nNote: the change to t/gitweb-lib.sh and some of changes to t9510 are\nincidental fixes; original commits should be fixed instead.\n\nThis is proof of concept (PoC) patch, showing how one can use\n\"tee\"-ing in capturing engine together with gitweb output caching.\nBecause we don't need and don't use 'generating_info' subroutine for\nprocess that is writing data (one that acquired writers lock) we are\n(or at least should be) now safe to have error pages not cached.\n\nCurrently the \"tee\"-ing support requires PerlIO::tee module from the\nPerlIO::Util distribution, as it was easiest way to add such feature.\nIn the future we would have probably to do something similar what\n'tee' in Capture::Tiny (or in other capture modules) does.  I'm not\nsure if PerlIO::Util is packaged as RPM package anywhere...; well, I have\ngoogled that ALT Linux has it: http://sisyphus.ru/en/srpm/perl-PerlIO-Util\n\nI have only ran tests, I haven't actually run gitweb with those\nchanges... :-P\n\n gitweb/gitweb.perl                             |   32 +++++++----\n gitweb/lib/GitwebCache/CacheOutput.pm          |   18 ++++++-\n gitweb/lib/GitwebCache/Capture/ToFile.pm       |   67 ++++++++++++++++++++++-\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   52 +++++++++++++-----\n t/gitweb-lib.sh                                |    2 +-\n t/t9510/test_capture_interface.pl              |   28 +++++++++-\n t/t9511/test_cache_interface.pl                |   29 ++++++++++\n 7 files changed, 194 insertions(+), 34 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 5ef668d..de283a0 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -325,19 +325,17 @@ our %cache_options = (\n \t# serving possibly stale data.\n \t'background_cache' => 1,\n \n-\t# Subroutine which would be called when gitweb has to wait for data to\n+\t# Subroutines which would be called when gitweb has to wait for data to\n \t# be generated (it can't serve stale data because there isn't any,\n-\t# or if it exists it is older than 'max_lifetime').  The default\n-\t# is to use git_generating_data_html(), which creates \"Generating...\"\n-\t# page, which would then redirect or redraw/rewrite the page when\n-\t# data is ready.\n-\t# Set it to `undef' to disable this feature.\n+\t# or if it exists it is older than 'max_lifetime').\n+\t# Set them to `undef' to disable this feature.\n \t#\n-\t# Such subroutine (if invoked from GitwebCache::FileCacheWithLocking)\n+\t# Such subroutines (if invoked from GitwebCache::FileCacheWithLocking)\n \t# is passed the following parameters: $cache instance, human-readable\n \t# $key to current page, and $sync_coderef subroutine to invoke to wait\n \t# (in a blocking way) for data.\n-\t'generating_info' => \\&git_generating_data_html,\n+\t'get_progress_info' => \\&git_get_progress_info_html,\n+\t'set_progress_info' => \\&git_set_progress_info_html,\n \n \t# How to handle runtime errors occurring during cache gets and cache\n \t# sets.  Options are:\n@@ -3721,9 +3719,21 @@ sub get_page_title {\n \treturn $title;\n }\n \n+sub git_get_progress_info_html {\n+\tgit_generating_data_html(\"Waiting\", @_);\n+}\n+\n+sub git_set_progress_info_html {\n+\t# minimum startup delay is 2 seconds, just in case, for error handling\n+\tlocal $generating_options{'startup_delay'} =\n+\t\t$generating_options{'startup_delay'} > 2 ? $generating_options{'startup_delay'} : 2;\n+\n+\tgit_generating_data_html(\"Generating\", @_);\n+}\n+\n # creates \"Generating...\" page when caching enabled and not in cache\n sub git_generating_data_html {\n-\tmy ($cache, $key, $sync_coderef) = @_;\n+\tmy ($msg, $cache, $key, $sync_coderef) = @_;\n \n \t# when should gitweb show \"Generating...\" page\n \tif ((defined $actions_info{$action}{'output_format'} &&\n@@ -3749,7 +3759,7 @@ sub git_generating_data_html {\n \t\t}\n \t}\n \n-\tmy $title = \"[Generating...] \" . get_page_title();\n+\tmy $title = \"[$msg...] \" . get_page_title();\n \t# TODO: the following line of code duplicates the one\n \t# in git_header_html, and it should probably be refactored.\n \tmy $mod_perl_version = $ENV{'MOD_PERL'} ? \" $ENV{'MOD_PERL'}\" : '';\n@@ -3786,7 +3796,7 @@ sub git_generating_data_html {\n EOF\n \n \tlocal $| = 1; # autoflush\n-\tprint STDOUT 'Generating...';\n+\tprint STDOUT \"$msg...\";\n \n \tmy $total_time = 0;\n \tmy $interval = $generating_options{'print_interval'} || 1;\ndiff --git a/gitweb/lib/GitwebCache/CacheOutput.pm b/gitweb/lib/GitwebCache/CacheOutput.pm\nindex 188d4ab..3bcd35b 100644\n--- a/gitweb/lib/GitwebCache/CacheOutput.pm\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -42,6 +42,14 @@ sub cache_output {\n \n \n \tmy $pid = $$;\n+\tmy $can_tee = $capture->can('tee');\n+\t# if $capture can tee, we don't need progress info for generating (on set).\n+\t# the below breaks encapsulation, but it is a bit simpler than\n+\t# $old = $cache->get_...; $cache->set_...(...); ...; $cache->set_...($old);\n+\tlocal $cache->{'set_progress_info'} = undef\n+\t\tif ($can_tee);\n+\n+\tmy $printed = 0;\n \tmy ($fh, $filename);\n \tmy ($capture_fh, $capture_filename);\n \teval { # this `eval` is to catch rethrown error, so we can print captured output\n@@ -54,7 +62,12 @@ sub cache_output {\n \t\t\t}\n \n \t\t\t# this `eval` is to be able to cache error output (up till 'die')\n-\t\t\teval { $capture->capture($code, $capture_fh); };\n+\t\t\tif ($can_tee && $$ == $pid) {\n+\t\t\t\t$printed = 1;\n+\t\t\t\teval { $capture->tee($code, $capture_fh); };\n+\t\t\t} else {\n+\t\t\t\teval { $capture->capture($code, $capture_fh); };\n+\t\t\t}\n \n \t\t\t# note that $cache can catch this error itself (like e.g. CHI);\n \t\t\t# use \"die\"-ing error handler to rethrow this exception to outside\n@@ -69,7 +82,8 @@ sub cache_output {\n \t\t$filename ||= $capture_filename;\n \t}\n \n-\tif (defined $fh || defined $filename) {\n+\tif ((defined $fh || defined $filename) &&\n+\t    !$printed) { # did we tee, i.e. already printed output?\n \t\t# set binmode only if $fh is defined (is a filehandle)\n \t\t# File::Copy::copy opens files given by filename in binary mode\n \t\tbinmode $fh,    ':raw' if (defined $fh);\ndiff --git a/gitweb/lib/GitwebCache/Capture/ToFile.pm b/gitweb/lib/GitwebCache/Capture/ToFile.pm\nindex d2dbf0f..0290ec4 100644\n--- a/gitweb/lib/GitwebCache/Capture/ToFile.pm\n+++ b/gitweb/lib/GitwebCache/Capture/ToFile.pm\n@@ -20,6 +20,10 @@ use warnings;\n use PerlIO;\n use Symbol qw(qualify_to_ref);\n \n+BEGIN {\n+\teval { use PerlIO::Util; };\n+}\n+\n # Constructor\n sub new {\n \tmy $class = shift;\n@@ -30,22 +34,41 @@ sub new {\n \treturn $self;\n }\n \n-sub capture {\n+sub capture_or_tee {\n \tmy $self = shift;\n \tmy $code = shift;\n+\tmy ($start, $stop) = @{ shift() };\n \n-\t$self->capture_start(@_); # pass rest of params\n+\t$self->$start(@_); # pass rest of params\n \teval { $code->(); 1; };\n \tmy $exit_code = $?; # save this for later\n \tmy $error = $@;     # save this for later\n \n-\tmy $got_out = $self->capture_stop();\n+\tmy $got_out = $self->$stop();\n \t$? = $exit_code;\n \tdie $error if $error;\n \n \treturn $got_out;\n }\n \n+sub capture {\n+\tmy ($self, $code, @args) = @_;\n+\n+\treturn\n+\t\t$self->capture_or_tee($code, ['capture_start', 'capture_stop'], @args);\n+}\n+\n+BEGIN {\n+\tif ($INC{'PerlIO/Util.pm'}) {\n+\t\t*tee = sub {\n+\t\t\tmy ($self, $code, @args) = @_;\n+\n+\t\t\treturn\n+\t\t\t\t$self->capture_or_tee($code, ['tee_start', 'tee_stop'], @args);\n+\t\t};\n+\t}\n+}\n+\n # ----------------------------------------------------------------------\n \n # Start capturing data (STDOUT)\n@@ -92,6 +115,44 @@ sub capture_stop {\n \treturn exists $self->{'to'} ? $self->{'to'} : $self->{'data'};\n }\n \n+# ......................................................................\n+\n+BEGIN {\n+\tif ($INC{'PerlIO/Util.pm'}) {\n+\t\t*tee_start = sub {\n+\t\t\tmy ($self, $to) = @_;\n+\n+\t\t\t# save layers, to replay them on top of 'tee' layer (?)\n+\t\t\tmy @layers = PerlIO::get_layers(\\*STDOUT);\n+\n+\t\t\t$self->{'to'} = $to;\n+\t\t\t*STDOUT->push_layer('tee' => $to);\n+\n+\t\t\t_relayer(\\*STDOUT, \\@layers); # is it necessary?\n+\n+\t\t\t# started tee-ing\n+\t\t\t$self->{'teeing'} = 1;\n+\t\t};\n+\t\t*tee_stop = sub {\n+\t\t\tmy $self = shift;\n+\n+\t\t\t# return if we didn't start tee-ing\n+\t\t\treturn unless delete $self->{'teeing'};\n+\n+\t\t\tmy @top_layers;\n+\t\t\twhile ((my $layer = *STDOUT->pop_layer()) ne 'tee') {\n+\t\t\t\tpush @top_layers, $layer;\n+\t\t\t}\n+\t\t\tbinmode(STDOUT, join(\":\", \":\", @top_layers));\n+\t\t\t# or is it binmode(STDOUT, join(\":\", \":raw\", @top_layers));\n+\n+\t\t\treturn exists $self->{'to'} ? $self->{'to'} : $self->{'data'};\n+\t\t};\n+\t}\n+}\n+\n+# ----------------------------------------------------------------------\n+\n # taken from Capture::Tiny by David Golden, Apache License 2.0\n # with debugging stripped out\n sub _relayer {\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex 291526e..09ae7b2 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -76,12 +76,17 @@ our $EXPIRE_NOW = 0;\n #  * 'background_cache' (boolean)\n #    This enables/disables regenerating cache in background process.\n #    Defaults to true.\n-#  * 'generating_info'\n+#  * 'get_progress_info',\n+#    'set_progress_info',\n+#    'generating_info' (code reference)\n #    Subroutine (code) called when process has to wait for cache entry\n #    to be (re)generated (when there is no not-too-stale data to serve\n #    instead), for other process (or bacground process).  It is passed\n #    $cache instance, $key, and $wait_code subroutine (code reference)\n #    to invoke (to call) to wait for cache entry to be ready.\n+#    'get_progress_info' gets called on getting data from cache, i.e.\n+#    when waiting for data to be generated, 'set_progress_info' gets\n+#    called when waiting to generate data; 'generating_info' sets both.\n #    Unset by default (which means no activity indicator).\n #  * 'on_error' (similar to CHI 'on_get_error'/'on_set_error')\n #    How to handle runtime errors occurring during cache gets and cache\n@@ -120,8 +125,14 @@ sub new {\n \t$self->{'background_cache'} =\n \t\texists $opts{'background_cache'} ? $opts{'background_cache'} :\n \t\t1;\n-\t$self->{'generating_info'} = $opts{'generating_info'}\n-\t\tif exists $opts{'generating_info'};\n+\t$self->{'get_progress_info'} =\n+\t\texists $opts{'get_progress_info'} ? $opts{'get_progress_info'} :\n+\t\texists $opts{'generating_info'}   ? $opts{'generating_info'} :\n+\t\tundef;\n+\t$self->{'set_progress_info'} =\n+\t\texists $opts{'set_progress_info'} ? $opts{'set_progress_info'} :\n+\t\texists $opts{'generating_info'}   ? $opts{'generating_info'} :\n+\t\tundef;\n \t$self->{'on_error'} =\n \t\texists $opts{'on_error'}      ? $opts{'on_error'} :\n \t\texists $opts{'on_get_error'}  ? $opts{'on_get_error'} :\n@@ -142,7 +153,7 @@ sub new {\n \n # creates get_depth() and set_depth($depth) etc. methods\n foreach my $i (qw(depth root namespace expires_in max_lifetime\n-                  background_cache generating_info\n+                  background_cache get_progress_info set_progress_info\n                   on_error)) {\n \tmy $field = $i;\n \tno strict 'refs';\n@@ -156,14 +167,25 @@ foreach my $i (qw(depth root namespace expires_in max_lifetime\n \t};\n }\n \n-# $cache->generating_info($wait_code);\n-# runs 'generating_info' subroutine, for activity indicator,\n-# checking if it is defined first.\n-sub generating_info {\n+sub set_generating_info {\n \tmy $self = shift;\n \n-\tif (defined $self->{'generating_info'}) {\n-\t\t$self->{'generating_info'}->($self, @_);\n+\t$self->set_get_progress_info(@_);\n+\t$self->set_set_progress_info(@_);\n+}\n+\n+# $cache->{get,set}_progress_info($key, $wait_code);\n+# runs '{get,set}_progress_info' subroutine, for activity indicator,\n+# checking if it is defined first.\n+foreach my $name qw(get_progress_info set_progress_info) {\n+\tmy $method = $name;\n+\tno strict 'refs';\n+\t*{\"$method\"} = sub {\n+\t\tmy $self = shift;\n+\n+\t\tif (defined $self->{$name}) {\n+\t\t\t$self->{$name}->($self, @_);\n+\t\t}\n \t}\n }\n \n@@ -269,11 +291,11 @@ sub _tempfile_to_path {\n # Wait for data to be available using (blocking) $code,\n # then return filehandle and filename to read from for $key.\n sub _wait_for_data {\n-\tmy ($self, $key, $sync_coderef) = @_;\n+\tmy ($self, $key, $progress_info, $sync_coderef) = @_;\n \tmy @result;\n \n \t# provide \"generating page...\" info, if exists\n-\t$self->generating_info($key, $sync_coderef);\n+\t$self->$progress_info($key, $sync_coderef);\n \t# generating info may exit, so we can not get there\n \n \t# wait for data to be available\n@@ -300,7 +322,7 @@ sub _set_maybe_background {\n \t\t# or if main process would show progress indicator\n \t\t$detach = @stale_result;\n \t\t$pid = fork()\n-\t\t\tif (@stale_result || $self->{'generating_info'});\n+\t\t\tif (@stale_result || $self->{'set_progress_info'});\n \t}\n \n \tif ($pid) {\n@@ -503,7 +525,7 @@ sub compute_fh {\n \t\t\t\t\tor $self->_handle_error(\"Couldn't reopen (for reading) lockfile '$lockfile': $!\");\n \n \t\t\t\teval {\n-\t\t\t\t\t@result = $self->_wait_for_data($key, sub {\n+\t\t\t\t\t@result = $self->_wait_for_data($key, 'set_progress_info', sub {\n \t\t\t\t\t\tflock($lock_fh, LOCK_SH);\n \t\t\t\t\t\t# or 'waitpid -1, 0;', or 'wait;', as we don't detach now in this situation\n \t\t\t\t\t});\n@@ -534,7 +556,7 @@ sub compute_fh {\n \t\t\t# wait for regeneration if no stale data to serve,\n \t\t\t# using shared / readers lock to sync (wait for data)\n \t\t\teval {\n-\t\t\t\t@result = $self->_wait_for_data($key, sub {\n+\t\t\t\t@result = $self->_wait_for_data($key, 'get_progress_info', sub {\n \t\t\t\t\tflock($lock_fh, LOCK_SH);\n \t\t\t\t});\n \t\t\t};\ndiff --git a/t/gitweb-lib.sh b/t/gitweb-lib.sh\nindex 8652c91..f0ef009 100755\n--- a/t/gitweb-lib.sh\n+++ b/t/gitweb-lib.sh\n@@ -57,7 +57,7 @@ gitweb_enable_caching () {\n \t\tcat >>gitweb_config.perl <<-\\EOF &&\n \t\t$caching_enabled = 1;\n \t\t$cache_options{\"expires_in\"} = -1;      # never expire cache for tests\n-\t\t$cache_options{\"cache_root\"} = \"$TRASH_DIRECTORY/cache\"; # to clear the right thing\n+\t\t$cache_options{\"cache_root\"} = \"cache\"; # to clear the right thing\n \t\t$cache_options{\"background_cache\"} = 0; # no background processes in test suite\n \t\t$cache_options{\"generating_info\"} = undef; # tests do not use web browser\n \t\tEOF\ndiff --git a/t/t9510/test_capture_interface.pl b/t/t9510/test_capture_interface.pl\nindex 6d90497..35e46ad 100755\n--- a/t/t9510/test_capture_interface.pl\n+++ b/t/t9510/test_capture_interface.pl\n@@ -116,8 +116,8 @@ $captured = $outer_capture->capture(sub {\n \tprint \"|post\";\n }, 'outer_actual');\n \n-my $inner = read_file('inner_actual');\n-my $outer = read_file('outer_actual');\n+$inner = read_file('inner_actual');\n+$outer = read_file('outer_actual');\n \n is($inner, \"INNER:pre|\",\n    'nested capture with die: inner output captured up to die');\n@@ -125,6 +125,30 @@ is($outer, \"pre|@=die from inner\\n|post\",\n    'nested capture with die: outer caught rethrown exception from inner');\n \n \n+# Testing tee feature, if available\n+#\n+SKIP: {\n+\tskip \"PerlIO::Util module not found\", 3\n+\t\tunless eval { require PerlIO::Util; 1 };\n+\n+\tcan_ok($capture, 'tee');\n+\n+\t$captured = $outer_capture->capture(sub {\n+\t\tprint \"pre|\";\n+\t\tmy $captured = $capture->tee(sub {\n+\t\t\tprint \"INNER\";\n+\t\t}, 'inner_actual');\n+\t\tprint \"|post\";\n+\t}, 'outer_actual');\n+\n+\t$inner = read_file('inner_actual');\n+\t$outer = read_file('outer_actual');\n+\n+\tis($inner, \"INNER\",          'tee: captured');\n+\tis($outer, \"pre|INNER|post\", 'tee: printed');\n+};\n+\n+\n done_testing();\n \n # Local Variables:\ndiff --git a/t/t9511/test_cache_interface.pl b/t/t9511/test_cache_interface.pl\nindex 1e8feb3..83f3894 100755\n--- a/t/t9511/test_cache_interface.pl\n+++ b/t/t9511/test_cache_interface.pl\n@@ -154,6 +154,14 @@ sub get_value_slow_fh {\n \tsleep $slow_time;\n \tprint {$fh} $value;\n }\n+sub tee_value_slow_fh {\n+\tmy $fh = shift;\n+\n+\t$call_count++;\n+\tsleep $slow_time;\n+\tprint       $value;\n+\tprint {$fh} $value;\n+}\n sub get_value_die {\n \t$call_count++;\n \tdie \"get_value_die\\n\";\n@@ -402,6 +410,26 @@ subtest 'generating progress info' => sub {\n \t);\n \n \n+\t# with background generation, tee-like, no stale data\n+\t$cache->set_set_progress_info(undef);\n+\t$cache->set_background_cache(1);\n+\t$cache->remove($key); # no data and no stale value\n+\t$call_count = 0;\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&tee_value_slow_fh);\n+\t\tprint \"$sep$call_count$sep\";\n+\t\tprint $data if defined $data;\n+\t};\n+\tmy $getting_output = (grep /\\Q${sep}0${sep}\\E/, @output)[0];\n+\tmy $setting_output = (grep /\\Q${sep}1${sep}\\E/, @output)[0];\n+\tis($getting_output, \"$progress_info${sep}0$sep$value\",\n+\t   'background, no stale, tee: waiting process prints progress, gets data');\n+\tis($setting_output, \"$value${sep}1$sep$value\",\n+\t   'background, no stale, tee: generating process prints data, sets data');\n+\t$cache->set_generating_info(\\&test_generating_info); # restore\n+\n+\n \t# with background generation, with stale value\n \tcache_set_fh($cache, $key, $stale_value);\n \t$cache->set_expires_in(0);    # set value is now expired\n@@ -427,6 +455,7 @@ subtest 'generating progress info' => sub {\n };\n $cache->set_expires_in(-1);\n \n+\n done_testing();\n \n \n-- \n1.7.3\n"},{"id":"158858","messageId":"4D225C6E.9000108@eaglescrag.net","threadId":"26127","inReplyTo":"201101032233.16174.jnareb@gmail.com","subject":"Re: [RFC PATCH v7 11/9] [PoC] gitweb/lib - tee, i.e. print and capture during cache entry generation","fromName":"J.H.","fromEmail":"warthog9@eaglescrag.net","sentAt":"2011-01-03T23:31:58Z","receivedAt":"2011-01-03T23:31:58Z","isPatch":true,"sender":{"key":"warthog9@kernel.org","avatar":"https://avatars.githubusercontent.com/u/2334704?v=4"},"body":"On 01/03/2011 01:33 PM, Jakub Narebski wrote:\n> Instead of having gitweb use progress info indicator / throbber to\n> notify user that data is being generated by current process, gitweb\n> can now (provided that PerlIO::tee from PerlIO::Util is available)\n> send page to web browser while simultaneously saving it to cache\n> (print and capture, i.e. tee), thus having incremental generating of\n> page serve as a progress indicator.\n\nIn general, and particularly for the large sites that caching is\ntargeted at, teeing is a really bad idea.  I've mentioned this several\ntimes before, and the progress indicator is a *MUCH* better idea.  I'm\nnot sure how many times I can say that, even if this was added it would\nhave the potential to exacerbate disk thrashing and overall make things\na lot more complex.\n\n1) Errors may still be generated in flight as the cache is being\ngenerated.  It would be better to let the cache run with a progress\nindicator and should an error occur, display the error instead of giving\nany output that may have been generated (and thus likely a broken page).\n\n2) Having multiple clients all waiting on the same page (in particular\nthe index page) can lead to invalid output.  In particular if you are\nteeing the output a reading client now must come in, read the current\ncontents of the file (as written), then pick up on the the tee after\nthat.  It's actually possible for the reading client to miss data as it\nmay be in flight to be written and the client is switching from reading\nthe file to reading the tee.  I don't see anything in your code to\nhandle that kind of switch over.\n\n3) This makes no allowance for the file to be generated completely in\nthe background while serving stale data in the interim.  Keep in mind\nthat it can (as Fedora has experienced) take *HOURS* to generate the\nindex page, teeing that output just means brokenness and isn't useful.\n\nIt's much better to have a simple, lightweight waiting message get\ndisplayed while things happen.  When they are done, output the completed\npage to all waiting clients.\n\n- John 'Warthog9' Hawley\n\nP.S. I'm back to work full-time on Wednesday, which I'll be catching up\non gitweb and trying to make forward progress on my gitweb code again.\n"},{"id":"158862","messageId":"201101040128.26826.jnareb@gmail.com","threadId":"26127","inReplyTo":"4D225C6E.9000108@eaglescrag.net","subject":"Re: [RFC PATCH v7 11/9] [PoC] gitweb/lib - tee, i.e. print and capture during cache entry generation","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-01-04T00:28:25Z","receivedAt":"2011-01-04T00:28:25Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Tue, 4 Jan 2011, J.H. wrote:\n> On 01/03/2011 01:33 PM, Jakub Narebski wrote:\n\n> > Instead of having gitweb use progress info indicator / throbber to\n> > notify user that data is being generated by current process, gitweb\n> > can now (provided that PerlIO::tee from PerlIO::Util is available)\n> > send page to web browser while simultaneously saving it to cache\n> > (print and capture, i.e. tee), thus having incremental generating of\n> > page serve as a progress indicator.\n> \n> In general, and particularly for the large sites that caching is\n> targeted at, teeing is a really bad idea.  I've mentioned this several\n> times before, and the progress indicator is a *MUCH* better idea.  I'm\n> not sure how many times I can say that, even if this was added it would\n> have the potential to exacerbate disk thrashing and overall make things\n> a lot more complex.\n\nIt might be true that tee-ing is bad for very large sites, as it\nincreases load a bit in those (I think) extremly rare cases where\nclients concurrently access the very same error page.  But it might\nbe a solution for those in between cases.  I think that incrementally\ngenerated page is better progress indicator than just \"Generating...\"\npage.\n\nAnyway this proof of concept patch is to show how such thing should\nbe implemented.  I don't think that it makes things a lot more complex;\nin this rewrite everything is quite well modularized, encapsulated, and\nisolated.\n\n\nBut the main intent behind this patch was to avoid bad interaction between\n'progress info' indicator (in the process that is generating page, see\nbelow), and non-cached error pages.\n\n> \n> 1) Errors may still be generated in flight as the cache is being\n> generated.  It would be better to let the cache run with a progress\n> indicator and should an error occur, display the error instead of giving\n> any output that may have been generated (and thus likely a broken page).\n\nOn the contrary, with tee-ing (and zero size sanity check) you would be\nable to see pages even if there are errors saving cache entry.  Though\nthis wouldn't help very large sites which cannot function without caching,\nit could be useful for smaller sites.\n \nBut see below.\n\n> 2) Having multiple clients all waiting on the same page (in particular\n> the index page) can lead to invalid output.  In particular if you are\n> teeing the output a reading client now must come in, read the current\n> contents of the file (as written), then pick up on the the tee after\n> that.  It's actually possible for the reading client to miss data as it\n> may be in flight to be written and the client is switching from reading\n> the file to reading the tee.  I don't see anything in your code to\n> handle that kind of switch over.\n\nErr... could you explain what do you mean by \"client is switching from\nreading the file to reading the tee\"?\n\n\nHmmm... I thought that the code is clear.  Generating data, whether it\nis captured to be displayed later, or tee-ed i.e. printed and captured\nto cache, is inside critical section, protected by exclusive lock.  Only\nafter cache entry is written (in full), the lock is released, and clients\nwaiting for data can access it; they use shared (readers) lock for sync.\n \nNote that in my rewrite (and I think also in _some_ cases in your version)\nfiles are written atomically, by writing to temporary file then renaming\nit to final destination.\n\n> 3) This makes no allowance for the file to be generated completely in\n> the background while serving stale data in the interim.  Keep in mind\n> that it can (as Fedora has experienced) take *HOURS* to generate the\n> index page, teeing that output just means brokenness and isn't useful.\n\nIt does make allowance.  cache_output from GitwebCache::CacheOutput uses\ncapturing and not tee-ing if we are in background process.  When there\nis stale data to serve, cache entry is (re)generated in background in\ndetached process.\n \nMoreover by default cache_output has safety in that error pages generated\nby such detached process are cached.\n\nNote also that in my rewrite you can simply (by changing one single \nconfiguration knob) configure gitweb to also cache error pages.  This\nmight be best and safest solution for very large sites with very large\ndisk space, but not so good for smaller sites.\n\n>\n> It's much better to have a simple, lightweight waiting message get\n> displayed while things happen.  When they are done, output the completed\n> page to all waiting clients.\n\nThe problem with 'lightweight waiting message', as it is implemented in\nyour code, and as I stole it ;-), is that it doesn't provide any indicator\nhow much work is already done, and how much work might there be left.\nWell, at least for now.\n\nWith tee-ing client (well, at least the one that is generating data; other\nwould get \"Generating...\", or rather \"Waiting...\" page) can estimate how\nlong would he/she had to wait, and literally see progress, not just some\nprogress indicator.\n\n\nP.S. In my rewrite clients would retry generating page if it was not\ngenerated when they were waiting for it, till they try their own hand\nat generating.  This protects against process generating data being \nkilled; see also test suite for caching interface.\n\n> - John 'Warthog9' Hawley\n> \n> P.S. I'm back to work full-time on Wednesday, which I'll be catching up\n> on gitweb and trying to make forward progress on my gitweb code again.\n\nI'll try to send much simplified (and easier to use in caching) error\nhandling using exceptions (die / eval used as throw / catch) today.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"158863","messageId":"201101040135.08638.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101222235525.7998.99816.stgit@localhost.localdomain","subject":"[RFC PATCH v7 2.5/9] gitweb: Make die_error just die, and use send_error to create error pages","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-01-04T00:35:07Z","receivedAt":"2011-01-04T00:35:07Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Unify error handling by treating errors from Perl (and thrown early in\nprocess using 'die STRING'), and errors from gitweb in the same way.\nThis means that in both cases error page is generated after an error\nis caught in run() subroutine.\n\ndie_error() subroutine is now split into three: gen_error() which\nmassages parameters (escaping HTML, turning HTTP status number into\nfull HTTP status code), die_error() which uses gen_error() and just\nthrows an error (and does not generate an error page), and\nsend_error() which catually generate error page based on provided\nerror / exception.\n\n\nSidenote: probably in the future instead of using simple hash for\nthrowing gitweb exception, gitweb would use some custom error class,\ne.g. derivative of Exception::Class (like SVN::Web does it).\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis is sent early to facilitate early comments.  It passes test suite,\nbut it was not extensively tested.\n\nNow die_error() functions mode like 'die'...\n\n gitweb/gitweb.perl |   47 ++++++++++++++++++++++++++++++++++++-----------\n 1 files changed, 36 insertions(+), 11 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex c7a1892..5854f73 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -1153,9 +1154,13 @@ sub run {\n \t\t\tif $pre_dispatch_hook;\n \n \t\teval { run_request() };\n-\t\tif (defined $@ && !ref($@)) {\n+\t\tmy $error = $@;\n+\t\tif ($error) {\n \t\t\t# some Perl error, but not one thrown by die_error\n-\t\t\tdie_error(undef, undef, $@, -error_handler => 1);\n+\t\t\t$error = gen_error(undef, undef, $error)\n+\t\t\t\tunless ref($error);\n+\n+\t\t\tsend_error($error);\n \t\t}\n \n \tDONE_REQUEST:\n@@ -3730,11 +3735,14 @@ sub git_footer_html {\n #      an unknown error occurred (e.g. the git binary died unexpectedly).\n # 503: The server is currently unavailable (because it is overloaded,\n #      or down for maintenance).  Generally, this is a temporary state.\n-sub die_error {\n+\n+# gen_error()  generates error object from parameters\n+# die_error()  uses gen_error() to generate error object and dies\n+# send_error() generates an error page from provided error object\n+sub gen_error {\n \tmy $status = shift || 500;\n \tmy $error = esc_html(shift) || \"Internal Server Error\";\n \tmy $extra = shift;\n-\tmy %opts = @_;\n \n \tmy %http_responses = (\n \t\t400 => '400 Bad Request',\n@@ -3743,23 +3751,40 @@ sub die_error {\n \t\t500 => '500 Internal Server Error',\n \t\t503 => '503 Service Unavailable',\n \t);\n-\tgit_header_html($http_responses{$status}, undef, %opts);\n+\n+\tmy $err = {\n+\t\t'status' => $status,\n+\t\t'http_status' => $http_responses{$status},\n+\t\t'error'  => $error,\n+\t\t'extra'  => $extra,\n+\t};\n+\treturn $err;\n+}\n+\n+sub die_error {\n+\tmy $error = gen_error(@_);\n+\tprint STDERR Dumper($error);\n+\tdie $error;\n+}\n+\n+sub send_error {\n+\tmy $error = shift;\n+\n+\tgit_header_html($error->{'http_status'}, undef);\n+\n \tprint <<EOF;\n <div class=\"page_body\">\n <br /><br />\n-$status - $error\n+$error->{'status'} - $error->{'error'}\n <br />\n EOF\n-\tif (defined $extra) {\n+\tif (defined $error->{'extra'}) {\n \t\tprint \"<hr />\\n\" .\n-\t\t      \"$extra\\n\";\n+\t\t      \"$error->{'extra'}\\n\";\n \t}\n \tprint \"</div>\\n\";\n \n \tgit_footer_html();\n-\n-\tdie {'status' => $status, 'error' => $error}\n-\t\tunless ($opts{'-error_handler'});\n }\n \n ## ----------------------------------------------------------------------\n-- \n1.7.3\n"},{"id":"158886","messageId":"201101041420.11577.jnareb@gmail.com","threadId":"26127","inReplyTo":"201101040128.26826.jnareb@gmail.com","subject":"Re: [RFC PATCH v7 11/9] [PoC] gitweb/lib - tee, i.e. print and capture during cache entry generation","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-01-04T13:20:10Z","receivedAt":"2011-01-04T13:20:10Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Jakub Narebski wrote:\n> On Tue, 4 Jan 2011, J.H. wrote:\n> > On 01/03/2011 01:33 PM, Jakub Narebski wrote:\n> \n> > > Instead of having gitweb use progress info indicator / throbber to\n> > > notify user that data is being generated by current process, gitweb\n> > > can now (provided that PerlIO::tee from PerlIO::Util is available)\n> > > send page to web browser while simultaneously saving it to cache\n> > > (print and capture, i.e. tee), thus having incremental generating of\n> > > page serve as a progress indicator.\n> > \n> > In general, and particularly for the large sites that caching is\n> > targeted at, teeing is a really bad idea.\n[...]\n> > 1) Errors may still be generated in flight as the cache is being\n> > generated.  It would be better to let the cache run with a progress\n> > indicator and should an error occur, display the error instead of giving\n> > any output that may have been generated (and thus likely a broken page).\n> \n> On the contrary, with tee-ing (and zero size sanity check) you would be\n> able to see pages even if there are errors saving cache entry.  Though\n> this wouldn't help very large sites which cannot function without caching,\n> it could be useful for smaller sites.\n\nI was not sure how Perl reacts to ENOSPC (No space left on device), \nwhich I think it is only error that can be generated in flight as\ncache is being generated (or as gitweb output is printed i.e. sent\nto browser and captured/tee-ed i.e. saved to cache entry file), so\nI have checked this (using loopback to create small filesystem).\n\nThe outcomes one worry about are the following:\n* Perl dies during printing - this leads to broken page send to\n  browser, and no cache entry generated\n* Perl prints output without dying at all; the page send to browser\n  via tee-in is all right, but cache entry is truncated which results\n  in broken page shown to other clients.\n\nBut what actually happens is actually different, and quite safe:\n* Perl prints output without dying, and dies on closing cache entry\n  file with ENOSPC.  This means that client generating data gets correct\n  output, and cache entry is not generated.  Other clients with my code\n  try their hand at generation and also get correct page, but not save\n  it to cache.\n\nThis means that no error page about problems with cache is shown, which\nis bad.  On the other hand, at least for smaller sites, gitweb keeps \nworking as if without cache for newer entries.\n  \nNote that observed behaviour might depend on operating system / filesystem\nparameters, such as buffer sizes.\n\n> But see below.\n\n[...]\n\n> Note also that in my rewrite you can simply (by changing one single \n> configuration knob) configure gitweb to also cache error pages.  This\n> might be best and safest solution for very large sites with very large\n> disk space, but not so good for smaller sites.\n\nErrr... now after rereading your email I see that caching error pages\nhas one problem: errors that come from the caching engine or capturing\nengine - those errors you cannot cache.  Sorry, my mistake.\n\n> > - John 'Warthog9' Hawley\n> > \n> > P.S. I'm back to work full-time on Wednesday, which I'll be catching up\n> > on gitweb and trying to make forward progress on my gitweb code again.\n> \n> I'll try to send much simplified (and easier to use in caching) error\n> handling using exceptions (die / eval used as throw / catch) today.\n\nSent as\n  [RFC PATCH v7 2.5/9] gitweb: Make die_error just die, and use send_error\n    to create error pages\n  Message-ID: <201101040135.08638.jnareb@gmail.com>\n  http://permalink.gmane.org/gmane.comp.version-control.git/164466\n\n-- \nJakub Narebski\nPoland\n"},{"id":"158936","messageId":"201101050327.00450.jnareb@gmail.com","threadId":"26127","inReplyTo":"20101222234843.7998.87068.stgit@localhost.localdomain","subject":"[RFC PATCH 11/9] [PoC] gitweb/lib - HTTP-aware output caching","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2011-01-05T02:26:59Z","receivedAt":"2011-01-05T02:26:59Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This commit adds new option, -http_output, to cache_output()\nsubroutine from GitwebCache::CacheOutput module.  When this subroutine\nis called as cache_output(..., -http_output => 1), it assumes that\ncached output is HTTP response, consisting of HTTP headers separated\nby CR LF pair from the HTTP body (contents of the page).  It adds then\nExpires and Cache-Control: max-age headers if they do not exist based\non current cache entry expiration time, and Content-Length header\nbased on the size of cache entry file.\n\nNew subtest in t9512 includes basic tests for this feature.\n\nEnable it in gitweb, via $cache_options{'cache_output'} hashref.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch is intended as proof of concept about making output caching\nin gitweb make use of the fact that we cache HTTP response.  In this\npatch the \"smarts\" (the \"HTTP awareness\") was added to the part of code\nresponsible by sending response to client.  Alternate solution would\nbe to add such \"smarts\" to saving captured output to cache file, or even\nto caching engine itself.  Each of those solutions has its advantages\nand disadvantages.\n\nJ.H., among others this patch is meant to illustrate that you don't need\ntreat output of 'snapshot' and 'blob_plain' views in a special way; you\ncan add Content-Length header in a action-agnostic way.\n\nThis patch replaces controversial \"[RFC PATCH v7 11/9] [PoC] gitweb/lib\n- tee, i.e. print and capture during cache entry generation\" for\nsimplicity, though it is fairly independent, and probably would apply\nwithout problems after it.\n\n\nNOTE: This is only RFC, and while it passes t9512, I haven't done extensive\ntesting with it.\n\n gitweb/gitweb.perl                             |    3 ++\n gitweb/lib/GitwebCache/CacheOutput.pm          |   32 ++++++++++++++++++++++++\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   24 ++++++++++++++++++\n t/t9512/test_cache_output.pl                   |   25 ++++++++++++++++++\n 4 files changed, 84 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex e3f02b0..f3c14a9 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -360,6 +360,9 @@ our %cache_options = (\n \t\t# pages will be cached if were generated in detached process).\n \t\t# Default is undef.\n \t\t'-cache_errors' => undef,\n+\t\t# Mark that we are caching HTTP response, and that we want extra treatment,\n+\t\t# i.e. automatic adding of Expires/Cache-Control and Content-Length headers\n+\t\t'-http_output' => 1,\n \t},\n );\n # You define site-wide options for \"Generating...\" page (if enabled) here\ndiff --git a/gitweb/lib/GitwebCache/CacheOutput.pm b/gitweb/lib/GitwebCache/CacheOutput.pm\nindex 188d4ab..e9884ca 100644\n--- a/gitweb/lib/GitwebCache/CacheOutput.pm\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -19,6 +19,7 @@ use warnings;\n \n use File::Copy qw();\n use Symbol qw(qualify_to_ref);\n+use CGI::Util qw(expires);\n \n use Exporter qw(import);\n our @EXPORT      = qw(cache_output);\n@@ -69,6 +70,37 @@ sub cache_output {\n \t\t$filename ||= $capture_filename;\n \t}\n \n+\tif ($opts{'-http_output'}) {\n+\t\t# we need filehandle; filename is not enough\n+\t\topen $fh, '<', $filename unless defined $fh;\n+\n+\t\t# get HTTP headers first\n+\t\tmy (@headers, %norm_headers);\n+\t\twhile (my $line = <$fh>) {\n+\t\t\tlast if $line eq \"\\r\\n\";\n+\t\t\tpush @headers, $line;\n+\t\t\tif ($line =~ /^([^:]+:)\\s+(.*)$/) {\n+\t\t\t\t(my $header = lc($1)) =~ s/_/-/;\n+\t\t\t\t$norm_headers{$header} = $2;\n+\t\t\t}\n+\t\t}\n+\t\tprint join('', @headers);\n+\n+\t\t# extra headers\n+\t\tif (!exists $norm_headers{lc('Expires')} &&\n+\t\t    !exists $norm_headers{lc('Cache-Control')}) {\n+\t\t\tmy $expires_in = $cache->expires_in($key);\n+\t\t\tprint \"Expires: \" . expires($expires_in, 'http').\"\\r\\n\".\n+\t\t\t      \"Cache-Control: max-age=$expires_in\\r\\n\";\n+\t\t}\n+\t\tif (!exists $norm_headers{lc('Content-Length')}) {\n+\t\t\tmy $length = (-s $fh) - (tell $fh);\n+\t\t\tprint \"Content-Length: $length\\r\\n\" if $length;\n+\t\t}\n+\n+\t\tprint \"\\r\\n\"; # separates headers from body\n+\t}\n+\n \tif (defined $fh || defined $filename) {\n \t\t# set binmode only if $fh is defined (is a filehandle)\n \t\t# File::Copy::copy opens files given by filename in binary mode\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex 291526e..813331a 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -457,6 +457,30 @@ sub is_valid {\n \treturn (($now - $mtime) < $expires_in);\n }\n \n+# $cache->expires_in($key)\n+#\n+# Returns number of seconds an entry would be valid, or undef\n+# if cache entry for given $key does not exists.\n+sub expires_in {\n+\tmy ($self, $key) = @_;\n+\n+\tmy $path = $self->path_to_key($key);\n+\n+\t# does file exists in cache?\n+\treturn undef unless -f $path;\n+\t# get its modification time\n+\tmy $mtime = (stat(_))[9] # _ to reuse stat structure used in -f test\n+\t\tor $self->_handle_error(\"Couldn't stat file '$path' for key '$key': $!\");\n+\n+\tmy $expires_in = $self->get_expires_in();\n+\n+\tmy $now = time();\n+\tprint STDERR __PACKAGE__.\"now=$now; mtime=$mtime; \".\n+\t\t\"expires_in=$expires_in; diff=\".($now - $mtime).\"\\n\";\n+\n+\treturn $expires_in == 0 ? 0 : ($self->get_expires_in() - ($now - $mtime));\n+}\n+\n # Getting and setting\n \n # ($fh, $filename) = $cache->compute_fh($key, $code);\ndiff --git a/t/t9512/test_cache_output.pl b/t/t9512/test_cache_output.pl\nindex 758848c..3492dcf 100755\n--- a/t/t9512/test_cache_output.pl\n+++ b/t/t9512/test_cache_output.pl\n@@ -6,6 +6,8 @@ use strict;\n \n use Test::More;\n \n+use CGI qw(:standard);\n+\n # test source version\n use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n \n@@ -158,5 +160,28 @@ subtest 'errors are cached with -cache_errors => 1' => sub {\n };\n \n \n+# caching HTTP output\n+subtest 'HTTP output' => sub {\n+\t$cache->remove($key);\n+\t$cache->set_expires_in(60);\n+\n+\tmy $header =\n+\t\theader(-status=>'200 OK', -type=>'text/plain', -charset => 'utf-8');\n+\tmy $data = \"1234567890\";\n+\t$action_output = $header.$data;\n+\t$test_data = capture_output_of_cache_output(\\&action, '-http_output' => 1);\n+\n+\t$header =~ s/\\r\\n$//;\n+\tmy $length = do { use bytes; length($data); };\n+\tlike($test_data, qr/^\\Q$header\\E/, 'http: starts with provided http header');\n+\tlike($test_data, qr/\\Q$data\\E$/,   'http: ends with body (payload)');\n+\tlike($test_data, qr/^Expires: /m,  'http: some \"Expires:\" header added');\n+\tlike($test_data, qr/^Cache-Control: max-age=\\d+\\r\\n/m,\n+\t                                   'http: \"Cache-Control:\" with max-age added');\n+\tlike($test_data, qr/^Content-Length: $length\\r\\n/m,\n+\t                                   'http: \"Content-Length:\" header with correct value'); \n+};\n+\n+\n done_testing();\n __END__\n-- \n1.7.3\n"}]}