Commit 793a4fc65e for bind

commit 793a4fc65ea1889236a9d5dbf5f3adfd41889c95
Author: Štěpán Balážik <stepan@isc.org>
Date:   Tue Sep 8 19:36:10 2026 +0200

    Match only the first matching query with a matcher

    Once declares that a handler answers only the first query its matcher
    matches, which filters/ans6 previously did with a match() method that
    spent itself on the first query evaluated, whatever it was.

    Assisted-by: Claude:claude-fable-5

diff --git a/bin/tests/system/filters/ans6/ans.py b/bin/tests/system/filters/ans6/ans.py
index ef77810bfe..5a682f36d6 100644
--- a/bin/tests/system/filters/ans6/ans.py
+++ b/bin/tests/system/filters/ans6/ans.py
@@ -14,9 +14,9 @@ import dns.rdataclass
 import dns.rdatatype
 import dns.rrset

-from isctest.asyncserver import AsyncDnsServer, QueryContext
+from isctest.asyncserver import AsyncDnsServer
 from isctest.asyncserver.handlers import StaticResponseHandler
-from isctest.asyncserver.matchers import Qname, Qtype
+from isctest.asyncserver.matchers import Once, Qname, Qtype

 DNS64_TRIGGER = "nodata.test."

@@ -44,19 +44,9 @@ class NodataOnceHandler(StaticResponseHandler):
     AAAA (answered by AaaaHandler), triggering the bug.
     """

-    matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.AAAA)
+    matcher = Once(Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.AAAA))
     authority = [soa()]

-    def __init__(self) -> None:
-        super().__init__()
-        self._answered = False
-
-    def match(self, qctx: QueryContext) -> bool:
-        if self._answered:
-            return False
-        self._answered = True
-        return super().match(qctx)
-

 class AaaaHandler(StaticResponseHandler):
     matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.AAAA)
diff --git a/bin/tests/system/isctest/asyncserver/matchers.py b/bin/tests/system/isctest/asyncserver/matchers.py
index dcdb37dba3..cc577331c9 100644
--- a/bin/tests/system/isctest/asyncserver/matchers.py
+++ b/bin/tests/system/isctest/asyncserver/matchers.py
@@ -309,3 +309,22 @@ class LabelCount(Matcher):

     def __str__(self) -> str:
         return f"QNAME of {self._count} labels"
+
+
+class Once(Matcher):
+    """
+    Match the first query the given matcher matches, and only that one.
+    """
+
+    def __init__(self, matcher: Matcher) -> None:
+        self._matcher = matcher
+        self._done = False
+
+    def match(self, qctx: QueryContext) -> bool:
+        if self._done or not self._matcher.match(qctx):
+            return False
+        self._done = True
+        return True
+
+    def __str__(self) -> str:
+        return f"once, {self._matcher}"