Skip to content

Instantly share code, notes, and snippets.

@sdboyer
Created March 15, 2017 13:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sdboyer/a3962742624e5e55104cfdbcec38d16f to your computer and use it in GitHub Desktop.
Save sdboyer/a3962742624e5e55104cfdbcec38d16f to your computer and use it in GitHub Desktop.
package gps
import (
"context"
"errors"
"fmt"
"net/url"
"sync"
radix "github.com/armon/go-radix"
)
type srcCommand interface {
}
type callManager struct {
}
type sourcesCompany struct {
actionChan chan func()
ctx context.Context
callMgr callManager
srcs map[string]sourceActor
deducer *deductionCoordinator
}
func (sc *sourcesCompany) getSourceFor(id ProjectIdentifier) (sourceActor, error) {
normalizedName := id.normalizedSource()
type ret struct {
sa sourceActor
err error
}
retchan := make(chan ret, 0)
sc.actionChan <- func() {
// Simple case - actor exists for this source, send it back right away
if srcActor, has := sa.srcs[normalizedName]; has {
retchan <- ret{sa: srcActor}
return
}
// No actor for this path yet. Set one up.
pd, err := sc.deducer.deduceRootPath(normalizedName)
if err != nil {
// As in the deducer, don't cache errors to make externally-driven
// retry strategies feasible.
retchan <- ret{err: err}
return
}
srcActor := sourceActor{}
}
retval := <-retchan
return retval.sa, retval.err
}
// sourceActors act as a gateway to all calls for data from sources.
type sourceActor struct {
maybe maybeSource
}
type deductionCoordinator struct {
ctx context.Context
callMgr callManager
rootxt *radix.Tree
deducext *deducerTrie
actionChan chan func()
}
func newDeductionCoordinator(ctx context.Context, cm callManager) *deductionCoordinator {
dc := &deductionCoordinator{
ctx: ctx,
callMgr: cm,
rootxt: radix.New(),
deducext: pathDeducerTrie(),
}
// Start listener loop
go func() {
for {
select {
case <-ctx.Done():
// TODO should this iterate over the rootxt and kill open vda?
close(dc.actionChan)
case action := <-dc.actionChan:
action()
}
}
}()
return dc
}
func (dc *deductionCoordinator) deduceRootPath(path string) (pathDeduction, error) {
retchan := make(chan interface{}, 0)
dc.actionChan <- func() {
vdaDeduce := func(vda *vanityDeducerActor) {
pd, err := vda.deduce(path)
if err != nil {
retchan <- err
} else {
retchan <- pd
}
}
// First, check the rootxt to see if there's a prefix match - if so, we
// can return that and move on.
if prefix, data, has := dc.rootxt.LongestPrefix(path); has && isPathPrefixOrEqual(prefix, path) {
switch d := data.(type) {
case maybeSource:
retchan <- pathDeduction{root: prefix, mb: d}
case *vanityDeducerActor:
// Multiple calls have come in for a similar path shape during
// the window in which the HTTP request to retrieve go get
// metadata is in flight. Fold this request in with the existing
// one(s) by giving it its own goroutine that awaits a response
// from the running vanityDeducerActor.
go vdaDeduce(d)
default:
panic(fmt.Sprintf("unexpected %T in deductionCoordinator.rootxt: %v", d, d))
}
// Finding either a finished maybeSource or an in-flight vanity
// deduction means there's nothing more to do on this action.
return
}
// No match. Try known path deduction first.
pd, err := dc.deduceKnownPaths(path)
if err == nil {
// Deduction worked; store it in the rootxt, send on retchan and
// terminate.
// FIXME deal with changing path vs. root. Probably needs to be
// predeclared and reused in the vda returnFunc
dc.rootxt.Insert(pd.root, pd.mb)
retchan <- pd
return
}
if err != noKnownPathMatch {
retchan <- err
return
}
// The err indicates no known path matched. It's still possible that
// retrieving go get metadata might do the trick.
vda := &vanityDeducerActor{
basePath: path,
callMgr: dc.callMgr,
ctx: dc.ctx,
// The vanity deducer will call this func with a completed
// pathDeduction if it succeeds in finding one. We process it
// back through the action channel to ensure serialized
// access to the rootxt map.
returnFunc: func(pd pathDeduction) {
dc.actionChan <- func() {
if pd.root != path {
// Clean out the vanity deducer, we don't need it
// anymore.
dc.rootxt.Delete(path)
}
dc.rootxt.Insert(pd.root, pd.mb)
}
},
}
// Save the vda in the rootxt so that calls checking on similar
// paths made while the request is in flight can be folded together.
dc.rootxt.Insert(path, vda)
// Spawn a new goroutine for the HTTP-backed deduction process.
go vdaDeduce(vda)
}
switch typ := (<-retchan).(type) {
case pathDeduction:
return typ, nil
case error:
return pathDeduction{}, typ
default:
panic("unreachable")
}
}
// pathDeduction represents the results of a successful import path deduction -
// a root path, plus a maybeSource that can be used to attempt to connect to
// the source.
type pathDeduction struct {
root string
mb maybeSource
}
var noKnownPathMatch = errors.New("no known path match")
func (dc *deductionCoordinator) deduceKnownPaths(path string) (pathDeduction, error) {
u, path, err := normalizeURI(path)
if err != nil {
return pathDeduction{}, err
}
// First, try the root path-based matches
if _, mtch, has := dc.deducext.LongestPrefix(path); has {
root, err := mtch.deduceRoot(path)
if err != nil {
return pathDeduction{}, err
}
mb, err := mtch.deduceSource(path, u)
if err != nil {
return pathDeduction{}, err
}
return pathDeduction{
root: root,
mb: mb,
}, nil
}
// Next, try the vcs extension-based (infix) matcher
exm := vcsExtensionDeducer{regexp: vcsExtensionRegex}
if root, err := exm.deduceRoot(path); err == nil {
mb, err := exm.deduceSource(path, u)
if err != nil {
return pathDeduction{}, err
}
return pathDeduction{
root: root,
mb: mb,
}, nil
}
return pathDeduction{}, noKnownPathMatch
}
type vanityDeducerActor struct {
once sync.Once
deduced pathDeduction
deduceErr error
basePath string
returnFunc func(pathDeduction)
callMgr callManager
ctx context.Context
}
func (vda *vanityDeducerActor) deduce(path string) (pathDeduction, error) {
vda.once.Do(func() {
// FIXME interact with callmgr
//vda.callMgr.Attach()
opath := path
// FIXME should we need this first return val?
u, path, err := normalizeURI(path)
if err != nil {
vda.deduceErr = err
return
}
pd := pathDeduction{}
// Make the HTTP call to attempt to retrieve go-get metadata
root, vcs, reporoot, err := parseMetadata(path, vda.ctx)
if err != nil {
vda.deduceErr = fmt.Errorf("unable to deduce repository and source type for: %q", opath)
return
}
pd.root = root
// If we got something back at all, then it supercedes the actual input for
// the real URL to hit
repoURL, err := url.Parse(reporoot)
if err != nil {
vda.deduceErr = fmt.Errorf("server returned bad URL when searching for vanity import: %q", reporoot)
return
}
switch vcs {
case "git":
pd.mb = maybeGitSource{url: repoURL}
case "bzr":
pd.mb = maybeBzrSource{url: repoURL}
case "hg":
pd.mb = maybeHgSource{url: repoURL}
default:
vda.deduceErr = fmt.Errorf("unsupported vcs type %s in go-get metadata from %s", vcs, path)
return
}
vda.deduced = pd
// All data is assigned for other goroutines that may be waiting. Now,
// send the pathDeduction back to the deductionCoordinator by calling
// the returnFunc. This will also remove the reference to this vda in
// the coordinator's trie.
//
// When this call finishes, it is guaranteed the coordinator will have
// at least begun running the action to insert the path deduction, which
// means no other deduction request will be able to interleave and
// request the same path before the pathDeduction can be processed, but
// after this vda has been dereferenced from the trie.
vda.returnFunc(pd)
})
return vda.deduced, vda.deduceErr
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment