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 }