Skip to content
Open
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
3 changes: 2 additions & 1 deletion docs/.agent/docs_coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
| HTTP/3 (QUIC) | `docs/cookbook/src/recipes/http3_quic.md` | `rustapi-core/src/http3.rs` (`Http3Server`) | OK |
| File Uploads | `docs/cookbook/src/recipes/file_uploads.md` | `rustapi-core/src/multipart.rs` (`Multipart`) | OK |
| Compression | `docs/cookbook/src/recipes/compression.md` | `rustapi-core/src/middleware/compression.rs` (`CompressionLayer`) | OK |
| Graceful Shutdown | `docs/cookbook/src/recipes/graceful_shutdown.md` | `rustapi-core/src/server.rs` (`run_with_shutdown`) | OK |
| **OpenAPI** | | | |
| Schema Derivation | `docs/cookbook/src/crates/rustapi_openapi.md` | `rustapi-macros/src/derive_schema.rs` (`#[derive(Schema)]`) | OK |
| References ($ref) | `docs/cookbook/src/recipes/openapi_refs.md` | `rustapi-openapi/src/schema.rs` (`SchemaRef`) | OK |
Expand All @@ -27,7 +28,7 @@
| Middleware (Advanced) | `recipes/advanced_middleware.md` | `rustapi-extras/src/{rate_limit, dedup, cache}` | OK |
| **Jobs** | | | |
| Job Queue (Crate) | `crates/rustapi_jobs.md` | `rustapi-jobs` | OK |
| Background Jobs (Recipe) | `recipes/background_jobs.md` | `rustapi-jobs` | OK |
| Background Jobs (Recipe) | `recipes/background_jobs.md` | `rustapi-jobs` | Updated |
| **Integrations** | | | |
| gRPC | `recipes/grpc_integration.md` | `rustapi-grpc` | OK |
| SSR | `recipes/server_side_rendering.md` | `rustapi-view` | OK |
Expand Down
4 changes: 2 additions & 2 deletions docs/.agent/last_run.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"last_processed_ref": "v0.1.335",
"date": "2026-02-19",
"notes": "Deleted incorrect recipes (tuning, action pattern). Updated File Uploads and WebSockets recipes for accuracy. Expanded Learning Path with mini projects."
"date": "2026-02-20",
"notes": "Added Graceful Shutdown recipe and mini-project. Corrected Background Jobs recipe."
}
22 changes: 22 additions & 0 deletions docs/.agent/run_report_2026-02-20.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Run Report: 2026-02-20

**Status**: Success
**Version Target**: v0.1.335 (No change)
**Focus**: Continuous Improvement (Cookbook & Learning Path)

## Changes

### Documentation Updates
- **Updated**: `docs/cookbook/src/recipes/background_jobs.md` - Corrected API usage (`.run()` instead of `.serve()`), macro syntax, and explicit generics for `enqueue`.
- **Created**: `docs/cookbook/src/recipes/graceful_shutdown.md` - New recipe covering graceful shutdown with `run_with_shutdown` and background task coordination.
- **Updated**: `docs/cookbook/src/learning/curriculum.md` - Added "The Graceful Exit" mini-project to Module 10.
- **Updated**: `docs/cookbook/src/SUMMARY.md` - Added "Graceful Shutdown" to the table of contents.
- **Updated**: `docs/.agent/docs_coverage.md` - Tracked new recipe.

## Improvements
- Verified `background_jobs.md` against codebase patterns.
- Expanded "Production Readiness" module in the learning path.

## Next Steps
- Continue expanding Phase 4 (Enterprise Scale) mini-projects.
- Consider adding a recipe for "Custom Error Handling" as previously identified.
1 change: 1 addition & 0 deletions docs/cookbook/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
- [Production Tuning](recipes/high_performance.md)
- [Response Compression](recipes/compression.md)
- [Resilience Patterns](recipes/resilience.md)
- [Graceful Shutdown](recipes/graceful_shutdown.md)
- [Audit Logging](recipes/audit_logging.md)
- [Time-Travel Debugging (Replay)](recipes/replay.md)
- [Deployment](recipes/deployment.md)
Expand Down
3 changes: 3 additions & 0 deletions docs/cookbook/src/learning/curriculum.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ Create a `POST /register` endpoint that accepts a JSON body `{"username": "...",
- **Expected Output:** A resilient API ready for deployment.
- **Pitfalls:** Setting timeouts too low for slow operations.

#### 🛠️ Mini Project: "The Graceful Exit"
Implement `run_with_shutdown` handling `Ctrl+C`. Verify that long-running requests complete successfully when the server receives the signal.

#### 🧠 Knowledge Check
1. Why is timeout middleware important?
2. What command generates a production Dockerfile?
Expand Down
25 changes: 13 additions & 12 deletions docs/cookbook/src/recipes/background_jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ async fn main() -> std::io::Result<()> {

// 5. Build application
RustApi::auto()
.with_state(queue) // Inject queue into state
.serve("127.0.0.1:3000")
.state(queue) // Inject queue into state
.run("127.0.0.1:3000")
.await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}
```

Expand All @@ -108,11 +109,17 @@ You can now inject the `JobQueue` into your request handlers using the `State` e
use rustapi_rs::prelude::*;
use rustapi_jobs::JobQueue;

#[rustapi::post("/register")]
#[derive(Deserialize)]
struct RegisterRequest {
username: String,
email: String,
}

#[rustapi_rs::post("/register")]
async fn register_user(
State(queue): State<JobQueue>,
Json(payload): Json<RegisterRequest>,
) -> Result<impl IntoResponse, ApiError> {
) -> Result<Json<serde_json::Value>> {
// ... logic to create user in DB ...
let user_id = "user_123".to_string(); // Simulated ID

Expand All @@ -121,19 +128,13 @@ async fn register_user(
queue.enqueue::<WelcomeEmailJob>(WelcomeEmailData {
user_id,
email: payload.email,
}).await.map_err(|e| ApiError::InternalServerError(e.to_string()))?;
}).await.map_err(|e| ApiError::internal(e.to_string()))?;

Ok(Json(json!({
Ok(Json(serde_json::json!({
"status": "registered",
"message": "Welcome email will be sent shortly"
})))
}

#[derive(Deserialize)]
struct RegisterRequest {
username: String,
email: String,
}
```

## Resilience and Retries
Expand Down
116 changes: 116 additions & 0 deletions docs/cookbook/src/recipes/graceful_shutdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Graceful Shutdown

Graceful shutdown is essential for production applications to ensure that in-flight requests are completed before the server stops. This prevents data loss and provides a smooth experience for users during deployments or restarts.

## How it Works

When a shutdown signal (like `SIGTERM` or `Ctrl+C`) is received:
1. The server stops accepting new connections.
2. Existing connections are allowed to finish their current request.
3. Background tasks are notified (if configured).
4. The application exits once all connections are closed or a timeout is reached.

## Implementation

RustAPI supports graceful shutdown via the `.run_with_shutdown()` method, which accepts a `Future` that resolves when shutdown should occur.

### Basic Example

Here is how to implement graceful shutdown listening for `Ctrl+C`:

```rust
use rustapi_rs::prelude::*;
use tokio::signal;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let app = RustApi::auto();

println!("Server running on http://127.0.0.1:3000");

app.run_with_shutdown("127.0.0.1:3000", shutdown_signal())
.await?;

Ok(())
}

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 => {},
}

println!("Shutdown signal received, starting graceful shutdown...");
}
```

## Shutdown with Background Tasks

If you have background tasks (like `rustapi-jobs` workers), you should coordinate their shutdown as well. You can use `tokio_util::sync::CancellationToken` or a broadcast channel.

```rust
use rustapi_rs::prelude::*;
use tokio::signal;
use tokio_util::sync::CancellationToken;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let token = CancellationToken::new();

// Start a background task
let child_token = token.child_token();
tokio::spawn(async move {
loop {
tokio::select! {
_ = child_token.cancelled() => {
println!("Background task shutting down");
return;
}
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
println!("Working...");
}
}
}
});

// Run server
RustApi::auto()
.run_with_shutdown("0.0.0.0:3000", async {
shutdown_signal().await;
token.cancel(); // Notify background tasks
})
.await?;

println!("Server exited successfully");
Ok(())
}
```

## Platform Specifics

### Docker

When stopping a container, Docker sends `SIGTERM`. If your app doesn't exit within a timeout (default 10s), it sends `SIGKILL`. Graceful shutdown ensures your app handles `SIGTERM` correctly.

### Kubernetes

Kubernetes also sends `SIGTERM` before terminating a pod. It waits for the `terminationGracePeriodSeconds` (default 30s) before force-killing.

Ensure your shutdown logic completes within these timeframes.
Loading