This is an automated email from the ASF dual-hosted git repository.

hello-stephen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new efedf10c7e3 [improvement](ci) Harden Litefuse OTLP reliability (#67416)
efedf10c7e3 is described below

commit efedf10c7e35877bc0aa36573cd1f2af621367b9
Author: shuke <[email protected]>
AuthorDate: Mon Sep 7 11:21:19 2026 +0800

    [improvement](ci) Harden Litefuse OTLP reliability (#67416)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #67413 (merged)
    
    Problem Summary:
    
    Large code reviews repeatedly serialize growing batches during Litefuse
    pre-chunking, transient HTTP failures stop export immediately, and the
    main review trace can pass read-back verification after only a small
    subset of its exported observations becomes visible.
    
    This PR makes three focused changes:
    
    1. Makes legacy pre-chunk size accounting linear in the encoded event
    data while preserving event order, exact encoded sizes, and the existing
    OTLP-aware truncation and adaptive HTTP 413 handling.
    2. Retries HTTP 429, 502, 503, and 504 within the configured per-export
    retry budget and delay, with an HTTP retry counter alongside existing
    transport and payload-size counters.
    3. Strengthens read-back verification of the **main review trace**: it
    must expose at least its exported observation count in unique, non-empty
    IDs, with no duplicate or missing IDs, in addition to the existing I/O
    and step checks.
    
    The existing legacy-observations-first read order, v2 and trace-detail
    fallbacks, and pagination remain unchanged. Main and subagent exports
    both use the chunking and transport improvements; read-back verification
    remains scoped to the main trace. Verifying separately exported subagent
    traces is outside this PR's scope.
---
 .github/scripts/emit_litefuse_otel_io.py      |  69 ++++++--
 .github/scripts/test_emit_litefuse_otel_io.py | 223 ++++++++++++++++++++++++++
 2 files changed, 280 insertions(+), 12 deletions(-)

diff --git a/.github/scripts/emit_litefuse_otel_io.py 
b/.github/scripts/emit_litefuse_otel_io.py
index 463876a89e8..e3a33f377b3 100644
--- a/.github/scripts/emit_litefuse_otel_io.py
+++ b/.github/scripts/emit_litefuse_otel_io.py
@@ -29,6 +29,9 @@ import urllib.parse
 import urllib.request
 
 
+OTLP_RETRYABLE_HTTP_STATUS_CODES = frozenset((429, 502, 503, 504))
+
+
 def read_text(path, max_chars, tail=False, optional=False):
     try:
         with open(path, "r", encoding="utf-8", errors="replace") as handle:
@@ -1138,22 +1141,22 @@ def chunk_payload(payload, max_payload_bytes, 
shrink_oversized=True):
     batch = payload.get("batch") or []
     chunks = []
     current = []
-    current_bytes = json_payload_bytes({"batch": current})
+    empty_payload_bytes = json_payload_bytes({"batch": []})
+    current_bytes = empty_payload_bytes
 
     for event in batch:
-        event_payload = {"batch": [event]}
-        event_bytes = json_payload_bytes(event_payload)
-        if event_bytes > max_payload_bytes and shrink_oversized:
+        event_json_bytes = len(compact_json_bytes(event))
+        event_payload_bytes = empty_payload_bytes + event_json_bytes
+        if event_payload_bytes > max_payload_bytes and shrink_oversized:
             event = shrink_event_for_payload(event, max_payload_bytes)
-            event_payload = {"batch": [event]}
-            event_bytes = json_payload_bytes(event_payload)
+            event_json_bytes = len(compact_json_bytes(event))
+            event_payload_bytes = empty_payload_bytes + event_json_bytes
 
-        candidate = {"batch": current + [event]}
-        candidate_bytes = json_payload_bytes(candidate)
+        candidate_bytes = current_bytes + event_json_bytes + (1 if current 
else 0)
         if current and candidate_bytes > max_payload_bytes:
             chunks.append(({"batch": current}, current_bytes))
             current = [event]
-            current_bytes = event_bytes
+            current_bytes = event_payload_bytes
         else:
             current.append(event)
             current_bytes = candidate_bytes
@@ -1490,6 +1493,7 @@ def post_payload(
     request_sizes = []
     payload_too_large_retry_count = 0
     transport_retry_count = 0
+    http_retry_count = 0
     post_attempt_count = 0
     singleton_413_attempts = {}
     singleton_413_sources = {}
@@ -1503,6 +1507,17 @@ def post_payload(
                 endpoint, public_key, secret_key, otlp_chunk, timeout_seconds
             )
         except urllib.error.HTTPError as exc:
+            if exc.code in OTLP_RETRYABLE_HTTP_STATUS_CODES:
+                http_retry_count += 1
+                if http_retry_count <= retry_attempts:
+                    time.sleep(retry_sleep_seconds)
+                    chunks.insert(0, (chunk, otlp_chunk, request_size))
+                    continue
+                error_body = exc.read().decode("utf-8", errors="replace")
+                raise RuntimeError(
+                    "Litefuse OTLP ingestion failed after HTTP retries: "
+                    f"HTTP {exc.code}: {truncate_text(error_body, 4_000)}"
+                ) from exc
             if exc.code != 413:
                 error_body = exc.read().decode("utf-8", errors="replace")
                 raise RuntimeError(
@@ -1572,6 +1587,7 @@ def post_payload(
         "max_request_size": max(request_sizes) if request_sizes else 0,
         "payload_too_large_retries": payload_too_large_retry_count,
         "transport_retries": transport_retry_count,
+        "http_retries": http_retry_count,
         "success_count": success_count,
     }
 
@@ -1696,8 +1712,13 @@ def context_events_readback_ok(input_object):
     return event_count == 0 and events_value in (None, "", {})
 
 
-def verify_trace(args, public_key, secret_key, trace_id):
+def verify_trace(
+    args, public_key, secret_key, trace_id, expected_observation_count
+):
     last_diagnostic = {}
+    required_observation_count = max(
+        args.min_observations, expected_observation_count
+    )
     for _ in range(args.verify_attempts):
         legacy_trace_error = ""
         try:
@@ -1726,6 +1747,14 @@ def verify_trace(args, public_key, secret_key, trace_id):
             for observation in observations
             if not (observation.get("input") and observation.get("output"))
         ]
+        observation_ids = [
+            str(observation.get("id"))
+            for observation in observations
+            if observation.get("id")
+        ]
+        unique_observation_count = len(set(observation_ids))
+        observations_missing_id_count = len(observations) - 
len(observation_ids)
+        duplicate_observation_count = len(observation_ids) - 
unique_observation_count
         step_observations = [
             observation
             for observation in observations
@@ -1799,6 +1828,10 @@ def verify_trace(args, public_key, secret_key, trace_id):
             "trace_input": bool(trace_input),
             "trace_output": bool(trace_output),
             "observation_count": len(observations),
+            "required_observation_count": required_observation_count,
+            "unique_observation_count": unique_observation_count,
+            "observations_missing_id_count": observations_missing_id_count,
+            "duplicate_observation_count": duplicate_observation_count,
             "step_observation_count": len(step_observations),
             "agent_message_count": len(agent_message_observations),
             "agent_message_input_keys": agent_message_input_keys,
@@ -1817,7 +1850,9 @@ def verify_trace(args, public_key, secret_key, trace_id):
             [
                 trace_input,
                 trace_output,
-                len(observations) >= args.min_observations,
+                unique_observation_count >= required_observation_count,
+                observations_missing_id_count == 0,
+                duplicate_observation_count == 0,
                 len(step_observations) >= args.min_step_observations,
                 not observations_missing_io,
                 not agent_message_observations or agent_message_structure_ok,
@@ -1829,6 +1864,10 @@ def verify_trace(args, public_key, secret_key, trace_id):
                 "trace_output": True,
                 "read_source": read_source,
                 "observation_count": len(observations),
+                "required_observation_count": required_observation_count,
+                "unique_observation_count": unique_observation_count,
+                "observations_missing_id_count": observations_missing_id_count,
+                "duplicate_observation_count": duplicate_observation_count,
                 "step_observation_count": len(step_observations),
                 "agent_message_count": len(agent_message_observations),
                 "agent_message_input_keys": agent_message_input_keys,
@@ -1992,7 +2031,13 @@ def main():
 
     if args.verify:
         try:
-            result["verified"] = verify_trace(args, public_key, secret_key, 
trace_id)
+            result["verified"] = verify_trace(
+                args,
+                public_key,
+                secret_key,
+                trace_id,
+                observation_count,
+            )
         except Exception as exc:
             result["verification_error"] = str(exc)
             print(json.dumps(result, sort_keys=True))
diff --git a/.github/scripts/test_emit_litefuse_otel_io.py 
b/.github/scripts/test_emit_litefuse_otel_io.py
index 2b4aea839de..3ccdb7b6d60 100644
--- a/.github/scripts/test_emit_litefuse_otel_io.py
+++ b/.github/scripts/test_emit_litefuse_otel_io.py
@@ -629,5 +629,228 @@ class LitefuseOtelExporterTest(unittest.TestCase):
         )
 
 
+    def test_prechunking_encodes_each_legacy_event_once(self):
+        span_events = [
+            self.span_event(f"{index:032x}", output_size=200)
+            for index in range(1, 101)
+        ]
+        original_compact_json_bytes = MODULE.compact_json_bytes
+        encode_count = 0
+
+        def recording_compact_json_bytes(value):
+            nonlocal encode_count
+            encode_count += 1
+            return original_compact_json_bytes(value)
+
+        with mock.patch.object(
+            MODULE,
+            "compact_json_bytes",
+            side_effect=recording_compact_json_bytes,
+        ):
+            chunks = MODULE.chunk_payload(
+                {"batch": span_events}, max_payload_bytes=5_000
+            )
+
+        self.assertEqual(encode_count, len(span_events))
+        self.assertEqual(
+            [event for chunk, _size in chunks for event in chunk["batch"]],
+            span_events,
+        )
+        self.assertTrue(
+            all(
+                size == MODULE.json_payload_bytes(chunk)
+                for chunk, size in chunks
+            )
+        )
+
+    def test_retries_retryable_otlp_http_status(self):
+        trace_event = {"type": "trace-create", "body": self.trace_body()}
+        payload = {"batch": [trace_event, self.span_event("2" * 32)]}
+        unavailable = urllib.error.HTTPError(
+            "https://litefuse.example/api/public/otel/v1/traces";,
+            503,
+            "Service Unavailable",
+            {},
+            io.BytesIO(b'{"message":"temporarily unavailable"}'),
+        )
+
+        with mock.patch.object(
+            MODULE.urllib.request,
+            "urlopen",
+            side_effect=[unavailable, FakeResponse()],
+        ):
+            status = MODULE.post_payload(
+                "https://litefuse.example/api/public/otel/v1/traces";,
+                "public",
+                "secret",
+                payload,
+                10_000,
+                30,
+                3,
+                0,
+            )
+
+        self.assertEqual(status["http_retries"], 1)
+        self.assertEqual(status["request_count"], 1)
+        self.assertEqual(status["success_count"], 1)
+
+    def test_verify_complete_main_trace_uses_legacy_observations(self):
+        args = mock.Mock(
+            base_url="https://litefuse.example";,
+            verify_attempts=1,
+            verify_sleep_seconds=0,
+            min_observations=3,
+            min_step_observations=1,
+        )
+        observations = [
+            {
+                "id": "review",
+                "name": "codex.review",
+                "input": {"prompt": "p"},
+                "output": {"text": "o"},
+            },
+            {
+                "id": "turn",
+                "name": "codex.turn",
+                "input": {"prompt": "p"},
+                "output": {"text": "o"},
+            },
+            {
+                "id": "command",
+                "name": "codex.command",
+                "input": {"command": "pwd"},
+                "output": {"status": "ok"},
+            },
+        ]
+
+        with mock.patch.object(MODULE, "fetch_trace", return_value={}), 
mock.patch.object(
+            MODULE, "fetch_observations_legacy", return_value={"data": 
observations}
+        ), mock.patch.object(MODULE, "fetch_observations_v2") as v2_fetch:
+            result = MODULE.verify_trace(
+                args, "public", "secret", "trace-id", len(observations)
+            )
+
+        self.assertEqual(result["read_source"], "legacy_observations")
+        self.assertEqual(result["required_observation_count"], 
len(observations))
+        v2_fetch.assert_not_called()
+
+    def test_verify_rejects_partially_visible_trace(self):
+        args = mock.Mock(
+            base_url="https://litefuse.example";,
+            verify_attempts=1,
+            verify_sleep_seconds=0,
+            min_observations=1,
+            min_step_observations=1,
+        )
+        observations = [
+            {
+                "id": "review",
+                "name": "codex.review",
+                "input": {"prompt": "p"},
+                "output": {"text": "o"},
+            },
+            {
+                "id": "command",
+                "name": "codex.command",
+                "input": {"command": "pwd"},
+                "output": {"status": "ok"},
+            },
+        ]
+
+        with mock.patch.object(MODULE, "fetch_trace", return_value={}), 
mock.patch.object(
+            MODULE, "fetch_observations_legacy", return_value={"data": 
observations}
+        ), mock.patch.object(MODULE, "fetch_observations_v2"):
+            with self.assertRaisesRegex(
+                RuntimeError, '"required_observation_count": 3'
+            ):
+                MODULE.verify_trace(
+                    args, "public", "secret", "trace-id", 3
+                )
+
+    def test_verify_rejects_duplicate_observation_ids(self):
+        args = mock.Mock(
+            base_url="https://litefuse.example";,
+            verify_attempts=1,
+            verify_sleep_seconds=0,
+            min_observations=1,
+            min_step_observations=1,
+        )
+        observations = [
+            {
+                "id": "review",
+                "name": "codex.review",
+                "input": {"prompt": "p"},
+                "output": {"text": "o"},
+            },
+            {
+                "id": "command",
+                "name": "codex.command",
+                "input": {"command": "pwd"},
+                "output": {"status": "ok"},
+            },
+            {
+                "id": "command",
+                "name": "codex.command",
+                "input": {"command": "pwd"},
+                "output": {"status": "ok"},
+            },
+        ]
+
+        with mock.patch.object(MODULE, "fetch_trace", return_value={}), 
mock.patch.object(
+            MODULE, "fetch_observations_legacy", return_value={"data": 
observations}
+        ), mock.patch.object(MODULE, "fetch_observations_v2"):
+            with self.assertRaisesRegex(
+                RuntimeError, '"duplicate_observation_count": 1'
+            ):
+                MODULE.verify_trace(
+                    args, "public", "secret", "trace-id", 2
+                )
+
+    def test_verify_rejects_missing_or_empty_observation_ids(self):
+        args = SimpleNamespace(
+            base_url="https://litefuse.example";,
+            verify_attempts=1,
+            verify_sleep_seconds=0,
+            min_observations=1,
+            min_step_observations=1,
+        )
+        complete_observations = [
+            {
+                "id": "review",
+                "name": "codex.review",
+                "input": {"prompt": "p"},
+                "output": {"text": "o"},
+            },
+            {
+                "id": "command",
+                "name": "codex.command",
+                "input": {"command": "pwd"},
+                "output": {"status": "ok"},
+            },
+        ]
+        for id_fields in ({}, {"id": None}, {"id": ""}):
+            with self.subTest(id_fields=id_fields):
+                observations = [
+                    *complete_observations,
+                    {
+                        **id_fields,
+                        "name": "codex.command",
+                        "input": {"command": "git status"},
+                        "output": {"status": "ok"},
+                    },
+                ]
+                with mock.patch.object(
+                    MODULE, "fetch_trace", return_value={}
+                ), mock.patch.object(
+                    MODULE, "fetch_observations_legacy", return_value={"data": 
observations}
+                ), mock.patch.object(MODULE, "fetch_observations_v2"):
+                    # The two valid IDs already satisfy the count requirement;
+                    # the additional ID-less row must independently fail 
verification.
+                    with self.assertRaisesRegex(
+                        RuntimeError, '"observations_missing_id_count": 1'
+                    ):
+                        MODULE.verify_trace(args, "public", "secret", 
"trace-id", 2)
+
+
 if __name__ == "__main__":
     unittest.main()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to