gitweb/lib - Use locking to avoid 'cache miss stampede' problem
[git/jnareb-git.git] / gitweb / lib / GitwebCache / FileCacheWithLocking.pm
blob1ea0e6030473139a738f368f1aa7cd3b77bc9d93
1 # gitweb - simple web interface to track changes in git repositories
3 # (C) 2006, John 'Warthog9' Hawley <warthog19@eaglescrag.net>
4 # (C) 2010, Jakub Narebski <jnareb@gmail.com>
6 # This program is licensed under the GPLv2
9 # Gitweb caching engine, simple file-based cache, with locking
12 # Based on GitwebCache::SimpleFileCache, minimalistic cache that
13 # stores data in the filesystem, without serialization.
15 # It uses file locks (flock) to have only one process generating data
16 # and writing to cache, when using CHI interface ->compute() method.
18 package GitwebCache::FileCacheWithLocking;
19 use base qw(GitwebCache::SimpleFileCache);
21 use strict;
22 use warnings;
24 use File::Path qw(mkpath);
25 use Fcntl qw(:flock);
27 # ......................................................................
28 # constructor is inherited from GitwebCache::SimpleFileCache
30 # ----------------------------------------------------------------------
31 # utility functions and methods
33 # Take an human readable key, and return path to be used for lockfile
34 # Ensures that file can be created, if needed.
35 sub get_lockname {
36 my ($self, $key) = @_;
38 my $lockfile = $self->path_to_key($key, \my $dir) . '.lock';
40 # ensure that directory leading to lockfile exists
41 if (!-d $dir) {
42 eval { mkpath($dir, 0, 0777); 1 }
43 or die "Couldn't mkpath '$dir' for lockfile: $!";
46 return $lockfile;
49 # ......................................................................
50 # interface methods
52 sub _compute_generic {
53 my ($self, $key,
54 $get_code, $set_code, $get_locked) = @_;
56 my @result = $get_code->();
57 return @result if @result;
59 my $lockfile = $self->get_lockname($key);
61 # this loop is to protect against situation where process that
62 # acquired exclusive lock (writer) dies or exits (die_error)
63 # before writing data to cache
64 my $lock_state; # needed for loop condition
65 do {
66 open my $lock_fh, '+>', $lockfile
67 or die "Could't open lockfile '$lockfile': $!";
68 $lock_state = flock($lock_fh, LOCK_EX | LOCK_NB);
69 if ($lock_state) {
70 # acquired writers lock
71 @result = $set_code->();
73 # closing lockfile releases lock
74 close $lock_fh
75 or die "Could't close lockfile '$lockfile': $!";
77 } else {
78 # get readers lock (wait for writer)
79 flock($lock_fh, LOCK_SH);
80 # closing lockfile releases lock
81 if ($get_locked) {
82 @result = $get_code->();
83 close $lock_fh
84 or die "Could't close lockfile '$lockfile': $!";
85 } else {
86 close $lock_fh
87 or die "Could't close lockfile '$lockfile': $!";
88 @result = $get_code->();
91 } until (@result || $lock_state);
92 # repeat until we have data, or we tried generating data oneself and failed
93 return @result;
96 # $data = $cache->compute($key, $code);
98 # Combines the get and set operations in a single call. Attempts to
99 # get $key; if successful, returns the value. Otherwise, calls $code
100 # and uses the return value as the new value for $key, which is then
101 # returned.
103 # Uses file locking to have only one process updating value for $key
104 # to avoid 'cache miss stampede' (aka 'stampeding herd') problem.
105 sub compute {
106 my ($self, $key, $code) = @_;
108 return ($self->_compute_generic($key,
109 sub {
110 return $self->get($key);
112 sub {
113 my $data = $code->();
114 $self->set($key, $data);
115 return $data;
117 0 # $self->get($key); is outside LOCK_SH critical section
118 ))[0]; # return single value: $data
121 # ($fh, $filename) = $cache->compute_fh($key, $code);
123 # Combines the get and set operations in a single call. Attempts to
124 # get $key; if successful, returns the filehandle it can be read from.
125 # Otherwise, calls $code passing filehandle to write to as a
126 # parameter; contents of this file is then used as the new value for
127 # $key; returns filehandle from which one can read newly generated data.
129 # Uses file locking to have only one process updating value for $key
130 # to avoid 'cache miss stampede' (aka 'stampeding herd') problem.
131 sub compute_fh {
132 my ($self, $key, $code_fh) = @_;
134 return $self->_compute_generic($key,
135 sub {
136 return $self->get_fh($key);
138 sub {
139 return $self->set_coderef_fh($key, $code_fh);
141 1 # $self->get_fh($key); just opens file
146 __END__
147 # end of package GitwebCache::FileCacheWithLocking