Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ complementarios como `rust-devops`, `rust-api-design` y
| 04 | Redes y VPC | `src/networking.rs` | implemented |
| 05 | Identidad y accesos | `src/iam.rs` | implemented |
| 06 | Servicios manejados | `src/managed_services.rs` | implemented |
| 07 | Serverless | `src/serverless.rs` | draft |
| 07 | Serverless | `src/serverless.rs` | implemented |
| 08 | Costos y FinOps | `src/finops.rs` | planned |
| 09 | AWS en la práctica | `src/aws_practice.rs` | planned |
| 10 | GCP en la práctica | `src/gcp_practice.rs` | planned |
Expand Down
8 changes: 4 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ esperada es:
| 04 | Redes y VPC | implemented |
| 05 | Identidad y accesos | implemented |
| 06 | Servicios manejados | implemented |
| 07 | Serverless | draft |
| 07 | Serverless | implemented |
| 08 | Costos y FinOps | planned |
| 09 | AWS en la práctica | planned |
| 10 | GCP en la práctica | planned |
Expand All @@ -74,6 +74,6 @@ esperada es:

## Siguiente paso natural

Completar el milestone `07. Serverless` con el flujo restante: modelo Rust
mínimo, capítulo narrativo, diagrama Mermaid, ejemplos progresivos, ejercicios,
soluciones y análisis de costos.
Completar el milestone `07. Serverless` con el flujo restante: capítulo
narrativo, diagrama Mermaid, ejemplos progresivos, ejercicios, soluciones y
análisis de costos.
2 changes: 1 addition & 1 deletion course.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"number": 7,
"title": "Serverless",
"slug": "serverless",
"status": "draft",
"status": "implemented",
"milestone": "07. Serverless",
"document": "docs/07-serverless.md",
"module": "src/serverless.rs",
Expand Down
33 changes: 21 additions & 12 deletions docs/07-serverless.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

- **Curso:** rust-cloud
- **Semestre:** 5
- **Estado:** draft
- **Estado:** implemented
- **Milestone:** 07. Serverless
- **Issue:** #25
- **Issues:** #25, #26
- **Módulo Rust:** `src/serverless.rs`

## Concepto
Expand Down Expand Up @@ -74,9 +74,10 @@ RFC-0001 §10: proveedor después de fundamentos.
- Los nombres, precios y límites de proveedor son material vivo y deben
revisarse cuando se usen ejemplos fechados.

## Requisitos para `src/serverless.rs`
## Modelo Rust mínimo

El módulo Rust mínimo deberá modelar, sin dependencias externas:
El módulo Rust mínimo vive en `src/serverless.rs` y modela, sin dependencias
externas:

- tipos de disparador: HTTP, cola, storage, scheduler y evento interno;
- unidad serverless con nombre, propósito y límites;
Expand All @@ -92,14 +93,22 @@ El módulo no debe intentar simular una plataforma serverless real. Su función
pedagógica: hacer visible el contrato de ejecución antes de hablar de AWS Lambda,
Cloud Functions, Cloud Run, workflows u otros productos.

## Decisiones pendientes

- Definir si una carga serverless se modela como función, workflow o workload
genérico.
- Nombrar errores y hallazgos públicos antes de escribir ejemplos.
- Definir umbrales educativos de timeout y concurrencia.
- Decidir cómo representar idempotencia sin sobreprometer seguridad real.
## Decisiones registradas

- La unidad serverless se modela como `ServerlessWorkload` para cubrir función,
contenedor y workflow sin atarse a un proveedor.
- Los disparadores viven en `TriggerKind`; el runtime educativo en
`RuntimeProfile`.
- Los límites viven en `timeout_seconds` y `max_concurrency`.
- Retries e idempotencia se modelan por separado con `RetryPolicy` e
`IdempotencyStrategy`.
- `StateAccess` distingue funciones sin estado de lecturas o escrituras en
estado externo.
- `ServerlessFinding` vuelve visibles retries sin idempotencia, concurrencia sin
límite, timeout alto, escritura de estado sin idempotencia y observabilidad
incompleta.

## Estado editorial

Este capítulo queda en `draft`. No está marcado como `reviewed` ni `published`.
Este capítulo queda en `implemented`. No está marcado como `reviewed` ni
`published`.
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod compute;
pub mod iam;
pub mod managed_services;
pub mod networking;
pub mod serverless;
pub mod service_models;
pub mod storage;

Expand Down Expand Up @@ -86,7 +87,7 @@ const PLANNED_CHAPTERS: [Chapter; 10] = [
Chapter {
number: 7,
title: "Serverless",
status: ChapterStatus::Draft,
status: ChapterStatus::Implemented,
},
Chapter {
number: 8,
Expand Down Expand Up @@ -133,6 +134,6 @@ mod tests {
assert_eq!(chapters[3].status, ChapterStatus::Implemented);
assert_eq!(chapters[4].status, ChapterStatus::Implemented);
assert_eq!(chapters[5].status, ChapterStatus::Implemented);
assert_eq!(chapters[6].status, ChapterStatus::Draft);
assert_eq!(chapters[6].status, ChapterStatus::Implemented);
}
}
270 changes: 270 additions & 0 deletions src/serverless.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
//! Serverless como contrato explícito de ejecución por eventos.
//!
//! Este módulo no simula una plataforma serverless real. Hace visibles
//! decisiones educativas: disparador, propósito, límite temporal, concurrencia,
//! retries, idempotencia, estado externo y observabilidad.

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriggerKind {
/// Petición HTTP o API gateway.
Http,
/// Mensaje en cola o stream.
Queue,
/// Cambio en almacenamiento de objetos.
Storage,
/// Ejecución programada.
Schedule,
/// Evento interno del dominio.
DomainEvent,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeProfile {
/// Handler pequeño de función.
Function,
/// Contenedor serverless.
Container,
/// Workflow orquestado.
Workflow,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryPolicy {
/// Sin retry automático.
None,
/// Retry acotado por intentos.
Fixed { attempts: u8 },
/// Retry con backoff.
Backoff { attempts: u8 },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdempotencyStrategy {
/// No se declaró estrategia.
None,
/// Clave idempotente por evento.
EventKey,
/// Escritura condicional en estado externo.
ConditionalWrite,
/// Compensación explícita si se duplica trabajo.
Compensation,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StateAccess {
/// No toca estado durable.
Stateless,
/// Lee estado externo.
ExternalRead,
/// Escribe estado externo.
ExternalWrite,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObservabilityPlan {
/// Registra inicio, salida y errores.
pub logs: bool,
/// Emite métricas de latencia, error o throughput.
pub metrics: bool,
/// Permite seguir un evento entre componentes.
pub correlation_id: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ServerlessRequirements {
/// Disparador de la ejecución.
pub trigger: TriggerKind,
/// Runtime educativo.
pub runtime: RuntimeProfile,
/// Timeout máximo en segundos.
pub timeout_seconds: Option<u16>,
/// Concurrencia máxima declarada.
pub max_concurrency: Option<u16>,
/// Política de retry.
pub retry_policy: RetryPolicy,
/// Estrategia de idempotencia.
pub idempotency: IdempotencyStrategy,
/// Estado durable tocado.
pub state_access: StateAccess,
/// Observabilidad mínima.
pub observability: ObservabilityPlan,
/// Propósito humano.
pub purpose: &'static str,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ServerlessWorkload {
name: &'static str,
requirements: ServerlessRequirements,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerlessDecisionError {
/// Falta nombre.
MissingName,
/// Falta propósito humano.
MissingPurpose,
/// Falta timeout.
MissingTimeout,
/// Timeout inválido.
InvalidTimeout(&'static str),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerlessFinding {
/// Retry sin idempotencia.
RetryWithoutIdempotency(&'static str),
/// Concurrencia sin límite explícito.
UnboundedConcurrency(&'static str),
/// Timeout alto para una función pequeña.
HighFunctionTimeout(&'static str),
/// Escritura de estado sin idempotencia.
StatefulWriteWithoutIdempotency(&'static str),
/// Observabilidad insuficiente.
MissingObservability(&'static str),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServerlessEvaluation {
findings: Vec<ServerlessFinding>,
}

impl ObservabilityPlan {
/// Observabilidad mínima recomendada para este modelo.
pub const fn standard() -> Self {
Self {
logs: true,
metrics: true,
correlation_id: true,
}
}

/// Sin observabilidad explícita.
pub const fn none() -> Self {
Self {
logs: false,
metrics: false,
correlation_id: false,
}
}

const fn is_complete(self) -> bool {
self.logs && self.metrics && self.correlation_id
}
}

impl ServerlessWorkload {
/// Crea una unidad serverless educativa.
pub fn new(
name: &'static str,
requirements: ServerlessRequirements,
) -> Result<Self, ServerlessDecisionError> {
if name.is_empty() {
return Err(ServerlessDecisionError::MissingName);
}

if requirements.purpose.is_empty() {
return Err(ServerlessDecisionError::MissingPurpose);
}

let timeout = requirements
.timeout_seconds
.ok_or(ServerlessDecisionError::MissingTimeout)?;
if timeout == 0 {
return Err(ServerlessDecisionError::InvalidTimeout(
"timeout_seconds debe ser mayor que cero",
));
}

Ok(Self { name, requirements })
}

/// Evalúa señales de riesgo.
pub fn evaluate(&self) -> ServerlessEvaluation {
let mut findings = Vec::new();

if retry_attempts(self.requirements.retry_policy) > 0
&& self.requirements.idempotency == IdempotencyStrategy::None
{
findings.push(ServerlessFinding::RetryWithoutIdempotency(self.name));
}

if self.requirements.max_concurrency.is_none() {
findings.push(ServerlessFinding::UnboundedConcurrency(self.name));
}

if self.requirements.runtime == RuntimeProfile::Function
&& self.requirements.timeout_seconds.unwrap_or_default() > 60
{
findings.push(ServerlessFinding::HighFunctionTimeout(self.name));
}

if self.requirements.state_access == StateAccess::ExternalWrite
&& self.requirements.idempotency == IdempotencyStrategy::None
{
findings.push(ServerlessFinding::StatefulWriteWithoutIdempotency(
self.name,
));
}

if !self.requirements.observability.is_complete() {
findings.push(ServerlessFinding::MissingObservability(self.name));
}

ServerlessEvaluation { findings }
}

/// Nombre del workload.
pub const fn name(&self) -> &'static str {
self.name
}

/// Requisitos declarados.
pub const fn requirements(&self) -> ServerlessRequirements {
self.requirements
}
}

impl ServerlessEvaluation {
/// Indica si no hay hallazgos educativos.
pub fn is_low_risk(&self) -> bool {
self.findings.is_empty()
}

/// Hallazgos detectados.
pub fn findings(&self) -> &[ServerlessFinding] {
&self.findings
}
}

const fn retry_attempts(policy: RetryPolicy) -> u8 {
match policy {
RetryPolicy::None => 0,
RetryPolicy::Fixed { attempts } | RetryPolicy::Backoff { attempts } => attempts,
}
}

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

#[test]
fn workload_requires_timeout() {
let requirements = ServerlessRequirements {
trigger: TriggerKind::Http,
runtime: RuntimeProfile::Function,
timeout_seconds: None,
max_concurrency: Some(10),
retry_policy: RetryPolicy::None,
idempotency: IdempotencyStrategy::None,
state_access: StateAccess::Stateless,
observability: ObservabilityPlan::standard(),
purpose: "responder healthcheck",
};

assert_eq!(
ServerlessWorkload::new("healthcheck", requirements),
Err(ServerlessDecisionError::MissingTimeout)
);
}
}
Loading
Loading