File Coverage

blib/lib/XAS/Lib/Lockmgr/Filesystem.pm
Criterion Covered Total %
statement 15 165 9.0
branch 0 64 0.0
condition 0 19 0.0
subroutine 5 28 17.8
pod 5 6 83.3
total 25 282 8.8


line stmt bran cond sub pod time code
1             package XAS::Lib::Lockmgr::Filesystem;
2              
3             our $VERSION = '0.03';
4              
5 1     1   1152 use DateTime;
  1         2  
  1         22  
6 1     1   3 use DateTime::Span;
  1         1  
  1         17  
7 1     1   3 use Try::Tiny::Retry ':all';
  1         1  
  1         106  
8 1     1   4 use XAS::Constants 'TRUE FALSE HASHREF';
  1         1  
  1         10  
9              
10             use XAS::Class
11 1         12 version => $VERSION,
12             base => 'XAS::Base',
13             mixin => 'XAS::Lib::Mixins::Process XAS::Lib::Mixins::Handlers',
14             utils => 'dotid',
15             import => 'class',
16             filesystem => 'Dir File',
17             accessors => 'deadlock breaklock timeout attempts _lockfile _lockdir',
18             vars => {
19             PARAMS => {
20             -key => 1,
21             -args => { optional => 1, type => HASHREF, default => {} },
22             }
23             }
24 1     1   144 ;
  1         1  
25              
26             #use Data::Dumper;
27              
28             # ----------------------------------------------------------------------
29             # Overrides
30             # ----------------------------------------------------------------------
31              
32             class('Badger::Filesystem')->methods(
33             directory_exists => sub {
34 0     0     my $self = shift;
35 0           my $dir = shift;
36 0   0       my $stats = $self->stat_path($dir) || return;
37 0 0         return -d $dir ? $stats : 0; # don't use the cached stat
38             },
39             file_exists => sub {
40 0     0     my $self = shift;
41 0           my $file = shift;
42 0   0       my $stats = $self->stat_path($file) || return;
43 0 0         return -f $file ? $stats : 0; # don't use the cached stat
44             }
45             );
46              
47             # ----------------------------------------------------------------------
48             # Public Methods
49             # ----------------------------------------------------------------------
50              
51             sub lock {
52 0     0 1   my $self = shift;
53              
54 0           my $stat = FALSE;
55 0           my $lock = $self->_lockfile();
56 0           my $dir = $self->_lockdir();
57              
58 0           $self->log->debug(sprintf('lock: %s', $dir));
59              
60             retry {
61              
62 0 0 0 0     if (($dir->exists) && ($lock->exists)) {
    0          
63              
64 0           $stat = TRUE;
65              
66             } elsif ($dir->exists) {
67              
68 0 0         if ($stat = $self->_dead_lock()) {
69              
70 0           $self->_make_lock();
71 0           $stat = TRUE;
72              
73             }
74              
75             } else {
76              
77 0           $self->_make_lock();
78 0           $stat = TRUE;
79              
80             }
81              
82             } retry_if {
83              
84 0     0     my $ex = $_;
85 0           my $exceptions = $self->exceptions;
86              
87 0 0 0       if (ref($ex) && $ex->isa('Badger::Exception')) {
88              
89 0           foreach my $exception (@$exceptions) {
90              
91 0 0         if ($ex->match_type($exception)) {
92              
93 0           die $ex;
94              
95             }
96              
97             }
98              
99             }
100              
101 0           $self->exception_handler($ex);
102              
103 0           1; # always retry
104              
105             } delay {
106              
107 0     0     my $attempts = shift;
108              
109 0 0         return if ($attempts > $self->attempts);
110 0           sleep int(rand($self->timeout));
111              
112             } catch {
113              
114 0     0     my $ex = $_;
115 0 0         my $msg = (ref($ex) eq 'Badger::Exception') ? $ex->info : $ex;
116              
117 0           $self->throw_msg(
118             dotid($self->class) . '.lock',
119             'lock_error',
120             $dir, $msg
121             );
122              
123 0           };
124              
125 0           return $stat;
126              
127             }
128              
129             sub unlock {
130 0     0 1   my $self = shift;
131              
132 0           my $stat = FALSE;
133 0           my $lock = $self->_lockfile();
134 0           my $dir = $self->_lockdir();
135              
136 0           $self->log->debug(sprintf('unlock: %s', $dir));
137              
138             try {
139              
140 0 0   0     $lock->delete if ($lock->exists);
141 0 0         $dir->delete if ($dir->exists);
142 0           $stat = TRUE;
143              
144             } catch {
145              
146 0     0     my $ex = $_;
147 0 0         my $msg = (ref($ex) eq 'Badger::Exception') ? $ex->info : $ex;
148              
149 0           $self->throw_msg(
150             dotid($self->class) . '.unlock',
151             'lock_error',
152             $dir, $msg
153             );
154              
155 0           };
156              
157 0           return $stat;
158              
159             }
160              
161             sub try_lock {
162 0     0 1   my $self = shift;
163              
164 0           my $stat = TRUE;
165 0           my $lock = $self->_lockfile();
166 0           my $dir = $self->_lockdir();
167              
168 0           $self->log->debug(sprintf('try_lock: %s', $dir));
169              
170             try {
171              
172 0 0   0     if ($dir->exists) {
173              
174 0           $self->log->warn_msg('lock_dir_error', $dir);
175 0           $stat = $self->_dead_lock();
176              
177             }
178              
179             } catch {
180              
181 0     0     my $ex = $_;
182 0 0         my $msg = (ref($ex) eq 'Badger::Exception') ? $ex->info : $ex;
183              
184 0           $self->throw_msg(
185             dotid($self->class) . '.unlock',
186             'lock_error',
187             $dir, $msg
188             );
189              
190 0           };
191            
192 0           return $stat
193              
194             }
195              
196             sub exceptions {
197 0     0 1   my $self = shift;
198              
199 0           my $class = dotid($self->class);
200 0           my @exceptions = [
201             'filesystem',
202             $class,
203             ];
204              
205 0           return \@exceptions;
206              
207             }
208              
209             sub destroy {
210 0     0 0   my $self = shift;
211              
212 0           my $lock = $self->_lockfile();
213 0           my $dir = $self->_lockdir();
214              
215             try { # removes a potential warning during global destruction
216              
217 0 0   0     $lock->delete if ($lock->exists);
218 0 0         $dir->delete if ($dir->exists);
219              
220 0           };
221              
222 0           return 1;
223              
224             }
225              
226             sub DESTROY {
227 0     0     my $self = shift;
228              
229 0           $self->destroy();
230              
231             }
232              
233             # ----------------------------------------------------------------------
234             # Private Methods
235             # ----------------------------------------------------------------------
236              
237             sub _make_lock {
238 0     0     my $self = shift;
239            
240 0           my $lock = $self->_lockfile();
241 0           my $dir = $self->_lockdir();
242              
243 0           $self->log->debug(sprintf('_make_lock: %s', $dir));
244              
245             # temporarily change the umask to create the
246             # directory and files with correct file permissions.
247             # this is a noop on windows.
248              
249 0           my $omode = umask(0033);
250 0           $dir->create;
251 0           $lock->create;
252 0           umask($omode);
253              
254             }
255              
256             sub _break_lock {
257 0     0     my $self = shift;
258              
259 0           my $lock = $self->_lockfile();
260 0           my $dir = $self->_lockdir();
261              
262 0           $self->log->debug(sprintf('_break_lock: %s', $dir));
263              
264 0 0         if ($dir->exists) {
265              
266 0           foreach my $file (@{$dir->files}) {
  0            
267              
268 0 0         $file->delete if ($file->exists);
269              
270             }
271              
272 0 0         $dir->delete if ($dir->exists);
273              
274             }
275              
276              
277             }
278              
279             sub _whose_lock {
280 0     0     my $self = shift;
281              
282 0           my $pid = undef;
283 0           my $host = undef;
284 0           my $time = undef;
285 0           my $lock = $self->_lockfile();
286 0           my $dir = $self->_lockdir();
287              
288 0           $self->log->debug(sprintf('_whose_lock: %s', $dir));
289              
290 0 0         if ($dir->exists) {
291              
292 0 0         if (my @files = $dir->files) {
293              
294             # should only be one file in the directory,
295             # but that file may disappear before this
296             # check.
297              
298 0 0         if ($files[0]->exists) {
299              
300 0           $host = $files[0]->basename;
301 0           $pid = $files[0]->extension;
302 0           $time = DateTime->from_epoch(
303             epoch => ($files[0]->stat)[9],
304             time_zone => 'local'
305             );
306              
307             }
308              
309             }
310              
311             }
312              
313 0           return $host, $pid, $time;
314              
315             }
316              
317             sub _dead_lock {
318 0     0     my $self = shift;
319              
320 0           my $stat = FALSE;
321 0           my $lock = $self->_lockfile();
322 0           my $dir = $self->_lockdir();
323 0           my $now = DateTime->now(time_zone => 'local');
324 0           my ($host, $pid, $time) = $self->_whose_lock();
325              
326 0           $self->log->debug(sprintf('_dead_lock: %s', $dir));
327              
328             my $break_lock = sub {
329              
330             # break the deadlock, irregardless of who owns the lock
331              
332 0     0     $self->_break_lock();
333 0           $self->log->warn_msg('lock_broken', $dir);
334 0           $stat = TRUE;
335              
336 0           };
337              
338 0 0 0       if (defined($host) && defined($pid) && defined($time)) {
      0        
339              
340 0           $time->set_time_zone('local');
341              
342 0           my $span = DateTime::Span->from_datetimes(
343             start => $now->clone->subtract(seconds => $self->deadlock),
344             end => $now->clone,
345             );
346              
347 0           $self->log->debug(sprintf('_dead_lock: host - %s', $host));
348 0           $self->log->debug(sprintf('_dead_lock: pid - %s', $pid));
349 0           $self->log->debug(sprintf('_dead_lock: start - %s', $span->start));
350 0           $self->log->debug(sprintf('_dead_lock: lock - %s', $time));
351 0           $self->log->debug(sprintf('_dead_lock: end - %s', $span->end));
352              
353 0 0         if ($span->contains($time)) {
354              
355 0           $self->log->debug('_dead_lock: within time span');
356              
357 0 0         if ($host eq $self->env->host) {
358              
359 0 0         if ($pid == $$) {
360              
361 0           $self->log->debug('_dead_lock: our lock');
362 0           $stat = TRUE;
363              
364             } else {
365              
366 0           my $status = $self->proc_status($pid, '_dead_lock');
367              
368 0 0 0       unless (($status == 3) || ($status == 2)) {
369              
370 0           $break_lock->();
371              
372             }
373              
374             }
375              
376             } else {
377              
378 0 0         if ($self->breaklock) {
379              
380 0           $break_lock->();
381              
382             } else {
383              
384 0           $self->throw_msg(
385             dotid($self->class) . '.deadlock.remote',
386             'lock_remote',
387             $dir
388             );
389              
390             }
391              
392             }
393              
394             } else {
395              
396 0           $break_lock->();
397              
398             }
399              
400             } else {
401              
402             # unable to retrieve lock information, break the deadlock,
403             # irregardless of who owns the lock
404              
405 0           $break_lock->();
406              
407             }
408              
409 0           return $stat;
410              
411             }
412              
413             sub init {
414 0     0 1   my $class = shift;
415              
416 0           my $self = $class->SUPER::init(@_);
417              
418 0           my $lockfile = $self->env->host . ".$$";
419              
420 0           $self->{'_lockfile'} = File($self->key, $lockfile);
421 0           $self->{'_lockdir'} = Dir($self->_lockfile->volume, $self->_lockfile->directory);
422              
423             $self->{'deadlock'} = defined($self->args->{'deadlock'})
424 0 0         ? $self->args->{'deadlock'}
425             : 1800;
426            
427             $self->{'breaklock'} = defined($self->args->{'breaklock'})
428 0 0         ? $self->args->{'breaklock'}
429             : 0;
430              
431             $self->{'timeout'} = defined($self->args->{'timeout'})
432 0 0         ? $self->args->{'timeout'}
433             : 30;
434              
435             $self->{'attempts'} = defined($self->args->{'attempts'})
436 0 0         ? $self->args->{'attempts'}
437             : 30;
438              
439 0           return $self;
440              
441             }
442              
443             1;
444              
445             __END__
446              
447             =head1 NAME
448              
449             XAS::Lib::Lockmgr::Filsystem - Use the file system for locking.
450              
451             =head1 SYNOPSIS
452              
453             use XAS::Lib::Lockmgr;
454              
455             my $key = '/var/lock/wpm/alerts';
456             my $lockmgr = XAS::Lib::Lockmgr->new();
457              
458             $lockmgr->add(
459             -key => $key,
460             -driver => 'Filesystem',
461             -args => {
462             timeout => 10,
463             attempts => 10,
464             breaklock => 1,
465             deadlock => 900,
466             }
467             );
468              
469             if ($lockmgr->try_lock($key)) {
470              
471             $lockmgr->lock($key);
472              
473             ...
474              
475             $lockmgr->unlock($key);
476              
477             }
478              
479             =head1 DESCRIPTION
480              
481             This class uses the manipulation of directories within the file system as a
482             mutex. This leverages the atomicity of creating directories and allows for
483             discretionary locking of resources.
484              
485             =head1 CONFIGURATION
486              
487             This module uses the following fields in -args.
488              
489             =over 4
490              
491             =item B<attempts>
492              
493             The number of attempts to aquire the lock. The default is 30.
494              
495             =item B<timeout>
496              
497             The number of seconds to wait between lock attempts. The default is 30.
498              
499             =item B<deadlock>
500              
501             The number of seconds before a deadlock is declated, defaults to 1800,
502              
503             =item B<breaklock>
504              
505             Break the lock irregardless of how owns the lock, defaults to FALSE.
506              
507             =back
508              
509             =head1 METHODS
510              
511             =head2 lock
512              
513             Attempt to aquire a lock. This is done by creating a directory and writing
514             a status file into that directory. Returns TRUE for success, FALSE otherwise.
515              
516             =head2 unlock
517              
518             Remove the lock. This is done by removing the status file and then the
519             directory. Returns TRUE for success, FALSE otherwise.
520              
521             =head2 try_lock
522              
523             Check to see if a lock could be aquired. Returns FALSE if the directory exists,
524             TRUE otherwise.
525              
526             =head2 exceptions
527              
528             Returns the exceptions that you may not want to continue lock attemtps if
529             triggered.
530              
531             =head1 SEE ALSO
532              
533             =over 4
534              
535             =item L<XAS::Lib::Lockmgr|XAS::Lib::Lockmgr>
536              
537             =item L<XAS|XAS>
538              
539             =back
540              
541             =head1 AUTHOR
542              
543             Kevin L. Esteb, E<lt>kevin@kesteb.usE<gt>
544              
545             =head1 COPYRIGHT AND LICENSE
546              
547             Copyright (c) 2012-2016 Kevin L. Esteb
548              
549             This is free software; you can redistribute it and/or modify it under
550             the terms of the Artistic License 2.0. For details, see the full text
551             of the license at http://www.perlfoundation.org/artistic_license_2_0.
552              
553             =cut