github.com/sdboyer/gps@v0.16.3/version_queue.go (about) 1 package gps 2 3 import ( 4 "fmt" 5 "strings" 6 ) 7 8 type failedVersion struct { 9 v Version 10 f error 11 } 12 13 type versionQueue struct { 14 id ProjectIdentifier 15 pi []Version 16 lockv, prefv Version 17 fails []failedVersion 18 b sourceBridge 19 failed bool 20 allLoaded bool 21 adverr error 22 } 23 24 func newVersionQueue(id ProjectIdentifier, lockv, prefv Version, b sourceBridge) (*versionQueue, error) { 25 vq := &versionQueue{ 26 id: id, 27 b: b, 28 } 29 30 // Lock goes in first, if present 31 if lockv != nil { 32 vq.lockv = lockv 33 vq.pi = append(vq.pi, lockv) 34 } 35 36 // Preferred version next 37 if prefv != nil { 38 vq.prefv = prefv 39 vq.pi = append(vq.pi, prefv) 40 } 41 42 if len(vq.pi) == 0 { 43 var err error 44 vq.pi, err = vq.b.listVersions(vq.id) 45 if err != nil { 46 // TODO(sdboyer) pushing this error this early entails that we 47 // unconditionally deep scan (e.g. vendor), as well as hitting the 48 // network. 49 return nil, err 50 } 51 vq.allLoaded = true 52 } 53 54 return vq, nil 55 } 56 57 func (vq *versionQueue) current() Version { 58 if len(vq.pi) > 0 { 59 return vq.pi[0] 60 } 61 62 return nil 63 } 64 65 // advance moves the versionQueue forward to the next available version, 66 // recording the failure that eliminated the current version. 67 func (vq *versionQueue) advance(fail error) error { 68 // Nothing in the queue means...nothing in the queue, nicely enough 69 if vq.adverr != nil || len(vq.pi) == 0 { // should be a redundant check, but just in case 70 return vq.adverr 71 } 72 73 // Record the fail reason and pop the queue 74 vq.fails = append(vq.fails, failedVersion{ 75 v: vq.pi[0], 76 f: fail, 77 }) 78 vq.pi = vq.pi[1:] 79 80 // *now*, if the queue is empty, ensure all versions have been loaded 81 if len(vq.pi) == 0 { 82 if vq.allLoaded { 83 // This branch gets hit when the queue is first fully exhausted, 84 // after a previous advance() already called ListVersions(). 85 return nil 86 } 87 vq.allLoaded = true 88 89 var vltmp []Version 90 vltmp, vq.adverr = vq.b.listVersions(vq.id) 91 if vq.adverr != nil { 92 return vq.adverr 93 } 94 // defensive copy - calling listVersions here means slice contents may 95 // be modified when removing prefv/lockv. 96 vq.pi = make([]Version, len(vltmp)) 97 copy(vq.pi, vltmp) 98 99 // search for and remove lockv and prefv, in a pointer GC-safe manner 100 // 101 // could use the version comparator for binary search here to avoid 102 // O(n) each time...if it matters 103 var delkeys []int 104 for k, pi := range vq.pi { 105 if pi == vq.lockv || pi == vq.prefv { 106 delkeys = append(delkeys, k) 107 } 108 } 109 110 for k, dk := range delkeys { 111 dk -= k 112 copy(vq.pi[dk:], vq.pi[dk+1:]) 113 // write nil to final position for GC safety 114 vq.pi[len(vq.pi)-1] = nil 115 vq.pi = vq.pi[:len(vq.pi)-1] 116 } 117 118 if len(vq.pi) == 0 { 119 // If listing versions added nothing (new), then return now 120 return nil 121 } 122 } 123 124 // We're finally sure that there's something in the queue. Remove the 125 // failure marker, as the current version may have failed, but the next one 126 // hasn't yet 127 vq.failed = false 128 129 // If all have been loaded and the queue is empty, we're definitely out 130 // of things to try. Return empty, though, because vq semantics dictate 131 // that we don't explicitly indicate the end of the queue here. 132 return nil 133 } 134 135 // isExhausted indicates whether or not the queue has definitely been exhausted, 136 // in which case it will return true. 137 // 138 // It may return false negatives - suggesting that there is more in the queue 139 // when a subsequent call to current() will be empty. Plan accordingly. 140 func (vq *versionQueue) isExhausted() bool { 141 if !vq.allLoaded { 142 return false 143 } 144 return len(vq.pi) == 0 145 } 146 147 func (vq *versionQueue) String() string { 148 var vs []string 149 150 for _, v := range vq.pi { 151 vs = append(vs, v.String()) 152 } 153 return fmt.Sprintf("[%s]", strings.Join(vs, ", ")) 154 }