{"thread":{"id":"25371","subject":"[PATCHv5 01/17] t/test-lib.sh: Export also GIT_BUILD_DIR in test_external","startedAt":"2010-10-06T22:01:45Z","lastAt":"2010-10-24T21:34:37Z","messageCount":41,"participants":["Jakub Narebski","Thomas Adam","Ævar Arnfjörð Bjarmason","J.H."],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"152787","messageId":"1286402526-13143-1-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":null,"subject":"[PATCHv5 00/17] gitweb: Simple file based output caching","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:45Z","receivedAt":"2010-10-06T22:01:45Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This 17+ patches long patch series is intended as a replacement for\nlarge 'gitweb: File based caching layer (from git.kernel.org)'\nmega-patch by John 'Warthog9' Hawley aka J.H., by starting small and\nadding features piece by piece.\n\nThis is fourthfifth version (5th release) of this series, and is\navailable on http://repo.or.cz/w/git/jnareb-git.git as\n'gitweb/cache-kernel-v5' branch.  Earlier versions are available there\nas branches 'gitweb/cache-kernel', 'gitweb/cache-kernel-v2',\n'gitweb/cache-kernel-v3' and 'gitweb/cache-kernel-v4'.  Previous version\nof this series was sent to git mailing list as:\n\n* [RFC PATCHv4 00/17] gitweb: Simple file based output caching\n  Message-Id: <1276531710-22945-1-git-send-email-jnareb@gmail.com>\n  http://thread.gmane.org/gmane.comp.version-control.git/149101\n\nThis version is based on top of aad24ad (gitweb: Move call to\nevaluate_git_version after evaluate_gitweb_config, 2010-10-05) in\n'gitweb/web' branch of http://repo.or.cz/w/git/jnareb-git.git\nrepository.\n\nThe 'gitweb/web' branch consist of the following commits on top of\n1e63341 (Merge branch 'maint', 2010-09-30) in 'master' branch of main\ngit repository:\n  gitweb/Makefile: Include gitweb/config.mak\n  t/gitweb-lib.sh: Add support for GITWEB_TEST_INSTALLED\n  gitweb/Makefile: Add 'test' and 'test-installed' targets\n  gitweb: Move call to evaluate_git_version after evaluate_gitweb_config\n\nThe first three patches were send as 3-part series:\n* [PATCHv2 0/3] Testing installed gitweb\n  Message-Id: <1285506146-8009-1-git-send-email-jnareb@gmail.com>\n  http://thread.gmane.org/gmane.comp.version-control.git/157222\n\nThose are required to be able to test installed version of gitweb and\nmodules, to check if there are any problems with process of installing\ngitweb.\n\nThe last patch in 'gitweb/web' branch was send as:\n* [PATCH] gitweb: Move call to evaluate_git_version after evaluate_gitweb_config \n  Message-ID: <20100926113431.28918.53550.stgit@localhost.localdomain>\n  http://article.gmane.org/gmane.comp.version-control.git/157220\n\nThis was needed during development of this series, to avoid spurious\noutput of gitweb tests when using `--debug' option.\n\n\nThe MAIN DIFFERENCE from previous version is that it is based on never\nversion of gitweb; in particular putting all code in subroutines: gitweb\ncalls run() subroutine on startup, and run_request() subroutine on each\nrequest (per connection) from the 'jn/gitweb-fastcgi' branch.\n\nThe patches\n  gitweb: Return or exit after done serving request\n  gitweb: Fix typo in hash key name in %opts in git_header_html\n\npresent at beginning of previous (fourth) version of this series are\nalready merged in.\n\nThe patches\n  gitweb/lib - Benchmarking GitwebCache::SimpleFileCache (in t/9603/)\n  gitweb/lib - Alternate ways of capturing output\n\nwhich were interleaved with other patches in previous version of this\nseries got moved to the end of series, and marked proof of concept\n(\"PoC\").\n\n\nThe following changes are available in the git repository at:\n\n  git://repo.or.cz/git/jnareb-git.git gitweb/cache-kernel-v5\n\n\nAll comments welcome!\n\nJ.H., could you comment on how this series relates to the gitweb code\n*currently* running on git.kernel.org wrt. code?  If possible, could you\ntry to compare performance of those two implementations, the one\npresented here in this series, and the one used by git.kernel.org.\n\nPasky or other repo.or.cz admins, could you tell us if and what kind of\ncaching gitweb used by repo.or.cz uses, and how it compares (wrt code\nand if possible also performance) to solution implemented in this\nseries?\n\n\nTODO list and areas of possible improvements would be send in separate\nemail.\n\n\nJakub Narebski (21):\n  t/test-lib.sh: Export also GIT_BUILD_DIR in test_external\n  gitweb: Prepare for splitting gitweb\n  gitweb/lib - Very simple file based cache\n  gitweb/lib - Stat-based cache expiration\n  gitweb/lib - Regenerate entry if the cache file has size of 0\n  gitweb/lib - Simple select(FH) based output capture\n  gitweb/lib - Cache captured output (using get/set)\n  gitweb: Add optional output caching (from gitweb/cache.pm)\n  gitweb/lib - Adaptive cache expiration time\n  gitweb/lib - Use CHI compatibile (compute method) caching interface\n  gitweb/lib - Use locking to avoid 'cache miss stampede' problem\n  gitweb/lib - No need for File::Temp when locking\n  gitweb/lib - Serve stale data when waiting for filling cache\n  gitweb/lib - Regenerate (refresh) cache in background\n  gitweb: Introduce %actions_info, gathering information about actions\n  gitweb: Show appropriate \"Generating...\" page when regenerating cache\n  gitweb: Add startup delay to activity indicator for cache\n  gitweb/lib - Add clear() and size() methods to caching interface\n  gitweb: Add beginnings of cache administration page\n  gitweb/lib - Benchmarking GitwebCache::SimpleFileCache (in t/9603/)\n  gitweb/lib - Alternate ways of capturing output\n\n gitweb/Makefile                                |   24 ++-\n gitweb/README                                  |   61 +++\n gitweb/gitweb.perl                             |  459 ++++++++++++++++++++++--\n gitweb/lib/GitwebCache/CacheOutput.pm          |   95 +++++\n gitweb/lib/GitwebCache/Capture.pm              |   66 ++++\n gitweb/lib/GitwebCache/Capture/PerlIO.pm       |   79 ++++\n gitweb/lib/GitwebCache/Capture/SelectFH.pm     |   82 ++++\n gitweb/lib/GitwebCache/Capture/TiedCapture.pm  |   76 ++++\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |  280 ++++++++++++++\n gitweb/lib/GitwebCache/SimpleFileCache.pm      |  472 ++++++++++++++++++++++++\n gitweb/lib/Tie/Restore.pm                      |   24 ++\n gitweb/lib/TiedCapture/PerlIO.pm               |   56 +++\n gitweb/lib/TiedCapture/String.pm               |   53 +++\n t/t9500-gitweb-standalone-no-errors.sh         |   20 +\n t/t9503-gitweb-caching-interface.sh            |   34 ++\n t/t9503/benchmark_caching_interface.pl         |  209 +++++++++++\n t/t9503/test_cache_interface.pl                |  407 ++++++++++++++++++++\n t/t9504-gitweb-capture-interface.sh            |   34 ++\n t/t9504/benchmark_capture_implementations.pl   |  226 +++++++++++\n t/t9504/test_capture_implementations.pl        |   85 +++++\n t/t9504/test_capture_interface.pl              |   76 ++++\n t/t9505-gitweb-cache.sh                        |   34 ++\n t/t9505/test_cache_output.pl                   |   70 ++++\n t/test-lib.sh                                  |    4 +-\n 24 files changed, 2997 insertions(+), 29 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/CacheOutput.pm\n create mode 100644 gitweb/lib/GitwebCache/Capture.pm\n create mode 100644 gitweb/lib/GitwebCache/Capture/PerlIO.pm\n create mode 100644 gitweb/lib/GitwebCache/Capture/SelectFH.pm\n create mode 100644 gitweb/lib/GitwebCache/Capture/TiedCapture.pm\n create mode 100644 gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n create mode 100644 gitweb/lib/GitwebCache/SimpleFileCache.pm\n create mode 100644 gitweb/lib/Tie/Restore.pm\n create mode 100644 gitweb/lib/TiedCapture/PerlIO.pm\n create mode 100644 gitweb/lib/TiedCapture/String.pm\n create mode 100755 t/t9503-gitweb-caching-interface.sh\n create mode 100755 t/t9503/benchmark_caching_interface.pl\n create mode 100755 t/t9503/test_cache_interface.pl\n create mode 100755 t/t9504-gitweb-capture-interface.sh\n create mode 100755 t/t9504/benchmark_capture_implementations.pl\n create mode 100755 t/t9504/test_capture_implementations.pl\n create mode 100755 t/t9504/test_capture_interface.pl\n create mode 100755 t/t9505-gitweb-cache.sh\n create mode 100755 t/t9505/test_cache_output.pl\n\n-- \n1.7.3\n"},{"id":"152784","messageId":"1286402526-13143-2-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 01/17] t/test-lib.sh: Export also GIT_BUILD_DIR in test_external","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:46Z","receivedAt":"2010-10-06T22:01:46Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This 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---\nThis is not strictly required, but if we modified t/gitweb-lib.sh to\nuse GIT_BUILD_DIR, we would want to do the same in the test scripts\nfor caching interface etc. included in this series.\n\nThis patch was not present in previous version of this series, and it\nhas no equivalent in original patch by J.H.\n\n t/test-lib.sh |    4 ++--\n 1 files changed, 2 insertions(+), 2 deletions(-)\n mode change 100644 => 100755 t/test-lib.sh\n\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 830e5e7..968d240 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -510,9 +510,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-- \n1.7.3\n"},{"id":"152785","messageId":"1286402526-13143-3-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 02/17] gitweb: Prepare for splitting gitweb","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:47Z","receivedAt":"2010-10-06T22:01:47Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Prepare 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\nto test instaleed version of gitweb and installed version of modules\n(for tests which 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---\nThis patch can serve as a way to introduce infrastructure required for\nsplitting gitweb into modules for better maintability.  This issue was\nto be first part (first milestone) for \"Integrated web client for git\"\nGSoC2010 project by Pavan Kumar Sankara (with Christian Couder as\nmentor, and Petr 'Pasky' Baudis as co-mentor)... which unfortunately\nfailed middterm evaluations.\n\nAs you can see later in series it makes it very easy to add new gitweb\nmodules which are to be installed.  Just add\n\n  GITWEB_MODULES += Module/Foo.pm\n\nto gitweb/Makefile for Module::Foo (present in gitweb/lib/Module/Foo.pm)\nand 'install-gitweb' target would automatically install it along with\ngitweb.\n\nDifferences from v4:\n* The 'install-modules' target in gitweb/Makefile uses shell 'for' loop,\n  instead of make's $(foreach) function; see commit message\n\n* Modules in GITWEB_MODULES would not include the 'lib/' prefix, so\n  for module in gitweb/lib/Module/Foo.pm it would be\n    GITWEB_MODULES += Module/Foo.pm\n  and not\n    GITWEB_MODULES += lib/Module/Foo.pm\n\n* Installing modules is left for separate 'install-modules' target,\n  which is prerequisite for 'install' target\n\n* Includes hunk about 'test-installed' target, making it possible to\n  test installed modules and not only installed gitweb.\n\n* 'install' and 'install-modules' targets are marked as .PHONY\n\n* Does not contain spurious vertical whitespace change (adding empty\n  line before 'binmode' invocation).\n\n* Commit message is much longer and more detailed.\n\n\nDifferences from relevant parts of J.H. patch (probably apply also to\nthe gitweb version running git.kernel.org):\n* It uses standard Perl way of naming and installing modules; the main\n  caching module is named GitwebCache::CacheOutput and it is in the\n  gitweb/lib/GitwebCache/CacheOutput.pm and is loaded using 'require'\n  and found thanks to 'use lib', rather than being in gitweb/cache.pm\n  and loaded using \"do 'cache.pm'\", and found by path.\n\n  This has the advantage of automatically detecting errors in the\n  caching module.  (Note that this patch series up to and including v2\n  also used 'do $cache_pm', but implemented error handling, while v3\n  used 'require $cache_pm' i.e. also finding cache module by path.)\n\n* Instead of one 'cache.pm' file, there are separate Perl modules for\n  handling different parts of work: file-based cache, capturing gitweb\n  output, and caching captured output are all in separate modules.\n\n  The code of those modules are made generic and disentangled from\n  gitweb.perl code, which makes it possible to test those modules in\n  isolation.\n\n* 'make install-gitweb' is newer than original patch by J.H.; this\n  patch series makes it possible to install gitweb, its dependencies\n  (static files) _and modules_, using this one simple command.\n\n gitweb/Makefile    |   17 +++++++++++++++--\n gitweb/gitweb.perl |    8 ++++++++\n 2 files changed, 23 insertions(+), 2 deletions(-)\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex df908a1..c2d72e4 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -56,6 +56,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@@ -151,20 +152,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 d521b4c..e4c08ba 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -9,6 +9,14 @@\n \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 set_message);\n-- \n1.7.3\n"},{"id":"152786","messageId":"1286402526-13143-4-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:48Z","receivedAt":"2010-10-06T22:01:48Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This 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\nLike in original patch by John 'Warthog9' Hawley (J.H.) (the one this\ncommit intends to be incremental step to), the data is stored in the\ncase as-is, without adding metadata (like expiration date), and\nwithout serialization (which means that one can store only scalar\ndata).\n\nTo be implemented (from original patch by J.H.):\n* cache expiration (based on file stats, current time and global\n  expiration time); currently elements in cache do not expire at all\n* actually using this cache in gitweb, with exception of error pages\n* adaptive cache expiration, based on average system load\n* optional locking interface, where only one process can update cache\n  (using flock)\n* server-side progress indicator when waiting for filling cache,\n  which in turn requires separating situations (like snapshots and\n  other non-HTML responses) where we should not show 'please wait'\n  message\n\nPossible extensions (beyound what was in original patch):\n* (optionally) show information about cache utilization\n* AJAX (JavaScript-based) progress indicator\n* JavaScript code to update relative dates in cached output\n* make cache size-aware (try to not exceed specified maximum size)\n* utilize X-Sendfile header (or equivalent) to show cached data\n  (optional, as it makes sense only if web server supports sendfile\n  feature and have it enabled)\n* variable expiration feature from CHI, allowing items to expire a bit\n  earlier than the stated expiration time to prevent cache miss\n  stampedes (although locking, if available, should take care of\n  this)\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---\nDifferences from v4:\n* The test got renamed a bit, because separate external tests would be\n  invoked by separate test scripts, for 'prove' to work correctly\n  (as test_external doesn't yet work as subtest).\n\n  The t/t9503-gitweb-caching-interface.sh gets a long description,\n  and is updated to follow conventions used post-TAPification.\n\n* The t/t9503/test_cache_interface.pl uses GIT_BUILD_DIR rather than\n  TEST_DIRECTORY, and respect GITWEBLIBDIR to make it possible to test\n  installed version of module.\n\n* Generating a temporary file to write to is refactored into \"private\"\n  method _tempfile_to_path\n\n* The call to 'mkdir' is enveloped in eval, as it dies on error.\n  make_path would be better here, but is not available for File::Path\n  before v2.00, which is in core only since 5.10\n\n\nDifferences from relevant parts of J.H. patch:\n* Does not use make_path, but older mkpath interface from File::Path;\n  this way we do not require File::Path version >= 2.0.\n\n* Writes to cache file atomically, by writing to unique temporary file\n  (from File::Temp at this point of series) and then renaming/moving\n  this file to the cache file.  This is to make caching more robust,\n  by preventing readers from getting partial data.  The issue this\n  prevents should be very rare, as we write whole data (whole page) at\n  once, after it is generated in full, so this situation can\n  theoretically happen only in the case of heavy load, many clients,\n  and very large page (over the size of (file)system cache).\n\n* Error reporting is done via \"die\", not via die_error from gitweb.\n  This is to avoid entangling cache.pm and gitweb.perl code (making\n  cache.pm dependent on gitweb).  This can be improved further by\n  passing die handler (wrapper around die_error in case of gitweb) to\n  cache (see CHI source code for how it can be solved).\n\n* The depth of cache hierarchy is configurable, while J.H. patch uses\n  hardcoded depth of 1 subdirectory.  It uses unpack rather than substr.\n  For 06b90e786e... key digest it uses 06/b90e786e... (with cache_depth = 1)\n  rather than 06/06b90e786e... in J.H. patch.  \n\n* GitwebCache::SimpleFileCache uses OO (object-oriented) interface,\n  with constructor that takes param names that are compatibile with\n  other cache engines (like CHI or Cache::Cache).\n\n  GitwebCache::SimpleFileCache is a proper Perl module.\n\n* GitwebCache::SimpleFileCache uses standard ->get, ->set, ->compute\n  methods, which should allow easy replacing it with CHI or Cache::Cache\n  cache (see later commit, adding caching support to gitweb).\n\n* Support for namespaces (though default is to have no namespace, at\n  least at this point of series).\n\n* ->fetch($key) and ->store($key, $data) use fast slurp / spew,\n  adapted from File::Slurp read and write subroutines, via\n  CHI::Driver::File\n\n* Tests of caching API in t/t9503-gitweb-caching-interface.sh\n\n* Better Perl style, more consistent with what gitweb itself uses.\n\n\n gitweb/lib/GitwebCache/SimpleFileCache.pm |  324 +++++++++++++++++++++++++++++\n t/t9503-gitweb-caching-interface.sh       |   34 +++\n t/t9503/test_cache_interface.pl           |   81 +++++++\n 3 files changed, 439 insertions(+), 0 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/SimpleFileCache.pm\n create mode 100755 t/t9503-gitweb-caching-interface.sh\n create mode 100755 t/t9503/test_cache_interface.pl\n\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nnew file mode 100644\nindex 0000000..a54f78f\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -0,0 +1,324 @@\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, simple file-based cache\n+#\n+\n+# Minimalistic cache that stores data in the filesystem, without serialization\n+# and currently without any kind of cache expiration (all keys last forever till\n+# they got explicitely removed).\n+#\n+# It follows Cache::Cache and CHI interfaces (but does not implement it fully)\n+\n+package GitwebCache::SimpleFileCache;\n+\n+use strict;\n+use warnings;\n+\n+use File::Path qw(mkpath);\n+use File::Temp qw(tempfile);\n+use Digest::MD5 qw(md5_hex);\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+# ......................................................................\n+# constructor\n+\n+# The options are set by passing in 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.\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+sub new {\n+\tmy ($proto, $p_options_hash_ref) = @_;\n+\n+\tmy $class = ref($proto) || $proto;\n+\tmy $self  = {};\n+\t$self = bless($self, $class);\n+\n+\tmy ($root, $depth, $ns);\n+\tif (defined $p_options_hash_ref) {\n+\t\t$root  =\n+\t\t\t$p_options_hash_ref->{'cache_root'} ||\n+\t\t\t$p_options_hash_ref->{'root_dir'};\n+\t\t$depth =\n+\t\t\t$p_options_hash_ref->{'cache_depth'} ||\n+\t\t\t$p_options_hash_ref->{'depth'};\n+\t\t$ns    = $p_options_hash_ref->{'namespace'};\n+\t}\n+\t$root  = $DEFAULT_CACHE_ROOT  unless defined($root);\n+\t$depth = $DEFAULT_CACHE_DEPTH unless defined($depth);\n+\t$ns    = $DEFAULT_NAMESPACE   unless defined($ns);\n+\n+\t$self->set_root($root);\n+\t$self->set_depth($depth);\n+\t$self->set_namespace($ns);\n+\n+\treturn $self;\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)) {\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+# utility functions and methods\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'} || 1).\" 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+# ----------------------------------------------------------------------\n+# \"private\" utility functions and methods\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 ($file, $dir) = @_;\n+\n+\t# tempfile will croak() if there is an error\n+\treturn tempfile(\"${file}_XXXXX\",\n+\t\t#DIR => $dir,\n+\t\t'UNLINK' => 0, # ensure that we don't unlink on close; file is renamed\n+\t\t'SUFFIX' => '.tmp');\n+}\n+\n+\n+# ----------------------------------------------------------------------\n+# worker methods\n+\n+sub fetch {\n+\tmy ($self, $key) = @_;\n+\n+\tmy $file = $self->path_to_key($key);\n+\treturn undef unless (defined $file && -f $file);\n+\n+\t# Fast slurp, adapted from File::Slurp::read, with unnecessary options removed\n+\t# via CHI::Driver::File (from CHI-0.33)\n+\tmy $buf = '';\n+\topen my $read_fh, '<', $file\n+\t\tor return undef;\n+\tbinmode $read_fh, ':raw';\n+\n+\tmy $size_left = -s $read_fh;\n+\n+\twhile ($size_left > 0) {\n+\t\tmy $read_cnt = sysread($read_fh, $buf, $size_left, length($buf));\n+\t\treturn undef unless defined $read_cnt;\n+\n+\t\tlast if $read_cnt == 0;\n+\t\t$size_left -= $read_cnt;\n+\t\t#last if $size_left <= 0;\n+\t}\n+\n+\tclose $read_fh\n+\t\tor die \"Couldn't close file '$file' opened for reading: $!\";\n+\treturn $buf;\n+}\n+\n+sub store {\n+\tmy ($self, $key, $data) = @_;\n+\n+\tmy $dir;\n+\tmy $file = $self->path_to_key($key, \\$dir);\n+\treturn undef unless (defined $file && defined $dir);\n+\n+\t# ensure that directory leading to cache file exists\n+\tif (!-d $dir) {\n+\t\teval { mkpath($dir, 0, 0777); 1 }\n+\t\t\tor die \"Couldn't create leading directory '$dir' (mkpath): $!\";\n+\t}\n+\n+\t# generate a temporary file\n+\tmy ($temp_fh, $tempname) = $self->_tempfile_to_path($file, $dir);\n+\tchmod 0666, $tempname\n+\t\tor warn \"Couldn't change permissions to 0666 / -rw-rw-rw- for '$tempname': $!\";\n+\n+\t# Fast spew, adapted from File::Slurp::write, with unnecessary options removed\n+\t# via CHI::Driver::File (from CHI-0.33)\n+\tmy $write_fh = $temp_fh;\n+\tbinmode $write_fh, ':raw';\n+\n+\tmy $size_left = length($data);\n+\tmy $offset = 0;\n+\n+\twhile ($size_left > 0) {\n+\t\tmy $write_cnt = syswrite($write_fh, $data, $size_left, $offset);\n+\t\treturn undef unless defined $write_cnt;\n+\t\t\n+\t\t$size_left -= $write_cnt;\n+\t\t$offset += $write_cnt; # == length($data);\n+\t}\n+\n+\tclose $temp_fh\n+\t\tor die \"Couldn't close temporary file '$tempname' opened for writing: $!\";\n+\trename($tempname, $file)\n+\t\tor die \"Couldn't rename temporary file '$tempname' to '$file': $!\";\n+}\n+\n+# get size of an element associated with the $key (not the size of whole cache)\n+sub get_size {\n+\tmy ($self, $key) = @_;\n+\n+\tmy $path = $self->path_to_key($key)\n+\t\tor return undef;\n+\tif (-f $path) {\n+\t\treturn -s $path;\n+\t}\n+\treturn 0;\n+}\n+\n+# ......................................................................\n+# interface methods\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 undef;\n+\treturn undef unless -f $file;\n+\tunlink($file)\n+\t\tor die \"Couldn't remove file '$file': $!\";\n+}\n+\n+# Getting and setting\n+\n+# $cache->set($key, $data);\n+#\n+# Associates $data with $key in the cache, overwriting any existing entry.\n+# Returns $data.\n+sub set {\n+\tmy ($self, $key, $data) = @_;\n+\n+\treturn unless (defined $key && defined $data);\n+\n+\t$self->store($key, $data);\n+\n+\treturn $data;\n+}\n+\n+# $data = $cache->get($key);\n+#\n+# Returns the data associated with $key.  If $key does not exist\n+# or has expired, returns undef.\n+sub get {\n+\tmy ($self, $key) = @_;\n+\n+\tmy $data = $self->fetch($key)\n+\t\tor return undef;\n+\n+\treturn $data;\n+}\n+\n+# $data = $cache->compute($key, $code);\n+#\n+# Combines the get and set operations in a single call.  Attempts to\n+# get $key; if successful, returns the value.  Otherwise, calls $code\n+# and uses the return value as the new value for $key, which is then\n+# returned.\n+sub compute {\n+\tmy ($self, $key, $code) = @_;\n+\n+\tmy $data = $self->get($key);\n+\tif (!defined $data) {\n+\t\t$data = $code->($self, $key);\n+\t\t$self->set($key, $data);\n+\t}\n+\n+\treturn $data;\n+}\n+\n+1;\n+__END__\n+# end of package GitwebCache::SimpleFileCache;\ndiff --git a/t/t9503-gitweb-caching-interface.sh b/t/t9503-gitweb-caching-interface.sh\nnew file mode 100755\nindex 0000000..819da1d\n--- /dev/null\n+++ b/t/t9503-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::* Perl API (in gitweb/lib/)' \\\n+\t\"$PERL_PATH\" \"$TEST_DIRECTORY\"/t9503/test_cache_interface.pl\n+\n+test_done\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nnew file mode 100755\nindex 0000000..6a7b715\n--- /dev/null\n+++ b/t/t9503/test_cache_interface.pl\n@@ -0,0 +1,81 @@\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+# Test creating a cache\n+#\n+BEGIN { use_ok('GitwebCache::SimpleFileCache'); }\n+my $cache = new_ok('GitwebCache::SimpleFileCache');\n+\n+# Test that default values are defined\n+#\n+ok(defined $GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT,\n+\t'$DEFAULT_CACHE_ROOT defined');\n+ok(defined $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH,\n+\t'$DEFAULT_CACHE_DEPTH defined');\n+\n+# Test accessors and default values for cache\n+#\n+SKIP: {\n+\tskip 'default values not defined', 3\n+\t\tunless ($GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT &&\n+\t\t        $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH);\n+\n+\tis($cache->get_namespace(), '', \"default namespace is ''\");\n+\tcmp_ok($cache->get_root(),  'eq', $GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT,\n+\t\t\"default cache root is '$GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT'\");\n+\tcmp_ok($cache->get_depth(), '==', $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH,\n+\t\t\"default cache depth is $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH\");\n+}\n+\n+# Test the getting, setting, and removal of a cached value\n+# (Cache::Cache interface)\n+#\n+my $key = 'Test Key';\n+my $value = 'Test Value';\n+\n+subtest 'Cache::Cache interface' => sub {\n+\tforeach my $method (qw(get set remove)) {\n+\t\tcan_ok($cache, $method);\n+\t}\n+\n+\t$cache->set($key, $value);\n+\tcmp_ok($cache->get_size($key), '>', 0, 'get_size after set, is greater than 0');\n+\tis($cache->get($key), $value,          'get after set, returns cached value');\n+\t$cache->remove($key);\n+\tok(!defined($cache->get($key)),        'get after remove, is undefined');\n+\n+\teval { $cache->remove('Not-Existent Key'); };\n+\tok(!$@,                                'remove on non-existent key doesn\\'t die');\n+\tdiag($@) if $@;\n+\n+\tdone_testing();\n+};\n+\n+# Test the getting and setting of a cached value\n+# (CHI interface)\n+#\n+my $call_count = 0;\n+sub get_value {\n+\t$call_count++;\n+\treturn $value;\n+}\n+subtest 'CHI interface' => sub {\n+\tcan_ok($cache, qw(compute));\n+\n+\tis($cache->compute($key, \\&get_value), $value, \"compute 1st time (set) returns '$value'\");\n+\tis($cache->compute($key, \\&get_value), $value, \"compute 2nd time (get) returns '$value'\");\n+\tis($cache->compute($key, \\&get_value), $value, \"compute 3rd time (get) returns '$value'\");\n+\tcmp_ok($call_count, '==', 1, 'get_value() is called once from compute');\n+\n+\tdone_testing();\n+};\n+\n+done_testing();\n-- \n1.7.3\n"},{"id":"152788","messageId":"1286402526-13143-5-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 04/17] gitweb/lib - Stat-based cache expiration","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:49Z","receivedAt":"2010-10-06T22:01:49Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add stat-based cache expiration to file-based GitwebCache::SimpleFileCache.\nContrary to the way other caching interfaces such as Cache::Cache and CHI\ndo it, the time cache element expires in is _global_ value associated with\ncache instance, and is not local property of cache entry.  (Currently cache\nentry does not store any metadata associated with entry... which means that\nthere is no need for serialization / marshalling / freezing and thawing.)\nDefault expire time is -1, which means never expire.\n\nTo check if cache entry is expired, GitwebCache::SimpleFileCache compares\ndifference between mtime (last modify time) of a cache file and current time\nwith (global) time to expire.  It is done using CHI-compatibile is_valid()\nmethod.\n\nAdd some tests checking that expiring works correctly (on the level of API).\n\nTo be implemented (from original patch by J.H.):\n* actually using this cache in gitweb, except error pages\n* adaptive cache expiration, based on average system load\n* optional locking interface, where only one process can update cache\n  (using flock)\n* server-side progress indicator when waiting for filling cache,\n  which in turn requires separating situations (like snapshots and\n  other non-HTML responses) where we should not show 'please wait'\n  message\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nNow that caching engine supports cache expiration, we can add caching\nsupport to gitweb.\n\nNo significant differences from v4 version of this patch.\n\nDifferences from relevant parts of J.H. patch:\n* It simply uses stat on last accessed file (checked for existence),\n  instead of opening file for reading (without error detection!), running\n  stat on it, and then closing it.\n\n* One can use expire time of -1 (or to be more exact less than 0) to set\n  expire time to never (cache is considered fresh forever, does not expire).\n\n* There are some tests in t9503 of cache expiration (one of those assume\n  that expire time of one day would be not expired in get after set).\n\n* It reuses stat structure (stat(_)), and calculates current time only\n  once.\n\n gitweb/lib/GitwebCache/SimpleFileCache.pm |   39 +++++++++++++++++++++++++++-\n t/t9503/test_cache_interface.pl           |   19 ++++++++++++++\n 2 files changed, 56 insertions(+), 2 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex a54f78f..74d7246 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -57,6 +57,10 @@ our $DEFAULT_NAMESPACE = '';\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 sub new {\n \tmy ($proto, $p_options_hash_ref) = @_;\n \n@@ -64,7 +68,7 @@ sub new {\n \tmy $self  = {};\n \t$self = bless($self, $class);\n \n-\tmy ($root, $depth, $ns);\n+\tmy ($root, $depth, $ns, $expires_in);\n \tif (defined $p_options_hash_ref) {\n \t\t$root  =\n \t\t\t$p_options_hash_ref->{'cache_root'} ||\n@@ -73,14 +77,19 @@ sub new {\n \t\t\t$p_options_hash_ref->{'cache_depth'} ||\n \t\t\t$p_options_hash_ref->{'depth'};\n \t\t$ns    = $p_options_hash_ref->{'namespace'};\n+\t\t$expires_in =\n+\t\t\t$p_options_hash_ref->{'default_expires_in'} ||\n+\t\t\t$p_options_hash_ref->{'expires_in'};\n \t}\n \t$root  = $DEFAULT_CACHE_ROOT  unless defined($root);\n \t$depth = $DEFAULT_CACHE_DEPTH unless defined($depth);\n \t$ns    = $DEFAULT_NAMESPACE   unless defined($ns);\n+\t$expires_in = -1 unless defined($expires_in); # <0 means never\n \n \t$self->set_root($root);\n \t$self->set_depth($depth);\n \t$self->set_namespace($ns);\n+\t$self->set_expires_in($expires_in);\n \n \treturn $self;\n }\n@@ -91,7 +100,7 @@ sub new {\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)) {\n+foreach my $i (qw(depth root namespace expires_in)) {\n \tmy $field = $i;\n \tno strict 'refs';\n \t*{\"get_$field\"} = sub {\n@@ -272,6 +281,31 @@ sub remove {\n \t\tor die \"Couldn't remove file '$file': $!\";\n }\n \n+# $cache->is_valid($key)\n+#\n+# Returns a boolean indicating whether $key exists in the cache\n+# and has not expired (global per-cache 'expires_in').\n+sub is_valid {\n+\tmy ($self, $key) = @_;\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 die \"Couldn't stat file '$path': $!\";\n+\n+\t# expire time can be set to never\n+\tmy $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 # $cache->set($key, $data);\n@@ -295,6 +329,7 @@ sub set {\n sub get {\n \tmy ($self, $key) = @_;\n \n+\treturn undef unless $self->is_valid($key);\n \tmy $data = $self->fetch($key)\n \t\tor return undef;\n \ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 6a7b715..adca88d 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -78,4 +78,23 @@ subtest 'CHI interface' => sub {\n \tdone_testing();\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');\n+\tis($cache->get($key), $value,            'get 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($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+\t$cache->set($key, $value);\n+\tok(!defined($cache->get($key)),          'cache is expired');\n+\n+\tdone_testing();\n+};\n+\n done_testing();\n-- \n1.7.3\n"},{"id":"152789","messageId":"1286402526-13143-6-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 05/17] gitweb/lib - Regenerate entry if the cache file has size of 0","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:50Z","receivedAt":"2010-10-06T22:01:50Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"If the file representing cache entry has size 0 (in bytes), the cache\nentry is considered invalid, regardless of its freshness, even if cache\nexpiration is turned off.\n\n[jh: This is a quick, and thankfully easy, check to regenerate the\ncache if the resulting file is of size 0.  This *SHOULDN'T* happen,\nbut it is possible that it could and thus adding the check.]\n\nBased-on-commit-by: John 'Warthog9' Hawley <warthog9@eaglescrag.net>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch is new and wasn't present in previous version of this\nseries.\n\nIt is inspired by commit 22eb7af (Gitweb - Regenerate the cache if the\nresulting file has size of 0, 2010-09-23) on 'master' branch of\ngit/warthog9/gitweb.git repository on git.kernel.org\n\n gitweb/lib/GitwebCache/SimpleFileCache.pm |    2 ++\n 1 files changed, 2 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex 74d7246..6833699 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -295,6 +295,8 @@ sub is_valid {\n \t# get its modification time\n \tmy $mtime = (stat(_))[9] # _ to reuse stat structure used in -f test\n \t\tor die \"Couldn't stat file '$path': $!\";\n+\t# cache entry is invalid if it is size 0 (in bytes)\n+\treturn 0 unless ((stat(_))[7] > 0);\n \n \t# expire time can be set to never\n \tmy $expires_in = $self->get_expires_in();\n-- \n1.7.3\n"},{"id":"152794","messageId":"1286402526-13143-7-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 06/17] gitweb/lib - Simple select(FH) based output capture","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:51Z","receivedAt":"2010-10-06T22:01:51Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add two packages: GitwebCache::Capture, which defines interface, and\nGitwebCache::Capture::SelectFH, which is actually implements simple\ncapturing.  GitwebCache::Capture::SelectFH captures output by using\nselect(FILEHANDLE) to change default filehandle for output.  This\nmeans that output of a \"print\" or a \"printf\" (or a \"write\") without\na filehandle would be captured.\n\nTo change mode of filehandle used for capturing correctly,\n  binmode select(), <mode>;\nneeds to be used in place of\n  binmode STDOUT, <mode>;\n\nCapturing is done using in-memory file held in Perl scalar.\n\nUsing select(FILEHANDLE) is a bit fragile as a method of capturing\noutput, as it assumes that we always use \"print\" or \"printf\" without\nfilehandle, and use select() which returns default filehandle for\noutput in place of explicit STDOUT.  On the other hand it has the\nadvantage of being simple.  Alternate solutions include using tie\n(like in CGI::Cache), or using PerlIO layers - but the last requires\nnon-standard PerlIO::Util module.\n\n\nIncludes separate tests for capturing output.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nYou can see alternate solutions for capturing output in 21/17 patch of\nthis series: \"gitweb/lib - Alternate ways of capturing output\" (an\nappendix to this series).\n\nDifferences from v4:\n* The capture interface tests are now invoked by a separate test\n  script t/t9504-gitweb-capture-interface.sh, for 'prove' to work\n  correctly (as test_external doesn't yet work as subtest).\n\n* The t/t9504/test_capture_interface.pl uses GIT_BUILD_DIR rather than\n  TEST_DIRECTORY, and respect GITWEBLIBDIR to make it possible to test\n  installed version of module.\n\n* Removed spurious changes and fixes to commits earlier in series\n  (patch cleanup).\n\n\nDifferences from relevant parts of J.H. patch:\n* Capturing gitweb output will be done without need to modify gitweb\n  to either save generated output into $output variable, and then\n  print it or save it in cache after it is generated in full (original\n  J.H. patch in \"Gitweb caching v2\"), or changing all print statements\n  to print to explicit filehandle which points to STDOUT if caching is\n  disabled and to in-memory file if caching is enabled (modified\n  J.H. patch in \"Gitweb caching v5\").\n\n* Contrary to the '$output .= <sth>' solution, and similar to the\n  'print {$out} <sth>' or 'print $out <sth>' (which can be thought of\n  as explicit version of select($out)), this way of capturing output\n  doesn't change gitweb behavior when caching is turned off; in\n  particular it preserves streaming.\n\n  Also the '$output .= <sth>' solution can affect performance because\n  of repeated string concatenation.\n\n* The most important issue is that I/O \"layers\" (PerlIO), like ':utf8'\n  or ':raw', are *already applied* to the output that is captured.\n  This means that captured output is *always* in binary (':raw') mode.\n  In Perl 6 language it means that data returned by capturing engine\n  is an equivalent of Buf, a collection of bytes, whether Buf or Str\n  (a colection of logical characters) is printed.\n\n  The overal result is that we would not need separate code path for\n  caching binary output, and separate naming conventions for cache\n  files for binary data.\n\n  The t9504 test is about checking if both ':utf8' and ':raw' output\n  is captured correctly.\n\n gitweb/lib/GitwebCache/Capture.pm          |   66 ++++++++++++++++++++++\n gitweb/lib/GitwebCache/Capture/SelectFH.pm |   82 ++++++++++++++++++++++++++++\n t/t9504-gitweb-capture-interface.sh        |   34 ++++++++++++\n t/t9504/test_capture_interface.pl          |   76 ++++++++++++++++++++++++++\n 4 files changed, 258 insertions(+), 0 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/Capture.pm\n create mode 100644 gitweb/lib/GitwebCache/Capture/SelectFH.pm\n create mode 100755 t/t9504-gitweb-capture-interface.sh\n create mode 100755 t/t9504/test_capture_interface.pl\n\ndiff --git a/gitweb/lib/GitwebCache/Capture.pm b/gitweb/lib/GitwebCache/Capture.pm\nnew file mode 100644\nindex 0000000..3e9fe81\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/Capture.pm\n@@ -0,0 +1,66 @@\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+# Output capturing for gitweb caching engine\n+#\n+\n+# It is base abstract class (a role) for capturing output of gitweb\n+# actions for gitweb caching engine.\n+# \n+# Child (derived) concrete classes, which actually implement some method\n+# of capturing STDOUT output, must implement the following methods:\n+# * ->new(), to create new object of a capturing class\n+# * ->start(), to start capturing output\n+# * ->stop(), to stop capturing output and return it\n+#\n+# Before starting capture by using capture_block etc. subroutines,\n+# one has to run <child class>->setup().\n+\n+package GitwebCache::Capture;\n+\n+use strict;\n+use warnings;\n+\n+use Exporter qw(import);\n+our @EXPORT    = qw(capture_start capture_stop capture_block);\n+our @EXPORT_OK = qw(setup_capture);\n+our %EXPORT_TAGS = (all => [ @EXPORT, @EXPORT_OK ]);\n+\n+# Holds object used for capture (of child class)\n+my $capture;\n+\n+sub setup_capture {\n+\tmy $self = shift || __PACKAGE__;\n+\n+\t$capture = $self->new(@_);\n+}\n+\n+sub capture {\n+\tmy ($self, $code) = @_;\n+\n+\t$self->start();\n+\t$code->();\n+\treturn $self->stop();\n+}\n+\n+# Wrap caching data; capture only STDOUT\n+sub capture_block (&) {\n+\tmy $code = shift;\n+\treturn $capture->capture($code);\n+}\n+\n+sub capture_start {\n+\t$capture->start(@_);\n+}\n+\n+sub capture_stop {\n+\treturn $capture->stop(@_);\n+}\n+\n+1;\n+__END__\n+# end of package GitwebCache::Capture;\ndiff --git a/gitweb/lib/GitwebCache/Capture/SelectFH.pm b/gitweb/lib/GitwebCache/Capture/SelectFH.pm\nnew file mode 100644\nindex 0000000..18ce5c3\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/Capture/SelectFH.pm\n@@ -0,0 +1,82 @@\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 using select(FH);\n+#\n+\n+# This module (class) captures output of 'print <sth>', 'printf <sth>'\n+# and 'write <sth>' (without a filehandle) by using select(FILEHANDLE)\n+# to change default filehandle for output, changing it to in-memory\n+# file (saving output to scalar).\n+#\n+# Note that when using this simplest way of capturing, to change mode of\n+# filehandle using for capturing correctly, \"binmode STDOUT, <mode>;\"\n+# has to be changed to \"binmode select(), <mode>;\".  This has no change\n+# if we are not capturing output using GitwebCache::Capture::SelectFH.\n+\n+package GitwebCache::Capture::SelectFH;\n+\n+use PerlIO;\n+\n+use strict;\n+use warnings;\n+\n+use base qw(GitwebCache::Capture);\n+use GitwebCache::Capture qw(:all);\n+\n+use Exporter qw(import);\n+our @EXPORT      = @GitwebCache::Capture::EXPORT;\n+our @EXPORT_OK   = @GitwebCache::Capture::EXPORT_OK;\n+our %EXPORT_TAGS = %GitwebCache::Capture::EXPORT_TAGS;\n+\n+# Constructor\n+sub new {\n+\tmy $proto = shift;\n+\n+\tmy $class = ref($proto) || $proto;\n+\tmy $self  = {};\n+\t$self = bless($self, $class);\n+\n+\t$self->{'oldfh'} = select();\n+\t$self->{'data'} = '';\n+\n+\treturn $self;\n+}\n+\n+# Start capturing data (STDOUT)\n+# (printed using 'print <sth>' or 'printf <sth>')\n+sub start {\n+\tmy $self = shift;\n+\n+\t$self->{'data'}    = '';\n+\t$self->{'data_fh'} = undef;\n+\t\n+\topen $self->{'data_fh'}, '>', \\$self->{'data'}\n+\t\tor die \"Couldn't open in-memory file for capture: $!\";\n+\t$self->{'oldfh'} = select($self->{'data_fh'});\n+\n+\t# note: this does not cover all cases\n+\tbinmode select(), ':utf8'\n+\t\tif ((PerlIO::get_layers($self->{'oldfh'}))[-1] eq 'utf8');\n+}\n+\n+# Stop capturing data (required for die_error)\n+sub stop {\n+\tmy $self = shift;\n+\n+\t# return if we didn't start capturing\n+\treturn unless defined $self->{'data_fh'};\n+\n+\tselect($self->{'oldfh'});\n+\tclose $self->{'data_fh'}\n+\t\tor die \"Couldn't close in-memory file for capture: $!\";\n+\treturn $self->{'data'};\n+}\n+\n+1;\n+__END__\n+# end of package GitwebCache::Capture::SelectFH;\ndiff --git a/t/t9504-gitweb-capture-interface.sh b/t/t9504-gitweb-capture-interface.sh\nnew file mode 100755\nindex 0000000..82623f1\n--- /dev/null\n+++ b/t/t9504-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\"/t9504/test_capture_interface.pl\n+\n+test_done\ndiff --git a/t/t9504/test_capture_interface.pl b/t/t9504/test_capture_interface.pl\nnew file mode 100755\nindex 0000000..55c402a\n--- /dev/null\n+++ b/t/t9504/test_capture_interface.pl\n@@ -0,0 +1,76 @@\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+# prototypes must be known at compile time, otherwise they do not work\n+BEGIN { use_ok('GitwebCache::Capture::SelectFH', qw(:all)); }\n+\n+# Test setting up capture\n+#\n+my $capture = new_ok('GitwebCache::Capture::SelectFH' => [], 'The $capture');\n+isa_ok($capture, 'GitwebCache::Capture', 'The $capture');\n+ok(setup_capture('GitwebCache::Capture::SelectFH'),\n+   'setup_capture with package name: GitwebCache::Capture::SelectFH');\n+ok(setup_capture($capture),\n+   'setup_capture with subclass object: $capture');\n+\n+# Test properties of capture_block\n+#\n+is(prototype('capture_block'), '&', 'capture_block has (&) prototype');\n+\n+# Test capturing\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_block captures simple data');\n+\n+binmode STDOUT, ':utf8';\n+$test_data = <<'EOF';\n+Áéí óú\n+ÄËÑÏÖ\n+Ábçdèfg\n+Zażółć gęsią jaźń\n+山田 太郎\n+ブレームのテストです。\n+\n+はれひほふ\n+\n+しているのが、いるので。\n+濱浜ほれぷりぽれまびぐりろへ。\n+EOF\n+utf8::decode($test_data);\n+$captured = capture_block {\n+\tbinmode select(), ':utf8';\n+\n+\tprint $test_data;\n+};\n+utf8::decode($captured);\n+is($captured, $test_data, 'capture_block captures utf8 data');\n+\n+$test_data = '|\\x{fe}\\x{ff}|\\x{9F}|\\000|'; # invalid utf-8\n+$captured = capture_block {\n+\tbinmode select(), ':raw';\n+\n+\tprint $test_data;\n+};\n+is($captured, $test_data, 'capture_block captures raw data');\n+\n+\n+done_testing();\n+\n+# Local Variables:\n+# encoding: utf-8\n+# End:\n-- \n1.7.3\n"},{"id":"152796","messageId":"1286402526-13143-8-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 07/17] gitweb/lib - Cache captured output (using get/set)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:52Z","receivedAt":"2010-10-06T22:01:52Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add GitwebCache::CacheOutput package, which contains cache_output\nsubroutine, and (re)exports capture_stop from GitwebCache::Capture.\nThe cache_output gets data from cache and prints it, or captures\noutput of provided subroutine (code reference), saves it to cache and\nprints it.  It currently uses Cache::Cache compatibile (get, set)\ninterface to cache.  Capturing is currently (not configurable) done\nusing GitwebCache::Capture::SelectFH introduced in previous commit,\nbut any class derived from GitwebCache::Capture (like provided example\nGitwebCache::Capture::TiedCapture and GitwebCache::Capture::PerlIO)\nwould work.\n\nGitweb would use cache_output to get page from cache, or to generate\npage and save it to cache.  The capture_stop would be used in\ndie_error subroutine, because error pages would not be cached.\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 t9505/test_cache_output.pl test, run as external test in\nt9505-gitweb-cache.sh.  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.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nDifferences from v4:\n* Similarly to other patches in this revision of gitweb caching\n  series, the new test for this new module, written in Perl, is now\n  invoked from a separate test script, to make it possible to run\n  'prove' on git test suite.\n\n  Also new test uses GIT_BUILD_DIR rather than TEST_DIRECTORY/.., and\n  it respects GITWEBLIBDIR to make it possible to test installed\n  version of this module.\n\n* GitwebCache::CacheOutput no longer loads GitwebCache::SimpleFileCache;\n  It is now assumed that cache_output() caller loads required packages\n  itself, so that ->get() and ->set() methods $cache instance, passed\n  as argument to cache_output(), work correctly.\n\n  This required small change to t/t9505/test_cache_output.pl\n\n\nDifferences from relevant parts of J.H. patch:\n* cache_fetch() subroutine, now named cache_output(), is much, much\n  simpler.  Well, at this point in the patch series it lacks most of\n  the features of original cache_fetch() by J.H.: it doesn't have\n  adaptive cache lifetime, nor locking to prevent 'stampeding herd'\n  problem, nor serving stale data when waiting for cache regeneration,\n  nor background data generation, nor activity indicator... but the\n  cache_output() itself doesn't change much in later commits, as those\n  features are added mainly via methods and subroutines that\n  cache_output() calls.\n\n* Capturing gitweb output would be done without need to extensively\n  modify gitweb to either 1) save generated output into $output\n  variable, i.e. replace 'print <sth>' by '$output .= <sth>', and print\n  it or save in cache after it is generated in full (original J.H. patch\n  in \"Gitweb caching v2\"), or 2) changing all print statements to print\n  to explicit filehandle, i.e. replace 'print <sth>' by \n  'print {$out} <sth>', which filehandle points to STDOUT if caching is\n  disabled and to in-memory file if caching is enabled (modified\n  J.H. patch in \"Gitweb caching v5\").\n\n  Currently capturing output is hardcoded to GitwebCache::Capture::SelectFH,\n  but changing it to other compatibile capturing engine requires (in\n  current version) change only only two lines in GitwebCache::CacheOutput\n  module.\n\n* It is assumed that capture generates binary output (with I/O filters\n  already applied), and that therefore we read from cache files in\n  binary mode, and we print both captured output and data retrieved from\n  cache in ':raw' mode (in binmode).\n\n* The key (human-readable unique id) for a page (for given capture) is\n  passed explicitely to cache_output().  The cache_output() subroutine\n  no longer depends of subroutines (like href()) and variables (like\n  $cation or %actions) defined in gitweb.perl.\n\n  This allows to simply use 'require <package>' instead of 'do $package_pm'\n  (which also means that it can be installed as module in PERL5LIB etc.).\n\n* Added tests for cache_output() behavior.\n\n gitweb/lib/GitwebCache/CacheOutput.pm |   64 ++++++++++++++++++++++++++++++\n t/t9505-gitweb-cache.sh               |   34 ++++++++++++++++\n t/t9505/test_cache_output.pl          |   70 +++++++++++++++++++++++++++++++++\n 3 files changed, 168 insertions(+), 0 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/CacheOutput.pm\n create mode 100755 t/t9505-gitweb-cache.sh\n create mode 100755 t/t9505/test_cache_output.pl\n\ndiff --git a/gitweb/lib/GitwebCache/CacheOutput.pm b/gitweb/lib/GitwebCache/CacheOutput.pm\nnew file mode 100644\nindex 0000000..bba73ee\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -0,0 +1,64 @@\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 GitwebCache::Capture::SelectFH qw(:all);\n+\n+use Exporter qw(import);\n+our @EXPORT      = qw(cache_output capture_stop);\n+our %EXPORT_TAGS = (all => [ @EXPORT ]);\n+\n+# cache_output($cache, $key, $action_code);\n+#\n+# Attempts to get $key from $cache; if successful, prints the value.\n+# Otherwise, calls $action_code, capture its output and use\n+# 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+our $CAPTURE_CLASS = 'GitwebCache::Capture::SelectFH';\n+\n+sub cache_output {\n+\tmy ($cache, $key, $code) = @_;\n+\n+\tmy $data = $cache->get($key);\n+\n+\t# capture and cache output, if there was nothing in the cache\n+\tif (!defined $data) {\n+\t\tmy $capture = $CAPTURE_CLASS;\n+\t\tsetup_capture($capture);\n+\n+\t\t# do not use 'capture_block' prototype\n+\t\t$data = &capture_block($code);\n+\t\t$cache->set($key, $data) if defined $data;\n+\t}\n+\n+\t# print cached data\n+\tif (defined $data) {\n+\t\t# select() instead of STDOUT is here for tests:\n+\t\tbinmode select(), ':raw';\n+\t\tprint $data;\n+\t}\n+\n+\treturn $data;\n+}\n+\n+1;\n+__END__\n+# end of package GitwebCache::CacheOutput;\ndiff --git a/t/t9505-gitweb-cache.sh b/t/t9505-gitweb-cache.sh\nnew file mode 100755\nindex 0000000..55d3e17\n--- /dev/null\n+++ b/t/t9505-gitweb-cache.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\"/t9505/test_cache_output.pl\n+\n+test_done\ndiff --git a/t/t9505/test_cache_output.pl b/t/t9505/test_cache_output.pl\nnew file mode 100755\nindex 0000000..0343591\n--- /dev/null\n+++ b/t/t9505/test_cache_output.pl\n@@ -0,0 +1,70 @@\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+# load GitwebCache::SimpleFileCache (gitweb.perl uses require too)\n+# GitwebCache::Capture::SelectFH is loaded by GitwebCache::CacheOutput\n+require_ok('GitwebCache::SimpleFileCache');\n+\n+# Test setting up $cache and $capture\n+my $cache   = new_ok('GitwebCache::SimpleFileCache'   => [], 'The $cache  ');\n+my $capture = new_ok('GitwebCache::Capture::SelectFH' => [], 'The $capture');\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+sub action {\n+\tprint $action_output;\n+}\n+\n+# Catch output printed by cache_fetch\n+# (only for 'print <sth>' and 'printf <sth>')\n+sub capture_output_of_cache_output {\n+\tmy $test_data = '';\n+\n+\topen my $test_data_fh, '>', \\$test_data;\n+\tmy $oldfh = select($test_data_fh);\n+\n+\tcache_output($cache, $key, \\&action);\n+\n+\tselect($oldfh);\n+\tclose $test_data_fh;\n+\n+\treturn $test_data;\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+$test_data = capture_output_of_cache_output();\n+is($test_data, $action_output,        'action output is printed (generated)');\n+is($cache->get($key), $action_output, 'action output is saved in cache (generated)');\n+\n+# second time (if cache is set/valid) reads from cache\n+$cache->set($key, $cached_output);\n+$test_data = capture_output_of_cache_output();\n+is($test_data, $cached_output,        'action output is printed (from cache)');\n+\n+done_testing();\n-- \n1.7.3\n"},{"id":"152795","messageId":"1286402526-13143-9-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 08/17] gitweb: Add optional output caching","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:53Z","receivedAt":"2010-10-06T22:01:53Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This commit actually adds output caching to gitweb, as we have now\nminimal features required for it in GitwebCache::SimpleFileCache (a\n'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\ncaching modules alongside generated gitweb.cgi, e.g. by using 'make\ninstall-gitweb'.  This is described in more detail in the new \"Gitweb\ncaching\" section in gitweb/README.\n\nCaching in theory can be done using any Perl module that implements\nCache::Cache compatibile get/set (method) interface.  The default is\nto use GitwebCache::SimpleFileCache.  Capturing and caching output is\ndone via cache_output subroutine from GitwebCache::CacheOutput.\n\nThe cache_output subroutine in GitwebCache::CacheOutput currently\ncaptures output using GitwebCache::Capture::SelectFH package, which in\nturn uses select(FILEHANDLE) to change default filehandle for output.\nThis means that a \"print\" or a \"printf\" (or a \"write\") in gitweb\nsource without a filehandle would be captured.  To change mode of\nfilehandle used for capturing correctly, \"binmode STDOUT, <mode>\" had\nto be changed to \"binmode select(), <mode>\".\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.\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.\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\n\nTo be implemented (from original patch by J.H.):\n* adaptive cache expiration, based on average system load\n* optional locking interface, where only one process can update cache\n  (using flock)\n* server-side progress indicator when waiting for filling cache,\n  which in turn requires separating situations (like snapshots and\n  other non-HTML responses) where we should not show 'please wait'\n  message\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nDifferences from v4:\n* Paths to modules to install gathered in GITWEB_MODULES variable in\n  gitweb/Makefile do not include common 'lib/' prefix, following the\n  change in \"gitweb: Prepare for splitting gitweb\"\n\n* Separate preparing cache for output caching into configure_caching()\n  subroutine; cache_output() is now called from dispatch(), following\n  update to gitweb.\n\n* Load needed module for cache (if $cache is set to name of module, it\n  loads this module using 'eval \"require $cache\"'; note that if $cache\n  is unset and $cache_enabled is true, then $cache is set to\n  'GitwebCache::SimpleFileCache' before this).  \n\n  This is connected with the fact that GitwebCache::CacheOutput no\n  longer includes 'use GitwebCache::SimpleFileCache;'.\n\n* Do not use die_error when configuring cache, as the code might be\n  called before $cgi is set (though I think it is not the case with\n  current gitweb version).\n\n* Updated gitweb/README\n\n* Remove spurious changes\n\n\nDifferences from relevant parts of J.H. patch:\n* Capturing gitweb output is done without need to extensively modify\n  gitweb; the only change related to capturing output is replacing \n  'binmode STDOUT, <mode>' with 'binmode select(), <mode>', required\n  for currently used select($out) based capturing.\n\n* Instead of using \"binary\" (sic!) valued $cache_enable (which means 0 or 1\n  valued $cache_enable), a set of two variables is used.  The $cache\n  variable can be used to select alternate caching engine / caching class.\n  The $caching_enabled variable is used to actually enable/disable cache.\n\n* The information about the time when page was generated is shown only if\n  'timed' feature is enabled in gitweb config, and it is shown in place of\n  usual time it took to generate page (shown when caching is not enabled).\n  This means that change to gitweb/gitweb.css was not needed.\n\n* cache_output() is run only when $caching_enabled.  Some of cache\n  initializations, like creating cache instance, are in\n  configure_cache() subroutine in gitweb.perl, and not at beginning of\n  cache_output() (which was n.b. called cache_fetch() in J.H. patch).\n\n* Cache options are contained in %cache_options hash, instead of individual\n  global variables (which were using non-Perlish camelCase notation).\n\n* There is information about caching in gitweb in gitweb/README\n\n* Some of new features, like incremental blame view, are turned off\n  when caching is enabled.\n\n* There is simple test of output caching in gitweb in t9500, namely\n  that gitweb runs without errors or warnings with caching enabled,\n  both when saving and when restoring from cache.\n\n gitweb/Makefile                        |    6 ++\n gitweb/README                          |   57 +++++++++++++\n gitweb/gitweb.perl                     |  140 +++++++++++++++++++++++++++-----\n t/t9500-gitweb-standalone-no-errors.sh |   20 +++++\n 4 files changed, 201 insertions(+), 22 deletions(-)\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex c2d72e4..ec14cd1 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -112,6 +112,12 @@ 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.pm\n+GITWEB_MODULES += GitwebCache/Capture/SelectFH.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 d481198..5e3a0bc 100644\n--- a/gitweb/README\n+++ b/gitweb/README\n@@ -236,6 +236,11 @@ not include variables usually directly set during build):\n    If server load exceed this value then return \"503 Service Unavailable\" error.\n    Server load is taken to be 0 if gitweb cannot determine its value.  Set it to\n    undefined value to turn it off.  The default is 300.\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@@ -308,6 +313,58 @@ 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 $cache\n+variable to _initialized_ instance of cache interface, e.g.:\n+\n+   use CHI;\n+   our $cache = CHI->new( driver => 'Memcached',\n+   \tservers => [ \"10.0.0.15:11211\", \"10.0.0.15:11212\" ],\n+   \tl1_cache => { driver => 'FastMmap', root_dir => '/var/cache/gitweb' }\n+   );\n+\n+Alternatively you can set $cache variable to the name of cache class,\n+e.g.:\n+\n+   our $cache = 'Cache::FileCache';\n+\n+In this case caching engine should support Cache::Cache or CHI names for\n+cache config (see below), and ignore unrecognized options.  Such caching\n+engine should also implement (at least) ->get($key) and ->set($key, $data)\n+methods (Cache::Cache and CHI compatible interface).\n+\n+If $cache is left unset (if it is left undefined), then gitweb would use\n+GitwebCache::SimpleFileCache as caching engine.  This engine is 'dumb' (but\n+fast) file based caching layer, currently without any support for cache size\n+limiting, or even removing expired / grossly expired entries.  It has\n+therefore the downside of requiring a huge amount of disk space if there are\n+a number of repositories involved.  It is not uncommon for git.kernel.org to\n+have on the order of 80G - 120G accumulate over the course of a few months.\n+It is therefore recommended that the cache directory be periodically\n+completely deleted; this operation is safe to perform.  Suggested mechanism\n+(substitute $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::SimpleFileCache is used), or\n+if $cache is name of cache class (e.g. $cache = 'Cache::FileCache').  You\n+can override cache options in 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 e4c08ba..f81a4a2 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -263,6 +263,44 @@ 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 (at least)\n+# get($key) and set($key, $data) methods (Cache::Cache and CHI interfaces).\n+# If unset, GitwebCache::SimpleFileCache 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+# Suggested mechanism:\n+# mv $cachedir $cachedir.flush && mkdir $cachedir && rm -rf $cachedir.flush\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+\n+\n # You define site-wide feature defaults here; override them with\n # $GITWEB_CONFIG as necessary.\n our %feature = (\n@@ -1055,7 +1093,15 @@ 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, $output_key, $actions{$action});\n+\t} else {\n+\t\t$actions{$action}->();\n+\t}\n }\n \n sub reset_timer {\n@@ -1071,6 +1117,8 @@ sub run_request {\n \tevaluate_gitweb_config();\n \tevaluate_git_version();\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@@ -1143,6 +1191,38 @@ sub run {\n \t1;\n }\n \n+sub configure_caching {\n+\tif (!eval { require GitwebCache::CacheOutput; 1; }) {\n+\t\t# cache is configured _before_ handling request, so $cgi is not defined,\n+\t\t# so we can't just \"die\" with sending error message to web browser\n+\t\t#die_error(500, \"Caching enabled and GitwebCache::CacheOutput not found\");\n+\n+\t\t# turn off caching and warn instead\n+\t\t$caching_enabled = 0;\n+\t\twarn \"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::SimpleFileCache';\n+\t\teval \"require $cache\";\n+\t\tdie $@ if $@;\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});\n+\t}\n+}\n+\n run();\n \n if (defined caller) {\n@@ -3405,7 +3485,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@@ -3579,17 +3661,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      Time::HiRes::tv_interval($t0, [Time::HiRes::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      Time::HiRes::tv_interval($t0, [Time::HiRes::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@@ -3598,8 +3688,8 @@ sub git_footer_html {\n \t}\n \n \tprint qq!<script type=\"text/javascript\" src=\"$javascript\"></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@@ -3640,6 +3730,10 @@ sub die_error {\n \t\t500 => '500 Internal Server Error',\n \t\t503 => '503 Service Unavailable',\n \t);\n+\n+\t# Do not cache error pages\n+\tcapture_stop() if ($caching_enabled);\n+\n \tgit_header_html($http_responses{$status}, undef, %opts);\n \tprint <<EOF;\n <div class=\"page_body\">\n@@ -5235,7 +5329,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@@ -5289,7 +5384,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      Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).\n \t\t\t      ' '.$number_of_git_cmds;\n@@ -5309,7 +5405,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@@ -5468,7 +5564,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 {\n@@ -5548,9 +5644,9 @@ sub git_blob_plain {\n \t\t\t($sandbox ? 'attachment' : 'inline')\n \t\t\t. '; filename=\"' . $save_as . '\"');\n \tlocal $/ = undef;\n-\tbinmode STDOUT, ':raw';\n+\tbinmode select(), ':raw';\n \tprint <$fd>;\n-\tbinmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi\n+\tbinmode select(), ':utf8'; # as set at the beginning of gitweb.cgi\n \tclose $fd;\n }\n \n@@ -5835,9 +5931,9 @@ sub git_snapshot {\n \n \topen my $fd, \"-|\", $cmd\n \t\tor die_error(500, \"Execute git-archive failed\");\n-\tbinmode STDOUT, ':raw';\n+\tbinmode select(), ':raw';\n \tprint <$fd>;\n-\tbinmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi\n+\tbinmode select(), ':utf8'; # as set at the beginning of gitweb.cgi\n \tclose $fd;\n }\n \ndiff --git a/t/t9500-gitweb-standalone-no-errors.sh b/t/t9500-gitweb-standalone-no-errors.sh\nindex 4f2b9b0..c8b4286 100755\n--- a/t/t9500-gitweb-standalone-no-errors.sh\n+++ b/t/t9500-gitweb-standalone-no-errors.sh\n@@ -676,4 +676,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+cat >>gitweb_config.perl <<\\EOF\n+$caching_enabled = 1;\n+$cache_options{'expires_in'} = -1; # never expire cache for tests\n+$cache_options{'cache_root'} = 'cache'; # to clear right thing\n+EOF\n+rm -rf cache\n+\n+test_expect_success \\\n+\t'caching enabled (project summary, first run)' \\\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)' \\\n+\t'gitweb_run \"p=.git;a=summary\"'\n+test_debug 'cat gitweb.log'\n+\n test_done\n-- \n1.7.3\n"},{"id":"152793","messageId":"1286402526-13143-10-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 09/17] gitweb/lib - Adaptive cache expiration time","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:54Z","receivedAt":"2010-10-06T22:01:54Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add to GitwebCache::SimpleFileCache support for adaptive lifetime\n(cache expiration) control.  Cache lifetime can be increased or\ndecreased by any factor, e.g. load average, through the definition\nof the 'check_load' callback.\n\nNote that using ->set_expires_in, or unsetting 'check_load' via\n->set_check_load(undef) turns off adaptive caching.\n\nMake gitweb automatically adjust cache lifetime by load, using\nget_loadavg() function.  Define and describe default parameters for\ndynamic (adaptive) cache expiration time control.\n\nThere are some very basic tests of dynamic expiration time in t9503,\nnamely checking if dynamic expire time is within given upper and lower\nbounds.\n\nTo be implemented (from original patch by J.H.):\n* optional locking interface, where only one process can update cache\n  (using flock)\n* server-side progress indicator when waiting for filling cache,\n  which in turn requires separating situations (like snapshots and\n  other non-HTML responses) where we should not show 'please wait'\n  message\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThere are no significant difference from v4 (previous version)\n\nDifferences from relevant parts of J.H. patch:\n* 'increase_factor' is configurable rather than hardcoded\n\n* 'check_load' is passed via conctructor parameter; gitweb by default\n  sets it to \\&get_loadavg.  This means that the caching engine is not\n  entangled with gitweb.\n\n* options are stored in %cache_options, e.g. as 'expires_max' (compatible\n  with Cache::Adaptive), and not as global variables with un-Perlish\n  camelCase names like $maxCacheTime.\n\n* API tests\n\n\n gitweb/gitweb.perl                        |   25 ++++++++-\n gitweb/lib/GitwebCache/SimpleFileCache.pm |   79 +++++++++++++++++++++++++++--\n t/t9503/test_cache_interface.pl           |   33 ++++++++++++\n 3 files changed, 130 insertions(+), 7 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex f81a4a2..e89f469 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -296,8 +296,29 @@ our %cache_options = (\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+\t# The (global) minimum expiration time for objects placed in the cache,\n+\t# in seconds.  If the dynamic adaptive cache exporation time is lower\n+\t# than this number, we set cache timeout to this minimum.\n+\t'expires_min' => 20,   # 20 seconds\n+\n+\t# The (global) maximum expiration time for dynamic (adaptive) caching\n+\t# algorithm, in seconds.  If the adaptive cache lifetime exceeds this\n+\t# number, we set cache timeout to this maximum.\n+\t# (If 'expires_min' >= 'expires_max', there is no adaptive cache timeout,\n+\t# and 'expires_min' is used as expiration time for objects in cache.)\n+\t'expires_max' => 1200, # 20 minutes\n+\n+\t# Cache lifetime will be increased by applying this factor to the result\n+\t# from 'check_load' callback (see below).\n+\t'expires_factor' => 60, # expire time in seconds for 1.0 (100% CPU) load\n+\n+\t# User supplied callback for deciding the cache policy, usually system\n+\t# load.  Multiplied by 'expires_factor' gives adaptive expiration time,\n+\t# in seconds, subject to the limits imposed by 'expires_min' and\n+\t# 'expires_max' bounds.  Set to undef (or delete) to turn off dynamic\n+\t# lifetime control.\n+\t# (Compatibile with Cache::Adaptive.)\n+\t'check_load' => \\&get_loadavg,\n );\n \n \ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex 6833699..72ab77a 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -61,6 +61,22 @@ our $DEFAULT_NAMESPACE = '';\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+#    Sets 'expires_min' to given value.\n+#  * 'expires_min' [seconds]\n+#    The minimum expiration time for objects in cache (e.g. with 0% CPU load).\n+#    Used as lower bound in adaptive cache lifetime / expiration.\n+#    Defaults to 20 seconds; 'expires_in' sets it also.\n+#  * 'expires_max' [seconds]\n+#    The maximum expiration time for objects in cache.\n+#    Used as upper bound in adaptive cache lifetime / expiration.\n+#    Defaults to 1200 seconds, if not set; \n+#    defaults to 'expires_min' if 'expires_in' is used.\n+#  * 'check_load'\n+#    Subroutine (code) used for adaptive cache lifetime / expiration.\n+#    If unset, adaptive caching is turned off; defaults to unset.\n+#  * 'increase_factor' [seconds / 100% CPU load]\n+#    Factor multiplying 'check_load' result when calculating cache lietime.\n+#    Defaults to 60 seconds for 100% SPU load ('check_load' returning 1.0).\n sub new {\n \tmy ($proto, $p_options_hash_ref) = @_;\n \n@@ -68,7 +84,8 @@ sub new {\n \tmy $self  = {};\n \t$self = bless($self, $class);\n \n-\tmy ($root, $depth, $ns, $expires_in);\n+\tmy ($root, $depth, $ns);\n+\tmy ($expires_min, $expires_max, $increase_factor, $check_load);\n \tif (defined $p_options_hash_ref) {\n \t\t$root  =\n \t\t\t$p_options_hash_ref->{'cache_root'} ||\n@@ -77,19 +94,31 @@ sub new {\n \t\t\t$p_options_hash_ref->{'cache_depth'} ||\n \t\t\t$p_options_hash_ref->{'depth'};\n \t\t$ns    = $p_options_hash_ref->{'namespace'};\n-\t\t$expires_in =\n+\t\t$expires_min =\n+\t\t\t$p_options_hash_ref->{'expires_min'} ||\n \t\t\t$p_options_hash_ref->{'default_expires_in'} ||\n \t\t\t$p_options_hash_ref->{'expires_in'};\n+\t\t$expires_max =\n+\t\t\t$p_options_hash_ref->{'expires_max'};\n+\t\t$increase_factor = $p_options_hash_ref->{'expires_factor'};\n+\t\t$check_load      = $p_options_hash_ref->{'check_load'};\n \t}\n \t$root  = $DEFAULT_CACHE_ROOT  unless defined($root);\n \t$depth = $DEFAULT_CACHE_DEPTH unless defined($depth);\n \t$ns    = $DEFAULT_NAMESPACE   unless defined($ns);\n-\t$expires_in = -1 unless defined($expires_in); # <0 means never\n+\t$expires_min = -1 unless defined($expires_min);\n+\t$expires_max = $expires_min\n+\t\tif (!defined($expires_max) && exists $p_options_hash_ref->{'expires_in'});\n+\t$expires_max = -1 unless (defined($expires_max));\n+\t$increase_factor = 60 unless defined($increase_factor);\n \n \t$self->set_root($root);\n \t$self->set_depth($depth);\n \t$self->set_namespace($ns);\n-\t$self->set_expires_in($expires_in);\n+\t$self->set_expires_min($expires_min);\n+\t$self->set_expires_max($expires_max);\n+\t$self->set_increase_factor($increase_factor);\n+\t$self->set_check_load($check_load);\n \n \treturn $self;\n }\n@@ -100,7 +129,7 @@ sub new {\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)) {\n+foreach my $i (qw(depth root namespace expires_min expires_max increase_factor check_load)) {\n \tmy $field = $i;\n \tno strict 'refs';\n \t*{\"get_$field\"} = sub {\n@@ -113,6 +142,46 @@ foreach my $i (qw(depth root namespace expires_in)) {\n \t};\n }\n \n+# ......................................................................\n+# pseudo-accessors\n+\n+# returns adaptive lifetime of cache entry for given $key [seconds]\n+sub get_expires_in {\n+\tmy ($self) = @_;\n+\n+\t# short-circuit\n+\tif (!defined $self->{'check_load'} ||\n+\t    $self->{'expires_max'} <= $self->{'expires_min'}) {\n+\t\treturn $self->{'expires_min'};\n+\t}\n+\n+\tmy $expires_in =\n+\t\t#$self->{'expires_min'} +\n+\t\t$self->{'increase_factor'} * $self->check_load();\n+\n+\tif ($expires_in < $self->{'expires_min'}) {\n+\t\treturn $self->{'expires_min'};\n+\t} elsif ($expires_in > $self->{'expires_max'}) {\n+\t\treturn $self->{'expires_max'};\n+\t}\n+\n+\treturn $expires_in;\n+}\n+\n+# sets expiration time to $duration, turns off adaptive cache lifetime\n+sub set_expires_in {\n+\tmy ($self, $duration) = @_;\n+\n+\t$self->{'expires_min'} = $self->{'expires_max'} = $duration;\n+}\n+\n+# runs 'check_load' subroutine, for adaptive cache lifetime.\n+# Note: check in caller that 'check_load' exists.\n+sub check_load {\n+\tmy $self = shift;\n+\treturn $self->{'check_load'}->();\n+}\n+\n # ----------------------------------------------------------------------\n # utility functions and methods\n \ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex adca88d..0ac0ed7 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -97,4 +97,37 @@ subtest 'cache expiration' => sub {\n \tdone_testing();\n };\n \n+# Test assertions for adaptive cache expiration\n+#\n+my $load = 0.0;\n+sub load { return $load; }\n+my $expires_min = 10;\n+my $expires_max = 30;\n+$cache->set_expires_in(-1);\n+$cache->set_expires_min($expires_min);\n+$cache->set_expires_max($expires_max);\n+$cache->set_check_load(\\&load);\n+subtest 'adaptive cache expiration' => sub {\n+\tcmp_ok($cache->get_expires_min(), '==', $expires_min,\n+\t       '\"expires min\" set correctly');\n+\tcmp_ok($cache->get_expires_max(), '==', $expires_max,\n+\t       '\"expires max\" set correctly');\n+\n+\t$load = 0.0;\n+\tcmp_ok($cache->get_expires_in(), '>=', $expires_min,\n+\t       '\"expires in\" bound from down for load=0');\n+\tcmp_ok($cache->get_expires_in(), '<=', $expires_max,\n+\t       '\"expires in\" bound from up   for load=0');\n+\t\n+\t$load = 1_000;\n+\tcmp_ok($cache->get_expires_in(), '>=', $expires_min,\n+\t       '\"expires in\" bound from down for heavy load');\n+\tcmp_ok($cache->get_expires_in(), '<=', $expires_max,\n+\t       '\"expires in\" bound from up   for heavy load');\n+\n+\tdone_testing();\n+};\n+\n+$cache->set_expires_in(-1);\n+\n done_testing();\n-- \n1.7.3\n"},{"id":"152798","messageId":"1286402526-13143-11-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 10/21] gitweb/lib - Use CHI compatibile (compute method) caching interface","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:55Z","receivedAt":"2010-10-06T22:01:55Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"If $cache provides CHI compatible ->compute($key, $code) method, use it\ninstead of Cache::Cache compatible ->get($key) and ->set($key, $data).\n\nGitwebCache::SimpleFileCache provides 'compute' method, which currently\nsimply use 'get' and 'set' methods in proscribed manner.  Nevertheless\n'compute' method can be more flexible in choosing when to refresh cache,\nand which process is to refresh/(re)generate cache entry.  This method\nwould use (advisory) locking to prevent 'cache miss stampede' (aka\n'stampeding herd') problem in the next commit.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch has no differences since v4.\n\nThis patch doesn't strictly have an equivalent in J.H. patch adding\ncaching to gitweb.\n\n\n gitweb/lib/GitwebCache/CacheOutput.pm |   31 +++++++++++++++++++++++++++++++\n 1 files changed, 31 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/CacheOutput.pm b/gitweb/lib/GitwebCache/CacheOutput.pm\nindex bba73ee..7a13b2f 100644\n--- a/gitweb/lib/GitwebCache/CacheOutput.pm\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -37,6 +37,37 @@ our $CAPTURE_CLASS = 'GitwebCache::Capture::SelectFH';\n sub cache_output {\n \tmy ($cache, $key, $code) = @_;\n \n+\tif ($cache->can('compute')) {\n+\t\t#other solution: goto &cache_output_compute;\n+\t\treturn cache_output_compute($cache, $key, $code);\n+\t} else {\n+\t\t#other solution: goto &cache_output_get_set;\n+\t\treturn cache_output_get_set($cache, $key, $code);\n+\t}\n+}\n+\n+# for $cache which can ->compute($key, $code)\n+sub cache_output_compute {\n+\tmy ($cache, $key, $code) = @_;\n+\n+\tmy $capture = $CAPTURE_CLASS;\n+\tsetup_capture($capture);\n+\n+\tmy $data = $cache->compute($key, sub { &capture_block($code) });\n+\n+\tif (defined $data) {\n+\t\t# select() instead of STDOUT is here for t9503 test:\n+\t\tbinmode select(), ':raw';\n+\t\tprint $data;\n+\t}\n+\n+\treturn $data;\n+}\n+\n+# for $cache which can ->get($key) and ->set($key, $data)\n+sub cache_output_get_set {\n+\tmy ($cache, $key, $code) = @_;\n+\n \tmy $data = $cache->get($key);\n \n \t# capture and cache output, if there was nothing in the cache\n-- \n1.7.3\n"},{"id":"152799","messageId":"1286402526-13143-12-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 11/17] gitweb/lib - Use locking to avoid 'cache miss stampede' problem","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:56Z","receivedAt":"2010-10-06T22:01:56Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Create GitwebCache::FileCacheWithLocking module (class), derived from\nGitwebCache::SimpleFileCache, where the ->compute($key, $code) method\nuse locking (via flock) to ensure that only one process would generate\ndata to update/fill-in cache; the rest would wait for the cache to\nbe (re)generated and would read data from cache.  If process that was\nto (re)generate data dies or exits, one of the readers would take its\nrole.\n\nCurrently this feature can not be disabled via %cache_options,\nalthough you can set $cache to 'GitwebCache::SimpleFileCache' instead.\nFuture new features (like: serving stale data while cache is being\nregenerated, (re)generating cache in background, activity indicator)\nall depend on locking.\n\n\nA test in t9503 shows that in the case where there are two clients\ntrying to simultaneously access non-existent or stale cache entry,\n(and generating data takes (artifically) a bit of time), if they are\nusing ->compute method the data is (re)generated once, as opposed to\nif those clients are just using ->get/->set methods.\n\nTo be implemented (from original patch by J.H.):\n* background building, and showing stale cache\n* server-side progress indicator when waiting for filling cache,\n  which in turn requires separating situations (like snapshots and\n  other non-HTML responses) where we should not show 'please wait'\n  message\n\nNote that there is slight inefficiency, in that filename for lockfile\ndepends on the filename for cache entry (it just adds '.lock' suffix),\nbut is recalculated from $key for both paths.\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThe only difference from v4, besides not including 'lib/' prefix\nwhen adding GitwebCache/FileCacheWithLocking.pm to GITWEB_MODULES\nis that GitwebCache::FileCacheWithLocking module is automatically\nloaded (require'd) by configure_cache() subroutine in gitweb.\n\n\nDifferences from relevant parts of J.H. patch:\n* Locking on separate *.lock file, for simpler work and robustness;\n  acquiring exclusive lock might require having file opened for\n  possible write, following\n\n    \"File Locking Tricks and Traps\" (http://perl.plover.com/yak/flock/)\n\n  J.H. patch used separate *.bg file for lock only for background\n  caching (to be implemented in one of next commits).\n\n* Ensure that ->compute() delivers $data, unless $code fails.  See\n  above about adding loop in ->compute() method.\n\n* Single variable $lock_state, in place of $lockingStatus,\n  $lockStatBG, $lockStat, $lockStatus in J.H. patch.\n\n* Use indirect (lexical) filehandles for lockfiles, instead of\n  barewords, i.e. global filehandles:\n\n    open my $lock_fh, '+>', $lockfile;\n\n  rather than equivalent of\n\n    open LOCK, '>:utf8', $lockfile\n\n* Do not use LOCK_UN: closing lockfile would unlock.  LOCK_UN doesn't\n  always work as intended.\n\n* You can turn off locking, but only by changing $cache to be\n  'GitwebCache::SimpleFileCache' again.\n\n* In my opinion much cleaner flow control\n\n* Tests that locking work correctly.\n\n gitweb/Makefile                                |    1 +\n gitweb/README                                  |    6 +-\n gitweb/gitweb.perl                             |    4 +-\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   95 ++++++++++++++\n t/t9503/test_cache_interface.pl                |  156 +++++++++++++++++++++++-\n 5 files changed, 257 insertions(+), 5 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex ec14cd1..a5327ba 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -115,6 +115,7 @@ GITWEB_FILES += static/git-logo.png static/git-favicon.png\n # gitweb output caching\n GITWEB_MODULES += GitwebCache/CacheOutput.pm\n GITWEB_MODULES += GitwebCache/SimpleFileCache.pm\n+GITWEB_MODULES += GitwebCache/FileCacheWithLocking.pm\n GITWEB_MODULES += GitwebCache/Capture.pm\n GITWEB_MODULES += GitwebCache/Capture/SelectFH.pm\n \ndiff --git a/gitweb/README b/gitweb/README\nindex 5e3a0bc..8e664c5 100644\n--- a/gitweb/README\n+++ b/gitweb/README\n@@ -341,8 +341,12 @@ cache config (see below), and ignore unrecognized options.  Such caching\n engine should also implement (at least) ->get($key) and ->set($key, $data)\n methods (Cache::Cache and CHI compatible interface).\n \n+You can set $cache to 'GitwebCache::SimpleFileCache' if you don't want\n+to use locking, but then some advanced features, like generating data in\n+background, wouldn't work because they require locking.\n+\n If $cache is left unset (if it is left undefined), then gitweb would use\n-GitwebCache::SimpleFileCache as caching engine.  This engine is 'dumb' (but\n+GitwebCache::FileCacheWithLocking as caching engine.  This engine is 'dumb' (but\n fast) file based caching layer, currently without any support for cache size\n limiting, or even removing expired / grossly expired entries.  It has\n therefore the downside of requiring a huge amount of disk space if there are\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex e89f469..5da9651 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -269,7 +269,7 @@ our %highlight_ext = (\n our $caching_enabled = 0;\n # Set to _initialized_ instance of cache interface implementing (at least)\n # get($key) and set($key, $data) methods (Cache::Cache and CHI interfaces).\n-# If unset, GitwebCache::SimpleFileCache would be used, which is 'dumb'\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@@ -1227,7 +1227,7 @@ sub configure_caching {\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::SimpleFileCache';\n+\t\t$cache ||= 'GitwebCache::FileCacheWithLocking';\n \t\teval \"require $cache\";\n \t\tdie $@ if $@;\n \t\t$cache = $cache->new({\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nnew file mode 100644\nindex 0000000..c91c0ee\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -0,0 +1,95 @@\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, simple file-based cache, with locking\n+#\n+\n+# Based on GitwebCache::SimpleFileCache, minimalistic cache that\n+# stores data in the filesystem, without serialization.\n+#\n+# It uses file locks (flock) to have only one process generating data\n+# and writing to cache, when using CHI interface ->compute() method.\n+\n+package GitwebCache::FileCacheWithLocking;\n+use base qw(GitwebCache::SimpleFileCache);\n+\n+use strict;\n+use warnings;\n+\n+use File::Path qw(mkpath);\n+use Fcntl qw(:flock);\n+\n+# ......................................................................\n+# constructor is inherited from GitwebCache::SimpleFileCache\n+\n+# ----------------------------------------------------------------------\n+# utility functions and methods\n+\n+# Take an human readable key, and return path to be used for lockfile\n+# Puts dirname of file path in second argument, if it is provided.\n+sub get_lockname {\n+\tmy $self = shift;\n+\n+\treturn $self->path_to_key(@_) . '.lock';\n+}\n+\n+# ......................................................................\n+# interface methods\n+\n+# $data = $cache->compute($key, $code);\n+#\n+# Combines the get and set operations in a single call.  Attempts to\n+# get $key; if successful, returns the value.  Otherwise, calls $code\n+# and uses the return value as the new value for $key, which is then\n+# returned.\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 {\n+\tmy ($self, $key, $code) = @_;\n+\n+\tmy $data = $self->get($key);\n+\treturn $data if defined $data;\n+\n+\tmy $dir;\n+\tmy $lockfile = $self->get_lockname($key, \\$dir);\n+\n+\t# ensure that directory leading to lockfile exists\n+\tif (!-d $dir) {\n+\t\teval { mkpath($dir, 0, 0777); 1 }\n+\t\t\tor die \"Couldn't mkpath '$dir' for lockfile: $!\";\n+\t}\n+\n+\t# this loop is to protect against situation where process that\n+\t# acquired exclusive lock (writer) dies or exits (die_error)\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 die \"Could't open lockfile '$lockfile': $!\";\n+\t\t$lock_state = flock($lock_fh, LOCK_EX | LOCK_NB);\n+\t\tif ($lock_state) {\n+\t\t\t# acquired writers lock\n+\t\t\t$data = $code->($self, $key);\n+\t\t\t$self->set($key, $data);\n+\t\t} else {\n+\t\t\t# get readers lock\n+\t\t\tflock($lock_fh, LOCK_SH);\n+\t\t\t$data = $self->fetch($key);\n+\t\t}\n+\t\t# closing lockfile releases lock\n+\t\tclose $lock_fh\n+\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n+\t} until (defined $data || $lock_state);\n+\t# repeat until we have data, or we tried generating data oneself and failed\n+\treturn $data;\n+}\n+\n+1;\n+__END__\n+# end of package GitwebCache::FileCacheWithLocking\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 0ac0ed7..4a941d8 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -11,8 +11,9 @@ use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n \n # Test creating a cache\n #\n-BEGIN { use_ok('GitwebCache::SimpleFileCache'); }\n-my $cache = new_ok('GitwebCache::SimpleFileCache');\n+BEGIN { use_ok('GitwebCache::FileCacheWithLocking'); }\n+my $cache = new_ok('GitwebCache::FileCacheWithLocking');\n+isa_ok($cache, 'GitwebCache::SimpleFileCache');\n \n # Test that default values are defined\n #\n@@ -130,4 +131,155 @@ subtest 'adaptive cache expiration' => sub {\n \n $cache->set_expires_in(-1);\n \n+# Test 'stampeding herd' / 'cache miss stampede' problem\n+# (probably should be run only if GIT_TEST_LONG)\n+#\n+my $slow_time = 1; # how many seconds to sleep in mockup of slow generation\n+sub get_value_slow {\n+\t$call_count++;\n+\tsleep $slow_time;\n+\treturn $value;\n+}\n+\n+sub cache_get_set {\n+\tmy ($cache, $key) = @_;\n+\n+\tmy $data = $cache->get($key);\n+\tif (!defined $data) {\n+\t\t$data = get_value_slow();\n+\t\t$cache->set($key, $data);\n+\t}\n+\n+\treturn $data;\n+}\n+\n+sub cache_compute {\n+\tmy ($cache, $key) = @_;\n+\n+\tmy $data = $cache->compute($key, \\&get_value_slow);\n+\treturn $data;\n+}\n+\n+sub run_child {\n+\tmy ($writer, $data) = @_;\n+\n+\tprint $writer \"$call_count\\0\";\n+\tprint $writer \"$data\\0\";\n+\t$writer->flush();\n+\n+\texit 0;\n+}\n+\n+sub run_parent {\n+\tmy ($reader, $data, $child) = @_;\n+\n+\tlocal $/ = \"\\0\";\n+\n+\tmy @lines = <$reader>;\n+\tchomp @lines;\n+\n+\twaitpid $child, 0;\n+\tclose $reader;\n+\n+\tmy ($child_count, $child_data) = @lines;\n+\treturn ($child_count, $child_data);\n+}\n+\n+sub count_total {\n+\tmy ($kid_fh, $data, $pid) = @_;\n+\n+\tif ($pid) {\n+\t\tmy ($child_count, $child_data) =\n+\t\t\trun_parent($kid_fh, $data, $pid);\n+\t\t$call_count += $child_count;\n+\n+\t} else {\n+\t\trun_child(\\*STDOUT, $data);\n+\t}\n+}\n+\n+my ($pid, $kid_fh);\n+\n+$call_count = 0;\n+$cache->remove($key);\n+$pid = open $kid_fh, '-|';\n+SKIP: {\n+\tskip \"cannot fork: $!\", 1\n+\t\tunless defined $pid;\n+\n+\tmy $data = cache_get_set($cache, $key);\n+\tcount_total($kid_fh, $data, $pid);\n+\n+\tcmp_ok($call_count, '==', 2, 'parallel get/set: get_value_slow() called twice');\n+}\n+\n+$call_count = 0;\n+$cache->remove($key);\n+$pid = open $kid_fh, '-|';\n+SKIP: {\n+\tskip \"cannot fork: $!\", 1\n+\t\tunless defined $pid;\n+\n+\tmy $data = cache_compute($cache, $key);\n+\tcount_total($kid_fh, $data, $pid);\n+\n+\tcmp_ok($call_count, '==', 1, 'parallel compute: get_value_slow() called once');\n+}\n+\n+\n+# Test that it doesn't hang if get_action exits/dies\n+#\n+sub get_value_die {\n+\t$call_count++;\n+\tdie \"get_value_die\\n\";\n+}\n+\n+sub cache_compute_catch {\n+\tmy ($cache, $key) = @_;\n+\n+\teval { $cache->compute($key, \\&get_value_die); };\n+\tmy $eval_error = $@;\n+\treturn $eval_error;\n+}\n+\n+sub catch_errors {\n+\tmy ($kid_fh, $eval_error, $pid) = @_;\n+\n+\tmy $child_eval_error;\n+\tif ($pid) {\n+\t\t(undef, $child_eval_error) =\n+\t\t\trun_parent($kid_fh, $eval_error, $pid);\n+\n+\t} else {\n+\t\trun_child(\\*STDOUT, $eval_error);\n+\t}\n+\n+\treturn ($eval_error, $child_eval_error);\n+}\n+\n+$call_count = 0;\n+my $result = eval {\n+\t$pid = open $kid_fh, '-|';\n+ SKIP: {\n+\t\tskip \"cannot fork: $!\", 2\n+\t\t\tunless defined $pid;\n+\n+\t\tlocal $SIG{ALRM} = sub { die \"alarm\\n\"; };\n+\t\talarm 4*$slow_time;\n+\n+\t\tmy $eval_error = cache_compute_catch($cache, 'No Key');\n+\t\tmy $child_eval_error;\n+\n+\t\t($eval_error, $child_eval_error) =\n+\t\t\tcatch_errors($kid_fh, $eval_error, $pid);\n+\n+\t\tis($eval_error,       \"get_value_die\\n\", 'get_value_die() died in parent');\n+\t\tis($child_eval_error, \"get_value_die\\n\", 'get_value_die() died in child');\n+\n+\t\talarm 0;\n+\t}\n+};\n+ok(!$@, 'no alarm call (neither process hung)');\n+diag($@) if $@;\n+\n done_testing();\n-- \n1.7.3\n"},{"id":"152803","messageId":"1286402526-13143-13-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 12/17] gitweb/lib - No need for File::Temp when locking","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:57Z","receivedAt":"2010-10-06T22:01:57Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"When using locking to ensure that only one process is generating data\nand updating cache, there is no need to use File::Temp for temporary\nfile.  This should improve performance.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch was not present in previous v4 version of this series.  It\nbrings this solution closer to the one in J.H. patch, because with\nlocking it does not use File::Temp; the locking is safety enough by\nitself.\n\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   16 ++++++++++++++++\n 1 files changed, 16 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex c91c0ee..e69047e 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -38,6 +38,22 @@ sub get_lockname {\n \treturn $self->path_to_key(@_) . '.lock';\n }\n \n+# ----------------------------------------------------------------------\n+# \"private\" utility functions and methods\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 ($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 # ......................................................................\n # interface methods\n \n-- \n1.7.3\n"},{"id":"152797","messageId":"1286402526-13143-14-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 13/17] gitweb/lib - Serve stale data when waiting for filling cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:58Z","receivedAt":"2010-10-06T22:01:58Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"When process fails to acquire exclusive (writers) lock, then instead\nof waiting for the other process to (re)generate and fill cache, serve\nstale (expired) data from cache.  This is of course possible only if\nthere is some stale data in cache for given key.\n\nThis feature of GitwebCache::FileCacheWithLocking is used only for an\n->update($key, $code) method.  It is controlled by 'max_lifetime'\ncache parameter; you can set it to -1 to always serve stale data\nif it exists, and you can set it to 0 (or any value smaller than\n'expires_min') to turn this feature off.\n\nThis feature, as it is implemented currently, makes ->update() method a\nbit asymmetric with respect to process that acquired writers lock and\nthose processes that didn't, which can be seen in the new test in t9503.\nThe process that is to regenerate (refresh) data in cache must wait for\nthe data to be generated in full before showing anything to client, while\nthe other processes show stale (expired) data immediately.  In order to\nremove or reduce this asymmetry gitweb would need to employ one of the two\nalternate solutions.  Either data should be (re)generated in background,\nso that process that acquired writers lock would generate data in\nbackground while serving stale data, or alternatively the process that\ngenerates data should pass output to original STDOUT while capturing it\n(\"tee\" output).\n\nWhen developing this feature, ->is_valid() method in base class\nGitwebCache::SimpleFileCache acquired additional extra optional\nparameter, where one can pass expire time instead of using whole-cache\nglobal (adaptive) expire time.\n\nTo be implemented (from original patch by J.H.):\n* background building,\n* server-side progress indicator when waiting for filling cache,\n  which in turn requires separating situations (like snapshots and\n  other non-HTML responses) where we should not show 'please wait'\n  message\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nNo changes since v4 of gitweb caching series.\n\n\nDifferences from relevant parts of J.H. patch:\n* Instead of using $maxCacheLife, use 'max_lifetime' option (with\n  'max_cache_lifetime' as optional spelling in GitwebCache::SimpleFileCache\n  constructor).\n\n* Use 5*60*60 seconds for 5 hours, rather than 18000 (or 18_000) seconds.\n\n* Serving stale data was enabled only when background cache regeneration was\n  enabled; here it is enabled for readers regardless whether background\n  generation (introduced in next commit) is turned on or not. \n\n* It is explicit that serving stale data when some process is\n  regenerating cache is possible only with locking enabled, i.e. when\n  using GitwebCache::FileCacheWithLocking.\n\n* Tests of API\n\n gitweb/gitweb.perl                             |    8 ++\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   94 +++++++++++++++++++++++-\n gitweb/lib/GitwebCache/SimpleFileCache.pm      |    9 +-\n t/t9503/test_cache_interface.pl                |   58 ++++++++++++++-\n 4 files changed, 160 insertions(+), 9 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 5da9651..c920656 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -319,6 +319,14 @@ our %cache_options = (\n \t# lifetime control.\n \t# (Compatibile with Cache::Adaptive.)\n \t'check_load' => \\&get_loadavg,\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# Set it to -1 to always serve existing data if it exists,\n+\t# set it to 0 to turn off serving stale data - always wait.\n+\t'max_lifetime' => 5*60*60, # 5 hours\n );\n \n \ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex e69047e..73cc9ec 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -25,7 +25,88 @@ use File::Path qw(mkpath);\n use Fcntl qw(:flock);\n \n # ......................................................................\n-# constructor is inherited from GitwebCache::SimpleFileCache\n+# constructor\n+\n+# The options are set by passing in 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.\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+#    Sets 'expires_min' to given value.\n+#  * 'expires_min' [seconds]\n+#    The minimum expiration time for objects in cache (e.g. with 0% CPU load).\n+#    Used as lower bound in adaptive cache lifetime / expiration.\n+#    Defaults to 20 seconds; 'expires_in' sets it also.\n+#  * 'expires_max' [seconds]\n+#    The maximum expiration time for objects in cache.\n+#    Used as upper bound in adaptive cache lifetime / expiration.\n+#    Defaults to 1200 seconds, if not set; \n+#    defaults to 'expires_min' if 'expires_in' is used.\n+#  * 'check_load'\n+#    Subroutine (code) used for adaptive cache lifetime / expiration.\n+#    If unset, adaptive caching is turned off; defaults to unset.\n+#  * 'increase_factor' [seconds / 100% CPU load]\n+#    Factor multiplying 'check_load' result when calculating cache lietime.\n+#    Defaults to 60 seconds for 100% SPU load ('check_load' returning 1.0).\n+#\n+# (all the above are inherited from GitwebCache::SimpleFileCache)\n+#\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+#    Defaults to -1 (never expire / always serve stale).\n+sub new {\n+\tmy ($proto, $p_options_hash_ref) = @_;\n+\n+\tmy $class = ref($proto) || $proto;\n+\tmy $self = $class->SUPER::new($p_options_hash_ref);\n+\n+\tmy ($max_lifetime);\n+\tif (defined $p_options_hash_ref) {\n+\t\t$max_lifetime =\n+\t\t\t$p_options_hash_ref->{'max_lifetime'} ||\n+\t\t\t$p_options_hash_ref->{'max_cache_lifetime'};\n+\t}\n+\t$max_lifetime = -1 unless defined($max_lifetime);\n+\n+\t$self->set_max_lifetime($max_lifetime);\n+\n+\treturn $self;\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(max_lifetime)) {\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 # utility functions and methods\n@@ -94,9 +175,14 @@ sub compute {\n \t\t\t$data = $code->($self, $key);\n \t\t\t$self->set($key, $data);\n \t\t} else {\n-\t\t\t# get readers lock\n-\t\t\tflock($lock_fh, LOCK_SH);\n-\t\t\t$data = $self->fetch($key);\n+\t\t\t# try to retrieve stale data\n+\t\t\t$data = $self->fetch($key)\n+\t\t\t\tif $self->is_valid($key, $self->get_max_lifetime());\n+\t\t\tif (!defined $data) {\n+\t\t\t\t# get readers lock if there is no stale data to serve\n+\t\t\t\tflock($lock_fh, LOCK_SH);\n+\t\t\t\t$data = $self->fetch($key);\n+\t\t\t}\n \t\t}\n \t\t# closing lockfile releases lock\n \t\tclose $lock_fh\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex 72ab77a..39f24f8 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -350,12 +350,13 @@ sub remove {\n \t\tor die \"Couldn't remove file '$file': $!\";\n }\n \n-# $cache->is_valid($key)\n+# $cache->is_valid($key[, $expires_in])\n #\n # Returns a boolean indicating whether $key exists in the cache\n-# and has not expired (global per-cache 'expires_in').\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) = @_;\n+\tmy ($self, $key, $expires_in) = @_;\n \n \tmy $path = $self->path_to_key($key);\n \n@@ -368,7 +369,7 @@ sub is_valid {\n \treturn 0 unless ((stat(_))[7] > 0);\n \n \t# expire time can be set to never\n-\tmy $expires_in = $self->get_expires_in();\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?\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 4a941d8..303bcf0 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -12,7 +12,9 @@ use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n # Test creating a cache\n #\n BEGIN { use_ok('GitwebCache::FileCacheWithLocking'); }\n-my $cache = new_ok('GitwebCache::FileCacheWithLocking');\n+my $cache = new_ok('GitwebCache::FileCacheWithLocking', [ {\n+\t'max_lifetime' => 0, # turn it off\n+} ]);\n isa_ok($cache, 'GitwebCache::SimpleFileCache');\n \n # Test that default values are defined\n@@ -282,4 +284,58 @@ my $result = eval {\n ok(!$@, 'no alarm call (neither process hung)');\n diag($@) if $@;\n \n+# Test that cache returns stale data in existing but expired cache situation\n+# (probably should be run only if GIT_TEST_LONG)\n+#\n+my $stale_value = 'Stale Value';\n+$cache->set($key, $stale_value);\n+$call_count = 0;\n+sub run_compute_forked {\n+\tmy ($pid, $kid_fh) = @_;\n+\n+\tmy $data = $cache->compute($key, \\&get_value_slow);\n+\n+\tmy $child_data;\n+\tif ($pid) {\n+\t\t(undef, $child_data) =\n+\t\t\trun_parent($kid_fh, $data, $pid);\n+\n+\t} else {\n+\t\trun_child(\\*STDOUT, $data);\n+\t}\n+\n+\treturn ($data, $child_data);\n+}\n+$cache->set_expires_in(0);    # expire now\n+$cache->set_max_lifetime(-1); # forever (always serve stale data)\n+$pid = open $kid_fh, '-|';\n+SKIP: {\n+\tskip \"cannot fork: $!\", 2\n+\t\tunless defined $pid;\n+\n+\tmy ($data, $child_data) = run_compute_forked($pid, $kid_fh);\n+\n+\t# returning stale data works\n+\tok($data eq $stale_value || $child_data eq $stale_value,\n+\t   'stale data in at least one process when expired');\n+\n+\t$cache->set_expires_in(-1); # never expire for next ->get\n+\tis($cache->get($key), $value, 'value got set correctly, even if stale data returned');\n+}\n+\n+$cache->set_expires_in(0);   # expire now\n+$cache->set_max_lifetime(0); # don't serve stale data\n+$pid = open $kid_fh, '-|';\n+SKIP: {\n+\tskip \"cannot fork: $!\", 2\n+\t\tunless defined $pid;\n+\n+\tmy ($data, $child_data) = run_compute_forked($pid, $kid_fh);\n+\n+\t# no returning stale data\n+\tisnt($data,       $stale_value, 'no stale data in parent if configured');\n+\tisnt($child_data, $stale_value, 'no stale data in child  if configured');\n+}\n+$cache->set_expires_in(-1);\n+\n done_testing();\n-- \n1.7.3\n"},{"id":"152800","messageId":"1286402526-13143-15-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 14/17] gitweb/lib - Regenerate (refresh) cache in background","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:01:59Z","receivedAt":"2010-10-06T22:01:59Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This commit removes asymmetry in serving stale data (if it 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 in the future from FastCGI): it would leave unreaped\nchildren as zombies (entries in process table).  We don't want to wait for\nbackground process, and we can't set $SIG{CHLD} to 'IGNORE' in gitweb to\nautomatically reap child 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\nTo be implemented (from original patch by J.H.):\n* server-side progress indicator when waiting for filling cache,\n  which in turn requires separating situations (like snapshots and\n  other non-HTML responses) where we should not show 'please wait'\n  message\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nNo differences from v4 (from previous version).\n\nDifferences from relevant parts of J.H. patch:\n* It is made explicit that background generation depends on using\n  locking.\n\n* Use boolean cache option 'background_cache', instead of CamelCase\n  global variable $backgroundCache.\n\n* Fork (run generating process in background) only if there is stale\n  data to serve (and if background cache is turned on).  In J.H. patch\n  forking was done unconditionally, only generation or exit depended\n  on $backgroundCache.\n\n* Daemonizes background process, detaching it from parent (using\n  setsid).  This way whether main process is short-lived (gitweb as\n  CGI) or long-lived (mod_perl or FastCGI), there would be no need to\n  wait and no zombies.\n\n* Lock before forking.\n\n* Tests (currently only of API).\n\n* In my opinion cleaner control flow.\n\n gitweb/gitweb.perl                             |    9 ++++\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   61 +++++++++++++++++++----\n gitweb/lib/GitwebCache/SimpleFileCache.pm      |    3 +-\n t/t9503/test_cache_interface.pl                |   12 ++++-\n 4 files changed, 70 insertions(+), 15 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex c920656..d9ac063 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -327,6 +327,15 @@ our %cache_options = (\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 - 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 \n \ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex 73cc9ec..f0d9d6f 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -23,6 +23,7 @@ use warnings;\n \n use File::Path qw(mkpath);\n use Fcntl qw(:flock);\n+use POSIX qw(setsid);\n \n # ......................................................................\n # constructor\n@@ -70,21 +71,27 @@ use Fcntl qw(:flock);\n #    than it, serve stale data when waiting for cache entry to be \n #    regenerated (refreshed).  Non-adaptive.\n #    Defaults to -1 (never expire / always serve stale).\n+#  * 'background_cache' (boolean)\n+#    This enables/disables regenerating cache in background process.\n+#    Defaults to true.\n sub new {\n \tmy ($proto, $p_options_hash_ref) = @_;\n \n \tmy $class = ref($proto) || $proto;\n \tmy $self = $class->SUPER::new($p_options_hash_ref);\n \n-\tmy ($max_lifetime);\n+\tmy ($max_lifetime, $background_cache);\n \tif (defined $p_options_hash_ref) {\n \t\t$max_lifetime =\n \t\t\t$p_options_hash_ref->{'max_lifetime'} ||\n \t\t\t$p_options_hash_ref->{'max_cache_lifetime'};\n+\t\t$background_cache = $p_options_hash_ref->{'background_cache'};\n \t}\n \t$max_lifetime = -1 unless defined($max_lifetime);\n+\t$background_cache = 1 unless defined($background_cache);\n \n \t$self->set_max_lifetime($max_lifetime);\n+\t$self->set_background_cache($background_cache);\n \n \treturn $self;\n }\n@@ -95,7 +102,7 @@ sub new {\n # http://perldesignpatterns.com/perldesignpatterns.html#AccessorPattern\n \n # creates get_depth() and set_depth($depth) etc. methods\n-foreach my $i (qw(max_lifetime)) {\n+foreach my $i (qw(max_lifetime background_cache)) {\n \tmy $field = $i;\n \tno strict 'refs';\n \t*{\"get_$field\"} = sub {\n@@ -148,9 +155,10 @@ sub _tempfile_to_path {\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 {\n-\tmy ($self, $key, $code) = @_;\n+\tmy ($self, $key, $coderef) = @_;\n+\tmy ($data, $stale_data);\n \n-\tmy $data = $self->get($key);\n+\t$data = $self->get($key);\n \treturn $data if defined $data;\n \n \tmy $dir;\n@@ -169,16 +177,47 @@ sub compute {\n \tdo {\n \t\topen my $lock_fh, '+>', $lockfile\n \t\t\tor die \"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\n-\t\t\t$data = $code->($self, $key);\n-\t\t\t$self->set($key, $data);\n+\t\t\t## acquired writers lock, have to generate data\n+\t\t\tmy $pid;\n+\t\t\tif ($self->{'background_cache'}) {\n+\t\t\t\t# try to retrieve stale data\n+\t\t\t\t$stale_data = $self->fetch($key)\n+\t\t\t\t\tif $self->is_valid($key, $self->get_max_lifetime());\n+\n+\t\t\t\t# fork if there is stale data, for background process\n+\t\t\t\t# to regenerate/refresh the cache (generate data)\n+\t\t\t\t$pid = fork() if (defined $stale_data);\n+\t\t\t}\n+\t\t\tif (!defined $pid || !$pid) {\n+\t\t\t\t## didn't fork, or are in background process\n+\n+\t\t\t\t# daemonize background process, detaching it from parent\n+\t\t\t\t# see also Proc::Daemonize, Apache2::SubProcess\n+\t\t\t\tif (defined $pid) {\n+\t\t\t\t\tPOSIX::setsid();  # or setpgrp(0, 0);\n+\t\t\t\t\tfork() && exit 0;\n+\t\t\t\t}\n+\n+\t\t\t\t$data = $coderef->($self, $key);\n+\t\t\t\t$self->set($key, $data);\n+\n+\t\t\t\tif (defined $pid) {\n+\t\t\t\t\t## in background process; parent will serve stale data\n+\t\t\t\t\tclose $lock_fh\n+\t\t\t\t\t\tor die \"Couldn't close lockfile '$lockfile' (background): $!\";\n+\t\t\t\t\texit 0;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\t\n \t\t} else {\n \t\t\t# try to retrieve stale data\n-\t\t\t$data = $self->fetch($key)\n+\t\t\t$stale_data = $self->fetch($key)\n \t\t\t\tif $self->is_valid($key, $self->get_max_lifetime());\n-\t\t\tif (!defined $data) {\n+\n+\t\t\tif (!defined $stale_data) {\n \t\t\t\t# get readers lock if there is no stale data to serve\n \t\t\t\tflock($lock_fh, LOCK_SH);\n \t\t\t\t$data = $self->fetch($key);\n@@ -187,9 +226,9 @@ sub compute {\n \t\t# closing lockfile releases lock\n \t\tclose $lock_fh\n \t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n-\t} until (defined $data || $lock_state);\n+\t} until (defined $data || defined $stale_data || $lock_state);\n \t# repeat until we have data, or we tried generating data oneself and failed\n-\treturn $data;\n+\treturn defined $data ? $data : $stale_data;\n }\n \n 1;\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex 39f24f8..2a3d9cf 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -129,7 +129,8 @@ sub new {\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_min expires_max increase_factor check_load)) {\n+foreach my $i (qw(depth root namespace\n+                  expires_min expires_max increase_factor check_load)) {\n \tmy $field = $i;\n \tno strict 'refs';\n \t*{\"get_$field\"} = sub {\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 303bcf0..7ee628f 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -17,6 +17,9 @@ my $cache = new_ok('GitwebCache::FileCacheWithLocking', [ {\n } ]);\n isa_ok($cache, 'GitwebCache::SimpleFileCache');\n \n+# compute can fork, don't generate zombies\n+#local $SIG{CHLD} = 'IGNORE';\n+\n # Test that default values are defined\n #\n ok(defined $GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT,\n@@ -308,18 +311,21 @@ sub run_compute_forked {\n }\n $cache->set_expires_in(0);    # expire now\n $cache->set_max_lifetime(-1); # forever (always serve stale data)\n+ok($cache->get_background_cache(), 'generate cache in background by default');\n $pid = open $kid_fh, '-|';\n SKIP: {\n-\tskip \"cannot fork: $!\", 2\n+\tskip \"cannot fork: $!\", 3\n \t\tunless defined $pid;\n \n \tmy ($data, $child_data) = run_compute_forked($pid, $kid_fh);\n \n \t# returning stale data works\n-\tok($data eq $stale_value || $child_data eq $stale_value,\n-\t   'stale data in at least one process when expired');\n+\tis($data,       $stale_value, 'stale data in parent when expired');\n+\tis($child_data, $stale_value, 'stale data in child  when expired');\n \n \t$cache->set_expires_in(-1); # never expire for next ->get\n+\tdiag('waiting 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($key), $value, 'value got set correctly, even if stale data returned');\n }\n \n-- \n1.7.3\n"},{"id":"152790","messageId":"1286402526-13143-16-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5 15/17] gitweb: Introduce %actions_info, gathering information about actions","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:02:00Z","receivedAt":"2010-10-06T22:02:00Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Currently 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 site.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis is new patch, which didn't exist in v4 (previous version) of\ngitweb caching series, and doesn't have equivalent in J.H. patch.\n\nWhile it is patch preparing infrastructure to be used by progress\nindicator (generating info), it is IMHO a good idea in itself, and\nthat is why it is not marked as RFC.\n\n gitweb/gitweb.perl |   20 ++++++++++++++++++++\n 1 files changed, 20 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex d9ac063..26d5619 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -807,6 +807,25 @@ our %allowed_options = (\n \t\"--no-merges\" => [ qw(rss atom log shortlog history) ],\n );\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+\tforeach my $action (keys %actions) {\n+\t\t$actions_info{$action}{'output_format'} = 'html';\n+\t}\n+\t# list all exceptions; undef means variable (no definite format)\n+\tmap { $actions_info{$_}{'output_format'} = 'text' }\n+\t\tqw(commitdiff_plain patch patches project_index blame_data);\n+\tmap { $actions_info{$_}{'output_format'} = 'xml' }\n+\t\tqw(rss atom opml); # there are different types (document formats) of XML\n+\tmap { $actions_info{$_}{'output_format'} = undef }\n+\t\tqw(blob_plain object);\n+\t$actions_info{'snapshot'}{'output_format'} = 'binary';\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@@ -1153,6 +1172,7 @@ sub run_request {\n \n \tevaluate_uri();\n \tevaluate_gitweb_config();\n+\tevaluate_actions_info();\n \tevaluate_git_version();\n \tcheck_loadavg();\n \tconfigure_caching()\n-- \n1.7.3\n"},{"id":"152791","messageId":"1286402526-13143-17-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5/RFC 16/17] gitweb: Show appropriate \"Generating...\" page when regenerating cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:02:01Z","receivedAt":"2010-10-06T22:02:01Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"When there exist stale/expired (but not too stale) version of\n(re)generated page in cache, gitweb returns stale version (and updates\ncache in background, assuming 'background_cache' is set to true value).\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\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,\nchecked by getting shared/readers lock on lockfile for cache entry).\nThe git_generating_data_html() subroutine, which is used by gitweb\nto implement 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 comments\nin %generating_options hash definition).\n\ngit_generating_data_html() is run only for actions that have HTML\noutput; only for those actions HTML \"Generating...\" page makes sense.\n\n\nThis implements final feature from the original gitweb output caching\npatch by J.H.\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nDifferences from v4:\n* New action_outputs_html() subroutine in gitweb, used for whitelisting in\n  git_generating_data_html().\n\n* No capture_stop(), as it is not needed: the 'generating_info' subroutine\n  (e.g. git_generating_data_html()) is run outside capturing output.\n\n* git_generating_data_html() now ends with 'goto DONE_GITWEB'\n\n\nDifferences from relevant parts of J.H. patch:\n* When providing \"Generating...\" page for process which acquired writers\n  lock, and spawned background process to (re)generate data in cache, we\n  have to drop writers lock for 'generating_info' subroutine to try to get\n  readers lock to know when data is finished generating.  This is done by\n  closing lockfile, and then reopening; I have found that LOCK_UN doesn't\n  work reliably enough.\n\n* The subroutine that is responsible for doing \"Generating...\" progress\n  info / activity indicator (cacheWaitForUpdate() subroutine in\n  J.H. patch, git_generating_data_html() in this patch) resides in\n  gitweb.perl, and not in caching module.  It is passed as callback (as\n  code reference) to $cache constructor, as 'generating_info' parameter.\n\n* The idea how git_generating_data_html() works for web browsers without\n  Ajax, using only plain HTML and server-side scripting is somewhat\n  described in comments.\n\n* gitweb prints generating info in more restricted set of situations; the\n  set of actions where gitweb does not generate activity indicator is\n  larger.  We could probably provide activity indicator also for\n  (possibly) non-HTML output, like 'blob_plain' or 'patches', provided\n  that 'User-Agent' denotes that we are using web browser.\n\n* \"Generating...\" info behavior can be configured (at least the timings)\n  via %generating_options hash, instead of having those options hardcoded.\n\n* Waiting is done using blocking flock + alarm, rather than \"busy wait\"\n  loop with non-blocking flock + sleep.\n\n* The page title shows that it is progress info, and for what action\n  (for git_generating_data_html()).\n\n* Ability to use sub-second precision if Time::HiRes is available\n  (for git_generating_data_html()).\n\n* Basic test for \"Generating...\" feature, though not for the\n  git_generating_data_html().\n\n* Fixed typo in DTD URL.\n\n\n gitweb/gitweb.perl                             |  120 +++++++++++++++++++++++-\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   52 ++++++++++-\n t/t9503/test_cache_interface.pl                |   61 ++++++++++++\n 3 files changed, 227 insertions(+), 6 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 26d5619..07fa825 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -21,7 +21,7 @@ use CGI qw(:standard :escapeHTML -nosticky);\n use CGI::Util qw(unescape);\n use CGI::Carp qw(fatalsToBrowser set_message);\n use Encode;\n-use Fcntl ':mode';\n+use Fcntl qw(:mode :flock);\n use File::Find qw();\n use File::Basename qw(basename);\n binmode STDOUT, ':utf8';\n@@ -336,8 +336,32 @@ our %cache_options = (\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 \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::SimpleFileCache)\n+\t# is passed the following parameters: $cache instance, human-readable\n+\t# $key to current page, and filehandle $lock_fh to lockfile.\n+\t'generating_info' => \\&git_generating_data_html,\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 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 \n # You define site-wide feature defaults here; override them with\n # $GITWEB_CONFIG as necessary.\n@@ -826,6 +850,11 @@ sub evaluate_actions_info {\n \t$actions_info{'snapshot'}{'output_format'} = 'binary';\n }\n \n+sub action_outputs_html {\n+\tmy $action = shift;\n+\treturn $actions_info{$action}{'output_format'} eq 'html';\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@@ -3532,6 +3561,93 @@ 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, $lock_fh) = @_;\n+\n+\t# whitelist of actions that should get \"Generating...\" page\n+\tunless (action_outputs_html($action)) {\n+\t\treturn;\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+\tprint STDOUT $cgi->header(-type => 'text/html', -charset => 'utf-8',\n+\t                          -status=> '200 OK', -expires => 'now');\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\t# check if we can use functions from Time::HiRes\n+\t\tif (defined $t0) {\n+\t\t\tlocal $SIG{ALRM} = $alarm_handler;\n+\t\t\tTime::HiRes::alarm($interval, $interval);\n+\t\t} else {\n+\t\t\tlocal $SIG{ALRM} = sub {\n+\t\t\t\t$alarm_handler->();\n+\t\t\t\talarm($interval);\n+\t\t\t};\n+\t\t\talarm($interval);\n+\t\t}\n+\t\tmy $lock_acquired;\n+\t\tdo {\n+\t\t\t# loop is needed here because SIGALRM (from 'alarm')\n+\t\t\t# can interrupt process of acquiring lock\n+\t\t\t$lock_acquired = flock($lock_fh, LOCK_SH); # blocking readers lock\n+\t\t} until ($lock_acquired);\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+\tgoto DONE_GITWEB;\n+\t#exit 0;\n+\t#return;\n+}\n+\n sub git_header_html {\n \tmy $status = shift || \"200 OK\";\n \tmy $expires = shift;\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex f0d9d6f..9211a9a 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -74,24 +74,32 @@ use POSIX qw(setsid);\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 opened $lock_fh filehandle to lockfile.\n+#    Unset by default (which means no activity indicator).\n sub new {\n \tmy ($proto, $p_options_hash_ref) = @_;\n \n \tmy $class = ref($proto) || $proto;\n \tmy $self = $class->SUPER::new($p_options_hash_ref);\n \n-\tmy ($max_lifetime, $background_cache);\n+\tmy ($max_lifetime, $background_cache, $generating_info);\n \tif (defined $p_options_hash_ref) {\n \t\t$max_lifetime =\n \t\t\t$p_options_hash_ref->{'max_lifetime'} ||\n \t\t\t$p_options_hash_ref->{'max_cache_lifetime'};\n \t\t$background_cache = $p_options_hash_ref->{'background_cache'};\n+\t\t$generating_info  = $p_options_hash_ref->{'generating_info'};\n \t}\n \t$max_lifetime = -1 unless defined($max_lifetime);\n \t$background_cache = 1 unless defined($background_cache);\n \n \t$self->set_max_lifetime($max_lifetime);\n \t$self->set_background_cache($background_cache);\n+\t$self->set_generating_info($generating_info);\n \n \treturn $self;\n }\n@@ -102,7 +110,7 @@ sub new {\n # http://perldesignpatterns.com/perldesignpatterns.html#AccessorPattern\n \n # creates get_depth() and set_depth($depth) etc. methods\n-foreach my $i (qw(max_lifetime background_cache)) {\n+foreach my $i (qw(max_lifetime background_cache generating_info)) {\n \tmy $field = $i;\n \tno strict 'refs';\n \t*{\"get_$field\"} = sub {\n@@ -115,6 +123,17 @@ foreach my $i (qw(max_lifetime background_cache)) {\n \t};\n }\n \n+# $cache->generating_info($key, $lock);\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 \n@@ -189,7 +208,8 @@ sub compute {\n \n \t\t\t\t# fork if there is stale data, for background process\n \t\t\t\t# to regenerate/refresh the cache (generate data)\n-\t\t\t\t$pid = fork() if (defined $stale_data);\n+\t\t\t\t$pid = fork()\n+\t\t\t\t\tif (defined $stale_data || $self->{'generating_info'});\n \t\t\t}\n \t\t\tif (!defined $pid || !$pid) {\n \t\t\t\t## didn't fork, or are in background process\n@@ -206,18 +226,42 @@ sub compute {\n \n \t\t\t\tif (defined $pid) {\n \t\t\t\t\t## in background process; parent will serve stale data\n+\t\t\t\t\t## or show activity indicator, and serve data\n \t\t\t\t\tclose $lock_fh\n \t\t\t\t\t\tor die \"Couldn't close lockfile '$lockfile' (background): $!\";\n \t\t\t\t\texit 0;\n \t\t\t\t}\n+\n+\t\t\t} else {\n+\t\t\t\t## forked, in parent process\n+\n+\t\t\t\t# provide \"generating page...\" info if there is no stale data to serve\n+\t\t\t\t# might exit, or force web browser to do redirection (refresh)\n+\t\t\t\tif (!defined $stale_data) {\n+\t\t\t\t\t# lock can get inherited across forks; unlock\n+\t\t\t\t\t# flock($lock_fh, LOCK_UN); # <-- this doesn't work\n+\t\t\t\t\tclose $lock_fh\n+\t\t\t\t\t\tor die \"Couldn't close lockfile '$lockfile' for reopen: $!\";\n+\t\t\t\t\topen $lock_fh, '<', $lockfile\n+\t\t\t\t\t\tor die \"Couldn't reopen (for reading) lockfile '$lockfile': $!\";\n+\n+\t\t\t\t\t$self->generating_info($key, $lock_fh);\n+\t\t\t\t\t# generating info may exit, so we can not get there\n+\t\t\t\t\t# wait for and get data from background process\n+\t\t\t\t\tflock($lock_fh, LOCK_SH);\n+\t\t\t\t\t$data = $self->fetch($key);\n+\t\t\t\t}\n \t\t\t}\n-\t\t\t\n+\n \t\t} else {\n \t\t\t# try to retrieve stale data\n \t\t\t$stale_data = $self->fetch($key)\n \t\t\t\tif $self->is_valid($key, $self->get_max_lifetime());\n \n \t\t\tif (!defined $stale_data) {\n+\t\t\t\t# there is no stale data to serve\n+\t\t\t\t# provide \"generating page...\" info\n+\t\t\t\t$self->generating_info($key, $lock_fh);\n \t\t\t\t# get readers lock if there is no stale data to serve\n \t\t\t\tflock($lock_fh, LOCK_SH);\n \t\t\t\t$data = $self->fetch($key);\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 7ee628f..1f876bd 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -344,4 +344,65 @@ SKIP: {\n }\n $cache->set_expires_in(-1);\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+# Catch output printed by ->compute\n+# (only for 'print <sth>' and 'printf <sth>')\n+sub capture_compute {\n+\tmy $output = '';\n+\n+\topen my $output_fh, '>', \\$output;\n+\tmy $oldfh = select($output_fh);\n+\n+\tmy $data = $cache->compute($key, \\&get_value_slow);\n+\n+\tselect($oldfh);\n+\tclose $output_fh;\n+\n+\treturn ($output, $data);\n+}\n+sub run_capture_compute_forked {\n+\tmy $pid = shift;\n+\n+\tmy ($output, $data) = capture_compute();\n+\tmy ($child_output, $child_data);\n+\n+\tif ($pid) {\n+\t\tlocal $/ = \"\\0\";\n+\t\tchomp($child_output = <$kid_fh>);\n+\t\tchomp($child_data   = <$kid_fh>);\n+\n+\t\twaitpid $pid, 0;\n+\t\tclose $kid_fh;\n+\t} else {\n+\t\tlocal $| = 1;\n+\t\t$output = '' unless defined $output;\n+\t\t$data   = '' unless defined $data;\n+\t\tprint \"$output\\0$data\\0\";\n+\t\texit 0;\n+\t}\n+\n+\treturn ($output, $data, $child_output, $child_data);\n+}\n+SKIP: {\n+\t$pid = open $kid_fh, '-|';\n+\tskip \"cannot fork: $!\", 4\n+\t\tunless defined $pid;\n+\n+\tmy ($output, $data, $child_output, $child_data) =\n+\t\trun_capture_compute_forked($pid);\n+\n+\tis($output,       $progress_info, 'progress info from parent');\n+\tis($child_output, $progress_info, 'progress info from child');\n+\tis($data,         $value,         'data info from parent');\n+\tis($child_data,   $value,         'data info from child');\n+}\n+\n done_testing();\n-- \n1.7.3\n"},{"id":"152801","messageId":"1286402526-13143-18-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PATCHv5/RFC 17/17] gitweb: Add startup delay to activity indicator for cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:02:02Z","receivedAt":"2010-10-06T22:02:02Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This adds support for [optional] startup delay to git_generating_data_html()\nsubroutine, which is used to provide \"Generating...\" page as activity\nindicator when waiting for page to be generated if caching.  If the data\n(page contents) gets generated within $generating_options{'staryp_delay'}\nseconds, the \"Generating...\" page won't get displayed.\n\nThis feature was created in response to complaint by Petr 'Pasky' Baudis'\nabout \"Generating...\" feature.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nNo changes since v4 (previous version).\n\nThis patch doesn't have equivalent in J.H. patch, and is rather\nextension of idea of progress indicator for (re)generating [cache]\ndata.  It was inspired by comment by Petr 'Pasky' Baudis on #git IRC\nchannel that one doesn't want to have \"Generating...\" page to be shown\nonly to immediately vanish.\n\n gitweb/gitweb.perl |   20 ++++++++++++++++++++\n 1 files changed, 20 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 07fa825..fee8739 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -354,6 +354,9 @@ our %cache_options = (\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@@ -3570,6 +3573,23 @@ sub git_generating_data_html {\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\tflock($lock_fh, LOCK_SH); # blocking readers lock\n+\t\t\talarm 0;\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-- \n1.7.3\n"},{"id":"152804","messageId":"1286402526-13143-19-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[RFC/PATCHv5 18/17] gitweb/lib - Add clear() and size() methods to caching interface","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:02:03Z","receivedAt":"2010-10-06T22:02:03Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add ->size() method, which following Cache::Cache interface returns\nestimated total size of all entries in whole cache (in the namsepace\nassiciated with give cache instance).  Note that ->get_size($key)\nreturns size of a single entry!\n\nAdd ->clear() method, which removes all entries from the namespace\nassociated with given cache instance.  For safety it requires\nnamespace to be set to true value, which means that it cannot be\nempty; therefore default namespace is changed to 'gitweb'.\n\nThe ->clear() method should be fairly safe, because it first renames\ndirectory (which should be atomic), and only then removes it\n(following code from CGI::Driver::File).\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThe only difference from previous version is that a comment about some\nmethod was moved to earlier commit that introduced it, where it\nbelongs.\n\nThis patch (implementing \"backend\") together with next patch\n(implementing \"interface\") are meant to provide nice web interface to\nsafely cleaning (pruning) cache, as described in comments and\ndocumentation (how to do this manually).\n\n gitweb/lib/GitwebCache/SimpleFileCache.pm |   48 ++++++++++++++++++++++++++--\n t/t9503/test_cache_interface.pl           |    1 -\n 2 files changed, 44 insertions(+), 5 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex 2a3d9cf..8f172cb 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -20,8 +20,9 @@ package GitwebCache::SimpleFileCache;\n use strict;\n use warnings;\n \n-use File::Path qw(mkpath);\n-use File::Temp qw(tempfile);\n+use File::Path qw(mkpath rmtree);\n+use File::Temp qw(tempfile mktemp);\n+use File::Find qw(find);\n use Digest::MD5 qw(md5_hex);\n \n # by default, the cache nests all entries on the filesystem single\n@@ -37,7 +38,7 @@ our $DEFAULT_CACHE_ROOT = \"cache\";\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+our $DEFAULT_NAMESPACE = \"gitweb\";\n \n # ......................................................................\n # constructor\n@@ -334,7 +335,7 @@ sub get_size {\n }\n \n # ......................................................................\n-# interface methods\n+# interface methods dealing with single item\n \n # Removing and expiring\n \n@@ -427,6 +428,45 @@ sub compute {\n \treturn $data;\n }\n \n+# ......................................................................\n+# interface methods dealing with whole namespace\n+\n+# $cache->clear();\n+#\n+# Remove all entries from the namespace.\n+# Namespace must be defined and not empty.\n+sub clear {\n+\tmy $self = shift;\n+\n+\treturn unless $self->get_namespace();\n+\n+\tmy $namespace_dir = $self->path_to_namespace();\n+\treturn if !-d $namespace_dir;\n+\n+\tmy $renamed_dir = mktemp($namespace_dir . '.XXXX');\n+\trename($namespace_dir, $renamed_dir);\n+\trmtree($renamed_dir);\n+\tdie \"Couldn't remove '$renamed_dir' directory\"\n+\t\tif -d $renamed_dir;\n+}\n+\n+# $size = $cache->size();\n+#\n+# Size of whole names (or whole cache if namespace empty)\n+sub size {\n+\tmy $self = shift;\n+\n+\tmy $namespace_dir = $self->path_to_namespace();\n+\treturn if !-d $namespace_dir;\n+\n+\tmy $total_size = 0;\n+\tmy $add_size = sub { $total_size += -s $_ };\n+\n+\tFile::Find::find({ wanted => $add_size, no_chdir => 1 }, $namespace_dir);\n+\n+\treturn $total_size;\n+}\n+\n 1;\n __END__\n # end of package GitwebCache::SimpleFileCache;\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 1f876bd..3a903cb 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -34,7 +34,6 @@ SKIP: {\n \t\tunless ($GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT &&\n \t\t        $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH);\n \n-\tis($cache->get_namespace(), '', \"default namespace is ''\");\n \tcmp_ok($cache->get_root(),  'eq', $GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT,\n \t\t\"default cache root is '$GitwebCache::SimpleFileCache::DEFAULT_CACHE_ROOT'\");\n \tcmp_ok($cache->get_depth(), '==', $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH,\n-- \n1.7.3\n"},{"id":"152792","messageId":"1286402526-13143-20-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[RFC PATCHv5 19/17] gitweb: Add beginnings of cache administration page","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:02:04Z","receivedAt":"2010-10-06T22:02:04Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Currently it shows estimated size of cache, and provides link to\nclearing cache.\n\nCache administration page is visible only on local computer; the same\nis true with respect to ability to clear cache.  Those are bare\nbeginnings of autorization framework.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nDifferences from v4:\n* action_is_cacheable() (is_cacheable() in previous version) now uses\n  %actions_info hash.\n\n* Adding actions related to gitweb cache administration is now done in\n  configure_caching() subroutine.\n\n* cache_admin_auth_ok() now returns true also for running gitweb as\n  script, when REMOTE_ADDR environment variable is not define.  It\n  also covers situation where non-standard (not in RFC 3875)\n  SERVER_ADDR is not defined.\n\n\nThis is \"frontend\" (interface) patch for gitweb cache management.\n\nThis is very much work in progress, sent to git mailing list as a\nproof of concept.  Would something like that (perhaps with more\nadvanced authorization for admin) be useful?\n\nThe 'cache' page looks like this:\n\n  Cache location      Size\t \n  ------------------+---------+--------------\n  cache/gitweb        14 KiB\t[Clear cache]\n  ------------------+---------+--------------\n\nwhere '[Clear cache]' is a submit button.\n\n gitweb/gitweb.perl |  119 ++++++++++++++++++++++++++++++++++++++++++++++++++--\n 1 files changed, 115 insertions(+), 4 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex fee8739..c413797 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -24,6 +24,8 @@ use Encode;\n use Fcntl qw(:mode :flock);\n use File::Find qw();\n use File::Basename qw(basename);\n+use POSIX;\n+\n binmode STDOUT, ':utf8';\n \n our $t0;\n@@ -851,6 +853,10 @@ sub evaluate_actions_info {\n \tmap { $actions_info{$_}{'output_format'} = undef }\n \t\tqw(blob_plain object);\n \t$actions_info{'snapshot'}{'output_format'} = 'binary';\n+\n+\t# specify uncacheable actions\n+\tmap { $actions_info{$_}{'uncacheable'} = 1 }\n+\t\tqw(cache clear_cache);\n }\n \n sub action_outputs_html {\n@@ -858,6 +864,11 @@ sub action_outputs_html {\n \treturn $actions_info{$action}{'output_format'} eq 'html';\n }\n \n+sub action_is_cacheable {\n+\tmy $action = shift;\n+\treturn !$actions_info{$action}{'uncacheable'};\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@@ -1178,12 +1189,13 @@ 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 ($action !~ m/^(?:opml|project_list|project_index|cache|clear_cache)$/ &&\n \t    !$project) {\n \t\tdie_error(400, \"Project needed\");\n \t}\n \n-\tif ($caching_enabled) {\n+\tif ($caching_enabled &&\n+\t    action_is_cacheable($action)) {\n \t\t# human readable key identifying gitweb output\n \t\tmy $output_key = href(-replay => 1, -full => 1, -path_info => 0);\n \n@@ -1204,11 +1216,11 @@ sub run_request {\n \n \tevaluate_uri();\n \tevaluate_gitweb_config();\n-\tevaluate_actions_info();\n \tevaluate_git_version();\n \tcheck_loadavg();\n \tconfigure_caching()\n \t\tif ($caching_enabled);\n+\tevaluate_actions_info();\n \n \t# $projectroot and $projects_list might be set in gitweb config file\n \t$projects_list ||= $projectroot;\n@@ -1311,6 +1323,10 @@ sub configure_caching {\n \t\t\t'depth' => $cache_options{'cache_depth'},\n \t\t});\n \t}\n+\n+\t# some actions are available only if cache is turned on\n+\t$actions{'cache'} = \\&git_cache_admin;\n+\t$actions{'clear_cache'} = \\&git_cache_clear;\n }\n \n run();\n@@ -3673,7 +3689,7 @@ sub git_header_html {\n \tmy $expires = shift;\n \tmy %opts = @_;\n \n-\tmy $title = get_page_title();\n+\tmy $title = $opts{'-title'} || get_page_title();\n \tmy $content_type;\n \t# require explicit support from the UA if we are to send the page as\n \t# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.\n@@ -7325,3 +7341,98 @@ XML\n </opml>\n XML\n }\n+\n+# see Number::Bytes::Human\n+sub human_readable_size {\n+\tmy $bytes = shift || return;\n+\n+\tmy @units = ('', 'KiB', 'MiB', 'GiB', 'TiB');\n+\tmy $block = 1024;\n+\n+\tmy $x = $bytes;\n+\tmy $unit;\n+\tforeach (@units) {\n+\t\t$unit = $_, last if POSIX::ceil($x) < $block;\n+\t\t$x /= $block;\n+\t}\n+\n+\tmy $num;\n+\tif ($x < 10.0) {\n+\t\t$num = sprintf(\"%.1f\", POSIX::ceil($x*10)/10); \n+\t} else {\n+\t\t$num = sprintf(\"%d\", POSIX::ceil($x));\n+\t}\n+\n+\treturn \"$num $unit\";\n+}\n+\n+sub cache_admin_auth_ok {\n+\tif (defined $ENV{'REMOTE_ADDR'}) {\n+\t\tif (defined $ENV{'SERVER_ADDR'}) {\n+\t\t\t# SERVER_ADDR is not in RFC 3875\n+\t\t\treturn $ENV{'SERVER_ADDR'} eq $ENV{'REMOTE_ADDR'};\n+\t\t} elsif ($ENV{'REMOTE_ADDR'} =~ m!^(?:127\\.0\\.0\\.1|::1/128)$!) {\n+\t\t\t# localhost in IPv4 or IPv6\n+\t\t\treturn 1;\n+\t\t}\n+\t} else {\n+\t\t# REMOTE_ADDR not defined, probably calling gitweb as script\n+\t\treturn 1;\n+\t}\n+\n+\t# restrict all but specified cases\n+\treturn 0;\n+}\n+\n+sub git_cache_admin {\n+\t$caching_enabled\n+\t\tor die_error(403, \"Caching disabled\");\n+\tcache_admin_auth_ok()\n+\t\tor die_error(403, \"Cache administration not allowed\");\n+\t$cache && ref($cache)\n+\t\tor die_error(500, \"Cache is not present\");\n+\n+\tgit_header_html(undef, undef,\n+\t\t-title => to_utf8($site_name) . \" - Gitweb cache\");\n+\n+\tprint <<'EOF_HTML';\n+<table class=\"cache_admin\">\n+<tr><th>Cache location</th><th>Size</th><th>&nbsp;</th></tr>\n+EOF_HTML\n+\tprint '<tr class=\"light\">' .\n+\t      '<td class=\"path\">' . esc_path($cache->path_to_namespace()) . '</td>' .\n+\t      '<td>';\n+\tmy $size;\n+\t$size = $cache->size()     if (!defined $size && $cache->can('size'));\n+\t$size = $cache->get_size() if (!defined $size && $cache->can('get_size'));\n+\tif (defined $size) {\n+\t\tprint human_readable_size($size);\n+\t}\n+\tprint '</td><td>';\n+\tif ($cache->can('clear')) {\n+\t\tprint $cgi->start_form({-method => \"POST\",\n+\t\t                        -action => $my_uri,\n+\t\t                        -enctype => CGI::URL_ENCODED}) .\n+\t\t      $cgi->input({-name=>\"a\", -value=>\"clear_cache\", -type=>\"hidden\"}) .\n+\t\t      $cgi->submit({-label => 'Clear cache'}) .\n+\t\t      $cgi->end_form();\n+\t}\n+\tprint <<'EOF_HTML';\n+</td></tr>\n+</table>\n+EOF_HTML\n+\n+\tgit_footer_html();\n+}\n+\n+sub git_cache_clear {\n+\tif (cache_admin_auth_ok() && $cache &&\n+\t    $cgi->request_method() eq 'POST') {\n+\n+\t\t$cache->clear();\n+\t}\n+\n+\t#print \"cleared\";\n+\tprint $cgi->redirect(-uri => href(action=>'cache', -full=>1),\n+\t                     -status => '303 See Other');\n+}\n-- \n1.7.3\n"},{"id":"152805","messageId":"1286402526-13143-21-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PoC PATCHv5 20/17] gitweb/lib - Benchmarking GitwebCache::SimpleFileCache (in t/9603/)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:02:05Z","receivedAt":"2010-10-06T22:02:05Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add t/t9503/benchmark_caching_interface.pl, which benchmarks 'set' and\n'get' methods from GitwebCache::SimpleFileCache, and compares them\nagainst Cache::FileCache, Cache::MemoryCache, and Cache::FastMmap\nif they are available.\n\nIncludes example benchmark results.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nFormerly as patch 06/17 in previous version of series, now marked PoC\n(\"proof of concept\"), and moved to the end of series.\n\nDifferences from v4:\n* Make benchmark as if for persistent (get/set only) and\n  non-persistent (setup+get/set) environment.\n\n* Add benchmark for Cache::NullCache (from Cache::Cache distribution)\n  as a kind of baseline.\n\n* Do tests for Cache::FastMmap both with and without setting\n  raw_values.\n\n* Include versions of modules used in benchmarks, and add new sample\n  benchmark results.\n\n t/t9503/benchmark_caching_interface.pl |  209 ++++++++++++++++++++++++++++++++\n 1 files changed, 209 insertions(+), 0 deletions(-)\n create mode 100755 t/t9503/benchmark_caching_interface.pl\n\ndiff --git a/t/t9503/benchmark_caching_interface.pl b/t/t9503/benchmark_caching_interface.pl\nnew file mode 100755\nindex 0000000..db06f6e\n--- /dev/null\n+++ b/t/t9503/benchmark_caching_interface.pl\n@@ -0,0 +1,209 @@\n+#!/usr/bin/perl\n+use lib (split(/:/, $ENV{GITPERLLIB}));\n+\n+use warnings;\n+use strict;\n+\n+use File::Spec;\n+use File::Path;\n+use Benchmark qw(:all);\n+\n+# benchmark source version\n+sub __DIR__ () {\n+\tFile::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);\n+}\n+use lib __DIR__.\"/../../gitweb/lib\";\n+use GitwebCache::SimpleFileCache;\n+\n+my $key = 'http://localhost/cgi-bin/gitweb.cgi?p=git/git.git;a=blob_plain;f=lorem.txt;hb=HEAD';\n+my $value = <<'EOF';\n+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do\n+eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad\n+minim veniam, quis nostrud exercitation ullamco laboris nisi ut\n+aliquip ex ea commodo consequat. Duis aute irure dolor in\n+reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla\n+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in\n+culpa qui officia deserunt mollit anim id est laborum.\n+EOF\n+\n+my $cache_root = __DIR__.'/cache';\n+my %cache_options = (\n+\t'cache_root' => $cache_root, # Cache::FileCache compatibile\n+\t'root_dir'   => $cache_root, # CHI::Driver::File compatibile\n+\t'cache_depth' => 1, # Cache::FileCache compatibile\n+\t'depth'       => 1, # CHI::Driver::File compatibile\n+\t'default_expires_in' => 24*60*60, # Cache::Cache compatibile\n+\t'expires_in'         => 24*60*60, # CHI compatibile\n+\t'expire_time'        => 24*60*60, # Cache::FastMmap compatibile\n+);\n+\n+mkdir($cache_root);\n+\n+my %caches;\n+\n+sub add_cache {\n+\tmy ($caches, $cache_key, $module, %extra_opts) = @_;\n+\tmy $namespace = $module;\n+\t$namespace =~ s/::/-/g;\n+\n+\t$caches->{$cache_key}{'module'} = $module;\n+\t$caches->{$cache_key}{'start'} = sub {\n+\t\t$module->new({\n+\t\t\t'namespace' => $namespace,\n+\t\t\t%cache_options,\n+\t\t\t%extra_opts,\n+\t\t});\n+\t};\n+\tmy $cache = $caches->{$cache_key}{'cache'}\n+\t\t= $caches->{$cache_key}{'start'}->();\n+\t$caches->{$cache_key}{'set'} = sub {\n+\t\t$cache->set($key, $value);\n+\t};\n+\t$caches->{$cache_key}{'get'} = sub {\n+\t\t$cache->get($key);\n+\t};\n+\t$caches->{$cache_key}{'setup+set'} = sub {\n+\t\t$caches->{$cache_key}{'start'}->();\n+\t\t$caches->{$cache_key}{'set'}->();\n+\t};\n+\t$caches->{$cache_key}{'setup+get'} = sub {\n+\t\t$caches->{$cache_key}{'start'}->();\n+\t\t$caches->{$cache_key}{'get'}->();\n+\t};\n+}\n+\n+add_cache(\\%caches, 'SimpleFileCache', 'GitwebCache::SimpleFileCache');\n+\n+# assume that all Cache::Cache modules are available if one of them is\n+if (eval { require Cache::FileCache; 1; }) {\n+\tadd_cache(\\%caches, 'C::FileCache',   'Cache::FileCache');\n+\n+\t# for reference\n+\trequire Cache::MemoryCache;\n+\tadd_cache(\\%caches, 'C::MemoryCache', 'Cache::MemoryCache');\n+\trequire Cache::NullCache;\n+\tadd_cache(\\%caches, 'C::NullCache',   'Cache::NullCache');\n+}\n+\n+if (eval { require Cache::FastMmap; 1 }) {\n+\tadd_cache(\\%caches, 'FastMmap', 'Cache::FastMmap',\n+\t\t'share_file' => $cache_root.'/Cache-FastMmap',\n+\t\t'init_file' => 1,  # clear any exiting values and re-initialize file\n+\t\t'raw_values' => 1, # don't freeze/thaw (serialize) data\n+\t);\n+\tadd_cache(\\%caches, 'FastMmap (S)', 'Cache::FastMmap',\n+\t\t'share_file' => $cache_root.'/Cache-FastMmap',\n+\t\t'init_file' => 1,  # clear any exiting values and re0initialize file\n+\t);\n+}\n+\n+my %codehash;\n+my $count = -10;\n+my $result_set;\n+\n+print '$cache->set($key, $value)'.\"\\n\";\n+%codehash = map { $_ => $caches{$_}{'set'} } keys %caches;\n+$result_set = timethese($count, \\%codehash);\n+cmpthese($result_set);\n+print \"\\n\";\n+\n+print '$cache->get($key)'.\"\\n\";\n+%codehash = map { $_ => $caches{$_}{'get'} } keys %caches;\n+$result_set = timethese($count, \\%codehash);\n+cmpthese($result_set);\n+print \"\\n\";\n+\n+## Cache::FastMmap shouldn't use \"'init_file' => 1\" for this\n+#\n+# print '$cache->new(...) + $cache->set($key, $value)'.\"\\n\";\n+# %codehash = map { $_ => $caches{$_}{'setup+set'} } keys %caches;\n+# $result_set = timethese($count, \\%codehash);\n+# cmpthese($result_set);\n+# print \"\\n\";\n+#\n+# print '$cache->new(...) + $cache->get($key)'.\"\\n\";\n+# %codehash = map { $_ => $caches{$_}{'setup+get'} } keys %caches;\n+# $result_set = timethese($count, \\%codehash);\n+# cmpthese($result_set);\n+# print \"\\n\";\n+\n+rmtree($cache_root);\n+\n+1;\n+__END__\n+## EXAMPLE OUTPUT ##\n+#\n+# Cache::FastMmap version 1.35\n+# Cache::Cache (Cache::FileCache, Cache::MemoryCache) version 1.05\n+#\n+# $cache->set($key, $value)\n+# Benchmark: running Cache::FastMmap, Cache::FileCache, Cache::MemoryCache, GitwebCache\n+#   for at least 10 CPU seconds...\n+# Cache::FastMmap:    11 wallclock secs ( 8.46 usr +  2.17 sys = 10.63 CPU) @ 15710.25/s (n=167000)\n+# Cache::FileCache:   15 wallclock secs ( 8.43 usr +  2.25 sys = 10.68 CPU) @ 356.27/s   (n=3805)\n+# Cache::MemoryCache: 13 wallclock secs ( 9.91 usr +  0.09 sys = 10.00 CPU) @ 3306.20/s  (n=33062)\n+# GitwebCache:        29 wallclock secs ( 7.02 usr +  3.47 sys = 10.49 CPU) @ 605.91/s   (n=6356)\n+#                       Rate Cache::FileCache GitwebCache Cache::MemoryCache Cache::FastMmap\n+# Cache::FileCache     356/s               --        -41%               -89%            -98%\n+# GitwebCache          606/s              70%          --               -82%            -96%\n+# Cache::MemoryCache  3306/s             828%        446%                 --            -79%\n+# Cache::FastMmap    15710/s            4310%       2493%               375%              --\n+#\n+# $cache->get($key)  # $key exists in cache\n+# Benchmark: running Cache::FastMmap, Cache::FileCache, Cache::MemoryCache, GitwebCache\n+#   for at least 10 CPU seconds...\n+# Cache::FastMmap:    13 wallclock secs ( 7.32 usr +  2.83 sys = 10.15 CPU) @ 24260.59/s (n=246245)\n+# Cache::FileCache:   12 wallclock secs ( 9.22 usr +  1.30 sys = 10.52 CPU) @ 972.62/s   (n=10232)\n+# Cache::MemoryCache: 14 wallclock secs ( 9.89 usr +  0.12 sys = 10.01 CPU) @ 3679.52/s  (n=36832)\n+# GitwebCache:        20 wallclock secs ( 8.16 usr +  2.36 sys = 10.52 CPU) @ 4401.05/s  (n=46299)\n+#                       Rate Cache::FileCache Cache::MemoryCache GitwebCache Cache::FastMmap\n+# Cache::FileCache     973/s               --               -74%        -78%            -96%\n+# Cache::MemoryCache  3680/s             278%                 --        -16%            -85%\n+# GitwebCache         4401/s             352%                20%          --            -82%\n+# Cache::FastMmap    24261/s            2394%               559%        451%              --\n+\n+## EXAMPLE OUTPUT (new version of test, modified SimpleFileCache) ##\n+#\n+## like for non-persistent environment\n+#\n+# $cache->new(...) + $cache->set($key, $value)\n+# Benchmark: running C::FileCache, C::MemoryCache, C::NullCache, SimpleFileCache\n+#   for at least 10 CPU seconds...\n+#                    Rate C::FileCache SimpleFileCache C::MemoryCache C::NullCache\n+# C::FileCache      271/s           --            -47%           -85%         -99%\n+# SimpleFileCache   510/s          88%              --           -71%         -98%\n+# C::MemoryCache   1766/s         553%            246%             --         -94%\n+# C::NullCache    29119/s       10660%           5612%          1549%           --\n+#\n+# $cache->new(...) + $cache->get($key)\n+# Benchmark: running C::FileCache, C::MemoryCache, C::NullCache, SimpleFileCache\n+#   for at least 10 CPU seconds...\n+#                    Rate C::FileCache C::MemoryCache SimpleFileCache C::NullCache\n+# C::FileCache      510/s           --           -73%            -82%         -98%\n+# C::MemoryCache   1905/s         273%             --            -32%         -93%\n+# SimpleFileCache  2806/s         450%            47%              --         -90%\n+# C::NullCache    28626/s        5509%          1402%            920%           --\n+#\n+## like for persistent environment\n+#\n+# $cache->set($key, $value)\n+# Benchmark: running C::FileCache, C::MemoryCache, C::NullCache,\n+#   FastMmap, FastMmap (S), SimpleFileCache for at least 10 CPU seconds...\n+#                     Rate C::FileCache SimpleFileCache C::MemoryCache FastMmap (S) FastMmap C::NullCache\n+# C::FileCache       309/s           --            -46%           -90%         -94%     -98%        -100%\n+# SimpleFileCache    574/s          86%              --           -82%         -88%     -96%        -100%\n+# C::MemoryCache    3168/s         925%            451%             --         -34%     -78%         -99%\n+# FastMmap (S)      4828/s        1462%            740%            52%           --     -66%         -99%\n+# FastMmap         14289/s        4524%           2387%           351%         196%       --         -98%\n+# C::NullCache    572686/s      185208%          99586%         17976%       11761%    3908%           --\n+#\n+# $cache->get($key)  # $key exists in cache\n+# Benchmark: running C::FileCache, C::MemoryCache, C::NullCache,\n+#   FastMmap, FastMmap (S), SimpleFileCache for at least 10 CPU seconds...\n+#                     Rate C::FileCache SimpleFileCache C::MemoryCache FastMmap (S) FastMmap C::NullCache\n+# C::FileCache       830/s           --           -77%            -79%         -94%     -96%        -100%\n+# C::MemoryCache    3539/s         326%             --            -12%         -75%     -84%         -99%\n+# SimpleFileCache   4040/s         387%            14%              --         -72%     -82%         -99%\n+# FastMmap (S)     14367/s        1631%           306%            256%           --     -35%         -97%\n+# FastMmap         22247/s        2580%           529%            451%          55%       --         -96%\n+# C::NullCache    546698/s       65759%         15348%          13431%        3705%    2357%           --\n-- \n1.7.3\n"},{"id":"152802","messageId":"1286402526-13143-22-git-send-email-jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[PoC PATCHv5 21/17] gitweb/lib - Alternate ways of capturing output","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T22:02:06Z","receivedAt":"2010-10-06T22:02:06Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Besides GitwebCache::Capture::SelectFH, which uses select(FH) to\nredirect 'print LIST' and 'printf FORMAT, LIST' to in-memory file to\ncapture output, add GitwebCache::Capture::TiedCapture which uses\ntie-ing filehandle to capture output, and GitwebCache::Capture::PerlIO\nwhich uses push_layer method from non-core PerlIO::Util module to\ncapture output.\n\nAdd test (which can be run standalone) for all those implementations,\nchecking ':utf8' and ':raw' output, and benchmark comparing them\n(includes example benchmark tests).  Please note that the test for\nalternate implementations is not run from t9504 test.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nFormerly as patch 08/17 in previous version of series, now marked PoC\n(\"proof of concept\"), and moved to the end of series.\n\nDifferences from v4:\n* GitwebCache::Capture::TiedCapture has now two (sub)versions: one\n  appending data from tied operations to a string (after conversion)\n  via TiedCapture::String (default), other redirecting tied operations\n  to save to in-memory file via TiedCapture::PerlIO (new).\n\n  Add tests for new TiedCapture::PerlIO.\n\n* Tie::Restore (non-core module from CPAN) is now in separate file;\n  should be probably in 'inc/' and not in 'lib/'.\n\n* Change name of field in TiedCapture::* from 'mode' to 'binmode'.\n\n* New example results of benchmark.\n\n* Add test checking that all those implementations work correctly for\n  capturing both ':utf8' and ':raw' printed data.\n\n gitweb/lib/GitwebCache/Capture/PerlIO.pm      |   79 +++++++++\n gitweb/lib/GitwebCache/Capture/TiedCapture.pm |   76 +++++++++\n gitweb/lib/Tie/Restore.pm                     |   24 +++\n gitweb/lib/TiedCapture/PerlIO.pm              |   56 ++++++\n gitweb/lib/TiedCapture/String.pm              |   53 ++++++\n t/t9504/benchmark_capture_implementations.pl  |  226 +++++++++++++++++++++++++\n t/t9504/test_capture_implementations.pl       |   85 +++++++++\n 7 files changed, 599 insertions(+), 0 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/Capture/PerlIO.pm\n create mode 100644 gitweb/lib/GitwebCache/Capture/TiedCapture.pm\n create mode 100644 gitweb/lib/Tie/Restore.pm\n create mode 100644 gitweb/lib/TiedCapture/PerlIO.pm\n create mode 100644 gitweb/lib/TiedCapture/String.pm\n create mode 100755 t/t9504/benchmark_capture_implementations.pl\n create mode 100755 t/t9504/test_capture_implementations.pl\n\ndiff --git a/gitweb/lib/GitwebCache/Capture/PerlIO.pm b/gitweb/lib/GitwebCache/Capture/PerlIO.pm\nnew file mode 100644\nindex 0000000..199aeed\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/Capture/PerlIO.pm\n@@ -0,0 +1,79 @@\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+# Output capturing using PerlIO layers\n+#\n+\n+# This module requires PaerlIO::Util installed.\n+\n+package GitwebCache::Capture::PerlIO;\n+\n+use PerlIO::Util;\n+\n+use strict;\n+use warnings;\n+\n+use base qw(GitwebCache::Capture);\n+use GitwebCache::Capture qw(:all);\n+\n+use Exporter qw(import);\n+our @EXPORT      = @GitwebCache::Capture::EXPORT;\n+our @EXPORT_OK   = @GitwebCache::Capture::EXPORT_OK;\n+our %EXPORT_TAGS = %GitwebCache::Capture::EXPORT_TAGS;\n+\n+# Constructor\n+sub new {\n+\tmy $proto = shift;\n+\n+\tmy $class = ref($proto) || $proto;\n+\tmy $self  = {};\n+\t$self = bless($self, $class);\n+\n+\t$self->{'data'} = '';\n+\n+\treturn $self;\n+}\n+\n+# Start capturing data (STDOUT)\n+# (printed using 'print <sth>' or 'printf <sth>')\n+sub start {\n+\tmy $self = shift;\n+\n+\t$self->{'data'}    = '';\n+\t*STDOUT->push_layer('scalar' => \\$self->{'data'});\n+\n+\t# push ':utf8' on top, if it was on top\n+\t*STDOUT->push_layer(':utf8')\n+\t\tif ((*STDOUT->get_layers())[-2] eq 'utf8');\n+}\n+\n+# Stop capturing data (required for die_error)\n+sub stop {\n+\tmy $self = shift;\n+\n+\t# return if we didn't start capturing\n+\tmy @layers = *STDOUT->get_layers();\n+\treturn unless grep { $_ eq 'scalar' } @layers;\n+\n+\tmy $was_utf8 = $layers[-1] eq 'utf8';\n+\t# stop saving to scalar, i.e. remove topmost 'scalar' layer,\n+\t# but remember that 'utf8' layer might be on top of it\n+\twhile ((my $layer = *STDOUT->pop_layer())) {\n+\t\tpop @layers;\n+\t\tlast if $layer eq 'scalar';\n+\t}\n+\t# restore ':utf8' mode, if needed\n+\tif ($was_utf8 && $layers[-1] ne 'utf8') {\n+\t\t*STDOUT->push_layer('utf8');\n+\t}\n+\n+\treturn $self->{'data'};\n+}\n+\n+1;\n+__END__\n+# end of package GitwebCache::Capture::PerlIO;\ndiff --git a/gitweb/lib/GitwebCache/Capture/TiedCapture.pm b/gitweb/lib/GitwebCache/Capture/TiedCapture.pm\nnew file mode 100644\nindex 0000000..6bed0f8\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/Capture/TiedCapture.pm\n@@ -0,0 +1,76 @@\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 by tie-ing filehandle\n+#\n+\n+package GitwebCache::Capture::TiedCapture;\n+\n+use PerlIO;\n+\n+use strict;\n+use warnings;\n+\n+use base qw(GitwebCache::Capture);\n+use GitwebCache::Capture qw(:all);\n+\n+use Exporter qw(import);\n+our @EXPORT      = @GitwebCache::Capture::EXPORT;\n+our @EXPORT_OK   = @GitwebCache::Capture::EXPORT_OK;\n+our %EXPORT_TAGS = %GitwebCache::Capture::EXPORT_TAGS;\n+\n+# Constructor\n+sub new {\n+\tmy $proto = shift;\n+\n+\tmy $class = ref($proto) || $proto;\n+\tmy $self  = {};\n+\t$self = bless($self, $class);\n+\n+\t$self->{'data'} = '';\n+\t$self->{'tied'} = undef;\n+\t$self->{'tie_class'} = shift || 'TiedCapture::String';\n+\teval \"require $self->{'tie_class'}\";\n+\n+\treturn $self;\n+}\n+\n+# Start capturing data (STDOUT)\n+# (printed using 'print <sth>' or 'printf <sth>')\n+sub start {\n+\tmy $self = shift;\n+\n+\t# savie tie\n+\t$self->{'tied'} = tied *STDOUT;\n+\n+\t$self->{'data'} = '';\n+\ttie *STDOUT, $self->{'tie_class'}, \\$self->{'data'};\n+\n+\t# re-binmode, so that tied class would pick it up\n+\tbinmode STDOUT,\n+\t\t(PerlIO::get_layers(*STDOUT))[-1] eq 'utf8' ? ':utf8' : ':raw';\n+}\n+\n+# Stop capturing data (required for die_error)\n+sub stop {\n+\tmy $self = shift;\n+\n+\t# return if we didn't start capturing\n+\treturn unless tied(*STDOUT)->isa($self->{'tie_class'});\n+\n+\t# restore ties, if there were any\n+\tuntie *STDOUT;\n+\tif ($self->{'tied'}) {\n+\t\ttie *STDOUT, 'Tie::Restore', $self->{'tied'};\n+\t}\n+\n+\treturn $self->{'data'};\n+}\n+\n+1;\n+__END__\n+# end of package GitwebCache::Capture::TiedCapture;\ndiff --git a/gitweb/lib/Tie/Restore.pm b/gitweb/lib/Tie/Restore.pm\nnew file mode 100644\nindex 0000000..687434e\n--- /dev/null\n+++ b/gitweb/lib/Tie/Restore.pm\n@@ -0,0 +1,24 @@\n+########################################################################\n+# This package should probably be put in `gitweb/inc/' instead\n+#\n+# taken from http://search.cpan.org/~robwalker/Tie-Restore-0.11/Restore.pm\n+# with POD documentation stripped out\n+\n+package Tie::Restore;\n+# Written by Robby Walker ( webmaster@pointwriter.com )\n+# for Point Writer ( http://www.pointwriter.com/ ).\n+\n+our $VERSION = '0.11';\n+$VERSION = eval $VERSION;\n+\n+# $object = tied %hash;                # save\n+# tie %hash, 'Tie::Restore', $object;  # restore\n+\n+sub TIESCALAR { $_[1] }\n+sub TIEARRAY  { $_[1] }\n+sub TIEHASH   { $_[1] }\n+sub TIEHANDLE { $_[1] }\n+\n+1;\n+__END__\n+# end of package Tie::Restore\ndiff --git a/gitweb/lib/TiedCapture/PerlIO.pm b/gitweb/lib/TiedCapture/PerlIO.pm\nnew file mode 100644\nindex 0000000..4bbd724\n--- /dev/null\n+++ b/gitweb/lib/TiedCapture/PerlIO.pm\n@@ -0,0 +1,56 @@\n+########################################################################\n+\n+package TiedCapture::PerlIO;\n+\n+our $VERSION = '0.001';\n+$VERSION = eval $VERSION;\n+\n+use strict;\n+use warnings;\n+\n+use PerlIO;\n+\n+sub TIEHANDLE {\n+\tmy ($proto, $dataref) = @_;\n+\tmy $class = ref($proto) || $proto;\n+\tmy $self = {};\n+\t$self = bless($self, $class);\n+\t$self->{'scalar'} = $dataref;\n+\t$self->{'binmode'} = ':utf8';\n+\n+\t$self->{'scalar_fh'} = undef;\n+\topen $self->{'scalar_fh'}, '>', $self->{'scalar'}\n+\t\tor die \"Couldn't open in-memory file for capture: $!\";\n+\n+\treturn $self;\n+}\n+\n+sub WRITE {\n+\tmy $self = shift;\n+\tsyswrite $self->{'scalar_fh'}, @_;\n+}\n+\n+sub PRINT {\n+\tmy $self = shift;\n+\tprint { $self->{'scalar_fh'} } @_;\n+}\n+\n+sub PRINTF {\n+\tmy $self = shift;\n+\tprintf { $self->{'scalar_fh'} } @_;\n+}\n+\n+sub BINMODE {\n+\tmy $self = shift;\n+\t$self->{'binmode'} = shift || ':raw';\n+\tbinmode $self->{'scalar_fh'}, $self->{'binmode'};\n+}\n+\n+#sub UNTIE {\n+#\tclose $self->{'scalar_fh'};\n+#\t$self->{'scalar_fh'} = undef;\n+#}\n+\n+1;\n+__END__\n+# end of package TiedCapture::PerlIO\ndiff --git a/gitweb/lib/TiedCapture/String.pm b/gitweb/lib/TiedCapture/String.pm\nnew file mode 100644\nindex 0000000..72b15a7\n--- /dev/null\n+++ b/gitweb/lib/TiedCapture/String.pm\n@@ -0,0 +1,53 @@\n+########################################################################\n+\n+package TiedCapture::String;\n+\n+our $VERSION = '0.001';\n+$VERSION = eval $VERSION;\n+\n+use strict;\n+use warnings;\n+\n+sub TIEHANDLE {\n+\tmy ($proto, $dataref) = @_;\n+\tmy $class = ref($proto) || $proto;\n+\tmy $self = {};\n+\t$self = bless($self, $class);\n+\t$self->{'scalar'} = $dataref;\n+\t$self->{'binmode'} = ':utf8';\n+\treturn $self;\n+}\n+\n+sub append_str {\n+\tmy ($self, $str) = @_;\n+\tutf8::encode($str) if ($self->{'binmode'} eq ':utf8');\n+\t${$self->{'scalar'}} .= $str;\n+}\n+\n+sub WRITE {\n+\tmy ($self, $buffer, $length, $offset) = @_;\n+\t$self->append_str(substr($buffer, $offset, $length));\n+}\n+\n+sub PRINT {\n+\tmy $self = shift;\n+\t$self->append_str(join('',@_));\n+}\n+\n+sub PRINTF {\n+\tmy $self = shift;\n+\t$self->append_str(sprintf(@_));\n+}\n+\n+sub BINMODE {\n+\tmy $self = shift;\n+\t$self->{'binmode'} = shift || ':raw';\n+}\n+\n+#sub UNTIE {\n+#\tlocal $^W = 0;\n+#}\n+\n+1;\n+__END__\n+# end of package TiedCapture::String\ndiff --git a/t/t9504/benchmark_capture_implementations.pl b/t/t9504/benchmark_capture_implementations.pl\nnew file mode 100755\nindex 0000000..588c1dc\n--- /dev/null\n+++ b/t/t9504/benchmark_capture_implementations.pl\n@@ -0,0 +1,226 @@\n+#!/usr/bin/perl\n+use lib (split(/:/, $ENV{GITPERLLIB}));\n+\n+use warnings;\n+use strict;\n+\n+use File::Spec;\n+use File::Path;\n+use Benchmark qw(:all);\n+\n+use PerlIO;\n+\n+# benchmark source version\n+sub __DIR__ () {\n+\tFile::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);\n+}\n+use lib __DIR__.\"/../../gitweb/lib\";\n+\n+# ....................................................................\n+\n+# Load modules (without importing)\n+#\n+my @modules =\n+\tmap { \"GitwebCache::Capture::$_\" }\n+\tqw(SelectFH TiedCapture);\n+foreach my $mod (@modules) {\n+\teval \"require $mod\";\n+}\n+if (eval { require PerlIO::Util; 1 }) {\n+\trequire GitwebCache::Capture::PerlIO;\n+\tpush @modules, 'GitwebCache::Capture::PerlIO';\n+}\n+\n+# Set up capturing, for each module\n+#\n+my @captures = map { $_->new() } @modules;\n+push @captures, GitwebCache::Capture::TiedCapture->new('TiedCapture::PerlIO');\n+\n+\n+my $test_data = <<'EOF';\n+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do\n+eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad\n+minim veniam, quis nostrud exercitation ullamco laboris nisi ut\n+aliquip ex ea commodo consequat. Duis aute irure dolor in\n+reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla\n+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in\n+culpa qui officia deserunt mollit anim id est laborum.\n+\n+Sed ut perspiciatis unde omnis iste natus error sit voluptatem\n+accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae\n+ab illo inventore veritatis et quasi architecto beatae vitae dicta\n+sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit\n+aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos\n+qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui\n+dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed\n+quia non numquam eius modi tempora incidunt ut labore et dolore magnam\n+aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum\n+exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex\n+ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in\n+ea voluptate velit esse quam nihil molestiae consequatur, vel illum\n+qui dolorem eum fugiat quo voluptas nulla pariatur?\n+EOF\n+\n+my @captured_output;\n+my $repeat = 100;\n+sub capture_output {\n+\tmy ($class, $mode) = @_;\n+\n+\t$class->start();\n+\tbinmode select(), $mode if defined($mode);\n+\tprint $test_data for (1..$repeat);\n+\n+\treturn $class->stop();\n+}\n+\n+my %codehash;\n+for (my $i = 0; $i < @captures; $i++) {\n+\tmy $capture = $captures[$i];\n+\tmy $name = ref($capture);\n+\t$name =~ s/^.*:://;\n+\t$name .= \" ($captures[$i]->{'tie_class'})\"\n+\t\tif $captures[$i]->{'tie_class'};\n+\t$name =~ s/TiedCapture:://;\n+\n+\t$codehash{$name} = sub { $captured_output[$i] = capture_output($capture) };\n+}\n+\n+# ....................................................................\n+\n+my $test_other_modules = 0;\n+\n+if ($test_other_modules) {\n+\n+\tif (eval { require Capture::Tiny; 1; }) {\n+\t\t$codehash{'Capture::Tiny'} = sub {\n+\t\t\tmy ($stdout, $stderr) = Capture::Tiny::capture(sub {\n+\t\t\t\tprint $test_data for (1..$repeat);\n+\t\t\t});\n+\t\t\tprint STDERR $stderr if defined($stderr);\n+\t\t};\n+\t}\n+\n+\tif (eval { require IO::CaptureOutput; 1; }) {\n+\t\t$codehash{'IO::CaptureOutput'} = sub {\n+\t\t\tmy ($stdout, $stderr);\n+\t\t\tIO::CaptureOutput::capture(sub {\n+\t\t\t\tprint $test_data for (1..$repeat);\n+\t\t\t}, \\$stdout, \\$stderr);\n+\t\t\tprint STDERR $stderr if defined($stderr);\n+\t\t};\n+\t\t# somehow it interferes with capturing in GitwebCache::Capture::PerlIO\n+\t\tdelete $codehash{'PerlIO'};\n+\t}\n+\n+\tif (eval { require IO::Capture::Stdout; 1; }) {\n+\t\t$codehash{'IO::Capture'} = sub {\n+\t\t\tmy $capture = IO::Capture::Stdout->new();\n+\n+\t\t\t$capture->start();\n+\t\t\tprint $test_data for (1..$repeat);\n+\t\t\t$capture->stop();\n+\n+\t\t\tmy $captured_output = join('', $capture->read());\n+\t\t};\n+\t}\n+} # end if ($test_other_modules)\n+\n+# ....................................................................\n+\n+print \"Capturing $repeat x \".length($test_data).\n+      \" = \".($repeat * length($test_data)).\" characters\\n\";\n+my $count = -10; # CPU seconds\n+my $result = timethese($count, \\%codehash);\n+cmpthese($result);\n+\n+#if (exists $codehash{PerlIO}) {\n+#\tcmpthese(-10, {\n+#\t\t'PerlIO::get_layers'  => sub { PerlIO::get_layers(*STDOUT); },\n+#\t\t'PerlIO::Util method' => sub { *STDOUT->get_layers(); },\n+#\t});\n+#}\n+\n+1;\n+__END__\n+## EXAMPLE OUTPUT ##\n+#\n+## 1 x $test_data, PerlIO using *STDOUT->get_layers();\n+# Benchmark: running PerlIO, SelectFH, TiedCapture for at least 10 CPU seconds...\n+#      PerlIO:  9 wallclock secs (10.38 usr +  0.13 sys = 10.51 CPU) @  9676.31/s (n=101698)\n+#    SelectFH: 12 wallclock secs (10.51 usr +  0.02 sys = 10.53 CPU) @ 12294.21/s (n=129458)\n+# TiedCapture: 10 wallclock secs (10.24 usr +  0.06 sys = 10.30 CPU) @  9489.22/s (n=97739)\n+#                Rate TiedCapture      PerlIO    SelectFH\n+# TiedCapture  9489/s          --         -2%        -23%\n+# PerlIO       9676/s          2%          --        -21%\n+# SelectFH    12294/s         30%         27%          --\n+#\n+## 10 x $test_data, PerlIO using *STDOUT->get_layers();\n+# Benchmark: running PerlIO, SelectFH, TiedCapture for at least 10 CPU seconds...\n+#      PerlIO:  9 wallclock secs (10.47 usr +  0.07 sys = 10.54 CPU) @ 7558.35/s (n=79665)\n+#    SelectFH: 11 wallclock secs (10.36 usr +  0.04 sys = 10.40 CPU) @ 8970.87/s (n=93297)\n+# TiedCapture: 11 wallclock secs (10.45 usr +  0.02 sys = 10.47 CPU) @ 2602.77/s (n=27251)\n+#               Rate TiedCapture      PerlIO    SelectFH\n+# TiedCapture 2603/s          --        -66%        -71%\n+# PerlIO      7558/s        190%          --        -16%\n+# SelectFH    8971/s        245%         19%          --\n+#\n+## 100 x $test_data, PerlIO using *STDOUT->get_layers();\n+# Benchmark: running PerlIO, SelectFH, TiedCapture for at least 50 CPU seconds...\n+#      PerlIO: 67 wallclock secs (35.28 usr + 17.82 sys = 53.10 CPU) @ 832.41/s (n=44201)\n+#    SelectFH: 73 wallclock secs (33.83 usr + 18.63 sys = 52.46 CPU) @ 830.06/s (n=43545)\n+# TiedCapture: 71 wallclock secs (50.93 usr +  0.41 sys = 51.34 CPU) @  95.31/s (n=4893)\n+#               Rate TiedCapture    SelectFH      PerlIO\n+# TiedCapture 95.3/s          --        -89%        -89%\n+# SelectFH     830/s        771%          --         -0%\n+# PerlIO       832/s        773%          0%          --\n+#\n+## 100 x $test_data, PerlIO using mix of *STDOUT->get_layers() and PerlIO::get_layers(*STDOUT);\n+# Capturing 100 x 1314 = 131400 characters\n+# Benchmark: timing 25000 iterations of PerlIO, SelectFH, TiedCapture...\n+#      PerlIO:  30 wallclock secs  (19.05 usr + 10.29 sys =  29.34 CPU) @ 852.08/s (n=25000)\n+#    SelectFH:  30 wallclock secs  (18.95 usr + 10.26 sys =  29.21 CPU) @ 855.87/s (n=25000)\n+# TiedCapture: 307 wallclock secs (267.37 usr +  2.95 sys = 270.32 CPU) @  92.48/s (n=25000)\n+#               Rate TiedCapture      PerlIO    SelectFH\n+# TiedCapture 92.5/s          --        -89%        -89%\n+# PerlIO       852/s        821%          --         -0%\n+# SelectFH     856/s        825%          0%          --\n+#\n+## 100 x $test_data (IO::CaptureOutput interferes with GitwebCache::Capture::PerlIO)\n+# Capturing 100 x 1314 = 131400 characters\n+# Benchmark: running IO::CaptureOutput, SelectFH, TiedCapture for at least 10 CPU seconds...\n+# IO::CaptureOutput: 12 wallclock secs ( 5.12 usr +  5.63 sys = 10.75 CPU) @ 126.60/s (n=1361)\n+#          SelectFH: 12 wallclock secs ( 6.93 usr +  3.45 sys = 10.38 CPU) @ 808.29/s (n=8390)\n+#       TiedCapture: 11 wallclock secs (10.11 usr +  0.01 sys = 10.12 CPU) @ 103.26/s (n=1045)\n+#                    Rate       TiedCapture IO::CaptureOutput          SelectFH\n+# TiedCapture       103/s                --              -18%              -87%\n+# IO::CaptureOutput 127/s               23%                --              -84%\n+# SelectFH          808/s              683%              538%                --\n+#\n+## PerlIO::get_layers   == PerlIO::get_layers(*STDOUT)\n+## PerlIU::Util method  == *STDOUT->get_layers()\n+#                        Rate PerlIO::Util method  PerlIO::get_layers\n+# PerlIO::Util method 54405/s                  --                -38%\n+# PerlIO::get_layers  87672/s                 61%                  --\n+\n+##\n+# Capturing 100 x 1314 = 131400 characters\n+# Benchmark: running PerlIO, SelectFH, TiedCapture (PerlIO), TiedCapture (String)\n+#   for at least 10 CPU seconds...\n+#                        Rate TiedCapture (String) TiedCapture (PerlIO) SelectFH PerlIO\n+# TiedCapture (String) 96.5/s                   --                 -76%     -88%   -88%\n+# TiedCapture (PerlIO)  407/s                 322%                   --     -48%   -48%\n+# SelectFH              787/s                 715%                  93%       --    -0%\n+# PerlIO                789/s                 717%                  94%       0%     --\n+#\n+# comment: you can see effects of perltie overhead and repeated string concatenation here.\n+\n+##\n+#\n+# Capturing 100 x 1314 = 131400 characters\n+# Benchmark: running IO::CaptureOutput, SelectFH, TiedCapture (PerlIO), TiedCapture (String)\n+#   for at least 10 CPU seconds...\n+#                       Rate TiedCapture (String) IO::CaptureOutput TiedCapture (PerlIO) SelectFH\n+# TiedCapture (String) 109/s                   --               -4%                 -72%     -84%\n+# IO::CaptureOutput    114/s                   4%                --                 -70%     -84%\n+# TiedCapture (PerlIO) 384/s                 253%              237%                   --     -45%\n+# SelectFH             693/s                 536%              509%                  80%       --\ndiff --git a/t/t9504/test_capture_implementations.pl b/t/t9504/test_capture_implementations.pl\nnew file mode 100755\nindex 0000000..86796ac\n--- /dev/null\n+++ b/t/t9504/test_capture_implementations.pl\n@@ -0,0 +1,85 @@\n+#!/usr/bin/perl\n+use lib (split(/:/, $ENV{GITPERLLIB}));\n+\n+use warnings;\n+use strict;\n+\n+use File::Spec;\n+\n+use Test::More;\n+\n+# test source version\n+#use if defined($ENV{TEST_DIRECTORY}),\n+#\tlib => \"$ENV{TEST_DIRECTORY}/../gitweb/lib\";\n+sub __DIR__ () {\n+\tFile::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);\n+}\n+use lib __DIR__.\"/../../gitweb/lib\";\n+\n+# ....................................................................\n+\n+# Load modules\n+my @modules =\n+\tmap { \"GitwebCache::Capture::$_\" }\n+\tqw(SelectFH TiedCapture);\n+require_ok($_) foreach @modules;\n+if (eval { require PerlIO::Util; 1 }) {\n+\trequire_ok('GitwebCache::Capture::PerlIO');\n+\tunshift @modules, 'GitwebCache::Capture::PerlIO';\n+}\n+\n+# Test setting up capture\n+#\n+my @captures = map { new_ok($_ => []) } @modules;\n+push @captures, new_ok('GitwebCache::Capture::TiedCapture' => ['TiedCapture::PerlIO']);\n+isa_ok($_, 'GitwebCache::Capture', ref($_)) foreach @captures;\n+\n+# Test capturing\n+#\n+diag('Should not print anything except test results and diagnostic');\n+\n+my $test_data;\n+my @captured_output;\n+sub capture {\n+\tmy ($class, $mode) = @_;\n+\n+\t$class->start();\n+\tbinmode select(), $mode if defined($mode);\n+\tprint $test_data;\n+\treturn $class->stop();\n+}\n+sub test_captures {\n+\tmy $mode = shift;\n+\n+\t@captured_output = map { capture($_, $mode); } @captures;\n+\tif ($mode eq ':utf8') {\n+\t\tutf8::decode($_) foreach @captured_output;\n+\t}\n+\tfor (my $i = 0; $i < @captures; $i++) {\n+\t\tmy $name = ref($captures[$i]);\n+\t\t$name .= \" ($captures[$i]->{'tie_class'})\"\n+\t\t\tif $captures[$i]->{'tie_class'};\n+\t\tmy $output = $captured_output[$i];\n+\t\tis($output, $test_data, \"$name captures $mode data\");\n+\t}\n+}\n+\n+\n+binmode STDOUT, ':utf8';\n+$test_data = 'Zażółć gęsią jaźń';\n+utf8::decode($test_data);\n+#diag(\"\\$test_data = $test_data (decoded)\\n\");\n+ok(utf8::is_utf8($test_data), '$test_data is utf8  (utf8::is_utf8)');\n+ok(utf8::valid($test_data),   '$test_data is valid (utf8::valid)');\n+test_captures(':utf8');\n+\n+$test_data = '|\\x{fe}\\x{ff}|\\x{9F}|\\000|'; # invalid utf-8\n+ok(!utf8::is_utf8($test_data), '$test_data is not utf8 (utf8::is_utf8)');\n+ok(utf8::valid($test_data),    '$test_data is valid    (utf8::valid)');\n+test_captures(':raw');\n+\n+done_testing();\n+\n+# Local Variables:\n+# encoding: utf-8\n+# End:\n-- \n1.7.3\n"},{"id":"152808","messageId":"AANLkTinPCOfknoN4aO_EdPwKorRiM6NU6ep0z_nnNug8@mail.gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-4-git-send-email-jnareb@gmail.com","subject":"Re: [PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Thomas Adam","fromEmail":"thomas@xteddy.org","sentAt":"2010-10-06T22:41:11Z","receivedAt":"2010-10-06T22:41:11Z","isPatch":false,"sender":{"key":"thomas@xteddy.org","avatar":"https://gravatar.com/avatar/e7256db4738e501e5d2e84f00bb0bd99503165729573848a03330301fc2adc4a?d=mp&s=160"},"body":"On 6 October 2010 23:01, Jakub Narebski <jnareb@gmail.com> wrote:\n> +# creates get_depth() and set_depth($depth) etc. methods\n> +foreach my $i (qw(depth root namespace)) {\n> +       my $field = $i;\n> +       no strict 'refs';\n\nFor each item, you'll set \"no strict refs\"?  This might be better off\noutside the loop.  It's still scoped appropriately inside the\nsubroutine.\n\n> +       my $file = $self->path_to_key($key);\n> +       return undef unless (defined $file && -f $file);\n\nPBP (Perl Best Practises) will tell you that explicitly returning\nundef is discouraged -- \"undef\" should be reserved for those errors\nyou cannot handle, not ones you don't want to.\n\n> +\n> +       # Fast slurp, adapted from File::Slurp::read, with unnecessary options removed\n> +       # via CHI::Driver::File (from CHI-0.33)\n> +       my $buf = '';\n> +       open my $read_fh, '<', $file\n> +               or return undef;\n\nDitto.\n\n> +       binmode $read_fh, ':raw';\n> +\n> +       my $size_left = -s $read_fh;\n> +\n> +       while ($size_left > 0) {\n> +               my $read_cnt = sysread($read_fh, $buf, $size_left, length($buf));\n> +               return undef unless defined $read_cnt;\n\nDitto.\n\n> +               last if $read_cnt == 0;\n> +               $size_left -= $read_cnt;\n> +               #last if $size_left <= 0;\n> +       }\n> +\n> +       close $read_fh\n> +               or die \"Couldn't close file '$file' opened for reading: $!\";\n> +       return $buf;\n> +}\n\n\"use Carp;\" would be more useful here, and hence croak() and confess().\n\n> +sub store {\n> +       my ($self, $key, $data) = @_;\n> +\n> +       my $dir;\n> +       my $file = $self->path_to_key($key, \\$dir);\n> +       return undef unless (defined $file && defined $dir);\n\nSee above.\n\n> +       # ensure that directory leading to cache file exists\n> +       if (!-d $dir) {\n> +               eval { mkpath($dir, 0, 0777); 1 }\n> +                       or die \"Couldn't create leading directory '$dir' (mkpath): $!\";\n> +       }\n\nWhy is this eval()ed?  It will still return false and set $! appropriately.\n\n> +       # generate a temporary file\n> +       my ($temp_fh, $tempname) = $self->_tempfile_to_path($file, $dir);\n> +       chmod 0666, $tempname\n> +               or warn \"Couldn't change permissions to 0666 / -rw-rw-rw- for '$tempname': $!\";\n> +\n> +       # Fast spew, adapted from File::Slurp::write, with unnecessary options removed\n> +       # via CHI::Driver::File (from CHI-0.33)\n> +       my $write_fh = $temp_fh;\n> +       binmode $write_fh, ':raw';\n> +\n> +       my $size_left = length($data);\n> +       my $offset = 0;\n> +\n> +       while ($size_left > 0) {\n> +               my $write_cnt = syswrite($write_fh, $data, $size_left, $offset);\n> +               return undef unless defined $write_cnt;\n\nAgain, with the undef.\n\n> +               $size_left -= $write_cnt;\n> +               $offset += $write_cnt; # == length($data);\n> +       }\n> +\n> +       close $temp_fh\n> +               or die \"Couldn't close temporary file '$tempname' opened for writing: $!\";\n> +       rename($tempname, $file)\n> +               or die \"Couldn't rename temporary file '$tempname' to '$file': $!\";\n> +}\n> +\n> +# get size of an element associated with the $key (not the size of whole cache)\n> +sub get_size {\n> +       my ($self, $key) = @_;\n> +\n> +       my $path = $self->path_to_key($key)\n> +               or return undef;\n\nAgain with the undef\n\n> +       if (-f $path) {\n> +               return -s $path;\n> +       }\n> +       return 0;\n> +}\n> +\n> +# ......................................................................\n> +# interface methods\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> +       my ($self, $key) = @_;\n> +\n> +       my $file = $self->path_to_key($key)\n> +               or return undef;\n> +       return undef unless -f $file;\n\nAgain with the undef.\n\n> +       unlink($file)\n> +               or die \"Couldn't remove file '$file': $!\";\n> +}\n> +\n> +# Getting and setting\n> +\n> +# $cache->set($key, $data);\n> +#\n> +# Associates $data with $key in the cache, overwriting any existing entry.\n> +# Returns $data.\n> +sub set {\n> +       my ($self, $key, $data) = @_;\n> +\n> +       return unless (defined $key && defined $data);\n\nreturn what?\n\n> +       $self->store($key, $data);\n> +\n> +       return $data;\n> +}\n> +\n> +# $data = $cache->get($key);\n> +#\n> +# Returns the data associated with $key.  If $key does not exist\n> +# or has expired, returns undef.\n> +sub get {\n> +       my ($self, $key) = @_;\n> +\n> +       my $data = $self->fetch($key)\n> +               or return undef;\n> +\n> +       return $data;\n> +}\n> +\n> +# $data = $cache->compute($key, $code);\n> +#\n> +# Combines the get and set operations in a single call.  Attempts to\n> +# get $key; if successful, returns the value.  Otherwise, calls $code\n> +# and uses the return value as the new value for $key, which is then\n> +# returned.\n> +sub compute {\n> +       my ($self, $key, $code) = @_;\n> +\n> +       my $data = $self->get($key);\n> +       if (!defined $data) {\n> +               $data = $code->($self, $key);\n> +               $self->set($key, $data);\n> +       }\n\nCan you guarantee $code here?\n\nunless( defined $code and ref $code eq 'CODE' )\n{\n        ....\n}\n\nWouldn't it be easier to eval{} this and checj $@?\n\n[...]\n\n-- Thomas Adam\n"},{"id":"152809","messageId":"AANLkTik1RzGno+TsZe5OZSU7SeJOhDKWFCZYmrFhH6SZ@mail.gmail.com","threadId":"25371","inReplyTo":"AANLkTinPCOfknoN4aO_EdPwKorRiM6NU6ep0z_nnNug8@mail.gmail.com","subject":"Re: [PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2010-10-06T22:44:31Z","receivedAt":"2010-10-06T22:44:31Z","isPatch":false,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"On Wed, Oct 6, 2010 at 22:41, Thomas Adam <thomas@xteddy.org> wrote:\n> On 6 October 2010 23:01, Jakub Narebski <jnareb@gmail.com> wrote:\n>> +# creates get_depth() and set_depth($depth) etc. methods\n>> +foreach my $i (qw(depth root namespace)) {\n>> +       my $field = $i;\n>> +       no strict 'refs';\n>\n> For each item, you'll set \"no strict refs\"?  This might be better off\n> outside the loop.  It's still scoped appropriately inside the\n> subroutine.\n>\n>> +       my $file = $self->path_to_key($key);\n>> +       return undef unless (defined $file && -f $file);\n>\n> PBP (Perl Best Practises) will tell you that explicitly returning\n> undef is discouraged -- \"undef\" should be reserved for those errors\n> you cannot handle, not ones you don't want to.\n\n[...]\n\n>> +       return unless (defined $key && defined $data);\n>\n> return what?\n\nfalse. You're right that \"return undef;\" is bad style, but \"return;\"\nis what you should use instead.\n\nThen you'll get undef in scalar context, and an empty list in list\ncontext. With \"return undef\" you'll always get an undef, so:\n\n    sub blah { retur undef }\n    my (@foo) = blah();\n\nWill make @foo = (undef), which'll make it evaluate to true in scalar\ncontext since there's an item in the array, and give you a useless\narray item.\n"},{"id":"152810","messageId":"AANLkTi=RFu5PzBfbF0qVjFnSHnf5GGedkgqKAA4v4nVG@mail.gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-9-git-send-email-jnareb@gmail.com","subject":"Re: [PATCHv5 08/17] gitweb: Add optional output caching","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2010-10-06T22:46:27Z","receivedAt":"2010-10-06T22:46:27Z","isPatch":false,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"On Wed, Oct 6, 2010 at 22:01, Jakub Narebski <jnareb@gmail.com> wrote:\n\n> +               $cache ||= 'GitwebCache::SimpleFileCache';\n> +               eval \"require $cache\";\n\nJust:\n\n    eval { require $cache };\n\nInstead?\n"},{"id":"152812","messageId":"AANLkTi=spE+RGdwz5f4xOTo-ijCNWF9fhZ06aA-tvu7j@mail.gmail.com","threadId":"25371","inReplyTo":"AANLkTik1RzGno+TsZe5OZSU7SeJOhDKWFCZYmrFhH6SZ@mail.gmail.com","subject":"Re: [PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Thomas Adam","fromEmail":"thomas@xteddy.org","sentAt":"2010-10-06T22:46:29Z","receivedAt":"2010-10-06T22:46:29Z","isPatch":false,"sender":{"key":"thomas@xteddy.org","avatar":"https://gravatar.com/avatar/e7256db4738e501e5d2e84f00bb0bd99503165729573848a03330301fc2adc4a?d=mp&s=160"},"body":"On 6 October 2010 23:44, Ævar Arnfjörð Bjarmason <avarab@gmail.com> wrote:\n> On Wed, Oct 6, 2010 at 22:41, Thomas Adam <thomas@xteddy.org> wrote:\n>> On 6 October 2010 23:01, Jakub Narebski <jnareb@gmail.com> wrote:\n>>> +# creates get_depth() and set_depth($depth) etc. methods\n>>> +foreach my $i (qw(depth root namespace)) {\n>>> +       my $field = $i;\n>>> +       no strict 'refs';\n>>\n>> For each item, you'll set \"no strict refs\"?  This might be better off\n>> outside the loop.  It's still scoped appropriately inside the\n>> subroutine.\n>>\n>>> +       my $file = $self->path_to_key($key);\n>>> +       return undef unless (defined $file && -f $file);\n>>\n>> PBP (Perl Best Practises) will tell you that explicitly returning\n>> undef is discouraged -- \"undef\" should be reserved for those errors\n>> you cannot handle, not ones you don't want to.\n>\n> [...]\n>\n>>> +       return unless (defined $key && defined $data);\n>>\n>> return what?\n>\n> false. You're right that \"return undef;\" is bad style, but \"return;\"\n> is what you should use instead.\n\nI didn't mean that.  :)  It was a question to ensure this is what was\nwanted, and not another undef.  I am well aware of the differences.\n\n-- Thomas Adam\n"},{"id":"152813","messageId":"AANLkTiki1io9quVPVzmZreOOzjpW3RfRcj-12995YL6s@mail.gmail.com","threadId":"25371","inReplyTo":"AANLkTi=spE+RGdwz5f4xOTo-ijCNWF9fhZ06aA-tvu7j@mail.gmail.com","subject":"Re: [PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2010-10-06T22:47:45Z","receivedAt":"2010-10-06T22:47:45Z","isPatch":false,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"On Wed, Oct 6, 2010 at 22:46, Thomas Adam <thomas@xteddy.org> wrote:\n> On 6 October 2010 23:44, Ævar Arnfjörð Bjarmason <avarab@gmail.com> wrote:\n>> On Wed, Oct 6, 2010 at 22:41, Thomas Adam <thomas@xteddy.org> wrote:\n>>> On 6 October 2010 23:01, Jakub Narebski <jnareb@gmail.com> wrote:\n>>>> +# creates get_depth() and set_depth($depth) etc. methods\n>>>> +foreach my $i (qw(depth root namespace)) {\n>>>> +       my $field = $i;\n>>>> +       no strict 'refs';\n>>>\n>>> For each item, you'll set \"no strict refs\"?  This might be better off\n>>> outside the loop.  It's still scoped appropriately inside the\n>>> subroutine.\n>>>\n>>>> +       my $file = $self->path_to_key($key);\n>>>> +       return undef unless (defined $file && -f $file);\n>>>\n>>> PBP (Perl Best Practises) will tell you that explicitly returning\n>>> undef is discouraged -- \"undef\" should be reserved for those errors\n>>> you cannot handle, not ones you don't want to.\n>>\n>> [...]\n>>\n>>>> +       return unless (defined $key && defined $data);\n>>>\n>>> return what?\n>>\n>> false. You're right that \"return undef;\" is bad style, but \"return;\"\n>> is what you should use instead.\n>\n> I didn't mean that.  :)  It was a question to ensure this is what was\n> wanted, and not another undef.  I am well aware of the differences.\n\nAh right, sorry about the noise then.\n"},{"id":"152815","messageId":"AANLkTinO+wo8z_peiDragtZ1Y8rJZ1UziMXh2YqSJaLD@mail.gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-7-git-send-email-jnareb@gmail.com","subject":"Re: [PATCHv5 06/17] gitweb/lib - Simple select(FH) based output capture","fromName":"Thomas Adam","fromEmail":"thomas@xteddy.org","sentAt":"2010-10-06T22:52:29Z","receivedAt":"2010-10-06T22:52:29Z","isPatch":false,"sender":{"key":"thomas@xteddy.org","avatar":"https://gravatar.com/avatar/e7256db4738e501e5d2e84f00bb0bd99503165729573848a03330301fc2adc4a?d=mp&s=160"},"body":"Hi --\n\nOn 6 October 2010 23:01, Jakub Narebski <jnareb@gmail.com> wrote:\n> +sub capture {\n> +       my ($self, $code) = @_;\n> +\n> +       $self->start();\n> +       $code->();\n\nunless( defined $code and $code eq 'CODE' )\n{\n       # Error?\n        ....\n}\n\n> +       return $self->stop();\n> +}\n> +\n> +# Wrap caching data; capture only STDOUT\n> +sub capture_block (&) {\n\nThis is prototyped deliberately?\n\n-- Thomas Adam\n"},{"id":"152816","messageId":"AANLkTimpXJT-R5vgdS4SvpuvhVOL-HCJxELMbfRcuqx5@mail.gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-19-git-send-email-jnareb@gmail.com","subject":"Re: [RFC/PATCHv5 18/17] gitweb/lib - Add clear() and size() methods to caching interface","fromName":"Thomas Adam","fromEmail":"thomas@xteddy.org","sentAt":"2010-10-06T22:56:42Z","receivedAt":"2010-10-06T22:56:42Z","isPatch":false,"sender":{"key":"thomas@xteddy.org","avatar":"https://gravatar.com/avatar/e7256db4738e501e5d2e84f00bb0bd99503165729573848a03330301fc2adc4a?d=mp&s=160"},"body":"On 6 October 2010 23:02, Jakub Narebski <jnareb@gmail.com> wrote:\n> Add ->size() method, which following Cache::Cache interface returns\n> estimated total size of all entries in whole cache (in the namsepace\n> assiciated with give cache instance).  Note that ->get_size($key)\n> returns size of a single entry!\n>\n> Add ->clear() method, which removes all entries from the namespace\n> associated with given cache instance.  For safety it requires\n> namespace to be set to true value, which means that it cannot be\n> empty; therefore default namespace is changed to 'gitweb'.\n>\n> The ->clear() method should be fairly safe, because it first renames\n> directory (which should be atomic), and only then removes it\n> (following code from CGI::Driver::File).\n>\n> Signed-off-by: Jakub Narebski <jnareb@gmail.com>\n> ---\n> The only difference from previous version is that a comment about some\n> method was moved to earlier commit that introduced it, where it\n> belongs.\n>\n> This patch (implementing \"backend\") together with next patch\n> (implementing \"interface\") are meant to provide nice web interface to\n> safely cleaning (pruning) cache, as described in comments and\n> documentation (how to do this manually).\n>\n>  gitweb/lib/GitwebCache/SimpleFileCache.pm |   48 ++++++++++++++++++++++++++--\n>  t/t9503/test_cache_interface.pl           |    1 -\n>  2 files changed, 44 insertions(+), 5 deletions(-)\n>\n> diff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n> index 2a3d9cf..8f172cb 100644\n> --- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n> +++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n> @@ -20,8 +20,9 @@ package GitwebCache::SimpleFileCache;\n>  use strict;\n>  use warnings;\n>\n> -use File::Path qw(mkpath);\n> -use File::Temp qw(tempfile);\n> +use File::Path qw(mkpath rmtree);\n> +use File::Temp qw(tempfile mktemp);\n> +use File::Find qw(find);\n>  use Digest::MD5 qw(md5_hex);\n>\n>  # by default, the cache nests all entries on the filesystem single\n> @@ -37,7 +38,7 @@ our $DEFAULT_CACHE_ROOT = \"cache\";\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> +our $DEFAULT_NAMESPACE = \"gitweb\";\n>\n>  # ......................................................................\n>  # constructor\n> @@ -334,7 +335,7 @@ sub get_size {\n>  }\n>\n>  # ......................................................................\n> -# interface methods\n> +# interface methods dealing with single item\n>\n>  # Removing and expiring\n>\n> @@ -427,6 +428,45 @@ sub compute {\n>        return $data;\n>  }\n>\n> +# ......................................................................\n> +# interface methods dealing with whole namespace\n> +\n> +# $cache->clear();\n> +#\n> +# Remove all entries from the namespace.\n> +# Namespace must be defined and not empty.\n> +sub clear {\n> +       my $self = shift;\n> +\n> +       return unless $self->get_namespace();\n> +\n> +       my $namespace_dir = $self->path_to_namespace();\n> +       return if !-d $namespace_dir;\n> +\n> +       my $renamed_dir = mktemp($namespace_dir . '.XXXX');\n> +       rename($namespace_dir, $renamed_dir);\n> +       rmtree($renamed_dir);\n> +       die \"Couldn't remove '$renamed_dir' directory\"\n> +               if -d $renamed_dir;\n> +}\n> +\n> +# $size = $cache->size();\n> +#\n> +# Size of whole names (or whole cache if namespace empty)\n> +sub size {\n> +       my $self = shift;\n> +\n> +       my $namespace_dir = $self->path_to_namespace();\n> +       return if !-d $namespace_dir;\n> +\n> +       my $total_size = 0;\n> +       my $add_size = sub { $total_size += -s $_ };\n> +\n> +       File::Find::find({ wanted => $add_size, no_chdir => 1 }, $namespace_dir);\n> +\n> +       return $total_size;\n> +}\n> +\n>  1;\n\nThis looks good to me.\n\nAcked-by:  Thomas Adam <thomas@xteddy.org>\n\n-- Thomas Adam\n"},{"id":"152817","messageId":"AANLkTi=DYHQLftyafoijzagTLmy76Uxsa0fiZ1ZO2wc=@mail.gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-4-git-send-email-jnareb@gmail.com","subject":"Re: [PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2010-10-06T22:57:41Z","receivedAt":"2010-10-06T22:57:41Z","isPatch":false,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"On Wed, Oct 6, 2010 at 22:01, Jakub Narebski <jnareb@gmail.com> wrote:\n\n> +sub new {\n> +       my ($proto, $p_options_hash_ref) = @_;\n> +\n> +       my $class = ref($proto) || $proto;\n> +       my $self  = {};\n> +       $self = bless($self, $class);\n\nYou use:\n\n    my $class = ref($proto) || $proto;\n\nThroughout the new class definitions. Presumably that's just\ncopy/pasted from some old docs and you don't actually want to support:\n\n    my $obj = Class->new;\n    my $obj2 = $obj->new;\n\nIt's better just to:\n\n    sub new {\n        my ($class, $options) = @_:\n        my $self = bless {}, $class;\n\nUnless there's some reason for the ref($proto) that I've missed.\n"},{"id":"152818","messageId":"201010070100.33478.jnareb@gmail.com","threadId":"25371","inReplyTo":"AANLkTinPCOfknoN4aO_EdPwKorRiM6NU6ep0z_nnNug8@mail.gmail.com","subject":"Re: [PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T23:00:31Z","receivedAt":"2010-10-06T23:00:31Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Thank you very much for those comments on code.\n\nOn Thu, 7 Oct 2010, Thomas Adam wrote:\n> On 6 October 2010 23:01, Jakub Narebski <jnareb@gmail.com> wrote:\n\n> > +# creates get_depth() and set_depth($depth) etc. methods\n> > +foreach my $i (qw(depth root namespace)) {\n> > +       my $field = $i;\n> > +       no strict 'refs';\n> \n> For each item, you'll set \"no strict refs\"?  This might be better off\n> outside the loop.  It's still scoped appropriately inside the\n> subroutine.\n\nOn the other hand this way scope where \"no strict 'refs';\" is active\nis limited... but I guess having \"no strict 'refs';\" outside loop would\nbe better.\n\n> \n> > +       my $file = $self->path_to_key($key);\n> > +       return undef unless (defined $file && -f $file);\n> \n> PBP (Perl Best Practises) will tell you that explicitly returning\n> undef is discouraged -- \"undef\" should be reserved for those errors\n> you cannot handle, not ones you don't want to.\n\nWell, Perl Best Practices are practices; sometimes there is good reason\nto not take them into account (though probably not in this case).\n\nExplicitly returning undef is discouraged because in list context the\nreturned undef (or rather 1-element list with 'undef' as sole element)\nis true-ish.\n\nOn the other hand\n\n> > +# $cache->set($key, $data);\n> > +#\n> > +# Associates $data with $key in the cache, overwriting any existing entry.\n> > +# Returns $data.\n> > +sub set {\n> > +       my ($self, $key, $data) = @_;\n> > +\n> > +       return unless (defined $key && defined $data);\n> \n> return what?\n \nas you can see 'return' statement with no value looks rather cryptic\nwith statement modifier (conditional).\n\nI should really have run gitweb, caching modules and tests through \nperlcritic...\n\n> > +               last if $read_cnt == 0;\n> > +               $size_left -= $read_cnt;\n> > +               #last if $size_left <= 0;\n> > +       }\n> > +\n> > +       close $read_fh\n> > +               or die \"Couldn't close file '$file' opened for reading: $!\";\n> > +       return $buf;\n> > +}\n> \n> \"use Carp;\" would be more useful here, and hence croak() and confess().\n\nFor a web application we usually do not want to have too detailed error\nmessage present to client (to web browser) to avoid leaking of sensitive\ninformation.\n \n> > +       # ensure that directory leading to cache file exists\n> > +       if (!-d $dir) {\n> > +               eval { mkpath($dir, 0, 0777); 1 }\n> > +                       or die \"Couldn't create leading directory '$dir' (mkpath): $!\";\n> > +       }\n> \n> Why is this eval()ed?  It will still return false and set $! appropriately.\n\nIIRC mkpath *dies on error*, rather than returning false.  For better\nerror handling we would need to use make_path, but File::Path 2.0+\nis in [stable] core only since Perl 5.10.\n \n[...]\n> > +# $data = $cache->compute($key, $code);\n> > +#\n> > +# Combines the get and set operations in a single call.  Attempts to\n> > +# get $key; if successful, returns the value.  Otherwise, calls $code\n> > +# and uses the return value as the new value for $key, which is then\n> > +# returned.\n> > +sub compute {\n> > +       my ($self, $key, $code) = @_;\n> > +\n> > +       my $data = $self->get($key);\n> > +       if (!defined $data) {\n> > +               $data = $code->($self, $key);\n> > +               $self->set($key, $data);\n> > +       }\n> \n> Can you guarantee $code here?\n\nI don't want to code too defensively, but perhaps check for this is in\norder... though what we should do if $code is not code reference?\n\n> \n> unless( defined $code and ref $code eq 'CODE' )\n\n\"ref($code) eq 'CODE'\" would be enough; 'undef' is not reference, and\nref(undef) returns \"\".\n\n> {\n>         ....\n> }\n> \n> Wouldn't it be easier to eval{} this and check $@?\n\nSo the answer is no.\n\n\nThanks again for your comments.\n-- \nJakub Narebski\nPoland\n"},{"id":"152820","messageId":"AANLkTikVumyaJoM3=Nbf9igVheLvMB5tcc5+E+wSkb40@mail.gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-7-git-send-email-jnareb@gmail.com","subject":"Re: [PATCHv5 06/17] gitweb/lib - Simple select(FH) based output capture","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2010-10-06T23:03:16Z","receivedAt":"2010-10-06T23:03:16Z","isPatch":false,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"On Wed, Oct 6, 2010 at 22:01, Jakub Narebski <jnareb@gmail.com> wrote:\n\n> * The most important issue is that I/O \"layers\" (PerlIO), like ':utf8'\n>  or ':raw', are *already applied* to the output that is captured.\n>  This means that captured output is *always* in binary (':raw') mode.\n>  In Perl 6 language it means that data returned by capturing engine\n>  is an equivalent of Buf, a collection of bytes, whether Buf or Str\n>  (a colection of logical characters) is printed.\n\n> +       # note: this does not cover all cases\n> +       binmode select(), ':utf8'\n> +               if ((PerlIO::get_layers($self->{'oldfh'}))[-1] eq 'utf8');\n\nI'm not sure but maybe we want to use \":Encoding(UTF-8)\" everywhere in\nthis series where you've used \":utf8\". I.e. use the Encoding UTF-8\nlayer instead of the internal utf8 layer.\n\nIt's more strict, see perldoc Encode's \"UTF-8 vs. utf8 vs. UTF8\". But\nmaybe we don't care.\n"},{"id":"152822","messageId":"201010070106.30864.jnareb@gmail.com","threadId":"25371","inReplyTo":"AANLkTi=RFu5PzBfbF0qVjFnSHnf5GGedkgqKAA4v4nVG@mail.gmail.com","subject":"Re: [PATCHv5 08/17] gitweb: Add optional output caching","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T23:06:30Z","receivedAt":"2010-10-06T23:06:30Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Thu, 7 Oct 2010, Ævar Arnfjörð Bjarmason wrote:\n> On Wed, Oct 6, 2010 at 22:01, Jakub Narebski <jnareb@gmail.com> wrote:\n> \n> > +               $cache ||= 'GitwebCache::SimpleFileCache';\n> > +               eval \"require $cache\";\n> \n> Just:\n> \n>     eval { require $cache };\n> \n> Instead?\n\nWouldn't work correctly.  We want to use 'require BAREWORD' version,\nwhere BAREWORD is name of module, e.g. GitwebCache::SimpleFileCache,\nand which makes Perl to search for GitwebCache/SimpleFileCache.pm\nin @INC.\n\nThe 'require STRING' version loads file given by a *path*, and it\ndoesn't do library lookup.\n\nFrom `perldoc -f require`:\n\n   But if you try this:\n\n      $class = 'Foo::Bar';\n      require $class;      # $class is not a bareword\n      #or\n      require \"Foo::Bar\";  # not a bareword because of the \"\"\n\n   The require function will look for the \"Foo::Bar\" file in the @INC array\n   and will complain about not finding \"Foo::Bar\" there.  In this case you\n   can do:\n\n      eval \"require $class\";\n\n\nWell, we could insert hooks into @INC, but I don't think we want to use\nsuch hack.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"152824","messageId":"AANLkTikVmHgk=jLCs11tJ7yhUL4H2gnM_pKkXOJSxYq7@mail.gmail.com","threadId":"25371","inReplyTo":"201010070100.33478.jnareb@gmail.com","subject":"Re: [PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Thomas Adam","fromEmail":"thomas@xteddy.org","sentAt":"2010-10-06T23:12:43Z","receivedAt":"2010-10-06T23:12:43Z","isPatch":false,"sender":{"key":"thomas@xteddy.org","avatar":"https://gravatar.com/avatar/e7256db4738e501e5d2e84f00bb0bd99503165729573848a03330301fc2adc4a?d=mp&s=160"},"body":"Hi --\n\nOn 7 October 2010 00:00, Jakub Narebski <jnareb@gmail.com> wrote:\n> Thank you very much for those comments on code.\n\nSure.\n\n> On the other hand this way scope where \"no strict 'refs';\" is active\n> is limited... but I guess having \"no strict 'refs';\" outside loop would\n> be better.\n\nDefinitely.\n\n>>\n>> > +       my $file = $self->path_to_key($key);\n>> > +       return undef unless (defined $file && -f $file);\n>>\n>> PBP (Perl Best Practises) will tell you that explicitly returning\n>> undef is discouraged -- \"undef\" should be reserved for those errors\n>> you cannot handle, not ones you don't want to.\n>\n> Well, Perl Best Practices are practices; sometimes there is good reason\n> to not take them into account (though probably not in this case).\n\nDo not ever underestimate Conway's book, Jakub -- seriously.   It's\nnot a Bible, sure, but to ignore it or otherwise eschew its advise is\nto shoot yourself in the foot.   :)\n\n> I should really have run gitweb, caching modules and tests through\n> perlcritic...\n\nAnd your other patches.   :)\n\n>> > +               last if $read_cnt == 0;\n>> > +               $size_left -= $read_cnt;\n>> > +               #last if $size_left <= 0;\n>> > +       }\n>> > +\n>> > +       close $read_fh\n>> > +               or die \"Couldn't close file '$file' opened for reading: $!\";\n>> > +       return $buf;\n>> > +}\n>>\n>> \"use Carp;\" would be more useful here, and hence croak() and confess().\n>\n> For a web application we usually do not want to have too detailed error\n> message present to client (to web browser) to avoid leaking of sensitive\n> information.\n\nUnless we use fatalstobrowser, it will still end up in Apache's error\nlog.  I don't see a problem here.\n\n>> > +       # ensure that directory leading to cache file exists\n>> > +       if (!-d $dir) {\n>> > +               eval { mkpath($dir, 0, 0777); 1 }\n>> > +                       or die \"Couldn't create leading directory '$dir' (mkpath): $!\";\n>> > +       }\n>>\n>> Why is this eval()ed?  It will still return false and set $! appropriately.\n>\n> IIRC mkpath *dies on error*, rather than returning false.  For better\n> error handling we would need to use make_path, but File::Path 2.0+\n> is in [stable] core only since Perl 5.10.\n\nUmm, maybe.   I've not verified this, but we shouldn't rely on this either.\n\n> I don't want to code too defensively, but perhaps check for this is in\n> order... though what we should do if $code is not code reference?\n\nconfess() ahem, or otherwise die() since it's a complete fail here.\nAppropriate logging is necessary here.\n\n>>\n>> unless( defined $code and ref $code eq 'CODE' )\n>\n> \"ref($code) eq 'CODE'\" would be enough; 'undef' is not reference, and\n> ref(undef) returns \"\".\n\nSure, but it still assumes $code is a reference, and there's no\nguarantee of that in the code path.\n\n>> {\n>>         ....\n>> }\n>>\n>> Wouldn't it be easier to eval{} this and check $@?\n>\n> So the answer is no.\n\nFantastic!   :)\n\n-- Thomas Adam\n"},{"id":"152825","messageId":"AANLkTi=Z6i-89mW8Gom3QQa8R1puV+wksNmh029GaUjS@mail.gmail.com","threadId":"25371","inReplyTo":"201010070106.30864.jnareb@gmail.com","subject":"Re: [PATCHv5 08/17] gitweb: Add optional output caching","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2010-10-06T23:16:10Z","receivedAt":"2010-10-06T23:16:10Z","isPatch":false,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"On Wed, Oct 6, 2010 at 23:06, Jakub Narebski <jnareb@gmail.com> wrote:\n> On Thu, 7 Oct 2010, Ævar Arnfjörð Bjarmason wrote:\n>> On Wed, Oct 6, 2010 at 22:01, Jakub Narebski <jnareb@gmail.com> wrote:\n>>\n>> > +               $cache ||= 'GitwebCache::SimpleFileCache';\n>> > +               eval \"require $cache\";\n>>\n>> Just:\n>>\n>>     eval { require $cache };\n>>\n>> Instead?\n>\n> Wouldn't work correctly.  We want to use 'require BAREWORD' version,\n> where BAREWORD is name of module, e.g. GitwebCache::SimpleFileCache,\n> and which makes Perl to search for GitwebCache/SimpleFileCache.pm\n> in @INC.\n\nAh, I've been using perl5i too much :)\n\n> Well, we could insert hooks into @INC, but I don't think we want to use\n> such hack.\n\nThere's also:\n\n    eval {\n         my $path = $cache;\n         $path =~ s[::][/]g;\n         $path .= \".pm\";\n         require $path;\n    };\n\nBut at that point it's probably easier just to use string eval. Unless there's\na performance issue (very unlikely).\n"},{"id":"152826","messageId":"201010070122.25355.jnareb@gmail.com","threadId":"25371","inReplyTo":"AANLkTinO+wo8z_peiDragtZ1Y8rJZ1UziMXh2YqSJaLD@mail.gmail.com","subject":"Re: [PATCHv5 06/17] gitweb/lib - Simple select(FH) based output capture","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T23:22:24Z","receivedAt":"2010-10-06T23:22:24Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Thomas Adam wrote:\n> On 6 October 2010 23:01, Jakub Narebski <jnareb@gmail.com> wrote:\n\n> > +sub capture {\n> > +       my ($self, $code) = @_;\n> > +\n> > +       $self->start();\n> > +       $code->();\n> \n> unless( defined $code and ref($code) eq 'CODE' )\n> {\n>        # Error?\n>         ....\n> }\n\nBut what to do if $code is *not* a code reference?  Well, except perhaps\nproviding better error message...\n\n> > +       return $self->stop();\n> > +}\n> > +\n> > +# Wrap caching data; capture only STDOUT\n> > +sub capture_block (&) {\n> \n> This is prototyped deliberately?\n\nYes, it is prototyped deliberately to be able to use it like this\n\n  my $data = capture_block {\n     ...\n  }\n\nsimilarly to the API used by Capture::Tiny (which is not in core, which\ncaptures also STDERR, and which do not provide capture_stop() equivalent\nfor die_error to be not captured and cached).\n\nOnly to explicitely discard prototype with '&capture_block($code)', when\nusing it in ->cache_output_* methods in GitwebCache::CacheOutput ;-)\n\n-- \nJakub Narebski\nPoland\n"},{"id":"152828","messageId":"201010070126.26242.jnareb@gmail.com","threadId":"25371","inReplyTo":"AANLkTikVumyaJoM3=Nbf9igVheLvMB5tcc5+E+wSkb40@mail.gmail.com","subject":"Re: [PATCHv5 06/17] gitweb/lib - Simple select(FH) based output capture","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T23:26:25Z","receivedAt":"2010-10-06T23:26:25Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Thu, 7 Oct 2010 01:03, Ævar Arnfjörð Bjarmason wrote:\n> On Wed, Oct 6, 2010 at 22:01, Jakub Narebski <jnareb@gmail.com> wrote:\n> \n> > * The most important issue is that I/O \"layers\" (PerlIO), like ':utf8'\n> >  or ':raw', are *already applied* to the output that is captured.\n> >  This means that captured output is *always* in binary (':raw') mode.\n> >  In Perl 6 language it means that data returned by capturing engine\n> >  is an equivalent of Buf, a collection of bytes, whether Buf or Str\n> >  (a colection of logical characters) is printed.\n> \n> > +       # note: this does not cover all cases\n> > +       binmode select(), ':utf8'\n> > +               if ((PerlIO::get_layers($self->{'oldfh'}))[-1] eq 'utf8');\n\nSidenote: I just realized that we can simply try to replay all but\nspecial layers from 'oldfh'.\n \n> I'm not sure but maybe we want to use \":Encoding(UTF-8)\" everywhere in\n> this series where you've used \":utf8\". I.e. use the Encoding UTF-8\n> layer instead of the internal utf8 layer.\n> \n> It's more strict, see perldoc Encode's \"UTF-8 vs. utf8 vs. UTF8\". But\n> maybe we don't care.\n\nI think for the time being we don't care. \n\nIn the future we could perhaps update gitweb to use ':encoding(UTF-8)'\nPerlIO layer rather than ':utf8' layer, and update capturing engine(s)\nappropriately.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"152830","messageId":"201010070132.12196.jnareb@gmail.com","threadId":"25371","inReplyTo":"AANLkTikVmHgk=jLCs11tJ7yhUL4H2gnM_pKkXOJSxYq7@mail.gmail.com","subject":"Re: [PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T23:32:11Z","receivedAt":"2010-10-06T23:32:11Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On 7 Oct 2010, Thomas Adam wrote:\n> On 7 October 2010 00:00, Jakub Narebski <jnareb@gmail.com> wrote:\n\n[...] \n>> I should really have run gitweb, caching modules and tests through\n>> perlcritic...\n> \n> And your other patches.   :)\n\nWell, you can't run (as far as I know) *patches* though perlcritic ;-)\n\n>>>> +               last if $read_cnt == 0;\n>>>> +               $size_left -= $read_cnt;\n>>>> +               #last if $size_left <= 0;\n>>>> +       }\n>>>> +\n>>>> +       close $read_fh\n>>>> +               or die \"Couldn't close file '$file' opened for reading: $!\";\n>>>> +       return $buf;\n>>>> +}\n>>>\n>>> \"use Carp;\" would be more useful here, and hence croak() and confess().\n>>\n>> For a web application we usually do not want to have too detailed error\n>> message present to client (to web browser) to avoid leaking of sensitive\n>> information.\n> \n> Unless we use fatalstobrowser, it will still end up in Apache's error\n> log.  I don't see a problem here.\n\nThe problem is that we do equivalent of fatalsToBrowser, i.e. set up\nerror handler with 'set_message(\\&handle_errors_html);'.\n \n>>> unless( defined $code and ref $code eq 'CODE' )\n>>\n>> \"ref($code) eq 'CODE'\" would be enough; 'undef' is not reference, and\n>> ref(undef) returns \"\".\n> \n> Sure, but it still assumes $code is a reference, and there's no\n> guarantee of that in the code path.\n\nYes, but if $code is not reference, then ref($code) evaluates to empty\nstring, and \"ref($code) eq 'CODE'\" comparison would fail... without any\nwarnings.  You don't need to check upfront for \"defined $code\"; it is\nthe only thing I wanted to nitpick.\n \n-- \nJakub Narebski\nPoland\n"},{"id":"152831","messageId":"201010070146.35858.jnareb@gmail.com","threadId":"25371","inReplyTo":"AANLkTi=DYHQLftyafoijzagTLmy76Uxsa0fiZ1ZO2wc=@mail.gmail.com","subject":"Re: [PATCHv5 03/17] gitweb/lib - Very simple file based cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-06T23:46:35Z","receivedAt":"2010-10-06T23:46:35Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Thu, 7 Oct 2010, Ævar Arnfjörð Bjarmason wrote:\n> On Wed, Oct 6, 2010 at 22:01, Jakub Narebski <jnareb@gmail.com> wrote:\n> \n> > +sub new {\n> > +       my ($proto, $p_options_hash_ref) = @_;\n> > +\n> > +       my $class = ref($proto) || $proto;\n> > +       my $self  = {};\n> > +       $self = bless($self, $class);\n> \n> You use:\n> \n>     my $class = ref($proto) || $proto;\n> \n> Throughout the new class definitions. Presumably that's just\n> copy/pasted from some old docs and you don't actually want to support:\n> \n>     my $obj = Class->new;\n>     my $obj2 = $obj->new;\n> \n> It's better just to:\n> \n>     sub new {\n>         my ($class, $options) = @_:\n>         my $self = bless {}, $class;\n> \n> Unless there's some reason for the ref($proto) that I've missed.\n\nYou are right, the 'my $class = ref($proto) || $proto;' dance is not\nnecessary; we would be always using Class->new().\n\nWill fix.\n-- \nJakub Narebski\nPoland\n"},{"id":"153172","messageId":"201010102232.08924.jnareb@gmail.com","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"[RFD] Possible improvements for output caching in gitweb","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-10-10T20:32:07Z","receivedAt":"2010-10-10T20:32:07Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"On Thu, 7 Oct 2010, Jakub Narebski wrote:\n\n> TODO list and areas of possible improvements would be send in separate\n> email.\n\nHere they are.  What do you think about them; which are needed, which \nones would be nice to have, which are not worth the trouble, and what's \nperhaps most important: which ones are missing.  Also in what order of \nimportance should they be worked upon.\n\nNote that in the list below there are deliberately missing improvements\nto the code (which were already commented on; thanks again).\n\n\nNew features and improvements related directly to cache or capture:\n\n* Ajax-y progress indicator (perhaps inside skeleton of page)\n  see 9982b6f (gitweb: Ajax-y \"Generating...\" page when regenerating\n  cache (WIP), 2010-01-24) on 'gitweb/cache-kernel' branch in the\n  http://repo.or.cz/w/git/jnareb-git.git repository.\n\n  Instead of relying on http-equiv refresh trick (which uses the fact\n  that web browsers render inclomplete page, and that refresh is done\n  only after page is received in full), use XMLHttpRequest to get\n  (re)generated version of the page, displaying progress info while at\n  it, and redraw page when data is received in full.  All of this only\n  when JavaScript is enabled, so I guess old trick should be kept as \n  fallback.\n\n  This of course assumes that progress info indicator is important...\n\n\n* error handler, like in CHI\n\n  Instead of using 'die <message>' and relying on CGI.pm and gitweb\n  catching the exception and displaying it (set_message from CGI.pm),\n  pass error handler (wrapped die_error) to cache constructor.  In the\n  case of CHI caching framework, there are 'on_get_error' and\n  'on_set_error' options.\n\n  In the original patches by J.H. subroutines from cache.pm used\n  die_error directly; this was possible only because the file was\n  loaded using \"do 'cache.pm';\" as a kind of mixin / role into gitweb\n  code.\n\n\n* $capture option to cache_output()\n\n  Currently in GitwebCache::OutputCache the capture engine used to\n  capture gitweb output to cache it is hardcoded; mind you, one would\n  need to change code only in two places to use different compatibile\n  caching engine, but still it would require changing code.  It would\n  be better to pass $capture as parameter to cache_output(), just like\n  $cache is.\n\n\n* POD documentation instead of comments + make doc\n\n  Currently gitweb caching modules  are documented (like original\n  cache.pm by J.H.) only in comments, and a bit in gitweb/README.\n  Though those modules are used only internally, it might be better\n  to use POD (perlpod) for documentation, like in Git.pm.\n\n  We migth also want to add 'doc' target in gitweb/Makefile, though\n  it might be difficult (see perl/Makefile.PL and generated perl.mak).\n\n\n* cache expire variation a la CHI\n\n  CHI (caching interface in Perl) supports 'expires_variance' parameter,\n  which according to documentation:\n\n   \"Controls the variable expiration feature, which allows items to\n    expire a little earlier than the stated expiration time to help\n    prevent cache miss stampedes.\n       \n    Value is between 0.0 and 1.0, with 0.0 meaning that items expire\n    exactly when specified (feature is disabled), and 1.0 meaning that\n    items might expire anytime from now til the stated expiration time.\n    The default is 0.0. A setting of 0.10 to 0.25 would introduce a\n    small amount of variation without interfering too much with intended\n    expiration times.\"\n\n  See http://p3rl.org/CHI (or CHI manpage, if you have it installed).\n  This feature is about *avoiding* cache miss stampede, while locking\n  is used to ensure that only one process is regenerating cache for\n  a given entry.\n\n\n* benchmarks for different caches under light and under heavy load;\n  profiling of gitweb with caching using Devel::NYTProf.\n  \n  The problem is to both prepare repositories, and to generate traffic\n  (or generate IO pressure) to represent real-life situation, where\n  supposedly gitweb is IO bound, rather than CPU bound.\n\n\n-------------------------------------------------------------\nBelow there are cache related improvements that require for \nGitwebCache::CacheOutput to be aware that it caches HTTP response,\nwhich consist of HTTP headers (text) separated by an empty line\nfrom a body of a request (which can be binary).\n\nThis can be done either by parsing response or a retrieved cache entry, \nor by storing headers and body separately, or by using some Perl data \nstructure (like for example the one used by PSGI) and storing it \nserialized (though serialization can affect performance).\n\n\n* X-Sendfile (or equivalent) support\n\n  Web server encountering such HTTP header will discard all output and\n  send the file specified by that header instead using web server\n  internals including all optimizations like caching-headers and\n  sendfile or mmap if configured.  For Apache it requires mod_xsendfile\n  module (https://tn123.org/mod_xsendfile/), lighttpd has it build in\n  (at least for FastCGI) but disabled by default; in Nginx similar\n  feature is called X-Accel-Redirect.\n\n  The idea is to use cache file for X-Sendfile contents; though this\n  might require storing headers and body of response separately, and\n  might be not much of speedup.\n\n\n* compressed cache entries (transfer-encoding) (?)\n\n  To reduce size taken by cache, and also reduce bandwidth taken by\n  serving gitweb requests, save body of response compressed.  Then,\n  if browser supports it, send compressed data with the HTTP header\n  'Transfer-Encoding:' set to appriopriate value.\n\n  The complication which, I think, we have to take into account is\n  that some (hopefully small amount) of web browsers and net downloaders\n  doesn't support transfer-encoding we plan to use (gzip or deflate).\n  Also gitweb should compress file which it knows to not compress well,\n  like already compressed snapshots (zip, tar.gz, tar.bz2) or images.\n\n  There was patch \" gitweb: Enable transparent compression for HTTP\n  output\" sent to git mailing list (using PerlIO::gzip), but in the\n  cached case we pay CPU cost only *once*.\n\n\n* Replace text/html with application/xml+xhtml in header\n  when reading from cache.\n\n  In the non-cached case, gitweb served page using either plain\n  'text/html' content type, or if web browser accepts it more advanced\n  'application/xhtml+xml' content type.  When caching is enabled, we\n  had to always use 'text/html', because web browser (e.g. lynx) might\n  not accept the other... but with cache being HTTP-aware, we can\n  replace 'text/html' with 'application/xhtml+xml' in 'Content-Type:'\n  HTTP header.\n\n\n* Expires-In / cache-age synchronized with cache lifetime,\n  Last-Modified synchronized with cache entry creation time.\n\n  Currently all cache entries have the same global (per cache instance)\n  expiration time.  The Expire header is not correlated with it.\n\n  There are two issues: when storing data in cache, we can set Expire\n  header (or cache-age pragma in Cache-Control header) to the expiration\n  time of cache entry and set Last-Modified to the time cache entry was\n  (re)generated (unless it is already set by gitweb).\n\n  The other issue is that some data doesn't change, ever.  We set expire\n  time to '+1d' (one day) in such case.  If we could mark those cache\n  entries as having longer / infinite lifetime to not regenerate them...\n\n\n* support for If-Modified-Since (external/browser caches)\n\n  When caching is enabled, we know when page was created.  We could\n  check for If-Modified-Since conditional request header, and return\n  '304 Not Modified' HTTP response if we would serve from the same\n  cache entry.  It would save bandwidth, and a bit of I/O.\n\n\n* ETag support - gitweb version + cache key hash, possibly also Range\n  requests.\n\n  We can compose strong ETag validator from cache key hash and gitweb\n  version string.  Maybe it would make possible to respond to Range\n  requests for resuming download of e.g. large snapshot file...\n\n  But it might be the fact that those features are unrelated...  \n\n\n----------------------------------------------------------------------\nBelow there are proposed gitweb improvements and features, which would \nalso improve caching support in gitweb:\n\n* Time::HiRes is in core + simplify progress indicator\n\n  Time::HiRes was first released with perl 5.007003 (5.7.3).  Because\n  gitweb requires at least Perl 5.8 for its Unicode / UTF-8 support,\n  we can assume that it is present.\n\n  This would simplify code in git_generating_data_html()\n\n\n* $per_request_config = 0/1 (default)/coderef\n  or just $per_request_config = coderef\n\n  If it would be possible to have config read only once in persistent\n  environments such as mod_perl and FastCGI, and not once per request,\n  it would improve performance when caching engine used has slow\n  initialization / creation time, like Moose-based CHI.\n\n  The basic idea is to put parts of config that change per request (like\n  e.g. gitosis or gitolite uses) in coderef in $per_request_config\n  variable; this coderef would be invoked once per config.  Example\n  configuration:\n\n    our $per_request_config = sub {\n       $ENV{GL_USER} = ($cgi && $cgi->remote_user) || \"gitweb\";\n    };\n\n\n* authenthication / authorization for admin stuff\n\n  Some kind of authenthications support would be needed for edit / write\n  support in gitweb, and also for controlling access to the cache\n  administration page.  We don't want anyone to be able to clear cache.\n\n  In the current proof-of-concept patch the cache administration page\n  is restricted to people accessing gitweb pages from localhost, or\n  running gitweb as a standalone script.\n\n\n* mod_perl handler\n\n  It might be possible with altering / modifying gitweb only slightly to\n  make it work *also* as native mod_perl handler, and not only via\n  ModPerl::Registry.  \n\n  This would make possible to initialize cache once per process\n  lifetime, and not once per request.\n\n-- \nJakub Narebski\nPoland\n"},{"id":"154350","messageId":"4CC4A66D.6050500@kernel.org","threadId":"25371","inReplyTo":"1286402526-13143-1-git-send-email-jnareb@gmail.com","subject":"Re: [PATCHv5 00/17] gitweb: Simple file based output caching","fromName":"J.H.","fromEmail":"warthog9@kernel.org","sentAt":"2010-10-24T21:34:37Z","receivedAt":"2010-10-24T21:34:37Z","isPatch":false,"sender":{"key":"warthog9@kernel.org","avatar":"https://avatars.githubusercontent.com/u/2334704?v=4"},"body":"> J.H., could you comment on how this series relates to the gitweb code\n> *currently* running on git.kernel.org wrt. code?  If possible, could you\n> try to compare performance of those two implementations, the one\n> presented here in this series, and the one used by git.kernel.org.\n\nHaven't been ignoring this, but been quite busy with a whole pile of\nother things (new hardware, kernel issues, patching exploits, generally\ngoing insane).\n\nI'm *HOPING* I can get around to this next week, I've also been working\non a couple of clean-ups in my own code which will likely land 'soon'.\n\n- John 'Warthog9' Hawley\n"}]}