Repository navigation
chore(release): v1.2.0 - #111
Merged
Merged
Conversation
…elds when offered
…ep a monthly reserve
…try and a full one
…ing's least severe level
…r or a doubling wait
…waiting out Steam's rate limit
… let it be stopped
## Problème Wallpaper Engine (et toute appli de fond d'écran) est détecté comme jeu en cours pendant tout le stream : Catapult change la catégorie, et chaque streamer doit ignorer le binding à la main. ## Changements - **Mapping admin « ignoré par défaut »** (`game_mapping.ignored_by_default`) : la catégorie Twitch devient facultative. Sans catégorie, les bindings gardent celle d'IGDB (AUTO/INCOMPLETE), seul l'ignore est appliqué. - **Activer le flag** ignore les bindings AUTO / INCOMPLETE / MAPPED du jeu ; les nouvelles détections arrivent ignorées. Avec « appliquer aux lives », les chaînes live sur le jeu passent tout de suite sur leur catégorie « aucun jeu ». - **Le désactiver, supprimer ou déplacer le mapping** désignore ce qu'il avait ignoré. L'ignore n'est touché que quand le flag change. - **Choix du streamer** (`game_binding.ignored_override`) : posé quand il ignore / désignore lui-même ; le mapping ne revient plus dessus. Les MANUAL ne sont jamais touchés. « Réinitialiser » efface le marqueur et réapplique la règle du mapping. - **Migration V104** : les bindings déjà ignorés sont marqués `ignored_override` (avant, seul le streamer pouvait ignorer). - **Panel admin** : interrupteur dans le formulaire, badge « Ignoré par défaut » dans la liste, exemple Wallpaper Engine dans le mock. - `ignoredByDefault` est un `Boolean` boxé dans `GameMappingRequest` / `GameMappingDto` (Jackson 3 refuse un primitif absent), `null` = `false`. ## Tests `AdminGameMappingServiceTest`, `BindingServiceMappingTest`, `MockGameMappingsStoreTest`, `admin-game-mappings.test.js`. Pas de `bootRun` lancé : à faire avant merge. ## Modules à redémarrer - `catapult-api` (migration V104 au démarrage) - `catapult-web`
The Helix calls concatenate the streamer's id into the query, which Spring takes as the URI template: one uri tag per streamer until Micrometer's cap (100) dropped every new one. Tag the template's path only.
IgdbWebsiteKeys keeps a game's Twitch website URL under "twitch" when IGDB has
no Twitch external_game, and the binding used it as the game_id: Twitch then
refused every update ("The ID in game_id is not valid"). Only accept a numeric
id there, falling back to IGDB's lookup and the name search; log the game_id
sent with Twitch's errors.
## Pourquoi Jusqu'à une éventuelle v2, on saute l'étape `beta` : les releases passent directement de `dev` à `main`. Les PR de report qui ramènent une release sur `dev` doivent se fusionner seules, et la CI lançait plusieurs jobs en double (push puis pull_request sur la même branche). ## Changements ### 1. `chore(release)` : promotion directe `dev` → `main` - Nouveau `.github/release/lib/flow.mjs` : `BETA = false` décrit la chaîne de promotion (`dev → main`). Pour réactiver beta, il suffit de remettre `BETA = true` et de remettre `beta` dans les branches de push de `promote.yml` : les scripts et les conditions des workflows acceptent toujours les paires beta. - Version : `dev → main` prend le dernier stable incrémenté selon les commits conventionnels (`nextDirectStable`), sans tag `-BETA.N`. Prochaine release attendue : `v1.2.0`. Si une beta plus haute est encore en attente, sa cible est reprise. - `ready.mjs` traite `dev → main` comme un stable (bump de `build.gradle.kts` sur `dev`, auto-merge épinglé). - `publish.mjs` : après un stable ou un hotfix, le report se fait directement `main → dev`. - `schedule.mjs` : les sorties programmées concernent `dev → main`. `--run` prend la cible dans `flow.mjs`, et `--target` ne sert plus que de repli : l'ancienne copie de `release-schedule.yml` encore sur `main` (qui passe `--target beta`) reste donc correcte jusqu'à la prochaine release. - Le tableau de bord liste aussi les promotions `dev → main`. - Workflows : `promote.yml` (push sur `dev` uniquement, cible calculée par le script), `release-ready.yml`, `release-schedule.yml`, `publish.yml` et `commitlint.yml` (job ignoré sur `dev → main` et `main → dev`) acceptent la nouvelle paire. ### 2. `chore(release)` : auto-merge des reports vers `dev` - Chaque PR de report ouverte par le bot reçoit l'auto-merge : commit de merge, épinglé sur la tête ouverte, avec le titre `chore(release): backport vX.Y.Z to dev` comme sujet. Ce sujet est déjà ignoré par la promotion. - Si GitHub refuse l'auto-merge, la PR reste ouverte et sa description demande la fusion manuelle, comme avant. - `allow_auto_merge` est déjà activé sur le dépôt : aucun réglage modifié. ### 3. `ci` : moins de jobs en double - `build`, `test`, `commitlint` et `release-scripts` ne tournent plus que sur `pull_request` (vers n'importe quelle base, donc les PR empilées gardent leurs checks). Il n'y a plus de trigger `push`. - La tête de `dev` est vérifiée par sa PR de promotion `dev → main`. Celle de `main` l'est par le report `main → dev`. - `renovate-merge.yml` continue de lancer `build` et `test` sur `dev` par `workflow_dispatch`. - `concurrency` avec `cancel-in-progress` pour les PR : un nouveau push annule le run qu'il remplace. Les runs lancés à la main ne sont jamais annulés. - Le filtre obsolète sur `master` est supprimé. - Les noms des checks requis (`build`, `test`, `commitlint`, `release-scripts`) ne changent pas : le ruleset n'a pas été touché. ## Modules à redémarrer Modules à redémarrer : aucun (CI uniquement). ## À reporter sur `main` / à faire après merge - `release-schedule.yml` est modifié ici. Il arrivera sur `main` avec la prochaine promotion `dev → main` et restera ainsi identique sur les deux branches. Pas besoin de le reporter à la main : `main` n'a pas bougé depuis, la fusion sera propre. D'ici là, le cron de `main` utilise l'ancienne copie, que le script gère. - La PR draft #104 (`dev → beta`, `v1.2.0-BETA.1`) est désormais obsolète : la fermer après le merge. Le prochain push sur `dev` ouvrira la promotion `dev → main`. - Le report #101 (`beta → dev`, `v1.1.0`) est toujours ouvert. Tant qu'il l'est, `ready.mjs` refuse la promotion vers `main` : le fusionner (commit de merge) ou activer son auto-merge. - La branche `beta` peut rester en place (inutilisée) pour une réactivation éventuelle.
Correctifs tirés des logs de prod (images `1.1.0`, pods `catapult-api`
et `catapult-web`, ~39 h de logs, aucun redémarrage).
## 1. SSE : `complete()` après l'erreur du conteneur (catapult-web)
```
Exception in thread "" java.lang.IllegalStateException: A non-container (application) thread attempted to use the AsyncContext after an error had occurred and the call to AsyncListener.onError() had returned. This is not allowed to avoid race conditions.
at org.apache.catalina.core.AsyncContextImpl.check(AsyncContextImpl.java:666)
...
at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.complete(ResponseBodyEmitter.java:280)
at fr.enimaloc.catapult.controller.SseStreams$Outbox.write(SseStreams.java:237)
```
**Cause** : quand le client se déconnecte pendant une écriture,
`emitter.send()` lève une `IOException` ; Tomcat appelle alors `onError`
et termine la requête lui-même. L'outbox appelait quand même
`emitter.complete()` (ce que la javadoc de Spring déconseille après une
erreur d'envoi) ; Tomcat refuse ce dispatch depuis un thread applicatif,
et l'exception sortait non rattrapée du thread virtuel d'écriture.
**Correctif** : plus de `complete()` après une `IOException` (le
conteneur s'en charge) ; les autres `complete()` de l'outbox (fermeture
demandée, flux trop lent, `IllegalStateException` à l'envoi) passent par
un `complete()` qui ignore le refus d'un flux déjà terminé par le
conteneur.
## 2. Cardinalité du tag `uri` de `http.client.requests` (catapult-api)
```
WARN .s.b.m.m.MaximumAllowableTagsMeterFilter : Reached the maximum number of 'uri' tags for 'http.client.requests'. Are you using 'uriVariables'?
```
**Cause** : les appels Helix concatènent l'id du streamer dans la query
(`"/helix/channels?broadcaster_id=" + id`, `/streams?user_id=`,
`/moderation/moderators?...`, etc.). Spring prend cette chaîne pour le
gabarit d'URI, d'où un tag par streamer, jusqu'au plafond de 100 au-delà
duquel toute nouvelle URI est écartée des métriques (y compris les
appels légitimes).
**Correctif** : une `ClientRequestObservationConvention` (reprise par
l'auto-configuration RestClient de Spring Boot) qui retire la query
string du tag `uri`. Les fichiers Steam Store / Xbox
(`SteamStoreServiceImpl`, `XboxStoreServiceImpl`, `XboxGameGetter`,
`XboxRawQueries`, `SteamRawQueries`, `WebClientConfig`) ne sont pas
modifiés : leurs ids dans le **chemin** sont traités par la branche des
limites de débit apprises. Reste encore un id dans le chemin en dehors
de ces fichiers : `GitLabClient` / `GitLabRawQueries` (`/issues/" +
iid`), un appel admin à faible volume.
## 3. `The ID in game_id is not valid` (catapult-api)
```
ERROR f.e.c.service.twitch.TwitchServiceImpl : Twitch API error for user 74e0f0cf-…: 400 BAD_REQUEST 400 Bad Request: "{"error":"Bad Request","status":400,"message":"The ID in game_id is not valid."}"
```
(2 fois le 11/11, au début du live puis sur une détection.)
**Cause probable** : `BindingService` lit l'id de catégorie Twitch dans
`websites.get("twitch")` du cache IGDB. Or `IgdbWebsiteKeys#links` y
range aussi l'**URL** du site Twitch d'un jeu (type de site 6) ; l'`uid`
d'un external_game Twitch ne l'écrase que si IGDB en a un. Pour un jeu
sans external_game Twitch mais avec un lien Twitch, la liaison partait
avec une URL comme `game_id`, refusée à chaque mise à jour. La valeur
exacte envoyée n'apparaît pas dans le log, donc ce n'est pas confirmé
sur ce cas précis.
**Correctif** : seul un id numérique est retenu à cet endroit ; sinon on
passe à `igdbService.findTwitchGameId` puis à la recherche par nom. Le
log d'erreur indique désormais le `game_id` envoyé, ce qui permettra de
confirmer. Les liaisons déjà enregistrées avec une URL restent à
re-résoudre (ou à corriger depuis l'admin).
## Vu dans les logs, non corrigé (bruit attendu ou déjà traité)
- `DTDD call failed: UnrecognizedPropertyException: Unrecognized field
"severity"` : déjà corrigé sur `dev` (8478f18), pas encore en prod.
- `Detection skipped … Steam did not answer in time` (9/24 h) : relève
de la branche Steam (limites de débit).
- EventSub `Connection reset`, keepalive timeouts, 429 `number of
websocket transports` : déjà gérés (reconnexion / nouvelle session).
- Minecraft `500 /friends`, `503 /presence` : erreurs ponctuelles du
service externe.
- Au démarrage de catapult-web : `I/O error on GET … catapult-api:8081`
pendant que l'API démarrait.
## Tests
`SseStreamsTest` (3 nouveaux cas + 1 adapté),
`HttpClientUriTagConventionTest`,
`BindingServiceTest#resolveOrCreate_ignoresATwitchWebsiteUrl_forTheCategoryId`.
Les tests Testcontainers / full-context ne tournent pas dans mon
environnement : la CI tranche.
**Modules à redémarrer : catapult-api, catapult-web**
… first limit Each write runs in its own transaction, apart from the caller's.
…f when nothing learned
## Limites de débit apprises
Catapult devine la limite des plateformes qui ne la documentent pas, à
partir de leurs 429. Il la respecte avant d'appeler et apprend le
meilleur délai pour reprendre. Le cas déclencheur est le Steam Store
pendant le recalcul des TW.
### Fonctionnement
- **Accroche** : un intercepteur sur le `RestClient` partagé.
`ExternalApiObservations.observe` reste dédié aux métriques. Les hôtes à
limite documentée sont exclus (`rate-limit.learning.excluded-hosts`) :
Web API Steam (laissée à `SteamRateLimiter`), Twitch, IGDB, DTDD,
GitLab, Battle.net.
- **Route** = méthode + hôte + gabarit d'URI. Les segments qui
ressemblent à des identifiants deviennent `{id}`, même dans une URL
concaténée. Au-delà de 50 routes, les nouvelles routes d'un hôte sont
regroupées dans `/{other}`.
- **Portée mixte** : chaque hôte a un budget partagé par ses routes. Une
route passe à sa propre limite quand elle prend un 429 alors que l'hôte
est loin de sa limite et que ses autres routes répondent encore. Un
admin peut aussi forcer la portée.
- **Limite** : nombre d'appels dans la fenêtre (10 s, 1 min, 5 min ou 1
h) qui correspond à l'attente. Elle est resserrée par les 429, au plus
divisée par deux par un même 429, et remontée de 10 % après 1 h sans
429. Elle n'est apprise que si la fenêtre a été comptée entièrement, ce
qui évite d'en déduire 1 appel par fenêtre juste après un redémarrage.
Le compteur est remis à zéro après chaque blocage.
- **Délai de reprise** (429 sans `Retry-After`) : le premier appel après
l'attente sert de test. S'il réussit, le délai suivant est 80 % du délai
courant. S'il échoue, le délai double. Il ne descend jamais sous le
dernier délai qui a échoué, oublié après 24 h. Un `Retry-After` est
suivi à la lettre.
- **Priorité** : un appel live passe sous la limite et renonce après 2
s, avec un 429 local marqué `X-Catapult-Local-Limit` et jamais appris.
Le travail de masse (recalcul des TW) garde 20 % de marge, attend son
tour et reste annulable.
- **Persistance** : table `learned_rate_limits` (V107), resynchronisée
entre les pods toutes les 60 s. Les compteurs d'appels restent propres à
chaque pod.
- **Steam Store** : le blocage ad hoc (attente de 60 s qui double) est
remplacé. `rateLimitedUntil()` s'appuie désormais sur le limiteur, ce
qui garde `TwSteamRetry` et le recalcul fonctionnels.
- **Xbox Store** : appels passés en gabarit d'URI.
### Admin
- API : `GET /api/internal/admin/rate-limits`, `PUT|DELETE
…/rate-limits/entry?id=` (correction ou remise à zéro). Les routes sont
ajoutées à `access-matrix.csv`.
- Nouvel onglet « Limites de débit » : chaque hôte puis ses routes, avec
limite, délai de reprise, nombre de 429, appels dans la fenêtre et
attente en cours. On peut corriger (appels/fenêtre, délai, portée ; un
champ vide reste à l'apprentissage) et remettre à zéro. Une correction
n'est plus modifiée par l'apprentissage tant qu'elle n'est pas remise à
zéro.
### Tests
Tests unitaires de l'apprentissage, du limiteur (blocage d'hôte,
correction figée, annulation BULK, séparation hôte/route, repli quand la
base est indisponible), de l'intercepteur, du Steam Store, des
contrôleurs, du rendu et du JS.
Localement, seuls échouent les tests Testcontainers/full-context
habituels du sandbox.
**Modules à redémarrer : catapult-api (migration V107) et
catapult-web.**
Report de `v1.1.0` (#100) de `beta` vers `dev`. Fusionne cette PR par commit de merge pour ramener la release sur `dev` ; tant qu'elle est ouverte, la promotion depuis et vers `dev` est refusée.
…y or monthly limit
… usage, updated live
## Clés & limites : un seul onglet, mis à jour en direct - **Regroupement** : les limites de débit apprises sont désormais dans l'onglet Clés, renommé « Clés & limites ». La table des limites vient après les cartes des clés Steam, DTDD et Minecraft. L'ancien lien `/admin#rate-limits` mène à cet onglet (nouvel attribut `data-hash-aliases` sur les onglets). - **Utilisation de chaque clé** (nouvelle colonne, comptée par l'instance de catapult-api qui répond) : - **Steam** : jetons pris dans la fenêtre sur 3 par 5 s (`SteamRateLimiter`), et appels du jour UTC sur la limite documentée de 100 000 (`steam.rate-limit.daily-limit`). - **DTDD** : appels de la dernière minute et du mois UTC, rapportés aux limites `X-RateLimit-Limit-*` annoncées par DTDD, ou à défaut à celles de l'offre gratuite, 30/min et 5000/mois (`dtdd.rate-limit.per-minute` / `per-month`). Le restant annoncé par DTDD est affiché à côté (celui de la minute seulement s'il a moins d'une minute). - **Mise à jour en direct** : tant que l'onglet est affiché, il est rafraîchi toutes les 3 s (`CatapultAdminTabs.patch`). Le rafraîchissement est suspendu quand un dialogue est ouvert ou que la page est cachée, et le champ d'ajout de clé n'est jamais touché. La liste des limites et son état vide sont tous deux rendus, l'un masqué, pour qu'un rafraîchissement fasse apparaître la première limite apprise. - **Mise en page** : la table des limites (`.admin-rl-row`) a enfin sa mise en page en grille, y compris sur mobile. **Modules à redémarrer : catapult-api et catapult-web.**
enimaloc
marked this pull request as ready for review
November 12, 2026 16:33
catapult-counterweight Bot
added a commit
that referenced
this pull request
Nov 12, 2026
Report de `v1.2.0` (#111) de `main` vers `dev`. L'auto-merge est activé : la PR se fusionne d'elle-même (commit de merge) dès que les checks passent ; tant qu'elle est ouverte, la promotion depuis et vers `dev` est refusée.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sors cette PR du draft pour publier la release ; elle fusionnera quand les checks seront verts. Pour la programmer, indique ci-dessous une date
AAAA-MM-JJ HH:MM, en heure de Paris.Sortie programmée :
Features
Fixes
Refactoring
Other