[Nixl] Separate transport-failure metrics from KV expiry (#55854)

Signed-off-by: NickLucche <nicolo.lucchesi@mistral.ai>

Nicolò Lucchesi committed Sep 18, 2026 at 18:08 UTC 164dca8d1e65b8f10346321f01b25e23caa642a6
9 files changed +224 -22
docs/features/nixl_connector_usage.md
+9 -6
@@ -418,12 +418,13 @@ activity for the last reporting interval. Example output:
418 ```text
419 KV Transfer metrics: Num successful transfers=4, Avg xfer time (ms)=1.381,
420 P90 xfer time (ms)=2.601, Avg post time (ms)=0.672, P90 post time (ms)=0.801,
421 -Avg MB per transfer=2.25, Throughput (MB/s)=1629.549, Avg number of descriptors=72.0
421 +Avg MB per transfer=2.25, Throughput (MB/s)=1629.549, Avg number of descriptors=72.0,
422 +Num failed transfers=0, Num KV expired reqs=0
423 ```
424
425 The table below describes each field. All timing values cover only the
425 -successful transfers recorded in the current interval; failed transfers are
426 -counted separately via Prometheus (see
426 +successful transfers recorded in the current interval; failures are reported
427 +as interval counts in the same line and via Prometheus (see
428 [Prometheus metrics](#prometheus-metrics) below).
429
430 | Metric | Unit | Description |
@@ -436,6 +437,8 @@ counted separately via Prometheus (see
437 | `Avg MB per transfer` | MB | Mean payload size per transfer, computed as `total bytes transferred / number of transfers`. Reflects the average KV cache footprint of a single request (sequence length × layers × head dimension × dtype bytes). |
438 | `Throughput (MB/s)` | MB/s | Effective bandwidth over the interval: `total MB transferred / total xfer time (s)` across all successful transfers. This is aggregate throughput, not per-request bandwidth. |
439 | `Avg number of descriptors` | count | Mean number of NIXL memory descriptors (scatter-gather segments) submitted per transfer. More descriptors indicate more fragmented or larger KV cache allocations; very high counts can increase descriptor-registration overhead. |
440 +| `Num failed transfers` | count | Failed NIXL transfers, handshakes, and completion notifications (`send_notif`) in the interval, grouped because all are sporadic lower-transport-layer events. |
441 +| `Num KV expired reqs` | count | Requests whose KV blocks expired before being read. Reported separately from the failure counts above because it is an autoscaler/lease-tuning signal rather than a transport issue. |
442
443 ### Prometheus metrics
444
@@ -448,9 +451,9 @@ exported when NixlConnector is active:
451 | `vllm:nixl_post_time_seconds` | Histogram | Time to submit the transfer request to the RDMA backend (seconds). |
452 | `vllm:nixl_bytes_transferred` | Histogram | Bytes moved per transfer. |
453 | `vllm:nixl_num_descriptors` | Histogram | Descriptor count per transfer. |
451 -| `vllm:nixl_num_failed_transfers` | Counter | Cumulative count of failed NIXL KV-block transfers. |
452 -| `vllm:nixl_num_failed_notifications` | Counter | Cumulative count of failed completion notifications (`send_notif`). |
453 -| `vllm:nixl_num_kv_expired_reqs` | Counter | Requests whose KV blocks expired on the prefiller before the decoder read them (tracked on the P instance). |
454 +| `vllm:nixl_num_failed_transfers` | Counter | Cumulative count of failed NIXL KV-block transfers, including handshake and notification (`send_notif`) failures. These are grouped because all are sporadic lower-transport-layer events. |
455 +| `vllm:nixl_num_failed_notifications` | Counter | Cumulative count of failed notifications (`send_notif`), retained for compatibility. These failures are also included in `vllm:nixl_num_failed_transfers`; do not sum the two counters. |
456 +| `vllm:nixl_num_kv_expired_reqs` | Counter | Requests whose KV blocks expired on the prefiller before the decoder read them (tracked on the P instance). Kept separate from the failure counters above: KV expiry is an autoscaler/lease-tuning signal, not a transport issue. |
457
458 !!! tip
459 High `vllm:nixl_num_kv_expired_reqs` indicates that the prefiller's lease
tests/v1/kv_connector/unit/test_bidirectional_kv_transfer.py
+3
@@ -969,6 +969,9 @@ def test_turn2_deadline_gate(dist_init, offset, expiry_delta, expect_declined):
969 assert worker.xfer_stats.data["num_kv_expired_reqs"] == (
970 [1] if expect_declined else []
971 )
972 + # KV expiry must not pollute the transport failure counters.
973 + assert worker.xfer_stats.data["num_failed_transfers"] == []
974 + assert worker.xfer_stats.data["num_failed_handshakes"] == []
975 _, done_recving = connector.get_finished(finished_req_ids=set())
976 assert "req" in done_recving
977
tests/v1/kv_connector/unit/test_multi_connector.py
+24
@@ -567,6 +567,8 @@ class TestMultiConnectorStats:
567 "num_descriptors": [10, 20],
568 "num_failed_transfers": [],
569 "num_failed_notifications": [],
570 + "num_failed_handshakes": [],
571 + "num_kv_expired_reqs": [],
572 }
573 }
574
@@ -590,6 +592,8 @@ class TestMultiConnectorStats:
592 "num_descriptors": [10],
593 "num_failed_transfers": [],
594 "num_failed_notifications": [],
595 + "num_failed_handshakes": [],
596 + "num_kv_expired_reqs": [],
597 },
598 "MockConnector": {"mock_field": [1, 2, 3]},
599 }
@@ -619,6 +623,8 @@ class TestMultiConnectorStats:
623 "num_descriptors": [10],
624 "num_failed_transfers": [],
625 "num_failed_notifications": [],
626 + "num_failed_handshakes": [],
627 + "num_kv_expired_reqs": [],
628 },
629 }
630
@@ -638,6 +644,8 @@ class TestMultiConnectorStats:
644 "num_descriptors": [10],
645 "num_failed_transfers": [],
646 "num_failed_notifications": [],
647 + "num_failed_handshakes": [],
648 + "num_kv_expired_reqs": [],
649 }
650 )
651 mock_stats = MockConnectorStats(data={"mock_field": [1, 2, 3]})
@@ -667,6 +675,8 @@ class TestMultiConnectorStats:
675 "num_descriptors": [10],
676 "num_failed_transfers": [],
677 "num_failed_notifications": [],
678 + "num_failed_handshakes": [],
679 + "num_kv_expired_reqs": [],
680 }
681 )
682
@@ -698,6 +708,8 @@ class TestMultiConnectorStats:
708 "num_descriptors": [10],
709 "num_failed_transfers": [],
710 "num_failed_notifications": [],
711 + "num_failed_handshakes": [],
712 + "num_kv_expired_reqs": [],
713 },
714 "ExampleConnector": {"some_field": [1, 2, 3]},
715 }
@@ -735,6 +747,8 @@ class TestMultiConnectorStats:
747 "num_descriptors": [10],
748 "num_failed_transfers": [],
749 "num_failed_notifications": [],
750 + "num_failed_handshakes": [],
751 + "num_kv_expired_reqs": [],
752 }
753 )
754 }
@@ -750,6 +764,8 @@ class TestMultiConnectorStats:
764 "num_descriptors": [20],
765 "num_failed_transfers": [],
766 "num_failed_notifications": [],
767 + "num_failed_handshakes": [],
768 + "num_kv_expired_reqs": [],
769 }
770 )
771 }
@@ -781,6 +797,8 @@ class TestMultiConnectorStats:
797 "num_descriptors": [10],
798 "num_failed_transfers": [],
799 "num_failed_notifications": [],
800 + "num_failed_handshakes": [],
801 + "num_kv_expired_reqs": [],
802 }
803 )
804 }
@@ -807,6 +825,8 @@ class TestMultiConnectorStats:
825 "num_descriptors": [10, 20],
826 "num_failed_transfers": [],
827 "num_failed_notifications": [],
828 + "num_failed_handshakes": [],
829 + "num_kv_expired_reqs": [],
830 }
831 )
832 }
@@ -833,6 +853,8 @@ class TestMultiConnectorStats:
853 "num_descriptors": [10, 20],
854 "num_failed_transfers": [],
855 "num_failed_notifications": [],
856 + "num_failed_handshakes": [],
857 + "num_kv_expired_reqs": [],
858 }
859 )
860 }
@@ -862,6 +884,8 @@ class TestMultiConnectorStats:
884 "num_descriptors": [10, 20],
885 "num_failed_transfers": [],
886 "num_failed_notifications": [],
887 + "num_failed_handshakes": [],
888 + "num_kv_expired_reqs": [],
889 }
890 )
891 }
tests/v1/kv_connector/unit/test_nixl_connector.py
+82
@@ -1682,6 +1682,83 @@ def test_kv_connector_stats_aggregation():
1682 assert all(not isinstance(v, np.generic) for v in cli_stats.values())
1683
1684
1685 +def test_kv_connector_stats_failure_grouping():
1686 + """Transfer, handshake and notification failures are reported as one
1687 + transport-failure count, while KV expiry is reported separately: the
1688 + former are sporadic lower-transport-layer events, the latter an
1689 + autoscaler signal."""
1690 + stats = NixlKVConnectorStats()
1691 + assert stats.is_empty()
1692 +
1693 + stats.record_failed_transfer()
1694 + stats.record_failed_handshake()
1695 + stats.record_failed_notification()
1696 + stats.record_kv_expired_req()
1697 + assert not stats.is_empty()
1698 +
1699 + # No successful transfers: latency stats are zero but the failure
1700 + # counts still surface.
1701 + reduced = stats.reduce()
1702 + assert reduced["Num successful transfers"] == 0
1703 + assert reduced["Num failed transfers"] == 3
1704 + assert reduced["Num KV expired reqs"] == 1
1705 +
1706 +
1707 +def test_nixl_prom_metrics_group_handshake_with_transfer_failures():
1708 + """vllm:nixl_num_failed_transfers counts handshake and notification
1709 + failures too, while vllm:nixl_num_kv_expired_reqs stays a separate
1710 + counter."""
1711 + from prometheus_client import CollectorRegistry, Counter, Gauge, Histogram
1712 +
1713 + from vllm.distributed.kv_transfer.kv_connector.v1.nixl.stats import (
1714 + NixlPromMetrics,
1715 + )
1716 +
1717 + registry = CollectorRegistry()
1718 +
1719 + class RegistryGauge(Gauge):
1720 + def __init__(self, *args, **kwargs):
1721 + super().__init__(*args, registry=registry, **kwargs)
1722 +
1723 + class RegistryCounter(Counter):
1724 + def __init__(self, *args, **kwargs):
1725 + super().__init__(*args, registry=registry, **kwargs)
1726 +
1727 + class RegistryHistogram(Histogram):
1728 + def __init__(self, *args, **kwargs):
1729 + super().__init__(*args, registry=registry, **kwargs)
1730 +
1731 + vllm_config = create_vllm_config()
1732 + metric_types = {
1733 + Gauge: RegistryGauge,
1734 + Counter: RegistryCounter,
1735 + Histogram: RegistryHistogram,
1736 + }
1737 + prom = NixlPromMetrics(
1738 + vllm_config,
1739 + metric_types,
1740 + labelnames=["engine"],
1741 + per_engine_labelvalues={0: ["engine-0"]},
1742 + )
1743 +
1744 + stats = NixlKVConnectorStats()
1745 + stats.record_failed_transfer()
1746 + stats.record_failed_handshake()
1747 + stats.record_failed_notification()
1748 + stats.record_kv_expired_req()
1749 + prom.observe(stats.data, engine_idx=0)
1750 +
1751 + def counter_value(name: str) -> float:
1752 + for metric in registry.collect():
1753 + for sample in metric.samples:
1754 + if sample.name == name:
1755 + return sample.value
1756 + raise AssertionError(f"metric {name} not found in registry")
1757 +
1758 + assert counter_value("vllm:nixl_num_failed_transfers_total") == 3.0
1759 + assert counter_value("vllm:nixl_num_kv_expired_reqs_total") == 1.0
1760 +
1761 +
1762 def test_multi_kv_connector_stats_aggregation():
1763 """Test MultiKVConnectorStats aggregation across TP ranks using
1764 KVOutputAggregator (used by MultiprocExecutor).
@@ -3029,6 +3106,11 @@ def test_handshake_failure_returns_finished(default_vllm_config, dist_init):
3106 assert request_id in done_recving
3107 assert connector.get_block_ids_with_load_errors() == {1, 2, 3}
3108
3109 + # Handshake failures are recorded as transport failures, separately
3110 + # from KV expiry.
3111 + assert connector.connector_worker.xfer_stats.data["num_failed_handshakes"]
3112 + assert connector.connector_worker.xfer_stats.data["num_failed_transfers"] == []
3113 +
3114
3115 @patch(
3116 "vllm.distributed.kv_transfer.kv_connector.v1.nixl.base_worker.NixlWrapper",
tests/v1/kv_connector/unit/test_nixl_push_connector.py
+8
@@ -47,6 +47,9 @@ from vllm.distributed.kv_transfer.kv_connector.v1.nixl.metadata import (
47 from vllm.distributed.kv_transfer.kv_connector.v1.nixl.push_worker import (
48 NixlPushConnectorWorker,
49 )
50 +from vllm.distributed.kv_transfer.kv_connector.v1.nixl.stats import (
51 + NixlKVConnectorStats,
52 +)
53 from vllm.distributed.kv_transfer.kv_connector.v1.nixl.tp_mapping import TPMapping
54 from vllm.distributed.kv_transfer.kv_connector.v1.nixl.utils import (
55 get_base_request_id,
@@ -360,6 +363,7 @@ class _StubWriterWorker(NixlPushConnectorWorker):
363 w._recv_failures = set()
364 w._recving_transfers = defaultdict(list)
365 w._is_hma_required = False
366 + w.xfer_stats = NixlKVConnectorStats()
367 w._reqs_to_process = set()
368 w._reqs_to_send = {}
369 w.consumer_notification_counts_by_req = defaultdict(int)
@@ -730,6 +734,10 @@ def test_do_start_push_kv_drops_request_on_handshake_failure():
734 assert xfer_calls == []
735 assert len(failures) == 1
736 assert failures[0]["failure_type"] == "push_handshake_failed"
737 + # The handshake metric is counted once by _ensure_handshake's own
738 + # callback (stubbed out here); the push side must not double-record it.
739 + assert w.xfer_stats.data["num_failed_handshakes"] == []
740 + assert w.xfer_stats.data["num_failed_transfers"] == []
741
742
743 def test_writer_loop_drains_deferred_push_inbox():
vllm/distributed/kv_transfer/kv_connector/v1/nixl/base_worker.py
+28 -5
@@ -1306,6 +1306,8 @@ class NixlBaseConnectorWorker:
1306 error=e,
1307 remote_engine_id=eid,
1308 )
1309 + # Count once per handshake, regardless of waiting requests.
1310 + self.xfer_stats.record_failed_handshake()
1311
1312 fut.add_done_callback(done_callback)
1313 return fut
@@ -2811,13 +2813,17 @@ class NixlBaseConnectorWorker:
2813 done_sending.update(self._replicated_pcp_done_sending)
2814 self._replicated_pcp_done_sending.clear()
2815
2814 - # Process receive failures reported by background threads.
2816 + # Process receive failures reported by background threads. Each
2817 + # producer already recorded a specific metric (handshake or
2818 + # notification), so only the failure bookkeeping runs here.
2819 while not self._failed_recv_reqs.empty():
2820 try:
2821 req_id = self._failed_recv_reqs.get_nowait()
2822 except queue.Empty:
2823 break
2820 - self._handle_failed_transfer(req_id, None, self._recv_failures)
2824 + self._handle_failed_transfer(
2825 + req_id, None, self._recv_failures, record_failed_transfer=False
2826 + )
2827
2828 failed_recv_reqs: set[ReqId] = set()
2829 if self._recv_failures:
@@ -3059,10 +3065,27 @@ class NixlBaseConnectorWorker:
3065 return True
3066
3067 def _handle_failed_transfer(
3062 - self, req_id: str, handle: int | None, failed_req_ids: set[str] | None = None
3068 + self,
3069 + req_id: str,
3070 + handle: int | None,
3071 + failed_req_ids: set[str] | None = None,
3072 + record_failed_transfer: bool = True,
3073 ) -> bool:
3064 - """Record a failure and release its handle, returning False to retain it."""
3065 - self.xfer_stats.record_failed_transfer()
3074 + """Record a failure and release its handle, returning False to retain it.
3075 +
3076 + Args:
3077 + req_id: The request ID.
3078 + handle: The transfer handle.
3079 + failed_req_ids: Requests observed as failed; each stays listed
3080 + until it has no outstanding handles.
3081 + record_failed_transfer: Whether to count the failure toward the
3082 + transport-failure metric. Callers that already recorded a more
3083 + specific metric (eg KV expiry, reported separately from
3084 + transport failures) pass False.
3085 +
3086 + """
3087 + if record_failed_transfer:
3088 + self.xfer_stats.record_failed_transfer()
3089 if failed_req_ids is not None:
3090 failed_req_ids.add(req_id)
3091 return handle is None or self._try_release_xfer_handle(req_id, handle)
vllm/distributed/kv_transfer/kv_connector/v1/nixl/pull_worker.py
+19 -2
@@ -149,7 +149,11 @@ class NixlPullConnectorWorker(NixlBaseConnectorWorker):
149 engine_id,
150 )
151 self.xfer_stats.record_kv_expired_req()
152 - self._handle_failed_transfer(req_id, None, self._recv_failures)
152 + # KV expiry is reported separately from transport failures, so only
153 + # the state cleanup side of _handle_failed_transfer runs here.
154 + self._handle_failed_transfer(
155 + req_id, None, self._recv_failures, record_failed_transfer=False
156 + )
157 return
158
159 if any(len(group) > 0 for group in meta.local_block_ids):
@@ -364,7 +368,20 @@ class NixlPullConnectorWorker(NixlBaseConnectorWorker):
368 remote_agents = self._remote_agents[meta.remote.engine_id]
369 for rank_to_notify, agent in remote_agents.items():
370 if rank_to_notify != (0, read_specs[0].remote_rank):
367 - self.nixl_wrapper.send_notif(agent, notif_msg=notif_id)
371 + try:
372 + self.nixl_wrapper.send_notif(agent, notif_msg=notif_id)
373 + except Exception as e:
374 + self._log_failure(
375 + failure_type="notification_failed",
376 + msg="Remote rank will not update request state. "
377 + "This may indicate network issues.",
378 + req_id=req_id,
379 + error=e,
380 + dst_engine_id=meta.remote.engine_id,
381 + remote_rank=rank_to_notify[1],
382 + remote_agent_name=agent,
383 + )
384 + self.xfer_stats.record_failed_notification()
385
386 def _read_blocks(
387 self,
vllm/distributed/kv_transfer/kv_connector/v1/nixl/push_worker.py
+4
@@ -365,6 +365,7 @@ class NixlPushConnectorWorker(NixlBaseConnectorWorker):
365 engine_id,
366 req_id,
367 )
368 + self.xfer_stats.record_failed_notification()
369 self._failed_recv_reqs.put(req_id)
370 return
371 for rank, agent_name in agents.items():
@@ -377,6 +378,9 @@ class NixlPushConnectorWorker(NixlBaseConnectorWorker):
378 error=e,
379 remote_rank=rank,
380 )
381 + self.xfer_stats.record_failed_notification()
382 + # Earlier registrations may still trigger WRITEs into D's blocks.
383 + # Keep the receive pending until those writes are finished.
384 logger.debug(
385 "Sent PUSH_REG for %s to engine %s (%dB)", req_id, engine_id, len(notif_msg)
386 )
vllm/distributed/kv_transfer/kv_connector/v1/nixl/stats.py
+47 -9
@@ -39,6 +39,7 @@ class NixlKVConnectorStats(KVConnectorStats):
39 "num_descriptors": [],
40 "num_failed_transfers": [],
41 "num_failed_notifications": [],
42 + "num_failed_handshakes": [],
43 "num_kv_expired_reqs": [],
44 }
45
@@ -57,6 +58,10 @@ class NixlKVConnectorStats(KVConnectorStats):
58 """Record a failed NIXL notification (send_notif)."""
59 self.data["num_failed_notifications"].append(1)
60
61 + def record_failed_handshake(self):
62 + """Record a failed NIXL handshake."""
63 + self.data["num_failed_handshakes"].append(1)
64 +
65 def record_kv_expired_req(self):
66 """Record a request that had its KV blocks expire."""
67 self.data["num_kv_expired_reqs"].append(1)
@@ -72,6 +77,7 @@ class NixlKVConnectorStats(KVConnectorStats):
77 self.num_successful_transfers == 0
78 and len(self.data["num_failed_transfers"]) == 0
79 and len(self.data["num_failed_notifications"]) == 0
80 + and len(self.data["num_failed_handshakes"]) == 0
81 and len(self.data["num_kv_expired_reqs"]) == 0
82 )
83
@@ -84,10 +90,22 @@ class NixlKVConnectorStats(KVConnectorStats):
90 return self
91
92 def reduce(self) -> dict[str, int | float]:
87 - # Compute compact representative stats suitable for CLI logging
93 + # Compute compact representative stats suitable for CLI logging.
94 + # Failure counts are reported on every interval: transfer, handshake
95 + # and notification failures are grouped as sporadic
96 + # lower-transport-layer events, while KV expiry is reported separately
97 + # as it is an actionable autoscaler signal rather than a transport
98 + # issue.
99 + failure_counts = {
100 + "Num failed transfers": len(self.data["num_failed_transfers"])
101 + + len(self.data["num_failed_handshakes"])
102 + + len(self.data["num_failed_notifications"]),
103 + "Num KV expired reqs": len(self.data["num_kv_expired_reqs"]),
104 + }
105 if self.num_successful_transfers == 0:
89 - # CLI logging only reports successful transfers stats. If all requests in
90 - # the interval were unsuccessful, Prom will report failures stats instead.
106 + # Timing / throughput stats only cover successful transfers. If
107 + # all requests in the interval were unsuccessful, the failure
108 + # counts above still surface what happened.
109 return {
110 "Num successful transfers": 0,
111 "Avg xfer time (ms)": 0,
@@ -97,6 +115,7 @@ class NixlKVConnectorStats(KVConnectorStats):
115 "Avg MB per transfer": 0,
116 "Throughput (MB/s)": 0,
117 "Avg number of descriptors": 0,
118 + **failure_counts,
119 }
120
121 xfer_time = np.asarray(self.data["transfer_duration"])
@@ -122,6 +141,7 @@ class NixlKVConnectorStats(KVConnectorStats):
141 "Avg MB per transfer": round(avg_mb.item(), 3),
142 "Throughput (MB/s)": round(throughput_mb_s.item(), 3),
143 "Avg number of descriptors": round(descs.mean().item(), 1),
144 + **failure_counts,
145 }
146
147 @property
@@ -212,15 +232,20 @@ class NixlPromMetrics(KVConnectorPromMetrics):
232 )
233 counter_nixl_num_failed_transfers = self._counter_cls(
234 name="vllm:nixl_num_failed_transfers",
215 - documentation="Number of failed NIXL KV Cache transfers.",
235 + documentation="Number of failed NIXL KV Cache transfers, including"
236 + " handshake and notification failures. NOTE: KV expiry is tracked"
237 + " separately in vllm:nixl_num_kv_expired_reqs.",
238 labelnames=labelnames,
239 )
240 self.counter_nixl_num_failed_transfers = create_metric_per_engine(
241 counter_nixl_num_failed_transfers, self.per_engine_labelvalues
242 )
243 +
244 counter_nixl_num_failed_notifications = self._counter_cls(
245 name="vllm:nixl_num_failed_notifications",
223 - documentation="Number of failed NIXL KV Cache notifications.",
246 + documentation="Number of failed NIXL KV Cache notifications. "
247 + "Retained for compatibility; these failures are also included in "
248 + "vllm:nixl_num_failed_transfers.",
249 labelnames=labelnames,
250 )
251 self.counter_nixl_num_failed_notifications = create_metric_per_engine(
@@ -254,13 +279,26 @@ class NixlPromMetrics(KVConnectorPromMetrics):
279 ):
280 for list_item in transfer_stats_data[list_item_key]:
281 prom_obj[engine_idx].observe(list_item)
257 - for counter_obj, counter_item_key in zip(
282 + for counter_obj, counter_item_keys in zip(
283 [
284 self.counter_nixl_num_failed_transfers,
285 self.counter_nixl_num_failed_notifications,
286 self.counter_nixl_num_kv_expired_reqs,
287 ],
263 - ["num_failed_transfers", "num_failed_notifications", "num_kv_expired_reqs"],
288 + [
289 + # Transfer, handshake and notification failures are grouped:
290 + # all are sporadic lower-transport-layer events. KV expiry is
291 + # reported separately as it signals autoscaler behavior, not
292 + # transport health.
293 + (
294 + "num_failed_transfers",
295 + "num_failed_handshakes",
296 + "num_failed_notifications",
297 + ),
298 + ("num_failed_notifications",),
299 + ("num_kv_expired_reqs",),
300 + ],
301 ):
265 - for list_item in transfer_stats_data[counter_item_key]:
266 - counter_obj[engine_idx].inc(list_item)
302 + for counter_item_key in counter_item_keys:
303 + for list_item in transfer_stats_data[counter_item_key]:
304 + counter_obj[engine_idx].inc(list_item)