github.com/Jeffail/benthos/v3@v3.65.0/lib/cache/aws_dynamodb.go (about)

     1  package cache
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/Jeffail/benthos/v3/internal/docs"
    10  	"github.com/Jeffail/benthos/v3/lib/log"
    11  	"github.com/Jeffail/benthos/v3/lib/metrics"
    12  	"github.com/Jeffail/benthos/v3/lib/types"
    13  	"github.com/Jeffail/benthos/v3/lib/util/aws/session"
    14  	"github.com/Jeffail/benthos/v3/lib/util/retries"
    15  	"github.com/aws/aws-sdk-go/aws"
    16  	"github.com/aws/aws-sdk-go/aws/awserr"
    17  	"github.com/aws/aws-sdk-go/service/dynamodb"
    18  	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
    19  	"github.com/aws/aws-sdk-go/service/dynamodb/expression"
    20  	"github.com/cenkalti/backoff/v4"
    21  )
    22  
    23  //------------------------------------------------------------------------------
    24  
    25  func init() {
    26  	Constructors[TypeAWSDynamoDB] = TypeSpec{
    27  		constructor: NewAWSDynamoDB,
    28  		Version:     "3.36.0",
    29  		Summary: `
    30  Stores key/value pairs as a single document in a DynamoDB table. The key is
    31  stored as a string value and used as the table hash key. The value is stored as
    32  a binary value using the ` + "`data_key`" + ` field name.`,
    33  		Description: `
    34  A prefix can be specified to allow multiple cache types to share a single
    35  DynamoDB table. An optional TTL duration (` + "`ttl`" + `) and field
    36  (` + "`ttl_key`" + `) can be specified if the backing table has TTL enabled.
    37  
    38  Strong read consistency can be enabled using the ` + "`consistent_read`" + `
    39  configuration field.
    40  
    41  ### Credentials
    42  
    43  By default Benthos will use a shared credentials file when connecting to AWS
    44  services. It's also possible to set them explicitly at the component level,
    45  allowing you to transfer data across accounts. You can find out more
    46  [in this document](/docs/guides/cloud/aws).`,
    47  		FieldSpecs: docs.FieldSpecs{
    48  			docs.FieldCommon("table", "The table to store items in."),
    49  			docs.FieldCommon("hash_key", "The key of the table column to store item keys within."),
    50  			docs.FieldCommon("data_key", "The key of the table column to store item values within."),
    51  			docs.FieldAdvanced("consistent_read", "Whether to use strongly consistent reads on Get commands."),
    52  			docs.FieldAdvanced("ttl", "An optional TTL to set for items, calculated from the moment the item is cached."),
    53  			docs.FieldAdvanced("ttl_key", "The column key to place the TTL value within."),
    54  		}.Merge(session.FieldSpecs()).Merge(retries.FieldSpecs()),
    55  	}
    56  
    57  	Constructors[TypeDynamoDB] = TypeSpec{
    58  		constructor: NewDynamoDB,
    59  		Status:      docs.StatusDeprecated,
    60  		Summary: `
    61  Stores key/value pairs as a single document in a DynamoDB table. The key is
    62  stored as a string value and used as the table hash key. The value is stored as
    63  a binary value using the ` + "`data_key`" + ` field name.`,
    64  		Description: `
    65  ## Alternatives
    66  
    67  This cache has been renamed to ` + "[`aws_dynamodb`](/docs/components/caches/aws_dynamodb)" + `.
    68  
    69  A prefix can be specified to allow multiple cache types to share a single
    70  DynamoDB table. An optional TTL duration (` + "`ttl`" + `) and field
    71  (` + "`ttl_key`" + `) can be specified if the backing table has TTL enabled.
    72  
    73  Strong read consistency can be enabled using the ` + "`consistent_read`" + `
    74  configuration field.
    75  
    76  ### Credentials
    77  
    78  By default Benthos will use a shared credentials file when connecting to AWS
    79  services. It's also possible to set them explicitly at the component level,
    80  allowing you to transfer data across accounts. You can find out more
    81  [in this document](/docs/guides/cloud/aws).`,
    82  		FieldSpecs: docs.FieldSpecs{
    83  			docs.FieldCommon("table", "The table to store items in."),
    84  			docs.FieldCommon("hash_key", "The key of the table column to store item keys within."),
    85  			docs.FieldCommon("data_key", "The key of the table column to store item values within."),
    86  			docs.FieldAdvanced("consistent_read", "Whether to use strongly consistent reads on Get commands."),
    87  			docs.FieldAdvanced("ttl", "An optional TTL to set for items, calculated from the moment the item is cached."),
    88  			docs.FieldAdvanced("ttl_key", "The column key to place the TTL value within."),
    89  		}.Merge(session.FieldSpecs()).Merge(retries.FieldSpecs()),
    90  	}
    91  }
    92  
    93  //------------------------------------------------------------------------------
    94  
    95  type sessionConfig struct {
    96  	session.Config `json:",inline" yaml:",inline"`
    97  }
    98  
    99  // DynamoDBConfig contains config fields for the DynamoDB cache type.
   100  type DynamoDBConfig struct {
   101  	sessionConfig  `json:",inline" yaml:",inline"`
   102  	ConsistentRead bool   `json:"consistent_read" yaml:"consistent_read"`
   103  	DataKey        string `json:"data_key" yaml:"data_key"`
   104  	HashKey        string `json:"hash_key" yaml:"hash_key"`
   105  	Table          string `json:"table" yaml:"table"`
   106  	TTL            string `json:"ttl" yaml:"ttl"`
   107  	TTLKey         string `json:"ttl_key" yaml:"ttl_key"`
   108  	retries.Config `json:",inline" yaml:",inline"`
   109  }
   110  
   111  // NewDynamoDBConfig creates a MemoryConfig populated with default values.
   112  func NewDynamoDBConfig() DynamoDBConfig {
   113  	rConf := retries.NewConfig()
   114  	rConf.MaxRetries = 3
   115  	rConf.Backoff.InitialInterval = "1s"
   116  	rConf.Backoff.MaxInterval = "5s"
   117  	rConf.Backoff.MaxElapsedTime = "30s"
   118  	return DynamoDBConfig{
   119  		sessionConfig: sessionConfig{
   120  			Config: session.NewConfig(),
   121  		},
   122  		ConsistentRead: false,
   123  		DataKey:        "",
   124  		HashKey:        "",
   125  		Table:          "",
   126  		TTL:            "",
   127  		TTLKey:         "",
   128  		Config:         rConf,
   129  	}
   130  }
   131  
   132  //------------------------------------------------------------------------------
   133  
   134  // DynamoDB is a DynamoDB based cache implementation.
   135  type DynamoDB struct {
   136  	client      dynamodbiface.DynamoDBAPI
   137  	conf        DynamoDBConfig
   138  	log         log.Modular
   139  	stats       metrics.Type
   140  	table       *string
   141  	ttl         time.Duration
   142  	backoffCtor func() backoff.BackOff
   143  	boffPool    sync.Pool
   144  
   145  	mLatency         metrics.StatTimer
   146  	mGetCount        metrics.StatCounter
   147  	mGetRetry        metrics.StatCounter
   148  	mGetFailed       metrics.StatCounter
   149  	mGetSuccess      metrics.StatCounter
   150  	mGetLatency      metrics.StatTimer
   151  	mGetNotFound     metrics.StatCounter
   152  	mSetCount        metrics.StatCounter
   153  	mSetRetry        metrics.StatCounter
   154  	mSetFailed       metrics.StatCounter
   155  	mSetSuccess      metrics.StatCounter
   156  	mSetLatency      metrics.StatTimer
   157  	mSetMultiCount   metrics.StatCounter
   158  	mSetMultiRetry   metrics.StatCounter
   159  	mSetMultiFailed  metrics.StatCounter
   160  	mSetMultiSuccess metrics.StatCounter
   161  	mSetMultiLatency metrics.StatTimer
   162  	mAddCount        metrics.StatCounter
   163  	mAddDupe         metrics.StatCounter
   164  	mAddRetry        metrics.StatCounter
   165  	mAddFailedDupe   metrics.StatCounter
   166  	mAddFailedErr    metrics.StatCounter
   167  	mAddSuccess      metrics.StatCounter
   168  	mAddLatency      metrics.StatTimer
   169  	mDelCount        metrics.StatCounter
   170  	mDelRetry        metrics.StatCounter
   171  	mDelFailedErr    metrics.StatCounter
   172  	mDelSuccess      metrics.StatCounter
   173  	mDelLatency      metrics.StatTimer
   174  }
   175  
   176  // NewAWSDynamoDB creates a new DynamoDB cache type.
   177  func NewAWSDynamoDB(conf Config, mgr types.Manager, log log.Modular, stats metrics.Type) (types.Cache, error) {
   178  	return newDynamoDB(conf.AWSDynamoDB, mgr, log, stats)
   179  }
   180  
   181  // NewDynamoDB creates a new DynamoDB cache type.
   182  func NewDynamoDB(conf Config, mgr types.Manager, log log.Modular, stats metrics.Type) (types.Cache, error) {
   183  	return newDynamoDB(conf.DynamoDB, mgr, log, stats)
   184  }
   185  
   186  func newDynamoDB(conf DynamoDBConfig, mgr types.Manager, log log.Modular, stats metrics.Type) (types.Cache, error) {
   187  	d := DynamoDB{
   188  		conf:  conf,
   189  		log:   log,
   190  		stats: stats,
   191  		table: aws.String(conf.Table),
   192  
   193  		mLatency:         stats.GetTimer("latency"),
   194  		mGetCount:        stats.GetCounter("get.count"),
   195  		mGetRetry:        stats.GetCounter("get.retry"),
   196  		mGetFailed:       stats.GetCounter("get.failed.error"),
   197  		mGetNotFound:     stats.GetCounter("get.failed.not_found"),
   198  		mGetSuccess:      stats.GetCounter("get.success"),
   199  		mGetLatency:      stats.GetTimer("get.latency"),
   200  		mSetCount:        stats.GetCounter("set.count"),
   201  		mSetRetry:        stats.GetCounter("set.retry"),
   202  		mSetFailed:       stats.GetCounter("set.failed.error"),
   203  		mSetSuccess:      stats.GetCounter("set.success"),
   204  		mSetLatency:      stats.GetTimer("set.latency"),
   205  		mSetMultiCount:   stats.GetCounter("set_multi.count"),
   206  		mSetMultiRetry:   stats.GetCounter("set_multi.retry"),
   207  		mSetMultiFailed:  stats.GetCounter("set_multi.failed.error"),
   208  		mSetMultiSuccess: stats.GetCounter("set_multi.success"),
   209  		mSetMultiLatency: stats.GetTimer("set_multi.latency"),
   210  		mAddCount:        stats.GetCounter("add.count"),
   211  		mAddDupe:         stats.GetCounter("add.failed.duplicate"),
   212  		mAddRetry:        stats.GetCounter("add.retry"),
   213  		mAddFailedDupe:   stats.GetCounter("add.failed.duplicate"),
   214  		mAddFailedErr:    stats.GetCounter("add.failed.error"),
   215  		mAddSuccess:      stats.GetCounter("add.success"),
   216  		mAddLatency:      stats.GetTimer("add.latency"),
   217  		mDelCount:        stats.GetCounter("delete.count"),
   218  		mDelRetry:        stats.GetCounter("delete.retry"),
   219  		mDelFailedErr:    stats.GetCounter("delete.failed.error"),
   220  		mDelSuccess:      stats.GetCounter("delete.success"),
   221  		mDelLatency:      stats.GetTimer("delete.latency"),
   222  	}
   223  
   224  	if d.conf.TTL != "" {
   225  		ttl, err := time.ParseDuration(d.conf.TTL)
   226  		if err != nil {
   227  			return nil, err
   228  		}
   229  		d.ttl = ttl
   230  	}
   231  
   232  	sess, err := d.conf.GetSession()
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	d.client = dynamodb.New(sess)
   238  	out, err := d.client.DescribeTable(&dynamodb.DescribeTableInput{
   239  		TableName: d.table,
   240  	})
   241  	if err != nil {
   242  		return nil, err
   243  	} else if out == nil ||
   244  		out.Table == nil ||
   245  		out.Table.TableStatus == nil ||
   246  		*out.Table.TableStatus != dynamodb.TableStatusActive {
   247  		return nil, fmt.Errorf("table '%s' must be active", d.conf.Table)
   248  	}
   249  
   250  	if d.backoffCtor, err = conf.Config.GetCtor(); err != nil {
   251  		return nil, err
   252  	}
   253  	d.boffPool = sync.Pool{
   254  		New: func() interface{} {
   255  			return d.backoffCtor()
   256  		},
   257  	}
   258  
   259  	return &d, nil
   260  }
   261  
   262  //------------------------------------------------------------------------------
   263  
   264  // Get attempts to locate and return a cached value by its key, returns an error
   265  // if the key does not exist.
   266  func (d *DynamoDB) Get(key string) ([]byte, error) {
   267  	d.mGetCount.Incr(1)
   268  
   269  	tStarted := time.Now()
   270  	boff := d.boffPool.Get().(backoff.BackOff)
   271  	defer func() {
   272  		boff.Reset()
   273  		d.boffPool.Put(boff)
   274  	}()
   275  
   276  	result, err := d.get(key)
   277  	for err != nil && err != types.ErrKeyNotFound {
   278  		wait := boff.NextBackOff()
   279  		if wait == backoff.Stop {
   280  			break
   281  		}
   282  		time.Sleep(wait)
   283  		d.mGetRetry.Incr(1)
   284  		result, err = d.get(key)
   285  	}
   286  	if err == nil {
   287  		d.mGetSuccess.Incr(1)
   288  	} else if err == types.ErrKeyNotFound {
   289  		d.mGetNotFound.Incr(1)
   290  	} else {
   291  		d.mGetFailed.Incr(1)
   292  	}
   293  
   294  	latency := int64(time.Since(tStarted))
   295  	d.mGetLatency.Timing(latency)
   296  	d.mLatency.Timing(latency)
   297  
   298  	return result, err
   299  }
   300  
   301  func (d *DynamoDB) get(key string) ([]byte, error) {
   302  	res, err := d.client.GetItem(&dynamodb.GetItemInput{
   303  		Key: map[string]*dynamodb.AttributeValue{
   304  			d.conf.HashKey: {
   305  				S: aws.String(key),
   306  			},
   307  		},
   308  		TableName:      d.table,
   309  		ConsistentRead: aws.Bool(d.conf.ConsistentRead),
   310  	})
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  
   315  	val, ok := res.Item[d.conf.DataKey]
   316  	if !ok || val.B == nil {
   317  		d.log.Debugf("key not found: %s", key)
   318  		return nil, types.ErrKeyNotFound
   319  	}
   320  	return val.B, nil
   321  }
   322  
   323  // Set attempts to set the value of a key.
   324  func (d *DynamoDB) Set(key string, value []byte) error {
   325  	d.mSetCount.Incr(1)
   326  
   327  	tStarted := time.Now()
   328  	boff := d.boffPool.Get().(backoff.BackOff)
   329  	defer func() {
   330  		boff.Reset()
   331  		d.boffPool.Put(boff)
   332  	}()
   333  
   334  	_, err := d.client.PutItem(d.putItemInput(key, value))
   335  	for err != nil {
   336  		wait := boff.NextBackOff()
   337  		if wait == backoff.Stop {
   338  			break
   339  		}
   340  		time.Sleep(wait)
   341  		d.mSetRetry.Incr(1)
   342  		_, err = d.client.PutItem(d.putItemInput(key, value))
   343  	}
   344  	if err == nil {
   345  		d.mSetSuccess.Incr(1)
   346  	} else {
   347  		d.mSetFailed.Incr(1)
   348  	}
   349  
   350  	latency := int64(time.Since(tStarted))
   351  	d.mSetLatency.Timing(latency)
   352  	d.mLatency.Timing(latency)
   353  
   354  	return err
   355  }
   356  
   357  // SetMulti attempts to set the value of multiple keys, if any keys fail to be
   358  // set an error is returned.
   359  func (d *DynamoDB) SetMulti(items map[string][]byte) error {
   360  	d.mSetMultiCount.Incr(1)
   361  
   362  	tStarted := time.Now()
   363  	boff := d.boffPool.Get().(backoff.BackOff)
   364  	defer func() {
   365  		boff.Reset()
   366  		d.boffPool.Put(boff)
   367  	}()
   368  
   369  	writeReqs := []*dynamodb.WriteRequest{}
   370  	for k, v := range items {
   371  		writeReqs = append(writeReqs, &dynamodb.WriteRequest{
   372  			PutRequest: &dynamodb.PutRequest{
   373  				Item: d.putItemInput(k, v).Item,
   374  			},
   375  		})
   376  	}
   377  
   378  	var err error
   379  	for len(writeReqs) > 0 {
   380  		wait := boff.NextBackOff()
   381  		var batchResult *dynamodb.BatchWriteItemOutput
   382  		batchResult, err = d.client.BatchWriteItem(&dynamodb.BatchWriteItemInput{
   383  			RequestItems: map[string][]*dynamodb.WriteRequest{
   384  				*d.table: writeReqs,
   385  			},
   386  		})
   387  		if err != nil {
   388  			d.log.Errorf("Write multi error: %v\n", err)
   389  		} else if unproc := batchResult.UnprocessedItems[*d.table]; len(unproc) > 0 {
   390  			writeReqs = unproc
   391  			err = fmt.Errorf("failed to set %v items", len(unproc))
   392  		} else {
   393  			writeReqs = nil
   394  		}
   395  
   396  		if err != nil {
   397  			if wait == backoff.Stop {
   398  				break
   399  			}
   400  			time.After(wait)
   401  			d.mSetMultiRetry.Incr(1)
   402  		}
   403  	}
   404  
   405  	if err == nil {
   406  		d.mSetMultiSuccess.Incr(1)
   407  	} else {
   408  		d.mSetMultiFailed.Incr(1)
   409  	}
   410  
   411  	latency := int64(time.Since(tStarted))
   412  	d.mSetMultiLatency.Timing(latency)
   413  	d.mLatency.Timing(latency)
   414  
   415  	return err
   416  }
   417  
   418  // Add attempts to set the value of a key only if the key does not already exist
   419  // and returns an error if the key already exists.
   420  func (d *DynamoDB) Add(key string, value []byte) error {
   421  	d.mAddCount.Incr(1)
   422  
   423  	tStarted := time.Now()
   424  	boff := d.boffPool.Get().(backoff.BackOff)
   425  	defer func() {
   426  		boff.Reset()
   427  		d.boffPool.Put(boff)
   428  	}()
   429  
   430  	err := d.add(key, value)
   431  	for err != nil && err != types.ErrKeyAlreadyExists {
   432  		wait := boff.NextBackOff()
   433  		if wait == backoff.Stop {
   434  			break
   435  		}
   436  		time.Sleep(wait)
   437  		d.mAddRetry.Incr(1)
   438  		err = d.add(key, value)
   439  	}
   440  	if err == nil {
   441  		d.mAddSuccess.Incr(1)
   442  	} else if err == types.ErrKeyAlreadyExists {
   443  		d.mAddFailedDupe.Incr(1)
   444  	} else {
   445  		d.mAddFailedErr.Incr(1)
   446  	}
   447  
   448  	latency := int64(time.Since(tStarted))
   449  	d.mAddLatency.Timing(latency)
   450  	d.mLatency.Timing(latency)
   451  
   452  	return err
   453  }
   454  
   455  func (d *DynamoDB) add(key string, value []byte) error {
   456  	input := d.putItemInput(key, value)
   457  
   458  	expr, err := expression.NewBuilder().
   459  		WithCondition(expression.AttributeNotExists(expression.Name(d.conf.HashKey))).
   460  		Build()
   461  	if err != nil {
   462  		return err
   463  	}
   464  	input.ExpressionAttributeNames = expr.Names()
   465  	input.ConditionExpression = expr.Condition()
   466  
   467  	if _, err = d.client.PutItem(input); err != nil {
   468  		if aerr, ok := err.(awserr.Error); ok {
   469  			if aerr.Code() == dynamodb.ErrCodeConditionalCheckFailedException {
   470  				return types.ErrKeyAlreadyExists
   471  			}
   472  		}
   473  		return err
   474  	}
   475  	return nil
   476  }
   477  
   478  // Delete attempts to remove a key.
   479  func (d *DynamoDB) Delete(key string) error {
   480  	d.mDelCount.Incr(1)
   481  
   482  	tStarted := time.Now()
   483  	boff := d.boffPool.Get().(backoff.BackOff)
   484  	defer func() {
   485  		boff.Reset()
   486  		d.boffPool.Put(boff)
   487  	}()
   488  
   489  	err := d.delete(key)
   490  	for err != nil {
   491  		wait := boff.NextBackOff()
   492  		if wait == backoff.Stop {
   493  			break
   494  		}
   495  		time.Sleep(wait)
   496  		d.mDelRetry.Incr(1)
   497  		err = d.delete(key)
   498  	}
   499  	if err == nil {
   500  		d.mDelSuccess.Incr(1)
   501  	} else {
   502  		d.mDelFailedErr.Incr(1)
   503  	}
   504  
   505  	latency := int64(time.Since(tStarted))
   506  	d.mDelLatency.Timing(latency)
   507  	d.mLatency.Timing(latency)
   508  
   509  	return err
   510  }
   511  
   512  func (d *DynamoDB) delete(key string) error {
   513  	_, err := d.client.DeleteItem(&dynamodb.DeleteItemInput{
   514  		Key: map[string]*dynamodb.AttributeValue{
   515  			d.conf.HashKey: {
   516  				S: aws.String(key),
   517  			},
   518  		},
   519  		TableName: d.table,
   520  	})
   521  	return err
   522  }
   523  
   524  // putItemInput creates a generic put item input for use in Set and Add operations
   525  func (d *DynamoDB) putItemInput(key string, value []byte) *dynamodb.PutItemInput {
   526  	input := dynamodb.PutItemInput{
   527  		Item: map[string]*dynamodb.AttributeValue{
   528  			d.conf.HashKey: {
   529  				S: aws.String(key),
   530  			},
   531  			d.conf.DataKey: {
   532  				B: value,
   533  			},
   534  		},
   535  		TableName: d.table,
   536  	}
   537  
   538  	if d.ttl != 0 && d.conf.TTLKey != "" {
   539  		input.Item[d.conf.TTLKey] = &dynamodb.AttributeValue{
   540  			N: aws.String(strconv.FormatInt(time.Now().Add(d.ttl).Unix(), 10)),
   541  		}
   542  	}
   543  
   544  	return &input
   545  }
   546  
   547  // CloseAsync shuts down the cache.
   548  func (d *DynamoDB) CloseAsync() {
   549  }
   550  
   551  // WaitForClose blocks until the cache has closed down.
   552  func (d *DynamoDB) WaitForClose(timeout time.Duration) error {
   553  	return nil
   554  }
   555  
   556  //------------------------------------------------------------------------------