You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

163 lines
4.9 KiB

  1. // Copyright 2015 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package bigquery
  15. import (
  16. "context"
  17. "fmt"
  18. "io"
  19. "net/http"
  20. "time"
  21. "cloud.google.com/go/internal"
  22. "cloud.google.com/go/internal/version"
  23. gax "github.com/googleapis/gax-go/v2"
  24. bq "google.golang.org/api/bigquery/v2"
  25. "google.golang.org/api/googleapi"
  26. "google.golang.org/api/option"
  27. htransport "google.golang.org/api/transport/http"
  28. )
  29. const (
  30. prodAddr = "https://www.googleapis.com/bigquery/v2/"
  31. // Scope is the Oauth2 scope for the service.
  32. Scope = "https://www.googleapis.com/auth/bigquery"
  33. userAgent = "gcloud-golang-bigquery/20160429"
  34. )
  35. var xGoogHeader = fmt.Sprintf("gl-go/%s gccl/%s", version.Go(), version.Repo)
  36. func setClientHeader(headers http.Header) {
  37. headers.Set("x-goog-api-client", xGoogHeader)
  38. }
  39. // Client may be used to perform BigQuery operations.
  40. type Client struct {
  41. // Location, if set, will be used as the default location for all subsequent
  42. // dataset creation and job operations. A location specified directly in one of
  43. // those operations will override this value.
  44. Location string
  45. projectID string
  46. bqs *bq.Service
  47. }
  48. // NewClient constructs a new Client which can perform BigQuery operations.
  49. // Operations performed via the client are billed to the specified GCP project.
  50. func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error) {
  51. o := []option.ClientOption{
  52. option.WithEndpoint(prodAddr),
  53. option.WithScopes(Scope),
  54. option.WithUserAgent(userAgent),
  55. }
  56. o = append(o, opts...)
  57. httpClient, endpoint, err := htransport.NewClient(ctx, o...)
  58. if err != nil {
  59. return nil, fmt.Errorf("bigquery: dialing: %v", err)
  60. }
  61. bqs, err := bq.New(httpClient)
  62. if err != nil {
  63. return nil, fmt.Errorf("bigquery: constructing client: %v", err)
  64. }
  65. bqs.BasePath = endpoint
  66. c := &Client{
  67. projectID: projectID,
  68. bqs: bqs,
  69. }
  70. return c, nil
  71. }
  72. // Close closes any resources held by the client.
  73. // Close should be called when the client is no longer needed.
  74. // It need not be called at program exit.
  75. func (c *Client) Close() error {
  76. return nil
  77. }
  78. // Calls the Jobs.Insert RPC and returns a Job.
  79. func (c *Client) insertJob(ctx context.Context, job *bq.Job, media io.Reader) (*Job, error) {
  80. call := c.bqs.Jobs.Insert(c.projectID, job).Context(ctx)
  81. setClientHeader(call.Header())
  82. if media != nil {
  83. call.Media(media)
  84. }
  85. var res *bq.Job
  86. var err error
  87. invoke := func() error {
  88. res, err = call.Do()
  89. return err
  90. }
  91. // A job with a client-generated ID can be retried; the presence of the
  92. // ID makes the insert operation idempotent.
  93. // We don't retry if there is media, because it is an io.Reader. We'd
  94. // have to read the contents and keep it in memory, and that could be expensive.
  95. // TODO(jba): Look into retrying if media != nil.
  96. if job.JobReference != nil && media == nil {
  97. err = runWithRetry(ctx, invoke)
  98. } else {
  99. err = invoke()
  100. }
  101. if err != nil {
  102. return nil, err
  103. }
  104. return bqToJob(res, c)
  105. }
  106. // Convert a number of milliseconds since the Unix epoch to a time.Time.
  107. // Treat an input of zero specially: convert it to the zero time,
  108. // rather than the start of the epoch.
  109. func unixMillisToTime(m int64) time.Time {
  110. if m == 0 {
  111. return time.Time{}
  112. }
  113. return time.Unix(0, m*1e6)
  114. }
  115. // runWithRetry calls the function until it returns nil or a non-retryable error, or
  116. // the context is done.
  117. // See the similar function in ../storage/invoke.go. The main difference is the
  118. // reason for retrying.
  119. func runWithRetry(ctx context.Context, call func() error) error {
  120. // These parameters match the suggestions in https://cloud.google.com/bigquery/sla.
  121. backoff := gax.Backoff{
  122. Initial: 1 * time.Second,
  123. Max: 32 * time.Second,
  124. Multiplier: 2,
  125. }
  126. return internal.Retry(ctx, backoff, func() (stop bool, err error) {
  127. err = call()
  128. if err == nil {
  129. return true, nil
  130. }
  131. return !retryableError(err), err
  132. })
  133. }
  134. // This is the correct definition of retryable according to the BigQuery team. It
  135. // also considers 502 ("Bad Gateway") and 503 ("Service Unavailable") errors
  136. // retryable; these are returned by systems between the client and the BigQuery
  137. // service.
  138. func retryableError(err error) bool {
  139. e, ok := err.(*googleapi.Error)
  140. if !ok {
  141. return false
  142. }
  143. var reason string
  144. if len(e.Errors) > 0 {
  145. reason = e.Errors[0].Reason
  146. }
  147. return e.Code == http.StatusServiceUnavailable || e.Code == http.StatusBadGateway || reason == "backendError" || reason == "rateLimitExceeded"
  148. }