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 general Go error to *spanner.Error.
119func toSpannerError(err error) error {
120	return toSpannerErrorWithCommitInfo(err, false)
121}
122
123// toSpannerErrorWithCommitInfo converts general Go error to *spanner.Error
124// with additional information if the error occurred during a Commit request.
125//
126// If err is already a *spanner.Error, err is returned unmodified.
127func toSpannerErrorWithCommitInfo(err error, errorDuringCommit bool) error {
128	if err == nil {
129		return nil
130	}
131	var se *Error
132	if errorAs(err, &se) {
133		return se
134	}
135	switch {
136	case err == context.DeadlineExceeded || err == context.Canceled:
137		desc := err.Error()
138		wrapped := status.FromContextError(err).Err()
139		if errorDuringCommit {
140			desc = fmt.Sprintf("%s, %s", desc, transactionOutcomeUnknownMsg)
141			wrapped = &TransactionOutcomeUnknownError{err: wrapped}
142		}
143		return &Error{status.FromContextError(err).Code(), wrapped, desc, ""}
144	case status.Code(err) == codes.Unknown:
145		return &Error{codes.Unknown, err, err.Error(), ""}
146	default:
147		statusErr := status.Convert(err)
148		code, desc := statusErr.Code(), statusErr.Message()
149		wrapped := err
150		if errorDuringCommit && (code == codes.DeadlineExceeded || code == codes.Canceled) {
151			desc = fmt.Sprintf("%s, %s", desc, transactionOutcomeUnknownMsg)
152			wrapped = &TransactionOutcomeUnknownError{err: wrapped}
153		}
154		return &Error{code, wrapped, desc, ""}
155	}
156}
157
158// ErrCode extracts the canonical error code from a Go error.
159func ErrCode(err error) codes.Code {
160	s, ok := status.FromError(err)
161	if !ok {
162		return codes.Unknown
163	}
164	return s.Code()
165}
166
167// ErrDesc extracts the Cloud Spanner error description from a Go error.
168func ErrDesc(err error) string {
169	var se *Error
170	if !errorAs(err, &se) {
171		return err.Error()
172	}
173	return se.Desc
174}
175
176// extractResourceType extracts the resource type from any ResourceInfo detail
177// included in the error.
178func extractResourceType(err error) (string, bool) {
179	var s *status.Status
180	var se *Error
181	if errorAs(err, &se) {
182		// Unwrap statusError.
183		s = status.Convert(se.Unwrap())
184	} else {
185		s = status.Convert(err)
186	}
187	if s == nil {
188		return "", false
189	}
190	for _, detail := range s.Details() {
191		if resourceInfo, ok := detail.(*errdetails.ResourceInfo); ok {
192			return resourceInfo.ResourceType, true
193		}
194	}
195	return "", false
196}
197