File Coverage

blib/lib/Database/ManagedHandle.pm
Criterion Covered Total %
statement 83 92 90.2
branch 18 30 60.0
condition 1 3 33.3
subroutine 14 14 100.0
pod 1 2 50.0
total 117 141 82.9


line stmt bran cond sub pod time code
1             package Database::ManagedHandle;
2             ## no critic (ControlStructures::ProhibitPostfixControls)
3              
4 3     3   1334262 use strict;
  3         5  
  3         108  
5 3     3   13 use warnings;
  3         5  
  3         303  
6              
7             # ABSTRACT: Manage database handles safely for long running processes
8              
9             our $VERSION = '0.003'; # VERSION: generated by DZP::OurPkgVersion
10              
11 3     3   26 use Carp qw( croak );
  3         6  
  3         201  
12 3     3   631 use Module::Load qw( load );
  3         1647  
  3         29  
13 3     3   838 use English qw( -no_match_vars );
  3         1024  
  3         23  
14 3     3   3180 use Storable qw( dclone );
  3         13559  
  3         310  
15              
16 3     3   61 use Moo;
  3         11  
  3         26  
17             with 'MooX::Singleton';
18              
19 3     3   1595 use DBI;
  3         9  
  3         128  
20 3     3   608 use Log::Any ();
  3         10969  
  3         3435  
21              
22             has _log => (
23             is => 'ro',
24             default => sub {
25             Log::Any->get_logger;
26             },
27             );
28              
29             has _handles => ( is => 'rw', );
30             has _config => ( is => 'rw', );
31              
32             sub BUILD {
33 3     3 0 4786 my ($self) = @_;
34 3         12 my $cfg_module = 'Database::ManagedHandleConfig';
35 3 50       20 if ( $ENV{DATABASE_MANAGED_HANDLE_CONFIG} ) {
36 3         26 ($cfg_module) = $ENV{DATABASE_MANAGED_HANDLE_CONFIG} =~ m/^([[:alnum:]:_]{1,})$/msx;
37             }
38 3         11 local $EVAL_ERROR = undef; # protect existing $@ ($EVAL_ERROR)
39 3         7 my $r = eval {
40 3         24 load $cfg_module;
41 3         335 1;
42             };
43 3 50 33     64 if ( $EVAL_ERROR || !$r ) {
44 0         0 croak 'Cannot load module ', $cfg_module, '. Error: ', $EVAL_ERROR;
45             }
46              
47             # Make a clone of the configuration for safety.
48             # Without dangling references, created object will be collected.
49 3         33 my $config = dclone( $cfg_module->new()->config() );
50              
51 3         6946 $self->_check_config_is_valid($config);
52 3         8 $self->_handles( { map { $_ => undef } keys %{ $config->{'databases'} } } );
  5         42  
  3         12  
53 3         16 $self->_config($config);
54 3         25 return;
55             }
56              
57             sub _check_config_is_valid {
58 3     3   11 my ( $self, $config ) = @_;
59 3         38 $self->_log->debugf( '_check_config_is_valid(%s)', $config );
60 3 50       29 croak 'Config missing item \'default\'' unless exists $config->{'default'};
61 3 50       15 croak 'Config missing item \'databases\'' unless exists $config->{'databases'};
62 3         9 my $default = $config->{'default'};
63 3 50       17 croak 'Config missing default database' unless exists $config->{'databases'}->{$default};
64 3         7 foreach my $db_name ( keys %{ $config->{'databases'} } ) {
  3         18  
65 5         12 my $db = $config->{'databases'}->{$db_name};
66 5         13 foreach (qw( dsn username password attr )) {
67 20 50       59 croak "Config missing item '$_' in database '$db_name'" unless exists $db->{$_};
68             }
69             }
70 3         10 return 1;
71             }
72              
73             sub dbh {
74 14     14 1 10269 my ( $self, $name ) = @_;
75 14         85 $self->_log->debugf( 'dbh(%s)', $name );
76              
77 14         80 my $handles = $self->_handles;
78 14         34 my $config = $self->_config;
79 14 100       42 $name = $config->{'default'} unless ($name);
80              
81             croak 'No database with name ' . $name . ' in config'
82 14 100       315 unless exists $config->{'databases'}->{$name};
83 13         26 my $dbh = $handles->{$name};
84              
85 13 100       40 if ( !$self->_verify_connection_working($dbh) ) {
86 5         26 $self->_log->infof( 'Connection not working for dbh %s, db %s. Connecting...', $dbh, $name );
87 5         26 $dbh = $self->_create_dbh( $config->{'databases'}->{$name} );
88 5         17 $handles->{$name} = $dbh;
89             }
90              
91 13         65 return $dbh;
92             }
93              
94             sub _create_dbh {
95 5     5   13 my ( $self, $cfg ) = @_;
96 5         22 $self->_log->debugf( 'Database::ManagedHandle::_create_dbh(%s)', $cfg );
97             $self->_log->debugf( 'Database::ManagedHandle::_create_dbh): Create and connect database handle for dsn \'%s\'',
98 5         30 $cfg->{'dsn'} );
99 5         52 my $dbh = DBI->connect( $cfg->{'dsn'}, $cfg->{'username'}, $cfg->{'password'}, $cfg->{'attr'}, );
100 5 50       7741 if ( !defined $dbh ) {
101 0         0 my $err = 'Could not open database. Error: %s';
102 0         0 croak sprintf $err, $DBI::errstr; ## no critic (Variables::ProhibitPackageVars)
103             }
104 5         19 return $dbh;
105             }
106              
107             # This is partly borrowed from Dancer::Plugin::Database::Core and modified
108             # Check the connection is alive
109             sub _verify_connection_working {
110 13     13   31 my ( $self, $dbh ) = @_;
111 13         49 $self->_log->debugf( 'Verify connection working for handle \'%s\'', $dbh );
112              
113             # If dbh is undef, obviously there is no connection.
114 13 100       65 return if ( !defined $dbh );
115              
116 8 50       134 if ( $dbh->{Active} ) {
117 8         19 local $EVAL_ERROR = undef; # protect existing $@ ($EVAL_ERROR)
118 8         35 my $result = eval { $dbh->ping };
  8         40  
119 8 50       306 return if $EVAL_ERROR;
120 8 50       36 if ( int $result ) {
121              
122             # DB driver itself claims all is OK, trust it:
123 8         33 return 1;
124             }
125             else {
126             # It was "0 but true", meaning the default DBI ping implementation.
127             # Implement our own basic check, by performing a real simple query.
128 0           local $EVAL_ERROR = undef; # protect existing $@ ($EVAL_ERROR)
129 0           my $r = eval {
130              
131             # Returns the number of rows affected or undef on error.
132             # A return value of -1 means the number of rows is not known,
133             # not applicable, or not available.
134             # https://metacpan.org/pod/DBI#do
135             # ($rows == 0) ? "0E0" : $rows; # always return true if no error
136             # Will return "0E0"
137 0           $dbh->do('SELECT 1');
138              
139             # The return value from eval will be the value of the last statement!
140             };
141 0 0         return if $EVAL_ERROR;
142 0           return $r;
143             }
144             }
145             else {
146 0           return;
147             }
148             }
149              
150             1;
151              
152             __END__
153              
154             =pod
155              
156             =encoding UTF-8
157              
158             =head1 NAME
159              
160             Database::ManagedHandle - Manage database handles safely for long running processes
161              
162             =head1 VERSION
163              
164             version 0.003
165              
166             =head1 SYNOPSIS
167              
168             # Create a class file from which the configuration can be read:
169             package Database::ManagedHandleConfig;
170             use Moo;
171             has config => (
172             is => 'ro',
173             default => sub {
174             return {
175             default => q{db1},
176             databases => {
177             db1 => {
178             dsn => "dbi:SQLite:uri=file:/tmp/first_db.sq3?mode=rwc",
179             username => undef,
180             password => undef,
181             attr => {},
182             },
183             db2 => {
184             dsn => 'dbi:Pg:dbname=db;host=go;port=5432;application_name=MyApp',
185             username => 'user',
186             password => 'pass',
187             attr => { ReadOnly => 0, AutoCommit => 0, PrintError => 0 },
188             },
189             },
190             };
191             },
192             );
193             1;
194              
195             # In your program code:
196             use Database::ManagedHandle;
197             my $mh1 = Database::ManagedHandle->instance;
198             my $dbh1 = $mh1->dbh();
199             my $ary_ref = $dbh1->selectall_arrayref( 'SELECT current_timestamp()' );
200              
201             # Another example:
202             Database::ManagedHandle->instance()->dbh('db2')->do( 'INSERT INTO t VALUES(1,2,3)' );
203              
204             =head1 DESCRIPTION
205              
206             Database::ManagedHandle is built for those long running web services or scripts
207             which can lose a database connection due to network issues, database being updated,
208             database itself closing unused connections after an idle period
209             or any other reason.
210              
211             Database::ManagedHandle uses L<Moo> and specifically the L<MooX::Singleton> role to become
212             a L<Singleton|https://en.wikipedia.org/wiki/Singleton_pattern>.
213             This ensures that there is always only one instance of the class
214             in the entire running program. This in turn means that the program needs
215             only one database handle and it is accessible from any part of the code.
216              
217             Database::ManagedHandle opens and reopens database handles when required.
218             It can house several handles. If there is more than one, then one handle
219             needs to be defined as the default.
220              
221             When the program first requests a database handle,
222             either a named handle or the default,
223             Database::ManagedHandle opens the database connection and
224             passes the opened handle to the program.
225             After using the handle, the program does not need to worry about it.
226             It can safely let the variable fall out of scope.
227              
228             During subsequent calls for the handle, Database::ManagedHandle
229             first ensures that the connection is still alive.
230             If not, it will establish the handle again.
231              
232             Do not keep a database handle around.
233             Only use the same handle for one operation, then purposefully undef it or let it
234             drop out of scope.
235             When you need it again, get it from Database::ManagedHandle.
236              
237             =head2 Configuration
238              
239             Database::ManagedHandle reads its configuration from
240             a class. By default, the class name is C<Database::ManagedHandleConfig>.
241             Alternatively, set environment variable B<DATABASE_MANAGED_HANDLE_CONFIG>, e.g.
242             C<DATABASE_MANAGED_HANDLE_CONFIG=MyHandles>.
243              
244             The configuration class must have two methods: C<new()> and C<config()>.
245             Neither takes any arguments. C<config()> returns a hash which has
246             the required information. See L</SYNOPSIS> for an example of how
247             to do this as a L<Moo> class.
248              
249             =head2 Logging
250              
251             Database::ManagedHandle uses the excellent L<Log::Any> to produce logging messages.
252              
253             The easiest way to get the logging messages printed is to add the following line
254             in the preamble of your program:
255              
256             use Log::Any::Adapter ('Stdout', log_level => 'debug' );
257              
258             Alternative, you can do this on the command line:
259              
260             perl '-MLog::Any::Adapter(Stdout, log_level=>trace)'
261              
262             =for Pod::Coverage BUILD
263              
264             =for stopwords dbh
265              
266             =head1 METHODS
267              
268             =head2 dbh
269              
270             Get the default database handle.
271              
272             my $dbh = Database::ManagedHandle->instance()->dbh();
273              
274             Get a database handle by its name.
275              
276             my $mh = Database::ManagedHandle->instance;
277             my $dbh = $mh->dbh( 'db_example' );
278              
279             =head1 THANKS
280              
281             Big thanks for L<Dancer::Plugin::Database> for being an inspiration
282             and example on how to verify database connection is still working.
283              
284             =head1 AUTHOR
285              
286             Mikko Koivunalho <mikkoi@cpan.org>
287              
288             =head1 COPYRIGHT AND LICENSE
289              
290             This software is copyright (c) 2023 by Mikko Koivunalho.
291              
292             This is free software; you can redistribute it and/or modify it under
293             the same terms as the Perl 5 programming language system itself.
294              
295             =cut