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  }