mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 00:00:33 +00:00
6.8 KiB
6.8 KiB
Windmill Backend - Rust Best Practices
Project Structure
Windmill uses a workspace-based architecture with multiple crates:
- windmill-api: API server functionality
- windmill-worker: Job execution
- windmill-common: Shared code used by all crates
- windmill-queue: Job & flow queuing
- windmill-audit: Audit logging
- Other specialized crates (git-sync, autoscaling, etc.)
Adding New Code
Module Organization
- Place new code in the appropriate crate based on functionality
- For API endpoints, create or modify files in
windmill-api/src/organized by domain - For shared functionality, use
windmill-common/src/ - Use the
_ee.rssuffix for enterprise-only modules - Follow existing patterns for file structure and organization
Error Handling
- Use the custom
Errorenum fromwindmill-common::error - Return
Result<T, Error>orJsonResult<T>for functions that can fail - Use the
?operator for error propagation - Add location tracking to errors using
#[track_caller]
Database Operations
- Use
sqlxfor database operations with prepared statements - Leverage existing database helper functions in
db.rsmodules - Use transactions for multi-step operations
- Handle database errors properly
API Endpoints
- Follow existing patterns in the
windmill-apicrate - Use axum's routing system and extractors
- Group related routes together
- Use consistent response formats (JSON)
- Follow proper authentication and authorization patterns
Performance Optimizations
When generating code, especially involving serde, sqlx, and tokio, prioritize performance by applying the following principles:
Serde Optimizations (Serialization & Deserialization)
- Specify Structure Explicitly: When defining structs for Serde (
#[derive(Serialize, Deserialize)]), use#[serde(...attributes extensively. This includes:#[serde(rename = "...")]or#[serde(alias = "...")]to map external names precisely, avoiding dynamic lookups.#[serde(default)]for optional fields with default values, reducing parsing complexity.#[serde(skip_serializing_if = "...")]to avoid writing fields that meet a certain condition (e.g.,Option::is_none(),Vec::is_empty(), or a custom function), reducing output size and serialization work.#[serde(skip_serializing)]or#[serde(skip_deserializing)]for fields that should not be included.
- Prefer Borrowing: Where possible and safe (data lifetime allows), use
Cow<'a, str>or&'a str(with#[serde(borrow)]) instead ofStringfor string fields during deserialization. This avoids allocating new strings, enabling zero-copy reading from the input buffer. Apply this principle to byte slices (&'a [u8]/Cow<'a, [u8]>) and potentially borrowed vectors as well. - Avoid Intermediate
Value: Unless the data structure is truly dynamic or unknown at compile time, deserialize directly into a well-defined struct or enum rather than intoserde_json::Value(or equivalent for other formats). This avoids unnecessary heap allocations and type switching.
SQLx Optimizations (Database Interaction)
- Select Only Necessary Columns: In
SELECTqueries, list specific column names rather than usingSELECT *. This reduces data transferred from the database and the work needed for hydration/deserialization. - Batch Operations: For multiple
INSERT,UPDATE, orDELETEstatements, prefer executing them in a single query if the database and driver support it efficiently (e.g.,INSERT INTO ... VALUES (...), (...), ...). This minimizes round trips to the database. - Avoid N+1 Queries: Do not loop through results of one query and execute a separate query for each item (e.g., fetching users, then querying for each user's profile in a loop). Instead, use JOINs or a single query with an
INclause to fetch related data efficiently. - Deserialize Directly: Use
#[derive(FromRow)]on structs and ensure the struct fields match the selected columns in the query. This allows SQLx to hydrate objects directly, avoiding intermediate data structures. - Parameterize Queries: Always use SQLx's query methods (
.bind(...)) to pass values as parameters rather than string formatting. This prevents SQL injection and allows the database to cache query plans, improving performance on repeated executions.
Tokio Optimizations (Asynchronous Runtime)
- Avoid Blocking Operations: Crucially, never perform blocking operations (synchronous file I/O,
std::thread::sleep, CPU-bound loops,std::sync::Mutex::lock, blocking network calls withouttokio::net) directly within anasync fnor a standardtokio::spawntask. Blocking pauses the entire worker thread, potentially starving other tasks. Usetokio::task::spawn_blockingfor CPU-intensive work or blocking I/O. - Use Tokio's Async Primitives: Prefer
tokio::sync(channels, mutexes, semaphores),tokio::io,tokio::net, andtokio::timeover theirstdcounterparts in asynchronous contexts. These are designed to yield control back to the scheduler. - Manage Concurrency: Be mindful of how many tasks are spawned. Creating a new task for every tiny piece of work can introduce overhead. Group related asynchronous operations where appropriate.
- Handle Shared State Efficiently: Use
Arcfor shared ownership in concurrent tasks. When shared state needs mutation, prefertokio::sync::Mutexoverstd::sync::Mutexinasynccode. Considertokio::sync::RwLockif reads significantly outnumber writes. Minimize the duration for which locks are held. - Understand
.await: Place.awaitstrategically to allow the runtime to switch to other ready tasks. Ensure that.awaitpoints to genuinely asynchronous operations. - Backpressure: If dealing with data streams or queues between tasks, implement backpressure mechanisms (e.g., bounded channels like
tokio::sync::mpsc::channel) to prevent one component from overwhelming another or critical resources like the database.
Enterprise Features
- Use feature flags for enterprise functionality
- Conditionally compile with
#[cfg(feature = "enterprise")] - Isolate enterprise code in separate modules
Code Style
- Group imports by external and internal crates
- Place struct/enum definitions before implementations
- Group similar functionality together
- Use descriptive naming consistent with the codebase
- Follow existing patterns for async code using tokio
Testing
- Write unit tests for core functionality
- Use the
#[cfg(test)]module for test code - For database tests, use the existing test utilities
Common Crates Used
- tokio: For async runtime
- axum: For web server and routing
- sqlx: For database operations
- serde: For serialization/deserialization
- tracing: For logging and diagnostics
- reqwest: For HTTP client functionality