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(¶ms.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
axumexemplos — https://github.com/tokio-rs/axum/tree/main/examplesrustylineexemplos — https://github.com/kkawakam/rustyline/tree/master/examplesdashmapdocumentation — mapa concorrente — https://docs.rs/dashmap- "Systems Performance" — Brendan Gregg — performance em sistemas reais
- "Designing Data-Intensive Applications" — Martin Kleppmann — arquitetura de sistemas
tokiotutorial — 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