Skip to main content

amaters_server/
version.rs

1//! Version compatibility and rolling upgrade support.
2//!
3//! This module provides version constants, compatibility checks, and a
4//! handshake protocol for multi-node clusters to detect incompatible
5//! version combinations before establishing connections.
6
7/// Minimum compatible version: nodes running this version or newer (with
8/// the same major) are accepted into the cluster.
9pub const MIN_COMPATIBLE_VERSION: (u64, u64, u64) = (0, 2, 0);
10
11/// Current version of this build, taken from `Cargo.toml` at compile time.
12pub const CURRENT_VERSION: (u64, u64, u64) = (0, 2, 2);
13
14/// Returns `true` when `peer_version` is considered compatible with this node.
15///
16/// Compatibility rules:
17/// - Major version must match exactly (breaking API changes live on majors).
18/// - Peer minor must be >= [`MIN_COMPATIBLE_VERSION`]'s minor so that a
19///   cluster composed of nodes at different 0.2.x patch levels can still
20///   operate together during a rolling upgrade.
21pub fn is_compatible(peer_version: (u64, u64, u64)) -> bool {
22    peer_version.0 == CURRENT_VERSION.0 && peer_version.1 >= MIN_COMPATIBLE_VERSION.1
23}
24
25/// Version handshake exchanged between nodes on initial connection.
26///
27/// Both sides send a [`VersionHandshake`] and then call
28/// [`is_compatible_with`][VersionHandshake::is_compatible_with] on the remote
29/// side's handshake before proceeding.
30#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
31pub struct VersionHandshake {
32    /// The exact version running on the sending node.
33    pub version: (u64, u64, u64),
34    /// The oldest version the sending node is willing to talk to.
35    pub min_compatible: (u64, u64, u64),
36    /// Stringified semver from `CARGO_PKG_VERSION`, useful for diagnostics.
37    pub build_id: String,
38}
39
40impl VersionHandshake {
41    /// Construct a handshake that describes *this* binary.
42    pub fn current() -> Self {
43        Self {
44            version: CURRENT_VERSION,
45            min_compatible: MIN_COMPATIBLE_VERSION,
46            build_id: env!("CARGO_PKG_VERSION").to_string(),
47        }
48    }
49
50    /// Returns `true` when `other` is compatible with this node.
51    ///
52    /// The check is intentionally symmetric: each side applies its own
53    /// local `is_compatible` rules against the remote version.
54    pub fn is_compatible_with(&self, other: &VersionHandshake) -> bool {
55        // Check that the other node's version is acceptable to us.
56        is_compatible(other.version)
57    }
58}
59
60// ─── Unit tests ──────────────────────────────────────────────────────────────
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_current_version_compatible_with_itself() {
68        assert!(is_compatible(CURRENT_VERSION));
69    }
70
71    #[test]
72    fn test_older_minor_version_compatible() {
73        // 0.2.0 is the minimum, so 0.2.0 must be compatible with 0.2.2
74        let old_minor = (0u64, 2u64, 0u64);
75        assert!(is_compatible(old_minor));
76
77        // 0.2.1 is also fine
78        let mid_minor = (0u64, 2u64, 1u64);
79        assert!(is_compatible(mid_minor));
80    }
81
82    #[test]
83    fn test_different_major_version_incompatible() {
84        let v1 = (1u64, 0u64, 0u64);
85        assert!(!is_compatible(v1));
86
87        let v2 = (2u64, 2u64, 0u64);
88        assert!(!is_compatible(v2));
89    }
90
91    #[test]
92    fn test_minor_below_minimum_incompatible() {
93        // 0.1.x is below MIN_COMPATIBLE_VERSION.minor = 2
94        let too_old = (0u64, 1u64, 99u64);
95        assert!(!is_compatible(too_old));
96    }
97
98    #[test]
99    fn test_version_handshake_serialization() {
100        let hs = VersionHandshake::current();
101        let json = serde_json::to_string(&hs).expect("serialise");
102        let back: VersionHandshake = serde_json::from_str(&json).expect("deserialise");
103        assert_eq!(hs, back);
104    }
105
106    #[test]
107    fn test_version_handshake_incompatible_major() {
108        let current = VersionHandshake::current();
109        let other = VersionHandshake {
110            version: (1u64, 0u64, 0u64),
111            min_compatible: (1u64, 0u64, 0u64),
112            build_id: "old-major".to_string(),
113        };
114        assert!(!current.is_compatible_with(&other));
115    }
116
117    #[test]
118    fn test_version_handshake_compatible_peer() {
119        let current = VersionHandshake::current();
120        let peer = VersionHandshake {
121            version: (0u64, 2u64, 1u64),
122            min_compatible: MIN_COMPATIBLE_VERSION,
123            build_id: "peer".to_string(),
124        };
125        assert!(current.is_compatible_with(&peer));
126    }
127}