File Coverage

blib/lib/CHI/Driver/HandlerSocket.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1              
2             package CHI::Driver::HandlerSocket;
3              
4 1     1   24653 use strict;
  1         2  
  1         34  
5 1     1   4 use warnings;
  1         3  
  1         23  
6              
7 1     1   397 use Moose;
  0            
  0            
8             use Moose::Util::TypeConstraints;
9              
10             use Net::HandlerSocket;
11             use Carp 'croak';
12              
13             extends 'CHI::Driver';
14              
15             use 5.006;
16             our $VERSION = '0.991';
17              
18             =head1 NAME
19              
20             CHI::Driver::HandlerSocket - Use DBI for cache storage, but access it using the Net::HandlerSocket API for MySQL
21              
22             =head1 SYNOPSIS
23              
24             use CHI;
25              
26             # Supply a DBI handle
27              
28             my $cache = CHI->new( driver => 'HandlerSocket', dbh => DBI->connect(...) );
29              
30             B: This module inherits tests from L but does not pass all of L's tests. It will not automatically install with L or L because of this. Also, it won't pass all tests without database login information and L skips the interactive prompts. You need to install this manually for now, I'm afraid.
31              
32             =head1 DESCRIPTION
33              
34             This driver uses a MySQL database table to store the cache.
35             It accesses it by way of the Net::HandlerSocket API and associated MySQL plug-in:
36              
37             L
38              
39             L
40              
41             Why cache things in a database? Isn't the database what people are trying to
42             avoid with caches?
43              
44             This is often true, but a simple primary key lookup is extremely fast in MySQL and HandlerSocket absolutely screams,
45             avoiding most of the locking that normally happens and completing as many updates/queries as it can at once under the same lock.
46             Avoiding parsing SQL is also a huge performance boost.
47              
48             =head1 ATTRIBUTES
49              
50             =over
51              
52             =item host
53              
54             =item read_port
55              
56             =item write_port
57              
58             Host and port the MySQL server with the SocketHandler plugin is running on. The connection is TCP.
59             Two connections are used, one for reading, one for writing, following the design of L.
60             The write port locks the table even for reads, reportedly.
61             Default is C, C<9998>, and C<9999>.
62              
63             =item namespace
64              
65             The namespace you pass in will be appended to the C and used as a
66             table name. That means that if you don't specify a namespace or table_prefix
67             the cache will be stored in a table called C.
68              
69             =item table_prefix
70              
71             This is the prefix that is used when building a table name. If you want to
72             just use the namespace as a literal table name, set this to undef. Defaults to
73             C.
74              
75             =item dbh
76              
77             The DBI handle used to communicate with the db.
78              
79             You may pass this handle in one of three forms:
80              
81             =over
82              
83             =item *
84              
85             a regular DBI handle
86              
87             =item *
88              
89             a L object
90              
91             XXXX doesn't work
92              
93             =item *
94              
95             a code reference that will be called each time and is expected to return a DBI
96             handle, e.g.
97              
98             sub { My::Rose::DB->new->dbh }
99              
100             XXXX doesn't work
101              
102             =back
103              
104             The last two options are valuable if your CHI object is going to live for
105             enough time that a single DBI handle might time out, etc.
106              
107             =head1 BUGS
108              
109             =item 0.9
110              
111             C still referenced L and would fail if it you didn't have it installed. Fixed.
112              
113             Tests will fail with a message about no tests run unless you run the install manuaully and give it valid DB login info.
114             Todo: insert a dummy C in there.
115              
116             Should have been specifying CHARSET=ASCII in the create statement to avoid L, where utf-8 characters count triple or quadruple or whatever.
117             Fixed, dubiously.
118              
119             Huh, turns out that I was developing against L 0.36. Running tests with 0.42 shows me 31 failing tests.
120              
121              
122             =head1 Authors
123              
124             L by Scott Walters (scott@slowass.net) for Plain Black Corp, L.
125             L is based on L.
126              
127             L Authors: Original version by Justin DeVuyst and Perrin Harkins. Currently maintained by
128             Jonathan Swartz.
129              
130             =head1 COPYRIGHT & LICENSE
131              
132             Copyright (c) Plain Black Corp 2011
133             Copyright (c) Scott Walters (scrottie) 2011
134             Copyright (c) Justin DeVuyst
135              
136             This program is free software; you can redistribute it and/or modify it under
137             the same terms as Perl itself.
138              
139             =cut
140              
141             has 'dbh' => ( is => 'rw', ); # isa => 'DBI::db',
142              
143             sub get_dbh {
144             my $self = shift;
145             my $dbh = $self->dbh or die "no dbh!";
146             return $dbh->dbh if eval { $dbh->ISA('DBIx::Connector'); };
147             return $dbh->() if eval { ref $dbh eq 'CODE' }; # tell me again what's wrong with UNIVERSAL::ISA.
148             # warn "dbh isn't a DBI::db; it's a " . ref $dbh unless eval { $dbh->ISA('DBI::db'); }; # "dbh isn't a DBI::db; it's a DBI::db"
149             return $dbh;
150             }
151              
152             has 'table_prefix' => ( is => 'rw', isa => 'Str', default => 'chi_', );
153              
154             has 'host' => ( is => 'ro', default => 'localhost', );
155              
156             has 'read_port' => ( is => 'ro', default => 9998, );
157              
158             has 'write_port' => ( is => 'ro', default => 9999, );
159              
160             has 'read_index' => ( is => 'ro', default => 1, );
161              
162             has 'write_index' => ( is => 'ro', default => 1, );
163              
164             has 'read_hs' => ( is => 'rw', isa => 'Net::HandlerSocket', );
165              
166             has 'write_hs' => ( is => 'rw', isa => 'Net::HandlerSocket', );
167              
168             has 'mysql_thread_stack' => ( is => 'rw', isa => 'Num', ); # HandlerSocket uses the stack to buffer writes; remember how large the stack is
169              
170             __PACKAGE__->meta->make_immutable;
171              
172             sub BUILD {
173             my ( $self, $args ) = @_;
174            
175             my $dbh = $self->get_dbh;
176              
177             my $table = $self->_table; # don't quote it
178            
179             my $database_name = do {
180             my $sth = $dbh->prepare( qq{ SELECT database() AS dbname } ) or die $dbh->errstr;
181             $sth->execute or die $sth->errstr;
182             my @row = $sth->fetchrow_array or die "couldn't figure out the name of the database";
183             $sth->finish;
184             $row[0];
185             };
186              
187             # HandlerSocket uses the stack to buffer writes; remember how large the stack is
188              
189             $self->mysql_thread_stack(do {
190             my $sth = $dbh->prepare( qq{ SHOW global variables WHERE Variable_name = 'thread_stack' } ) or die $dbh->errstr;
191             $sth->execute or die $sth->errstr;
192             my @row = $sth->fetchrow_array || do {
193             # every time you use a magic number in code, a devil gets his horns; seriously though, this is this
194             # particular MySQL releases default thread stack size
195             warn "couldn't figure out the thread_stack size; oh well, guessing";
196             (131072);
197             };
198             $sth->finish;
199             # 5824 is the amount of data my MySQL version/install said had already been used of the stack before the
200             # unaccomodatable request came in; 2 is a fudge factor
201             # if this is less than 0 for some reason, then all writes will go to DBI, which is probably necessary in that case
202             $row[0] - 5824 * 2;
203             });
204              
205             # warn "host: @{[ $self->host ]} port: @{[ $self->read_port ]} database_name: $database_name table: $table read_index: @{[ $self->read_index ]} write_index: @{[ $self->write_index ]} thread_stack: @{[ $self->mysql_thread_stack ]}";
206              
207             # CREATE TABLE IF NOT EXISTS $table ( `key` VARCHAR( 600 ), `value` BLOB, PRIMARY KEY ( `key` ) ) CHARSET=ASCII # fails 30 tests right now
208             # CREATE TABLE IF NOT EXISTS $table ( `key` VARCHAR( 300 ), `value` TEXT, PRIMARY KEY ( `key` ) ) CHARSET=utf8 # fails 220 tests
209              
210             $dbh->do( qq{
211             CREATE TABLE IF NOT EXISTS $table ( `key` VARCHAR( 600 ), `value` BLOB, PRIMARY KEY ( `key` ) ) CHARSET=ASCII
212             } ) or croak $dbh->errstr;
213              
214             # from https://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL/blob/master/docs-en/perl-client.en.txt:
215              
216             # The first argument for open_index is an integer value which is
217             # used to identify an open table, which is only valid within the
218             # same Net::HandlerSocket object. The 4th argument is the name of
219             # index to open. If 'PRIMARY' is specified, the primary index is
220             # open. The 5th argument is a comma-separated list of column names.
221              
222             my $read_hs = Net::HandlerSocket->new({ host => $self->host, port => $self->read_port, }) or die;
223             $read_hs->open_index($self->read_index, $database_name, $table, 'PRIMARY', 'value') and die $read_hs->get_error;
224             $self->read_hs($read_hs);
225              
226             my $write_hs = Net::HandlerSocket->new({ host => $self->host, port => $self->write_port, });
227             $write_hs->open_index($self->write_index, $database_name, $table, 'PRIMARY', 'key,value') and die $write_hs->get_error;
228             $self->write_hs($write_hs);
229              
230             return;
231             }
232            
233             sub _table {
234             my $self = shift;
235             return $self->table_prefix() . $self->namespace();
236             }
237              
238             sub fetch {
239             my ( $self, $key, ) = @_;
240              
241             my $index = $self->read_index;
242             my $hs = $self->read_hs;
243              
244             # from https://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL/blob/master/docs-en/perl-client.en.txt:
245              
246             # The first argument must be an integer which has specified as the
247             # first argument for open_index on the same Net::HandlerSocket
248             # object. The second argument specifies the search operation. The
249             # current version of handlersocket supports '=', '>=', '<=', '>',
250             # and '<'. The 3rd argument specifies the key to find, which must
251             # an arrayref whose length is equal to or smaller than the number
252             # of key columns of the index. The 4th and the 5th arguments
253             # specify the maximum number of records to be retrieved, and the
254             # number of records skipped before retrieving records. The columns
255             # to be retrieved are specified by the 5th argument for the
256             # corresponding open_index call.
257              
258             my $res = $hs->execute_single($index, '=', [ $key ], 1, 0);
259             my $status = shift @$res; $status and die $hs->get_error;
260             return $res->[0];
261              
262             }
263              
264            
265             sub store_dbi {
266             my ( $self, $key, $data, ) = @_;
267              
268             my $dbh = $self->get_dbh;
269             my $table = $dbh->quote_identifier( $self->_table );
270              
271             # XXX - should actually just prepare this as once or as needed, or maybe that's what prepare_cached does...? wait, MySQL doesn't cache parsed SQL anyway like Postgres does so maybe there's no point.
272              
273             my $sth = $dbh->prepare_cached( qq{
274             INSERT INTO $table
275             ( `key`, `value` )
276             VALUES ( ?, ? )
277             ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)
278             } );
279             $sth->execute( $key, $data );
280             $sth->finish;
281              
282             return;
283             }
284              
285             sub store {
286             my ( $self, $key, $data, ) = @_;
287              
288             my $index = $self->write_index;
289             my $hs = $self->write_hs;
290              
291             # if HandlerSocket doesn't have enough stack to buffer the write, kick back to DBI
292              
293             if( length $data > $self->mysql_thread_stack ) {
294             warn "debug: punted back to store_dbi";
295             return $self->store_dbi( $key, $data );
296             }
297              
298             # from https://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL/blob/master/docs-en/perl-client.en.txt:
299              
300             # The 6th argument for execute_single specifies the modification
301             # operation. The current version supports 'U' and 'D'. For the 'U'
302             # operation, the 7th argument specifies the new value for the row.
303              
304             my $res;
305             my $status;
306              
307             my $rarr = $hs->execute_multi( [
308             [ $index, '=', [ $key ], 1, 0, 'D' ], # gaaah
309             [ $index, '+', [ $key, $data ] ],
310             ] );
311             for my $res (@$rarr) {
312             die $hs->get_error() if $res->[0] != 0;
313             # results in shift(@$res);
314             }
315              
316             return;
317             }
318              
319             sub remove {
320             my ( $self, $key, ) = @_;
321              
322             my $index = $self->write_index;
323             my $dbh = $self->get_dbh;
324             my $hs = $self->write_hs;
325              
326             my $res = $hs->execute_single($index, '=', [ $key ], 1, 0, 'D');
327             my $status = shift @$res; $status and die $hs->get_error;
328              
329             return;
330             }
331              
332             sub clear {
333             my $self = shift;
334              
335             my $dbh = $self->get_dbh;
336             my $table = $dbh->quote_identifier( $self->_table );
337            
338             my $sth = $dbh->prepare_cached( qq{ DELETE FROM $table } ) or croak $dbh->errstr;
339             $sth->execute() or croak $sth->errstr;
340             $sth->finish();
341            
342             return;
343             }
344              
345             sub get_keys {
346             my ( $self, ) = @_;
347              
348             my $dbh = $self->get_dbh;
349             my $table = $dbh->quote_identifier( $self->_table );
350            
351             my $sth = $dbh->prepare_cached( "SELECT DISTINCT `key` FROM $table" ) or croak $dbh->errstr;
352             $sth->execute() or croak $sth->errstr;
353             my $results = $sth->fetchall_arrayref( [0] );
354             $_ = $_->[0] for @{$results};
355              
356             return @{$results};
357             }
358              
359             sub get_namespaces { croak 'not supported' }
360            
361             1;
362              
363             __END__