2019-05-20 01:51:40 +00:00
package consumer
import (
"context"
"fmt"
"sync"
"time"
"github.com/twinj/uuid"
2019-05-27 16:46:44 +00:00
"github.com/harlow/kinesis-consumer/storage"
2019-05-20 01:51:40 +00:00
)
2019-05-20 19:47:55 +00:00
// TODO change logging to actual logger
2019-05-27 16:46:44 +00:00
// ConsumerGroupCheckpoint is a simple struct for managing the consumergroup and heartbeat of updating leases
2019-05-20 01:51:40 +00:00
type ConsumerGroupCheckpoint struct {
HeartBeatDuration time . Duration
2019-05-27 16:46:44 +00:00
LeaseDuration time . Duration
2019-05-20 01:51:40 +00:00
OwnerID string
2019-05-27 16:46:44 +00:00
Storage Storage
2019-05-20 01:51:40 +00:00
done chan struct { }
2019-05-27 16:46:44 +00:00
kinesis Kinesis
leasesMutex * sync . Mutex
leases map [ string ] * storage . Lease // Initially, this will only be one
2019-05-20 01:51:40 +00:00
}
func ( cgc ConsumerGroupCheckpoint ) Get ( shardID string ) ( string , error ) {
2019-05-27 16:46:44 +00:00
return cgc . leases [ shardID ] . Checkpoint , nil
2019-05-20 01:51:40 +00:00
}
func ( cgc ConsumerGroupCheckpoint ) Set ( shardID , sequenceNumber string ) error {
2019-05-27 16:46:44 +00:00
cgc . leasesMutex . Lock ( )
defer cgc . leasesMutex . Unlock ( )
2019-05-20 01:51:40 +00:00
2019-05-27 16:46:44 +00:00
cgc . leases [ shardID ] . Checkpoint = sequenceNumber
2019-05-20 01:51:40 +00:00
return nil
}
func NewConsumerGroupCheckpoint (
2019-05-27 16:46:44 +00:00
Storage Storage ,
2019-05-20 01:51:40 +00:00
kinesis Kinesis ,
leaseDuration time . Duration ,
heartBeatDuration time . Duration ) * ConsumerGroupCheckpoint {
return & ConsumerGroupCheckpoint {
HeartBeatDuration : heartBeatDuration ,
2019-05-27 16:46:44 +00:00
LeaseDuration : leaseDuration ,
2019-05-20 01:51:40 +00:00
OwnerID : uuid . NewV4 ( ) . String ( ) , // generated owner id
2019-05-27 16:46:44 +00:00
Storage : Storage ,
2019-05-20 01:51:40 +00:00
done : make ( chan struct { } ) ,
2019-05-27 16:46:44 +00:00
kinesis : kinesis ,
leasesMutex : & sync . Mutex { } ,
leases : make ( map [ string ] * storage . Lease , 1 ) ,
2019-05-20 01:51:40 +00:00
}
}
// Start is a blocking call that will attempt to acquire a lease on every tick of leaseDuration
2019-05-20 19:47:55 +00:00
// If a lease is successfully acquired it will be added to the channel otherwise it will continue to retry
2019-05-20 01:51:40 +00:00
func ( cgc ConsumerGroupCheckpoint ) Start ( ctx context . Context , shardc chan string ) {
fmt . Printf ( "Starting ConsumerGroupCheckpoint for Consumer %s \n" , cgc . OwnerID )
tick := time . NewTicker ( cgc . LeaseDuration )
defer tick . Stop ( )
2019-05-27 16:46:44 +00:00
var currentLeases map [ string ] storage . Lease
var previousLeases map [ string ] storage . Lease
2019-05-20 01:51:40 +00:00
for {
select {
case <- tick . C :
2019-05-27 16:46:44 +00:00
if len ( cgc . leases ) > 0 { // only do anything if there are no current leases
continue
}
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
}
2019-05-20 01:51:40 +00:00
2019-05-27 16:46:44 +00:00
lease := cgc . CreateOrGetExpiredLease ( currentLeases , previousLeases )
previousLeases = currentLeases
if lease == nil || lease . LeaseKey == "" {
continue // lease wasn't acquired continue
2019-05-20 01:51:40 +00:00
}
2019-05-27 16:46:44 +00:00
// 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
2019-05-20 01:51:40 +00:00
}
}
}
// 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.
2019-05-27 16:46:44 +00:00
func ( cgc ConsumerGroupCheckpoint ) CreateOrGetExpiredLease ( currentLeases map [ string ] storage . Lease , previousLeases map [ string ] storage . Lease ) * storage . Lease {
cgc . leasesMutex . Lock ( )
defer cgc . leasesMutex . Unlock ( )
2019-05-20 01:51:40 +00:00
listOfShards , err := cgc . kinesis . ListAllShards ( )
if err != nil {
//TODO log error
// TODO return error
}
shardIDsNotYetTaken := getShardIDsNotLeased ( listOfShards , currentLeases )
2019-05-27 16:46:44 +00:00
var currentLease * storage . Lease
2019-05-20 01:51:40 +00:00
if len ( shardIDsNotYetTaken ) > 0 {
fmt . Println ( "Grabbing lease from shardIDs not taken" )
shardId := shardIDsNotYetTaken [ 0 ] //grab the first one //TODO randomize
2019-05-27 16:46:44 +00:00
tempLease := storage . Lease {
2019-05-20 01:51:40 +00:00
LeaseKey : shardId ,
Checkpoint : "0" , // we don't have this yet
LeaseCounter : 1 ,
LeaseOwner : cgc . OwnerID ,
HeartbeatID : uuid . NewV4 ( ) . String ( ) ,
LastUpdateTime : time . Now ( ) ,
}
if err := cgc . Storage . CreateLease ( tempLease ) ; err != nil {
fmt . Printf ( "Error is happening create the lease" )
} else {
//success
if isLeaseInvalidOrChanged ( cgc , tempLease ) {
//Lease must have been acquired by another worker
return nil
}
fmt . Printf ( "Successfully Acquired lease %v" , tempLease )
currentLease = & tempLease //successfully acquired the lease
}
}
if currentLease == nil || currentLease . LeaseKey == "" && len ( previousLeases ) > 0 {
for _ , lease := range currentLeases {
// 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
2019-05-27 16:46:44 +00:00
updatedLease := storage . Lease {
2019-05-20 19:47:55 +00:00
LeaseKey : lease . LeaseKey ,
2019-05-20 01:51:40 +00:00
Checkpoint : lease . Checkpoint ,
2019-05-20 19:47:55 +00:00
LeaseCounter : lease . LeaseCounter + 1 ,
2019-05-20 01:51:40 +00:00
LeaseOwner : cgc . OwnerID ,
HeartbeatID : uuid . NewV4 ( ) . String ( ) ,
LastUpdateTime : time . Now ( ) ,
2019-05-20 19:47:55 +00:00
}
if err := cgc . Storage . UpdateLease ( lease , updatedLease ) ; err != nil {
2019-05-20 01:51:40 +00:00
fmt . Printf ( "Error is happening updating the lease" )
} else {
if isLeaseInvalidOrChanged ( cgc , lease ) {
return nil //should not be a valid lease at this point
}
fmt . Printf ( "Successfully Acquired Expired lease %v\n" , lease )
2019-05-20 19:47:55 +00:00
currentLease = & updatedLease //successfully acquired the lease
2019-05-20 01:51:40 +00:00
break
}
}
}
}
return currentLease
}
2019-05-20 19:47:55 +00:00
// heartbeatLoop should constantly update the lease that is provided
2019-05-27 16:46:44 +00:00
func ( cgc ConsumerGroupCheckpoint ) heartbeatLoop ( lease * storage . Lease ) {
cgc . leasesMutex . Lock ( )
defer cgc . leasesMutex . Unlock ( )
2019-05-20 01:51:40 +00:00
fmt . Println ( "Starting heartbeat loop" )
ticker := time . NewTicker ( cgc . HeartBeatDuration )
defer ticker . Stop ( )
defer close ( cgc . done )
for {
select {
case <- ticker . C :
2019-05-20 19:47:55 +00:00
if isLeaseInvalidOrChanged ( cgc , * lease ) || lease . IsExpired ( cgc . LeaseDuration ) {
2019-05-27 16:46:44 +00:00
delete ( cgc . leases , lease . LeaseKey )
2019-05-20 01:51:40 +00:00
}
2019-05-27 16:46:44 +00:00
updatedLease := storage . Lease {
2019-05-20 19:47:55 +00:00
LeaseKey : lease . LeaseKey ,
2019-05-20 01:51:40 +00:00
Checkpoint : lease . Checkpoint ,
LeaseCounter : lease . LeaseCounter ,
LeaseOwner : lease . LeaseOwner ,
2019-05-20 19:47:55 +00:00
HeartbeatID : uuid . NewV4 ( ) . String ( ) ,
LastUpdateTime : time . Now ( ) ,
}
// TODO handle error
cgc . Storage . UpdateLease ( * lease , updatedLease )
lease = & updatedLease
2019-05-20 01:51:40 +00:00
fmt . Printf ( "Sucessfully updated lease %v\n" , lease )
case <- cgc . done :
return
}
}
}
// isLeaseInvalidOrChanged checks to see if the lease changed
2019-05-27 16:46:44 +00:00
func isLeaseInvalidOrChanged ( cgc ConsumerGroupCheckpoint , lease storage . Lease ) bool {
2019-05-20 01:51:40 +00:00
leaseCurrent , _ := cgc . Storage . GetLease ( lease . LeaseKey )
if lease . LeaseKey != leaseCurrent . LeaseKey || cgc . OwnerID != leaseCurrent . LeaseOwner || leaseCurrent . LeaseCounter != lease . LeaseCounter {
fmt . Printf ( "The lease changed\n" )
return true
}
return false
}
// getShardIDsNotLeased finds any open shards where there are no leases yet created
2019-05-27 16:46:44 +00:00
func getShardIDsNotLeased ( shardIDs [ ] string , leases map [ string ] storage . Lease ) [ ] string {
2019-05-20 01:51:40 +00:00
var shardIDsNotUsed [ ] string
for _ , shardID := range shardIDs {
if _ , ok := leases [ shardID ] ; ! ok {
shardIDsNotUsed = append ( shardIDsNotUsed , shardID )
}
}
return shardIDsNotUsed
}