// Reviews Rust microservices in the depot/ directory with focus on Axum web framework, SQLx database patterns, Docker deployment, and WebSocket communication. Use when reviewing depot service changes (discovery, dispatch, map-api, gps-status, mapper), adding new endpoints, modifying database schemas, implementing WebSocket protocols, or debugging service integration issues. Covers Tokio async patterns, REST/WebSocket API design, PostgreSQL migrations, error handling, health checks, and Docker multi-stage builds.
Reviews Rust microservices in the depot/ directory with focus on Axum web framework, SQLx database patterns, Docker deployment, and WebSocket communication. Use when reviewing depot service changes (discovery, dispatch, map-api, gps-status, mapper), adding new endpoints, modifying database schemas, implementing WebSocket protocols, or debugging service integration issues. Covers Tokio async patterns, REST/WebSocket API design, PostgreSQL migrations, error handling, health checks, and Docker multi-stage builds.
#[tokio::main]asyncfnmain() {
// Tokio runtime handles everything
}
Router is created with all routes defined
Uses Router::new() with chained .route() calls
Groups related routes together (RESTful patterns)
WebSocket routes use get() method with ws.on_upgrade()
State is attached with .with_state()
✅ GOOD:
letapp = Router::new()
// CRUD for zones
.route("/zones", post(create_zone))
.route("/zones", get(list_zones))
.route("/zones/{id}", get(get_zone))
.route("/zones/{id}", put(update_zone))
.route("/zones/{id}", delete(delete_zone))
// WebSocket
.route("/ws", get(ws_handler))
// Health check
.route("/health", get(health))
.layer(CorsLayer::permissive())
.with_state(state);
CORS is configured appropriately
Uses CorsLayer::permissive() for internal services
Applied as layer via .layer(CorsLayer::permissive())
Enables cross-origin requests from console
✅ GOOD:
use tower_http::cors::CorsLayer;
Router::new()
.route("/endpoint", get(handler))
.layer(CorsLayer::permissive()) // Allows all origins for internal services
Server binds to correct address
Port loaded from PORT env var with sensible default
Binds to 0.0.0.0 (all interfaces) for Docker compatibility
Sets max_connections appropriately (10-20 for services)
Connection URL from DATABASE_URL env var
Handles connection errors gracefully
✅ GOOD:
letdatabase_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
letpool = PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await
.expect("Failed to connect to database");
Migrations are applied on startup
Migration SQL files in migrations/ directory
Applied manually in run_migrations() function
Checks if migration already applied before running
Logs migration status
✅ GOOD:
asyncfnrun_migrations(pool: &PgPool) {
letmigration = include_str!("../migrations/001_initial.sql");
// Check if already migratedlettable_exists: bool = sqlx::query_scalar(
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'zones')"
)
.fetch_one(pool)
.await
.unwrap_or(false);
if !table_exists {
info!("Running migration...");
sqlx::raw_sql(migration).execute(pool).await.expect("Migration failed");
}
}
Database queries use parameterized queries
NEVER use string concatenation for SQL
Always use .bind() for parameters
Prevents SQL injection vulnerabilities
❌ BAD (SQL injection vulnerability):
letquery = format!("SELECT * FROM zones WHERE id = '{}'", user_input);
sqlx::query(&query).fetch_one(&pool).await?;
✅ GOOD:
sqlx::query_as::<_, Zone>(
"SELECT * FROM zones WHERE id = $1"
)
.bind(id) // Parameterized - safe!
.fetch_one(&pool)
.await?
Query results use appropriate methods
.fetch_one() - exactly one row expected, error if zero or multiple
.fetch_optional() - zero or one row, returns Option<T>
.fetch_all() - multiple rows, returns Vec<T>
.execute() - for INSERT/UPDATE/DELETE, returns rows affected
✅ GOOD:
// Get by ID - must existletzone: Zone = sqlx::query_as("SELECT ... WHERE id = $1")
.bind(id)
.fetch_one(&state.db)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Get by ID - may not existletzone: Option<Zone> = sqlx::query_as("SELECT ... WHERE id = $1")
.bind(id)
.fetch_optional(&state.db)
.await?;
JSONB fields are handled correctly
Use #[sqlx(json)] attribute on struct fields
Serialize/deserialize with serde
Type must be serde_json::Value in database model
Cast to concrete types when needed
✅ GOOD:
#[derive(FromRow, Serialize)]structZone {
pub id: Uuid,
#[sqlx(json)]pub waypoints: serde_json::Value, // Stored as JSONB
}
// When parsingletwaypoints: Vec<Waypoint> = serde_json::from_value(zone.waypoints)?;
Validate structure before inserting (prevent invalid data)
Common Issues:
Not clearing rover.current_task when task ends (rover stuck)
Forgetting to broadcast updates to console clients
Not rolling back database changes if WebSocket send fails
Race condition between task creation and rover disconnect
Map API Service (depot/map-api)
Purpose: Serve processed maps and 3D assets to console and other clients
Key Files:
src/main.rs - Main service implementation (~450 LOC)
Dockerfile - Multi-stage build
Endpoints:
GET /maps - List all maps
GET /maps/{id} - Get map manifest (metadata)
GET /maps/{id}/{asset} - Download asset (splat.ply, pointcloud.laz, mesh.glb, thumbnail.jpg)
GET /sessions - List all sessions
GET /sessions/{id} - Get session metadata
GET /maps/{id}/sessions - Get sessions used to build map
GET /health - Health check
Specific Concerns:
Map manifest loading is efficient
Reads maps/index.json for map list
Lazily loads manifests from maps/{name}/manifest.json
Caches manifests in RwLock<HashMap<Uuid, MapManifest>>
Reloads on each list/get request (eventual consistency)
Asset serving is correct
Validates asset exists in manifest before serving
Sets correct Content-Type header for each asset type
Reads entire file into memory (acceptable for small assets)
Returns 404 if asset file missing
File paths are constructed safely
Uses PathBuf::join() to build paths
Validates file exists before serving
No path traversal vulnerabilities
Common Issues:
Serving assets not listed in manifest (security issue)
Not setting Content-Type header (browser confusion)
Not handling missing files gracefully
GPS Status Service (depot/gps-status)
Purpose: Monitor RTK base station status and broadcast to console
Key Files:
src/main.rs - Main service implementation (~400 LOC)
Dockerfile - Multi-stage build
Endpoints:
GET /status - Current RTK base station status
GET /ws - WebSocket for live status updates to console
GET /health - Health check
Specific Concerns:
RTK status is polled correctly
Background task polls base station via serial/TCP
Parses RTCM3/NMEA messages for status
Broadcasts status updates via WebSocket
Handles base station disconnection gracefully
WebSocket updates are throttled
Status sent at reasonable interval (1-5 seconds)
Prevents overwhelming clients with updates
Initial status sent on connection
Common Issues:
Not handling base station disconnection
Sending updates too frequently (CPU/bandwidth waste)
Not validating RTCM3 message checksums
Mapper Service (depot/mapper)
Purpose: Orchestrate map processing pipeline (batch job, not always running)
Key Files:
src/main.rs - Main orchestrator (~1000 LOC)
Dockerfile - Multi-stage build
Operation:
Runs as batch job (not a long-running service)
Scans sessions directory for new sessions
Queues sessions for processing
Invokes splat-worker for 3D reconstruction
Generates map manifests and assets
Updates index.json
Specific Concerns:
Session discovery is efficient
Scans filesystem for new sessions
Reads metadata.json from each session
Filters by GPS bounds, frame counts, etc.
Deduplicates sessions already processed
Processing pipeline is fault-tolerant
Tracks session status (pending, processing, processed, failed)
Retries failed sessions with exponential backoff
Logs errors for manual intervention
Doesn't block on failed sessions
Map manifest generation is correct
Calculates GPS bounds from all sessions
Lists all available assets (splat, pointcloud, mesh, thumbnail)
Includes session references
Updates index.json atomically
Common Issues:
Not handling concurrent mapper invocations (file conflicts)
Not validating session metadata before processing
Not cleaning up temporary files on failure
Quick Commands
Development
# Check code (no build)
cargo check -p discovery
cargo check -p dispatch
# Build service
cargo build -p discovery --release
# Run tests
cargo test -p dispatch
# Run service locally (requires dependencies)cd depot/discovery
PORT=4860 cargo run
# Run with logging
RUST_LOG=discovery=debug cargo run
Docker
# Build service imagecd depot/discovery
docker build -t depot-discovery .
# Run service container
docker run -p 4860:4860 -e RUST_LOG=info depot-discovery
# Build all services via Docker Composecd depot
docker compose build
# Start all services
docker compose up -d
# View logs
docker compose logs -f discovery
# Restart service
docker compose restart dispatch
# Stop all services
docker compose down
Database (Dispatch Only)
# Connect to PostgreSQL
docker compose exec postgres psql -U postgres -d dispatch
# View tables
\dt
# Query zones
SELECT id, name, zone_type FROM zones;
# Query tasks
SELECT id, status, rover_id, progress FROM tasks ORDER BY created_at DESC LIMIT 10;
# Reset database (DESTRUCTIVE)
docker compose down -v # Removes volumes
docker compose up -d postgres
docker compose restart dispatch # Migrations run on startup