File Coverage

blib/lib/Dir/Flock.pm
Criterion Covered Total %
statement 93 150 62.0
branch 17 72 23.6
condition 6 22 27.2
subroutine 18 22 81.8
pod 4 4 100.0
total 138 270 51.1


line stmt bran cond sub pod time code
1             package Dir::Flock;
2 6     6   78 use strict;
  6         192  
  6         396  
3 6     6   66 use warnings;
  6         30  
  6         456  
4 6     6   36 use Carp;
  6         6  
  6         810  
5 6     6   4014 use File::Temp;
  6         93870  
  6         468  
6 6     6   42 use Time::HiRes 1.92;
  6         186  
  6         120  
7 6     6   864 use Fcntl ':flock';
  6         12  
  6         762  
8 6     6   42 use Data::Dumper;
  6         6  
  6         9192  
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 944   50 944   3558 my $host = $ENV{HOSTNAME} || "localhost";
34 944 50       5529 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 236     236 1 737 my ($dir, $timeout) = @_;
56 236 50       3990 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 236 0 33     3050 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 236         902 my $P = _pid();
67 236         862 my $now = Time::HiRes::time;
68 236         409 my $last_check = $now;
69 236   50     1497 my $expire = $now + ($timeout || 0);
70 236         892 my $lockdata = [ $P, $now ];
71 236         573 my $filename = _create_tempfile( $dir );
72 236         1087 _write_lock_data($lockdata, "$dir/$filename");
73 236         953 while (_oldest_file($dir) ne $filename) {
74 206 50       468 $_DEBUG && print STDERR "$P $filename not the oldest file...\n";
75 206 50 33     538 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 206         1566298 Time::HiRes::sleep $PAUSE_LENGTH;
81 206 50       3370 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 236 50       943 $_DEBUG && print STDERR "$P lock successful to $filename\n";
88 236         934 $LOCK{$dir} = [ _pid(), $filename ];
89 236         946 push @TMPFILE, "$dir/$filename";
90 236         1270 1;
91             }
92              
93             sub _create_token {
94 236     236   823 my ($n) = @_;
95 236         2438 my @bag = ('a'..'z', '0'..'9');
96 236         782 my $token = join("", map { $bag[rand(@bag)] } 0..$n);
  3068         6846  
97 236 50       811 $_DEBUG && print STDERR _pid()," created token: $token\n";
98 236         844 $token;
99             }
100              
101             sub _create_tempfile {
102 236     236   363 my $dir = shift;
103 236         601 my $file = $LOCKFILE_STUB . "_" . _pid() . _create_token($TEMPFILE_XLENGTH);
104 236         493 return $file;
105             }
106              
107             sub getDir {
108 11     11 1 57 my $rootdir = shift;
109 11 50 33     386 if (-f $rootdir && ! -d $rootdir) {
110 11         365 require Cwd;
111 11         70 require File::Basename;
112 11         1667 $rootdir = File::Basename::dirname(Cwd::abs_path($rootdir));
113             }
114 11         145 my $tmpdir = File::Temp::tempdir( DIR => $rootdir, CLEANUP => 1 );
115 11         5874 $tmpdir;
116             }
117              
118             sub _oldest_file {
119 442     442   1316 my ($dir) = @_;
120 442         643 my $dh;
121 442         619057 Time::HiRes::sleep 0.001;
122 442         15917 opendir $dh, $dir;
123 442         15547 my @f1 = grep /^$LOCKFILE_STUB/, readdir $dh;
124 442         6340 closedir $dh;
125             my @f = map {
126 442         1276 my @s = Time::HiRes::stat("$dir/$_");
  1129         16654  
127 1129         11499 [ $_, $s[9] ]
128             } @f1;
129              
130             # the defined() check is necessary in case file disappears between
131             # the time it is readdir'd and the time it is stat'd.
132 442 50       2584 my @F = sort { $a->[1] <=> $b->[1] || $a->[0] cmp $b->[0] }
  884         3338  
133             grep(defined $_->[1], @f);
134 442 50       1019 $_DEBUG && print STDERR _pid()," Files:", Dumper(\@F),"\n";
135 442 50       3455 @F && $F[0][0];
136             }
137              
138             sub _ping_oldest_file {
139 0     0   0 my $dir = shift;
140 0         0 my $file = _oldest_file($dir);
141 0 0       0 return unless $file;
142 0 0       0 open my $fh, "<", "$dir/$file" or return;
143 0         0 my @data = <$fh>;
144 0         0 close $fh;
145 0         0 my $status;
146 0         0 my ($host,$pid,$tid) = split /_/, $data[0];
147 0 0       0 $_DEBUG && print STDERR _pid(), ": ping host=$host pid=$pid tid=$tid\n";
148 0 0 0     0 if ($host eq $ENV{HOSTNAME} || $host eq 'localhost') {
149             # TODO: more robust way to inspect process on local machine.
150             # kill 'ZERO',... can mislead for a number of reasons, such as
151             # if the process is owned by a different user.
152 0         0 $status = kill 'ZERO', $pid;
153 0 0       0 $_DEBUG && print STDERR _pid(), " local kill ZERO => $pid: $status\n";
154             } else {
155             # TODO: need a more robust way to inspect a process on a remote machine
156 0         0 my $c1 = system("ssh $host kill -0 $pid");
157 0         0 $status = ($c1 == 0);
158 0 0       0 $_DEBUG && print STDERR _pid(),
159             " remote kill ZERO => $host:$pid: $status\n";
160             }
161 0 0       0 if (! $status) {
162 0         0 warn "Dir::Flock: lock holder that created $dir/$file appears dead\n";
163 0         0 unlink("$dir/$file");
164             }
165             }
166              
167             sub _write_lock_data {
168 236     236   489 my ($data, $filename) = @_;
169 236         21829 open my $fh, ">", $filename;
170 236         2896 print $fh $data->[0], "\n"; # process/thread identifier
171 236         3697 print $fh $data->[1], "\n"; # file creation time
172 236         8567 close $fh;
173             }
174              
175             sub unlock {
176 236     236 1 615 my ($dir) = @_;
177 236 50       1117 if (!defined $LOCK{$dir}) {
178 0 0       0 !__inGD() && carp "Dir::Flock::unlock: lock not held by ",
179             _pid()," or any proc";
180 0         0 return;
181             }
182 236 50       908 if ($LOCK{$dir}[0] ne _pid()) {
183 0 0       0 !__inGD() && carp "Dir::Flock::unlock: lock not held by ",_pid();
184 0         0 return;
185             }
186 236 50       647 $_DEBUG && print STDERR _pid()," unlocking $dir/$LOCK{$dir}[1]\n";
187 236 50       3889 if (! -f "$dir/$LOCK{$dir}[1]") {
188             !__inGD() && carp "Dir::Flock::unlock: lock file is missing ",
189 0 0       0 Dumper($LOCK{$dir});
190 0         0 return;
191             }
192 236         14200 my $z = unlink("$dir/$LOCK{$dir}[1]");
193 236         1106 delete $LOCK{$dir};
194 236 50       602 if (! $z) {
195             !__inGD() && carp "Dir::Flock::unlock: failed to unlink lock file ",
196 0 0       0 Dumper($LOCK{$dir});
197 0         0 return; # this could be bad
198             }
199 236         793 $z;
200             }
201              
202             BEGIN {
203 6 50   6   72 if (defined(${^GLOBAL_PHASE})) {
204 6 0   0   426 eval 'sub __inGD(){%{^GLOBAL_PHASE} eq q{DESTRUCT} && __END()};1'
  0         0  
205             } else {
206 0         0 require B;
207 0         0 eval 'sub __inGD(){${B::main_cv()}==0 && __END()};1'
208             }
209             }
210              
211             END {
212 6     6   30 no warnings 'redefine';
  6         12  
  6         840  
213 5     5   2356 *DB::DB = sub {};
214 5         518 *__inGD = sub () { 1 };
215 5         1784 unlink @TMPFILE;
216             }
217              
218             1;
219              
220             =head1 NAME
221              
222             Dir::Flock - advisory locking of a dedicated directory
223              
224             =head1 VERSION
225              
226             0.01
227              
228             =head1 SYNOPSIS
229              
230             use Dir::Flock;
231             my $dir = Dir::Flock::getDir("/home/mob/projects/foo");
232             my $success = Dir::Flock::lock($dir);
233             # ... synchronized code
234             $success = Dir::Flock::unlock($dir);
235              
236             =head1 DESCRIPTION
237              
238             C implements advisory locking of a directory.
239             The use case is to execute synchronized code (code that should
240             only be executed by one process or thread at a time) or provide
241             exclusive access to a file or other resource. C has
242             more overhead than some of the other synchronization techniques
243             available to Perl programmers, but it might be the only technique
244             that works on NFS (Networked File System).
245              
246             =head2 Algorithm
247              
248             File locking is difficult on NFS because, as I understand it, each
249             node maintains its own cache of filesystem contents. When a system
250             call checks whether a lock exists on a file, the filesystem driver
251             might just inspect the cached file rather than the file on the
252             server, and it might miss an action taken by another node to lock
253             a file.
254              
255             The cache is not used, again, as I understand it, when the filesystem
256             driver reads a directory. If advisory locking is accomplished through
257             reading the contents of a directory, it will not be affected by NFS's
258             caching behavior.
259              
260             To acquire a lock in a directory, this module writes a small file
261             into the directory. Then it checks if this new file is the "oldest"
262             file in the directory. If it is the oldest file, then the process
263             has acquired the lock. If there is already an older file in the
264             directory, than that file specifies what process has a lock on the
265             directory, and we have to wait and try again later. To unlock the
266             directory, the module simply deletes the file in the directory
267             that represents its lock.
268              
269             =head1 FUNCTIONS
270              
271             =head2 lock
272              
273             =head2 $success = Dir::Flock::lock( $directory [, $timeout ] )
274              
275             Attempts to obtain an exclusive lock on the given directory. While
276             the directory is locked, the C call on the same directory from
277             other processes or threads will block until the directory is unlocked
278             (see L<"unlock">). Returns true if the lock was successfully acquired.
279              
280             If an optional C<$timeout> argument is provided, the function will
281             try for at least C<$timeout> seconds to acquire the lock, and return
282             a false value if it is not successful in that time. Use a timeout of
283             zero to make a "non-blocking" lock request.
284              
285             =head2 unlock
286              
287             =head2 $success = Dir::Flock::unlock( $directory )
288              
289             Releases the exclusive lock on the given directory held by this
290             process. Returns a false value if the current process did not
291             possess the lock on the directory.
292              
293             =head2 getDir
294              
295             =head2 $tmp_directory = getDir( $root )
296              
297             Creates a temporary and empty directory in a subdirectory of C<$root>
298             that is suitable for use as a synchronization object. The directory
299             will automatically be cleaned up when the process that called this
300             function exits.
301              
302             =head2 flock
303              
304             =head2 $success = flock( $dir, $op )
305              
306             If you prefer the semantics of L, the C
307             function from this package provides them in terms of the L<"lock">
308             and L<"unlock"> functions. Shared locks are not supported in
309             this version.
310              
311             =head1 LIMITATIONS
312              
313             Requires a version of L with the C function,
314             namely v1.92 or better (though later versions seem to have some
315             fixes related to the stat function). Requires operating system
316             support for subsecond file timestamp (output
317             C<&Time::HiRes::d_hires_stat> and look for a positive value to
318             indicate that your system has such support) and filesystem
319             support (FAT is not likely to work).
320              
321              
322              
323              
324              
325             =cut
326              
327             =begin TODO
328              
329             Shared (non-exclusive) locks
330              
331             The lock directory will hold "shared" files and "exclusive" files.
332             For an exclusive lock, write an exclusive file but erase it and retry
333             if there is an older shared or exclusive file
334             For a shared lock, write a shared file but erase and retry if there
335             is an older exclusive file
336              
337              
338             Directory lock object that unlocks when it goes out of scope
339              
340             {
341             my $lock = Dir::Flock::lockobj($dir);
342             }
343              
344             Block semantics
345              
346             Dir::Flock::sync $directory BLOCK
347             Dir::Flock::sync_ex $directory BLOCK
348             Dir::Flock::sync_sh $directory BLOCK
349              
350             Enhancements to the lock file
351              
352             e.g., lock file specification is:
353             1024 char header with host, process, thread, start time information
354             additional lines with timestamps of when the process was verified
355             to be alive
356             then to check a process that holds the lock, you seek to 1024 in the
357             lock file, read a line, and see if the process needs to be checked
358             again
359              
360             =end TODO