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  }