File Coverage

blib/lib/Prancer/Session/Store/Database/Driver.pm
Criterion Covered Total %
statement 139 180 77.2
branch 15 42 35.7
condition 12 30 40.0
subroutine 25 32 78.1
pod 4 5 80.0
total 195 289 67.4


line stmt bran cond sub pod time code
1             package Prancer::Session::Store::Database::Driver;
2              
3 2     2   2658 use strict;
  2         4  
  2         61  
4 2     2   7 use warnings FATAL => 'all';
  2         2  
  2         51  
5              
6 2     2   6 use version;
  2         2  
  2         8  
7             our $VERSION = '1.01';
8              
9 2     2   936 use Plack::Session::Store;
  2         1566  
  2         77  
10 2     2   909 use parent qw(Plack::Session::Store);
  2         519  
  2         10  
11              
12 2     2   3167 use DBI;
  2         28675  
  2         162  
13 2     2   752 use Storable qw(nfreeze thaw);
  2         2997  
  2         137  
14 2     2   494 use MIME::Base64 qw(encode_base64 decode_base64);
  2         501  
  2         95  
15 2     2   565 use Try::Tiny;
  2         1330  
  2         96  
16 2     2   9 use Carp;
  2         11  
  2         2824  
17              
18             # even though this *should* work automatically, it was not
19             our @CARP_NOT = qw(Prancer Try::Tiny);
20              
21             sub new {
22 3     3 1 5 my ($class, $config) = @_;
23              
24             # this is the only required field
25 3 50       9 unless ($config->{'database'}) {
26 0         0 croak "could not initialize session handler: no database name configured";
27             }
28              
29             # initialize the serializer that will be used
30 3 50       5 my $self = bless($class->SUPER::new(%{$config || {}}), $class);
  3         24  
31 3     1   50 $self->{'_serializer'} = sub { encode_base64(nfreeze(shift)) };
  1         3  
32 3     1   10 $self->{'_deserializer'} = sub { thaw(decode_base64(shift)) };
  1         7  
33              
34 3         6 $self->{'_database'} = $config->{'database'};
35 3         5 $self->{'_username'} = $config->{'username'};
36 3         3 $self->{'_password'} = $config->{'password'};
37 3         5 $self->{'_hostname'} = $config->{'hostname'};
38 3         6 $self->{'_port'} = $config->{'port'};
39 3         5 $self->{'_charset'} = $config->{'charset'};
40 3   50     15 $self->{'_check_threshold'} = $config->{'connection_check_threshold'} // 30;
41 3   50     14 $self->{'_dsn_extra'} = $config->{'dsn_extra'} || {};
42 3   50     19 $self->{'_on_connect'} = $config->{'on_connect'} || [];
43 3   50     12 $self->{'_table'} = $config->{'table'} || "sessions";
44 3   50     8 $self->{'_timeout'} = $config->{'expiration_timeout'} // 1800;
45 3   50     6 $self->{'_autopurge'} = $config->{'autopurge'} // 1;
46 3   50     8 $self->{'_autopurge_probability'} = $config->{'autopurge_probability'} || 0.1;
47 3         6 $self->{'_application'} = $config->{'application'};
48              
49             # store a pool of database connection handles
50 3         3 $self->{'_handles'} = {};
51              
52 3         10 return $self;
53             }
54              
55             sub handle {
56 9     9 0 49 my $self = shift;
57              
58             # to be fork safe and thread safe, use a combination of the PID and TID
59             # (if running with use threads) to make sure no two processes/threads share
60             # a handle. implementation based on DBIx::Connector by David E. Wheeler
61 9         76 my $pid_tid = $$;
62 9 50       32 $pid_tid .= "_" . threads->tid() if $INC{'threads.pm'};
63              
64             # see if we have a matching handle
65 9   100     34 my $handle = $self->{'_handles'}->{$pid_tid} || undef;
66              
67 9 100       20 if ($handle->{'dbh'}) {
68 6 50 33     33 if ($handle->{'dbh'}{'Active'} && $self->{'_check_threshold'} &&
      33        
69             (time - $handle->{'last_connection_check'} < $self->{'_check_threshold'})) {
70              
71             # the handle has been checked recently so just return it
72 6         87 return $handle->{'dbh'};
73             } else {
74 0 0       0 if ($self->_check_connection($handle->{'dbh'})) {
75 0         0 $handle->{'last_connection_check'} = time;
76 0         0 return $handle->{'dbh'};
77             } else {
78             # try to disconnect but don't care if it fails
79 0 0       0 if ($handle->{'dbh'}) {
80 0     0   0 try { $handle->{'dbh'}->disconnect() } catch {};
  0         0  
  0         0  
81             }
82              
83             # try to connect again and save the new handle
84 0         0 $handle->{'dbh'} = $self->_get_connection();
85 0         0 return $handle->{'dbh'};
86             }
87             }
88             } else {
89 3         22 $handle->{'dbh'} = $self->_get_connection();
90 2 50       5 if ($handle->{'dbh'}) {
91 2         9 $handle->{'last_connection_check'} = time;
92 2         5 $self->{'_handles'}->{$pid_tid} = $handle;
93 2         6 return $handle->{'dbh'};
94             }
95             }
96              
97 0         0 return;
98             }
99              
100             sub _get_connection {
101 3     3   4 my $self = shift;
102 3         4 my $dbh = undef;
103              
104             try {
105 3   50 3   73 $dbh = DBI->connect(@{$self->{'_dsn'}}) || die "${\$DBI::errstr}\n";
106              
107             # run any on_connect sql
108 2         899 $dbh->do($_) for (@{$self->{'_on_connect'}});
  2         9  
109             } catch {
110 1 50   1   445 my $error = (defined($_) ? $_ : "unknown");
111 1         111 croak "could not initialize database connection: ${error}";
112 3         25 };
113              
114 2         29 return $dbh;
115             }
116              
117             # Check the connection is alive
118             sub _check_connection {
119 0     0   0 my $self = shift;
120 0         0 my $dbh = shift;
121 0 0       0 return 0 unless $dbh;
122              
123 0 0 0     0 if ($dbh->{'Active'} && (my $result = $dbh->ping())) {
124 0 0       0 if (int($result)) {
125             # DB driver itself claims all is OK, trust it
126 0         0 return 1;
127             } else {
128             # it was "0 but true", meaning the DBD doesn't implement ping and
129             # instead we got the default DBI ping implementation. implement
130             # our own basic check, by performing a real simple query.
131             return try {
132 0     0   0 return $dbh->do("SELECT 1");
133             } catch {
134 0     0   0 return 0;
135 0         0 };
136             }
137             }
138              
139 0         0 return 0;
140             }
141              
142             sub fetch {
143 2     2 1 2896 my ($self, $session_id) = @_;
144 2         5 my $dbh = $self->handle();
145 2         4 my $result = undef;
146              
147             try {
148 2     2   43 my $now = time();
149 2         3 my $table = $self->{'_table'};
150 2         3 my $application = $self->{'_application'};
151              
152 2         12 my $sth = $dbh->prepare(qq|
153             SELECT data
154             FROM ${table}
155             WHERE id = ?
156             AND application = ?
157             AND timeout >= ?
158             |);
159 2         240 $sth->execute($session_id, $application, $now);
160 2         127 my ($data) = $sth->fetchrow();
161 2         62 $sth->finish();
162              
163             # deserialize the data if there is any
164 2 100       36 $result = (defined($data) ? $self->{'_deserializer'}->($data) : undef);
165              
166             # maybe we'll purge old sessions sometimes
167 2         32 $self->_purge();
168              
169 2         11 $dbh->commit();
170             } catch {
171 0     0   0 try { $dbh->rollback(); } catch {};
  0         0  
  0         0  
172              
173 0 0       0 my $error = (defined($_) ? $_ : "unknown");
174 0         0 carp "error fetching from session: ${error}";
175 2         19 };
176              
177 2         406 return $result;
178             }
179              
180             sub store {
181 1     1 1 2460 my ($self, $session_id, $data) = @_;
182 1         3 my $dbh = $self->handle();
183              
184             try {
185 1     1   23 my $now = time();
186 1         2 my $table = $self->{'_table'};
187 1         3 my $application = $self->{'_application'};
188 1         2 my $timeout = ($now + $self->{'_timeout'});
189 1         4 my $serialized = $self->{'_serializer'}->($data);
190              
191 1         39 my $insert_sth = $dbh->prepare(qq|
192             INSERT INTO ${table} (id, application, timeout, data)
193             SELECT ?, ?, ?, ?
194             WHERE NOT EXISTS (
195             SELECT 1
196             FROM ${table}
197             WHERE id = ?
198             AND application = ?
199             AND timeout >= ?
200             )
201             |);
202 1         81 $insert_sth->execute($session_id, $application, $timeout, $serialized, $session_id, $application, $now);
203 1         73 $insert_sth->finish();
204              
205 1         14 my $update_sth = $dbh->prepare(qq|
206             UPDATE ${table}
207             SET timeout = ?, data = ?
208             WHERE id = ?
209             AND application = ?
210             AND timeout >= ?
211             |);
212 1         68 $update_sth->execute($timeout, $serialized, $session_id, $application, $now);
213 1         60 $update_sth->finish();
214              
215             # maybe we'll purge old sessions sometimes
216 1         12 $self->_purge();
217              
218 1         3 $dbh->commit();
219             } catch {
220 0     0   0 try { $dbh->rollback(); } catch {};
  0         0  
  0         0  
221              
222 0 0       0 my $error = (defined($_) ? $_ : "unknown");
223 0         0 carp "error fetching from session: ${error}";
224 1         11 };
225              
226 1         174 return;
227             }
228              
229             sub remove {
230 2     2 1 5592 my ($self, $session_id) = @_;
231 2         6 my $dbh = $self->handle();
232              
233             try {
234 2     2   48 my $table = $self->{'_table'};
235 2         3 my $application = $self->{'_application'};
236              
237 2         11 my $sth = $dbh->prepare(qq|
238             DELETE
239             FROM ${table}
240             WHERE id = ?
241             AND application = ?
242             |);
243 2         168 $sth->execute($session_id, $application);
244 2         132 $sth->finish();
245              
246             # maybe we'll purge old sessions sometimes
247 2         23 $self->_purge();
248              
249 2         6 $dbh->commit();
250             } catch {
251 0     0   0 try { $dbh->rollback(); } catch {};
  0         0  
  0         0  
252              
253 0 0       0 my $error = (defined($_) ? $_ : "unknown");
254 0         0 carp "error fetching from session: ${error}";
255 2         21 };
256              
257 2         334 return;
258             }
259              
260             sub _purge {
261 5     5   5 my $self = shift;
262              
263             # 10% of the time we will also purge old sessions
264 5 100       13 if ($self->{'_autopurge'}) {
265 1         94 my $chance = rand();
266 1 50       9 if ($chance <= $self->{'_autopurge_probability'}) {
267 1         2 my $now = time();
268 1         3 my $dbh = $self->handle();
269 1         3 my $table = $self->{'_table'};
270 1         2 my $application = $self->{'_application'};
271              
272 1         5 my $delete_sth = $dbh->prepare(qq|
273             DELETE
274             FROM ${table}
275             WHERE application = ?
276             AND timeout < ?
277             |);
278 1         100 $delete_sth->execute($application, $now);
279 1         63 $delete_sth->finish();
280             }
281             }
282              
283 5         22 return;
284             }
285              
286             # stolen from Hash::Merge::Simple
287             ## no critic (ProhibitUnusedPrivateSubroutines)
288             sub _merge {
289 3     3   5 my ($self, $left, @right) = @_;
290              
291 3 50       10 return $left unless @right;
292 3 50       7 return $self->_merge($left, $self->_merge(@right)) if @right > 1;
293              
294 3         3 my ($right) = @right;
295 3         4 my %merged = %{$left};
  3         10  
296              
297 3         4 for my $key (keys %{$right}) {
  3         9  
298 0         0 my ($hr, $hl) = map { ref($_->{$key}) eq "HASH" } $right, $left;
  0         0  
299              
300 0 0 0     0 if ($hr and $hl) {
301 0         0 $merged{$key} = $self->_merge($left->{$key}, $right->{$key});
302             } else {
303 0         0 $merged{$key} = $right->{$key};
304             }
305             }
306              
307 3         9 return \%merged;
308             }
309              
310             1;