package workflow_builder import ( "encoding/json" "fmt" "strconv" "sync" "cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/tools" ) // ── considersCache (signal-only) ───────────────────────────────────────────── // considersCache stocke les canaux en attente d'un PB_CONSIDERS, // indexés par "executionsId:dataType". Un même message NATS réveille // tous les waiters enregistrés sous la même clé (broadcast). type considersCache struct { mu sync.Mutex pending map[string][]chan struct{} } var globalConsidersCache = &considersCache{ pending: make(map[string][]chan struct{}), } // considersKey construit la clé du cache à partir de l'ID d'exécution, // du type de données et du peer compute (SourcePeerID). // peerID permet de différencier plusieurs waiters COMPUTE_RESOURCE du même // executionsId (1 local + N distants en parallèle). func considersKey(executionsId string, dataType tools.DataType, peerID string) string { key := executionsId + ":" + strconv.Itoa(dataType.EnumIndex()) if peerID != "" { key += ":" + peerID } return key } // register inscrit un nouveau canal d'attente pour la clé donnée. // Retourne le canal à lire et une fonction de désinscription à appeler en defer. func (c *considersCache) register(key string) (<-chan struct{}, func()) { ch := make(chan struct{}, 1) c.mu.Lock() c.pending[key] = append(c.pending[key], ch) c.mu.Unlock() unregister := func() { c.mu.Lock() defer c.mu.Unlock() list := c.pending[key] for i, existing := range list { if existing == ch { c.pending[key] = append(list[:i], list[i+1:]...) break } } if len(c.pending[key]) == 0 { delete(c.pending, key) } } return ch, unregister } // confirm réveille tous les waiters enregistrés sous la clé donnée // et les supprime du cache. func (c *considersCache) confirm(key string) { c.mu.Lock() list := c.pending[key] delete(c.pending, key) c.mu.Unlock() for _, ch := range list { select { case ch <- struct{}{}: default: } } } // ── sourcePresignedCache (value-bearing) ───────────────────────────────────── // sourcePresignedCache stocke les canaux en attente d'une URL pré-signée pour // une source privée (isReachable=false), indexés par la clé sourceConsidersKey. // La valeur transportée est l'URL pré-signée elle-même. type sourcePresignedCache struct { mu sync.Mutex pending map[string][]chan string } var globalSourceCache = &sourcePresignedCache{ pending: make(map[string][]chan string), } // sourceConsidersKey construit une clé unique pour une demande de source privée. // La clé encode l'executionsID, le peerID du propriétaire et le resourceID // pour permettre des requêtes parallèles distinctes. func sourceConsidersKey(executionsID, peerID, resourceID string) string { return executionsID + ":src:" + peerID + ":" + resourceID } // register inscrit un nouveau canal d'attente pour la clé donnée. // Retourne le canal à lire et une fonction de désinscription à appeler en defer. func (c *sourcePresignedCache) register(key string) (<-chan string, func()) { ch := make(chan string, 1) c.mu.Lock() c.pending[key] = append(c.pending[key], ch) c.mu.Unlock() unregister := func() { c.mu.Lock() defer c.mu.Unlock() list := c.pending[key] for i, existing := range list { if existing == ch { c.pending[key] = append(list[:i], list[i+1:]...) break } } if len(c.pending[key]) == 0 { delete(c.pending, key) } } return ch, unregister } // confirm réveille tous les waiters enregistrés sous la clé donnée // en leur transmettant l'URL pré-signée, puis les supprime du cache. func (c *sourcePresignedCache) confirm(key, url string) { c.mu.Lock() list := c.pending[key] delete(c.pending, key) c.mu.Unlock() for _, ch := range list { select { case ch <- url: default: } } } // ── StartConsidersListener ──────────────────────────────────────────────────── // StartConsidersListener démarre un abonné NATS global via ListenNats (oclib) // qui reçoit les messages CONSIDERS_EVENT et réveille les goroutines en attente. // // Deux chemins de dispatch : // - Si presigned_url est présent dans le payload → globalSourceCache (Phase 4). // - Sinon → globalConsidersCache (Phases COMPUTE / STORAGE, signal sans valeur). // // Doit être appelé une seule fois au démarrage. func StartConsidersListener() { log := logs.GetLogger() log.Info().Msg("Considers NATS listener starting on " + tools.CONSIDERS_EVENT.GenerateKey()) go tools.NewNATSCaller().ListenNats(map[tools.NATSMethod]func(tools.NATSResponse){ tools.CONSIDERS_EVENT: func(resp tools.NATSResponse) { fmt.Println("CONSIDERS") var body struct { ExecutionsID string `json:"executions_id"` PeerID string `json:"peer_id,omitempty"` // PresignedURL est non-vide uniquement pour les réponses de source privée (Phase 4). PresignedURL string `json:"presigned_url,omitempty"` // ResourceID identifie la ressource Processing/Data pour la Phase 4. ResourceID string `json:"resource_id,omitempty"` } if err := json.Unmarshal(resp.Payload, &body); err != nil { log.Error().Msg("CONSIDERS_EVENT: cannot unmarshal payload: " + err.Error()) return } if body.PresignedURL != "" { // Phase 4 — source privée : transmettre l'URL pré-signée. key := sourceConsidersKey(body.ExecutionsID, body.PeerID, body.ResourceID) log.Info().Msg(fmt.Sprintf("CONSIDERS_EVENT (presigned) dispatched for key=%s", key)) globalSourceCache.confirm(key, body.PresignedURL) } else { // Phases COMPUTE / STORAGE — simple signal. key := considersKey(body.ExecutionsID, resp.Datatype, body.PeerID) log.Info().Msg(fmt.Sprintf("CONSIDERS_EVENT dispatched for key=%s", key)) globalConsidersCache.confirm(key) } }, }) }