File Coverage

blib/lib/EMDIS/ECS/LockedHash.pm
Criterion Covered Total %
statement 163 187 87.1
branch 51 70 72.8
condition 10 18 55.5
subroutine 24 25 96.0
pod 0 11 0.0
total 248 311 79.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             #
3             # Copyright (C) 2002-2016 National Marrow Donor Program. All rights reserved.
4             #
5             # For a description of this module, please refer to the POD documentation
6             # embedded at the bottom of the file (e.g. perldoc EMDIS::ECS::LockedHash).
7              
8             package EMDIS::ECS::LockedHash;
9              
10 2     2   18388 use Data::Dumper;
  2         11956  
  2         96  
11 2     2   1016 use EMDIS::ECS qw($VERSION);
  2         6  
  2         242  
12 2     2   12 use Fcntl qw(:DEFAULT :flock);
  2         18  
  2         586  
13             #use File::lockf; # potential alternate locking method
14 2     2   1118 use SDBM_File;
  2         1004  
  2         72  
15 2     2   12 use strict;
  2         4  
  2         38  
16 2     2   8 use vars qw($VERSION);
  2         14  
  2         3096  
17              
18             # ----------------------------------------------------------------------
19             # Constructor. Requires name of the database file and name of lock file
20             # as parameters. Also accepts optional lock timeout parameter.
21             sub new {
22 7     7 0 2239 my $class = shift;
23 7         25 my $dbfile = shift;
24 7         30 my $lockfile = shift;
25 7         11 my $lock_timeout = shift;
26              
27             # validate aaarghs
28 7 100       26 if(!defined $dbfile) {
29 2         28 warn "EMDIS::ECS::LockedHash::new() failed: missing database file name.";
30 2         14 return undef;
31             }
32 5 100       16 if(!defined $lockfile) {
33 2         20 warn "EMDIS::ECS::LockedHash::new() failed: missing lock file name.";
34 2         12 return undef;
35             }
36 3 100       16 $lock_timeout = 10 unless defined $lock_timeout;
37              
38             # define this object
39 3         22 my $this = {};
40 3         11 bless $this, $class;
41 3         28 $this->{dbfile} = $dbfile;
42 3         19 $this->{lockfile} = $lockfile;
43 3         12 $this->{lock_timeout} = $lock_timeout;
44 3         20 $this->{ERROR} = '';
45 3         22 $this->{LOCK} = 0;
46 3         17 $this->{TIED} = '';
47              
48             # open lock file and retain file handle
49 3 50       281 if(!sysopen($this->{FH_LOCK}, $this->{lockfile}, O_RDWR|O_CREAT)) {
50 0         0 warn "EMDIS::ECS::LockedHash::new() failed: unable to access lock file " .
51             "'$this->{lockfile}': $!";
52 0         0 return undef;
53             }
54              
55             # tie/untie db file, to test whether it's accessible
56 3 50       41 if(!$this->_tie()) {
57 0         0 warn "EMDIS::ECS::LockedHash::new() failed: " . $this->ERROR;
58 0         0 return undef;
59             }
60 3         10 $this->_untie;
61              
62 3         11 return $this;
63             }
64              
65             # ----------------------------------------------------------------------
66             # set/get error description
67             sub ERROR {
68 40     40 0 359 my $this = shift;
69 40         96 my $err = shift;
70 40 100       86 if(defined $err) {
71 33         107 $this->{ERROR} = $err;
72             }
73 40         118 return $this->{ERROR};
74             }
75              
76             # ----------------------------------------------------------------------
77             # set/get locked status indicator
78             sub LOCK {
79 59     59 0 98 my $this = shift;
80 59         78 my $status = shift;
81 59 100       119 if(defined $status) {
82 18         26 $this->{LOCK} = $status;
83             }
84 59         167 return $this->{LOCK};
85             }
86              
87             # ----------------------------------------------------------------------
88             # set/get tied status indicator
89             sub TIED {
90 43     43 0 71 my $this = shift;
91 43         66 my $status = shift;
92 43 100       85 if(defined $status) {
93 21         47 $this->{TIED} = $status;
94             }
95 43         135 return $this->{TIED};
96             }
97              
98             # ----------------------------------------------------------------------
99             # Read one key-value from the database under a shared lock
100             sub read {
101 5     5 0 321 my $this = shift;
102 5         15 my $key = shift;
103 5         10 my $value = undef;
104              
105 5         13 $this->ERROR(''); # reset error status
106             # check lock status
107 5 100 66     10 if($this->LOCK != LOCK_SH and $this->LOCK != LOCK_EX) {
108 1         4 $this->ERROR(
109             "EMDIS::ECS::LockedHash::read() requires shared or exclusive lock.");
110 1         5 return undef;
111             }
112             # read value from hash
113 4         9 $value = undef;
114 4 100       60 $value = $this->{hash}->{$key} if exists $this->{hash}->{$key};
115 4 100 100     37 if(defined($value) and ($value =~ /^\$\w+\s*=\s*\{.*\}\s*\;\s*$/s)) {
116             # convert Dumper() string to hash ref
117 1         13 $value =~ s/^\$\w+/\$value/; # convert "$VAR1 = ..." to "$value = ..."
118 1         187 eval($value); # eval "$value = ..." string
119             }
120 4         15 return $value;
121             }
122              
123             # ----------------------------------------------------------------------
124             # Write one key-value to the database under an exclusive lock
125             sub write {
126 3     3 0 7 my $this = shift;
127 3         15 my $key = shift;
128 3         8 my $value = shift;
129              
130 3         9 $this->ERROR(''); # reset error status
131             # check lock status
132 3 100       8 if($this->LOCK != LOCK_EX) {
133 1         4 $this->ERROR("EMDIS::ECS::LockedHash::write() requires exclusive lock.");
134 1         11 return '';
135             }
136             # write value to hash
137 2 100       8 if(ref $value) {
138 1         21 local $Data::Dumper::Indent = 0;
139 1         21 $value = Dumper($value); # convert ref to Dumper() string
140             }
141 2         317 $this->{hash}->{$key} = $value;
142 2         15 return 1; # successful
143             }
144              
145             # ----------------------------------------------------------------------
146             # Delete a key-value under an exclusive lock
147             sub delete {
148 2     2 0 9 my $this = shift;
149 2         3 my $key = shift;
150 2         4 my $value = undef;
151              
152 2         6 $this->ERROR(''); # reset error status
153             # check lock status
154 2 100       6 if($this->LOCK != LOCK_EX) {
155 1         4 $this->ERROR("EMDIS::ECS::LockedHash::delete() requires exclusive lock.");
156 1         12 return '';
157             }
158             # delete value from hash
159 1         28 $value = delete $this->{hash}->{$key};
160 1         8 return 1; # successful
161             }
162              
163             # ----------------------------------------------------------------------
164             # Return a list of key values under a shared lock
165             sub keys {
166 3     3 0 7 my $this = shift;
167 3         7 my @ks = ();
168              
169 3         7 $this->ERROR(''); # reset error status
170             # check lock status
171 3 100 66     8 if($this->LOCK != LOCK_SH and $this->LOCK != LOCK_EX) {
172 1         3 $this->ERROR(
173             "EMDIS::ECS::LockedHash::keys() requires shared or exclusive lock.");
174 1         5 return '';
175             }
176             # get keys from hash
177 2         4 @ks = keys %{$this->{hash}};
  2         53  
178 2         10 return @ks;
179             }
180              
181             # ----------------------------------------------------------------------
182             # Obtain (advisory) lock and tie internal hash to db file.
183             sub lock {
184 6     6 0 13 my $this = shift;
185 6         8 my $lock_type = shift;
186 6         13 my $oldlock = $this->LOCK;
187 6 50       24 $lock_type = LOCK_EX unless $lock_type; # default = LOCK_EX
188 6         24 $this->ERROR(''); # reset error status
189 6 50       19 return 1 if $oldlock == $lock_type; # already locked
190 6         9 my $locked = 0;
191 6         9 my $attempt = 0;
192 6   66     47 while(!$locked and $attempt++ < 5) {
193 6 50       21 sleep 2 if $attempt > 1;
194 6         15 $this->ERROR(''); # reset error status
195 6         13 $locked = $this->_lock($lock_type);
196             }
197 6 50       14 if(!$locked) {
198 0         0 $this->ERROR("EMDIS::ECS::LockedHash::lock() failed: " . $this->ERROR);
199 0         0 return '';
200             }
201 6 50 33     53 if(!$this->TIED and !$this->_tie()) {
202 0         0 $this->ERROR("EMDIS::ECS::LockedHash::lock() failed: " . $this->ERROR);
203 0         0 return '';
204             }
205 6         30 return 1; # successful
206             }
207              
208             # ----------------------------------------------------------------------
209             # Release (advisory) lock and untie internal hash.
210             sub unlock {
211 6     6 0 217 my $this = shift;
212 6         18 $this->_untie();
213 6         12 $this->_unlock();
214             }
215              
216             # ----------------------------------------------------------------------
217             # Quickly delete all key-values under an exclusive lock
218             sub undef {
219 2     2 0 8 my $this = shift;
220              
221 2         5 $this->ERROR(''); # reset error status
222             # check lock status
223 2 100       12 if($this->LOCK != LOCK_EX) {
224 1         5 $this->ERROR("EMDIS::ECS::LockedHash::undef() requires exclusive lock.");
225 1         6 return '';
226             }
227             # delete everything from hash
228 1         3 undef %{$this->{hash}};
  1         30  
229 1         68 return 1; # successful
230             }
231              
232              
233             # ----------------------------------------------------------------------
234             # untie hash and close lock file when perl object passes out of scope
235             sub DESTROY {
236 1     1   30 my $this = shift;
237 1         17 $this->_untie();
238             close($this->{FH_LOCK})
239 1 50       269 if defined $this->{FH_LOCK};
240             }
241              
242             # ----------------------------------------------------------------------
243             # Select UNIX or Win32 version of _lock
244             sub _lock
245             {
246 10 50   10   92 $^O =~ /MSWin32/ ? _lock_win32(@_) : _lock_unix(@_);
247             }
248              
249             # ----------------------------------------------------------------------
250             # Internal subroutine: obtain (advisory) lock, using time limit to
251             # avoid indefinite blocking. Returns true if able to obtain lock within
252             # time limit; otherwise returns false.
253             sub _lock_unix {
254 10     10   22 my $this = shift;
255 10         13 my $lock_type = shift;
256 10 100       24 $lock_type = LOCK_EX unless defined $lock_type;
257 10         17 my $result = 1;
258              
259             # set up "local" SIG_ALRM handler
260             # (Note: not using "local $SIG{PIPE}" because it ignores die())
261 10         38 my $oldsigalrm = $SIG{ALRM};
262             $SIG{ALRM} = sub {
263 1     1   79 die "timeout - $this->{lock_timeout} second time limit exceeded\n";
264 10         233 };
265              
266             # attempt to obtain lock, with time limit
267 10         45 eval {
268 10         99 alarm($this->{lock_timeout}); # set alarm
269             die "flock() failed: $!\n"
270 10 100       2000382 unless flock($this->{FH_LOCK}, $lock_type);
271             # File::lockf -- potential alternate locking method:
272             # my $status = File::lockf::lock($this->{FH_LOCK}, 0);
273             # die "lockf failed: $status\n"
274             # if $status != 0;
275 9         50 alarm(0); # turn off alarm
276             };
277 10 100       35 if($@) {
278 1         34 alarm(0); # turn off alarm
279 1         30 $this->ERROR("EMDIS::ECS::LockedHash::_lock_unix() failed: $@");
280 1         112 $this->LOCK(0); # reset status indicator
281 1         3 $result = '';
282             }
283             # restore previous SIG_ALRM handler
284 10 50       41 if(defined $oldsigalrm) { $SIG{ALRM} = $oldsigalrm; }
  0         0  
285 10         174 else { delete $SIG{ALRM}; }
286 10 100       59 $this->LOCK($lock_type) # set status indicator
287             if $result;
288 10         37 return $result; # successful
289             }
290              
291             # ----------------------------------------------------------------------
292             # Internal subroutine: obtain (advisory) lock, using time limit to
293             # avoid indefinite blocking. Returns true if able to obtain lock within
294             # time limit; otherwise returns false.
295             sub _lock_win32 {
296 0     0   0 my $this = shift;
297 0         0 my $lock_type = shift;
298 0 0       0 $lock_type = LOCK_EX unless defined $lock_type;
299 0         0 my $result = 1;
300              
301             # attempt to obtain lock, with time limit
302             # (uses polling method to obtain lock -- somewhat more crude than
303             # the unix method, which uses blocking with SIGALRM to enforce timeout)
304 0         0 my $timeoutCount = 0;
305 0         0 my $locked;
306 0   0     0 while (!($locked = flock($this->{FH_LOCK}, $lock_type | LOCK_NB)) and
307             ($timeoutCount++ <= $this->{lock_timeout})) {
308 0         0 sleep 1;
309             }
310              
311 0 0       0 if(!$locked) {
312 0         0 $this->ERROR("EMDIS::ECS::LockedHash::_lock_win32() failed: $@");
313 0         0 $this->LOCK(0); # reset status indicator
314 0         0 $result = '';
315             }
316 0 0       0 $this->LOCK($lock_type) # set status indicator
317             if $result;
318 0         0 return $result; # successful
319             }
320              
321             # ----------------------------------------------------------------------
322             # Internal subroutine: tie hash to db file
323             sub _tie {
324 10     10   20 my $this = shift;
325 10         655 $this->{db_obj} = tie(%{$this->{hash}}, 'SDBM_File', $this->{dbfile},
326 10 50       19 O_CREAT|O_RDWR, (defined $EMDIS::ECS::FILEMODE ? $EMDIS::ECS::FILEMODE : 0664))
    50          
327             or $this->ERROR(
328             "EMDIS::ECS::LockedHash::_tie() failed ($this->{dbfile}): $!");
329 10 50       39 if($this->{db_obj}) {
330 10         33 $this->TIED(1); # set status indicator
331             } else {
332 0         0 $this->TIED(''); # reset status indicator
333             }
334 10         20 return $this->TIED;
335             }
336              
337             # ----------------------------------------------------------------------
338             # Internal subroutine: release (advisory) lock
339             sub _unlock {
340 8     8   22 my $this = shift;
341 8         75 flock($this->{FH_LOCK}, LOCK_UN);
342             # File::lockf -- potential alternate locking method:
343             # my $status = File::lockf::ulock($this->{FH_LOCK}, 0);
344 8         97 $this->LOCK(0); # reset status indicator
345             }
346              
347             # ----------------------------------------------------------------------
348             # Internal subroutine: untie hash from db file
349             # (mainly, insure that output is flushed to disk)
350             sub _untie {
351 11     11   20 my $this = shift;
352             untie $this->{hash}
353 11 100       30 if exists $this->{hash};
354 11         28 delete $this->{hash};
355 11         153 delete $this->{db_obj};
356 11         38 $this->TIED(''); # reset status indicator
357             }
358              
359             1;
360              
361             __DATA__