496 lines
24 KiB
Markdown
496 lines
24 KiB
Markdown
# oc-discovery — Architecture et analyse technique
|
||
|
||
> **Convention de lecture**
|
||
> Les points marqués ✅ ont été corrigés dans le code. Les points marqués ⚠️ restent ouverts.
|
||
|
||
## Table des matières
|
||
|
||
1. [Vue d'ensemble](#1-vue-densemble)
|
||
2. [Hiérarchie des rôles](#2-hiérarchie-des-rôles)
|
||
3. [Mécanismes principaux](#3-mécanismes-principaux)
|
||
- 3.1 Heartbeat long-lived (node → indexer)
|
||
- 3.2 Scoring de confiance
|
||
- 3.3 Enregistrement auprès des natifs (indexer → native)
|
||
- 3.4 Pool d'indexeurs : fetch + consensus
|
||
- 3.5 Self-delegation et offload loop
|
||
- 3.6 Résilience du mesh natif
|
||
- 3.7 DHT partagée
|
||
- 3.8 PubSub gossip (indexer registry)
|
||
- 3.9 Streams applicatifs (node ↔ node)
|
||
4. [Tableau récapitulatif](#4-tableau-récapitulatif)
|
||
5. [Risques et limites globaux](#5-risques-et-limites-globaux)
|
||
6. [Pistes d'amélioration](#6-pistes-damélioration)
|
||
|
||
---
|
||
|
||
## 1. Vue d'ensemble
|
||
|
||
`oc-discovery` est un service de découverte P2P pour le réseau OpenCloud. Il repose sur
|
||
**libp2p** (transport TCP + PSK réseau privé) et une **DHT Kademlia** (préfixe `oc`)
|
||
pour indexer les pairs. L'architecture est intentionnellement hiérarchique : des _natifs_
|
||
stables servent de hubs autoritaires auxquels des _indexeurs_ s'enregistrent, et des _nœuds_
|
||
ordinaires découvrent des indexeurs via ces natifs.
|
||
|
||
```
|
||
┌──────────────┐ heartbeat ┌──────────────────┐
|
||
│ Node │ ───────────────────► │ Indexer │
|
||
│ (libp2p) │ ◄─────────────────── │ (DHT server) │
|
||
└──────────────┘ stream applicatif └────────┬─────────┘
|
||
│ subscribe / heartbeat
|
||
▼
|
||
┌──────────────────┐
|
||
│ Native Indexer │◄──► autres natifs
|
||
│ (hub autoritaire│ (mesh)
|
||
└──────────────────┘
|
||
```
|
||
|
||
Tous les participants partagent une **clé pré-partagée (PSK)** qui isole le réseau
|
||
des connexions libp2p externes non autorisées.
|
||
|
||
---
|
||
|
||
## 2. Hiérarchie des rôles
|
||
|
||
| Rôle | Binaire | Responsabilité |
|
||
|---|---|---|
|
||
| **Node** | `node_mode=node` | Se fait indexer, publie/consulte des records DHT |
|
||
| **Indexer** | `node_mode=indexer` | Reçoit les heartbeats, écrit en DHT, s'enregistre auprès des natifs |
|
||
| **Native Indexer** | `node_mode=native` | Hub : tient le registre des indexeurs vivants, évalue le consensus, sert de fallback |
|
||
|
||
Un même processus peut cumuler les rôles node+indexer ou indexer+native.
|
||
|
||
---
|
||
|
||
## 3. Mécanismes principaux
|
||
|
||
### 3.1 Heartbeat long-lived (node → indexer)
|
||
|
||
**Fonctionnement**
|
||
|
||
Un stream libp2p **persistant** (`/opencloud/heartbeat/1.0`) est ouvert depuis le nœud
|
||
vers chaque indexeur de son pool (`StaticIndexers`). Toutes les 20 secondes, le nœud
|
||
envoie un `Heartbeat` JSON sur ce stream. L'indexeur répond en enregistrant le peer dans
|
||
`StreamRecords[ProtocolHeartbeat]` avec une expiry de 2 min.
|
||
|
||
Si `sendHeartbeat` échoue (stream reset, EOF, timeout), le peer est retiré de
|
||
`StaticIndexers` et `replenishIndexersFromNative` est déclenché.
|
||
|
||
**Avantages**
|
||
- Détection rapide de déconnexion (erreur sur le prochain encode).
|
||
- Un seul stream par pair réduit la pression sur les connexions TCP.
|
||
- Le channel de nudge (`indexerHeartbeatNudge`) permet un reconnect immédiat sans
|
||
attendre le ticker de 20 s.
|
||
|
||
**Limites / risques**
|
||
- ⚠️ Un seul stream persistant : si la couche TCP reste ouverte mais "gelée" (middlebox,
|
||
NAT silencieux), l'erreur peut ne pas remonter avant plusieurs minutes.
|
||
- ⚠️ `StaticIndexers` est une map partagée globale : si deux goroutines appellent
|
||
`replenishIndexersFromNative` simultanément (cas de perte multiple), on peut avoir
|
||
des écritures concurrentes non protégées hors des sections critiques.
|
||
|
||
---
|
||
|
||
### 3.2 Scoring de confiance
|
||
|
||
**Fonctionnement**
|
||
|
||
Avant d'enregistrer un heartbeat dans `StreamRecords`, l'indexeur vérifie un **score
|
||
minimum** calculé par `CheckHeartbeat` :
|
||
|
||
```
|
||
Score = (0.4 × uptime_ratio + 0.4 × bpms + 0.2 × diversity) × 100
|
||
```
|
||
|
||
- `uptime_ratio` : durée de présence du peer / durée depuis le démarrage de l'indexeur.
|
||
- `bpms` : débit mesuré via un stream dédié (`/opencloud/probe/1.0`) normalisé par 50 Mbps.
|
||
- `diversity` : ratio d'IP /24 distincts parmi les indexeurs que le peer déclare.
|
||
|
||
Deux seuils sont appliqués selon l'état du peer :
|
||
- **Premier heartbeat** (peer absent de `StreamRecords`, uptime = 0) : seuil à **40**.
|
||
- **Heartbeats suivants** (uptime accumulé) : seuil à **75**.
|
||
|
||
**Avantages**
|
||
- Décourage les peers éphémères ou lents d'encombrer le registre.
|
||
- La diversité réseau réduit le risque de concentration sur un seul sous-réseau.
|
||
- Le stream de probe dédié évite de polluer le stream JSON heartbeat avec des données binaires.
|
||
- Le double seuil permet aux nouveaux peers d'être admis dès leur première connexion.
|
||
|
||
**Limites / risques**
|
||
- ✅ **Deadlock logique de démarrage corrigé** : avec uptime = 0 le score maximal était 60,
|
||
en-dessous du seuil de 75. Les nouveaux peers étaient silencieusement rejetés à jamais.
|
||
→ Seuil abaissé à **40** pour le premier heartbeat (`isFirstHeartbeat`), 75 ensuite.
|
||
- ⚠️ Les seuils (40 / 75) restent câblés en dur, sans possibilité de configuration.
|
||
- ⚠️ La mesure de bande passante envoie entre 512 et 2048 octets par heartbeat : à 20 s
|
||
d'intervalle et 500 nœuds max, cela représente ~50 KB/s de trafic probe en continu.
|
||
- ⚠️ `diversity` est calculé sur les adresses que le nœud *déclare* avoir — ce champ est
|
||
auto-rapporté et non vérifié, facilement falsifiable.
|
||
|
||
---
|
||
|
||
### 3.3 Enregistrement auprès des natifs (indexer → native)
|
||
|
||
**Fonctionnement**
|
||
|
||
Chaque indexeur (non-natif) envoie périodiquement (toutes les 60 s) une
|
||
`IndexerRegistration` JSON sur un stream one-shot (`/opencloud/native/subscribe/1.0`)
|
||
vers chaque natif configuré. Le natif :
|
||
|
||
1. Stocke l'entrée en cache local avec un TTL de **90 s** (`IndexerTTL`).
|
||
2. Gossipe le `PeerID` sur le topic PubSub `oc-indexer-registry` aux autres natifs.
|
||
3. Persiste l'entrée en DHT de manière asynchrone (retry jusqu'à succès).
|
||
|
||
**Avantages**
|
||
- Stream jetable : pas de ressource longue durée côté natif pour les enregistrements.
|
||
- Le cache local est immédiatement disponible pour `handleNativeGetIndexers` sans
|
||
attendre la DHT.
|
||
- La dissémination PubSub permet à d'autres natifs de connaître l'indexeur sans
|
||
qu'il ait besoin de s'y enregistrer directement.
|
||
|
||
**Limites / risques**
|
||
- ✅ **TTL trop serré corrigé** : le TTL de 66 s n'était que 10 % au-dessus de l'intervalle
|
||
de 60 s — un léger retard réseau pouvait expirer un indexeur sain entre deux renewals.
|
||
→ `IndexerTTL` porté à **90 s** (+50 %).
|
||
- ⚠️ Si le `PutValue` DHT échoue définitivement (réseau partitionné), le natif possède
|
||
l'entrée mais les autres natifs qui n'ont pas reçu le message PubSub ne la connaissent
|
||
jamais — incohérence silencieuse.
|
||
- ⚠️ `RegisterWithNative` ignore les adresses en `127.0.0.1`, mais ne gère pas
|
||
les adresses privées (RFC1918) qui seraient non routables depuis d'autres hôtes.
|
||
|
||
---
|
||
|
||
### 3.4 Pool d'indexeurs : fetch + consensus
|
||
|
||
**Fonctionnement**
|
||
|
||
Lors de `ConnectToNatives` (démarrage ou replenish), le nœud/indexeur :
|
||
|
||
1. **Fetch** : envoie `GetIndexersRequest` au premier natif répondant
|
||
(`/opencloud/native/indexers/1.0`), reçoit une liste de candidats.
|
||
2. **Consensus (round 1)** : interroge **tous** les natifs configurés en parallèle
|
||
(`/opencloud/native/consensus/1.0`, timeout 3 s, collecte sur 4 s).
|
||
Un indexeur est confirmé si **strictement plus de 50 %** des natifs répondants
|
||
le considèrent vivant.
|
||
3. **Consensus (round 2)** : si le pool est insuffisant, les suggestions des natifs
|
||
(indexeurs qu'ils connaissent mais qui n'étaient pas dans les candidats initiaux)
|
||
sont soumises à un second round.
|
||
|
||
**Avantages**
|
||
- La règle de majorité absolue empêche un natif compromis ou désynchronisé d'injecter
|
||
des indexeurs fantômes.
|
||
- Le double round permet de compléter le pool avec des alternatives connues des natifs
|
||
sans sacrifier la vérification.
|
||
- Si le fetch retourne un **fallback** (natif comme indexeur), le consensus est skippé —
|
||
cohérent car il n'y a qu'une seule source.
|
||
|
||
**Limites / risques**
|
||
- ⚠️ Avec **un seul natif** configuré (très courant en dev/test), le consensus est trivial
|
||
(100 % d'un seul vote) — la règle de majorité ne protège rien dans ce cas.
|
||
- ⚠️ `fetchIndexersFromNative` s'arrête au **premier natif répondant** (séquentiellement) :
|
||
si ce natif a un cache périmé ou partiel, le nœud obtient un pool sous-optimal sans
|
||
consulter les autres.
|
||
- ⚠️ Le timeout de collecte global (4 s) est fixe : sur un réseau lent ou géographiquement
|
||
distribué, des natifs valides peuvent être éliminés faute de réponse à temps.
|
||
- ⚠️ `replaceStaticIndexers` **ajoute** sans jamais retirer d'anciens indexeurs expirés :
|
||
le pool peut accumuler des entrées mortes que seul le heartbeat purge ensuite.
|
||
|
||
---
|
||
|
||
### 3.5 Self-delegation et offload loop
|
||
|
||
**Fonctionnement**
|
||
|
||
Si un natif ne dispose d'aucun indexeur vivant lors d'un `handleNativeGetIndexers`,
|
||
il se désigne lui-même comme indexeur temporaire (`selfDelegate`) : il retourne sa propre
|
||
adresse multiaddr et ajoute le demandeur dans `responsiblePeers`, dans la limite de
|
||
`maxFallbackPeers` (50). Au-delà, la délégation est refusée et une réponse vide est
|
||
retournée pour que le nœud tente un autre natif.
|
||
|
||
Toutes les 30 s, `runOffloadLoop` vérifie si des indexeurs réels sont de nouveau
|
||
disponibles. Si oui, pour chaque peer responsable :
|
||
- **Stream présent** : `Reset()` du stream heartbeat — le peer reçoit une erreur,
|
||
déclenche `replenishIndexersFromNative` et migre vers de vrais indexeurs.
|
||
- **Stream absent** (peer jamais admis par le scoring) : `ClosePeer()` sur la connexion
|
||
réseau — le peer reconnecte et re-demande ses indexeurs au natif.
|
||
|
||
**Avantages**
|
||
- Continuité de service : un nœud n'est jamais bloqué en l'absence temporaire d'indexeurs.
|
||
- La migration est automatique et transparente pour le nœud.
|
||
- `Reset()` (vs `Close()`) interrompt les deux sens du stream, garantissant que le peer
|
||
reçoit bien une erreur.
|
||
- La limite de 50 empêche le natif de se retrouver surchargé lors de pénuries prolongées.
|
||
|
||
**Limites / risques**
|
||
- ✅ **Offload sans stream corrigé** : si le heartbeat n'avait jamais été enregistré dans
|
||
`StreamRecords` (score < seuil — cas amplifié par le bug de scoring), l'offload
|
||
échouait silencieusement et le peer restait dans `responsiblePeers` indéfiniment.
|
||
→ Branche `else` : `ClosePeer()` + suppression de `responsiblePeers`.
|
||
- ✅ **`responsiblePeers` illimité corrigé** : le natif acceptait un nombre arbitraire
|
||
de peers en self-delegation, devenant lui-même un indexeur surchargé.
|
||
→ `selfDelegate` vérifie `len(responsiblePeers) >= maxFallbackPeers` et retourne
|
||
`false` si saturé.
|
||
- ⚠️ La délégation reste non coordonnée entre natifs : un natif surchargé refuse (retourne
|
||
vide) mais ne redirige pas explicitement vers un natif voisin qui aurait de la capacité.
|
||
|
||
---
|
||
|
||
### 3.6 Résilience du mesh natif
|
||
|
||
**Fonctionnement**
|
||
|
||
Quand le heartbeat vers un natif échoue, `replenishNativesFromPeers` tente de trouver
|
||
un remplaçant dans cet ordre :
|
||
|
||
1. `fetchNativeFromNatives` : demande à chaque natif vivant (`/opencloud/native/peers/1.0`)
|
||
une adresse de natif inconnue.
|
||
2. `fetchNativeFromIndexers` : demande à chaque indexeur connu
|
||
(`/opencloud/indexer/natives/1.0`) ses natifs configurés.
|
||
3. Si aucun remplaçant et `remaining ≤ 1` : `retryLostNative` relance un ticker de 30 s
|
||
qui retente la connexion directe au natif perdu.
|
||
|
||
`EnsureNativePeers` maintient des heartbeats de natif à natif via `ProtocolHeartbeat`,
|
||
avec une **unique goroutine** couvrant toute la map `StaticNatives`.
|
||
|
||
**Avantages**
|
||
- Le gossip multi-hop via indexeurs permet de retrouver un natif même si aucun pair
|
||
direct ne le connaît.
|
||
- `retryLostNative` gère le cas d'un seul natif (déploiement minimal).
|
||
- La reconnexion automatique (`retryLostNative`) déclenche `replenishIndexersIfNeeded`
|
||
pour restaurer aussi le pool d'indexeurs.
|
||
|
||
**Limites / risques**
|
||
- ✅ **Goroutines heartbeat multiples corrigé** : `EnsureNativePeers` démarrait une
|
||
goroutine `SendHeartbeat` par adresse native (N natifs → N goroutines → N² heartbeats
|
||
par tick). → Utilisation de `nativeMeshHeartbeatOnce` : une seule goroutine itère sur
|
||
`StaticNatives`.
|
||
- ⚠️ `retryLostNative` tourne indéfiniment sans condition d'arrêt liée à la vie du processus
|
||
(pas de `context.Context`). Si le binaire est gracefully shutdown, cette goroutine
|
||
peut bloquer.
|
||
- ⚠️ La découverte transitoire (natif → indexeur → natif) est à sens unique : un indexeur
|
||
ne connaît que les natifs de sa propre config, pas les nouveaux natifs qui auraient
|
||
rejoint après son démarrage.
|
||
|
||
---
|
||
|
||
### 3.7 DHT partagée
|
||
|
||
**Fonctionnement**
|
||
|
||
Tous les indexeurs et natifs participent à une DHT Kademlia (préfixe `oc`, mode
|
||
`ModeServer`). Deux namespaces sont utilisés :
|
||
|
||
- `/node/<DID>` → `PeerRecord` JSON signé (publié par les indexeurs sur heartbeat de nœud).
|
||
- `/indexer/<PeerID>` → `liveIndexerEntry` JSON avec TTL (publié par les natifs).
|
||
|
||
Chaque natif lance `refreshIndexersFromDHT` (toutes les 30 s) qui ré-hydrate son cache
|
||
local depuis la DHT pour les PeerIDs connus (`knownPeerIDs`) dont l'entrée locale a expiré.
|
||
|
||
**Avantages**
|
||
- Persistance décentralisée : un record survit à la perte d'un seul natif ou indexeur.
|
||
- Validation des entrées : `PeerRecordValidator` et `IndexerRecordValidator` rejettent
|
||
les records malformés ou expirés au moment du `PutValue`.
|
||
- L'index secondaire `/name/<name>` permet la résolution par nom humain.
|
||
|
||
**Limites / risques**
|
||
- ⚠️ La DHT Kademlia en réseau privé (PSK) est fonctionnelle mais les nœuds bootstrap
|
||
ne sont pas configurés explicitement : la découverte dépend de connexions déjà établies,
|
||
ce qui peut ralentir la convergence au démarrage.
|
||
- ⚠️ `PutValue` est réessayé en boucle infinie si `"failed to find any peer in table"` —
|
||
une panne de réseau prolongée génère des goroutines bloquées.
|
||
- ⚠️ Si la PSK est compromise, un attaquant peut écrire dans la DHT ; les `liveIndexerEntry`
|
||
d'indexeurs ne sont pas signées, contrairement aux `PeerRecord`.
|
||
- ⚠️ `refreshIndexersFromDHT` prune `knownPeerIDs` si la DHT n'a aucune entrée fraîche,
|
||
mais ne prune pas `liveIndexers` — une entrée expirée reste en mémoire jusqu'au GC
|
||
ou au prochain refresh.
|
||
|
||
---
|
||
|
||
### 3.8 PubSub gossip (indexer registry)
|
||
|
||
**Fonctionnement**
|
||
|
||
Quand un indexeur s'enregistre auprès d'un natif, ce dernier publie l'adresse sur le
|
||
topic GossipSub `oc-indexer-registry`. Les autres natifs abonnés mettent à jour leur
|
||
`knownPeerIDs` sans attendre la DHT.
|
||
|
||
Le `TopicValidator` rejette tout message dont le contenu n'est pas un multiaddr
|
||
parseable valide avant qu'il n'atteigne la boucle de traitement.
|
||
|
||
**Avantages**
|
||
- Dissémination quasi-instantanée entre natifs connectés.
|
||
- Complément utile à la DHT pour les registrations récentes qui n'ont pas encore
|
||
été persistées.
|
||
- Le filtre syntaxique bloque les messages malformés avant propagation dans le mesh.
|
||
|
||
**Limites / risques**
|
||
- ✅ **`TopicValidator` sans validation corrigé** : le validateur acceptait systématiquement
|
||
tous les messages (`return true`), permettant à un natif compromis de gossiper
|
||
n'importe quelle donnée.
|
||
→ Le validateur vérifie désormais que le message est un multiaddr parseable
|
||
(`pp.AddrInfoFromString`).
|
||
- ⚠️ La validation reste syntaxique uniquement : l'origine du message (l'émetteur
|
||
est-il un natif légitime ?) n'est pas vérifiée.
|
||
- ⚠️ Si le natif redémarre, il perd son abonnement et manque les messages publiés
|
||
pendant son absence. La re-hydratation depuis la DHT compense, mais avec un délai
|
||
pouvant aller jusqu'à 30 s.
|
||
- ⚠️ Le gossip ne porte que le `Addr` de l'indexeur, pas sa TTL ni sa signature.
|
||
|
||
---
|
||
|
||
### 3.9 Streams applicatifs (node ↔ node)
|
||
|
||
**Fonctionnement**
|
||
|
||
`StreamService` gère les streams entre nœuds partenaires (relations `PARTNER` stockées
|
||
en base) via des protocols dédiés (`/opencloud/resource/*`). Un heartbeat partenaire
|
||
(`ProtocolHeartbeatPartner`) maintient les connexions actives. Les events sont routés
|
||
via `handleEvent` et le système NATS en parallèle.
|
||
|
||
**Avantages**
|
||
- TTL par protocol (`PersistantStream`, `WaitResponse`) adapte le comportement au
|
||
type d'échange (longue durée pour le planner, courte pour les CRUDs).
|
||
- La GC (`gc()` toutes les 8 s, démarrée une seule fois dans `InitStream`) libère
|
||
rapidement les streams expirés.
|
||
|
||
**Limites / risques**
|
||
- ✅ **Fuite de goroutines GC corrigée** : `HandlePartnerHeartbeat` appelait
|
||
`go s.StartGC(30s)` à chaque heartbeat reçu (~20 s), créant un nouveau ticker
|
||
goroutine infini à chaque appel.
|
||
→ Appel supprimé ; la GC lancée par `InitStream` est suffisante.
|
||
- ✅ **Boucle infinie sur EOF corrigée** : `readLoop` effectuait `s.Stream.Close();
|
||
continue` après une erreur de décodage, re-tentant indéfiniment de lire un stream
|
||
fermé.
|
||
→ Remplacé par `return` ; les defers (`Close`, `delete`) nettoient correctement.
|
||
- ⚠️ La récupération de partenaires depuis `conf.PeerIDS` est marquée `TO REMOVE` :
|
||
présence de code provisoire en production.
|
||
|
||
---
|
||
|
||
## 4. Tableau récapitulatif
|
||
|
||
| Mécanisme | Protocole | Avantage principal | État du risque |
|
||
|---|---|---|---|
|
||
| Heartbeat node→indexer | `/opencloud/heartbeat/1.0` | Détection rapide de perte | ⚠️ Stream TCP gelé non détecté |
|
||
| Scoring de confiance | (inline dans heartbeat) | Filtre les pairs instables | ✅ Deadlock corrigé (seuil 40/75) |
|
||
| Enregistrement natif | `/opencloud/native/subscribe/1.0` | TTL ample, cache immédiat | ✅ TTL porté à 90 s |
|
||
| Fetch pool d'indexeurs | `/opencloud/native/indexers/1.0` | Prend le 1er natif répondant | ⚠️ Natif au cache périmé possible |
|
||
| Consensus | `/opencloud/native/consensus/1.0` | Majorité absolue | ⚠️ Trivial avec 1 seul natif |
|
||
| Self-delegation + offload | (in-memory) | Disponibilité sans indexeur | ✅ Limite 50 peers + ClosePeer |
|
||
| Mesh natif | `/opencloud/native/peers/1.0` | Gossip multi-hop | ✅ Goroutines dédupliquées |
|
||
| DHT | `/oc/kad/1.0.0` | Persistance décentralisée | ⚠️ Retry infini, pas de bootstrap |
|
||
| PubSub registry | `oc-indexer-registry` | Dissémination rapide | ✅ Validation multiaddr |
|
||
| Streams applicatifs | `/opencloud/resource/*` | TTL par protocol | ✅ Fuite GC + EOF corrigés |
|
||
|
||
---
|
||
|
||
## 5. Risques et limites globaux
|
||
|
||
### Sécurité
|
||
|
||
- ⚠️ **Adresses auto-rapportées non vérifiées** : le champ `IndexersBinded` dans le heartbeat
|
||
est auto-déclaré par le nœud et sert à calculer la diversité. Un pair malveillant peut
|
||
gonfler son score en déclarant de fausses adresses.
|
||
- ⚠️ **PSK comme seule barrière d'entrée** : si la PSK est compromise (elle est statique et
|
||
fichier-based), tout l'isolement réseau saute. Il n'y a pas de rotation de clé ni
|
||
d'authentification supplémentaire par pair.
|
||
- ⚠️ **DHT sans ACL sur les entrées indexeur** : la signature des `PeerRecord` est vérifiée
|
||
à la lecture, mais les `liveIndexerEntry` ne sont pas signées. La validation PubSub
|
||
bloque les multiaddrs invalides mais pas les adresses d'indexeurs légitimes usurpées.
|
||
|
||
### Disponibilité
|
||
|
||
- ⚠️ **Single point of failure natif** : avec un seul natif, la perte de celui-ci stoppe
|
||
toute attribution d'indexeurs. `retryLostNative` pallie, mais sans indexeurs, les nœuds
|
||
ne peuvent pas publier.
|
||
- ⚠️ **Bootstrap DHT** : sans nœuds bootstrap explicites, la DHT met du temps à converger
|
||
si les connexions initiales sont peu nombreuses.
|
||
|
||
### Cohérence
|
||
|
||
- ⚠️ **`replaceStaticIndexers` n'efface jamais** : d'anciens indexeurs morts restent dans
|
||
`StaticIndexers` jusqu'à ce que le heartbeat échoue. Un nœud peut avoir un pool
|
||
surévalué contenant des entrées inatteignables.
|
||
- ⚠️ **`TimeWatcher` global** : défini une seule fois au démarrage de `ConnectToIndexers`.
|
||
Si l'indexeur tourne depuis longtemps, les nouveaux nœuds auront un `uptime_ratio`
|
||
durablement faible. Le seuil abaissé à 40 pour le premier heartbeat atténue l'impact
|
||
initial, mais les heartbeats suivants devront accumuler un uptime suffisant.
|
||
|
||
---
|
||
|
||
## 6. Pistes d'amélioration
|
||
|
||
Les pistes déjà implémentées sont marquées ✅. Les pistes ouvertes restent à traiter.
|
||
|
||
### ✅ Score : double seuil pour les nouveaux peers
|
||
~~Remplacer le seuil binaire~~ — **Implémenté** : seuil à 40 pour le premier heartbeat
|
||
(peer absent de `StreamRecords`), 75 pour les suivants. Un peer peut désormais être admis
|
||
dès sa première connexion sans bloquer sur l'uptime nul.
|
||
_Fichier : `common/common_stream.go`, `CheckHeartbeat`_
|
||
|
||
### ✅ TTL indexeur aligné avec l'intervalle de renouvellement
|
||
~~TTL de 66 s trop proche de 60 s~~ — **Implémenté** : `IndexerTTL` passé à **90 s**.
|
||
_Fichier : `indexer/native.go`_
|
||
|
||
### ✅ Limite de la self-delegation
|
||
~~`responsiblePeers` illimité~~ — **Implémenté** : `selfDelegate` retourne `false` quand
|
||
`len(responsiblePeers) >= maxFallbackPeers` (50). Le site d'appel retourne une réponse
|
||
vide et logue un warning.
|
||
_Fichier : `indexer/native.go`_
|
||
|
||
### ✅ Validation PubSub des adresses gossipées
|
||
~~`TopicValidator` accepte tout~~ — **Implémenté** : le validateur vérifie que le message
|
||
est un multiaddr parseable via `pp.AddrInfoFromString`.
|
||
_Fichier : `indexer/native.go`, `subscribeIndexerRegistry`_
|
||
|
||
### ✅ Goroutines heartbeat dédupliquées dans `EnsureNativePeers`
|
||
~~Une goroutine par adresse native~~ — **Implémenté** : `nativeMeshHeartbeatOnce`
|
||
garantit qu'une seule goroutine `SendHeartbeat` couvre toute la map `StaticNatives`.
|
||
_Fichier : `common/native_stream.go`_
|
||
|
||
### ✅ Fuite de goroutines GC dans `HandlePartnerHeartbeat`
|
||
~~`go s.StartGC(30s)` à chaque heartbeat~~ — **Implémenté** : appel supprimé ; la GC
|
||
de `InitStream` est suffisante.
|
||
_Fichier : `stream/service.go`_
|
||
|
||
### ✅ Boucle infinie sur EOF dans `readLoop`
|
||
~~`continue` après `Stream.Close()`~~ — **Implémenté** : remplacé par `return` pour
|
||
laisser les defers nettoyer proprement.
|
||
_Fichier : `stream/service.go`_
|
||
|
||
---
|
||
|
||
### ⚠️ Fetch pool : interroger tous les natifs en parallèle
|
||
|
||
`fetchIndexersFromNative` s'arrête au premier natif répondant. Interroger tous les natifs
|
||
en parallèle et fusionner les listes (similairement à `clientSideConsensus`) éviterait
|
||
qu'un natif au cache périmé fournisse un pool sous-optimal.
|
||
|
||
### ⚠️ Consensus avec quorum configurable
|
||
|
||
Le seuil de confirmation (`count*2 > total`) est câblé en dur. Le rendre configurable
|
||
(ex. `consensus_quorum: 0.67`) permettrait de durcir la règle sur des déploiements
|
||
à 3+ natifs sans modifier le code.
|
||
|
||
### ⚠️ Désenregistrement explicite
|
||
|
||
Ajouter un protocole `/opencloud/native/unsubscribe/1.0` : quand un indexeur s'arrête
|
||
proprement, il notifie les natifs pour invalider son TTL immédiatement plutôt qu'attendre
|
||
90 s.
|
||
|
||
### ⚠️ Bootstrap DHT explicite
|
||
|
||
Configurer les natifs comme nœuds bootstrap DHT via `dht.BootstrapPeers` pour accélérer
|
||
la convergence Kademlia au démarrage.
|
||
|
||
### ⚠️ Context propagé dans les goroutines longue durée
|
||
|
||
`retryLostNative`, `refreshIndexersFromDHT` et `runOffloadLoop` ne reçoivent aucun
|
||
`context.Context`. Les passer depuis `InitNative` permettrait un arrêt propre lors du
|
||
shutdown du processus.
|
||
|
||
### ⚠️ Redirection explicite lors du refus de self-delegation
|
||
|
||
Quand un natif refuse la self-delegation (pool saturé), retourner vide force le nœud à
|
||
réessayer sans lui indiquer vers qui se tourner. Une liste de natifs alternatifs dans la
|
||
réponse (`AlternativeNatives []string`) permettrait au nœud de trouver directement un
|
||
natif moins chargé.
|