Ir al contenido

Relleno histórico

Un relleno histórico recorre cada superficie de GitHub hasta donde la API responda, una vez, para que la historia empiece antes de la primera pasada.

Ventana de terminal
ghchronicle -config config.yaml -backfill

Una pasada es un incremento. Un relleno histórico es un recorrido. Son intenciones opuestas y la herramienta las trata como tales.

PasadaRelleno
Páginas por colectorel pequeño valor por defecto del colectorhasta que se acabe la API, o hasta el límite dado
Al llegar a la reservasaltar la familia y avisaresperar a que la ventana se reinicie y seguir
Qué familias correnlas que tienen el intervalo vencido, las lentas por turnos en el serviciotodas las activas, diga lo que diga el fichero de estado
Si se parano hay nada que guardar; el siguiente tic barreun punto de control guarda su sitio, y el mismo comando sigue desde ahí
Con qué frecuenciasegún horario, siemprea propósito, normalmente una vez

Una pasada normal nunca debe bloquear. Protege la reserva para que lo demás que use el mismo token siga funcionando, y salta una familia antes que dormir. Un relleno se lanza a propósito y lo único que importa es que termine, así que aparca hasta que el presupuesto vuelva a estar entero. Y un relleno que se para a medias guarda su sitio, así que el mismo comando lanzado otra vez sigue desde ahí y no desde el primer repositorio.

Un recorrido sobre una cuenta real dura horas, y siempre aparece un motivo para pararlo: el del autor llevaba seis horas y media cuando hubo que sustituir el binario que lo ejecutaba. Pararlo costaba el recorrido entero.

El sitio se guarda en un fichero junto al de estado, con su mismo nombre y -progress.json en lugar de .json. No se configura. Se escribe después de cada repositorio y no después de cada familia, y eso está medido, no elegido: en una cuenta de cincuenta y nueve repositorios las familias de cuenta tardan cuatro minutos entre todas, y luego actions tarda dos horas y tres cuartos y commits más todavía, así que un punto de control al final de cada familia guardaría los cuatro minutos y tiraría el resto.

Lo que anota es lo que ya se ha entregado. Un repositorio entra en el fichero solo cuando su recorrido completo ha llegado a todos los destinos configurados, y eso es lo que hace seguro saltárselo a la vuelta: no queda cola alguna de su propia paginación en ninguna parte. Un repositorio cuyo colector falló, o cuyas filas rechazó un almacén, no se anota, y el recorrido vuelve a él. Volver a él cuesta peticiones y nada más, porque un punto lleva la fecha en que ocurrió la cosa y recolectarlo otra vez reescribe las mismas filas.

level=INFO msg="backfill stopped, and what it had written is kept" file=/var/lib/ghchronicle/backfill-progress.json running_for=6h40m7s families_complete=9 family=actions repositories_written=23 resume="run the same command again"

El fichero está pensado para leerse, y dice lo mismo con más detalle: las familias que terminaron y cuándo, la familia que estaba en curso, y cuáles de sus repositorios están hechos. Se borra cuando el recorrido ha cubierto todas las familias que se le pidieron, así que un punto de control que existe es un recorrido con trabajo pendiente.

Llegar al final sin error no es lo mismo. Una familia truncada por un límite de ritmo secundario se devuelve como pasada y no como fallo, y una familia que falló en todos sus repositorios se deja a propósito sin marcar; un recorrido puede terminar ordenadamente con cualquiera de las dos detrás. El punto de control se queda, y la línea del final nombra las familias que faltan.

Una pasada normal no guarda nada de esto. Ni escribe un punto de control ni lee ninguno: el fichero de estado que lleva habla de cadencias, y un relleno a medias no pinta nada ahí.

Un punto de control que no puede leer. Una máquina que pierde la corriente puede dejar un fichero vacío o escrito a medias. La ejecución se para con una frase que lo nombra en vez de rehacer el recorrido en silencio: lo que ese fichero lista pueden ser horas de la historia de alguien, y empezar de cero es la respuesta que parece un éxito.

Un punto de control de otro recorrido. El fichero anota lo que se le pidió al recorrido: la API que lee, los objetivos, las familias activas, el límite de fecha tal y como se escribió y los almacenes a los que escribe. Si algo de eso cambió entre la parada y la vuelta, la lista de repositorios que no hay que volver a recorrer significa otra cosa, y la ejecución se para y nombra el ajuste que cambió.

Los almacenes están ahí porque un registro en este fichero significa que las filas llegaron a todos los destinos, y todos los destinos son los que tenía aquel proceso. Añade un almacén con el recorrido parado y la vuelta se salta todo lo que anotó la primera mitad, así que el almacén nuevo se queda con la cola del recorrido y con nada de antes. Apunta uno existente a otro sitio y se abre el mismo agujero en otro lugar. Solo se comparan los ajustes que puedes leer: la contraseña de un almacén nunca se escribe en este fichero.

El límite es el que fallaría en silencio. Un recorrido limitado a un año anota sus repositorios como escritos; una vuelta sin límite se saltaría todos ellos y dejaría un almacén que parece completo y se corta hace un año, sin que nada en las filas lo diga.

Los dos rechazos dejan el fichero donde está. Devuelve el ajuste a lo que era para seguir, o borra el fichero para recorrer otra vez desde el primer repositorio.

Sustituir el binario no es uno de ellos. Eso es justo lo que paró el recorrido para el que se escribió todo esto, así que un punto de control escrito por otra versión se retoma, con una línea que dice qué versión escribió lo que el fichero nombra.

Ventana de terminal
ghchronicle -config config.yaml -backfill-status

Lee el punto de control y lo imprime: cuándo empezó el recorrido, cuánto hace que anotó algo por última vez, cuántas familias van completas de cuántas, en qué familia estaba y por dónde, y la orden que lo continúa, con el -backfill-since y el -families con los que empezó. La relectura en curso de una migración se imprime después, con lo que escribe y dónde. No pregunta nada a GitHub ni escribe nada, así que puede lanzarse con un recorrido en marcha, y no necesita token: la credencial está porque una pasada le pregunta a GitHub, y esto no le pregunta a nadie. Si no hay ningún recorrido a medias lo dice, y nombra el fichero que ha buscado.

Una pasada puede acabar con familias pendientes y sin un solo error. Lo normal es un mal rato al otro lado: un 502 en dos repositorios de sesenta deja su familia sin marcar, y el recorrido termina ordenadamente con una familia de menos.

Ventana de terminal
ghchronicle -config config.yaml -backfill -backfill-retry 1h

espera una hora y vuelve a por lo que quede, y sigue haciéndolo hasta que no quede nada. Cuesta casi nada, porque al retomar solo recorre lo que el punto de control no tiene ya: dos repositorios de sesenta, no sesenta.

Se para solo cuando una pasada no anota nada nuevo. Esa es toda la regla, y a propósito no es una lista de qué errores merecen reintento: un repositorio borrado falla igual cada hora, y una lista de códigos reintentables queda desfasada en cuanto GitHub responde algo que antes no respondía. Una pasada que no gana nada es una pasada cuyo obstáculo no lo despeja esperar. Hay además un tope de diez pasadas, como red de seguridad y no como ajuste.

Sin la opción no espera ni vuelve, que es lo que hacía antes de que esto existiera.

Cuando un cubo está en su reserva o por debajo, el relleno espera. La espera no se adivina: cada respuesta de GitHub dice exactamente cuándo se reinicia su ventana, así que el colector duerme hasta ese instante más un segundo de holgura y registra lo que está haciendo.

level=INFO msg="rate limit reserve reached, waiting for the window to reset" family=commits wait=23m11s

Dos guardas sobre esa espera. Está limitada a una hora, para que un desfase de reloj o una cabecera obsoleta no se conviertan en un sueño ilimitado, y tiene un suelo de un segundo, para que no se convierta en un bucle activo. Un contexto cancelado termina la espera de inmediato, que es lo que hace que Ctrl+C funcione durante un relleno nocturno.

-backfill-since en la línea de órdenes, o backfill.since en el fichero de configuración. Cuatro formas de escribirlo, porque cada cual tira de una:

ValorSignifica
2024-01-01esa fecha
90dhace noventa días
2yhace dos años
720huna duración de Go antes de ahora
vacío, all, unlimitedsin límite alguno

Sin límite significa que el recorrido solo se detiene donde se detiene la API, por muchas horas o días que eso lleve, con una pausa en cada reinicio de límite por el camino.

Ventana de terminal
ghchronicle -config config.yaml -backfill -backfill-since 2y
Ventana de terminal
ghchronicle -config config.yaml -backfill -families discussions,outbound

recorre las familias nombradas y ninguna otra, en todos los almacenes configurados. Es lo que necesita un almacén que perdió el historial de una familia: borrado a mano, o enviado a través de un Telegraf cuyo almacén se despejó, sin recorrer cada otra familia que aún guarda. Los nombres son las familias que imprime -groups. Uno que no es una familia, y la opción sin -backfill, se rechazan con 2, como una línea de órdenes que no se entiende; una familia que la configuración apaga se rechaza con 1 antes de preguntar nada, ni a GitHub ni a los almacenes, incluida la comprobación de migraciones del arranque, porque el recorrido no leería nada de ella y acabaría completo.

Su punto de control es el del relleno, y registra las familias que se le pidieron, así que un relleno de otras familias, o de todas, lo rechaza y nombra la familia que difiere, y -backfill-status imprime la línea para retomarlo con el mismo -families.

Una migración que despejó un almacén vuelve a leer su historial con un relleno propio, la relectura: las familias que escriben lo despejado, escribiendo solo eso en los almacenes de los que se despejó; ver volver a leer el historial. Guarda su punto de control junto al del relleno, con -refill.json en el nombre, así que un relleno en curso y una relectura ni se rechazan ni se pisan, y -backfill-status imprime los dos.

Una instalación nueva debería lanzar un relleno antes de arrancar el servicio, o justo después. La primera pasada es un incremento más ancho, no una historia: un mes de ejecuciones de workflows, la historia de estrellas entera y la página más reciente de todo lo demás. Los puntos van fechados, así que a cualquier almacén le vale; a un dashboard a noventa días o a dos años no, porque la historia que dibuja empieza el día en que se instaló el colector.

Medido tras un día de pasadas y sin relleno, sobre repositorios con cientos de pull requests cada uno: pull requests, alrededor de una quinta parte de las que declaran los repositorios, porque una pasada lee una sola página de cincuenta por repositorio por muchas que tenga; issues, alrededor de la mitad; commits, bastante menos de una décima parte y ninguno de más de treinta días; jobs y pasos de una décima parte de las ejecuciones de workflows, así que la espera en cola, los jobs más lentos y los pasos que fallan se calcularon sobre esa décima parte. A dos años, Pull requests merged marcaba una quinta parte de lo que decía Pull requests merged, ever. Estrellas y forks estaban completos, porque la primera pasada los recorre hasta el final de todos modos, y también releases, despliegues y alertas, porque ninguno de esos repositorios pasaba de los cien de cada uno que lee una pasada.

  • Toda la historia de commits, en lugar de la última página. Esta es la única familia en la que un relleno es cualitativamente distinto y no solo más ancho: sin él la serie de líneas cambiadas empieza el día en que instalaste el colector.

  • Cada ejecución de workflow que GitHub todavía conserve, expandida en sus jobs, y en sus pasos mientras GitHub los siga sirviendo. Medido el 24 de septiembre de 2026, GitHub listaba todos los jobs de una ejecución de hace 278 días, pero ningún paso de las ejecuciones creadas antes del 12 de abril, unos cinco meses y medio atrás. Un job así, si terminó en éxito, en fallo o por tiempo agotado, se escribe sin el campo steps en lugar de con un 0, ya que ejecutó al menos uno.

  • Los repositorios archivados, enteros, diga lo que diga include_archived. Su historia es la historia de la cuenta y ya no se mueve, que es justo por lo que una pasada los deja fuera y por lo que un solo recorrido basta. Los forks quedan como estén configurados. Lo que sí sigue moviéndose en un repositorio archivado, sus estrellas, sus forks y sus watchers, una pasada lo lee sin relleno: el listado que ya paga dice qué repositorios están archivados, y cada pasada de totals pregunta por todos ellos, una consulta por cada veinticinco, por dos filas de cada uno. Una es gh_repo_archived, fechada cuando se archivó el repositorio. La otra es su gh_repo_total, sellada en la pasada como la de un repositorio recogido, y esa es la fila de la que se leen los totales de estrellas y forks de la cuenta.

  • Todas las pull requests e issues, en páginas de cincuenta, donde una pasada lee lo que cambió desde la pasada anterior, y una vez al día cada elemento abierto y lo que se movió desde el día anterior.

  • Todas las pull requests e issues que la cuenta abrió en repositorios ajenos y que ya se fusionaron o se cerraron, en páginas de cien ordenadas por cuándo se movió cada una por última vez, donde una pasada lee cada uno de los tres estados cerrados hasta una cadencia antes de la pasada anterior. Eso le basta a una pasada porque fusionar o cerrar sube el elemento arriba del todo, por mucho tiempo que haga que se abrió, y es una página salvo que en ese tiempo se hayan movido más de cien elementos; una pasada que leía una página y ni una más perdía el elemento que cien actualizaciones posteriores, un bot que bloquea hilos viejos o un reetiquetado, habían empujado a la segunda. Las que siguen abiertas no se acotan ni por página ni por fecha: cada pasada las lee todas, porque cada una recibe una fila por cada día que sigue abierta. En la 2.5.1 y anteriores cada una de las cinco búsquedas leía sus cien más recientes y paraba, igual en una pasada que en un relleno.

    GitHub sirve mil resultados de cualquier búsqueda y ni uno más, así que una cuenta que pase de mil en uno de esos estados conserva los mil que se movieron más recientemente. Eso se dice en el log, una vez por recuento, en lugar de dejar que se note en dos paneles que no cuadran:

    level=WARN msg="outbound search read fewer items than it counts, GitHub serves a thousand at most" kind=pull_request state=merged count=2860 read=1000
  • Todas las páginas de artefactos, de actividad del repositorio y de análisis de code scanning, donde una pasada lee cinco, dos y una.

  • Cada release, despliegue y discusión, y cada alerta de Dependabot y de code scanning, donde una pasada lee la página más reciente de cada lista.

  • Cada comentario en issues y en discusiones que la cuenta dejó en cualquier sitio, y cada respuesta suya que se aceptó, donde una pasada lee los cien comentarios más recientes de cada tipo y las quinientas respuestas aceptadas más recientes.

  • Cada estrella que dio la cuenta, donde una pasada lee las quinientas más recientes.

  • Las pull requests en coautoría que hay detrás del progreso de Pair Extraordinaire, recorridas sobre toda la vida de la cuenta, donde una pasada suma los días desde la anterior y recorre la historia entera una vez por semana.

  • Todas las páginas de cada lista de stargazers, donde una pasada lee las cien estrellas más recientes de un repositorio cuya lista ya recorrió entera una vez. Hasta la 2.5.0 un primer recorrido que fallaba se anotaba como hecho, y un relleno leía una lista anotada por su primera y su última página, así que las estrellas de en medio no se leían nunca; ahora un relleno llega a ellas.

  • La bandeja entera, hilos leídos incluidos, donde una pasada lee los hilos no leídos que se movieron y, una vez al día, veinte páginas de cincuenta con los leídos.

  • Cada mes de facturación que GitHub todavía responda, hasta tres meses vacíos seguidos, donde una pasada lee dos.

  • Las cien entregas de webhook más recientes de cada hook, donde una pasada lee treinta.

  • Como mucho 500 logs de jobs fallidos por repositorio, ninguno de hace más de noventa días, si joblogs tiene cadencia; una pasada lee como mucho diez.

Dos cosas que una pasada recuerda, un relleno las deja de lado. Un 403 o 404 que una pasada recuerda durante un día se vuelve a preguntar, porque un relleno no consulta esa memoria. Y lee el fichero de caché que hay junto al de estado pero no escribe nada en él: las páginas que recorre son unas que ninguna pasada pide, y guardadas allí echarían las de las pasadas, así que un servicio arrancado después empezaría más frío de lo que se paró.

Un relleno corre todas las familias activas diga lo que diga el fichero de estado. No enciende ninguna. Las tres familias que vienen con cadencia 0, deps, history y joblogs, siguen apagadas si no se les ha dado una por nombre bajo every.families, y -backfill no cambia eso.

history es la que sorprende, porque recorrer cada año pasado del calendario de contribuciones es justo lo que tiene en la cabeza quien pide “toda la historia”. Dale antes una cadencia:

every:
families:
history: 24h

En cadencias está por qué default y groups tampoco pueden encender estas tres.

Ambos se descubrieron ejecutándolo, no leyendo documentación.

Dependabot rechaza los números de página. Responde con un error a page= sin más y pagina por cursor, así que el recorrido de alertas está escrito contra cursores.

La pasarela de GraphQL se rinde con cien pull requests, y con cincuenta commits. Pedir cien pull requests con sus revisiones en una consulta responde un 502 en HTML al cabo de unos diez segundos, y lo mismo pedir cincuenta commits de un repositorio activo con las comprobaciones que lleva cada uno: doce páginas de los recorridos de commits del propio autor se toparon con él, en seis repositorios. Ambos recorridos parten su página por la mitad y reintentan sobre el mismo cursor, en silencio: ninguno de los dos colectores lleva registrador, así que un relleno de un repositorio activo lo muestra solo como una familia más lenta, nunca como una línea. La página se parte mientras sea mayor que diez, así que cincuenta se vuelve a pedir con veinticinco, doce y seis. Una página con la que la pasarela aún se rinde en ese tamaño más pequeño, y un tiempo límite en cualquier recorrido sin página más pequeña que pedir, es un fallo: se escriben las filas leídas antes, la línea collector failed dice que la consulta es demasiado grande para una sola petición, y el repositorio no se anota en el punto de control, así que al reanudar se vuelve a recorrer. Hasta 2.6.3 el recorrido de commits tomaba el tiempo límite por el final del historial y anotaba el repositorio como recorrido. Solo esa respuesta parte la página: un 503, o un 502 que vuelve antes de diez segundos, no es una consulta con la que a la pasarela se le acabó el tiempo, y el cliente la vuelve a preguntar una vez más con el mismo tamaño.

Es idempotente. Los puntos se indexan por medida, etiquetas y marca de tiempo, así que un relleno ejecutado dos veces reescribe las mismas filas en vez de duplicarlas, en todo almacén que guarde la historia. Lo que cuesta es cuota de API y tiempo.

Dos cosas que conviene hacer antes: ejecutar -list para confirmar el conjunto de repositorios, y comprobar que el almacén al que escribes es de los que guardan fechas. Rellenar hacia Prometheus recoge muchísima historia y después la reduce entera a un único valor actual.