1717# front-proxy makes order track arrival, not work. calibrate() detects that (and estimates the readable
1818# delta) so the caller never applies the oracle blind - it falls back to classic time-based instead.
1919
20+ import socket
21+ import ssl
2022import threading
2123
2224from lib .core .enums import DBMS
@@ -49,18 +51,32 @@ def buildConditionPair(condition, heavy, cheap="0"):
4951 return condExpr , negExpr
5052
5153
52- def _pairOrder (conn , reqA , reqB , timeout ):
54+ def _pairOrder (connSource , reqA , reqB , timeout , retries = 2 ):
5355 """Send reqA and reqB as one coalesced pair; return the stream id that finished FIRST plus the two
54- stream ids in send order (reqA got the lower id)."""
55- order , _results = conn .exchange_pair ([reqA , reqB ], timeout )
56- loSid = conn .next_sid - 4
57- hiSid = conn .next_sid - 2
58- return order [0 ], loSid , hiSid
56+ stream ids in send order (reqA got the lower id).
57+
58+ `connSource` is either a live _H2Connection or a zero-arg callable returning one. The callable form
59+ lets a dropped connection be replaced transparently and the pair re-sent: a long extraction routinely
60+ outlives a single HTTP/2 connection (the server retires it with GOAWAY after its per-connection request
61+ cap), and a coalesced boolean-read pair is idempotent, so re-sending it on a fresh connection is safe.
62+ A raw connection (used by calibration and the self-test) is not retried - it simply raises."""
63+ attempt = 0
64+ while True :
65+ conn = connSource () if callable (connSource ) else connSource
66+ try :
67+ order , _results = conn .exchange_pair ([reqA , reqB ], timeout )
68+ return order [0 ], conn .next_sid - 4 , conn .next_sid - 2
69+ except (socket .error , ssl .SSLError , IOError ):
70+ conn .close () # retire; a callable source reopens on the next pass
71+ attempt += 1
72+ if not callable (connSource ) or attempt > retries :
73+ raise
5974
6075
61- def readBit (conn , reqCond , reqNeg , votes = 5 , timeout = 30 ):
76+ def readBit (connSource , reqCond , reqNeg , votes = 5 , timeout = 30 ):
6277 """Read one boolean by the cond-last FRACTION over symmetric pairs, ESCALATING when the fraction is
63- ambiguous (load-degraded).
78+ ambiguous (load-degraded). `connSource` is a live connection or a factory (see _pairOrder) so a
79+ connection dropped mid-vote (e.g. server GOAWAY) is replaced and that pair re-sent transparently.
6480
6581 reqCond does the heavy work iff the guessed condition is TRUE; reqNeg does the SAME heavy work iff the
6682 condition is FALSE (it carries the negated condition). Exactly one runs heavy, so whichever finishes
@@ -83,10 +99,10 @@ def readBit(conn, reqCond, reqNeg, votes=5, timeout=30):
8399 cap = votes * 5 # escalation ceiling for ambiguous (load-degraded) bits
84100 while True :
85101 if i % 2 == 0 :
86- first , loSid , _hiSid = _pairOrder (conn , reqCond , reqNeg , timeout )
102+ first , loSid , _hiSid = _pairOrder (connSource , reqCond , reqNeg , timeout )
87103 condSid = loSid
88104 else :
89- first , _loSid , hiSid = _pairOrder (conn , reqNeg , reqCond , timeout )
105+ first , _loSid , hiSid = _pairOrder (connSource , reqNeg , reqCond , timeout )
90106 condSid = hiSid
91107 if first != condSid : # reqCond finished last -> it ran heavy -> vote says TRUE
92108 condLast += 1
@@ -159,6 +175,16 @@ def __init__(self, host, port, refReq, proxy=None, asymVotes=12, votes=5, timeou
159175 def _conn (self ):
160176 conn = getattr (self ._local , "conn" , None )
161177 if conn is None or not conn .usable :
178+ if conn is not None : # retire the dead one so reconnects don't leak sockets
179+ try :
180+ conn .close ()
181+ except Exception :
182+ pass
183+ with self ._lock :
184+ try :
185+ self ._conns .remove (conn )
186+ except ValueError :
187+ pass
162188 conn = self ._local .conn = connect (self .host , self .port , self .proxy , self .timeout )
163189 with self ._lock :
164190 self ._conns .append (conn )
@@ -172,9 +198,11 @@ def readBitFromSpecs(self, condSpec, negSpec=None):
172198 be used for a heavy-base condition (the trivial-base reference is not comparable). Specs are the
173199 (url, method, headers, post) tuples from getPage(buildOnly=True)."""
174200 reqCond = _specToReq (condSpec , self .host )
201+ # Pass the bound _conn factory (not a resolved connection) so a pair whose connection is retired
202+ # mid-read (server GOAWAY on a long dump) is transparently re-sent on a fresh one.
175203 if negSpec is not None :
176- return readBit (self ._conn () , reqCond , _specToReq (negSpec , self .host ), votes = self .votes , timeout = self .timeout )
177- return readBitAsymmetric (self ._conn () , reqCond , self .refReq , self .asymVotes , self .timeout )
204+ return readBit (self ._conn , reqCond , _specToReq (negSpec , self .host ), votes = self .votes , timeout = self .timeout )
205+ return readBitAsymmetric (self ._conn , reqCond , self .refReq , self .asymVotes , self .timeout )
178206
179207 def close (self ):
180208 with self ._lock :
@@ -655,7 +683,7 @@ def readBitLive(conn, condition, vector=None, votes=1, timeout=30):
655683 return readBit (conn , reqCond , reqNeg , votes = votes , timeout = timeout )
656684
657685
658- def readBitAsymmetric (conn , reqCond , reqRef , votes = 12 , timeout = 30 ):
686+ def readBitAsymmetric (connSource , reqCond , reqRef , votes = 12 , timeout = 30 ):
659687 """Read one boolean WITHOUT the negated comparison - only the condition request and a FIXED always-heavy
660688 reference. When the condition is TRUE, reqCond runs the SAME heavy work as reqRef, so they race ~50/50
661689 and reqCond finishes last about HALF the votes; when FALSE - or when the comparison is NULL / the DBMS
@@ -668,10 +696,10 @@ def readBitAsymmetric(conn, reqCond, reqRef, votes=12, timeout=30):
668696 condLast = 0
669697 for i in range (votes ):
670698 if i % 2 == 0 :
671- order , _ = conn . exchange_pair ([ reqCond , reqRef ] , timeout ); condSid = conn . next_sid - 4
699+ first , condSid , _hiSid = _pairOrder ( connSource , reqCond , reqRef , timeout )
672700 else :
673- order , _ = conn . exchange_pair ([ reqRef , reqCond ] , timeout ); condSid = conn . next_sid - 2
674- if order [ 0 ] != condSid : # cond finished last -> it ran the heavy branch this vote
701+ first , _loSid , condSid = _pairOrder ( connSource , reqRef , reqCond , timeout )
702+ if first != condSid : # cond finished last -> it ran the heavy branch this vote
675703 condLast += 1
676704 return condLast * 4 >= votes # >= 25% cond-last -> TRUE (mid-way between ~50% and ~0%)
677705
0 commit comments