File Coverage

blib/lib/IPC/Locker.pm
Criterion Covered Total %
statement 148 214 69.1
branch 68 148 45.9
condition 25 53 47.1
subroutine 22 32 68.7
pod 10 14 71.4
total 273 461 59.2


line stmt bran cond sub pod time code
1             # See copyright, etc in below POD section.
2             ######################################################################
3              
4             =head1 NAME
5              
6             IPC::Locker - Distributed lock handler
7              
8             =head1 SYNOPSIS
9              
10             use IPC::Locker;
11              
12             my $lock = IPC::Locker->lock(lock=>'one_per_machine',
13             host=>'example.std.com',
14             port=>223);
15              
16             if ($lock->lock()) { something; }
17             if ($lock->locked()) { something; }
18              
19             $lock->unlock();
20              
21             =head1 DESCRIPTION
22              
23             L will query a remote lockerd server to obtain a lock around a
24             critical section. When the critical section completes, the lock may be
25             returned.
26              
27             This is useful for distributed utilities which run on many machines, and
28             cannot use file locks or other such mechanisms due to NFS or lack of common
29             file systems.
30              
31             Multiple locks may be requested, in which case the first lock to be free
32             will be used. Lock requests are serviced in a first-in-first-out order,
33             and the locker can optionally free locks for any processes that cease to
34             exist.
35              
36             =over 4
37              
38             =item new ([parameter=>value ...]);
39              
40             Create a lock structure.
41              
42             =item lock ([parameter=>value ...]);
43              
44             Try to obtain the lock, return the lock object if successful, else undef.
45              
46             =item locked ()
47              
48             Return true if the lock has been obtained.
49              
50             =item lock_name ()
51              
52             Return the name of the lock.
53              
54             =item unlock ()
55              
56             Remove the given lock. This will be called automatically when the object
57             is destroyed.
58              
59             =item ping ()
60              
61             A simplified version of ping_status; polls the server to see if it is up.
62             Returns true if up, otherwise undef.
63              
64             =item ping_status ()
65              
66             Polls the server to see if it is up. Returns hash reference with {ok}
67             indicating if up, and {status} with status information. If called without
68             an object, defaults to call new() with connect_tries=>1, under the
69             assumption that a quick go/nogo response is desired.
70              
71             =item break_lock ()
72              
73             Remove current locker for the given lock.
74              
75             =item owner ([parameter=>value ...]);
76              
77             Returns a string of who has the lock or undef if not currently locked.
78             Note that this information is not atomic, and may change asynchronously; do
79             not use this to tell if the lock will be available, to do that, try to
80             obtain the lock and then release it if you got it.
81              
82             =back
83              
84             =head1 PARAMETERS
85              
86             =over 4
87              
88             =item block
89              
90             Boolean flag, true indicates wait for the lock when calling lock() and die
91             if an error occurs. False indicates to just return false. Defaults to
92             true.
93              
94             =item connect_tries
95              
96             If none of the lockerd hosts are available or other network errors are
97             encountered, perform this number of retries, with a random connect_delay to
98             connect_delay*2 interval between them before signalling an error.
99              
100             =item connect_delay
101              
102             The minimum seconds to wait between each of the connect_tries, and
103             one-half of the maximum random wait. Defaults to 30 seconds.
104              
105             =item destroy_unlock
106              
107             Boolean flag, true indicates destruction of the lock variable should unlock
108             the lock, only if the current process id matches the pid passed to the
109             constructor. Set to false if destruction should not close the lock, such
110             as when other children destroying the lock variable should not unlock the
111             lock.
112              
113             =item family
114              
115             The family of transport to use, either INET or UNIX. Defaults to INET.
116              
117             =item host
118              
119             The name of the host containing the lock server. It may also be an array
120             of hostnames, where if the first one is down, subsequent ones will be
121             tried. Defaults to value of IPCLOCKER_HOST or localhost.
122              
123             =item port
124              
125             The port number (INET) or name (UNIX) of the lock server. Defaults to
126             IPCLOCKER_PORT environment variable, else 'lockerd' looked up via
127             /etc/services, else 1751.
128              
129             =item lock
130              
131             The name of the lock. This may also be a reference to an array of lock names,
132             and the first free lock will be returned.
133              
134             =item lock_list
135              
136             Return a list of lock and lock owner pairs. (You can assign this to a hash
137             for easier parsing.)
138              
139             =item pid
140              
141             The process ID that owns the lock, defaults to the current process id.
142              
143             =item print_broke
144              
145             A function to print a message when the lock is broken. The only argument
146             is self. Defaults to print a message if verbose is set.
147              
148             =item print_down
149              
150             A function to print a message when the lock server is unavailable. The
151             first argument is self. Defaults to a croak message.
152              
153             =item print_obtained
154              
155             A function to print a message when the lock is obtained after a delay. The
156             only argument is self. Defaults to print a message if verbose is set.
157              
158             =item print_retry
159              
160             A function to print a message when the lock server is unavailable, and is
161             about to be retried. The first argument is self. Defaults to a print
162             message.
163              
164             =item print_waiting
165              
166             A function to print a message when the lock is busy and needs to be waited
167             for. The first argument is self, second the name of the lock. Defaults to
168             print a message if verbose is set.
169              
170             =item timeout
171              
172             The maximum time in seconds that the lock may be held before being forced
173             open, passed to the server when the lock is created. Thus if the requester
174             dies, the lock will be released after that amount of time. Zero disables
175             the timeout. Defaults to 30 minutes.
176              
177             =item user
178              
179             Name to request the lock under, defaults to host_pid_user
180              
181             =item autounlock
182              
183             True to cause the server to automatically timeout a lock if the locking
184             process has died. For the process to be detected, it must be on the same
185             host as either the locker client (the host making the lock call), or the
186             locker server. Defaults false.
187              
188             =item verbose
189              
190             True to print messages when waiting for locks. Defaults false.
191              
192             =back
193              
194             =head1 ENVIRONMENT
195              
196             =over 4
197              
198             =item IPCLOCKER_HOST
199              
200             Hostname of L server, or colon separated list including backup
201             servers. Defaults to localhost.
202              
203             =item IPCLOCKER_PORT
204              
205             The port number (INET) or name (UNIX) of the lock server. Defaults to
206             'lockerd' looked up via /etc/services, else 1751.
207              
208             =back
209              
210             =head1 DISTRIBUTION
211              
212             The latest version is available from CPAN and from L.
213              
214             Copyright 1999-2022 by Wilson Snyder. This package is free software; you
215             can redistribute it and/or modify it under the terms of either the GNU
216             Lesser General Public License Version 3 or the Perl Artistic License Version 2.0.
217              
218             =head1 AUTHORS
219              
220             Wilson Snyder
221              
222             =head1 SEE ALSO
223              
224             L, L
225              
226             L, L, L, L
227              
228             =cut
229              
230             ######################################################################
231              
232             package IPC::Locker;
233             require 5.004;
234             require Exporter;
235             @ISA = qw(Exporter);
236              
237 3     3   79995 use Socket;
  3         14  
  3         1088  
238 3     3   1282 use Time::HiRes qw(gettimeofday tv_interval);
  3         3406  
  3         8  
239 3     3   466 use IO::Socket;
  3         6  
  3         14  
240              
241 3     3   2547 use IPC::PidStat;
  3         6  
  3         88  
242 3     3   14 use strict;
  3         5  
  3         65  
243 3     3   21 use vars qw($VERSION $Debug $Default_Port $Default_Family $Default_UNIX_port $Default_PidStat_Port);
  3         5  
  3         170  
244 3     3   14 use Carp;
  3         4  
  3         7504  
245              
246             ######################################################################
247             #### Configuration Section
248              
249             # Other configurable settings.
250             $Debug = 0;
251              
252             $VERSION = '1.502';
253              
254             ######################################################################
255             #### Useful Globals
256              
257             $Default_Port = ($ENV{IPCLOCKER_PORT}||'lockerd'); # Number (1751) or name to lookup in /etc/services
258             $Default_Port = 1751 if ($Default_Port !~ /^\d+$/ && !getservbyname ($Default_Port,""));
259             $Default_PidStat_Port = 'pidstatd'; # Number (1752) or name to lookup in /etc/services
260             $Default_PidStat_Port = 1752 if !getservbyname ($Default_PidStat_Port,"");
261             $Default_Family = 'INET';
262             $Default_UNIX_port = '/var/locks/lockerd';
263              
264             ######################################################################
265             #### Creator
266              
267             sub new {
268 6 50   6 1 1005725 @_ >= 1 or croak 'usage: IPC::Locker->new ({options})';
269 6         17 my $proto = shift;
270 6   33     48 my $class = ref($proto) || $proto;
271 6         15 my $hostname = hostfqdn();
272             my $self = {
273             #Documented
274             host=>($ENV{IPCLOCKER_HOST}||'localhost'),
275             port=>$Default_Port,
276             lock=>['lock'],
277             timeout=>60*10, block=>1,
278             pid=>$$,
279             #user=> # below
280             hostname=>$hostname,
281             autounlock=>0,
282             destroy_unlock=>1,
283             verbose=>$Debug,
284             connect_tries=>3,
285             connect_delay=>30,
286 0 0   0   0 print_broke=>sub {my $self=shift; print "Broke lock from $_[0] at ".(scalar(localtime))."\n" if $self->{verbose};},
  0         0  
287 0 0   0   0 print_obtained=>sub {my $self=shift; print "Obtained lock at ".(scalar(localtime))."\n" if $self->{verbose};},
  0         0  
288 0 0   0   0 print_waiting=>sub {my $self=shift; print "Waiting for lock from $_[0] at ".(scalar(localtime))."\n" if $self->{verbose};},
  0         0  
289 0 0   0   0 print_retry=>sub {my ($self,$sleep)=@_; print "Unable to connect to server, retrying connection in ${sleep} sec at ".(scalar(localtime))."\n" if $self->{verbose};},
  0         0  
290 6   50     240 print_down=>undef,
291             family=>$Default_Family,
292             #Internal
293             locked=>0,
294             @_,};
295 6   50     37 $self->{user} ||= hostfqdn() . "_".$self->{pid}."_" . ($ENV{USER} || "");
      66        
296 6         24 foreach (_array_or_one($self->{lock})) {
297 6 50       33 ($_ !~ /\s/) or carp "%Error: Lock names cannot contain whitespace: $_\n";
298             }
299 6         13 bless $self, $class;
300 6 50       16 _timelog("Locker->new ",$self->lock_name_list,"\n") if $Debug;
301 6         18 return $self;
302             }
303              
304             ######################################################################
305             #### Static Accessors
306              
307             sub hostfqdn {
308 11     11 0 37 return IPC::PidStat::hostfqdn();
309             }
310              
311             ######################################################################
312             #### Accessors
313              
314             sub locked () {
315 21 50 33 21 1 31 my $self = shift; ($self && ref($self)) or croak 'usage: $self->locked()';
  21         71  
316 21 100       43 return $self if $self->{locked};
317 12         29 return undef;
318             }
319              
320             sub ping {
321 1     1 1 3 my $self = shift;
322 1         3 my $res = $self->ping_status(@_);
323 1 50       4 if ($res->{ok}) {
324 1         4 return $self;
325             } else {
326 0         0 return undef;
327             }
328             }
329              
330             sub ping_status {
331 1     1 1 2 my $self = shift;
332             # Return OK and status message, for nagios like checks
333 1 50       3 $self = $self->new(connect_tries=>1, @_) if (!ref($self));
334 1         2 my $ok = 0;
335 1         4 my $start_time = [gettimeofday()];
336 1         2 eval {
337 1         3 $self->_request("");
338 1         2 $ok = 1;
339             };
340 1         23 my $elapsed = tv_interval ( $start_time, [gettimeofday]);
341              
342 1 50       19 if (!$ok) {
343 0         0 return ({ok=>undef,status=>"No response from lockerd on $self->{host}:$self->{port}"});
344             } else {
345 1         19 return ({ok=>1,status=>sprintf("%1.3f second response on $self->{host}:$self->{port}", $elapsed)});
346             }
347             }
348              
349             ######################################################################
350             #### Constructor
351              
352             sub lock {
353 8     8 1 957 my $self = shift;
354 8 100       24 $self = $self->new(@_) if (!ref($self));
355 8 100       14 if (!$self->locked) {
356 6         20 $self->_request("LOCK");
357 6 50       17 croak $self->{error} if $self->{error};
358             }
359 8 100       39 return ($self) if $self->{locked};
360 2         15 return undef;
361             }
362              
363             ######################################################################
364             #### Destructor/Unlock
365              
366             sub DESTROY () {
367 6 50 33 6   601 my $self = shift; ($self && ref($self)) or croak 'usage: $self->DESTROY()';
  6         27  
368 6 50 33     32 if ($self->{destroy_unlock} && $self->{pid} && $self->{pid}==$$) {
      33        
369 6         12 $self->unlock();
370             }
371             }
372              
373             sub unlock {
374 8 50 33 8 1 8 my $self = shift; ($self && ref($self)) or croak 'usage: $self->unlock()';
  8         21  
375 8 100       14 if ($self->locked) {
376 4         8 $self->_request("UNLOCK");
377 4 50       10 croak $self->{error} if $self->{error};
378             }
379 8         198 return ($self);
380             }
381              
382             sub break_lock {
383 0 0   0 1 0 my $self = shift; ($self) or croak 'usage: $self->break_lock()';
  0         0  
384 0 0       0 $self = $self->new(@_) if (!ref($self));
385 0         0 $self->_request("BREAK_LOCK");
386 0 0       0 croak $self->{error} if $self->{error};
387 0         0 return ($self);
388             }
389              
390             sub dead_pid {
391 0 0   0 0 0 my $self = shift; (ref $self) or croak 'usage: $self->dead_pid()';
  0         0  
392 0         0 my %args = (host => hostfqdn(),
393             pid => -1,
394             @_);
395             # Used internally to indicate a pid is gone.
396 0         0 $self->_request("DEAD_PID $args{host} $args{pid}");
397 0 0       0 croak $self->{error} if $self->{error};
398 0         0 return ($self);
399             }
400              
401             ######################################################################
402             #### User utilities: owner
403              
404             sub owner {
405 1 50   1 1 2 my $self = shift; ($self) or croak 'usage: $self->status()';
  1         3  
406 1 50       2 $self = $self->new(@_) if (!ref($self));
407 1         3 $self->_request ("STATUS");
408 1 50       3 croak $self->{error} if $self->{error};
409 1 50 0     3 _timelog("Locker->owner = ",($self->{owner}||''),"\n") if $Debug;
410 1         4 return $self->{owner};
411             }
412              
413             sub lock_name {
414 2 50   2 1 248 my $self = shift; ($self) or croak 'usage: $self->lock_name()';
  2         5  
415 2 50 33     5 if (ref $self->{lock}
416 0         0 && $#{$self->{lock}}<1) {
417 0         0 return $self->{lock}[0];
418             } else {
419 2         9 return $self->{lock};
420             }
421             }
422              
423             sub lock_list {
424 1     1 1 3 my $self = shift;
425 1 50       3 $self = $self->new(@_) if (!ref($self));
426 1         3 $self->_request("LOCK_LIST");
427 1 50       3 croak $self->{error} if $self->{error};
428 1         2 return @{$self->{lock_list}};
  1         9  
429             }
430              
431             ######################################################################
432             ######################################################################
433             #### Guts: Sending and receiving messages
434              
435             sub _request {
436 13     13   14 my $self = shift;
437 13         46 my $cmd = shift;
438              
439 13         29 my @hostlist = ('localhost');
440 13 50       26 if ($self->{family} eq 'INET') {
441 13         17 @hostlist = ($self->{host});
442 13 50       44 @hostlist = split (':', $self->{host}) if (!ref($self->{host}));
443 13 50       21 @hostlist = @{$self->{host}} if (ref($self->{host}) eq "ARRAY");
  0         0  
444             }
445              
446 13         12 my $ok;
447             try:
448 13   50     35 for (my $tries = 0; $tries < ($self->{connect_tries}||1); $tries++) {
449 13 50       17 if ($tries > 0) {
450 0         0 my $sleep = $self->{connect_delay} + int(rand($self->{connect_delay}));
451 0 0       0 _timelog("Locker->connect_delay $sleep sec\n") if $Debug;
452 0         0 &{$self->{print_retry}} ($self, $sleep);
  0         0  
453 0         0 sleep($sleep);
454             }
455 13         15 foreach my $host (@hostlist) {
456 13         31 $ok = $self->_request_attempt($cmd,$host);
457 13 50       24 if ($ok) {
458 13 50       21 if ($host ne $hostlist[0]) {
459             # Reorganize host list so whoever responded is first
460             # This is so if we grab a lock we'll try to return it to the same host
461 0         0 $self->{host} = [$host, grep( ($_ ne $host), @hostlist)];
462             }
463 13         21 last try;
464             }
465             }
466             }
467              
468 13 50       17 if (!$ok) {
469 0 0       0 if (defined $self->{print_down}) {
470 0         0 &{$self->{print_down}} ($self);
  0         0  
471 0         0 return;
472             }
473             croak "%Error: Can't locate lock server on "
474 0 0       0 . (($self->{family} eq 'INET') ? (join " or ", @hostlist) : "UNIX port")
475             ." $self->{port}\n"
476             . "\tYou probably need to run lockerd\n$self->_request(): Stopped";
477             }
478              
479 13 50       25 _timelog("Locker->DONE\n") if $Debug;
480             }
481              
482             sub _request_attempt {
483 13     13   14 my $self = shift;
484 13         12 my $cmd = shift;
485 13         27 my $host = shift;
486             # Return true if request was successful
487              
488             # IO::Socket::INET nastily undef's $@. Since this may get called
489             # in a destructor due to an error, that looses the error message.
490             # Workaround: save the error and restore at the end.
491 13         12 my $preerror = $@;
492              
493             retry:
494              
495             # If adding new features, only send the new feature to the server
496             # if the feature is on. This allows for newer clients that don't
497             # need to the new feature to still talk to older servers.
498             my $req = ("user $self->{user}\n"
499 13         24 ."locks ".join(' ',@{_array_or_one($self->{lock})})."\n");
  13         18  
500             $req.= ("block ".($self->{block}||0)."\n"
501 13 100 100     52 ."timeout ".($self->{timeout}||0)."\n") if $cmd ne 'UNLOCK';
      50        
502             $req.= ("autounlock ".($self->{autounlock}||0)."\n"
503             ."pid ".($self->{pid}||$$)."\n"
504             ."hostname ".($self->{hostname})."\n"
505 13 100 50     47 ) if $self->{autounlock} && $cmd ne 'UNLOCK';
      33        
      100        
506 13         23 $req.= ("$cmd\n"
507             ."\n" # End of group. Some day we may not always send EOF immediately
508             ."EOF\n");
509 13 50       17 _timelog("Locker->REQ\nR ",join("\nR ",split(/\n/,$req)),"\n") if $Debug;
510              
511 13         12 my $fh;
512 13 50       22 if ($self->{family} eq 'INET') {
    0          
513 13 50       15 _timelog("Locker->Trying host $host $self->{port}\n") if $Debug;
514             $fh = IO::Socket::INET->new( Proto => _tcp_proto(),
515             PeerAddr => $host,
516 13         39 PeerPort => $self->{port}, );
517             } elsif ($self->{family} eq 'UNIX') {
518 0 0       0 _timelog("Locker->Trying UNIX socket\n") if $Debug;
519 0         0 $fh = IO::Socket::UNIX->new( Peer => $self->{port}, );
520             } else {
521 0         0 croak "IPC::Locker->_request(): No or wrong transport specified.";
522             }
523              
524 13 50       4366 return undef if !$fh;
525              
526 13         37 $self->{lock_list} = [];
527              
528 13         328 print $fh "$req\n";
529 13         9277 while (defined (my $line = <$fh>)) {
530 52         69 chomp $line;
531 52 100       544 next if $line =~ /^\s*$/;
532 28         87 my @args = split /\s+/, $line;
533 28         39 my $cmd = shift @args;
534 28 50       41 _timelog("RESP $line\n") if $Debug;
535 28 100       40 $self->{locked} = $args[0] if ($cmd eq "locked");
536 28 100       42 $self->{owner} = $args[0] if ($cmd eq "owner");
537 28 50       33 $self->{error} = $args[0] if ($cmd eq "error");
538 28 100       32 if ($cmd eq "lockname") { # LOCK request's reply
539 5         10 $self->{lock} = [$args[0]];
540 5 50       5 $self->{lock} = $self->{lock}[0] if ($#{$self->{lock}}<1); # Back compatible
  5         15  
541             }
542 28 100 66     60 if ($cmd eq 'lock' && @args == 2) { # LOCK_LIST request's reply
543 1         3 push @{$self->{lock_list}}, @args;
  1         3  
544             }
545 28 50       30 if ($cmd eq "autounlock_check") {
546             # See if we can break the lock because the lock holder ran on this same machine.
547 0         0 my ($lname,$lhost,$lpid,$supports_dead) = @args;
548 0 0       0 if ($self->{hostname} eq $lhost) {
549 0 0       0 if (IPC::PidStat::local_pid_doesnt_exist($lpid)) {
550 0 0       0 _timelog("Autounlock_LOCAL $lname $lhost $lpid $supports_dead\n") if $Debug;
551 0 0       0 if ($supports_dead) { # 1.480 server and newer
552 0         0 $self->dead_pid(host=>$lhost, pid=>$lpid);
553             } else { # This has a potential race case, which may kill the wrong lock
554 0         0 $self->break_lock(lock=>$self->{lock});
555             }
556 0         0 $fh->close();
557 0         0 goto retry;
558             }
559             }
560             }
561 28 50       35 &{$self->{print_obtained}} ($self,@args) if ($cmd eq "print_obtained");
  0         0  
562 28 50       32 &{$self->{print_waiting}} ($self,@args) if ($cmd eq "print_waiting");
  0         0  
563 28 50       33 &{$self->{print_broke}} ($self,@args) if ($cmd eq "print_broke");
  0         0  
564 28 0 33     79 print "$1\n" if ($line =~ /^ECHO\s+(.*)$/ && $self->{verbose}); #debugging
565             }
566             # Note above break_lock also has prologue close
567 13         68 $fh->close();
568              
569 13   66     645 $@ = $preerror || $@; # User's error is more important than any we make
570 13         56 return 1;
571             }
572              
573             ######################################################################
574              
575             our $_Tcp_Proto;
576             sub _tcp_proto {
577             # We don't want creating a socket to have to keep reading /etc/services
578             # One would have thought IO::Socket etc kept this for us...
579 13 100   13   19 if (!defined $_Tcp_Proto) {
580 1 50       109 $_Tcp_Proto = getprotobyname("tcp")
581             or die "Could not determine the protocol number for tcp";
582             }
583 13         152 return $_Tcp_Proto;
584             }
585              
586             sub _array_or_one {
587 19 100   19   52 return [$_[0]] if !ref $_[0];
588 10         24 return $_[0];
589             }
590              
591             sub colon_joined_list {
592 0     0 0   my $item = shift;
593 0 0         return $item if !ref $item;
594 0           return (join ":",@{$item});
  0            
595             }
596              
597             sub lock_name_list {
598 0     0 0   my $self = shift;
599 0           return colon_joined_list($self->{lock});
600             }
601              
602             ######################################################################
603             #### Logging
604              
605             sub _timelog {
606 0     0     my $msg = join('',@_);
607 0           my ($time, $time_usec) = Time::HiRes::gettimeofday();
608 0           my ($sec,$min,$hour,$mday,$mon) = localtime($time);
609 0           printf +("[%02d/%02d %02d:%02d:%02d.%06d] %s",
610             $mon+1, $mday, $hour, $min, $sec, $time_usec, $msg);
611             }
612              
613             sub _timelog_split {
614 0     0     my $first = shift;
615 0           my $prefix = shift;
616 0           my $text = shift;
617 0           my $msg = $first . join("\n$prefix", split(/\n+/, "\n$text")) . "\n";
618 0           _timelog($msg)
619             }
620              
621             ######################################################################
622             #### Package return
623             1;