state.rs 6.01 KB
Newer Older
Ryan Olson's avatar
Ryan Olson committed
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
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Session state types for the unified session model.
//!
//! This module provides the core state machine types:
//! - [`ControlRole`]: Whether this session is controller, controllee, or neutral
//! - [`AttachmentState`]: Whether a peer is attached
//! - [`SessionPhase`]: The current operational phase of the session

use serde::{Deserialize, Serialize};

use crate::InstanceId;

/// Control role in a session relationship.
///
/// Sessions can dynamically transition between roles:
/// - Start as `Neutral` (independent, can initiate in either direction)
/// - Become `Controller` when issuing commands to a peer
/// - Become `Controllee` when executing commands from a peer
///
/// Control can be transferred bidirectionally via `YieldControl`/`AcquireControl`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ControlRole {
    /// Independent - can initiate control in either direction.
    /// This is the initial state and the state after yielding control.
    #[default]
    Neutral,
    /// Currently controlling a peer session (issues commands).
    Controller,
    /// Currently being controlled by a peer session (executes commands).
    Controllee,
}

impl ControlRole {
    /// Check if this role can issue control commands.
    pub fn can_command(&self) -> bool {
        matches!(self, ControlRole::Controller)
    }

    /// Check if this role should respond to control commands.
    pub fn responds_to_commands(&self) -> bool {
        matches!(self, ControlRole::Controllee)
    }

    /// Check if this role is neutral (can transition either way).
    pub fn is_neutral(&self) -> bool {
        matches!(self, ControlRole::Neutral)
    }

    /// Get the opposite role.
    ///
    /// - `Controller` ↔ `Controllee`
    /// - `Neutral` → `Neutral` (no opposite)
    pub fn opposite(&self) -> ControlRole {
        match self {
            ControlRole::Controller => ControlRole::Controllee,
            ControlRole::Controllee => ControlRole::Controller,
            ControlRole::Neutral => ControlRole::Neutral,
        }
    }
}

/// Attachment state - whether a peer is connected.
///
/// Valid state combinations:
/// - `Neutral + Unattached`: Initial state, waiting for connection
/// - `Neutral + Attached`: Post-yield state, peer still connected
/// - `Controllee + Unattached`: Waiting for controller to attach
/// - `Controllee + Attached`: Being actively controlled
/// - `Controller + Attached`: Actively controlling (Controller + Unattached is invalid)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AttachmentState {
    /// No peer attached.
    #[default]
    Unattached,
    /// Peer attached with the given instance ID.
    Attached { peer: InstanceId },
}

impl AttachmentState {
    /// Check if a peer is attached.
    pub fn is_attached(&self) -> bool {
        matches!(self, AttachmentState::Attached { .. })
    }

    /// Get the attached peer's instance ID if attached.
    pub fn peer(&self) -> Option<InstanceId> {
        match self {
            AttachmentState::Attached { peer } => Some(*peer),
            AttachmentState::Unattached => None,
        }
    }
}

/// Operational phase of a session.
///
/// Represents the lifecycle of block operations within a session:
/// 1. `Searching` - Initial discovery/search phase
/// 2. `Holding` - Blocks found and held, no staging yet
/// 3. `Staging` - Transfer in progress (G3→G2, G4→G2, etc.)
/// 4. `Ready` - All blocks in target tier, ready for transfer
/// 5. `Complete` - Session completed successfully
/// 6. `Failed` - Session failed or cancelled
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SessionPhase {
    /// Initial search/discovery phase.
    #[default]
    Searching,
    /// Blocks found and held, no staging started.
    Holding,
    /// Transfer/staging in progress (any direction).
    Staging,
    /// All blocks in target tier, ready for RDMA pull.
    Ready,
    /// Session completed successfully.
    Complete,
    /// Session failed or was cancelled.
    Failed,
}

impl SessionPhase {
    /// Check if the session is in a terminal state.
    pub fn is_terminal(&self) -> bool {
        matches!(self, SessionPhase::Complete | SessionPhase::Failed)
    }

    /// Check if the session is active (not terminal).
    pub fn is_active(&self) -> bool {
        !self.is_terminal()
    }

    /// Check if blocks are ready for transfer.
    pub fn is_ready(&self) -> bool {
        matches!(self, SessionPhase::Ready)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_control_role_transitions() {
        let role = ControlRole::Neutral;
        assert!(role.is_neutral());
        assert!(!role.can_command());
        assert!(!role.responds_to_commands());

        let role = ControlRole::Controller;
        assert!(!role.is_neutral());
        assert!(role.can_command());
        assert!(!role.responds_to_commands());

        let role = ControlRole::Controllee;
        assert!(!role.is_neutral());
        assert!(!role.can_command());
        assert!(role.responds_to_commands());
    }

    #[test]
    fn test_attachment_state() {
        let state = AttachmentState::Unattached;
        assert!(!state.is_attached());
        assert!(state.peer().is_none());

        let peer_id = InstanceId::new_v4();
        let state = AttachmentState::Attached { peer: peer_id };
        assert!(state.is_attached());
        assert_eq!(state.peer(), Some(peer_id));
    }

    #[test]
    fn test_session_phase() {
        assert!(!SessionPhase::Searching.is_terminal());
        assert!(!SessionPhase::Holding.is_terminal());
        assert!(!SessionPhase::Staging.is_terminal());
        assert!(!SessionPhase::Ready.is_terminal());
        assert!(SessionPhase::Complete.is_terminal());
        assert!(SessionPhase::Failed.is_terminal());

        assert!(SessionPhase::Ready.is_ready());
        assert!(!SessionPhase::Staging.is_ready());
    }
}