Rust

Projetos Integradores — Construindo Sistemas Completos do Zero Já leu

23 min de leitura

Projetos Integradores — Construindo Sistemas Completos do Zero
Rust — Artigo #51 Projetos Integradores — Construindo Sistemas Completos do Zero Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano Cinquenta artigos

Rust — Artigo #51

Projetos Integradores — Construindo Sistemas Completos do Zero

Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano


Cinquenta artigos de fundamentos, padrões, ferramentas e domínios específicos. Agora o momento de integrar. Um projeto real não é uma coleção de módulos independentes — é um sistema onde autenticação fala com banco de dados, que fala com cache, que expõe uma API, que é monitorada por métricas, que é implantada com Docker. A complexidade emerge da integração.

Este artigo apresenta três projetos integradores completos em escala crescente, cada um combinando técnicas de múltiplos artigos anteriores. Não são demonstrações de features isoladas — são sistemas que você poderia colocar em produção.


Projeto 1: Motor de busca de texto completo

Um motor de busca do zero: indexação invertida, ranking TF-IDF, busca booleana, persistência, e API HTTP.

[package]
name = "buscador"
version = "0.1.0"
edition = "2021"

[dependencies]
axum          = "0.7"
tokio         = { version = "1", features = ["full"] }
serde         = { version = "1", features = ["derive"] }
serde_json    = "1"
unicode-segmentation = "1"
rust-stemmers = "1"
dashmap       = "5"
rayon         = "1"
anyhow        = "1"
tracing       = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

Índice invertido

// src/indice.rs
use dashmap::DashMap;
use rayon::prelude::*;
use rust_stemmers::{Algorithm, Stemmer};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use unicode_segmentation::UnicodeSegmentation;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Documento {
    pub id: u64,
    pub titulo: String,
    pub corpo: String,
    pub tags: Vec<String>,
    pub timestamp: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntradaIndice {
    pub doc_id: u64,
    pub frequencia: u32,
    pub posicoes: Vec<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResultadoBusca {
    pub doc_id: u64,
    pub titulo: String,
    pub score: f64,
    pub trecho: String,
}

pub struct IndiceInvertido {
    // termo → lista de (doc_id, freq, posições)
    indice: DashMap<String, Vec<EntradaIndice>>,
    // doc_id → documento
    documentos: DashMap<u64, Documento>,
    // doc_id → número de termos no documento
    comprimentos: DashMap<u64, u32>,
    proximo_id: AtomicU64,
    stemmer: Stemmer,
}

impl IndiceInvertido {
    pub fn novo() -> Arc<Self> {
        Arc::new(IndiceInvertido {
            indice: DashMap::new(),
            documentos: DashMap::new(),
            comprimentos: DashMap::new(),
            proximo_id: AtomicU64::new(1),
            stemmer: Stemmer::create(Algorithm::Portuguese),
        })
    }

    pub fn indexar(&self, titulo: &str, corpo: &str, tags: Vec<String>) -> u64 {
        let id = self.proximo_id.fetch_add(1, Ordering::SeqCst);

        let texto_completo = format!("{titulo} {corpo}");
        let termos = self.tokenizar(&texto_completo);
        let n_termos = termos.len() as u32;

        // Conta frequência e posições de cada termo
        let mut freq_pos: HashMap<String, (u32, Vec<u32>)> = HashMap::new();
        for (pos, termo) in termos.iter().enumerate() {
            let entry = freq_pos.entry(termo.clone()).or_insert((0, vec![]));
            entry.0 += 1;
            entry.1.push(pos as u32);
        }

        // Adiciona ao índice invertido
        for (termo, (freq, posicoes)) in freq_pos {
            self.indice
                .entry(termo)
                .or_insert_with(Vec::new)
                .push(EntradaIndice {
                    doc_id: id,
                    frequencia: freq,
                    posicoes,
                });
        }

        // Indexa as tags
        for tag in &tags {
            let termo = format!("tag:{}", tag.to_lowercase());
            self.indice
                .entry(termo)
                .or_insert_with(Vec::new)
                .push(EntradaIndice {
                    doc_id: id,
                    frequencia: 1,
                    posicoes: vec![],
                });
        }

        let doc = Documento {
            id,
            titulo: titulo.to_string(),
            corpo: corpo.to_string(),
            tags,
            timestamp: chrono::Utc::now().timestamp(),
        };

        self.documentos.insert(id, doc);
        self.comprimentos.insert(id, n_termos);

        tracing::info!(
            doc_id = id,
            titulo = %titulo,
            n_termos,
            "Documento indexado"
        );

        id
    }

    pub fn buscar(&self, consulta: &str, max_resultados: usize) -> Vec<ResultadoBusca> {
        let termos_busca = self.tokenizar(consulta);
        if termos_busca.is_empty() {
            return vec![];
        }

        let n_docs = self.documentos.len() as f64;

        // TF-IDF scoring
        let mut scores: HashMap<u64, f64> = HashMap::new();

        for termo in &termos_busca {
            let postings = match self.indice.get(termo) {
                Some(p) => p,
                None => continue,
            };

            // IDF = log(N / df) onde df = doc frequency
            let df = postings.len() as f64;
            let idf = (n_docs / df).ln().max(0.0);

            for entrada in postings.iter() {
                // TF = freq / comprimento do documento
                let comprimento = *self.comprimentos
                    .get(&entrada.doc_id)
                    .map(|v| v.value())
                    .unwrap_or(&1) as f64;

                let tf = entrada.frequencia as f64 / comprimento;

                // TF-IDF normalizado
                *scores.entry(entrada.doc_id).or_insert(0.0) += tf * idf;

                // Boost para termos no título
                if let Some(doc) = self.documentos.get(&entrada.doc_id) {
                    if doc.titulo.to_lowercase().contains(termo.as_str()) {
                        *scores.entry(entrada.doc_id).or_insert(0.0) += idf * 2.0;
                    }
                }
            }
        }

        // Ordena por score e retorna top N
        let mut resultados: Vec<(u64, f64)> = scores.into_iter().collect();
        resultados.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
        resultados.truncate(max_resultados);

        resultados.into_iter().filter_map(|(doc_id, score)| {
            let doc = self.documentos.get(&doc_id)?;
            let trecho = extrair_trecho(&doc.corpo, &termos_busca, 200);

            Some(ResultadoBusca {
                doc_id,
                titulo: doc.titulo.clone(),
                score,
                trecho,
            })
        }).collect()
    }

    pub fn buscar_booleana(&self, consulta: &ConsultaBooleana) -> Vec<u64> {
        match consulta {
            ConsultaBooleana::Termo(t) => {
                let termo = self.normalizar_termo(t);
                self.indice
                    .get(&termo)
                    .map(|p| p.iter().map(|e| e.doc_id).collect())
                    .unwrap_or_default()
            }

            ConsultaBooleana::E(esq, dir) => {
                let set_esq: HashSet<u64> =
                    self.buscar_booleana(esq).into_iter().collect();
                let set_dir: HashSet<u64> =
                    self.buscar_booleana(dir).into_iter().collect();
                set_esq.intersection(&set_dir).copied().collect()
            }

            ConsultaBooleana::Ou(esq, dir) => {
                let set_esq: HashSet<u64> =
                    self.buscar_booleana(esq).into_iter().collect();
                let set_dir: HashSet<u64> =
                    self.buscar_booleana(dir).into_iter().collect();
                set_esq.union(&set_dir).copied().collect()
            }

            ConsultaBooleana::Nao(inner) => {
                let excluidos: HashSet<u64> =
                    self.buscar_booleana(inner).into_iter().collect();
                self.documentos
                    .iter()
                    .map(|e| *e.key())
                    .filter(|id| !excluidos.contains(id))
                    .collect()
            }
        }
    }

    fn tokenizar(&self, texto: &str) -> Vec<String> {
        texto
            .unicode_words()
            .map(|w| self.normalizar_termo(w))
            .filter(|t| t.len() > 2 && !STOPWORDS.contains(t.as_str()))
            .collect()
    }

    fn normalizar_termo(&self, termo: &str) -> String {
        let lower = termo.to_lowercase();
        self.stemmer.stem(&lower).to_string()
    }

    pub fn estatisticas(&self) -> EstatisticasIndice {
        EstatisticasIndice {
            n_documentos: self.documentos.len(),
            n_termos_unicos: self.indice.len(),
            tamanho_medio_doc: if self.documentos.is_empty() {
                0.0
            } else {
                self.comprimentos.iter().map(|e| *e.value() as f64).sum::<f64>()
                    / self.documentos.len() as f64
            },
        }
    }
}

#[derive(Debug, Serialize)]
pub struct EstatisticasIndice {
    pub n_documentos: usize,
    pub n_termos_unicos: usize,
    pub tamanho_medio_doc: f64,
}

pub enum ConsultaBooleana {
    Termo(String),
    E(Box<ConsultaBooleana>, Box<ConsultaBooleana>),
    Ou(Box<ConsultaBooleana>, Box<ConsultaBooleana>),
    Nao(Box<ConsultaBooleana>),
}

fn extrair_trecho(
    texto: &str,
    termos: &[String],
    max_len: usize,
) -> String {
    // Encontra a posição do primeiro termo no texto
    let lower = texto.to_lowercase();
    let pos = termos.iter()
        .filter_map(|t| lower.find(t.as_str()))
        .min()
        .unwrap_or(0);

    let inicio = pos.saturating_sub(50);
    let fim = (pos + max_len).min(texto.len());

    let trecho = &texto[inicio..fim];
    if inicio > 0 {
        format!("...{}", trecho.trim())
    } else {
        trecho.trim().to_string()
    }
}

static STOPWORDS: &[&str] = &[
    "de", "do", "da", "dos", "das", "em", "no", "na", "nos", "nas",
    "um", "uma", "uns", "umas", "o", "a", "os", "as", "e", "ou",
    "que", "se", "com", "por", "para", "como", "mais", "mas",
];

API HTTP

// src/api.rs
use axum::{
    extract::{Path, Query, State},
    routing::{get, post, delete},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::indice::{IndiceInvertido, ResultadoBusca, EstatisticasIndice};

type EstadoCompartilhado = Arc<IndiceInvertido>;

#[derive(Deserialize)]
struct ParamsBusca {
    q: String,
    #[serde(default = "limite_padrao")]
    limite: usize,
}
fn limite_padrao() -> usize { 10 }

#[derive(Deserialize)]
struct BodyIndexar {
    titulo: String,
    corpo: String,
    #[serde(default)]
    tags: Vec<String>,
}

#[derive(Serialize)]
struct RespostaIndexar {
    id: u64,
    mensagem: String,
}

#[derive(Serialize)]
struct RespostaBusca {
    consulta: String,
    total: usize,
    resultados: Vec<ResultadoBusca>,
    tempo_ms: u128,
}

async fn indexar(
    State(indice): State<EstadoCompartilhado>,
    Json(body): Json<BodyIndexar>,
) -> Json<RespostaIndexar> {
    let inicio = std::time::Instant::now();

    let id = indice.indexar(&body.titulo, &body.corpo, body.tags);

    tracing::info!(
        doc_id = id,
        duracao_ms = inicio.elapsed().as_millis(),
        "Documento indexado via API"
    );

    Json(RespostaIndexar {
        id,
        mensagem: format!("Documento {id} indexado com sucesso"),
    })
}

async fn buscar(
    State(indice): State<EstadoCompartilhado>,
    Query(params): Query<ParamsBusca>,
) -> Json<RespostaBusca> {
    let inicio = std::time::Instant::now();
    let consulta = params.q.clone();

    let resultados = indice.buscar(&params.q, params.limite);

    let duracao = inicio.elapsed().as_millis();

    tracing::info!(
        consulta = %consulta,
        n_resultados = resultados.len(),
        duracao_ms = duracao,
        "Busca realizada"
    );

    Json(RespostaBusca {
        consulta,
        total: resultados.len(),
        resultados,
        tempo_ms: duracao,
    })
}

async fn obter_documento(
    State(indice): State<EstadoCompartilhado>,
    Path(id): Path<u64>,
) -> Result<Json<crate::indice::Documento>, axum::http::StatusCode> {
    indice
        .documentos
        .get(&id)
        .map(|doc| Json(doc.clone()))
        .ok_or(axum::http::StatusCode::NOT_FOUND)
}

async fn estatisticas(
    State(indice): State<EstadoCompartilhado>,
) -> Json<EstatisticasIndice> {
    Json(indice.estatisticas())
}

pub fn criar_router(indice: Arc<IndiceInvertido>) -> Router {
    Router::new()
        .route("/documentos",       post(indexar))
        .route("/documentos/:id",   get(obter_documento))
        .route("/buscar",           get(buscar))
        .route("/estatisticas",     get(estatisticas))
        .with_state(indice)
}

Main e teste de integração

// src/main.rs
mod indice;
mod api;

use std::sync::Arc;
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env()
            .add_directive("buscador=info".parse()?))
        .init();

    let indice = indice::IndiceInvertido::novo();

    // Indexa documentos de exemplo
    let exemplos = vec![
        (
            "Introdução ao Rust",
            "Rust é uma linguagem de programação de sistemas focada em segurança e performance.",
            vec!["rust", "programação", "sistemas"],
        ),
        (
            "Async/Await em Rust",
            "O modelo assíncrono do Rust permite concorrência eficiente sem threads.",
            vec!["rust", "async", "concorrência"],
        ),
        (
            "Gerenciamento de Memória",
            "O sistema de ownership do Rust garante segurança de memória sem garbage collector.",
            vec!["rust", "memória", "ownership"],
        ),
        (
            "Traits e Generics",
            "Traits definem comportamento compartilhado. Generics permitem código reutilizável.",
            vec!["rust", "traits", "generics"],
        ),
    ];

    for (titulo, corpo, tags) in exemplos {
        let tags = tags.into_iter().map(String::from).collect();
        indice.indexar(titulo, corpo, tags);
    }

    let stats = indice.estatisticas();
    tracing::info!(
        n_docs = stats.n_documentos,
        n_termos = stats.n_termos_unicos,
        "Índice inicializado"
    );

    let router = api::criar_router(Arc::clone(&indice));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;

    tracing::info!("Servidor iniciado em http://0.0.0.0:3000");

    axum::serve(listener, router).await?;
    Ok(())
}
# Testando o motor de busca:

# Indexar documento
curl -X POST http://localhost:3000/documentos 
  -H "Content-Type: application/json" 
  -d '{"titulo":"Novo Artigo","corpo":"Conteúdo sobre Rust e async programming","tags":["rust","async"]}'

# Buscar
curl "http://localhost:3000/buscar?q=ownership+memoria"

# Estatísticas
curl http://localhost:3000/estatisticas

Projeto 2: Sistema de filas de tarefas distribuído

Um sistema de job queue com workers, retry, prioridade, e monitoramento.

[dependencies]
tokio        = { version = "1", features = ["full"] }
serde        = { version = "1", features = ["derive"] }
serde_json   = "1"
uuid         = { version = "1", features = ["v4"] }
chrono       = { version = "0.4", features = ["serde"] }
dashmap      = "5"
tokio-util   = "0.7"
anyhow       = "1"
thiserror    = "1"
tracing      = "0.1"
axum         = "0.7"
// src/fila.rs
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::sync::Arc;
use tokio::sync::{Mutex, Notify};
use uuid::Uuid;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Prioridade {
    Baixa = 1,
    Normal = 5,
    Alta = 10,
    Critica = 100,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum StatusTarefa {
    Pendente,
    Processando { worker_id: String },
    Concluida,
    Falhou { tentativas: u32, ultimo_erro: String },
    Cancelada,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tarefa {
    pub id: String,
    pub tipo: String,
    pub payload: serde_json::Value,
    pub prioridade: Prioridade,
    pub status: StatusTarefa,
    pub criada_em: DateTime<Utc>,
    pub processada_em: Option<DateTime<Utc>>,
    pub tentativas: u32,
    pub max_tentativas: u32,
    pub executar_apos: Option<DateTime<Utc>>,
}

impl Tarefa {
    pub fn nova(
        tipo: &str,
        payload: serde_json::Value,
        prioridade: Prioridade,
    ) -> Self {
        Tarefa {
            id: Uuid::new_v4().to_string(),
            tipo: tipo.to_string(),
            payload,
            prioridade,
            status: StatusTarefa::Pendente,
            criada_em: Utc::now(),
            processada_em: None,
            tentativas: 0,
            max_tentativas: 3,
            executar_apos: None,
        }
    }

    pub fn com_delay(mut self, delay: std::time::Duration) -> Self {
        self.executar_apos = Some(
            Utc::now() + chrono::Duration::from_std(delay).unwrap()
        );
        self
    }

    pub fn com_max_tentativas(mut self, n: u32) -> Self {
        self.max_tentativas = n;
        self
    }

    pub fn pronta_para_executar(&self) -> bool {
        match &self.executar_apos {
            None => true,
            Some(t) => Utc::now() >= *t,
        }
    }
}

// Wrapper para BinaryHeap com prioridade
#[derive(Debug)]
struct TarefaNaFila {
    prioridade: i32,
    criada_em: DateTime<Utc>,
    id: String,
}

impl PartialEq for TarefaNaFila {
    fn eq(&self, other: &Self) -> bool {
        self.prioridade == other.prioridade
            && self.criada_em == other.criada_em
    }
}

impl Eq for TarefaNaFila {}

impl PartialOrd for TarefaNaFila {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for TarefaNaFila {
    fn cmp(&self, other: &Self) -> Ordering {
        // Maior prioridade primeiro; em empate, mais antiga primeiro
        self.prioridade.cmp(&other.prioridade)
            .then(other.criada_em.cmp(&self.criada_em))
    }
}

pub struct FilaTarefas {
    heap: Mutex<BinaryHeap<TarefaNaFila>>,
    tarefas: DashMap<String, Tarefa>,
    notificador: Arc<Notify>,
}

impl FilaTarefas {
    pub fn nova() -> Arc<Self> {
        Arc::new(FilaTarefas {
            heap: Mutex::new(BinaryHeap::new()),
            tarefas: DashMap::new(),
            notificador: Arc::new(Notify::new()),
        })
    }

    pub async fn enfileirar(&self, tarefa: Tarefa) -> String {
        let id = tarefa.id.clone();
        let prioridade = tarefa.prioridade as i32;
        let criada_em = tarefa.criada_em;

        self.tarefas.insert(id.clone(), tarefa);

        self.heap.lock().await.push(TarefaNaFila {
            prioridade,
            criada_em,
            id: id.clone(),
        });

        self.notificador.notify_one();

        tracing::info!(tarefa_id = %id, "Tarefa enfileirada");
        id
    }

    pub async fn obter_proxima(&self) -> Option<Tarefa> {
        loop {
            let id = {
                let mut heap = self.heap.lock().await;
                loop {
                    let topo = heap.peek()?;
                    let tarefa = self.tarefas.get(&topo.id)?;

                    if !tarefa.pronta_para_executar() {
                        // Não há tarefa pronta ainda
                        drop(tarefa);
                        return None;
                    }

                    if matches!(tarefa.status, StatusTarefa::Pendente) {
                        break topo.id.clone();
                    }

                    // Remove tarefas em estado terminal do heap
                    heap.pop();
                }
            };

            // Tenta mudar status para Processando
            if let Some(mut tarefa) = self.tarefas.get_mut(&id) {
                if matches!(tarefa.status, StatusTarefa::Pendente) {
                    let worker_id = format!("worker-{}", Uuid::new_v4());
                    tarefa.status = StatusTarefa::Processando {
                        worker_id: worker_id.clone(),
                    };
                    tarefa.tentativas += 1;
                    tarefa.processada_em = Some(Utc::now());
                    return Some(tarefa.clone());
                }
            }
        }
    }

    pub async fn aguardar_tarefa(&self) {
        self.notificador.notified().await;
    }

    pub async fn concluir(&self, id: &str) {
        if let Some(mut tarefa) = self.tarefas.get_mut(id) {
            tarefa.status = StatusTarefa::Concluida;
            tracing::info!(
                tarefa_id = %id,
                tentativas = tarefa.tentativas,
                "Tarefa concluída"
            );
        }
    }

    pub async fn falhar(&self, id: &str, erro: &str) {
        if let Some(mut tarefa) = self.tarefas.get_mut(id) {
            if tarefa.tentativas < tarefa.max_tentativas {
                // Recoloca na fila com backoff exponencial
                let delay = std::time::Duration::from_secs(
                    2u64.pow(tarefa.tentativas)
                );
                tarefa.executar_apos = Some(
                    Utc::now() + chrono::Duration::from_std(delay).unwrap()
                );
                tarefa.status = StatusTarefa::Pendente;

                tracing::warn!(
                    tarefa_id = %id,
                    tentativa = tarefa.tentativas,
                    max = tarefa.max_tentativas,
                    erro = %erro,
                    delay_s = delay.as_secs(),
                    "Tarefa falhou — reagendada"
                );

                self.notificador.notify_one();
            } else {
                tarefa.status = StatusTarefa::Falhou {
                    tentativas: tarefa.tentativas,
                    ultimo_erro: erro.to_string(),
                };

                tracing::error!(
                    tarefa_id = %id,
                    tentativas = tarefa.tentativas,
                    erro = %erro,
                    "Tarefa falhou definitivamente"
                );
            }
        }
    }

    pub fn status(&self, id: &str) -> Option<StatusTarefa> {
        self.tarefas.get(id).map(|t| t.status.clone())
    }

    pub fn metricas(&self) -> MetricasFila {
        let mut pendentes = 0;
        let mut processando = 0;
        let mut concluidas = 0;
        let mut falhas = 0;

        for tarefa in self.tarefas.iter() {
            match &tarefa.status {
                StatusTarefa::Pendente => pendentes += 1,
                StatusTarefa::Processando { .. } => processando += 1,
                StatusTarefa::Concluida => concluidas += 1,
                StatusTarefa::Falhou { .. } => falhas += 1,
                StatusTarefa::Cancelada => {}
            }
        }

        MetricasFila { pendentes, processando, concluidas, falhas }
    }
}

#[derive(Debug, Serialize)]
pub struct MetricasFila {
    pub pendentes: u32,
    pub processando: u32,
    pub concluidas: u32,
    pub falhas: u32,
}
// src/worker.rs
use crate::fila::FilaTarefas;
use serde_json::Value;
use std::sync::Arc;
use std::time::Duration;

pub type HandlerFn = Box<
    dyn Fn(String, Value) -> std::pin::Pin<
        Box<dyn std::future::Future<Output = Result<(), String>> + Send>
    > + Send + Sync,
>;

pub struct Worker {
    id: String,
    fila: Arc<FilaTarefas>,
    handlers: Arc<std::collections::HashMap<String, HandlerFn>>,
}

impl Worker {
    pub fn novo(
        id: &str,
        fila: Arc<FilaTarefas>,
        handlers: Arc<std::collections::HashMap<String, HandlerFn>>,
    ) -> Self {
        Worker {
            id: id.to_string(),
            fila,
            handlers,
        }
    }

    pub async fn executar(self) {
        tracing::info!(worker_id = %self.id, "Worker iniciado");

        loop {
            match self.fila.obter_proxima().await {
                Some(tarefa) => {
                    let id = tarefa.id.clone();
                    let tipo = tarefa.tipo.clone();

                    tracing::info!(
                        worker_id = %self.id,
                        tarefa_id = %id,
                        tipo = %tipo,
                        tentativa = tarefa.tentativas,
                        "Processando tarefa"
                    );

                    let resultado = match self.handlers.get(&tipo) {
                        Some(handler) => {
                            handler(id.clone(), tarefa.payload.clone()).await
                        }
                        None => Err(format!(
                            "Handler não encontrado para tipo '{tipo}'"
                        )),
                    };

                    match resultado {
                        Ok(()) => self.fila.concluir(&id).await,
                        Err(e) => self.fila.falhar(&id, &e).await,
                    }
                }

                None => {
                    // Sem tarefas disponíveis — aguarda notificação
                    tokio::select! {
                        _ = self.fila.aguardar_tarefa() => {}
                        _ = tokio::time::sleep(Duration::from_secs(5)) => {}
                    }
                }
            }
        }
    }
}

// Pool de workers
pub struct PoolWorkers {
    fila: Arc<FilaTarefas>,
    handlers: Arc<std::collections::HashMap<String, HandlerFn>>,
    n_workers: usize,
}

impl PoolWorkers {
    pub fn novo(fila: Arc<FilaTarefas>, n_workers: usize) -> Self {
        PoolWorkers {
            fila,
            handlers: Arc::new(std::collections::HashMap::new()),
            n_workers,
        }
    }

    pub fn registrar_handler<F, Fut>(
        mut self,
        tipo: &str,
        handler: F,
    ) -> Self
    where
        F: Fn(String, Value) -> Fut + Send + Sync + 'static,
        Fut: std::future::Future<Output = Result<(), String>>
            + Send + 'static,
    {
        let handlers = Arc::get_mut(&mut self.handlers)
            .expect("Não pode registrar handlers após iniciar workers");

        handlers.insert(
            tipo.to_string(),
            Box::new(move |id, payload| Box::pin(handler(id, payload))),
        );
        self
    }

    pub fn iniciar(self) -> Vec<tokio::task::JoinHandle<()>> {
        let handlers = Arc::clone(&self.handlers);
        (0..self.n_workers)
            .map(|i| {
                let worker = Worker::novo(
                    &format!("worker-{i}"),
                    Arc::clone(&self.fila),
                    Arc::clone(&handlers),
                );
                tokio::spawn(worker.executar())
            })
            .collect()
    }
}
// src/main.rs (fila de tarefas)
mod fila;
mod worker;

use fila::{FilaTarefas, Prioridade, Tarefa};
use worker::PoolWorkers;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter("info")
        .init();

    let fila = FilaTarefas::nova();

    // Configura pool de workers com handlers
    let pool = PoolWorkers::novo(Arc::clone(&fila), 4)
        .registrar_handler("email", |id, payload| async move {
            tracing::info!(tarefa_id = %id, "Enviando email");
            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
            tracing::info!(tarefa_id = %id, para = %payload["para"], "Email enviado");
            Ok(())
        })
        .registrar_handler("relatorio", |id, payload| async move {
            tracing::info!(tarefa_id = %id, "Gerando relatório");
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;

            // Simula falha ocasional
            if rand::random::<f32>() < 0.3 {
                return Err("Falha simulada no relatório".to_string());
            }

            tracing::info!(tarefa_id = %id, "Relatório gerado");
            Ok(())
        })
        .registrar_handler("webhook", |id, payload| async move {
            tracing::info!(tarefa_id = %id, url = %payload["url"], "Enviando webhook");
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
            Ok(())
        });

    let _handles = pool.iniciar();

    // Enfileira algumas tarefas de demonstração
    for i in 0..20 {
        let prioridade = match i % 4 {
            0 => Prioridade::Alta,
            1 => Prioridade::Normal,
            2 => Prioridade::Baixa,
            _ => Prioridade::Critica,
        };

        let tipo = match i % 3 {
            0 => "email",
            1 => "relatorio",
            _ => "webhook",
        };

        let tarefa = Tarefa::nova(
            tipo,
            serde_json::json!({
                "para": format!("user{}@exemplo.com", i),
                "url": format!("https://hooks.exemplo.com/{i}"),
                "id_relatorio": i,
            }),
            prioridade,
        );

        fila.enfileirar(tarefa).await;
    }

    // Monitoramento
    tokio::spawn({
        let fila = Arc::clone(&fila);
        async move {
            let mut interval = tokio::time::interval(
                std::time::Duration::from_secs(2)
            );
            for _ in 0..10 {
                interval.tick().await;
                let m = fila.metricas();
                tracing::info!(
                    pendentes = m.pendentes,
                    processando = m.processando,
                    concluidas = m.concluidas,
                    falhas = m.falhas,
                    "Métricas da fila"
                );
            }
        }
    });

    // Aguarda processamento
    tokio::time::sleep(std::time::Duration::from_secs(15)).await;

    let m = fila.metricas();
    println!("
══ Resumo Final ══");
    println!("  Pendentes   : {}", m.pendentes);
    println!("  Processando : {}", m.processando);
    println!("  Concluídas  : {}", m.concluidas);
    println!("  Falhas      : {}", m.falhas);

    Ok(())
}

Projeto 3: Shell interativo com histórico e completions

[dependencies]
rustyline  = "13"
serde      = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow     = "1"
colored    = "2"
dirs       = "5"
// src/main.rs (shell)
use colored::*;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::HistoryHinter;
use rustyline::validate::Validator;
use rustyline::{CompletionType, Config, Editor, Helper};
use std::borrow::Cow;
use std::collections::HashMap;

// Estado do shell
struct EstadoShell {
    variaveis: HashMap<String, String>,
    historico_comandos: Vec<String>,
    diretorio_atual: std::path::PathBuf,
    ultimo_status: i32,
}

impl EstadoShell {
    fn novo() -> Self {
        EstadoShell {
            variaveis: HashMap::new(),
            historico_comandos: Vec::new(),
            diretorio_atual: std::env::current_dir()
                .unwrap_or_else(|_| std::path::PathBuf::from(".")),
            ultimo_status: 0,
        }
    }

    fn prompt(&self) -> String {
        let dir = self.diretorio_atual
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_else(|| "/".to_string());

        let status_cor = if self.ultimo_status == 0 {
            "❯".green().to_string()
        } else {
            format!("❯[{}]", self.ultimo_status).red().to_string()
        };

        format!("{} {} ", dir.blue().bold(), status_cor)
    }
}

// Builtins — comandos internos do shell
fn executar_builtin(
    cmd: &str,
    args: &[&str],
    estado: &mut EstadoShell,
) -> Option<i32> {
    match cmd {
        "cd" => {
            let destino = args.first()
                .map(|s| std::path::PathBuf::from(s))
                .unwrap_or_else(|| {
                    dirs::home_dir().unwrap_or_else(|| ".".into())
                });

            let destino = if destino.is_absolute() {
                destino
            } else {
                estado.diretorio_atual.join(destino)
            };

            match std::env::set_current_dir(&destino) {
                Ok(()) => {
                    estado.diretorio_atual = destino
                        .canonicalize()
                        .unwrap_or(destino);
                    Some(0)
                }
                Err(e) => {
                    eprintln!("{}: {}", "cd: erro".red(), e);
                    Some(1)
                }
            }
        }

        "export" => {
            for arg in args {
                let partes: Vec<&str> = arg.splitn(2, '=').collect();
                if partes.len() == 2 {
                    estado.variaveis.insert(
                        partes[0].to_string(),
                        partes[1].to_string(),
                    );
                    std::env::set_var(partes[0], partes[1]);
                }
            }
            Some(0)
        }

        "history" => {
            for (i, cmd) in estado.historico_comandos.iter().enumerate() {
                println!("{:>4}  {}", i + 1, cmd);
            }
            Some(0)
        }

        "echo" => {
            let saida = args.iter()
                .map(|a| expandir_variaveis(a, &estado.variaveis))
                .collect::<Vec<_>>()
                .join(" ");
            println!("{saida}");
            Some(0)
        }

        "pwd" => {
            println!("{}", estado.diretorio_atual.display());
            Some(0)
        }

        "exit" | "quit" => {
            let codigo = args.first()
                .and_then(|s| s.parse::<i32>().ok())
                .unwrap_or(0);
            std::process::exit(codigo);
        }

        "help" => {
            println!("{}", "Comandos disponíveis:".bold());
            println!("  {}      — muda diretório", "cd".cyan());
            println!("  {}  — define variável de ambiente", "export".cyan());
            println!("  {} — exibe histórico", "history".cyan());
            println!("  {}    — exibe texto", "echo".cyan());
            println!("  {}     — exibe diretório atual", "pwd".cyan());
            println!("  {}    — sai do shell", "exit".cyan());
            Some(0)
        }

        _ => None, // não é builtin
    }
}

fn expandir_variaveis(s: &str, variaveis: &HashMap<String, String>) -> String {
    let mut resultado = s.to_string();
    for (chave, valor) in variaveis {
        resultado = resultado.replace(&format!("${chave}"), valor);
        resultado = resultado.replace(&format!("${{{chave}}}"), valor);
    }
    resultado
}

fn executar_comando(
    linha: &str,
    estado: &mut EstadoShell,
) -> i32 {
    let linha = linha.trim();
    if linha.is_empty() || linha.starts_with('#') {
        return 0;
    }

    // Expansão de variáveis
    let expandida = expandir_variaveis(linha, &estado.variaveis);

    // Tokenização simples (sem suporte a strings com espaços por simplicidade)
    let tokens: Vec<&str> = expandida.split_whitespace().collect();
    if tokens.is_empty() { return 0; }

    let cmd = tokens[0];
    let args = &tokens[1..];

    // Tenta builtin primeiro
    if let Some(status) = executar_builtin(cmd, args, estado) {
        return status;
    }

    // Executa comando externo
    match std::process::Command::new(cmd)
        .args(args)
        .current_dir(&estado.diretorio_atual)
        .status()
    {
        Ok(status) => status.code().unwrap_or(1),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            eprintln!("{}: comando não encontrado: {}", "erro".red(), cmd);
            127
        }
        Err(e) => {
            eprintln!("{}: {}", "erro".red(), e);
            1
        }
    }
}

// Helper para rustyline (autocomplete, hints)
struct ShellHelper {
    completer: FilenameCompleter,
    hinter: HistoryHinter,
}

impl Helper for ShellHelper {}
impl Validator for ShellHelper {}

impl Highlighter for ShellHelper {
    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
        Cow::Owned(hint.dimmed().to_string())
    }

    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
        // Destaca o primeiro token (comando) em ciano
        let tokens: Vec<&str> = line.splitn(2, ' ').collect();
        if tokens.is_empty() {
            return Cow::Borrowed(line);
        }

        let cmd_destacado = tokens[0].cyan().to_string();
        if tokens.len() == 1 {
            Cow::Owned(cmd_destacado)
        } else {
            Cow::Owned(format!("{} {}", cmd_destacado, tokens[1]))
        }
    }

    fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
        true
    }
}

impl Completer for ShellHelper {
    type Candidate = Pair;

    fn complete(
        &self,
        line: &str,
        pos: usize,
        ctx: &rustyline::Context<'_>,
    ) -> rustyline::Result<(usize, Vec<Pair>)> {
        // Completa arquivos após o primeiro token
        if line.contains(' ') {
            self.completer.complete(line, pos, ctx)
        } else {
            // Completa comandos no PATH
            let mut candidatos = Vec::new();
            let prefixo = &line[..pos];

            // Builtins
            for builtin in &["cd", "export", "history", "echo", "pwd", "exit", "help"] {
                if builtin.starts_with(prefixo) {
                    candidatos.push(Pair {
                        display: builtin.to_string(),
                        replacement: builtin.to_string(),
                    });
                }
            }

            Ok((0, candidatos))
        }
    }
}

impl rustyline::hint::Hinter for ShellHelper {
    type Hint = String;

    fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>)
        -> Option<String>
    {
        self.hinter.hint(line, pos, ctx)
    }
}

fn main() -> anyhow::Result<()> {
    let mut estado = EstadoShell::novo();

    let config = Config::builder()
        .history_ignore_space(true)
        .completion_type(CompletionType::List)
        .build();

    let helper = ShellHelper {
        completer: FilenameCompleter::new(),
        hinter: HistoryHinter::new(),
    };

    let mut rl = Editor::with_config(config)?;
    rl.set_helper(Some(helper));

    // Carrega histórico
    let historico_path = dirs::home_dir()
        .map(|h| h.join(".rsh_history"));

    if let Some(ref path) = historico_path {
        let _ = rl.load_history(path);
    }

    println!("{}", "rsh — Rust Shell".bold().green());
    println!("Digite {} para ajuda
", "help".cyan());

    loop {
        let prompt = estado.prompt();

        match rl.readline(&prompt) {
            Ok(linha) => {
                let linha = linha.trim().to_string();
                if !linha.is_empty() {
                    rl.add_history_entry(&linha)?;
                    estado.historico_comandos.push(linha.clone());
                }

                estado.ultimo_status = executar_comando(&linha, &mut estado);
            }

            Err(ReadlineError::Interrupted) => {
                println!("^C");
                estado.ultimo_status = 130;
            }

            Err(ReadlineError::Eof) => {
                println!("exit");
                break;
            }

            Err(e) => {
                eprintln!("Erro: {e}");
                break;
            }
        }
    }

    // Salva histórico
    if let Some(ref path) = historico_path {
        let _ = rl.save_history(path);
    }

    Ok(())
}

Integrando tudo: o que cada projeto demonstra

// Motor de busca (Projeto 1):
// ✓ Artigo #05 — Structs e Enums (Documento, EntradaIndice)
// ✓ Artigo #10 — Traits (IndexableStore)
// ✓ Artigo #13 — Iteradores (tokenizar, processar resultados)
// ✓ Artigo #19 — Concorrência (DashMap, AtomicU64)
// ✓ Artigo #20 — Async/await (endpoints Axum)
// ✓ Artigo #22 — Web APIs (Router, handlers)
// ✓ Artigo #33 — Performance (rayon para indexação paralela)
// ✓ Artigo #38 — Documentação (doc comments)
// ✓ Artigo #48 — Observabilidade (tracing)

// Fila de tarefas (Projeto 2):
// ✓ Artigo #07 — Error handling (Result, thiserror)
// ✓ Artigo #09 — Lifetimes (referências em handlers)
// ✓ Artigo #19 — Concorrência (Mutex, Notify, spawn)
// ✓ Artigo #20 — Async (workers assíncronos)
// ✓ Artigo #36 — Design patterns (Builder para PoolWorkers)
// ✓ Artigo #43 — Sistemas distribuídos (retry, backoff)
// ✓ Artigo #48 — Produção (logs estruturados, métricas)

// Shell (Projeto 3):
// ✓ Artigo #02 — Tipos básicos (parsing de tokens)
// ✓ Artigo #05 — Enums (estado do shell)
// ✓ Artigo #06 — Pattern matching (dispatch de comandos)
// ✓ Artigo #11 — Closures (handlers de completions)
// ✓ Artigo #15 — std::process, std::env
// ✓ Artigo #40 — CLI (readline, completions)

Fontes e leituras recomendadas

  • "Crafting Interpreters" — Robert Nystrom — para projetos de linguagem — https://craftinginterpreters.com
  • axum exemplos — https://github.com/tokio-rs/axum/tree/main/examples
  • rustyline exemplos — https://github.com/kkawakam/rustyline/tree/master/examples
  • dashmap documentation — mapa concorrente — https://docs.rs/dashmap
  • "Systems Performance" — Brendan Gregg — performance em sistemas reais
  • "Designing Data-Intensive Applications" — Martin Kleppmann — arquitetura de sistemas
  • tokio tutorial — https://tokio.rs/tokio/tutorial
  • Repositório awesome-rust — projetos de referência — https://github.com/rust-unofficial/awesome-rust

Artigo #51 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #52: Epílogo — O que vem depois, como continuar crescendo, e a filosofia do Rust


Comentários

Mais em Rust

Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores
Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores

Se voc&ecirc; vem de Python, JavaScript ou Java, j&aacute; sabe o que &eacute...

Autenticação JWT — Protegendo sua API com Tokens
Autenticação JWT — Protegendo sua API com Tokens

Nos dois artigos anteriores construímos uma API REST com persistência real. M...

O Futuro do Rust — Async Traits, GATs, Especialização e o Roadmap da Linguagem
O Futuro do Rust — Async Traits, GATs, Especialização e o Roadmap da Linguagem

Rust — Artigo #50 O Futuro do Rust — Async Traits, GATs, Especialização e o R...