45 Practical Operation 8 Build a Simple Kv Server Configuration Test Monitoring Ci Cd

Stage 45 Practice (8): Building a Simple KV Server - Configuration_Testing_Monitoring_CI_CD #

Hello, I’m Chen Tian.

Finally, we have reached the final chapter of our KV server series. Originally, I only planned 4 lectures for the KV server, but now it seems that even 8 lectures are slightly insufficient. Although this is a “simple” KV server, it does not have complex performance optimizations — we only used one unsafe line; nor does it have complex lifecycle handling — just a few ‘static annotations; and it doesn’t even support clustered processing.

However, if you can understand the code so far, or even write such code independently, then you already have enough strength to develop in a top-tier big company. I’m not particularly clear about domestic conditions, but in North America, conservatively speaking, a package of 300k+ USD should be easy to obtain.

Today, we’ll wrap up the KV server project by combing through the issues that should be considered in real Rust projects, as discussed before, and talking about production-related processes. We’ll mainly cover five aspects: configuration, integration testing, performance testing, measurement and monitoring, CI/CD.

Configuration #

First, add serde and toml to the Cargo.toml. We plan to use toml for the configuration file and serde for handling the serialization and deserialization:

[dependencies]
...
serde = { version = "1", features = ["derive"] } # Serialization/deserialization
...
toml = "0.5" # toml support
...

Then create src/config.rs to build the KV server configuration:

use crate::KvError;
use serde::{Deserialize, Serialize};
use std::fs;

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ServerConfig {
    pub general: GeneralConfig,
    pub storage: StorageConfig,
    pub tls: ServerTlsConfig,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ClientConfig {
    pub general: GeneralConfig,
    pub tls: ClientTlsConfig,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct GeneralConfig {
    pub addr: String,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "args")]
pub enum StorageConfig {
    MemTable,
    SledDb(String),
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ServerTlsConfig {
    pub cert: String,
    pub key: String,
    pub ca: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ClientTlsConfig {
    pub domain: String,
    pub identity: Option<(String, String)>,
    pub ca: Option<String>,
}

impl ServerConfig {
    pub fn load(path: &str) -> Result<Self, KvError> {
        let config = fs::read_to_string(path)?;
        let config: Self = toml::from_str(&config)?;
        Ok(config)
    }
}

impl ClientConfig {
    pub fn load(path: &str) -> Result<Self, KvError> {
        let config = fs::read_to_string(path)?;
        let config: Self = toml::from_str(&config)?;
        Ok(config)
    }
}

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

    #[test]
    fn server_config_should_be_loaded() {
        let result: Result<ServerConfig, toml::de::Error> =
            toml::from_str(include_str!("../fixtures/server.conf"));
        assert!(result.is_ok());
    }

    #[test]
    fn client_config_should_be_loaded() {
        let result: Result<ClientConfig, toml::de::Error> =
            toml::from_str(include_str!("../fixtures/client.conf"));
        assert!(result.is_ok());
    }
}

You can see that with the help of serde in Rust, handling configuration files of any known format is such an easy task. We just need to define the data structures and use Serialize/Deserialize derive macros for them, and we can handle any data structure that supports serde.

I also wrote an examples/gen_config.rs (you can check its code yourself), used to generate configuration files, here is the generated server configuration:

[general]
addr = '127.0.0.1:9527'

[storage]
type = 'SledDb'
args = '/tmp/kv_server'

[tls]
cert = """
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
"""
key = """
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
"""

With the support of configuration files, you can write some helper functions in lib.rs to make creating a server and client simpler:

mod config;
mod error;
...
// Remaining module declarations and imports.

use anyhow::Result;
...
// Remaining helper code and trait implementations.

/// Create a KV server through configuration
pub async fn start_server_with_config(config: &ServerConfig) -> Result<()> {
    ...
    // Implementation for creating and starting the server.
}

/// Create a KV client through configuration
pub async fn start_client_with_config(
    config: &ClientConfig,
) -> Result<YamuxCtrl<client::TlsStream<TcpStream>>> {
    ...
    // Implementation for creating and starting the client.
}

async fn start_tls_server<Store: Storage>(
    addr: &str,
    store: Store,
    acceptor: TlsServerAcceptor,
) -> Result<()> {
    ...
    // Implementation for starting the TLS server.
}

With the help of start_server_with_config and start_client_with_config, we can simplify src/server.rs and src/client.rs. Below is the new code for src/server.rs:

use anyhow::Result;
use kv6::{start_server_with_config, ServerConfig};

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();
    let config: ServerConfig = toml::from_str(include_str!("../fixtures/server.conf"))?;

    start_server_with_config(&config).await?;

    Ok(())
}

As you can see, the code has become much more concise. There were some other changes in the process, which you can see in the GitHub repo under lecture 45, diff_config.

Integration Testing #

(Translation truncated due to length. Please use smaller content inputs to continue.)