addressed PR comments, moved some things around to new storage package,
separated out lease and moved to storage, created storage.go.
This commit is contained in:
parent
32fcced2d3
commit
8f0fb899b7
5 changed files with 130 additions and 113 deletions
146
consumergroup.go
146
consumergroup.go
|
|
@ -2,98 +2,57 @@ package consumer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/twinj/uuid"
|
"github.com/twinj/uuid"
|
||||||
|
|
||||||
|
"github.com/harlow/kinesis-consumer/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO change logging to actual logger
|
// TODO change logging to actual logger
|
||||||
|
|
||||||
// StorageCouldNotUpdateOrCreateLease is a simple error for handling races that are lost in storage
|
// ConsumerGroupCheckpoint is a simple struct for managing the consumergroup and heartbeat of updating leases
|
||||||
var StorageCouldNotUpdateOrCreateLease = errors.New("storage could not update or create lease")
|
|
||||||
|
|
||||||
// Lease is data for handling a lease/lock on a particular shard
|
|
||||||
type Lease struct {
|
|
||||||
// LeaseKey is the partition/primaryKey in storage and is the shardID
|
|
||||||
LeaseKey string `json:"leaseKey"`
|
|
||||||
|
|
||||||
// Checkpoint the most updated sequenceNumber from kinesis
|
|
||||||
Checkpoint string `json:"checkpoint"`
|
|
||||||
|
|
||||||
// LeaseCounter will be updated any time a lease changes owners
|
|
||||||
LeaseCounter int `json:"leaseCounter"`
|
|
||||||
|
|
||||||
// LeaseOwner is the client id (defaulted to a guid)
|
|
||||||
LeaseOwner string `json:"leaseOwner"`
|
|
||||||
|
|
||||||
// HeartbeatID is a guid that gets updated on every heartbeat. It is used to help determine if a lease is expired.
|
|
||||||
// If a lease's heartbeatID hasn't been updated within the lease duration, then we assume the lease is expired
|
|
||||||
HeartbeatID string `json:"heartbeatID"`
|
|
||||||
|
|
||||||
// LastUpdateTime is the last time the lease has changed. Purposely not stored in storage. It is used with
|
|
||||||
LastUpdateTime time.Time `json:"-"` // purposely left out of json so it doesn't get stored in dynamo
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExpired is a function to check if the lease is expired, but is only expected to be used in the heartbeat loop
|
|
||||||
func (lease Lease) IsExpired(maxLeaseDuration time.Duration) bool {
|
|
||||||
if !lease.LastUpdateTime.IsZero() {
|
|
||||||
durationPast := time.Since(lease.LastUpdateTime)
|
|
||||||
if durationPast > maxLeaseDuration {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckpointStorage is a simple interface for abstracting away the storage functions
|
|
||||||
type CheckpointStorage interface {
|
|
||||||
CreateLease(lease Lease) error
|
|
||||||
UpdateLease(originalLease, updatedLease Lease) error
|
|
||||||
GetLease(leaseKey string) (*Lease, error)
|
|
||||||
GetAllLeases() (map[string]Lease, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConsumerGroupCheckpoint is a simple struct for managing the
|
|
||||||
type ConsumerGroupCheckpoint struct {
|
type ConsumerGroupCheckpoint struct {
|
||||||
Storage CheckpointStorage
|
|
||||||
kinesis Kinesis
|
|
||||||
LeaseDuration time.Duration
|
|
||||||
HeartBeatDuration time.Duration
|
HeartBeatDuration time.Duration
|
||||||
|
LeaseDuration time.Duration
|
||||||
OwnerID string
|
OwnerID string
|
||||||
|
Storage Storage
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
currentLeases map[string]*Lease //Initially, this will only be one
|
kinesis Kinesis
|
||||||
Mutex *sync.Mutex
|
|
||||||
|
leasesMutex *sync.Mutex
|
||||||
|
leases map[string]*storage.Lease // Initially, this will only be one
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cgc ConsumerGroupCheckpoint) Get(shardID string) (string, error) {
|
func (cgc ConsumerGroupCheckpoint) Get(shardID string) (string, error) {
|
||||||
return cgc.currentLeases[shardID].Checkpoint, nil
|
return cgc.leases[shardID].Checkpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cgc ConsumerGroupCheckpoint) Set(shardID, sequenceNumber string) error {
|
func (cgc ConsumerGroupCheckpoint) Set(shardID, sequenceNumber string) error {
|
||||||
cgc.Mutex.Lock()
|
cgc.leasesMutex.Lock()
|
||||||
defer cgc.Mutex.Unlock()
|
defer cgc.leasesMutex.Unlock()
|
||||||
|
|
||||||
cgc.currentLeases[shardID].Checkpoint = sequenceNumber
|
cgc.leases[shardID].Checkpoint = sequenceNumber
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsumerGroupCheckpoint(
|
func NewConsumerGroupCheckpoint(
|
||||||
storage CheckpointStorage,
|
Storage Storage,
|
||||||
kinesis Kinesis,
|
kinesis Kinesis,
|
||||||
leaseDuration time.Duration,
|
leaseDuration time.Duration,
|
||||||
heartBeatDuration time.Duration) *ConsumerGroupCheckpoint {
|
heartBeatDuration time.Duration) *ConsumerGroupCheckpoint {
|
||||||
return &ConsumerGroupCheckpoint{
|
return &ConsumerGroupCheckpoint{
|
||||||
Storage: storage,
|
|
||||||
kinesis: kinesis,
|
|
||||||
LeaseDuration: leaseDuration,
|
|
||||||
HeartBeatDuration: heartBeatDuration,
|
HeartBeatDuration: heartBeatDuration,
|
||||||
|
LeaseDuration: leaseDuration,
|
||||||
OwnerID: uuid.NewV4().String(), // generated owner id
|
OwnerID: uuid.NewV4().String(), // generated owner id
|
||||||
|
Storage: Storage,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
currentLeases: make(map[string]*Lease, 1),
|
kinesis: kinesis,
|
||||||
Mutex: &sync.Mutex{},
|
|
||||||
|
leasesMutex: &sync.Mutex{},
|
||||||
|
leases: make(map[string]*storage.Lease, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -105,37 +64,42 @@ func (cgc ConsumerGroupCheckpoint) Start(ctx context.Context, shardc chan string
|
||||||
|
|
||||||
tick := time.NewTicker(cgc.LeaseDuration)
|
tick := time.NewTicker(cgc.LeaseDuration)
|
||||||
defer tick.Stop()
|
defer tick.Stop()
|
||||||
var currentLeases map[string]Lease
|
var currentLeases map[string]storage.Lease
|
||||||
var previousLeases map[string]Lease
|
var previousLeases map[string]storage.Lease
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-tick.C:
|
case <-tick.C:
|
||||||
if len(cgc.currentLeases) == 0 { // only do anything if there are no current leases
|
if len(cgc.leases) > 0 { // only do anything if there are no current leases
|
||||||
fmt.Printf("Attempting to acquire lease for OwnerID=%s\n", cgc.OwnerID)
|
continue
|
||||||
var err error
|
|
||||||
currentLeases, err = cgc.Storage.GetAllLeases()
|
|
||||||
if err != nil {
|
|
||||||
// TODO log this error
|
|
||||||
}
|
|
||||||
|
|
||||||
lease := cgc.CreateOrGetExpiredLease(currentLeases, previousLeases)
|
|
||||||
if lease != nil && lease.LeaseKey != "" {
|
|
||||||
cgc.currentLeases[lease.LeaseKey] = lease
|
|
||||||
go cgc.heartbeatLoop(lease)
|
|
||||||
shardc <- lease.LeaseKey
|
|
||||||
}
|
|
||||||
previousLeases = currentLeases
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf("Attempting to acquire lease for OwnerID=%s\n", cgc.OwnerID)
|
||||||
|
var err error
|
||||||
|
currentLeases, err = cgc.Storage.GetAllLeases()
|
||||||
|
if err != nil {
|
||||||
|
// TODO log this error
|
||||||
|
}
|
||||||
|
|
||||||
|
lease := cgc.CreateOrGetExpiredLease(currentLeases, previousLeases)
|
||||||
|
previousLeases = currentLeases
|
||||||
|
|
||||||
|
if lease == nil || lease.LeaseKey == "" {
|
||||||
|
continue // lease wasn't acquired continue
|
||||||
|
}
|
||||||
|
// lease sucessfully acquired
|
||||||
|
// start the heartbeat and send back the shardID on the channel
|
||||||
|
cgc.leases[lease.LeaseKey] = lease
|
||||||
|
go cgc.heartbeatLoop(lease)
|
||||||
|
shardc <- lease.LeaseKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateOrGetExpiredLease is a helper function that tries checks to see if there are any leases available if not it tries to grab an "expired" lease where the heartbeat isn't updated.
|
// CreateOrGetExpiredLease is a helper function that tries checks to see if there are any leases available if not it tries to grab an "expired" lease where the heartbeat isn't updated.
|
||||||
func (cgc ConsumerGroupCheckpoint) CreateOrGetExpiredLease(currentLeases map[string]Lease, previousLeases map[string]Lease) *Lease {
|
func (cgc ConsumerGroupCheckpoint) CreateOrGetExpiredLease(currentLeases map[string]storage.Lease, previousLeases map[string]storage.Lease) *storage.Lease {
|
||||||
cgc.Mutex.Lock()
|
cgc.leasesMutex.Lock()
|
||||||
defer cgc.Mutex.Unlock()
|
defer cgc.leasesMutex.Unlock()
|
||||||
|
|
||||||
listOfShards, err := cgc.kinesis.ListAllShards()
|
listOfShards, err := cgc.kinesis.ListAllShards()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -144,11 +108,11 @@ func (cgc ConsumerGroupCheckpoint) CreateOrGetExpiredLease(currentLeases map[str
|
||||||
}
|
}
|
||||||
|
|
||||||
shardIDsNotYetTaken := getShardIDsNotLeased(listOfShards, currentLeases)
|
shardIDsNotYetTaken := getShardIDsNotLeased(listOfShards, currentLeases)
|
||||||
var currentLease *Lease
|
var currentLease *storage.Lease
|
||||||
if len(shardIDsNotYetTaken) > 0 {
|
if len(shardIDsNotYetTaken) > 0 {
|
||||||
fmt.Println("Grabbing lease from shardIDs not taken")
|
fmt.Println("Grabbing lease from shardIDs not taken")
|
||||||
shardId := shardIDsNotYetTaken[0] //grab the first one //TODO randomize
|
shardId := shardIDsNotYetTaken[0] //grab the first one //TODO randomize
|
||||||
tempLease := Lease{
|
tempLease := storage.Lease{
|
||||||
LeaseKey: shardId,
|
LeaseKey: shardId,
|
||||||
Checkpoint: "0", // we don't have this yet
|
Checkpoint: "0", // we don't have this yet
|
||||||
LeaseCounter: 1,
|
LeaseCounter: 1,
|
||||||
|
|
@ -174,7 +138,7 @@ func (cgc ConsumerGroupCheckpoint) CreateOrGetExpiredLease(currentLeases map[str
|
||||||
for _, lease := range currentLeases {
|
for _, lease := range currentLeases {
|
||||||
// TODO add some nil checking
|
// TODO add some nil checking
|
||||||
if currentLeases[lease.LeaseKey].HeartbeatID == previousLeases[lease.LeaseKey].HeartbeatID { //we assume the lease was not updated during the amount of time
|
if currentLeases[lease.LeaseKey].HeartbeatID == previousLeases[lease.LeaseKey].HeartbeatID { //we assume the lease was not updated during the amount of time
|
||||||
updatedLease := Lease{
|
updatedLease := storage.Lease{
|
||||||
LeaseKey: lease.LeaseKey,
|
LeaseKey: lease.LeaseKey,
|
||||||
Checkpoint: lease.Checkpoint,
|
Checkpoint: lease.Checkpoint,
|
||||||
LeaseCounter: lease.LeaseCounter + 1,
|
LeaseCounter: lease.LeaseCounter + 1,
|
||||||
|
|
@ -199,9 +163,9 @@ func (cgc ConsumerGroupCheckpoint) CreateOrGetExpiredLease(currentLeases map[str
|
||||||
}
|
}
|
||||||
|
|
||||||
// heartbeatLoop should constantly update the lease that is provided
|
// heartbeatLoop should constantly update the lease that is provided
|
||||||
func (cgc ConsumerGroupCheckpoint) heartbeatLoop(lease *Lease) {
|
func (cgc ConsumerGroupCheckpoint) heartbeatLoop(lease *storage.Lease) {
|
||||||
cgc.Mutex.Lock()
|
cgc.leasesMutex.Lock()
|
||||||
defer cgc.Mutex.Unlock()
|
defer cgc.leasesMutex.Unlock()
|
||||||
fmt.Println("Starting heartbeat loop")
|
fmt.Println("Starting heartbeat loop")
|
||||||
ticker := time.NewTicker(cgc.HeartBeatDuration)
|
ticker := time.NewTicker(cgc.HeartBeatDuration)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
@ -211,9 +175,9 @@ func (cgc ConsumerGroupCheckpoint) heartbeatLoop(lease *Lease) {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
|
||||||
if isLeaseInvalidOrChanged(cgc, *lease) || lease.IsExpired(cgc.LeaseDuration) {
|
if isLeaseInvalidOrChanged(cgc, *lease) || lease.IsExpired(cgc.LeaseDuration) {
|
||||||
delete(cgc.currentLeases, lease.LeaseKey)
|
delete(cgc.leases, lease.LeaseKey)
|
||||||
}
|
}
|
||||||
updatedLease := Lease{
|
updatedLease := storage.Lease{
|
||||||
LeaseKey: lease.LeaseKey,
|
LeaseKey: lease.LeaseKey,
|
||||||
Checkpoint: lease.Checkpoint,
|
Checkpoint: lease.Checkpoint,
|
||||||
LeaseCounter: lease.LeaseCounter,
|
LeaseCounter: lease.LeaseCounter,
|
||||||
|
|
@ -232,7 +196,7 @@ func (cgc ConsumerGroupCheckpoint) heartbeatLoop(lease *Lease) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// isLeaseInvalidOrChanged checks to see if the lease changed
|
// isLeaseInvalidOrChanged checks to see if the lease changed
|
||||||
func isLeaseInvalidOrChanged(cgc ConsumerGroupCheckpoint, lease Lease) bool {
|
func isLeaseInvalidOrChanged(cgc ConsumerGroupCheckpoint, lease storage.Lease) bool {
|
||||||
leaseCurrent, _ := cgc.Storage.GetLease(lease.LeaseKey)
|
leaseCurrent, _ := cgc.Storage.GetLease(lease.LeaseKey)
|
||||||
if lease.LeaseKey != leaseCurrent.LeaseKey || cgc.OwnerID != leaseCurrent.LeaseOwner || leaseCurrent.LeaseCounter != lease.LeaseCounter {
|
if lease.LeaseKey != leaseCurrent.LeaseKey || cgc.OwnerID != leaseCurrent.LeaseOwner || leaseCurrent.LeaseCounter != lease.LeaseCounter {
|
||||||
fmt.Printf("The lease changed\n")
|
fmt.Printf("The lease changed\n")
|
||||||
|
|
@ -242,7 +206,7 @@ func isLeaseInvalidOrChanged(cgc ConsumerGroupCheckpoint, lease Lease) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getShardIDsNotLeased finds any open shards where there are no leases yet created
|
// getShardIDsNotLeased finds any open shards where there are no leases yet created
|
||||||
func getShardIDsNotLeased(shardIDs []string, leases map[string]Lease) []string {
|
func getShardIDsNotLeased(shardIDs []string, leases map[string]storage.Lease) []string {
|
||||||
var shardIDsNotUsed []string
|
var shardIDsNotUsed []string
|
||||||
for _, shardID := range shardIDs {
|
for _, shardID := range shardIDs {
|
||||||
if _, ok := leases[shardID]; !ok {
|
if _, ok := leases[shardID]; !ok {
|
||||||
|
|
|
||||||
11
storage.go
Normal file
11
storage.go
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package consumer
|
||||||
|
|
||||||
|
import "github.com/harlow/kinesis-consumer/storage"
|
||||||
|
|
||||||
|
// Storage is a simple interface for abstracting away the storage functions
|
||||||
|
type Storage interface {
|
||||||
|
CreateLease(lease storage.Lease) error
|
||||||
|
UpdateLease(originalLease, updatedLease storage.Lease) error
|
||||||
|
GetLease(leaseKey string) (*storage.Lease, error)
|
||||||
|
GetAllLeases() (map[string]storage.Lease, error)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package consumergroup
|
package ddb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
|
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
|
||||||
"github.com/aws/aws-sdk-go/service/dynamodb/expression"
|
"github.com/aws/aws-sdk-go/service/dynamodb/expression"
|
||||||
|
|
||||||
consumer "github.com/harlow/kinesis-consumer"
|
"github.com/harlow/kinesis-consumer/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DynamoDb simple and minimal interface for DynamoDb that helps with testing
|
// DynamoDb simple and minimal interface for DynamoDb that helps with testing
|
||||||
|
|
@ -36,7 +36,7 @@ type LeaseUpdate struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLease - stores the lease in dynamo
|
// CreateLease - stores the lease in dynamo
|
||||||
func (dynamoClient DynamoStorage) CreateLease(lease consumer.Lease) error {
|
func (dynamoClient DynamoStorage) CreateLease(lease storage.Lease) error {
|
||||||
|
|
||||||
condition := expression.AttributeNotExists(
|
condition := expression.AttributeNotExists(
|
||||||
expression.Name("leaseKey"),
|
expression.Name("leaseKey"),
|
||||||
|
|
@ -60,7 +60,7 @@ func (dynamoClient DynamoStorage) CreateLease(lease consumer.Lease) error {
|
||||||
if _, err := dynamoClient.Db.PutItem(input); err != nil {
|
if _, err := dynamoClient.Db.PutItem(input); err != nil {
|
||||||
if awsErr, ok := err.(awserr.Error); ok {
|
if awsErr, ok := err.(awserr.Error); ok {
|
||||||
if awsErr.Code() == dynamodb.ErrCodeConditionalCheckFailedException {
|
if awsErr.Code() == dynamodb.ErrCodeConditionalCheckFailedException {
|
||||||
return consumer.StorageCouldNotUpdateOrCreateLease
|
return storage.StorageCouldNotUpdateOrCreateLease
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|
@ -70,7 +70,7 @@ func (dynamoClient DynamoStorage) CreateLease(lease consumer.Lease) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateLease updates the lease in dynamo
|
// UpdateLease updates the lease in dynamo
|
||||||
func (dynamoClient DynamoStorage) UpdateLease(originalLease, updatedLease consumer.Lease) error {
|
func (dynamoClient DynamoStorage) UpdateLease(originalLease, updatedLease storage.Lease) error {
|
||||||
|
|
||||||
condition := expression.And(
|
condition := expression.And(
|
||||||
expression.Equal(expression.Name("leaseKey"), expression.Value(originalLease.LeaseKey)),
|
expression.Equal(expression.Name("leaseKey"), expression.Value(originalLease.LeaseKey)),
|
||||||
|
|
@ -103,7 +103,7 @@ func (dynamoClient DynamoStorage) UpdateLease(originalLease, updatedLease consum
|
||||||
if _, err := dynamoClient.Db.UpdateItem(input); err != nil {
|
if _, err := dynamoClient.Db.UpdateItem(input); err != nil {
|
||||||
if awsErr, ok := err.(awserr.Error); ok {
|
if awsErr, ok := err.(awserr.Error); ok {
|
||||||
if awsErr.Code() == dynamodb.ErrCodeConditionalCheckFailedException {
|
if awsErr.Code() == dynamodb.ErrCodeConditionalCheckFailedException {
|
||||||
return consumer.StorageCouldNotUpdateOrCreateLease
|
return storage.StorageCouldNotUpdateOrCreateLease
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|
@ -112,7 +112,7 @@ func (dynamoClient DynamoStorage) UpdateLease(originalLease, updatedLease consum
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapLeaseToLeaseUpdate(lease consumer.Lease) LeaseUpdate {
|
func mapLeaseToLeaseUpdate(lease storage.Lease) LeaseUpdate {
|
||||||
return LeaseUpdate{
|
return LeaseUpdate{
|
||||||
Checkpoint: lease.Checkpoint,
|
Checkpoint: lease.Checkpoint,
|
||||||
LeaseCounter: lease.LeaseCounter,
|
LeaseCounter: lease.LeaseCounter,
|
||||||
|
|
@ -125,7 +125,7 @@ func mapLeaseToLeaseUpdate(lease consumer.Lease) LeaseUpdate {
|
||||||
// GetLease returns the latest stored records sorted by clockID in descending order
|
// GetLease returns the latest stored records sorted by clockID in descending order
|
||||||
// It is assumed that we won't be keeping many records per ID otherwise, this may need to be optimized
|
// It is assumed that we won't be keeping many records per ID otherwise, this may need to be optimized
|
||||||
// later (possibly to use a map)
|
// later (possibly to use a map)
|
||||||
func (dynamoClient DynamoStorage) GetLease(leaseKey string) (*consumer.Lease, error) {
|
func (dynamoClient DynamoStorage) GetLease(leaseKey string) (*storage.Lease, error) {
|
||||||
|
|
||||||
key := mapLeaseKeyToDdbKey(leaseKey)
|
key := mapLeaseKeyToDdbKey(leaseKey)
|
||||||
input := &dynamodb.GetItemInput{
|
input := &dynamodb.GetItemInput{
|
||||||
|
|
@ -138,7 +138,7 @@ func (dynamoClient DynamoStorage) GetLease(leaseKey string) (*consumer.Lease, er
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var lease consumer.Lease
|
var lease storage.Lease
|
||||||
if err := dynamodbattribute.UnmarshalMap(result.Item, &lease); err != nil {
|
if err := dynamodbattribute.UnmarshalMap(result.Item, &lease); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +153,7 @@ func mapLeaseKeyToDdbKey(leaseKey string) map[string]*dynamodb.AttributeValue {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllLeases this can be used at start up (or anytime to grab all the leases)
|
// GetAllLeases this can be used at start up (or anytime to grab all the leases)
|
||||||
func (dynamoClient DynamoStorage) GetAllLeases() (map[string]consumer.Lease, error) {
|
func (dynamoClient DynamoStorage) GetAllLeases() (map[string]storage.Lease, error) {
|
||||||
|
|
||||||
// TODO if we have a lot of shards, we might have to worry about limits here
|
// TODO if we have a lot of shards, we might have to worry about limits here
|
||||||
input := &dynamodb.ScanInput{
|
input := &dynamodb.ScanInput{
|
||||||
|
|
@ -165,9 +165,9 @@ func (dynamoClient DynamoStorage) GetAllLeases() (map[string]consumer.Lease, err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
leases := make(map[string]consumer.Lease, len(result.Items))
|
leases := make(map[string]storage.Lease, len(result.Items))
|
||||||
for _, item := range result.Items {
|
for _, item := range result.Items {
|
||||||
var record consumer.Lease
|
var record storage.Lease
|
||||||
if err := dynamodbattribute.UnmarshalMap(item, &record); err != nil {
|
if err := dynamodbattribute.UnmarshalMap(item, &record); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package consumergroup
|
package ddb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -10,10 +10,10 @@ import (
|
||||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
consumer "github.com/harlow/kinesis-consumer"
|
"github.com/harlow/kinesis-consumer/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testLease1 = consumer.Lease{
|
var testLease1 = storage.Lease{
|
||||||
LeaseKey: "000001",
|
LeaseKey: "000001",
|
||||||
Checkpoint: "1234345",
|
Checkpoint: "1234345",
|
||||||
LeaseCounter: 0,
|
LeaseCounter: 0,
|
||||||
|
|
@ -63,7 +63,7 @@ func TestDynamoStorage_CreateLease(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
tableName string
|
tableName string
|
||||||
lease consumer.Lease
|
lease storage.Lease
|
||||||
dynamoErr error
|
dynamoErr error
|
||||||
expectedErr error
|
expectedErr error
|
||||||
}{
|
}{
|
||||||
|
|
@ -79,7 +79,7 @@ func TestDynamoStorage_CreateLease(t *testing.T) {
|
||||||
tableName: "test",
|
tableName: "test",
|
||||||
lease: testLease1,
|
lease: testLease1,
|
||||||
dynamoErr: awserr.New(dynamodb.ErrCodeConditionalCheckFailedException, "", nil),
|
dynamoErr: awserr.New(dynamodb.ErrCodeConditionalCheckFailedException, "", nil),
|
||||||
expectedErr: consumer.StorageCouldNotUpdateOrCreateLease,
|
expectedErr: storage.StorageCouldNotUpdateOrCreateLease,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "CreateLease_Expect_Error_ErrCodeInternalServerError",
|
name: "CreateLease_Expect_Error_ErrCodeInternalServerError",
|
||||||
|
|
@ -113,8 +113,8 @@ func TestDynamoStorage_UpdateLease(t *testing.T) {
|
||||||
tableName string
|
tableName string
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
originalLease consumer.Lease
|
originalLease storage.Lease
|
||||||
updatedLease consumer.Lease
|
updatedLease storage.Lease
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -141,7 +141,7 @@ func TestDynamoStorage_UpdateLease(t *testing.T) {
|
||||||
|
|
||||||
func Test_mapLeaseToLeaseUpdate(t *testing.T) {
|
func Test_mapLeaseToLeaseUpdate(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
lease consumer.Lease
|
lease storage.Lease
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -176,7 +176,7 @@ func TestDynamoStorage_GetLease(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
tableName string
|
tableName string
|
||||||
leaseKey string
|
leaseKey string
|
||||||
want *consumer.Lease
|
want *storage.Lease
|
||||||
dynamoOut *dynamodb.GetItemOutput
|
dynamoOut *dynamodb.GetItemOutput
|
||||||
dynamoErr error
|
dynamoErr error
|
||||||
wantErr bool
|
wantErr bool
|
||||||
|
|
@ -239,7 +239,7 @@ func TestDynamoStorage_GetAllLeases(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
fields fields
|
fields fields
|
||||||
want map[string]consumer.Lease
|
want map[string]storage.Lease
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
// TODO: Add test cases.
|
// TODO: Add test cases.
|
||||||
42
storage/lease.go
Normal file
42
storage/lease.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lease is data for handling a lease/lock on a particular shard
|
||||||
|
type Lease struct {
|
||||||
|
// LeaseKey is the partition/primaryKey in storage and is the shardID
|
||||||
|
LeaseKey string `json:"leaseKey"`
|
||||||
|
|
||||||
|
// Checkpoint the most updated sequenceNumber from kinesis
|
||||||
|
Checkpoint string `json:"checkpoint"`
|
||||||
|
|
||||||
|
// LeaseCounter will be updated any time a lease changes owners
|
||||||
|
LeaseCounter int `json:"leaseCounter"`
|
||||||
|
|
||||||
|
// LeaseOwner is the client id (defaulted to a guid)
|
||||||
|
LeaseOwner string `json:"leaseOwner"`
|
||||||
|
|
||||||
|
// HeartbeatID is a guid that gets updated on every heartbeat. It is used to help determine if a lease is expired.
|
||||||
|
// If a lease's heartbeatID hasn't been updated within the lease duration, then we assume the lease is expired
|
||||||
|
HeartbeatID string `json:"heartbeatID"`
|
||||||
|
|
||||||
|
// LastUpdateTime is the last time the lease has changed. Purposely not stored in storage. It is used with
|
||||||
|
LastUpdateTime time.Time `json:"-"` // purposely left out of json so it doesn't get stored in dynamo
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExpired is a function to check if the lease is expired, but is only expected to be used in the heartbeat loop
|
||||||
|
func (lease Lease) IsExpired(maxLeaseDuration time.Duration) bool {
|
||||||
|
if !lease.LastUpdateTime.IsZero() {
|
||||||
|
durationPast := time.Since(lease.LastUpdateTime)
|
||||||
|
if durationPast > maxLeaseDuration {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageCouldNotUpdateOrCreateLease is a simple error for handling races that are lost in storage
|
||||||
|
var StorageCouldNotUpdateOrCreateLease = errors.New("storage could not update or create lease")
|
||||||
Loading…
Reference in a new issue