Aller au contenu principal

Garder vos données fraîches

Un tableau de bord ne vaut son URL que si les chiffres qu'il affiche sont à jour. C'est la partie qu'une session d'agent ne peut pas laisser derrière elle : votre agent construit le tableau de bord puis s'en va, et les données sous-jacentes doivent continuer d'arriver selon un planning, pendant des mois, sans que personne ne soit dans la boucle.

La fraîcheur est donc un état de plateforme, pas du code de tableau de bord. Vous la configurez une fois contre un jeu de données ; elle continue de fonctionner que quelqu'un maintienne ou non une session ouverte.

Les trois façons de faire avancer les données​

Ce que c'estQui le déclenche
Actualisation à la demandeRetirer un jeu de données maintenantVous, votre agent, ou votre CI — POST /v1/datasets/{dataset_id}/refresh
Chargement incrémentalNe récupérer que ce qui a changé depuis la dernière exécutionLe même appel, avec mode: "incremental"
Actualisation planifiéeUne expression cron que la plateforme exécute pour vousPersonne — le poller cron de la plateforme

Actualiser un jeu de données maintenant​

result = client.datasets.refresh(dataset_id)
print(result.completed, result.effective_mode, result.message)
const result = await client.datasets.refresh(datasetId);
console.log(result.completed, result.effectiveMode, result.message);

Deux résultats possibles, et la différence compte :

  • 200 OK — le travail s'est réellement terminé au cours de la requête. completed est true et dataset est la ligne finale. C'est ce qui se passe pour un re-téléchargement de jeu de données public.
  • 202 Accepted — l'actualisation a été démarrée. La ligne est verrouillée et marquée PROCESSING, et un connecteur s'exécute quelques secondes plus tard. completed est false. Sondez le jeu de données, ou écoutez le websocket, jusqu'à ce que processingStatus sorte de PROCESSING.

Un 202 n'est ni un échec ni une réussite — c'est une promesse. Traitez « l'appel est revenu » et « les données sont à jour » comme deux faits différents, et vérifiez le second avant de dire à quelqu'un que le tableau de bord est à jour. L'appel qui le vérifie est get_data_freshness — tout ce qui précède cette section concerne la circulation des données, pas leur ancienneté.

Chargements incrémentaux, et l'exécution qui n'est pas encore incrémentale​

Passez un mode pour ne récupérer que les nouvelles lignes :

from vibedasher.models import RefreshRequest

result = client.datasets.refresh(dataset_id, RefreshRequest(mode="incremental"))
print(result.requested_mode, "->", result.effective_mode)

requestedMode renvoie ce que vous avez demandé. effectiveMode est ce qui s'est réellement exécuté, et ce n'est pas toujours la même chose. Une demande incrémentale contre un jeu de données sans point de contrôle existant renvoie effectiveMode: "bootstrap" : elle s'exécute comme un écrasement complet et enregistre le filigrane, si bien que l'exécution suivante est la première véritablement incrémentale.

Basez votre logique sur effectiveMode, jamais sur ce que vous avez demandé. (C'est la même règle que pour client.query, qui remonte la voie d'exécution réellement utilisée plutôt que celle que vous avez demandée — un principe qui mérite d'être intériorisé : la plateforme vous dit ce qui s'est passé, et c'est à vous de le lire.)

Pour forcer la prochaine exécution à tout récupérer à nouveau, réinitialisez le filigrane :

client.datasets.reset_checkpoint(dataset_id)

Actualisation planifiée​

Une planification est une ligne que vous créez une fois. Elle porte une expression cron et le projet dbt qu'elle actualise :

from vibedasher.models import RefreshCreate

schedule = client.refreshes.create_refresh(
RefreshCreate(name="nightly", project_id=project_id, cron="0 3 * * *")
)
print(schedule.refresh.next_run, schedule.schedule_registration.registered)

La réponse porte un bloc scheduleRegistration. Vérifiez registered. Il est true seulement quand la planification est réellement active ; quand il est false, reason et message expliquent pourquoi, et nextRun est nul.

Ce champ existe à cause d'une vraie panne, et l'histoire est le meilleur argument en faveur de la conception actuelle. L'implémentation d'origine enregistrait chaque planification comme sa propre règle cloud, enveloppait l'appel dans un except large, et répondait 201 Created avec un nextRun confiant. Le rôle de l'API n'avait pas la permission de créer ces règles, si bien que rien n'a jamais été enregistré — et chaque client a été informé que ses données seraient actualisées. Cela a été découvert le 2026‑09‑05 : aucune règle n'existait, et la seule planification en base avait un nextRun vieux de cinq mois et un nombre d'exécutions de zéro.

Le correctif n'a pas été une meilleure gestion des erreurs. Ce fut la suppression de la chose qui pouvait échouer : la ligne est la planification. Un poller cron s'exécute toutes les cinq minutes, réclame chaque ligne due par compare-and-set, et l'exécute. Il n'y a pas d'enregistrement externe qui puisse réussir ou échouer, donc la seule chose qui peut mal se passer à la création est une expression cron qui ne peut pas être évaluée — et c'est désormais rejeté d'emblée, pas au moment du déclenchement.

Deux conséquences à prévoir :

  • Une planification se déclenche dans environ cinq minutes après son heure prévue, pas à la seconde près. Cron signifie ici « pas avant », pas « exactement à ».
  • scheduleRegistration.backend affiche db_cron. Il est conservé dans la réponse parce que des clients s'appuient dessus.

Lire ce qui s'est passé​

Chaque ligne de planification porte son propre historique — lastRun, lastStatus, lastError, executionCount, nextRun :

for r in client.refreshes.iter_all():
print(r.name, r.last_status, r.last_run, r.execution_count, r.last_error)

executionCount est le champ à surveiller. Une planification qui existe depuis une semaine avec executionCount: 0 n'est pas lente — elle ne s'exécute pas, et c'est exactement l'échec que la conception ci-dessus a été construite pour rendre visible plutôt que silencieux.

Quel âge ont ces chiffres ?​

Tout ce qui précède répond à « le pipeline fonctionne-t-il ? ». Cela ne répond pas à « ce que montre ce tableau de bord est-il à jour ? », et les deux divergent dans les deux sens :

  • Une planification peut s'exécuter parfaitement et ne rien charger de nouveau. executionCount augmente, lastStatus vaut success, et la ligne la plus récente date toujours de mars.
  • Un jeu de données peut n'avoir aucune planification, et alors executionCount n'a aucune ligne à lire. Un import statique est le cas courant : ses lignes ne changent que lorsque quelqu'un en téléverse de nouvelles, donc une liste de planifications en bonne santé peut coexister avec un jeu de données vieux de neuf jours sans rien en dire. L'absence de planification n'est pas une planification au compteur défaillant.

Un seul appel répond à la vraie question du lecteur, mesurée sur les lignes plutôt que sur la comptabilité interne de la plateforme :

f = client.datasets.get_freshness(dataset_id).freshness
print(f.as_of, f.age_days, f.status)
const { freshness } = await client.datasets.getFreshness(datasetId);
console.log(freshness.asOf, freshness.ageDays, freshness.status);

En MCP il s'agit de get_data_freshness, qui prend exactement un de viz_id ou dataset_id. Avec un viz_id, il résout chaque jeu de données lu par le tableau de bord et renvoie le plus ancien — un tableau de bord n'est jamais plus frais que son entrée la plus vieille. La route REST est GET /v1/datasets/{dataset_id}/freshness.

Les trois champs, et pourquoi ils sont trois​

ChampCe que c'est
asOfLa valeur la plus récente dans la colonne temporelle des données elles-mêmes. C'est la réponse, et elle ne peut pas flatter : elle est lue dans les lignes. ageDays en donne l'ancienneté ; une valeur négative signifie des données post-datées.
lastLoadedAtQuand les lignes ont atterri pour la dernière fois. Un fait de pipeline, pas un âge — un fichier chargé ce matin peut ne rien contenir de plus récent que mars. Ne le citez jamais comme date de référence.
refreshSi quelque chose est planifié pour rafraîchir les chiffres, y compris la réponse « rien, et rien ne peut l'être » pour un fichier plat. C'est le champ qui couvre le cas « aucune planification » décrit plus haut.

status indique quelle confiance accorder à asOf : MEASURED (il est réel), NO_TIME_COLUMN (les données ne portent aucune colonne de date, donc n'ont pas de date de référence à avoir), ou UNKNOWN — qui signifie que le fait n'a pas été établi et n'est jamais un succès.

lastLoadedSource nomme quelle horloge a répondu à lastLoadedAt. iceberg_snapshot est l'heure d'écriture physique de l'instantané courant de la table — le seul véritable instant d'atterrissage, et qu'un renommage ne peut pas déplacer. not_established signifie que lastLoadedAt vaut null volontairement : ce chemin de lecture n'a pas consulté l'instantané, donc plutôt que de répondre avec l'updatedAt de la ligne — une horloge d'écriture que toute modification fait avancer, et qui a une fois signalé des lignes atterries neuf jours après les faits — le champ renonce. Un champ qui renonce vaut strictement mieux qu'un champ qui répond avec la mauvaise horloge. La route ci-dessus est l'appel qui lit la bonne. get_viz et list_vizzes portent un dataFreshness gratuit, précis sur la posture de rafraîchissement et UNKNOWN sur la date de référence ; cet appel est ce qui résout cet UNKNOWN.

Ce qui n'est pas l'âge des données​

Ni publishStatus, ni lastBuild.createdAt, ni processingStatus, ni updatedAt ne répondent à cette question. Ils indiquent, dans l'ordre : si un bundle existe, quand le code a été compilé, si le fichier s'est analysé une fois, et quand la ligne a changé pour la dernière fois. Chacun d'eux paraît à jour sur un tableau de bord dont les chiffres ont des semaines. Si vous êtes sur le point de rapporter une fraîcheur à partir de l'un d'eux, appelez plutôt la route de fraîcheur.

Cela coûte un SELECT MAX(<colonne de date>) par jeu de données — la même sonde que le tableau de bord rendu exécute déjà à son montage.

Où vit chaque geste​

  • Les planifications sont la surface MCP. create_refresh, list_refreshes, get_refresh et delete_refresh couvrent tout le cycle de vie d'une actualisation récurrente depuis votre agent.
  • « Actualise maintenant » passe par le SDK et l'API REST. Une exécution ponctuelle y tient en un seul appel — voir la Référence API.
  • Les projets dbt portent leurs propres routes d'exécution et d'actualisation (/v1/dbt/projects/{project_id}/run, /v1/dbt/projects/{project_id}/refreshes).