| 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 : |