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  }