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 }