File Coverage

lib/Lock/File.pm
Criterion Covered Total %
statement 143 159 89.9
branch 53 70 75.7
condition 27 37 72.9
subroutine 24 28 85.7
pod 7 12 58.3
total 254 306 83.0


line stmt bran cond sub pod time code
1             package Lock::File;
2             {
3             $Lock::File::VERSION = '1.03';
4             }
5              
6             # ABSTRACT: file locker with an automatic out-of-scope unlocking mechanism
7              
8              
9 21     21   97375 use strict;
  21         25  
  21         659  
10 21     21   107 no warnings;
  21         21  
  21         836  
11 21     21   103 use Fcntl qw(:DEFAULT :flock);
  21         31  
  21         11055  
12              
13 21     21   9056 use Lock::File::Alarm;
  21         42  
  21         575  
14              
15 21     21   20153 use Log::Any qw($log);
  21         53585  
  21         103  
16 21     21   34104 use POSIX qw(:errno_h);
  21         180904  
  21         206  
17 21     21   46233 use Carp;
  21         44  
  21         1608  
18              
19 21     21   107 use base qw(Exporter);
  21         23  
  21         54958  
20             our @EXPORT_OK = qw( lockfile lockfile_multi lockfile_any lockf lockf_multi lockf_any );
21             our %EXPORT_TAGS = (all => \@EXPORT_OK);
22              
23             sub DESTROY {
24 1140     1140   21933624 local $@;
25 1140         2422 my ($self) = @_;
26 1140         13192 my $fh = $self->{_fh};
27 1140 50       4081 return unless defined $fh; # already released
28 1140 100 66     116567 unlink $self->{_fname} if $self->{_remove} and $self->{_fname};
29 1140         250871 flock $fh, LOCK_UN; # don't check result code - sometimes this handle appeares to be already closed
30 1140         72772 delete $self->{_fh}; # closes the file if opened by us
31             }
32              
33             my %defaults = (
34             shared => 0,
35             blocking => 1,
36             timeout => undef,
37             mode => undef,
38             remove => 0,
39             );
40              
41             sub _validate {
42 1837     1837   13334 my ($opts, @attrs) = @_;
43 1837         18780 my $opts_copy = {%$opts};
44 1824         23336 delete $opts_copy->{$_} for @attrs;
45 1824 100       11048 die "Unexpected options: ".join(',', keys %$opts_copy) if %$opts_copy;
46             }
47              
48             sub lockfile ($;$) {
49 1673     1673 1 110256878 return __PACKAGE__->new(@_);
50             }
51              
52             sub lockf ($;$) {
53 17     17 0 19674629 warn "lockf() is deprecated, use lockfile() instead";
54 17         1441 goto &lockfile;
55             }
56              
57             sub new {
58 1673     1673 0 3780 my $class = shift;
59 1673         5507 my ($param, $opts) = @_;
60 1673 50 33     17715 if (@_ > 2 or @_ < 1) {
61 0         0 croak "invalid lockfile arguments";
62             }
63              
64 1673   100     6017 $opts ||= {};
65 1673         6719 _validate($opts, qw/ blocking shared timeout mode remove /);
66              
67 1658 100 100     13910 if (exists $opts->{blocking} and not $opts->{blocking} and defined $opts->{timeout}) {
      100        
68 1         16 die "non-blocking mode is incompatible with timeout option";
69             }
70 1657         18984 $opts = {%defaults, %$opts};
71              
72 1657         3812 my ($fh, $fname);
73 1657 50       4856 if (ref $param eq "") { # filename instead of filehandle
74 1657         2888 $fname = $param;
75             } else {
76 0         0 $fh = $param;
77             }
78              
79 1657         10898 $fh = _lock_and_check($fh, $fname, $opts);
80 1622 100       4932 unless ($fh) {
81 482         2346 return;
82             }
83              
84 1140         15727 return bless {
85             _fh => $fh,
86             _fname => $fname,
87             _remove => $opts->{remove},
88             } => $class;
89             }
90              
91             sub _log_message ($;$) {
92 15     15   120 my ($bang, $question) = @_;
93 15         90 my @msg;
94 15 50       135 if ($bang) {
95 15         120 push @msg, "error ".int($bang)." '$bang'";
96             }
97 15 50 33     135 if (defined $question and $question > 0) {
98 0 0       0 push @msg, "kill by signal ".($question & 127) if ($question & 127);
99 0 0       0 push @msg, "core dumped" if ($question & 128);
100 0 0       0 push @msg, "exit code ".($question >> 8) if $question >> 8;
101             }
102 15         285 return join ", ", @msg;
103             }
104              
105             sub _open {
106 3214     3214   12781 my ($chmod, $fname) = @_;
107              
108 3214         5732 my $fh;
109             my $res;
110 3214 100       9343 if (defined $chmod) {
111 30 50       150 $chmod = oct($chmod) if $chmod =~ s/^0//;
112 30         60 my $mode = O_WRONLY|O_CREAT|O_APPEND;
113 30         405 my $umask = umask(0);
114 30         1845 $res = sysopen $fh, $fname, $mode, $chmod;
115 30         135 umask($umask);
116             }
117             else {
118 3184         861588 $res = open $fh, '>>', $fname;
119             }
120 3214 100       11397 die "open $fname with mode $chmod failed: ", _log_message($!) unless $res;
121 3199         10465 return $fh;
122             }
123              
124             sub _lock_and_check {
125 1657     1657   3441 my ($fh, $fname, $opts) = @_;
126              
127 1657 50       10163 unless (defined $fname) { # no unlink/lockile race when locking an already opened filehandle
128 0 0       0 return _lock(@_) ? $fh : undef;
129             }
130              
131 1657         2661 while () {
132 3214         32248 $fh = _open($opts->{mode}, $fname);
133 3199         154129 my $lock = _lock($fh, $fname, $opts);
134 3179 100       31581 return unless $lock;
135              
136 2697 100       269423 unless (-e $fname) {
137 481         4542 $log->debug("$fname: locked but removed");
138 481         3114 next;
139             }
140 2216 100       174419 unless ((stat $fh)[1] eq (stat $fname)[1]) {
141 1076         9398 $log->debug("$fname: locked but removed and created back");
142 1076         12234 next;
143             }
144 1140         5250 return $fh;
145             }
146             }
147              
148             sub _xflock {
149 1543     1543   3577 my ($fh, $mode) = @_;
150 1543 100       72122376 flock $fh, $mode or die "flock failed: $!";
151             }
152              
153             sub _lock {
154 3199     3199   10990 my ($fh, $fname, $opts) = @_;
155              
156 3199   50     23600 $fname ||= ''; # TODO - discover $fname from $fh, it's possible in most cases with some /proc magic
157              
158 3199 100       12242 my $mode = ($opts->{shared} ? LOCK_SH : LOCK_EX);
159              
160 3199 100 100     36817 if (
      66        
161             not $opts->{blocking}
162             or (defined $opts->{timeout} and not $opts->{timeout}) # timeout=0
163             ) {
164 643 100       8448 return 1 if flock ($fh, $mode | LOCK_NB);
165 483 50       6927 if ($! == EWOULDBLOCK) {
166 483 100       1337 croak "flock $fname failed: timed out" if defined $opts->{timeout}; # timeout=0
167 482         1245 return 0;
168             }
169 0   0     0 croak "flock ".($fname || '')." failed: $!";
170             }
171              
172 2556 100       35695 unless (flock ($fh, $mode | LOCK_NB)) {
173 1541         5911 my $msg = "$fname already locked, wait...";
174 1541         8702 $log->debug($msg);
175             } else {
176 1015         4078 return 1;
177             }
178              
179 1541 100       14381 if ($opts->{timeout}) {
180 55     19   1345 local $SIG{ALRM} = sub { croak "flock $fname failed: timed out" };
  19         4864  
181 55         1256 my $alarm = Lock::File::Alarm->new($opts->{timeout});
182 55         148 _xflock($fh, $mode);
183             } else {
184 1486         3783 _xflock($fh, $mode);
185             }
186 1522         7547 return 1;
187             }
188              
189             sub lockfile_multi ($$;$) {
190 158     158 1 83492984 my ($fname, $max, $opts) = @_;
191 158   100     13783 $opts ||= {};
192 158         3802 _validate($opts, qw/ remove mode /);
193              
194             # to make sure no one will mess up the things
195             # TODO - apply opts to metalock too?
196 145         2028 my $metalock = lockfile("$fname.meta", { remove => 1 });
197              
198 145         297826 my %flist = map { $_ => 1 } grep { /^\Q$fname\E\.\d+$/ } glob "\Q$fname\E.*";
  671         4146  
  816         6943  
199              
200 145         529 my $locked = 0;
201 145         196 my $ret;
202              
203             # try to get lock on existing file
204 145         1118 for my $file (keys %flist) {
205 545         2566 my $lock = lockfile($file, { blocking => 0, %$opts });
206 545 100       1852 $locked++ unless $lock;
207 545   100     1877 $ret ||= $lock;
208 545 100       1589 if ($locked >= $max) {
209 78         111 undef $ret;
210 78         425 last;
211             }
212             }
213              
214             # try non-existing files
215 145 100 100     998 if ($locked < $max and not $ret) {
216 50         420 for my $i (0 .. ($max-1)) {
217 135         433 my $file = "$fname.$i";
218 135 100       571 next if $flist{$file};
219 50         222 my $lock = lockfile($file, { blocking => 0, %$opts });
220 50 50       315 die "Unable to obtain lock on new multilock file $file" unless $lock; # this should never happen
221 50         67 $ret = $lock;
222 50         149 last;
223             }
224             }
225              
226 145 100       1034 return $ret if defined $ret;
227 78         987 return undef;
228             }
229              
230             sub lockf_multi {
231 0     0 0 0 warn "lockf_multi() is deprecated, use lockfile_multi() instead";
232 0         0 goto &lockfile_multi;
233             }
234              
235             sub lockfile_any ($;$) {
236 6     6 1 2110 my ($flist, $opts) = @_;
237 6   50     38 $opts ||= {};
238 6         20 _validate($opts, qw/ remove mode /);
239              
240 6         8 for my $fname (@$flist) {
241 10         40 my $lock = lockfile($fname, { blocking => 0, %$opts });
242 10 100       52 return $lock if $lock;
243             }
244              
245 2         62 return undef;
246             }
247              
248             sub lockf_any {
249 0     0 0 0 warn "lockf_any() is deprecated, use lockfile_any() instead";
250 0         0 goto &lockfile_any;
251             }
252              
253             sub name {
254 17     17 1 153 my $self = shift;
255 17         193 return $self->{_fname};
256             }
257              
258             sub share {
259 1     1 1 908585 my $self = shift;
260 1         31 _xflock($self->{_fh}, LOCK_SH);
261             }
262              
263             sub unshare {
264 1     1 1 302481 my $self = shift;
265 1         52 _xflock($self->{_fh}, LOCK_EX);
266             }
267              
268             sub unlock {
269 0     0 1   my $self = shift;
270 0           $self->DESTROY();
271             }
272              
273             sub unlockf {
274 0     0 0   warn "unlockf() method is deprecated, use ->unlock() instead";
275 0           goto &{ $_[0]->can('unlock') };
  0            
276             }
277              
278              
279             1;
280              
281             __END__