Added condition expressions to ddb for updates and puts. rearranged some of the code in consumergroup to accomodate conditional expressions.
Moved the LeaseUpdate from consumergroup to ddb.go - it is only needed by ddb.go, and it is ddb.go specific. Added some comments.
This commit is contained in:
parent
b47b611696
commit
f3eb53a703
3 changed files with 98 additions and 46 deletions
|
|
@ -9,13 +9,27 @@ import (
|
||||||
"github.com/twinj/uuid"
|
"github.com/twinj/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO change logging to actual logger
|
||||||
|
|
||||||
// Lease is data for handling a lease/lock on a particular shard
|
// Lease is data for handling a lease/lock on a particular shard
|
||||||
type Lease struct {
|
type Lease struct {
|
||||||
LeaseKey string `json:"leaseKey"` // This is the partitionKey in dynamo
|
// LeaseKey is the partition/primaryKey in storage and is the shardID
|
||||||
Checkpoint string `json:"checkpoint"` // the most updated sequenceNumber from kinesis
|
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"`
|
LeaseCounter int `json:"leaseCounter"`
|
||||||
|
|
||||||
|
// LeaseOwner is the client id (defaulted to a guid)
|
||||||
LeaseOwner string `json:"leaseOwner"`
|
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"`
|
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
|
LastUpdateTime time.Time `json:"-"` // purposely left out of json so it doesn't get stored in dynamo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,23 +41,13 @@ func (lease Lease) IsExpired(maxLeaseDuration time.Duration) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// LeaseUpdate is a single entry from either journal - subscription or entitlement - with some state information
|
|
||||||
type LeaseUpdate struct {
|
|
||||||
Checkpoint string `json:":cp"`
|
|
||||||
LeaseCounter int `json:":lc"`
|
|
||||||
LeaseOwner string `json:":lo"`
|
|
||||||
HeartbeatID string `json:":hb"`
|
|
||||||
LastUpdateTime time.Time `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckpointStorage is a simple interface for abstracting away the storage functions
|
// CheckpointStorage is a simple interface for abstracting away the storage functions
|
||||||
type CheckpointStorage interface {
|
type CheckpointStorage interface {
|
||||||
CreateLease(lease Lease) error
|
CreateLease(lease Lease) error
|
||||||
UpdateLease(leaseKey string, leaseUpdate LeaseUpdate) error
|
UpdateLease(originalLease, updatedLease Lease) error
|
||||||
GetLease(leaseKey string) (*Lease, error)
|
GetLease(leaseKey string) (*Lease, error)
|
||||||
GetAllLeases() (map[string]Lease, error)
|
GetAllLeases() (map[string]Lease, error)
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +95,7 @@ func NewConsumerGroupCheckpoint(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start is a blocking call that will attempt to acquire a lease on every tick of leaseDuration
|
// Start is a blocking call that will attempt to acquire a lease on every tick of leaseDuration
|
||||||
// If a lease is successfully acquired it will be returned otherwise it will continue to retry
|
// If a lease is successfully acquired it will be added to the channel otherwise it will continue to retry
|
||||||
func (cgc ConsumerGroupCheckpoint) Start(ctx context.Context, shardc chan string) {
|
func (cgc ConsumerGroupCheckpoint) Start(ctx context.Context, shardc chan string) {
|
||||||
fmt.Printf("Starting ConsumerGroupCheckpoint for Consumer %s \n", cgc.OwnerID)
|
fmt.Printf("Starting ConsumerGroupCheckpoint for Consumer %s \n", cgc.OwnerID)
|
||||||
|
|
||||||
|
|
@ -166,21 +170,22 @@ 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
|
||||||
lease.LeaseCounter = lease.LeaseCounter + 1 //update lease counter
|
updatedLease := Lease{
|
||||||
if err := cgc.Storage.UpdateLease(lease.LeaseKey, LeaseUpdate{
|
LeaseKey: lease.LeaseKey,
|
||||||
Checkpoint: lease.Checkpoint,
|
Checkpoint: lease.Checkpoint,
|
||||||
LeaseCounter: lease.LeaseCounter,
|
LeaseCounter: lease.LeaseCounter + 1,
|
||||||
LeaseOwner: cgc.OwnerID,
|
LeaseOwner: cgc.OwnerID,
|
||||||
HeartbeatID: uuid.NewV4().String(),
|
HeartbeatID: uuid.NewV4().String(),
|
||||||
LastUpdateTime: time.Now(),
|
LastUpdateTime: time.Now(),
|
||||||
}); err != nil {
|
}
|
||||||
|
if err := cgc.Storage.UpdateLease(lease, updatedLease); err != nil {
|
||||||
fmt.Printf("Error is happening updating the lease")
|
fmt.Printf("Error is happening updating the lease")
|
||||||
} else {
|
} else {
|
||||||
if isLeaseInvalidOrChanged(cgc, lease) {
|
if isLeaseInvalidOrChanged(cgc, lease) {
|
||||||
return nil //should not be a valid lease at this point
|
return nil //should not be a valid lease at this point
|
||||||
}
|
}
|
||||||
fmt.Printf("Successfully Acquired Expired lease %v\n", lease)
|
fmt.Printf("Successfully Acquired Expired lease %v\n", lease)
|
||||||
currentLease = &lease //successfully acquired the lease
|
currentLease = &updatedLease //successfully acquired the lease
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -189,8 +194,10 @@ func (cgc ConsumerGroupCheckpoint) CreateOrGetExpiredLease(currentLeases map[str
|
||||||
return currentLease
|
return currentLease
|
||||||
}
|
}
|
||||||
|
|
||||||
// heartbeatLoop - this 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 *Lease) {
|
||||||
|
cgc.Mutex.Lock()
|
||||||
|
defer cgc.Mutex.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()
|
||||||
|
|
@ -198,23 +205,21 @@ func (cgc ConsumerGroupCheckpoint) heartbeatLoop(lease *Lease) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
//TODO also check to see if the lease is expired
|
|
||||||
if !isLeaseInvalidOrChanged(cgc, *lease) {
|
|
||||||
//TODO remove the lease from the consumer group checklist
|
|
||||||
|
|
||||||
|
if isLeaseInvalidOrChanged(cgc, *lease) || lease.IsExpired(cgc.LeaseDuration) {
|
||||||
|
delete(cgc.currentLeases, lease.LeaseKey)
|
||||||
}
|
}
|
||||||
// TODO handle error
|
updatedLease := Lease{
|
||||||
heartbeatID := uuid.NewV4().String()
|
LeaseKey: lease.LeaseKey,
|
||||||
updateTime := time.Now()
|
|
||||||
cgc.Storage.UpdateLease(lease.LeaseKey, LeaseUpdate{
|
|
||||||
Checkpoint: lease.Checkpoint,
|
Checkpoint: lease.Checkpoint,
|
||||||
LeaseCounter: lease.LeaseCounter,
|
LeaseCounter: lease.LeaseCounter,
|
||||||
LeaseOwner: lease.LeaseOwner,
|
LeaseOwner: lease.LeaseOwner,
|
||||||
HeartbeatID: heartbeatID,
|
HeartbeatID: uuid.NewV4().String(),
|
||||||
LastUpdateTime: updateTime,
|
LastUpdateTime: time.Now(),
|
||||||
})
|
}
|
||||||
lease.HeartbeatID = heartbeatID
|
// TODO handle error
|
||||||
lease.LastUpdateTime = updateTime
|
cgc.Storage.UpdateLease(*lease, updatedLease)
|
||||||
|
lease = &updatedLease
|
||||||
fmt.Printf("Sucessfully updated lease %v\n", lease)
|
fmt.Printf("Sucessfully updated lease %v\n", lease)
|
||||||
case <-cgc.done:
|
case <-cgc.done:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
package consumergroup
|
package consumergroup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||||
"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"
|
||||||
|
|
||||||
consumer "github.com/harlow/kinesis-consumer"
|
consumer "github.com/harlow/kinesis-consumer"
|
||||||
)
|
)
|
||||||
|
|
@ -22,32 +25,62 @@ type DynamoStorage struct {
|
||||||
tableName string
|
tableName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LeaseUpdate is a simple structure for mapping a lease to an "UpdateItem"
|
||||||
|
type LeaseUpdate struct {
|
||||||
|
Checkpoint string `json:":cp"`
|
||||||
|
LeaseCounter int `json:":lc"`
|
||||||
|
LeaseOwner string `json:":lo"`
|
||||||
|
HeartbeatID string `json:":hb"`
|
||||||
|
LastUpdateTime time.Time `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
// 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 consumer.Lease) error {
|
||||||
|
|
||||||
|
condition := expression.AttributeNotExists(
|
||||||
|
expression.Name("leaseKey"),
|
||||||
|
)
|
||||||
|
expr, err := expression.NewBuilder().WithCondition(condition).Build()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
av, err := dynamodbattribute.MarshalMap(lease)
|
av, err := dynamodbattribute.MarshalMap(lease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO add conditional expression
|
|
||||||
input := &dynamodb.PutItemInput{
|
input := &dynamodb.PutItemInput{
|
||||||
Item: av,
|
Item: av,
|
||||||
TableName: aws.String(dynamoClient.tableName),
|
TableName: aws.String(dynamoClient.tableName),
|
||||||
|
ConditionExpression: expr.Condition(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := dynamoClient.Db.PutItem(input); err != nil {
|
if _, err := dynamoClient.Db.PutItem(input); err != nil {
|
||||||
|
// TODO need to handle ErrCodeConditionalCheckFailedException and repackage error as known error to client
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add conditional expressions
|
// 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(leaseKey string, leaseUpdate consumer.LeaseUpdate) error {
|
|
||||||
|
|
||||||
key := mapShardIdToKey(leaseKey)
|
condition := expression.And(
|
||||||
|
expression.Equal(expression.Name("leaseKey"), expression.Value(originalLease.LeaseKey)),
|
||||||
|
expression.Equal(expression.Name("checkpoint"), expression.Value(originalLease.Checkpoint)),
|
||||||
|
expression.Equal(expression.Name("leaseCounter"), expression.Value(originalLease.LeaseCounter)),
|
||||||
|
expression.Equal(expression.Name("leaseOwner"), expression.Value(originalLease.LeaseOwner)),
|
||||||
|
expression.Equal(expression.Name("heartbeatID"), expression.Value(originalLease.HeartbeatID)),
|
||||||
|
)
|
||||||
|
expr, err := expression.NewBuilder().WithCondition(condition).Build()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := mapLeaseKeyToDdbKey(updatedLease.LeaseKey)
|
||||||
|
leaseUpdate := mapLeaseToLeaseUpdate(updatedLease)
|
||||||
update, err := dynamodbattribute.MarshalMap(leaseUpdate)
|
update, err := dynamodbattribute.MarshalMap(leaseUpdate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -59,21 +92,33 @@ func (dynamoClient DynamoStorage) UpdateLease(leaseKey string, leaseUpdate consu
|
||||||
ReturnValues: aws.String("UPDATED_NEW"),
|
ReturnValues: aws.String("UPDATED_NEW"),
|
||||||
TableName: aws.String(dynamoClient.tableName),
|
TableName: aws.String(dynamoClient.tableName),
|
||||||
UpdateExpression: aws.String("set checkpoint = :cp, leaseCounter= :lc, leaseOwner= :lo, heartbeatID= :hb"),
|
UpdateExpression: aws.String("set checkpoint = :cp, leaseCounter= :lc, leaseOwner= :lo, heartbeatID= :hb"),
|
||||||
|
ConditionExpression: expr.Condition(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := dynamoClient.Db.UpdateItem(input); err != nil {
|
if _, err := dynamoClient.Db.UpdateItem(input); err != nil {
|
||||||
|
// TODO need to handle ErrCodeConditionalCheckFailedException and repackage error as known error to client
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapLeaseToLeaseUpdate(lease consumer.Lease) LeaseUpdate {
|
||||||
|
return LeaseUpdate{
|
||||||
|
Checkpoint: lease.Checkpoint,
|
||||||
|
LeaseCounter: lease.LeaseCounter,
|
||||||
|
LeaseOwner: lease.LeaseOwner,
|
||||||
|
HeartbeatID: lease.HeartbeatID,
|
||||||
|
LastUpdateTime: lease.LastUpdateTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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(shardID string) (*consumer.Lease, error) {
|
func (dynamoClient DynamoStorage) GetLease(leaseKey string) (*consumer.Lease, error) {
|
||||||
|
|
||||||
key := mapShardIdToKey(shardID)
|
key := mapLeaseKeyToDdbKey(leaseKey)
|
||||||
input := &dynamodb.GetItemInput{
|
input := &dynamodb.GetItemInput{
|
||||||
Key: key,
|
Key: key,
|
||||||
TableName: aws.String(dynamoClient.tableName),
|
TableName: aws.String(dynamoClient.tableName),
|
||||||
|
|
@ -92,9 +137,9 @@ func (dynamoClient DynamoStorage) GetLease(shardID string) (*consumer.Lease, err
|
||||||
return &lease, nil
|
return &lease, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapShardIdToKey(shardID string) map[string]*dynamodb.AttributeValue {
|
func mapLeaseKeyToDdbKey(leaseKey string) map[string]*dynamodb.AttributeValue {
|
||||||
return map[string]*dynamodb.AttributeValue{
|
return map[string]*dynamodb.AttributeValue{
|
||||||
"leaseKey": {S: aws.String(shardID)},
|
"leaseKey": {S: aws.String(leaseKey)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ type KinesisClient interface {
|
||||||
ListShards(*kinesis.ListShardsInput) (*kinesis.ListShardsOutput, error)
|
ListShards(*kinesis.ListShardsInput) (*kinesis.ListShardsOutput, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kinesis is a convenience struct that includes streamname and client
|
||||||
type Kinesis struct {
|
type Kinesis struct {
|
||||||
client KinesisClient
|
client KinesisClient
|
||||||
streamName string
|
streamName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListAllShards pulls a list of shard IDs from the kinesis api
|
// ListAllShards pulls a list of shard IDs from the kinesis api
|
||||||
|
// this could also be used by broker.go or any other future "group" implementation that needs to get the shards.
|
||||||
func (k Kinesis) ListAllShards() ([]string, error) {
|
func (k Kinesis) ListAllShards() ([]string, error) {
|
||||||
var ss []string
|
var ss []string
|
||||||
var listShardsInput = &kinesis.ListShardsInput{
|
var listShardsInput = &kinesis.ListShardsInput{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue