File Coverage

blib/lib/CHI/Driver/MongoDB.pm
Criterion Covered Total %
statement 21 90 23.3
branch 0 16 0.0
condition 0 9 0.0
subroutine 7 21 33.3
pod 0 8 0.0
total 28 144 19.4


line stmt bran cond sub pod time code
1             package CHI::Driver::MongoDB;
2             # vim:syntax=perl:tabstop=4:number:noexpandtab:
3             $CHI::Driver::MongoDB::VERSION = '0.0001';
4             # ABSTRACT: MongoDB driver for CHI
5              
6 1     1   44868 use Moo;
  1         8069  
  1         4  
7 1     1   1371 use MongoDB;
  1         782695  
  1         25  
8 1     1   651 use URI::Escape::XS;
  1         1830  
  1         65  
9 1     1   6 use Try::Tiny;
  1         1  
  1         39  
10 1     1   376 use Time::Moment;
  1         1014  
  1         23  
11              
12 1     1   5 use strict;
  1         1  
  1         14  
13 1     1   3 use warnings;
  1         1  
  1         724  
14              
15             extends 'CHI::Driver';
16              
17             has 'mongodb' => (
18             is => 'ro',
19             lazy => 1,
20             init_arg => undef,
21             builder => '_build_mongodb',
22             );
23              
24             has 'mongodb_options' => (
25             is => 'rw',
26             default => sub { {} },
27             );
28              
29             has 'connection_uri' => (
30             is => 'ro',
31             lazy => 1,
32             default => sub { 'mongodb://127.0.0.1:27017' },
33             );
34              
35             has 'db_name' => (
36             is => 'ro',
37             lazy => 1,
38             default => sub {'_CHI_'},
39             );
40              
41             has '_coll' => (
42             is => 'rw',
43             lazy => 1,
44             predicate => 1,
45             init_arg => undef,
46             builder => '_build_coll',
47             clearer => 1,
48             );
49              
50              
51             sub BUILD {
52 0     0 0   my ( $self, $params ) = @_;
53 0           foreach my $param (qw/ mongodb_options connection_uri db_name /) {
54 0 0         if ( exists $params->{$param} ) {
55 0           delete $params->{$param};
56             }
57             }
58 0           my $codec = MongoDB::BSON->new( dt_type => 'Time::Moment' );
59              
60             my %options = (
61             bson_codec => $codec,
62 0           %{ $self->mongodb_options() },
63 0           %{ $self->non_common_constructor_params($params) },
  0            
64             );
65 0           $self->mongodb_options( \%options );
66             }
67              
68              
69             sub _build_mongodb {
70 0     0     my $self = shift;
71              
72 0           my %opts = %{ $self->mongodb_options() };
  0            
73 0           my $uri = $self->connection_uri;
74              
75 0           return MongoDB->connect( $uri, \%opts );
76             }
77              
78              
79             sub _build_coll {
80 0     0     my $self = shift;
81              
82 0           my $ns = sprintf( "%s.%s", $self->db_name, encodeURIComponent( $self->namespace ) );
83              
84 0           my $coll = $self->mongodb->get_namespace($ns);
85              
86 0 0         if ( $self->expires_on_backend ) {
87 0           $coll->indexes->create_one( [ expireAt => 1 ], { expireAfterSeconds => 0 } );
88             }
89              
90 0           return $coll;
91             }
92              
93              
94             sub fetch {
95 0     0 0   my ( $self, $key ) = @_;
96              
97 0           $key = encodeURIComponent($key);
98 0           my $doc = $self->_coll->find_id( $key, { payload => 1 } );
99              
100 0 0 0       return "" . $doc->{'payload'} if defined($doc) and ref($doc) eq 'HASH';
101 0           return undef;
102             }
103              
104              
105             sub fetch_multi_hashref {
106 0     0 0   my ( $self, $keys ) = @_;
107              
108 0           my @esc = map { encodeURIComponent($_) } @{$keys};
  0            
  0            
109 0           my $qresult = $self->_coll->find(
110             { _id => { '$in' => \@esc } },
111             {
112             batchSize => 100,
113             projection => {
114             _id => 1,
115             payload => 1
116             }
117             }
118             )->result;
119              
120 0           my %ret = ();
121 0           while ( my @batch = $qresult->batch ) {
122 0           map { my $k = decodeURIComponent( $_->{'_id'} ); $ret{$k} = "" . $_->{'payload'} } @batch;
  0            
  0            
123             }
124              
125 0           return \%ret;
126             }
127              
128              
129             sub store {
130 0     0 0   my ( $self, $key, $data, $expires_in ) = @_;
131              
132 0           $key = encodeURIComponent($key);
133 0           my $doc = {
134             _id => $key,
135             payload => MongoDB::BSON::Binary->new( data => $data ),
136             };
137 0 0         if ( defined $expires_in ) {
138 0           $doc->{'expireAt'} = Time::Moment->from_epoch( time() + $expires_in );
139             }
140 0           my $result = $self->_coll->update_one( { _id => $key }, { '$set' => $doc }, { upsert => 1 } );
141              
142 0 0 0       warn "update_one() operation failed: unexpected result"
143             unless ref($result) eq 'MongoDB::UnacknowledgedResult'
144             or ref($result) eq 'MongoDB::UpdateResult';
145             try {
146 0 0   0     $result->assert if $result->acknowledged;
147             }
148             catch {
149 0     0     warn "update_one() operation failed: deletion not asserted";
150 0           };
151              
152 0           return undef;
153             } ## end sub store
154              
155             # TODO
156             #sub store_multi {
157             # my ( $self, $key_data, $options ) = @_;
158             #}
159              
160              
161             sub remove {
162 0     0 0   my ( $self, $key ) = @_;
163              
164 0           $key = encodeURIComponent($key);
165 0           my $result = $self->_coll->delete_one( { _id => $key } );
166              
167 0 0 0       warn "delete_one() operation failed: unexpected result"
168             unless ref($result) eq 'MongoDB::UnacknowledgedResult'
169             or ref($result) eq 'MongoDB::DeleteResult';
170             try {
171 0 0   0     $result->assert if $result->acknowledged;
172             }
173             catch {
174 0     0     warn "delete_one() operation failed: deletion not asserted";
175 0           };
176              
177 0           return undef;
178             }
179              
180              
181             sub clear {
182 0     0 0   my ($self) = @_;
183              
184 0           my $coll = $self->_coll;
185 0           $self->_clear_coll;
186 0           $coll->drop;
187              
188 0           return undef;
189             }
190              
191              
192             sub get_keys {
193 0     0 0   my ($self) = @_;
194              
195 0           my @allKeys = ();
196 0           my $qresult = $self->_coll->find(
197             {},
198             {
199             batchSize => 100,
200             projection => { _id => 1 }
201             }
202             )->result;
203 0           while ( my @batch = $qresult->batch ) {
204 0           push @allKeys, map { decodeURIComponent( $_->{'_id'} ) } @batch;
  0            
205             }
206 0           return @allKeys;
207             }
208              
209              
210             sub get_namespaces {
211 0     0 0   my $self = shift;
212 0           return $self->mongodb->get_database( $self->db_name )->collection_names;
213             }
214              
215             1;
216              
217             __END__
218              
219             =pod
220              
221             =encoding utf-8
222              
223             =head1 NAME
224              
225             CHI::Driver::MongoDB - MongoDB driver for CHI
226              
227             =head1 VERSION
228              
229             version 0.0001
230              
231             =head1 SYNOPSIS
232              
233             use CHI;
234            
235             my $cache = CHI->new (
236             driver => 'MongoDB',
237             namespace => 'foo',
238             # optional, default: _CHI_
239             db_name => '...',
240             # optional, default: mongodb://127.0.0.1:27017
241             connection_uri => '...',
242             # optional
243             mongodb_options => { ... },
244             );
245              
246             =head1 DESCRIPTION
247              
248             Driver to use L<MongoDB> as storage back end for L<CHI>.
249              
250             L<CHI> C<namespace>s are translated to L<MongoDB> collections, so you
251             can use the same database name for all CHI instances (or simply use
252             the default and omit the parameter).
253              
254             The driver supports the C<expires_on_backend> option, but be aware that
255             the expiration actually happens within a short but unspecified time
256             frame B<after> the exact expiration timeout (cf. L</"FAILING TESTS">).
257              
258             By default the MongoDB server is expected to be available on localhost,
259             port 27017. Pass the C<connection_uri> param to override this.
260             For testing purposes, set the C<MONGODB_CONNECTION_URI> environment
261             variable.
262              
263             =head1 WARNING
264              
265             This module is currently considered to be a B<beta release>.
266              
267             While the (mostly) succeeding test suite shows that there probably
268             are no major issues endangering your data it has only been tested
269             with MongoDB 3.2.x and the MongoDB Perl module v1.4.5.
270              
271             Please open a bug report on L<https://rt.perl.org/> or send me a
272             mail if you encounter any problems.
273              
274             =head1 CONSTRUCTOR OPTIONS
275              
276             =over 4
277              
278             =item C<connection_uri>: String
279              
280             Where the MongoDB server is listening. By default,
281             C<mongodb://127.0.0.1:27017> is used.
282              
283             See L<MongoDB::MongoClient/"CONNECTION-STRING-URI"> for details.
284              
285             =item C<db_name>: String
286              
287             The database name inside MongoDB. Defaults to C<_CHI_>.
288             The name is arbitrary but should of course not clash with your other
289             databases.
290              
291             =item C<mongodb_options>: HASHREF
292              
293             Arbitrary options which are passed to the constructor of the underlying
294             L<MongoDB::MongoClient> object.
295              
296             =back
297              
298             =head1 FAILING TESTS
299              
300             Currently, you should expect four failing tests from the CHI test suite:
301             94, 205-206, 904.
302              
303             =over 4
304              
305             =item B<94>: C<Failed test 'threw Regexp ((?^:discard timeout .* reached))'>
306              
307             I do not know yet, why the test functions succeeds while it is supposed
308             to C<die>.
309              
310             =item B<205>: C<Failed test 'cannot get_object(key0) after expire'>
311              
312             =item B<206>: C<Failed test 'cannot get_object(key1) after expire'>
313              
314             MongoDB uses a dedicated thread to remove documents whose lifetime has
315             expired. It checks the stored documents periodically but that means
316             there is a short period of time between the moment a document expires
317             and the moment it is actually removed.
318             This should be no problem for our caching purposes but is the reason
319             why these tests fail.
320              
321             =item B<904> C<Failed test 'test_serializers died (Insecure dependency in eval while running with -T switch...>
322              
323             No idea yet what is wrong here.
324              
325             =back
326              
327             Fixing the second and third failing test may not be possible at all,
328             but I haven't yet found a way to disable indiviual tests.
329              
330             Other than these four, 944 subtests succeed while 67 are skipped.
331              
332             I have seen test runs where for some unknown reason a large number of
333             tests fail. I do not know why that happens. The MongoDB database is
334             dropped and recreated on every run of the test suite.
335             On simply re-running the whole test usually only the four tests shown
336             above fail.
337              
338             =head1 TODO
339              
340             In no particular order:
341              
342             =over 4
343              
344             =item Allow passing in a preconfigured L<MongoDB> object.
345              
346             =item Allow using L<Mango> instead of L<MongoDB>.
347              
348             =item Implement C<store_multi ( $key_data, $options )> method.
349              
350             =item Implement LRU discard policy.
351              
352             =item Implement support for size awareness in the back end.
353              
354             B<Caveat>: As of version 3.2 MongoDB supports collections that either
355             have a finite size (in bytes or in number of stored documents) or
356             support the automatic expiration handling but not both!
357              
358             =back
359              
360             =head1 SEE ALSO
361              
362             L<CHI>, L<CHI::Driver::Development>, L<MongoDB>
363              
364             =head1 AUTHOR
365              
366             Heiko Jansen <hjansen@cpan.org>
367              
368             =head1 COPYRIGHT AND LICENSE
369              
370             This software is copyright (c) 2016 by Heiko Jansen.
371              
372             This is free software; you can redistribute it and/or modify it under
373             the same terms as the Perl 5 programming language system itself.
374              
375             =cut