diff --git a/README.md b/README.md index 883700c281..d6b987b6b7 100644 --- a/README.md +++ b/README.md @@ -24,26 +24,63 @@ Build the same static site and validate the same links as CI: npm run build ``` -## Ukrainian translations +## Translations -English is the default locale. Current documentation and site pages are also -available in Ukrainian under `/uk/`. Preview that locale with: +English is the default locale. Ukrainian is available under `/uk/`. Spanish +onboarding and core guides are available under `/es/`. Untranslated reference +pages show the English text with a language notice and a link to the English +route. Blog articles and versioned 1.x documentation remain in English. + +Preview one locale with: ```bash npm run start -- --locale uk +npm run start -- --locale es ``` -Translate current docs in `i18n/uk/docusaurus-plugin-content-docs/current/` -and standalone pages in `i18n/uk/docusaurus-plugin-content-pages/`. Keep the +Translate current docs in `i18n//docusaurus-plugin-content-docs/current/` +and standalone pages in `i18n//docusaurus-plugin-content-pages/`. Keep the English source file paths, examples, commands, API names and explicit heading -anchors intact. Keep workflow, activity, worker, signal, timer, query, update -and saga as English technical terms. +anchors intact. Ukrainian keeps workflow, activity, worker, signal, timer, +query, update and saga as English technical terms. Spanish uses workflow and +worker, with actividad, señal, temporizador, consulta and actualización in +prose. Never translate identifiers in code, protocol fields or type names. Use Docusaurus translation markers for component text. Run `npm run write-translations -- --locale uk` to extract new messages, then -translate them in `i18n/uk/code.json` and the plugin message files. Build both -locales with `npm run build` before submitting a change. Blog articles and the -versioned 1.x docs use the English fallback. +translate them in `i18n//code.json` and the plugin message files. Build +all locales with `npm run build` before submitting a change. + +When changing an English guide that already has a translation, review and +update that translation in the same pull request. Review the prose for fluent +language and the technical contract for accuracy. Keep executable code blocks +identical to the English source. The build checks this for Spanish. If a +translation cannot be brought current, remove the stale translated file so +readers see the current English source and the fallback notice. Artifact +version placeholders stay shared with English and update automatically. + +The documentation maintainers own translation upkeep. The existing build also +compares reviewed English source hashes for Spanish and reports the guides +needing review. Prose edits produce a notice, while changed executable examples +must still match. After reviewing and +updating the translation, run `node scripts/check-doc-translations.js --record-review` +and commit `i18n/es/source-hashes.json` with it. This command still checks the +code blocks. Recording hashes alone does not review the prose. + +The pinned Docusaurus 3.10.2 utility patch resolves relative Markdown links +between translated and English fallback files. It uses Docusaurus's document +map and preserves strict missing-link checks and version boundaries. The +build exercises both directions. Recheck and remove the patch when upgrading +Docusaurus if the upstream resolver handles these cases. + +For a new locale, qualify installation, a completed first workflow, core +concepts, safe recovery, navigation, search, language switching and fallback +before publication. Check representative pages in a browser, including mobile, +and verify canonical and alternate-language links. Spanish is the first +expansion in [#169](https://github.com/durable-workflow/durable-workflow.github.io/issues/169). +Portuguese (Brazil), Simplified Chinese and Japanese remain subsequent stages, +subject to audience evidence and language review. Country totals alone do not +establish a reader's preferred language. ## Repository layout diff --git a/docusaurus.config.js b/docusaurus.config.js index 637902627d..dcecec3fc6 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -53,10 +53,11 @@ const config = { i18n: { defaultLocale: 'en', - locales: ['en', 'uk'], + locales: ['en', 'uk', 'es'], localeConfigs: { en: {label: 'English', htmlLang: 'en'}, uk: {label: 'Українська', htmlLang: 'uk'}, + es: {label: 'Español', htmlLang: 'es'}, }, }, diff --git a/i18n/es/code.json b/i18n/es/code.json new file mode 100644 index 0000000000..0262dd701b --- /dev/null +++ b/i18n/es/code.json @@ -0,0 +1,32 @@ +{ + "theme.SearchPage.existingResultsTitle": {"message": "Resultados de búsqueda de «{query}»"}, + "theme.SearchPage.emptyResultsTitle": {"message": "Buscar en la documentación"}, + "theme.SearchPage.searchContext.everywhere": {"message": "En todas partes"}, + "theme.SearchPage.documentsFound.plurals": {"message": "Se ha encontrado {count} documento|Se han encontrado {count} documentos"}, + "theme.SearchPage.noResultsText": {"message": "No se han encontrado documentos"}, + "theme.SearchBar.noResultsText": {"message": "No hay resultados"}, + "theme.SearchBar.seeAllOutsideContext": {"message": "Ver todos los resultados fuera de «{context}»"}, + "theme.SearchBar.searchInContext": {"message": "Ver todos los resultados en «{context}»"}, + "theme.SearchBar.seeAll": {"message": "Ver todos los resultados"}, + "theme.SearchBar.label": {"message": "Buscar"}, + "docs.translationFallback": {"message": "Esta página está disponible en inglés."}, + "docs.readInEnglish": {"message": "Leer en inglés"}, + "homepage.tagline": {"message": "Ejecución duradera para PHP, Python y Rust."}, + "homepage.getStarted": {"message": "Empieza aquí"}, + "homepage.deploymentModes": {"message": "Elige un modelo de despliegue"}, + "homepage.description": {"message": "Plataforma de ejecución duradera para PHP, Python y Rust, disponible como Cloud gestionado, Server autogestionado o entorno integrado en Laravel."}, + "homepage.cloudDescription": {"message": "Ejecuta workers de PHP, Python o Rust en un espacio de nombres gestionado mientras Durable Workflow opera el entorno de orquestación."}, + "homepage.features.languages.title": {"message": "Desarrolla con PHP, Python o Rust"}, + "homepage.features.languages.description": {"message": "Crea clientes, workflows y actividades con SDK oficiales que comparten un protocolo público y un modelo de datos Avro portable."}, + "homepage.features.runtime.title": {"message": "Elige quién opera el entorno"}, + "homepage.features.runtime.description": {"message": "Usa Durable Workflow Cloud, ejecuta un Server independiente o conserva el entorno integrado dentro de tu aplicación Laravel."}, + "homepage.features.recovery.title": {"message": "Recupera el trabajo después de un fallo"}, + "homepage.features.recovery.description": {"message": "El historial duradero, la reproducción determinista, los temporizadores, los reintentos, las señales, las actualizaciones, los workflows hijos y las sagas permiten continuar procesos largos."}, + "promotion.eyebrow": {"message": "Servicio gestionado · grupo limitado"}, + "promotion.title": {"message": "Grupo de lanzamiento de Durable Workflow Cloud"}, + "promotion.requestAccess": {"message": "Solicita acceso anticipado"}, + "navbar.github.repository": {"message": "Repositorio de GitHub"}, + "navbar.github.repositoryStars": {"message": "{repository} ({count} estrellas)"}, + "navbar.github.starCount": {"message": "Estrellas en GitHub: {count}"}, + "navbar.github.stars": {"message": "Estrellas en GitHub"} +} diff --git a/i18n/es/docusaurus-plugin-content-docs/current.json b/i18n/es/docusaurus-plugin-content-docs/current.json new file mode 100644 index 0000000000..fe10fd0875 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,25 @@ +{ + "version.label": {"message": "2.0"}, + "sidebar.tutorialSidebar.category.Service Mode": {"message": "Modo servicio"}, + "sidebar.tutorialSidebar.category.Service Mode.link.generated-index.description": {"message": "Conecta clientes y workers oficiales de PHP, Python o Rust a Cloud o Server autogestionado. Cloud incluye Managed Waterline. Para Server, Waterline se despliega por separado."}, + "sidebar.tutorialSidebar.category.CLI": {"message": "CLI"}, + "sidebar.tutorialSidebar.category.CLI.link.generated-index.description": {"message": "Instala el cliente de operación dw y consulta sus comandos."}, + "sidebar.tutorialSidebar.category.SDKs": {"message": "SDK"}, + "sidebar.tutorialSidebar.category.SDKs.link.generated-index.description": {"message": "Crea clientes y workers en modo servicio con los SDK oficiales de PHP, Python y Rust."}, + "sidebar.tutorialSidebar.category.Embedded": {"message": "Modo integrado"}, + "sidebar.tutorialSidebar.category.Embedded.link.generated-index.description": {"message": "Instala, desarrolla, prueba y configura workflows dentro de una aplicación Laravel."}, + "sidebar.tutorialSidebar.category.Defining Workflows": {"message": "Definición de workflows"}, + "sidebar.tutorialSidebar.category.Defining Workflows.link.generated-index.description": {"message": "Define workflows, actividades, datos, identificadores y estados en Laravel integrado."}, + "sidebar.tutorialSidebar.category.Features": {"message": "Funciones"}, + "sidebar.tutorialSidebar.category.Features.link.generated-index.description": {"message": "Elige mensajes, comandos duraderos, esperas, reintentos, versiones y límites de historial para Laravel integrado."}, + "sidebar.tutorialSidebar.category.Constraints": {"message": "Restricciones"}, + "sidebar.tutorialSidebar.category.Constraints.link.generated-index.description": {"message": "Conoce las reglas de determinismo y los límites estructurales del modo integrado."}, + "sidebar.tutorialSidebar.category.Configuration": {"message": "Configuración"}, + "sidebar.tutorialSidebar.category.Configuration.link.generated-index.description": {"message": "Configura almacenamiento, conexiones, límites y retención para Laravel integrado."}, + "sidebar.tutorialSidebar.category.Run And Operate": {"message": "Ejecución y operación"}, + "sidebar.tutorialSidebar.category.Run And Operate.link.generated-index.description": {"message": "Despliega, observa, diagnostica, recupera y mantén tus workflows."}, + "sidebar.tutorialSidebar.category.External Execution And Ingress": {"message": "Ejecución externa y entrada de solicitudes"}, + "sidebar.tutorialSidebar.category.External Execution And Ingress.link.generated-index.description": {"message": "Conecta API HTTP, workers externos, colas y otras integraciones de protocolo."}, + "sidebar.tutorialSidebar.category.AI And Automation": {"message": "IA y automatización"}, + "sidebar.tutorialSidebar.category.AI And Automation.link.generated-index.description": {"message": "Proporciona documentación, manifiestos, comandos y herramientas estables a agentes y automatización."} +} diff --git a/i18n/es/docusaurus-plugin-content-docs/current/constraints/activity-constraints.md b/i18n/es/docusaurus-plugin-content-docs/current/constraints/activity-constraints.md new file mode 100644 index 0000000000..c4a73f4bb9 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/constraints/activity-constraints.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 3 +--- + +# Restricciones de las actividades {#activity-constraints} + +Las actividades realizan las operaciones de entrada/salida y los efectos +externos de un workflow. Su código se ejecuta **al menos una vez**. Los +reintentos, el vencimiento de una lease y la reentrega pueden hacer que el +mismo trabajo lógico se ejecute varias veces. + +Si una actividad cobra un pago, envía un correo, escribe en otro sistema o +modifica un recurso externo, la operación debe ser segura al repetirse. + +## Consecuencias prácticas {#what-this-means-in-practice} + +- `activity_execution_id` es la identidad idempotente predeterminada de una + ejecución lógica de actividad. +- Cada intento tiene además su propio `activity_attempt_id`. +- Un worker puede completar el efecto externo, perder su lease y comunicar el + resultado tarde. El motor puede rechazar ese resultado porque otro worker + ya registró el resultado duradero. El efecto externo puede haber ocurrido. +- Las actividades pueden usar entrada/salida, el reloj del sistema y estado + mutable del proceso. El código de workflow debe mantenerse determinista. + +## Patrones de idempotencia recomendados {#preferred-idempotency-patterns} + +Muchas API externas aceptan una cabecera `Idempotency-Key`. Cuando el servicio +lo permita, usa la identidad lógica de la actividad: + +- Prefiere `activity_execution_id` si los reintentos deben representar la + misma solicitud lógica. +- Usa `activity_attempt_id` cuando el sistema externo necesite distinguir + cada intento de ese trabajo. + +También puedes: + +- Escribir en un recurso externo con un nombre o una clave determinista. +- Usar operaciones upsert o tablas de deduplicación con un identificador duradero. +- Diseñar la operación para que repetirla no cambie el resultado. + +Algunas operaciones ya son idempotentes: codificar el mismo vídeo puede +producir el mismo archivo, y borrar un archivo ya borrado no produce otro +efecto. + +En otras operaciones, duplicar puede ser preferible a perder el resultado. +Si no sabes si un proveedor envió un correo, puede ser más seguro enviarlo de +nuevo que omitir la notificación. Decide esa compensación de forma explícita. + +## Suposiciones que debes evitar {#what-not-to-assume} + +- Un intento de actividad puede llegar a más de un worker. +- Un reintento no demuestra que el efecto externo anterior haya fallado. +- Un resultado tardío no demuestra que la actividad no se ejecutara. +- Mantén los efectos externos en actividades. Llevarlos al código de workflow + convierte el problema de idempotencia en un problema de determinismo. + +Consulta el contrato completo en +[Garantías de ejecución e idempotencia](./execution-guarantees.md) y el modelo +de recuperación para operadores en [Fallos y recuperación](../failures-and-recovery.md). diff --git a/i18n/es/docusaurus-plugin-content-docs/current/constraints/execution-guarantees.md b/i18n/es/docusaurus-plugin-content-docs/current/constraints/execution-guarantees.md new file mode 100644 index 0000000000..34244f1ed2 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/constraints/execution-guarantees.md @@ -0,0 +1,185 @@ +--- +sidebar_position: 3.5 +title: Garantías de ejecución e idempotencia +description: Contrato público de v2 para reproducción, reintentos, vencimiento de leases, reentrega e identidades idempotentes. +tags: + - constraints + - idempotency + - retries + - replay +keywords: + - garantías de ejecución + - idempotencia + - reproducción + - reentrega +--- + +# Garantías de ejecución e idempotencia {#execution-guarantees-and-idempotency} + +Durable Workflow v2 distingue entre **reproducción de workflows** y +**ejecución de actividades**: + +- El código del workflow se reproduce a partir del historial confirmado y + debe ser determinista. +- Las actividades realizan efectos externos y se ejecutan **al menos una vez**. +- El historial registra los resultados confirmados de workflows y actividades + exactamente una vez por identificador duradero, aunque el transporte + entregue el trabajo varias veces. + +Estas garantías permiten reanudar después de reinicios de workers, vencimiento +de leases, reentrega de tareas y despliegues graduales sin perder el punto de +ejecución. + +## Reproducción y reintento {#replay-is-not-retry} + +Las tareas de workflow reconstruyen el estado a partir del historial confirmado +y deciden el siguiente paso. La reproducción vuelve a ejecutar el cuerpo del +workflow. Las actividades, señales y efectos externos ya registrados se +recuperan del historial, sin repetirse. + +Por eso, el código del workflow debe mantenerse determinista. Usa funciones +seguras como [`Workflow::now()`](../defining-workflows/workflow-api.md), +[`sideEffect(...)`](../features/side-effects.md), consultas, actualizaciones, +resultados de actividades, memos y atributos de búsqueda para cruzar el límite +de persistencia duradera. + +## Las actividades se ejecutan al menos una vez {#activity-execution-is-at-least-once} + +Las actividades realizan los efectos externos. Su contrato permite: + +- Que un intento de actividad sea reclamado más de una vez. +- Que el vencimiento de una lease provoque una reentrega a otro worker. +- Que un worker termine el trabajo externo, pierda su lease y comunique el + resultado tarde. +- Que un reintento programe un nuevo intento duradero de la misma actividad + lógica. + +La ejecución duplicada forma parte del contrato. El autor de la aplicación +debe hacer que la actividad o el sistema remoto sean seguros al repetirse. + +Consulta [Restricciones de las actividades](./activity-constraints.md) para +escribirlas y [Fallos y recuperación](../failures-and-recovery.md) para operarlas. + +## Qué se registra exactamente una vez {#what-is-exactly-once} + +Un worker puede recibir varias veces un trabajo con efectos externos. Los +hechos duraderos confirmados son la autoridad y no se duplican para el mismo +identificador duradero. + +En la práctica: + +- Una decisión confirmada del workflow se guarda una vez en el historial + tipado para el ID de su comando o paso. +- El resultado terminal confirmado de un intento se guarda una vez para ese + `activity_attempt_id`. +- La reproducción lee esos hechos y reconstruye el estado del workflow. + +La distinción esencial es: + +- **El transporte y los workers entregan trabajo al menos una vez.** +- **El historial duradero confirmado registra cada hecho exactamente una vez + por identificador duradero.** + +## Vencimiento de leases y reentrega {#lease-expiry-and-redelivery} + +El vencimiento de una lease es una vía normal de recuperación: + +- Una tarea reclamada tiene un propietario de lease y una hora de vencimiento. +- Si la lease vence antes de que el worker comunique progreso o finalización, + la tarea puede volver a entregarse. +- Otro worker puede reclamar el mismo trabajo lógico. + +La reentrega recupera trabajo cuyo estado en el worker o en el transporte es +incierto. Los hechos ya confirmados siguen en el historial. + +Ante síntomas de duplicación, investiga por separado: + +1. ¿Ocurrió el efecto externo más de una vez? +2. ¿Registró el estado duradero más de un resultado confirmado para el mismo + identificador? + +La primera cuestión requiere actividades idempotentes. La segunda pertenece +al contrato del motor. + +## Identidades idempotentes predeterminadas {#default-idempotency-surfaces} + +Estos identificadores estables permiten deduplicar trabajo: + +| Identificador | Qué identifica | Uso habitual | +| --- | --- | --- | +| `workflow_instance_id` | Una instancia pública de workflow | Deduplicar inicios e identificar el proceso de negocio | +| `workflow_run_id` | Una ejecución duradera concreta | Fijar la ejecución consultada, exportada o diagnosticada | +| `workflow_command_id` | Un comando externo que modifica estado | Deduplicar reintentos de solicitudes del cliente | +| `activity_execution_id` | Una actividad lógica a través de sus reintentos | Clave idempotente predeterminada para efectos externos | +| `activity_attempt_id` | Un intento concreto de esa actividad | Correlación cuando el sistema remoto distingue intentos | +| `schedule_id` | Una definición de programación | Deduplicar propiedad y disparos de la programación | +| `idempotencyKey` de un flujo de mensajes | Un envío lógico de mensaje | Evitar la entrada duplicada cuando el emisor reintenta | + +Usa `activity_execution_id` como clave idempotente predeterminada de una +operación externa. Elige `activity_attempt_id` cuando el destino necesite +distinguir cada intento. + +```php +use Workflow\V2\Activity; + +final class ChargeCard extends Activity +{ + public function handle(array $payload): string + { + return app(PaymentGateway::class)->charge( + $payload, + idempotencyKey: $this->activityId(), + attemptCorrelation: $this->attemptId(), + ); + } +} +``` + +## Qué debe ser idempotente en tu aplicación {#what-developers-must-make-idempotent} + +El framework se encarga de reproducir el workflow y reconstruir el estado a +partir del historial confirmado. + +Tu aplicación debe hacer seguros al repetirse los efectos externos, como: + +- Pagos y operaciones de facturación. +- Correos, mensajes y webhooks. +- Escrituras en otra base de datos o servicio. +- Creación o subida de archivos. +- Comandos que crean o modifican estado fuera del historial del workflow. + +Patrones habituales: + +- Pasar una clave idempotente a la API remota. +- Escribir en un recurso determinista, como una clave de objeto conocida. +- Usar un upsert o una transacción con un identificador duradero. +- Hacer que repetir la acción no produzca otro efecto. + +## Guía para operadores {#operator-guidance} + +Al diagnosticar una ejecución en Waterline, el CLI o los registros de Server: + +- La entrega duplicada después del vencimiento de una lease es esperable. + Comprueba el resultado duradero del intento. +- El motor resuelve los informes tardíos de finalización o fallo. Su rechazo + no demuestra que el efecto externo no ocurriera. +- La reproducción de tareas recupera el workflow dentro de su ejecución. +- Investiga la ausencia de workers compatibles, las leases atascadas y las + reparaciones repetidas antes de volver a ejecutar efectos externos. + +Durante la operación, distingue la **incertidumbre del transporte** del +**resultado duradero**. Durable Workflow expone ambos para poder investigarlos. + +## Guías relacionadas {#related-guides} + +- [Resumen](./overview.md): separación entre workflows y actividades. +- [Restricciones de los workflows](./workflow-constraints.md): determinismo. +- [Restricciones de las actividades](./activity-constraints.md): seguridad de + los efectos externos e idempotencia. +- [Fallos y recuperación](../failures-and-recovery.md): reintentos, plazos y reparación. +- [Modelo de ejecución de actividades](../features/activity-execution-model.md): + actividades en cola, actividades locales, sesiones de workers y ejecución sticky. +- [Actividades locales](../features/local-activities.md): intentos en el mismo + proceso, heartbeats de tareas, reintentos y reproducción en frío. +- [Ejecución sticky](../features/sticky-execution.md): cachés de reproducción + y recuperación correcta mediante reproducción en frío. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md b/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md new file mode 100644 index 0000000000..243bde9e38 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/constraints/overview.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 1 +--- + +# Resumen {#overview} + +Los workflows y las actividades tienen restricciones distintas. El código de +un workflow se **reproduce a partir del historial**. El código de una actividad +puede **volver a intentarse**. Las reglas dependen de estas dos formas de +ejecución. + +Empieza por [Workflows idempotentes y deterministas](/docs/constraints/idempotent-vs-deterministic/) +para ver una comparación con ejemplos que muestran por qué una propiedad no +implica la otra. + +- **El código del workflow debe ser determinista.** Cada vez que el workflow + continúa, el motor reproduce su historial para reconstruir el estado. Esto + ocurre también al cambiar de worker, después de un reinicio o despliegue, y + durante ejecuciones largas. La reproducción vuelve a ejecutar el cuerpo del + workflow y reutiliza los resultados registrados de las actividades. Ante el + mismo historial, el workflow debe tomar las mismas decisiones en el mismo + orden. Su cuerpo no puede consultar el reloj del sistema ni una caché que + pueda cambiar, generar números aleatorios o hacer llamadas de red. Usa + [`Workflow::now()`](../defining-workflows/workflow-api.md), + [`sideEffect(...)`](../features/side-effects.md), actividades y otros helpers + para obtener valores que deban quedar registrados de forma duradera. + +- **El código de las actividades debe ser idempotente.** Los intentos de una + actividad se ejecutan **al menos una vez**. Los reintentos, el vencimiento de + una lease y la entrega repetida pueden hacer que el mismo trabajo lógico se + ejecute más de una vez. El framework registra como máximo un resultado + terminal por intento en el estado duradero, pero el cuerpo de una actividad + puede empezar a ejecutarse varias veces antes de que el motor reciba el + informe que prevalece. Diseña las actividades para admitir esta repetición. + Usa una clave de idempotencia, un recurso de destino determinista o una + operación naturalmente idempotente cuando el efecto externo no deba duplicarse. + +- **Event sourcing conserva el historial de los pasos duraderos.** El motor + registra cada paso como un evento con un tipo definido, por ejemplo la + finalización de una actividad, el disparo de un temporizador, la recepción de una + señal o el valor de un side effect. Al reproducir el historial, devuelve los + resultados registrados al cuerpo del workflow sin volver a enviar esas + actividades, temporizadores o señales. Cada evento de estado duradero asociado a un + identificador se registra exactamente una vez en el historial, aunque el + transporte haya entregado el trabajo más de una vez. + +El determinismo y la idempotencia permiten continuar workflows a través de +despliegues, reinicios de workers y reintentos distribuidos. El motor conserva +la posición de la ejecución, y la aplicación evita duplicar los efectos +externos que ha diseñado para poder repetirse de forma segura. + +Consulta [Garantías de ejecución e idempotencia](./execution-guarantees.md) +para conocer el contrato público de v2 sobre reproducción, entregas repetidas, +vencimiento de leases e historial duradero con eventos registrados exactamente +una vez. Después, lee [Restricciones de los workflows](./workflow-constraints.md) +para escribir código determinista y [Restricciones de las actividades](./activity-constraints.md) +para hacer seguras las ejecuciones que pueden repetirse. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/constraints/workflow-constraints.md b/i18n/es/docusaurus-plugin-content-docs/current/constraints/workflow-constraints.md new file mode 100644 index 0000000000..332148c39a --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/constraints/workflow-constraints.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 2 +--- + +# Restricciones de los workflows {#workflow-constraints} + +Para ser determinista, una clase de workflow no puede depender de estados +externos o servicios que cambien con el tiempo. Su código no debe consultar +directamente la fecha y hora actuales, el usuario de la sesión, recursos de +red externos ni otras fuentes de estado variable. + +Ten en cuenta estas reglas al escribir un workflow: + +- No uses `Carbon::now()` para consultar la fecha y hora actuales. El resultado + cambia entre llamadas. Usa `Workflow\V2\Workflow::now()` o el helper + `Workflow\V2\now()`, que devuelven una hora del workflow segura para la + reproducción del historial. +- No uses `Auth::user()` para obtener el usuario actual. El resultado depende + de la sesión. Pasa el usuario como entrada al iniciar el workflow. +- No hagas peticiones de red a recursos externos. Pueden responder lentamente + o dejar de estar disponibles. Pasa los datos necesarios como entradas al + iniciar el workflow o usa una actividad para obtenerlos. +- No generes valores aleatorios directamente en el workflow. Si necesitas + hacerlo durante la ejecución, usa un side effect para registrar el resultado. + También puedes pasar el valor aleatorio como entrada al iniciar el workflow. + +## Comprobaciones al iniciar la aplicación {#boot-time-guardrails} + +Cuando registras clases de workflow en `workflows.v2.types.workflows`, el paquete +las analiza al iniciar la aplicación para detectar llamadas claramente +incompatibles con la reproducción, como `Carbon::now()`, `Auth::user()`, `DB::`, +`Http::` o `random_int()`. La opción `workflows.v2.guardrails.boot` determina +cómo se comunican los hallazgos: + +| Modo | Comportamiento | +|------|----------------| +| `warn` (predeterminado) | Registra una advertencia por cada hallazgo. La aplicación puede iniciarse. | +| `silent` | Omite por completo el análisis al iniciar la aplicación. | +| `throw` | Lanza una `LogicException` ante el primer hallazgo. Es útil en CI. | + +```php +// config/workflows.php +'v2' => [ + 'guardrails' => [ + 'boot' => env('DW_V2_GUARDRAILS_BOOT', 'warn'), + ], +], +``` + +Configura `DW_V2_GUARDRAILS_BOOT=throw` en CI para que la compilación falle si +se añaden llamadas incompatibles con la reproducción. Mantén `warn` en +producción para que un hallazgo previo no impida un despliegue. + +En la primera versión, el análisis al iniciar la aplicación es la única +comprobación del modo workflow que puede bloquear el inicio. El runtime no +repite el diagnóstico de determinismo al reclamar una tarea de workflow. Esta +decisión es deliberada en 2.0: el análisis detecta problemas en los workflows +PHP registrados localmente antes del despliegue. Waterline muestra los cambios +en la huella de la definición de ejecuciones largas sin convertir la entrega +de tareas entre versiones del código en una nueva causa de fallos de despliegue. + +Las ejecuciones anteriores al registro de huellas de definición también siguen +una política conservadora. Si una ejecución llega a una nueva rama de +`getVersion()` y su evento `WorkflowStarted` es anterior a la captura de la +huella, el runtime mantiene esa ejecución en `WorkflowStub::DEFAULT_VERSION`. +Consulta [Versionado](../features/versioning.md). diff --git a/i18n/es/docusaurus-plugin-content-docs/current/defining-workflows/workflows.md b/i18n/es/docusaurus-plugin-content-docs/current/defining-workflows/workflows.md new file mode 100644 index 0000000000..5aa374a79d --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/defining-workflows/workflows.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 1 +title: Workflows +description: Define clases de workflow de Durable Workflow v2 y mantén determinista el código de orquestación. +tags: + - authoring + - workflows + - determinism +--- + +# Workflows {#workflows} + +En Laravel integrado, los workflows y las actividades son clases que extienden +`Workflow` y `Activity`. Un workflow coordina actividades en serie, en paralelo +o combinando ambos patrones. + +Crea la clase con el comando Artisan `make:workflow`: + +```php +php artisan make:workflow MyWorkflow +``` + +Extiende `Workflow` e implementa el método `handle()`: + +```php +use function Workflow\V2\activity; +use Workflow\V2\Workflow; + +class MyWorkflow extends Workflow +{ + public function handle() + { + return activity(MyActivity::class); + } +} +``` diff --git a/i18n/es/docusaurus-plugin-content-docs/current/failures-and-recovery.md b/i18n/es/docusaurus-plugin-content-docs/current/failures-and-recovery.md new file mode 100644 index 0000000000..530fc981e8 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/failures-and-recovery.md @@ -0,0 +1,280 @@ +--- +sidebar_position: 11 +title: Fallos y recuperación +description: Diagnostica fallos de actividades, excepciones permanentes, vencimiento de plazos y acciones de recuperación. +tags: + - failures + - recovery + - operations +keywords: + - workflow failures + - non retryable exception + - workflow recovery +--- + +# Fallos y recuperación {#failures-and-recovery} + +Antes de investigar un fallo, ten presente el contrato de ejecución: + +- Las tareas de workflow se recuperan reproduciendo el historial confirmado. +- Las actividades se ejecutan al menos una vez y pueden repetirse. +- El vencimiento de una lease y la reentrega son vías normales de recuperación. + No demuestran que el worker anterior no realizara el efecto externo. + +Consulta [Garantías de ejecución e idempotencia](./constraints/execution-guarantees.md) +para conocer la semántica exacta de reintentos, reentregas y resultados duraderos. + +## Manejo de excepciones {#handling-exceptions} + +Cuando una actividad lanza una excepción, el sistema aplica su política de +reintentos hasta agotar `$tries`. Después entrega la excepción al workflow. +Configura `$tries = 1` para que el workflow la reciba en el primer fallo. + +```php +use Exception; +use Workflow\V2\Activity; + +class MyActivity extends Activity +{ + public int $tries = 1; + + public function handle(): void + { + throw new Exception(); + } +} +``` + +```php +use Exception; +use function Workflow\V2\activity; +use Workflow\V2\Workflow; + +class MyWorkflow extends Workflow +{ + public function handle(): void + { + try { + $result = activity(MyActivity::class); + } catch (Exception) { + // handle the exception here + } + } +} +``` + +## Excepciones que no admiten reintentos {#non-retryable-exceptions} + +Algunas excepciones representan fallos permanentes. Si una actividad lanza una +excepción que no admite reintentos, el motor marca la actividad como fallida y +deja de reintentar inmediatamente. + +```php +use Workflow\V2\Activity; +use Workflow\Exceptions\NonRetryableException; + +class MyNonRetryableActivity extends Activity +{ + public function handle(): void + { + throw new NonRetryableException('This is a non-retryable error'); + } +} +``` + +## Proceso de recuperación {#recovery-process} + +Para corregir una actividad que está fallando: + +1. Consulta sus registros y localiza los errores o excepciones. +2. Identifica la causa y corrige el código. +3. Despliega la corrección donde se ejecutan los workers de la cola. +4. Reinicia o sustituye gradualmente los workers para que carguen el código + nuevo y puedan reclamar trabajo de forma segura. +5. Espera al reintento o a que la reparación o reentrega asigne la tarea + duradera a un worker sano. +6. Verifica el resultado duradero en Waterline, una exportación del historial + o la API. Una línea del registro del worker no basta para confirmar el estado. +7. Si la actividad sigue fallando, continúa el diagnóstico. + +Mientras queden reintentos, el workflow puede seguir en ejecución aunque la +actividad falle. Si corriges la causa antes de agotar los intentos, puede +continuar hasta completarse. Agotar `$tries` con una excepción sin manejar +cierra el workflow como fallido. + +## Aplicación de los plazos del workflow {#workflow-timeout-enforcement} + +Al configurar `StartOptions::withExecutionTimeout()` o +`StartOptions::withRunTimeout()`, el motor guarda un plazo en la ejecución. +El plazo de ejecución abarca todo el workflow lógico, incluidas las ejecuciones +continue-as-new. El plazo de run se reinicia con cada nueva ejecución. + +Si el plazo ha vencido al iniciar una tarea de workflow, el motor cierra la +ejecución inmediatamente: + +- Cancela las actividades abiertas, los temporizadores y las tareas pendientes, + y registra eventos tipados como `ActivityCancelled` y `TimerCancelled`. +- Registra un `WorkflowFailure` con `failure_category = timeout` y + `propagation_kind = timeout`. +- Registra `WorkflowTimedOut` con `timeout_kind` igual a `execution_timeout` + o `run_timeout`. +- Cambia el estado a `failed` con `closed_reason = timed_out`. +- Notifica a los workflows padres que esperan al hijo. + +El supervisor de tareas busca también ejecuciones no terminales con plazos +vencidos y sin tarea de workflow abierta. Esto incluye las que esperan una +actividad o un temporizador. Crea una tarea para que el ejecutor aplique el +vencimiento en la siguiente pasada. + +Waterline muestra `failure_category` en la columna de categoría de excepciones +y en los detalles de fallos de la cronología. Las exportaciones del historial +lo incluyen en `failures[*]`. V2 guarda la clasificación al registrar el fallo. +Las filas importadas de v1 que no puedan clasificarse siguen visibles como +diagnósticos sin clasificar. + +## Reintentos de actividades {#activity-retries} + +`Workflow\V2\Activity` usa `$tries = 1` de forma predeterminada. El fallo se +entrega inmediatamente al workflow, salvo que la actividad configure reintentos. + +```php +use RuntimeException; +use Workflow\V2\Activity; + +class ChargeCard extends Activity +{ + public int $tries = 3; + + public function backoff(): array + { + return [5, 30]; + } + + public function handle(): string + { + throw new RuntimeException('temporary gateway failure'); + } +} +``` + +Cuando falla un intento y aún quedan reintentos, el motor cierra su fila en +`activity_attempts`, devuelve `activity_executions` a `pending`, registra +`ActivityRetryScheduled` y crea una tarea duradera cuyo `available_at` deriva +de `backoff()`. El workflow sigue esperando la misma actividad lógica y recibe +la excepción si falla el último intento. + +La tarea guarda `retry_of_task_id`, `retry_after_attempt_id`, +`retry_after_attempt` y `retry_backoff_seconds` para explicar su programación. +El detalle de la ejecución reconstruye el intento fallido en +`activities[*].attempts` a partir del historial tipado, muestra +`ActivityRetryScheduled` en la cronología y expone los contadores +`operator_metrics.activities.retrying`, +`operator_metrics.activities.failed_attempts` y +`operator_metrics.backlog.retrying_activities`. + +`Workflow\Exceptions\NonRetryableExceptionContract` interrumpe la política: +una excepción permanente falla la actividad y reanuda el workflow con la +excepción inmediatamente. + +### Identidad de la actividad e idempotencia {#activity-execution-identity-and-idempotency} + +El vencimiento de una lease, la pérdida del worker, un informe de finalización +retrasado y la reentrega también pueden producir otro intento o un informe +obsoleto de la misma actividad lógica. + +- `activity_execution_id` identifica la actividad lógica a través de reintentos + y reentregas. Úsalo como clave idempotente predeterminada para efectos remotos. +- `activity_attempt_id` identifica un intento. Úsalo cuando el sistema externo + necesite distinguir intentos. +- Un informe tardío de un intento sustituido no demuestra que el motor haya + confirmado el mismo intento dos veces. + +Al investigar una finalización tardía después de vencer una lease: + +- Consulta Waterline, la exportación del historial o la API para saber qué + intento confirmó su resultado. +- Un informe tardío rechazado no demuestra que el efecto externo no ocurriera. +- Comprueba el sistema externo por su clave idempotente antes de forzar un + reintento o una reparación manual. + +Haz el efecto remoto idempotente bajo `activity_execution_id` y consulta el +resultado duradero para saber si el motor aceptó el informe del intento. + +### Marcas de fallos permanentes {#non-retryable-failure-markers} + +Si la excepción implementa `Workflow\Exceptions\NonRetryableExceptionContract`, +el motor guarda `non_retryable = true` en `WorkflowFailure` y en el evento +tipado (`ActivityFailed`, `WorkflowFailed`, `UpdateCompleted`). Esta marca +duradera comunica un fallo permanente a operadores, workers y herramientas. + +La marca aparece en todas las superficies de observación: + +- **Filas de fallos:** columna booleana `workflow_failures.non_retryable`. +- **Eventos del historial:** campo `non_retryable` del evento tipado. +- **Instantáneas de fallos:** `non_retryable` en `FailureSnapshots::forRun()`. +- **Detalle de ejecución:** `non_retryable` en el array de excepciones. +- **Cronología:** `non_retryable` en los metadatos del fallo. +- **Exportaciones:** `non_retryable` en `failures[*]`. +- **Waterline:** una etiqueta de fallo sin reintentos junto a la categoría. +- **Puente de workers externos:** `complete()` acepta `non_retryable` para + informar del fallo sin que el proceso anfitrión resuelva la clase de excepción. + +Sin ese contrato, `non_retryable` vale `false` por defecto. Decláralo antes de +registrar el fallo para que operadores y SDK puedan distinguir los fallos +permanentes de los que admiten reintentos. + +```php +use Workflow\Exceptions\NonRetryableExceptionContract; + +class PaymentDeclinedException extends \RuntimeException implements NonRetryableExceptionContract +{ + // This failure will be marked as non-retryable in the durable record. +} +``` + +## Reintentos a nivel de workflow {#workflow-level-retry} + +Durable Workflow v2 no reintenta automáticamente un workflow completo. Un fallo +por excepción sin manejar, límite estructural o vencimiento cierra la +ejecución. El motor no inicia otra ejecución de esa instancia automáticamente. + +El diseño usa estas herramientas: + +- **Reintentos de actividades:** `$tries`, `backoff()` y excepciones permanentes + gestionan fallos transitorios de cada operación. +- **Reproducción del workflow:** ante un error transitorio de infraestructura, + como un fallo de base de datos o del worker, el sistema vuelve a entregar la + tarea y reanuda desde el historial confirmado dentro de la misma ejecución. +- **Continue-as-new:** `continueAsNew()` inicia explícitamente una ejecución + nueva para renovar el estado o limitar el historial de workflows largos. +- **Reparación:** `repair()` y la reparación automática del bucle del worker + recuperan ejecuciones cuyo transporte de tareas duraderas se perdió. + +Si necesitas reintentos a nivel de workflow, modélalos explícitamente: + +```php +use function Workflow\V2\activity; +use Throwable; +use Workflow\V2\Workflow; + +class RetryableWorkflow extends Workflow +{ + public function handle(string $orderId): void + { + try { + activity(ProcessOrderActivity::class, $orderId); + } catch (Throwable $e) { + // Record the failure, then start a new workflow + // for retry-at-workflow-level scenarios. + activity(NotifyFailureActivity::class, $orderId, $e->getMessage()); + } + } +} +``` + +## Guías relacionadas {#related-guides} + +- [Garantías de ejecución e idempotencia](./constraints/execution-guarantees.md): + contrato de reproducción, reintentos, leases y reentrega. +- [Monitorización](./monitoring.md): observación de fallos en Waterline, + exportaciones, registros de workers y telemetría del entorno. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/how-it-works.md b/i18n/es/docusaurus-plugin-content-docs/current/how-it-works.md new file mode 100644 index 0000000000..0c7175312d --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/how-it-works.md @@ -0,0 +1,144 @@ +--- +sidebar_position: 10 +--- + +# Cómo funciona {#how-it-works} + +En el modo Laravel integrado, Durable Workflow usa trabajos en cola y +persistencia basada en eventos para crear corrutinas duraderas. Los workflows +se suspenden mediante funciones basadas en Fiber y se reanudan reproduciendo +su historial. + +## Entorno de ejecución {#runtime} + +Un workflow es una clase cuyo método `handle()` llama directamente a funciones +como `activity()`, `await()`, `timer()`, `sideEffect()`, `child()` y `all([...])`. +Cada llamada suspende el workflow hasta que termina el paso duradero y lo +reanuda con el resultado registrado. + +Cada paso produce un evento duradero. Al despertar el workflow, el motor +reproduce el historial, reconstruye el estado y ejecuta el siguiente paso +pendiente. Así puede recuperarse después de reinicios de workers, despliegues +y fallos de máquinas sin perder su posición. + +`WorkflowStub::make()` reserva el ID público de la instancia. Al iniciar el +workflow se crean su primera ejecución y su primera tarea. Cada ejecución +tiene un ID propio. Operaciones como `signal()`, `cancel()` y `terminate()` +actúan sobre la ejecución actual de la instancia. + +## Persistencia basada en eventos {#event-sourcing} + +El estado actual se reconstruye a partir de una secuencia de eventos guardados. +Ese historial permite inspeccionar la ejecución y reanudar el workflow si el +worker falla. + +## Corrutinas {#coroutines} + +Las corrutinas pueden suspenderse y reanudarse. Los puntos de suspensión +duradera se expresan mediante llamadas directas basadas en Fiber, como +`activity()`, `await()`, `timer()` y `sideEffect()`. + +El método `handle()` contiene el código del workflow. El motor comprueba si el +paso ya terminó de forma duradera. Si es así, recupera el resultado del +historial. Si está pendiente, programa la actividad, el temporizador o el +workflow hijo y suspende el workflow hasta que ese paso termine o falle. + +## Actividades {#activities} + +Un workflow coordina actividades y sus resultados. Cuando llega a una llamada +de actividad, se suspende hasta recibir su resultado y continúa desde ese punto. + +Para recuperar una tarea tras un fallo, el motor reproduce los eventos +confirmados y reconstruye el estado con las mismas entradas y salidas. + +En v2, las actividades ordinarias son tareas duraderas en cola y pueden +ejecutarse en cualquier worker compatible. Las +[actividades locales](./features/local-activities.md) realizan trabajo breve +en el proceso del worker del workflow, conservando el historial y los reintentos. +Las [sesiones de workers](./features/worker-sessions.md) añaden una lease +explícita si varios pasos necesitan el mismo recurso local. Para registrar un +valor una sola vez sin poner una actividad en cola, usa +[`sideEffect(...)`](./features/side-effects.md). Consulta el contrato en +[Modelo de ejecución de actividades](./features/activity-execution-model.md). + +## Garantías de ejecución {#execution-guarantees} + +El código de workflow y el de actividad tienen garantías distintas: + +- **El código del workflow se reproduce.** La reentrega reconstruye el estado + desde el historial y ejecuta el código determinista. Los efectos externos + ya registrados no se repiten. +- **Las actividades se ejecutan al menos una vez.** Los reintentos, la pérdida + de un worker y el vencimiento de leases pueden causar entregas duplicadas. +- **La identidad de la actividad es duradera.** `activity_execution_id` + identifica la actividad lógica. `activity_attempt_id` identifica un intento. + Usa el primero como clave idempotente remota y el segundo cuando el destino + necesite correlacionar cada intento por separado. + +Consulta [Garantías de ejecución e idempotencia](./constraints/execution-guarantees.md), +[Modelo de ejecución de actividades](./features/activity-execution-model.md) y +[Fallos y recuperación](./failures-and-recovery.md) para el contrato completo. + +## Colas {#queues} + +Laravel admite colas mediante Amazon SQS, Redis o una base de datos relacional. +Los workflows y las actividades usan trabajos en cola. Un workflow se despacha +varias veces: ejecuta sus decisiones, programa trabajo y sale mientras espera. +Una actividad suele terminar en un intento, pero los reintentos, el vencimiento +de una lease o la pérdida del worker pueden provocar nuevas entregas. + +## Ejemplo {#example} + +```php +use Workflow\V2\Workflow; +use function Workflow\V2\{activity, all}; + +class MyWorkflow extends Workflow +{ + public function handle(): array + { + return [ + activity(TestActivity::class), + activity(TestOtherActivity::class), + fn () => all([ + fn () => activity(TestParallelActivity::class), + fn () => activity(TestParallelOtherActivity::class), + ]), + ]; + } +} +``` + +## Diagrama de secuencia {#sequence-diagram} + +El diagrama muestra cómo avanza un workflow entre actividades secuenciales +y paralelas. + +import ThemedImage from '@site/src/components/ThemedImage'; + + + +1. El workflow se despacha como trabajo en cola. +2. Programa `TestActivity` y sale. Al terminar, la actividad guarda su resultado + y vuelve a despachar el workflow. +3. El workflow reproduce el historial de la base de datos para reconstruir su + estado. No necesita mantener un proceso activo mientras espera actividades. +4. Continúa con `TestOtherActivity`. Al terminar, esta guarda su resultado y + vuelve a despachar el workflow. +5. El workflow reconstruye otra vez el estado a partir del historial. +6. Programa dos actividades paralelas. Ambas guardan sus resultados al terminar + y devuelven el control al workflow. +7. El workflow reproduce el historial una última vez y completa su ejecución. + +## Determinismo {#determinism} + +Con el mismo historial, el código del workflow debe producir los mismos +comandos. Consulta [Restricciones](./constraints/overview.md) para conocer las +reglas y las funciones seguras, como `Workflow\now()`, `sideEffect()` y +`getVersion()`, que evitan decisiones no deterministas. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/installation.md b/i18n/es/docusaurus-plugin-content-docs/current/installation.md new file mode 100644 index 0000000000..e308abaa86 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/installation.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 3 +--- + +# Instalación {#installation} + +Esta guía explica cómo instalar el paquete PHP de Durable Workflow en una aplicación Laravel. + +Si estás eligiendo entre integrar el paquete en tu aplicación y usar el Server +independiente, empieza por [Modos de despliegue](/docs/polyglot/deployment-modes). +Esta página describe la integración en Laravel. + +> **¿Prefieres empezar con una aplicación que ya funciona?** La +> [aplicación de ejemplo](/docs/sample-app) es un proyecto Laravel 13 ejecutable +> con Durable Workflow 2.0. Incluye un workflow para cada patrón, un entorno +> Codespaces y una opción con `docker compose`. Clona el proyecto y ejecuta +> `php artisan app:init` para aplicar esta misma instalación. Vuelve a esta guía +> cuando quieras añadir Durable Workflow a tu propia aplicación Laravel. + +## Requisitos {#requirements} + +- PHP 8.1 o posterior +- Laravel 9 o posterior + +Durable Workflow funciona con los drivers de colas que admite Laravel, excepto +el driver `sync`. Entre ellos están: + +- Amazon SQS +- Beanstalkd +- Database +- Redis + +Cada driver tiene sus propios [requisitos previos](https://laravel.com/docs/12.x/queues#driver-prerequisites). + +Durable Workflow también necesita un driver de caché que admita +[bloqueos](https://laravel.com/docs/12.x/cache#atomic-locks). + +## Instalar Durable Workflow {#installing-durable-workflow} + +Instala Durable Workflow con Composer: + +```bash +composer require %%artifact.workflowComposerPackage%% +``` + +Usa `durable-workflow/workflow:^2.0` para que Composer pueda instalar +actualizaciones compatibles de la serie 2.x. Consulta +[Compatibilidad de versiones](/docs/compatibility) para conocer las reglas +de compatibilidad entre el runtime y los paquetes. + +El paquete carga sus migraciones automáticamente. Después de instalarlo, +basta con ejecutar las migraciones habituales: + +```bash +php artisan migrate +``` + +## Ejecutar workers {#running-workers} + +Durable Workflow usa colas para ejecutar workflows y actividades en segundo +plano. Necesitas ejecutar el [comando `queue:work`](https://laravel.com/docs/12.x/queues#the-queue-work-command) +o usar [Horizon](https://laravel.com/docs/12.x/horizon) para administrar los +workers de las colas. Sin un worker, los workflows y las actividades no se +procesan. Para ejecutarlos en paralelo, necesitas más de un worker. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/introduction.md b/i18n/es/docusaurus-plugin-content-docs/current/introduction.md new file mode 100644 index 0000000000..70fb0d17c1 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/introduction.md @@ -0,0 +1,147 @@ +--- +sidebar_position: 1 +description: Elige Durable Workflow Cloud, Server autogestionado o Laravel integrado y completa tu primer workflow con un SDK oficial. +tags: + - concepts + - getting-started + - workflows +keywords: + - durable workflow + - motor de workflows políglota + - orquestación duradera +--- + +import PythonPackageReleaseLink from '@site/src/components/PythonPackageReleaseLink'; + +# Introducción {#introduction} + +Durable Workflow 2.0 guarda el estado y el historial de los workflows fuera de +los procesos de la aplicación. Así, los workers de PHP, Python y Rust pueden +reanudar su trabajo después de un reinicio. Empieza con la +[Guía de inicio rápido](/docs/quickstart/) para completar un workflow antes de +consultar el [Índice de capacidades](/docs/capabilities/). + +## Elige un modelo de despliegue {#choose-a-deployment-model} + +### Modo servicio {#service-mode} + +Las aplicaciones se conectan a un entorno de ejecución remoto mediante los +SDK oficiales. Elige quién se encarga de operarlo: + +- **[Durable Workflow Cloud](/docs/polyglot/cloud-control-plane/)** es la + opción gestionada. Durable Workflow opera la orquestación, la persistencia + y Managed Waterline. Tu equipo ejecuta los clientes y workers del SDK en el + espacio de nombres provisionado. **Los usuarios de Cloud no instalan ni + ejecutan Server ni un servicio Waterline separado.** +- **[Server autogestionado](/docs/polyglot/server/)** ofrece la misma interfaz + de servicio. Tu equipo despliega, protege, escala, respalda y actualiza el + entorno de ejecución. Puedes desplegar Waterline como servicio separado + para observar el espacio de nombres gestionado por Server. + +Ambas opciones comparten el plano de control HTTP+JSON versionado, el protocolo +de workers, el modelo de espacios de nombres y el formato de datos independiente +del lenguaje. Consulta [Modos de despliegue](/docs/polyglot/deployment-modes/) +para conocer las responsabilidades de cada modelo. + +### Laravel integrado {#embedded-laravel} + +El modo integrado permite que una aplicación Laravel mantenga el estado, las +colas, la configuración y las herramientas de operación en su propia +infraestructura. Instala `durable-workflow/workflow`, sin conectarse a Cloud ni +necesitar un Server separado. El paquete Waterline integrado lee ese estado +dentro de la aplicación. + +Elige la [Instalación integrada](/docs/installation/) cuando quieras que la +aplicación sea responsable de ese entorno. + +Si vienes de v1 o estás reconsiderando un despliegue integrado de 2.0, consulta +la [Guía de adopción en Laravel](/docs/laravel-adoption/) para comparar las +rutas ejecutables del modo integrado y del SDK PHP antes de cambiar el tráfico. + +## Elige un SDK para el modo servicio {#choose-a-service-mode-sdk} + +- **[SDK PHP](/docs/polyglot/php/):** instala `durable-workflow/sdk` en una + aplicación PHP independiente del framework o en un worker remoto. +- **[SDK Python](/docs/polyglot/python/):** escribe workflows deterministas y + actividades, y usa el cliente asíncrono del plano de control. La + versión estable de + Python figura en el mismo manifiesto de versiones + estables que la guía de inicio de Server. +- **[SDK Rust](/docs/polyglot/rust/):** escribe workflows deterministas y + actividades, y ejecuta servicios de workers nativos. + +Los tres implementan la misma interfaz pública. La guía de +[Capacidades de clientes y workers](/docs/polyglot/cli-python-parity/) explica +qué funciones admiten y dónde difieren sus interfaces. + +## Tu primer workflow completado {#your-first-completed-workflow} + +La [Guía de inicio rápido](/docs/quickstart/) indica el objetivo, la elección +del entorno, los requisitos, el tiempo estimado y el resultado esperado. +Puedes seguir una ruta de PHP, Python o Rust. La ruta local usa artefactos +publicados sin descargar el código fuente. La ruta de Cloud usa los datos de +conexión del espacio de nombres gestionado, sin ejecutar Server. + +## Cómo encajan las piezas del modo servicio {#how-service-mode-fits-together} + +Un despliegue en modo servicio tiene tres partes: + +- **El entorno de ejecución** mantiene el estado duradero, los comandos y el + historial, la asignación de tareas, los temporizadores, las programaciones, + los espacios de nombres y los protocolos autenticados. Cloud lo opera en + los espacios gestionados. Tu equipo lo opera en un despliegue autogestionado. +- **Los workers de la aplicación** ejecutan workflows y actividades con los + SDK de PHP, Python o Rust. Pueden desplegarse junto a la aplicación o como + servicios independientes, y escalar por separado. +- **Los clientes y las herramientas de operación** inician, inspeccionan y + controlan el mismo estado mediante los SDK, el CLI `dw`, las API HTTP, los + esquemas legibles por máquinas, Waterline y las interfaces para agentes. + +## Un contrato público de ejecución duradera {#one-public-durable-execution-contract} + +Los SDK oficiales comparten nombres de tipos de workflow y actividad +registrados como cadenas, y un formato público para los datos. Ese formato +identifica su codec y contiene valores portables en lugar de serialización +PHP, pickles de Python o tipos internos de Rust. + +Los workers reconstruyen las decisiones a partir de los comandos y del +historial duradero. Las entradas y los resultados de actividades y workflows +hijos pueden cruzar lenguajes cuando los workers anuncian el mismo codec +público y registran los mismos nombres de tipos. Consulta el +[Índice de capacidades](/docs/capabilities/) y la información del entorno +antes de depender de una función concreta de un SDK. + +## Aprende con los ejemplos de tu modelo {#learn-from-the-matching-examples} + +- **Modo servicio y varios lenguajes:** sigue la + [Guía de inicio rápido](/docs/quickstart/) y la guía del SDK elegido. +- **Laravel integrado:** explora los patrones nativos de Laravel y la + información de Waterline en la galería [Sample App](/docs/sample-app/). + +La galería integrada está orientada a Laravel. Para Cloud o Server +autogestionado, empieza con la guía del modo servicio. + +## Operación por agentes mediante un contrato explícito {#agent-operable-by-contract} + +Las personas y los agentes autónomos usan el mismo contrato legible por +máquinas. El ciclo verificable es **Descubrir → Cambiar → Ejecutar → Diagnosticar +→ Reparar**: manifiestos de versiones y capacidades, comandos explícitos, +resultados estructurados, historial tipado, diagnósticos de workers y colas, +modificaciones seguras y verificación posterior. Consulta el +[Ciclo de operación de agentes](/docs/agent-operating-loop/) y el +[Evaluador de motores para agentes de IA](/docs/ai-agent-workflow-engine/). + +## ¿Necesitas un workflow? {#do-you-need-a-workflow} + +Probablemente lo necesitas si: + +- El proceso dura minutos, horas o días. +- Debes esperar una aprobación humana. +- Debes esperar un webhook u otro evento externo. +- Quieres pausar y continuar sin mantener un proceso en ejecución. +- Debes reanudar después de un fallo sin perder el estado ni duplicar trabajo. + +Si solo necesitas ejecutar cinco trabajos en orden y detenerte ante el primer +fallo, una cadena de trabajos suele bastar. Durable Workflow resulta útil +cuando el siguiente paso depende de un evento externo, una espera o una +decisión que no puede conocerse de antemano. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/polyglot/deployment-modes.md b/i18n/es/docusaurus-plugin-content-docs/current/polyglot/deployment-modes.md new file mode 100644 index 0000000000..199baacece --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/polyglot/deployment-modes.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 2 +title: Modos de despliegue +description: Elige el modo servicio con Durable Workflow Cloud o Server autogestionado, o integra el entorno en Laravel. +tags: + - deployment + - server + - laravel + - polyglot +keywords: + - modos de despliegue de Durable Workflow + - modo integrado + - modo servicio +--- + +import ProductPromotion from '@site/src/components/ProductPromotion'; + +# Modos de despliegue {#deployment-modes} + + +Elige el servicio gestionado para tus workers de PHP, Python o Rust sin operar +el entorno de orquestación. + + +Durable Workflow v2 tiene dos modos de despliegue: + +- **Modo servicio:** las aplicaciones y los workers se conectan mediante SDK a + un entorno remoto. Elige [Cloud](/docs/polyglot/cloud-control-plane/) o + [Server autogestionado](/docs/polyglot/server/). +- **Modo integrado:** una aplicación Laravel instala + `durable-workflow/workflow` y opera directamente el entorno. + +Cloud y Server autogestionado son alternativas dentro del modo servicio. +Cloud opera la orquestación, la persistencia y Managed Waterline. El cliente +ejecuta sus aplicaciones y workers. **En Cloud no instalas ni despliegas tu +propio Server ni un servicio Waterline separado.** Server autogestionado no +incluye Waterline. Puedes desplegarlo por separado para observar un espacio +de nombres gestionado por Server. + +Usa esta página para elegir quién mantiene tus workflows o planificar un +cambio de modelo. Los equipos Laravel pueden consultar la +[Guía de adopción y transición](/docs/laravel-adoption/), incluido el puente +Laravel del SDK PHP y su implementación de pruebas. + +## Elige un entorno en modo servicio {#choose-a-service-mode-runtime} + +| Entorno | Quién opera el estado duradero | Qué ejecuta tu equipo | Primer paso | +| --- | --- | --- | --- | +| Durable Workflow Cloud | Durable Workflow opera la persistencia, las actualizaciones, el endpoint y Managed Waterline. | Clientes y workers de PHP, Python o Rust con credenciales provisionadas. | [Entorno gestionado de Cloud](/docs/polyglot/cloud-control-plane/) | +| Server autogestionado | Tu equipo despliega, protege, escala, respalda y actualiza Server y su persistencia. | Server, clientes y workers. Waterline puede desplegarse como servicio separado. | [Server autogestionado](/docs/polyglot/server/) | + +Ambos usan el mismo modelo de clientes y workers. Cambian la operación del +entorno y las credenciales. + +## El mismo modelo duradero con distintas interfaces {#same-durable-model-different-boundary} + +Los dos modos comparten el núcleo de v2. Cambian el alojamiento, la +autenticación y el transporte. El modo servicio expone HTTP+JSON, sin gRPC +obligatorio ni un segundo motor. + +| Superficie | Modo integrado | Modo servicio | Contrato compartido | +| --- | --- | --- | --- | +| Estado duradero | Laravel aloja el paquete y su estado. | Cloud o Server mantiene el estado detrás de la API. | IDs de instancia y ejecución, historial tipado, resultados de comandos, reintentos, reparación y exportación. | +| Plano de control | Código de la aplicación, `WorkflowStub` o herramientas locales. | API, CLI o SDK con autenticación y cabeceras de protocolo. PHP usa `DurableWorkflow\Client` de `durable-workflow/sdk`. | Política de inicios duplicados, selección de ejecución, IDs de comandos y resultados. Los comandos posteriores van al entorno que aceptó el inicio. | +| Workers | Workers de colas Laravel dentro de la aplicación. | Registro, long polling, heartbeat y finalización por HTTP+JSON. PHP usa `DurableWorkflow\Worker`. | Leases, compatibilidad, reproducción y actividades ejecutadas al menos una vez. | +| Despacho predeterminado | Colas Laravel dentro de la aplicación. | Despacho por polling para workers externos. Server permite cambiarlo explícitamente. | Ciclo de disponibilidad, lease y reparación de tareas duraderas. | +| Tipos de workflow y actividad | Los alias PHP pueden resolver clases locales. | Los workers anuncian los tipos que admiten. | Nombres públicos estables e independientes del lenguaje. Evita usar nombres completos de clases PHP como contrato público. | +| Observación | Waterline integrado lee el estado de Laravel. | Cloud incluye Managed Waterline. Server admite un Waterline separado, además de API, CLI y SDK. | Estado, atributos de búsqueda, memos, diagnósticos de colas e historial del entorno que posee la ejecución. Waterline no combina entornos ni espacios de nombres. | +| Autenticación | La aplicación Laravel controla rutas y sesiones. | La selección de espacio de nombres y la autenticación de Server son obligatorias. | Nombres de espacios, colas, marcas de compatibilidad y contrato Avro estable durante la transición. | +| Conexión | Servicios internos o configuración de la aplicación. | URL base remota explícita. | No depender de `APP_URL`, `APP_KEY`, localhost ni compartir contenedor. | +| Migración | Las ejecuciones existentes permanecen en su entorno. | Las nuevas empiezan en el entorno elegido. | No hay migración automática de ejecuciones activas. Exportar sirve para auditoría y diagnóstico, no para importar estado activo. | + +## Cuándo elegir el modo integrado {#choose-embedded-mode-when} + +- Tu aplicación Laravel reúne código, workers y acceso de operadores. +- Sus colas y su autenticación son la interfaz adecuada. +- Quieres un entorno autocontenido y no necesitas workers de otros lenguajes. +- Tus operadores pueden usar Waterline integrado o las herramientas de la aplicación. + +Empieza con [Instalación integrada](/docs/installation/) y la +[Documentación del modo integrado](/docs/category/embedded/). + +## Cuándo elegir el modo servicio {#choose-service-mode-when} + +- Varias aplicaciones o equipos comparten un entorno de workflows. +- Los workers, clientes u operadores no usan todos Laravel/PHP. +- Necesitas autenticación remota y espacios de nombres explícitos. +- Quieres escalar por separado las API, el despacho y los workers mediante la + [Topología de roles de Server](/docs/polyglot/server-role-topology). +- Quieres observar Server autogestionado con un Waterline separado. + Cloud ya incluye Managed Waterline. + +Para el servicio gestionado, empieza con [Cloud](/docs/polyglot/cloud-control-plane/). +Para autogestión, consulta [Server](/docs/polyglot/server/) y +[Despliegues autogestionados](/docs/deployment/). Elige después el +[SDK PHP](/docs/polyglot/php/), [SDK Python](/docs/polyglot/python/) o +[SDK Rust](/docs/polyglot/rust/). Para un Waterline separado, consulta la +[API de Server](/docs/polyglot/server-api-reference/) y +[Monitorización](/docs/monitoring#waterline-service). + +## Transición al modo servicio autogestionado {#migration-tooling-to-self-hosted-service-mode} + +La ruta admitida es una adopción gradual: + +- Sigue [Migración del modo integrado a Server](/docs/polyglot/embedded-to-server). +- Comprueba la versión, topología y capacidades con `GET /api/cluster/info`. +- Registra workers con `POST /api/worker/register` y verifica que ejecutan los + nombres de tipos estables elegidos. +- Antes de cambiar el tráfico, comprueba la cobertura de workers compatibles + mediante `GET /api/system/operator-metrics`, `dw worker:list` o Waterline. +- Consulta [Capacidades de clientes y workers](/docs/polyglot/cli-python-parity/) + al sustituir llamadas locales por automatización remota. +- Usa Waterline o la exportación del historial para auditoría y diagnóstico. + Una exportación no importa ejecuciones activas en otro entorno. + +Respeta estas tres reglas: + +1. Las ejecuciones existentes permanecen donde empezaron. +2. Las nuevas usan nombres estables de tipos, espacios de nombres, colas y el + contrato Avro desde el primer cambio de tráfico. +3. Señales, consultas, actualizaciones, reparación, cancelación, terminación y + archivo se dirigen al entorno que posee la ejecución. + +## Referencias relacionadas {#related-references} + +- [Instalación](/docs/installation) +- [Server](/docs/polyglot/server) +- [SDK PHP](/docs/polyglot/php) +- [Entorno gestionado de Cloud](/docs/polyglot/cloud-control-plane) +- [Migración a Server](/docs/polyglot/embedded-to-server) +- [Topología de roles](/docs/polyglot/server-role-topology) +- [Configuración de Server](/docs/polyglot/server-config-reference) diff --git a/i18n/es/docusaurus-plugin-content-docs/current/quickstart.md b/i18n/es/docusaurus-plugin-content-docs/current/quickstart.md new file mode 100644 index 0000000000..fd19587f75 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/quickstart.md @@ -0,0 +1,485 @@ +--- +sidebar_position: 2 +title: Inicio rápido de Durable Workflow 2.0 +description: Elige un entorno en modo servicio y completa tu primer workflow con PHP, Python o Rust. +tags: + - quickstart + - getting-started + - PHP + - Python + - Rust +keywords: + - Durable Workflow quickstart + - Durable Workflow 2.0 quickstart + - standalone PHP SDK quickstart + - Python SDK quickstart + - Rust SDK quickstart + - standalone server Docker quickstart +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import PythonPackageReleaseLink from '@site/src/components/PythonPackageReleaseLink'; + +# Inicio rápido de Durable Workflow 2.0 {#durable-workflow-20-quickstart} + +## Antes de empezar {#before-you-begin} + +**Objetivo:** ejecutar un workflow en modo servicio y leer su resultado +duradero con PHP, Python o Rust. + +**Tiempo estimado:** unos 15 minutos una vez disponible el entorno de ejecución. + +**Resultado final:** el SDK elegido inicia un worker y un workflow, e imprime +el ID del workflow, `status=completed` y `Hello, !`. + +**Requisitos:** + +- `curl` y una terminal. +- Docker para la ruta local autogestionada, o un espacio de nombres + provisionado en Durable Workflow Cloud. +- Las herramientas de un lenguaje: PHP 8.1+ con Composer, Python 3.10+ o Rust 1.86+. + +El modo servicio no requiere Laravel. La ruta de Laravel integrado aparece +por separado al final de esta guía. + +## 1. Elige tu entorno en modo servicio {#1-choose-your-service-mode-runtime} + +| Entorno | Cuándo elegirlo | Siguiente paso | +| --- | --- | --- | +| Durable Workflow Cloud | Quieres que Durable Workflow opere el entorno, la persistencia y Managed Waterline. | Sigue [Tu primer workflow en Cloud](/docs/polyglot/cloud-control-plane/#cloud-first-workflow), con programas completos de PHP, Python y Rust, credenciales provisionadas y un resultado `completed`. **No ejecutes Server ni un servicio Waterline separado.** | +| Server autogestionado | Quieres operar el entorno o realizar este ejercicio local con artefactos publicados. | Continúa con Docker y `curl`. Despliega Waterline por separado si necesitas su interfaz de operación. | + +Los ejemplos siguientes usan un Server local autogestionado, sin cuenta ni +descarga del código fuente. Cloud usa los mismos SDK y workers. Sustituye la +conexión local de desarrollo por los datos provisionados que aparecen en +[Entorno gestionado de Cloud](/docs/polyglot/cloud-control-plane/). + +## 2. Inicia el Server local {#2-start-the-local-server} + +Si elegiste Cloud, pasa al siguiente paso. Para la ruta autogestionada, +despliega y ejecuta la configuración fijada. Inicia Server con SQLite y un +token de desarrollo, sin descargar el código fuente. + +
+Iniciar la imagen fijada de Server + + +```bash +export DW_SERVER_IMAGE=%%artifact.serverDockerHubImage%% +export DW_AUTH_TOKEN=dev-token + +docker volume create durable-workflow-quickstart + +docker run --rm \ + -v durable-workflow-quickstart:/app/database \ + -e DW_AUTH_DRIVER=token \ + -e DW_AUTH_TOKEN="$DW_AUTH_TOKEN" \ + "$DW_SERVER_IMAGE" server-bootstrap + +docker rm -f durable-workflow-server >/dev/null 2>&1 || true +docker run -d --name durable-workflow-server \ + -p 8080:8080 \ + -v durable-workflow-quickstart:/app/database \ + -e DW_AUTH_DRIVER=token \ + -e DW_AUTH_TOKEN="$DW_AUTH_TOKEN" \ + "$DW_SERVER_IMAGE" + +until curl -sf http://localhost:8080/api/ready >/dev/null; do sleep 1; done +curl -H "Authorization: Bearer $DW_AUTH_TOKEN" \ + http://localhost:8080/api/cluster/info +``` + +
+ +**Resultado esperado:** la comprobación de disponibilidad responde +correctamente y la información del clúster identifica el Server local. +Déjalo en ejecución mientras completas la ruta de un lenguaje. + +## 3. Elige un lenguaje {#choose-one-language} + +Puedes elegir cualquiera de los tres SDK oficiales. Solo se muestra la +pestaña seleccionada para que sigas una ruta completa. + + + + +Requisitos: PHP 8.1 o posterior y Composer. Esta ruta usa +`durable-workflow/sdk`, independiente del framework. + +1. **Instala el SDK.** + + +```bash +mkdir durable-workflow-php-quickstart +cd durable-workflow-php-quickstart +composer require %%artifact.phpSdkComposerPackage%% +``` + +2. **Añade el worker y el cliente.** Abre el código completo y copia los + dos archivos en el proyecto nuevo. + +
+Código PHP completo y ejecutable + +El worker registra un tipo de workflow y un tipo de actividad en su propia +cola de tareas. + + +```bash +cat > worker.php <<'PHP' +registerActivity( + 'quickstart.greet', + static fn (ActivityContext $context, string $name): string => "Hello, {$name}!", +); + +$worker->registerWorkflow( + 'quickstart.greeter', + static function (WorkflowContext $context, string $name): array { + $greeting = $context->activity('quickstart.greet', [$name]); + + return ['greeting' => $greeting, 'language' => 'php']; + }, +); + +$worker->run(); +PHP +``` + +#### Cliente y lectura del resultado {#client-and-result-reader} + +El cliente inicia un workflow con un nombre único, espera a la ejecución +seleccionada y consulta el estado terminal duradero guardado por Server. + + +```bash +cat > start.php <<'PHP' +startWorkflow( + workflowType: 'quickstart.greeter', + workflowId: $workflowId, + taskQueue: 'quickstart-php', + input: ['PHP'], +); + +$result = $handle->result(timeoutSeconds: 30); +$execution = $handle->describeSelectedRun(); + +echo "workflow_id={$execution->workflowId}\n"; +echo "status={$execution->status}\n"; +echo 'result='.json_encode($result, JSON_THROW_ON_ERROR)."\n"; +PHP +``` + +
+ +3. **Ejecuta el worker y el cliente.** + + +```bash +php worker.php > quickstart-worker.log 2>&1 & +export QUICKSTART_WORKER_PID=$! +trap 'kill "$QUICKSTART_WORKER_PID" 2>/dev/null || true' EXIT + +php start.php + +kill "$QUICKSTART_WORKER_PID" 2>/dev/null || true +trap - EXIT +``` + +**Resultado esperado:** `status=completed` y un resultado con +`"greeting":"Hello, PHP!"`. Has ejecutado un worker PHP independiente y +consultado su resultado duradero sin Laravel. + +Continúa con la [Guía del SDK PHP](/docs/polyglot/php/). + +
+ + +Requisitos: Python 3.10 o posterior. El programa ejecuta el worker y el cliente +en un proceso. Ambos se comunican con Server mediante las API públicas de +workers y del plano de control. + +1. **Instala el SDK.** + + Usa la versión estable + del SDK Python del manifiesto. El requisito + exacto generado mantiene esta ruta en la versión estable documentada. + + +```bash +mkdir durable-workflow-python-quickstart +cd durable-workflow-python-quickstart + +python3 -m venv .venv +. .venv/bin/activate +pip install %%artifact.pythonPackagePin%% +``` + +2. **Crea y ejecuta el worker y el cliente.** Abre el programa completo. + Su último comando lo ejecuta. + +
+Código Python completo y ejecutable + + +```bash +cat > greeter.py <<'PY' +import asyncio +import time + +from durable_workflow import Client, Worker, activity, workflow + + +@activity.defn(name="quickstart.greet") +async def greet(name: str) -> dict: + return {"greeting": f"Hello, {name}!", "language": "python"} + + +@workflow.defn(name="quickstart.greeter") +class GreeterWorkflow: + def run(self, ctx, name): + return (yield ctx.schedule_activity("quickstart.greet", [name])) + + +async def main(): + workflow_id = f"quickstart-python-greeter-{int(time.time())}" + + async with Client( + "http://localhost:8080", + token="dev-token", + namespace="default", + ) as client: + handle = await client.start_workflow( + workflow_type="quickstart.greeter", + task_queue="quickstart-python", + workflow_id=workflow_id, + input=["Python"], + ) + + worker = Worker( + client, + task_queue="quickstart-python", + workflows=[GreeterWorkflow], + activities=[greet], + ) + await worker.run_until(workflow_id=workflow_id, timeout=30.0) + + result = await handle.result(timeout=10.0) + execution = await handle.describe_run() + + print(f"workflow_id={execution.workflow_id}") + print(f"status={execution.status}") + print(f"result={result}") + + +asyncio.run(main()) +PY + +python greeter.py +``` + +
+ +**Resultado esperado:** `status=completed` y un resultado con +`Hello, Python!`. Las dos últimas llamadas del SDK leen de Server el resultado +y el estado terminal duradero de la ejecución seleccionada. + +Continúa con la [Guía del SDK Python](/docs/polyglot/python/). + +
+ + +Requisitos: Rust 1.86 o posterior. Este ejemplo ejecuta un worker nativo y un +cliente en un proceso Tokio. + +1. **Instala el SDK.** + + +```bash +cargo new durable-workflow-rust-quickstart +cd durable-workflow-rust-quickstart +%%artifact.rustCargoAddCommand%% +cargo add tokio --features macros,rt-multi-thread,time +``` + +2. **Crea y ejecuta el worker y el cliente.** Abre el programa completo. + Su último comando lo compila y ejecuta. + +
+Código Rust completo y ejecutable + + +```bash +cat > src/main.rs <<'RS' +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use durable_workflow::{json, Client, Result, Worker, WorkflowResultOptions}; + +#[tokio::main] +async fn main() -> Result<()> { + let client = Client::builder("http://localhost:8080") + .token(Some("dev-token".to_string())) + .namespace("default") + .build()?; + let task_queue = "quickstart-rust"; + let mut worker = Worker::new(client.clone(), task_queue); + + worker.register_activity("quickstart.greet", |_context, arguments| async move { + let name = arguments + .get(0) + .and_then(|value| value.as_str()) + .unwrap_or("Rust"); + Ok(json!({"greeting": format!("Hello, {name}!"), "language": "rust"})) + }); + + worker.register_workflow("quickstart.greeter", |context, input| async move { + let name = input.get(0).and_then(|value| value.as_str()).unwrap_or("Rust"); + context.activity("quickstart.greet", json!([name])).await + }); + + worker.register().await?; + let workflow_id = format!("quickstart-rust-greeter-{}", unique_suffix()); + let handle = client + .start_workflow( + "quickstart.greeter", + task_queue, + &workflow_id, + json!(["Rust"]), + ) + .await?; + + let watcher = handle.clone(); + worker + .run_until(async move { + loop { + if watcher.describe().await.is_ok_and(|run| run.is_terminal()) { + break; + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + }) + .await?; + + let result = handle.result(WorkflowResultOptions::default()).await?; + let execution = handle.describe_selected_run().await?; + + println!("workflow_id={workflow_id}"); + println!("status={}", execution.status.as_deref().unwrap_or("unknown")); + println!("result={result}"); + Ok(()) +} + +fn unique_suffix() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() +} +RS + +cargo run +``` + +
+ +**Resultado esperado:** `status=completed` y un resultado JSON con +`"greeting":"Hello, Rust!"`. El ejemplo espera a que el worker termine la +ejecución y lee su estado duradero y el resultado decodificado. + +Continúa con la [Guía del SDK Rust](/docs/polyglot/rust/). + +
+
+ +## 4. Elimina los recursos del Server local {#4-clean-up-the-local-server} + +Cloud no necesita un Server local. Al terminar el ejercicio autogestionado: + +```bash +docker rm -f durable-workflow-server +docker volume rm durable-workflow-quickstart +``` + +## Ruta separada: Laravel integrado {#separate-path-embedded-laravel} + +Laravel integrado es un modelo de despliegue PHP para aplicaciones que quieren +mantener el estado de los workflows, las colas, la configuración y las +herramientas de operación dentro de su infraestructura Laravel. Instala +`durable-workflow/workflow`. Esta ruta no usa Server ni `durable-workflow/sdk`. + +Inicia una aplicación integrada nueva con el paquete publicado: + +```bash +composer create-project laravel/laravel durable-workflow-laravel-quickstart +cd durable-workflow-laravel-quickstart +composer require %%artifact.workflowComposerPackage%% +php artisan migrate +php artisan queue:work +``` + +Para añadir la interfaz de operación a esa misma aplicación, instala el +paquete Composer de Waterline integrado: + +```bash +composer require %%artifact.waterlineComposerPackage%% +php artisan waterline:install +``` + +Este paquete Composer corresponde al modo integrado. El servicio Waterline +autogestionado se despliega por separado. Laravel integrado no ejecuta Server +ni instala los SDK del modo servicio. + +Continúa con la [Instalación integrada](/docs/installation/) para configurar +una cola Laravel distinta de `sync`. Después, +[define](/docs/defining-workflows/workflows/) e +[inicia](/docs/defining-workflows/starting-workflows/) un workflow integrado. +[Modos de despliegue](/docs/polyglot/deployment-modes/) compara esta ruta con +la plataforma en modo servicio. + +## Siguientes pasos {#next-steps} + +- Consulta el [Índice de capacidades](/docs/capabilities/) para comprobar las + funciones del entorno y SDK elegidos. +- Sigue la guía del [SDK PHP](/docs/polyglot/php/), + [SDK Python](/docs/polyglot/python/) o [SDK Rust](/docs/polyglot/rust/). +- Compara el ciclo de vida, los mensajes, las programaciones, la visibilidad y + la ejecución en [Capacidades de clientes y workers](/docs/polyglot/cli-python-parity/). +- Opera el [Entorno gestionado de Cloud](/docs/polyglot/cloud-control-plane/) + o [Server autogestionado](/docs/polyglot/server/). Añade el + [CLI](/docs/polyglot/cli/) cuando necesites automatización en la terminal. +- Planifica el despliegue seguro con + [Compatibilidad y enrutamiento de workers](/docs/polyglot/worker-compatibility-routing/) + y [Despliegue por build ID](/docs/polyglot/worker-build-id-rollout/). + +Para las funciones de Laravel integrado, como temporizadores, señales, +consultas, actividades y workflows hijos, usa la +[Documentación del modo integrado](/docs/category/embedded/). + +La [Suite de conformidad de la plataforma](/docs/platform-conformance/) +documenta la validación de versiones: matriz exacta de artefactos, +comprobaciones del código público, registros completos, criterios de tiempo, +limpieza y contrato de inicio rápido legible por máquinas. diff --git a/i18n/es/docusaurus-theme-classic/footer.json b/i18n/es/docusaurus-theme-classic/footer.json new file mode 100644 index 0000000000..37cc3824d4 --- /dev/null +++ b/i18n/es/docusaurus-theme-classic/footer.json @@ -0,0 +1,10 @@ +{ + "link.title.Docs": {"message": "Documentación"}, + "link.title.Community": {"message": "Comunidad"}, + "link.title.More": {"message": "Más recursos"}, + "link.item.label.Introduction": {"message": "Introducción"}, + "link.item.label.Installation": {"message": "Instalación"}, + "link.item.label.Durable Workflow vs Temporal": {"message": "Durable Workflow y Temporal"}, + "link.item.label.LLM Docs": {"message": "Documentación para LLM"}, + "copyright": {"message": "Copyright © 2026 Durable Workflow."} +} diff --git a/i18n/es/docusaurus-theme-classic/navbar.json b/i18n/es/docusaurus-theme-classic/navbar.json new file mode 100644 index 0000000000..e0f74322f4 --- /dev/null +++ b/i18n/es/docusaurus-theme-classic/navbar.json @@ -0,0 +1,6 @@ +{ + "logo.alt": {"message": "Logotipo de Durable Workflow"}, + "item.label.Docs": {"message": "Documentación"}, + "item.label.Blog": {"message": "Blog (en inglés)"}, + "item.label.Star on GitHub": {"message": "Apoya el proyecto en GitHub"} +} diff --git a/i18n/es/source-hashes.json b/i18n/es/source-hashes.json new file mode 100644 index 0000000000..205ce704ec --- /dev/null +++ b/i18n/es/source-hashes.json @@ -0,0 +1,13 @@ +{ + "constraints/activity-constraints.md": "5a953d0e95e750b6c02d1c72b72199fb3ff4687540cebc56aa3b216cf667bb05", + "constraints/execution-guarantees.md": "e0903f1d6d7461191f2190453f9f96ce9bdcf6811e7e99c7d51f81e2f58ef27d", + "constraints/overview.md": "fd6c842c6c22d6f0df37a52de15ca74b34664f5ecd78af0c08b1e0cdb9a2be1f", + "constraints/workflow-constraints.md": "a69c8a118dc790a5d7b6c91806cb7ceca3015004a0fe549aee3094b09cbf17ad", + "defining-workflows/workflows.md": "a0cc26ae208ad3cee612f49e8db41324ee18f700fe67d9f533e48c700df5c386", + "failures-and-recovery.md": "0d2b8c71b7b54a0160c9bd631348a8223c0d05749f7224ec3ea5feaf74a0a3b5", + "how-it-works.md": "119a8aa26802a02aca05e94b80558a4b19b9ac86524ddec0d25416ea24ced688", + "installation.md": "1e2e04a8f23414d0e81c72b166700fd6350129ab81b2f22a6205856c23741967", + "introduction.md": "06bf96d164d402d4e376c6308b20f96d324110b2e0cf96c2a1a67b39906072e3", + "polyglot/deployment-modes.md": "ee38a2ca890e6ba5d52fffa1fa0bc65e64a1fc82e47af92463983e523d4e3f49", + "quickstart.md": "984de0b5ef14b403a283e597641d9b236e1f3ddb76d460aadaf936ac37be143d" +} diff --git a/i18n/uk/code.json b/i18n/uk/code.json index 39136bb1be..b0eccbccfe 100644 --- a/i18n/uk/code.json +++ b/i18n/uk/code.json @@ -1,4 +1,10 @@ { + "docs.translationFallback": { + "message": "Ця сторінка доступна англійською." + }, + "docs.readInEnglish": { + "message": "Читати англійською" + }, "theme.ErrorPageContent.title": { "message": "На сторінці стався збій.", "description": "The title of the fallback page when the page crashed" diff --git a/package.json b/package.json index caac5bdf54..2976e98007 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "postinstall": "patch-package", "prestart": "node scripts/check-dependency-security.js", "start": "docusaurus start", - "build": "npm run check:dependency-security && npm run check:node-toolchain && node scripts/check-local-search.test.js && docusaurus build && node scripts/generate-llms.js && node scripts/generate-llms-full.js", + "build": "npm run check:dependency-security && npm run check:node-toolchain && node scripts/check-local-search.test.js && node scripts/check-doc-translations.js && node scripts/check-localized-links.test.js && docusaurus build && node scripts/generate-llms.js && node scripts/generate-llms-full.js", "check:node-toolchain": "node scripts/check-node-toolchain.js && node scripts/check-node-toolchain.test.js", "check:dependency-security": "node scripts/check-dependency-security.js && node scripts/check-dependency-security.test.js", "check:platform-protocol-specs": "node scripts/check-platform-protocol-specs.js", diff --git a/patches/@docusaurus+utils+3.10.2.patch b/patches/@docusaurus+utils+3.10.2.patch new file mode 100644 index 0000000000..af65c324dd --- /dev/null +++ b/patches/@docusaurus+utils+3.10.2.patch @@ -0,0 +1,39 @@ +diff --git a/node_modules/@docusaurus/utils/lib/markdownLinks.js b/node_modules/@docusaurus/utils/lib/markdownLinks.js +index 9c571a2..d14cd2c 100644 +--- a/node_modules/@docusaurus/utils/lib/markdownLinks.js ++++ b/node_modules/@docusaurus/utils/lib/markdownLinks.js +@@ -28,7 +28,12 @@ function resolveMarkdownLinkPathname(linkPathname, context) { + // ./file.md and ../file.md are always resolved from + // - the current file dir + else if (linkPathname.startsWith('./') || linkPathname.startsWith('../')) { +- return [path_1.default.dirname(sourceFilePath)]; ++ const sourceDir = path_1.default.dirname(sourceFilePath); ++ const contentDirs = (0, dataFileUtils_1.getContentPathList)(contentPaths); ++ const sourceRoot = contentDirs.find((dir) => sourceDir === dir || sourceDir.startsWith(`${dir}${path_1.default.sep}`)); ++ return sourceRoot ++ ? [sourceDir, ...contentDirs.map((dir) => path_1.default.join(dir, path_1.default.relative(sourceRoot, sourceDir)))] ++ : [sourceDir]; + } + // file.md is resolved from + // - the current file dir, +diff --git a/node_modules/@docusaurus/utils/src/markdownLinks.ts b/node_modules/@docusaurus/utils/src/markdownLinks.ts +index 5a7ad51..80fac25 100644 +--- a/node_modules/@docusaurus/utils/src/markdownLinks.ts ++++ b/node_modules/@docusaurus/utils/src/markdownLinks.ts +@@ -74,7 +74,15 @@ export function resolveMarkdownLinkPathname( + // ./file.md and ../file.md are always resolved from + // - the current file dir + else if (linkPathname.startsWith('./') || linkPathname.startsWith('../')) { +- return [path.dirname(sourceFilePath)]; ++ const sourceDir = path.dirname(sourceFilePath); ++ const contentDirs = getContentPathList(contentPaths); ++ const sourceRoot = contentDirs.find( ++ (dir) => sourceDir === dir || sourceDir.startsWith(`${dir}${path.sep}`), ++ ); ++ return sourceRoot ++ ? [sourceDir, ...contentDirs.map((dir) => ++ path.join(dir, path.relative(sourceRoot, sourceDir)))] ++ : [sourceDir]; + } + // file.md is resolved from + // - the current file dir, diff --git a/scripts/check-doc-translations.js b/scripts/check-doc-translations.js new file mode 100644 index 0000000000..912984fb6c --- /dev/null +++ b/scripts/check-doc-translations.js @@ -0,0 +1,45 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const {createHash} = require('node:crypto'); + +const root = path.resolve(__dirname, '..'); +const translatedRoot = path.join(root, 'i18n/es/docusaurus-plugin-content-docs/current'); +const reviewFile = path.join(root, 'i18n/es/source-hashes.json'); +const currentHashes = {}; + +function codeBlocks(file) { + const text = fs.readFileSync(file, 'utf8'); + return [...text.matchAll(/^```[^\n]*\n[\s\S]*?^```\s*$/gm)].map(match => match[0].trimEnd()); +} + +let checked = 0; +function visit(directory) { + for (const entry of fs.readdirSync(directory, {withFileTypes: true})) { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) { + visit(file); + } else if (/\.mdx?$/.test(entry.name)) { + const relative = path.relative(translatedRoot, file); + const source = path.join(root, 'docs', relative); + assert.deepEqual(codeBlocks(file), codeBlocks(source), + `Spanish executable examples differ from English: ${relative}`); + currentHashes[relative] = createHash('sha256') + .update(fs.readFileSync(source)).digest('hex'); + checked += 1; + } + } +} + +visit(translatedRoot); +if (process.argv.includes('--record-review')) { + fs.writeFileSync(reviewFile, `${JSON.stringify(currentHashes, null, 2)}\n`); +} else { + const reviewedHashes = JSON.parse(fs.readFileSync(reviewFile, 'utf8')); + const changedSources = Object.keys(currentHashes) + .filter(file => currentHashes[file] !== reviewedHashes[file]); + if (changedSources.length) { + console.warn(`Spanish translation review needed: ${changedSources.join(', ')}. See README.md.`); + } +} +console.log(`Spanish code blocks match English in ${checked} guides.`); diff --git a/scripts/check-localized-links.test.js b/scripts/check-localized-links.test.js new file mode 100644 index 0000000000..83ac98e9d9 --- /dev/null +++ b/scripts/check-localized-links.test.js @@ -0,0 +1,38 @@ +const assert = require('node:assert/strict'); +const {resolveMarkdownLinkPathname} = require('@docusaurus/utils'); + +const siteDir = '/site'; +const localizedRoot = '/site/i18n/es/docusaurus-plugin-content-docs/current'; +const context = { + siteDir, + contentPaths: {contentPath: '/site/docs', contentPathLocalized: localizedRoot}, + sourceToPermalink: new Map([ + ['@site/docs/defining-workflows/workflow-api.md', '/es/docs/defining-workflows/workflow-api/'], + ['@site/i18n/es/docusaurus-plugin-content-docs/current/defining-workflows/workflows.md', '/es/docs/defining-workflows/workflows/'], + ['@site/docs/features/versioning.md', '/es/docs/features/versioning/'], + ['@site/docs/custom.md', '/es/docs/a-custom-slug/'], + ]), +}; + +function resolve(sourceFilePath, linkPathname, overrides = {}) { + return resolveMarkdownLinkPathname(linkPathname, {...context, ...overrides, sourceFilePath}); +} + +assert.equal(resolve(`${localizedRoot}/defining-workflows/workflows.md`, './workflow-api.md'), + '/es/docs/defining-workflows/workflow-api/'); +assert.equal(resolve('/site/docs/defining-workflows/workflow-api.md', './workflows.md'), + '/es/docs/defining-workflows/workflows/'); +assert.equal(resolve(`${localizedRoot}/constraints/workflow-constraints.md`, '../features/versioning.md'), + '/es/docs/features/versioning/'); +assert.equal(resolve(`${localizedRoot}/introduction.md`, './custom.md'), '/es/docs/a-custom-slug/'); +assert.equal(resolve(`${localizedRoot}/introduction.md`, './missing.md'), null); +assert.equal(resolve('/site/docs/introduction.md', './custom.md', { + contentPaths: {contentPath: '/site/docs', contentPathLocalized: undefined}, + sourceToPermalink: new Map([['@site/docs/custom.md', '/docs/a-custom-slug/']]), +}), '/docs/a-custom-slug/'); +assert.equal(resolve('/site/versioned_docs/version-1.x/introduction.md', './custom.md', { + contentPaths: {contentPath: '/site/versioned_docs/version-1.x', contentPathLocalized: undefined}, + sourceToPermalink: new Map([['@site/versioned_docs/version-1.x/custom.md', '/es/docs/1.x/custom/']]), +}), '/es/docs/1.x/custom/'); + +console.log('PASS seven relative-link cases, including both fallback directions, custom slugs, missing targets and version isolation.'); diff --git a/src/theme/DocItem/Content/index.js b/src/theme/DocItem/Content/index.js new file mode 100644 index 0000000000..f600fbcae1 --- /dev/null +++ b/src/theme/DocItem/Content/index.js @@ -0,0 +1,32 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import Translate from '@docusaurus/Translate'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {useDoc} from '@docusaurus/plugin-content-docs/client'; +import DocItemContent from '@theme-original/DocItem/Content'; + +export default function DocItemContentWrapper(props) { + const {i18n: {currentLocale, defaultLocale}} = useDocusaurusContext(); + const {metadata} = useDoc(); + const usesEnglishFallback = currentLocale !== defaultLocale && + !metadata.source.startsWith(`@site/i18n/${currentLocale}/`); + const englishPath = metadata.permalink.replace(`/${currentLocale}/`, '/').replace(/\/?$/, '/'); + + return ( + <> + {usesEnglishFallback && ( + + )} +
+ +
+ + ); +} diff --git a/src/theme/DocItem/Metadata/index.js b/src/theme/DocItem/Metadata/index.js index f3909830d4..84a42a38be 100644 --- a/src/theme/DocItem/Metadata/index.js +++ b/src/theme/DocItem/Metadata/index.js @@ -25,7 +25,12 @@ function SupplementalMetadata({canonicalPath, title, description}) { export default function DocItemMetadata() { const {metadata, frontMatter, assets} = useDoc(); - const canonicalPath = frontMatter.canonical_path; + const {i18n: {currentLocale, defaultLocale}} = useDocusaurusContext(); + const usesEnglishFallback = currentLocale !== defaultLocale && + !metadata.source.startsWith(`@site/i18n/${currentLocale}/`); + const canonicalPath = frontMatter.canonical_path || (usesEnglishFallback + ? metadata.permalink.replace(`/${currentLocale}/`, '/').replace(/\/?$/, '/') + : undefined); return ( <> diff --git a/src/theme/Footer/index.js b/src/theme/Footer/index.js index 6a0b193cd8..f3b25d524b 100644 --- a/src/theme/Footer/index.js +++ b/src/theme/Footer/index.js @@ -1,9 +1,11 @@ import React, { useEffect, useRef } from 'react'; import Footer from '@theme-original/Footer'; import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; export default function FooterWrapper(props) { const footerRef = useRef(null); + const {i18n: {currentLocale, defaultLocale}} = useDocusaurusContext(); useEffect(() => { if (!ExecutionEnvironment.canUseDOM || !footerRef.current) { @@ -13,13 +15,16 @@ export default function FooterWrapper(props) { // Canonical /llms-full.txt tracks the stable unversioned docs line. // Only the explicit prerelease docs path should switch to the 2.0 bundle. const pathname = window.location.pathname; - const isV2Docs = /^\/(?:uk\/)?docs\//.test(pathname); + const docsPrefix = currentLocale === defaultLocale + ? '/docs/' + : `/${currentLocale}/docs/`; + const isV2Docs = pathname.startsWith(docsPrefix); const llmDocsLink = footerRef.current.querySelector('a[href*="llms-full.txt"]'); if (llmDocsLink && isV2Docs) { llmDocsLink.setAttribute('href', 'https://durable-workflow.com/llms-full-2.0.txt'); } - }, []); + }, [currentLocale, defaultLocale]); return (