1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! Per-fetch observability — which tier actually served a blob, from whom,
//! how many bytes, and how long it took (R423-T7).
//!
//! ## Why this exists
//!
//! W160 sizes the seed's egress bill on an assumed **50% swarm hit rate**. That
//! number was never measured, and until this module it was not *measurable*:
//! [`crate::source::FetchChain`] knew exactly which tier served every blob and
//! then dropped the answer on the floor one line later, at
//! `if let Some((_tier, bytes))`. The CLI's own fetch event reported
//! `tier: "fetched"` — a constant string carrying no information at all.
//!
//! So the cost envelope was theoretical, and the one experiment that would
//! falsify it could not be run. A [`FetchReport`] is that experiment's datum.
//!
//! ## Shape
//!
//! One report per completed [`crate::Asset::fetch`], hit or miss, carrying the
//! tuple R423-T7 asks for: class, blake3, tier, bytes, peer, duration.
//!
//! **A miss is a report too.** `tier: None` is the "no source had it" case, and
//! it has to be emitted rather than silently dropped: a hit-rate denominator
//! built only from successes is not a hit rate. That is the same class of
//! mistake as a monitor that renders zero machines when it could not read its
//! inventory — an absence reported as a value.
//!
//! ## What it deliberately does not measure
//!
//! `bytes_served` here is **download**-side: bytes this node pulled in. The
//! upload side — bytes this node served *to* a peer — is not visible from the
//! fetch chain at all, and reporting a download as if it were an upload would
//! make the egress reconciliation this ticket exists for silently wrong. See
//! the module docs on `FetchObserver` for where that hook has to go instead.
use Arc;
use crate::;
/// One completed fetch, as a metric.
///
/// Emitted through the [`FetchObserver`] registered on
/// [`crate::AssetClassConfig::observer`], and returned directly by
/// [`crate::Asset::fetch_reported`].
/// Callback invoked once per completed fetch.
///
/// Cheap to clone (`Arc`), invoked on the fetch task, so the closure must be
/// `Send + Sync` and **must not block** — a sink that does I/O should hand the
/// report to a channel and return.
///
/// ## Where the upload side has to go, and why it is not here
///
/// This observer sees only fetches *this* node performs. A seed's egress is
/// bytes it **serves**, which happens inside `iroh-blobs`' provider event
/// stream — a surface `xlb` does not currently subscribe to anywhere. Wiring it
/// is a separate change to `transport::blobs`, not a variant of this type, and
/// until it exists no reading here can be reconciled against a hosting bill.
/// Stated plainly so nobody builds an egress dashboard on the wrong number.
pub type FetchObserver = ;