diff --git a/logging/logging.go b/logging/logging.go index 6fe8d8c5338..61c06fe4df7 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -91,6 +91,9 @@ const ( // timeout is to allow clients to degrade gracefully if underlying logging // service is temporarily impaired for some reason. defaultWriteTimeout = 10 * time.Minute + + // Part of the error message when the payload contains invalid UTF-8 characters. + utfErrorString = "string field contains invalid UTF-8" ) var ( @@ -264,12 +267,28 @@ type loggerRetryer struct { defaultRetryer gax.Retryer } +func newLoggerRetryer() gax.Retryer { + // Copied from CallOptions.WriteLogEntries in apiv2/logging_client.go. + d := gax.OnCodes([]codes.Code{ + codes.DeadlineExceeded, + codes.Internal, + codes.Unavailable, + }, gax.Backoff{ + Initial: 100 * time.Millisecond, + Max: 60000 * time.Millisecond, + Multiplier: 1.30, + }) + + r := &loggerRetryer{defaultRetryer: d} + return r +} + func (r *loggerRetryer) Retry(err error) (pause time.Duration, shouldRetry bool) { s, ok := status.FromError(err) if !ok { return r.defaultRetryer.Retry(err) } - if s.Code() == codes.Internal && strings.Contains(s.Message(), "string field contains invalid UTF-8") { + if strings.Contains(s.Message(), utfErrorString) { return 0, false } return r.defaultRetryer.Retry(err) @@ -734,8 +753,7 @@ func (l *Logger) writeLogEntries(entries []*logpb.LogEntry) { ctx, cancel := context.WithTimeout(ctx, defaultWriteTimeout) defer cancel() - r := &loggerRetryer{} - _, err := l.client.client.WriteLogEntries(ctx, req, gax.WithRetry(func() gax.Retryer { return r })) + _, err := l.client.client.WriteLogEntries(ctx, req, gax.WithRetry(newLoggerRetryer)) if err != nil { l.client.error(err) } diff --git a/logging/logging_unexported_test.go b/logging/logging_unexported_test.go index bce4b2c5c78..967f6ff00d2 100644 --- a/logging/logging_unexported_test.go +++ b/logging/logging_unexported_test.go @@ -18,6 +18,7 @@ package logging import ( "encoding/json" + "fmt" "net/http" "net/url" "testing" @@ -31,8 +32,43 @@ import ( "google.golang.org/api/support/bundler" mrpb "google.golang.org/genproto/googleapis/api/monitoredres" logtypepb "google.golang.org/genproto/googleapis/logging/type" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) +func TestLoggerRetryer_Retry(t *testing.T) { + for _, tst := range []struct { + name string + err error + wantRetry bool + }{ + { + name: "non_status_no_retry", + err: fmt.Errorf("non-API error, do not retry"), + wantRetry: false, + }, + { + name: "invalid_utf_no_retry", + err: status.Error(codes.Internal, utfErrorString), + wantRetry: false, + }, + { + // Just testing one of the configured codes to ensure the default + // retryer is triggered. + name: "unavailable_retry", + err: status.Error(codes.Unavailable, "Unavailable"), + wantRetry: true, + }, + } { + t.Run(tst.name, func(t *testing.T) { + _, gotRetry := newLoggerRetryer().Retry(tst.err) + if gotRetry != tst.wantRetry { + t.Errorf("Retry(%v) = shouldRetry got %v want %v", tst.err, gotRetry, tst.wantRetry) + } + }) + } +} + func TestLoggerCreation(t *testing.T) { const logID = "testing" c := &Client{parent: "projects/PROJECT_ID"}