go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/rules/exporter/client.go (about) 1 // Copyright 2023 The LUCI Authors. 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 15 // Package exporter provides methods to interact with the failure_assocation_rules BigQuery table. 16 package exporter 17 18 import ( 19 "context" 20 "fmt" 21 22 "cloud.google.com/go/bigquery" 23 "cloud.google.com/go/bigquery/storage/managedwriter" 24 "google.golang.org/protobuf/proto" 25 26 "go.chromium.org/luci/common/bq" 27 "go.chromium.org/luci/common/errors" 28 29 "go.chromium.org/luci/analysis/internal/bqutil" 30 bqpb "go.chromium.org/luci/analysis/proto/bq" 31 ) 32 33 // Client provides methods to export and read failure_association_rules_history BigQuery tale 34 type Client struct { 35 // projectID is the name of the GCP project that contains LUCI Analysis datasets. 36 projectID string 37 // bqClient is the BigQuery client. 38 bqClient *bigquery.Client 39 // mwClient is the BigQuery managed writer. 40 mwClient *managedwriter.Client 41 } 42 43 // NewClient creates a new client for exporting failure association rules 44 // via the BigQuery Write API. 45 func NewClient(ctx context.Context, projectID string) (s *Client, reterr error) { 46 if projectID == "" { 47 return nil, errors.New("GCP Project must be specified") 48 } 49 bqClient, err := bqutil.Client(ctx, projectID) 50 if err != nil { 51 return nil, errors.Annotate(err, "creating BQ client").Err() 52 } 53 defer func() { 54 if reterr != nil { 55 // This method failed for some reason, clean up the 56 // BigQuery client. Swallow any error returned by the Close() 57 // call. 58 bqClient.Close() 59 } 60 }() 61 62 mwClient, err := bqutil.NewWriterClient(ctx, projectID) 63 if err != nil { 64 return nil, errors.Annotate(err, "creating managed writer client").Err() 65 } 66 return &Client{ 67 projectID: projectID, 68 bqClient: bqClient, 69 mwClient: mwClient, 70 }, nil 71 } 72 73 // Close releases resources held by the client. 74 func (c *Client) Close() (reterr error) { 75 // Ensure both bqClient and mwClient Close() methods 76 // are called, even if one panics or fails. 77 defer func() { 78 err := c.mwClient.Close() 79 if reterr == nil { 80 reterr = err 81 } 82 }() 83 return c.bqClient.Close() 84 } 85 86 // Insert inserts the given rows in BigQuery. 87 func (c *Client) Insert(ctx context.Context, rows []*bqpb.FailureAssociationRulesHistoryRow) error { 88 if err := c.ensureSchema(ctx); err != nil { 89 return errors.Annotate(err, "ensure schema").Err() 90 } 91 tableName := fmt.Sprintf("projects/%s/datasets/%s/tables/%s", c.projectID, bqutil.InternalDatasetID, tableName) 92 writer := bqutil.NewWriter(c.mwClient, tableName, tableSchemaDescriptor) 93 payload := make([]proto.Message, len(rows)) 94 for i, r := range rows { 95 payload[i] = r 96 } 97 return writer.AppendRowsWithPendingStream(ctx, payload) 98 } 99 100 // NewestLastUpdated get the largest value in the lastUpdated field from the BigQuery table. 101 // The last_updated field is synced from the spanner table which is a spanner commit timestamp. 102 // This the newest last_updated field indicate newest update of the failureAssociationRules spanner table 103 // which has been synced to BigQuery. 104 func (c *Client) NewestLastUpdated(ctx context.Context) (bigquery.NullTimestamp, error) { 105 if err := c.ensureSchema(ctx); err != nil { 106 return bigquery.NullTimestamp{}, errors.Annotate(err, "ensure schema").Err() 107 } 108 q := c.bqClient.Query(` 109 SELECT MAX(last_update_time) as LastUpdateTime 110 FROM failure_association_rules_history 111 `) 112 q.DefaultDatasetID = bqutil.InternalDatasetID 113 job, err := q.Run(ctx) 114 if err != nil { 115 return bigquery.NullTimestamp{}, errors.Annotate(err, "querying max last update").Err() 116 } 117 it, err := job.Read(ctx) 118 119 if err != nil { 120 return bigquery.NullTimestamp{}, err 121 } 122 type result struct { 123 LastUpdateTime bigquery.NullTimestamp 124 } 125 var lastUpdatedResult result 126 err = it.Next(&lastUpdatedResult) 127 if err != nil { 128 return bigquery.NullTimestamp{}, errors.Annotate(err, "obtain next row").Err() 129 } 130 return lastUpdatedResult.LastUpdateTime, nil 131 } 132 133 // schemaApplier ensures BQ schema matches the row proto definitions. 134 var schemaApplier = bq.NewSchemaApplyer(bq.RegisterSchemaApplyerCache(1)) 135 136 func (c *Client) ensureSchema(ctx context.Context) error { 137 // Dataset for the project may have to be manually created. 138 table := c.bqClient.Dataset(bqutil.InternalDatasetID).Table(tableName) 139 if err := schemaApplier.EnsureTable(ctx, table, tableMetadata); err != nil { 140 return errors.Annotate(err, "ensuring %s table", tableName).Err() 141 } 142 return nil 143 }