internal/pkg: optional dependency graph size limit
All checks were successful
Test / Create distribution (push) Successful in 47s
Test / Sandbox (push) Successful in 2m48s
Test / ShareFS (push) Successful in 4m50s
Test / Hpkg (push) Successful in 5m10s
Test / Sandbox (race detector) (push) Successful in 5m17s
Test / Hakurei (push) Successful in 5m44s
Test / Hakurei (race detector) (push) Successful in 7m46s
Test / Flake checks (push) Successful in 1m45s

This provides a quick check against cyclic dependencies without hurting cure performance.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-01-14 18:25:46 +09:00
parent 1667df9c43
commit 088d35e4e6

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"io"
"io/fs"
"iter"
"os"
"path"
"path/filepath"
@@ -217,6 +218,23 @@ type FloodArtifact interface {
Artifact
}
// Flood returns an iterator over the dependency tree of an [Artifact].
func Flood(a Artifact) iter.Seq[Artifact] {
return func(yield func(Artifact) bool) {
for _, d := range a.Dependencies() {
if !yield(d) {
return
}
for d0 := range Flood(d) {
if !yield(d0) {
return
}
}
}
}
}
// TrivialArtifact refers to an [Artifact] that cures without requiring that
// any other [Artifact] is cured before it. Its dependency tree is ignored after
// computing its identifier.
@@ -405,9 +423,11 @@ type Cache struct {
// Directory where all [Cache] related files are placed.
base *check.Absolute
// Whether to validate [File.Data] for a [KnownChecksum] file. This
// Whether to validate [File.Cure] for a [KnownChecksum] file. This
// significantly reduces performance.
strict bool
// Maximum size of a dependency graph.
threshold uintptr
// Synchronises access to dirChecksum.
checksumMu sync.RWMutex
@@ -432,6 +452,12 @@ func (c *Cache) IsStrict() bool { return c.strict }
// This method is not safe for concurrent use with any other method.
func (c *Cache) SetStrict(strict bool) { c.strict = strict }
// SetThreshold imposes a maximum size on the dependency graph, checked on every
// call to Cure. The zero value disables this check entirely.
//
// This method is not safe for concurrent use with any other method.
func (c *Cache) SetThreshold(threshold uintptr) { c.threshold = threshold }
// A ChecksumMismatchError describes an [Artifact] with unexpected content.
type ChecksumMismatchError struct {
// Actual and expected checksums.
@@ -894,12 +920,40 @@ func (e InvalidArtifactError) Error() string {
return "artifact " + Encode(e) + " cannot be cured"
}
// DependencyError refers to an artifact with a dependency tree larger than the
// threshold specified by a previous call to [Cache.SetThreshold].
type DependencyError struct{ A Artifact }
func (e DependencyError) Error() string {
return "artifact has too many dependencies"
}
// Cure cures the [Artifact] and returns its pathname and [Checksum]. Direct
// calls to Cure are not subject to the cures limit.
func (c *Cache) Cure(a Artifact) (
pathname *check.Absolute,
checksum Checksum,
err error,
) {
if c.threshold > 0 {
var n uintptr
for range Flood(a) {
if n == c.threshold {
err = DependencyError{a}
return
}
n++
}
}
return c.cure(a)
}
// cure implements Cure without checking the full dependency graph.
func (c *Cache) cure(a Artifact) (
pathname *check.Absolute,
checksum Checksum,
err error,
) {
id := Ident(a)
ids := Encode(id)
@@ -1190,7 +1244,7 @@ func (c *Cache) Cure(a Artifact) (
func (pending *pendingArtifactDep) cure(c *Cache) {
defer pending.Done()
pathname, _, err := c.Cure(pending.a)
pathname, _, err := c.cure(pending.a)
if err == nil {
*pending.resP = pathname
return