- Inicio
- Documentation
- Nativos
- Ejecución y cancelación de tareas nativas en Rust (`pi-natives`)
Ejecución y cancelación de tareas nativas en Rust (`pi-natives`)
Este documento describe cómo crates/pi-natives planifica el trabajo nativo y cómo la cancelación fluye desde las opciones de JS (timeoutMs, AbortSignal) hasta la ejecución en Rust.
Archivos de implementación
Sección titulada «Archivos de implementación»crates/pi-natives/src/task.rscrates/pi-natives/src/grep.rscrates/pi-natives/src/glob.rscrates/pi-natives/src/fd.rscrates/pi-natives/src/shell.rscrates/pi-natives/src/pty.rscrates/pi-natives/src/html.rscrates/pi-natives/src/image.rscrates/pi-natives/src/clipboard.rscrates/pi-natives/src/text.rscrates/pi-natives/src/ps.rs
Primitivas principales (task.rs)
Sección titulada «Primitivas principales (task.rs)»task.rs define tres piezas fundamentales:
-
task::blocking(tag, cancel_token, work)- Envuelve
napi::AsyncTask/Task. compute()se ejecuta en hilos de trabajo de libuv (para llamadas al sistema bloqueantes/síncronas o con uso intensivo de CPU).- Devuelve una
Promise<T>de JS.
- Envuelve
-
task::future(env, tag, work)- Envuelve
env.spawn_future(...). - Ejecuta trabajo asíncrono en el runtime de Tokio.
- Devuelve
PromiseRaw<'env, T>.
- Envuelve
-
CancelToken/AbortToken/AbortReasonCancelToken::new(timeout_ms, signal)combina un plazo límite + unAbortSignalopcional.CancelToken::heartbeat()es la cancelación cooperativa para bucles bloqueantes.CancelToken::wait()es la espera de cancelación asíncrona (Signal/Timeout/UserCtrl-C).AbortTokenpermite que código externo solicite la cancelación (abort(reason)).
blocking vs future: modelo de ejecución y selección
Sección titulada «blocking vs future: modelo de ejecución y selección»Usar task::blocking
Sección titulada «Usar task::blocking»Se usa cuando el trabajo tiene uso intensivo de CPU o es fundamentalmente síncrono/bloqueante:
- escaneo de archivos/regex (
grep,glob,fuzzy_find) - bucle síncrono interno de PTY (
run_pty_syncmediantespawn_blocking) - conversiones de portapapeles/imagen/html
Comportamiento:
- La clausura de trabajo recibe un
CancelTokenclonado. - La cancelación solo se observa donde el código verifica
ct.heartbeat()?. - Un
Err(...)en la clausura rechaza la promesa de JS.
Usar task::future
Sección titulada «Usar task::future»Se usa cuando el trabajo debe hacer await de operaciones asíncronas:
- orquestación de sesiones de shell (
shell.run,executeShell) - competencia de tareas (
tokio::select!) entre completación y cancelación
Comportamiento:
- El future puede competir la completación normal contra
ct.wait(). - En la ruta de cancelación, las implementaciones asíncronas típicamente propagan la cancelación a los subsistemas internos (por ejemplo,
tokio_util::CancellationToken) y opcionalmente fuerzan la cancelación tras un tiempo de gracia.
Mapeo de API JS ↔ exportación Rust (relevante para tareas/cancelación)
Sección titulada «Mapeo de API JS ↔ exportación Rust (relevante para tareas/cancelación)»| API expuesta a JS | Exportación Rust (#[napi]) | Planificador | Conexión de cancelación |
|---|---|---|---|
grep(options, onMatch?) | grep | task::blocking("grep", ct, ...) | CancelToken::new(options.timeoutMs, options.signal) + ct.heartbeat() |
glob(options, onMatch?) | glob | task::blocking("glob", ct, ...) | CancelToken::new(...) + ct.heartbeat() en el bucle de filtrado |
fuzzyFind(options) | fuzzy_find | task::blocking("fuzzy_find", ct, ...) | CancelToken::new(...) + ct.heartbeat() en el bucle de puntuación |
shell.run(options, onChunk?) | Shell::run | task::future(env, "shell.run", ...) | ct.wait() compitiendo contra la tarea de ejecución; se conecta con CancellationToken de Tokio |
executeShell(options, onChunk?) | execute_shell | task::future(env, "shell.execute", ...) | igual que el anterior |
pty.start(options, onChunk?) | PtySession::start | task::future(env, "pty.start", ...) + spawn_blocking interno | CancelToken verificado en el bucle síncrono de PTY mediante heartbeat() |
htmlToMarkdown(html, options?) | html_to_markdown | task::blocking("html_to_markdown", (), ...) | ninguna (token ()) |
PhotonImage.parse/encode/resize | PhotonImage::{parse,encode,resize} | task::blocking(...) | ninguna (token ()) |
copyToClipboard/readImageFromClipboard | copy_to_clipboard / read_image_from_clipboard | task::blocking(...) | ninguna (token ()) |
text.rs y ps.rs actualmente no usan task::blocking/task::future y por lo tanto no participan en esta ruta de cancelación.
Ciclo de vida de la cancelación y transiciones de estado
Sección titulada «Ciclo de vida de la cancelación y transiciones de estado»Ciclo de vida de CancelToken
Sección titulada «Ciclo de vida de CancelToken»CancelToken es cooperativo y con estado:
Created ├─ no signal + no timeout -> passive token (never aborts unless externally emplaced) ├─ signal registered -> waits for AbortSignal callback └─ deadline set -> timeout check becomes active
Running ├─ heartbeat()/wait() sees signal -> AbortReason::Signal ├─ heartbeat()/wait() sees deadline -> AbortReason::Timeout ├─ wait() sees Ctrl-C -> AbortReason::User └─ no abort -> continue
Aborted (terminal) └─ first abort reason wins (atomic flag + notifier)Cancelación antes del inicio vs durante la ejecución
Sección titulada «Cancelación antes del inicio vs durante la ejecución»-
Antes del inicio / antes de la primera verificación de cancelación:
- Los usuarios de
task::futureque compiten conct.wait()pueden resolver la cancelación inmediatamente una vez que entran enselect!. - Los usuarios de
task::blockingsolo observan la cancelación cuando el código de la clausura alcanzaheartbeat(). Si la clausura no hace heartbeat temprano, la cancelación se retrasa.
- Los usuarios de
-
Durante la ejecución:
blocking: el siguienteheartbeat()devuelveErr("Aborted: ...").future: la ramact.wait()gana elselect!, luego el código cancela la maquinaria asíncrona subordinada (para shell: cancela el token de Tokio, espera hasta 2s, luego aborta la tarea).
Expectativas de heartbeat para bucles de larga duración
Sección titulada «Expectativas de heartbeat para bucles de larga duración»heartbeat() debe ejecutarse con una cadencia predecible en bucles con conjuntos de trabajo ilimitados o grandes.
Patrones observados:
glob::filter_entries: verifica cada entrada antes de filtrar/comparar.fd::score_entries: verifica cada candidato escaneado.grep_sync: verificación explícita de cancelación antes de la fase de búsqueda pesada, además de llamadas a fs-cache que también reciben el token.run_pty_sync: verifica en cada tick del bucle (~16ms de cadencia de sleep) y mata el proceso hijo al cancelar.
Regla práctica: ningún bucle sobre entrada de tamaño externo debe exceder un intervalo corto acotado sin un heartbeat.
Comportamiento de fallos y propagación de errores a JS
Sección titulada «Comportamiento de fallos y propagación de errores a JS»Tareas bloqueantes
Sección titulada «Tareas bloqueantes»Ruta de error:
- La clausura devuelve
Err(napi::Error)(incluyendo cancelación porheartbeat()). Task::compute()devuelveErr.AsyncTaskrechaza la promesa de JS.
Cadenas de error típicas:
Aborted: TimeoutAborted: Signal- Errores de dominio (
Failed to decode image: ...,Conversion error: ..., etc.)
Tareas de tipo future
Sección titulada «Tareas de tipo future»Ruta de error:
- El cuerpo asíncrono devuelve
Err(napi::Error)o el fallo del join se mapea (... task failed: {err}). - La promesa generada por
task::futurese rechaza. - Algunas APIs devuelven intencionalmente resultados de cancelación estructurados en lugar de rechazo (
ShellRunResult/ShellExecuteResultcon flagscancelled/timed_outyexit_code: None).
División en el reporte de cancelación
Sección titulada «División en el reporte de cancelación»- Cancelación como error: la mayoría de las exportaciones bloqueantes que usan
heartbeat()?. - Cancelación como resultado tipado: APIs estilo shell/pty de comandos que modelan la cancelación en estructuras de resultado.
Elija un modelo por API y documéntelo explícitamente.
Errores comunes
Sección titulada «Errores comunes»-
Heartbeat faltante en bucles bloqueantes
- Síntoma: el timeout/signal parece ignorarse hasta que el bucle termina.
- Solución: agregar
ct.heartbeat()?al inicio del bucle y antes de pasos costosos por elemento.
-
Secciones largas no cancelables
- Síntoma: picos de latencia en la cancelación durante una sola llamada grande (decodificación, ordenamiento, compresión, etc.).
- Solución: dividir el trabajo en fragmentos con límites de heartbeat; si es imposible, documentar la latencia.
-
Bloqueo del ejecutor asíncrono
- Síntoma: la API asíncrona se detiene cuando código pesado síncrono se ejecuta directamente en el future.
- Solución: mover bloques de CPU/síncronos a
task::blockingotokio::task::spawn_blocking.
-
Semánticas de cancelación inconsistentes
- Síntoma: una API rechaza al cancelar, otra resuelve con flags, confundiendo a los consumidores.
- Solución: estandarizar por dominio y mantener la documentación del wrapper alineada.
-
Olvidar el puente de cancelación en tareas asíncronas anidadas
- Síntoma: el token externo se cancela pero las tareas internas de lectores/subprocesos siguen ejecutándose.
- Solución: conectar la cancelación al token/signal interno y aplicar un tiempo de gracia + cancelación forzada como respaldo.
Lista de verificación para nuevas exportaciones cancelables
Sección titulada «Lista de verificación para nuevas exportaciones cancelables»-
Clasificar el trabajo correctamente:
- Con uso intensivo de CPU o bloqueo síncrono ->
task::blocking - I/O asíncrono / orquestación con
await->task::future
- Con uso intensivo de CPU o bloqueo síncrono ->
-
Exponer las entradas de cancelación cuando sea necesario:
- incluir
timeoutMsysignalen las opciones#[napi(object)] - crear
let ct = task::CancelToken::new(timeout_ms, signal);
- incluir
-
Conectar la cancelación a través de todas las capas:
- bucles bloqueantes:
ct.heartbeat()?a intervalos estables - orquestación asíncrona: competir con
ct.wait()y cancelar sub-tareas/tokens
- bucles bloqueantes:
-
Decidir el contrato de cancelación:
- rechazar la promesa con un error de cancelación, o
- resolver con un tipo
{ cancelled, timedOut, ... } - mantener este contrato consistente para la familia de APIs
-
Propagar fallos con contexto:
- mapear errores mediante
Error::from_reason(format!("...: {err}")) - incluir prefijos específicos de etapa (
spawn,decode,wait, etc.)
- mapear errores mediante
-
Manejar la cancelación antes del inicio y durante la ejecución:
- la verificación/espera de cancelación debe ocurrir antes del cuerpo costoso y durante la ejecución prolongada
-
Validar que no hay mal uso del ejecutor:
- no ejecutar trabajo síncrono largo directamente dentro de futures asíncronos sin
spawn_blocking/wrapper de tarea bloqueante
- no ejecutar trabajo síncrono largo directamente dentro de futures asíncronos sin