github.com/sdboyer/gps@v0.16.3/source.go (about)

     1  package gps
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  
     9  	"github.com/sdboyer/gps/pkgtree"
    10  )
    11  
    12  // sourceState represent the states that a source can be in, depending on how
    13  // much search and discovery work ahs been done by a source's managing gateway.
    14  //
    15  // These are basically used to achieve a cheap approximation of a FSM.
    16  type sourceState int32
    17  
    18  const (
    19  	sourceIsSetUp sourceState = 1 << iota
    20  	sourceExistsUpstream
    21  	sourceExistsLocally
    22  	sourceHasLatestVersionList
    23  	sourceHasLatestLocally
    24  )
    25  
    26  type srcReturnChans struct {
    27  	ret chan *sourceGateway
    28  	err chan error
    29  }
    30  
    31  func (rc srcReturnChans) awaitReturn() (sg *sourceGateway, err error) {
    32  	select {
    33  	case sg = <-rc.ret:
    34  	case err = <-rc.err:
    35  	}
    36  	return
    37  }
    38  
    39  type sourceCoordinator struct {
    40  	supervisor *supervisor
    41  	srcmut     sync.RWMutex // guards srcs and nameToURL maps
    42  	srcs       map[string]*sourceGateway
    43  	nameToURL  map[string]string
    44  	psrcmut    sync.Mutex // guards protoSrcs map
    45  	protoSrcs  map[string][]srcReturnChans
    46  	deducer    deducer
    47  	cachedir   string
    48  }
    49  
    50  func newSourceCoordinator(superv *supervisor, deducer deducer, cachedir string) *sourceCoordinator {
    51  	return &sourceCoordinator{
    52  		supervisor: superv,
    53  		deducer:    deducer,
    54  		cachedir:   cachedir,
    55  		srcs:       make(map[string]*sourceGateway),
    56  		nameToURL:  make(map[string]string),
    57  		protoSrcs:  make(map[string][]srcReturnChans),
    58  	}
    59  }
    60  
    61  func (sc *sourceCoordinator) getSourceGatewayFor(ctx context.Context, id ProjectIdentifier) (*sourceGateway, error) {
    62  	if sc.supervisor.getLifetimeContext().Err() != nil {
    63  		return nil, errors.New("sourceCoordinator has been terminated")
    64  	}
    65  
    66  	normalizedName := id.normalizedSource()
    67  
    68  	sc.srcmut.RLock()
    69  	if url, has := sc.nameToURL[normalizedName]; has {
    70  		srcGate, has := sc.srcs[url]
    71  		sc.srcmut.RUnlock()
    72  		if has {
    73  			return srcGate, nil
    74  		}
    75  		panic(fmt.Sprintf("%q was URL for %q in nameToURL, but no corresponding srcGate in srcs map", url, normalizedName))
    76  	}
    77  	sc.srcmut.RUnlock()
    78  
    79  	// No gateway exists for this path yet; set up a proto, being careful to fold
    80  	// together simultaneous attempts on the same path.
    81  	rc := srcReturnChans{
    82  		ret: make(chan *sourceGateway),
    83  		err: make(chan error),
    84  	}
    85  
    86  	// The rest of the work needs its own goroutine, the results of which will
    87  	// be re-joined to this call via the return chans.
    88  	go sc.setUpSourceGateway(ctx, normalizedName, rc)
    89  	return rc.awaitReturn()
    90  }
    91  
    92  // Not intended to be called externally - call getSourceGatewayFor instead.
    93  func (sc *sourceCoordinator) setUpSourceGateway(ctx context.Context, normalizedName string, rc srcReturnChans) {
    94  	sc.psrcmut.Lock()
    95  	if chans, has := sc.protoSrcs[normalizedName]; has {
    96  		// Another goroutine is already working on this normalizedName. Fold
    97  		// in with that work by attaching our return channels to the list.
    98  		sc.protoSrcs[normalizedName] = append(chans, rc)
    99  		sc.psrcmut.Unlock()
   100  		return
   101  	}
   102  
   103  	sc.protoSrcs[normalizedName] = []srcReturnChans{rc}
   104  	sc.psrcmut.Unlock()
   105  
   106  	doReturn := func(sg *sourceGateway, err error) {
   107  		sc.psrcmut.Lock()
   108  		if sg != nil {
   109  			for _, rc := range sc.protoSrcs[normalizedName] {
   110  				rc.ret <- sg
   111  			}
   112  		} else if err != nil {
   113  			for _, rc := range sc.protoSrcs[normalizedName] {
   114  				rc.err <- err
   115  			}
   116  		} else {
   117  			panic("sg and err both nil")
   118  		}
   119  
   120  		delete(sc.protoSrcs, normalizedName)
   121  		sc.psrcmut.Unlock()
   122  	}
   123  
   124  	pd, err := sc.deducer.deduceRootPath(ctx, normalizedName)
   125  	if err != nil {
   126  		// As in the deducer, don't cache errors so that externally-driven retry
   127  		// strategies can be constructed.
   128  		doReturn(nil, err)
   129  		return
   130  	}
   131  
   132  	// It'd be quite the feat - but not impossible - for a gateway
   133  	// corresponding to this normalizedName to have slid into the main
   134  	// sources map after the initial unlock, but before this goroutine got
   135  	// scheduled. Guard against that by checking the main sources map again
   136  	// and bailing out if we find an entry.
   137  	var srcGate *sourceGateway
   138  	sc.srcmut.RLock()
   139  	if url, has := sc.nameToURL[normalizedName]; has {
   140  		if srcGate, has := sc.srcs[url]; has {
   141  			sc.srcmut.RUnlock()
   142  			doReturn(srcGate, nil)
   143  			return
   144  		}
   145  		panic(fmt.Sprintf("%q was URL for %q in nameToURL, but no corresponding srcGate in srcs map", url, normalizedName))
   146  	}
   147  	sc.srcmut.RUnlock()
   148  
   149  	srcGate = newSourceGateway(pd.mb, sc.supervisor, sc.cachedir)
   150  
   151  	// The normalized name is usually different from the source URL- e.g.
   152  	// github.com/sdboyer/gps vs. https://github.com/sdboyer/gps. But it's
   153  	// possible to arrive here with a full URL as the normalized name - and
   154  	// both paths *must* lead to the same sourceGateway instance in order to
   155  	// ensure disk access is correctly managed.
   156  	//
   157  	// Therefore, we now must query the sourceGateway to get the actual
   158  	// sourceURL it's operating on, and ensure it's *also* registered at
   159  	// that path in the map. This will cause it to actually initiate the
   160  	// maybeSource.try() behavior in order to settle on a URL.
   161  	url, err := srcGate.sourceURL(ctx)
   162  	if err != nil {
   163  		doReturn(nil, err)
   164  		return
   165  	}
   166  
   167  	// We know we have a working srcGateway at this point, and need to
   168  	// integrate it back into the main map.
   169  	sc.srcmut.Lock()
   170  	defer sc.srcmut.Unlock()
   171  	// Record the name -> URL mapping, even if it's a self-mapping.
   172  	sc.nameToURL[normalizedName] = url
   173  
   174  	if sa, has := sc.srcs[url]; has {
   175  		// URL already had an entry in the main map; use that as the result.
   176  		doReturn(sa, nil)
   177  		return
   178  	}
   179  
   180  	sc.srcs[url] = srcGate
   181  	doReturn(srcGate, nil)
   182  }
   183  
   184  // sourceGateways manage all incoming calls for data from sources, serializing
   185  // and caching them as needed.
   186  type sourceGateway struct {
   187  	cachedir string
   188  	maybe    maybeSource
   189  	srcState sourceState
   190  	src      source
   191  	cache    singleSourceCache
   192  	mu       sync.Mutex // global lock, serializes all behaviors
   193  	suprvsr  *supervisor
   194  }
   195  
   196  func newSourceGateway(maybe maybeSource, superv *supervisor, cachedir string) *sourceGateway {
   197  	sg := &sourceGateway{
   198  		maybe:    maybe,
   199  		cachedir: cachedir,
   200  		suprvsr:  superv,
   201  	}
   202  	sg.cache = sg.createSingleSourceCache()
   203  
   204  	return sg
   205  }
   206  
   207  func (sg *sourceGateway) syncLocal(ctx context.Context) error {
   208  	sg.mu.Lock()
   209  	defer sg.mu.Unlock()
   210  
   211  	_, err := sg.require(ctx, sourceIsSetUp|sourceExistsLocally|sourceHasLatestLocally)
   212  	return err
   213  }
   214  
   215  func (sg *sourceGateway) existsInCache(ctx context.Context) bool {
   216  	sg.mu.Lock()
   217  	defer sg.mu.Unlock()
   218  
   219  	_, err := sg.require(ctx, sourceIsSetUp|sourceExistsLocally)
   220  	if err != nil {
   221  		return false
   222  	}
   223  
   224  	return sg.srcState&sourceExistsLocally != 0
   225  }
   226  
   227  func (sg *sourceGateway) existsUpstream(ctx context.Context) bool {
   228  	sg.mu.Lock()
   229  	defer sg.mu.Unlock()
   230  
   231  	_, err := sg.require(ctx, sourceIsSetUp|sourceExistsUpstream)
   232  	if err != nil {
   233  		return false
   234  	}
   235  
   236  	return sg.srcState&sourceExistsUpstream != 0
   237  }
   238  
   239  func (sg *sourceGateway) exportVersionTo(ctx context.Context, v Version, to string) error {
   240  	sg.mu.Lock()
   241  	defer sg.mu.Unlock()
   242  
   243  	_, err := sg.require(ctx, sourceIsSetUp|sourceExistsLocally)
   244  	if err != nil {
   245  		return err
   246  	}
   247  
   248  	r, err := sg.convertToRevision(ctx, v)
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	return sg.suprvsr.do(ctx, sg.src.upstreamURL(), ctExportTree, func(ctx context.Context) error {
   254  		return sg.src.exportRevisionTo(ctx, r, to)
   255  	})
   256  }
   257  
   258  func (sg *sourceGateway) getManifestAndLock(ctx context.Context, pr ProjectRoot, v Version, an ProjectAnalyzer) (Manifest, Lock, error) {
   259  	sg.mu.Lock()
   260  	defer sg.mu.Unlock()
   261  
   262  	r, err := sg.convertToRevision(ctx, v)
   263  	if err != nil {
   264  		return nil, nil, err
   265  	}
   266  
   267  	m, l, has := sg.cache.getManifestAndLock(r, an)
   268  	if has {
   269  		return m, l, nil
   270  	}
   271  
   272  	_, err = sg.require(ctx, sourceIsSetUp|sourceExistsLocally)
   273  	if err != nil {
   274  		return nil, nil, err
   275  	}
   276  
   277  	name, vers := an.Info()
   278  	label := fmt.Sprintf("%s:%s.%v", sg.src.upstreamURL(), name, vers)
   279  	err = sg.suprvsr.do(ctx, label, ctGetManifestAndLock, func(ctx context.Context) error {
   280  		m, l, err = sg.src.getManifestAndLock(ctx, pr, r, an)
   281  		return err
   282  	})
   283  	if err != nil {
   284  		return nil, nil, err
   285  	}
   286  
   287  	sg.cache.setManifestAndLock(r, an, m, l)
   288  	return m, l, nil
   289  }
   290  
   291  // FIXME ProjectRoot input either needs to parameterize the cache, or be
   292  // incorporated on the fly on egress...?
   293  func (sg *sourceGateway) listPackages(ctx context.Context, pr ProjectRoot, v Version) (pkgtree.PackageTree, error) {
   294  	sg.mu.Lock()
   295  	defer sg.mu.Unlock()
   296  
   297  	r, err := sg.convertToRevision(ctx, v)
   298  	if err != nil {
   299  		return pkgtree.PackageTree{}, err
   300  	}
   301  
   302  	ptree, has := sg.cache.getPackageTree(r)
   303  	if has {
   304  		return ptree, nil
   305  	}
   306  
   307  	_, err = sg.require(ctx, sourceIsSetUp|sourceExistsLocally)
   308  	if err != nil {
   309  		return pkgtree.PackageTree{}, err
   310  	}
   311  
   312  	label := fmt.Sprintf("%s:%s", pr, sg.src.upstreamURL())
   313  	err = sg.suprvsr.do(ctx, label, ctListPackages, func(ctx context.Context) error {
   314  		ptree, err = sg.src.listPackages(ctx, pr, r)
   315  		return err
   316  	})
   317  	if err != nil {
   318  		return pkgtree.PackageTree{}, err
   319  	}
   320  
   321  	sg.cache.setPackageTree(r, ptree)
   322  	return ptree, nil
   323  }
   324  
   325  func (sg *sourceGateway) convertToRevision(ctx context.Context, v Version) (Revision, error) {
   326  	// When looking up by Version, there are four states that may have
   327  	// differing opinions about version->revision mappings:
   328  	//
   329  	//   1. The upstream source/repo (canonical)
   330  	//   2. The local source/repo
   331  	//   3. The local cache
   332  	//   4. The input (params to this method)
   333  	//
   334  	// If the input differs from any of the above, it's likely because some lock
   335  	// got written somewhere with a version/rev pair that has since changed or
   336  	// been removed. But correct operation dictates that such a mis-mapping be
   337  	// respected; if the mis-mapping is to be corrected, it has to be done
   338  	// intentionally by the caller, not automatically here.
   339  	r, has := sg.cache.toRevision(v)
   340  	if has {
   341  		return r, nil
   342  	}
   343  
   344  	if sg.srcState&sourceHasLatestVersionList != 0 {
   345  		// We have the latest version list already and didn't get a match, so
   346  		// this is definitely a failure case.
   347  		return "", fmt.Errorf("version %q does not exist in source", v)
   348  	}
   349  
   350  	// The version list is out of date; it's possible this version might
   351  	// show up after loading it.
   352  	_, err := sg.require(ctx, sourceIsSetUp|sourceHasLatestVersionList)
   353  	if err != nil {
   354  		return "", err
   355  	}
   356  
   357  	r, has = sg.cache.toRevision(v)
   358  	if !has {
   359  		return "", fmt.Errorf("version %q does not exist in source", v)
   360  	}
   361  
   362  	return r, nil
   363  }
   364  
   365  func (sg *sourceGateway) listVersions(ctx context.Context) ([]PairedVersion, error) {
   366  	sg.mu.Lock()
   367  	defer sg.mu.Unlock()
   368  
   369  	// TODO(sdboyer) The problem here is that sourceExistsUpstream may not be
   370  	// sufficient (e.g. bzr, hg), but we don't want to force local b/c git
   371  	// doesn't need it
   372  	_, err := sg.require(ctx, sourceIsSetUp|sourceExistsUpstream|sourceHasLatestVersionList)
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  
   377  	return sg.cache.getAllVersions(), nil
   378  }
   379  
   380  func (sg *sourceGateway) revisionPresentIn(ctx context.Context, r Revision) (bool, error) {
   381  	sg.mu.Lock()
   382  	defer sg.mu.Unlock()
   383  
   384  	_, err := sg.require(ctx, sourceIsSetUp|sourceExistsLocally)
   385  	if err != nil {
   386  		return false, err
   387  	}
   388  
   389  	if _, exists := sg.cache.getVersionsFor(r); exists {
   390  		return true, nil
   391  	}
   392  
   393  	present, err := sg.src.revisionPresentIn(r)
   394  	if err == nil && present {
   395  		sg.cache.markRevisionExists(r)
   396  	}
   397  	return present, err
   398  }
   399  
   400  func (sg *sourceGateway) sourceURL(ctx context.Context) (string, error) {
   401  	sg.mu.Lock()
   402  	defer sg.mu.Unlock()
   403  
   404  	_, err := sg.require(ctx, sourceIsSetUp)
   405  	if err != nil {
   406  		return "", err
   407  	}
   408  
   409  	return sg.src.upstreamURL(), nil
   410  }
   411  
   412  // createSingleSourceCache creates a singleSourceCache instance for use by
   413  // the encapsulated source.
   414  func (sg *sourceGateway) createSingleSourceCache() singleSourceCache {
   415  	// TODO(sdboyer) when persistent caching is ready, just drop in the creation
   416  	// of a source-specific handle here
   417  	return newMemoryCache()
   418  }
   419  
   420  func (sg *sourceGateway) require(ctx context.Context, wanted sourceState) (errState sourceState, err error) {
   421  	todo := (^sg.srcState) & wanted
   422  	var flag sourceState = 1
   423  
   424  	for todo != 0 {
   425  		if todo&flag != 0 {
   426  			// Assign the currently visited bit to errState so that we can
   427  			// return easily later.
   428  			//
   429  			// Also set up addlState so that individual ops can easily attach
   430  			// more states that were incidentally satisfied by the op.
   431  			errState = flag
   432  			var addlState sourceState
   433  
   434  			switch flag {
   435  			case sourceIsSetUp:
   436  				sg.src, addlState, err = sg.maybe.try(ctx, sg.cachedir, sg.cache, sg.suprvsr)
   437  			case sourceExistsUpstream:
   438  				err = sg.suprvsr.do(ctx, sg.src.sourceType(), ctSourcePing, func(ctx context.Context) error {
   439  					if !sg.src.existsUpstream(ctx) {
   440  						return fmt.Errorf("%s does not exist upstream", sg.src.upstreamURL())
   441  					}
   442  					return nil
   443  				})
   444  			case sourceExistsLocally:
   445  				if !sg.src.existsLocally(ctx) {
   446  					err = sg.suprvsr.do(ctx, sg.src.sourceType(), ctSourceInit, func(ctx context.Context) error {
   447  						return sg.src.initLocal(ctx)
   448  					})
   449  
   450  					if err == nil {
   451  						addlState |= sourceHasLatestLocally
   452  					} else {
   453  						err = fmt.Errorf("%s does not exist in the local cache and fetching failed: %s", sg.src.upstreamURL(), err)
   454  					}
   455  				}
   456  			case sourceHasLatestVersionList:
   457  				var pvl []PairedVersion
   458  				err = sg.suprvsr.do(ctx, sg.src.sourceType(), ctListVersions, func(ctx context.Context) error {
   459  					pvl, err = sg.src.listVersions(ctx)
   460  					return err
   461  				})
   462  
   463  				if err != nil {
   464  					sg.cache.storeVersionMap(pvl, true)
   465  				}
   466  			case sourceHasLatestLocally:
   467  				err = sg.suprvsr.do(ctx, sg.src.sourceType(), ctSourceFetch, func(ctx context.Context) error {
   468  					return sg.src.updateLocal(ctx)
   469  				})
   470  			}
   471  
   472  			if err != nil {
   473  				return
   474  			}
   475  
   476  			checked := flag | addlState
   477  			sg.srcState |= checked
   478  			todo &= ^checked
   479  		}
   480  
   481  		flag <<= 1
   482  	}
   483  
   484  	return 0, nil
   485  }
   486  
   487  // source is an abstraction around the different underlying types (git, bzr, hg,
   488  // svn, maybe raw on-disk code, and maybe eventually a registry) that can
   489  // provide versioned project source trees.
   490  type source interface {
   491  	existsLocally(context.Context) bool
   492  	existsUpstream(context.Context) bool
   493  	upstreamURL() string
   494  	initLocal(context.Context) error
   495  	updateLocal(context.Context) error
   496  	listVersions(context.Context) ([]PairedVersion, error)
   497  	getManifestAndLock(context.Context, ProjectRoot, Revision, ProjectAnalyzer) (Manifest, Lock, error)
   498  	listPackages(context.Context, ProjectRoot, Revision) (pkgtree.PackageTree, error)
   499  	revisionPresentIn(Revision) (bool, error)
   500  	exportRevisionTo(context.Context, Revision, string) error
   501  	sourceType() string
   502  }