Este artículo detalla paso a paso cómo ejecutar tareas largas en lotes en WordPress utilizando la API REST y JavaScript en el cliente. Se explica la estrategia más segura y escalable para evitar timeouts, controlar el progreso, manejar errores y, si procede, combinar con mecanismos de ejecución en segundo plano (Action Scheduler o colas). Incluye ejemplos completos de código PHP y JavaScript listos para integrar.
Contents
Resumen del problema y enfoque
Las tareas largas (por ejemplo: reindexar miles de posts, regenerar thumbnails, procesar CSVs masivos, sincronizar datos externos) suelen exceder el tiempo máximo de ejecución de PHP y agotar memoria. Intentar ejecutarlas en una sola petición HTTP provoca timeouts y estados intermedios inconsistentes.
La solución más práctica cuando se interactúa desde una interfaz web es partir el trabajo en lotes pequeños (chunks) y orquestar la ejecución con llamadas repetidas a un endpoint REST que procese cada lote. El cliente (JavaScript) lanza solicitudes encadenadas o concurrentes controladas, muestra progreso y reintentos, y el servidor procesa cada lote en poco tiempo para evitar timeouts. Para operaciones más pesadas o asíncronas se puede delegar en una cola o Action Scheduler.
Estrategia recomendada
- Enfoque principal: endpoint REST que procesa un lote por petición cliente JS orquesta llamadas repetitivas hasta completar.
- Alternativa para tareas muy pesadas: encolar por lote en Action Scheduler o una cola externa y procesar asíncronamente.
- Ventajas del enfoque REST JS: simple, evita timeouts, permite feedback en tiempo real y control de reintentos.
Implementación paso a paso: enfoque REST JS (cliente iterativo)
- Crear endpoint REST que reciba parámetros: tamaño de lote (batch_size), offset/cursor o lista de IDs.
- En el servidor procesar como máximo N elementos por llamada, devolver estado parcial y un cursor para la siguiente llamada.
- Control de concurrencia/locking para evitar ejecuciones paralelas conflictivas.
- Cliente JS que lance llamadas repetidas hasta que el servidor indique que todo está procesado mostrar progreso, manejar errores y backoff.
- Opcional: persistir progreso en base de datos/transients para reanudar si el cliente se desconecta.
1) Registrar endpoint REST y procesar un lote (PHP)
Ejemplo básico que procesa lotes de IDs de posts. Guarda un cursor en opción para persistencia y usa un bloqueo simple para evitar ejecuciones concurrentes.
// register-rest-route.php add_action(rest_api_init, function() { register_rest_route(mi-plugin/v1, /procesar-lote, array( methods => POST, callback => mi_plugin_procesar_lote, permission_callback => function() { return current_user_can(edit_posts) // ajustar según necesidad } )) }) function mi_plugin_procesar_lote(WP_REST_Request request) { // Parámetros: batch_size, cursor (opcional) params = request->get_json_params() batch_size = isset(params[batch_size]) ? intval(params[batch_size]) : 50 cursor = isset(params[cursor]) ? sanitize_text_field(params[cursor]) : null // Lock simple para evitar concurrencia (5 minutos) if (! wp_cache_add(mi_plugin_task_lock, 1, mi_plugin, 300) ) { return new WP_REST_Response(array( success => false, message => Task locked by another process ), 423) } try { // Ejemplo: obtener IDs de posts pendientes usando cursor por offset args = array( post_type => post, posts_per_page => batch_size, fields => ids, orderby => ID, order => ASC, no_found_rows => true, ) if (cursor) { // cursor aquí es el último ID procesado obtenemos posts con ID mayores args[meta_query] = array( array( key => _dummy, // simplificar: si se usa cursor por paged, ajustar ) ) // Alternativa: usar offset paged o filtrar por date_query o por ID via post__not_in } query = new WP_Query(args) ids = query->posts processed = 0 last_id = cursor foreach (ids as id) { // Aquí iría la lógica concreta para procesar cada item: // ejemplo: actualizar un meta, regenerar un thumbnail, etc. // Asegurarse de que la operación por item sea rápida y idempotente. update_post_meta(id, _mi_plugin_procesado, current_time(mysql)) processed last_id = id } // Comprobar si quedan más items done = (count(ids) < batch_size) // Guardar cursor si se desea persistencia update_option(mi_plugin_last_cursor, done ? : last_id, false) wp_cache_delete(mi_plugin_task_lock, mi_plugin) return rest_ensure_response(array( success => true, processed => processed, done => done, next_cursor => done ? null : last_id, )) } catch (Exception e) { wp_cache_delete(mi_plugin_task_lock, mi_plugin) return new WP_REST_Response(array( success => false, message => e->getMessage() ), 500) } }
Notas sobre el ejemplo PHP:
- El locking usa wp_cache_add con un grupo propio para infraestructuras con múltiples servidores usar un sistema de cache compartida (Redis, Memcached) o una tabla de bloqueo en la DB.
- Usar fields => ids y no_found_rows => true para consultas más rápidas.
- La estrategia de cursor puede ser por offset/paged, por último ID procesado o por una tabla/columna de estado. Evitar offset grandes en consultas muy grandes en su lugar usar filtrado por ID o un índice que permita recorrer en orden.
2) Frontend JavaScript: llamada iterativa con fetch
El cliente lanza peticiones encadenadas hasta que el servidor indique que todo está procesado. Usar X-WP-Nonce para peticiones autenticadas desde el administrador.
// frontend.js async function procesarLotes({batchSize = 50, concurrency = 1} = {}) { const url = /wp-json/mi-plugin/v1/procesar-lote let cursor = null let done = false let processedTotal = 0 // Cola simple para concurrencia limitada const queue = [] for (let i = 0 i < concurrency i ) queue.push(Promise.resolve()) while (!done) { // Esperar a que haya slot libre en la cola await Promise.race(queue) const slotIndex = queue.findIndex(p => p p.isFulfilled) // si se usa librería Promise que marca isFulfilled // Simplificar: esperaremos la primera Promise en resolverse // Lanzamos una petición const body = { batch_size: batchSize } if (cursor) body.cursor = cursor const fetchPromise = fetch(url, { method: POST, headers: { Content-Type: application/json, X-WP-Nonce: window.wpApiSettings ? window.wpApiSettings.nonce : }, credentials: same-origin, body: JSON.stringify(body) }).then(r => r.json()).then(data => { if (!data.success data.status === 423) { // locked: esperar y reintentar con backoff return new Promise((resolve) => setTimeout(resolve, 2000)).then(() => ({ retry: true })) } if (!data.success) throw new Error(data.message Error en lote) processedTotal = data.processed 0 cursor = data.next_cursor done = !!data.done // Actualizar UI (barra de progreso) aquí // updateProgress(processedTotal, done) return data }).catch(err => { console.error(Error procesando lote:, err) // Reintento simple con delay return new Promise((resolve) => setTimeout(resolve, 3000)).then(() => ({ retry: true })) }) // Reemplazar la primera promesa de la cola por la nueva queue.push(fetchPromise) // Mantener la cola con un tamaño máximo = concurrency if (queue.length > concurrency) queue.shift() // Si el fetch indicó que hay que reintentar (por lock o error), no avanzar el cursor y seguir el bucle const res = await fetchPromise if (res res.retry) { // small backoff await new Promise(resolve => setTimeout(resolve, 1000)) } } // Final // mostrar resultado final // showDone(processedTotal) }
Notas sobre JS:
- El ejemplo muestra un patrón básico. Para producción conviene usar una cola real en JS (Promise pool) y evitar race conditions en la UI.
- Incluir indicadores claros: porcentaje, elementos procesados, tiempo estimado restante.
- Proveer botón de cancelar que invoque un endpoint para marcar la tarea como cancelada.
3) Ejemplo: encolar lotes con Action Scheduler (alternativa)
Si el procesamiento por lote sigue siendo demasiado costoso (ej. llamadas externas lentas, regeneraciones de imágenes), en vez de procesar directamente desde el request REST puedes encolar cada lote con Action Scheduler y dejar que los workers lo ejecuten en segundo plano.
// encolar-lotes.php function mi_plugin_encolar_lotes(items, batch_size = 50) { if (! function_exists(as_schedule_single_action)) { // Action Scheduler no está disponible return new WP_Error(no_action_scheduler, Action Scheduler no disponible) } // Particionar items en batches chunks = array_chunk(items, batch_size) foreach (chunks as index => chunk) { hook = mi_plugin_procesar_chunk args = array(ids => chunk) // Programar con pequeño offset para distribuir carga as_schedule_single_action(time() (index 5), hook, args, mi_plugin_group) } } // Handler para Action Scheduler add_action(mi_plugin_procesar_chunk, function(args) { ids = args[ids] foreach (ids as id) { // Procesar cada ID (debe ser idempotente) } })
Ventajas de Action Scheduler:
- Robustez en procesamiento asíncrono, reintentos automáticos y logging.
- Indicado si el proceso puede tardar mucho por item o necesita reintentos automáticos.
Buenas prácticas y consideraciones
- Idempotencia: las operaciones deben ser idempotentes o tener marcado de estado por item para evitar duplicados en reintentos.
- Seguridad: usar permission_callback adecuado y X-WP-Nonce para peticiones desde el panel. Validar y sanear todos los parámetros entrantes.
- Locking: implementar bloqueo cuando sea crítico evitar ejecución concurrente usar cache compartida en infra multi-servidor.
- Tamaño del lote: elegir un batch_size que mantenga cada petición por debajo del límite de tiempo y memoria. Probar con valores (50, 100, 200) según la operación.
- Consultas eficientes: usar fields=>ids, no_found_rows=>true, evitar ORDER BY costoso sin índice, y paginar correctamente.
- Persistencia de estado: almacenar cursor en option o en custom table para reanudar si el cliente se desconecta.
- Monitoreo: registrar métricas: tiempo por item, fallos, reintentos ofrecer página admin para seguimiento.
- Backoff y límites: en caso de locks o errores temporales aplicar backoff exponencial y límites de reintentos para evitar bucles infinitos.
Manejo de errores y reintentos
- Definir códigos de error claros en respuestas REST para distinguir lock (423), errores temporales (5xx) y errores irreparables (4xx).
- Implementar contador de reintentos por item o por lote para evitar ciclos infinitos.
- Registrar fallos en la DB para análisis posterior y re-procesado manual si es necesario.
Optimización para grandes volúmenes
- Evitar OFFSET cuando la tabla es muy grande usar cursor por ID o por clave indexada.
- Si se procesan ficheros grandes, usar multipart upload o procesar el fichero en chunks y almacenar piezas temporales en filesystem o S3.
- Si se hospeda en clúster, almacenar estado en base de datos central o Redis para coordinar workers.
Monitoreo y UX
- Mostrar progreso en la UI con barras y logs parciales.
- Permitir cancelar y reanudar la tarea.
- Enviar notificación (email/admin notice) al completar o en fallos críticos.
Recursos útiles
Resumen final
La forma más segura y práctica de ejecutar tareas largas desde un entorno web en WordPress es fragmentarlas en lotes y procesarlas mediante un endpoint REST que gestiona un lote por petición, orquestado por JavaScript en el cliente. Para trabajos más pesados o de larga duración, delegar la ejecución en Action Scheduler o un sistema de colas es la opción más robusta. Aplicando locking, idempotencia, control de tamaño de lote y buenas prácticas de consultas se consigue un proceso fiable y escalable.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |