2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-21 15:18:19 +00:00

perf(blackboxtest): stop listening once a DHCP step has its reply

Every DHCP step listened for its whole timeout, even after the reply it
waited for had arrived. The 15 DISCOVERs of pxe-arch-matrix.conf took
30 seconds on a management node that answers in milliseconds.

A step now stops at the first reply of the type it expects. A step that
asserts on offers, or expects none, still listens for the whole timeout,
because it must hear every server or prove that none answered.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-09-18 09:31:10 -03:00
parent 4964191011
commit 8690c0ab0f
2 changed files with 31 additions and 5 deletions
@@ -228,8 +228,9 @@ class Wire(object):
def send(self, frame):
self.socket.send(frame)
def collect(self, session, deadline):
"""Every reply to this session's xid until `deadline`.
def collect(self, session, deadline, stop=()):
"""Every reply to this session's xid until `deadline`, or until a
reply of a type in `stop` arrives.
A reply of an unexpected type is kept, so a NAK where an ACK was
wanted is reported as a NAK rather than as a timeout.
@@ -237,7 +238,8 @@ class Wire(object):
found = []
while True:
remaining = deadline - time.monotonic()
if remaining <= 0 or not select.select([self.socket], [], [], remaining)[0]:
if remaining <= 0 or any(r.fields["msgtype"] in stop for r in found) \
or not select.select([self.socket], [], [], remaining)[0]:
return found
frame = self.socket.recv(2048)
if frame is None:
@@ -293,7 +295,8 @@ def run_step(step, session, context, timeout, retries):
sent = describe(step, session, trans, context)
session.wire.send(build_frame(step, session, trans, context,
session.wire.scapy))
replies = session.wire.collect(session, time.monotonic() + timeout)
replies = session.wire.collect(session, time.monotonic() + timeout,
stop_on(step, wanted))
if any(r.fields["msgtype"] in wanted for r in replies) \
or (replies and not wanted):
break
@@ -328,6 +331,18 @@ def run_step(step, session, context, timeout, retries):
return chosen, extras, sent
def stop_on(step, wanted):
"""The reply types that end a step's listening window early.
A step that counts `offers` must hear every server, so it listens for the
whole timeout. So does `expect = none`, where silence is the result.
"""
if any("offers" in (a.target, a.value)
for a in step.assertions):
return frozenset()
return wanted
def distinct_servers(replies):
"""One reply per answering server.
+12 -1
View File
@@ -164,7 +164,8 @@ class FakeWire(object):
def send(self, frame):
self.sent += 1
def collect(self, session, deadline):
def collect(self, session, deadline, stop=()):
self.stop = stop
return self.answers.pop(0) if self.answers else []
def close(self):
@@ -239,6 +240,16 @@ class StepLoop(unittest.TestCase):
self.assertEqual(extras["met"], met)
self.assertEqual(session.state, dhcp.INIT)
def test_a_step_stops_listening_once_its_reply_arrived(self):
# Unless it counts the servers that answered, or proves silence.
cases = (("", {"OFFER"}), ("assert =\n offers == 1\n", set()),
("expect = none\n", set()))
for extra, stop in cases:
step, = self.step("[step d]\ntype = discover\n" + extra)
session = dhcp.Session(FakeWire([reply("OFFER")]))
dhcp.run_step(step, session, subst.Context(), 0.1, 1)
self.assertEqual(set(session.wire.stop), stop, extra)
def test_a_step_mac_replaces_the_random_one(self):
step, = self.step("[step d]\ntype = discover\nmac = 02-00-00-00-00-AA\n")
session = dhcp.Session(FakeWire())