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