From 22e48dfd624cd1a49b68ab8b3e239bfeb7ce6038 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Fri, 21 Jun 2024 09:59:32 -0700 Subject: [PATCH] Introduce grpcutil (#228) Same as https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/33688 but merging this code here will let me create and test a PR in that repository, whereas it will be messy to build off this work in the same repository. I expect this package to be deleted after https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/33688 and https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/33579 merged, as discussed in #225. Part of #227. --- NOTICE | 15 ++++ collector/grpcutil/timeout.go | 108 +++++++++++++++++++++++++++++ collector/grpcutil/timeout_test.go | 54 +++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 NOTICE create mode 100644 collector/grpcutil/timeout.go create mode 100644 collector/grpcutil/timeout_test.go diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..c6ccacb1 --- /dev/null +++ b/NOTICE @@ -0,0 +1,15 @@ +collector/grpcutil contains code derived from gRPC-Go: + +Copyright 2014 gRPC authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/collector/grpcutil/timeout.go b/collector/grpcutil/timeout.go new file mode 100644 index 00000000..03e9bf2f --- /dev/null +++ b/collector/grpcutil/timeout.go @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package grpcutil + +import ( + "fmt" + "math" + "strconv" + "time" +) + +const maxTimeoutValue int64 = 100000000 - 1 + +// div does integer division and round-up the result. Note that this is +// equivalent to (d+r-1)/r but has less chance to overflow. +func div(d, r time.Duration) int64 { + if d%r > 0 { + return int64(d/r + 1) + } + return int64(d / r) +} + +type timeoutUnit uint8 + +const ( + hour timeoutUnit = 'H' + minute timeoutUnit = 'M' + second timeoutUnit = 'S' + millisecond timeoutUnit = 'm' + microsecond timeoutUnit = 'u' + nanosecond timeoutUnit = 'n' +) + +// EncodeTimeout encodes the duration to the format grpc-timeout +// header accepts. This is copied from the gRPC-Go implementation, +// with two branches of the original six branches removed, leaving the +// four you see for milliseconds, seconds, minutes, and hours. This +// code will not encode timeouts less than one millisecond. See: +// +// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests +func EncodeTimeout(t time.Duration) string { + if t < time.Millisecond { + return "0m" + } + if d := div(t, time.Millisecond); d <= maxTimeoutValue { + return fmt.Sprintf("%d%c", d, millisecond) + } + if d := div(t, time.Second); d <= maxTimeoutValue { + return fmt.Sprintf("%d%c", d, second) + } + if d := div(t, time.Minute); d <= maxTimeoutValue { + return fmt.Sprintf("%d%c", d, minute) + } + // Note that maxTimeoutValue * time.Hour > MaxInt64. + return fmt.Sprintf("%d%c", div(t, time.Hour), hour) +} + +func timeoutUnitToDuration(u timeoutUnit) (d time.Duration, ok bool) { + switch u { + case hour: + return time.Hour, true + case minute: + return time.Minute, true + case second: + return time.Second, true + case millisecond: + return time.Millisecond, true + case microsecond: + return time.Microsecond, true + case nanosecond: + return time.Nanosecond, true + default: + } + return +} + +// DecodeTimeout parses a string associated with the "grpc-timeout" +// header. Note this will accept all valid gRPC units including +// microseconds and nanoseconds, which EncodeTimeout avoids. This is +// specified in: +// +// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests +func DecodeTimeout(s string) (time.Duration, error) { + size := len(s) + if size < 2 { + return 0, fmt.Errorf("transport: timeout string is too short: %q", s) + } + if size > 9 { + // Spec allows for 8 digits plus the unit. + return 0, fmt.Errorf("transport: timeout string is too long: %q", s) + } + unit := timeoutUnit(s[size-1]) + d, ok := timeoutUnitToDuration(unit) + if !ok { + return 0, fmt.Errorf("transport: timeout unit is not recognized: %q", s) + } + t, err := strconv.ParseInt(s[:size-1], 10, 64) + if err != nil { + return 0, err + } + const maxHours = math.MaxInt64 / int64(time.Hour) + if d == time.Hour && t > maxHours { + // This timeout would overflow math.MaxInt64; clamp it. + return time.Duration(math.MaxInt64), nil + } + return d * time.Duration(t), nil +} diff --git a/collector/grpcutil/timeout_test.go b/collector/grpcutil/timeout_test.go new file mode 100644 index 00000000..9554b6dd --- /dev/null +++ b/collector/grpcutil/timeout_test.go @@ -0,0 +1,54 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package grpcutil + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestTimeoutEncode(t *testing.T) { + // Note the gRPC specification limits durations to 8 digits, + // so the use of 123456789 as a multiplier below forces the + // next-larger unit to be used. + require.Equal(t, "0m", EncodeTimeout(-time.Second)) + require.Equal(t, "1000m", EncodeTimeout(time.Second)) + require.Equal(t, "123m", EncodeTimeout(123*time.Millisecond)) + require.Equal(t, "123457S", EncodeTimeout(123456789*time.Millisecond)) + require.Equal(t, "2057614M", EncodeTimeout(123456789*time.Second)) + require.Equal(t, "2057614H", EncodeTimeout(123456789*time.Minute)) +} + +func mustDecode(t *testing.T, s string) time.Duration { + d, err := DecodeTimeout(s) + require.NoError(t, err, "must parse a timeout") + return d +} + +func TestTimeoutDecode(t *testing.T) { + // Note the gRPC specification limits durations to 8 digits, + // so the use of 123456789 as a multiplier below forces the + // next-larger unit to be used. + require.Equal(t, time.Duration(0), mustDecode(t, "0m")) + require.Equal(t, time.Second, mustDecode(t, "1000m")) + require.Equal(t, 123*time.Millisecond, mustDecode(t, "123m")) + require.Equal(t, 123*time.Second, mustDecode(t, "123S")) + require.Equal(t, 123*time.Minute, mustDecode(t, "123M")) + require.Equal(t, 123*time.Hour, mustDecode(t, "123H")) + + // these are not encoded by EncodeTimeout, but will be decoded + require.Equal(t, 123*time.Microsecond, mustDecode(t, "123u")) + require.Equal(t, 123*time.Nanosecond, mustDecode(t, "123n")) + + // error cases + testError := func(s string) { + _, err := DecodeTimeout(s) + require.Error(t, err) + } + testError("123x") + testError("x") + testError("") +}