github.com/v2fly/tools@v0.100.0/internal/imports/mod_cache.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package imports 6 7 import ( 8 "context" 9 "fmt" 10 "sync" 11 12 "github.com/v2fly/tools/internal/gopathwalk" 13 ) 14 15 // To find packages to import, the resolver needs to know about all of the 16 // the packages that could be imported. This includes packages that are 17 // already in modules that are in (1) the current module, (2) replace targets, 18 // and (3) packages in the module cache. Packages in (1) and (2) may change over 19 // time, as the client may edit the current module and locally replaced modules. 20 // The module cache (which includes all of the packages in (3)) can only 21 // ever be added to. 22 // 23 // The resolver can thus save state about packages in the module cache 24 // and guarantee that this will not change over time. To obtain information 25 // about new modules added to the module cache, the module cache should be 26 // rescanned. 27 // 28 // It is OK to serve information about modules that have been deleted, 29 // as they do still exist. 30 // TODO(suzmue): can we share information with the caller about 31 // what module needs to be downloaded to import this package? 32 33 type directoryPackageStatus int 34 35 const ( 36 _ directoryPackageStatus = iota 37 directoryScanned 38 nameLoaded 39 exportsLoaded 40 ) 41 42 type directoryPackageInfo struct { 43 // status indicates the extent to which this struct has been filled in. 44 status directoryPackageStatus 45 // err is non-nil when there was an error trying to reach status. 46 err error 47 48 // Set when status >= directoryScanned. 49 50 // dir is the absolute directory of this package. 51 dir string 52 rootType gopathwalk.RootType 53 // nonCanonicalImportPath is the package's expected import path. It may 54 // not actually be importable at that path. 55 nonCanonicalImportPath string 56 57 // Module-related information. 58 moduleDir string // The directory that is the module root of this dir. 59 moduleName string // The module name that contains this dir. 60 61 // Set when status >= nameLoaded. 62 63 packageName string // the package name, as declared in the source. 64 65 // Set when status >= exportsLoaded. 66 67 exports []string 68 } 69 70 // reachedStatus returns true when info has a status at least target and any error associated with 71 // an attempt to reach target. 72 func (info *directoryPackageInfo) reachedStatus(target directoryPackageStatus) (bool, error) { 73 if info.err == nil { 74 return info.status >= target, nil 75 } 76 if info.status == target { 77 return true, info.err 78 } 79 return true, nil 80 } 81 82 // dirInfoCache is a concurrency safe map for storing information about 83 // directories that may contain packages. 84 // 85 // The information in this cache is built incrementally. Entries are initialized in scan. 86 // No new keys should be added in any other functions, as all directories containing 87 // packages are identified in scan. 88 // 89 // Other functions, including loadExports and findPackage, may update entries in this cache 90 // as they discover new things about the directory. 91 // 92 // The information in the cache is not expected to change for the cache's 93 // lifetime, so there is no protection against competing writes. Users should 94 // take care not to hold the cache across changes to the underlying files. 95 // 96 // TODO(suzmue): consider other concurrency strategies and data structures (RWLocks, sync.Map, etc) 97 type dirInfoCache struct { 98 mu sync.Mutex 99 // dirs stores information about packages in directories, keyed by absolute path. 100 dirs map[string]*directoryPackageInfo 101 listeners map[*int]cacheListener 102 } 103 104 type cacheListener func(directoryPackageInfo) 105 106 // ScanAndListen calls listener on all the items in the cache, and on anything 107 // newly added. The returned stop function waits for all in-flight callbacks to 108 // finish and blocks new ones. 109 func (d *dirInfoCache) ScanAndListen(ctx context.Context, listener cacheListener) func() { 110 ctx, cancel := context.WithCancel(ctx) 111 112 // Flushing out all the callbacks is tricky without knowing how many there 113 // are going to be. Setting an arbitrary limit makes it much easier. 114 const maxInFlight = 10 115 sema := make(chan struct{}, maxInFlight) 116 for i := 0; i < maxInFlight; i++ { 117 sema <- struct{}{} 118 } 119 120 cookie := new(int) // A unique ID we can use for the listener. 121 122 // We can't hold mu while calling the listener. 123 d.mu.Lock() 124 var keys []string 125 for key := range d.dirs { 126 keys = append(keys, key) 127 } 128 d.listeners[cookie] = func(info directoryPackageInfo) { 129 select { 130 case <-ctx.Done(): 131 return 132 case <-sema: 133 } 134 listener(info) 135 sema <- struct{}{} 136 } 137 d.mu.Unlock() 138 139 stop := func() { 140 cancel() 141 d.mu.Lock() 142 delete(d.listeners, cookie) 143 d.mu.Unlock() 144 for i := 0; i < maxInFlight; i++ { 145 <-sema 146 } 147 } 148 149 // Process the pre-existing keys. 150 for _, k := range keys { 151 select { 152 case <-ctx.Done(): 153 return stop 154 default: 155 } 156 if v, ok := d.Load(k); ok { 157 listener(v) 158 } 159 } 160 161 return stop 162 } 163 164 // Store stores the package info for dir. 165 func (d *dirInfoCache) Store(dir string, info directoryPackageInfo) { 166 d.mu.Lock() 167 _, old := d.dirs[dir] 168 d.dirs[dir] = &info 169 var listeners []cacheListener 170 for _, l := range d.listeners { 171 listeners = append(listeners, l) 172 } 173 d.mu.Unlock() 174 175 if !old { 176 for _, l := range listeners { 177 l(info) 178 } 179 } 180 } 181 182 // Load returns a copy of the directoryPackageInfo for absolute directory dir. 183 func (d *dirInfoCache) Load(dir string) (directoryPackageInfo, bool) { 184 d.mu.Lock() 185 defer d.mu.Unlock() 186 info, ok := d.dirs[dir] 187 if !ok { 188 return directoryPackageInfo{}, false 189 } 190 return *info, true 191 } 192 193 // Keys returns the keys currently present in d. 194 func (d *dirInfoCache) Keys() (keys []string) { 195 d.mu.Lock() 196 defer d.mu.Unlock() 197 for key := range d.dirs { 198 keys = append(keys, key) 199 } 200 return keys 201 } 202 203 func (d *dirInfoCache) CachePackageName(info directoryPackageInfo) (string, error) { 204 if loaded, err := info.reachedStatus(nameLoaded); loaded { 205 return info.packageName, err 206 } 207 if scanned, err := info.reachedStatus(directoryScanned); !scanned || err != nil { 208 return "", fmt.Errorf("cannot read package name, scan error: %v", err) 209 } 210 info.packageName, info.err = packageDirToName(info.dir) 211 info.status = nameLoaded 212 d.Store(info.dir, info) 213 return info.packageName, info.err 214 } 215 216 func (d *dirInfoCache) CacheExports(ctx context.Context, env *ProcessEnv, info directoryPackageInfo) (string, []string, error) { 217 if reached, _ := info.reachedStatus(exportsLoaded); reached { 218 return info.packageName, info.exports, info.err 219 } 220 if reached, err := info.reachedStatus(nameLoaded); reached && err != nil { 221 return "", nil, err 222 } 223 info.packageName, info.exports, info.err = loadExportsFromFiles(ctx, env, info.dir, false) 224 if info.err == context.Canceled || info.err == context.DeadlineExceeded { 225 return info.packageName, info.exports, info.err 226 } 227 // The cache structure wants things to proceed linearly. We can skip a 228 // step here, but only if we succeed. 229 if info.status == nameLoaded || info.err == nil { 230 info.status = exportsLoaded 231 } else { 232 info.status = nameLoaded 233 } 234 d.Store(info.dir, info) 235 return info.packageName, info.exports, info.err 236 }