package index import ( "context" "fmt" "maps" "reflect" "slices" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) // The new-form fingerprint of a module is written, and written again with no merge, when what it is of changes // (novox/hq ADR 0465 clause 2; issue 1444): a module's tree and lock hashes come with a merge and are kept in // modules..Inputs2; its bases' and its toolchain's come from here. The indexer holds no graph, so each recompute reads // the manifest of every module from manifests. and takes the modules in the order their bases come first. // // **No module's fingerprint takes the builder as an input** (novox/hq ADR 0530 §1, issue 1887 I1): the builder orders and // records builds and makes no artifact's bytes (ADR 0498 §1), so its Builder input is empty for every module, the builder's // own included; a change of how an artifact is built is a change of its build module's recipe (ADR 0530 §2). // ToolchainModules are the build module of each language, whose state toolchains (delivery.ToolchainsBucket) the // indexer reads: the language's agreed toolchain is the one every node's entry in it agrees on. var ToolchainModules = map[string]string{"go": "mesh-build-go", "typescript": "mesh-build-node"} // ToolchainStreams are the stream each build module carries its toolchain.reported event on, by module, as the bus names it // (a stream's local name in capitals, catalogue.StreamBusName): `toolchains-go` and `toolchains-node`, declared by the // build modules (streams, with the event toolchain.reported) and heard by this module (consumes). var ToolchainStreams = map[string]string{"mesh-build-go": "TOOLCHAINS_GO", "mesh-build-node": "TOOLCHAINS_NODE"} // PutToolchain takes one change of a build module's toolchains bucket in, without writing any module: a put of a node's // toolchain of a language (key ., delivery.ToolchainKey), or its delete. It answers whether what the // language's toolchain is of (version and recipe) changed. A key, or a value, that is not a toolchain of the module's // language is said in the log and left out. func (ix *Indexer) PutToolchain(module, key string, value []byte, deleted bool) bool { language := "" for l, m := range ToolchainModules { if m == module { language = l } } if language == "" { ix.logf("toolchains of %q are not read: it is no build module the indexer knows", module) return false } node, keyLanguage, err := delivery.ParseToolchainKey(key) if err != nil || keyLanguage != language { ix.logf("%s_toolchains.%s is not a %s toolchain's key, left out: %v", module, key, language, err) return false } ix.mu.Lock() defer ix.mu.Unlock() if deleted { if _, had := ix.toolchains[language][node]; !had { return false } delete(ix.toolchains[language], node) return true } tc, err := delivery.DecodeToolchain(value) if err != nil || tc.Language != language { ix.logf("%s_toolchains.%s is not a %s toolchain, left out: %v", module, key, language, err) return false } if ix.toolchains == nil { ix.toolchains = map[string]map[string]delivery.Toolchain{} } if ix.toolchains[language] == nil { ix.toolchains[language] = map[string]delivery.Toolchain{} } cur, had := ix.toolchains[language][node] ix.toolchains[language][node] = tc return !had || cur.Version != tc.Version || cur.Recipe != tc.Recipe } // ApplyToolchain takes one change of a build module's toolchains in (PutToolchain) and, when the language's toolchain // changed, writes the modules again that it moves (Recompute). The same version and recipe seen again writes nothing. func (ix *Indexer) ApplyToolchain(ctx context.Context, module, key string, value []byte, deleted bool) error { if !ix.PutToolchain(module, key, value, deleted) { return nil } return ix.Recompute(ctx) } // agreed is the toolchain of each language every reporting node agrees on, or why there is none. func (ix *Indexer) agreed() map[string]toolchainOrNone { ix.mu.Lock() defer ix.mu.Unlock() out := map[string]toolchainOrNone{} for language := range ToolchainModules { var entries []delivery.NodeToolchain for _, node := range slices.Sorted(maps.Keys(ix.toolchains[language])) { entries = append(entries, delivery.NodeToolchain{Node: node, Toolchain: ix.toolchains[language][node]}) } tc, none := delivery.AgreedToolchain(entries) out[language] = toolchainOrNone{tc, none} } return out } type toolchainOrNone struct { tc delivery.Toolchain2 none string } // known is a module the indexer holds the inputs of: its record as read, the manifest's shape and the key it is written at. type known struct { key string cur delivery.Module languages map[string]string bases []string contexts bool } // Recompute writes again every module whose new-form fingerprint, or reason for none, is no longer what it holds: from // the inputs kept at its merge (Inputs2), its manifest (manifests.), the fingerprints of its bases and of the // builder, and the toolchains the build modules report. A module is computed after its bases and the builder, whatever // the order the bucket lists them in; modules that stand on each other have a cycle as their reason. A module is written // only when its fingerprint or its reason changes, and only if its manifest is still the one computed from. A module with // no kept inputs (a message that carried no manifest) is left as it is. func (ix *Indexer) Recompute(ctx context.Context) error { return ix.recomputeWith(ctx, nil) } // recomputeWith is Recompute for a merge as well: the records the merge gives (overlay, by module key) stand in place // of what the bucket holds, are computed with the others, and are written once, with their final fingerprint or reason // (issue 1444: a merge writes each module once). A record of the overlay that has no kept inputs is written as it is. func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]delivery.Module) error { if err := ix.fillPaths(ctx); err != nil { return err } if err := ix.recomputeModules(ctx, overlay); err != nil { return err } // What the modules moved to reaches the candidates' fingerprints too (a base, the builder, a toolchain). return ix.RecomputeCandidates(ctx) } func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]delivery.Module) error { if ix.Modules == nil { return nil } if ix.Manifests == nil { return ix.writeOverlay(ctx, overlay, nil) } ix.recompute.Lock() defer ix.recompute.Unlock() keys, err := ix.Modules.Keys(ctx) if err != nil { return fmt.Errorf("listing the modules: %w", err) } for key := range overlay { if !slices.Contains(keys, key) { keys = append(keys, key) } } slices.Sort(keys) held := map[string]delivery.Module{} stale := map[string]bool{} // values an SDK that could not read their manifest wrote, judged again (issue 1923) all := map[string]*known{} for _, key := range keys { cur, ok := overlay[key] if !ok { value, _, found, err := ix.Modules.Get(ctx, key) if err != nil { return fmt.Errorf("modules.%s: %w", key, err) } if !found { continue } if cur, err = delivery.DecodeModule(value); err != nil { ix.logf("modules.%s holds what is not a module, left: %v", key, err) continue } } held[key] = cur if cur.Manifest == "" || cur.Inputs2 == nil { continue } raw, m, err := ix.manifestRaw(ctx, cur.Manifest) if err != nil { ix.logf("modules.%s: its manifest %s cannot be read, left: %v", key, cur.Manifest, err) continue } if again, ok := ix.rejudged(ctx, m, cur, raw); ok { cur, stale[key] = again, true held[key] = cur } all[key] = &known{key: key, cur: cur, languages: languagesOf(m), bases: basesOf(m), contexts: hasContext(m)} } tools := ix.agreed() type result struct { fp, none string tcs map[string]delivery.ToolchainUsed // by language, when there is a fingerprint } done := map[string]result{} visiting := map[string]bool{} var of func(module string, chain []string) result of = func(module string, chain []string) result { if r, ok := done[module]; ok { return r } k, ours := all[module] if !ours { // a module the indexer holds no inputs of: its record, as written cur, has := held[module] switch { case !has: return result{fp: "", none: "the indexer holds no record of it"} case cur.FP2 != "": return result{fp: cur.FP2, none: ""} case cur.None2 != "": return result{fp: "", none: cur.None2} } return result{fp: "", none: "its record carries no fingerprint of the new form"} } if visiting[module] { return result{fp: "", none: "a cycle of bases: " + strings.Join(append(chain, module), " -> ")} } visiting[module] = true defer delete(visiting, module) chain = append(chain, module) kept := *k.cur.Inputs2 kept.Bases, kept.Builder = nil, "" var missing []string for _, b := range k.bases { if r := of(b, chain); r.fp != "" { kept.Bases = append(kept.Bases, r.fp) } else { missing = append(missing, fmt.Sprintf("the base %s has no fingerprint (%s)", b, r.none)) } } slices.Sort(kept.Bases) slices.Sort(missing) toolchains := map[string]delivery.Toolchain2{} var noToolchain string for _, artifact := range slices.Sorted(maps.Keys(k.languages)) { t := tools[k.languages[artifact]] if t.none != "" { if noToolchain == "" { noToolchain = t.none } continue } toolchains[artifact] = t.tc } var r result switch { case k.contexts: r.none = None2Context case len(missing) > 0: r.none = strings.Join(missing, "; ") case noToolchain != "": r.none = noToolchain default: r.fp, r.none = delivery.SourceFingerprint2(kept.With(module, toolchains)) if r.fp != "" { r.tcs = toolchainsUsed(toolchains) } } done[module] = r k.cur.Inputs2 = &kept return r } finals := map[string]delivery.Module{} for _, key := range slices.Sorted(maps.Keys(all)) { k := all[key] r := of(key, nil) if _, merged := overlay[key]; merged { final := overlay[key] final.FP2, final.None2, final.Inputs2, final.Toolchains = r.fp, r.none, k.cur.Inputs2, r.tcs // What the bases gave is kept as it was while the value it gave does not change, so a merge // that moves nothing for the module writes nothing (as Recompute does). if value, _, found, err := ix.Modules.Get(ctx, key); err == nil && found { if old, err := delivery.DecodeModule(value); err == nil && old.Inputs2 != nil && old.FP2 == r.fp && old.None2 == r.none { in := *final.Inputs2 in.Bases, in.Builder = old.Inputs2.Bases, old.Inputs2.Builder final.Inputs2 = &in } } finals[key] = final continue } if !stale[key] && k.cur.FP2 == r.fp && k.cur.None2 == r.none && reflect.DeepEqual(k.cur.Toolchains, r.tcs) { continue } digest, inputs, paths := k.cur.Manifest, k.cur.Inputs2, k.cur.ArtifactPaths err := write(ctx, ix.Modules, key, func(value []byte, found bool) ([]byte, bool, error) { if !found { return nil, false, nil } cur, err := delivery.DecodeModule(value) if err != nil || cur.Manifest != digest || cur.Inputs2 == nil { return nil, false, nil // a merge changed it meanwhile; its own recompute follows } if cur.FP2 == r.fp && cur.None2 == r.none && reflect.DeepEqual(cur.Toolchains, r.tcs) { return nil, false, nil } cur.FP2, cur.None2, cur.Toolchains = r.fp, r.none, r.tcs cur.Inputs2, cur.ArtifactPaths = inputs, paths data, err := delivery.EncodeModule(cur) return data, false, err }) if err != nil { return fmt.Errorf("modules.%s: %w", key, err) } } return ix.writeOverlay(ctx, overlay, finals) } // writeOverlay writes the records of a merge, each once and only if it differs from what the bucket holds; finals has // the ones that were computed, the others are written as the merge gave them. func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]delivery.Module) error { for _, key := range slices.Sorted(maps.Keys(overlay)) { next := overlay[key] if f, ok := finals[key]; ok { next = f } err := write(ctx, ix.Modules, key, func(value []byte, found bool) ([]byte, bool, error) { if found { if cur, err := delivery.DecodeModule(value); err == nil && reflect.DeepEqual(cur, withV(next)) { return nil, false, nil } } data, err := delivery.EncodeModule(next) return data, false, err }) if err != nil { return fmt.Errorf("modules.%s: %w", key, err) } } return nil } // manifestRaw is manifests.: the manifest as stored and parsed by the running SDK. func (ix *Indexer) manifestRaw(ctx context.Context, digest string) (string, catalogue.Manifest, error) { key, err := delivery.ManifestsKey(digest) if err != nil { return "", catalogue.Manifest{}, err } value, _, found, err := ix.Manifests.Get(ctx, key) if err != nil { return "", catalogue.Manifest{}, err } if !found { return "", catalogue.Manifest{}, fmt.Errorf("manifests.%s is not there", key) } kept, err := delivery.DecodeManifest(value) if err != nil { return "", catalogue.Manifest{}, err } m, err := catalogue.ParseManifestForInputs([]byte(kept.JSON)) return kept.JSON, m, err } // rejudged is a value an SDK that could not read its manifest wrote, as the running SDK judges it: its kept inputs (the // tree, the lock hashes by file name, the named paths' objects) taken through the manifest as a merge takes them, and the // reason that gives. It answers false for any other value, and for one whose manifest the running SDK still cannot make // inputs of (issue 1923). func (ix *Indexer) rejudged(ctx context.Context, m catalogue.Manifest, cur delivery.Module, raw string) (delivery.Module, bool) { if cur.FP2 != "" || cur.Inputs2 == nil || !strings.HasPrefix(cur.None2, None2Unreadable) { return cur, false } in := inputsOfNamed(m.Module, raw, cur.Inputs2.Tree, cur.Inputs2.Locks, cur.Inputs2.Paths, ix.baseFP(ctx)) if in.Kept == nil { return cur, false } cur.Inputs2, cur.None2, cur.ArtifactPaths = in.Kept, in.None2, in.ArtifactPaths return cur, true } // hasContext is whether an artifact of the manifest reads another repository's tree. func hasContext(m catalogue.Manifest) bool { if m.Build == nil { return false } for _, a := range m.Build.Artifacts { if a.Context != nil { return true } } return false } // toolchainsUsed are the toolchains a fingerprint was computed with, by language (an artifact's toolchain is its language's). func toolchainsUsed(by map[string]delivery.Toolchain2) map[string]delivery.ToolchainUsed { if len(by) == 0 { return nil } out := map[string]delivery.ToolchainUsed{} for _, t := range by { out[t.Language] = delivery.ToolchainUsed{Version: t.Version, Recipe: t.Recipe} } return out } // RecomputeAtStart is Recompute once the indexer has started. While the indexer has read no toolchain at all (it has just started and the toolchains buckets are not settled), every // Go module would be written as having no toolchain reported and then back: it computes nothing then, and the settling of // the toolchains computes them. func (ix *Indexer) RecomputeAtStart(ctx context.Context) error { ix.mu.Lock() none := len(ix.toolchains) == 0 ix.mu.Unlock() if none { ix.logf("no toolchain is read yet: the new-form fingerprints are computed when one is") return nil } return ix.Recompute(ctx) } // fillPaths gives every module whose key names no directory (a key written before the indexer learned paths) the one its // newest build record names, once, by compare-and-set, changing nothing else (issue 1614): the builder needs it, and a // rarely merged repository would leave it without one for weeks. A key that names a directory is never written here. // It reads the controller's build records for nothing else: since R4g no fingerprint takes a build record (ADR 0530 §1). func (ix *Indexer) fillPaths(ctx context.Context) error { if ix.Modules == nil || ix.Builds == nil { return nil } held, err := ix.heldModules(ctx) if err != nil { return err } for _, key := range slices.Sorted(maps.Keys(held)) { if held[key].Path != "" { continue } dir, ok := ix.Builds.PathOf(key, held[key].Repository) if !ok { continue } err := write(ctx, ix.Modules, key, func(value []byte, found bool) ([]byte, bool, error) { if !found { return nil, false, nil } cur, err := delivery.DecodeModule(value) if err != nil || cur.Path != "" { return nil, false, nil } cur.Path = dir data, err := delivery.EncodeModule(cur) return data, false, err }) if err != nil { return fmt.Errorf("modules.%s: %w", key, err) } ix.logf("modules.%s names no directory: %q filled from its newest build record (issue 1614)", key, dir) } return nil }