Cron jobs
Las tareas programadas están en Google Cloud Scheduler (project api-cards-prod, region us-central1). Cada job hace POST a un endpoint del Cloud Run autenticado con X-Internal-Cron-Secret.
Panel y comandos
Sección titulada «Panel y comandos»| Panel | https://console.cloud.google.com/cloudscheduler?project=api-cards-prod |
| Listar | gcloud scheduler jobs list --project=api-cards-prod --location=us-central1 |
| Pausar uno | gcloud scheduler jobs pause <name> --project=... --location=... |
| Forzar ejecución manual | gcloud scheduler jobs run <name> --project=... --location=... |
Staging
Sección titulada «Staging»Staging tiene un solo job propio: orders-awaiting-payment-cleanup-staging
(*/5 * * * *, mismo endpoint pero del servicio staging, creado 2026-06-12).
Sin él, las órdenes awaiting_payment de staging nunca expiraban y quedaban
“Esperando pago” para siempre. El resto de los jobs (orders-tick, reconcile,
backup) no existen en staging — sus efectos hay que dispararlos a mano
contra el endpoint correspondiente con el INTERNAL_CRON_SECRET de staging
(Secret Manager → internal-cron-secret-staging).
Crons en GitHub Actions (no Cloud Scheduler)
Sección titulada «Crons en GitHub Actions (no Cloud Scheduler)»Sync diario del catálogo
Sección titulada «Sync diario del catálogo»| Workflow | tcgcards-api/.github/workflows/sync-incremental.yml |
| Frecuencia | 17 9 * * * — al minuto 17 y no al 0 a propósito: al minuto cero GitHub encola las tareas programadas de media plataforma y esta se atrasaba entre una y dos horas |
| Qué hace | npm run sync:incremental: recorre todas las fuentes del registry (8 TCG vía TCGplayer + Mitos y Leyendas vía api.myl.cl) y actualiza sets/cartas según el cooldown por antigüedad. Para Mitos también espeja imágenes nuevas a R2. Los sets nuevos se publican atómicos (edición completa o nada). |
| Forzar | Botón Run workflow en GitHub Actions, o local: MYL_SYNC=true npm run sync:incremental -- --tcg=mitos (con envs R2 para imágenes) |
Arte de los sets de la portada
Sección titulada «Arte de los sets de la portada»| Workflow | tcgcards-api/.github/workflows/mirror-set-art.yml |
| Frecuencia | Encadenado al sync (workflow_run sobre Sync Catalog (incremental)), no a una hora fija. Red de seguridad por horario a las 23 12 * * * por si GitHub descarta la corrida del sync. Darle “una hora más tarde” NO servía: las tareas programadas de GitHub se atrasan entre una y dos horas, así que el sync podía terminar después |
| Qué hace | npm run mirror:set-art: primero reconcilia cardsCount de todos los sets (ver abajo), y después, de los 24 sets más recientes, prueba hasta 10 cartas candidatas hasta juntar 3 que sirvan, verifica que las imágenes existan de verdad y las espeja en R2 en tres piezas — carta de 280px para el abanico del destacado, franja 16:10 de 440px para la baldosa, y miniatura de 64px que el destacado dibuja desenfocada de fondo. Escribe showcaseImages, showcaseTile, showcaseBackdrop, showcaseCheckedAt y showcaseCardsCount en sets. |
| Forzar | Botón Run workflow, con opciones dry_run y force |
| Idempotente | Sí — un set con arte y sin cambios se salta sin descargar nada |
Qué rechaza (y por qué importa): el CDN de TCGplayer no avisa con errores limpios. Una imagen inexistente responde 403 con application/xml, y un producto sin arte devuelve un 200 perfectamente válido con el cartel «Image Coming Soon», que se reconoce porque mide exactamente 1000×573 — ninguna carta real mide eso. Lo que no verifica no se guarda: el set muestra el logo de su juego, nunca una imagen rota.
Por qué prueba 10 candidatas y no 3 (ago 2026): elige por precio de mercado descendente, y TCGplayer bloquea justo las imágenes de las cartas más caras. En «Vendetta» de Riftbound las tres más valiosas son “Signature” y las tres responden 403, así que el set se quedó con el logo teniendo 245 cartas con imagen buena — la cuarta servía perfecto. Ahora se baja por la lista hasta juntar tres, y el bucle corta apenas las junta: un set sano sigue descargando exactamente tres imágenes.
Por qué reconcilia el conteo acá y no solo de noche (ago 2026): este proceso está encadenado al sync, que es justo el momento en que puede nacer un set huérfano — TCGplayer le cambia el slug a un set, nace un documento nuevo, las cartas se mudan y el viejo queda con su cardsCount congelado. El reconciliador nocturno es un Cloud Run Job con su propio horario, en otro sistema, y no se puede encadenar a un workflow de GitHub: hoy corre antes del sync, así que un huérfano viviría casi un día entero y podía llegar a ser el destacado de la portada llevando a una página vacía. Corregirlo acá cierra la ventana. La familia nocturna se queda como red; es idempotente.
Cómo un set pasa de logo a carta, solo: se reintenta cualquier set que no tenga sus 3 imágenes, así que el arte aparece al día siguiente de existir arriba. Y se guarda showcaseCardsCount: mientras la cantidad de cartas del set siga cambiando, se vuelve a elegir — un set entra a la portada con 10 cartas cargadas y termina con 200, y las tres mejores de diez casi nunca son las representativas. Cuando deja de crecer, deja de tocarse solo.
Va en GitHub y no en Cloud Scheduler por dos razones: las credenciales de R2 viven como secretos de GitHub (no hay ninguna en Secret Manager), y los Cloud Run Jobs fijan la imagen, así que cada despliegue que tocara este código obligaría a actualizarla a mano o el proceso quedaría corriendo código viejo en silencio.
Sin alerta de heartbeat, a diferencia de auto-price y refresh-prices: si se detiene, el arte se congela y los sets nuevos muestran su logo. Molesto, no grave.
Jobs activos
Sección titulada «Jobs activos»mongo-backup-hourly
Sección titulada «mongo-backup-hourly»| Frecuencia | 0 * * * * (cada hora) |
| Qué hace | Gatilla el Cloud Run Job mongo-backup: corre mongodump --archive --gzip y sube el dump a gs://tcgcards-mongo-backups/ (retención 30 días) |
| Código | tcgcards-api/scripts/backup-mongo.sh + Dockerfile.backup |
| RPO efectivo | ~1 hora |
| Runbook | Ver runbook restore |
Es una segunda capa independiente de los snapshots diarios nativos de Atlas (esos los gestiona Atlas; ver infra). El dump en GCS vive fuera de Atlas, así que sobrevive aunque pase algo con la cuenta/cluster.
refresh-prices-daily
Sección titulada «refresh-prices-daily»Refresca los precios de referencia de TCGplayer (cards.marketPrices, en USD) para mantenerlos frescos (≤24h). Mismo patrón que el backup: el Scheduler gatilla un Cloud Run Job, no un endpoint HTTP.
| Frecuencia | 0 4 * * * (diario 4 AM Chile, America/Santiago) |
| Qué hace | Gatilla el Cloud Run Job refresh-prices en modo backfill: recorre los 1.526 sets de TCGplayer (oldest-first por sets.pricesRefreshedAt), baja precios de mp-search-api.tcgplayer.com (50/página, una petición cada 300 ms) y hace $set de marketPrices en las cartas (un bulkWrite por set). ~1h13, con un límite duro de 3 h (timeoutSeconds: 10800). |
| Código | tcgcards-api/src/jobs/refresh-prices.ts + src/services/PriceRefreshEngine.ts |
| Costo | Es el mayor consumidor de Cloud Run del proyecto. A 1 req/s tardaba 2h13 y se llevaba el 64% (~225.000 vCPU-s/mes, contra 91.000 del API de producción). A 300 ms baja a ~120.000. El tramo gratuito son 180.000 vCPU-s/mes compartidos por toda la cuenta. |
| SA / auth | OAuth con la compute SA (...-compute@, con roles/run.invoker sobre el job) |
| Idempotente | Sí — re-correr solo refresca de nuevo (escritura quirúrgica) |
Seguridad (write a ~200k docs): escribe SOLO marketPrices vía bulkWrite ($set, sin upsert → no inserta, sin deletes, skip-null → no pisa un precio bueno con null), filtra source=tcgplayer → Mitos intocable. Validado en staging (cero inserts, sin corrupción) y dry-run contra prod.
Correr a mano / cambiar modo:
# ejecutar yagcloud run jobs execute refresh-prices --project=api-cards-prod --region=us-central1# cambiar modo (backfill = todo; sample = 1/TCG; --dry-run = no escribe; page-size MÁX 50)gcloud run jobs update refresh-prices --project=api-cards-prod --region=us-central1 \ --args "dist/jobs/refresh-prices.js,--mode=backfill,--page-size=50,--min-interval-ms=300"OJO
pageSize≤ 50: TCGplayer rechazasize>50con HTTP 400.
El ritmo contra TCGplayer (--min-interval-ms)
Sección titulada «El ritmo contra TCGplayer (--min-interval-ms)»El intervalo entre peticiones es nuestro, no un límite de TCGplayer. Vivía fijo en 1 s dentro del código; desde 2026-08-15 se configura por argumento, así que subirlo o bajarlo es este jobs update: no hay que recompilar ni desplegar.
Medido contra su buscador (2026-08-15): responde en ~550 ms, y aguantó ráfagas a 2/s, 4/s y 8/s sin un solo rechazo. Como el recorrido es secuencial, por debajo de ~550 ms bajar el número ya no acelera — el cuello pasa a ser su tiempo de respuesta. Pedir 300 ms da ~1,8 peticiones/s reales, y ahí se acaba la ganancia fácil: para más habría que pedir en paralelo, que es otro cambio.
Piso de 200 ms, por si alguien escribe --min-interval-ms=3 en vez de =300. Como por debajo de ~550 ms no se gana velocidad, el piso no cuesta nada.
Cómo saber si te pasaste. La línea refresh-prices finished del log trae:
minIntervalMs: 300tcgplayer: { throttled: 0, transient: 0 }throttled cuenta los HTTP 429 — TCGplayer diciéndonos que vamos muy rápido. transient cuenta 502/503/504, que son ruido de pasarela y no dicen nada sobre el ritmo. Si throttled sube de 0 sale además un warn aparte, y toca volver a subir el intervalo.
Sets huérfanos: por qué el job se puso lento (jul 2026)
Sección titulada «Sets huérfanos: por qué el job se puso lento (jul 2026)»TCGplayer renombra sets cada tanto (Sandstorm → EX Sandstorm, Crown Zenith → SWSH: Crown Zenith). Cuando lo hace, el sync crea el nombre nuevo e importa sus cartas —que se mudan, porque el id de carta es tcg-productId— y el set viejo queda en la base para siempre: SyncEngine nunca borra lo que desaparece de la lista remota.
El problema es lo que hace TCGplayer con ese nombre viejo:
| Filtro enviado | Resultados |
|---|---|
setName=sandstorm | 29.629 |
setName=ex-sandstorm | 100 ✅ |
| sin filtro de set | 29.629 |
No devuelve un error: descarta el filtro y manda el juego completo. El job entonces pagina ~198 veces (tope 9.900 ÷ 50) por un set del que no actualiza nada.
Pasó dos veces en silencio —1 may (2 huérfanos) y 25 jul (15 más)—. Con 20 huérfanos el trabajo casi se duplicaba (≈3.960 peticiones inútiles contra ≈4.600 reales), el job cruzó su límite de 3 h y el 30 y 31 de julio lo mataron antes de registrar su latido: por eso saltó la alerta.
Guard (desde 2026-07-31): si la primera página trae productos de varios sets distintos, el filtro fue ignorado → el set se omite tras 1 petición. Se cuenta como setsSkippedUnrecognized en el log de cierre, no como fallo: el set no se marca como refrescado (sería mentir) pero tampoco alimenta el circuit breaker, porque veinte huérfanos abortarían la corrida de los ~1.500 sanos.
Limpiar los que ya existan: scripts/purge-orphan-tcgplayer-sets.ts. Sin --confirm no escribe nada; exige cuatro condiciones a la vez —una verificada en vivo contra TCGplayer—, guarda copia de los documentos en backups/ y comprueba que el total de cartas no cambie.
Histórico: este refresco vivió un tiempo como workflow de GitHub Actions (
refresh-prices.yml), pero se movió a Cloud Run Job porque en repo privado GH Actions costaba ~$5-13/mes y se estimó que en Cloud Run cabría gratis. Esa estimación no se cumplió (ver la fila «Costo» arriba): igual sigue saliendo más barato que GH Actions, pero no es gratis. No reactivar ese workflow (sería doble refresco). Ver decisiones.
auto-price-daily
Sección titulada «auto-price-daily»Recalcula los precios de las publicaciones de los vendedores con Precios dinámicos activos (la marca visible de lo que internamente se llama autoPrice/autoPricing). Es opt-in: el vendedor elige que sus cartas de TCGplayer se repricen solas cada día. Mismo patrón que el backup y refresh-prices: el Scheduler gatilla un Cloud Run Job, no un endpoint HTTP. Creado 2026-06-30.
| Frecuencia | 0 8 * * * (diario 8 AM Chile, America/Santiago) |
| Qué hace | Gatilla el Cloud Run Job auto-price: recalcula precio_CLP = round100(market_USD × usdRate) para las publicaciones de los vendedores que activaron Precios dinámicos (cada vendedor con su propio usdRate). Corre después de refresh-prices (4 AM) a propósito, para trabajar con las referencias de TCGplayer ya frescas del día. Con 0 vendedores activados es no-op. |
| Código | tcgcards-api/src/jobs/auto-price.ts + src/jobs/auto-price.run.ts + src/listings/autoPricePlan.ts |
| Costo | $0 (cabe en el free tier de Cloud Run) |
| SA / auth | OAuth con la compute SA (1033181994095-compute@developer.gserviceaccount.com, con roles/run.invoker sobre el job auto-price) |
| Idempotente | Sí — escritura quirúrgica con guard CAS {status:active, autoPrice:true} (re-correr re-calcula sobre las mismas referencias) |
Seguridad (no toca el flujo de pago): solo cartas de catálogo de TCGplayer (card.source == "tcgplayer") → Mitos (api.myl.cl), sellado, accesorios y cartas custom nunca se repricen. Gate de frescura 26h: el set (sets.pricesRefreshedAt) y la carta (cards.marketPrices.pricesUpdatedAt) deben estar frescos; si no, se salta (no_fresh_price) y no se pisa un precio con una referencia rancia. El precio nuevo es round100 (redondeo half-up al múltiplo de $100 más cercano, aritmética entera) y respeta el mínimo de publicación $50. La escritura usa guard CAS {status:active, autoPrice:true}, así que no pisa una venta o edición manual ocurrida entre la lectura y la escritura. No toca el flujo de pago: las órdenes congelan el precio al crearse.
Heartbeat: cada corrida loguea auto-price run OK (con candidates/applied); esa línea es la señal de vida del job (si no aparece en 26-48h, el Scheduler murió y los precios quedaron congelados).
Correr a mano / flags:
# ejecutar yagcloud run jobs execute auto-price --project=api-cards-prod --region=us-central1# flags opcionales del entrypoint: --dry-run (no escribe), --seller=<id> (acota a un vendedor)gcloud run jobs update auto-price --project=api-cards-prod --region=us-central1 \ --args "dist/jobs/auto-price.js,--dry-run"Misma imagen que el API: el Job
auto-pricecorre la misma imagen de prod del serviciotcgcards-api(que ya traedist/jobs/auto-price.js), así que se actualiza solo cuando se redespliega el API.
OJO permisos: el Scheduler necesita que la compute SA (
1033181994095-compute@developer.gserviceaccount.com) tengaroles/run.invokersobre el jobauto-price(gcloud run jobs add-iam-policy-binding auto-price --member=... --role=roles/run.invoker), si no falla conPERMISSION_DENIED.
reconcile-denorm-daily
Sección titulada «reconcile-denorm-daily»Diario 05:00 America/Santiago (antes de refresh-prices y auto-price). Corre el Cloud Run Job reconcile-denorm (npm run reconcile-denorm): audita y corrige las CUATRO familias de datos denormalizados — (a) resúmenes de stock en cards vs sus listings reales, (b) las 7 copias denormalizadas en listings vs su carta, (c) soldCount/lastSoldAt de listings vs las órdenes (contador incremental: un desvío no se autocorrige solo — jul 2026), (d) cardsCount de cada set vs sus cartas reales (ago 2026 — ver abajo). Noches sanas = 0 escrituras; si corrige >0 emite un logger.warn con conteos → investigar la causa (un hook que no disparó, una corrección del sync upstream). Idempotente; --dry-run disponible para auditoría manual.
# ejecutar yagcloud run jobs execute reconcile-denorm --project api-cards-prod --region us-central1 --wait# ver últimas ejecucionesgcloud run jobs executions list --job reconcile-denorm --project api-cards-prod --region us-central1Familia (d), cardsCount de los sets (ago 2026): el slug de un set de TCGplayer es su urlValue y el _id se arma con él, así que cuando TCGplayer renombra un set nace un documento NUEVO, las cartas se mudan al nuevo y el viejo queda huérfano con su conteo congelado — el sync no lo corrige jamás porque solo visita los sets que TCGplayer lista hoy. Eso puso un set con cero cartas como destacado de «Nuevos lanzamientos», duplicando al set real y llevando a una página vacía. Con el conteo verdadero el huérfano queda en 0 y desaparece solo de la portada y del catálogo. No borra nada: el documento queda por si TCGplayer vuelve al slug anterior. Primera corrida en prod (ago 2026): 1.642 sets revisados, 93 conteos corregidos, 2 huérfanos.
Heartbeat: loguea reconcile-denorm run OK SOLO en corrida real exitosa → métrica reconcile_denorm_heartbeat + política dead-man’s switch (~30h) al correo (patrón clonado de auto-price). El dry-run loguea otro mensaje y NO late.
⚠️ GOTCHA de imágenes de jobs (aplica a TODOS los Cloud Run Jobs): los jobs fijan su imagen al crearse — el deploy del servicio NO los actualiza. Si un deploy cambia lógica que un job comparte (p. ej. los hooks de recálculo que usa auto-price), hay que actualizar la imagen del job: gcloud run jobs update <job> --image <imagen-del-deploy>. Descubierto en el rollout de perf-escala (2026-07-03).
orders-tick
Sección titulada «orders-tick»| Frecuencia | 0 * * * * (cada hora) |
| Endpoint | POST /api/v1/internal/cron/orders-tick |
| Código | src/cron/orderTickService.ts |
| Qué hace | 4 sub-tareas (ver abajo) |
| Idempotente | Sí — re-procesar es seguro |
Sub-tareas que ejecuta:
| Sub-tarea | Trigger | Acción |
|---|---|---|
processPending | Orden creada hace >72h, vendedor no aceptó | Auto-cancela + restaura stock + mail |
processAccepted | Orden aceptada hace >168h (7 días), no se envió | Auto-cancela + restaura stock + mail |
processShipped | Orden enviada hace >336h (14 días), buyer no confirmó | Auto-completa (libera pago al vendedor) + mail |
processExpiredSuspensions | Usuario con suspendedUntil vencido | Desuspende automáticamente |
Warning de 24h antes: para cada deadline, manda email “se cancelará/recibirá en ~24h” cuando faltan menos de 24h.
Recordatorios proactivos (campo reminderCount en la orden, idempotente vía CAS claimReminder): además del aviso SLA, en la rama intermedia de cada fase el cron empuja recordatorios:
| Fase | Hitos (desde que entró a la fase) | A quién | Para |
|---|---|---|---|
pending | 24h | vendedor | aceptar la venta |
accepted | 48h y 96h | vendedor | entregar (envío o presencial) |
shipped | cada 48h (48–288h) | comprador | confirmar recepción (libera el pago al vendedor) |
Se cortan apenas el actor avanza la orden (reminderCount se resetea a 0 en cada transición — accept/ship). Son transaccionales (Resend, no pasan por preferencias) como el resto del ciclo de orden; el email es fire-and-forget. Template: notifications/templates/orderReminder.ts.
Métodos de entrega (jul 2026): cuando la orden es a retiro, los recordatorios accept/deliver1/deliver2 (al vendedor) nombran el punto de retiro por su título (Order.pickupPoint.title, ej. “acéptala para que el comprador pueda retirar su pedido en tu punto de retiro «X»”); el recordatorio receive (al comprador) ajusta el copy a “retiraste” en vez de “recibiste”.
orders-awaiting-payment-cleanup
Sección titulada «orders-awaiting-payment-cleanup»| Frecuencia | */5 * * * * (cada 5 min) |
| Endpoint | POST /api/v1/internal/cron/orders-awaiting-payment-cleanup |
| Código | src/cron/awaitingPaymentCleanupService.ts |
| Qué hace | Cancela órdenes individuales en awaiting_payment cuyo paymentTimeoutAt ya pasó (30 min por defecto). Restaura stock. Manda mail de timeout. |
| Idempotente | Sí |
| Índice clave | {status:1, paymentTimeoutAt:1} en orders (agregado 2026-05-20) |
También maneja orphan pending orders: órdenes que quedaron en estado pending sin paymentId por crash del proceso entre create() y updateById(). Las cancela si llevan >60 min.
Carrito multi-vendedor: las dos queries de barrido (timeout y orphan) filtran checkoutId: null, así que no tocan sub-órdenes de checkout (que legítimamente tienen paymentId: null) — cancelarlas sin reembolsar sería pérdida de plata. Los checkouts vencidos se expiran aparte, vía checkoutRepo.findExpired → CheckoutService.failCheckout, que cancela cada sub-orden aún en awaiting_payment, restaura su stock, marca el checkout expired y manda un mail consolidado checkoutFailed. Todo idempotente.
wallet-reconcile-daily
Sección titulada «wallet-reconcile-daily»| Frecuencia | 0 3 * * * (diario 3am UTC) |
| Endpoint | POST /api/v1/internal/cron/wallet-reconcile |
| Código | src/cron/walletReconcile.ts → ReconcileService.reconcileAll() |
| Qué hace | Reconcilia user.walletBalance (denormalizado) contra la suma del ledger wallet_entries. Loguea ERROR si encuentra divergencias. |
| Idempotente | Sí (read-only) |
Si encuentra una divergencia, escribe log estructurado [wallet-reconcile] DIVERGENCES DETECTED que debe disparar alerta en Sentry. La acción de corrección es manual (endpoint /admin/reconcile o intervención de un dev).
refunds-reconcile-daily
Sección titulada «refunds-reconcile-daily»| Frecuencia | 0 4 * * * (diario 4am UTC) |
| Endpoint | POST /api/v1/internal/cron/refunds-reconcile |
| Código | src/payments/services/RefundReconcileService.ts |
| Qué hace | Detecta órdenes canceladas con paymentId cuyo refund no se procesó correctamente. Log ERROR si encuentra inconsistencias. |
| Idempotente | Sí (read-only) |
portfolio-snapshot-daily
Sección titulada «portfolio-snapshot-daily»| Frecuencia | 5 0 * * * America/Santiago (diario 00:05 hora de Chile) |
| Endpoint | POST /api/v1/internal/cron/portfolio-snapshot |
| Código | src/portfolio/services/PortfolioService.ts (snapshotDiario) |
| Qué hace | El historiador del Portafolio: congela el valor publicado de cada vendedor (Σ precio × cantidad de listings activos, en centavos) como el punto del día. Fotografía a quien tiene activos o a quien su último snapshot era > 0 — así la caída a cero queda registrada el día real. |
| Idempotente | Sí (upsert sobre índice único {sellerId, date}; re-corridas el mismo día actualizan el valor, incluida la caída a cero intradía) |
Medido al diseñarlo (2026-08-18): la agregación completa tarda 277 ms con 3.435 publicaciones activas y 131 vendedores. Escala con las publicaciones activas.
Creado el 2026-08-19 y probado con gcloud scheduler jobs run: la primera
corrida escribió 131 snapshots verificados en la base. La cabecera
X-Internal-Cron-Secret va literal en el scheduler — con este ya son 10
copias del secreto (actualizar el conteo del runbook al rotarlo). El horario
es 01:05 y no 00:05 a propósito: el salto al horario de verano chileno elimina
la hora 00 un domingo al año y Cloud Scheduler puede omitir esa corrida.
No es un cron: alerta de uso del escáner
Sección titulada «No es un cron: alerta de uso del escáner»La alerta “OCR al 80% del límite diario” no es un job de Cloud Scheduler. Se dispara inline dentro de POST /api/v1/scan/identify-image: en cada scan se incrementa el contador del día (scanner_usage) y, si pasó el umbral (400/500) y aún no se alertó hoy (alertedAt80), se manda el email al admin. Por eso no aparece en la lista de jobs ni necesita Cloud Scheduler. El contador en scanner_usage resetea por fecha UTC (no hay job de reset). Código: src/scanner/services/ScannerAlertService.ts.
Cómo cambiar la frecuencia de un cron
Sección titulada «Cómo cambiar la frecuencia de un cron»gcloud scheduler jobs update http <job-name> \ --schedule="*/10 * * * *" \ --project=api-cards-prod \ --location=us-central1Cambia el schedule (cron expression). Las request a la API no cambian; solo el timing.
Cómo agregar un cron nuevo
Sección titulada «Cómo agregar un cron nuevo»gcloud scheduler jobs create http nuevo-cron \ --project=api-cards-prod \ --location=us-central1 \ --schedule="0 * * * *" \ --time-zone="UTC" \ --uri="https://tcgcards-api-1033181994095.us-central1.run.app/api/v1/internal/cron/nuevo-cron" \ --http-method=POST \ --oidc-service-account-email=mongo-backup-scheduler@api-cards-prod.iam.gserviceaccount.com \ --headers="X-Internal-Cron-Secret=<valor del secret>"(Antes de crear, hay que implementar el endpoint en el código + redeploy.)
Cuellos de botella conocidos
Sección titulada «Cuellos de botella conocidos»Detallado en la conversación de 2026-05-20:
| Job | Estado actual | Punto de quiebre |
|---|---|---|
orders-awaiting-payment-cleanup | OK con índice nuevo | ~1.000 órdenes vencidas simultáneas |
orders-tick | OK pero usa findActiveByStatus() que trae todo a memoria | ~5.000 órdenes activas (RAM + tiempo) |
wallet-reconcile-daily | OK | ~50.000 usuarios con wallet (depende de Cloud Run timeout) |
refunds-reconcile-daily | OK | sin riesgo previsible |
mongo-backup-hourly | OK | sin riesgo (no depende de nuestro código) |
Si lo notamos crecer a esos volúmenes hay fixes preparados — ver decisiones técnicas para detalles.