File Coverage

blib/lib/MojoX/UserAgent/Throttler.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package MojoX::UserAgent::Throttler;
2              
3 1     1   26026 use Mojo::Base -strict;
  1         11502  
  1         13  
4              
5 1     1   923 use version; our $VERSION = qv('0.1.1'); # REMINDER: update Changes
  1         2470  
  1         9  
6              
7             # REMINDER: update dependencies in Build.PL
8 1     1   887 use Mojo::UserAgent;
  1         331255  
  1         14  
9 1     1   41 use Mojo::Util qw( monkey_patch );
  1         1  
  1         63  
10 1     1   4 use Sub::Util 1.40 qw( set_subname );
  1         32  
  1         54  
11 1     1   265 use Sub::Throttler 0.002000 qw( throttle_me throttle_me_sync done_cb );
  0            
  0            
12              
13              
14             # https://github.com/kraih/mojo/issues/663
15             # Inconsistent behavior of Mojo::UserAgent::DESTROY:
16             # - sync requests always executed, even when started while DESTROY
17             # - for all active async requests which was started before DESTROY user's
18             # callback will be called with error in $tx
19             # - for all async requests which was started while DESTROY user's callback
20             # won't be called
21             # To emulate this behaviour with throttling:
22             # - sync request: always executed, even when started while DESTROY
23             # - new async request while DESTROY: ignored
24             # - delayed async request (it was delayed before DESTROY):
25             # * if it start before DESTROY: let Mojo::UserAgent handle it using
26             # done_cb($done,$cb)
27             # * if it start while DESTROY: do $done->(0) and call user's callback
28             # with error in $tx
29             # * if it still delayed after DESTROY: call user's callback with error
30             # in $tx
31              
32             use constant START_ARGS => 3;
33              
34             my %Delayed; # $ua => { $tx => [$tx, $cb], … }
35             my %IsDestroying; # $ua => 1
36              
37             my $ORIG_start = \&Mojo::UserAgent::start;
38             my $ORIG_DESTROY= \&Mojo::UserAgent::DESTROY;
39              
40             monkey_patch 'Mojo::UserAgent',
41             start => set_subname('Mojo::UserAgent::start', sub {
42             # WARNING Async call return undef instead of (undocumented) connection $id.
43             ## no critic (ProhibitExplicitReturnUndef)
44             my ($self, $tx, $cb) = @_;
45             if (START_ARGS == @_ && $cb) {
46             if ($IsDestroying{ $self }) {
47             # $cb->($self, $tx->client_close(1)); # to fix issue 663 or not to fix?
48             return undef;
49             }
50             else {
51             $Delayed{ $self }{ $tx } = [ $tx, $cb ];
52             }
53             }
54             my $done = ref $_[-1] eq 'CODE' ? &throttle_me || return undef : &throttle_me_sync;
55             ($self, $tx, $cb) = @_;
56             if ($cb) {
57             if ($IsDestroying{ $self }) {
58             $done->(0);
59             }
60             else {
61             delete $Delayed{ $self }{ $tx };
62             $self->$ORIG_start($tx, done_cb($done, $cb));
63             }
64             return undef;
65             }
66             else {
67             $tx = $self->$ORIG_start($tx);
68             $done->();
69             return $tx;
70             }
71             }),
72             DESTROY => sub {
73             my ($self) = @_;
74             $IsDestroying{ $self } = 1;
75             for (values %{ delete $Delayed{ $self } || {} }) {
76             my ($tx, $cb) = @{ $_ };
77             $cb->($self, $tx->client_close(1));
78             }
79             $self->$ORIG_DESTROY;
80             delete $IsDestroying{ $self };
81             return;
82             };
83              
84              
85             1; # Magic true value required at end of module
86             __END__
87              
88             =encoding utf8
89              
90             =head1 NAME
91              
92             MojoX::UserAgent::Throttler - add throttling support to Mojo::UserAgent
93              
94              
95             =head1 SYNOPSIS
96              
97             use MojoX::UserAgent::Throttler;
98             use Sub::Throttler::SOME_ALGORITHM;
99              
100             my $throttle = Sub::Throttler::SOME_ALGORITHM->new(...);
101             $throttle->apply_to_methods('Mojo::UserAgent');
102              
103             =head1 DESCRIPTION
104              
105             This module helps throttle L<Mojo::UserAgent> using L<Sub::Throttler>.
106              
107             While in most cases this module isn't needed and existing functionality of
108             Sub::Throttler is enough to throttle Mojo::UserAgent, there are two
109             special cases which needs extra handling - when B<Mojo::UserAgent object
110             is destroyed while there are delayed requests>, and when B<new async
111             requests start while destroying Mojo::UserAgent object>.
112              
113             To handle these cases it won't be enough to just do usual:
114              
115             throttle_it('Mojo::UserAgent::start');
116              
117             Instead you'll have to write L<Sub::Throttler/"custom wrapper"> plus add
118             wrapper for Mojo::UserAgent::DESTROY. Both are provided by this module and
119             activated when you load it.
120              
121             So, when using this module you shouldn't manually call throttle_it() like
122             shown above - just use this module and then setup throttling algorithms as
123             you need and apply them to L<Mojo::UserAgent/"start"> - this will let you
124             throttle all (sync/async, GET/POST/etc.) requests.
125             Use L<Sub::Throttler::algo/"apply_to"> to customize throttling based on
126             request method, hostname, etc.
127              
128             =head2 EXAMPLE
129              
130             use MojoX::UserAgent::Throttler;
131             use Sub::Throttler::Limit;
132             my $throttle = Sub::Throttler::Limit->new(limit=>5);
133             # Example policy:
134             # - don't throttle sync calls
135             # - throttle async GET requests by host
136             # - throttle other async requests by method, per $ua object
137             # I.e. allow up to 5 parallel GET requests to each host globally for
138             # all Mojo::UserAgent objects plus up to 5 parallel non-GET requests
139             # per each Mojo::UserAgent object.
140             $throttle->apply_to(sub {
141             my ($this, $name, @params) = @_;
142             if (ref $this eq 'Mojo::UserAgent') {
143             my ($tx, $cb) = @params;
144             if (!$cb) {
145             return;
146             } elsif ('GET' eq uc $tx->req->method) {
147             return { $tx->req->url->host => 1 };
148             } else {
149             return { "$this " . uc $tx->req->method => 1 };
150             }
151             }
152             return;
153             });
154              
155              
156             =head1 BUGS AND LIMITATIONS
157              
158             No bugs have been reported.
159              
160              
161             =head1 SUPPORT
162              
163             Please report any bugs or feature requests through the web interface at
164             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=MojoX-UserAgent-Throttler>.
165             I will be notified, and then you'll automatically be notified of progress
166             on your bug as I make changes.
167              
168             You can also look for information at:
169              
170             =over
171              
172             =item * RT: CPAN's request tracker
173              
174             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=MojoX-UserAgent-Throttler>
175              
176             =item * AnnoCPAN: Annotated CPAN documentation
177              
178             L<http://annocpan.org/dist/MojoX-UserAgent-Throttler>
179              
180             =item * CPAN Ratings
181              
182             L<http://cpanratings.perl.org/d/MojoX-UserAgent-Throttler>
183              
184             =item * Search CPAN
185              
186             L<http://search.cpan.org/dist/MojoX-UserAgent-Throttler/>
187              
188             =back
189              
190              
191             =head1 AUTHOR
192              
193             Alex Efros C<< <powerman@cpan.org> >>
194              
195              
196             =head1 LICENSE AND COPYRIGHT
197              
198             Copyright 2014 Alex Efros <powerman@cpan.org>.
199              
200             This program is distributed under the MIT (X11) License:
201             L<http://www.opensource.org/licenses/mit-license.php>
202              
203             Permission is hereby granted, free of charge, to any person
204             obtaining a copy of this software and associated documentation
205             files (the "Software"), to deal in the Software without
206             restriction, including without limitation the rights to use,
207             copy, modify, merge, publish, distribute, sublicense, and/or sell
208             copies of the Software, and to permit persons to whom the
209             Software is furnished to do so, subject to the following
210             conditions:
211              
212             The above copyright notice and this permission notice shall be
213             included in all copies or substantial portions of the Software.
214              
215             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
216             EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
217             OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
218             NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
219             HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
220             WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
221             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
222             OTHER DEALINGS IN THE SOFTWARE.
223