thruput-node-supervisor 0.42.0

Local node supervisor for Thruput development
Documentation
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
//! Error types for the node supervisor.

use std::{fmt, io, path::PathBuf, process::ExitStatus, time::Duration};

use thruput_hogd_backend::IntroOutcome;
use thruput_json_rpc_types::NetworkHashText;
use thruput_x23_transport::ConnectError;

/// Unix child-process signal used by the supervisor.
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChildSignal(i32);

#[cfg(unix)]
impl ChildSignal {
    /// Create a signal wrapper from a raw libc signal number.
    pub const fn from_raw(signal: i32) -> Self {
        Self(signal)
    }

    /// Raw libc signal number.
    pub const fn raw(self) -> i32 {
        self.0
    }

    const fn name(self) -> Option<&'static str> {
        match self.0 {
            libc::SIGTERM => Some("SIGTERM"),
            libc::SIGKILL => Some("SIGKILL"),
            _ => None,
        }
    }
}

#[cfg(unix)]
impl fmt::Display for ChildSignal {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.name() {
            Some(name) => f.write_str(name),
            None => write!(f, "signal {}", self.raw()),
        }
    }
}

/// Errors returned by the node supervisor.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum NodeSupervisorError {
    /// The chain's private faucet state cannot be safely loaded or persisted.
    #[error("invalid local funding state at {}: {reason}", path.display())]
    LocalFundingState {
        /// File or directory whose state failed validation.
        path: PathBuf,
        /// Diagnostic without private material.
        reason: String,
    },
    /// Local airdrop resource or transfer configuration is invalid.
    #[error("invalid local airdrop configuration: {reason}")]
    InvalidLocalAirdropConfig {
        /// Human-readable validation failure.
        reason: &'static str,
    },
    /// The configured protocol override is not the wire width of a token.
    #[error("invalid protocol override: {source}")]
    InvalidProtocolOverride {
        /// Why the text cannot name a protocol token.
        #[from]
        source: thruput_json_rpc_types::protocol_token::ProtocolTokenWidthError,
    },
    /// A configured binary path does not exist.
    #[error("configured hog-devnode binary path does not exist: {}", path.display())]
    ConfiguredPathMissing {
        /// Missing configured path.
        path: PathBuf,
    },

    /// A configured binary path exists but is not executable.
    #[error("configured hog-devnode binary path is not executable: {}", path.display())]
    ConfiguredPathNotExecutable {
        /// Non-executable configured path.
        path: PathBuf,
    },

    /// The supervisor could not locate a devnode binary.
    #[error("hog-devnode binary not found")]
    HogdDevNotFound,

    /// The supervisor could not determine its current executable path.
    #[error("failed to resolve current executable path: {source}")]
    CurrentExe {
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The supervisor could not start the configured child process.
    #[error("failed to spawn child process `{program}`: {source}")]
    Spawn {
        /// Program path or name.
        program: String,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The configured hogd-dev bootstrap manifest request could not be represented.
    #[error("invalid hogd-dev bootstrap manifest request: {reason}")]
    InvalidBootstrapManifestConfig {
        /// Human-readable reason.
        reason: String,
    },

    /// The supervisor could not create a private bootstrap-manifest directory.
    #[error("failed to create hogd-dev bootstrap manifest temp dir: {source}")]
    BootstrapManifestTempDir {
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The supervisor could not serialize the bootstrap manifest.
    #[error("failed to serialize hogd-dev bootstrap manifest: {source}")]
    BootstrapManifestSerialize {
        /// Underlying JSON error.
        #[source]
        source: serde_json::Error,
    },

    /// The supervisor could not write the bootstrap manifest file.
    #[error("failed to write hogd-dev bootstrap manifest {}: {source}", path.display())]
    BootstrapManifestWrite {
        /// Manifest path.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The child process was spawned without a stdout pipe.
    #[error("child process `{program}` did not expose stdout")]
    MissingStdout {
        /// Program path or name.
        program: String,
    },

    /// The child process was spawned without a stderr pipe.
    #[error("child process `{program}` did not expose stderr")]
    MissingStderr {
        /// Program path or name.
        program: String,
    },

    /// The child process did not report readiness before the startup timeout.
    #[error("child process `{program}` did not report readiness within {timeout:?}")]
    ReadinessTimeout {
        /// Program path or name.
        program: String,
        /// Startup timeout.
        timeout: Duration,
    },

    /// The child process wrote a readiness line exceeding the configured cap.
    #[error("child process `{program}` wrote a readiness line larger than {max_bytes} bytes")]
    ReadinessTooLarge {
        /// Program path or name.
        program: String,
        /// Maximum allowed readiness line size.
        max_bytes: usize,
    },

    /// The child process exited before reporting readiness.
    #[error("child process `{program}` exited before readiness with status {status}")]
    ExitedBeforeReady {
        /// Program path or name.
        program: String,
        /// Child exit status.
        status: ExitStatus,
    },

    /// The supervisor could not read the readiness line.
    #[error("failed to read readiness from child process `{program}`: {source}")]
    ReadinessRead {
        /// Program path or name.
        program: String,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The child process wrote malformed readiness JSON.
    #[error("invalid readiness JSON from child process `{program}`: {source}")]
    InvalidReadinessJson {
        /// Program path or name.
        program: String,
        /// Raw readiness line.
        line: String,
        /// JSON parsing error.
        #[source]
        source: serde_json::Error,
    },

    /// The child process wrote a readiness line with an unsupported schema.
    #[error(
        "invalid readiness schema version from child process `{program}`: expected {expected}, got {actual}"
    )]
    InvalidReadinessSchemaVersion {
        /// Program path or name.
        program: String,
        /// Expected schema version.
        expected: u32,
        /// Actual schema version.
        actual: u32,
    },

    /// The child process wrote a readiness line with an unexpected status.
    #[error("invalid readiness status from child process `{program}`: {status}")]
    InvalidReadinessStatus {
        /// Program path or name.
        program: String,
        /// Status value from the readiness line.
        status: String,
    },

    /// The supervisor could not read the readiness runtime manifest.
    #[error(
        "failed to read runtime manifest `{path}` advertised by child process `{program}`: {source}"
    )]
    RuntimeManifestRead {
        /// Program path or name.
        program: String,
        /// Manifest path advertised in readiness.
        path: String,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The advertised runtime manifest path or permissions are unsafe.
    #[error("unsafe runtime manifest `{path}` advertised by child process `{program}`: {reason}")]
    RuntimeManifestUnsafePermissions {
        /// Program path or name.
        program: String,
        /// Manifest path advertised in readiness.
        path: String,
        /// Validation failure.
        reason: String,
    },

    /// The child process advertised a malformed runtime manifest hash.
    #[error("invalid runtime manifest hash from child process `{program}`: {hash}")]
    InvalidRuntimeManifestHash {
        /// Program path or name.
        program: String,
        /// Advertised hash.
        hash: String,
    },

    /// The runtime manifest bytes did not match the advertised hash.
    #[error(
        "runtime manifest hash mismatch from child process `{program}` for `{path}`: expected {expected}, got {actual}"
    )]
    RuntimeManifestHashMismatch {
        /// Program path or name.
        program: String,
        /// Manifest path advertised in readiness.
        path: String,
        /// Hash advertised in readiness.
        expected: String,
        /// Hash computed from manifest bytes.
        actual: String,
    },

    /// The runtime manifest could not be parsed or projected.
    #[error("invalid runtime manifest from child process `{program}`: {reason}")]
    InvalidRuntimeManifest {
        /// Program path or name.
        program: String,
        /// Validation failure.
        reason: String,
    },

    /// The runtime manifest advertised a different network than the bootstrap request.
    #[error(
        "hogd-dev runtime manifest network hash mismatch: bootstrap requested {expected}, runtime advertised {observed}"
    )]
    RuntimeNetworkHashMismatch {
        /// Network hash requested in the bootstrap manifest.
        expected: NetworkHashText,
        /// Network hash advertised by the runtime manifest.
        observed: NetworkHashText,
    },

    /// The runtime manifest advertised a different protocol token than requested.
    #[error(
        "hogd-dev runtime manifest protocol mismatch: supervisor requested {expected}, runtime advertised {observed:?}"
    )]
    RuntimeProtocolMismatch {
        /// Protocol token requested by the supervisor.
        expected: String,
        /// Protocol token advertised by the runtime manifest, if present.
        observed: Option<String>,
    },

    /// The supervisor could not signal the child process group.
    #[cfg(unix)]
    #[error("failed to send {signal} to child process group `{program}`: {source}")]
    Signal {
        /// Program path or name.
        program: String,
        /// Signal sent to the child process group.
        signal: ChildSignal,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The supervisor could not query the child process state.
    #[error("failed to query child process `{program}` status: {source}")]
    Status {
        /// Program path or name.
        program: String,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The supervisor could not wait for the child process to exit.
    #[error("failed to wait for child process `{program}`: {source}")]
    Wait {
        /// Program path or name.
        program: String,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The supervisor could not request child process termination.
    #[error("failed to stop child process `{program}`: {source}")]
    Stop {
        /// Program path or name.
        program: String,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The supervisor could not bind the JSON-RPC server to its listen address.
    #[error("failed to bind JSON-RPC server to {address}: {source}")]
    RpcBind {
        /// Configured listen address.
        address: String,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The supervisor could not raise the process file-descriptor limit for JSON-RPC.
    #[cfg(unix)]
    #[error("failed to raise JSON-RPC nofile limit toward {target}: {source}")]
    RpcNofileLimit {
        /// Desired soft nofile limit.
        target: u64,
        /// Underlying OS error.
        #[source]
        source: io::Error,
    },

    /// The configured JSON-RPC connection budget exceeds the hard file-descriptor limit.
    #[cfg(unix)]
    #[error("JSON-RPC nofile hard limit {hard} is below requested target {target}")]
    RpcNofileHardLimit {
        /// Desired soft nofile limit.
        target: u64,
        /// Current hard nofile limit.
        hard: u64,
    },

    /// The supervisor could not read the JSON-RPC server's bound address.
    #[error("failed to read JSON-RPC bound address: {source}")]
    RpcLocalAddr {
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The supervisor stop signal failed.
    #[error("failed to wait for supervisor stop signal: {source}")]
    StopSignal {
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// The supervisor could not connect to hogd-dev's `SchedulerCoord` x23 endpoint.
    #[error("failed to connect hogd-dev SchedulerCoord endpoint {endpoint}: {source}")]
    SchedulerConnect {
        /// `SchedulerCoord` endpoint advertised by hogd-dev readiness.
        endpoint: std::net::SocketAddr,
        /// x23 connection error.
        #[source]
        source: ConnectError,
    },

    /// The supervisor could not connect to hogd-dev's `SchedGateway` x23 endpoint.
    #[error("failed to connect hogd-dev SchedGateway endpoint {endpoint}: {source}")]
    SchedulerGatewayConnect {
        /// `SchedGateway` endpoint advertised by hogd-dev readiness.
        endpoint: std::net::SocketAddr,
        /// x23 connection error.
        #[source]
        source: ConnectError,
    },

    /// The scheduler tip belongs to a different network than the runtime manifest.
    #[error(
        "hogd-dev scheduler network hash mismatch: runtime manifest expected {expected}, scheduler reported {observed}"
    )]
    SchedulerNetworkHashMismatch {
        /// Network hash advertised by the runtime manifest.
        expected: NetworkHashText,
        /// Network hash observed in the scheduler tip proposal header.
        observed: NetworkHashText,
    },

    /// The coordinator and public scheduler gateway advertised different startup tips.
    #[error(
        "hogd-dev scheduler planes disagree at startup: coordinator {coordinator}, gateway {gateway}"
    )]
    SchedulerPlaneTipMismatch {
        /// Debug representation of the coordinator tip.
        coordinator: String,
        /// Debug representation of the scheduler gateway tip.
        gateway: String,
    },

    /// A hogd-dev scheduler RPC failed.
    #[error("hogd-dev scheduler RPC `{op}` failed: {source}")]
    SchedulerRpc {
        /// Scheduler RPC operation name.
        op: &'static str,
        /// x23 RPC error.
        #[source]
        source: rpc_driver::RpcError,
    },

    /// A hogd-dev scheduler gateway RPC failed during startup.
    #[error("hogd-dev scheduler gateway RPC `{op}` failed: {source}")]
    SchedulerGatewayRpc {
        /// Scheduler gateway RPC operation name.
        op: &'static str,
        /// x23 RPC error.
        #[source]
        source: rpc_driver::RpcError,
    },

    /// The scheduler rejected a startup operation.
    #[error("hogd-dev scheduler rejected `{op}`: {result}")]
    SchedulerRejected {
        /// Scheduler RPC operation name.
        op: &'static str,
        /// Scheduler result value.
        result: String,
    },

    /// The supervisor could not connect to hogd-dev's `IndexerBlockExplorer` x23 endpoint.
    #[error("failed to connect hogd-dev IndexerBlockExplorer endpoint {endpoint}: {source}")]
    IndexerConnect {
        /// `IndexerBlockExplorer` endpoint advertised by hogd-dev readiness.
        endpoint: std::net::SocketAddr,
        /// x23 connection error.
        #[source]
        source: ConnectError,
    },

    /// A hogd-dev indexer RPC failed during startup probing.
    #[error("hogd-dev indexer RPC `{op}` failed: {error}")]
    IndexerRpc {
        /// Indexer RPC operation name.
        op: &'static str,
        /// x23 RPC error message.
        error: String,
    },

    /// The supervisor could not connect to hogd-dev's `DbGateway` x23 endpoint.
    #[error("failed to connect hogd-dev DbGateway endpoint {endpoint}: {source}")]
    DbGatewayConnect {
        /// `DbGateway` endpoint advertised by hogd-dev readiness.
        endpoint: std::net::SocketAddr,
        /// x23 connection error.
        #[source]
        source: ConnectError,
    },

    /// A hogd-dev `DbGateway` RPC failed during startup probing.
    #[error("hogd-dev DbGateway RPC `{op}` failed: {error}")]
    DbGatewayRpc {
        /// `DbGateway` RPC operation name.
        op: &'static str,
        /// x23 RPC error message.
        error: String,
    },

    /// The supervisor could not connect to hogd-dev's `ViewGateway` x23 endpoint.
    #[error("failed to connect hogd-dev ViewGateway endpoint {endpoint}: {source}")]
    ViewGatewayConnect {
        /// `ViewGateway` endpoint advertised by hogd-dev readiness.
        endpoint: std::net::SocketAddr,
        /// x23 connection error.
        #[source]
        source: ConnectError,
    },

    /// A hogd-dev `ViewGateway` RPC failed during startup probing.
    #[error("hogd-dev ViewGateway RPC `{op}` failed: {error}")]
    ViewGatewayRpc {
        /// `ViewGateway` RPC operation name.
        op: &'static str,
        /// x23 RPC error message.
        error: String,
    },

    /// An x23 service rejected the intro handshake with protocol details.
    #[error(
        "{}",
        intro_rejected_message(
            service,
            *outcome,
            outcome_name,
            *is_protocol_mismatch,
            protocol,
            expected_protocol.as_deref()
        )
    )]
    ProtocolIntroRejected {
        /// x23 service that rejected the intro handshake.
        service: &'static str,
        /// Raw outcome discriminant from the x23 service.
        outcome: i32,
        /// Decoded intro outcome name when known.
        outcome_name: &'static str,
        /// Whether the intro outcome is a protocol mismatch.
        is_protocol_mismatch: bool,
        /// Protocol token sent by the Rust supervisor.
        protocol: String,
        /// Protocol token advertised by readiness, when available.
        expected_protocol: Option<String>,
    },

    /// The supervisor could not operate on the hogd-dev PID file.
    #[error("hogd-dev PID file operation failed at {}: {source}", path.display())]
    PidFile {
        /// PID file path or parent directory involved in the failed operation.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },

    /// Another managed local node appears to be running.
    #[error(
        "thruput local node already appears to be running (PID file {}, hogd-dev pid {pid:?}); stop it before starting another node",
        path.display()
    )]
    LocalNodeAlreadyRunning {
        /// PID file path that is locked by the running node.
        path: PathBuf,
        /// Existing hogd-dev PID if it could be read from the PID file.
        pid: Option<u32>,
    },

    /// Forced shutdown did not conclusively stop the supervised process group.
    #[error(
        "child process `{program}` did not fully stop after SIGKILL (process group {process_group:?}); preserving its PID file to prevent unsafe restart"
    )]
    ShutdownIncomplete {
        /// Program path or name used to spawn the child.
        program: String,
        /// Process group that may still be alive.
        process_group: Option<i32>,
    },

    /// Persistent local-node lifecycle guarantees require Unix process groups.
    #[error("persistent hog-devnode state is only supported on Unix platforms")]
    PersistentStateUnsupported,

    /// The supervisor could not prepare a private persistent data directory.
    #[error("hog-devnode data directory operation failed at {}: {source}", path.display())]
    DataDir {
        /// Persistent data directory involved in the failed operation.
        path: PathBuf,
        /// Underlying I/O error.
        #[source]
        source: io::Error,
    },
}

impl NodeSupervisorError {
    /// Report an x23 service that did not accept the protocol this build
    /// speaks.
    ///
    /// `expected_protocol` is what hogd-dev advertised in its readiness
    /// payload, when the supervisor has it: naming both sides turns "intro
    /// rejected" into an actionable version mismatch.
    pub(crate) fn intro_rejected(
        outcome: IntroOutcome,
        protocol: &str,
        expected_protocol: Option<&str>,
    ) -> Self {
        Self::ProtocolIntroRejected {
            service: outcome.service(),
            outcome: outcome.code(),
            outcome_name: outcome.name(),
            is_protocol_mismatch: outcome.mismatched_protocol(),
            protocol: protocol.to_string(),
            expected_protocol: expected_protocol.map(str::to_string),
        }
    }
}

fn intro_rejected_message(
    service: &str,
    outcome: i32,
    outcome_name: &str,
    is_protocol_mismatch: bool,
    protocol: &str,
    expected_protocol: Option<&str>,
) -> String {
    if is_protocol_mismatch {
        return match expected_protocol {
            Some(expected_protocol) => format!(
                "{service} intro mismatch: thruput node speaks protocol {protocol}, \
                 hogd-dev expects {expected_protocol} ({outcome_name}; binary is built against \
                 a different protocol commit)"
            ),
            None => format!(
                "{service} intro mismatch: thruput node speaks protocol {protocol}, \
                 but hogd-dev rejected it ({outcome_name})"
            ),
        };
    }

    format!(
        "hogd-dev {service} intro rejected with outcome {outcome_name} \
         (discriminant {outcome}); thruput node speaks protocol {protocol}"
    )
}