Skip to content

Commit 22e48df

Browse files
authoredJun 21, 2024··
Introduce grpcutil (#228)
Same as open-telemetry/opentelemetry-collector-contrib#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 open-telemetry/opentelemetry-collector-contrib#33688 and open-telemetry/opentelemetry-collector-contrib#33579 merged, as discussed in #225. Part of #227.
1 parent dd6e224 commit 22e48df

File tree

3 files changed

+177
-0
lines changed

3 files changed

+177
-0
lines changed
 

‎NOTICE

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
collector/grpcutil contains code derived from gRPC-Go:
2+
3+
Copyright 2014 gRPC authors.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.

‎collector/grpcutil/timeout.go

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package grpcutil
5+
6+
import (
7+
"fmt"
8+
"math"
9+
"strconv"
10+
"time"
11+
)
12+
13+
const maxTimeoutValue int64 = 100000000 - 1
14+
15+
// div does integer division and round-up the result. Note that this is
16+
// equivalent to (d+r-1)/r but has less chance to overflow.
17+
func div(d, r time.Duration) int64 {
18+
if d%r > 0 {
19+
return int64(d/r + 1)
20+
}
21+
return int64(d / r)
22+
}
23+
24+
type timeoutUnit uint8
25+
26+
const (
27+
hour timeoutUnit = 'H'
28+
minute timeoutUnit = 'M'
29+
second timeoutUnit = 'S'
30+
millisecond timeoutUnit = 'm'
31+
microsecond timeoutUnit = 'u'
32+
nanosecond timeoutUnit = 'n'
33+
)
34+
35+
// EncodeTimeout encodes the duration to the format grpc-timeout
36+
// header accepts. This is copied from the gRPC-Go implementation,
37+
// with two branches of the original six branches removed, leaving the
38+
// four you see for milliseconds, seconds, minutes, and hours. This
39+
// code will not encode timeouts less than one millisecond. See:
40+
//
41+
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
42+
func EncodeTimeout(t time.Duration) string {
43+
if t < time.Millisecond {
44+
return "0m"
45+
}
46+
if d := div(t, time.Millisecond); d <= maxTimeoutValue {
47+
return fmt.Sprintf("%d%c", d, millisecond)
48+
}
49+
if d := div(t, time.Second); d <= maxTimeoutValue {
50+
return fmt.Sprintf("%d%c", d, second)
51+
}
52+
if d := div(t, time.Minute); d <= maxTimeoutValue {
53+
return fmt.Sprintf("%d%c", d, minute)
54+
}
55+
// Note that maxTimeoutValue * time.Hour > MaxInt64.
56+
return fmt.Sprintf("%d%c", div(t, time.Hour), hour)
57+
}
58+
59+
func timeoutUnitToDuration(u timeoutUnit) (d time.Duration, ok bool) {
60+
switch u {
61+
case hour:
62+
return time.Hour, true
63+
case minute:
64+
return time.Minute, true
65+
case second:
66+
return time.Second, true
67+
case millisecond:
68+
return time.Millisecond, true
69+
case microsecond:
70+
return time.Microsecond, true
71+
case nanosecond:
72+
return time.Nanosecond, true
73+
default:
74+
}
75+
return
76+
}
77+
78+
// DecodeTimeout parses a string associated with the "grpc-timeout"
79+
// header. Note this will accept all valid gRPC units including
80+
// microseconds and nanoseconds, which EncodeTimeout avoids. This is
81+
// specified in:
82+
//
83+
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
84+
func DecodeTimeout(s string) (time.Duration, error) {
85+
size := len(s)
86+
if size < 2 {
87+
return 0, fmt.Errorf("transport: timeout string is too short: %q", s)
88+
}
89+
if size > 9 {
90+
// Spec allows for 8 digits plus the unit.
91+
return 0, fmt.Errorf("transport: timeout string is too long: %q", s)
92+
}
93+
unit := timeoutUnit(s[size-1])
94+
d, ok := timeoutUnitToDuration(unit)
95+
if !ok {
96+
return 0, fmt.Errorf("transport: timeout unit is not recognized: %q", s)
97+
}
98+
t, err := strconv.ParseInt(s[:size-1], 10, 64)
99+
if err != nil {
100+
return 0, err
101+
}
102+
const maxHours = math.MaxInt64 / int64(time.Hour)
103+
if d == time.Hour && t > maxHours {
104+
// This timeout would overflow math.MaxInt64; clamp it.
105+
return time.Duration(math.MaxInt64), nil
106+
}
107+
return d * time.Duration(t), nil
108+
}

‎collector/grpcutil/timeout_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package grpcutil
5+
6+
import (
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestTimeoutEncode(t *testing.T) {
14+
// Note the gRPC specification limits durations to 8 digits,
15+
// so the use of 123456789 as a multiplier below forces the
16+
// next-larger unit to be used.
17+
require.Equal(t, "0m", EncodeTimeout(-time.Second))
18+
require.Equal(t, "1000m", EncodeTimeout(time.Second))
19+
require.Equal(t, "123m", EncodeTimeout(123*time.Millisecond))
20+
require.Equal(t, "123457S", EncodeTimeout(123456789*time.Millisecond))
21+
require.Equal(t, "2057614M", EncodeTimeout(123456789*time.Second))
22+
require.Equal(t, "2057614H", EncodeTimeout(123456789*time.Minute))
23+
}
24+
25+
func mustDecode(t *testing.T, s string) time.Duration {
26+
d, err := DecodeTimeout(s)
27+
require.NoError(t, err, "must parse a timeout")
28+
return d
29+
}
30+
31+
func TestTimeoutDecode(t *testing.T) {
32+
// Note the gRPC specification limits durations to 8 digits,
33+
// so the use of 123456789 as a multiplier below forces the
34+
// next-larger unit to be used.
35+
require.Equal(t, time.Duration(0), mustDecode(t, "0m"))
36+
require.Equal(t, time.Second, mustDecode(t, "1000m"))
37+
require.Equal(t, 123*time.Millisecond, mustDecode(t, "123m"))
38+
require.Equal(t, 123*time.Second, mustDecode(t, "123S"))
39+
require.Equal(t, 123*time.Minute, mustDecode(t, "123M"))
40+
require.Equal(t, 123*time.Hour, mustDecode(t, "123H"))
41+
42+
// these are not encoded by EncodeTimeout, but will be decoded
43+
require.Equal(t, 123*time.Microsecond, mustDecode(t, "123u"))
44+
require.Equal(t, 123*time.Nanosecond, mustDecode(t, "123n"))
45+
46+
// error cases
47+
testError := func(s string) {
48+
_, err := DecodeTimeout(s)
49+
require.Error(t, err)
50+
}
51+
testError("123x")
52+
testError("x")
53+
testError("")
54+
}

0 commit comments

Comments
 (0)
Please sign in to comment.