File Coverage

blib/lib/IPC/Lock/WithTTL.pm
Criterion Covered Total %
statement 69 70 98.5
branch 21 26 80.7
condition 3 5 60.0
subroutine 11 11 100.0
pod 3 4 75.0
total 107 116 92.2


line stmt bran cond sub pod time code
1             package IPC::Lock::WithTTL;
2              
3 6     6   130323 use strict;
  6         7  
  6         136  
4 6     6   20 use warnings;
  6         8  
  6         187  
5              
6             our $VERSION = '0.02';
7              
8 6     6   18 use Carp;
  6         11  
  6         271  
9 6     6   2313 use Smart::Args;
  6         84735  
  6         374  
10             use Class::Accessor::Lite (
11 6         36 rw => [qw(ttl)],
12             ro => [qw(file kill_old_proc)],
13 6     6   2488 );
  6         4284  
14 6     6   437 use Fcntl qw(:DEFAULT :flock :seek);
  6         9  
  6         3944  
15              
16             sub new {
17 5     5 1 1542 args(my $class,
18             my $file => { isa => 'Str' },
19             my $ttl => { isa => 'Int', default => 0 },
20             my $kill_old_proc => { isa => 'Bool', default => 0 },
21             );
22              
23 5         659 my $self = bless {
24             file => $file,
25             ttl => $ttl,
26             kill_old_proc => $kill_old_proc,
27             #
28             _fh => undef,
29             }, $class;
30              
31 5         11 return $self;
32             }
33              
34             sub _fh {
35 17     17   47 args(my $self);
36              
37 17 100       317 unless ($self->{_fh}) {
38 5 50       64 open $self->{_fh}, '+>>', $self->file or croak $!;
39             }
40              
41 17         464 return $self->{_fh};
42             }
43              
44             sub acquire {
45 9     9 1 12123364 args(my $self,
46             my $ttl => { isa => 'Int', optional => 1 },
47             );
48 9 50       934 $self->ttl($ttl) if $ttl;
49              
50 9         40 my $fh = $self->_fh;
51 9 50       105 flock $fh, LOCK_EX or return;
52              
53 9         50 seek $fh, 0, SEEK_SET;
54 9         217 my($heartbeat) = <$fh>;
55 9   100     65 $heartbeat ||= "0 0";
56 9         80 my($pid, $expiration) = split /\s+/, $heartbeat;
57 9         37 $pid += 0; $expiration += 0;
  9         15  
58              
59 9         17 my $now = time();
60 9         10 my $new_expiration;
61 9         14 my $acquired = 0;
62 9 100       53 if ($pid == 0) {
    100          
63             # Previous task finished successfully
64 5 100       11 if ($now >= $expiration) {
65             # expired
66 4         20 $new_expiration = $self->update_heartbeat;
67 4         7 $acquired = 1;
68             } else {
69             # not expired
70 1         2 $acquired = 0;
71             }
72             } elsif ($pid != $$) {
73             # Other task is in process?
74 2 100       6 if ($now >= $expiration) {
75             # expired (Last task may have terminated abnormally)
76 1         14 $new_expiration = $self->update_heartbeat;
77              
78 1 50 33     4 if ($self->kill_old_proc && $pid > 0) {
79 0         0 kill 'KILL', $pid;
80             }
81 1         10 $acquired = 1;
82             } else {
83             # not expired (Still running)
84 1         1 $acquired = 0;
85             }
86             } else {
87             # Previous task done by this process
88 2 100       5 if ($now >= $expiration) {
89             # expired (Last task may have terminated abnormally)
90 1         25 $new_expiration = $self->update_heartbeat;
91 1         4 $acquired = 1;
92             } else {
93             # not expired (Last task may have terminated abnormally)
94 1         3 $new_expiration = $self->update_heartbeat;
95 1         2 $acquired = 1;
96             }
97             }
98              
99 9         270 flock $fh, LOCK_UN;
100 9 100       26 if ($acquired) {
101 7 100       74 return wantarray ? (1, { pid => $$, expiration => $new_expiration })
102             : 1;
103             } else {
104 2 50       14 return wantarray ? (0, { pid => $pid, expiration => $expiration })
105             : 0;
106             }
107             }
108              
109             sub release {
110 1     1 1 1243 args(my $self);
111              
112 1         19 $self->update_heartbeat(pid => 0);
113 1         22 undef $self->{_fh};
114              
115 1         14 return 1;
116             }
117              
118             sub update_heartbeat {
119 8     8 0 53 args(my $self,
120             my $pid => { isa => 'Int', default => $$ },
121             );
122              
123 8         396 my $fh = $self->_fh;
124              
125 8         44 my $expiration = time() + $self->ttl;
126              
127 8         47 seek $fh, 0, SEEK_SET;
128 8         29312 truncate $fh, 0;
129 8         15 print {$fh} join(' ', $pid, $expiration)."\n";
  8         50  
130              
131 8         23 return $expiration;
132             }
133              
134             1;
135              
136             __END__
137              
138             =encoding utf-8
139              
140             =begin html
141              
142             <a href="https://travis-ci.org/hirose31/IPC-Lock-WithTTL"><img src="https://travis-ci.org/hirose31/IPC-Lock-WithTTL.png?branch=master" alt="Build Status" /></a>
143             <a href="https://coveralls.io/r/hirose31/IPC-Lock-WithTTL?branch=master"><img src="https://coveralls.io/repos/hirose31/IPC-Lock-WithTTL/badge.png?branch=master" alt="Coverage Status" /></a>
144              
145             =end html
146              
147             =head1 NAME
148              
149             IPC::Lock::WithTTL - run only one process up to given timeout
150              
151             =head1 SYNOPSIS
152              
153             use IPC::Lock::WithTTL;
154            
155             my $lock = IPC::Lock::WithTTL->new(
156             file => '/tmp/lockme',
157             ttl => 5,
158             kill_old_proc => 0,
159             );
160            
161             my($r, $hb) = $lock->acquire;
162            
163             if ($r) {
164             infof("Got lock! yay!!");
165             } else {
166             critf("Cannot get lock. Try after at %d", $hb->{expiration});
167             exit 1;
168             }
169            
170             $lock->release;
171              
172             =head1 DESCRIPTION
173              
174             IPC::Lock::WithTTL provides inter process locking feature.
175             This locking has timeout feature, so we can use following cases:
176              
177             * Once send an alert email, don't send same kind of alert email within 10 minutes.
178             * We want to prevent the situation that script for failover some system is invoked more than one processes at same time and invoked many times in short time.
179              
180             =head1 DETAIL
181              
182             =head2 SEQUENCE
183              
184             1. flock a heartbeat file (specified by file param in new) with LOCK_EX
185             return if failed to flock.
186             2. read a heartbeat file and examine PID and expiration (describe later)
187             return if I should not go ahead.
188             3. update a heartbeat file with my PID and new expiration.
189             4. ACQUIRED LOCK
190             5. unlock a lock file.
191             6. process main logic.
192             7. RELEASE LOCK with calling $lock->release method.
193             In that method update a heartbeat file with PID=0 and new expiration.
194              
195             =head2 DETAIL OF EXAMINATION OF PID AND EXPIRATION
196              
197             Format of a heartbeat file (lock file) is:
198              
199             PID EXPIRATION
200              
201             Next action table by PID and expiration
202              
203             PID expired? Next action Description
204             =========================================================================
205             not mine yes acquired lock*1 Another process is running or
206             - - - - - - - - - - - - - - - - - - exited abnormally (without leseasing
207             not mine no return lock).
208             -------------------------------------------------------------------------
209             mine yes acquired lock Previously myself acquired lock but
210             - - - - - - - - - - - - - - - - - - does not release lock.
211             mine no acquired lock
212             -------------------------------------------------------------------------
213             0 yes acquired lock Previously someone acquired and
214             - - - - - - - - - - - - - - - - - - released lock successfully.
215             0 no return
216             -------------------------------------------------------------------------
217            
218             *1 try to kill another process if you enable kill_old_proc option in new().
219              
220             =head1 METHODS
221              
222             =over 4
223              
224             =item B<new>($args:Hash)
225              
226             file => Str (required)
227             File path of heartbeat file. IPC::Lock::WithTTL also flock this file.
228            
229             ttl => Int (default is 0)
230             TTL to exipire. expiration time set to now + TTL.
231            
232             kill_old_proc => Boolean (default is 0)
233             Try to kill old process which might exit abnormally.
234              
235             =item B<acquire>(ttl => $TTL:Int)
236              
237             Try to acquire lock. ttl option set TTL to expire (override ttl in new())
238              
239             This method returns scalar or list by context.
240              
241             Scalar context
242             =========================================================================
243             Acquired lock successfully
244             1
245             -----------------------------------------------------------------------
246             Failed to acquire lock
247             0
248            
249             List context
250             =========================================================================
251             Acquired lock successfully
252             (1, { pid => PID, expiration => time_to_expire })
253             PID is mine. expiration is setted by me.
254             -----------------------------------------------------------------------
255             Failed to acquire lock
256             (0, { pid => PID, expiration => time_to_expire })
257             PID is another process. expiration is setted by another process.
258              
259             =item B<release>()
260              
261             Update a heartbeat file (PID=0 and new expiration) and release lock.
262              
263             =back
264              
265             =head1 AUTHOR
266              
267             HIROSE Masaaki E<lt>hirose31 _at_ gmail.comE<gt>
268              
269             =head1 REPOSITORY
270              
271             L<https://github.com/hirose31/IPC-Lock-WithTTL>
272              
273             git clone git://github.com/hirose31/IPC-Lock-WithTTL.git
274              
275             patches and collaborators are welcome.
276              
277             =head1 SEE ALSO
278              
279             L<IPC::Lock|IPC::Lock>
280              
281             =head1 LICENSE
282              
283             This library is free software; you can redistribute it and/or modify
284             it under the same terms as Perl itself.
285              
286             =cut
287              
288             # for Emacsen
289             # Local Variables:
290             # mode: cperl
291             # cperl-indent-level: 4
292             # indent-tabs-mode: nil
293             # coding: utf-8
294             # End:
295              
296             # vi: set ts=4 sw=4 sts=0 :