github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/chunk/client/aws/dynamodb_table_client.go (about) 1 package aws 2 3 import ( 4 "context" 5 "strings" 6 7 "github.com/aws/aws-sdk-go/aws" 8 "github.com/aws/aws-sdk-go/aws/awserr" 9 "github.com/aws/aws-sdk-go/service/dynamodb" 10 "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" 11 "github.com/go-kit/log/level" 12 "github.com/grafana/dskit/backoff" 13 "github.com/pkg/errors" 14 "github.com/prometheus/client_golang/prometheus" 15 "github.com/weaveworks/common/instrument" 16 "golang.org/x/time/rate" 17 18 "github.com/grafana/loki/pkg/storage/config" 19 "github.com/grafana/loki/pkg/storage/stores/series/index" 20 "github.com/grafana/loki/pkg/util/log" 21 ) 22 23 // Pluggable auto-scaler implementation 24 type autoscale interface { 25 PostCreateTable(ctx context.Context, desc config.TableDesc) error 26 // This whole interface is very similar to chunk.TableClient, but 27 // DescribeTable needs to mutate desc 28 DescribeTable(ctx context.Context, desc *config.TableDesc) error 29 UpdateTable(ctx context.Context, current config.TableDesc, expected *config.TableDesc) error 30 } 31 32 type callManager struct { 33 limiter *rate.Limiter 34 backoffConfig backoff.Config 35 } 36 37 type dynamoTableClient struct { 38 DynamoDB dynamodbiface.DynamoDBAPI 39 callManager callManager 40 autoscale autoscale 41 metrics *dynamoDBMetrics 42 } 43 44 // NewDynamoDBTableClient makes a new DynamoTableClient. 45 func NewDynamoDBTableClient(cfg DynamoDBConfig, reg prometheus.Registerer) (index.TableClient, error) { 46 dynamoDB, err := dynamoClientFromURL(cfg.DynamoDB.URL) 47 if err != nil { 48 return nil, err 49 } 50 51 callManager := callManager{ 52 limiter: rate.NewLimiter(rate.Limit(cfg.APILimit), 1), 53 backoffConfig: cfg.BackoffConfig, 54 } 55 56 var autoscale autoscale 57 if cfg.Metrics.URL != "" { 58 autoscale, err = newMetricsAutoScaling(cfg) 59 if err != nil { 60 return nil, err 61 } 62 } 63 64 return dynamoTableClient{ 65 DynamoDB: dynamoDB, 66 callManager: callManager, 67 autoscale: autoscale, 68 metrics: newMetrics(reg), 69 }, nil 70 } 71 72 func (d dynamoTableClient) Stop() { 73 } 74 75 func (d dynamoTableClient) backoffAndRetry(ctx context.Context, fn func(context.Context) error) error { 76 return d.callManager.backoffAndRetry(ctx, fn) 77 } 78 79 func (d callManager) backoffAndRetry(ctx context.Context, fn func(context.Context) error) error { 80 if d.limiter != nil { // Tests will have a nil limiter. 81 _ = d.limiter.Wait(ctx) 82 } 83 84 backoff := backoff.New(ctx, d.backoffConfig) 85 for backoff.Ongoing() { 86 if err := fn(ctx); err != nil { 87 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ThrottlingException" { 88 level.Warn(log.WithContext(ctx, log.Logger)).Log("msg", "got error, backing off and retrying", "err", err, "retry", backoff.NumRetries()) 89 backoff.Wait() 90 continue 91 } else { 92 return err 93 } 94 } 95 return nil 96 } 97 return backoff.Err() 98 } 99 100 func (d dynamoTableClient) ListTables(ctx context.Context) ([]string, error) { 101 table := []string{} 102 err := d.backoffAndRetry(ctx, func(ctx context.Context) error { 103 return instrument.CollectedRequest(ctx, "DynamoDB.ListTablesPages", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 104 return d.DynamoDB.ListTablesPagesWithContext(ctx, &dynamodb.ListTablesInput{}, func(resp *dynamodb.ListTablesOutput, _ bool) bool { 105 for _, s := range resp.TableNames { 106 table = append(table, *s) 107 } 108 return true 109 }) 110 }) 111 }) 112 return table, err 113 } 114 115 func chunkTagsToDynamoDB(ts config.Tags) []*dynamodb.Tag { 116 var result []*dynamodb.Tag 117 for k, v := range ts { 118 result = append(result, &dynamodb.Tag{ 119 Key: aws.String(k), 120 Value: aws.String(v), 121 }) 122 } 123 return result 124 } 125 126 func (d dynamoTableClient) CreateTable(ctx context.Context, desc config.TableDesc) error { 127 var tableARN *string 128 if err := d.backoffAndRetry(ctx, func(ctx context.Context) error { 129 return instrument.CollectedRequest(ctx, "DynamoDB.CreateTable", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 130 input := &dynamodb.CreateTableInput{ 131 TableName: aws.String(desc.Name), 132 AttributeDefinitions: []*dynamodb.AttributeDefinition{ 133 { 134 AttributeName: aws.String(hashKey), 135 AttributeType: aws.String(dynamodb.ScalarAttributeTypeS), 136 }, 137 { 138 AttributeName: aws.String(rangeKey), 139 AttributeType: aws.String(dynamodb.ScalarAttributeTypeB), 140 }, 141 }, 142 KeySchema: []*dynamodb.KeySchemaElement{ 143 { 144 AttributeName: aws.String(hashKey), 145 KeyType: aws.String(dynamodb.KeyTypeHash), 146 }, 147 { 148 AttributeName: aws.String(rangeKey), 149 KeyType: aws.String(dynamodb.KeyTypeRange), 150 }, 151 }, 152 } 153 154 if desc.UseOnDemandIOMode { 155 input.BillingMode = aws.String(dynamodb.BillingModePayPerRequest) 156 } else { 157 input.BillingMode = aws.String(dynamodb.BillingModeProvisioned) 158 input.ProvisionedThroughput = &dynamodb.ProvisionedThroughput{ 159 ReadCapacityUnits: aws.Int64(desc.ProvisionedRead), 160 WriteCapacityUnits: aws.Int64(desc.ProvisionedWrite), 161 } 162 } 163 164 output, err := d.DynamoDB.CreateTableWithContext(ctx, input) 165 if err != nil { 166 return err 167 } 168 if output.TableDescription != nil { 169 tableARN = output.TableDescription.TableArn 170 } 171 return nil 172 }) 173 }); err != nil { 174 return err 175 } 176 177 if d.autoscale != nil { 178 err := d.autoscale.PostCreateTable(ctx, desc) 179 if err != nil { 180 return err 181 } 182 } 183 184 tags := chunkTagsToDynamoDB(desc.Tags) 185 if len(tags) > 0 { 186 return d.backoffAndRetry(ctx, func(ctx context.Context) error { 187 return instrument.CollectedRequest(ctx, "DynamoDB.TagResource", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 188 _, err := d.DynamoDB.TagResourceWithContext(ctx, &dynamodb.TagResourceInput{ 189 ResourceArn: tableARN, 190 Tags: tags, 191 }) 192 if relevantError(err) { 193 return err 194 } 195 return nil 196 }) 197 }) 198 } 199 return nil 200 } 201 202 func (d dynamoTableClient) DeleteTable(ctx context.Context, name string) error { 203 return d.backoffAndRetry(ctx, func(ctx context.Context) error { 204 return instrument.CollectedRequest(ctx, "DynamoDB.DeleteTable", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 205 input := &dynamodb.DeleteTableInput{TableName: aws.String(name)} 206 _, err := d.DynamoDB.DeleteTableWithContext(ctx, input) 207 if err != nil { 208 return err 209 } 210 211 return nil 212 }) 213 }) 214 } 215 216 func (d dynamoTableClient) DescribeTable(ctx context.Context, name string) (desc config.TableDesc, isActive bool, err error) { 217 var tableARN *string 218 err = d.backoffAndRetry(ctx, func(ctx context.Context) error { 219 return instrument.CollectedRequest(ctx, "DynamoDB.DescribeTable", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 220 out, err := d.DynamoDB.DescribeTableWithContext(ctx, &dynamodb.DescribeTableInput{ 221 TableName: aws.String(name), 222 }) 223 if err != nil { 224 return err 225 } 226 desc.Name = name 227 if out.Table != nil { 228 if provision := out.Table.ProvisionedThroughput; provision != nil { 229 if provision.ReadCapacityUnits != nil { 230 desc.ProvisionedRead = *provision.ReadCapacityUnits 231 } 232 if provision.WriteCapacityUnits != nil { 233 desc.ProvisionedWrite = *provision.WriteCapacityUnits 234 } 235 } 236 if out.Table.TableStatus != nil { 237 isActive = (*out.Table.TableStatus == dynamodb.TableStatusActive) 238 } 239 if out.Table.BillingModeSummary != nil { 240 desc.UseOnDemandIOMode = *out.Table.BillingModeSummary.BillingMode == dynamodb.BillingModePayPerRequest 241 } 242 tableARN = out.Table.TableArn 243 } 244 return err 245 }) 246 }) 247 if err != nil { 248 return 249 } 250 251 err = d.backoffAndRetry(ctx, func(ctx context.Context) error { 252 return instrument.CollectedRequest(ctx, "DynamoDB.ListTagsOfResource", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 253 out, err := d.DynamoDB.ListTagsOfResourceWithContext(ctx, &dynamodb.ListTagsOfResourceInput{ 254 ResourceArn: tableARN, 255 }) 256 if relevantError(err) { 257 return err 258 } 259 desc.Tags = make(map[string]string, len(out.Tags)) 260 for _, tag := range out.Tags { 261 desc.Tags[*tag.Key] = *tag.Value 262 } 263 return nil 264 }) 265 }) 266 267 if d.autoscale != nil { 268 err = d.autoscale.DescribeTable(ctx, &desc) 269 } 270 return 271 } 272 273 // Filter out errors that we don't want to see 274 // (currently only relevant in integration tests) 275 func relevantError(err error) bool { 276 if err == nil { 277 return false 278 } 279 if strings.Contains(err.Error(), "Tagging is not currently supported in DynamoDB Local.") { 280 return false 281 } 282 return true 283 } 284 285 func (d dynamoTableClient) UpdateTable(ctx context.Context, current, expected config.TableDesc) error { 286 if d.autoscale != nil { 287 err := d.autoscale.UpdateTable(ctx, current, &expected) 288 if err != nil { 289 return err 290 } 291 } 292 level.Debug(log.Logger).Log("msg", "Updating Table", 293 "expectedWrite", expected.ProvisionedWrite, 294 "currentWrite", current.ProvisionedWrite, 295 "expectedRead", expected.ProvisionedRead, 296 "currentRead", current.ProvisionedRead, 297 "expectedOnDemandMode", expected.UseOnDemandIOMode, 298 "currentOnDemandMode", current.UseOnDemandIOMode) 299 if (current.ProvisionedRead != expected.ProvisionedRead || 300 current.ProvisionedWrite != expected.ProvisionedWrite) && 301 !expected.UseOnDemandIOMode { 302 level.Info(log.Logger).Log("msg", "updating provisioned throughput on table", "table", expected.Name, "old_read", current.ProvisionedRead, "old_write", current.ProvisionedWrite, "new_read", expected.ProvisionedRead, "new_write", expected.ProvisionedWrite) 303 if err := d.backoffAndRetry(ctx, func(ctx context.Context) error { 304 return instrument.CollectedRequest(ctx, "DynamoDB.UpdateTable", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 305 var dynamoBillingMode string 306 updateTableInput := &dynamodb.UpdateTableInput{ 307 TableName: aws.String(expected.Name), 308 ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ 309 ReadCapacityUnits: aws.Int64(expected.ProvisionedRead), 310 WriteCapacityUnits: aws.Int64(expected.ProvisionedWrite), 311 }, 312 } 313 // we need this to be a separate check for the billing mode, as aws returns 314 // an error if we set a table to the billing mode it is currently on. 315 if current.UseOnDemandIOMode != expected.UseOnDemandIOMode { 316 dynamoBillingMode = dynamodb.BillingModeProvisioned 317 level.Info(log.Logger).Log("msg", "updating billing mode on table", "table", expected.Name, "old_mode", current.UseOnDemandIOMode, "new_mode", expected.UseOnDemandIOMode) 318 updateTableInput.BillingMode = aws.String(dynamoBillingMode) 319 } 320 321 _, err := d.DynamoDB.UpdateTableWithContext(ctx, updateTableInput) 322 return err 323 }) 324 }); err != nil { 325 recordDynamoError(expected.Name, err, "DynamoDB.UpdateTable", d.metrics) 326 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "LimitExceededException" { 327 level.Warn(log.Logger).Log("msg", "update limit exceeded", "err", err) 328 } else { 329 return err 330 } 331 } 332 } else if expected.UseOnDemandIOMode && current.UseOnDemandIOMode != expected.UseOnDemandIOMode { 333 // moved the enabling of OnDemand mode to it's own block to reduce complexities & interactions with the various 334 // settings used in provisioned mode. Unfortunately the boilerplate wrappers for retry and tracking needed to be copied. 335 if err := d.backoffAndRetry(ctx, func(ctx context.Context) error { 336 return instrument.CollectedRequest(ctx, "DynamoDB.UpdateTable", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 337 level.Info(log.Logger).Log("msg", "updating billing mode on table", "table", expected.Name, "old_mode", current.UseOnDemandIOMode, "new_mode", expected.UseOnDemandIOMode) 338 updateTableInput := &dynamodb.UpdateTableInput{TableName: aws.String(expected.Name), BillingMode: aws.String(dynamodb.BillingModePayPerRequest)} 339 _, err := d.DynamoDB.UpdateTableWithContext(ctx, updateTableInput) 340 return err 341 }) 342 }); err != nil { 343 recordDynamoError(expected.Name, err, "DynamoDB.UpdateTable", d.metrics) 344 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "LimitExceededException" { 345 level.Warn(log.Logger).Log("msg", "update limit exceeded", "err", err) 346 } else { 347 return err 348 } 349 } 350 } 351 352 if !current.Tags.Equals(expected.Tags) { 353 var tableARN *string 354 if err := d.backoffAndRetry(ctx, func(ctx context.Context) error { 355 return instrument.CollectedRequest(ctx, "DynamoDB.DescribeTable", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 356 out, err := d.DynamoDB.DescribeTableWithContext(ctx, &dynamodb.DescribeTableInput{ 357 TableName: aws.String(expected.Name), 358 }) 359 if err != nil { 360 return err 361 } 362 if out.Table != nil { 363 tableARN = out.Table.TableArn 364 } 365 return nil 366 }) 367 }); err != nil { 368 return err 369 } 370 371 return d.backoffAndRetry(ctx, func(ctx context.Context) error { 372 return instrument.CollectedRequest(ctx, "DynamoDB.TagResource", d.metrics.dynamoRequestDuration, instrument.ErrorCode, func(ctx context.Context) error { 373 _, err := d.DynamoDB.TagResourceWithContext(ctx, &dynamodb.TagResourceInput{ 374 ResourceArn: tableARN, 375 Tags: chunkTagsToDynamoDB(expected.Tags), 376 }) 377 if relevantError(err) { 378 return errors.Wrap(err, "applying tags") 379 } 380 return nil 381 }) 382 }) 383 } 384 return nil 385 }