File Coverage

blib/lib/File/SharedNFSLock.pm
Criterion Covered Total %
statement 87 109 79.8
branch 18 42 42.8
condition 3 6 50.0
subroutine 20 21 95.2
pod 6 6 100.0
total 134 184 72.8


line stmt bran cond sub pod time code
1             package File::SharedNFSLock;
2 2     2   80200 use 5.008001;
  2         9  
  2         208  
3 2     2   12 use strict;
  2         3  
  2         176  
4 2     2   13 use warnings;
  2         8  
  2         144  
5 2     2   11 use File::Spec;
  2         3  
  2         67  
6 2     2   2123 use Sys::Hostname ();
  2         2624  
  2         40  
7 2     2   2126 use Time::HiRes ();
  2         4580  
  2         66  
8 2     2   17 use Carp 'croak';
  2         5  
  2         163  
9              
10 2     2   11 use constant STAT_NLINKS => 3;
  2         4  
  2         209  
11 2     2   12 use constant DEBUG => 0;
  2         4  
  2         3320  
12              
13             our $VERSION = '0.04';
14              
15             =head1 NAME
16              
17             File::SharedNFSLock - Inter-machine advisory file locking on NFS volumes
18              
19             =head1 SYNOPSIS
20              
21             use File::SharedNFSLock;
22             my $flock = File::SharedNFSLock->new(
23             file => 'some_file_on_nfs',
24             );
25             my $got_lock = $flock->lock(); # blocks for $timeout_acquire seconds if necessary
26             if ($got_lock) {
27             # hack hack hack...
28             }
29             $flock->unlock;
30            
31             # meanwhile, on another machine or in another process:
32             my $flock = File::SharedNFSLock->new(
33             file => 'some_file_on_nfs',
34             );
35             my $got_lock = $flock->lock(); # blocks for timeout or until first process is done
36             # ...
37              
38             =head1 DESCRIPTION
39              
40             This module implements advisory file locking on NFS (or non-NFS) filesystems.
41              
42             NFS (at least before v4) is evil. File locking on NFS volumes is worse. This
43             module attempts to implement file locking on NFS volumes using lock files and
44             hard links. It's in production use at our site, but if it doesn't work for you,
45             I'm not surprised!
46              
47             Note that the lock files are always written to the same directory as the original
48             file! There is always one lock file per process that tries to acquire the lock.
49             This module does B do signal handling. You will have to do that yourself.
50              
51             =head2 ALGORITHM
52              
53             I use the fact that hard links are (err, appear to be) atomic even with NFS.
54             So I write a process-specific, unique lock file and then hard-link it to the
55             real thing. Afterwards, C tells me the number of hard-linked instances
56             of the file (when polling my unique, private file). This indicates that I
57             have acquired the lock.
58              
59             The algorithm was snatched from a document called I by
60             I. I found it at L.
61             Look for chapter III, I, concern I: I.
62             The described workaround is, I quote:
63              
64             The solution for performing atomic file locking using a lockfile
65             is to create a unique file on the same fs (e.g., incorporating
66             hostname and pid), use link(2) to make a link to the lockfile and
67             use stat(2) on the unique file to check if its link count has
68             increased to 2. Do not use the return value of the link() call.
69              
70             =head1 METHODS
71              
72             =head2 new
73              
74             Creates a new lock object but does B attempt to acquire
75             the lock (see C below). Takes named arguments.
76             All times in the parameters are in seconds and can
77             be floating point values, indicating a fraction of a second.
78              
79             Mandatory argument: I pointing at the file that
80             is to be locked.
81              
82             Optional arguments: I indicates
83             the number of seconds to wait between attempts to
84             acquire the lock. Defaults to 1 second.
85              
86             I indicates the total
87             time that may be spent trying to acquire a lock when
88             C is called. After this time has elapsed, we
89             bail out without having acquired a lock. Default: 60 seconds.
90             If set to 0, the lock acquisition effectively becomes non-blocking.
91              
92             I indicates the number of seconds since the creation of
93             an existing lock file, after which this alien lock file is to be considered stale.
94             A stale lock will be removed and replaced with our own lock (watch out!).
95             Default: 5 minutes. Set this to 0 to disable the feature.
96              
97             I is an optional parameter that will uniquely identify
98             the lock. If you want to attempt locking the same file from
99             the same process in different locations, they must set
100             a unique token (host name, process id and thread id are used additionally).
101             Set this to C<1> to have a random token auto-generated.
102              
103             =cut
104              
105             SCOPE: {
106             my @chars = ('a'..'z', 'A'..'Z', 0..9);
107             sub new {
108 1     1 1 16 my $class = shift;
109 1         5 my %args = @_;
110 1 50       4 croak("Need 'file' argument!")
111             if not defined $args{file};
112              
113 1         4 my $uniquetoken = delete $args{unique_token};
114 1 50       4 if (defined $uniquetoken) {
115 0 0       0 if ($uniquetoken eq '1') {
116 0         0 $args{token} = join '', map $chars[rand @chars], (1..20);
117             }
118             else {
119 0         0 $args{token} = $uniquetoken;
120             }
121             }
122              
123 1         7 my $self = bless {
124             poll_interval => 1., # seconds
125             timeout_acquire => 60., # seconds
126             timeout_stale => 5*60., # seconds
127             token => '',
128             %args,
129             hostname => Sys::Hostname::hostname(),
130             } => $class;
131 1         22 if (DEBUG) {
132             warn "New lock for file '$self->{file}' (not acquired yet).\n"
133             ."Time out for acquisition: $self->{timeout_acquire}\n"
134             ."Time out for stale locks: $self->{timeout_stale}\n"
135             ."Poll interval : $self->{poll_interval}\n";
136             }
137 1         7 return $self;
138             }
139             } # end SCOPE
140              
141             =head2 lock
142              
143             Attempts to acquire a lock on the file.
144             Returns 1 on success, 0 on failure (time out).
145              
146             =cut
147              
148             sub lock {
149 1     1 1 2 my $self = shift;
150 1         2 warn "Getting lock on ".$self->{file}."\n" if DEBUG;
151              
152 1 50       4 return 1 if $self->got_lock;
153 1         2 warn "It is not locked already... ".$self->{file}."\n" if DEBUG;
154              
155 1         6 my $before_time = Time::HiRes::time();
156 1         2 warn "Before time is $before_time\n" if DEBUG;
157 1         2 while (1) {
158 1 50       5 if ($self->_write_lock_file()) {
159 1         5 return 1;
160             } else {
161             # check whether lock is stale
162 0 0       0 if ($self->_is_stale_lock) {
163 0         0 unlink $self->_lock_file;
164 0         0 unlink $self->_unique_lock_file;
165             } else {
166             # hmm. lock valid, wait a bit or bail out
167 0         0 my $now = Time::HiRes::time();
168 0         0 warn "Time now is $now\n" if DEBUG;
169 0 0       0 if ($now-$before_time > $self->{timeout_acquire}) {
170 0         0 $self->_unlink_lock_file;
171 0         0 return 0;
172             }
173              
174 0 0       0 Time::HiRes::sleep($self->{poll_interval}) if $self->{poll_interval};
175             }
176             }
177             } # end while(1)
178             }
179              
180             =head2 unlock
181              
182             Releases the lock, deletes the lock file.
183             This is automatically called on destruction of the
184             lock object!
185              
186             =cut
187              
188             sub unlock {
189 2     2 1 5 my $self = shift;
190 2         7 $self->_unlink_lock_file;
191             }
192              
193             =head2 got_lock
194              
195             Checks whether we have the lock on the file. Prefer calling got_lock() instead
196             of its older form, locked().
197              
198             I This is a fairly expensive operation requiring a C call.
199              
200             =cut
201              
202             sub got_lock {
203 8     8 1 14 my $self = shift;
204             # Check whether somebody else timed out the lock
205 8         19 my $nlinks = ( stat($self->_unique_lock_file) )[STAT_NLINKS];
206 8 100 66     41 if ( (defined $nlinks) and ($nlinks == 2) ) {
207 3         3 warn "got_lock: LOCKED with ".$self->_unique_lock_file."\n" if DEBUG;
208 3         15 return 1;
209             } else {
210 5         6 warn "got_lock: NOT LOCKED with ".$self->_unique_lock_file."\n" if DEBUG;
211 5         21 return 0;
212             }
213             }
214             *locked = \&got_lock;
215              
216             =head2 is_locked
217              
218             Checks file is currently locked by someone.
219              
220             =cut
221              
222             sub is_locked {
223             # Simply check for presence of lock_file
224 5 100   5 1 17 return (-f shift->_lock_file) ? 1 : 0;
225             }
226              
227              
228             =head2 wait
229              
230             Wait until the file becomes free of any lock. This uses the I
231             constructor passed to new().
232              
233             =cut
234              
235             sub wait {
236 1     1 1 2 my $self = shift;
237 1         7 while ( $self->is_locked ) {
238 0 0       0 Time::HiRes::sleep($self->{poll_interval}) if $self->{poll_interval};
239             }
240 1         4 return 1;
241             }
242              
243             sub DESTROY {
244 1     1   294 my $self = shift;
245 1         5 $self->unlock;
246             }
247              
248             sub _unlink_lock_file {
249 2     2   3 my $self = shift;
250 2 100       5 if ($self->got_lock) {
251 1         1 warn "_unlink_lock_file: locked, removing main lock file\n" if DEBUG;
252 1         5 unlink($self->_lock_file);
253             }
254 2         4 warn "_unlink_lock_file: removing unique lock file\n" if DEBUG;
255 2         4 unlink($self->_unique_lock_file);
256             }
257              
258             sub _write_lock_file {
259 1     1   2 my $self = shift;
260 1         3 my $unique_lock_file = $self->_unique_lock_file;
261 1 50       25 unlink($unique_lock_file) if -e $unique_lock_file;
262              
263             # Create process-specific lock file
264 1 50       114 open my $fh, '>', $unique_lock_file
265             or die "Could not open unique lock file for writing: $!";
266 1         41 print $fh Time::HiRes::time(), "\012", $unique_lock_file, "\012";
267 1         53 close $fh;
268              
269             # Attempt locking via linking
270 1         4 my $linked = link($unique_lock_file, $self->_lock_file);
271 1 50 33     6 if ( (not $linked) && ($! =~ m/not permitted/i) ) {
272 0         0 die "Error: The filesystem that holds file ".$self->{file}." does not ".
273             "support link().\n";
274             }
275              
276 1         4 return $self->got_lock
277             }
278              
279             sub _unique_lock_file {
280 11     11   15 my $self = shift;
281 11 100       319 return $self->{unique_lock_file} if defined $self->{unique_lock_file};
282 1 50       5 my $thread_id = exists $INC{'threads.pm'} ? threads->tid : '';
283 1         3 my $unique_lock_file = join( '.',
284             $self->_lock_file, $self->{hostname}, $$, $thread_id, $self->{token});
285 1         4 $self->{unique_lock_file} = $unique_lock_file;
286 1         78 return $self->{unique_lock_file};
287             }
288              
289             sub _lock_file {
290 8     8   13 my $self = shift;
291 8 100       195 return $self->{lock_file} if defined $self->{lock_file};
292 1         30 my ($volume, $path, $lock_file) = File::Spec->splitpath( $self->{file} );
293 1         3 $lock_file .= '.lock';
294 1         16 $lock_file = File::Spec->catpath($volume, $path, $lock_file);
295 1         3 $self->{lock_file} = $lock_file;
296 1         37 return $lock_file;
297             }
298              
299             sub _is_stale_lock {
300 0     0     my $self = shift;
301 0 0         return 0 if not $self->{timeout_stale};
302              
303 0 0         open my $fh, '<', $self->_lock_file # race?
304             or return 1; # FIXME warning?
305              
306 0           local $/ = "\012";
307 0           my @lines = <$fh>;
308 0 0         if (Time::HiRes::time()-$lines[0] > $self->{timeout_stale}) {
309 0           return 1;
310             }
311 0           return 0;
312             }
313              
314             1;
315              
316             __END__