File Coverage

blib/lib/McBain/WithGearmanXS.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package McBain::WithGearmanXS;
2              
3             # ABSTRACT: Load a McBain API as a Gearman worker
4              
5 1     1   27539 use warnings;
  1         4  
  1         41  
6 1     1   6 use strict;
  1         2  
  1         38  
7              
8 1     1   7 use Carp;
  1         6  
  1         94  
9 1     1   548 use Gearman::XS qw(:constants);
  0            
  0            
10             use Gearman::XS::Worker;
11             use JSON;
12              
13             our $VERSION = "2.000000";
14             $VERSION = eval $VERSION;
15              
16             =head1 NAME
17            
18             McBain::WithGearmanXS - Load a McBain API as a Gearman worker
19              
20             =head1 SYNOPSIS
21              
22             # write your API as you normally would, and create
23             # a simple start-up script:
24              
25             #!/usr/bin/perl -w
26              
27             use warnings;
28             use strict;
29             use MyAPI -withGearmanXS;
30              
31             MyAPI->work('localhost', 4730);
32              
33             =head1 DESCRIPTION
34              
35             C turns your L API into a L,
36             making it easy to consume your APIs with L.
37              
38             When your API is loaded, this module will traverse its routes, and create a queue for each
39             methods found. So, for example, if your API looks like this...
40              
41             package MyAPI;
42              
43             use McBain;
44              
45             get '/multiply' => (
46             ...
47             );
48              
49             post '/divide' => (
50             ...
51             );
52              
53             ...then the following queues will be created:
54              
55             GET:/multiply
56             POST:/divide
57              
58             The workers created receive payloads in JSON format, and convert them into the hash-refs your
59             API's methods expect to receive. Results are sent back to the clients in JSON as well. Note
60             that if an API method does not return a hash-ref, this runner module will automatically
61             turn it into a hash-ref to ensure that conversion into JSON will be possible. The created
62             hash-ref will have one key - holding the method's name, with whatever was returned from the
63             method as its value. For example, if method C in topic C returns an
64             integer (say 7), then the client will get the JSON C<{ "GET:/math/divide": 7 }>.
65              
66             =head2 CAVEATS
67              
68             There are some disadvantages to using this runner:
69              
70             =over
71              
72             =item * Since a queue is created for every route and method in the API, C cannot
73             intercept calls to routes that do not exist.
74              
75             =item * You can't use regular expressions when defining queues. Well, you can, but they
76             won't work.
77              
78             =item * Gearman provides no way of providing a detailed error message when jobs fail,
79             therefore all C can do is indicate that the job has failed and no more.
80              
81             =back
82              
83             The first two I hope to overcome in a next version, or even a different runner, by creating
84             just one queue that simply forwards the requests to C.
85              
86             =head1 METHODS EXPORTED TO YOUR API
87              
88             =head2 work( [ $host, $port ] )
89              
90             Connects the Gearman worker created by the module to the Gearman server
91             running at the host and port provided. If none are provided, C
92             and C<4730> are used, respectively.
93              
94             The method never returns, so that the worker listens for jobs continuously.
95              
96             =head1 METHODS REQUIRED BY MCBAIN
97              
98             This runner module implements the following methods required by C:
99              
100             =head2 init( )
101              
102             Creates the L method for the root topic of the API.
103              
104             =cut
105              
106             sub init {
107             my ($class, $target) = @_;
108              
109             if ($target->is_root) {
110             no strict 'refs';
111             *{"${target}::work"} = sub {
112             my ($pkg, $host, $port) = @_;
113              
114             $host ||= 'localhost';
115             $port ||= 4730;
116              
117             my $worker = Gearman::XS::Worker->new;
118              
119             $class->_register_functions($worker, $pkg, $McBain::INFO{$target});
120              
121             $worker->add_server($host, $port) == GEARMAN_SUCCESS
122             || confess "Can't connect to gearman server at $host:$port, ".$worker->error;
123              
124             while (1) {
125             $worker->work();
126             }
127             };
128             }
129             }
130              
131             =head2 generate_env( $job )
132              
133             Receives the L object and creates C's standard env
134             hash-ref from it.
135              
136             =cut
137              
138             sub generate_env {
139             my ($self, $job) = @_;
140              
141             confess { code => 400, error => "Namespace must match : where METHOD is one of GET, POST, PUT, DELETE or OPTIONS" }
142             unless $job->function_name =~ m/^(GET|POST|PUT|DELETE|OPTIONS):[^:]+$/;
143              
144             my ($method, $route) = split(/:/, $job->function_name);
145              
146             return {
147             METHOD => $method,
148             ROUTE => $route,
149             PAYLOAD => $job->workload ? decode_json($job->workload) : {}
150             };
151             }
152              
153             =head2 generate_res( $env, $res )
154              
155             Converts the result from an API method in JSON. Read the discussion under
156             L for more info.
157              
158             =cut
159              
160             sub generate_res {
161             my ($self, $env, $res) = @_;
162              
163             $res = { $env->{METHOD}.':'.$env->{ROUTE} => $res }
164             unless ref $res eq 'HASH';
165              
166             return encode_json($res);
167             }
168              
169             =head2 handle_exception( $err, $job )
170              
171             Simply calls C<< $job->send_fail >> to return a job failed
172             status to the client.
173              
174             =cut
175              
176             sub handle_exception {
177             my ($class, $err, $job) = @_;
178              
179             $job->send_fail;
180             }
181              
182             sub _register_functions {
183             my ($class, $worker, $target, $topic) = @_;
184              
185             foreach my $route (keys %$topic) {
186             foreach my $meth (keys %{$topic->{$route}}) {
187             my $namespace = $meth.':'.$route;
188             $namespace =~ s!/$!!
189             unless $route eq '/';
190             unless (
191             $worker->add_function($namespace, 0, sub {
192             $target->call($_[0]);
193             }, {}) == GEARMAN_SUCCESS
194             ) {
195             confess "Can't register function $namespace, ".$worker->error;
196             }
197             }
198             }
199             }
200              
201             =head1 CONFIGURATION AND ENVIRONMENT
202            
203             No configuration files are required.
204            
205             =head1 DEPENDENCIES
206            
207             C depends on the following CPAN modules:
208            
209             =over
210              
211             =item * L
212            
213             =item * L
214              
215             =item * L
216            
217             =back
218              
219             =head1 INCOMPATIBILITIES WITH OTHER MODULES
220              
221             None reported.
222              
223             =head1 BUGS AND LIMITATIONS
224              
225             Please report any bugs or feature requests to
226             C, or through the web interface at
227             L.
228              
229             =head1 SUPPORT
230              
231             You can find documentation for this module with the perldoc command.
232              
233             perldoc McBain::WithGearmanXS
234              
235             You can also look for information at:
236              
237             =over 4
238            
239             =item * RT: CPAN's request tracker
240            
241             L
242            
243             =item * AnnoCPAN: Annotated CPAN documentation
244            
245             L
246            
247             =item * CPAN Ratings
248            
249             L
250            
251             =item * Search CPAN
252            
253             L
254            
255             =back
256            
257             =head1 AUTHOR
258            
259             Ido Perlmuter
260            
261             =head1 LICENSE AND COPYRIGHT
262            
263             Copyright (c) 2013, Ido Perlmuter C<< ido@ido50.net >>.
264            
265             This module is free software; you can redistribute it and/or
266             modify it under the same terms as Perl itself, either version
267             5.8.1 or any later version. See L
268             and L.
269            
270             The full text of the license can be found in the
271             LICENSE file included with this module.
272            
273             =head1 DISCLAIMER OF WARRANTY
274            
275             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
276             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
277             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
278             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
279             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
280             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
281             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
282             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
283             NECESSARY SERVICING, REPAIR, OR CORRECTION.
284            
285             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
286             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
287             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
288             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
289             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
290             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
291             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
292             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
293             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
294             SUCH DAMAGES.
295            
296             =cut
297              
298             1;
299             __END__