-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix issues with unpopulated gc_rules and handling of Xd duration (#8686…
…) (#15595) * Fix issues with unpopulated gc_rules and handling of Xd duration * Fix copt/paste issues * Extract duration_parsing_helper.go * Extract duration_parsing_helper.go * Add unit tests * Add time.UNIT * Fixed duration helper * Add better handling for days and hours * Add better handling for days and hours * fixed issue with diffs between gc_rules * minor fix * Add tests * package name fix * fix tests * fix tests * fix tests * fix tests * fix tests Signed-off-by: Modular Magician <[email protected]>
- Loading branch information
1 parent
1903caa
commit bca6b60
Showing
6 changed files
with
484 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
```release-note:bug | ||
bigtable: fixed permadiff in `google_bigtable_gc_policy.gc_rules` when `mode` is specified | ||
``` | ||
```release-note:bug | ||
bigtable: fixed permadiff in `google_bigtable_gc_policy.gc_rules` when `max_age` is specified using increments larger than hours | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
package bigtable | ||
|
||
import ( | ||
"errors" | ||
"time" | ||
) | ||
|
||
var unitMap = map[string]uint64{ | ||
"ns": uint64(time.Nanosecond), | ||
"us": uint64(time.Microsecond), | ||
"µs": uint64(time.Microsecond), // U+00B5 = micro symbol | ||
"μs": uint64(time.Microsecond), // U+03BC = Greek letter mu | ||
"ms": uint64(time.Millisecond), | ||
"s": uint64(time.Second), | ||
"m": uint64(time.Minute), | ||
"h": uint64(time.Hour), | ||
"d": uint64(time.Hour) * 24, | ||
"w": uint64(time.Hour) * 24 * 7, | ||
} | ||
|
||
// ParseDuration parses a duration string. | ||
// A duration string is a possibly signed sequence of | ||
// decimal numbers, each with optional fraction and a unit suffix, | ||
// such as "300ms", "-1.5h" or "2h45m". | ||
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | ||
func ParseDuration(s string) (time.Duration, error) { | ||
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ | ||
orig := s | ||
var d uint64 | ||
neg := false | ||
|
||
// Consume [-+]? | ||
if s != "" { | ||
c := s[0] | ||
if c == '-' || c == '+' { | ||
neg = c == '-' | ||
s = s[1:] | ||
} | ||
} | ||
// Special case: if all that is left is "0", this is zero. | ||
if s == "0" { | ||
return 0, nil | ||
} | ||
if s == "" { | ||
return 0, errors.New("time: invalid duration " + quote(orig)) | ||
} | ||
for s != "" { | ||
var ( | ||
v, f uint64 // integers before, after decimal point | ||
scale float64 = 1 // value = v + f/scale | ||
) | ||
|
||
var err error | ||
|
||
// The next character must be [0-9.] | ||
if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { | ||
return 0, errors.New("time: invalid duration " + quote(orig)) | ||
} | ||
// Consume [0-9]* | ||
pl := len(s) | ||
v, s, err = leadingInt(s) | ||
if err != nil { | ||
return 0, errors.New("time: invalid duration " + quote(orig)) | ||
} | ||
pre := pl != len(s) // whether we consumed anything before a period | ||
|
||
// Consume (\.[0-9]*)? | ||
post := false | ||
if s != "" && s[0] == '.' { | ||
s = s[1:] | ||
pl := len(s) | ||
f, scale, s = leadingFraction(s) | ||
post = pl != len(s) | ||
} | ||
if !pre && !post { | ||
// no digits (e.g. ".s" or "-.s") | ||
return 0, errors.New("time: invalid duration " + quote(orig)) | ||
} | ||
|
||
// Consume unit. | ||
i := 0 | ||
for ; i < len(s); i++ { | ||
c := s[i] | ||
if c == '.' || '0' <= c && c <= '9' { | ||
break | ||
} | ||
} | ||
if i == 0 { | ||
return 0, errors.New("time: missing unit in duration " + quote(orig)) | ||
} | ||
u := s[:i] | ||
s = s[i:] | ||
unit, ok := unitMap[u] | ||
if !ok { | ||
return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig)) | ||
} | ||
if v > 1<<63/unit { | ||
// overflow | ||
return 0, errors.New("time: invalid duration " + quote(orig)) | ||
} | ||
v *= unit | ||
if f > 0 { | ||
// float64 is needed to be nanosecond accurate for fractions of hours. | ||
// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) | ||
v += uint64(float64(f) * (float64(unit) / scale)) | ||
if v > 1<<63 { | ||
// overflow | ||
return 0, errors.New("time: invalid duration " + quote(orig)) | ||
} | ||
} | ||
d += v | ||
if d > 1<<63 { | ||
return 0, errors.New("time: invalid duration " + quote(orig)) | ||
} | ||
} | ||
if neg { | ||
return -time.Duration(d), nil | ||
} | ||
if d > 1<<63-1 { | ||
return 0, errors.New("time: invalid duration " + quote(orig)) | ||
} | ||
return time.Duration(d), nil | ||
} | ||
|
||
var errLeadingInt = errors.New("time: bad [0-9]*") // never printed | ||
|
||
// leadingInt consumes the leading [0-9]* from s. | ||
func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) { | ||
i := 0 | ||
for ; i < len(s); i++ { | ||
c := s[i] | ||
if c < '0' || c > '9' { | ||
break | ||
} | ||
if x > 1<<63/10 { | ||
// overflow | ||
return 0, rem, errLeadingInt | ||
} | ||
x = x*10 + uint64(c) - '0' | ||
if x > 1<<63 { | ||
// overflow | ||
return 0, rem, errLeadingInt | ||
} | ||
} | ||
return x, s[i:], nil | ||
} | ||
|
||
// leadingFraction consumes the leading [0-9]* from s. | ||
// It is used only for fractions, so does not return an error on overflow, | ||
// it just stops accumulating precision. | ||
func leadingFraction(s string) (x uint64, scale float64, rem string) { | ||
i := 0 | ||
scale = 1 | ||
overflow := false | ||
for ; i < len(s); i++ { | ||
c := s[i] | ||
if c < '0' || c > '9' { | ||
break | ||
} | ||
if overflow { | ||
continue | ||
} | ||
if x > (1<<63-1)/10 { | ||
// It's possible for overflow to give a positive number, so take care. | ||
overflow = true | ||
continue | ||
} | ||
y := x*10 + uint64(c) - '0' | ||
if y > 1<<63 { | ||
overflow = true | ||
continue | ||
} | ||
x = y | ||
scale *= 10 | ||
} | ||
return x, scale, s[i:] | ||
} | ||
|
||
// These are borrowed from unicode/utf8 and strconv and replicate behavior in | ||
// that package, since we can't take a dependency on either. | ||
const ( | ||
lowerhex = "0123456789abcdef" | ||
runeSelf = 0x80 | ||
runeError = '\uFFFD' | ||
) | ||
|
||
func quote(s string) string { | ||
buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes | ||
buf[0] = '"' | ||
for i, c := range s { | ||
if c >= runeSelf || c < ' ' { | ||
// This means you are asking us to parse a time.Duration or | ||
// time.Location with unprintable or non-ASCII characters in it. | ||
// We don't expect to hit this case very often. We could try to | ||
// reproduce strconv.Quote's behavior with full fidelity but | ||
// given how rarely we expect to hit these edge cases, speed and | ||
// conciseness are better. | ||
var width int | ||
if c == runeError { | ||
width = 1 | ||
if i+2 < len(s) && s[i:i+3] == string(runeError) { | ||
width = 3 | ||
} | ||
} else { | ||
width = len(string(c)) | ||
} | ||
for j := 0; j < width; j++ { | ||
buf = append(buf, `\x`...) | ||
buf = append(buf, lowerhex[s[i+j]>>4]) | ||
buf = append(buf, lowerhex[s[i+j]&0xF]) | ||
} | ||
} else { | ||
if c == '"' || c == '\\' { | ||
buf = append(buf, '\\') | ||
} | ||
buf = append(buf, string(c)...) | ||
} | ||
} | ||
buf = append(buf, '"') | ||
return string(buf) | ||
} |
157 changes: 157 additions & 0 deletions
157
google/services/bigtable/duration_parsing_helper_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
package bigtable_test | ||
|
||
import ( | ||
"math" | ||
"math/rand" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
"github.com/hashicorp/terraform-provider-google/google/services/bigtable" | ||
) | ||
|
||
var parseDurationTests = []struct { | ||
in string | ||
want time.Duration | ||
}{ | ||
// simple | ||
{"0", 0}, | ||
{"5s", 5 * time.Second}, | ||
{"30s", 30 * time.Second}, | ||
{"1478s", 1478 * time.Second}, | ||
// sign | ||
{"-5s", -5 * time.Second}, | ||
{"+5s", 5 * time.Second}, | ||
{"-0", 0}, | ||
{"+0", 0}, | ||
// decimal | ||
{"5.0s", 5 * time.Second}, | ||
{"5.6s", 5*time.Second + 600*time.Millisecond}, | ||
{"5.s", 5 * time.Second}, | ||
{".5s", 500 * time.Millisecond}, | ||
{"1.0s", 1 * time.Second}, | ||
{"1.00s", 1 * time.Second}, | ||
{"1.004s", 1*time.Second + 4*time.Millisecond}, | ||
{"1.0040s", 1*time.Second + 4*time.Millisecond}, | ||
{"100.00100s", 100*time.Second + 1*time.Millisecond}, | ||
// different units | ||
{"10ns", 10 * time.Nanosecond}, | ||
{"11us", 11 * time.Microsecond}, | ||
{"12µs", 12 * time.Microsecond}, // U+00B5 | ||
{"12μs", 12 * time.Microsecond}, // U+03BC | ||
{"13ms", 13 * time.Millisecond}, | ||
{"14s", 14 * time.Second}, | ||
{"15m", 15 * time.Minute}, | ||
{"16h", 16 * time.Hour}, | ||
{"5d", 5 * 24 * time.Hour}, | ||
// composite durations | ||
{"3h30m", 3*time.Hour + 30*time.Minute}, | ||
{"10.5s4m", 4*time.Minute + 10*time.Second + 500*time.Millisecond}, | ||
{"-2m3.4s", -(2*time.Minute + 3*time.Second + 400*time.Millisecond)}, | ||
{"1h2m3s4ms5us6ns", 1*time.Hour + 2*time.Minute + 3*time.Second + 4*time.Millisecond + 5*time.Microsecond + 6*time.Nanosecond}, | ||
{"39h9m14.425s", 39*time.Hour + 9*time.Minute + 14*time.Second + 425*time.Millisecond}, | ||
// large value | ||
{"52763797000ns", 52763797000 * time.Nanosecond}, | ||
// more than 9 digits after decimal point, see https://golang.org/issue/6617 | ||
{"0.3333333333333333333h", 20 * time.Minute}, | ||
// 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64 | ||
{"9007199254740993ns", (1<<53 + 1) * time.Nanosecond}, | ||
// largest duration that can be represented by int64 in nanoseconds | ||
{"9223372036854775807ns", (1<<63 - 1) * time.Nanosecond}, | ||
{"9223372036854775.807us", (1<<63 - 1) * time.Nanosecond}, | ||
{"9223372036s854ms775us807ns", (1<<63 - 1) * time.Nanosecond}, | ||
{"-9223372036854775808ns", -1 << 63 * time.Nanosecond}, | ||
{"-9223372036854775.808us", -1 << 63 * time.Nanosecond}, | ||
{"-9223372036s854ms775us808ns", -1 << 63 * time.Nanosecond}, | ||
// largest negative value | ||
{"-9223372036854775808ns", -1 << 63 * time.Nanosecond}, | ||
// largest negative round trip value, see https://golang.org/issue/48629 | ||
{"-2562047h47m16.854775808s", -1 << 63 * time.Nanosecond}, | ||
// huge string; issue 15011. | ||
{"0.100000000000000000000h", 6 * time.Minute}, | ||
// This value tests the first overflow check in leadingFraction. | ||
{"0.830103483285477580700h", 49*time.Minute + 48*time.Second + 372539827*time.Nanosecond}, | ||
} | ||
|
||
func TestParseDuration(t *testing.T) { | ||
for _, tc := range parseDurationTests { | ||
d, err := bigtable.ParseDuration(tc.in) | ||
if err != nil || d != tc.want { | ||
t.Errorf("bigtable.ParseDuration(%q) = %v, %v, want %v, nil", tc.in, d, err, tc.want) | ||
} | ||
} | ||
} | ||
|
||
var parseDurationErrorTests = []struct { | ||
in string | ||
expect string | ||
}{ | ||
// invalid | ||
{"", `""`}, | ||
{"3", `"3"`}, | ||
{"-", `"-"`}, | ||
{"s", `"s"`}, | ||
{".", `"."`}, | ||
{"-.", `"-."`}, | ||
{".s", `".s"`}, | ||
{"+.s", `"+.s"`}, | ||
{"\x85\x85", `"\x85\x85"`}, | ||
{"\xffff", `"\xffff"`}, | ||
{"hello \xffff world", `"hello \xffff world"`}, | ||
{"\uFFFD", `"\xef\xbf\xbd"`}, // utf8.RuneError | ||
{"\uFFFD hello \uFFFD world", `"\xef\xbf\xbd hello \xef\xbf\xbd world"`}, // utf8.RuneError | ||
// overflow | ||
{"9223372036854775810ns", `"9223372036854775810ns"`}, | ||
{"9223372036854775808ns", `"9223372036854775808ns"`}, | ||
{"-9223372036854775809ns", `"-9223372036854775809ns"`}, | ||
{"9223372036854776us", `"9223372036854776us"`}, | ||
{"3000000h", `"3000000h"`}, | ||
{"9223372036854775.808us", `"9223372036854775.808us"`}, | ||
{"9223372036854ms775us808ns", `"9223372036854ms775us808ns"`}, | ||
} | ||
|
||
func TestParseDurationErrors(t *testing.T) { | ||
for _, tc := range parseDurationErrorTests { | ||
_, err := bigtable.ParseDuration(tc.in) | ||
if err == nil { | ||
t.Errorf("bigtable.ParseDuration(%q) = _, nil, want _, non-nil", tc.in) | ||
} else if !strings.Contains(err.Error(), tc.expect) { | ||
t.Errorf("bigtable.ParseDuration(%q) = _, %q, error does not contain %q", tc.in, err, tc.expect) | ||
} | ||
} | ||
} | ||
|
||
func TestParseDurationRoundTrip(t *testing.T) { | ||
// https://golang.org/issue/48629 | ||
max0 := time.Duration(math.MaxInt64) | ||
max1, err := bigtable.ParseDuration(max0.String()) | ||
if err != nil || max0 != max1 { | ||
t.Errorf("round-trip failed: %d => %q => %d, %v", max0, max0.String(), max1, err) | ||
} | ||
|
||
min0 := time.Duration(math.MinInt64) | ||
min1, err := bigtable.ParseDuration(min0.String()) | ||
if err != nil || min0 != min1 { | ||
t.Errorf("round-trip failed: %d => %q => %d, %v", min0, min0.String(), min1, err) | ||
} | ||
|
||
for i := 0; i < 100; i++ { | ||
// Resolutions finer than milliseconds will result in | ||
// imprecise round-trips. | ||
d0 := time.Duration(rand.Int31()) * time.Millisecond | ||
s := d0.String() | ||
d1, err := bigtable.ParseDuration(s) | ||
if err != nil || d0 != d1 { | ||
t.Errorf("round-trip failed: %d => %q => %d, %v", d0, s, d1, err) | ||
} | ||
} | ||
} | ||
|
||
func BenchmarkParseDuration(b *testing.B) { | ||
for i := 0; i < b.N; i++ { | ||
bigtable.ParseDuration("9007199254.740993ms") | ||
bigtable.ParseDuration("9007199254740993ns") | ||
} | ||
} |
Oops, something went wrong.