# Relleno histórico

Un recorrido deliberado hasta el final de cada superficie, hasta dónde llega, y las tres cosas que ningún relleno alcanza.

Source: https://docs.jmrp.io/ghchronicle/es/how/backfill/

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.

```sh
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.

## La diferencia en una tabla

|                        | Pasada                                    | Relleno                                                  |
| ---------------------- | ----------------------------------------- | -------------------------------------------------------- |
| Páginas por colector   | el pequeño valor por defecto del colector | hasta que se acabe la API, o hasta el límite dado        |
| Al llegar a la reserva | saltar la familia y avisar                | esperar a que la ventana se reinicie y seguir            |
| Qué familias corren    | las que tienen el intervalo vencido, las lentas [por turnos](https://docs.jmrp.io/ghchronicle/es/configuration/cadences/#las-familias-lentas-se-turnan) en el servicio | todas las activas, diga lo que diga el fichero de estado |
| Si se para             | no hay nada que guardar; el siguiente tic barre | un punto de control guarda su sitio, y el mismo comando sigue desde ahí |
| Con qué frecuencia     | según horario, siempre                    | a 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.

## Pararlo, y retomarlo

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.

```text
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í.

### Las dos cosas que rechaza

**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.

### Ver hasta dónde ha llegado

```sh
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.

### Volver solo a por lo que falta

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.

```sh
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.

## La espera

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.

```text
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.

## Hasta dónde

`-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:

| Valor                     | Significa                         |
| ------------------------- | --------------------------------- |
| `2024-01-01`              | esa fecha                         |
| `90d`                     | hace noventa días                 |
| `2y`                      | hace dos años                     |
| `720h`                    | una duración de Go antes de ahora |
| vacío, `all`, `unlimited` | sin 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.

```sh
ghchronicle -config config.yaml -backfill -backfill-since 2y
```

## Solo algunas familias

```sh
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`.

### La relectura de una migración

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](https://docs.jmrp.io/ghchronicle/es/install/upgrading/#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.

## Lánzalo una vez, primero

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.

## Qué alcanza que una pasada no

- 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:

  ```text
  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](https://docs.jmrp.io/ghchronicle/es/api/#un-rechazo-también-se-recuerda)
se vuelve a preguntar, porque un relleno no consulta esa memoria. Y lee el
[fichero de caché](https://docs.jmrp.io/ghchronicle/es/configuration/#la-caché-que-hay-a-su-lado)
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ó.

## Qué no enciende

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:

```yaml
every:
  families:
    history: 24h
```

En [cadencias](https://docs.jmrp.io/ghchronicle/es/configuration/cadences/) está por qué `default` y
`groups` tampoco pueden encender estas tres.

## Dos endpoints que necesitaron trato aparte

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](https://docs.jmrp.io/ghchronicle/es/api/#una-respuesta-que-github-no-pudo-terminar-se-pregunta-una-vez-más)
con el mismo tamaño.

> **Tres cosas no se pueden rellenar a ningún precio**
>
> Ninguna espera cambia esto, y son la razón de que el proyecto exista.
>
> - **El feed de eventos guarda trescientos eventos**, y ninguno de hace más de
>   treinta días. Pasado ese techo GitHub responde 422 "pagination is limited
>   for this resource", que el colector lee como el final de los datos.
> - **El tráfico son catorce días.** Nada más viejo llegó nunca a guardarse en
>   GitHub.
> - **Los logs de los jobs se borran al acabar el periodo de retención del
>   repositorio**, noventa días por omisión, y responden 410 después. Un relleno
>   de `joblogs` solo pide los últimos noventa días, diga lo que diga el ajuste.
>   Las ejecuciones a las que pertenecen se guardaban cuatrocientos días o más;
>   desde el 1 de octubre de 2026 GitHub aplica el mismo ajuste de retención a
>   las propias ejecuciones: noventa días por omisión, como mucho noventa en un
>   repositorio público y hasta cuatrocientos en uno privado, así que un relleno
>   posterior a esa fecha solo llega tan atrás como ese ajuste.

## Cómo lanzar uno con seguridad

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.
