🦀

Rustをプロダクショングレードで使うときのよくあるパターン10選

に公開

Rustをプロダクショングレードで使うときのよくあるパターン10選

プロダクション環境でRustを使用する際、単に動くコードを書くだけでなく、保守性、パフォーマンス、安全性を考慮した設計が重要です。本記事では、実際のプロダクション開発で頻繁に使用される10個のパターンを、Rustの言語仕様とともに詳しく解説します。

1. Result型とエラーハンドリングの徹底

なぜ重要か

Rustには例外機構がありません。代わりにResult<T, E>型を使用してエラーを表現します。これにより、エラーが発生する可能性のある処理が型システムで明示され、コンパイル時にエラー処理の漏れを防げます。

実装パターン

use std::fs::File;
use std::io::{self, Read};

// カスタムエラー型の定義
#[derive(Debug)]
enum AppError {
    IoError(io::Error),
    ParseError(String),
    ValidationError(String),
}

// From トレイトを実装して ? 演算子を使えるようにする
impl From<io::Error> for AppError {
    fn from(error: io::Error) -> Self {
        AppError::IoError(error)
    }
}

// Result型を返す関数
fn read_config(path: &str) -> Result<String, AppError> {
    let mut file = File::open(path)?; // ? 演算子でエラーを早期リターン
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    
    // バリデーション
    if contents.is_empty() {
        return Err(AppError::ValidationError("Config file is empty".to_string()));
    }
    
    Ok(contents)
}

// エラーハンドリングの例
fn main() {
    match read_config("config.toml") {
        Ok(config) => println!("Config loaded: {}", config),
        Err(AppError::IoError(e)) => eprintln!("IO error: {}", e),
        Err(AppError::ParseError(e)) => eprintln!("Parse error: {}", e),
        Err(AppError::ValidationError(e)) => eprintln!("Validation error: {}", e),
    }
}

ベストプラクティス

  • thiserrorクレートを使用してエラー型の定義を簡潔にする
  • anyhowクレートを使用してアプリケーションレベルのエラーハンドリングを簡素化
  • エラーメッセージには十分なコンテキスト情報を含める

2. 所有権とライフタイムの適切な管理

なぜ重要か

Rustの所有権システムは、メモリ安全性を保証する核心機能です。各値には単一の所有者があり、所有者がスコープを抜けると値は自動的に解放されます。これにより、ガベージコレクタなしでメモリリークやダングリングポインタを防げます。

実装パターン

// 所有権の移動を避けるための参照の使用
struct User {
    name: String,
    email: String,
}

impl User {
    // &self で不変参照を受け取る
    fn display_name(&self) -> &str {
        &self.name
    }
    
    // &mut self で可変参照を受け取る
    fn update_email(&mut self, new_email: String) {
        self.email = new_email;
    }
}

// ライフタイムパラメータの明示
struct Cache<'a> {
    data: &'a str,  // 'a はライフタイムパラメータ
}

impl<'a> Cache<'a> {
    fn new(data: &'a str) -> Self {
        Cache { data }
    }
    
    // 戻り値のライフタイムは入力と同じ
    fn get_data(&self) -> &'a str {
        self.data
    }
}

// スマートポインタの使用例
use std::rc::Rc;
use std::cell::RefCell;

// 複数の所有者が必要な場合
fn shared_ownership() {
    let shared_data = Rc::new(vec![1, 2, 3]);
    let clone1 = Rc::clone(&shared_data);
    let clone2 = Rc::clone(&shared_data);
    
    println!("Reference count: {}", Rc::strong_count(&shared_data)); // 3
}

// 内部可変性が必要な場合
struct Counter {
    value: RefCell<i32>,
}

impl Counter {
    fn new() -> Self {
        Counter { value: RefCell::new(0) }
    }
    
    fn increment(&self) {
        *self.value.borrow_mut() += 1;
    }
}

ベストプラクティス

  • 可能な限り参照を使用し、所有権の移動を避ける
  • Cloneは慎重に使用し、パフォーマンスへの影響を考慮する
  • 循環参照を避けるため、Weak<T>を適切に使用する

3. トレイトによる抽象化とジェネリクス

なぜ重要か

トレイトはRustにおけるインターフェースの仕組みで、異なる型に共通の振る舞いを定義できます。ジェネリクスと組み合わせることで、型安全で再利用可能なコードを書けます。

実装パターン

// トレイトの定義
trait Repository<T> {
    fn find_by_id(&self, id: u64) -> Option<T>;
    fn save(&mut self, entity: T) -> Result<(), String>;
    fn delete(&mut self, id: u64) -> Result<(), String>;
}

// ジェネリックな実装
struct InMemoryRepository<T> {
    data: std::collections::HashMap<u64, T>,
    next_id: u64,
}

impl<T> InMemoryRepository<T> {
    fn new() -> Self {
        InMemoryRepository {
            data: std::collections::HashMap::new(),
            next_id: 1,
        }
    }
}

// トレイトの実装
impl<T: Clone> Repository<T> for InMemoryRepository<T> {
    fn find_by_id(&self, id: u64) -> Option<T> {
        self.data.get(&id).cloned()
    }
    
    fn save(&mut self, entity: T) -> Result<(), String> {
        let id = self.next_id;
        self.data.insert(id, entity);
        self.next_id += 1;
        Ok(())
    }
    
    fn delete(&mut self, id: u64) -> Result<(), String> {
        self.data.remove(&id)
            .map(|_| ())
            .ok_or_else(|| format!("Entity with id {} not found", id))
    }
}

// トレイト境界を使用した関数
fn process_repository<T, R>(repo: &R, id: u64) 
where 
    R: Repository<T>,
    T: std::fmt::Debug,
{
    if let Some(entity) = repo.find_by_id(id) {
        println!("Found entity: {:?}", entity);
    }
}

// 派生マクロの活用
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Product {
    id: u64,
    name: String,
    price: f64,
}

ベストプラクティス

  • トレイトは小さく、単一責任の原則に従って設計する
  • ジェネリクスには適切なトレイト境界を設定する
  • 標準ライブラリのトレイト(Debug, Clone, Send, Syncなど)を積極的に実装する

4. 非同期プログラミングとasync/await

なぜ重要か

プロダクション環境では、I/Oバウンドなタスクを効率的に処理することが重要です。Rustのasync/awaitは、非同期処理を同期的なコードのように書けるため、可読性とパフォーマンスを両立できます。

実装パターン

use tokio;
use std::time::Duration;

// 非同期関数の定義
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    let response = reqwest::get(url).await?;
    let body = response.text().await?;
    Ok(body)
}

// タイムアウト処理
async fn fetch_with_timeout(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let timeout_duration = Duration::from_secs(5);
    
    match tokio::time::timeout(timeout_duration, fetch_data(url)).await {
        Ok(Ok(data)) => Ok(data),
        Ok(Err(e)) => Err(Box::new(e)),
        Err(_) => Err("Request timed out".into()),
    }
}

// 並行処理
async fn fetch_multiple_urls(urls: Vec<&str>) -> Vec<Result<String, Box<dyn std::error::Error>>> {
    let futures: Vec<_> = urls.into_iter()
        .map(|url| fetch_with_timeout(url))
        .collect();
    
    // すべてのFutureを並行に実行
    futures::future::join_all(futures).await
}

// エラーハンドリングと再試行
async fn fetch_with_retry(url: &str, max_retries: u32) -> Result<String, String> {
    let mut retries = 0;
    
    loop {
        match fetch_data(url).await {
            Ok(data) => return Ok(data),
            Err(e) => {
                retries += 1;
                if retries >= max_retries {
                    return Err(format!("Failed after {} retries: {}", max_retries, e));
                }
                
                // 指数バックオフ
                let delay = Duration::from_millis(100 * 2u64.pow(retries));
                tokio::time::sleep(delay).await;
            }
        }
    }
}

// メイン関数
#[tokio::main]
async fn main() {
    let urls = vec![
        "https://api.example.com/data1",
        "https://api.example.com/data2",
        "https://api.example.com/data3",
    ];
    
    let results = fetch_multiple_urls(urls).await;
    
    for (i, result) in results.iter().enumerate() {
        match result {
            Ok(data) => println!("URL {} fetched successfully", i),
            Err(e) => eprintln!("URL {} failed: {}", i, e),
        }
    }
}

ベストプラクティス

  • 適切な非同期ランタイム(tokio、async-std)を選択する
  • CPUバウンドなタスクは別スレッドで実行する
  • select!マクロを使用して複数のFutureを効率的に処理する

5. 構造化ログとメトリクス

なぜ重要か

プロダクション環境では、アプリケーションの動作を監視し、問題を迅速に特定できることが重要です。構造化ログとメトリクスは、この目的を達成するための基盤となります。

実装パターン

use tracing::{info, warn, error, instrument};
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: u64,
    username: String,
    email: String,
}

// 関数レベルのトレーシング
#[instrument(skip(password))] // パスワードはログに含めない
async fn authenticate_user(username: &str, password: &str) -> Result<User, String> {
    info!(username = %username, "Attempting to authenticate user");
    
    // 認証ロジック(仮実装)
    if username == "admin" && password == "secret" {
        let user = User {
            id: 1,
            username: username.to_string(),
            email: "admin@example.com".to_string(),
        };
        
        info!(user_id = %user.id, "User authenticated successfully");
        Ok(user)
    } else {
        warn!(username = %username, "Authentication failed");
        Err("Invalid credentials".to_string())
    }
}

// メトリクスの実装
use prometheus::{Counter, Histogram, Registry};
use lazy_static::lazy_static;

lazy_static! {
    static ref HTTP_REQUESTS_TOTAL: Counter = Counter::new(
        "http_requests_total", 
        "Total number of HTTP requests"
    ).unwrap();
    
    static ref HTTP_REQUEST_DURATION: Histogram = Histogram::with_opts(
        prometheus::HistogramOpts::new(
            "http_request_duration_seconds",
            "HTTP request duration in seconds"
        )
    ).unwrap();
}

// メトリクス付きハンドラー
async fn handle_request(path: &str) -> Result<String, Box<dyn std::error::Error>> {
    HTTP_REQUESTS_TOTAL.inc();
    let timer = HTTP_REQUEST_DURATION.start_timer();
    
    let result = process_request(path).await;
    
    timer.observe_duration();
    
    result
}

async fn process_request(path: &str) -> Result<String, Box<dyn std::error::Error>> {
    // リクエスト処理ロジック
    Ok(format!("Processed: {}", path))
}

// 構造化ログの設定
fn setup_logging() {
    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
    
    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer()
            .json() // JSON形式でログ出力
            .with_target(true)
            .with_thread_ids(true)
            .with_thread_names(true))
        .init();
}

ベストプラクティス

  • tracingクレートを使用して構造化ログを実装
  • センシティブな情報はログから除外する
  • 適切なログレベルを使用する(DEBUG, INFO, WARN, ERROR)
  • Prometheusなどの標準的なメトリクス形式を採用

6. 設定管理とシークレット処理

なぜ重要か

プロダクション環境では、環境ごとに異なる設定値を管理し、シークレット情報を安全に扱う必要があります。Rustでは型安全な設定管理が可能です。

実装パターン

use serde::{Deserialize, Serialize};
use config::{Config, ConfigError, Environment, File};

#[derive(Debug, Deserialize, Serialize)]
struct DatabaseConfig {
    host: String,
    port: u16,
    username: String,
    #[serde(skip_serializing)] // シリアライズ時に除外
    password: String,
    database_name: String,
    max_connections: u32,
}

#[derive(Debug, Deserialize, Serialize)]
struct ServerConfig {
    host: String,
    port: u16,
    workers: usize,
}

#[derive(Debug, Deserialize, Serialize)]
struct AppConfig {
    database: DatabaseConfig,
    server: ServerConfig,
    jwt_secret: String,
    log_level: String,
}

impl AppConfig {
    fn new() -> Result<Self, ConfigError> {
        let config = Config::builder()
            // デフォルト値を設定
            .set_default("server.host", "127.0.0.1")?
            .set_default("server.port", 8080)?
            .set_default("server.workers", num_cpus::get())?
            .set_default("database.max_connections", 10)?
            .set_default("log_level", "info")?
            
            // 設定ファイルを読み込む(環境に応じて)
            .add_source(File::with_name("config/default"))
            .add_source(File::with_name(&format!("config/{}", 
                std::env::var("RUN_ENV").unwrap_or_else(|_| "development".to_string())
            )).required(false))
            
            // 環境変数でオーバーライド(APP_ プレフィックス)
            .add_source(Environment::with_prefix("APP").separator("_"))
            
            .build()?;
        
        // 設定を構造体にデシリアライズ
        let mut app_config: AppConfig = config.try_deserialize()?;
        
        // シークレットの追加処理
        app_config.database.password = Self::load_secret("DB_PASSWORD")?;
        app_config.jwt_secret = Self::load_secret("JWT_SECRET")?;
        
        Ok(app_config)
    }
    
    fn load_secret(key: &str) -> Result<String, ConfigError> {
        // 環境変数から読み込む
        std::env::var(key)
            .map_err(|_| ConfigError::Message(format!("Secret {} not found", key)))
    }
    
    // 設定の検証
    fn validate(&self) -> Result<(), String> {
        if self.database.username.is_empty() {
            return Err("Database username cannot be empty".to_string());
        }
        
        if self.server.port == 0 {
            return Err("Server port must be greater than 0".to_string());
        }
        
        if self.jwt_secret.len() < 32 {
            return Err("JWT secret must be at least 32 characters".to_string());
        }
        
        Ok(())
    }
}

// 設定のホットリロード
use notify::{Watcher, RecursiveMode, watcher};
use std::sync::{Arc, RwLock};
use std::sync::mpsc::channel;
use std::time::Duration;

fn watch_config(config: Arc<RwLock<AppConfig>>) {
    let (tx, rx) = channel();
    
    let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap();
    watcher.watch("config/", RecursiveMode::Recursive).unwrap();
    
    std::thread::spawn(move || {
        loop {
            match rx.recv() {
                Ok(_) => {
                    match AppConfig::new() {
                        Ok(new_config) => {
                            if let Err(e) = new_config.validate() {
                                error!("Invalid configuration: {}", e);
                                continue;
                            }
                            
                            let mut config_guard = config.write().unwrap();
                            *config_guard = new_config;
                            info!("Configuration reloaded successfully");
                        }
                        Err(e) => error!("Failed to reload configuration: {}", e),
                    }
                }
                Err(e) => error!("Watch error: {}", e),
            }
        }
    });
}

ベストプラクティス

  • 環境変数とファイルベースの設定を組み合わせる
  • シークレットは専用の管理システム(Vault、AWS Secrets Manager)から読み込む
  • 設定の検証を実装し、起動時にチェックする
  • センシティブな情報のログ出力を防ぐ

7. テストとモックの戦略

なぜ重要か

Rustの強力な型システムを活用しても、ビジネスロジックの正確性を保証するにはテストが不可欠です。モックを使用することで、外部依存を分離したテストが可能になります。

実装パターン

// テスト対象のコード
#[derive(Debug, PartialEq)]
struct Order {
    id: u64,
    user_id: u64,
    total: f64,
    status: OrderStatus,
}

#[derive(Debug, PartialEq)]
enum OrderStatus {
    Pending,
    Confirmed,
    Shipped,
    Delivered,
}

// 依存性注入のためのトレイト
trait PaymentGateway {
    fn process_payment(&self, amount: f64) -> Result<String, String>;
}

trait NotificationService {
    fn send_notification(&self, user_id: u64, message: &str) -> Result<(), String>;
}

struct OrderService<P: PaymentGateway, N: NotificationService> {
    payment_gateway: P,
    notification_service: N,
}

impl<P: PaymentGateway, N: NotificationService> OrderService<P, N> {
    fn new(payment_gateway: P, notification_service: N) -> Self {
        OrderService {
            payment_gateway,
            notification_service,
        }
    }
    
    fn process_order(&self, order: &mut Order) -> Result<(), String> {
        // 支払い処理
        let transaction_id = self.payment_gateway.process_payment(order.total)?;
        
        // ステータス更新
        order.status = OrderStatus::Confirmed;
        
        // 通知送信
        self.notification_service.send_notification(
            order.user_id,
            &format!("Order {} confirmed. Transaction: {}", order.id, transaction_id)
        )?;
        
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use mockall::*;
    
    // モックの定義
    #[automock]
    trait PaymentGateway {
        fn process_payment(&self, amount: f64) -> Result<String, String>;
    }
    
    #[automock]
    trait NotificationService {
        fn send_notification(&self, user_id: u64, message: &str) -> Result<(), String>;
    }
    
    #[test]
    fn test_process_order_success() {
        // モックの設定
        let mut mock_payment = MockPaymentGateway::new();
        mock_payment
            .expect_process_payment()
            .with(eq(100.0))
            .times(1)
            .returning(|_| Ok("TX123".to_string()));
        
        let mut mock_notification = MockNotificationService::new();
        mock_notification
            .expect_send_notification()
            .with(eq(1), mockall::predicate::str::contains("Order 1 confirmed"))
            .times(1)
            .returning(|_, _| Ok(()));
        
        // テスト実行
        let service = OrderService::new(mock_payment, mock_notification);
        let mut order = Order {
            id: 1,
            user_id: 1,
            total: 100.0,
            status: OrderStatus::Pending,
        };
        
        assert!(service.process_order(&mut order).is_ok());
        assert_eq!(order.status, OrderStatus::Confirmed);
    }
    
    #[test]
    fn test_process_order_payment_failure() {
        // 支払い失敗のモック
        let mut mock_payment = MockPaymentGateway::new();
        mock_payment
            .expect_process_payment()
            .returning(|_| Err("Payment declined".to_string()));
        
        let mock_notification = MockNotificationService::new();
        
        let service = OrderService::new(mock_payment, mock_notification);
        let mut order = Order {
            id: 2,
            user_id: 1,
            total: 100.0,
            status: OrderStatus::Pending,
        };
        
        let result = service.process_order(&mut order);
        assert!(result.is_err());
        assert_eq!(order.status, OrderStatus::Pending); // ステータスは変更されない
    }
    
    // プロパティベーステスト
    use proptest::prelude::*;
    
    proptest! {
        #[test]
        fn test_order_total_always_positive(
            id in 1..1000u64,
            user_id in 1..100u64,
            total in 0.01..10000.0f64
        ) {
            let order = Order {
                id,
                user_id,
                total,
                status: OrderStatus::Pending,
            };
            
            prop_assert!(order.total > 0.0);
        }
    }
    
    // 統合テスト
    #[test]
    fn test_integration_with_real_services() {
        // テスト用の実装を使用
        struct TestPaymentGateway;
        impl PaymentGateway for TestPaymentGateway {
            fn process_payment(&self, _amount: f64) -> Result<String, String> {
                Ok("TEST_TX_123".to_string())
            }
        }
        
        struct TestNotificationService;
        impl NotificationService for TestNotificationService {
            fn send_notification(&self, _user_id: u64, _message: &str) -> Result<(), String> {
                Ok(())
            }
        }
        
        let service = OrderService::new(TestPaymentGateway, TestNotificationService);
        let mut order = Order {
            id: 3,
            user_id: 1,
            total: 50.0,
            status: OrderStatus::Pending,
        };
        
        assert!(service.process_order(&mut order).is_ok());
    }
}

// ベンチマークテスト
#[cfg(test)]
mod benches {
    use super::*;
    use test::Bencher;
    
    #[bench]
    fn bench_process_order(b: &mut Bencher) {
        struct DummyPaymentGateway;
        impl PaymentGateway for DummyPaymentGateway {
            fn process_payment(&self, _: f64) -> Result<String, String> {
                Ok("dummy".to_string())
            }
        }
        
        struct DummyNotificationService;
        impl NotificationService for DummyNotificationService {
            fn send_notification(&self, _: u64, _: &str) -> Result<(), String> {
                Ok(())
            }
        }
        
        let service = OrderService::new(DummyPaymentGateway, DummyNotificationService);
        
        b.iter(|| {
            let mut order = Order {
                id: 1,
                user_id: 1,
                total: 100.0,
                status: OrderStatus::Pending,
            };
            service.process_order(&mut order)
        });
    }
}

ベストプラクティス

  • 単体テスト、統合テスト、E2Eテストを適切に使い分ける
  • mockallクレートを使用してモックを簡単に作成
  • プロパティベーステストで境界値をカバー
  • CIパイプラインでテストを自動実行

8. 並行処理とデータ競合の防止

なぜ重要か

マルチスレッド環境でのデータ競合は、予測不可能なバグの原因となります。Rustの型システムは、コンパイル時にデータ競合を防ぐことができます。

実装パターン

use std::sync::{Arc, Mutex, RwLock, atomic::{AtomicU64, Ordering}};
use std::thread;
use std::collections::HashMap;
use parking_lot;
use dashmap::DashMap;

// 基本的なMutexの使用
struct SharedCounter {
    count: Mutex<u64>,
}

impl SharedCounter {
    fn new() -> Self {
        SharedCounter {
            count: Mutex::new(0),
        }
    }
    
    fn increment(&self) {
        let mut count = self.count.lock().unwrap();
        *count += 1;
    }
    
    fn get(&self) -> u64 {
        *self.count.lock().unwrap()
    }
}

// RwLockを使用した読み取り最適化
struct Cache {
    data: RwLock<HashMap<String, String>>,
}

impl Cache {
    fn new() -> Self {
        Cache {
            data: RwLock::new(HashMap::new()),
        }
    }
    
    fn get(&self, key: &str) -> Option<String> {
        self.data.read().unwrap().get(key).cloned()
    }
    
    fn insert(&self, key: String, value: String) {
        self.data.write().unwrap().insert(key, value);
    }
}

// Atomicを使用したロックフリーカウンター
struct AtomicCounter {
    count: AtomicU64,
}

impl AtomicCounter {
    fn new() -> Self {
        AtomicCounter {
            count: AtomicU64::new(0),
        }
    }
    
    fn increment(&self) -> u64 {
        self.count.fetch_add(1, Ordering::SeqCst)
    }
    
    fn get(&self) -> u64 {
        self.count.load(Ordering::SeqCst)
    }
}

// より高性能な並行データ構造
struct HighPerformanceCache {
    // DashMapは内部でシャーディングを使用
    data: DashMap<String, String>,
    // parking_lotのMutexは標準ライブラリより高速
    stats: parking_lot::Mutex<CacheStats>,
}

struct CacheStats {
    hits: u64,
    misses: u64,
}

impl HighPerformanceCache {
    fn new() -> Self {
        HighPerformanceCache {
            data: DashMap::new(),
            stats: parking_lot::Mutex::new(CacheStats { hits: 0, misses: 0 }),
        }
    }
    
    fn get(&self, key: &str) -> Option<String> {
        let result = self.data.get(key).map(|v| v.clone());
        
        let mut stats = self.stats.lock();
        if result.is_some() {
            stats.hits += 1;
        } else {
            stats.misses += 1;
        }
        
        result
    }
    
    fn insert(&self, key: String, value: String) {
        self.data.insert(key, value);
    }
}

// チャネルを使用したアクター風パターン
use std::sync::mpsc::{channel, Sender};

enum CacheCommand {
    Get { key: String, response: Sender<Option<String>> },
    Insert { key: String, value: String },
    Clear,
}

struct ActorCache {
    sender: Sender<CacheCommand>,
}

impl ActorCache {
    fn new() -> Self {
        let (sender, receiver) = channel();
        
        thread::spawn(move || {
            let mut cache = HashMap::new();
            
            for command in receiver {
                match command {
                    CacheCommand::Get { key, response } => {
                        let _ = response.send(cache.get(&key).cloned());
                    }
                    CacheCommand::Insert { key, value } => {
                        cache.insert(key, value);
                    }
                    CacheCommand::Clear => {
                        cache.clear();
                    }
                }
            }
        });
        
        ActorCache { sender }
    }
    
    fn get(&self, key: String) -> Option<String> {
        let (response_sender, response_receiver) = channel();
        self.sender.send(CacheCommand::Get {
            key,
            response: response_sender,
        }).unwrap();
        response_receiver.recv().unwrap()
    }
    
    fn insert(&self, key: String, value: String) {
        self.sender.send(CacheCommand::Insert { key, value }).unwrap();
    }
}

// 使用例
fn concurrent_example() {
    let cache = Arc::new(HighPerformanceCache::new());
    let mut handles = vec![];
    
    // 複数のスレッドから同時アクセス
    for i in 0..10 {
        let cache_clone = Arc::clone(&cache);
        let handle = thread::spawn(move || {
            for j in 0..1000 {
                let key = format!("key_{}_{}", i, j);
                let value = format!("value_{}_{}", i, j);
                cache_clone.insert(key.clone(), value);
                
                // 読み取りテスト
                if let Some(v) = cache_clone.get(&key) {
                    assert_eq!(v, format!("value_{}_{}", i, j));
                }
            }
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

ベストプラクティス

  • 適切な同期プリミティブを選択(Mutex、RwLock、Atomic)
  • parking_lotクレートでパフォーマンスを向上
  • DashMapなどの並行データ構造を活用
  • デッドロックを避けるため、ロックの順序を統一

9. ビルドとデプロイの最適化

なぜ重要か

プロダクション環境では、ビルドサイズの削減と起動時間の短縮が重要です。Rustは様々な最適化オプションを提供しています。

実装パターン

# Cargo.toml
[package]
name = "production-app"
version = "1.0.0"
edition = "2021"

# 依存関係の最適化
[dependencies]
# 必要な機能のみを有効化
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] }
serde = { version = "1", features = ["derive"] }
# 開発時のみ必要な依存関係
[dev-dependencies]
criterion = "0.5"
proptest = "1"

# リリースビルドの最適化
[profile.release]
opt-level = 3          # 最大最適化
lto = true            # Link Time Optimization
codegen-units = 1     # 単一のコード生成ユニット
strip = true          # シンボル情報を削除
panic = "abort"       # パニック時のスタック巻き戻しを無効化

# カスタムプロファイル(プロファイリング用)
[profile.release-with-debug]
inherits = "release"
debug = true

# ベンチマーク用プロファイル
[profile.bench]
opt-level = 3
// build.rs - ビルドスクリプト
use std::env;
use std::process::Command;

fn main() {
    // Git情報を埋め込む
    let output = Command::new("git")
        .args(&["rev-parse", "HEAD"])
        .output()
        .unwrap();
    let git_hash = String::from_utf8(output.stdout).unwrap();
    println!("cargo:rustc-env=GIT_HASH={}", git_hash.trim());
    
    // ビルド時刻を埋め込む
    let build_time = chrono::Utc::now().to_rfc3339();
    println!("cargo:rustc-env=BUILD_TIME={}", build_time);
    
    // 条件付きコンパイル
    if env::var("CARGO_FEATURE_MONITORING").is_ok() {
        println!("cargo:rustc-cfg=monitoring_enabled");
    }
}

// main.rs - バージョン情報の使用
const VERSION: &str = env!("CARGO_PKG_VERSION");
const GIT_HASH: &str = env!("GIT_HASH");
const BUILD_TIME: &str = env!("BUILD_TIME");

fn print_version() {
    println!("Version: {}", VERSION);
    println!("Git Hash: {}", GIT_HASH);
    println!("Build Time: {}", BUILD_TIME);
}

// 条件付きコンパイルの使用
#[cfg(monitoring_enabled)]
fn setup_monitoring() {
    // モニタリングのセットアップ
}

#[cfg(not(monitoring_enabled))]
fn setup_monitoring() {
    // 何もしない
}
# Dockerfile - マルチステージビルド
# ビルドステージ
FROM rust:1.70 AS builder

WORKDIR /app
COPY Cargo.toml Cargo.lock ./

# 依存関係のキャッシュ
RUN mkdir src && \
    echo "fn main() {}" > src/main.rs && \
    cargo build --release && \
    rm -rf src

# ソースコードをコピーしてビルド
COPY . .
RUN cargo build --release

# 実行ステージ
FROM debian:bookworm-slim

# 必要なライブラリのみインストール
RUN apt-get update && \
    apt-get install -y ca-certificates && \
    rm -rf /var/lib/apt/lists/*

# ユーザーを作成
RUN useradd -m -u 1001 appuser

WORKDIR /app

# バイナリのみコピー
COPY --from=builder /app/target/release/production-app /app/

# 所有権を設定
RUN chown -R appuser:appuser /app

USER appuser

EXPOSE 8080

CMD ["./production-app"]
# .github/workflows/release.yml - GitHub Actions
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-gnu
          - x86_64-unknown-linux-musl
          - aarch64-unknown-linux-gnu
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Install Rust
      uses: actions-rs/toolchain@v1
      with:
        toolchain: stable
        target: ${{ matrix.target }}
        override: true
    
    - name: Cache cargo registry
      uses: actions/cache@v3
      with:
        path: ~/.cargo/registry
        key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
    
    - name: Build
      run: cargo build --release --target ${{ matrix.target }}
    
    - name: Strip binary
      run: strip target/${{ matrix.target }}/release/production-app
    
    - name: Upload artifact
      uses: actions/upload-artifact@v3
      with:
        name: production-app-${{ matrix.target }}
        path: target/${{ matrix.target }}/release/production-app

ベストプラクティス

  • LTOとコード生成ユニットの最適化でバイナリサイズを削減
  • マルチステージDockerビルドで最小限のイメージを作成
  • 複数のターゲットアーキテクチャをサポート
  • CIでのキャッシュを活用してビルド時間を短縮

10. グレースフルシャットダウンとリソース管理

なぜ重要か

プロダクション環境では、アプリケーションの停止時にデータの整合性を保ち、進行中の処理を適切に完了させる必要があります。

実装パターン

use tokio::signal;
use tokio::sync::{broadcast, mpsc};
use tokio::time::{timeout, Duration};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

// シャットダウン管理
struct ShutdownManager {
    shutdown_tx: broadcast::Sender<()>,
    is_shutting_down: Arc<AtomicBool>,
}

impl ShutdownManager {
    fn new() -> Self {
        let (shutdown_tx, _) = broadcast::channel(1);
        ShutdownManager {
            shutdown_tx,
            is_shutting_down: Arc::new(AtomicBool::new(false)),
        }
    }
    
    fn subscribe(&self) -> broadcast::Receiver<()> {
        self.shutdown_tx.subscribe()
    }
    
    fn is_shutting_down(&self) -> bool {
        self.is_shutting_down.load(Ordering::SeqCst)
    }
    
    async fn shutdown(&self) {
        self.is_shutting_down.store(true, Ordering::SeqCst);
        let _ = self.shutdown_tx.send(());
    }
}

// グレースフル停止可能なワーカー
struct Worker {
    id: u32,
    shutdown_rx: broadcast::Receiver<()>,
    task_rx: mpsc::Receiver<Task>,
}

struct Task {
    id: u64,
    data: String,
}

impl Worker {
    async fn run(mut self) {
        loop {
            tokio::select! {
                // シャットダウンシグナル
                _ = self.shutdown_rx.recv() => {
                    info!("Worker {} received shutdown signal", self.id);
                    break;
                }
                
                // タスク処理
                Some(task) = self.task_rx.recv() => {
                    self.process_task(task).await;
                }
            }
        }
        
        // 残りのタスクを処理
        self.drain_tasks().await;
        info!("Worker {} shutdown complete", self.id);
    }
    
    async fn process_task(&self, task: Task) {
        info!("Worker {} processing task {}", self.id, task.id);
        // タスク処理のシミュレーション
        tokio::time::sleep(Duration::from_millis(100)).await;
    }
    
    async fn drain_tasks(&mut self) {
        let drain_timeout = Duration::from_secs(30);
        let start = tokio::time::Instant::now();
        
        while let Ok(Some(task)) = timeout(
            drain_timeout.saturating_sub(start.elapsed()),
            self.task_rx.recv()
        ).await {
            self.process_task(task).await;
        }
    }
}

// リソースのクリーンアップ
struct ResourceManager {
    database_pool: Arc<DatabasePool>,
    cache: Arc<Cache>,
}

struct DatabasePool {
    // 実際のデータベース接続プール
}

impl DatabasePool {
    async fn close(&self) {
        info!("Closing database connections...");
        // すべての接続を閉じる
        tokio::time::sleep(Duration::from_millis(500)).await;
        info!("Database connections closed");
    }
}

impl ResourceManager {
    async fn cleanup(&self) {
        // 並行してクリーンアップ
        let (db_result, cache_result) = tokio::join!(
            self.database_pool.close(),
            self.cache.flush()
        );
        
        info!("All resources cleaned up");
    }
}

// メインアプリケーション
struct Application {
    shutdown_manager: ShutdownManager,
    workers: Vec<mpsc::Sender<Task>>,
    resource_manager: ResourceManager,
}

impl Application {
    async fn run(&self) {
        // シグナルハンドラーの設定
        let shutdown_manager = self.shutdown_manager.clone();
        tokio::spawn(async move {
            shutdown_signal().await;
            info!("Shutdown signal received");
            shutdown_manager.shutdown().await;
        });
        
        // アプリケーションのメインループ
        let mut shutdown_rx = self.shutdown_manager.subscribe();
        
        loop {
            tokio::select! {
                _ = shutdown_rx.recv() => {
                    info!("Application shutting down...");
                    break;
                }
                
                // 通常の処理
                _ = self.process_requests() => {}
            }
        }
        
        // グレースフルシャットダウン
        self.graceful_shutdown().await;
    }
    
    async fn process_requests(&self) {
        // リクエスト処理のシミュレーション
        tokio::time::sleep(Duration::from_secs(1)).await;
    }
    
    async fn graceful_shutdown(&self) {
        info!("Starting graceful shutdown...");
        
        // 新規リクエストの受付を停止
        
        // ワーカーへのタスク送信を停止
        for sender in &self.workers {
            drop(sender);
        }
        
        // タイムアウト付きでシャットダウンを待つ
        let shutdown_timeout = Duration::from_secs(60);
        
        match timeout(shutdown_timeout, self.wait_for_workers()).await {
            Ok(_) => info!("All workers shut down successfully"),
            Err(_) => error!("Worker shutdown timed out"),
        }
        
        // リソースのクリーンアップ
        self.resource_manager.cleanup().await;
        
        info!("Graceful shutdown complete");
    }
    
    async fn wait_for_workers(&self) {
        // ワーカーの終了を待つロジック
        tokio::time::sleep(Duration::from_secs(5)).await;
    }
}

// クロスプラットフォーム対応のシグナルハンドリング
async fn shutdown_signal() {
    let ctrl_c = async {
        signal::ctrl_c()
            .await
            .expect("failed to install Ctrl+C handler");
    };

    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("failed to install signal handler")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }
}

// メイン関数
#[tokio::main]
async fn main() {
    // ロギングの初期化
    setup_logging();
    
    // アプリケーションの初期化
    let app = Application::new().await;
    
    // アプリケーション実行
    app.run().await;
    
    info!("Application terminated");
}

ベストプラクティス

  • シグナルハンドリングで適切な停止処理を実装
  • タイムアウトを設定して無限に待機することを防ぐ
  • リソースの並行クリーンアップで停止時間を短縮
  • 進行中の処理を適切に完了させる

まとめ

これらの10個のパターンは、Rustでプロダクショングレードなアプリケーションを構築する際の基礎となります。各パターンは独立して使用できますが、組み合わせることでより堅牢なシステムを構築できます。

重要なポイント:

  1. Rustの型システムを最大限活用してコンパイル時にエラーを防ぐ
  2. 適切な抽象化により、テスタブルで保守しやすいコードを書く
  3. パフォーマンスと安全性のバランスを考慮する
  4. プロダクション環境での運用を見据えた設計をする

これらのパターンを実践することで、安全で高性能、かつ保守しやすいRustアプリケーションを構築できるでしょう。

GitHubで編集を提案

Discussion