github.com/cilium/ebpf@v0.15.1-0.20240517100537-8079b37aa138/internal/feature.go (about)

     1  package internal
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sync"
     7  )
     8  
     9  // ErrNotSupported indicates that a feature is not supported by the current kernel.
    10  var ErrNotSupported = errors.New("not supported")
    11  
    12  // UnsupportedFeatureError is returned by FeatureTest() functions.
    13  type UnsupportedFeatureError struct {
    14  	// The minimum Linux mainline version required for this feature.
    15  	// Used for the error string, and for sanity checking during testing.
    16  	MinimumVersion Version
    17  
    18  	// The name of the feature that isn't supported.
    19  	Name string
    20  }
    21  
    22  func (ufe *UnsupportedFeatureError) Error() string {
    23  	if ufe.MinimumVersion.Unspecified() {
    24  		return fmt.Sprintf("%s not supported", ufe.Name)
    25  	}
    26  	return fmt.Sprintf("%s not supported (requires >= %s)", ufe.Name, ufe.MinimumVersion)
    27  }
    28  
    29  // Is indicates that UnsupportedFeatureError is ErrNotSupported.
    30  func (ufe *UnsupportedFeatureError) Is(target error) bool {
    31  	return target == ErrNotSupported
    32  }
    33  
    34  // FeatureTest caches the result of a [FeatureTestFn].
    35  //
    36  // Fields should not be modified after creation.
    37  type FeatureTest struct {
    38  	// The name of the feature being detected.
    39  	Name string
    40  	// Version in the form Major.Minor[.Patch].
    41  	Version string
    42  	// The feature test itself.
    43  	Fn FeatureTestFn
    44  
    45  	mu     sync.RWMutex
    46  	done   bool
    47  	result error
    48  }
    49  
    50  // FeatureTestFn is used to determine whether the kernel supports
    51  // a certain feature.
    52  //
    53  // The return values have the following semantics:
    54  //
    55  //	err == ErrNotSupported: the feature is not available
    56  //	err == nil: the feature is available
    57  //	err != nil: the test couldn't be executed
    58  type FeatureTestFn func() error
    59  
    60  // NewFeatureTest is a convenient way to create a single [FeatureTest].
    61  func NewFeatureTest(name, version string, fn FeatureTestFn) func() error {
    62  	ft := &FeatureTest{
    63  		Name:    name,
    64  		Version: version,
    65  		Fn:      fn,
    66  	}
    67  
    68  	return ft.execute
    69  }
    70  
    71  // execute the feature test.
    72  //
    73  // The result is cached if the test is conclusive.
    74  //
    75  // See [FeatureTestFn] for the meaning of the returned error.
    76  func (ft *FeatureTest) execute() error {
    77  	ft.mu.RLock()
    78  	result, done := ft.result, ft.done
    79  	ft.mu.RUnlock()
    80  
    81  	if done {
    82  		return result
    83  	}
    84  
    85  	ft.mu.Lock()
    86  	defer ft.mu.Unlock()
    87  
    88  	// The test may have been executed by another caller while we were
    89  	// waiting to acquire ft.mu.
    90  	if ft.done {
    91  		return ft.result
    92  	}
    93  
    94  	err := ft.Fn()
    95  	if err == nil {
    96  		ft.done = true
    97  		return nil
    98  	}
    99  
   100  	if errors.Is(err, ErrNotSupported) {
   101  		var v Version
   102  		if ft.Version != "" {
   103  			v, err = NewVersion(ft.Version)
   104  			if err != nil {
   105  				return fmt.Errorf("feature %s: %w", ft.Name, err)
   106  			}
   107  		}
   108  
   109  		ft.done = true
   110  		ft.result = &UnsupportedFeatureError{
   111  			MinimumVersion: v,
   112  			Name:           ft.Name,
   113  		}
   114  
   115  		return ft.result
   116  	}
   117  
   118  	// We couldn't execute the feature test to a point
   119  	// where it could make a determination.
   120  	// Don't cache the result, just return it.
   121  	return fmt.Errorf("detect support for %s: %w", ft.Name, err)
   122  }
   123  
   124  // FeatureMatrix groups multiple related feature tests into a map.
   125  //
   126  // Useful when there is a small number of discrete features which are known
   127  // at compile time.
   128  //
   129  // It must not be modified concurrently with calling [FeatureMatrix.Result].
   130  type FeatureMatrix[K comparable] map[K]*FeatureTest
   131  
   132  // Result returns the outcome of the feature test for the given key.
   133  //
   134  // It's safe to call this function concurrently.
   135  func (fm FeatureMatrix[K]) Result(key K) error {
   136  	ft, ok := fm[key]
   137  	if !ok {
   138  		return fmt.Errorf("no feature probe for %v", key)
   139  	}
   140  
   141  	return ft.execute()
   142  }
   143  
   144  // FeatureCache caches a potentially unlimited number of feature probes.
   145  //
   146  // Useful when there is a high cardinality for a feature test.
   147  type FeatureCache[K comparable] struct {
   148  	mu       sync.RWMutex
   149  	newTest  func(K) *FeatureTest
   150  	features map[K]*FeatureTest
   151  }
   152  
   153  func NewFeatureCache[K comparable](newTest func(K) *FeatureTest) *FeatureCache[K] {
   154  	return &FeatureCache[K]{
   155  		newTest:  newTest,
   156  		features: make(map[K]*FeatureTest),
   157  	}
   158  }
   159  
   160  func (fc *FeatureCache[K]) Result(key K) error {
   161  	// NB: Executing the feature test happens without fc.mu taken.
   162  	return fc.retrieve(key).execute()
   163  }
   164  
   165  func (fc *FeatureCache[K]) retrieve(key K) *FeatureTest {
   166  	fc.mu.RLock()
   167  	ft := fc.features[key]
   168  	fc.mu.RUnlock()
   169  
   170  	if ft != nil {
   171  		return ft
   172  	}
   173  
   174  	fc.mu.Lock()
   175  	defer fc.mu.Unlock()
   176  
   177  	if ft := fc.features[key]; ft != nil {
   178  		return ft
   179  	}
   180  
   181  	ft = fc.newTest(key)
   182  	fc.features[key] = ft
   183  	return ft
   184  }