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