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 //------------------------------------------------------------------------------