github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/timecmp/cmp.go (about)

     1  // Package timecmp provides utility functions for comparing apiserver and stdlib times.
     2  //
     3  // There are a couple big pitfalls when using apiserver time types.
     4  //
     5  // First, the apiserver time types (metav1.Time & metav1.MicroTime) have second and microsecond
     6  // granularity once serialized, respectively. Internally, however, they are wrappers around
     7  // the Go stdlib times. As a result, initialized values that have not yet round-tripped to
     8  // the server can have more granularity than they should.
     9  //
    10  // To address this issue, there are convenience constructors in tilt/pkg/apis that should be
    11  // used for conversions from Go stdlib time types, including Now(). These are similar to the
    12  // ones provided by metav1 itself except that they _immediately_ truncate.
    13  //
    14  // The second issue is addressed by this package, which is that internal timestamps within
    15  // the Tilt engine often have higher granularity, which means comparisons can be problematic.
    16  // For example, if an internal timestamp of an operation is held as a Go stdlib time.Time value
    17  // and then stored on an entity as a metav1.Time object, future comparisons might not behave as
    18  // expected since the latter value will be truncated.
    19  //
    20  // The comparison functions provided by this package normalize values to the lowest granularity
    21  // of the values being compared before performing the actual comparison.
    22  package timecmp
    23  
    24  import (
    25  	"fmt"
    26  	"reflect"
    27  	"time"
    28  
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  )
    31  
    32  // commonTime is the common interface between apiserver + Go stdlib time types necessary for
    33  // normalization.
    34  type commonTime interface {
    35  	Truncate(duration time.Duration) time.Time
    36  }
    37  
    38  // Equal returns true of the normalized versions of a and b are equal.
    39  //
    40  // Values are normalized to the lowest granularity between the two values: seconds if either
    41  // is metav1.Time, microseconds if either is metav1.MicroTime, or monotonically-stripped if
    42  // both are time.Time. Nil time values are normalized to the zero-time.
    43  func Equal(a, b commonTime) bool {
    44  	aNorm, bNorm := normalize(a, b)
    45  	return aNorm.Equal(bNorm)
    46  }
    47  
    48  // Before returns true if the normalized version of a is strictly before the normalized version of b.
    49  //
    50  // Values are normalized to the lowest granularity between the two values: seconds if either
    51  // is metav1.Time, microseconds if either is metav1.MicroTime, or monotonically-stripped if
    52  // both are time.Time. Nil time values are normalized to the zero-time.
    53  func Before(a, b commonTime) bool {
    54  	aNorm, bNorm := normalize(a, b)
    55  	return aNorm.Before(bNorm)
    56  }
    57  
    58  // BeforeOrEqual returns true if the normalized version of a is before or equal to the normalized version of b.
    59  //
    60  // Values are normalized to the lowest granularity between the two values: seconds if either
    61  // is metav1.Time, microseconds if either is metav1.MicroTime, or monotonically-stripped if
    62  // both are time.Time. Nil time values are normalized to the zero-time.
    63  func BeforeOrEqual(a, b commonTime) bool {
    64  	aNorm, bNorm := normalize(a, b)
    65  	return aNorm.Before(bNorm) || aNorm.Equal(bNorm)
    66  }
    67  
    68  // After returns true if the normalized version of a is strictly after the normalized version of b.
    69  //
    70  // Values are normalized to the lowest granularity between the two values: seconds if either
    71  // is metav1.Time, microseconds if either is metav1.MicroTime, or monotonically-stripped if
    72  // both are time.Time. Nil time values are normalized to the zero-time.
    73  func After(a, b commonTime) bool {
    74  	aNorm, bNorm := normalize(a, b)
    75  	return aNorm.After(bNorm)
    76  }
    77  
    78  // AfterOrEqual returns true if the normalized version of a is after or equal to the normalized version of b.
    79  //
    80  // Values are normalized to the lowest granularity between the two values: seconds if either
    81  // is metav1.Time, microseconds if either is metav1.MicroTime, or monotonically-stripped if
    82  // both are time.Time. Nil time values are normalized to the zero-time.
    83  func AfterOrEqual(a, b commonTime) bool {
    84  	aNorm, bNorm := normalize(a, b)
    85  	return aNorm.After(bNorm) || aNorm.Equal(bNorm)
    86  }
    87  
    88  // normalize returns versions of a and b truncated to the lowest available granularity.
    89  //
    90  //   - If either is metav1.Time, a and b are truncated to time.Second.
    91  //   - If either is metav1.MicroTime, a and b are truncated to time.Microsecond.
    92  //   - If both a and b are time.Time, a and b have their monotonic clock reading stripped but are otherwise untouched.
    93  //   - If either is nil, nil value(s) are converted to the zero time and the non-nil value (if present) has the
    94  //     monotonic clock reading stripped.
    95  //   - Otherwise, this function will panic.
    96  func normalize(a, b commonTime) (time.Time, time.Time) {
    97  	var anySeconds bool
    98  	var anyMicroseconds bool
    99  	for _, x := range []commonTime{a, b} {
   100  		switch x.(type) {
   101  		case metav1.Time, *metav1.Time:
   102  			anySeconds = true
   103  		case metav1.MicroTime, *metav1.MicroTime:
   104  			anyMicroseconds = true
   105  		// stdlib time is accepted, but has nanosecond-granularity, so nothing more to do
   106  		case time.Time, *time.Time:
   107  		case nil:
   108  			// coerce nils to zero time or strip off monotonic clock reading,
   109  			// granularity isn't important since at least one value is nil
   110  			return truncate(a, 0), truncate(b, 0)
   111  		default:
   112  			panic(fmt.Errorf("unexpected type for time normalization: %T", x))
   113  		}
   114  	}
   115  
   116  	if anySeconds {
   117  		return truncate(a, time.Second), truncate(b, time.Second)
   118  	}
   119  
   120  	if anyMicroseconds {
   121  		return truncate(a, time.Microsecond), truncate(b, time.Microsecond)
   122  	}
   123  
   124  	// truncate with value <= 0 will strip off monotonic clock reading but
   125  	// otherwise leave untouched; this is actually desirable because Windows
   126  	// does not provide monotonically increasing clock readings, so this
   127  	// reduces the likelihood of non-portable time logic being introduced
   128  	return truncate(a, 0), truncate(b, 0)
   129  }
   130  
   131  func isNil(v commonTime) bool {
   132  	if v == nil {
   133  		return true
   134  	}
   135  
   136  	// K8s types will come back with typed nils, so we need to use reflection
   137  	// to handle them properly
   138  	x := reflect.ValueOf(v)
   139  	if x.Kind() == reflect.Ptr && x.IsNil() {
   140  		return true
   141  	}
   142  
   143  	return false
   144  }
   145  
   146  func truncate(v commonTime, d time.Duration) time.Time {
   147  	if isNil(v) {
   148  		return time.Time{}
   149  	}
   150  	return v.Truncate(d)
   151  }