blob: 07e558704c9026f1bc18f2a8810c1d5d85189c4f [file] [edit]
// Copyright 2016 Google LLC
//
// 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.
// Package lro supports Long Running Operations for the Google Cloud Libraries.
//
// This package is still experimental and subject to change.
package longrunning
import (
"context"
"errors"
"testing"
"time"
pb "cloud.google.com/go/longrunning/autogen/longrunningpb"
gax "github.com/googleapis/gax-go/v2"
"github.com/googleapis/gax-go/v2/apierror"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace"
"google.golang.org/genproto/googleapis/rpc/errdetails"
rpcstatus "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
)
type getterService struct {
operationsClient
// clock represents the fake current time of the service.
// It is the running sum of the of the duration we have slept.
clock time.Duration
// getTimes records the times at which GetOperation is called.
getTimes []time.Duration
// results are the fake results that GetOperation should return.
results []*pb.Operation
}
func (s *getterService) GetOperation(context.Context, *pb.GetOperationRequest, ...gax.CallOption) (*pb.Operation, error) {
i := len(s.getTimes)
s.getTimes = append(s.getTimes, s.clock)
if i >= len(s.results) {
return nil, errors.New("unexpected call")
}
return s.results[i], nil
}
func (s *getterService) sleeper() sleeper {
return func(_ context.Context, d time.Duration) error {
s.clock += d
return nil
}
}
func TestWait(t *testing.T) {
responseDur := durationpb.New(42 * time.Second)
responseAny, err := anypb.New(responseDur)
if err != nil {
t.Fatal(err)
}
s := &getterService{
results: []*pb.Operation{
{Name: "foo"},
{Name: "foo"},
{Name: "foo"},
{Name: "foo"},
{Name: "foo"},
{
Name: "foo",
Done: true,
Result: &pb.Operation_Response{
Response: responseAny,
},
},
},
}
op := &Operation{
c: s,
proto: &pb.Operation{Name: "foo"},
}
if op.Done() {
t.Fatal("operation should not have completed yet")
}
var resp durationpb.Duration
bo := gax.Backoff{
Initial: 1 * time.Second,
Max: 3 * time.Second,
}
if err := op.wait(context.Background(), &resp, &bo, s.sleeper()); err != nil {
t.Fatal(err)
}
if !proto.Equal(&resp, responseDur) {
t.Errorf("response, got %v, want %v", &resp, responseDur)
}
if !op.Done() {
t.Errorf("operation should have completed")
}
maxWait := []time.Duration{
1 * time.Second,
2 * time.Second,
3 * time.Second,
3 * time.Second,
3 * time.Second,
}
for i := 0; i < len(s.getTimes)-1; i++ {
w := s.getTimes[i+1] - s.getTimes[i]
if mw := maxWait[i]; w > mw {
t.Errorf("backoff, waited %s, max %s", w, mw)
}
}
}
func TestPollRequestError(t *testing.T) {
const opName = "foo"
// All calls error.
s := &getterService{}
op := &Operation{
c: s,
proto: &pb.Operation{Name: opName},
}
if err := op.Poll(context.Background(), nil); err == nil {
t.Fatalf("Poll should error")
}
if n := op.Name(); n != opName {
t.Errorf("operation name, got %q, want %q", n, opName)
}
if op.Done() {
t.Errorf("operation should not have completed; we failed to fetch state")
}
}
func TestPollErrorResult(t *testing.T) {
const (
errCode = codes.NotFound
errMsg = "my error"
)
details := &errdetails.ErrorInfo{Reason: "things happen"}
a, err := anypb.New(details)
if err != nil {
t.Fatalf("anypb.New() = %v", err)
}
op := &Operation{
proto: &pb.Operation{
Name: "foo",
Done: true,
Result: &pb.Operation_Error{
Error: &rpcstatus.Status{
Code: int32(errCode),
Message: errMsg,
Details: []*anypb.Any{a},
},
},
},
}
err = op.Poll(context.Background(), nil)
if got := status.Code(err); got != errCode {
t.Errorf("error code, want %s, got %s", errCode, got)
}
if got := grpc.ErrorDesc(err); got != errMsg {
t.Errorf("error code, want %s, got %s", errMsg, got)
}
if !op.Done() {
t.Errorf("operation should have completed")
}
var ae *apierror.APIError
errors.As(err, &ae)
if got := ae.Details().ErrorInfo.Reason; got != details.Reason {
t.Errorf("got %q, want %q", got, details.Reason)
}
}
type errService struct {
operationsClient
errCancel, errDelete error
}
func (s *errService) CancelOperation(context.Context, *pb.CancelOperationRequest, ...gax.CallOption) error {
return s.errCancel
}
func (s *errService) DeleteOperation(context.Context, *pb.DeleteOperationRequest, ...gax.CallOption) error {
return s.errDelete
}
func TestCancelReturnsError(t *testing.T) {
s := &errService{
errCancel: errors.New("cancel error"),
}
op := &Operation{
c: s,
proto: &pb.Operation{Name: "foo"},
}
if got, want := op.Cancel(context.Background()), s.errCancel; got != want {
t.Errorf("cancel, got error %s, want %s", got, want)
}
}
func TestDeleteReturnsError(t *testing.T) {
s := &errService{
errDelete: errors.New("delete error"),
}
op := &Operation{
c: s,
proto: &pb.Operation{Name: "foo"},
}
if got, want := op.Delete(context.Background()), s.errDelete; got != want {
t.Errorf("cancel, got error %s, want %s", got, want)
}
}
func TestInternalNewOperationWithMetadata(t *testing.T) {
opName := "test-operation"
op := InternalNewOperationWithMetadata(nil, &pb.Operation{Name: "foo"}, opName)
if op.opName != opName {
t.Errorf("expected opName to be %q, got %q", opName, op.opName)
}
sc := trace.SpanContext{}
op.SetParentSpanContext(sc)
if !op.initSpanContext.Equal(sc) {
t.Error("expected initSpanContext to match the set SpanContext")
}
}
func TestWaitTraced(t *testing.T) {
t.Setenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING", "true")
if !gax.IsFeatureEnabled("TRACING") {
t.Skip("TRACING feature flag is not enabled")
}
sr := tracetest.NewSpanRecorder()
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
oldTP := otel.GetTracerProvider()
defer otel.SetTracerProvider(oldTP)
otel.SetTracerProvider(tp)
responseDur := durationpb.New(42 * time.Second)
responseAny, err := anypb.New(responseDur)
if err != nil {
t.Fatal(err)
}
s := &getterService{
results: []*pb.Operation{
{Name: "foo"},
{Name: "foo"},
{
Name: "foo",
Done: true,
Result: &pb.Operation_Response{
Response: responseAny,
},
},
},
}
op := &Operation{
c: s,
proto: &pb.Operation{Name: "foo"},
opName: "*speech.Client.BatchRecognizeOperation",
}
ctx := context.Background()
tracer := otel.GetTracerProvider().Tracer("test-tracer")
parentCtx, parentSpan := tracer.Start(ctx, "creation-span")
op.SetParentSpanContext(parentSpan.SpanContext())
var resp durationpb.Duration
err = op.waitWithInterval(parentCtx, &resp, 3*time.Millisecond, s.sleeper())
parentSpan.End()
if err != nil {
t.Fatal(err)
}
spans := sr.Ended()
var waitSpan sdktrace.ReadOnlySpan
var pollSpans []sdktrace.ReadOnlySpan
var sleepSpans []sdktrace.ReadOnlySpan
for _, span := range spans {
switch span.Name() {
case "*speech.Client.BatchRecognizeOperation.Wait":
waitSpan = span
case "*longrunning.OperationsClient.GetOperation":
pollSpans = append(pollSpans, span)
case "LRO Sleep":
sleepSpans = append(sleepSpans, span)
}
}
if waitSpan == nil {
t.Fatal("expected a T2 LRO Wait span, got nil")
}
links := waitSpan.Links()
if len(links) != 1 {
t.Fatalf("expected 1 span link, got %d", len(links))
}
if links[0].SpanContext.SpanID() != parentSpan.SpanContext().SpanID() {
t.Errorf("expected span link pointing to parent span, got different span ID")
}
waitAttrs := waitSpan.Attributes()
hasResID := false
for _, attr := range waitAttrs {
if attr.Key == "gcp.resource.destination.id" && attr.Value.AsString() == "foo" {
hasResID = true
}
}
if !hasResID {
t.Error("expected wait span to have gcp.resource.destination.id attribute set to 'foo'")
}
// Verify T3 Poll Spans
if len(pollSpans) != 3 {
t.Fatalf("expected 3 poll spans, got %d", len(pollSpans))
}
verifyPollAndSleepSpans(t, pollSpans, sleepSpans, "foo")
}
func verifyPollAndSleepSpans(t *testing.T, pollSpans, sleepSpans []sdktrace.ReadOnlySpan, expectedResID string) {
t.Helper()
for i, pollSpan := range pollSpans {
attrs := pollSpan.Attributes()
var resID string
var hasCount bool
var count int
var done bool
for _, attr := range attrs {
if attr.Key == "gcp.resource.destination.id" {
resID = attr.Value.AsString()
}
if attr.Key == "gcp.longrunning.poll_attempt_count" {
hasCount = true
count = int(attr.Value.AsInt64())
}
if attr.Key == "gcp.longrunning.done" {
done = attr.Value.AsBool()
}
}
if resID != expectedResID {
t.Errorf("poll span %d expected gcp.resource.destination.id to be '%s', got '%s'", i, expectedResID, resID)
}
if !hasCount || count != i+1 {
t.Errorf("poll span %d expected count to be %d, got %d", i, i+1, count)
}
if i != len(pollSpans)-1 && done {
t.Errorf("poll span %d expected gcp.longrunning.done to be set", i)
}
if i == len(pollSpans)-1 {
if !done {
t.Error("expected done to be true on terminal poll span")
}
hasStatus := false
var statusCode int
for _, attr := range attrs {
if attr.Key == "gcp.longrunning.status_code" {
statusCode = int(attr.Value.AsInt64())
hasStatus = true
}
}
if !hasStatus || statusCode != 0 {
t.Errorf("expected status code 0 on terminal poll, got %d (hasStatus: %t)", statusCode, hasStatus)
}
}
}
// Verify T5 Sleep Spans
expectedSleeps := len(pollSpans) - 1
if len(sleepSpans) != expectedSleeps {
t.Errorf("expected %d sleep spans, got %d", expectedSleeps, len(sleepSpans))
}
}
func TestWaitTracedResumed(t *testing.T) {
t.Setenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING", "true")
if !gax.IsFeatureEnabled("TRACING") {
t.Skip("TRACING feature flag is not enabled")
}
sr := tracetest.NewSpanRecorder()
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
oldTP := otel.GetTracerProvider()
defer otel.SetTracerProvider(oldTP)
otel.SetTracerProvider(tp)
responseDur := durationpb.New(42 * time.Second)
responseAny, err := anypb.New(responseDur)
if err != nil {
t.Fatal(err)
}
s := &getterService{
results: []*pb.Operation{
{Name: "foo"},
{
Name: "foo",
Done: true,
Result: &pb.Operation_Response{
Response: responseAny,
},
},
},
}
op := &Operation{
c: s,
proto: &pb.Operation{Name: "foo"},
opName: "*speech.BatchRecognizeOperation",
}
ctx := context.Background()
tracer := otel.GetTracerProvider().Tracer("test-tracer")
parentCtx, parentSpan := tracer.Start(ctx, "resumed-polling")
var resp durationpb.Duration
err = op.waitWithInterval(parentCtx, &resp, 3*time.Millisecond, s.sleeper())
parentSpan.End()
if err != nil {
t.Fatal(err)
}
spans := sr.Ended()
var waitSpan sdktrace.ReadOnlySpan
var pollSpans []sdktrace.ReadOnlySpan
var sleepSpans []sdktrace.ReadOnlySpan
for _, span := range spans {
switch span.Name() {
case "*speech.BatchRecognizeOperation.Wait":
waitSpan = span
case "*longrunning.OperationsClient.GetOperation":
pollSpans = append(pollSpans, span)
case "LRO Sleep":
sleepSpans = append(sleepSpans, span)
}
}
if waitSpan == nil {
t.Fatal("expected a T2 LRO Wait span, got nil")
}
links := waitSpan.Links()
if len(links) != 0 {
t.Fatalf("expected 0 span links for resumed LRO tracing, got %d", len(links))
}
if len(pollSpans) != 2 {
t.Fatalf("expected 2 poll spans, got %d", len(pollSpans))
}
verifyPollAndSleepSpans(t, pollSpans, sleepSpans, "foo")
}
func TestWaitTracedFallback(t *testing.T) {
t.Setenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING", "true")
if !gax.IsFeatureEnabled("TRACING") {
t.Skip("TRACING feature flag is not enabled")
}
sr := tracetest.NewSpanRecorder()
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
oldTP := otel.GetTracerProvider()
defer otel.SetTracerProvider(oldTP)
otel.SetTracerProvider(tp)
responseDur := durationpb.New(42 * time.Second)
responseAny, err := anypb.New(responseDur)
if err != nil {
t.Fatal(err)
}
s := &getterService{
results: []*pb.Operation{
{Name: "foo"},
{
Name: "foo",
Done: true,
Result: &pb.Operation_Response{
Response: responseAny,
},
},
},
}
op := &Operation{
c: s,
proto: &pb.Operation{Name: "foo"},
}
ctx := context.Background()
tracer := otel.GetTracerProvider().Tracer("test-tracer")
parentCtx, parentSpan := tracer.Start(ctx, "fallback-polling")
var resp durationpb.Duration
err = op.waitWithInterval(parentCtx, &resp, 3*time.Millisecond, s.sleeper())
parentSpan.End()
if err != nil {
t.Fatal(err)
}
spans := sr.Ended()
var waitSpan sdktrace.ReadOnlySpan
var pollSpans []sdktrace.ReadOnlySpan
var sleepSpans []sdktrace.ReadOnlySpan
for _, span := range spans {
switch span.Name() {
case "*longrunning.Operation.Wait":
waitSpan = span
case "*longrunning.OperationsClient.GetOperation":
pollSpans = append(pollSpans, span)
case "LRO Sleep":
sleepSpans = append(sleepSpans, span)
}
}
if waitSpan == nil {
t.Fatal("expected a T2 LRO Wait span, got nil")
}
links := waitSpan.Links()
if len(links) != 0 {
t.Fatalf("expected 0 span links for fallback LRO tracing, got %d", len(links))
}
if len(pollSpans) != 2 {
t.Fatalf("expected 2 poll spans, got %d", len(pollSpans))
}
verifyPollAndSleepSpans(t, pollSpans, sleepSpans, "foo")
}