Filename | /usr/local/lib/perl5/site_perl/Mail/SpamAssassin/AsyncLoop.pm |
Statements | Executed 305169 statements in 1.82s |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
8743 | 3 | 1 | 1.23s | 6.88s | bgsend_and_start_lookup | Mail::SpamAssassin::AsyncLoop::
1968 | 1 | 1 | 357ms | 392ms | start_lookup | Mail::SpamAssassin::AsyncLoop::
15518 | 2 | 1 | 127ms | 127ms | CORE:subst (opcode) | Mail::SpamAssassin::AsyncLoop::
470 | 1 | 1 | 15.3ms | 15.3ms | new | Mail::SpamAssassin::AsyncLoop::
1 | 1 | 1 | 65µs | 180µs | BEGIN@49 | Mail::SpamAssassin::AsyncLoop::
1 | 1 | 1 | 55µs | 68µs | BEGIN@36 | Mail::SpamAssassin::AsyncLoop::
1 | 1 | 1 | 28µs | 102µs | BEGIN@50 | Mail::SpamAssassin::AsyncLoop::
1 | 1 | 1 | 28µs | 33µs | BEGIN@38 | Mail::SpamAssassin::AsyncLoop::
1 | 1 | 1 | 27µs | 83µs | BEGIN@39 | Mail::SpamAssassin::AsyncLoop::
1 | 1 | 1 | 24µs | 24µs | BEGIN@43 | Mail::SpamAssassin::AsyncLoop::
1 | 1 | 1 | 22µs | 60µs | BEGIN@37 | Mail::SpamAssassin::AsyncLoop::
1 | 1 | 1 | 22µs | 393µs | BEGIN@41 | Mail::SpamAssassin::AsyncLoop::
1 | 1 | 1 | 20µs | 205µs | BEGIN@44 | Mail::SpamAssassin::AsyncLoop::
0 | 0 | 0 | 0s | 0s | __ANON__[:354] | Mail::SpamAssassin::AsyncLoop::
0 | 0 | 0 | 0s | 0s | abort_remaining_lookups | Mail::SpamAssassin::AsyncLoop::
0 | 0 | 0 | 0s | 0s | complete_lookups | Mail::SpamAssassin::AsyncLoop::
0 | 0 | 0 | 0s | 0s | domain_to_search_list | Mail::SpamAssassin::AsyncLoop::
0 | 0 | 0 | 0s | 0s | get_lookup | Mail::SpamAssassin::AsyncLoop::
0 | 0 | 0 | 0s | 0s | last_poll_responses_time | Mail::SpamAssassin::AsyncLoop::
0 | 0 | 0 | 0s | 0s | log_lookups_timing | Mail::SpamAssassin::AsyncLoop::
0 | 0 | 0 | 0s | 0s | report_id_complete | Mail::SpamAssassin::AsyncLoop::
0 | 0 | 0 | 0s | 0s | set_response_packet | Mail::SpamAssassin::AsyncLoop::
Line | State ments |
Time on line |
Calls | Time in subs |
Code |
---|---|---|---|---|---|
1 | # <@LICENSE> | ||||
2 | # Licensed to the Apache Software Foundation (ASF) under one or more | ||||
3 | # contributor license agreements. See the NOTICE file distributed with | ||||
4 | # this work for additional information regarding copyright ownership. | ||||
5 | # The ASF licenses this file to you under the Apache License, Version 2.0 | ||||
6 | # (the "License"); you may not use this file except in compliance with | ||||
7 | # the License. You may obtain a copy of the License at: | ||||
8 | # | ||||
9 | # http://www.apache.org/licenses/LICENSE-2.0 | ||||
10 | # | ||||
11 | # Unless required by applicable law or agreed to in writing, software | ||||
12 | # distributed under the License is distributed on an "AS IS" BASIS, | ||||
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
14 | # See the License for the specific language governing permissions and | ||||
15 | # limitations under the License. | ||||
16 | # </@LICENSE> | ||||
17 | |||||
18 | =head1 NAME | ||||
19 | |||||
20 | Mail::SpamAssassin::AsyncLoop - scanner asynchronous event loop | ||||
21 | |||||
22 | =head1 DESCRIPTION | ||||
23 | |||||
24 | An asynchronous event loop used for long-running operations, performed "in the | ||||
25 | background" during the Mail::SpamAssassin::check() scan operation, such as DNS | ||||
26 | blocklist lookups. | ||||
27 | |||||
28 | =head1 METHODS | ||||
29 | |||||
30 | =over 4 | ||||
31 | |||||
32 | =cut | ||||
33 | |||||
34 | package Mail::SpamAssassin::AsyncLoop; | ||||
35 | |||||
36 | 2 | 74µs | 2 | 82µs | # spent 68µs (55+14) within Mail::SpamAssassin::AsyncLoop::BEGIN@36 which was called:
# once (55µs+14µs) by Mail::SpamAssassin::PerMsgStatus::BEGIN@60 at line 36 # spent 68µs making 1 call to Mail::SpamAssassin::AsyncLoop::BEGIN@36
# spent 14µs making 1 call to strict::import |
37 | 2 | 65µs | 2 | 99µs | # spent 60µs (22+38) within Mail::SpamAssassin::AsyncLoop::BEGIN@37 which was called:
# once (22µs+38µs) by Mail::SpamAssassin::PerMsgStatus::BEGIN@60 at line 37 # spent 60µs making 1 call to Mail::SpamAssassin::AsyncLoop::BEGIN@37
# spent 38µs making 1 call to warnings::import |
38 | 2 | 72µs | 2 | 38µs | # spent 33µs (28+5) within Mail::SpamAssassin::AsyncLoop::BEGIN@38 which was called:
# once (28µs+5µs) by Mail::SpamAssassin::PerMsgStatus::BEGIN@60 at line 38 # spent 33µs making 1 call to Mail::SpamAssassin::AsyncLoop::BEGIN@38
# spent 5µs making 1 call to bytes::import |
39 | 2 | 66µs | 2 | 139µs | # spent 83µs (27+56) within Mail::SpamAssassin::AsyncLoop::BEGIN@39 which was called:
# once (27µs+56µs) by Mail::SpamAssassin::PerMsgStatus::BEGIN@60 at line 39 # spent 83µs making 1 call to Mail::SpamAssassin::AsyncLoop::BEGIN@39
# spent 56µs making 1 call to re::import |
40 | |||||
41 | 2 | 74µs | 2 | 764µs | # spent 393µs (22+371) within Mail::SpamAssassin::AsyncLoop::BEGIN@41 which was called:
# once (22µs+371µs) by Mail::SpamAssassin::PerMsgStatus::BEGIN@60 at line 41 # spent 393µs making 1 call to Mail::SpamAssassin::AsyncLoop::BEGIN@41
# spent 371µs making 1 call to Time::HiRes::import |
42 | |||||
43 | 2 | 64µs | 1 | 24µs | # spent 24µs within Mail::SpamAssassin::AsyncLoop::BEGIN@43 which was called:
# once (24µs+0s) by Mail::SpamAssassin::PerMsgStatus::BEGIN@60 at line 43 # spent 24µs making 1 call to Mail::SpamAssassin::AsyncLoop::BEGIN@43 |
44 | 2 | 102µs | 2 | 391µs | # spent 205µs (20+186) within Mail::SpamAssassin::AsyncLoop::BEGIN@44 which was called:
# once (20µs+186µs) by Mail::SpamAssassin::PerMsgStatus::BEGIN@60 at line 44 # spent 205µs making 1 call to Mail::SpamAssassin::AsyncLoop::BEGIN@44
# spent 186µs making 1 call to Exporter::import |
45 | |||||
46 | 1 | 10µs | our @ISA = qw(); | ||
47 | |||||
48 | # obtain timer resolution if possible | ||||
49 | # spent 180µs (65+115) within Mail::SpamAssassin::AsyncLoop::BEGIN@49 which was called:
# once (65µs+115µs) by Mail::SpamAssassin::PerMsgStatus::BEGIN@60 at line 59 | ||||
50 | 2 | 138µs | 2 | 177µs | # spent 102µs (28+74) within Mail::SpamAssassin::AsyncLoop::BEGIN@50 which was called:
# once (28µs+74µs) by Mail::SpamAssassin::PerMsgStatus::BEGIN@60 at line 50 # spent 102µs making 1 call to Mail::SpamAssassin::AsyncLoop::BEGIN@50
# spent 74µs making 1 call to vars::import |
51 | eval { | ||||
52 | 1 | 54µs | 3 | 104µs | $timer_resolution = Time::HiRes->can('clock_getres') # spent 87µs making 1 call to Time::HiRes::AUTOLOAD
# spent 11µs making 1 call to Time::HiRes::clock_getres
# spent 5µs making 1 call to UNIVERSAL::can |
53 | ? Time::HiRes::clock_getres(Time::HiRes::CLOCK_REALTIME()) | ||||
54 | : 0.001; # wild guess, assume resolution is better than 1s | ||||
55 | 1 | 2µs | 1; | ||
56 | 1 | 13µs | } or do { | ||
57 | $timer_resolution = 1; # Perl's builtin timer ticks at one second | ||||
58 | }; | ||||
59 | 1 | 5.71ms | 1 | 180µs | } # spent 180µs making 1 call to Mail::SpamAssassin::AsyncLoop::BEGIN@49 |
60 | |||||
61 | ############################################################################# | ||||
62 | |||||
63 | # spent 15.3ms within Mail::SpamAssassin::AsyncLoop::new which was called 470 times, avg 33µs/call:
# 470 times (15.3ms+0s) by Mail::SpamAssassin::PerMsgStatus::new at line 279 of Mail/SpamAssassin/PerMsgStatus.pm, avg 33µs/call | ||||
64 | # called from PerMsgStatus, a new AsyncLoop object is created | ||||
65 | # for each new message processing | ||||
66 | 470 | 1.16ms | my $class = shift; | ||
67 | 470 | 1.17ms | $class = ref($class) || $class; | ||
68 | |||||
69 | 470 | 1.05ms | my ($main) = @_; | ||
70 | 470 | 7.47ms | my $self = { | ||
71 | main => $main, | ||||
72 | queries_started => 0, | ||||
73 | queries_completed => 0, | ||||
74 | total_queries_started => 0, | ||||
75 | total_queries_completed => 0, | ||||
76 | pending_lookups => { }, | ||||
77 | timing_by_query => { }, | ||||
78 | all_lookups => { }, # keyed by "rr_type/domain" | ||||
79 | }; | ||||
80 | |||||
81 | 470 | 1.36ms | bless ($self, $class); | ||
82 | 470 | 3.87ms | $self; | ||
83 | } | ||||
84 | |||||
85 | # Given a domain name, produces a listref of successively stripped down | ||||
86 | # parent domains, e.g. a domain '2.10.Example.COM' would produce a list: | ||||
87 | # '2.10.example.com', '10.example.com', 'example.com', 'com', '' | ||||
88 | # | ||||
89 | sub domain_to_search_list { | ||||
90 | my ($domain) = @_; | ||||
91 | $domain =~ s/^\.+//; $domain =~ s/\.+\z//; # strip leading and trailing dots | ||||
92 | my @search_keys; | ||||
93 | if ($domain =~ /\[/) { # don't split address literals | ||||
94 | @search_keys = ( $domain, '' ); # presumably an address literal | ||||
95 | } else { | ||||
96 | local $1; | ||||
97 | $domain = lc $domain; | ||||
98 | for (;;) { | ||||
99 | push(@search_keys, $domain); | ||||
100 | last if $domain eq ''; | ||||
101 | # strip one level | ||||
102 | $domain = ($domain =~ /^ (?: [^.]* ) \. (.*) \z/xs) ? $1 : ''; | ||||
103 | } | ||||
104 | if (@search_keys > 20) { # enforce some sanity limit | ||||
105 | @search_keys = @search_keys[$#search_keys-19 .. $#search_keys]; | ||||
106 | } | ||||
107 | } | ||||
108 | return \@search_keys; | ||||
109 | } | ||||
110 | |||||
111 | # --------------------------------------------------------------------------- | ||||
112 | |||||
113 | =item $ent = $async->start_lookup($ent, $master_deadline) | ||||
114 | |||||
115 | Register the start of a long-running asynchronous lookup operation. | ||||
116 | C<$ent> is a hash reference containing the following items: | ||||
117 | |||||
118 | =over 4 | ||||
119 | |||||
120 | =item key (required) | ||||
121 | |||||
122 | A key string, unique to this lookup. This is what is reported in | ||||
123 | debug messages, used as the key for C<get_lookup()>, etc. | ||||
124 | |||||
125 | =item id (required) | ||||
126 | |||||
127 | An ID string, also unique to this lookup. Typically, this is the DNS packet ID | ||||
128 | as returned by DnsResolver's C<bgsend> method. Sadly, the Net::DNS | ||||
129 | architecture forces us to keep a separate ID string for this task instead of | ||||
130 | reusing C<key> -- if you are not using DNS lookups through DnsResolver, it | ||||
131 | should be OK to just reuse C<key>. | ||||
132 | |||||
133 | =item type (required) | ||||
134 | |||||
135 | A string, typically one word, used to describe the type of lookup in log | ||||
136 | messages, such as C<DNSBL>, C<MX>, C<TXT>. | ||||
137 | |||||
138 | =item zone (optional) | ||||
139 | |||||
140 | A zone specification (typically a DNS zone name - e.g. host, domain, or RBL) | ||||
141 | which may be used as a key to look up per-zone settings. No semantics on this | ||||
142 | parameter is imposed by this module. Currently used to fetch by-zone timeouts. | ||||
143 | |||||
144 | =item timeout_initial (optional) | ||||
145 | |||||
146 | An initial value of elapsed time for which we are willing to wait for a | ||||
147 | response (time in seconds, floating point value is allowed). When elapsed | ||||
148 | time since a query started exceeds the timeout value and there are no other | ||||
149 | queries to wait for, the query is aborted. The actual timeout value ranges | ||||
150 | from timeout_initial and gradually approaches timeout_min (see next parameter) | ||||
151 | as the number of already completed queries approaches the number of all | ||||
152 | queries started. | ||||
153 | |||||
154 | If a caller does not explicitly provide this parameter or its value is | ||||
155 | undefined, a default initial timeout value is settable by a configuration | ||||
156 | variable rbl_timeout. | ||||
157 | |||||
158 | If a value of the timeout_initial parameter is below timeout_min, the initial | ||||
159 | timeout is set to timeout_min. | ||||
160 | |||||
161 | =item timeout_min (optional) | ||||
162 | |||||
163 | A lower bound (in seconds) to which the actual timeout approaches as the | ||||
164 | number of queries completed approaches the number of all queries started. | ||||
165 | Defaults to 0.2 * timeout_initial. | ||||
166 | |||||
167 | =back | ||||
168 | |||||
169 | C<$ent> is returned by this method, with its contents augmented by additional | ||||
170 | information. | ||||
171 | |||||
172 | =cut | ||||
173 | |||||
174 | # spent 392ms (357+35.0) within Mail::SpamAssassin::AsyncLoop::start_lookup which was called 1968 times, avg 199µs/call:
# 1968 times (357ms+35.0ms) by Mail::SpamAssassin::AsyncLoop::bgsend_and_start_lookup at line 359, avg 199µs/call | ||||
175 | 1968 | 4.22ms | my ($self, $ent, $master_deadline) = @_; | ||
176 | |||||
177 | 1968 | 5.93ms | my $id = $ent->{id}; | ||
178 | 1968 | 5.07ms | my $key = $ent->{key}; | ||
179 | 1968 | 5.33ms | defined $id && $id ne '' or die "oops, no id"; | ||
180 | 1968 | 4.01ms | $key or die "oops, no key"; | ||
181 | 1968 | 4.23ms | $ent->{type} or die "oops, no type"; | ||
182 | |||||
183 | 1968 | 34.5ms | 1968 | 12.6ms | my $now = time; # spent 12.6ms making 1968 calls to Time::HiRes::time, avg 6µs/call |
184 | 1968 | 5.91ms | $ent->{start_time} = $now if !defined $ent->{start_time}; | ||
185 | |||||
186 | # are there any applicable per-zone settings? | ||||
187 | 1968 | 5.18ms | my $zone = $ent->{zone}; | ||
188 | 1968 | 3.43ms | my $settings; # a ref to a by-zone or to global settings | ||
189 | 1968 | 6.39ms | my $conf_by_zone = $self->{main}->{conf}->{by_zone}; | ||
190 | 1968 | 3.81ms | if (defined $zone && $conf_by_zone) { | ||
191 | # dbg("async: searching for by_zone settings for $zone"); | ||||
192 | $zone =~ s/^\.//; $zone =~ s/\.\z//; # strip leading and trailing dot | ||||
193 | for (;;) { # 2.10.example.com, 10.example.com, example.com, com, '' | ||||
194 | if (exists $conf_by_zone->{$zone}) { | ||||
195 | $settings = $conf_by_zone->{$zone}; | ||||
196 | last; | ||||
197 | } elsif ($zone eq '') { | ||||
198 | last; | ||||
199 | } else { # strip one level, careful with address literals | ||||
200 | $zone = ($zone =~ /^( (?: [^.] | \[ (?: \\. | [^\]\\] )* \] )* ) | ||||
201 | \. (.*) \z/xs) ? $2 : ''; | ||||
202 | } | ||||
203 | } | ||||
204 | } | ||||
205 | |||||
206 | 1968 | 3.64ms | dbg("async: applying by_zone settings for %s", $zone) if $settings; | ||
207 | |||||
208 | 1968 | 4.11ms | my $t_init = $ent->{timeout_initial}; # application-specified has precedence | ||
209 | 1968 | 3.26ms | $t_init = $settings->{rbl_timeout} if $settings && !defined $t_init; | ||
210 | 1968 | 5.79ms | $t_init = $self->{main}->{conf}->{rbl_timeout} if !defined $t_init; | ||
211 | 1968 | 3.36ms | $t_init = 0 if !defined $t_init; # last-resort default, just in case | ||
212 | |||||
213 | 1968 | 4.08ms | my $t_end = $ent->{timeout_min}; # application-specified has precedence | ||
214 | 1968 | 3.31ms | $t_end = $settings->{rbl_timeout_min} if $settings && !defined $t_end; | ||
215 | 1968 | 5.06ms | $t_end = $self->{main}->{conf}->{rbl_timeout_min} if !defined $t_end; # added for bug 7070 | ||
216 | 1968 | 5.76ms | $t_end = 0.2 * $t_init if !defined $t_end; | ||
217 | 1968 | 4.55ms | $t_end = 0 if $t_end < 0; # just in case | ||
218 | 1968 | 3.44ms | $t_init = $t_end if $t_init < $t_end; | ||
219 | |||||
220 | 1968 | 3.95ms | my $clipped_by_master_deadline = 0; | ||
221 | 1968 | 6.83ms | if (defined $master_deadline) { | ||
222 | 1968 | 36.6ms | 1968 | 9.21ms | my $time_avail = $master_deadline - time; # spent 9.21ms making 1968 calls to Time::HiRes::time, avg 5µs/call |
223 | 1968 | 3.79ms | $time_avail = 0.5 if $time_avail < 0.5; # give some slack | ||
224 | 1968 | 4.45ms | if ($t_init > $time_avail) { | ||
225 | $t_init = $time_avail; $clipped_by_master_deadline = 1; | ||||
226 | $t_end = $time_avail if $t_end > $time_avail; | ||||
227 | } | ||||
228 | } | ||||
229 | 1968 | 7.89ms | $ent->{timeout_initial} = $t_init; | ||
230 | 1968 | 10.8ms | $ent->{timeout_min} = $t_end; | ||
231 | |||||
232 | $ent->{display_id} = # identifies entry in debug logging and similar | ||||
233 | 9840 | 26.2ms | join(", ", grep { defined } | ||
234 | 11808 | 87.2ms | map { ref $ent->{$_} ? @{$ent->{$_}} : $ent->{$_} } | ||
235 | qw(sets rules rulename type key) ); | ||||
236 | |||||
237 | 1968 | 13.8ms | $self->{pending_lookups}->{$key} = $ent; | ||
238 | |||||
239 | 1968 | 4.01ms | $self->{queries_started}++; | ||
240 | 1968 | 3.82ms | $self->{total_queries_started}++; | ||
241 | dbg("async: starting: %s (timeout %.1fs, min %.1fs)%s", | ||||
242 | $ent->{display_id}, $ent->{timeout_initial}, $ent->{timeout_min}, | ||||
243 | 1968 | 14.9ms | 1968 | 13.2ms | !$clipped_by_master_deadline ? '' : ', capped by time limit'); # spent 13.2ms making 1968 calls to Mail::SpamAssassin::Logger::dbg, avg 7µs/call |
244 | |||||
245 | 1968 | 20.9ms | $ent; | ||
246 | } | ||||
247 | |||||
248 | # --------------------------------------------------------------------------- | ||||
249 | |||||
250 | =item $ent = $async->bgsend_and_start_lookup($domain, $type, $class, $ent, $cb, %options) | ||||
251 | |||||
252 | A common idiom: calls C<bgsend>, followed by a call to C<start_lookup>, | ||||
253 | returning the argument $ent object as modified by C<start_lookup> and | ||||
254 | filled-in with a query ID. | ||||
255 | |||||
256 | =cut | ||||
257 | |||||
258 | # spent 6.88s (1.23+5.65) within Mail::SpamAssassin::AsyncLoop::bgsend_and_start_lookup which was called 8743 times, avg 787µs/call:
# 8043 times (1.14s+3.89s) by Mail::SpamAssassin::Plugin::URIDNSBL::lookup_single_dnsbl at line 1071 of Mail/SpamAssassin/Plugin/URIDNSBL.pm, avg 625µs/call
# 383 times (52.0ms+960ms) by Mail::SpamAssassin::Plugin::URIDNSBL::lookup_a_record at line 999 of Mail/SpamAssassin/Plugin/URIDNSBL.pm, avg 2.64ms/call
# 317 times (43.6ms+796ms) by Mail::SpamAssassin::Plugin::URIDNSBL::lookup_domain_ns at line 914 of Mail/SpamAssassin/Plugin/URIDNSBL.pm, avg 2.65ms/call | ||||
259 | 8743 | 65.7ms | my($self, $domain, $type, $class, $ent, $cb, %options) = @_; | ||
260 | 8743 | 14.6ms | $ent = {} if !$ent; | ||
261 | 8743 | 186ms | 8743 | 59.0ms | $domain =~ s/\.+\z//s; # strip trailing dots, these sometimes still sneak in # spent 59.0ms making 8743 calls to Mail::SpamAssassin::AsyncLoop::CORE:subst, avg 7µs/call |
262 | 8743 | 23.5ms | $ent->{id} = undef; | ||
263 | 8743 | 32.3ms | $ent->{query_type} = $type; | ||
264 | 8743 | 71.4ms | $ent->{query_domain} = $domain; | ||
265 | 8743 | 15.6ms | $ent->{type} = $type if !exists $ent->{type}; | ||
266 | 8743 | 15.5ms | $cb = $ent->{completed_callback} if !$cb; # compatibility with SA < 3.4 | ||
267 | |||||
268 | 8743 | 59.7ms | my $key = $ent->{key} || ''; | ||
269 | |||||
270 | 8743 | 65.1ms | my $dnskey = uc($type) . '/' . lc($domain); | ||
271 | 8743 | 22.0ms | my $dns_query_info = $self->{all_lookups}{$dnskey}; | ||
272 | |||||
273 | 8743 | 30.5ms | if ($dns_query_info) { # DNS query already underway or completed | ||
274 | 6775 | 65.1ms | my $id = $ent->{id} = $dns_query_info->{id}; # re-use existing query | ||
275 | 6775 | 11.3ms | return if !defined $id; # presumably blocked, or other fatal failure | ||
276 | 13550 | 177ms | 6775 | 68.4ms | my $id_tail = $id; $id_tail =~ s{^\d+/IN/}{}; # spent 68.4ms making 6775 calls to Mail::SpamAssassin::AsyncLoop::CORE:subst, avg 10µs/call |
277 | 6775 | 18.7ms | lc($id_tail) eq lc($dnskey) | ||
278 | or info("async: unmatched id %s, key=%s", $id, $dnskey); | ||||
279 | |||||
280 | 6775 | 13.9ms | my $pkt = $dns_query_info->{pkt}; | ||
281 | 6775 | 30.4ms | if (!$pkt) { # DNS query underway, still waiting for results | ||
282 | # just add our query to the existing one | ||||
283 | 13550 | 75.2ms | push(@{$dns_query_info->{applicants}}, [$ent,$cb]); | ||
284 | dbg("async: query %s already underway, adding no.%d %s", | ||||
285 | 6775 | 12.3ms | $id, scalar @{$dns_query_info->{applicants}}, | ||
286 | 6775 | 73.2ms | 6775 | 58.6ms | $ent->{rulename} || $key); # spent 58.6ms making 6775 calls to Mail::SpamAssassin::Logger::dbg, avg 9µs/call |
287 | |||||
288 | } else { # DNS query already completed, re-use results | ||||
289 | # answer already known, just do the callback and be done with it | ||||
290 | if (!$cb) { | ||||
291 | dbg("async: query %s already done, re-using for %s", $id, $key); | ||||
292 | } else { | ||||
293 | dbg("async: query %s already done, re-using for %s, callback", | ||||
294 | $id, $key); | ||||
295 | eval { | ||||
296 | $cb->($ent, $pkt); 1; | ||||
297 | } or do { | ||||
298 | chomp $@; | ||||
299 | # resignal if alarm went off | ||||
300 | die "async: (1) $@\n" if $@ =~ /__alarm__ignore__\(.*\)/s; | ||||
301 | warn sprintf("query %s completed, callback %s failed: %s\n", | ||||
302 | $id, $key, $@); | ||||
303 | }; | ||||
304 | } | ||||
305 | } | ||||
306 | } | ||||
307 | |||||
308 | else { # no existing query, open a new DNS query | ||||
309 | 1968 | 20.8ms | $dns_query_info = $self->{all_lookups}{$dnskey} = {}; # new query needed | ||
310 | 1968 | 3.30ms | my($id, $blocked); | ||
311 | 1968 | 6.80ms | my $dns_query_blockages = $self->{main}->{conf}->{dns_query_blocked}; | ||
312 | 1968 | 3.26ms | if ($dns_query_blockages) { | ||
313 | my $search_list = domain_to_search_list($domain); | ||||
314 | foreach my $parent_domain (@$search_list) { | ||||
315 | $blocked = $dns_query_blockages->{$parent_domain}; | ||||
316 | last if defined $blocked; # stop at first defined, can be true or false | ||||
317 | } | ||||
318 | } | ||||
319 | 1968 | 7.63ms | if ($blocked) { | ||
320 | dbg("async: blocked by dns_query_restriction: %s", $dnskey); | ||||
321 | } else { | ||||
322 | 1968 | 14.4ms | 1968 | 13.8ms | dbg("async: launching %s for %s", $dnskey, $key); # spent 13.8ms making 1968 calls to Mail::SpamAssassin::Logger::dbg, avg 7µs/call |
323 | $id = $self->{main}->{resolver}->bgsend($domain, $type, $class, sub { | ||||
324 | my($pkt, $pkt_id, $timestamp) = @_; | ||||
325 | # this callback sub is called from DnsResolver::poll_responses() | ||||
326 | # dbg("async: in a bgsend_and_start_lookup callback, id %s", $pkt_id); | ||||
327 | if ($pkt_id ne $id) { | ||||
328 | warn "async: mismatched dns id: got $pkt_id, expected $id\n"; | ||||
329 | return; | ||||
330 | } | ||||
331 | $self->set_response_packet($pkt_id, $pkt, $ent->{key}, $timestamp); | ||||
332 | $dns_query_info->{pkt} = $pkt; | ||||
333 | my $cb_count = 0; | ||||
334 | foreach my $tuple (@{$dns_query_info->{applicants}}) { | ||||
335 | my($appl_ent, $appl_cb) = @$tuple; | ||||
336 | if ($appl_cb) { | ||||
337 | dbg("async: calling callback on key %s%s", $key, | ||||
338 | !defined $appl_ent->{rulename} ? '' | ||||
339 | : ", rule ".$appl_ent->{rulename}); | ||||
340 | $cb_count++; | ||||
341 | eval { | ||||
342 | $appl_cb->($appl_ent, $pkt); 1; | ||||
343 | } or do { | ||||
344 | chomp $@; | ||||
345 | # resignal if alarm went off | ||||
346 | die "async: (2) $@\n" if $@ =~ /__alarm__ignore__\(.*\)/s; | ||||
347 | warn sprintf("query %s completed, callback %s failed: %s\n", | ||||
348 | $id, $appl_ent->{key}, $@); | ||||
349 | }; | ||||
350 | } | ||||
351 | } | ||||
352 | delete $dns_query_info->{applicants}; | ||||
353 | dbg("async: query $id completed, no callbacks run") if !$cb_count; | ||||
354 | 1968 | 45.7ms | 1968 | 5.06s | }); # spent 5.06s making 1968 calls to Mail::SpamAssassin::DnsResolver::bgsend, avg 2.57ms/call |
355 | } | ||||
356 | 1968 | 4.09ms | return if !defined $id; | ||
357 | 1968 | 8.64ms | $dns_query_info->{id} = $ent->{id} = $id; | ||
358 | 3936 | 15.9ms | push(@{$dns_query_info->{applicants}}, [$ent,$cb]); | ||
359 | 1968 | 17.4ms | 1968 | 392ms | $self->start_lookup($ent, $options{master_deadline}); # spent 392ms making 1968 calls to Mail::SpamAssassin::AsyncLoop::start_lookup, avg 199µs/call |
360 | } | ||||
361 | 8743 | 188ms | return $ent; | ||
362 | } | ||||
363 | |||||
364 | # --------------------------------------------------------------------------- | ||||
365 | |||||
366 | =item $ent = $async->get_lookup($key) | ||||
367 | |||||
368 | Retrieve the pending-lookup object for the given key C<$key>. | ||||
369 | |||||
370 | If the lookup is complete, this will return C<undef>. | ||||
371 | |||||
372 | Note that a lookup is still considered "pending" until C<complete_lookups()> is | ||||
373 | called, even if it has been reported as complete via C<set_response_packet()>. | ||||
374 | |||||
375 | =cut | ||||
376 | |||||
377 | sub get_lookup { | ||||
378 | my ($self, $key) = @_; | ||||
379 | return $self->{pending_lookups}->{$key}; | ||||
380 | } | ||||
381 | |||||
382 | # --------------------------------------------------------------------------- | ||||
383 | |||||
384 | =item $async->log_lookups_timing() | ||||
385 | |||||
386 | Log sorted timing for all completed lookups. | ||||
387 | |||||
388 | =cut | ||||
389 | |||||
390 | sub log_lookups_timing { | ||||
391 | my ($self) = @_; | ||||
392 | my $timings = $self->{timing_by_query}; | ||||
393 | for my $key (sort { $timings->{$a} <=> $timings->{$b} } keys %$timings) { | ||||
394 | dbg("async: timing: %.3f %s", $timings->{$key}, $key); | ||||
395 | } | ||||
396 | } | ||||
397 | |||||
398 | # --------------------------------------------------------------------------- | ||||
399 | |||||
400 | =item $alldone = $async->complete_lookups() | ||||
401 | |||||
402 | Perform a poll of the pending lookups, to see if any are completed. | ||||
403 | Callbacks on completed queries will be called from poll_responses(). | ||||
404 | |||||
405 | If there are no lookups remaining, or if too much time has elapsed since | ||||
406 | any results were returned, C<1> is returned, otherwise C<0>. | ||||
407 | |||||
408 | =cut | ||||
409 | |||||
410 | sub complete_lookups { | ||||
411 | my ($self, $timeout, $allow_aborting_of_expired) = @_; | ||||
412 | my $alldone = 0; | ||||
413 | my $anydone = 0; | ||||
414 | my $allexpired = 1; | ||||
415 | my %typecount; | ||||
416 | |||||
417 | my $pending = $self->{pending_lookups}; | ||||
418 | $self->{queries_started} = 0; | ||||
419 | $self->{queries_completed} = 0; | ||||
420 | |||||
421 | my $now = time; | ||||
422 | |||||
423 | if (defined $timeout && $timeout > 0 && | ||||
424 | %$pending && $self->{total_queries_started} > 0) | ||||
425 | { | ||||
426 | # shrink a 'select' timeout if a caller specified unnecessarily long | ||||
427 | # value beyond the latest deadline of any outstanding request; | ||||
428 | # can save needless wait time (up to 1 second in harvest_dnsbl_queries) | ||||
429 | my $r = $self->{total_queries_completed} / $self->{total_queries_started}; | ||||
430 | my $r2 = $r * $r; # 0..1 | ||||
431 | my $max_deadline; | ||||
432 | while (my($key,$ent) = each %$pending) { | ||||
433 | my $t_init = $ent->{timeout_initial}; | ||||
434 | my $dt = $t_init - ($t_init - $ent->{timeout_min}) * $r2; | ||||
435 | my $deadline = $ent->{start_time} + $dt; | ||||
436 | $max_deadline = $deadline if !defined $max_deadline || | ||||
437 | $deadline > $max_deadline; | ||||
438 | } | ||||
439 | if (defined $max_deadline) { | ||||
440 | # adjust to timer resolution, only deals with 1s and with fine resolution | ||||
441 | $max_deadline = 1 + int $max_deadline | ||||
442 | if $timer_resolution == 1 && $max_deadline > int $max_deadline; | ||||
443 | my $sufficient_timeout = $max_deadline - $now; | ||||
444 | $sufficient_timeout = 0 if $sufficient_timeout < 0; | ||||
445 | if ($timeout > $sufficient_timeout) { | ||||
446 | dbg("async: reducing select timeout from %.1f to %.1f s", | ||||
447 | $timeout, $sufficient_timeout); | ||||
448 | $timeout = $sufficient_timeout; | ||||
449 | } | ||||
450 | } | ||||
451 | } | ||||
452 | |||||
453 | # trap this loop in an eval { } block, as Net::DNS could throw | ||||
454 | # die()s our way; in particular, process_dnsbl_results() has | ||||
455 | # thrown die()s before (bug 3794). | ||||
456 | eval { | ||||
457 | |||||
458 | if (%$pending) { # any outstanding requests still? | ||||
459 | $self->{last_poll_responses_time} = $now; | ||||
460 | my $nfound = $self->{main}->{resolver}->poll_responses($timeout); | ||||
461 | dbg("async: select found %s responses ready (t.o.=%.1f)", | ||||
462 | !$nfound ? 'no' : $nfound, $timeout); | ||||
463 | } | ||||
464 | $now = time; # capture new timestamp, after possible sleep in 'select' | ||||
465 | |||||
466 | # A callback routine may generate another DNS query, which may insert | ||||
467 | # an entry into the %$pending hash thus invalidating the each() context. | ||||
468 | # So, make sure that callbacks are not called while the each() context | ||||
469 | # is open. [Bug 6937] | ||||
470 | # | ||||
471 | while (my($key,$ent) = each %$pending) { | ||||
472 | my $id = $ent->{id}; | ||||
473 | if (exists $self->{finished}->{$id}) { | ||||
474 | delete $self->{finished}->{$id}; | ||||
475 | $anydone = 1; | ||||
476 | $ent->{finish_time} = $now if !defined $ent->{finish_time}; | ||||
477 | my $elapsed = $ent->{finish_time} - $ent->{start_time}; | ||||
478 | dbg("async: completed in %.3f s: %s", $elapsed, $ent->{display_id}); | ||||
479 | $self->{timing_by_query}->{". $key"} += $elapsed; | ||||
480 | $self->{queries_completed}++; | ||||
481 | $self->{total_queries_completed}++; | ||||
482 | delete $pending->{$key}; | ||||
483 | } | ||||
484 | } | ||||
485 | |||||
486 | if (%$pending) { # still any requests outstanding? are they expired? | ||||
487 | my $r = | ||||
488 | !$allow_aborting_of_expired || !$self->{total_queries_started} ? 1.0 | ||||
489 | : $self->{total_queries_completed} / $self->{total_queries_started}; | ||||
490 | my $r2 = $r * $r; # 0..1 | ||||
491 | while (my($key,$ent) = each %$pending) { | ||||
492 | $typecount{$ent->{type}}++; | ||||
493 | my $t_init = $ent->{timeout_initial}; | ||||
494 | my $dt = $t_init - ($t_init - $ent->{timeout_min}) * $r2; | ||||
495 | # adjust to timer resolution, only deals with 1s and fine resolution | ||||
496 | $dt = 1 + int $dt if $timer_resolution == 1 && $dt > int $dt; | ||||
497 | $allexpired = 0 if $now <= $ent->{start_time} + $dt; | ||||
498 | } | ||||
499 | dbg("async: queries completed: %d, started: %d", | ||||
500 | $self->{queries_completed}, $self->{queries_started}); | ||||
501 | } | ||||
502 | |||||
503 | # ensure we don't get stuck if a request gets lost in the ether. | ||||
504 | if (! %$pending) { | ||||
505 | $alldone = 1; | ||||
506 | } | ||||
507 | elsif ($allexpired && $allow_aborting_of_expired) { | ||||
508 | # avoid looping forever if we haven't got all results. | ||||
509 | dbg("async: escaping: lost or timed out requests or responses"); | ||||
510 | $self->abort_remaining_lookups(); | ||||
511 | $alldone = 1; | ||||
512 | } | ||||
513 | else { | ||||
514 | dbg("async: queries active: %s%s at %s", | ||||
515 | join (' ', map { "$_=$typecount{$_}" } sort keys %typecount), | ||||
516 | $allexpired ? ', all expired' : '', scalar(localtime(time))); | ||||
517 | $alldone = 0; | ||||
518 | } | ||||
519 | 1; | ||||
520 | |||||
521 | } or do { | ||||
522 | my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; | ||||
523 | # resignal if alarm went off | ||||
524 | die "async: (3) $eval_stat\n" if $eval_stat =~ /__alarm__ignore__\(.*\)/s; | ||||
525 | dbg("async: caught complete_lookups death, aborting: %s", $eval_stat); | ||||
526 | $alldone = 1; # abort remaining | ||||
527 | }; | ||||
528 | |||||
529 | return wantarray ? ($alldone,$anydone) : $alldone; | ||||
530 | } | ||||
531 | |||||
532 | # --------------------------------------------------------------------------- | ||||
533 | |||||
534 | =item $async->abort_remaining_lookups() | ||||
535 | |||||
536 | Abort any remaining lookups. | ||||
537 | |||||
538 | =cut | ||||
539 | |||||
540 | sub abort_remaining_lookups { | ||||
541 | my ($self) = @_; | ||||
542 | |||||
543 | my $pending = $self->{pending_lookups}; | ||||
544 | my $foundcnt = 0; | ||||
545 | my $now = time; | ||||
546 | |||||
547 | while (my($key,$ent) = each %$pending) { | ||||
548 | dbg("async: aborting after %.3f s, %s: %s", | ||||
549 | $now - $ent->{start_time}, | ||||
550 | (defined $ent->{timeout_initial} && | ||||
551 | $now > $ent->{start_time} + $ent->{timeout_initial} | ||||
552 | ? 'past original deadline' : 'deadline shrunk'), | ||||
553 | $ent->{display_id} ); | ||||
554 | $foundcnt++; | ||||
555 | $self->{timing_by_query}->{"X $key"} = $now - $ent->{start_time}; | ||||
556 | $ent->{finish_time} = $now if !defined $ent->{finish_time}; | ||||
557 | delete $pending->{$key}; | ||||
558 | } | ||||
559 | |||||
560 | # call any remaining callbacks, indicating the query has been aborted | ||||
561 | # | ||||
562 | my $all_lookups_ref = $self->{all_lookups}; | ||||
563 | foreach my $dnskey (keys %$all_lookups_ref) { | ||||
564 | my $dns_query_info = $all_lookups_ref->{$dnskey}; | ||||
565 | my $cb_count = 0; | ||||
566 | foreach my $tuple (@{$dns_query_info->{applicants}}) { | ||||
567 | my($ent, $cb) = @$tuple; | ||||
568 | if ($cb) { | ||||
569 | dbg("async: calling callback/abort on key %s%s", $dnskey, | ||||
570 | !defined $ent->{rulename} ? '' : ", rule ".$ent->{rulename}); | ||||
571 | $cb_count++; | ||||
572 | eval { | ||||
573 | $cb->($ent, undef); 1; | ||||
574 | } or do { | ||||
575 | chomp $@; | ||||
576 | # resignal if alarm went off | ||||
577 | die "async: (2) $@\n" if $@ =~ /__alarm__ignore__\(.*\)/s; | ||||
578 | warn sprintf("query %s aborted, callback %s failed: %s\n", | ||||
579 | $dnskey, $ent->{key}, $@); | ||||
580 | }; | ||||
581 | } | ||||
582 | dbg("async: query $dnskey aborted, no callbacks run") if !$cb_count; | ||||
583 | } | ||||
584 | delete $dns_query_info->{applicants}; | ||||
585 | } | ||||
586 | |||||
587 | dbg("async: aborted %d remaining lookups", $foundcnt) if $foundcnt > 0; | ||||
588 | delete $self->{last_poll_responses_time}; | ||||
589 | $self->{main}->{resolver}->bgabort(); | ||||
590 | 1; | ||||
591 | } | ||||
592 | |||||
593 | # --------------------------------------------------------------------------- | ||||
594 | |||||
595 | =item $async->set_response_packet($id, $pkt, $key, $timestamp) | ||||
596 | |||||
597 | Register a "response packet" for a given query. C<$id> is the ID for the | ||||
598 | query, and must match the C<id> supplied in C<start_lookup()>. C<$pkt> is the | ||||
599 | packet object for the response. A parameter C<$key> identifies an entry in a | ||||
600 | hash %{$self->{pending_lookups}} where the object which spawned this query can | ||||
601 | be found, and through which futher information about the query is accessible. | ||||
602 | |||||
603 | C<$pkt> may be undef, indicating that no response packet is available, but a | ||||
604 | query has completed (e.g. was aborted or dismissed) and is no longer "pending". | ||||
605 | |||||
606 | The DNS resolver's response packet C<$pkt> will be made available to a callback | ||||
607 | subroutine through its argument as well as in C<$ent-<gt>{response_packet}>. | ||||
608 | |||||
609 | =cut | ||||
610 | |||||
611 | sub set_response_packet { | ||||
612 | my ($self, $id, $pkt, $key, $timestamp) = @_; | ||||
613 | $self->{finished}->{$id} = 1; # only key existence matters, any value | ||||
614 | $timestamp = time if !defined $timestamp; | ||||
615 | my $pending = $self->{pending_lookups}; | ||||
616 | if (!defined $key) { # backward compatibility with 3.2.3 and older plugins | ||||
617 | # a third-party plugin did not provide $key in a call, search for it: | ||||
618 | if ($id eq $pending->{$id}->{id}) { # I feel lucky, key==id ? | ||||
619 | $key = $id; | ||||
620 | } else { # then again, maybe not, be more systematic | ||||
621 | for my $tkey (keys %$pending) { | ||||
622 | if ($id eq $pending->{$tkey}->{id}) { $key = $tkey; last } | ||||
623 | } | ||||
624 | } | ||||
625 | dbg("async: got response on id $id, search found key $key"); | ||||
626 | } | ||||
627 | if (!defined $key) { | ||||
628 | info("async: no key, response packet not remembered, id $id"); | ||||
629 | } else { | ||||
630 | my $ent = $pending->{$key}; | ||||
631 | my $ent_id = $ent->{id}; | ||||
632 | if (!defined $ent_id) { | ||||
633 | # should not happen, troubleshooting | ||||
634 | info("async: ignoring response, id %s, ent_id is undef: %s", | ||||
635 | $id, join(', ', %$ent)); | ||||
636 | } elsif ($id ne $ent_id) { | ||||
637 | info("async: ignoring response, mismatched id $id, expected $ent_id"); | ||||
638 | } else { | ||||
639 | $ent->{finish_time} = $timestamp; | ||||
640 | $ent->{response_packet} = $pkt; | ||||
641 | } | ||||
642 | } | ||||
643 | 1; | ||||
644 | } | ||||
645 | |||||
646 | =item $async->report_id_complete($id,$key,$key,$timestamp) | ||||
647 | |||||
648 | Legacy. Equivalent to $self->set_response_packet($id,undef,$key,$timestamp), | ||||
649 | i.e. providing undef as a response packet. Register that a query has | ||||
650 | completed and is no longer "pending". C<$id> is the ID for the query, | ||||
651 | and must match the C<id> supplied in C<start_lookup()>. | ||||
652 | |||||
653 | One or the other of C<set_response_packet()> or C<report_id_complete()> | ||||
654 | should be called, but not both. | ||||
655 | |||||
656 | =cut | ||||
657 | |||||
658 | sub report_id_complete { | ||||
659 | my ($self, $id, $key, $timestamp) = @_; | ||||
660 | $self->set_response_packet($id, undef, $key, $timestamp); | ||||
661 | } | ||||
662 | |||||
663 | # --------------------------------------------------------------------------- | ||||
664 | |||||
665 | =item $time = $async->last_poll_responses_time() | ||||
666 | |||||
667 | Get the time of the last call to C<poll_responses()> (which is called | ||||
668 | from C<complete_lookups()>. If C<poll_responses()> was never called or | ||||
669 | C<abort_remaining_lookups()> has been called C<last_poll_responses_time()> | ||||
670 | will return undef. | ||||
671 | |||||
672 | =cut | ||||
673 | |||||
674 | sub last_poll_responses_time { | ||||
675 | my ($self) = @_; | ||||
676 | return $self->{last_poll_responses_time}; | ||||
677 | } | ||||
678 | |||||
679 | 1 | 10µs | 1; | ||
680 | |||||
681 | =back | ||||
682 | |||||
683 | =cut | ||||
# spent 127ms within Mail::SpamAssassin::AsyncLoop::CORE:subst which was called 15518 times, avg 8µs/call:
# 8743 times (59.0ms+0s) by Mail::SpamAssassin::AsyncLoop::bgsend_and_start_lookup at line 261, avg 7µs/call
# 6775 times (68.4ms+0s) by Mail::SpamAssassin::AsyncLoop::bgsend_and_start_lookup at line 276, avg 10µs/call |