create-sia-app 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +179 -0
- package/package.json +29 -0
- package/template/CLAUDE.md +160 -0
- package/template/README.md +102 -0
- package/template/_gitignore +5 -0
- package/template/biome.json +40 -0
- package/template/index.html +13 -0
- package/template/package.json +30 -0
- package/template/rust/README.md +16 -0
- package/template/rust/sia-sdk-rs/.changeset/added_cancel_function_to_cancel_inflight_packed_uploads.md +6 -0
- package/template/rust/sia-sdk-rs/.changeset/check_if_we_have_enough_hosts_prior_to_encoding_in_upload_slabs.md +16 -0
- package/template/rust/sia-sdk-rs/.changeset/fix_slab_length_in_packed_object.md +5 -0
- package/template/rust/sia-sdk-rs/.changeset/fix_upload_racing_race_conditon.md +13 -0
- package/template/rust/sia-sdk-rs/.changeset/improved_parallelism_of_packed_uploads.md +5 -0
- package/template/rust/sia-sdk-rs/.changeset/progress_callback_will_now_be_called_as_expected_for_packed_uploads.md +5 -0
- package/template/rust/sia-sdk-rs/.github/dependabot.yml +10 -0
- package/template/rust/sia-sdk-rs/.github/workflows/main.yml +36 -0
- package/template/rust/sia-sdk-rs/.github/workflows/prepare-release.yml +34 -0
- package/template/rust/sia-sdk-rs/.github/workflows/release.yml +30 -0
- package/template/rust/sia-sdk-rs/.rustfmt.toml +4 -0
- package/template/rust/sia-sdk-rs/Cargo.lock +4127 -0
- package/template/rust/sia-sdk-rs/Cargo.toml +3 -0
- package/template/rust/sia-sdk-rs/LICENSE +21 -0
- package/template/rust/sia-sdk-rs/README.md +30 -0
- package/template/rust/sia-sdk-rs/indexd/CHANGELOG.md +79 -0
- package/template/rust/sia-sdk-rs/indexd/Cargo.toml +79 -0
- package/template/rust/sia-sdk-rs/indexd/benches/upload.rs +258 -0
- package/template/rust/sia-sdk-rs/indexd/src/app_client.rs +1710 -0
- package/template/rust/sia-sdk-rs/indexd/src/builder.rs +354 -0
- package/template/rust/sia-sdk-rs/indexd/src/download.rs +379 -0
- package/template/rust/sia-sdk-rs/indexd/src/hosts.rs +659 -0
- package/template/rust/sia-sdk-rs/indexd/src/lib.rs +827 -0
- package/template/rust/sia-sdk-rs/indexd/src/mock.rs +162 -0
- package/template/rust/sia-sdk-rs/indexd/src/object_encryption.rs +125 -0
- package/template/rust/sia-sdk-rs/indexd/src/quic.rs +575 -0
- package/template/rust/sia-sdk-rs/indexd/src/rhp4.rs +52 -0
- package/template/rust/sia-sdk-rs/indexd/src/slabs.rs +497 -0
- package/template/rust/sia-sdk-rs/indexd/src/upload.rs +629 -0
- package/template/rust/sia-sdk-rs/indexd/src/wasm_time.rs +41 -0
- package/template/rust/sia-sdk-rs/indexd/src/web_transport.rs +398 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/CHANGELOG.md +76 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/Cargo.toml +47 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/README.md +10 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/example.py +130 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/bin/uniffi-bindgen.rs +3 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/builder.rs +377 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/io.rs +155 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/lib.rs +1039 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/logging.rs +58 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/tls.rs +23 -0
- package/template/rust/sia-sdk-rs/indexd_wasm/Cargo.toml +33 -0
- package/template/rust/sia-sdk-rs/indexd_wasm/src/lib.rs +818 -0
- package/template/rust/sia-sdk-rs/knope.toml +54 -0
- package/template/rust/sia-sdk-rs/sia_derive/CHANGELOG.md +38 -0
- package/template/rust/sia-sdk-rs/sia_derive/Cargo.toml +19 -0
- package/template/rust/sia-sdk-rs/sia_derive/src/lib.rs +278 -0
- package/template/rust/sia-sdk-rs/sia_sdk/CHANGELOG.md +91 -0
- package/template/rust/sia-sdk-rs/sia_sdk/Cargo.toml +59 -0
- package/template/rust/sia-sdk-rs/sia_sdk/benches/merkle_root.rs +12 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/blake2.rs +22 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/consensus.rs +767 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v1.rs +257 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v2.rs +291 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding.rs +26 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async/v2.rs +367 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async.rs +6 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encryption.rs +303 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/erasure_coding.rs +347 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/lib.rs +15 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/macros.rs +435 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/merkle.rs +112 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/merkle.rs +357 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/rpc.rs +1507 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/types.rs +146 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp.rs +7 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/seed.rs +278 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/signing.rs +236 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/common.rs +677 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/currency.rs +450 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/specifier.rs +110 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/spendpolicy.rs +778 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/utils.rs +117 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/v1.rs +1737 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/v2.rs +1726 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/work.rs +59 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types.rs +16 -0
- package/template/scripts/setup-rust.js +29 -0
- package/template/src/App.tsx +13 -0
- package/template/src/components/DevNote.tsx +21 -0
- package/template/src/components/auth/ApproveScreen.tsx +84 -0
- package/template/src/components/auth/AuthFlow.tsx +77 -0
- package/template/src/components/auth/ConnectScreen.tsx +214 -0
- package/template/src/components/auth/LoadingScreen.tsx +8 -0
- package/template/src/components/auth/RecoveryScreen.tsx +182 -0
- package/template/src/components/upload/UploadZone.tsx +314 -0
- package/template/src/index.css +9 -0
- package/template/src/lib/constants.ts +8 -0
- package/template/src/lib/format.ts +35 -0
- package/template/src/lib/hex.ts +13 -0
- package/template/src/lib/sdk.ts +25 -0
- package/template/src/lib/wasm-env.ts +5 -0
- package/template/src/main.tsx +12 -0
- package/template/src/stores/auth.ts +86 -0
- package/template/tsconfig.app.json +31 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +26 -0
- package/template/vite.config.ts +18 -0
- package/template/wasm/indexd_wasm/indexd_wasm.d.ts +309 -0
- package/template/wasm/indexd_wasm/indexd_wasm.js +1507 -0
- package/template/wasm/indexd_wasm/indexd_wasm_bg.wasm +0 -0
- package/template/wasm/indexd_wasm/package.json +31 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
use std::collections::{HashMap, VecDeque};
|
|
2
|
+
use std::fmt::{Debug, Display};
|
|
3
|
+
use std::sync::{Arc, RwLock};
|
|
4
|
+
use std::time::Duration;
|
|
5
|
+
|
|
6
|
+
use priority_queue::PriorityQueue;
|
|
7
|
+
use sia::rhp::Host;
|
|
8
|
+
use sia::signing::PublicKey;
|
|
9
|
+
use sia::types::v2::NetAddress;
|
|
10
|
+
use std::sync::Mutex;
|
|
11
|
+
use thiserror::Error;
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Default, Clone)]
|
|
14
|
+
struct RPCAverage(Option<f64>); // exponential moving average of latency in milliseconds
|
|
15
|
+
|
|
16
|
+
impl RPCAverage {
|
|
17
|
+
const ALPHA: f64 = 0.2;
|
|
18
|
+
fn add_sample(&mut self, sample: Duration) {
|
|
19
|
+
match self.0 {
|
|
20
|
+
Some(avg) => {
|
|
21
|
+
self.0 =
|
|
22
|
+
Some(Self::ALPHA * (sample.as_millis() as f64) + (1.0 - Self::ALPHA) * avg);
|
|
23
|
+
}
|
|
24
|
+
None => {
|
|
25
|
+
self.0 = Some(sample.as_millis() as f64);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn avg(&self) -> Duration {
|
|
31
|
+
match self.0 {
|
|
32
|
+
Some(avg) => Duration::from_millis(avg as u64),
|
|
33
|
+
None => Duration::from_secs(3600), // 1h if no samples
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl Display for RPCAverage {
|
|
39
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
40
|
+
self.avg().fmt(f)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl PartialEq for RPCAverage {
|
|
45
|
+
fn eq(&self, other: &Self) -> bool {
|
|
46
|
+
self.avg() == other.avg()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl Eq for RPCAverage {}
|
|
51
|
+
|
|
52
|
+
impl Ord for RPCAverage {
|
|
53
|
+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
54
|
+
self.avg().cmp(&other.avg())
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl PartialOrd for RPCAverage {
|
|
59
|
+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
60
|
+
Some(self.cmp(other))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[derive(Debug, Default, Clone)]
|
|
65
|
+
struct FailureRate(Option<f64>); // exponential moving average of failure rate
|
|
66
|
+
|
|
67
|
+
impl FailureRate {
|
|
68
|
+
const ALPHA: f64 = 0.2;
|
|
69
|
+
|
|
70
|
+
fn add_sample(&mut self, success: bool) {
|
|
71
|
+
let sample = if success { 0.0 } else { 1.0 };
|
|
72
|
+
match self.0 {
|
|
73
|
+
Some(rate) => {
|
|
74
|
+
self.0 = Some(Self::ALPHA * sample + (1.0 - Self::ALPHA) * rate);
|
|
75
|
+
}
|
|
76
|
+
None => {
|
|
77
|
+
self.0 = Some(sample);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Computes the failure rate as an integer percentage (0-100)
|
|
83
|
+
fn rate(&self) -> i64 {
|
|
84
|
+
match self.0 {
|
|
85
|
+
Some(rate) => (rate * 100.0).round() as i64,
|
|
86
|
+
None => 0, // presume no failures if no samples
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl Display for FailureRate {
|
|
92
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
93
|
+
write!(f, "{}%", self.rate())
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl PartialEq for FailureRate {
|
|
98
|
+
fn eq(&self, other: &Self) -> bool {
|
|
99
|
+
self.rate() == other.rate()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
impl Eq for FailureRate {}
|
|
104
|
+
|
|
105
|
+
impl PartialOrd for FailureRate {
|
|
106
|
+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
107
|
+
Some(self.cmp(other))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
impl Ord for FailureRate {
|
|
112
|
+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
113
|
+
self.rate().cmp(&other.rate())
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
|
118
|
+
struct HostMetric {
|
|
119
|
+
rpc_write_avg: RPCAverage,
|
|
120
|
+
rpc_read_avg: RPCAverage,
|
|
121
|
+
failure_rate: FailureRate,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
impl HostMetric {
|
|
125
|
+
fn add_write_sample(&mut self, d: Duration) {
|
|
126
|
+
self.rpc_write_avg.add_sample(d);
|
|
127
|
+
self.failure_rate.add_sample(true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn add_read_sample(&mut self, d: Duration) {
|
|
131
|
+
self.rpc_read_avg.add_sample(d);
|
|
132
|
+
self.failure_rate.add_sample(true);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn add_failure(&mut self) {
|
|
136
|
+
self.failure_rate.add_sample(false);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
impl Ord for HostMetric {
|
|
141
|
+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
142
|
+
match other.failure_rate.cmp(&self.failure_rate) {
|
|
143
|
+
// lower failure rate is higher priority
|
|
144
|
+
std::cmp::Ordering::Equal => {
|
|
145
|
+
// use average of read and write RPC times as tiebreaker
|
|
146
|
+
let avg_self = (self
|
|
147
|
+
.rpc_write_avg
|
|
148
|
+
.avg()
|
|
149
|
+
.saturating_add(self.rpc_read_avg.avg()))
|
|
150
|
+
/ 2;
|
|
151
|
+
let avg_other = (other
|
|
152
|
+
.rpc_write_avg
|
|
153
|
+
.avg()
|
|
154
|
+
.saturating_add(other.rpc_read_avg.avg()))
|
|
155
|
+
/ 2;
|
|
156
|
+
avg_other.cmp(&avg_self) // lower average latency is higher priority
|
|
157
|
+
}
|
|
158
|
+
ord => ord,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
impl PartialOrd for HostMetric {
|
|
164
|
+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
165
|
+
Some(self.cmp(other))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[derive(Debug)]
|
|
170
|
+
struct HostInfo {
|
|
171
|
+
addresses: Vec<NetAddress>,
|
|
172
|
+
good_for_upload: bool,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[derive(Debug)]
|
|
176
|
+
struct HostsInner {
|
|
177
|
+
hosts: HashMap<PublicKey, HostInfo>,
|
|
178
|
+
preferred_hosts: PriorityQueue<PublicKey, HostMetric>,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Manages a list of known hosts and their performance metrics.
|
|
182
|
+
///
|
|
183
|
+
/// It allows updating the list of hosts, recording performance samples,
|
|
184
|
+
/// and prioritizing hosts based on their metrics.
|
|
185
|
+
///
|
|
186
|
+
/// It can be safely shared across threads and cloned.
|
|
187
|
+
#[derive(Debug, Clone)]
|
|
188
|
+
pub struct Hosts {
|
|
189
|
+
inner: Arc<RwLock<HostsInner>>,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
impl Default for Hosts {
|
|
193
|
+
fn default() -> Self {
|
|
194
|
+
Self::new()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
impl Hosts {
|
|
199
|
+
pub fn new() -> Self {
|
|
200
|
+
Self {
|
|
201
|
+
inner: Arc::new(RwLock::new(HostsInner {
|
|
202
|
+
hosts: HashMap::new(),
|
|
203
|
+
preferred_hosts: PriorityQueue::new(),
|
|
204
|
+
})),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
pub fn addresses(&self, host_key: &PublicKey) -> Option<Vec<NetAddress>> {
|
|
209
|
+
let inner = self.inner.read().unwrap();
|
|
210
|
+
inner.hosts.get(host_key).map(|h| h.addresses.clone())
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Sorts a list of hosts according to their priority in the client's
|
|
214
|
+
/// preferred hosts queue. The function `f` is used to extract the
|
|
215
|
+
/// public key from each item.
|
|
216
|
+
pub fn prioritize<H, F>(&self, hosts: &mut [H], f: F)
|
|
217
|
+
where
|
|
218
|
+
F: Fn(&H) -> &PublicKey,
|
|
219
|
+
{
|
|
220
|
+
let inner = self.inner.read().unwrap();
|
|
221
|
+
let preferred_hosts = &inner.preferred_hosts;
|
|
222
|
+
hosts.sort_by(|a, b| {
|
|
223
|
+
preferred_hosts
|
|
224
|
+
.get_priority(f(b))
|
|
225
|
+
.cmp(&preferred_hosts.get_priority(f(a)))
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Updates the list of known hosts.
|
|
230
|
+
///
|
|
231
|
+
/// Existing hosts not in the new list are removed, but their metrics are retained
|
|
232
|
+
/// in case they reappear later.
|
|
233
|
+
pub fn update(&self, hosts: Vec<Host>) {
|
|
234
|
+
let mut inner = self.inner.write().unwrap();
|
|
235
|
+
inner.hosts.clear();
|
|
236
|
+
for host in hosts {
|
|
237
|
+
inner.hosts.insert(
|
|
238
|
+
host.public_key,
|
|
239
|
+
HostInfo {
|
|
240
|
+
addresses: host.addresses,
|
|
241
|
+
good_for_upload: host.good_for_upload,
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
if !inner.preferred_hosts.contains(&host.public_key) {
|
|
245
|
+
inner
|
|
246
|
+
.preferred_hosts
|
|
247
|
+
.push(host.public_key, HostMetric::default());
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Returns the number of known hosts that are good for upload.
|
|
253
|
+
pub fn available_for_upload(&self) -> usize {
|
|
254
|
+
let inner = self.inner.read().unwrap();
|
|
255
|
+
inner
|
|
256
|
+
.hosts
|
|
257
|
+
.iter()
|
|
258
|
+
.filter(|(_, h)| h.good_for_upload)
|
|
259
|
+
.count()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Returns a list of all known hosts, sorted by priority.
|
|
263
|
+
pub fn hosts(&self) -> Vec<PublicKey> {
|
|
264
|
+
let inner = self.inner.read().unwrap();
|
|
265
|
+
let preferred_hosts = &inner.preferred_hosts;
|
|
266
|
+
let mut hosts = inner.hosts.iter().map(|h| *h.0).collect::<Vec<_>>();
|
|
267
|
+
|
|
268
|
+
hosts.sort_by(|a, b| {
|
|
269
|
+
preferred_hosts
|
|
270
|
+
.get_priority(b)
|
|
271
|
+
.cmp(&preferred_hosts.get_priority(a))
|
|
272
|
+
});
|
|
273
|
+
hosts
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Creates a queue of hosts that are good to upload to for sequential
|
|
277
|
+
/// access sorted by priority.
|
|
278
|
+
pub fn upload_queue(&self) -> HostQueue {
|
|
279
|
+
let inner = self.inner.read().unwrap();
|
|
280
|
+
let preferred_hosts = &inner.preferred_hosts;
|
|
281
|
+
let mut hosts = inner
|
|
282
|
+
.hosts
|
|
283
|
+
.iter()
|
|
284
|
+
.filter_map(|(hk, h)| h.good_for_upload.then_some(*hk))
|
|
285
|
+
.collect::<Vec<_>>();
|
|
286
|
+
|
|
287
|
+
hosts.sort_by(|a, b| {
|
|
288
|
+
preferred_hosts
|
|
289
|
+
.get_priority(b)
|
|
290
|
+
.cmp(&preferred_hosts.get_priority(a))
|
|
291
|
+
});
|
|
292
|
+
HostQueue::new(hosts)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// Adds a failure for the given host, updating its metrics and priority.
|
|
296
|
+
pub fn add_failure(&self, host_key: &PublicKey) {
|
|
297
|
+
let mut inner = self.inner.write().unwrap();
|
|
298
|
+
inner
|
|
299
|
+
.preferred_hosts
|
|
300
|
+
.change_priority_by(host_key, |metric| {
|
|
301
|
+
metric.add_failure();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// Adds a read sample for the given host, updating its metrics and priority.
|
|
306
|
+
pub fn add_read_sample(&self, host_key: &PublicKey, duration: Duration) {
|
|
307
|
+
let mut inner = self.inner.write().unwrap();
|
|
308
|
+
inner
|
|
309
|
+
.preferred_hosts
|
|
310
|
+
.change_priority_by(host_key, |metric| {
|
|
311
|
+
metric.add_read_sample(duration);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Adds a write sample for the given host, updating its metrics and priority.
|
|
316
|
+
pub fn add_write_sample(&self, host_key: &PublicKey, duration: Duration) {
|
|
317
|
+
let mut inner = self.inner.write().unwrap();
|
|
318
|
+
inner
|
|
319
|
+
.preferred_hosts
|
|
320
|
+
.change_priority_by(host_key, |metric| {
|
|
321
|
+
metric.add_write_sample(duration);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[derive(Debug, Error)]
|
|
327
|
+
pub enum QueueError {
|
|
328
|
+
#[error("no more hosts available")]
|
|
329
|
+
NoMoreHosts,
|
|
330
|
+
#[error("not enough initial hosts")]
|
|
331
|
+
InsufficientHosts,
|
|
332
|
+
#[error("client closed")]
|
|
333
|
+
Closed,
|
|
334
|
+
#[error("internal mutex error")]
|
|
335
|
+
MutexError,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// Maximum number of times to retry a host before giving up
|
|
339
|
+
const MAX_HOST_RETRIES: usize = 3;
|
|
340
|
+
|
|
341
|
+
#[derive(Debug)]
|
|
342
|
+
struct HostQueueInner {
|
|
343
|
+
hosts: VecDeque<PublicKey>,
|
|
344
|
+
attempts: HashMap<PublicKey, usize>,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// A thread-safe queue of host public keys.
|
|
348
|
+
#[derive(Debug, Clone)]
|
|
349
|
+
pub struct HostQueue {
|
|
350
|
+
inner: Arc<Mutex<HostQueueInner>>,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
impl HostQueue {
|
|
354
|
+
pub fn new(hosts: Vec<PublicKey>) -> Self {
|
|
355
|
+
Self {
|
|
356
|
+
inner: Arc::new(Mutex::new(HostQueueInner {
|
|
357
|
+
hosts: VecDeque::from(hosts),
|
|
358
|
+
attempts: HashMap::new(),
|
|
359
|
+
})),
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
pub fn pop_front(&self) -> Result<(PublicKey, usize), QueueError> {
|
|
364
|
+
let mut inner = self.inner.lock().map_err(|_| QueueError::MutexError)?;
|
|
365
|
+
let host_key = inner.hosts.pop_front().ok_or(QueueError::NoMoreHosts)?;
|
|
366
|
+
|
|
367
|
+
let attempts = inner.attempts.get(&host_key).cloned().unwrap_or(0);
|
|
368
|
+
Ok((host_key, attempts + 1))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
pub fn pop_n(&self, n: usize) -> Result<Vec<(PublicKey, usize)>, QueueError> {
|
|
372
|
+
let mut inner = self.inner.lock().map_err(|_| QueueError::MutexError)?;
|
|
373
|
+
if inner.hosts.len() < n {
|
|
374
|
+
return Err(QueueError::NoMoreHosts);
|
|
375
|
+
}
|
|
376
|
+
let mut result = Vec::with_capacity(n);
|
|
377
|
+
for _ in 0..n {
|
|
378
|
+
let host_key = inner.hosts.pop_front().ok_or(QueueError::NoMoreHosts)?;
|
|
379
|
+
let attempts = inner.attempts.get(&host_key).cloned().unwrap_or(0);
|
|
380
|
+
result.push((host_key, attempts + 1));
|
|
381
|
+
}
|
|
382
|
+
Ok(result)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
pub fn retry(&self, host: PublicKey) -> Result<(), QueueError> {
|
|
386
|
+
let mut inner = self.inner.lock().map_err(|_| QueueError::MutexError)?;
|
|
387
|
+
let attempts = inner.attempts.get(&host).cloned().unwrap_or(0);
|
|
388
|
+
|
|
389
|
+
// Only retry if we haven't exceeded the maximum retry limit
|
|
390
|
+
if attempts < MAX_HOST_RETRIES {
|
|
391
|
+
inner.hosts.push_back(host);
|
|
392
|
+
inner
|
|
393
|
+
.attempts
|
|
394
|
+
.entry(host)
|
|
395
|
+
.and_modify(|e| *e += 1)
|
|
396
|
+
.or_insert(1);
|
|
397
|
+
}
|
|
398
|
+
Ok(())
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#[cfg(test)]
|
|
403
|
+
mod test {
|
|
404
|
+
use sia::signing::PrivateKey;
|
|
405
|
+
|
|
406
|
+
use super::*;
|
|
407
|
+
|
|
408
|
+
#[test]
|
|
409
|
+
fn test_failure_rate() {
|
|
410
|
+
let mut fr = FailureRate::default();
|
|
411
|
+
assert_eq!(fr.rate(), 0, "initial failure rate should be 0%");
|
|
412
|
+
fr.add_sample(false);
|
|
413
|
+
assert_eq!(fr.rate(), 100, "initial failure should be 100%");
|
|
414
|
+
|
|
415
|
+
for _ in 0..5 {
|
|
416
|
+
fr.add_sample(true);
|
|
417
|
+
}
|
|
418
|
+
assert!(
|
|
419
|
+
fr.rate() < 100,
|
|
420
|
+
"failure rate should decrease after successes"
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
let mut fr2 = FailureRate::default();
|
|
424
|
+
for _ in 0..5 {
|
|
425
|
+
fr2.add_sample(true);
|
|
426
|
+
}
|
|
427
|
+
assert_eq!(
|
|
428
|
+
fr2.rate(),
|
|
429
|
+
0,
|
|
430
|
+
"failure rate should be 0% after only successes"
|
|
431
|
+
);
|
|
432
|
+
assert_eq!(
|
|
433
|
+
fr.cmp(&fr2),
|
|
434
|
+
std::cmp::Ordering::Greater,
|
|
435
|
+
"higher failure rate should be greater"
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#[test]
|
|
440
|
+
fn test_rpc_average() {
|
|
441
|
+
let mut avg = RPCAverage::default();
|
|
442
|
+
avg.add_sample(Duration::from_millis(100));
|
|
443
|
+
assert_eq!(
|
|
444
|
+
avg.avg(),
|
|
445
|
+
Duration::from_millis(100),
|
|
446
|
+
"initial average should be first sample"
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
avg.add_sample(Duration::from_millis(200));
|
|
450
|
+
assert!(
|
|
451
|
+
avg.avg() > Duration::from_millis(100),
|
|
452
|
+
"average should increase after higher sample"
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
avg.add_sample(Duration::from_millis(50));
|
|
456
|
+
assert!(
|
|
457
|
+
avg.avg() < Duration::from_millis(200),
|
|
458
|
+
"average should decrease after lower sample"
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
let mut avg2 = RPCAverage::default();
|
|
462
|
+
avg2.add_sample(Duration::from_millis(150));
|
|
463
|
+
assert_eq!(
|
|
464
|
+
avg.cmp(&avg2),
|
|
465
|
+
std::cmp::Ordering::Less,
|
|
466
|
+
"lower average should be lesser"
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
#[test]
|
|
471
|
+
fn test_host_metric_ordering() {
|
|
472
|
+
let mut hosts = vec![
|
|
473
|
+
HostMetric::default(),
|
|
474
|
+
HostMetric::default(),
|
|
475
|
+
HostMetric::default(),
|
|
476
|
+
];
|
|
477
|
+
hosts[0].failure_rate.add_sample(false);
|
|
478
|
+
hosts[1].failure_rate.add_sample(false);
|
|
479
|
+
hosts[2].failure_rate.add_sample(false);
|
|
480
|
+
for _ in 0..10 {
|
|
481
|
+
hosts[0].failure_rate.add_sample(true);
|
|
482
|
+
}
|
|
483
|
+
for _ in 0..5 {
|
|
484
|
+
hosts[1].failure_rate.add_sample(true);
|
|
485
|
+
}
|
|
486
|
+
hosts.sort();
|
|
487
|
+
|
|
488
|
+
let rates = hosts
|
|
489
|
+
.into_iter()
|
|
490
|
+
.rev()
|
|
491
|
+
.map(|h| h.failure_rate)
|
|
492
|
+
.collect::<Vec<FailureRate>>();
|
|
493
|
+
assert!(
|
|
494
|
+
rates.is_sorted(),
|
|
495
|
+
"hosts should be sorted by failure rate desc"
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
let mut hosts = vec![
|
|
499
|
+
HostMetric::default(),
|
|
500
|
+
HostMetric::default(),
|
|
501
|
+
HostMetric::default(),
|
|
502
|
+
];
|
|
503
|
+
hosts[0]
|
|
504
|
+
.rpc_write_avg
|
|
505
|
+
.add_sample(Duration::from_millis(100));
|
|
506
|
+
hosts[1]
|
|
507
|
+
.rpc_write_avg
|
|
508
|
+
.add_sample(Duration::from_millis(1000));
|
|
509
|
+
hosts[2]
|
|
510
|
+
.rpc_write_avg
|
|
511
|
+
.add_sample(Duration::from_millis(500));
|
|
512
|
+
hosts.sort();
|
|
513
|
+
|
|
514
|
+
let rates = hosts
|
|
515
|
+
.into_iter()
|
|
516
|
+
.rev()
|
|
517
|
+
.map(|h| h.rpc_write_avg)
|
|
518
|
+
.collect::<Vec<RPCAverage>>();
|
|
519
|
+
assert!(
|
|
520
|
+
rates.is_sorted(),
|
|
521
|
+
"hosts should be sorted by rpc write avg desc"
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#[test]
|
|
526
|
+
fn test_host_priority_queue() {
|
|
527
|
+
let mut pq = PriorityQueue::<PublicKey, HostMetric>::new();
|
|
528
|
+
let mut hosts = vec![];
|
|
529
|
+
for _ in 0..5 {
|
|
530
|
+
let pk = PrivateKey::from_seed(&rand::random()).public_key();
|
|
531
|
+
pq.push(pk, HostMetric::default());
|
|
532
|
+
hosts.push(pk);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// initially, the order is the same as insertion
|
|
536
|
+
assert_eq!(pq.peek().unwrap().0, &hosts[0]);
|
|
537
|
+
|
|
538
|
+
// fourth host has a sample, should have highest priority
|
|
539
|
+
pq.change_priority_by(&hosts[3], |metric| {
|
|
540
|
+
metric.add_write_sample(Duration::from_millis(100));
|
|
541
|
+
});
|
|
542
|
+
assert_eq!(pq.peek().unwrap().0, &hosts[3]);
|
|
543
|
+
|
|
544
|
+
// add a faster sample to second host, should have higher priority than fourth
|
|
545
|
+
pq.change_priority_by(&hosts[1], |metric| {
|
|
546
|
+
metric.add_read_sample(Duration::from_millis(50));
|
|
547
|
+
});
|
|
548
|
+
assert_eq!(pq.peek().unwrap().0, &hosts[1]);
|
|
549
|
+
|
|
550
|
+
// add a failure to the second host, should lower its priority below fourth
|
|
551
|
+
pq.change_priority_by(&hosts[1], |metric| {
|
|
552
|
+
metric.add_failure();
|
|
553
|
+
});
|
|
554
|
+
assert_eq!(pq.peek().unwrap().0, &hosts[3]);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
#[test]
|
|
558
|
+
fn test_upload_queue() {
|
|
559
|
+
let hosts_manager = Hosts::new();
|
|
560
|
+
|
|
561
|
+
let hk1 = PrivateKey::from_seed(&rand::random()).public_key();
|
|
562
|
+
let hk2 = PrivateKey::from_seed(&rand::random()).public_key();
|
|
563
|
+
let hk3 = PrivateKey::from_seed(&rand::random()).public_key();
|
|
564
|
+
|
|
565
|
+
hosts_manager.update(vec![
|
|
566
|
+
Host {
|
|
567
|
+
public_key: hk1,
|
|
568
|
+
addresses: vec![],
|
|
569
|
+
country_code: String::new(),
|
|
570
|
+
latitude: 0.0,
|
|
571
|
+
longitude: 0.0,
|
|
572
|
+
good_for_upload: false,
|
|
573
|
+
},
|
|
574
|
+
Host {
|
|
575
|
+
public_key: hk2,
|
|
576
|
+
addresses: vec![],
|
|
577
|
+
country_code: String::new(),
|
|
578
|
+
latitude: 0.0,
|
|
579
|
+
longitude: 0.0,
|
|
580
|
+
good_for_upload: true,
|
|
581
|
+
},
|
|
582
|
+
Host {
|
|
583
|
+
public_key: hk3,
|
|
584
|
+
addresses: vec![],
|
|
585
|
+
country_code: String::new(),
|
|
586
|
+
latitude: 0.0,
|
|
587
|
+
longitude: 0.0,
|
|
588
|
+
good_for_upload: false,
|
|
589
|
+
},
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
let queue = hosts_manager.upload_queue();
|
|
593
|
+
let (first, _) = queue.pop_front().unwrap();
|
|
594
|
+
assert_eq!(first, hk2);
|
|
595
|
+
assert!(
|
|
596
|
+
queue.pop_front().is_err(),
|
|
597
|
+
"queue should only have one host"
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[test]
|
|
602
|
+
fn test_host_queue_pop_n() {
|
|
603
|
+
let hosts: Vec<_> = (0..5)
|
|
604
|
+
.map(|_| PrivateKey::from_seed(&rand::random()).public_key())
|
|
605
|
+
.collect();
|
|
606
|
+
let queue = HostQueue::new(hosts.clone());
|
|
607
|
+
|
|
608
|
+
// pop 3 hosts
|
|
609
|
+
let popped = queue.pop_n(3).expect("should pop 3 hosts");
|
|
610
|
+
assert_eq!(popped.len(), 3);
|
|
611
|
+
assert_eq!(popped[0].0, hosts[0]);
|
|
612
|
+
assert_eq!(popped[1].0, hosts[1]);
|
|
613
|
+
assert_eq!(popped[2].0, hosts[2]);
|
|
614
|
+
|
|
615
|
+
// all should have attempts = 1
|
|
616
|
+
assert!(popped.iter().all(|(_, attempts)| *attempts == 1));
|
|
617
|
+
|
|
618
|
+
// pop remaining 2
|
|
619
|
+
let popped = queue.pop_n(2).expect("should pop 2 hosts");
|
|
620
|
+
assert_eq!(popped.len(), 2);
|
|
621
|
+
assert_eq!(popped[0].0, hosts[3]);
|
|
622
|
+
assert_eq!(popped[1].0, hosts[4]);
|
|
623
|
+
|
|
624
|
+
// queue should be empty
|
|
625
|
+
assert!(matches!(queue.pop_front(), Err(QueueError::NoMoreHosts)));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
#[test]
|
|
629
|
+
fn test_host_queue_pop_n_not_enough_hosts() {
|
|
630
|
+
let hosts: Vec<_> = (0..3)
|
|
631
|
+
.map(|_| PrivateKey::from_seed(&rand::random()).public_key())
|
|
632
|
+
.collect();
|
|
633
|
+
let queue = HostQueue::new(hosts.clone());
|
|
634
|
+
|
|
635
|
+
// try to pop more than available
|
|
636
|
+
let result = queue.pop_n(5);
|
|
637
|
+
assert!(matches!(result, Err(QueueError::NoMoreHosts)));
|
|
638
|
+
|
|
639
|
+
// queue should be unchanged - can still pop all 3
|
|
640
|
+
let popped = queue.pop_n(3).expect("should pop 3");
|
|
641
|
+
assert_eq!(popped.len(), 3);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
#[test]
|
|
645
|
+
fn test_host_queue_pop_n_zero() {
|
|
646
|
+
let hosts: Vec<_> = (0..3)
|
|
647
|
+
.map(|_| PrivateKey::from_seed(&rand::random()).public_key())
|
|
648
|
+
.collect();
|
|
649
|
+
let queue = HostQueue::new(hosts);
|
|
650
|
+
|
|
651
|
+
// pop 0 hosts should succeed with empty vec
|
|
652
|
+
let popped = queue.pop_n(0).expect("should succeed");
|
|
653
|
+
assert!(popped.is_empty());
|
|
654
|
+
|
|
655
|
+
// queue should be unchanged - can still pop 3
|
|
656
|
+
let popped = queue.pop_n(3).expect("should pop 3");
|
|
657
|
+
assert_eq!(popped.len(), 3);
|
|
658
|
+
}
|
|
659
|
+
}
|