sitcod3 commited on
Commit
03c7b2c
·
verified ·
1 Parent(s): 1a43824

Upload folder using huggingface_hub

Browse files
app/validator/mailbox_provider.py CHANGED
@@ -178,7 +178,11 @@ class AutonomousSmtpProvider(MailboxProvider):
178
  pairs = [(e, mx) for e, mx in pairs if e and mx]
179
  if not pairs:
180
  return {}, {}
181
- results = self.batch.verify_batch(pairs)
 
 
 
 
182
  domains = {e.rsplit("@", 1)[-1].lower(): mx for e, mx in pairs}
183
  catch_all = {
184
  domain: self.batch.catch_all_cache.get(domain) for domain in domains
 
178
  pairs = [(e, mx) for e, mx in pairs if e and mx]
179
  if not pairs:
180
  return {}, {}
181
+ # Prefer the concurrent path when the batch engine supports it.
182
+ if hasattr(self.batch, "verify_batch_async"):
183
+ results = await self.batch.verify_batch_async(pairs)
184
+ else:
185
+ results = self.batch.verify_batch(pairs)
186
  domains = {e.rsplit("@", 1)[-1].lower(): mx for e, mx in pairs}
187
  catch_all = {
188
  domain: self.batch.catch_all_cache.get(domain) for domain in domains
app/validator/mx_batch.py CHANGED
@@ -18,6 +18,7 @@ bulk job.
18
  """
19
  from __future__ import annotations
20
 
 
21
  import random
22
  import time
23
  from dataclasses import dataclass
@@ -81,6 +82,7 @@ class MxBatchVerifier:
81
  max_connections_per_host: int = 2,
82
  fail_threshold: int = 3,
83
  cooldown_seconds: int = 300,
 
84
  ):
85
  self.timeout = timeout
86
  self.helo_domain = helo_domain
@@ -96,6 +98,7 @@ class MxBatchVerifier:
96
  # `cooldown_seconds` and report rows unavailable immediately instead.
97
  self.fail_threshold = max(1, fail_threshold)
98
  self.cooldown_seconds = cooldown_seconds
 
99
  self._consecutive_failures = 0
100
  self._open_until = 0.0
101
 
@@ -123,22 +126,73 @@ class MxBatchVerifier:
123
  def verify_batch(
124
  self, pairs: Iterable[tuple[str, str]]
125
  ) -> dict[str, SMTPResult]:
126
- """Verify pairs of (email, mx_host). Returns email -> SMTPResult."""
127
- pairs = list(pairs)
128
- if not pairs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  return {}
130
 
131
- # Group by (domain, mx) — catch-all is a domain property, and one domain
132
- # maps to one MX host, so this is the natural session unit.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  groups: dict[tuple[str, str], list[str]] = {}
134
  for email, mx_host in pairs:
 
 
135
  domain = email.rsplit("@", 1)[-1].lower()
136
  groups.setdefault((domain, mx_host), []).append(email)
137
-
138
- results: dict[str, SMTPResult] = {}
139
- for (domain, mx_host), emails in groups.items():
140
- results.update(self._verify_domain(domain, mx_host, emails))
141
- return results
142
 
143
  def _verify_domain(
144
  self, domain: str, mx_host: str, emails: list[str]
 
18
  """
19
  from __future__ import annotations
20
 
21
+ import asyncio
22
  import random
23
  import time
24
  from dataclasses import dataclass
 
82
  max_connections_per_host: int = 2,
83
  fail_threshold: int = 3,
84
  cooldown_seconds: int = 300,
85
+ bulk_concurrency: int = 20,
86
  ):
87
  self.timeout = timeout
88
  self.helo_domain = helo_domain
 
98
  # `cooldown_seconds` and report rows unavailable immediately instead.
99
  self.fail_threshold = max(1, fail_threshold)
100
  self.cooldown_seconds = cooldown_seconds
101
+ self.bulk_concurrency = max(1, bulk_concurrency)
102
  self._consecutive_failures = 0
103
  self._open_until = 0.0
104
 
 
126
  def verify_batch(
127
  self, pairs: Iterable[tuple[str, str]]
128
  ) -> dict[str, SMTPResult]:
129
+ """Verify pairs of (email, mx_host), sequentially. Returns email -> result."""
130
+ groups = self._group(pairs)
131
+ results: dict[str, SMTPResult] = {}
132
+ for (domain, mx_host), emails in groups.items():
133
+ results.update(self._verify_domain(domain, mx_host, emails))
134
+ return results
135
+
136
+ async def verify_batch_async(
137
+ self, pairs: Iterable[tuple[str, str]]
138
+ ) -> dict[str, SMTPResult]:
139
+ """Verify many pairs with DOMAINS PROBED CONCURRENTLY.
140
+
141
+ Blocking SMTP work runs in a thread pool bounded by `bulk_concurrency`.
142
+ Sequential probing makes wall-clock the sum of every domain's latency
143
+ (timeout x domains on a blocked host); real bulk throughput comes from
144
+ parallelism across domains, not speed per domain.
145
+ """
146
+ groups = self._group(pairs)
147
+ if not groups:
148
  return {}
149
 
150
+ semaphore = asyncio.Semaphore(self.bulk_concurrency)
151
+
152
+ async def run_domain(domain: str, mx_host: str, emails: list[str]):
153
+ async with semaphore:
154
+ return await asyncio.to_thread(
155
+ self._verify_domain, domain, mx_host, emails
156
+ )
157
+
158
+ chunks = await asyncio.gather(
159
+ *(run_domain(d, m, e) for (d, m), e in groups.items()),
160
+ return_exceptions=True,
161
+ )
162
+ results: dict[str, SMTPResult] = {}
163
+ for chunk in chunks:
164
+ if isinstance(chunk, Exception):
165
+ continue
166
+ results.update(chunk)
167
+ # Any group that raised must still return a row per address.
168
+ missing = [
169
+ (d, m, e)
170
+ for (d, m), e in groups.items()
171
+ if not any(addr in results for addr in e)
172
+ ]
173
+ for domain, mx_host, emails in missing:
174
+ for email in emails:
175
+ results.setdefault(
176
+ email,
177
+ SMTPResult(
178
+ verified=None,
179
+ error=f"smtp_unreachable: {mx_host} (probe aborted)",
180
+ ),
181
+ )
182
+ return results
183
+
184
+ @staticmethod
185
+ def _group(
186
+ pairs: Iterable[tuple[str, str]]
187
+ ) -> dict[tuple[str, str], list[str]]:
188
+ """Group (email, mx_host) pairs by (domain, mx) — the session unit."""
189
  groups: dict[tuple[str, str], list[str]] = {}
190
  for email, mx_host in pairs:
191
+ if not email or not mx_host:
192
+ continue
193
  domain = email.rsplit("@", 1)[-1].lower()
194
  groups.setdefault((domain, mx_host), []).append(email)
195
+ return groups
 
 
 
 
196
 
197
  def _verify_domain(
198
  self, domain: str, mx_host: str, emails: list[str]
tests/test_mx_batch.py CHANGED
@@ -197,5 +197,74 @@ class EgressCircuitBreakerTests(unittest.TestCase):
197
  self.assertGreaterEqual(v.fail_threshold, 2)
198
 
199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  if __name__ == "__main__":
201
  unittest.main()
 
197
  self.assertGreaterEqual(v.fail_threshold, 2)
198
 
199
 
200
+ class ParallelDomainTests(unittest.IsolatedAsyncioTestCase):
201
+ """Domains must be probed CONCURRENTLY (with a cap).
202
+
203
+ Sequential probing means wall-clock = sum of every domain's latency; on a
204
+ blocked host that is timeout x domains. Commercial validators get bulk
205
+ throughput from parallelism across domains, not speed per domain.
206
+ """
207
+
208
+ async def test_domains_are_probed_in_parallel(self):
209
+ import asyncio
210
+ import time
211
+
212
+ active = {"now": 0, "peak": 0}
213
+
214
+ class SlowConn(FakeConnection):
215
+ def open(self):
216
+ active["now"] += 1
217
+ active["peak"] = max(active["peak"], active["now"])
218
+ time.sleep(0.25)
219
+ active["now"] -= 1
220
+ self.ready = True
221
+ return True
222
+
223
+ FakeConnection.registry = {f"mx{i}.test": {"*": 550} for i in range(8)}
224
+ v = MxBatchVerifier(
225
+ connection_factory=SlowConn, bulk_concurrency=8, max_connections_per_host=1
226
+ )
227
+ pairs = [(f"u{i}@dom{i}.test", f"mx{i}.test") for i in range(8)]
228
+
229
+ t0 = time.time()
230
+ out = await v.verify_batch_async(pairs)
231
+ elapsed = time.time() - t0
232
+
233
+ self.assertEqual(len(out), 8)
234
+ # 8 x 0.25s sequential = 2.0s; parallel should be well under 1s.
235
+ self.assertLess(elapsed, 1.2, f"domains ran sequentially ({elapsed:.2f}s)")
236
+ self.assertGreater(active["peak"], 2, "no real concurrency observed")
237
+
238
+ async def test_concurrency_cap_is_respected(self):
239
+ import time
240
+
241
+ active = {"now": 0, "peak": 0}
242
+
243
+ class SlowConn(FakeConnection):
244
+ def open(self):
245
+ active["now"] += 1
246
+ active["peak"] = max(active["peak"], active["now"])
247
+ time.sleep(0.15)
248
+ active["now"] -= 1
249
+ self.ready = True
250
+ return True
251
+
252
+ FakeConnection.registry = {f"mx{i}.test": {"*": 550} for i in range(10)}
253
+ v = MxBatchVerifier(
254
+ connection_factory=SlowConn, bulk_concurrency=3, max_connections_per_host=1
255
+ )
256
+ pairs = [(f"u{i}@dom{i}.test", f"mx{i}.test") for i in range(10)]
257
+ out = await v.verify_batch_async(pairs)
258
+
259
+ self.assertEqual(len(out), 10)
260
+ self.assertLessEqual(active["peak"], 3, "exceeded the concurrency cap")
261
+
262
+ async def test_sync_verify_batch_still_works(self):
263
+ FakeConnection.registry = {"mx.corp.test": {"a@corp.test": 250, "*": 550}}
264
+ v = MxBatchVerifier(connection_factory=FakeConnection)
265
+ out = v.verify_batch([("a@corp.test", "mx.corp.test")])
266
+ self.assertTrue(out["a@corp.test"].verified)
267
+
268
+
269
  if __name__ == "__main__":
270
  unittest.main()