1/* 2Copyright 2017 Google LLC 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package spanner 18 19import ( 20 "context" 21 "fmt" 22 23 "google.golang.org/genproto/googleapis/rpc/errdetails" 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26) 27 28// Error is the structured error returned by Cloud Spanner client. 29type Error struct { 30 // Code is the canonical error code for describing the nature of a 31 // particular error. 32 // 33 // Deprecated: The error code should be extracted from the wrapped error by 34 // calling ErrCode(err error). This field will be removed in a future 35 // release. 36 Code codes.Code 37 // err is the wrapped error that caused this Spanner error. The wrapped 38 // error can be read with the Unwrap method. 39 err error 40 // Desc explains more details of the error. 41 Desc string 42 // additionalInformation optionally contains any additional information 43 // about the error. 44 additionalInformation string 45} 46 47// TransactionOutcomeUnknownError is wrapped in a Spanner error when the error 48// occurred during a transaction, and the outcome of the transaction is 49// unknown as a result of the error. This could be the case if a timeout or 50// canceled error occurs after a Commit request has been sent, but before the 51// client has received a response from the server. 52type TransactionOutcomeUnknownError struct { 53 // err is the wrapped error that caused this TransactionOutcomeUnknownError 54 // error. The wrapped error can be read with the Unwrap method. 55 err error 56} 57 58const transactionOutcomeUnknownMsg = "transaction outcome unknown" 59 60// Error implements error.Error. 61func (*TransactionOutcomeUnknownError) Error() string { return transactionOutcomeUnknownMsg } 62 63// Unwrap returns the wrapped error (if any). 64func (e *TransactionOutcomeUnknownError) Unwrap() error { return e.err } 65 66// Error implements error.Error. 67func (e *Error) Error() string { 68 if e == nil { 69 return fmt.Sprintf("spanner: OK") 70 } 71 code := ErrCode(e) 72 if e.additionalInformation == "" { 73 return fmt.Sprintf("spanner: code = %q, desc = %q", code, e.Desc) 74 } 75 return fmt.Sprintf("spanner: code = %q, desc = %q, additional information = %s", code, e.Desc, e.additionalInformation) 76} 77 78// Unwrap returns the wrapped error (if any). 79func (e *Error) Unwrap() error { 80 return e.err 81} 82 83// GRPCStatus returns the corresponding gRPC Status of this Spanner error. 84// This allows the error to be converted to a gRPC status using 85// `status.Convert(error)`. 86func (e *Error) GRPCStatus() *status.Status { 87 err := unwrap(e) 88 for { 89 // If the base error is nil, return status created from e.Code and e.Desc. 90 if err == nil { 91 return status.New(e.Code, e.Desc) 92 } 93 code := status.Code(err) 94 if code != codes.Unknown { 95 return status.New(code, e.Desc) 96 } 97 err = unwrap(err) 98 } 99} 100 101// decorate decorates an existing spanner.Error with more information. 102func (e *Error) decorate(info string) { 103 e.Desc = fmt.Sprintf("%v, %v", info, e.Desc) 104} 105 106// spannerErrorf generates a *spanner.Error with the given description and a 107// status error with the given error code as its wrapped error. 108func spannerErrorf(code codes.Code, format string, args ...interface{}) error { 109 msg := fmt.Sprintf(format, args...) 110 wrapped := status.Error(code, msg) 111 return &Error{ 112 Code: code, 113 err: wrapped, 114 Desc: msg, 115 } 116} 117 118// ToSpannerError converts a general Go error to *spanner.Error. If the given 119// error is already a *spanner.Error, the original error will be returned. 120// 121// Spanner Errors are normally created by the Spanner client library from the 122// returned status of a RPC. This method can also be used to create Spanner 123// errors for use in tests. The recommended way to create test errors is 124// calling this method with a status error, e.g. 125// ToSpannerError(status.New(codes.NotFound, "Table not found").Err()) 126func ToSpannerError(err error) error { 127 return toSpannerErrorWithCommitInfo(err, false) 128} 129 130// toSpannerErrorWithCommitInfo converts general Go error to *spanner.Error 131// with additional information if the error occurred during a Commit request. 132// 133// If err is already a *spanner.Error, err is returned unmodified. 134func toSpannerErrorWithCommitInfo(err error, errorDuringCommit bool) error { 135 if err == nil { 136 return nil 137 } 138 var se *Error 139 if errorAs(err, &se) { 140 return se 141 } 142 switch { 143 case err == context.DeadlineExceeded || err == context.Canceled: 144 desc := err.Error() 145 wrapped := status.FromContextError(err).Err() 146 if errorDuringCommit { 147 desc = fmt.Sprintf("%s, %s", desc, transactionOutcomeUnknownMsg) 148 wrapped = &TransactionOutcomeUnknownError{err: wrapped} 149 } 150 return &Error{status.FromContextError(err).Code(), wrapped, desc, ""} 151 case status.Code(err) == codes.Unknown: 152 return &Error{codes.Unknown, err, err.Error(), ""} 153 default: 154 statusErr := status.Convert(err) 155 code, desc := statusErr.Code(), statusErr.Message() 156 wrapped := err 157 if errorDuringCommit && (code == codes.DeadlineExceeded || code == codes.Canceled) { 158 desc = fmt.Sprintf("%s, %s", desc, transactionOutcomeUnknownMsg) 159 wrapped = &TransactionOutcomeUnknownError{err: wrapped} 160 } 161 return &Error{code, wrapped, desc, ""} 162 } 163} 164 165// ErrCode extracts the canonical error code from a Go error. 166func ErrCode(err error) codes.Code { 167 s, ok := status.FromError(err) 168 if !ok { 169 return codes.Unknown 170 } 171 return s.Code() 172} 173 174// ErrDesc extracts the Cloud Spanner error description from a Go error. 175func ErrDesc(err error) string { 176 var se *Error 177 if !errorAs(err, &se) { 178 return err.Error() 179 } 180 return se.Desc 181} 182 183// extractResourceType extracts the resource type from any ResourceInfo detail 184// included in the error. 185func extractResourceType(err error) (string, bool) { 186 var s *status.Status 187 var se *Error 188 if errorAs(err, &se) { 189 // Unwrap statusError. 190 s = status.Convert(se.Unwrap()) 191 } else { 192 s = status.Convert(err) 193 } 194 if s == nil { 195 return "", false 196 } 197 for _, detail := range s.Details() { 198 if resourceInfo, ok := detail.(*errdetails.ResourceInfo); ok { 199 return resourceInfo.ResourceType, true 200 } 201 } 202 return "", false 203} 204