File Coverage

blib/lib/Lock/Server.pm
Criterion Covered Total %
statement 183 203 90.1
branch 31 42 73.8
condition 21 35 60.0
subroutine 25 25 100.0
pod 4 4 100.0
total 264 309 85.4


line stmt bran cond sub pod time code
1             package Lock::Server;
2              
3             =head1 NAME
4              
5             Lock::Server - Light-weight socket based resource locking manager.
6              
7             =head1 DESCRIPTION
8              
9             This creates a child process socket server that takes lock and
10             unlock requests. The lock requests only return once a lock is
11             obtained or a timeout has occurred. A lock may only be locked
12             for a specific amount of time before the lock is timed out.
13              
14             This does not do deadlock detection, relying on the timeouts to
15             prevent the system from getting in a hopelessly tangled state.
16             Care should be taken, as with any resource locking system, with
17             the use of Lock::Server. Adjust the timeouts for what makes sense
18             with the system you are designing. The lock requests return with the
19             time that the lock will expire.
20              
21             =head1 SYNPOSIS
22              
23             use Lock::Server;
24             use Lock::Server::Client;
25              
26             my $lockServer = new Lock::Server( {
27             lock_timeout => 10, #seconds. default is 3
28             lock_attempt_timeout => 12, #seconds. default is 4
29             port => 888, #default is 8004
30             host => 'localhost', #default 127.0.0.1
31             } );
32              
33             if( my $childPid = $lockServer->start ) {
34             print "Lock server started in child thread $childPid\n";
35             }
36              
37             my $lockClient_A = $lockServer->client( "CLIENT_A" );
38             my $lockClient_B =
39             new Lock::Server::Client( "CLIENT_B", 'localhost', 888 );
40              
41             if( $lockClient_A->lock( "KEYA" ) ) {
42             print "Lock Successfull for locker A and KEYA\n";
43             } else {
44             print "Could not obtain lock in 12 seconds.\n";
45             }
46              
47             # KEYA for LockerI times out after 10 seconds.
48             # Lock Client B waits until it can obtain the lock
49             if( $lockClient_B->lock( "KEYA" ) ) {
50             print "Lock Successfull for Client B lock 'KEYA'\n";
51             } else {
52             print "Could not obtain lock in 12 seconds.\n";
53             }
54              
55             # KEYA for LockerII is now freed. The next locker
56             # attempting to lock KEYA will then obtain the lock.
57             if( $lockClientB->unlock( "KEYA" ) ) {
58             print "Unlock Successfull\n";
59             }
60              
61             if( $lockServer->stop ) {
62             print "Lock server shut down.\n";
63             }
64              
65             =head1 METHODS
66            
67             =cut
68              
69 8     8   5992 use strict;
  8         8  
  8         200  
70 8     8   32 use warnings;
  8         16  
  8         200  
71 8     8   32 no warnings 'uninitialized';
  8         40  
  8         224  
72              
73              
74 8     8   7448 use IO::Socket::INET;
  8         206640  
  8         56  
75              
76 8     8   5504 use vars qw($VERSION);
  8         16  
  8         16400  
77              
78             $VERSION = '1.0';
79              
80              
81             $Lock::Server::DEBUG = 0;
82              
83             =head2 Lock::Server::new( $args )
84              
85             Creates a new lock server for the given optional arguments.
86            
87             Arguments are :
88             * port - port to serve on. Defaults to 8004
89             * lock_timeout - low long should a lock last in seconds
90             * lock_attempt_timeout - how long should a requester
91             wait for a lock in seconds
92              
93             =cut
94             sub new {
95 8     8 1 1157832 my( $pkg, $args ) = @_;
96 8   33     72 my $class = ref( $pkg ) || $pkg;
97             bless {
98             lock_timeout => $args->{lock_timeout} || 3,
99             lock_attempt_timeout => $args->{lock_attempt_timeout} || 4,
100             host => $args->{host} || '127.0.0.1',
101 8   50     184 port => $args->{port} || 8004,
      50        
      50        
      50        
102             _pids => {},
103             _id2pid => {},
104             _locks => {},
105             _locker_counts => {},
106             }, $class;
107             } #new
108              
109              
110             =head2 client( lockername )
111              
112             Returns a client with the given name that can send lock and unlock requests for keys.
113              
114             =cut
115             sub client {
116 8     8 1 330 my( $self, $name ) = @_;
117 8         685 Lock::Server::Client->new( $name, $self->{host}, $self->{port} );
118             }
119              
120             =head2 stop
121              
122             Kills the lock server, breaking off any connections that are waiting for a lock.
123              
124             =cut
125             sub stop {
126 1     1 1 5 my $self = shift;
127 1 50       11 if( my $pid = $self->{server_pid} ) {
128 1         16 $self->{error} = "Sending INT signal to lock server of pid '$pid'";
129 1         3260 kill 'INT', $pid;
130 1         17 return 1;
131             }
132 0         0 $self->{error} = "No lock server running";
133 0         0 return 0;
134             }
135              
136             =head2 start
137              
138             Starts the lock server in a child process, opening up a tcpip socket.
139              
140             =cut
141             sub start {
142 8     8 1 48 my $self = shift;
143 8         152 my $listener_socket = new IO::Socket::INET(
144             Listen => 10,
145             LocalAddr => "$self->{host}:$self->{port}",
146             );
147 8 50       2560 unless( $listener_socket ) {
148 0         0 $self->{error} = "Unable to open socket on port '$self->{port}' : $! $@\n";
149 0         0 _log( "unable to start lock server : $@ $!.\n" );
150 0         0 return 0;
151             }
152 8         32 $listener_socket->autoflush;
153 8 100       9192 if( my $pid = fork ) {
154             # parent
155 5         170 $self->{server_pid} = $pid;
156 5         2675 return $pid;
157             } else {
158             # child
159             $SIG{INT} = sub {
160 1     1   307665 _log( "lock server : got INT signal. Shutting down.\n" );
161 1 50       15 $listener_socket && $listener_socket->close;
162 1         47 for my $pid (keys %{ $self->{_pids} } ) {
  1         13  
163 2         21 kill 'HUP', $pid;
164             }
165 1         114 exit;
166 3         984 };
167              
168 3         1254 while( my $connection = $listener_socket->accept ) {
169 93         9718211 _log( "lock server : incoming request\n" );
170 93         14571 my $req = <$connection>;
171 93         282 chomp $req;
172 93         434 _log( "lock server : got request <$req>\n" );
173              
174 93 100       709 if( $req =~ /^CHECK (\S+)/ ) {
175 19         154 $self->_check( $connection, $1 );
176             } else {
177 74         941 my( $cmd, $key, $locker_id ) = ( $req =~ /^(\S+) (\S+) (\S+)/ );
178 74 100       689 if( $cmd eq 'LOCK' ) {
    100          
    50          
179 22         89 $self->_lock( $connection, $locker_id, $key );
180             } elsif( $cmd eq 'UNLOCK' ) {
181 17         148 $self->_unlock( $connection, $locker_id, $key );
182             } elsif( $cmd eq 'VERIFY' ) {
183 35         153 $self->_verify( $connection, $locker_id, $key );
184             } else {
185 0         0 _log( "lock server : did not understand request\n" );
186 0         0 $connection->close;
187             }
188             }
189             }
190             }
191             } #start
192              
193             sub _check {
194 19     19   141 my( $self, $connection, $key_to_check ) = @_;
195              
196 19         102 _log( "locker server check for key '$key_to_check'\n" );
197              
198 19   100     220 $self->{_locks}{$key_to_check} ||= [];
199 19         60 my $lockers = $self->{_locks}{$key_to_check};
200              
201            
202             #check for timed out lockers
203 19         78 my $t = time;
204 19   100     334 while( @$lockers && $t > $self->{_locker_counts}{$lockers->[0]}{$key_to_check} ) {
205 1         36 _log( "lock server _check : '$key_to_check' timed out for locker '$lockers->[0]'\n" );
206 1 50       3 if( 1 == keys %{ $self->{_locker_counts}{$lockers->[0]} } ) {
  1         28  
207 0         0 delete $self->{_locker_counts}{$lockers->[0]};
208             } else {
209 1         4 delete $self->{_locker_counts}{$lockers->[0]}{$key_to_check};
210             }
211 1         12 shift @$lockers;
212             }
213              
214              
215 19 100       96 if( @$lockers ) {
216 12         255 print $connection "1\n";
217             } else {
218 7         529 print $connection "0\n";
219             }
220 19         142 $connection->close;
221             }
222              
223             sub _log {
224 369     369   781 my $msg = shift;
225 369 50       1640 print STDERR "\t\t$msg\n" if $Lock::Server::DEBUG;
226             }
227              
228             sub _lock {
229 22     22   61 my( $self, $connection, $locker_id, $key_to_lock ) = @_;
230              
231 22         91 _log( "lock server : lock request for '$locker_id' and key '$key_to_lock'\n" );
232              
233 22   100     151 $self->{_locks}{$key_to_lock} ||= [];
234 22         64 my $lockers = $self->{_locks}{$key_to_lock};
235              
236             #check for timed out lockers
237 22         66 my $t = time;
238 22   66     167 while( @$lockers && $t > $self->{_locker_counts}{$lockers->[0]}{$key_to_lock} ) {
239 0         0 _log( "lock '$key_to_lock' timed out for locker '$lockers->[0]'\n" );
240 0 0       0 if( 1 == keys %{ $self->{_locker_counts}{$lockers->[0]} } ) {
  0         0  
241 0         0 delete $self->{_locker_counts}{$lockers->[0]};
242             } else {
243 0         0 delete $self->{_locker_counts}{$lockers->[0]}{$key_to_lock};
244             }
245 0         0 shift @$lockers;
246             }
247              
248              
249 22 100       74 if( 0 < (grep { $_ eq $locker_id } @$lockers) ) {
  8         75  
250 3         12 _log( "lock request error. '$locker_id' already in the lock queue\n" );
251 3         66 print $connection "0\n";
252 3         84 return;
253             }
254              
255             # store when this times out
256 19         48 my $timeout_time = time + $self->{lock_timeout};
257 19         72 $self->{_locker_counts}{$locker_id}{$key_to_lock} = $timeout_time;
258 19         182 push @$lockers, $locker_id;
259              
260 19         90 _log( "lock request : there are now ".scalar(@$lockers)." lockers\n" );
261 19 100       58 if( @$lockers > 1 ) {
262 5 100       7037 if( (my $pid=fork)) {
263 3         129 $self->{_id2pid}{$locker_id} = $pid;
264 3         114 $self->{_pids}{$pid} = 1;
265 3         232 _log( "lock request : parent process associating '$locker_id' with pid '$pid' ".scalar(@$lockers)." lockers\n" );
266             # parent
267             } else {
268             # child
269             $SIG{HUP} = sub {
270 1     1   38 _log( "lock request : child $$ got HUP, so is now locked.\n" );
271 1         161 print $connection "$timeout_time\n";
272 1         53 $connection->close;
273 1         1154 undef $connection;
274 1         287 exit;
275 2         499 };
276 2         206 _log( "lock request : child $$ ready to wait\n" );
277 2         7000311 sleep $self->{lock_attempt_timeout};
278 2         215 print $connection "0\n";
279 1         41 $connection->close;
280 1         262 exit;
281             }
282             } else {
283 14         29 _log( "lock request : no need to invoke more processes. locking\n" );
284 14         374 print $connection "$timeout_time\n";
285 14         69 $connection->close;
286             }
287             } #_lock
288              
289             sub _unlock {
290 17     17   47 my( $self, $connection, $locker_id, $key_to_unlock ) = @_;
291 17         70 _log( "lock server unlock for key '$key_to_unlock' for locker '$locker_id'\n" );
292              
293 17   50     88 $self->{_locks}{$key_to_unlock} ||= [];
294 17         35 my $lockers = $self->{_locks}{$key_to_unlock};
295              
296 17 100       85 if( $lockers->[0] eq $locker_id ) {
297 11         27 shift @$lockers;
298 11         75 delete $self->{_locker_counts}{$locker_id}{$key_to_unlock};
299 11 100       18 if( 0 == scalar(keys %{$self->{_locker_counts}{$locker_id}}) ) {
  11         158  
300 7         46 _log( "unlock : remove information about '$locker_id'\n" );
301 7         22 delete $self->{_id2pid}{$locker_id};
302 7         77 delete $self->{_locker_counts}{$locker_id};
303             }
304 11         67 _log( "unlocking '$locker_id'\n" );
305 11 100       39 if( @$lockers ) {
306 2         26 my $next_locker_id = $lockers->[0];
307 2         6 my $pid = $self->{_id2pid}{$next_locker_id};
308 2         42 _log( "unlock : next locker in queue is '$next_locker_id'. Sending kill signal to its pid '$pid'\n" );
309 2         130 kill 'HUP', $pid;
310             } else {
311 9         34 _log( "unlock : now no one waiting on a lock for key '$key_to_unlock'\n" );
312             }
313 11         29 _log( "unlock : done, informing connection\n" );
314 11         352 print $connection "1\n";
315 11         76 $connection->close;
316             } else {
317 6         24 _log( "unlock error : Wrong locker_id to unlock for unlock for locker '$locker_id' and key '$key_to_unlock'. The locker_id must be the one at the front of the queue\n" );
318             # "Wrong locker_id to unlock. The locker_id must be the one at the front of the queue";
319 6         123 print $connection "0\n";
320 6         27 $connection->close;
321             }
322             } #_unlock
323              
324             sub _verify {
325 35     35   76 my( $self, $connection, $locker_id, $key_to_check ) = @_;
326              
327 35         129 _log( "locker server check for key '$key_to_check' for locker '$locker_id'\n" );
328              
329 35   50     135 $self->{_locks}{$key_to_check} ||= [];
330 35         65 my $lockers = $self->{_locks}{$key_to_check};
331              
332             #check for timed out lockers
333 35         56 my $t = time;
334 35   66     247 while( @$lockers && $t > $self->{_locker_counts}{$lockers->[0]}{$key_to_check} ) {
335 0         0 _log( "lock '$key_to_check' timed out for locker '$lockers->[0]'\n" );
336 0 0       0 if( 1 == keys %{ $self->{_locker_counts}{$lockers->[0]} } ) {
  0         0  
337 0         0 delete $self->{_locker_counts}{$lockers->[0]};
338             } else {
339 0         0 delete $self->{_locker_counts}{$lockers->[0]}{$key_to_check};
340             }
341 0         0 shift @$lockers;
342             }
343              
344 35 100       95 if( $lockers->[0] eq $locker_id ) {
345 17         371 print $connection "1\n";
346             } else {
347 18         425 print $connection "0\n";
348             }
349 35         137 $connection->close;
350             }
351              
352              
353              
354             =head1 Helper package
355              
356             =head2 NAME
357              
358             Lock::Server::Client - client for locking server.
359              
360             =head2 DESCRIPTION
361              
362             Sends request to a Lock::Server to lock, unlock and check locks.
363              
364             =head2 METHODS
365              
366             =cut
367             package Lock::Server::Client;
368              
369 8     8   40 use strict;
  8         72  
  8         264  
370 8     8   40 use warnings;
  8         16  
  8         272  
371 8     8   40 no warnings 'uninitialized';
  8         8  
  8         320  
372              
373 8     8   32 use IO::Socket::INET;
  8         16  
  8         32  
374              
375             =head3 new( lockername, host, port )
376              
377             Creates a client object with the given name for the host and port.
378            
379             =cut
380             sub new {
381 9     9   308 my( $pkg, $lockerName, $host, $port ) = @_;
382 9 50       149 die "Must supply locker name" unless $lockerName;
383              
384 9   50     117 $host ||= '127.0.0.1';
385 9   50     108 $port ||= '8004';
386              
387 9   33     216 my $class = ref( $pkg ) || $pkg;
388 9         381 bless {
389             host => $host,
390             port => $port,
391             name => $lockerName,
392             }, $class;
393             } #new
394              
395             =head3 isLocked( key )
396              
397             Returns true if the key is locked by anyone.
398              
399             =cut
400             sub isLocked {
401 23     23   5000402 my( $self, $key ) = @_;
402 23         499 my $sock = new IO::Socket::INET( "$self->{host}:$self->{port}" );
403              
404 23         10857 $sock->print( "CHECK $key\n" );
405 23         8815 my $resp = <$sock>;
406 23         249 $sock->close;
407 23         1975 chomp $resp;
408 23         363 $resp;
409             }
410              
411             =head3 lockedByMe( key )
412              
413             Returns true if the key is locked by this client or
414             anyone with the name of this client. The name was given in the constructor.
415              
416             =cut
417             sub lockedByMe {
418 49     49   131 my( $self, $key ) = @_;
419 49         378 my $sock = new IO::Socket::INET( "$self->{host}:$self->{port}" );
420              
421 49         17833 $sock->print( "VERIFY $key $self->{name}\n" );
422 49         13249 my $resp = <$sock>;
423 49         225 $sock->close;
424 49         2105 chomp $resp;
425 49         402 $resp;
426             }
427              
428             =head3 lock( key )
429              
430             Attempt to get the lock for the given key. Returns true if the lock
431             was obtained.
432              
433             =cut
434             sub lock {
435 21     21   334 my( $self, $key ) = @_;
436 21         477 my $sock = new IO::Socket::INET( "$self->{host}:$self->{port}" );
437              
438 21         10515 $sock->print( "LOCK $key $self->{name}\n" );
439 21         7011246 my $resp = <$sock>;
440 21         207 $sock->close;
441 21         1105 chomp $resp;
442 21         253 $resp;
443             }
444              
445             =head3 unlock( key )
446              
447             Attempt to get unlock the given key. Returns true if the
448             key was locked to this client ( or someting with the same name ).
449              
450             =cut
451             sub unlock {
452 23     23   2000249 my( $self, $key ) = @_;
453 23         196 my $sock = new IO::Socket::INET( "$self->{host}:$self->{port}" );
454 23         10562 $sock->print( "UNLOCK $key $self->{name}\n" );
455 23         8463 my $resp = <$sock>;
456 23         98 $sock->close;
457 23         1057 chomp $resp;
458 23         200 $resp;
459             }
460              
461             1;
462              
463              
464             __END__