Skip to content

Commit 480dbc5

Browse files
authored
[Remove Nixpkgs] move concurrency code to own file (#1412)
## Summary No logic change. Just grouping this code into its own file, ordered by public functions/methods on top. Also fixed this placeholder error message: #1318 (comment) ## How was it tested? compiles
1 parent 159c801 commit 480dbc5

File tree

2 files changed

+227
-213
lines changed

2 files changed

+227
-213
lines changed

internal/devpkg/narinfo_cache.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package devpkg
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"strings"
8+
"sync"
9+
"time"
10+
"unicode"
11+
12+
"github.com/pkg/errors"
13+
"go.jetpack.io/devbox/internal/boxcli/featureflag"
14+
"go.jetpack.io/devbox/internal/lock"
15+
"go.jetpack.io/devbox/internal/nix"
16+
"go.jetpack.io/devbox/internal/vercheck"
17+
"golang.org/x/sync/errgroup"
18+
)
19+
20+
// BinaryCache is the store from which to fetch this package's binaries.
21+
// It is used as FromStore in builtins.fetchClosure.
22+
const BinaryCache = "https://cache.nixos.org"
23+
24+
// isNarInfoInCache checks if the .narinfo for this package is in the `BinaryCache`.
25+
// This cannot be a field on the Package struct, because that struct
26+
// is constructed multiple times in a request (TODO: we could fix that).
27+
var isNarInfoInCache = struct {
28+
// The key is the `Package.Raw` string.
29+
status map[string]bool
30+
lock sync.RWMutex
31+
// re-use httpClient to re-use the connection
32+
httpClient http.Client
33+
}{
34+
status: map[string]bool{},
35+
httpClient: http.Client{},
36+
}
37+
38+
// IsInBinaryCache returns true if the package is in the binary cache.
39+
// ALERT: Callers must call FillNarInfoCache before calling this function.
40+
func (p *Package) IsInBinaryCache() (bool, error) {
41+
42+
if eligible, err := p.isEligibleForBinaryCache(); err != nil {
43+
return false, err
44+
} else if !eligible {
45+
return false, nil
46+
}
47+
48+
// Check if the narinfo is present in the binary cache
49+
isNarInfoInCache.lock.RLock()
50+
exists, ok := isNarInfoInCache.status[p.Raw]
51+
isNarInfoInCache.lock.RUnlock()
52+
if !ok {
53+
return false, errors.Errorf(
54+
"narInfo cache miss: %v. Call FillNarInfoCache before invoking IsInBinaryCache",
55+
p.Raw,
56+
)
57+
}
58+
return exists, nil
59+
}
60+
61+
// FillNarInfoCache checks the remote binary cache for the narinfo of each
62+
// package in the list, and caches the result.
63+
// Callers of IsInBinaryCache must call this function first.
64+
func FillNarInfoCache(ctx context.Context, packages ...*Package) error {
65+
66+
// Pre-compute values read in fillNarInfoCache
67+
// so they can be read from multiple go-routines without locks
68+
_, err := nix.Version()
69+
if err != nil {
70+
return err
71+
}
72+
_ = nix.System()
73+
for _, p := range packages {
74+
_, err := p.lockfile.Resolve(p.Raw)
75+
if err != nil {
76+
return err
77+
}
78+
}
79+
80+
group, _ := errgroup.WithContext(ctx)
81+
for _, p := range packages {
82+
// If the package's NarInfo status is already known, skip it
83+
isNarInfoInCache.lock.RLock()
84+
_, ok := isNarInfoInCache.status[p.Raw]
85+
isNarInfoInCache.lock.RUnlock()
86+
if ok {
87+
continue
88+
}
89+
pkg := p // copy the loop variable since its used in a closure below
90+
group.Go(func() error {
91+
err := pkg.fillNarInfoCache()
92+
if err != nil {
93+
// default to false if there was an error, so we don't re-try
94+
isNarInfoInCache.lock.Lock()
95+
isNarInfoInCache.status[pkg.Raw] = false
96+
isNarInfoInCache.lock.Unlock()
97+
}
98+
return err
99+
})
100+
}
101+
return group.Wait()
102+
}
103+
104+
// fillNarInfoCache fills the cache value for the narinfo of this package,
105+
// if it is eligible for the binary cache.
106+
// NOTE: this must be concurrency safe.
107+
func (p *Package) fillNarInfoCache() error {
108+
if eligible, err := p.isEligibleForBinaryCache(); err != nil {
109+
return err
110+
} else if !eligible {
111+
return nil
112+
}
113+
114+
sysInfo, err := p.sysInfoIfExists()
115+
if err != nil {
116+
return err
117+
} else if sysInfo == nil {
118+
return errors.New(
119+
"sysInfo is nil, but should not be because" +
120+
" the package is eligible for binary cache",
121+
)
122+
}
123+
124+
pathParts := newStorePathParts(sysInfo.StorePath)
125+
reqURL := BinaryCache + "/" + pathParts.hash + ".narinfo"
126+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
127+
defer cancel()
128+
req, err := http.NewRequestWithContext(ctx, http.MethodHead, reqURL, nil)
129+
if err != nil {
130+
return err
131+
}
132+
res, err := isNarInfoInCache.httpClient.Do(req)
133+
if err != nil {
134+
return err
135+
}
136+
// read the body fully, and close it to ensure the connection is reused.
137+
_, _ = io.Copy(io.Discard, res.Body)
138+
defer res.Body.Close()
139+
140+
isNarInfoInCache.lock.Lock()
141+
isNarInfoInCache.status[p.Raw] = res.StatusCode == 200
142+
isNarInfoInCache.lock.Unlock()
143+
return nil
144+
}
145+
146+
func (p *Package) isEligibleForBinaryCache() (bool, error) {
147+
sysInfo, err := p.sysInfoIfExists()
148+
if err != nil {
149+
return false, err
150+
}
151+
return sysInfo != nil, nil
152+
}
153+
154+
// sysInfoIfExists returns the system info for the user's system. If the sysInfo
155+
// is missing, then nil is returned
156+
// NOTE: this is called from multiple go-routines and needs to be concurrency safe.
157+
// Hence, we compute nix.Version, nix.System and lockfile.Resolve prior to calling this
158+
// function from within a goroutine.
159+
func (p *Package) sysInfoIfExists() (*lock.SystemInfo, error) {
160+
if !featureflag.RemoveNixpkgs.Enabled() {
161+
return nil, nil
162+
}
163+
164+
if !p.isVersioned() {
165+
return nil, nil
166+
}
167+
168+
version, err := nix.Version()
169+
if err != nil {
170+
return nil, err
171+
}
172+
173+
// enable for nix >= 2.17
174+
if vercheck.SemverCompare(version, "2.17.0") < 0 {
175+
return nil, err
176+
}
177+
178+
entry, err := p.lockfile.Resolve(p.Raw)
179+
if err != nil {
180+
return nil, err
181+
}
182+
183+
userSystem := nix.System()
184+
185+
if entry.Systems == nil {
186+
return nil, nil
187+
}
188+
189+
// Check if the user's system's info is present in the lockfile
190+
sysInfo, ok := entry.Systems[userSystem]
191+
if !ok {
192+
return nil, nil
193+
}
194+
return sysInfo, nil
195+
}
196+
197+
// storePath are the constituent parts of
198+
// /nix/store/<hash>-<name>-<version>
199+
//
200+
// This is a helper struct for analyzing the string representation
201+
type storePathParts struct {
202+
hash string
203+
name string
204+
version string
205+
}
206+
207+
// newStorePathParts splits a Nix store path into its hash, name and version
208+
// components in the same way that Nix does.
209+
//
210+
// See https://nixos.org/manual/nix/stable/language/builtins.html#builtins-parseDrvName
211+
func newStorePathParts(path string) storePathParts {
212+
path = strings.TrimPrefix(path, "/nix/store/")
213+
// path is now <hash>-<name>-<version
214+
215+
hash, name := path[:32], path[33:]
216+
dashIndex := 0
217+
for i, r := range name {
218+
if dashIndex != 0 && !unicode.IsLetter(r) {
219+
return storePathParts{hash: hash, name: name[:dashIndex], version: name[i:]}
220+
}
221+
dashIndex = 0
222+
if r == '-' {
223+
dashIndex = i
224+
}
225+
}
226+
return storePathParts{hash: hash, name: name}
227+
}

0 commit comments

Comments
 (0)