File Coverage

blib/lib/DBIx/Locker.pm
Criterion Covered Total %
statement 61 69 88.4
branch 14 20 70.0
condition 14 33 42.4
subroutine 16 19 84.2
pod 8 8 100.0
total 113 149 75.8


line stmt bran cond sub pod time code
1 1     1   124987 use strict;
  1         12  
  1         46  
2 1     1   6 use warnings;
  1         1  
  1         42  
3 1     1   19 use 5.008;
  1         3  
4              
5             package DBIx::Locker 1.102;
6             # ABSTRACT: locks for db resources that might not be totally insane
7              
8 1     1   6 use Carp ();
  1         2  
  1         15  
9 1     1   5 use DBI;
  1         2  
  1         31  
10 1     1   535 use Data::GUID ();
  1         20647  
  1         27  
11 1     1   474 use DBIx::Locker::Lock;
  1         3  
  1         32  
12 1     1   751 use JSON 2 ();
  1         10436  
  1         29  
13 1     1   454 use Sys::Hostname ();
  1         911  
  1         314  
14              
15             #pod =head1 DESCRIPTION
16             #pod
17             #pod ...and a B<warning>.
18             #pod
19             #pod DBIx::Locker was written to replace some lousy database resource locking code.
20             #pod The code would establish a MySQL lock with C<GET_LOCK> to lock arbitrary
21             #pod resources. Unfortunately, the code would also silently reconnect in case of
22             #pod database connection failure, silently losing the connection-based lock.
23             #pod DBIx::Locker locks by creating a persistent row in a "locks" table.
24             #pod
25             #pod Because DBIx::Locker locks are stored in a table, they won't go away. They
26             #pod have to be purged regularly. (A program for doing this, F<dbix_locker_purge>,
27             #pod is included.) The locked resource is just a string. All records in the lock
28             #pod (or semaphore) table are unique on the lock string.
29             #pod
30             #pod This is the I<entire> mechanism. This is quick and dirty and quite effective,
31             #pod but it's not highly efficient. If you need high speed locks with multiple
32             #pod levels of resolution, or anything other than a quick and brutal solution,
33             #pod I<keep looking>.
34             #pod
35             #pod =head1 STORAGE
36             #pod
37             #pod To use this module you'll need to create the lock table, which should have five
38             #pod columns:
39             #pod
40             #pod =over
41             #pod
42             #pod =item * C<id> Autoincrementing ID is recommended
43             #pod
44             #pod =item * C<lockstring> varchar(128) with a unique constraint
45             #pod
46             #pod =item * C<created> datetime
47             #pod
48             #pod =item * C<expires> datetime
49             #pod
50             #pod =item * C<locked_by> text
51             #pod
52             #pod =back
53             #pod
54             #pod See the C<sql> directory included in this dist for DDL for your database.
55             #pod
56             #pod =method new
57             #pod
58             #pod my $locker = DBIx::Locker->new(\%arg);
59             #pod
60             #pod This returns a new locker.
61             #pod
62             #pod Valid arguments are:
63             #pod
64             #pod dbh - a database handle to use for locking
65             #pod dbi_args - an arrayref of args to pass to DBI->connect to reconnect to db
66             #pod table - the table for locks
67             #pod
68             #pod =cut
69              
70             sub new {
71 1     1 1 33722 my ($class, $arg) = @_;
72              
73             my $guts = {
74             dbh => $arg->{dbh},
75             dbi_args => ($arg->{dbi_args} || $class->default_dbi_args),
76 1   33     15 table => ($arg->{table} || $class->default_table),
      33        
77             };
78              
79             Carp::confess("cannot use a dbh without RaiseError")
80 1 50 33     6 if $guts->{dbh} and not $guts->{dbh}{RaiseError};
81              
82 1   50     6 my $dbi_attr = $guts->{dbi_args}[3] ||= {};
83              
84             Carp::confess("RaiseError cannot be disabled")
85 1 50 33     12 if exists $dbi_attr->{RaiseError} and not $dbi_attr->{RaiseError};
86              
87 1         5 $dbi_attr->{RaiseError} = 1;
88              
89 1         5 return bless $guts => $class;
90             }
91              
92             #pod =method default_dbi_args
93             #pod
94             #pod =method default_table
95             #pod
96             #pod These methods may be defined in subclasses to provide defaults to be used when
97             #pod constructing a new locker.
98             #pod
99             #pod =cut
100              
101             sub default_dbi_args {
102 0     0 1 0 Carp::confess('dbi_args not given and no default defined')
103             }
104              
105             sub default_table {
106 0     0 1 0 Carp::Confess('table not given and no default defined')
107             }
108              
109             #pod =method dbh
110             #pod
111             #pod This method returns the locker's dbh.
112             #pod
113             #pod =cut
114              
115             sub dbh {
116 18     18 1 61 my ($self) = @_;
117 18 100 66     94 return $self->{dbh} if $self->{dbh} and eval { $self->{dbh}->ping };
  17         138  
118              
119             die("couldn't connect to database: $DBI::errstr")
120 1 50       3 unless my $dbh = DBI->connect(@{ $self->{dbi_args} });
  1         10  
121              
122 1         581 return $self->{dbh} = $dbh;
123             }
124              
125             #pod =method table
126             #pod
127             #pod This method returns the name of the table in the database in which locks are
128             #pod stored.
129             #pod
130             #pod =cut
131              
132             sub table {
133             return $_[0]->{table}
134 17     17 1 486 }
135              
136             #pod =method lock
137             #pod
138             #pod my $lock = $locker->lock($lockstring, \%arg);
139             #pod
140             #pod This method attempts to return a new DBIx::Locker::Lock.
141             #pod
142             #pod =cut
143              
144             my $JSON;
145 1     1   435 BEGIN { $JSON = JSON->new->canonical(1)->space_after(1); }
146              
147             sub lock {
148 8     8 1 4462 my ($self, $lockstring, $arg) = @_;
149 8   50     70 $arg ||= {};
150              
151 8 100       506 Carp::confess("calling ->lock in void context is not permitted")
152             unless defined wantarray;
153              
154 6 50 33     44 Carp::confess("no lockstring provided")
155             unless defined $lockstring and length $lockstring;
156              
157 6   50     48 my $expires = $arg->{expires} ||= 3600;
158              
159 6 50 33     44 Carp::confess("expires must be a positive integer")
160             unless $expires > 0 and $expires == int $expires;
161              
162 6         16 $expires = time + $expires;
163              
164 6         38 my $locked_by = {
165             host => Sys::Hostname::hostname(),
166             guid => Data::GUID->new->as_string,
167             pid => $$,
168             };
169              
170 6         1443 my $table = $self->table;
171 6         24 my $dbh = $self->dbh;
172              
173 6         447 local $dbh->{RaiseError} = 0;
174 6         129 local $dbh->{PrintError} = 0;
175              
176 6         107 my $rows = $dbh->do(
177             "INSERT INTO $table (lockstring, created, expires, locked_by)
178             VALUES (?, ?, ?, ?)",
179             undef,
180             $lockstring,
181             $self->_time_to_string,
182             $self->_time_to_string([ localtime($expires) ]),
183             $JSON->encode($locked_by),
184             );
185              
186 6 50 33     62151 die(
    100 66        
187             "could not lock resource <$lockstring>" . (
188             $dbh->err && $dbh->errstr
189             ? (': ' . $dbh->errstr)
190             : ''
191             )
192             ) unless $rows and $rows == 1;
193              
194 5         48 my $lock = DBIx::Locker::Lock->new({
195             locker => $self,
196             lock_id => $self->last_insert_id,
197             expires => $expires,
198             locked_by => $locked_by,
199             lockstring => $lockstring,
200             });
201              
202 5         128 return $lock;
203             }
204              
205             sub _time_to_string {
206 14     14   57 my ($self, $time) = @_;
207              
208 14 100       329 $time = [ localtime ] unless $time;
209 14         459 return sprintf '%04u-%02u-%02u %02u:%02u:%02u',
210             $time->[5] + 1900, $time->[4]+1, $time->[3],
211             $time->[2], $time->[1], $time->[0];
212             }
213              
214             #pod =method purge_expired_locks
215             #pod
216             #pod This method deletes expired semaphores.
217             #pod
218             #pod =cut
219              
220             sub purge_expired_locks {
221 0     0 1 0 my ($self) = @_;
222              
223 0         0 my $dbh = $self->dbh;
224 0         0 local $dbh->{RaiseError} = 0;
225 0         0 local $dbh->{PrintError} = 0;
226              
227 0         0 my $table = $self->table;
228              
229 0         0 my $rows = $dbh->do(
230             "DELETE FROM $table WHERE expires < ?",
231             undef,
232             $self->_time_to_string,
233             );
234             }
235              
236             #pod =method last_insert_id
237             #pod
238             #pod This method exists so that subclasses can do something else to support their
239             #pod DBD for getting the id of the created lock. For example, with DBD::ODBC and
240             #pod SQL Server it should be:
241             #pod
242             #pod sub last_insert_id { ($_[0]->dbh->selectrow_array('SELECT @@IDENTITY'))[0] }
243             #pod
244             #pod =cut
245              
246             sub last_insert_id {
247 5     5 1 39 $_[0]->dbh->last_insert_id(undef, undef, $_[0]->table, 'id')
248             }
249              
250             1;
251              
252             __END__
253              
254             =pod
255              
256             =encoding UTF-8
257              
258             =head1 NAME
259              
260             DBIx::Locker - locks for db resources that might not be totally insane
261              
262             =head1 VERSION
263              
264             version 1.102
265              
266             =head1 DESCRIPTION
267              
268             ...and a B<warning>.
269              
270             DBIx::Locker was written to replace some lousy database resource locking code.
271             The code would establish a MySQL lock with C<GET_LOCK> to lock arbitrary
272             resources. Unfortunately, the code would also silently reconnect in case of
273             database connection failure, silently losing the connection-based lock.
274             DBIx::Locker locks by creating a persistent row in a "locks" table.
275              
276             Because DBIx::Locker locks are stored in a table, they won't go away. They
277             have to be purged regularly. (A program for doing this, F<dbix_locker_purge>,
278             is included.) The locked resource is just a string. All records in the lock
279             (or semaphore) table are unique on the lock string.
280              
281             This is the I<entire> mechanism. This is quick and dirty and quite effective,
282             but it's not highly efficient. If you need high speed locks with multiple
283             levels of resolution, or anything other than a quick and brutal solution,
284             I<keep looking>.
285              
286             =head1 PERL VERSION
287              
288             This library should run on perls released even a long time ago. It should work
289             on any version of perl released in the last five years.
290              
291             Although it may work on older versions of perl, no guarantee is made that the
292             minimum required version will not be increased. The version may be increased
293             for any reason, and there is no promise that patches will be accepted to lower
294             the minimum required perl.
295              
296             =head1 METHODS
297              
298             =head2 new
299              
300             my $locker = DBIx::Locker->new(\%arg);
301              
302             This returns a new locker.
303              
304             Valid arguments are:
305              
306             dbh - a database handle to use for locking
307             dbi_args - an arrayref of args to pass to DBI->connect to reconnect to db
308             table - the table for locks
309              
310             =head2 default_dbi_args
311              
312             =head2 default_table
313              
314             These methods may be defined in subclasses to provide defaults to be used when
315             constructing a new locker.
316              
317             =head2 dbh
318              
319             This method returns the locker's dbh.
320              
321             =head2 table
322              
323             This method returns the name of the table in the database in which locks are
324             stored.
325              
326             =head2 lock
327              
328             my $lock = $locker->lock($lockstring, \%arg);
329              
330             This method attempts to return a new DBIx::Locker::Lock.
331              
332             =head2 purge_expired_locks
333              
334             This method deletes expired semaphores.
335              
336             =head2 last_insert_id
337              
338             This method exists so that subclasses can do something else to support their
339             DBD for getting the id of the created lock. For example, with DBD::ODBC and
340             SQL Server it should be:
341              
342             sub last_insert_id { ($_[0]->dbh->selectrow_array('SELECT @@IDENTITY'))[0] }
343              
344             =head1 STORAGE
345              
346             To use this module you'll need to create the lock table, which should have five
347             columns:
348              
349             =over
350              
351             =item * C<id> Autoincrementing ID is recommended
352              
353             =item * C<lockstring> varchar(128) with a unique constraint
354              
355             =item * C<created> datetime
356              
357             =item * C<expires> datetime
358              
359             =item * C<locked_by> text
360              
361             =back
362              
363             See the C<sql> directory included in this dist for DDL for your database.
364              
365             =head1 AUTHOR
366              
367             Ricardo SIGNES <rjbs@semiotic.systems>
368              
369             =head1 CONTRIBUTORS
370              
371             =for stopwords Arthur Axel 'fREW' Schmidt Chris Nehren Hans Dieter Pearcey Matthew Horsfall Rob N ★ Sergiy Borodych
372              
373             =over 4
374              
375             =item *
376              
377             Arthur Axel 'fREW' Schmidt <frioux@gmail.com>
378              
379             =item *
380              
381             Chris Nehren <apeiron@cpan.org>
382              
383             =item *
384              
385             Hans Dieter Pearcey <hdp@cpan.org>
386              
387             =item *
388              
389             Matthew Horsfall <wolfsage@gmail.com>
390              
391             =item *
392              
393             Rob N ★ <robn@robn.io>
394              
395             =item *
396              
397             Sergiy Borodych <sergiy.borodych@gmail.com>
398              
399             =back
400              
401             =head1 COPYRIGHT AND LICENSE
402              
403             This software is copyright (c) 2021 by Ricardo SIGNES.
404              
405             This is free software; you can redistribute it and/or modify it under
406             the same terms as the Perl 5 programming language system itself.
407              
408             =cut