-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathmain.rs
More file actions
4935 lines (4506 loc) · 166 KB
/
Copy pathmain.rs
File metadata and controls
4935 lines (4506 loc) · 166 KB
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2026 Oxide Computer Company
use anyhow::{Result, anyhow, bail};
use bytes::Bytes;
use clap::Parser;
use futures::StreamExt;
use futures::stream::FuturesOrdered;
use human_bytes::human_bytes;
use indicatif::{ProgressBar, ProgressStyle};
use oximeter::types::ProducerRegistry;
use rand::prelude::*;
use rand_chacha::rand_core::SeedableRng;
use serde::{Deserialize, Serialize};
use signal_hook::consts::signal::*;
use signal_hook_tokio::Signals;
use slog::{Logger, info, o, warn};
use std::fmt;
use std::io::Write;
use std::net::{IpAddr, SocketAddr};
use std::num::NonZeroU64;
use std::path::PathBuf;
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
};
use tokio::sync::mpsc;
use tokio::time::{Duration, Instant};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
mod cli;
mod protocol;
mod stats;
pub use stats::*;
use crucible::volume::VolumeBuilder;
use crucible::volume::VolumeExtentInfo;
use crucible::*;
use crucible_client_types::RegionExtentInfo;
use crucible_protocol::CRUCIBLE_MESSAGE_VERSION;
use dsc_client::{Client, types::DownstairsState};
use repair_client::Client as repair_client;
/*
* The various tests this program supports.
*/
/// Client: A Crucible Upstairs test program
#[allow(clippy::derive_partial_eq_without_eq, clippy::upper_case_acronyms)]
#[derive(Debug, Parser)]
#[clap(name = "workload", term_width = 80)]
#[clap(about = "Workload the program will execute.", long_about = None)]
enum Workload {
Balloon,
Big,
Biggest,
/// Send many random writes, then report how long a single read takes
Bufferbloat {
#[clap(flatten)]
cfg: BufferbloatWorkload,
},
Burst,
/// Starts a CLI client
Cli {
/// Address the cli client will try to connect to
#[clap(long, short, default_value = "0.0.0.0:5050", action)]
attach: SocketAddr,
},
/// Start a server and listen on the given address and port
CliServer {
/// Address for the cliserver to listen on
#[clap(long, short, default_value = "0.0.0.0", action)]
listen: IpAddr,
/// Port for the cliserver to listen on
#[clap(long, short, default_value_t = 5050, action)]
port: u16,
},
Deactivate,
Demo,
Dep,
Dirty,
/// Write to one random block in every extent, then flush.
FastFill,
Fill {
/// Don't do the verify step after filling the disk.
#[clap(long, action)]
skip_verify: bool,
},
Generic,
/// Do random read and flush IOs
GenericRead,
Nothing,
One,
/// Measure performance with a random read workload
RandRead {
#[clap(flatten)]
cfg: RandReadWriteWorkload,
},
/// Measure performance with a random write workload
RandWrite {
#[clap(flatten)]
cfg: RandReadWriteWorkload,
},
/// Run IO, and as soon as we get a final ACK, drop the volume to
/// see if we can leave IOs outstanding on one of the downstairs.
/// This test works best if one of the downstairs is running with
/// lossy option set, which will make it go slower than the others.
Repair,
/// Test the downstairs replay path.
/// Stop a downstairs, then run some IO, then start that downstairs back
/// up. Verify all IO to all downstairs finishes.
/// This test requires a dsc server to control the downstairs.
Replay,
/// Test the downstairs replacement path.
/// Run IO to the upstairs, then replace a downstairs, then run
/// more IO and verify it all works as expected.
Replace {
/// Before each replacement, do a fill of the disk so the replace will
/// have to copy the entire disk..
#[clap(long, action)]
fast_fill: bool,
/// The address:port of a running downstairs for replacement
#[clap(long, action)]
replacement: SocketAddr,
},
/// Test that we can replace a downstairs when the upstairs is not active.
ReplaceBeforeActive {
/// The address:port of a running downstairs for replacement
#[clap(long, action)]
replacement: SocketAddr,
},
/// Test replacement of a downstairs while doing the initial reconciliation.
ReplaceReconcile {
/// The address:port of a running downstairs for replacement
#[clap(long, action)]
replacement: SocketAddr,
},
Span,
Verify,
Version,
/// Select a random offset/length, then Write/Flush/Read that
/// offset/length.
WFR,
/// Do reads and writes, keep going if there are errors.
Yolo,
}
#[derive(Debug, Parser)]
#[clap(name = "client", term_width = 80)]
#[clap(about = "A Crucible upstairs test client", long_about = None)]
pub struct Opt {
// TLS options
#[clap(long, action)]
cert_pem: Option<String>,
/// For tests that support it, run until a SIGUSR1 signal is received.
#[clap(long, global = true, action, conflicts_with = "count")]
continuous: bool,
/// IP:Port for the upstairs control http server
#[clap(long, global = true, action)]
control: Option<SocketAddr>,
/// For tests that support it, pass this count value for the number
/// of loops the test should do.
#[clap(short, long, global = true, action)]
count: Option<usize>,
/// IP:Port for a dsc server.
/// Some tests require a dsc enpoint to control the downstairs.
/// A dsc endpoint can also be used to construct the initial Volume.
#[clap(long, global = true, action)]
dsc: Option<SocketAddr>,
/// How long to wait before the auto flush check fires
#[clap(long, global = true, action)]
flush_timeout: Option<f32>,
#[clap(short, global = true, long = "gen", default_value_t = 0, action)]
generation: u64,
/// The key for an encrypted downstairs.
#[clap(short, global = true, long, action)]
key: Option<String>,
/// TLS option
#[clap(long, action)]
key_pem: Option<String>,
/// Spin up a dropshot endpoint and serve metrics from it.
/// This will use the values in metric-register and metric-collect
#[clap(long, global = true, action)]
metrics: bool,
/// IP:Port for the Oximeter register address, which is Nexus.
#[clap(long, global = true, default_value = "127.0.0.1:12221", action)]
metric_register: SocketAddr,
/// IP:Port for the Oximeter listen address
#[clap(long, global = true, default_value = "127.0.0.1:55443", action)]
metric_collect: SocketAddr,
/// Don't print out IOs as we do them.
#[clap(long, global = true, action)]
quiet: bool,
/// quit after all crucible work queues are empty.
#[clap(short, global = true, long, action, conflicts_with = "stable")]
quit: bool,
/// For the verify and yolo tests, if this option is included we will allow
/// the write log range of data to pass the verify_volume check.
#[clap(long, global = true, action)]
range: bool,
/// Set the read_only option when starting the upstairs.
/// Note that setting this won't prevent you from sending writes to the
/// downstairs. You are responsible for dealing with the fallout.
#[clap(long, global = true, action)]
read_only: bool,
/// Retry for activate, as long as it takes. If we pass this arg, the
/// test will retry the initial activate command as long as it takes.
#[clap(long, global = true, action)]
retry_activate: bool,
/// TLS option
#[clap(long, action)]
root_cert_pem: Option<String>,
/// Quit only after all crucible work queues are empty and all downstairs
/// are reporting active.
#[clap(global = true, long, action, conflicts_with = "quit")]
stable: bool,
/// The IP:Port where each downstairs is listening.
#[clap(short, long, global = true, action)]
target: Vec<SocketAddr>,
/// A UUID to use for the upstairs.
#[clap(long, global = true, action)]
uuid: Option<Uuid>,
/// Read in a VCR from a file for use in constructing a volume
#[clap(long, global = true, value_name = "VCRFILE", action)]
vcr_file: Option<PathBuf>,
/// In addition to any tests, verify the volume on startup.
/// This only has value if verify_in is also set.
#[clap(long, global = true, requires = "verify_in")]
verify_at_start: bool,
/// In addition to any tests, verify the volume after the tests
/// have completed. If you don't supply a verify_in file, then the
/// verify will only check what this test run has written.
#[clap(long, global = true, action)]
verify_at_end: bool,
/// For tests that support it, load the expected write count from
/// the provided file. The addition of a --verify-at-start option will
/// also have the test verify what it imports from the file is valid.
#[clap(long, global = true, value_name = "INFILE", action)]
verify_in: Option<PathBuf>,
/// For tests that support it, save the write count into the
/// provided file.
#[clap(long, global = true, value_name = "FILE", action)]
verify_out: Option<PathBuf>,
/// A test workload that crutest will execute.
#[clap(subcommand)]
workload: Workload,
}
pub fn opts() -> Result<Opt> {
let opt: Opt = Opt::parse();
Ok(opt)
}
#[derive(Copy, Clone, Debug, clap::Args)]
struct BufferbloatWorkload {
/// Size in blocks of each IO
#[clap(long, default_value_t = 1, action)]
io_size: usize,
/// Number of outstanding IOs at the same time.
#[clap(long, default_value_t = 1, action)]
io_depth: usize,
/// Number of seconds to run
#[clap(long, default_value_t = 10, action)]
time: u64,
/// Print the volume log (at `INFO` level) to `stderr`
///
/// If this is not set, then only `ERROR` messages are logged
///
/// By default, this is not set, because the volume log is noisy and
/// interrupts our intentional logging.
#[clap(short, long)]
verbose: bool,
}
#[derive(Copy, Clone, Debug, clap::Args)]
struct RandReadWriteWorkload {
/// Size in blocks of each IO
#[clap(long, default_value_t = 1, action)]
io_size: usize,
/// Number of outstanding IOs at the same time.
#[clap(long, default_value_t = 1, action)]
io_depth: usize,
/// Number of seconds to run
#[clap(long, default_value_t = 60, action)]
time: u64,
/// Completely fill the disk with random data first
#[clap(long)]
fill: bool,
/// Print the volume log (at `INFO` level) to `stderr`
///
/// If this is not set, then only `ERROR` messages are logged
///
/// By default, this is not set, because the volume log is noisy and
/// interrupts our intentional logging.
#[clap(short, long)]
verbose: bool,
/// Print values in bytes, without human-readable units
#[clap(short, long)]
raw: bool,
/// Time per sample printed to the output
#[clap(long, default_value_t = 1.0)]
sample_time: f64,
/// Number of subsamples per sample
#[clap(long, default_value_t = NonZeroU64::new(10).unwrap())]
subsample_count: NonZeroU64,
}
/// Mode flags for `rand_write_read_workload`
#[derive(Copy, Clone, Debug)]
enum RandReadWriteMode {
Read,
Write,
}
/// Configuration for `rand_read_write_workload`
#[derive(Copy, Clone, Debug)]
struct RandReadWriteConfig {
mode: RandReadWriteMode,
encrypted: bool,
io_depth: usize,
blocks_per_io: usize,
/// Print raw bytes, without human-friendly formatting
raw: bool,
/// Total amount of time to run
time_secs: u64,
/// Rate at which we should print samples
sample_time_secs: f64,
/// Number of subsamples for each `sample_time_secs`, for standard deviation
subsample_count: NonZeroU64,
fill: bool,
}
impl RandReadWriteConfig {
fn new(
cfg: RandReadWriteWorkload,
encrypted: bool,
mode: RandReadWriteMode,
) -> Self {
RandReadWriteConfig {
encrypted,
io_depth: cfg.io_depth,
blocks_per_io: cfg.io_size,
time_secs: cfg.time,
raw: cfg.raw,
sample_time_secs: cfg.sample_time,
subsample_count: cfg.subsample_count,
fill: cfg.fill,
mode,
}
}
}
/// For tests that need to pick an operation to do.
#[derive(Debug, Copy, Clone)]
enum Op {
Flush,
Read,
Write,
}
/// Configuration for `bufferbloat_workload`
#[derive(Copy, Clone, Debug)]
struct BufferbloatConfig {
encrypted: bool,
io_depth: usize,
blocks_per_io: usize,
time_secs: u64,
}
impl BufferbloatConfig {
fn new(cfg: BufferbloatWorkload, encrypted: bool) -> Self {
BufferbloatConfig {
encrypted,
io_depth: cfg.io_depth,
blocks_per_io: cfg.io_size,
time_secs: cfg.time,
}
}
}
/*
* All the tests need this basic info about the disk.
* Not all tests make use of the write_log yet, but perhaps someday..
*/
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DiskInfo {
volume_info: VolumeExtentInfo,
write_log: WriteLog,
max_block_io: usize,
}
impl DiskInfo {
pub fn block_size(&self) -> u64 {
self.volume_info.block_size
}
}
/*
* All the tests need this basic set of information about the disk.
*/
async fn get_disk_info(volume: &Volume) -> Result<DiskInfo, CrucibleError> {
/*
* These query requests have the side effect of preventing the test from
* starting before the upstairs is ready.
*/
let volume_info = volume.volume_extent_info().await?;
let total_size = volume.total_size().await?;
let total_blocks = (total_size / volume_info.block_size) as usize;
/*
* Limit the max IO size (in blocks) to be 1MiB or the size
* of the volume, whichever is smaller
*/
const MAX_IO_BYTES: usize = 1024 * 1024;
let mut max_block_io = MAX_IO_BYTES / volume_info.block_size as usize;
if total_blocks < max_block_io {
max_block_io = total_blocks;
}
println!(
"Disk: sv:{} bs:{} ts:{} tb:{} max_io:{} or {}",
volume_info.volumes.len(),
volume_info.block_size,
total_size,
total_blocks,
max_block_io,
(max_block_io as u64 * volume_info.block_size),
);
/*
* Create the write log that tracks the number of writes to each block,
* so we can know what to expect for reads.
*/
let write_log = WriteLog::new(total_blocks);
Ok(DiskInfo {
volume_info,
write_log,
max_block_io,
})
}
/**
* The write log is a recording of the number of times we have written to
* a specific block (index in the Vec). The write count is used to generate
* a known pattern to either fill the block with, or to expect from the
* block when reading.
*
* This is fine for an initial fill/verify framework of sorts, but there
* are many kinds of errors this will not find. There are also many high
* performance better coverage kinds of data integrity tests, and the intent
* here is to balance urgency with rigor in that we can make use of external
* tests for the more complicated cases, and catch the easy ones here.
*
* In addition to the current write count, we make a second copy of the
* write count when the commit method is called. This can be used to record
* the write count of a disk at a specific time (like a flush) and then
* later used to verify that a given block has data in it from at minimum
* that commit, but up to the current write count.
*
* The "seed" is the current counter as a u8 for a given block.
*/
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WriteLog {
count_cur: Vec<u32>,
count_min: Vec<u32>,
}
impl WriteLog {
pub fn new(size: usize) -> Self {
let count_cur = vec![0_u32; size];
let count_min = vec![0_u32; size];
WriteLog {
count_cur,
count_min,
}
}
pub fn is_empty(&self) -> bool {
self.count_cur.is_empty()
}
pub fn len(&self) -> usize {
self.count_cur.len()
}
// If the write count is zero then we have no record of what this
// block contains.
pub fn unwritten(&self, index: usize) -> bool {
self.count_cur[index] == 0
}
// This is called when we are about to write to a block and we want
// to indicate that the write counter should be updated.
pub fn update_wc(&mut self, index: usize) {
assert!(self.count_cur[index] >= self.count_min[index]);
// TODO: handle more than u32 max writes to the same location.
self.count_cur[index] = self.count_cur[index].wrapping_add(1);
}
pub fn undo_update_wc(&mut self, index: usize) {
assert!(self.count_cur[index] >= self.count_min[index]);
// TODO: handle more than u32 max writes to the same location.
self.count_cur[index] = self.count_cur[index].wrapping_sub(1);
}
// This returns the value we should expect to find at the given index,
// and will fit in a u8. If we are using the seed to fill a write
// volume, then update_wc() should be called first to increment the
// counter before we get a new seed.
fn get_seed(&self, index: usize) -> u8 {
(self.count_cur[index] & 0xff) as u8
}
// This is called before a test where we expect to be recovering and we
// want to record the current write log values as a minimum of what
// we expect the counters to be.
pub fn commit(&mut self) {
self.count_min = self.count_cur.clone();
}
// In repair/recovery, when there is IO after a flush, it's possible
// that data never made it to storage. We are asking to verify a
// given value is in the range of possible values that could exist
// for the index. Any valid value in the range between count_min to
// count_cur.
//
// For this to work correctly, the test must issue a commit() of the
// WriteLog when it knows that the current write count is the minimum.
// This is only acceptable in a very specific recovery/repair situation
// and not part of a normal test.
//
// If update is set to true, then we also change the count_cur to match
// the given value (corrected to be a u32 and not a u8).
pub fn validate_seed_range(
&mut self,
index: usize,
value: u8,
update: bool,
) -> bool {
let res;
if self.count_min[index] & 0xff > self.count_cur[index] & 0xff {
// Special case when the min and max cross a u8 boundary.
let min_adjusted_value = (self.count_min[index] & 0xff) as u8;
let cur_adjusted_value = (self.count_cur[index] & 0xff) as u8;
println!(
"SPEC v:{} min_av:{} cur_av:{} cm:{} cc:{}",
value,
min_adjusted_value,
cur_adjusted_value,
self.count_min[index],
self.count_cur[index],
);
let mut new_cur = value as u32;
if value >= min_adjusted_value {
res = true;
// Figure out the delta between value and the minimum,
// then add that to the non-adjusted minimum and make
// that our new maximum.
let delta = (value - min_adjusted_value) as u32;
new_cur = self.count_min[index] + delta;
println!("new cur is {} from min", new_cur);
} else if value <= cur_adjusted_value {
res = true;
// Figure out the delta between value and the max(cur)
// and then subtract that from the current cur to set
// our new expected value.
let delta = (cur_adjusted_value - value) as u32;
new_cur = self.count_cur[index] - delta;
println!("new cur is {} from cur", new_cur);
} else {
// The value in not in the expected range
res = false;
}
// If update requested (and we are in the range) then update
// the counter to reflect the new "max".
if update && res {
if new_cur != self.count_cur[index] {
println!("Adjusting new cur to {}", new_cur);
self.count_cur[index] = new_cur;
} else {
println!("No adjustment necessary");
}
}
} else {
// The regular case, just be sure we are between the
// lower and upper expected values.
let shift = self.count_min[index] / 256;
let s_value = value as u32 + (256 * shift);
res = s_value >= self.count_min[index]
&& s_value <= self.count_cur[index];
// Only update if requested and the range was valid.
if update && res && self.count_cur[index] != s_value {
println!(
"Update block {} to {} (min:{} max:{} res:{})",
index,
s_value,
self.count_min[index],
self.count_cur[index],
res,
);
self.count_cur[index] = s_value;
}
}
res
}
// Set the current write count to a specific value.
// You should only be using this if you know what you are doing.
#[cfg(test)]
fn set_wc(&mut self, index: usize, value: u32) {
self.count_cur[index] = value;
}
}
async fn load_write_log(
volume: &Volume,
di: &mut DiskInfo,
vi: PathBuf,
verify: bool,
) -> Result<()> {
/*
* Fill the write count from a provided file.
*/
di.write_log = match read_json(&vi) {
Ok(write_log) => write_log,
Err(e) => bail!("Error {:?} reading verify config {:?}", e, vi),
};
println!("Loading write count information from file {vi:?}");
if di.write_log.len() != di.volume_info.total_blocks() {
bail!(
"Verify file {vi:?} blocks:{} does not match disk:{}",
di.write_log.len(),
di.volume_info.total_blocks()
);
}
/*
* Only verify the volume if requested.
*/
if verify && let Err(e) = verify_volume(volume, di, false).await {
bail!("Initial volume verify failed: {:?}", e)
}
Ok(())
}
// How to determine when a test will stop running.
// Either by count, or a message over a channel.
enum WhenToQuit {
Count {
count: usize,
},
Signal {
shutdown_rx: mpsc::Receiver<SignalAction>,
},
}
#[derive(Debug)]
enum SignalAction {
Shutdown,
Verify,
}
// When a signal is received, send a message over a channel.
async fn handle_signals(
mut signals: Signals,
shutdown_tx: mpsc::Sender<SignalAction>,
) {
while let Some(signal) = signals.next().await {
match signal {
SIGUSR1 => {
shutdown_tx.send(SignalAction::Shutdown).await.unwrap();
}
SIGUSR2 => {
shutdown_tx.send(SignalAction::Verify).await.unwrap();
}
x => {
panic!("Received unsupported signal {}", x);
}
}
}
}
// Construct a volume and a list of targets for use by the tests.
// Our choice of how to construct the volume depends on what options we
// have been given.
//
// If we have been provided a vcr file, this will get first priority and all
// other options will be ignored.
//
// Second choice is if we are provided the address for a dsc server. We can
// use the dsc server to determine part of what we need to create a Volume.
// The rest of what we need we can gather from the CrucibleOpts, which are
// built from options provided on the command line, or their defaults.
//
// For the final choice we have to construct a Volume by asking our downstairs
// for information that we need up front, which we then combine with
// CrucibleOpts. This will work as long as one of the downstairs is up
// already. If we have a test that requires no downstairs to be running on
// startup, then we need to provide a VCR file, or use the dsc server.
//
// While making our volume, we also record the targets that become part of
// the volume in a separate Vec. In some cases we no longer have access to
// the target information after the volume is constructed, and some tests also
// want the specific targets, so we make and return that list here.
async fn make_a_volume(
opt: &Opt,
volume_logger: Logger,
test_log: &Logger,
pr: Option<ProducerRegistry>,
) -> Result<(Volume, Vec<SocketAddr>)> {
let up_uuid = opt.uuid.unwrap_or_else(Uuid::new_v4);
let mut crucible_opts = CrucibleOpts {
id: up_uuid,
target: opt.target.clone(),
lossy: false,
flush_timeout: opt.flush_timeout,
key: opt.key.clone(),
cert_pem: opt.cert_pem.clone(),
key_pem: opt.key_pem.clone(),
root_cert_pem: opt.root_cert_pem.clone(),
control: opt.control,
read_only: opt.read_only,
};
if let Some(vcr_file) = &opt.vcr_file {
let vcr: VolumeConstructionRequest = match read_json(vcr_file) {
Ok(vcr) => vcr,
Err(e) => {
bail!("Error {:?} reading VCR from {:?}", e, vcr_file)
}
};
info!(test_log, "Using VCR: {:?}", vcr);
if opt.generation != 0 {
warn!(test_log, "gen option is ignored when VCR is provided");
}
if !opt.target.is_empty() {
warn!(test_log, "targets are ignored when VCR is provided");
}
let targets = vcr.targets();
let volume = Volume::construct(vcr, pr, volume_logger).await.unwrap();
Ok((volume, targets))
} else if opt.dsc.is_some() {
// We were given a dsc endpoint, use that to create a VCR that
// represents our Volume.
if !opt.target.is_empty() {
warn!(test_log, "targets are ignored when dsc option is provided");
}
let dsc = opt.dsc.unwrap();
let dsc_url = format!("http://{}", dsc);
let dsc_client = Client::new(&dsc_url);
let ri = match dsc_client.dsc_get_region_info().await {
Ok(res) => res.into_inner(),
Err(e) => {
bail!("Failed to get region info from {:?}: {}", dsc_url, e);
}
};
info!(test_log, "Use this region info from dsc: {:?}", ri);
let extent_info = RegionExtentInfo {
block_size: ri.block_size,
blocks_per_extent: ri.blocks_per_extent,
extent_count: ri.extent_count,
};
let res = dsc_client.dsc_get_region_count().await.unwrap();
let regions = res.into_inner();
if regions < 3 {
bail!("Found {regions} regions. We need at least 3");
}
let sv_count = regions / 3;
let region_remainder = regions % 3;
info!(
test_log,
"dsc has {} regions. This means {} sub_volumes", regions, sv_count
);
if region_remainder != 0 {
warn!(
test_log,
"{} regions from dsc will not be part of any sub_volume",
region_remainder,
);
}
// We start by creating the volume builder
let mut builder =
VolumeBuilder::new(extent_info.block_size, volume_logger);
// Now, loop over regions we found from dsc and make a
// sub_volume at every three.
let mut targets = Vec::new();
let mut cid = 0;
for sv in 0..sv_count {
let mut sv_targets = Vec::new();
for _ in 0..3 {
let port = dsc_client.dsc_get_port(cid).await.unwrap();
let tar = SocketAddr::new(
dsc.ip(),
port.into_inner().try_into().unwrap(),
);
sv_targets.push(tar);
targets.push(tar);
cid += 1;
}
info!(test_log, "SV {:?} has targets: {:?}", sv, sv_targets);
crucible_opts.target = sv_targets;
builder
.add_subvolume_create_guest(
crucible_opts.clone(),
extent_info.clone(),
opt.generation,
pr.clone(),
)
.await
.unwrap();
}
Ok((Volume::from(builder), targets))
} else {
// We were not provided a VCR, so, we have to make one by using
// the repair port on a downstairs to get region information that
// we require. Once we have that information, we can build a VCR
// from it.
// For each sub-volume, we need to know:
// block_size, blocks_per_extent, and extent_size. We can get any
// of the target downstairs to give us this info, if they are
// running. We don't care which one responds. Any mismatch will
// be detected later in the process and handled by the upstairs.
let mut extent_info_result = None;
for target in &crucible_opts.target {
let port = target.port() + crucible_common::REPAIR_PORT_OFFSET;
let addr = SocketAddr::new(target.ip(), port);
let repair_url = format!("http://{addr}");
info!(test_log, "look at: {repair_url}");
let repair_client = repair_client::new(&repair_url);
match repair_client.get_region_info().await {
Ok(ri) => {
info!(test_log, "RI is: {:?}", ri);
extent_info_result = Some(RegionExtentInfo {
block_size: ri.block_size(),
blocks_per_extent: ri.extent_size().value,
extent_count: ri.extent_count(),
});
break;
}
Err(e) => {
warn!(
test_log,
"Failed to get info from {:?} {:?}", repair_url, e
);
}
}
}
let extent_info = match extent_info_result {
Some(ei) => ei,
None => {
bail!("Can't determine extent info to build a Volume");
}
};
let targets = crucible_opts.target.clone();
let mut builder =
VolumeBuilder::new(extent_info.block_size, volume_logger);
builder
.add_subvolume_create_guest(
crucible_opts.clone(),
extent_info,
opt.generation,
pr,
)
.await
.unwrap();
Ok((Volume::from(builder), targets))
}
}
/**
* A test program that makes use use of the interfaces that Crucible exposes.
*/
#[tokio::main]
async fn main() -> Result<()> {
let opt = opts()?;
let is_encrypted = opt.key.is_some();
// If we just want the version, print that and exit.
if let Workload::Version = opt.workload {
let info = crucible_common::BuildInfo::default();
println!("{}", info);
println!(
"Upstairs <-> Downstairs Message Version: {}",
CRUCIBLE_MESSAGE_VERSION
);
return Ok(());
}
if matches!(opt.workload, Workload::Verify) && opt.verify_in.is_none() {
bail!("Verify requires verify_in file");
}
// If just want the cli, then start that after our runtime. The cli
// does not need upstairs started, as that should happen in the
// cli-server code.
if let Workload::Cli { attach } = opt.workload {
cli::start_cli_client(attach).await?;
return Ok(());
}
// Opt out of verbose logs for certain tests
let log_level = match opt.workload {
Workload::RandRead {
cfg: RandReadWriteWorkload { verbose, .. },
}
| Workload::RandWrite {
cfg: RandReadWriteWorkload { verbose, .. },
}
| Workload::Bufferbloat {
cfg: BufferbloatWorkload { verbose, .. },
} => {
if verbose {
slog::Level::Info
} else {
slog::Level::Error
}
}
_ => slog::Level::Info,
};
let volume_logger = crucible_common::build_logger_with_level(log_level);
let test_log = volume_logger.new(o!("task" => "crutest".to_string()));
let pr;
if opt.metrics {
// If metrics are desired, we create and register the server
// first. Once we have the server, we clone the ProducerRegistry
// so we can pass that on to the upstairs.
// Finally, spin out a task with the server to provide the endpoint
// so metrics can be collected by Oximeter.
println!(
"Creating a metric collect endpoint at {}",
opt.metric_collect
);
match client_oximeter(opt.metric_collect, opt.metric_register) {
Err(e) => {
println!("Failed to register with Oximeter {:?}", e);
pr = None;
}
Ok(server) => {
pr = Some(server.registry().clone());
// Now Spawn the metric endpoint.
tokio::spawn(async move {
server.serve_forever().await.unwrap();
});
}
}
} else {
pr = None;
}
// Build a Volume for all the tests to use.
let (volume, mut targets) =
make_a_volume(&opt, volume_logger.clone(), &test_log, pr).await?;