con un clic
rust-async-tokio
Tokio,
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Tokio,
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
Dependency hygiene for the Rocky engine workspace — how to add/update deps via [workspace.dependencies], MSRV 1.88 policy, cargo-audit / cargo-deny / cargo-machete usage, and how the weekly security audit surfaces advisories.
Rocky CLI JSON-output schema cascade. Use when editing any `*Output` struct in `engine/crates/rocky-cli/src/output.rs` (or `commands/doctor.rs`), adding a new CLI command schema, or when a change could affect `schemas/*.schema.json`, `sdk/python/src/rocky_sdk/types_generated/`, or `editors/vscode/src/types/generated/`. Also use when CI reports `codegen-drift`.
Canonical `rocky.toml` authoring reference. Use when writing or reviewing a Rocky pipeline config — covers the 4 pipeline types (replication, transformation, quality, snapshot), adapter variants (duckdb/databricks/snowflake/fivetran), minimal-config defaults, env-var substitution, governance, checks, hooks, and the ${VAR:-default} syntax.
Top-level router for Rocky development tasks. Use this FIRST for any request to change Rocky — it points at the right subskill based on what's being touched. Also use for quick monorepo orientation (build, test, lint, install, where-is-what).
Rocky DSL (`.rocky` file) cross-subproject cascade. Use when adding/changing a Rocky DSL keyword, operator, or pipeline step — changes must land in the engine parser, compiler, VS Code TextMate grammar, and snippets together in one PR.
End-to-end checklist for adding a new `rocky <command>` subcommand across the engine, JSON schema export, Dagster Pydantic types, Dagster resource wiring, and VS Code extension command. Use whenever a new top-level CLI verb needs to be exposed.
| name | rust-async-tokio |
| description | Tokio, |
Rocky is async end-to-end. Tokio is the only runtime; there's a single #[tokio::main] entrypoint at engine/rocky/src/main.rs:471. Adapter I/O (Databricks REST, Snowflake REST, Fivetran REST, Valkey, webhooks) all runs on the same runtime.
tokio = { version = "1", features = ["full"] } (from engine/Cargo.toml). The "full" feature is deliberate: we use macros, rt-multi-thread, time, fs, net, sync, and process across the workspace, and pinning a narrower feature set per crate creates friction when async code moves between crates.engine/rocky/src/main.rs:471 has #[tokio::main] on async fn main() -> anyhow::Result<()>. Library crates never spawn their own runtime — they take &self on async methods and trust the binary to drive the reactor.tokio re-export. Don't cargo add tokio in a sub-crate; inherit from [workspace.dependencies] in engine/Cargo.toml.Every async trait uses #[async_trait] from the async-trait crate (async-trait = "0.1" in [workspace.dependencies]). This is a hard rule from engine/CLAUDE.md.
// DO — matches the pattern used throughout rocky-adapter-sdk
#[async_trait::async_trait]
pub trait WarehouseAdapter: Send + Sync {
async fn execute(&self, sql: &str) -> Result<StatementResult, AdapterError>;
async fn describe_table(&self, qname: &QualifiedName) -> Result<TableSchema, AdapterError>;
}
// DON'T — native async-in-traits still has object-safety gaps in 2024 edition
pub trait WarehouseAdapter: Send + Sync {
async fn execute(&self, sql: &str) -> Result<StatementResult, AdapterError>;
}
Concrete examples: crates/rocky-adapter-sdk/src/traits.rs has four #[async_trait] trait definitions at lines 219, 333, 346, 372 — those are the shapes adapters must match.
When in doubt, read how rocky-databricks or rocky-fivetran implement the trait — both are full end-to-end examples.
Rocky uses adaptive concurrency on remote calls that can be rate-limited. The canonical implementation is crates/rocky-databricks/src/throttle.rs::AdaptiveThrottle:
increase_interval successes.on_rate_limit() (triggered by 429 / 503 / "TEMPORARILY_UNAVAILABLE"), halve the current concurrency down to min_concurrency.min_concurrency (≥ 1) or above max_concurrency.AtomicUsize + AtomicU64 inside an Arc<ThrottleInner>, so .clone() shares state across tasks. No locks on the hot path.When to reach for this pattern: any new adapter that talks to a rate-limited remote API. Don't re-invent it; either reuse AdaptiveThrottle directly (if it's a Databricks-family API) or copy its shape. The tests at crates/rocky-databricks/src/throttle.rs:122 cover the invariants you'd want to preserve (starts at max, halves on rate limit, never below min, never above max, clone shares state).
How to wire it:
AdaptiveThrottle per warehouse/endpoint at adapter construction.tokio::sync::Semaphore whose available_permits() is refreshed from throttle.current() before each batch.throttle.on_success()throttle.on_rate_limit() and retry after a small delaytracing, not println!Hard rule from engine/CLAUDE.md: use tracing, not println! or eprintln!. The subscriber is initialized in crates/rocky-observe/src/tracing_setup.rs and emits structured JSON lines when RUST_LOG / tracing_subscriber::EnvFilter is set.
use tracing::{info, warn, error, debug};
// DO — structured fields, no interpolation in the message
info!(
connector_id = %connector.id,
table_count = tables.len(),
"discover completed"
);
warn!(
from = old,
to = new,
"adaptive throttle: rate limit detected, reducing concurrency"
);
// DON'T — stringify values into the message
info!("discover completed: connector={}, tables={}", connector.id, tables.len());
%value → uses Display?value → uses Debugfield = literal → treats as JSON literalwarn! is the level rocky-databricks/src/throttle.rs uses for rate-limit detection — follow that pattern for any adaptive-concurrency event you add.
Any network call needs a timeout. Use tokio::time::timeout:
use std::time::Duration;
use tokio::time::timeout;
let result = timeout(Duration::from_secs(30), client.execute(sql))
.await
.context("databricks execute timed out after 30s")??;
// ^^ one ? for Elapsed → anyhow, one ? for the inner Result
Rule of thumb: if a function makes an HTTP/SQL/network call and does not have an outer timeout, it's a bug waiting to happen. The anyhow .context layer is what surfaces the timeout reason into the Dagster event log — don't skip it.
select!tokio::select! is the right tool when you want to race two futures (e.g. "wait for this statement to finish or for the user to ctrl-C"). Guidelines:
tokio::select! docs on which tokio primitives are cancel-safe and which aren't. (Hint: AsyncRead::read_buf is not cancel-safe.)tokio::sync::oneshot for "signal this future to stop" over manual flags.MutexGuard across an .await in any select! branch.| Situation | Use |
|---|---|
| Fan out N tasks and wait for all | futures::future::try_join_all |
| Fan out N tasks, take first result | futures::future::select_ok |
| Fire-and-forget background worker | tokio::spawn(...) — but the spawned future must own its data ('static), and you must handle its JoinHandle if it can fail |
| Parallel CPU-bound chunks | tokio::task::spawn_blocking — not spawn. Blocking the reactor starves other adapters. |
duckdb and sqlparser operations are CPU-bound and should be wrapped in spawn_blocking if they're called from an async context on a hot path.
| Anti-pattern | Why |
|---|---|
std::thread::spawn in async code | Bypasses the runtime; task never wakes correctly. Use tokio::spawn or spawn_blocking. |
std::sync::Mutex held across .await | Can deadlock the reactor. Use tokio::sync::Mutex or (better) restructure to avoid holding the lock. |
Per-crate #[tokio::main] or nested runtimes | There's exactly one runtime, driven by engine/rocky/src/main.rs. Libraries don't own the runtime. |
futures::executor::block_on inside async code | Nested block_on will panic under tokio::main. |
async fn foo(...) -> Box<dyn Future<...>> | Use #[async_trait] for trait methods or impl Future for free functions. |
Spinning on throttle.current() in a busy loop | Drive concurrency off a Semaphore; the throttle is a signal, not a gate. |
rust-error-handling — async errors follow the same two-tier model (library → thiserror, CLI → anyhow with .context).rust-clippy-triage — the async-family clippy lints (e.g. clippy::unused_async, clippy::await_holding_lock) fire in this surface area.rust-unsafe — duckdb calls are sync/FFI and need spawn_blocking, not direct await.