sitcod3 commited on
Commit
1a43824
·
verified ·
1 Parent(s): 45aeb2f

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app/validator/mx_batch.py +36 -0
  2. tests/test_mx_batch.py +52 -0
app/validator/mx_batch.py CHANGED
@@ -79,6 +79,8 @@ class MxBatchVerifier:
79
  mail_from: str = "probe@validator.local",
80
  catch_all_cache: Optional[CatchAllCache] = None,
81
  max_connections_per_host: int = 2,
 
 
82
  ):
83
  self.timeout = timeout
84
  self.helo_domain = helo_domain
@@ -88,6 +90,27 @@ class MxBatchVerifier:
88
  self._factory = connection_factory or (
89
  lambda host, **kw: SmtpConnection(host, **kw)
90
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  def _new_connection(self, mx_host: str):
93
  return self._factory(
@@ -130,9 +153,21 @@ class MxBatchVerifier:
130
  }
131
 
132
  out: dict[str, SMTPResult] = {}
 
 
 
 
 
 
 
 
 
 
 
133
  conn = self._new_connection(mx_host)
134
  try:
135
  if not conn.open():
 
136
  return {
137
  e: SMTPResult(
138
  verified=None,
@@ -140,6 +175,7 @@ class MxBatchVerifier:
140
  )
141
  for e in emails
142
  }
 
143
 
144
  if cached is None:
145
  ca = conn.probe(_gibberish(domain))
 
79
  mail_from: str = "probe@validator.local",
80
  catch_all_cache: Optional[CatchAllCache] = None,
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
 
90
  self._factory = connection_factory or (
91
  lambda host, **kw: SmtpConnection(host, **kw)
92
  )
93
+ # Egress circuit breaker. On a host that blocks outbound port 25 every
94
+ # connect burns the full timeout, so a 10k job would take ~27h. After
95
+ # `fail_threshold` consecutive connect failures we stop dialling for
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
+
102
+ @property
103
+ def circuit_open(self) -> bool:
104
+ return time.monotonic() < self._open_until
105
+
106
+ def _record_connect_failure(self) -> None:
107
+ self._consecutive_failures += 1
108
+ if self._consecutive_failures >= self.fail_threshold:
109
+ self._open_until = time.monotonic() + self.cooldown_seconds
110
+
111
+ def _record_connect_success(self) -> None:
112
+ self._consecutive_failures = 0
113
+ self._open_until = 0.0
114
 
115
  def _new_connection(self, mx_host: str):
116
  return self._factory(
 
153
  }
154
 
155
  out: dict[str, SMTPResult] = {}
156
+
157
+ # Fail fast while egress is known-bad: do not dial, do not wait.
158
+ if self.circuit_open:
159
+ return {
160
+ e: SMTPResult(
161
+ verified=None,
162
+ error=f"smtp_unreachable: {mx_host} (egress circuit open)",
163
+ )
164
+ for e in emails
165
+ }
166
+
167
  conn = self._new_connection(mx_host)
168
  try:
169
  if not conn.open():
170
+ self._record_connect_failure()
171
  return {
172
  e: SMTPResult(
173
  verified=None,
 
175
  )
176
  for e in emails
177
  }
178
+ self._record_connect_success()
179
 
180
  if cached is None:
181
  ca = conn.probe(_gibberish(domain))
tests/test_mx_batch.py CHANGED
@@ -145,5 +145,57 @@ class CatchAllCacheTests(unittest.TestCase):
145
  self.assertIsNone(c.get("never-seen.test"))
146
 
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  if __name__ == "__main__":
149
  unittest.main()
 
145
  self.assertIsNone(c.get("never-seen.test"))
146
 
147
 
148
+ class EgressCircuitBreakerTests(unittest.TestCase):
149
+ """When outbound port 25 is blocked (HF Spaces, most PaaS) every connect
150
+ burns the full timeout. A bulk job must detect that once and fail fast
151
+ instead of spending timeout × N — 10k rows at 10s/row is ~27 hours."""
152
+
153
+ def setUp(self):
154
+ FakeConnection.registry = {}
155
+ FakeConnection.opened = []
156
+
157
+ def test_circuit_opens_after_threshold_and_stops_connecting(self):
158
+ class DeadConn(FakeConnection):
159
+ def open(self):
160
+ self.ready = False
161
+ return False
162
+
163
+ FakeConnection.registry = {"mx.a.test": {"*": 250}}
164
+ v = MxBatchVerifier(connection_factory=DeadConn, fail_threshold=2, cooldown_seconds=60)
165
+
166
+ v.verify_batch([("a@a.test", "mx.a.test")])
167
+ v.verify_batch([("b@b.test", "mx.b.test")])
168
+ self.assertTrue(v.circuit_open)
169
+
170
+ before = len(FakeConnection.opened)
171
+ out = v.verify_batch([("c@c.test", "mx.c.test")])
172
+ # No new connection attempted while the circuit is open.
173
+ self.assertEqual(len(FakeConnection.opened), before)
174
+ self.assertIsNone(out["c@c.test"].verified)
175
+ self.assertIn("unreachable", (out["c@c.test"].error or "").lower())
176
+
177
+ def test_successful_connection_resets_failure_count(self):
178
+ class FlakyConn(FakeConnection):
179
+ fail_next = True
180
+
181
+ def open(self):
182
+ if FlakyConn.fail_next:
183
+ self.ready = False
184
+ return False
185
+ self.ready = True
186
+ return True
187
+
188
+ FakeConnection.registry = {"mx.a.test": {"*": 250}}
189
+ v = MxBatchVerifier(connection_factory=FlakyConn, fail_threshold=2, cooldown_seconds=60)
190
+ v.verify_batch([("a@a.test", "mx.a.test")])
191
+ FlakyConn.fail_next = False
192
+ v.verify_batch([("b@b.test", "mx.b.test")])
193
+ self.assertFalse(v.circuit_open)
194
+
195
+ def test_default_threshold_is_conservative(self):
196
+ v = MxBatchVerifier(connection_factory=FakeConnection)
197
+ self.assertGreaterEqual(v.fail_threshold, 2)
198
+
199
+
200
  if __name__ == "__main__":
201
  unittest.main()