{"thread":{"id":"25978","subject":"[PATCH 06/24] gitweb/lib - Simple output capture by redirecting STDOUT","startedAt":"2010-12-06T23:10:45Z","lastAt":"2010-12-06T23:11:09Z","messageCount":25,"participants":["Jakub Narebski"],"isPatch":true,"patchVersion":1,"patchTotal":24},"messages":[{"id":"157460","messageId":"1291677069-6559-1-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":null,"subject":"[PATCHv6/RFC 00/24] gitweb: Simple file based output caching","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:45Z","receivedAt":"2010-12-06T23:10:45Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"[I Cc-ed everybody who *might* be interested in this series.  I am\n sorry if it included somebody by mistake]\n\nThis 22+ patches long series (2 last patches are proof of concept) is\nintended as replacement (rewrite) of \"Gitweb caching v7\" series from\nJohn 'Warthog9' Hawley (J.H.):\n\n  http://thread.gmane.org/gmane.comp.version-control.git/160147\n\nThis is sixth version (6th release) of this series, and is available\nin the following repositories (links are to web interface):\n\n  http://repo.or.cz/w/git/jnareb-git.git\n  http://github.com/jnareb/git\n\nas 'gitweb/cache-kernel-v6' branch.  Earlier versions are available at\nhttp://repo.or.cz/w/git/jnareb-git.git as 'gitweb/cache-kernel-v5'\n(previous version) to 'gitweb/cache-kernel' (first version).\n\nPrevious version of this series was sent to git mailing list as:\n\n  [PATCHv5 00/17] gitweb: Simple file based output caching\n  Message-Id: <1286402526-13143-1-git-send-email-jnareb@gmail.com>\n  http://thread.gmane.org/gmane.comp.version-control.git/158313\n\nYou can find link to next to previous version, et cetera.\n\n\nThe main ideas in lifted from J.H. patches are the following\n(features in common with \"Gitweb caching v7\" series by John Hawley):\n\n* caching captured output of gitweb in flat files, without any\n  serialization (caching raw data)\n\n* using global (per-cache, not per-entry) expiration time, and\n  using difference between mtime of cache file and current time\n  for expiration\n\n* using file locking (flock) to prevent 'cache miss stampede'\n  problem, i.e. to ensure that only one process is (re)generating\n  cache entry\n\n* serving stale but not too old version, and regenerating data\n  in background, to avoid waiting for data to be regenerated\n\n* progress info indicator based on http-equiv refresh trick\n  (described in more detail how it works in the commit message)\n\n* capturing gitweb output by redirecting STDOUT to cache entry file\n\n\nThe main differences between this patch series and \"Gitweb caching v7\"\n(and my minimal fixups in \"Gitweb caching v7.[1-3]\") are the following:\n\n* features are added piece by piece in multiple patches (22 patches\n  covering v7 features vs 3-4 patches in v7/v7.x series), making it\n  hopefully easier to review, as patches are smaller.  OTOH this series\n  is much longer...\n\n* In J.H. series subroutines responsible for capturing gitweb output are\n  in gitweb.perl, and subroutines responsible for caching are in lib/cache.pl\n  (cache.pm in original patch).  cache.pl/cache.pm uses variables and\n  subroutines from gitweb script, so it couldn't be made into Perl module;\n  therefore we have to use 'do' rather than 'require' to load it.\n\n  In this series GitwebCache::Capture::Simple module is responsible for\n  capturing [gitweb] output, GitwebCache::SimpleFileCache and\n  GitwebCache::FileCacheWithLocking are responsible for caching, and\n  GitwebCache::CacheOutput is about caching captured output (ties them\n  together).  This allowed \"unit\" testing, i.e. testing each module\n  in isolation (tests t9503 - t9505).\n\n* GitwebCache::CacheOutput::cache_output (equivalent of cache_fetch from\n  cache.pm in J.H. patch) supports any cache supporting ->get / ->set or\n  ->compute interface (e.g. Cache::FileCache from Cache::Cache, or CHI\n  with 'File' driver, or Cache::FastMmap) - it is described in gitweb/README\n  in \"Gitweb caching\" section.\n\n  For this capturing engine (GitwebCache::Capture::Simple) supports returning\n  captured output (via capturing to in-memory file).\n\n  Tested once upon a time with Cache::FileCache $cache.\n\n* There is no difference between treating actions with binary output or\n  possibly binary output like 'snapshot' or 'blob_plain' (which use binary\n  or ':raw' mode) and other actions (which use text or ':utf8' mode).\n  GitwebCache::Capture::Simple captures transformed output i.e. raw bytes,\n  so data from cache is dumped to STDOUT (to web browser) in ':raw' mode.\n\n* Instead of disabling caching of 'blame_incremental' action (so it is\n  used without caching), this alternate to plain 'blame' action is\n  disabled if caching is turned off.\n\n  In the future 'blame_interactive' would use cache for caching its\n  initial output and for caching 'blame_data' it uses.\n\n* Configuring cache is done via %cache_options (and %generating_options)\n  instead of via gitweb config variables.  For example instead of \n  $minCacheTime there is $cache_options{'expires_min'}.\n\n  It is also more configurable than in J.H. patch; more parameters can be\n  changed (like e.g. factor multiplying get_loadavg() in adaptive cache\n  lifetime; 'check_load', 'generating_info', 'on_error' are configurable\n  callbacks).\n\n  \"gitweb: Support legacy options used by kernel.org caching engine\"\n  patch in this series makes this rewrite support configuration variables\n  used by \"Gitweb caching v7\" series.\n\n* This rewrite uses lexical filehandles, i.e.\n\n    open my $fh, '>', $filename\n\n  instead of globals that J.H. patch uses\n\n    open FH, '>', $filename\n\n  (though it hides it in \"open(cacheFile, '<', $filename)\").  J.H. is\n  working on \"Gitweb caching v8\" and I think he would address that issue\n  there.\n\n* When generating cache in background process, the background process\n  daemonizes itself.  Therefore it should be safe to enable / use\n  'background_cache' also for persistent environments, like mod_perl via\n  ModPerl::Registry, FastCGI when run as gitweb.fcgi, PSGI via gitweb.psgi\n  wrapper that git-instaweb generates.\n\n- Other changes might be mentioned in comments to individual patches\n\nTwo last patches in this series introduce proof of concept cache\nadministration page, where you can currently check how much file space is\nused by cache, and where you can also safely clean cache (remove all\nentries).  Those two patches are slightly outside scope of \"gitweb output\ncaching\", and that is why I refer to this series as 22+ patches long\n(there are 24 patches in total).\n\nPrevious version of this series had\n  gitweb/lib - Benchmarking GitwebCache::SimpleFileCache (in t/9603/)\n  gitweb/lib - Alternate ways of capturing output\nas two last patche in the series.  They are missing in this release.\n\n\nThe following changes since commit 0b0cd0e0a29a139f418991dd769ea4266ffec370:\n\n  Merge branch 'jn/ignore-doc' (2010-12-03 16:13:06 -0800)\n\nare available in the git repository at:\n\n  git://repo.or.cz/git/jnareb-git.git gitweb/cache-kernel-v6\n\nJakub Narebski (24):\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 output capture by redirecting STDOUT\n  gitweb/lib - Cache captured output (using get/set)\n  gitweb: Add optional output caching\n  gitweb/lib - Adaptive cache expiration time\n  gitweb/lib - Use CHI compatibile (compute method) caching interface\n  gitweb/lib - capture output directly to cache entry file\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/lib - Configure running 'generating_info' when generating data\n  gitweb: Add startup delay to activity indicator for cache\n  gitweb/lib - Add support for setting error handler in cache\n  gitweb: Wrap die_error to use as error handler for caching engine\n  gitweb: Support legacy options used by kernel.org caching engine\n  gitweb/lib - Add clear() and size() methods to caching interface\n  gitweb: Add beginnings of cache administration page (proof of\n    concept)\n\n gitweb/Makefile                                |   23 +-\n gitweb/README                                  |   62 +++\n gitweb/gitweb.perl                             |  544 +++++++++++++++++++-\n gitweb/lib/GitwebCache/CacheOutput.pm          |  131 +++++\n gitweb/lib/GitwebCache/Capture/Simple.pm       |  110 ++++\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |  376 ++++++++++++++\n gitweb/lib/GitwebCache/SimpleFileCache.pm      |  592 ++++++++++++++++++++++\n t/gitweb-lib.sh                                |   12 +\n t/t9500-gitweb-standalone-no-errors.sh         |   20 +\n t/t9501-gitweb-standalone-http-status.sh       |   21 +\n t/t9502-gitweb-standalone-parse-output.sh      |   33 ++\n t/t9503-gitweb-caching-interface.sh            |   34 ++\n t/t9503/test_cache_interface.pl                |  647 ++++++++++++++++++++++++\n t/t9504-gitweb-capture-interface.sh            |   34 ++\n t/t9504/test_capture_interface.pl              |  108 ++++\n t/t9505-gitweb-cache.sh                        |   39 ++\n t/t9505/test_cache_output.pl                   |   86 ++++\n t/test-lib.sh                                  |    4 +-\n 18 files changed, 2850 insertions(+), 26 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/CacheOutput.pm\n create mode 100644 gitweb/lib/GitwebCache/Capture/Simple.pm\n create mode 100644 gitweb/lib/GitwebCache/FileCacheWithLocking.pm\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 create mode 100755 t/t9504-gitweb-capture-interface.sh\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":"157463","messageId":"1291677069-6559-2-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 01/24] t/test-lib.sh: Export also GIT_BUILD_DIR in test_external","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:46Z","receivedAt":"2010-12-06T23:10:46Z","isPatch":true,"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 is the same as in previous version of this series.  It has\nno equivalent in \"Gitweb caching v7\" by J.H.; it was not needed there\nbecause that series doesn't have separate \"unit\" tests for individual\ncomponents of gitweb output caching.\n\n t/test-lib.sh |    4 ++--\n 1 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 48fa516..c077fa4 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -552,9 +552,9 @@ test_external () {\n \t\t# Announce the script to reduce confusion about the\n \t\t# test output that follows.\n \t\tsay_color \"\" \"# run $test_count: $descr ($*)\"\n-\t\t# Export TEST_DIRECTORY, TRASH_DIRECTORY and GIT_TEST_LONG\n+\t\t# Export TEST_DIRECTORY, GIT_BUILD_DIR, TRASH_DIRECTORY and GIT_TEST_LONG\n \t\t# to be able to use them in script\n-\t\texport TEST_DIRECTORY TRASH_DIRECTORY GIT_TEST_LONG\n+\t\texport TEST_DIRECTORY GIT_BUILD_DIR TRASH_DIRECTORY GIT_TEST_LONG\n \t\t# Run command; redirect its stderr to &4 as in\n \t\t# test_run_, but keep its stdout on our stdout even in\n \t\t# non-verbose mode.\n-- \n1.7.3\n"},{"id":"157457","messageId":"1291677069-6559-3-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 02/24] gitweb: Prepare for splitting gitweb","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:47Z","receivedAt":"2010-12-06T23:10:47Z","isPatch":true,"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 to\nallow testing installed version of gitweb and installed version of\nmodules (for future tests which would check individual (sub)modules).\n\n\nUsing __DIR__ from Dir::Self module (not in core, that's why currently\ngitweb includes excerpt of code from Dir::Self defining __DIR__) was\nchosen over using FindBin-based solution (in core since perl 5.00307,\nwhile gitweb itself requires at least perl 5.8.0) because FindBin uses\nBEGIN block, which is a problem under mod_perl and other persistent\nPerl environments (thought there are workarounds).\n\nAt Pavan Kumar Sankara suggestion gitweb/Makefile uses\n\n  install [OPTION]... SOURCE... DIRECTORY\n\nformat (2nd format) with single SOURCE rather than\n\n  install [OPTION]... SOURCE DEST\n\nformat (1st format) because of security reasons (race conditions).\nModern GNU install has `-T' / `--no-target-directory' option, but we\ncannot rely that the $(INSTALL) we are using supports this option.\n\nThe install-modules target in gitweb/Makefile uses shell 'for' loop,\ninstead of make's $(foreach) function, to avoid possible problem with\ngenerating a command line that exceeded the maximum argument list\nlength.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nIn previous version there was a BUG: shell variables should not be in\nsingle quotes.\n\nThis is fixed version, the same as in\n\n  [PATCHv7.4 0/4] Gitweb caching v7.4\n  Message-ID: <1291404335-25541-1-git-send-email-jnareb@gmail.com>\n  http://thread.gmane.org/gmane.comp.version-control.git/162830\n\nseries, which are a bit fixed up and a tiny bit cleaned up version of\n\"Gitweb caching v7\" series from John 'Warthog9' Hawley:\n\n  http://thread.gmane.org/gmane.comp.version-control.git/160147\n\n\nSidenote: this patch can serve as a way to introduce infrastructure\nrequired for splitting gitweb into modules for better maintability.\nThis issue was to be first part (first milestone) for \"Integrated web\nclient for git\" GSoC2010 project by Pavan Kumar Sankara (with Christian\nCouder as mentor, and Petr 'Pasky' Baudis as co-mentor)... which\nunfortunately failed 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\n\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 0a6ac00..e6029e1 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -57,6 +57,7 @@ PERL_PATH  ?= /usr/bin/perl\n bindir_SQ = $(subst ','\\'',$(bindir))#'\n gitwebdir_SQ = $(subst ','\\'',$(gitwebdir))#'\n gitwebstaticdir_SQ = $(subst ','\\'',$(gitwebdir)/static)#'\n+gitweblibdir_SQ = $(subst ','\\'',$(gitwebdir)/lib)#'\n SHELL_PATH_SQ = $(subst ','\\'',$(SHELL_PATH))#'\n PERL_PATH_SQ  = $(subst ','\\'',$(PERL_PATH))#'\n DESTDIR_SQ    = $(subst ','\\'',$(DESTDIR))#'\n@@ -153,20 +154,32 @@ test:\n \n test-installed:\n \tGITWEB_TEST_INSTALLED='$(DESTDIR_SQ)$(gitwebdir_SQ)' \\\n+\tGITWEBLIBDIR='$(DESTDIR_SQ)$(gitweblibdir_SQ)' \\\n \t\t$(MAKE) -C ../t gitweb-test\n \n ### Installation rules\n \n-install: all\n+install: all install-modules\n \t$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitwebdir_SQ)'\n \t$(INSTALL) -m 755 $(GITWEB_PROGRAMS) '$(DESTDIR_SQ)$(gitwebdir_SQ)'\n \t$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitwebstaticdir_SQ)'\n \t$(INSTALL) -m 644 $(GITWEB_FILES) '$(DESTDIR_SQ)$(gitwebstaticdir_SQ)'\n \n+install-modules:\n+\tinstall_dirs=\"$(sort $(dir $(GITWEB_MODULES)))\" && \\\n+\tfor dir in $$install_dirs; do \\\n+\t\ttest -d '$(DESTDIR_SQ)$(gitweblibdir_SQ)'/\"$$dir\" || \\\n+\t\t$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitweblibdir_SQ)'/\"$$dir\"; \\\n+\tdone\n+\tgitweb_modules=\"$(GITWEB_MODULES)\" && \\\n+\tfor mod in $$gitweb_modules; do \\\n+\t\t$(INSTALL) -m 644 \"lib/$$mod\" '$(DESTDIR_SQ)$(gitweblibdir_SQ)'/\"$$(dirname $$mod)\"; \\\n+\tdone\n+\n ### Cleaning rules\n \n clean:\n \t$(RM) gitweb.cgi static/gitweb.min.js static/gitweb.min.css GITWEB-BUILD-OPTIONS\n \n-.PHONY: all clean install test test-installed .FORCE-GIT-VERSION-FILE FORCE\n+.PHONY: all clean install install-modules test test-installed .FORCE-GIT-VERSION-FILE FORCE\n \ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 679f2da..cfa511c 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -10,6 +10,14 @@\n use 5.008;\n use strict;\n use warnings;\n+\n+use File::Spec;\n+# __DIR__ is taken from Dir::Self __DIR__ fragment\n+sub __DIR__ () {\n+\tFile::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);\n+}\n+use lib __DIR__ . '/lib';\n+\n use CGI qw(:standard :escapeHTML -nosticky);\n use CGI::Util qw(unescape);\n use CGI::Carp qw(fatalsToBrowser set_message);\n-- \n1.7.3\n"},{"id":"157461","messageId":"1291677069-6559-4-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 03/24] gitweb/lib - Very simple file based cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:48Z","receivedAt":"2010-12-06T23:10:48Z","isPatch":true,"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\nThe data is stored in the cache as-is, without adding metadata (like\nexpiration date), and without serialization (which means that one can\nstore only scalar data).  At this point there is no support for\nexpiring cache entries.\n\n\nThe code of GitwebCaching::SimpleFileCache package in gitweb/lib\nwas heavily based on file-based cache in Cache::Cache package, i.e.\non Cache::FileCache, Cache::FileBackend and Cache::BaseCache, and on\nfile-based cache in CHI, i.e. on CHI::Driver::File and CHI::Driver\n(including implementing atomic write, something that original patch\nlacks).  It tries to follow more modern CHI architecture, but without\nrequiring Moose.  It is much simplified compared to both interfaces\nand their file-based drivers.\n\nThis patch does not yet enable output caching in gitweb (it doesn't\nhave all required features yet); on the other hand it includes tests\nof cache Perl API in t/t9503-gitweb-caching-interface.sh.\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis version is cleaned up from remainders of old-style code inspired\nby code in Cach::Cache distribution, and of cargo-culted code from\nexamples in Perl documentation; it uses more modern Perl coding\nconventions.  Therefore it has significantly less warnings from\nPerl::Critic (though they are not eliminated entirely).\n\nContrary to some earlier versions we do not try to be operating system\nagnostic by using File::Spec; concatenating paths with '/' taken as\ndirectory separator is faster, and that is what gitweb uses anyway.\n\nNote that writing to cache entry file is done atomically via File::Temp\nand renaming file to final version.  This way we can ensure that gitweb\nwouldn't get halfway written file.  Use of File::Temp (which gives\nsafety but unfortunately has negative impact on performance) will be\nmade not necessary with introduction of locking.\n\nBecause GitwebCache::SimpleFileCache (and its derivatives) use standard\n->new, ->get, ->set, ->compute methods / API / interface, then other\ncaching engines such as Cache::Cache, Cache, CHI, Cache::Memcached or\nCache::FastMmap can be used instead of this one.\n\nFast spew / fast slurp might be interesting, but they are obsoleted\n(unused) when redirecting output directly to cache entry file in later\npatch.\n\n gitweb/lib/GitwebCache/SimpleFileCache.pm |  336 +++++++++++++++++++++++++++++\n t/t9503-gitweb-caching-interface.sh       |   34 +++\n t/t9503/test_cache_interface.pl           |   85 ++++++++\n 3 files changed, 455 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..2f26a6c\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -0,0 +1,336 @@\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 $class = shift;\n+\tmy %opts = ref $_[0] ? %{ $_[0] } : @_;\n+\n+\tmy $self = {};\n+\t$self = bless($self, $class);\n+\n+\tmy ($root, $depth, $ns);\n+\tif (%opts) {\n+\t\t$root =\n+\t\t\t$opts{'cache_root'} ||\n+\t\t\t$opts{'root_dir'};\n+\t\t$depth =\n+\t\t\t$opts{'cache_depth'} ||\n+\t\t\t$opts{'depth'};\n+\t\t$ns = $opts{'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+# ......................................................................\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+# ----------------------------------------------------------------------\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'}] 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+sub read_file {\n+\tmy $filename = shift;\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, '<', $filename\n+\t\tor return;\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 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 '$filename' opened for reading: $!\";\n+\treturn $buf;\n+}\n+\n+sub write_fh {\n+\tmy ($write_fh, $filename, $data) = @_;\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+\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 unless defined $write_cnt;\n+\n+\t\t$size_left -= $write_cnt;\n+\t\t$offset += $write_cnt; # == length($data);\n+\t}\n+\n+\tclose $write_fh\n+\t\tor die \"Couldn't close file '$filename' opened for writing: $!\";\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 unless (defined $file && -f $file);\n+\n+\treturn read_file($file);\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 unless (defined $file && defined $dir);\n+\n+\t# ensure that directory leading to cache file exists\n+\tif (!-d $dir) {\n+\t\t# mkpath will croak()/die() if there is an error\n+\t\tmkpath($dir, 0, 0777);\n+\t}\n+\n+\t# generate a temporary file\n+\tmy ($temp_fh, $tempname) = _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+\twrite_fh($temp_fh, $tempname, $data);\n+\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+# ......................................................................\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;\n+\treturn 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+\treturn $self->fetch($key);;\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->();\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..1a3f374\n--- /dev/null\n+++ b/t/t9503/test_cache_interface.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 Test::More;\n+\n+# test source version\n+use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n+\n+\n+# Test creating a cache\n+#\n+BEGIN { use_ok('GitwebCache::SimpleFileCache'); }\n+diag(\"Using lib '$INC[0]'\");\n+diag(\"Testing '$INC{'GitwebCache/SimpleFileCache.pm'}'\");\n+\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":"157458","messageId":"1291677069-6559-5-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 04/24] gitweb/lib - Stat-based cache expiration","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:49Z","receivedAt":"2010-12-06T23:10:49Z","isPatch":true,"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 when cache element expires in is _global_ value associated\nwith cache instance, and is not local property of cache entry.  (Currently\ncache entry does not store any metadata associated with entry... which\nmeans that there is no need for serialization / marshalling / freezing and\nthawing.)  Default 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-compatible is_valid()\nmethod.\n\nIncludes some tests checking that expiring works correctly (on the level of\nAPI) in t9503.\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nCompared to \"Gitweb caching v7\" series, checking if cache entry is\nexpired (based on mtime of cache file) is simpler: we reuse stat\nstructure from test if file even exists (via stat(_)).\n\n gitweb/lib/GitwebCache/SimpleFileCache.pm |   40 +++++++++++++++++++++++++++-\n t/t9503/test_cache_interface.pl           |   19 +++++++++++++\n 2 files changed, 57 insertions(+), 2 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex 2f26a6c..790383d 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 $class = shift;\n \tmy %opts = ref $_[0] ? %{ $_[0] } : @_;\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 (%opts) {\n \t\t$root =\n \t\t\t$opts{'cache_root'} ||\n@@ -73,14 +77,19 @@ sub new {\n \t\t\t$opts{'cache_depth'} ||\n \t\t\t$opts{'depth'};\n \t\t$ns = $opts{'namespace'};\n+\t\t$expires_in =\n+\t\t\t$opts{'default_expires_in'} ||\n+\t\t\t$opts{'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@@ -92,7 +101,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@@ -287,6 +296,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@@ -310,6 +344,8 @@ sub set {\n sub get {\n \tmy ($self, $key) = @_;\n \n+\treturn unless $self->is_valid($key);\n+\n \treturn $self->fetch($key);;\n }\n \ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 1a3f374..1517fb6 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -82,4 +82,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":"157465","messageId":"1291677069-6559-6-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 05/24] gitweb/lib - Regenerate entry if the cache file has size of 0","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:50Z","receivedAt":"2010-12-06T23:10:50Z","isPatch":true,"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 unchanged from previos version of this series.\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\nThe \"gitweb: File based caching layer (from git.kernel.org)\" in\n\"Gitweb caching v7\" by J.H. incudes the same test.\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 790383d..bf74f7c 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -310,6 +310,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":"157442","messageId":"1291677069-6559-7-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 06/24] gitweb/lib - Simple output capture by redirecting STDOUT","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:51Z","receivedAt":"2010-12-06T23:10:51Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add GitwebCache::Capture::Simple package, which captures output by\nredirecting STDOUT to in-memory file (saving what is printed to\nscalar), earlier saving original STDOUT to restore it when finished\ncapturing.\n\nGitwebCache::Capture::Simple preserves PerlIO layers, both those set\nbefore started capturing output, and those set during capture.  The\nexceptions is the 'scalar' layer, which needs additional parameter,\nand which for proper handling needs non-core module PerlIO::Util.\n\nNo care was taken to handle the following special cases (prior to\nstarting capture): closed STDOUT, STDOUT reopened to scalar reference,\ntied STDOUT.  You shouldn't modify STDOUT during capture.\n\nIncludes separate tests for capturing output in\nt9504/test_capture_interface.pl which is run as external test from\nt9504-gitweb-capture-interface.sh.  It tests capturing of utf8 data\nprinted in :utf8 mode, and of binary data (containing invalid utf8) in\n:raw mode.\n\nNote that nested capturing doesn't work (and probably couldn't be made\nto work when capturing to in-memory file), but this feature wouldn't\nbe needed for capturing gitweb output (to cache it).\n\n\nThis patch was based on \"gitweb: add output buffering and associated\nfunctions\" patch by John 'Warthog9' Hawley (J.H.) in \"Gitweb caching v7\"\nseries, and on code of Capture::Tiny by David Golden (Apache License 2.0).\n\nBased-on-work-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nIn previous version of this series the basic (default) capturing\nengine was GitwebCache::Capture::SelectFH, which made use of\nselect(FH) to change default filehandle used for print and printf\nwithout filehandle argument (print LIST).  This required changing\n  binmode STDOUT, <mode>\nto\n  binmode select(), <mode>\nin gitweb.perl, which is now not necessary.\n\nTo simplify code this time we don't use GitwebCache::Capture as base\nclass providing common features, but we reserve space to add it if we\nfeel it necessary (e.g. when adding more capturing engines).\n\nEven re-layering is probably not necessary in the case of gitweb, as\nwe set ':utf8' mode at beginning, and if we change it to ':raw' it is\nalways after we started capture.\n\nOriginal \"Gitweb caching v7\" used capturing to in-memory files in the\ncase when caching was disabled; in my minimal fixup i.e. in the\n\"Gitweb caching v7.x\" threads capturing to in-memory file is not done\nat all; output is redirected straight to cache file (or equivalent).\nThe same would be done later in this series.\n\n gitweb/lib/GitwebCache/Capture/Simple.pm           |   96 ++++++++++++++++++++\n ...erface.sh => t9504-gitweb-capture-interface.sh} |   10 +-\n t/t9504/test_capture_interface.pl                  |   91 +++++++++++++++++++\n 3 files changed, 192 insertions(+), 5 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/Capture/Simple.pm\n copy t/{t9503-gitweb-caching-interface.sh => t9504-gitweb-capture-interface.sh} (66%)\n create mode 100755 t/t9504/test_capture_interface.pl\n\ndiff --git a/gitweb/lib/GitwebCache/Capture/Simple.pm b/gitweb/lib/GitwebCache/Capture/Simple.pm\nnew file mode 100644\nindex 0000000..3585e58\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/Capture/Simple.pm\n@@ -0,0 +1,96 @@\n+# gitweb - simple web interface to track changes in git repositories\n+#\n+# (C) 2010, Jakub Narebski <jnareb@gmail.com>\n+#\n+# This program is licensed under the GPLv2\n+\n+#\n+# Simple output capturing via redirecting STDOUT to in-memory file.\n+#\n+\n+# This is the same mechanism that Capture::Tiny uses, only simpler;\n+# we don't capture STDERR at all, we don't tee, we don't support\n+# capturing output of external commands.\n+\n+package GitwebCache::Capture::Simple;\n+\n+use strict;\n+use warnings;\n+\n+use PerlIO;\n+\n+# Constructor\n+sub new {\n+\tmy $class = shift;\n+\n+\tmy $self = {};\n+\t$self = bless($self, $class);\n+\n+\treturn $self;\n+}\n+\n+sub capture {\n+\tmy ($self, $code) = @_;\n+\n+\t$self->capture_start();\n+\t$code->();\n+\treturn $self->capture_stop();\n+}\n+\n+# ----------------------------------------------------------------------\n+\n+# Start capturing data (STDOUT)\n+sub capture_start {\n+\tmy $self = shift;\n+\n+\t# save copy of real STDOUT via duplicating it\n+\tmy @layers = PerlIO::get_layers(\\*STDOUT);\n+\topen $self->{'orig_stdout'}, \">&\", \\*STDOUT\n+\t\tor die \"Couldn't dup STDOUT for capture: $!\";\n+\n+\t# close STDOUT, so that it isn't used anymode (to have it fd0)\n+\tclose STDOUT;\n+\n+\t# reopen STDOUT as in-memory file\n+\t$self->{'data'} = '';\n+\tunless (open STDOUT, '>', \\$self->{'data'}) {\n+\t\topen STDOUT, '>&', fileno($self->{'orig_stdout'});\n+\t\tdie \"Couldn't reopen STDOUT as in-memory file for capture: $!\";\n+\t}\n+\t_relayer(\\*STDOUT, \\@layers);\n+\n+\t# started capturing\n+\t$self->{'capturing'} = 1;\n+}\n+\n+# Stop capturing data (required for die_error)\n+sub capture_stop {\n+\tmy $self = shift;\n+\n+\t# return if we didn't start capturing\n+\treturn unless delete $self->{'capturing'};\n+\n+\t# close in-memory file, and restore original STDOUT\n+\tmy @layers = PerlIO::get_layers(\\*STDOUT);\n+\tclose STDOUT;\n+\topen STDOUT, '>&', fileno($self->{'orig_stdout'});\n+\t_relayer(\\*STDOUT, \\@layers);\n+\n+\treturn $self->{'data'};\n+}\n+\n+# taken from Capture::Tiny by David Golden, Apache License 2.0\n+# with debugging stripped out, and added filtering out 'scalar' layer\n+sub _relayer {\n+\tmy ($fh, $layers) = @_;\n+\n+\tmy %seen = ( unix => 1, perlio => 1, scalar => 1 ); # filter these out\n+\tmy @unique = grep { !$seen{$_}++ } @$layers;\n+\n+\tbinmode($fh, join(\":\", \":raw\", @unique));\n+}\n+\n+\n+1;\n+__END__\n+# end of package GitwebCache::Capture::Simple\ndiff --git a/t/t9503-gitweb-caching-interface.sh b/t/t9504-gitweb-capture-interface.sh\nsimilarity index 66%\ncopy from t/t9503-gitweb-caching-interface.sh\ncopy to t/t9504-gitweb-capture-interface.sh\nindex 819da1d..82623f1 100755\n--- a/t/t9503-gitweb-caching-interface.sh\n+++ b/t/t9504-gitweb-capture-interface.sh\n@@ -3,10 +3,10 @@\n # Copyright (c) 2010 Jakub Narebski\n #\n \n-test_description='gitweb caching interface\n+test_description='gitweb capturing interface\n \n-This test checks caching interface used in gitweb caching, and caching\n-infrastructure (GitwebCache::* modules).'\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@@ -28,7 +28,7 @@ fi\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+\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..47ab804\n--- /dev/null\n+++ b/t/t9504/test_capture_interface.pl\n@@ -0,0 +1,91 @@\n+#!/usr/bin/perl\n+use lib (split(/:/, $ENV{GITPERLLIB}));\n+\n+use warnings;\n+use strict;\n+use utf8;\n+\n+use Test::More;\n+\n+# test source version\n+use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n+\n+# ....................................................................\n+\n+use_ok('GitwebCache::Capture::Simple');\n+diag(\"Using lib '$INC[0]'\");\n+diag(\"Testing '$INC{'GitwebCache/Capture/Simple.pm'}'\");\n+\n+# Test setting up capture\n+#\n+my $capture = new_ok('GitwebCache::Capture::Simple' => [], 'The $capture');\n+\n+# Test capturing\n+#\n+sub capture_block (&) {\n+\treturn $capture->capture(shift);\n+}\n+\n+diag('Should not print anything except test results and diagnostic');\n+my $test_data = 'Capture this';\n+my $captured = capture_block {\n+\tprint $test_data;\n+};\n+is($captured, $test_data, 'capture simple data');\n+\n+binmode STDOUT, ':utf8';\n+$test_data = <<'EOF';\n+Zażółć gęsią jaźń\n+EOF\n+utf8::decode($test_data);\n+$captured = capture_block {\n+\tbinmode STDOUT, ':utf8';\n+\n+\tprint $test_data;\n+};\n+utf8::decode($captured);\n+is($captured, $test_data, 'capture utf8 data');\n+\n+$test_data = '|\\x{fe}\\x{ff}|\\x{9F}|\\000|'; # invalid utf-8\n+$captured = capture_block {\n+\tbinmode STDOUT, ':raw';\n+\n+\tprint $test_data;\n+};\n+is($captured, $test_data, 'capture raw data');\n+\n+# Test nested capturing\n+#\n+TODO: {\n+\tlocal $TODO = \"not required for capturing gitweb output\";\n+\tno warnings;\n+\n+\tmy $outer_capture = GitwebCache::Capture::Simple->new();\n+\t$captured = $outer_capture->capture(sub {\n+\t\tprint \"pre|\";\n+\t\tmy $captured = $capture->capture(sub {\n+\t\t\tprint \"INNER\";\n+\t\t});\n+\t\tprint lc($captured);\n+\t\tprint \"|post\";\n+\t});\n+\tis($captured, \"pre|inner|post\", 'nested capture');\n+}\n+\n+SKIP: {\n+\tskip \"Capture::Tiny not available\", 1\n+\t\tunless eval { require Capture::Tiny; };\n+\n+\t$captured = Capture::Tiny::capture(sub {\n+\t\tmy $inner = $capture->capture(sub {\n+\t\t\tprint \"INNER\";\n+\t\t});\n+\t});\n+\tis($captured, '', \"doesn't print while capturing\");\n+}\n+\n+done_testing();\n+\n+# Local Variables:\n+# coding: utf-8\n+# End:\n-- \n1.7.3\n"},{"id":"157443","messageId":"1291677069-6559-8-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 07/24] gitweb/lib - Cache captured output (using get/set)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:52Z","receivedAt":"2010-12-06T23:10:52Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add GitwebCache::CacheOutput package, which contains cache_output\nsubroutine.  The cache_output gets data from cache and prints it, or\ncaptures output of provided subroutine (code reference), saves it to\ncache and prints it.  It currently uses Cache::Cache compatibile (get,\nset) interface to cache.  The capture_stop currently simply runs\n$capture->capture_stop().\n\nCapturing output is done using GitwebCache::Capture::Simple compatibile\ncapture (->new(), ->capture($code)) passed as one of parameters; the\ndefault solution is to use GitwebCache::Capture::Simple.\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 die_error\nsubroutine, 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.  It checks that cache_output behaves correctly,\nnamely that it saves and restores action output in cache, and that it\nprints generated output or cached output, depending on whether there\nexist data in cache.  This test requires Capture::Tiny to be installed\n(because GitwebCache::Capture::Simple dosn't support recursive\ncapturing).\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nIn this version capture_stop is defined in GitwebCache::CacheOutput,\ninstead of being re-exported from GitwebCache::Capture (which no\nlonger exists).  Compared to previous version cache_output now expects\n$capture (selecting capturing engine) as second argument, and \ncapture_stop expect $cache and $capture, while it was paramless before.\nt9505 now requires Capture::Tiny, because it needs reliable nested\ncapture.\n\ncapture_stop is here only in the case in the future we would accept\nwider range of possible $capture values, e.g. \\&Capture::Tiny::capture\n(reference to capturing subroutine).\n\n\ncache_output($cache, $capture, $key, $code) is roughly equivalent to\ncache_fetch($action) from cache.pm file in \"Gitweb caching v7\" series\nby J.H.... but nothing in GitwebCache::CacheOutput is gitweb specific.\nThis module is simply about caching captured output, using given caching\nengine, given capturing engine, given key to cache entry, and given \ncoderef output of which is to be captured and cached, and then printing\ngenerated data or data from cache.\n\nSimilarly to J.H. patch, and differently from CGI::Cache or\nPlack::Middleware::Cache accessing cache is/would be quite explicit.\n\n gitweb/lib/GitwebCache/CacheOutput.pm              |   90 ++++++++++++++++++++\n ...-caching-interface.sh => t9505-gitweb-cache.sh} |   15 ++-\n t/t9505/test_cache_output.pl                       |   86 +++++++++++++++++++\n 3 files changed, 186 insertions(+), 5 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/CacheOutput.pm\n copy t/{t9503-gitweb-caching-interface.sh => t9505-gitweb-cache.sh} (58%)\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..458e314\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -0,0 +1,90 @@\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 Exporter qw(import);\n+our @EXPORT      = qw(cache_output capture_stop);\n+our %EXPORT_TAGS = (all => [ @EXPORT ]);\n+\n+# cache_output($cache, $capture, $key, $action_code);\n+#\n+# Attempts to get $key from $cache; if successful, prints the value.\n+# Otherwise, calls $action_code, capture its output using $capture,\n+# and use the captured output as the new value for $key in $cache,\n+# then print captured output.\n+#\n+# It is assumed that captured data is already converted and it is\n+# in ':raw' format (and thus restored in ':raw' from cache)\n+\n+# default capture class (engine), if none provided\n+our $DEFAULT_CAPTURE_CLASS = 'GitwebCache::Capture::Simple';\n+sub cache_output {\n+\tmy ($cache, $capture, $key, $code) = @_;\n+\n+\t$capture = setup_capture($capture);\n+\n+\t# check if data is in the cache\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\t$data = $capture->capture($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\tbinmode STDOUT, ':raw';\n+\t\tprint $data;\n+\t}\n+\n+\treturn $data;\n+}\n+\n+# capture_stop($cache, $capture);\n+#\n+# Stops capturing output; to be used in die_error, so that error pages\n+# are not cached (not captured and cached).\n+sub capture_stop {\n+\tmy ($cache, $capture) = @_;\n+\n+\tif (defined $capture) {\n+\t\treturn $capture->capture_stop();\n+\t}\n+\treturn;\n+}\n+\n+# ......................................................................\n+# helper subroutines\n+\n+# setup capture engine\n+sub setup_capture {\n+\tmy $capture = shift;\n+\n+\t$capture ||= $DEFAULT_CAPTURE_CLASS;\n+\tif (!ref($capture)) {\n+\t\teval \"require $capture;\" or die $@;\n+\t\t$capture = $capture->new();\n+\t}\n+\n+\treturn $capture;\n+}\n+\n+1;\n+__END__\n+# end of package GitwebCache::CacheOutput\ndiff --git a/t/t9503-gitweb-caching-interface.sh b/t/t9505-gitweb-cache.sh\nsimilarity index 58%\ncopy from t/t9503-gitweb-caching-interface.sh\ncopy to t/t9505-gitweb-cache.sh\nindex 819da1d..181577f 100755\n--- a/t/t9503-gitweb-caching-interface.sh\n+++ b/t/t9505-gitweb-cache.sh\n@@ -3,10 +3,10 @@\n # Copyright (c) 2010 Jakub Narebski\n #\n \n-test_description='gitweb caching interface\n+test_description='gitweb cache\n \n-This test checks caching interface used in gitweb caching, and caching\n-infrastructure (GitwebCache::* modules).'\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@@ -22,13 +22,18 @@ fi\n \ttest_done\n }\n \n+\"$PERL_PATH\" -MCapture::Tiny -e 0 >/dev/null 2>&1 || {\n+\tskip_all='perl module Capture::Tiny 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+\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..167bb36\n--- /dev/null\n+++ b/t/t9505/test_cache_output.pl\n@@ -0,0 +1,86 @@\n+#!/usr/bin/perl\n+use lib (split(/:/, $ENV{GITPERLLIB}));\n+\n+use warnings;\n+use strict;\n+\n+use Test::More;\n+use Capture::Tiny qw(capture);\n+\n+# test source version\n+use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n+\n+# ....................................................................\n+\n+# prototypes must be known at compile time, otherwise they do not work\n+BEGIN { use_ok('GitwebCache::CacheOutput'); }\n+\n+require_ok('GitwebCache::SimpleFileCache');\n+require_ok('GitwebCache::Capture::Simple');\n+\n+diag(\"Using lib '$INC[0]'\");\n+diag(\"Testing '$INC{'GitwebCache/CacheOutput.pm'}'\");\n+diag(\"Testing '$INC{'GitwebCache/SimpleFileCache.pm'}'\");\n+diag(\"Testing '$INC{'GitwebCache/Capture/Simple.pm'}'\");\n+\n+\n+# Test setting up $cache and $capture\n+my $cache   = new_ok('GitwebCache::SimpleFileCache' => [], 'The $cache  ');\n+my $capture = new_ok('GitwebCache::Capture::Simple' => [], '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+my $no_capture_output = <<\"EOF\";\n+$action_output# (no capture)\n+EOF\n+sub no_capture {\n+\tcapture_stop($cache, $capture);\n+\tprint $no_capture_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 $code = shift;\n+\n+\tmy ($stdout, $stderr) = capture {\n+\t\tcache_output($cache, $capture, $key, $code);\n+\t};\n+\tprint STDERR $stderr;\n+\treturn $stdout;\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(\\&action);\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(\\&action);\n+is($test_data, $cached_output,        'action output is printed (from cache)');\n+\n+# test using capture_stop\n+$cache->remove($key);\n+$test_data = capture_output_of_cache_output(\\&no_capture);\n+is($test_data, $no_capture_output,    'no_capture output is printed (generated)');\n+ok(! $cache->get($key),               'no_capture output is not captured and not cached');\n+\n+done_testing();\n-- \n1.7.3\n"},{"id":"157449","messageId":"1291677069-6559-9-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 08/24] gitweb: Add optional output caching","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:53Z","receivedAt":"2010-12-06T23:10:53Z","isPatch":true,"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\n(a 'dumb' but fast file-based cache engine).  To enable cache you need\n(at least) set $caching_enabled to true in gitweb config, and copy\nrequired modules alongside generated gitweb.cgi - this is described\nin more detail in the new \"Gitweb caching\" section in gitweb/README.\n\"make install-gitweb\" would install all modules alongside gitweb\nitself.\n\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\nis done via cache_output subroutine from GitwebCache::CacheOutput.\n\nThe cache_output subroutine in GitwebCache::CacheOutput currently uses\nGitwebCache::Capture::Simple compatibile capturing engine passed as\none of parameters to cache_output subroutine.  The default is to use\nGitwebCache::Capture::Simple package.\n\nCapturing and caching is designed in such way that there is no\nbehaviour change if $caching_enabled is false.  If caching is not\nenabled, then capturing is also turned off.\n\nEnabling caching causes the following additional changes to gitweb\noutput:\n* Disables content-type negotiation (choosing between 'text/html'\n  mimetype and 'application/xhtml+xml') when caching, as there is no\n  content-type negotiation done when retrieving page from cache.\n  Use lowest common denominator of 'text/html' mimetype which can\n  be used by all browsers.  This may change in the future.\n* Disable optional timing info (how much time it took to generate the\n  original page, and how many git commands it took), and in its place show\n  unconditionally when page was originally generated (in GMT / UTC\n  timezone).\n* Disable 'blame_incremental' view, as it doesn't make sense without\n  printing data as soon as it is generated (which would require tee-ing\n  when capturing output for caching)... and it doesn't work currently\n  anyway.  Alternate solution would be to run 'blame_incremental' view\n  with caching disabled.\n\n\nAdd basic tests of caching support to t9500-gitweb-standalone-no-errors\ntest: set $caching_enabled to true and check for errors for first time\nrun (generating cache) and second time run (retrieving from cache) for a\nsingle view - summary view for a project.\n\nCheck in the t9501-gitweb-standalone-http-status test that gitweb at\nleast correctly handles \"404 Not Found\" error pages also in the case\nwhen gitweb caching is enabled.\n\nCheck in the t9502-gitweb-standalone-parse-output test that gitweb\nproduces the same output with and without caching, for first and\nsecond run, with binary or text output.\n\nAll those tests make use of new gitweb_enable_caching subroutine added\nto gitweb-lib.sh\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nGitweb caching tests are (almost) the same as in\n\n  [PATCHv7.4 4/4] gitweb: Minimal testing of gitweb caching\n  Message-Id: <1291404335-25541-4-git-send-email-jnareb@gmail.com>\n  http://thread.gmane.org/gmane.comp.version-control.git/162830/focus=162834\n\nThe difference is of course in setting up tests, i.e. in the contents\nof gitweb_enable_caching function in t/gitweb-lib.sh.\n\nOnly the t9500 tests were present in previous version of this series.\n\n\nOne of differences from \"Gitweb caching v7\" by J.H. (and minimal fixup of\nthereof) is that in \"gitweb: File based caching layer (from git.kernel.org)\"\nhashed (internal) key is composed of $my_url ($cgi->url()) and\n$ENV{'QUERY_STRING'}... which meant that it ignores path_info (which is\nWRONG, as you can have different views that differ only in path_info part;\ndifferent views which shouldn't be lumped together under one cache key), and\nthat links which differ only in ordering of parameters (e.g. handcrafted\nURL) would result in different cache keys.\n\nI have chosen here to use href(-replay => 1, -full => 1, -path_info => 0) as\nhuman-readable cache key, which means that if different link gives use the\nsame action then it gives us the same cache key.\n\n\nNOTE: I had to put quick fix about base url, so sha1 of gitweb.perl preimage\nmight not match.\n\n gitweb/Makefile                           |    5 +\n gitweb/README                             |   58 ++++++++++++\n gitweb/gitweb.perl                        |  144 +++++++++++++++++++++++++----\n t/gitweb-lib.sh                           |   11 ++\n t/t9500-gitweb-standalone-no-errors.sh    |   20 ++++\n t/t9501-gitweb-standalone-http-status.sh  |   13 +++\n t/t9502-gitweb-standalone-parse-output.sh |   33 +++++++\n 7 files changed, 265 insertions(+), 19 deletions(-)\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex e6029e1..d67c138 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -113,6 +113,11 @@ endif\n \n GITWEB_FILES += static/git-logo.png static/git-favicon.png\n \n+# gitweb output caching\n+GITWEB_MODULES += GitwebCache/CacheOutput.pm\n+GITWEB_MODULES += GitwebCache/SimpleFileCache.pm\n+GITWEB_MODULES += GitwebCache/Capture/Simple.pm\n+\n GITWEB_REPLACE = \\\n \t-e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \\\n \t-e 's|++GIT_BINDIR++|$(bindir)|g' \\\ndiff --git a/gitweb/README b/gitweb/README\nindex bf3664f..3dc01bd 100644\n--- a/gitweb/README\n+++ b/gitweb/README\n@@ -246,6 +246,12 @@ not include variables usually directly set during build):\n    http://www.andre-simon.de due to assumptions about parameters and output).\n    Useful if highlight is not installed on your webserver's PATH.\n    [Default: highlight]\n+ * $caching_enabled\n+   If true, gitweb would use caching to speed up generating response.\n+   Currently supported is only output (response) caching.  See \"Gitweb caching\"\n+   section below for details on how to configure and customize caching.\n+   The default is false (caching is disabled).\n+\n \n Projects list file format\n ~~~~~~~~~~~~~~~~~~~~~~~~~\n@@ -317,6 +323,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 cfa511c..3286709 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -270,6 +270,48 @@ 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+# or to name of class of cache interface implementing said methods.\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+# Set to _initialized_ instance of GitwebCache::Capture compatibile capturing\n+# engine, i.e. one implementing ->new() constructor, and ->capture($code)\n+# method.  If unset (default), the GitwebCache::Capture::Simple would be used.\n+our $capture;\n+\n # You define site-wide feature defaults here; override them with\n # $GITWEB_CONFIG as necessary.\n our %feature = (\n@@ -1069,7 +1111,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, $capture, $output_key, $actions{$action});\n+\t} else {\n+\t\t$actions{$action}->();\n+\t}\n }\n \n sub reset_timer {\n@@ -1085,6 +1135,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@@ -1157,6 +1209,42 @@ 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+\tunless (defined $capture && ref($capture)) {\n+\t\trequire GitwebCache::Capture::Simple;\n+\t\t$capture = GitwebCache::Capture::Simple->new();\n+\t}\n+}\n+\n run();\n \n if (defined caller) {\n@@ -3420,7 +3508,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@@ -3445,7 +3535,9 @@ sub git_header_html {\n EOF\n \t# the stylesheet, favicon etc urls won't work correctly with path_info\n \t# unless we set the appropriate base URL\n-\tif ($ENV{'PATH_INFO'}) {\n+\t# if caching is enabled we can get it from cache for path_info when it\n+\t# is generated without path_info\n+\tif ($ENV{'PATH_INFO'} || $caching_enabled) {\n \t\tprint \"<base href=\\\"\".esc_url($base_url).\"\\\" />\\n\";\n \t}\n \t# print out each stylesheet that exist, providing backwards capability\n@@ -3594,17 +3686,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@@ -3613,8 +3713,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@@ -3655,6 +3755,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($cache, $capture) if ($capture && $caching_enabled);\n+\n \tgit_header_html($http_responses{$status}, undef, %opts);\n \tprint <<EOF;\n <div class=\"page_body\">\n@@ -5250,7 +5354,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@@ -5304,7 +5409,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@@ -5324,7 +5430,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@@ -5483,7 +5589,7 @@ sub git_blame {\n }\n \n sub git_blame_incremental {\n-\tgit_blame_common('incremental');\n+\tgit_blame_common(!$caching_enabled ? 'incremental' : undef);\n }\n \n sub git_blame_data {\ndiff --git a/t/gitweb-lib.sh b/t/gitweb-lib.sh\nindex b9bb95f..4ce067f 100644\n--- a/t/gitweb-lib.sh\n+++ b/t/gitweb-lib.sh\n@@ -52,6 +52,17 @@ EOF\n \texport SCRIPT_NAME\n }\n \n+gitweb_enable_caching () {\n+\ttest_expect_success 'enable caching' '\n+\t\tcat >>gitweb_config.perl <<-\\EOF &&\n+\t\t$caching_enabled = 1;\n+\t\t$cache_options{\"expires_in\"} = -1;      # never expire cache for tests\n+\t\t$cache_options{\"cache_root\"} = \"cache\"; # to clear the right thing\n+\t\tEOF\n+\t\trm -rf cache/\n+\t'\n+}\n+\n gitweb_run () {\n \tGATEWAY_INTERFACE='CGI/1.1'\n \tHTTP_ACCEPT='*/*'\ndiff --git a/t/t9500-gitweb-standalone-no-errors.sh b/t/t9500-gitweb-standalone-no-errors.sh\nindex 21cd286..cc9cee5 100755\n--- a/t/t9500-gitweb-standalone-no-errors.sh\n+++ b/t/t9500-gitweb-standalone-no-errors.sh\n@@ -677,4 +677,24 @@ test_expect_success HIGHLIGHT \\\n \t gitweb_run \"p=.git;a=blob;f=test.sh\"'\n test_debug 'cat gitweb.log'\n \n+# ----------------------------------------------------------------------\n+# caching\n+\n+gitweb_enable_caching\n+\n+test_expect_success \\\n+\t'caching enabled (project summary, first run, generating cache)' \\\n+\t'gitweb_run \"p=.git;a=summary\"'\n+test_debug 'cat gitweb.log'\n+\n+test_expect_success \\\n+\t'caching enabled (project summary, second run, cached version)' \\\n+\t'gitweb_run \"p=.git;a=summary\"'\n+test_debug 'cat gitweb.log'\n+\n+test_expect_success \\\n+\t'caching enabled (non-existent commit, not cached error page)' \\\n+\t'gitweb_run \"p=.git;a=commit;h=non-existent\"'\n+test_debug 'cat gitweb.log'\n+\n test_done\ndiff --git a/t/t9501-gitweb-standalone-http-status.sh b/t/t9501-gitweb-standalone-http-status.sh\nindex 2487da1..168e494 100755\n--- a/t/t9501-gitweb-standalone-http-status.sh\n+++ b/t/t9501-gitweb-standalone-http-status.sh\n@@ -134,5 +134,18 @@ cat >>gitweb_config.perl <<\\EOF\n our $maxload = undef;\n EOF\n \n+# ----------------------------------------------------------------------\n+# output caching\n+\n+gitweb_enable_caching\n+\n+test_expect_success 'caching enabled (non-existent commit, 404 error)' '\n+\tgitweb_run \"p=.git;a=commit;h=non-existent\" &&\n+\tgrep \"Status: 404 Not Found\" gitweb.headers &&\n+\tgrep \"404 - Unknown commit object\" gitweb.body\n+'\n+test_debug 'echo \"headers\" && cat gitweb.headers'\n+test_debug 'echo \"body\"    && cat gitweb.body'\n+\n \n test_done\ndiff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh\nindex dd83890..bc8cb92 100755\n--- a/t/t9502-gitweb-standalone-parse-output.sh\n+++ b/t/t9502-gitweb-standalone-parse-output.sh\n@@ -112,4 +112,37 @@ test_expect_success 'snapshot: hierarchical branch name (xx/test)' '\n '\n test_debug 'cat gitweb.headers'\n \n+\n+# ----------------------------------------------------------------------\n+# whether gitweb with caching enabled produces the same output\n+\n+test_expect_success 'setup for caching tests (utf8 commit, binary file)' '\n+\t. \"$TEST_DIRECTORY\"/t3901-utf8.txt &&\n+\tcp \"$TEST_DIRECTORY\"/test9200a.png image.png &&\n+\tgit add image.png &&\n+\tgit commit -F \"$TEST_DIRECTORY\"/t3900/1-UTF-8.txt &&\n+\tgitweb_run \"p=.git;a=patch\" &&\n+\tmv gitweb.body no_cache.html &&\n+\tgitweb_run \"p=.git;a=blob_plain;f=image.png\" &&\n+\tmv gitweb.body no_cache.png\n+'\n+\n+gitweb_enable_caching\n+\n+for desc in 'generating cache' 'cached version'; do\n+\ttest_expect_success \"caching enabled, HTML output, $desc\" '\n+\t\tgitweb_run \"p=.git;a=patch\" &&\n+\t\tmv gitweb.body cache.html &&\n+\t\ttest_cmp no_cache.html cache.html\n+\t'\n+done\n+\n+for desc in 'generating cache' 'cached version'; do\n+\ttest_expect_success \"caching enabled, binary output, $desc\" '\n+\t\tgitweb_run \"p=.git;a=blob_plain;f=image.png\" &&\n+\t\tmv gitweb.body cache.png &&\n+\t\tcmp no_cache.png cache.png\n+\t'\n+done\n+\n test_done\n-- \n1.7.3\n"},{"id":"157450","messageId":"1291677069-6559-10-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 09/24] gitweb/lib - Adaptive cache expiration time","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:54Z","receivedAt":"2010-12-06T23:10:54Z","isPatch":true,"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\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\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nDifferences from relevant parts of J.H. patch:\n* 'increase_factor' is configurable rather than hardcoded 60.\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 (and for example can be tested separately).\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 4322b28..12e04a1 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -304,8 +304,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 # Set to _initialized_ instance of GitwebCache::Capture compatibile capturing\n # engine, i.e. one implementing ->new() constructor, and ->capture($code)\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex bf74f7c..581a574 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 $class = shift;\n \tmy %opts = ref $_[0] ? %{ $_[0] } : @_;\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 (%opts) {\n \t\t$root =\n \t\t\t$opts{'cache_root'} ||\n@@ -77,19 +94,31 @@ sub new {\n \t\t\t$opts{'cache_depth'} ||\n \t\t\t$opts{'depth'};\n \t\t$ns = $opts{'namespace'};\n-\t\t$expires_in =\n+\t\t$expires_min =\n+\t\t\t$opts{'expires_min'} ||\n \t\t\t$opts{'default_expires_in'} ||\n \t\t\t$opts{'expires_in'};\n+\t\t$expires_max =\n+\t\t\t$opts{'expires_max'};\n+\t\t$increase_factor = $opts{'expires_factor'};\n+\t\t$check_load      = $opts{'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 $opts{'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@@ -101,7 +130,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_in)) {\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 {\n@@ -114,6 +144,45 @@ 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\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 1517fb6..9513043 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -101,4 +101,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":"157444","messageId":"1291677069-6559-11-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 10/24] gitweb/lib - Use CHI compatibile (compute method) caching interface","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:55Z","receivedAt":"2010-12-06T23:10:55Z","isPatch":true,"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).\nIn the future other compatibile but differently named methods, like\nCache::FastMmap's ->get_and_set($key, $code) method, could also be\nsupported; though CHI which has ->compute() includes CHI::Driver::FastMmap\nwrapper.\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 less importance that it had before, because when using\nGitwebCache::FileCacheWithLocking it is ->compute_fh interface that would\nbe used, not ->compute.  As far as I understand CHI ->compute looks the\nsame simple wrapper around ->get/->set that GitwebCache::SimpleFileCache\nuses.\n\nOn the other hand (as you would see later) ->compute and ->compute_fh have\nlot of code in common, and it is easier to test behavior of ->compute in\nt9503 tests.\n\n gitweb/lib/GitwebCache/CacheOutput.pm |   36 +++++++++++++++++++++++++++-----\n 1 files changed, 30 insertions(+), 6 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/CacheOutput.pm b/gitweb/lib/GitwebCache/CacheOutput.pm\nindex 458e314..4a96ac9 100644\n--- a/gitweb/lib/GitwebCache/CacheOutput.pm\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -38,6 +38,36 @@ sub cache_output {\n \n \t$capture = setup_capture($capture);\n \n+\tmy $data;\n+\tif ($cache->can('compute')) {\n+\t\t$data = cache_output_compute($cache, $capture, $key, $code);\n+\t} else {\n+\t\t$data = cache_output_get_set($cache, $capture, $key, $code);\n+\t}\n+\n+\tif (defined $data) {\n+\t\tbinmode STDOUT, ':raw';\n+\t\tprint $data;\n+\t}\n+\n+\treturn $data;\n+}\n+\n+# for $cache which can ->compute($key, $code)\n+sub cache_output_compute {\n+\tmy ($cache, $capture, $key, $code) = @_;\n+\n+\tmy $data = $cache->compute($key, sub {\n+\t\t$capture->capture($code);\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, $capture, $key, $code) = @_;\n+\n \t# check if data is in the cache\n \tmy $data = $cache->get($key);\n \n@@ -47,12 +77,6 @@ sub cache_output {\n \t\t$cache->set($key, $data) if defined $data;\n \t}\n \n-\t# print cached data\n-\tif (defined $data) {\n-\t\tbinmode STDOUT, ':raw';\n-\t\tprint $data;\n-\t}\n-\n \treturn $data;\n }\n \n-- \n1.7.3\n"},{"id":"157445","messageId":"1291677069-6559-12-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 11/24] gitweb/lib - capture output directly to cache entry file","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:56Z","receivedAt":"2010-12-06T23:10:56Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This commit makes it possible to capture output redirecting STDOUT to\ncache entry file; this way we don't need to store whole output in\nmemory in order to cache it.\n\nThe ->capture() method in GitwebCache::Capture::Simple is extended so\nit signature is now ->capture($code[, $to]).  This means that it\naccepts optional $to parameter, which specifies either filehandle open\nfor writing, or a file name, which would be used to capture output\ninstead of in-memory file.  The ->capture_start() and ->capture_stop()\nmethods that are used by ->capture() also got updated.\n\nGitwebCache::SimpleFileCache acquired new ->compute_fh($key, $code_fh)\nmethod.  Instead of passing and returning data like ->compute(), it\npasses and returns filehandles and filenames.  While $code parameter\nin ->compute($key, $code) call is expected to generate and return data\nto be cached, the $code_fh parameter in ->compute_fh($key, $code_fh)\ncall is expected to print generated data to filehandle provided as its\nparameter.  Instead of returning data (either from cache or generated)\nlike ->compute(), the new ->compute_fh() method returns filehandle and\nfilename to read data from.\n\nThe cache_output subroutine in GitwebCache::CacheOutput now uses\n->compute_fh method if cache supports it.  The contents of file that\nis returned by ->compute_fh is dumped to STDOUT using File::Copy::copy.\n\nNote that ->compute_fh interface is non-standard, and is not supported\nby any caching module known to author.  Closest to it is ->handle($key)\nmethod in the Cache[1] interface (proxied to ->entry($key)->handle()).\n\n[1]: http://p3rl.org/Cache\n\n\nAdded tests for capturing to file in t9503, and of ->compute_fh method\nin t9504.\n\nInspired-by-idea-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch was inspired of behavior of cache and capturing output in\n\"Gitweb caching v7\" series (or at least my v7.4 minimal rewrite).  It\nwas not present in previous version of this series... this means that\nit might be less well thought out.\n\n\nThe major difference from the way it is done in \"Gitweb caching v7\" by\nJ.H. is that it tries to keep those three areas: caching, capturing\nand integration with gitweb (caching captured output) separate.  As it\nwas said earlier, it allows to \"unit test\" those modules/packages\nseparately.  I think it also makes this solution easier to maintain.\n\nAs to why I didn't follow Cache interface, i.e. ->entry($key)->handle():\nit would require to create at least two additional classes, a class for\ncache entry to avoid race condition between checking availability and\ngetting ->handle(), and a IO::Handle / IO::File derivative which allows to\nspecify code ran when closing file - for locking, and for atomic write\n(unlock and rename file to final destination).  That's why I went with\n->compute_fh interface.\n\nNote that dumping generated output or output from cache to STDOUT is done\nusing File::Copy: this too was inspired by code in \"Gitweb caching v7\" by\nJ.H.  (File::Copy was first released with perl 5.002, and we require for\ngitweb at least 5.008).\n\n gitweb/lib/GitwebCache/CacheOutput.pm     |   17 ++++++++\n gitweb/lib/GitwebCache/Capture/Simple.pm  |   28 ++++++++++---\n gitweb/lib/GitwebCache/SimpleFileCache.pm |   59 +++++++++++++++++++++++++++++\n t/t9503/test_cache_interface.pl           |   32 +++++++++++++++\n t/t9504/test_capture_interface.pl         |   17 ++++++++\n 5 files changed, 146 insertions(+), 7 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/CacheOutput.pm b/gitweb/lib/GitwebCache/CacheOutput.pm\nindex 4a96ac9..7aeb895 100644\n--- a/gitweb/lib/GitwebCache/CacheOutput.pm\n+++ b/gitweb/lib/GitwebCache/CacheOutput.pm\n@@ -17,6 +17,8 @@ package GitwebCache::CacheOutput;\n use strict;\n use warnings;\n \n+use File::Copy;\n+\n use Exporter qw(import);\n our @EXPORT      = qw(cache_output capture_stop);\n our %EXPORT_TAGS = (all => [ @EXPORT ]);\n@@ -38,6 +40,21 @@ sub cache_output {\n \n \t$capture = setup_capture($capture);\n \n+\tif ($cache->can('compute_fh')) {\n+\t\tmy ($fh, $filename) = $cache->compute_fh($key, sub {\n+\t\t\tmy $fh = shift;\n+\t\t\t$capture->capture($code, $fh);\n+\t\t});\n+\n+\t\tif (defined $fh) {\n+\t\t\tbinmode $fh, ':raw';\n+\t\t\tbinmode STDOUT, ':raw';\n+\t\t\tcopy($fh, \\*STDOUT);\n+\t\t}\n+\n+\t\treturn;\n+\t}\n+\n \tmy $data;\n \tif ($cache->can('compute')) {\n \t\t$data = cache_output_compute($cache, $capture, $key, $code);\ndiff --git a/gitweb/lib/GitwebCache/Capture/Simple.pm b/gitweb/lib/GitwebCache/Capture/Simple.pm\nindex 3585e58..74dd240 100644\n--- a/gitweb/lib/GitwebCache/Capture/Simple.pm\n+++ b/gitweb/lib/GitwebCache/Capture/Simple.pm\n@@ -18,6 +18,7 @@ use strict;\n use warnings;\n \n use PerlIO;\n+use Symbol qw(qualify_to_ref);\n \n # Constructor\n sub new {\n@@ -32,7 +33,7 @@ sub new {\n sub capture {\n \tmy ($self, $code) = @_;\n \n-\t$self->capture_start();\n+\t$self->capture_start(@_[2..$#_]); # pass rest of params\n \t$code->();\n \treturn $self->capture_stop();\n }\n@@ -42,6 +43,7 @@ sub capture {\n # Start capturing data (STDOUT)\n sub capture_start {\n \tmy $self = shift;\n+\tmy $to = shift;\n \n \t# save copy of real STDOUT via duplicating it\n \tmy @layers = PerlIO::get_layers(\\*STDOUT);\n@@ -51,11 +53,23 @@ sub capture_start {\n \t# close STDOUT, so that it isn't used anymode (to have it fd0)\n \tclose STDOUT;\n \n-\t# reopen STDOUT as in-memory file\n-\t$self->{'data'} = '';\n-\tunless (open STDOUT, '>', \\$self->{'data'}) {\n-\t\topen STDOUT, '>&', fileno($self->{'orig_stdout'});\n-\t\tdie \"Couldn't reopen STDOUT as in-memory file for capture: $!\";\n+\tif (defined $to) {\n+\t\t$self->{'to'} = $to;\n+\t\tif (defined fileno(qualify_to_ref($to))) {\n+\t\t\t# if $to is filehandle, redirect\n+\t\t\topen STDOUT, '>&', fileno($to);\n+\t\t} elsif (! ref($to)) {\n+\t\t\t# if $to is name of file, open it\n+\t\t\topen STDOUT, '>', $to;\n+\t\t}\n+\n+\t} else {\n+\t\t# reopen STDOUT as in-memory file\n+\t\t$self->{'data'} = '';\n+\t\tunless (open STDOUT, '>', \\$self->{'data'}) {\n+\t\t\topen STDOUT, '>&', fileno($self->{'orig_stdout'});\n+\t\t\tdie \"Couldn't reopen STDOUT as in-memory file for capture: $!\";\n+\t\t}\n \t}\n \t_relayer(\\*STDOUT, \\@layers);\n \n@@ -76,7 +90,7 @@ sub capture_stop {\n \topen STDOUT, '>&', fileno($self->{'orig_stdout'});\n \t_relayer(\\*STDOUT, \\@layers);\n \n-\treturn $self->{'data'};\n+\treturn exists $self->{'to'} ? $self->{'to'} : $self->{'data'};\n }\n \n # taken from Capture::Tiny by David Golden, Apache License 2.0\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex 581a574..12af44f 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -438,6 +438,65 @@ sub compute {\n \treturn $data;\n }\n \n+# ......................................................................\n+# nonstandard interface methods\n+\n+sub get_fh {\n+\tmy ($self, $key) = @_;\n+\n+\tmy $path = $self->path_to_key($key);\n+\treturn unless (defined $path);\n+\n+\treturn unless ($self->is_valid($key));\n+\n+\topen my $fh, '<', $path or return;\n+\treturn ($fh, $path);\n+}\n+\n+sub set_coderef_fh {\n+\tmy ($self, $key, $code) = @_;\n+\n+\tmy $path = $self->path_to_key($key, \\my $dir);\n+\treturn unless (defined $path && defined $dir);\n+\n+\t# ensure that directory leading to cache file exists\n+\tif (!-d $dir) {\n+\t\t# mkpath will croak()/die() if there is an error\n+\t\tmkpath($dir, 0, 0777);\n+\t}\n+\n+\t# generate a temporary file\n+\tmy ($fh, $tempfile) = _tempfile_to_path($path, $dir);\n+\n+\t# code writes to filehandle or file\n+\t$code->($fh, $tempfile);\n+\n+\tclose $fh;\n+\trename($tempfile, $path)\n+\t\tor die \"Couldn't rename temporary file '$tempfile' to '$path': $!\";\n+\n+\topen $fh, '<', $path or return;\n+\treturn ($fh, $path);\n+}\n+\n+# ($fh, $filename) = $cache->compute_fh($key, $code);\n+#\n+# Combines the get and set operations in a single call.  Attempts to\n+# get $key; if successful, returns the filehandle it can be read from.\n+# Otherwise, calls $code passing filehandle to write to as a\n+# parameter; contents of this file is then used as the new value for\n+# $key; returns filehandle from which one can read newly generated data.\n+sub compute_fh {\n+\tmy ($self, $key, $code) = @_;\n+\n+\tmy ($fh, $filename) = $self->get_fh($key);\n+\tif (!defined $fh) {\n+\t\t($fh, $filename) = $self->set_coderef_fh($key, $code);\n+\t}\n+\n+\treturn ($fh, $filename);\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 9513043..0b4b09f 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -82,6 +82,38 @@ subtest 'CHI interface' => sub {\n \tdone_testing();\n };\n \n+# Test the getting and setting of a cached value\n+# (compute_fh interface)\n+#\n+$call_count = 0;\n+sub write_value {\n+\tmy $fh = shift;\n+\t$call_count++;\n+\tprint {$fh} $value;\n+}\n+sub compute_fh_output {\n+\tmy ($cache, $key, $code_fh) = @_;\n+\n+\tmy ($fh, $filename) = $cache->compute_fh($key, $code_fh);\n+\n+\tlocal $/ = undef;\n+\treturn <$fh>;\n+}\n+subtest 'compute_fh interface' => sub {\n+\tcan_ok($cache, qw(compute_fh));\n+\n+\t$cache->remove($key);\n+\tis(compute_fh_output($cache, $key, \\&write_value), $value,\n+\t   \"compute_fh 1st time (set) returns '$value'\");\n+\tis(compute_fh_output($cache, $key, \\&write_value), $value,\n+\t   \"compute_fh 2nd time (get) returns '$value'\");\n+\tis(compute_fh_output($cache, $key, \\&write_value), $value,\n+\t   \"compute_fh 3rd time (get) returns '$value'\");\n+\tcmp_ok($call_count, '==', 1, 'write_value() is called once from compute_fh');\n+\n+\tdone_testing();\n+};\n+\n # Test cache expiration\n #\n subtest 'cache expiration' => sub {\ndiff --git a/t/t9504/test_capture_interface.pl b/t/t9504/test_capture_interface.pl\nindex 47ab804..26c9303 100755\n--- a/t/t9504/test_capture_interface.pl\n+++ b/t/t9504/test_capture_interface.pl\n@@ -6,6 +6,7 @@ use strict;\n use utf8;\n \n use Test::More;\n+use File::Compare;\n \n # test source version\n use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n@@ -84,6 +85,22 @@ SKIP: {\n \tis($captured, '', \"doesn't print while capturing\");\n }\n \n+# Test capturing to file\n+#\n+my $test_data = 'Capture this';\n+open my $fh, '>', 'expected' or die \"Couldn't open file for writing: $!\";\n+print {$fh} $test_data;\n+close $fh;\n+\n+$capture->capture(sub { print $test_data; }, 'actual');\n+cmp_ok(compare('expected', 'actual'), '==', 0, 'capturing to file via filename');\n+\n+open my $fh, '>', 'actual' or die \"Couldn't open file for writing: $!\";\n+$capture->capture(sub { print $test_data; }, $fh);\n+close $fh;\n+cmp_ok(compare('expected', 'actual'), '==', 0, 'capturing to file via filehandle');\n+\n+\n done_testing();\n \n # Local Variables:\n-- \n1.7.3\n"},{"id":"157453","messageId":"1291677069-6559-13-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 12/24] gitweb/lib - Use locking to avoid 'cache miss stampede' problem","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:57Z","receivedAt":"2010-12-06T23:10:57Z","isPatch":true,"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.  This also means that if some data is not cached (like e.g. error\npages in gitweb), then it would work correctly even in rare case of\nsimultaneous access to the same non-cached entry.\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\nsituation when those clients are just using ->get/->set methods.\n\nThe situation where subroutine used to generate data dies is also\ntested in t9503.\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 idea of using locking to ensure that only one precess is updating\ncache entry was inspired by original patch by J.H. about output caching\nin gitweb (and perhaps also, to lesser extent, locking in Cache module).\n\nThe way file locking is implemented, with a separate *.lock file, was\nbased in\n\n  \"File Locking Tricks and Traps\" (http://perl.plover.com/yak/flock/)\n\nIn \"Gitweb caching v7\" locking is done usually on the cache entry file,\ni.e. one read from/written to.  It looks like there are in some cases\nmultiple locks for the same entry...\n\nThis code does not use LOCK_UN: closing lockfile would unlock.  LOCK_UN\ndoesn't always work as intended (see mentioned article; also tested that\nLOCK_UN does not work found by tests provided, either this or in the\nfuture commit).\n\nNote that flock(..., LOCK_EX | LOCK_NB) is used to select the process\nthat would be (re)generating cache entry, and it is used to create\ncritical section - this means that it is unlocked when cache entry file\nis written in full.  flock(..., LOCK_SH) on the other hand is used to\nwait for aid process to finish; it is used only for synchronization.\nIn sane situation, i.e. when cache expiration time is longer than it\ntakes to read from cache, shared (readers) lock can be released just\nafter acquiring, but just in case opening file is in critical section\nprotected by this lock.\n\n->compute and ->compute_fh ensure that data is generated even if process\ngenerating data fails... unless no process can generate data.  It is\ndone thank to loop that waits either for data, or for a chance to\nregenerate it themselves.  This shouldn't matter in most situations.\n\n\nCompared to previous version of this series there was no ->compute_fh\n(and was no common ->_compute_generic).\n\n\nAlternate solution to locking to prevent 'cache miss stampede' problem,\nis \"expires_variance\" (float) parameter used by CHI, which allows items\nto expire little earlier than stated expiration time.  CHI also has\ndeprecated \"busy_lock\" (duration) which bumps expiration time, but it\ndoubles number of writes.\n\nComparing (benchmarking) those different solutions is yet to be done...\n\n gitweb/Makefile                                |    1 +\n gitweb/README                                  |    6 +-\n gitweb/gitweb.perl                             |    4 +-\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |  147 ++++++++++++\n t/t9503/test_cache_interface.pl                |  283 ++++++++++++++++++++++--\n 5 files changed, 424 insertions(+), 17 deletions(-)\n create mode 100644 gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex d67c138..acc4f5c 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -116,6 +116,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/Simple.pm\n \n GITWEB_REPLACE = \\\ndiff --git a/gitweb/README b/gitweb/README\nindex 3dc01bd..98e8101 100644\n--- a/gitweb/README\n+++ b/gitweb/README\n@@ -351,8 +351,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 12e04a1..72683be 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -277,7 +277,7 @@ 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 # or to name of class of cache interface implementing said methods.\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@@ -1245,7 +1245,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..1ea0e60\n--- /dev/null\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -0,0 +1,147 @@\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+# Ensures that file can be created, if needed.\n+sub get_lockname {\n+\tmy ($self, $key) = @_;\n+\n+\tmy $lockfile = $self->path_to_key($key, \\my $dir) . '.lock';\n+\n+\t# ensure that directory leading to lockfile exists\n+\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+\treturn $lockfile;\n+}\n+\n+# ......................................................................\n+# interface methods\n+\n+sub _compute_generic {\n+\tmy ($self, $key,\n+\t    $get_code, $set_code, $get_locked) = @_;\n+\n+\tmy @result = $get_code->();\n+\treturn @result if @result;\n+\n+\tmy $lockfile = $self->get_lockname($key);\n+\n+\t# this loop is to protect against situation where process that\n+\t# acquired exclusive lock (writer) dies or exits (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@result = $set_code->();\n+\n+\t\t\t# closing lockfile releases lock\n+\t\t\tclose $lock_fh\n+\t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n+\n+\t\t} else {\n+\t\t\t# get readers lock (wait for writer)\n+\t\t\tflock($lock_fh, LOCK_SH);\n+\t\t\t# closing lockfile releases lock\n+\t\t\tif ($get_locked) {\n+\t\t\t\t@result = $get_code->();\n+\t\t\t\tclose $lock_fh\n+\t\t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n+\t\t\t} else {\n+\t\t\t\tclose $lock_fh\n+\t\t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n+\t\t\t\t@result = $get_code->();\n+\t\t\t}\n+\t\t}\n+\t} until (@result || $lock_state);\n+\t# repeat until we have data, or we tried generating data oneself and failed\n+\treturn @result;\n+}\n+\n+# $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+\treturn ($self->_compute_generic($key,\n+\t\tsub {\n+\t\t\treturn $self->get($key);\n+\t\t},\n+\t\tsub {\n+\t\t\tmy $data = $code->();\n+\t\t\t$self->set($key, $data);\n+\t\t\treturn $data;\n+\t\t},\n+\t\t0 # $self->get($key); is outside LOCK_SH critical section\n+\t))[0]; # return single value: $data\n+}\n+\n+# ($fh, $filename) = $cache->compute_fh($key, $code);\n+#\n+# Combines the get and set operations in a single call.  Attempts to\n+# get $key; if successful, returns the filehandle it can be read from.\n+# Otherwise, calls $code passing filehandle to write to as a\n+# parameter; contents of this file is then used as the new value for\n+# $key; returns filehandle from which one can read newly generated data.\n+#\n+# Uses file locking to have only one process updating value for $key\n+# to avoid 'cache miss stampede' (aka 'stampeding herd') problem.\n+sub compute_fh {\n+\tmy ($self, $key, $code_fh) = @_;\n+\n+\treturn $self->_compute_generic($key,\n+\t\tsub {\n+\t\t\treturn $self->get_fh($key);\n+\t\t},\n+\t\tsub {\n+\t\t\treturn $self->set_coderef_fh($key, $code_fh);\n+\t\t},\n+\t\t1 # $self->get_fh($key); just opens file\n+\t);\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 0b4b09f..c6a28f8 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -4,6 +4,12 @@ use lib (split(/:/, $ENV{GITPERLLIB}));\n use warnings;\n use strict;\n \n+use POSIX qw(dup2);\n+use Fcntl qw(:DEFAULT);\n+use IO::Handle;\n+use IO::Select;\n+use IO::Pipe;\n+\n use Test::More;\n \n # test source version\n@@ -12,11 +18,12 @@ use lib $ENV{GITWEBLIBDIR} || \"$ENV{GIT_BUILD_DIR}/gitweb/lib\";\n \n # Test creating a cache\n #\n-BEGIN { use_ok('GitwebCache::SimpleFileCache'); }\n+BEGIN { use_ok('GitwebCache::FileCacheWithLocking'); }\n diag(\"Using lib '$INC[0]'\");\n-diag(\"Testing '$INC{'GitwebCache/SimpleFileCache.pm'}'\");\n+diag(\"Testing '$INC{'GitwebCache/FileCacheWithLocking.pm'}'\");\n \n-my $cache = new_ok('GitwebCache::SimpleFileCache');\n+my $cache = new_ok('GitwebCache::FileCacheWithLocking');\n+isa_ok($cache, 'GitwebCache::SimpleFileCache');\n \n # Test that default values are defined\n #\n@@ -39,6 +46,9 @@ SKIP: {\n \t\t\"default cache depth is $GitwebCache::SimpleFileCache::DEFAULT_CACHE_DEPTH\");\n }\n \n+# ----------------------------------------------------------------------\n+# CACHE API\n+\n # Test the getting, setting, and removal of a cached value\n # (Cache::Cache interface)\n #\n@@ -91,23 +101,15 @@ sub write_value {\n \t$call_count++;\n \tprint {$fh} $value;\n }\n-sub compute_fh_output {\n-\tmy ($cache, $key, $code_fh) = @_;\n-\n-\tmy ($fh, $filename) = $cache->compute_fh($key, $code_fh);\n-\n-\tlocal $/ = undef;\n-\treturn <$fh>;\n-}\n subtest 'compute_fh interface' => sub {\n \tcan_ok($cache, qw(compute_fh));\n \n \t$cache->remove($key);\n-\tis(compute_fh_output($cache, $key, \\&write_value), $value,\n+\tis(cache_compute_fh($cache, $key, \\&write_value), $value,\n \t   \"compute_fh 1st time (set) returns '$value'\");\n-\tis(compute_fh_output($cache, $key, \\&write_value), $value,\n+\tis(cache_compute_fh($cache, $key, \\&write_value), $value,\n \t   \"compute_fh 2nd time (get) returns '$value'\");\n-\tis(compute_fh_output($cache, $key, \\&write_value), $value,\n+\tis(cache_compute_fh($cache, $key, \\&write_value), $value,\n \t   \"compute_fh 3rd time (get) returns '$value'\");\n \tcmp_ok($call_count, '==', 1, 'write_value() is called once from compute_fh');\n \n@@ -166,4 +168,257 @@ subtest 'adaptive cache expiration' => sub {\n \n $cache->set_expires_in(-1);\n \n+# ----------------------------------------------------------------------\n+# CONCURRENT ACCESS\n+sub parallel_run (&); # forward declaration of prototype\n+\n+# Test 'stampeding herd' / 'cache miss stampede' problem\n+#\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+sub get_value_slow_fh {\n+\tmy $fh = shift;\n+\n+\t$call_count++;\n+\tsleep $slow_time;\n+\tprint {$fh} $value;\n+}\n+\n+sub get_value_die {\n+\t$call_count++;\n+\tdie \"get_value_die\\n\";\n+}\n+\n+my $lock_file = \"$0.$$.lock\";\n+sub get_value_die_once {\n+\tif (sysopen my $fh, $lock_file, (O_WRONLY | O_CREAT | O_EXCL)) {\n+\t\tclose $fh;\n+\t\tdie \"get_value_die_once\\n\";\n+\t} else {\n+\t\tsleep $slow_time;\n+\t\treturn $value;\n+\t}\n+}\n+\n+my @output;\n+my $sep = '|';\n+my $total_count = 0;\n+\n+note(\"Following tests contain artifical delay of $slow_time seconds\");\n+subtest 'parallel access' => sub {\n+\t$cache->remove($key);\n+\t@output = parallel_run {\n+\t\t$call_count = 0;\n+\t\tmy $data = cache_get_set($cache, $key, \\&get_value_slow);\n+\t\tprint \"$data$sep$call_count\";\n+\t};\n+\t$total_count = 0;\n+\tforeach (@output) {\n+\t\tmy ($child_out, $child_count) = split(quotemeta $sep, $_);\n+\t\t#is($child_out, $value, \"get/set (parallel) returns '$value'\");\n+\t\t$total_count += $child_count;\n+\t}\n+\tcmp_ok($total_count, '==', 2, 'parallel get/set: get_value_slow() called twice');\n+\n+\t$cache->remove($key);\n+\t@output = parallel_run {\n+\t\t$call_count = 0;\n+\t\tmy $data = cache_compute($cache, $key, \\&get_value_slow);\n+\t\tprint \"$data$sep$call_count\";\n+\t};\n+\t$total_count = 0;\n+\tforeach (@output) {\n+\t\tmy ($child_out, $child_count) = split(quotemeta $sep, $_);\n+\t\t#is($child_out, $value, \"compute (parallel) returns '$value'\");\n+\t\t$total_count += $child_count;\n+\t}\n+\tcmp_ok($total_count, '==', 1, 'parallel compute: get_value_slow() called once');\n+\n+\t$cache->remove($key);\n+\t@output = parallel_run {\n+\t\t$call_count = 0;\n+\t\tmy $data = cache_compute_fh($cache, $key, \\&get_value_slow_fh);\n+\t\tprint \"$data$sep$call_count\";\n+\t};\n+\t$total_count = 0;\n+\tforeach (@output) {\n+\t\tmy ($child_out, $child_count) = split(quotemeta $sep, $_);\n+\t\t#is($child_out, $value, \"compute_fh (parallel) returns '$value'\");\n+\t\t$total_count += $child_count;\n+\t}\n+\tcmp_ok($total_count, '==', 1, 'parallel compute_fh: get_value_slow_fh() called once');\n+\n+\teval {\n+\t\tlocal $SIG{ALRM} = sub { die \"alarm\\n\"; };\n+\t\talarm 4*$slow_time;\n+\n+\t\t@output = parallel_run {\n+\t\t\t$call_count = 0;\n+\t\t\tmy $data = eval { cache_compute($cache, 'No Key', \\&get_value_die); };\n+\t\t\tmy $eval_error = $@;\n+\t\t\tprint \"$data\" if defined $data;\n+\t\t\tprint \"$sep\";\n+\t\t\tprint \"$eval_error\" if defined $eval_error;\n+\t\t};\n+\t\tis_deeply(\n+\t\t\t\\@output,\n+\t\t\t[ ( \"${sep}get_value_die\\n\" ) x 2 ],\n+\t\t\t'parallel compute: get_value_die() died in both'\n+\t\t);\n+\n+\t\talarm 0;\n+\t};\n+\tok(!$@, 'parallel compute: no alarm call (neither process hung)');\n+\tdiag($@) if $@;\n+\n+\t$cache->remove($key);\n+\tunlink($lock_file);\n+\t@output = parallel_run {\n+\t\tmy $data = eval { cache_compute($cache, $key, \\&get_value_die_once); };\n+\t\tmy $eval_error = $@;\n+\t\tprint \"$data\" if defined $data;\n+\t\tprint \"$sep\";\n+\t\tprint \"$eval_error\" if defined $eval_error;\n+\t};\n+\tis_deeply(\n+\t\t[sort @output],\n+\t\t[sort (\"$value$sep\", \"${sep}get_value_die_once\\n\")],\n+\t\t'parallel compute: return correct value even if other process died'\n+\t);\n+\tunlink($lock_file);\n+\n+\tdone_testing();\n+};\n+\n done_testing();\n+\n+\n+#######################################################################\n+#######################################################################\n+#######################################################################\n+\n+# use ->get($key) and ->set($key, $data) interface\n+sub cache_get_set {\n+\tmy ($cache, $key, $code) = @_;\n+\n+\tmy $data = $cache->get($key);\n+\tif (!defined $data) {\n+\t\t$data = $code->();\n+\t\t$cache->set($key, $data);\n+\t}\n+\n+\treturn $data;\n+}\n+\n+# use ->compute($key, $code) interface\n+sub cache_compute {\n+\tmy ($cache, $key, $code) = @_;\n+\n+\tmy $data = $cache->compute($key, $code);\n+\treturn $data;\n+}\n+# use ->compute_fh($key, $code_fh) interface\n+sub cache_compute_fh {\n+\tmy ($cache, $key, $code_fh) = @_;\n+\n+\tmy ($fh, $filename) = $cache->compute_fh($key, $code_fh);\n+\n+\tlocal $/ = undef;\n+\treturn <$fh>;\n+}\n+\n+# from http://aaroncrane.co.uk/talks/pipes_and_processes/\n+sub fork_child (&) {\n+\tmy ($child_process_code) = @_;\n+\n+\tmy $pid = fork();\n+\tdie \"Failed to fork: $!\\n\" if !defined $pid;\n+\n+\treturn $pid if $pid != 0;\n+\n+\t# Now we're in the new child process\n+\t$child_process_code->();\n+\texit;\n+}\n+\n+sub parallel_run (&) {\n+\tmy $child_code = shift;\n+\tmy $nchildren = 2;\n+\n+\tmy %children;\n+\tmy (%pid_for_child, %fd_for_child);\n+\tmy $sel = IO::Select->new();\n+\tforeach my $child_idx (1..$nchildren) {\n+\t\tmy $pipe = IO::Pipe->new()\n+\t\t\tor die \"Failed to create pipe: $!\\n\";\n+\n+\t\tmy $pid = fork_child {\n+\t\t\t$pipe->writer()\n+\t\t\t\tor die \"$$: Child \\$pipe->writer(): $!\\n\";\n+\t\t\tdup2(fileno($pipe), fileno(STDOUT))\n+\t\t\t\tor die \"$$: Child $child_idx failed to reopen stdout to pipe: $!\\n\";\n+\t\t\tclose $pipe\n+\t\t\t\tor die \"$$: Child $child_idx failed to close pipe: $!\\n\";\n+\n+\t\t\t# From Test-Simple-0.96/t/subtest/fork.t\n+\t\t\t#\n+\t\t\t# Force all T::B output into the pipe (redirected to STDOUT),\n+\t\t\t# for the parent builder as well as the current subtest builder.\n+\t\t\t{\n+\t\t\t\tno warnings 'redefine';\n+\t\t\t\t*Test::Builder::output         = sub { *STDOUT };\n+\t\t\t\t*Test::Builder::failure_output = sub { *STDOUT };\n+\t\t\t\t*Test::Builder::todo_output    = sub { *STDOUT };\n+\t\t\t}\n+\n+\t\t\t$child_code->();\n+\n+\t\t\t*STDOUT->flush();\n+\t\t\tclose(STDOUT);\n+\t\t};\n+\n+\t\t$pid_for_child{$pid} = $child_idx;\n+\t\t$pipe->reader()\n+\t\t\tor die \"Failed to \\$pipe->reader(): $!\\n\";\n+\t\t$fd_for_child{$pipe} = $child_idx;\n+\t\t$sel->add($pipe);\n+\n+\t\t$children{$child_idx} = {\n+\t\t\t'pid'    => $pid,\n+\t\t\t'stdout' => $pipe,\n+\t\t\t'output' => '',\n+\t\t};\n+\t}\n+\n+\twhile (my @ready = $sel->can_read()) {\n+\t\tforeach my $fh (@ready) {\n+\t\t\tmy $buf = '';\n+\t\t\tmy $nread = sysread($fh, $buf, 1024);\n+\n+\t\t\texists $fd_for_child{$fh}\n+\t\t\t\tor die \"Cannot find child for fd: $fh\\n\";\n+\n+\t\t\tif ($nread > 0) {\n+\t\t\t\t$children{$fd_for_child{$fh}}{'output'} .= $buf;\n+\t\t\t} else {\n+\t\t\t\t$sel->remove($fh);\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\twhile (%pid_for_child) {\n+\t\tmy $pid = waitpid -1, 0;\n+\t\twarn \"Child $pid_for_child{$pid} ($pid) failed with status: $?\\n\"\n+\t\t\tif $? != 0;\n+\t\tdelete $pid_for_child{$pid};\n+\t}\n+\n+\treturn map { $children{$_}{'output'} } keys %children;\n+}\n+\n+__END__\n-- \n1.7.3\n"},{"id":"157452","messageId":"1291677069-6559-14-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 13/24] gitweb/lib - No need for File::Temp when locking","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:58Z","receivedAt":"2010-12-06T23:10:58Z","isPatch":true,"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\nThe _tempfile_to_path subroutine got promoted to _tempfile_to_path\nmethod, because we want to choose correct one dynamically, based on\nthe type of object (polymorphism).\n\nIdea-inspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nIn previous version of this patch _tempfile_to_path was subroutine\nrather than method... which mans that GitwebCache::SimpleFileCache\nversion was being picked up because that is the package where methods\nthat use it are.\n\n\"Gitweb caching v7\" either writes directly to cache entry file, or\nuses file with name based on original cache entry as temporary output\nfile; the latter is just like GitwebCache::FileCacheWithLocking does\nit after this patch.\n\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   16 ++++++++++++++++\n gitweb/lib/GitwebCache/SimpleFileCache.pm      |    6 +++---\n 2 files changed, 19 insertions(+), 3 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex 1ea0e60..4d8114d 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -46,6 +46,22 @@ sub get_lockname {\n \treturn $lockfile;\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 ($self, $file, $dir) = @_;\n+\n+\tmy $tempname = \"$file.tmp\";\n+\topen my $temp_fh, '>', $tempname\n+\t\tor die \"Couldn't open temporary file '$tempname' for writing: $!\";\n+\n+\treturn ($temp_fh, $tempname);\n+}\n+\n # ......................................................................\n # interface methods\n \ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex 12af44f..aeb91d4 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -288,7 +288,7 @@ sub write_fh {\n # return filehandle and filename of open temporary file,\n # like File::Temp::tempfile\n sub _tempfile_to_path {\n-\tmy ($file, $dir) = @_;\n+\tmy ($self, $file, $dir) = @_;\n \n \t# tempfile will croak() if there is an error\n \treturn tempfile(\"${file}_XXXXX\",\n@@ -324,7 +324,7 @@ sub store {\n \t}\n \n \t# generate a temporary file\n-\tmy ($temp_fh, $tempname) = _tempfile_to_path($file, $dir);\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@@ -466,7 +466,7 @@ sub set_coderef_fh {\n \t}\n \n \t# generate a temporary file\n-\tmy ($fh, $tempfile) = _tempfile_to_path($path, $dir);\n+\tmy ($fh, $tempfile) = $self->_tempfile_to_path($path, $dir);\n \n \t# code writes to filehandle or file\n \t$code->($fh, $tempfile);\n-- \n1.7.3\n"},{"id":"157447","messageId":"1291677069-6559-15-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 14/24] gitweb/lib - Serve stale data when waiting for filling cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:10:59Z","receivedAt":"2010-12-06T23:10:59Z","isPatch":true,"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\n->update($key, $code) and ->update_fh($key, $code_fh) methods.  It is\ncontrolled by 'max_lifetime' cache parameter; you can set it to -1 to\nalways serve stale data if it exists, and you can set it to 0 (or any\nvalue smaller than '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 can show stale (expired) data immediately.  In order\nto remove or reduce this asymmetry gitweb would need to employ one of\ntwo alternate 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\nNote that process that got stale data serves it immediately, therefore\nit wouldn't be available to regenerate data if process regenerating\ndata died; see commented-out TODO test in t9503.  Otherwise it would\nhave to wait to check if data got regenerated, which would negate the\nidea of serving stale data for a fast return.\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\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nCompared to version in previous version of this series the parallel\naccess test got much improved (this actually started in earlier\ncommit).\n\nThis is the part that is possible _without_ regenerating cache in\nbackground.\n\nNote that here it is explicit that serving stale data when some\nprocess is regenerating cache is possible only with locking enabled,\ni.e. when using GitwebCache::FileCacheWithLocking.\n\n gitweb/gitweb.perl                             |    8 ++\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |  105 ++++++++++++++++++++++--\n gitweb/lib/GitwebCache/SimpleFileCache.pm      |   22 ++++--\n t/t9503/test_cache_interface.pl                |   68 +++++++++++++++-\n 4 files changed, 189 insertions(+), 14 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 72683be..454766c 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -327,6 +327,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 # Set to _initialized_ instance of GitwebCache::Capture compatibile capturing\n # engine, i.e. one implementing ->new() constructor, and ->capture($code)\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex 4d8114d..1d32810 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 $class = shift;\n+\tmy %opts = ref $_[0] ? %{ $_[0] } : @_;\n+\n+\tmy $self = $class->SUPER::new(\\%opts);\n+\n+\tmy ($max_lifetime);\n+\tif (%opts) {\n+\t\t$max_lifetime =\n+\t\t\t$opts{'max_lifetime'} ||\n+\t\t\t$opts{'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@@ -67,7 +148,7 @@ sub _tempfile_to_path {\n \n sub _compute_generic {\n \tmy ($self, $key,\n-\t    $get_code, $set_code, $get_locked) = @_;\n+\t    $get_code, $fetch_code, $set_code, $fetch_locked) = @_;\n \n \tmy @result = $get_code->();\n \treturn @result if @result;\n@@ -91,17 +172,23 @@ sub _compute_generic {\n \t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n \n \t\t} else {\n+\t\t\t# try to retrieve stale data\n+\t\t\t@result = $fetch_code->()\n+\t\t\t\tif $self->is_valid($key, $self->get_max_lifetime());\n+\t\t\treturn @result if @result;\n+\n \t\t\t# get readers lock (wait for writer)\n+\t\t\t# if there is no stale data to serve\n \t\t\tflock($lock_fh, LOCK_SH);\n \t\t\t# closing lockfile releases lock\n-\t\t\tif ($get_locked) {\n-\t\t\t\t@result = $get_code->();\n+\t\t\tif ($fetch_locked) {\n+\t\t\t\t@result = $fetch_code->();\n \t\t\t\tclose $lock_fh\n \t\t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n \t\t\t} else {\n \t\t\t\tclose $lock_fh\n \t\t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n-\t\t\t\t@result = $get_code->();\n+\t\t\t\t@result = $fetch_code->();\n \t\t\t}\n \t\t}\n \t} until (@result || $lock_state);\n@@ -126,6 +213,9 @@ sub compute {\n \t\t\treturn $self->get($key);\n \t\t},\n \t\tsub {\n+\t\t\treturn $self->fetch($key);\n+\t\t},\n+\t\tsub {\n \t\t\tmy $data = $code->();\n \t\t\t$self->set($key, $data);\n \t\t\treturn $data;\n@@ -152,9 +242,12 @@ sub compute_fh {\n \t\t\treturn $self->get_fh($key);\n \t\t},\n \t\tsub {\n+\t\t\treturn $self->fetch_fh($key);\n+\t\t},\n+\t\tsub {\n \t\t\treturn $self->set_coderef_fh($key, $code_fh);\n \t\t},\n-\t\t1 # $self->get_fh($key); just opens file\n+\t\t1 # $self->fetch_fh($key); just opens file\n \t);\n }\n \ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex aeb91d4..21ec434 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -365,12 +365,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@@ -383,7 +384,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?\n@@ -441,18 +442,25 @@ sub compute {\n # ......................................................................\n # nonstandard interface methods\n \n-sub get_fh {\n+sub fetch_fh {\n \tmy ($self, $key) = @_;\n \n \tmy $path = $self->path_to_key($key);\n \treturn unless (defined $path);\n \n-\treturn unless ($self->is_valid($key));\n-\n \topen my $fh, '<', $path or return;\n \treturn ($fh, $path);\n }\n \n+\n+sub get_fh {\n+\tmy ($self, $key) = @_;\n+\n+\treturn unless ($self->is_valid($key));\n+\n+\treturn $self->fetch_fh($key);\n+}\n+\n sub set_coderef_fh {\n \tmy ($self, $key, $code) = @_;\n \ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex c6a28f8..8a52261 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -22,7 +22,9 @@ BEGIN { use_ok('GitwebCache::FileCacheWithLocking'); }\n diag(\"Using lib '$INC[0]'\");\n diag(\"Testing '$INC{'GitwebCache/FileCacheWithLocking.pm'}'\");\n \n-my $cache = new_ok('GitwebCache::FileCacheWithLocking');\n+my $cache = new_ok('GitwebCache::FileCacheWithLocking', [ {\n+\t'max_lifetime' => 0, # turn it off\n+} ]);\n isa_ok($cache, 'GitwebCache::SimpleFileCache');\n \n # Test that default values are defined\n@@ -295,6 +297,70 @@ subtest 'parallel access' => sub {\n \tdone_testing();\n };\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+\n+subtest 'serving stale data when (re)generating' => sub {\n+\t$cache->set($key, $stale_value);\n+\t$call_count = 0;\n+\t$cache->set_expires_in(0);    # expire now\n+\t$cache->set_max_lifetime(-1); # forever (always serve stale data)\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute($cache, $key, \\&get_value_slow);\n+\t\tprint $data;\n+\t};\n+\tok(scalar(grep { $_ eq $stale_value } @output),\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,\n+\t   'value got set correctly, even if stale data returned');\n+\n+\n+# \t$cache->set($key, $stale_value);\n+# \tunlink($lock_file);\n+# \t@output = parallel_run {\n+# \t\tmy $data = eval { cache_compute($cache, $key, \\&get_value_die_once); };\n+# \t\tmy $eval_error = $@;\n+# \t\tprint \"$data\" if defined $data;\n+# \t\tprint \"$sep\";\n+# \t\tprint \"$eval_error\" if defined $eval_error;\n+# \t};\n+#  TODO: {\n+# \t\tlocal $TODO = 'not implemented';\n+#\n+# \t\tis_deeply(\n+# \t\t\t[sort @output],\n+# \t\t\t[sort (\"$value${sep}\", \"${sep}get_value_die_once\\n\")],\n+# \t\t\t'return non-stale value, even if process regenerating it died'\n+# \t\t);\n+#\n+# \t\t$cache->set_expires_in(-1); # never expire for next ->get\n+# \t\tis($cache->get($key), $value,\n+# \t\t   'value got regenerated, even if process regenerating it died');\n+# \t};\n+# \tunlink($lock_file);\n+\n+\t$cache->set($key, $stale_value);\n+\t$cache->set_expires_in(0);   # expire now\n+\t$cache->set_max_lifetime(0); # don't serve stale data\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute($cache, $key, \\&get_value_slow);\n+\t\tprint $data;\n+\t};\n+\t# no returning stale data\n+\tok(!scalar(grep { $_ eq $stale_value } @output),\n+\t   'no stale data if configured');\n+\n+\n+\tdone_testing();\n+};\n+$cache->set_expires_in(-1);\n+\n done_testing();\n \n \n-- \n1.7.3\n"},{"id":"157467","messageId":"1291677069-6559-16-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 15/24] gitweb/lib - Regenerate (refresh) cache in background","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:00Z","receivedAt":"2010-12-06T23:11:00Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"This commit removes asymmetry in serving stale data (if stale data exists)\nwhen regenerating cache in GitwebCache::FileCacheWithLocking.  The process\nthat acquired exclusive (writers) lock, and is therefore selected to\nbe the one that (re)generates data to fill the cache, can now generate\ndata in background, while serving stale data.\n\nThose background processes are daemonized, i.e. detached from the main\nprocess (the one returning data or stale data).  Otherwise there might be a\nproblem when gitweb is running as (part of) long-lived process, for example\nfrom mod_perl or from FastCGI: it would leave unreaped children as zombies\n(entries in process table).  We don't want to wait for background process,\nand we can't set $SIG{CHLD} to 'IGNORE' in gitweb to automatically reap\nchild processes, because this interferes with using\n  open my $fd, '-|', git_cmd(), 'param', ...\n    or die_error(...)\n  # read from <$fd>\n  close $fd\n    or die_error(...)\nIn the above code \"close\" for magic \"-|\" open calls waitpid...  and we\nwould would die with \"No child processes\".  Removing 'or die' would\npossibly remove ability to react to other errors.\n\nThis feature can be enabled or disabled on demand via 'background_cache'\ncache parameter.  It is turned on by default.\n\n\nThe t9503 test got updated to test both case with background generation\nenabled and case with background generation disabled.\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nCompared to previous version of this series ->_set_maybe_background\nwas extracted (refactored) from ->_compute_generic (in earlier version\nit would be just ->compute).  This hopefully makes code easier to\nunderstand.\n\nDifferences to approach taken in \"Gitweb caching v7\" by J.H.\n* It is made explicit that background generation depends on using\n  locking.  It doesn't matter for J.H. series, as you canot turn off\n  using locking there.\n\n* Forking (running generating process in background) is done only if\n  there is stale data to serve (and if background cache is turned on).\n  In J.H. series forking was done unconditionally, only generation or\n  exit depended on $backgroundCache (and technical/for debugging\n  $cacheDoFork).\n\n* Locking is done before forking, as forking background process is done\n  only for the process regenerating cache.\n\n* Daemonizes background process, detaching it from parent (using\n  setsid).  This way whether main process is short-lived (gitweb as CGI)\n  or long-lived (mod_perl, PSGI or FastCGI), there would be no need to\n  wait and no zombies.\n\n gitweb/gitweb.perl                             |    9 +++\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   64 ++++++++++++++++++++++--\n t/t9503/test_cache_interface.pl                |   40 ++++++++++++++-\n 3 files changed, 106 insertions(+), 7 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 454766c..f202d6b 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -335,6 +335,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 # Set to _initialized_ instance of GitwebCache::Capture compatibile capturing\n # engine, i.e. one implementing ->new() constructor, and ->capture($code)\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex 1d32810..82e88f1 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 $class = shift;\n \tmy %opts = ref $_[0] ? %{ $_[0] } : @_;\n \n \tmy $self = $class->SUPER::new(\\%opts);\n \n-\tmy ($max_lifetime);\n+\tmy ($max_lifetime, $background_cache);\n \tif (%opts) {\n \t\t$max_lifetime =\n \t\t\t$opts{'max_lifetime'} ||\n \t\t\t$opts{'max_cache_lifetime'};\n+\t\t$background_cache = $opts{'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@@ -146,6 +153,52 @@ sub _tempfile_to_path {\n # ......................................................................\n # interface methods\n \n+sub _set_maybe_background {\n+\tmy ($self, $key, $fetch_code, $set_code) = @_;\n+\n+\tmy $pid;\n+\tmy (@result, @stale_result);\n+\n+\tif ($self->{'background_cache'}) {\n+\t\t# try to retrieve stale data\n+\t\t@stale_result = $fetch_code->()\n+\t\t\tif $self->is_valid($key, $self->get_max_lifetime());\n+\n+\t\t# fork if there is stale data, for background process\n+\t\t# to regenerate/refresh the cache (generate data)\n+\t\t$pid = fork() if (@stale_result);\n+\t}\n+\n+\tif ($pid) {\n+\t\t## forked and are in parent process\n+\t\t# reap child, which spawned grandchild process (detaching it)\n+\t\twaitpid $pid, 0;\n+\n+\t}\telse {\n+\t\t## didn't fork, or are in background process\n+\n+\t\t# daemonize background process, detaching it from parent\n+\t\t# see also Proc::Daemonize, Apache2::SubProcess\n+\t\tif (defined $pid) {\n+\t\t\t## in background process\n+\t\t\tPOSIX::setsid(); # or setpgrp(0, 0);\n+\t\t\tfork() && CORE::exit(0);\n+\t\t}\n+\n+\t\t@result = $set_code->();\n+\n+\t\tif (defined $pid) {\n+\t\t\t## in background process; parent will serve stale data\n+\n+\t\t\t# lockfile will be automatically closed on exit,\n+\t\t\t# and therefore lockfile would be unlocked\n+\t\t\tCORE::exit(0);\n+\t\t}\n+\t}\n+\n+\treturn @result > 0 ? @result : @stale_result;\n+}\n+\n sub _compute_generic {\n \tmy ($self, $key,\n \t    $get_code, $fetch_code, $set_code, $fetch_locked) = @_;\n@@ -162,16 +215,19 @@ sub _compute_generic {\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@result = $set_code->();\n+\t\t\t## acquired writers lock, have to generate data\n+\t\t\t@result = $self->_set_maybe_background($key, $fetch_code, $set_code);\n \n \t\t\t# closing lockfile releases lock\n \t\t\tclose $lock_fh\n \t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n \n \t\t} else {\n+\t\t\t## didn't acquire writers lock, get stale data or wait for regeneration\n+\n \t\t\t# try to retrieve stale data\n \t\t\t@result = $fetch_code->()\n \t\t\t\tif $self->is_valid($key, $self->get_max_lifetime());\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 8a52261..7f08863 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -24,9 +24,13 @@ diag(\"Testing '$INC{'GitwebCache/FileCacheWithLocking.pm'}'\");\n \n my $cache = new_ok('GitwebCache::FileCacheWithLocking', [ {\n \t'max_lifetime' => 0, # turn it off\n+\t'background_cache' => 0,\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@@ -303,6 +307,9 @@ subtest 'parallel access' => sub {\n my $stale_value = 'Stale Value';\n \n subtest 'serving stale data when (re)generating' => sub {\n+\t# without background generation\n+\t$cache->set_background_cache(0);\n+\n \t$cache->set($key, $stale_value);\n \t$call_count = 0;\n \t$cache->set_expires_in(0);    # expire now\n@@ -312,12 +319,39 @@ subtest 'serving stale data when (re)generating' => sub {\n \t\tmy $data = cache_compute($cache, $key, \\&get_value_slow);\n \t\tprint $data;\n \t};\n-\tok(scalar(grep { $_ eq $stale_value } @output),\n-\t   'stale data in at least one process when expired');\n+\t# returning stale data works\n+\tis_deeply(\n+\t\t[sort @output],\n+\t\t[sort ($value, $stale_value)],\n+\t\t'no background: stale data returned by one process'\n+\t);\n+\n+\t$cache->set_expires_in(-1); # never expire for next ->get\n+\tis($cache->get($key), $value,\n+\t   'no background: value got set correctly, even if stale data returned');\n+\n+\n+\t# with background generation\n+\t$cache->set_background_cache(1);\n+\n+\t$cache->set($key, $stale_value);\n+\t$cache->set_expires_in(0);    # set value is now expired\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute($cache, $key, \\&get_value_slow);\n+\t\tprint $data;\n+\t};\n+\t# returning stale data works\n+\tis_deeply(\n+\t\t\\@output,\n+\t\t[ ($stale_value) x 2 ],\n+\t\t'background: stale data returned by both process when expired'\n+\t);\n \n \t$cache->set_expires_in(-1); # never expire for next ->get\n+\tnote('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,\n-\t   'value got set correctly, even if stale data returned');\n+\t   'background: value got set correctly by background process');\n \n \n # \t$cache->set($key, $stale_value);\n-- \n1.7.3\n"},{"id":"157446","messageId":"1291677069-6559-17-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 16/24] gitweb: Introduce %actions_info, gathering information about actions","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:01Z","receivedAt":"2010-12-06T23:11:01Z","isPatch":true,"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 place.\n\n\nCurrently not used; will be used in next commit, to check if action\nproduces HTML output and therefore we can use HTML-specific progress\nindicator.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\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\nThis patch is the same as in the previous version.\n\nIt has no equivalent in \"Gitweb cachning v7\" series by J.H.\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 f202d6b..da95388 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -818,6 +818,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@@ -1226,6 +1245,7 @@ sub evaluate_argv {\n \n sub run {\n \tevaluate_argv();\n+\tevaluate_actions_info();\n \n \t$pre_listen_hook->()\n \t\tif $pre_listen_hook;\n-- \n1.7.3\n"},{"id":"157448","messageId":"1291677069-6559-18-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 17/24] gitweb: Show appropriate \"Generating...\" page when regenerating cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:02Z","receivedAt":"2010-12-06T23:11:02Z","isPatch":true,"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\nNote that without generating data in background, process generating\ndata wouldn't print progress info, because 'generating_info' can exit\n(and in the case of gitweb's git_generating_data_html does exit).\n\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\nThe git_generating_data_html() subroutine would return early (not showing\nHTML-base progress indicator) if action does not return HTML output, or\nif web browser / user agent is a robot / web crawler (or gitweb is run as\nstandalone script).  In such cases HTML \"Generating...\" page does not make\nmuch sense.\n\nFor this purpose new subrotuines action_outputs_html($action) (which uses\n%actions_info introduced in earlier commit) and browser_is_robot() (which\nuses HTTP::BrowserDetect if possible, and fall backs on simple check of\nUser-Agent string) were added.\n\n\nWARNING: If 'generating_info' subroutine always exits, like\ngit_generating_data_html() currently does, then there would be problems\nwith error pages, which are not cached... unless the process generating\ndata does not use 'generating_info', see the next commit.  The initial\ndelay introduced in 2nd next commit migitates this issue somewhat.\n\nAlternate solution would be to print output when generating data\n('tee'-ing it).\n\n\nAdd test for simple (not exiting) 'generating_info' subroutine, for both\ncase with background generation disabled and enabled to t9503.\n\nInspired-by-code-by: John 'Warthog9' Hawley <warthog9@kernel.org>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nNOTE that after patch introducing DONE_REQUEST, it is this label that\nshould be used as target label at the end of git_generating_data_html()\nrather than DONE_GITWEB.\n\n\nPrevious version of this patch lacked browser_is_robot() check in\ngit_generating_data_html().  We also now assume that Time::HiRes is\nalways available (it is in core for Perl 5.8.x that is requirement for\nrunning gitweb).\n\nDifferences from relevant parts of \"Gitweb caching v7\" series by J.H.:\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  Note that there might be problem with error pages (die_error exists,\n  and its output does not enter cache); it is addressed in next commit\n  in this series.  I am not sure if in J.H. patch process generating\n  data also shows \"Generating...\" page; on the other hand if I\n  understand it correctly it has initial delay of 2 seconds, which also\n  solves this issue as described in 2nd next commit.\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 (is not a\n  robot... which we now check).\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.\n\n* Basic test for \"Generating...\" feature, though not for the\n  git_generating_data_html(); see commit message.\n\n gitweb/gitweb.perl                             |  131 +++++++++++++++++++++++-\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   84 ++++++++++++----\n t/gitweb-lib.sh                                |    1 +\n t/t9503/test_cache_interface.pl                |   79 ++++++++++++++\n 4 files changed, 276 insertions(+), 19 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex da95388..181b85d 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -22,7 +22,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@@ -344,6 +344,31 @@ 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+\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 # Set to _initialized_ instance of GitwebCache::Capture compatibile capturing\n # engine, i.e. one implementing ->new() constructor, and ->capture($code)\n@@ -837,6 +862,23 @@ 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+sub browser_is_robot {\n+\treturn 1 if !exists $ENV{'HTTP_USER_AGENT'}; # gitweb run as script\n+\tif (eval { require HTTP::BrowserDetect; }) {\n+\t\tmy $browser = HTTP::BrowserDetect->new();\n+\t\treturn $browser->robot();\n+\t}\n+\t# fallback on detecting known web browsers\n+\treturn 0 if ($ENV{'HTTP_USER_AGENT'} =~ /\\b(?:Mozilla|Opera|Safari|IE)\\b/);\n+\t# be conservative; if not sure, assume non-interactive\n+\treturn 1;\n+}\n+\n # fill %input_params with the CGI parameters. All values except for 'opt'\n # should be single values, but opt can be an array. We should probably\n # build an array of parameters that can be multi-valued, but since for the time\n@@ -3555,6 +3597,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+\tif (!action_outputs_html($action) ||\n+\t    browser_is_robot()) {\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+\tmy %no_cache = (\n+\t\t# HTTP/1.0\n+\t\t-Pragma => 'no-cache',\n+\t\t# HTTP/1.1\n+\t\t-Cache_Control => join(', ', qw(private no-cache no-store must-revalidate\n+\t\t                                max-age=0 pre-check=0 post-check=0)),\n+\t);\n+\tprint STDOUT $cgi->header(-type => 'text/html', -charset => 'utf-8',\n+\t                          -status=> '200 OK', -expires => 'now',\n+\t                          %no_cache);\n+\tprint STDOUT <<\"EOF\";\n+<?xml version=\"1.0\" encoding=\"utf-8\"?>\n+<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n+                      \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n+<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en-US\" lang=\"en-US\">\n+<!-- git web interface version $version -->\n+<!-- git core binaries version $git_version -->\n+<head>\n+<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n+<meta http-equiv=\"refresh\" content=\"0\" />\n+<meta name=\"generator\" content=\"gitweb/$version git/$git_version$mod_perl_version\" />\n+<meta name=\"robots\" content=\"noindex, nofollow\" />\n+<title>$title</title>\n+</head>\n+<body>\n+EOF\n+\n+\tlocal $| = 1; # autoflush\n+\tprint STDOUT 'Generating...';\n+\n+\tmy $total_time = 0;\n+\tmy $interval = $generating_options{'print_interval'} || 1;\n+\tmy $timeout  = $generating_options{'timeout'};\n+\tmy $alarm_handler = sub {\n+\t\tlocal $! = 1;\n+\t\tprint STDOUT '.';\n+\t\t$total_time += $interval;\n+\t\tif ($total_time > $timeout) {\n+\t\t\tdie \"timeout\\n\";\n+\t\t}\n+\t};\n+\teval {\n+\t\tlocal $SIG{ALRM} = $alarm_handler;\n+\t\tTime::HiRes::alarm($interval, $interval);\n+\t\tmy $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 82e88f1..694c318 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 $class = shift;\n \tmy %opts = ref $_[0] ? %{ $_[0] } : @_;\n \n \tmy $self = $class->SUPER::new(\\%opts);\n \n-\tmy ($max_lifetime, $background_cache);\n+\tmy ($max_lifetime, $background_cache, $generating_info);\n \tif (%opts) {\n \t\t$max_lifetime =\n \t\t\t$opts{'max_lifetime'} ||\n \t\t\t$opts{'max_cache_lifetime'};\n \t\t$background_cache = $opts{'background_cache'};\n+\t\t$generating_info  = $opts{'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@@ -153,6 +172,33 @@ sub _tempfile_to_path {\n # ......................................................................\n # interface methods\n \n+sub _wait_for_data {\n+\tmy ($self, $key,\n+\t    $lock_fh, $lockfile,\n+\t    $fetch_code, $fetch_locked) = @_;\n+\tmy @result;\n+\n+\t# provide \"generating page...\" info, if exists\n+\t$self->generating_info($key, $lock_fh);\n+\t# generating info may exit, so we can not get there\n+\n+\t# get readers lock, i.e. wait for writer,\n+\t# which might be background process\n+\tflock($lock_fh, LOCK_SH);\n+\t# closing lockfile releases lock\n+\tif ($fetch_locked) {\n+\t\t@result = $fetch_code->();\n+\t\tclose $lock_fh\n+\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n+\t} else {\n+\t\tclose $lock_fh\n+\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n+\t\t@result = $fetch_code->();\n+\t}\n+\n+\treturn @result;\n+}\n+\n sub _set_maybe_background {\n \tmy ($self, $key, $fetch_code, $set_code) = @_;\n \n@@ -165,8 +211,10 @@ sub _set_maybe_background {\n \t\t\tif $self->is_valid($key, $self->get_max_lifetime());\n \n \t\t# fork if there is stale data, for background process\n-\t\t# to regenerate/refresh the cache (generate data)\n-\t\t$pid = fork() if (@stale_result);\n+\t\t# to regenerate/refresh the cache (generate data),\n+\t\t# or if main process would show progress indicator\n+\t\t$pid = fork()\n+\t\t\tif (@stale_result || $self->{'generating_info'});\n \t}\n \n \tif ($pid) {\n@@ -221,10 +269,19 @@ sub _compute_generic {\n \t\t\t## acquired writers lock, have to generate data\n \t\t\t@result = $self->_set_maybe_background($key, $fetch_code, $set_code);\n \n-\t\t\t# closing lockfile releases lock\n+\t\t\t# closing lockfile releases writer lock\n \t\t\tclose $lock_fh\n \t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n \n+\t\t\tif (!@result) {\n+\t\t\t\t# wait for background process to finish generating data\n+\t\t\t\topen $lock_fh, '<', $lockfile\n+\t\t\t\t\tor die \"Couldn't reopen (for reading) lockfile '$lockfile': $!\";\n+\n+\t\t\t\t@result = $self->_wait_for_data($key, $lock_fh, $lockfile,\n+\t\t\t\t                                $fetch_code, $fetch_locked);\n+\t\t\t}\n+\n \t\t} else {\n \t\t\t## didn't acquire writers lock, get stale data or wait for regeneration\n \n@@ -233,19 +290,10 @@ sub _compute_generic {\n \t\t\t\tif $self->is_valid($key, $self->get_max_lifetime());\n \t\t\treturn @result if @result;\n \n-\t\t\t# get readers lock (wait for writer)\n-\t\t\t# if there is no stale data to serve\n-\t\t\tflock($lock_fh, LOCK_SH);\n-\t\t\t# closing lockfile releases lock\n-\t\t\tif ($fetch_locked) {\n-\t\t\t\t@result = $fetch_code->();\n-\t\t\t\tclose $lock_fh\n-\t\t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n-\t\t\t} else {\n-\t\t\t\tclose $lock_fh\n-\t\t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n-\t\t\t\t@result = $fetch_code->();\n-\t\t\t}\n+\t\t\t# wait for regeneration\n+\t\t\t@result = $self->_wait_for_data($key, $lock_fh, $lockfile,\n+\t\t\t                                $fetch_code, $fetch_locked);\n+\n \t\t}\n \t} until (@result || $lock_state);\n \t# repeat until we have data, or we tried generating data oneself and failed\ndiff --git a/t/gitweb-lib.sh b/t/gitweb-lib.sh\nindex 4ce067f..d191f0e 100755\n--- a/t/gitweb-lib.sh\n+++ b/t/gitweb-lib.sh\n@@ -58,6 +58,7 @@ gitweb_enable_caching () {\n \t\t$caching_enabled = 1;\n \t\t$cache_options{\"expires_in\"} = -1;      # never expire cache for tests\n \t\t$cache_options{\"cache_root\"} = \"cache\"; # to clear the right thing\n+\t\t$cache_options{\"generating_info\"} = undef;\n \t\tEOF\n \t\trm -rf cache/\n \t'\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 7f08863..81f0b76 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -395,6 +395,85 @@ subtest 'serving stale data when (re)generating' => sub {\n };\n $cache->set_expires_in(-1);\n \n+\n+# Test 'generating_info' feature\n+#\n+$cache->remove($key);\n+my $progress_info = \"Generating...\";\n+sub test_generating_info {\n+\tlocal $| = 1;\n+\tprint \"$progress_info\";\n+}\n+$cache->set_generating_info(\\&test_generating_info);\n+\n+subtest 'generating progress info' => sub {\n+\tmy @progress;\n+\n+\t# without background generation\n+\t$cache->set_background_cache(0);\n+\t$cache->remove($key);\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute($cache, $key, \\&get_value_slow);\n+\t\tprint \"$sep$data\";\n+\t};\n+\t@progress = map { s/^(.*)\\Q${sep}\\E//o && $1 } @output;\n+\tis_deeply(\n+\t\t[sort @progress],\n+\t\t[sort ('', $progress_info)],\n+\t\t'no background: one process waiting for data prints progress info'\n+\t);\n+\tis_deeply(\n+\t\t\\@output,\n+\t\t[ ($value) x 2 ],\n+\t\t'no background: both processes return correct value'\n+\t);\n+\n+\n+\t# without background generation, with stale value\n+\t$cache->set($key, $stale_value);\n+\t$cache->set_expires_in(0);    # set value is now expired\n+\t$cache->set_max_lifetime(-1); # stale data never expire\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute($cache, $key, \\&get_value_slow);\n+\t\tprint \"$sep$data\";\n+\t};\n+\tis_deeply(\n+\t\t[sort @output],\n+\t## no progress for generating process without background generation;\n+\t#\t[sort (\"$progress_info$sep$value\", \"$sep$stale_value\")],\n+\t\t[sort (\"$sep$value\", \"$sep$stale_value\")],\n+\t\t'no background, stale data: generating gets data, other gets stale data'\n+\t) or diag('@output is ', join \", \", sort @output);\n+\t$cache->set_expires_in(-1);\n+\n+\n+\t# with background generation\n+\t$cache->set_background_cache(1);\n+\t$cache->remove($key);\n+\n+\t@output = parallel_run {\n+\t\tmy $data = cache_compute($cache, $key, \\&get_value_slow);\n+\t\tprint \"$sep$data\";\n+\t};\n+\t@progress = map { s/^(.*)\\Q${sep}\\E//o && $1 } @output;\n+\tis_deeply(\n+\t\t\\@progress,\n+\t\t[ ($progress_info) x 2],\n+\t\t'background: both process print progress info'\n+\t);\n+\tis_deeply(\n+\t\t\\@output,\n+\t\t[ ($value) x 2 ],\n+\t\t'background: both processes return correct value'\n+\t);\n+\n+\n+\tdone_testing();\n+};\n+$cache->set_expires_in(-1);\n+\n+\n done_testing();\n \n \n-- \n1.7.3\n"},{"id":"157468","messageId":"1291677069-6559-19-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 18/24] gitweb/lib - Configure running 'generating_info' when generating data","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:03Z","receivedAt":"2010-12-06T23:11:03Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Add a new 'generating_info_is_safe' cache option; if true, then\nprocess generating data (one that acquired exclusive writer's lock)\nwould run 'generating_info' if there is no stale data and background\ncache generation is enabled.\n\nIf function generating (or printing) exits, which leads to cache entry\nnot being generated (for gitweb this means that there are pages which\nare not cached, i.e. error pages), and 'generating_info' also exits,\nthis could result in bad behavior.  Therefore this new option is false\nby default.\n\n\nUpdates t9503 test appropriately.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch was not present in previous version of this series.\n\nThis is one solution to the problem mentioned in commit message;\nanother is to use initial delay in \"Generating...\" page, as it is done\nin next patch, and as it was done in previous version of this series.\n\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   17 ++++++++++++++---\n t/t9503/test_cache_interface.pl                |    1 +\n 2 files changed, 15 insertions(+), 3 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex 694c318..0823c55 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -80,26 +80,35 @@ use POSIX qw(setsid);\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+#  * 'generating_info_is_safe' (boolean)\n+#    If true, run 'generating_info' subroutine also in the project that\n+#    is generating data.  This has effect only when 'background_cache'\n+#    is true (both 'background_cache' and 'generating_info_is_safe' must\n+#    be true for project generating data to run 'generating_info').\n+#    Defaults to false for safety.\n sub new {\n \tmy $class = shift;\n \tmy %opts = ref $_[0] ? %{ $_[0] } : @_;\n \n \tmy $self = $class->SUPER::new(\\%opts);\n \n-\tmy ($max_lifetime, $background_cache, $generating_info);\n+\tmy ($max_lifetime, $background_cache, $generating_info, $gen_info_is_safe);\n \tif (%opts) {\n \t\t$max_lifetime =\n \t\t\t$opts{'max_lifetime'} ||\n \t\t\t$opts{'max_cache_lifetime'};\n \t\t$background_cache = $opts{'background_cache'};\n \t\t$generating_info  = $opts{'generating_info'};\n+\t\t$gen_info_is_safe = $opts{'generating_info_is_safe'};\n \t}\n \t$max_lifetime = -1 unless defined($max_lifetime);\n \t$background_cache = 1 unless defined($background_cache);\n+\t$gen_info_is_safe = 0 unless defined($gen_info_is_safe);\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+\t$self->set_generating_info_is_safe($gen_info_is_safe);\n \n \treturn $self;\n }\n@@ -110,7 +119,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(max_lifetime background_cache generating_info)) {\n+foreach my $i (qw(max_lifetime background_cache\n+                  generating_info generating_info_is_safe)) {\n \tmy $field = $i;\n \tno strict 'refs';\n \t*{\"get_$field\"} = sub {\n@@ -214,7 +224,8 @@ sub _set_maybe_background {\n \t\t# to regenerate/refresh the cache (generate data),\n \t\t# or if main process would show progress indicator\n \t\t$pid = fork()\n-\t\t\tif (@stale_result || $self->{'generating_info'});\n+\t\t\tif (@stale_result ||\n+\t\t\t    ($self->{'generating_info'} && $self->{'generating_info_is_safe'}));\n \t}\n \n \tif ($pid) {\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 81f0b76..480cfbc 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -333,6 +333,7 @@ subtest 'serving stale data when (re)generating' => sub {\n \n \t# with background generation\n \t$cache->set_background_cache(1);\n+\t$cache->set_generating_info_is_safe(1);\n \n \t$cache->set($key, $stale_value);\n \t$cache->set_expires_in(0);    # set value is now expired\n-- \n1.7.3\n"},{"id":"157464","messageId":"1291677069-6559-20-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH 19/24] gitweb: Add startup delay to activity indicator for cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:04Z","receivedAt":"2010-12-06T23:11:04Z","isPatch":true,"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\n\nNOTE: This startup delay allows to use git_generating_data_html() also\nfor process generating data, assuming that die_error(), which turns of\ncapturing and which output (error page) is not cache, finishes within\nstartup delay.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch doesn't have equivalent in J.H. series, 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\nThe additional advantage is that it solves problem with link that\nleads to error page: die_error exits / ends request without generating\ncache entry, so if \"Generating...\" is done also for process generating\ndata in background (perhaps it isn't true in J.H. series; I am not\nsure about code flow there), then gitweb would endlessly show\n\"Generating...\" page if it didn't wait and exited / ended request also\nfor die_error case.\n\nThe only difference compared to previous version of this series is that\ngit_generating_data_html is now marked as 'generating_info_is_safe'.\n\n gitweb/gitweb.perl |   28 ++++++++++++++++++++++++++++\n 1 files changed, 28 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 181b85d..c974e79 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -357,11 +357,22 @@ our %cache_options = (\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+\t# This enables/disables using 'generating_info' subroutine by process\n+\t# generating data, when not too stale data is not available (data is then\n+\t# generated in background).  Because git_generating_data_html() includes\n+\t# initial delay (of 1 second by default), and we can assume that die_error\n+\t# finishes within this time, then generating error pages should be safe\n+\t# from infinite \"Generating page...\" loop.\n+\t'generating_info_is_safe' => 1,\n );\n # You define site-wide options for \"Generating...\" page (if enabled) here\n # (which means that $cache_options{'generating_info'} is set to coderef);\n # override them with $GITWEB_CONFIG as necessary.\n our %generating_options = (\n+\t# The delay before displaying \"Generating...\" page, in seconds.  It is\n+\t# intended for \"Generating...\" page to be shown only when really needed.\n+\t'startup_delay' => 1,\n \t# The time between generating new piece of output to prevent from\n \t# redirection before data is ready, i.e. time between printing each\n \t# dot in activity indicator / progress info, in seconds.\n@@ -3607,6 +3618,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":"157462","messageId":"1291677069-6559-21-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH/RFC 20/24] gitweb/lib - Add support for setting error handler in cache","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:05Z","receivedAt":"2010-12-06T23:11:05Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"GitwebCache::SimpleFileCache and GitwebCache::FileCacheWithLocking\nacquired support for 'on_error'/'error_handler' parameter, which\naccepts the same values as 'on_get_error' and 'on_set_error' option in\nCHI, with exception of support for \"log\".  The default is \"die\" (as it\nwas before), though it now uses \"croak\" from Carp, rather than plain\n\"die\".\n\nAdded basic test for 'on_error' in t9503: setting it to error handler\nthat dies, and setting it to 'ignore'.\n\n\nThe way error handler coderef is invoked is slightly different from\nthe way CHI does it: the error handler doesn't pass key and raw error\nmessage as subsequent parameters.\n\nBecause '$self->_handle_error($msg)' is used in place of 'die $msg',\nread_file and write_fh subroutines had to be converted to methods, to\nhave access to $self.  Alternate solution would be to catch errors\nusing \"eval BLOCK\" in ->get() and ->set() etc., like in CHI::Driver.\n\nNote that external subroutines that croak()/die() on error (like\n'mkpath' or 'tempfile') are now wrapped in eval block (this would be\nnot necessary if alternate solution described above was used).\n\n\nWhile at it refactor ensuring that directory exists (for opening file\nfor writing, possibly creating it) into ->ensure_path($dir) method.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis and the next patch are new and were not present in previous version\nof this series.  That is why this and next patch are marked as RFC.\n\nThe way CHI does it, by wrapping commands in eval { ... } (or using\nTry::Tiny) in ->get, ->set (and in our case also ->compute and\n->compute_fh, as those no longer are defined in term of ->get and ->set,\nor ->get/->set-like operations) could be a better solution.  Some\ndicsussion / thinking on it is required.\n\n\nIn \"Gitweb caching v7\" series cache.pl / cache.pm used die_error\ndirectly, and that is why it couldn't be a proper Perl module, and why\nit had to be loaded using 'do <file>' rather than 'require <package>'.\n\nNote however that compared to \"Gitweb caching v7\" in this patch we\nleak more, possibly sensitive, information like exact file that was\nattempted to open and location in source file.  OTOH it helps\ndebugging errors in gitweb.\n\n\nNote that we might want to treat on_set_error and on_get_error\ndifferently, especially ENOSPC (No space left on device) on set.\nThis is left for (optionally) the future commit.\n\n gitweb/lib/GitwebCache/FileCacheWithLocking.pm |   25 ++++---\n gitweb/lib/GitwebCache/SimpleFileCache.pm      |   88 +++++++++++++++++------\n t/t9503/test_cache_interface.pl                |   44 ++++++++++++\n 3 files changed, 124 insertions(+), 33 deletions(-)\n\ndiff --git a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\nindex 0823c55..d994b3a 100644\n--- a/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n+++ b/gitweb/lib/GitwebCache/FileCacheWithLocking.pm\n@@ -63,6 +63,14 @@ use POSIX qw(setsid);\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+#  * 'on_error' (similar to CHI 'on_get_error'/'on_set_error')\n+#    How to handle runtime errors occurring during cache gets and cache\n+#    sets, which may or may not be considered fatal in your application.\n+#    Options are:\n+#    * \"die\" (the default) - call die() with an appropriate message\n+#    * \"warn\" - call warn() with an appropriate message\n+#    * \"ignore\" - do nothing\n+#    * <coderef> - call this code reference with an appropriate message\n #\n # (all the above are inherited from GitwebCache::SimpleFileCache)\n #\n@@ -155,10 +163,7 @@ sub get_lockname {\n \tmy $lockfile = $self->path_to_key($key, \\my $dir) . '.lock';\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+\t$self->ensure_path($dir);\n \n \treturn $lockfile;\n }\n@@ -174,7 +179,7 @@ sub _tempfile_to_path {\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+\t\tor $self->_handle_error(\"Couldn't open temporary file '$tempname' for writing: $!\");\n \n \treturn ($temp_fh, $tempname);\n }\n@@ -199,10 +204,10 @@ sub _wait_for_data {\n \tif ($fetch_locked) {\n \t\t@result = $fetch_code->();\n \t\tclose $lock_fh\n-\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n+\t\t\tor $self->_handle_error(\"Could't close lockfile '$lockfile': $!\");\n \t} else {\n \t\tclose $lock_fh\n-\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n+\t\t\tor $self->_handle_error(\"Could't close lockfile '$lockfile': $!\");\n \t\t@result = $fetch_code->();\n \t}\n \n@@ -273,7 +278,7 @@ sub _compute_generic {\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\tor $self->_handle_error(\"Could't open lockfile '$lockfile': $!\");\n \n \t\t$lock_state = flock($lock_fh, LOCK_EX | LOCK_NB);\n \t\tif ($lock_state) {\n@@ -282,12 +287,12 @@ sub _compute_generic {\n \n \t\t\t# closing lockfile releases writer lock\n \t\t\tclose $lock_fh\n-\t\t\t\tor die \"Could't close lockfile '$lockfile': $!\";\n+\t\t\t\tor $self->_handle_error(\"Could't close lockfile '$lockfile': $!\");\n \n \t\t\tif (!@result) {\n \t\t\t\t# wait for background process to finish generating data\n \t\t\t\topen $lock_fh, '<', $lockfile\n-\t\t\t\t\tor die \"Couldn't reopen (for reading) lockfile '$lockfile': $!\";\n+\t\t\t\t\tor $self->_handle_error(\"Couldn't reopen (for reading) lockfile '$lockfile': $!\");\n \n \t\t\t\t@result = $self->_wait_for_data($key, $lock_fh, $lockfile,\n \t\t\t\t                                $fetch_code, $fetch_locked);\ndiff --git a/gitweb/lib/GitwebCache/SimpleFileCache.pm b/gitweb/lib/GitwebCache/SimpleFileCache.pm\nindex 21ec434..8d0a6d9 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -20,6 +20,7 @@ package GitwebCache::SimpleFileCache;\n use strict;\n use warnings;\n \n+use Carp;\n use File::Path qw(mkpath);\n use File::Temp qw(tempfile);\n use Digest::MD5 qw(md5_hex);\n@@ -77,6 +78,14 @@ our $DEFAULT_NAMESPACE = '';\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+#  * 'on_error' (similar to CHI 'on_get_error'/'on_set_error')\n+#    How to handle runtime errors occurring during cache gets and cache\n+#    sets, which may or may not be considered fatal in your application.\n+#    Options are:\n+#    * \"die\" (the default) - call die() with an appropriate message\n+#    * \"warn\" - call warn() with an appropriate message\n+#    * \"ignore\" - do nothing\n+#    * <coderef> - call this code reference with an appropriate message\n sub new {\n \tmy $class = shift;\n \tmy %opts = ref $_[0] ? %{ $_[0] } : @_;\n@@ -86,6 +95,7 @@ sub new {\n \n \tmy ($root, $depth, $ns);\n \tmy ($expires_min, $expires_max, $increase_factor, $check_load);\n+\tmy ($on_error);\n \tif (%opts) {\n \t\t$root =\n \t\t\t$opts{'cache_root'} ||\n@@ -102,6 +112,11 @@ sub new {\n \t\t\t$opts{'expires_max'};\n \t\t$increase_factor = $opts{'expires_factor'};\n \t\t$check_load      = $opts{'check_load'};\n+\t\t$on_error =\n+\t\t\t$opts{'on_error'} ||\n+\t\t\t$opts{'on_get_error'} ||\n+\t\t\t$opts{'on_set_error'} ||\n+\t\t\t$opts{'error_handler'};\n \t}\n \t$root  = $DEFAULT_CACHE_ROOT  unless defined($root);\n \t$depth = $DEFAULT_CACHE_DEPTH unless defined($depth);\n@@ -111,6 +126,9 @@ sub new {\n \t\tif (!defined($expires_max) && exists $opts{'expires_in'});\n \t$expires_max = -1 unless (defined($expires_max));\n \t$increase_factor = 60 unless defined($increase_factor);\n+\t$on_error = \"die\"\n+\t\tunless (defined $on_error &&\n+\t\t        (ref($on_error) eq 'CODE' || $on_error =~ /^die|warn|ignore$/));\n \n \t$self->set_root($root);\n \t$self->set_depth($depth);\n@@ -119,6 +137,7 @@ sub new {\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+\t$self->set_on_error($on_error);\n \n \treturn $self;\n }\n@@ -131,7 +150,8 @@ sub new {\n \n # creates get_depth() and set_depth($depth) etc. methods\n foreach my $i (qw(depth root namespace\n-                  expires_min expires_max increase_factor check_load)) {\n+                  expires_min expires_max increase_factor check_load\n+                  on_error)) {\n \tmy $field = $i;\n \tno strict 'refs';\n \t*{\"get_$field\"} = sub {\n@@ -234,7 +254,7 @@ sub path_to_key {\n }\n \n sub read_file {\n-\tmy $filename = shift;\n+\tmy ($self, $filename) = @_;\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@@ -255,12 +275,12 @@ sub read_file {\n \t}\n \n \tclose $read_fh\n-\t\tor die \"Couldn't close file '$filename' opened for reading: $!\";\n+\t\tor $self->_handle_error(\"Couldn't close file '$filename' opened for reading: $!\");\n \treturn $buf;\n }\n \n sub write_fh {\n-\tmy ($write_fh, $filename, $data) = @_;\n+\tmy ($self, $write_fh, $filename, $data) = @_;\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@@ -278,7 +298,20 @@ sub write_fh {\n \t}\n \n \tclose $write_fh\n-\t\tor die \"Couldn't close file '$filename' opened for writing: $!\";\n+\t\tor $self->_handle_error(\"Couldn't close file '$filename' opened for writing: $!\");\n+}\n+\n+sub ensure_path {\n+\tmy $self = shift;\n+\tmy $dir = shift || return;\n+\n+\tif (!-d $dir) {\n+\t\t# mkpath will croak()/die() if there is an error\n+\t\teval {\n+\t\t\tmkpath($dir, 0, 0777);\n+\t\t\t1;\n+\t\t} or $self->_handle_error($@);\n+\t}\n }\n \n # ----------------------------------------------------------------------\n@@ -291,12 +324,27 @@ sub _tempfile_to_path {\n \tmy ($self, $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+\tmy ($temp_fh, $tempname);\n+\teval {\n+\t\t($temp_fh, $tempname) = tempfile(\"${file}_XXXXX\",\n+\t\t\t#DIR => $dir,\n+\t\t\t'UNLINK' => 0, # ensure that we don't unlink on close; file is renamed\n+\t\t\t'SUFFIX' => '.tmp');\n+\t} or $self->_handle_error($@);\n+\treturn ($temp_fh, $tempname);\n }\n \n+# based on _handle_get_error and _dispatch_error_msg from CHI::Driver\n+sub _handle_error {\n+\tmy ($self, $error) = @_;\n+\n+\tfor ($self->get_on_error()) {\n+\t\t(ref($_) eq 'CODE') && do { $_->($error) };\n+\t\t/^ignore$/ && do { };\n+\t\t/^warn$/   && do { carp $error };\n+\t\t/^die$/    && do { croak $error };\n+\t}\n+}\n \n # ----------------------------------------------------------------------\n # worker methods\n@@ -307,7 +355,7 @@ sub fetch {\n \tmy $file = $self->path_to_key($key);\n \treturn unless (defined $file && -f $file);\n \n-\treturn read_file($file);\n+\treturn $self->read_file($file);\n }\n \n sub store {\n@@ -318,20 +366,17 @@ sub store {\n \treturn unless (defined $file && defined $dir);\n \n \t# ensure that directory leading to cache file exists\n-\tif (!-d $dir) {\n-\t\t# mkpath will croak()/die() if there is an error\n-\t\tmkpath($dir, 0, 0777);\n-\t}\n+\t$self->ensure_path($dir);\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-\twrite_fh($temp_fh, $tempname, $data);\n+\t$self->write_fh($temp_fh, $tempname, $data);\n \n \trename($tempname, $file)\n-\t\tor die \"Couldn't rename temporary file '$tempname' to '$file': $!\";\n+\t\tor $self->_handle_error(\"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@@ -362,7 +407,7 @@ sub remove {\n \t\tor return;\n \treturn unless -f $file;\n \tunlink($file)\n-\t\tor die \"Couldn't remove file '$file': $!\";\n+\t\tor $self->_handle_error(\"Couldn't remove file '$file': $!\");\n }\n \n # $cache->is_valid($key[, $expires_in])\n@@ -379,7 +424,7 @@ sub is_valid {\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+\t\tor $self->_handle_error(\"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@@ -468,10 +513,7 @@ sub set_coderef_fh {\n \treturn unless (defined $path && defined $dir);\n \n \t# ensure that directory leading to cache file exists\n-\tif (!-d $dir) {\n-\t\t# mkpath will croak()/die() if there is an error\n-\t\tmkpath($dir, 0, 0777);\n-\t}\n+\t$self->ensure_path($dir);\n \n \t# generate a temporary file\n \tmy ($fh, $tempfile) = $self->_tempfile_to_path($path, $dir);\n@@ -481,7 +523,7 @@ sub set_coderef_fh {\n \n \tclose $fh;\n \trename($tempfile, $path)\n-\t\tor die \"Couldn't rename temporary file '$tempfile' to '$path': $!\";\n+\t\tor $self->_handle_error(\"Couldn't rename temporary file '$tempfile' to '$path': $!\");\n \n \topen $fh, '<', $path or return;\n \treturn ($fh, $path);\ndiff --git a/t/t9503/test_cache_interface.pl b/t/t9503/test_cache_interface.pl\nindex 480cfbc..28a5c5e 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -9,6 +9,7 @@ use Fcntl qw(:DEFAULT);\n use IO::Handle;\n use IO::Select;\n use IO::Pipe;\n+use File::Basename;\n \n use Test::More;\n \n@@ -475,6 +476,49 @@ subtest 'generating progress info' => sub {\n $cache->set_expires_in(-1);\n \n \n+# ----------------------------------------------------------------------\n+# ERROR HANDLING\n+\n+# Test 'on_error' handler\n+#\n+sub test_handler {\n+\tdie \"test_handler\\n\"; # newline needed\n+}\n+SKIP: {\n+\t# prepare error condition\n+\tmy $is_prepared = 1;\n+\t$is_prepared &&= $cache->set($key, $value);\n+\t$is_prepared &&= chmod 0555, dirname($cache->path_to_key($key));\n+\n+\tmy $ntests = 1; # in subtest\n+\tskip \"could't prepare error condition for 'on_error' tests\", $ntests\n+\t\tunless $is_prepared;\n+\tskip \"cannot test reliably 'on_error' as root (id=$>)\", $ntests\n+\t\tunless $> != 0;\n+\n+\tsubtest 'error handler' => sub {\n+\t\tmy ($result, $error);\n+\n+\t\t# check that error handler works\n+\t\t$cache->set_on_error(\\&test_handler);\n+\t\t$result = eval {\n+\t\t\t$cache->remove($key);\n+\t\t} or $error = $@;\n+\t\tok(!defined $result,         'on_error: died on error (via handler)');\n+\t\tdiag(\"result is $result\") if defined $result;\n+\t\tis($error, \"test_handler\\n\", 'on_error: test_handler was used');\n+\n+\t\t# check that \"ignore\" works\n+\t\t$cache->set_on_error('ignore');\n+\t\t$result = eval {\n+\t\t\t$cache->remove($key);\n+\t\t} or $error = $@;\n+\t\tok(defined $result,          'on_error: error ignored if requested');\n+\t};\n+}\n+chmod 0777, dirname($cache->path_to_key($key));\n+\n+\n done_testing();\n \n \n-- \n1.7.3\n"},{"id":"157451","messageId":"1291677069-6559-22-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH/RFC 21/24] gitweb: Wrap die_error to use as error handler for caching engine","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:06Z","receivedAt":"2010-12-06T23:11:06Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Use cache_error_handler subroutine, wrapping die_error (and\nHTML-escaping error message), as 'on_error' handler for\nGitwebCache::SimpleFileCache and its derivatives, and as both\n'on_get_error' and 'on_set_error' handler for CHI based caching\nengine.\n\nAdded single test in t9501 that checks if error in caching layer\nproduces \"500 Internal Server Error\".\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch and previous one this depends on were not present in previous\nversion of this series.  That is why this patch was marked as an\nRFC... well that and we also need to decide how error in caching layer\nshould be shown.\n\nThe error message is slightly more fancy than what \"Gitweb caching v7\"\nseries by J.H. would show on error in the caching layer.\n\n gitweb/gitweb.perl                       |   27 +++++++++++++++++++++++++++\n t/t9501-gitweb-standalone-http-status.sh |    8 ++++++++\n 2 files changed, 35 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex c974e79..5904d27 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -365,6 +365,20 @@ our %cache_options = (\n \t# finishes within this time, then generating error pages should be safe\n \t# from infinite \"Generating page...\" loop.\n \t'generating_info_is_safe' => 1,\n+\n+\t# How to handle runtime errors occurring during cache gets and cache\n+\t# sets.  Options are:\n+\t#  * \"die\" (the default) - call die() with an appropriate message\n+\t#  * \"warn\" - call warn() with an appropriate message\n+\t#  * \"ignore\" - do nothing\n+\t#  * <coderef> - call this code reference with an appropriate message\n+\t# Note that gitweb catches 'die <message>' via custom handle_errors_html\n+\t# handler, set via set_message() from CGI::Carp.  'warn <message>' are\n+\t# written to web server logs.\n+\t#\n+\t# The default is to use cache_error_handler, which wraps die_error.\n+\t# Only first argument passed to cache_error_handler is used (c.f. CHI)\n+\t'on_error' => \\&cache_error_handler,\n );\n # You define site-wide options for \"Generating...\" page (if enabled) here\n # (which means that $cache_options{'generating_info'} is set to coderef);\n@@ -1186,6 +1200,17 @@ sub configure_gitweb_features {\n \t}\n }\n \n+# custom error handler for caching engine (Internal Server Error)\n+sub cache_error_handler {\n+\tmy $error = shift;\n+\n+\t$error = to_utf8($error);\n+\t$error =\n+\t\t\"Error in caching layer: <i>\".ref($cache).\"</i><br>\\n\".\n+\t\tCGI::escapeHTML($error);\n+\t# die_error() would exit\n+\tdie_error(undef, undef, $error);\n+}\n # custom error handler: 'die <message>' is Internal Server Error\n sub handle_errors_html {\n \tmy $msg = shift; # it is already HTML escaped\n@@ -1348,6 +1373,8 @@ sub configure_caching {\n \t\t\t# (CHI compatibile initialization)\n \t\t\t'root_dir' => $cache_options{'cache_root'},\n \t\t\t'depth' => $cache_options{'cache_depth'},\n+\t\t\t'on_get_error' => $cache_options{'on_error'},\n+\t\t\t'on_set_error' => $cache_options{'on_error'},\n \t\t});\n \t}\n \tunless (defined $capture && ref($capture)) {\ndiff --git a/t/t9501-gitweb-standalone-http-status.sh b/t/t9501-gitweb-standalone-http-status.sh\nindex 168e494..af89422 100755\n--- a/t/t9501-gitweb-standalone-http-status.sh\n+++ b/t/t9501-gitweb-standalone-http-status.sh\n@@ -147,5 +147,13 @@ test_expect_success 'caching enabled (non-existent commit, 404 error)' '\n test_debug 'echo \"headers\" && cat gitweb.headers'\n test_debug 'echo \"body\"    && cat gitweb.body'\n \n+test_expect_success 'caching errors are 500 Internal Server Error' '\n+\tchmod 0000 cache/ &&\n+\ttest_when_finished \"chmod 0777 cache/\" &&\n+\tgitweb_run \"p=.git\" &&\n+\tgrep \"Status: 500 Internal Server Error\" gitweb.headers &&\n+\tgrep \"500 - Internal Server Error\" gitweb.body\n+'\n+test_debug 'echo \"headers\" && cat gitweb.headers'\n \n test_done\n-- \n1.7.3\n"},{"id":"157466","messageId":"1291677069-6559-23-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[PATCH/RFC 22/24] gitweb: Support legacy options used by kernel.org caching engine","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:07Z","receivedAt":"2010-12-06T23:11:07Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Try to translate between config variables used by gitweb caching\npatches[1] by John 'Warthog9' Hawley (J.H.) used, among others, by\ngit.kernel.org, and new %cache_options options, but only if the legacy\nconfig variables were set in the config file.\n\nNote that $cache_enable is *not* translated to $caching_enabled.\n\nFootnotes:\n~~~~~~~~~~\n[1] See for example \"Gitweb caching v7\" thread on git mailing list:\n    http://thread.gmane.org/gmane.comp.version-control.git/160147\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis patch was not present in previous version of this series.\n\nThe drawback of this patch is that if any legacy config variable is set\nin config file, then any change to %cache_options covering the same area\nas legacy config variable would be ignored.  I don't see a better\nsolution, unless we can somehow check if value of %cache_options was\nchanged via gitweb config file.\n\nI wonder how useful this patch would be; do people using caching from\n\"Gitweb caching v7\" (caching feom git.kernel.org) configure it, or do\nthey use default configuration?\n\n gitweb/gitweb.perl |   16 ++++++++++++++++\n 1 files changed, 16 insertions(+), 0 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 5904d27..1521bf2 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -284,6 +284,9 @@ our $caching_enabled = 0;\n # Suggested mechanism:\n # mv $cachedir $cachedir.flush && mkdir $cachedir && rm -rf $cachedir.flush\n our $cache;\n+\n+# Legacy options configuring behavior of git.kernel.org caching\n+our ($minCacheTime, $maxCacheTime, $cachedir, $backgroundCache, $maxCacheLife);\n # You define site-wide cache options defaults here; override them with\n # $GITWEB_CONFIG as necessary.\n our %cache_options = (\n@@ -1363,6 +1366,19 @@ sub configure_caching {\n \t\t$cache ||= 'GitwebCache::FileCacheWithLocking';\n \t\teval \"require $cache\";\n \t\tdie $@ if $@;\n+\n+\t\t# support for legacy config variables configuring cache behavior\n+\t\t# (those variables are/were used by caching engine by John Hawley,\n+\t\t# used among others by custom gitweb at http://git.kernel.org);\n+\t\t# it assumes that if those variables are defined, then we should\n+\t\t# use them - no provision is made for having both legacy variables\n+\t\t# and new %cache_options set in config file(s).\n+\t\t$cache_options{'cache_root'} = $cachedir if defined $cachedir;\n+\t\t$cache_options{'expires_min'} = $minCacheTime if defined $minCacheTime;\n+\t\t$cache_options{'expires_max'} = $maxCacheTime if defined $maxCacheTime;\n+\t\t$cache_options{'background_cache'} = $backgroundCache if defined $backgroundCache;\n+\t\t$cache_options{'max_lifetime'} = $maxCacheLife if defined $maxCacheLife;\n+\n \t\t$cache = $cache->new({\n \t\t\t%cache_options,\n \t\t\t#'cache_root' => '/tmp/cache',\n-- \n1.7.3\n"},{"id":"157454","messageId":"1291677069-6559-24-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[RFC/PATCH 23/24] gitweb/lib - Add clear() and size() methods to caching interface","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:08Z","receivedAt":"2010-12-06T23:11:08Z","isPatch":true,"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---\nThis patch is unchanged from previous version of this series.\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\nThis is less an RFC than next patch, but without next patch it is of\nvery limited use.\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 8d0a6d9..cd489f8 100644\n--- a/gitweb/lib/GitwebCache/SimpleFileCache.pm\n+++ b/gitweb/lib/GitwebCache/SimpleFileCache.pm\n@@ -21,8 +21,9 @@ use strict;\n use warnings;\n \n use Carp;\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@@ -38,7 +39,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@@ -393,7 +394,7 @@ sub get_size {\n \n \n # ......................................................................\n-# interface methods\n+# interface methods dealing with single item\n \n # Removing and expiring\n \n@@ -547,6 +548,45 @@ sub compute_fh {\n \treturn ($fh, $filename);\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 $File::Find::name };\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 28a5c5e..031c895 100755\n--- a/t/t9503/test_cache_interface.pl\n+++ b/t/t9503/test_cache_interface.pl\n@@ -46,7 +46,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":"157459","messageId":"1291677069-6559-25-git-send-email-jnareb@gmail.com","threadId":"25978","inReplyTo":"1291677069-6559-1-git-send-email-jnareb@gmail.com","subject":"[RFC PATCH 24/24] gitweb: Add beginnings of cache administration page (proof of concept)","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2010-12-06T23:11:09Z","receivedAt":"2010-12-06T23:11:09Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Currently cache administration page ('cache' action) shows estimated\nsize of cache, and provides link to clearing 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\nIf you can use cache administration page, you will see 'admin' link at\nthe botom of the page to it.\n\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\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\nCompared to previous series there is now 'admin' link that leads to\ncache administration page, which is present only if you have\npermissions to access said page.\n\nAlso for empty cache we show '-'.  \n\nCode was made slightly safer, though still I wouldn't use it in\nproduction, as there might be a chance that remote attacker could\npossibly clear cache (meaning I didn't prove that it couldn't happen).\n\n\nIn the final version this patch should probably be split into a few\nsmaller commits, for example:\n* adding support for $opts{'-title'} to git_header_html\n* action_is_cacheable subroutine\n* human_readable_size (which could also be used in 'tree' view at request)\n* xxx_admin_auth_ok, i.e. authorization and authentication for admin and\n  write actions in gitweb\n* 'cache' (i.e. cache admin) and 'clear_cache' (worker) methods\n\n gitweb/gitweb.perl |  138 ++++++++++++++++++++++++++++++++++++++++++++++++++--\n 1 files changed, 134 insertions(+), 4 deletions(-)\n\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 1521bf2..66e240f 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -25,6 +25,8 @@ use Encode;\n use Fcntl qw(:mode :flock);\n use File::Find qw();\n use File::Basename qw(basename);\n+use POSIX; # for POSIX::ceil($x)\n+\n binmode STDOUT, ':utf8';\n \n our $t0;\n@@ -888,6 +890,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@@ -895,6 +901,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 sub browser_is_robot {\n \treturn 1 if !exists $ENV{'HTTP_USER_AGENT'}; # gitweb run as script\n \tif (eval { require HTTP::BrowserDetect; }) {\n@@ -1245,12 +1256,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@@ -1397,6 +1409,10 @@ sub configure_caching {\n \t\trequire GitwebCache::Capture::Simple;\n \t\t$capture = GitwebCache::Capture::Simple->new();\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@@ -3760,7 +3776,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@@ -3936,10 +3952,18 @@ sub git_footer_html {\n \n \t} else {\n \t\tprint $cgi->a({-href => href(project=>undef, action=>\"opml\"),\n-\t\t              -class => $feed_class}, \"OPML\") . \" \";\n+\t\t              -class => $feed_class}, \"OPML\") . \"\\n\";\n \t\tprint $cgi->a({-href => href(project=>undef, action=>\"project_index\"),\n \t\t              -class => $feed_class}, \"TXT\") . \"\\n\";\n+\n \t}\n+\n+\tif ($actions{'cache'} &&\n+\t    cache_admin_auth_ok()) {\n+\t\tprint $cgi->a({-href => href(project=>undef, action=>\"cache\"),\n+\t\t              -class => $feed_class}, \"<i>admin</i>\") . \"\\n\";\n+\t}\n+\n \tprint \"</div>\\n\"; # class=\"page_footer\"\n \n \t# timing info doesn't make much sense with output (response) caching,\n@@ -7412,3 +7436,109 @@ 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+\tif ($cache->can('size')) {\n+\t\t$size = $cache->size();\n+\t} elsif ($cache->can('get_size')) {\n+\t\t$size = $cache->get_size();\n+\t}\n+\tif (defined $size) {\n+\t\tprint human_readable_size($size);\n+\t} else {\n+\t\tprint '-';\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+\t$caching_enabled\n+\t\tor die_error(403, \"Caching disabled\");\n+\tcache_admin_auth_ok()\n+\t\tor die_error(403, \"Clearing cache not allowed\");\n+\t$cache && ref($cache)\n+\t\tor die_error(500, \"Cache is not present\");\n+\n+\tif ($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"}]}