Skip to content

Commit fd1db20

Browse files
authored
feat(storage): add Writer.ChunkTransferTimeout (#11111)
Expose the ChunkTransferTimeout MediaOption through the manual client layer. This allows users to set a longer timeout for chunk retries in case of stalls in resumable uploads if desired. Added emulator based unit tests to cover all the scenarios.
1 parent f82fffd commit fd1db20

File tree

6 files changed

+209
-56
lines changed

6 files changed

+209
-56
lines changed

storage/client.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ type openWriterParams struct {
237237
chunkSize int
238238
// chunkRetryDeadline - see `Writer.ChunkRetryDeadline`.
239239
// Optional.
240-
chunkRetryDeadline time.Duration
240+
chunkRetryDeadline time.Duration
241+
chunkTransferTimeout time.Duration
241242

242243
// Object/request properties
243244

storage/client_test.go

+139
Original file line numberDiff line numberDiff line change
@@ -1506,6 +1506,145 @@ func TestRetryReadStallEmulated(t *testing.T) {
15061506
}
15071507
}
15081508

1509+
func TestWriterChunkTransferTimeoutEmulated(t *testing.T) {
1510+
transportClientTest(skipGRPC("service is not implemented"), t, func(t *testing.T, ctx context.Context, project, bucket string, client storageClient) {
1511+
_, err := client.CreateBucket(ctx, project, bucket, &BucketAttrs{}, nil)
1512+
if err != nil {
1513+
t.Fatalf("creating bucket: %v", err)
1514+
}
1515+
1516+
chunkSize := 2 * 1024 * 1024 // 2 MiB
1517+
fileSize := 5 * 1024 * 1024 // 5 MiB
1518+
tests := []struct {
1519+
name string
1520+
instructions map[string][]string
1521+
chunkTransferTimeout time.Duration
1522+
expectedSuccess bool
1523+
}{
1524+
{
1525+
name: "stall-on-first-chunk-with-chunk-transfer-timeout-zero",
1526+
instructions: map[string][]string{
1527+
"storage.objects.insert": {"stall-for-10s-after-1024K"},
1528+
},
1529+
chunkTransferTimeout: 0,
1530+
expectedSuccess: false,
1531+
},
1532+
{
1533+
name: "stall-on-first-chunk-with-chunk-transfer-timeout-nonzero",
1534+
instructions: map[string][]string{
1535+
"storage.objects.insert": {"stall-for-10s-after-1024K"},
1536+
},
1537+
chunkTransferTimeout: 100 * time.Millisecond,
1538+
expectedSuccess: true,
1539+
},
1540+
{
1541+
name: "stall-on-second-chunk-with-chunk-transfer-timeout-zero",
1542+
instructions: map[string][]string{
1543+
"storage.objects.insert": {"stall-for-10s-after-3072K"},
1544+
},
1545+
chunkTransferTimeout: 0,
1546+
expectedSuccess: false,
1547+
},
1548+
{
1549+
name: "stall-on-second-chunk-with-chunk-transfer-timeout-nonzero",
1550+
instructions: map[string][]string{
1551+
"storage.objects.insert": {"stall-for-10s-after-3072K"},
1552+
},
1553+
chunkTransferTimeout: 100 * time.Millisecond,
1554+
expectedSuccess: true,
1555+
},
1556+
{
1557+
name: "stall-on-first-chunk-twice-with-chunk-transfer-timeout-zero",
1558+
instructions: map[string][]string{
1559+
"storage.objects.insert": {"stall-for-10s-after-1024K", "stall-for-10s-after-1024K"},
1560+
},
1561+
chunkTransferTimeout: 0,
1562+
expectedSuccess: false,
1563+
},
1564+
{
1565+
name: "stall-on-first-chunk-twice-with-chunk-transfer-timeout-nonzero",
1566+
instructions: map[string][]string{
1567+
"storage.objects.insert": {"stall-for-10s-after-1024K", "stall-for-10s-after-1024K"},
1568+
},
1569+
chunkTransferTimeout: 100 * time.Millisecond,
1570+
expectedSuccess: true,
1571+
},
1572+
}
1573+
1574+
for _, tc := range tests {
1575+
t.Run(tc.name, func(t *testing.T) {
1576+
testID := createRetryTest(t, client, tc.instructions)
1577+
var cancel context.CancelFunc
1578+
rCtx := callctx.SetHeaders(ctx, "x-retry-test-id", testID)
1579+
rCtx, cancel = context.WithTimeout(rCtx, 1*time.Second)
1580+
defer cancel()
1581+
1582+
prefix := time.Now().Nanosecond()
1583+
want := &ObjectAttrs{
1584+
Bucket: bucket,
1585+
Name: fmt.Sprintf("%d-object-%d", prefix, time.Now().Nanosecond()),
1586+
Generation: defaultGen,
1587+
}
1588+
1589+
var gotAttrs *ObjectAttrs
1590+
params := &openWriterParams{
1591+
attrs: want,
1592+
bucket: bucket,
1593+
chunkSize: chunkSize,
1594+
chunkTransferTimeout: tc.chunkTransferTimeout,
1595+
ctx: rCtx,
1596+
donec: make(chan struct{}),
1597+
setError: func(_ error) {}, // no-op
1598+
progress: func(_ int64) {}, // no-op
1599+
setObj: func(o *ObjectAttrs) { gotAttrs = o },
1600+
}
1601+
1602+
pw, err := client.OpenWriter(params)
1603+
if err != nil {
1604+
t.Fatalf("failed to open writer: %v", err)
1605+
}
1606+
buffer := bytes.Repeat([]byte("A"), fileSize)
1607+
_, err = pw.Write(buffer)
1608+
if tc.expectedSuccess {
1609+
if err != nil {
1610+
t.Fatalf("failed to populate test data: %v", err)
1611+
}
1612+
if err := pw.Close(); err != nil {
1613+
t.Fatalf("closing object: %v", err)
1614+
}
1615+
select {
1616+
case <-params.donec:
1617+
}
1618+
if gotAttrs == nil {
1619+
t.Fatalf("Writer finished, but resulting object wasn't set")
1620+
}
1621+
if diff := cmp.Diff(gotAttrs.Name, want.Name); diff != "" {
1622+
t.Fatalf("Resulting object name: got(-),want(+):\n%s", diff)
1623+
}
1624+
1625+
r, err := veneerClient.Bucket(bucket).Object(want.Name).NewReader(ctx)
1626+
if err != nil {
1627+
t.Fatalf("opening reading: %v", err)
1628+
}
1629+
wantLen := len(buffer)
1630+
got := make([]byte, wantLen)
1631+
n, err := r.Read(got)
1632+
if n != wantLen {
1633+
t.Fatalf("expected to read %d bytes, but got %d", wantLen, n)
1634+
}
1635+
if diff := cmp.Diff(got, buffer); diff != "" {
1636+
t.Fatalf("checking written content: got(-),want(+):\n%s", diff)
1637+
}
1638+
} else {
1639+
if !errors.Is(err, context.DeadlineExceeded) {
1640+
t.Fatalf("expected context deadline exceeded found %v", err)
1641+
}
1642+
}
1643+
})
1644+
}
1645+
})
1646+
}
1647+
15091648
// createRetryTest creates a bucket in the emulator and sets up a test using the
15101649
// Retry Test API for the given instructions. This is intended for emulator tests
15111650
// of retry behavior that are not covered by conformance tests.

storage/go.mod

+17-17
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,32 @@ retract [v1.25.0, v1.27.0] // due to https://github.com/googleapis/google-cloud-
77
require (
88
cloud.google.com/go v0.116.0
99
cloud.google.com/go/compute/metadata v0.5.2
10-
cloud.google.com/go/iam v1.2.1
11-
cloud.google.com/go/longrunning v0.6.1
10+
cloud.google.com/go/iam v1.2.2
11+
cloud.google.com/go/longrunning v0.6.2
1212
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1
1313
github.com/google/go-cmp v0.6.0
1414
github.com/google/uuid v1.6.0
15-
github.com/googleapis/gax-go/v2 v2.13.0
15+
github.com/googleapis/gax-go/v2 v2.14.0
1616
go.opentelemetry.io/contrib/detectors/gcp v1.29.0
1717
go.opentelemetry.io/otel v1.29.0
1818
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0
1919
go.opentelemetry.io/otel/sdk v1.29.0
2020
go.opentelemetry.io/otel/sdk/metric v1.29.0
21-
golang.org/x/oauth2 v0.23.0
22-
golang.org/x/sync v0.8.0
23-
google.golang.org/api v0.203.0
24-
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53
25-
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9
21+
golang.org/x/oauth2 v0.24.0
22+
golang.org/x/sync v0.9.0
23+
google.golang.org/api v0.210.0
24+
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697
25+
google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f
2626
google.golang.org/grpc v1.67.1
2727
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a
28-
google.golang.org/protobuf v1.35.1
28+
google.golang.org/protobuf v1.35.2
2929
)
3030

3131
require (
3232
cel.dev/expr v0.16.1 // indirect
33-
cloud.google.com/go/auth v0.10.2 // indirect
34-
cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect
35-
cloud.google.com/go/monitoring v1.21.1 // indirect
33+
cloud.google.com/go/auth v0.11.0 // indirect
34+
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
35+
cloud.google.com/go/monitoring v1.21.2 // indirect
3636
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
3737
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
3838
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
@@ -53,10 +53,10 @@ require (
5353
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
5454
go.opentelemetry.io/otel/metric v1.29.0 // indirect
5555
go.opentelemetry.io/otel/trace v1.29.0 // indirect
56-
golang.org/x/crypto v0.28.0 // indirect
57-
golang.org/x/net v0.30.0 // indirect
56+
golang.org/x/crypto v0.29.0 // indirect
57+
golang.org/x/net v0.31.0 // indirect
5858
golang.org/x/sys v0.27.0 // indirect
59-
golang.org/x/text v0.19.0 // indirect
60-
golang.org/x/time v0.7.0 // indirect
61-
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
59+
golang.org/x/text v0.20.0 // indirect
60+
golang.org/x/time v0.8.0 // indirect
61+
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
6262
)

0 commit comments

Comments
 (0)