blob: 01bb8c018476ea7172111e57f92256fad71390cd [file] [log] [blame]
package gps
import (
"fmt"
"sync"
)
type source interface {
syncLocal() error
checkExistence(sourceExistence) bool
exportVersionTo(Version, string) error
getManifestAndLock(ProjectRoot, Version) (Manifest, Lock, error)
listPackages(ProjectRoot, Version) (PackageTree, error)
listVersions() ([]Version, error)
revisionPresentIn(Revision) (bool, error)
}
type sourceMetaCache struct {
//Version string // TODO(sdboyer) use this
infos map[Revision]projectInfo
ptrees map[Revision]PackageTree
vMap map[UnpairedVersion]Revision
rMap map[Revision][]UnpairedVersion
// TODO(sdboyer) mutexes. actually probably just one, b/c complexity
}
// projectInfo holds manifest and lock
type projectInfo struct {
Manifest
Lock
}
type existence struct {
// The existence levels for which a search/check has been performed
s sourceExistence
// The existence levels verified to be present through searching
f sourceExistence
}
func newMetaCache() *sourceMetaCache {
return &sourceMetaCache{
infos: make(map[Revision]projectInfo),
ptrees: make(map[Revision]PackageTree),
vMap: make(map[UnpairedVersion]Revision),
rMap: make(map[Revision][]UnpairedVersion),
}
}
type baseVCSSource struct {
// Object for the cache repository
crepo *repo
// Indicates the extent to which we have searched for, and verified, the
// existence of the project/repo.
ex existence
// ProjectAnalyzer used to fulfill getManifestAndLock
an ProjectAnalyzer
// The project metadata cache. This is (or is intended to be) persisted to
// disk, for reuse across solver runs.
dc *sourceMetaCache
// lvfunc allows the other vcs source types that embed this type to inject
// their listVersions func into the baseSource, for use as needed.
lvfunc func() (vlist []Version, err error)
// lock to serialize access to syncLocal
synclock sync.Mutex
// Globalish flag indicating whether a "full" sync has been performed. Also
// used as a one-way gate to ensure that the full syncing routine is never
// run more than once on a given source instance.
allsync bool
// The error, if any, that occurred on syncLocal
syncerr error
// Whether the cache has the latest info on versions
cvsync bool
}
func (bs *baseVCSSource) getManifestAndLock(r ProjectRoot, v Version) (Manifest, Lock, error) {
if err := bs.ensureCacheExistence(); err != nil {
return nil, nil, err
}
rev, err := bs.toRevOrErr(v)
if err != nil {
return nil, nil, err
}
// Return the info from the cache, if we already have it
if pi, exists := bs.dc.infos[rev]; exists {
return pi.Manifest, pi.Lock, nil
}
// Cache didn't help; ensure our local is fully up to date.
err = bs.syncLocal()
if err != nil {
return nil, nil, err
}
bs.crepo.mut.Lock()
// Always prefer a rev, if it's available
if pv, ok := v.(PairedVersion); ok {
err = bs.crepo.r.UpdateVersion(pv.Underlying().String())
} else {
err = bs.crepo.r.UpdateVersion(v.String())
}
bs.crepo.mut.Unlock()
if err != nil {
// TODO(sdboyer) More-er proper-er error
panic(fmt.Sprintf("canary - why is checkout/whatever failing: %s %s %s", bs.crepo.r.LocalPath(), v.String(), unwrapVcsErr(err)))
}
bs.crepo.mut.RLock()
m, l, err := bs.an.DeriveManifestAndLock(bs.crepo.r.LocalPath(), r)
// TODO(sdboyer) cache results
bs.crepo.mut.RUnlock()
if err == nil {
if l != nil {
l = prepLock(l)
}
// If m is nil, prepManifest will provide an empty one.
pi := projectInfo{
Manifest: prepManifest(m),
Lock: l,
}
bs.dc.infos[rev] = pi
return pi.Manifest, pi.Lock, nil
}
return nil, nil, unwrapVcsErr(err)
}
// toRevision turns a Version into a Revision, if doing so is possible based on
// the information contained in the version itself, or in the cache maps.
func (dc *sourceMetaCache) toRevision(v Version) Revision {
switch t := v.(type) {
case Revision:
return t
case PairedVersion:
return t.Underlying()
case UnpairedVersion:
// This will return the empty rev (empty string) if we don't have a
// record of it. It's up to the caller to decide, for example, if
// it's appropriate to update the cache.
return dc.vMap[t]
default:
panic(fmt.Sprintf("Unknown version type %T", v))
}
}
// toUnpaired turns a Version into an UnpairedVersion, if doing so is possible
// based on the information contained in the version itself, or in the cache
// maps.
//
// If the input is a revision and multiple UnpairedVersions are associated with
// it, whatever happens to be the first is returned.
func (dc *sourceMetaCache) toUnpaired(v Version) UnpairedVersion {
switch t := v.(type) {
case UnpairedVersion:
return t
case PairedVersion:
return t.Unpair()
case Revision:
if upv, has := dc.rMap[t]; has && len(upv) > 0 {
return upv[0]
}
return nil
default:
panic(fmt.Sprintf("unknown version type %T", v))
}
}
func (bs *baseVCSSource) revisionPresentIn(r Revision) (bool, error) {
// First and fastest path is to check the data cache to see if the rev is
// present. This could give us false positives, but the cases where that can
// occur would require a type of cache staleness that seems *exceedingly*
// unlikely to occur.
if _, has := bs.dc.infos[r]; has {
return true, nil
} else if _, has := bs.dc.rMap[r]; has {
return true, nil
}
err := bs.ensureCacheExistence()
if err != nil {
return false, err
}
bs.crepo.mut.RLock()
defer bs.crepo.mut.RUnlock()
return bs.crepo.r.IsReference(string(r)), nil
}
func (bs *baseVCSSource) ensureCacheExistence() error {
// Technically, methods could could attempt to return straight from the
// metadata cache even if the repo cache doesn't exist on disk. But that
// would allow weird state inconsistencies (cache exists, but no repo...how
// does that even happen?) that it'd be better to just not allow so that we
// don't have to think about it elsewhere
if !bs.checkExistence(existsInCache) {
if bs.checkExistence(existsUpstream) {
bs.crepo.mut.Lock()
if bs.crepo.synced {
// A second ensure call coming in while the first is completing
// isn't terribly unlikely, especially for a large repo. In that
// event, the synced flag will have flipped on by the time we
// acquire the lock. If it has, there's no need to do this work
// twice.
bs.crepo.mut.Unlock()
return nil
}
err := bs.crepo.r.Get()
if err != nil {
bs.crepo.mut.Unlock()
return fmt.Errorf("failed to create repository cache for %s with err:\n%s", bs.crepo.r.Remote(), unwrapVcsErr(err))
}
bs.crepo.synced = true
bs.ex.s |= existsInCache
bs.ex.f |= existsInCache
bs.crepo.mut.Unlock()
} else {
return fmt.Errorf("project %s does not exist upstream", bs.crepo.r.Remote())
}
}
return nil
}
// checkExistence provides a direct method for querying existence levels of the
// source. It will only perform actual searching (local fs or over the network)
// if no previous attempt at that search has been made.
//
// Note that this may perform read-ish operations on the cache repo, and it
// takes a lock accordingly. This makes it unsafe to call from a segment where
// the cache repo mutex is already write-locked, as deadlock will occur.
func (bs *baseVCSSource) checkExistence(ex sourceExistence) bool {
if bs.ex.s&ex != ex {
if ex&existsInVendorRoot != 0 && bs.ex.s&existsInVendorRoot == 0 {
panic("should now be implemented in bridge")
}
if ex&existsInCache != 0 && bs.ex.s&existsInCache == 0 {
bs.crepo.mut.RLock()
bs.ex.s |= existsInCache
if bs.crepo.r.CheckLocal() {
bs.ex.f |= existsInCache
}
bs.crepo.mut.RUnlock()
}
if ex&existsUpstream != 0 && bs.ex.s&existsUpstream == 0 {
bs.crepo.mut.RLock()
bs.ex.s |= existsUpstream
if bs.crepo.r.Ping() {
bs.ex.f |= existsUpstream
}
bs.crepo.mut.RUnlock()
}
}
return ex&bs.ex.f == ex
}
// syncLocal ensures the local data we have about the source is fully up to date
// with what's out there over the network.
func (bs *baseVCSSource) syncLocal() error {
// Ensure we only have one goroutine doing this at a time
bs.synclock.Lock()
defer bs.synclock.Unlock()
// ...and that we only ever do it once
if bs.allsync {
// Return the stored err, if any
return bs.syncerr
}
bs.allsync = true
// First, ensure the local instance exists
bs.syncerr = bs.ensureCacheExistence()
if bs.syncerr != nil {
return bs.syncerr
}
_, bs.syncerr = bs.lvfunc()
if bs.syncerr != nil {
return bs.syncerr
}
// This case is really just for git repos, where the lvfunc doesn't
// guarantee that the local repo is synced
if !bs.crepo.synced {
bs.crepo.mut.Lock()
err := bs.crepo.r.Update()
if err != nil {
bs.syncerr = fmt.Errorf("failed fetching latest updates with err: %s", unwrapVcsErr(err))
bs.crepo.mut.Unlock()
return bs.syncerr
}
bs.crepo.synced = true
bs.crepo.mut.Unlock()
}
return nil
}
func (bs *baseVCSSource) listPackages(pr ProjectRoot, v Version) (ptree PackageTree, err error) {
if err = bs.ensureCacheExistence(); err != nil {
return
}
var r Revision
if r, err = bs.toRevOrErr(v); err != nil {
return
}
// Return the ptree from the cache, if we already have it
var exists bool
if ptree, exists = bs.dc.ptrees[r]; exists {
return
}
// Not in the cache; check out the version and do the analysis
bs.crepo.mut.Lock()
// Check out the desired version for analysis
if r != "" {
// Always prefer a rev, if it's available
err = bs.crepo.r.UpdateVersion(string(r))
} else {
// If we don't have a rev, ensure the repo is up to date, otherwise we
// could have a desync issue
if !bs.crepo.synced {
err = bs.crepo.r.Update()
if err != nil {
err = fmt.Errorf("could not fetch latest updates into repository: %s", unwrapVcsErr(err))
return
}
bs.crepo.synced = true
}
err = bs.crepo.r.UpdateVersion(v.String())
}
if err == nil {
ptree, err = ListPackages(bs.crepo.r.LocalPath(), string(pr))
// TODO(sdboyer) cache errs?
if err == nil {
bs.dc.ptrees[r] = ptree
}
} else {
err = unwrapVcsErr(err)
}
bs.crepo.mut.Unlock()
return
}
// toRevOrErr makes all efforts to convert a Version into a rev, including
// updating the cache repo (if needed). It does not guarantee that the returned
// Revision actually exists in the repository (as one of the cheaper methods may
// have had bad data).
func (bs *baseVCSSource) toRevOrErr(v Version) (r Revision, err error) {
r = bs.dc.toRevision(v)
if r == "" {
// Rev can be empty if:
// - The cache is unsynced
// - A version was passed that used to exist, but no longer does
// - A garbage version was passed. (Functionally indistinguishable from
// the previous)
if !bs.cvsync {
// call the lvfunc to sync the meta cache
_, err = bs.lvfunc()
if err != nil {
return
}
}
r = bs.dc.toRevision(v)
// If we still don't have a rev, then the version's no good
if r == "" {
err = fmt.Errorf("version %s does not exist in source %s", v, bs.crepo.r.Remote())
}
}
return
}
func (bs *baseVCSSource) exportVersionTo(v Version, to string) error {
return bs.crepo.exportVersionTo(v, to)
}