File Coverage

blib/lib/Dir/Flock.pm
Criterion Covered Total %
statement 94 152 61.8
branch 18 74 24.3
condition 6 25 24.0
subroutine 18 22 81.8
pod 4 4 100.0
total 140 277 50.5


line stmt bran cond sub pod time code
1             package Dir::Flock;
2 6     6   126 use strict;
  6         54  
  6         480  
3 6     6   96 use warnings;
  6         36  
  6         582  
4 6     6   36 use Carp;
  6         6  
  6         972  
5 6     6   4812 use File::Temp;
  6         111204  
  6         564  
6 6     6   54 use Time::HiRes 1.92;
  6         210  
  6         114  
7 6     6   1080 use Fcntl ':flock';
  6         12  
  6         960  
8 6     6   48 use Data::Dumper;
  6         6  
  6         12036  
9              
10             our $VERSION = '0.01';
11             my @TMPFILE;
12             my %LOCK;
13              
14             # configuration that may be updated by user
15             our $LOCKFILE_STUB = "dir-flock-";
16             our $PAUSE_LENGTH = 0.5; # seconds
17             our $HEARTBEAT_CHECK = 30; # seconds
18             our $TEMPFILE_XLENGTH = 12;
19              
20             our $_DEBUG = $ENV{DEBUG} || $ENV{DIR_FLOCK_DEBUG} || 0;
21              
22             sub _set_errno {
23 0     0   0 my $mnemonic = shift;
24 0 0       0 if (exists $!{$mnemonic}) {
25 0         0 $! = 0;
26 0   0     0 $!++ until $!{$mnemonic} || $! > 1024;
27             } else {
28 0         0 $! = 127;
29             }
30             }
31              
32             sub _pid {
33 972   50 972   2891 my $host = $ENV{HOSTNAME} || "localhost";
34 972 50       6092 join("_", $host, $$, $INC{"threads.pm"} ? threads->tid : ());
35             }
36              
37             sub flock {
38 0     0 1 0 my ($dir, $op) = @_;
39 0         0 my $timeout = undef;
40 0 0       0 if ($op & LOCK_NB) {
41 0         0 $timeout = 0;
42             }
43 0 0       0 if ($op & LOCK_EX) {
44 0         0 return Dir::Flock::lock($dir,$timeout);
45             }
46 0 0       0 if ($op & LOCK_UN) {
47 0         0 return unlock($dir);
48             }
49 0         0 carp "Dir::Flock::flock: invalid operation";
50 0         0 _set_errno("EINVAL");
51 0         0 return;
52             }
53              
54             sub lock {
55 243     243 1 632 my ($dir, $timeout) = @_;
56 243 50       5242 if (! -d $dir) {
57 0         0 carp "Dir::Flock::lock: '$dir' not a directory";
58 0         0 _set_errno("ENOTDIR");
59 0         0 return;
60             }
61 243 0 33     3675 if (! -r $dir && -w $dir && -x $dir) {
      33        
62 0         0 carp "Dir::Flock::lock: '$dir' not an accessible directory";
63 0         0 _set_errno("ENOENT");
64 0         0 return;
65             }
66 243         924 my $P = _pid();
67 243         984 my $now = Time::HiRes::time;
68 243         431 my $last_check = $now;
69 243   50     1118 my $expire = $now + ($timeout || 0);
70 243         713 my $lockdata = [ $P, $now ];
71 243         692 my $filename = _create_tempfile( $dir );
72 243         1029 _write_lock_data($lockdata, "$dir/$filename");
73 243         1172 while (_oldest_file($dir) ne $filename) {
74 201 50       524 $_DEBUG && print STDERR "$P $filename not the oldest file...\n";
75 201 50 33     489 if (defined($timeout) && Time::HiRes::time > $expire) {
76 0 0       0 $_DEBUG && print STDERR "$P timeout waiting for lock\n";
77 0         0 unlink "$dir/$filename";
78 0         0 return;
79             }
80 201         1416174 Time::HiRes::sleep $PAUSE_LENGTH;
81 201 50       2867 if (Time::HiRes::time > $last_check + $HEARTBEAT_CHECK) {
82 0 0       0 $_DEBUG && print STDERR "$P checking for heartbeat of lock holder\n";
83 0         0 _ping_oldest_file($dir);
84 0         0 $last_check = Time::HiRes::time;
85             }
86             }
87 243 50       716 $_DEBUG && print STDERR "$P lock successful to $filename\n";
88 243         674 $LOCK{$dir} = [ _pid(), $filename ];
89 243         1127 push @TMPFILE, "$dir/$filename";
90 243         1140 1;
91             }
92              
93             sub _create_token {
94 243     243   575 my ($n) = @_;
95 243         2412 my @bag = ('a'..'z', '0'..'9');
96 243         869 my $token = join("", map { $bag[rand(@bag)] } 0..$n);
  3159         7176  
97 243 50       934 $_DEBUG && print STDERR _pid()," created token: $token\n";
98 243         1125 $token;
99             }
100              
101             sub _create_tempfile {
102 243     243   441 my $dir = shift;
103 243         601 my $file = $LOCKFILE_STUB . "_" . _pid() . _create_token($TEMPFILE_XLENGTH);
104 243         548 return $file;
105             }
106              
107             sub getDir {
108 11     11 1 73 my $rootdir = shift;
109 11 50 33     526 if (-f $rootdir && ! -d $rootdir) {
110 11         527 require Cwd;
111 11         85 require File::Basename;
112 11         1835 $rootdir = File::Basename::dirname(Cwd::abs_path($rootdir));
113             }
114 11         156 my $tmpdir = File::Temp::tempdir( DIR => $rootdir, CLEANUP => 1 );
115 11         7291 $tmpdir;
116             }
117              
118             sub _oldest_file {
119 444     444   1385 my ($dir) = @_;
120 444         615 my $dh;
121 444         485404 Time::HiRes::sleep 0.001;
122 444         2433 my @f1;
123 444 50       17286 if (opendir $dh, $dir) {
124 444         16688 @f1 = grep /^$LOCKFILE_STUB/, readdir $dh;
125 444         6171 closedir $dh;
126             }
127             my @f = map {
128 444         1355 my @s = Time::HiRes::stat("$dir/$_");
  1119         18564  
129 1119         5700 [ $_, $s[9] ]
130             } @f1;
131              
132             # the defined() check is necessary in case file disappears between
133             # the time it is readdir'd and the time it is stat'd.
134 444 50       2505 my @F = sort { $a->[1] <=> $b->[1] || $a->[0] cmp $b->[0] }
  827         3032  
135             grep(defined $_->[1], @f);
136 444 50       1091 $_DEBUG && print STDERR _pid()," Files:", Dumper(\@F),"\n";
137 444 50       4142 @F && $F[0][0];
138             }
139              
140             sub _ping_oldest_file {
141 0     0   0 my $dir = shift;
142 0         0 my $file = _oldest_file($dir);
143 0 0       0 return unless $file;
144 0 0       0 open my $fh, "<", "$dir/$file" or return;
145 0         0 my @data = <$fh>;
146 0         0 close $fh;
147 0         0 my $status;
148 0         0 my ($host,$pid,$tid) = split /_/, $data[0];
149 0 0       0 $_DEBUG && print STDERR _pid(), ": ping host=$host pid=$pid tid=$tid\n";
150 0 0 0     0 if ($host eq $ENV{HOSTNAME} || $host eq 'localhost') {
151             # TODO: more robust way to inspect process on local machine.
152             # kill 'ZERO',... can mislead for a number of reasons, such as
153             # if the process is owned by a different user.
154 0         0 $status = kill 'ZERO', $pid;
155 0 0       0 $_DEBUG && print STDERR _pid(), " local kill ZERO => $pid: $status\n";
156             } else {
157             # TODO: need a more robust way to inspect a process on a remote machine
158 0         0 my $c1 = system("ssh $host kill -0 $pid");
159 0         0 $status = ($c1 == 0);
160 0 0       0 $_DEBUG && print STDERR _pid(),
161             " remote kill ZERO => $host:$pid: $status\n";
162             }
163 0 0       0 if (! $status) {
164 0         0 warn "Dir::Flock: lock holder that created $dir/$file appears dead\n";
165 0         0 unlink("$dir/$file");
166             }
167             }
168              
169             sub _write_lock_data {
170 243     243   491 my ($data, $filename) = @_;
171 243         19062 open my $fh, ">", $filename;
172 243         2767 print $fh $data->[0], "\n"; # process/thread identifier
173 243         2533 print $fh $data->[1], "\n"; # file creation time
174 243         38966 close $fh;
175             }
176              
177             sub unlock {
178 243     243 1 589 my ($dir) = @_;
179 243 50       677 if (!defined $LOCK{$dir}) {
180 0 0 0     0 if (!__inGD() && !$Dir::Flock::SUPPRESS_RELOCK_WARNINGS) {
181 0         0 carp "Dir::Flock::unlock: lock not held by ",
182             _pid()," or any proc";
183             }
184 0         0 return;
185             }
186 243 50       678 if ($LOCK{$dir}[0] ne _pid()) {
187 0 0       0 !__inGD() && carp "Dir::Flock::unlock: lock not held by ",_pid();
188 0         0 return;
189             }
190 243 50       640 $_DEBUG && print STDERR _pid()," unlocking $dir/$LOCK{$dir}[1]\n";
191 243 50       4660 if (! -f "$dir/$LOCK{$dir}[1]") {
192             !__inGD() && carp "Dir::Flock::unlock: lock file is missing ",
193 0 0       0 Dumper($LOCK{$dir});
194 0         0 return;
195             }
196 243         13679 my $z = unlink("$dir/$LOCK{$dir}[1]");
197 243         1348 delete $LOCK{$dir};
198 243 50       675 if (! $z) {
199             !__inGD() && carp "Dir::Flock::unlock: failed to unlink lock file ",
200 0 0       0 Dumper($LOCK{$dir});
201 0         0 return; # this could be bad
202             }
203 243         993 $z;
204             }
205              
206             BEGIN {
207 6 50   6   72 if (defined(${^GLOBAL_PHASE})) {
208 6 0   0   618 eval 'sub __inGD(){%{^GLOBAL_PHASE} eq q{DESTRUCT} && __END()};1'
  0         0  
209             } else {
210 0         0 require B;
211 0         0 eval 'sub __inGD(){${B::main_cv()}==0 && __END()};1'
212             }
213             }
214              
215             END {
216 6     6   48 no warnings 'redefine';
  6         12  
  6         1020  
217 5     5   2384 *DB::DB = sub {};
218 5         284 *__inGD = sub () { 1 };
219 5         1773 unlink @TMPFILE;
220             }
221              
222             1;
223              
224             =head1 NAME
225              
226             Dir::Flock - advisory locking of a dedicated directory
227              
228             =head1 VERSION
229              
230             0.01
231              
232             =head1 SYNOPSIS
233              
234             use Dir::Flock;
235             my $dir = Dir::Flock::getDir("/home/mob/projects/foo");
236             my $success = Dir::Flock::lock($dir);
237             # ... synchronized code
238             $success = Dir::Flock::unlock($dir);
239              
240             =head1 DESCRIPTION
241              
242             C implements advisory locking of a directory.
243             The use case is to execute synchronized code (code that should
244             only be executed by one process or thread at a time) or provide
245             exclusive access to a file or other resource. C has
246             more overhead than some of the other synchronization techniques
247             available to Perl programmers, but it might be the only technique
248             that works on NFS (Networked File System).
249              
250             =head2 Algorithm
251              
252             File locking is difficult on NFS because, as I understand it, each
253             node maintains its own cache of filesystem contents. When a system
254             call checks whether a lock exists on a file, the filesystem driver
255             might just inspect the cached file rather than the file on the
256             server, and it might miss an action taken by another node to lock
257             a file.
258              
259             The cache is not used, again, as I understand it, when the filesystem
260             driver reads a directory. If advisory locking is accomplished through
261             reading the contents of a directory, it will not be affected by NFS's
262             caching behavior.
263              
264             To acquire a lock in a directory, this module writes a small file
265             into the directory. Then it checks if this new file is the "oldest"
266             file in the directory. If it is the oldest file, then the process
267             has acquired the lock. If there is already an older file in the
268             directory, than that file specifies what process has a lock on the
269             directory, and we have to wait and try again later. To unlock the
270             directory, the module simply deletes the file in the directory
271             that represents its lock.
272              
273             =head1 FUNCTIONS
274              
275             =head2 lock
276              
277             =head2 $success = Dir::Flock::lock( $directory [, $timeout ] )
278              
279             Attempts to obtain an exclusive lock on the given directory. While
280             the directory is locked, the C call on the same directory from
281             other processes or threads will block until the directory is unlocked
282             (see L<"unlock">). Returns true if the lock was successfully acquired.
283              
284             If an optional C<$timeout> argument is provided, the function will
285             try for at least C<$timeout> seconds to acquire the lock, and return
286             a false value if it is not successful in that time. Use a timeout of
287             zero to make a "non-blocking" lock request.
288              
289             =head2 unlock
290              
291             =head2 $success = Dir::Flock::unlock( $directory )
292              
293             Releases the exclusive lock on the given directory held by this
294             process. Returns a false value if the current process did not
295             possess the lock on the directory.
296              
297             =head2 getDir
298              
299             =head2 $tmp_directory = getDir( $root )
300              
301             Creates a temporary and empty directory in a subdirectory of C<$root>
302             that is suitable for use as a synchronization object. The directory
303             will automatically be cleaned up when the process that called this
304             function exits.
305              
306             =head2 flock
307              
308             =head2 $success = flock( $dir, $op )
309              
310             If you prefer the semantics of L, the C
311             function from this package provides them in terms of the L<"lock">
312             and L<"unlock"> functions. Shared locks are not supported in
313             this version.
314              
315             =head1 LIMITATIONS
316              
317             Requires a version of L with the C function,
318             namely v1.92 or better (though later versions seem to have some
319             fixes related to the stat function). Requires operating system
320             support for subsecond file timestamp (output
321             C<&Time::HiRes::d_hires_stat> and look for a positive value to
322             indicate that your system has such support) and filesystem
323             support (FAT is not likely to work).
324              
325              
326              
327              
328              
329             =cut
330              
331             =begin TODO
332              
333             Shared (non-exclusive) locks
334              
335             The lock directory will hold "shared" files and "exclusive" files.
336             For an exclusive lock, write an exclusive file but erase it and retry
337             if there is an older shared or exclusive file
338             For a shared lock, write a shared file but erase and retry if there
339             is an older exclusive file
340              
341              
342             Directory lock object that unlocks when it goes out of scope
343              
344             {
345             my $lock = Dir::Flock::lockobj($dir);
346             }
347              
348             Block semantics
349              
350             Dir::Flock::sync $directory BLOCK
351             Dir::Flock::sync_ex $directory BLOCK
352             Dir::Flock::sync_sh $directory BLOCK
353              
354             Enhancements to the lock file
355              
356             e.g., lock file specification is:
357             1024 char header with host, process, thread, start time information
358             additional lines with timestamps of when the process was verified
359             to be alive
360             then to check a process that holds the lock, you seek to 1024 in the
361             lock file, read a line, and see if the process needs to be checked
362             again
363              
364             =end TODO