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
3 changes: 2 additions & 1 deletion docs/.agent/docs_coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
| Security | `recipes/csrf_protection.md` | `rustapi-extras/src/security` | OK |
| Observability | `crates/rustapi_extras.md` | `rustapi-extras/src/telemetry` | OK |
| **Jobs** | | | |
| Job Queue | `crates/rustapi_jobs.md` | `rustapi-jobs` | OK |
| Job Queue (Crate) | `crates/rustapi_jobs.md` | `rustapi-jobs` | OK |
| Background Jobs (Recipe) | `recipes/background_jobs.md` | `rustapi-jobs` | NEW |
| **Learning** | | | |
| Structured Path | `learning/curriculum.md` | N/A | OK |
| **Recipes** | | | |
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": "2025-02-21",
"notes": "Enhanced learning path with quizzes and capstones. Fixed file upload recipe. Added deployment docs tracking."
"date": "2025-02-24",
"notes": "Added background jobs recipe and expanded learning path with Module 10."
}
33 changes: 33 additions & 0 deletions docs/.agent/run_report_2025-02-24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Docs Maintenance Run Report: 2025-02-24

## 1. Version Detection
- **Repo Version**: `v0.1.335`
- **Previous Processed Version**: `v0.1.335`
- **Result**: No version change detected. Proceeding with Continuous Improvement phase.

## 2. Changes Summary
This run focuses on expanding the cookbook and refining the learning path to include background job processing and testing.

### New Content
- **Cookbook Recipe**: `docs/cookbook/src/recipes/background_jobs.md` - Comprehensive guide to `rustapi-jobs`.
- **Learning Path Module**: Added "Module 10: Background Jobs & Testing" to `docs/cookbook/src/learning/curriculum.md`.

### Updates
- Updated `docs/cookbook/src/SUMMARY.md` to include the new recipe.
- Updated `docs/cookbook/src/learning/curriculum.md` to enhance the Phase 3 Capstone project.

## 3. Improvement Details
- **Background Jobs**: Added a detailed recipe covering:
- Defining `Job` structs and handlers.
- Setting up `JobQueue` with `InMemoryBackend`.
- Enqueueing jobs from API handlers.
- Running the job worker.
- Verification of job execution.

- **Learning Path**:
- Added explicit module for `rustapi-jobs` usage.
- Reinforced testing practices in the curriculum.

## 4. Open Questions / TODOs
- Investigate adding `rustapi-jobs` as a re-export in `rustapi-rs` for better "batteries-included" experience in future versions.
- Consider adding more backend examples (Redis, Postgres) to the cookbook recipe when environment setup allows.
1 change: 1 addition & 0 deletions docs/cookbook/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
- [CSRF Protection](recipes/csrf_protection.md)
- [Database Integration](recipes/db_integration.md)
- [File Uploads](recipes/file_uploads.md)
- [Background Jobs](recipes/background_jobs.md)
- [Custom Middleware](recipes/custom_middleware.md)
- [Real-time Chat](recipes/websockets.md)
- [Production Tuning](recipes/high_performance.md)
Expand Down
15 changes: 15 additions & 0 deletions docs/cookbook/src/learning/curriculum.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,26 @@ This curriculum is designed to take you from a RustAPI beginner to an advanced u
2. What command generates a production Dockerfile?
3. How do you enable compression for responses?

### Module 10: Background Jobs & Testing
- **Prerequisites:** Phase 3.
- **Reading:** [Background Jobs Recipe](../recipes/background_jobs.md), [Testing Strategy](../concepts/testing.md).
- **Task:**
1. Implement a job that sends a "Welcome" email (simulated) when a user registers.
2. Write an integration test using `TestClient` to verify the registration endpoint.
- **Expected Output:** Registration returns 200 immediately; console logs show "Sending welcome email to ..." shortly after. Tests pass.
- **Pitfalls:** Forgetting to start the job worker loop.

#### 🧠 Knowledge Check
1. Why use background jobs for email sending?
2. Which backend is suitable for local development?
3. How do you enqueue a job from a handler?

### 🏆 Phase 3 Capstone: "The Real-Time Collaboration Tool"
**Objective:** Build a real-time collaborative note-taking app.
**Requirements:**
- **Auth:** Users must log in to edit notes.
- **Real-time:** Changes to a note are broadcast to all viewers via WebSockets.
- **Jobs:** When a note is deleted, schedule a background job to archive it (simulate archive).
- **Resilience:** Rate limit API requests to prevent abuse.
- **Deployment:** specify a `Dockerfile` for the application.

Expand Down
170 changes: 170 additions & 0 deletions docs/cookbook/src/recipes/background_jobs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Background Jobs

RustAPI provides a robust background job processing system through the `rustapi-jobs` crate. This allows you to offload time-consuming tasks (like sending emails, processing images, or generating reports) from the main request/response cycle, keeping your API fast and responsive.

## Setup

First, add `rustapi-jobs` to your `Cargo.toml`. Since `rustapi-jobs` is not re-exported by the main crate by default, you must include it explicitly.

```toml
[dependencies]
rustapi-rs = "0.1"
rustapi-jobs = "0.1"
serde = { version = "1.0", features = ["derive"] }
async-trait = "0.1"
tokio = { version = "1.0", features = ["full"] }
```

## Defining a Job

A job consists of a data structure (the payload) and an implementation of the `Job` trait.

```rust,no_run
use rustapi_jobs::{Job, JobContext, Result};
use serde::{Deserialize, Serialize};
use async_trait::async_trait;


// 1. Define the job payload
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WelcomeEmailData {
pub user_id: String,
pub email: String,
}

// 2. Define the job handler struct
#[derive(Clone)]
pub struct WelcomeEmailJob;

// 3. Implement the Job trait
#[async_trait]
impl Job for WelcomeEmailJob {
// Unique name for the job type
const NAME: &'static str = "send_welcome_email";

// The payload type
type Data = WelcomeEmailData;

async fn execute(&self, ctx: JobContext, data: Self::Data) -> Result<()> {
println!("Processing job {} (attempt {})", ctx.job_id, ctx.attempt);
println!("Sending welcome email to {} ({})", data.email, data.user_id);

// Simulate work
tokio::time::sleep(std::time::Duration::from_millis(100)).await;

Ok(())
}
}
```

## Registering and Running the Queue

In your main application setup, you need to:
1. Initialize the backend (Memory, Redis, or Postgres).
2. Create the `JobQueue`.
3. Register your job handlers.
4. Start the worker loop in a background task.
5. Add the `JobQueue` to your application state so handlers can use it.

```rust,no_run
use rustapi_rs::prelude::*;
use rustapi_jobs::{JobQueue, InMemoryBackend};
// use crate::jobs::{WelcomeEmailJob, WelcomeEmailData}; // Import your job

#[tokio::main]
async fn main() -> std::io::Result<()> {
// 1. Initialize backend
// For production, use Redis or Postgres backend
let backend = InMemoryBackend::new();

// 2. Create queue
let queue = JobQueue::new(backend);

// 3. Register jobs
// You must register an instance of the job handler
queue.register_job(WelcomeEmailJob).await;

// 4. Start worker in background
let queue_for_worker = queue.clone();
tokio::spawn(async move {
if let Err(e) = queue_for_worker.start_worker().await {
eprintln!("Worker failed: {}", e);
}
});

// 5. Build application
RustApi::auto()
.with_state(queue) // Inject queue into state
.serve("127.0.0.1:3000")
.await
}
Comment on lines +75 to +100
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The application wiring example uses builder methods that don't exist on RustApi in this repo (.with_state(...) / .serve(...)), and the main return type (std::io::Result<()>) won’t match RustApi::run(...)’s error type. Update the snippet to use .state(queue) and .run("127.0.0.1:3000") (or return Result<(), Box<dyn Error + Send + Sync>>).

Copilot uses AI. Check for mistakes.
```

## Enqueueing Jobs

You can now inject the `JobQueue` into your request handlers using the `State` extractor and enqueue jobs.

```rust,no_run
use rustapi_rs::prelude::*;
use rustapi_jobs::JobQueue;

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

// Enqueue the background job
// The queue will handle serialization and persistence
queue.enqueue::<WelcomeEmailJob>(WelcomeEmailData {
user_id,
email: payload.email,
}).await.map_err(|e| ApiError::InternalServerError(e.to_string()))?;

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

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

## Resilience and Retries

`rustapi-jobs` handles failures automatically. If your `execute` method returns an `Err`, the job will be:
1. Marked as failed.
2. Optionally scheduled for retry with **exponential backoff** if retries are enabled.
3. Retried up to `max_attempts` when you configure it via `EnqueueOptions`.

By default, `EnqueueOptions::new()` sets `max_attempts` to `0`, so a failed job will **not** be retried unless you explicitly opt in by calling `.max_attempts(...)` with a value greater than the current `attempts` count.
To customize retry behavior, use `enqueue_opts`:

```rust,no_run
use rustapi_jobs::EnqueueOptions;

queue.enqueue_opts::<WelcomeEmailJob>(
data,
EnqueueOptions::new()
.max_attempts(5) // Retry up to 5 times
.delay(std::time::Duration::from_secs(60)) // Initial delay
).await?;
```

## Backends

While `InMemoryBackend` is great for testing and simple apps, production systems should use persistent backends:

- **Redis**: High performance, good for volatile queues. Enable `redis` feature in `rustapi-jobs`.
- **Postgres**: Best for reliability and transactional safety. Enable `postgres` feature.

```toml
# In Cargo.toml
rustapi-jobs = { version = "0.1", features = ["redis"] }
```
Loading