File Coverage

blib/lib/MooseX/Role/MongoDB.pm
Criterion Covered Total %
statement 39 110 35.4
branch 0 34 0.0
condition 0 37 0.0
subroutine 13 26 50.0
pod n/a
total 52 207 25.1


line stmt bran cond sub pod time code
1 1     1   1310983 use v5.10;
  1         2  
  1         29  
2 1     1   3 use strict;
  1         1  
  1         20  
3 1     1   3 use warnings;
  1         2  
  1         34  
4              
5             package MooseX::Role::MongoDB;
6             # ABSTRACT: Provide MongoDB connections, databases and collections
7              
8             our $VERSION = '0.008';
9              
10 1     1   3 use Moose::Role 2;
  1         13  
  1         6  
11 1     1   3992 use MooseX::AttributeShortcuts;
  1         174614  
  1         5  
12              
13 1     1   19096 use Carp ();
  1         1  
  1         12  
14 1     1   9 use MongoDB;
  1         1  
  1         18  
15 1     1   3 use MongoDB::MongoClient 0.702;
  1         25  
  1         21  
16 1     1   524 use Socket 1.96 qw/:addrinfo SOCK_RAW/; # IPv6 capable
  1         2468  
  1         290  
17 1     1   315 use String::Flogger qw/flog/;
  1         1939  
  1         4  
18 1     1   494 use Type::Params qw/compile/;
  1         7566  
  1         6  
19 1     1   133 use Types::Standard qw/:types/;
  1         1  
  1         3  
20 1     1   2093 use namespace::autoclean;
  1         1  
  1         6  
21              
22             #--------------------------------------------------------------------------#
23             # Dependencies
24             #--------------------------------------------------------------------------#
25              
26             #pod =requires _logger
27             #pod
28             #pod You must provide a private method that returns a logging object. It must
29             #pod implement at least the C<info> and C<debug> methods. L<MooseX::Role::Logger>
30             #pod version 0.002 or later is recommended, but other logging roles may be
31             #pod sufficient.
32             #pod
33             #pod =cut
34              
35             requires '_logger';
36              
37             #--------------------------------------------------------------------------#
38             # Configuration attributes
39             #--------------------------------------------------------------------------#
40              
41             has _mongo_client_class => (
42             is => 'lazy',
43             isa => 'Str',
44             );
45              
46 0     0     sub _build__mongo_client_class { return 'MongoDB::MongoClient' }
47              
48             has _mongo_client_options => (
49             is => 'lazy',
50             isa => HashRef, # hashlike?
51             );
52              
53 0     0     sub _build__mongo_client_options { return {} }
54              
55             has _mongo_default_database => (
56             is => 'lazy',
57             isa => Str,
58             );
59              
60 0     0     sub _build__mongo_default_database { return 'test' }
61              
62             #--------------------------------------------------------------------------#
63             # Caching attributes
64             #--------------------------------------------------------------------------#
65              
66             has _mongo_pid => (
67             is => 'rwp', # private setter so we can update on fork
68             isa => 'Num',
69             default => sub { $$ },
70             );
71              
72             has _mongo_client => (
73             is => 'lazy',
74             isa => InstanceOf ['MongoDB::MongoClient'],
75             clearer => 1,
76             predicate => '_has_mongo_client',
77             );
78              
79             sub _build__mongo_client {
80 0     0     my ($self) = @_;
81 0           my $options = { %{ $self->_mongo_client_options } };
  0            
82 0 0         if ( exists $options->{host} ) {
83 0           $options->{host} = $self->_host_names_to_ip( $options->{host} );
84             }
85 0   0       $options->{db_name} //= $self->_mongo_default_database;
86 0           $self->_mongo_log( debug => "connecting to MongoDB with %s", $options );
87 0           return MongoDB::MongoClient->new($options);
88             }
89              
90             has _mongo_database_cache => (
91             is => 'lazy',
92             isa => HashRef,
93             clearer => 1,
94             );
95              
96 0     0     sub _build__mongo_database_cache { return {} }
97              
98             has _mongo_collection_cache => (
99             is => 'lazy',
100             isa => HashRef,
101             clearer => 1,
102             );
103              
104 0     0     sub _build__mongo_collection_cache { return {} }
105              
106             #--------------------------------------------------------------------------#
107             # Role methods
108             #--------------------------------------------------------------------------#
109              
110             #pod =method _mongo_database
111             #pod
112             #pod $obj->_mongo_database( $database_name );
113             #pod
114             #pod Returns a L<MongoDB::Database>. The argument is the database name.
115             #pod With no argument, the default database name is used.
116             #pod
117             #pod =cut
118              
119             sub _mongo_database {
120 0     0     state $check = compile( Object, Optional [Str] );
121 0           my ( $self, $database ) = $check->(@_);
122 0   0       $database //= $self->_mongo_default_database;
123 0           $self->_mongo_check_connection;
124 0           $self->_mongo_log( debug => "retrieving database $database" );
125 0   0       return $self->_mongo_database_cache->{$database} //=
126             $self->_mongo_client->get_database($database);
127             }
128              
129             #pod =method _mongo_collection
130             #pod
131             #pod $obj->_mongo_collection( $database_name, $collection_name );
132             #pod $obj->_mongo_collection( $collection_name );
133             #pod
134             #pod Returns a L<MongoDB::Collection>. With two arguments, the first argument is
135             #pod the database name and the second is the collection name. With a single
136             #pod argument, the argument is the collection name from the default database name.
137             #pod
138             #pod =cut
139              
140             sub _mongo_collection {
141 0     0     state $check = compile( Object, Str, Optional [Str] );
142 0           my ( $self, @args ) = $check->(@_);
143 0 0         my ( $database, $collection ) =
144             @args > 1 ? @args : ( $self->_mongo_default_database, $args[0] );
145 0           $self->_mongo_check_connection;
146 0           $self->_mongo_log( debug => "retrieving collection $database.$collection" );
147 0   0       return $self->_mongo_collection_cache->{$database}{$collection} //=
148             $self->_mongo_database($database)->get_collection($collection);
149             }
150              
151             #pod =method _mongo_clear_caches
152             #pod
153             #pod $obj->_mongo_clear_caches;
154             #pod
155             #pod Clears the MongoDB client, database and collection caches. The next
156             #pod request for a database or collection will reconnect to the MongoDB.
157             #pod
158             #pod =cut
159              
160             sub _mongo_clear_caches {
161 0     0     my ($self) = @_;
162 0           $self->_clear_mongo_collection_cache;
163 0           $self->_clear_mongo_database_cache;
164 0           $self->_clear_mongo_client;
165 0           return 1;
166             }
167              
168             #--------------------------------------------------------------------------#
169             # Private methods
170             #--------------------------------------------------------------------------#
171              
172             # check if we've forked and need to reconnect
173             sub _mongo_check_connection {
174 0     0     my ($self) = @_;
175              
176 0 0         my $mc = $self->_has_mongo_client ? $self->_mongo_client : undef;
177              
178             # alpha driver manages forks for us, so we don't need to
179 0   0       my $is_alpha = $mc && eval { $mc->VERSION(v0.998.0) };
180              
181 0           my $reset_reason;
182 0 0 0       if ( $$ != $self->_mongo_pid ) {
    0 0        
183 0 0         $reset_reason = "PID change" unless $is_alpha;
184 0           $self->_set__mongo_pid($$);
185             }
186             elsif ( !$is_alpha && $mc && !$mc->connected ) {
187 0           $reset_reason = "Not connected";
188             }
189              
190 0 0         if ($reset_reason) {
191 0           $self->_mongo_log( debug => "clearing MongoDB caches: $reset_reason" );
192 0           $self->_mongo_clear_caches;
193             }
194              
195 0           return;
196             }
197              
198             sub _mongo_log {
199 0     0     my ( $self, $level, @msg ) = @_;
200 0           $msg[0] = "$self ($$) $msg[0]";
201 0           $self->_logger->$level( flog( [@msg] ) );
202             }
203              
204             sub _parse_connection_uri {
205 0     0     my ( $self, $uri ) = @_;
206 0           my %parse;
207 0 0         if (
208             $uri =~ m{ ^
209             mongodb://
210             (?: ([^:]*) : ([^@]*) @ )? # [username:password@]
211             ([^/]*) # host1[:port1][,host2[:port2],...[,hostN[:portN]]]
212             (?:
213             / ([^?]*) # /[database]
214             (?: [?] (.*) )? # [?options]
215             )?
216             $ }x
217             )
218             {
219             return {
220 0   0       username => $1 // '',
      0        
      0        
      0        
      0        
221             password => $2 // '',
222             hostpairs => $3 // '',
223             db_name => $4 // '',
224             options => $5 // '',
225             };
226             }
227 0           return;
228             }
229              
230             sub _host_names_to_ip {
231 0     0     my ( $self, $uri ) = @_;
232 0 0         my $parsed = $self->_parse_connection_uri($uri)
233             or Carp::confess("Could not parse connection string '$uri'\n");
234              
235             # convert hostnames to IP addresses to work around
236             # some MongoDB bugs/inefficiencies
237 0           my @pairs;
238 0           for my $p ( split /,/, $parsed->{hostpairs} ) {
239 0           my ( $host, $port ) = split /:/, $p;
240 0           my $ipaddr;
241 0           for my $family ( Socket::AF_INET(), Socket::AF_INET6() ) {
242 0           my ( $err, $res ) =
243             getaddrinfo( $host, "", { family => $family, socktype => SOCK_RAW } );
244 0 0         next if $err;
245 0           ( $err, $ipaddr ) = getnameinfo( $res->{addr}, NI_NUMERICHOST, NIx_NOSERV );
246 0 0         last if defined $ipaddr;
247             }
248 0 0         Carp::croak "Cannot resolve address for '$host'" unless defined $ipaddr;
249 0 0 0       $ipaddr .= ":$port" if defined $port && length $port;
250 0           push @pairs, $ipaddr;
251             }
252              
253             # reassemble new host URI
254 0           my $new_host = "mongodb://";
255 0 0         $new_host .= "$parsed->{username}:$parsed->{password}\@"
256             if length $parsed->{username};
257 0           $new_host .= join( ",", @pairs );
258 0 0 0       $new_host .= "/" if length $parsed->{db_name} || length $parsed->{options};
259 0 0         $new_host .= $parsed->{db_name} if length $parsed->{db_name};
260 0 0         $new_host .= "?$parsed->{options}" if length $parsed->{options};
261              
262 0           return $new_host;
263             }
264              
265             1;
266              
267              
268             # vim: ts=4 sts=4 sw=4 et:
269              
270             __END__
271              
272             =pod
273              
274             =encoding UTF-8
275              
276             =head1 NAME
277              
278             MooseX::Role::MongoDB - Provide MongoDB connections, databases and collections
279              
280             =head1 VERSION
281              
282             version 0.008
283              
284             =head1 SYNOPSIS
285              
286             In your module:
287              
288             package MyData;
289             use Moose;
290             with 'MooseX::Role::MongoDB';
291              
292             has database => (
293             is => 'ro',
294             isa => 'Str',
295             required => 1,
296             );
297              
298             has client_options => (
299             is => 'ro',
300             isa => 'HashRef',
301             default => sub { {} },
302             );
303              
304             sub _build__mongo_default_database { return $_[0]->database }
305             sub _build__mongo_client_options { return $_[0]->client_options }
306              
307             sub do_stuff {
308             my ($self) = @_;
309              
310             # get "test" database
311             my $db = $self->_mongo_database("test");
312              
313             # get "books" collection from default database
314             my $books = $self->_mongo_collection("books");
315              
316             # get "books" collection from another database
317             my $other = $self->_mongo_collection("otherdb" => "books");
318              
319             # ... do stuff with them
320             }
321              
322             In your code:
323              
324             my $obj = MyData->new(
325             database => 'MyDB',
326             client_options => {
327             host => "mongodb://example.net:27017",
328             username => "willywonka",
329             password => "ilovechocolate",
330             },
331             );
332              
333             $obj->do_stuff;
334              
335             =head1 DESCRIPTION
336              
337             This role helps create and manage L<MongoDB> objects. All MongoDB objects will
338             be generated lazily on demand and cached. The role manages a single
339             L<MongoDB::MongoClient> connection, but many L<MongoDB::Database> and
340             L<MongoDB::Collection> objects.
341              
342             The role also compensates for dropped connections and forks. If these are
343             detected, the object caches are cleared and new connections and objects will be
344             generated in the new process.
345              
346             Note that a lost connection will not be detectable until I<after> an exception
347             is thrown due to a failed operation.
348              
349             When using this role, you should not hold onto MongoDB objects for long if
350             there is a chance of your code forking. Instead, request them again
351             each time you need them.
352              
353             =head1 REQUIREMENTS
354              
355             =head2 _logger
356              
357             You must provide a private method that returns a logging object. It must
358             implement at least the C<info> and C<debug> methods. L<MooseX::Role::Logger>
359             version 0.002 or later is recommended, but other logging roles may be
360             sufficient.
361              
362             =head1 METHODS
363              
364             =head2 _mongo_database
365              
366             $obj->_mongo_database( $database_name );
367              
368             Returns a L<MongoDB::Database>. The argument is the database name.
369             With no argument, the default database name is used.
370              
371             =head2 _mongo_collection
372              
373             $obj->_mongo_collection( $database_name, $collection_name );
374             $obj->_mongo_collection( $collection_name );
375              
376             Returns a L<MongoDB::Collection>. With two arguments, the first argument is
377             the database name and the second is the collection name. With a single
378             argument, the argument is the collection name from the default database name.
379              
380             =head2 _mongo_clear_caches
381              
382             $obj->_mongo_clear_caches;
383              
384             Clears the MongoDB client, database and collection caches. The next
385             request for a database or collection will reconnect to the MongoDB.
386              
387             =for Pod::Coverage BUILD
388              
389             =head1 CONFIGURING
390              
391             The role uses several private attributes to configure itself:
392              
393             =over 4
394              
395             =item *
396              
397             C<_mongo_client_class> — name of the client class
398              
399             =item *
400              
401             C<_mongo_client_options> — passed to client constructor
402              
403             =item *
404              
405             C<_mongo_default_database> — default name used if not specified
406              
407             =back
408              
409             Each of these have lazy builders that you can override in your class to
410             customize behavior of the role.
411              
412             The builders are:
413              
414             =over 4
415              
416             =item *
417              
418             C<_build__mongo_client_class> — default is C<MongoDB::MongoClient>
419              
420             =item *
421              
422             C<_build__mongo_client_options> — default is an empty hash reference
423              
424             =item *
425              
426             C<_build__mongo_default_database> — default is the string 'test'
427              
428             =back
429              
430             You will generally want to at least override C<_build__mongo_client_options> to
431             allow connecting to different hosts. You may want to set it explicitly or you
432             may want to have your own public attribute for users to set (as shown in the
433             L</SYNOPSIS>). The choice is up to you.
434              
435             If a MongoDB C<host> string is provided in the client options hash, any host
436             names will be converted to IP addresses to avoid known bugs using
437             authentication over SSL.
438              
439             Note that the C<_mongo_default_database> is also used as the default database for
440             authentication, unless a C<db_name> is provided to C<_mongo_client_options>.
441              
442             =head1 LOGGING
443              
444             Currently, only 'debug' level logs messages are generated for tracing MongoDB
445             interaction activity across forks. See the tests for an example of how to
446             enable it.
447              
448             =head1 SEE ALSO
449              
450             =over 4
451              
452             =item *
453              
454             L<Moose>
455              
456             =item *
457              
458             L<MongoDB>
459              
460             =back
461              
462             =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
463              
464             =head1 SUPPORT
465              
466             =head2 Bugs / Feature Requests
467              
468             Please report any bugs or feature requests through the issue tracker
469             at L<https://github.com/dagolden/MooseX-Role-MongoDB/issues>.
470             You will be notified automatically of any progress on your issue.
471              
472             =head2 Source Code
473              
474             This is open source software. The code repository is available for
475             public review and contribution under the terms of the license.
476              
477             L<https://github.com/dagolden/MooseX-Role-MongoDB>
478              
479             git clone https://github.com/dagolden/MooseX-Role-MongoDB.git
480              
481             =head1 AUTHOR
482              
483             David Golden <dagolden@cpan.org>
484              
485             =head1 CONTRIBUTOR
486              
487             =for stopwords Alexandr Ciornii
488              
489             Alexandr Ciornii <alexchorny@gmail.com>
490              
491             =head1 COPYRIGHT AND LICENSE
492              
493             This software is Copyright (c) 2013 by David Golden.
494              
495             This is free software, licensed under:
496              
497             The Apache License, Version 2.0, January 2004
498              
499             =cut