package worknode
import (
"fleetmanager/db/dao"
"fleetmanager/db/redis"
"fleetmanager/logger"
"fleetmanager/setting"
"fleetmanager/utils/wait"
"fleetmanager/workflow"
"regexp"
"time"
"github.com/google/uuid"
)
const (
DefaultTakeOverTaskInterval = 5 * 60
DefaultDeadWorkNodeCheckTaskInterval = 5 * 60
DefaultHeartBeatTaskInterval = 1 * 60
MaxDeadMinites = 10
FormatDateTime = "2006-01-02 15:04:05"
TimeFormatPattern = `^20[\d]{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`
)
var WorkNodeId string
var TLogger *logger.FMLogger
func Init() error {
u, _ := uuid.NewUUID()
WorkNodeId = u.String()
wn := &dao.WorkNode{
Id: WorkNodeId,
State: dao.WorkNodeStateRunning,
}
TLogger = logger.R.WithField(logger.WorkNodeId, WorkNodeId)
return dao.InsertWorkNode(wn)
}
func StartWorkNodeTakeOverPeriodTask(stopCh <-chan struct{}) {
go wait.Until(func() {
TLogger.Info("TakeOverWorkNodeTask start, WorkNode: %s", WorkNodeId)
if err := takeOverWorkNode(); err != nil {
logger.R.Warn("work node take over error: %v", err)
return
}
TLogger.Debug("TakeOverWorkNodeTask finished, WorkNode: %s", WorkNodeId)
}, time.Duration(DefaultTakeOverTaskInterval)*time.Second, stopCh)
}
func StartWorkNodeHeartBeatPeriodTask(stopCh <-chan struct{}) {
go wait.Until(func() {
TLogger.Debug("HeartBeatTask start, WorkNode: %s ", WorkNodeId)
if err := heartBeat(); err != nil {
logger.R.Warn("heart beat error: %v", err)
return
}
TLogger.Debug("HeartBeatTask start, WorkNode: %s ", WorkNodeId)
}, time.Duration(DefaultHeartBeatTaskInterval)*time.Second, stopCh)
go (func() {
if err := stopWorkNode(stopCh); err != nil {
TLogger.Warn("stop work node error: %v", err)
}
})()
}
func StartDeadWorkNodeUpdatePeriodTask(stopCh <-chan struct{}) {
go wait.Until(func() {
deadTime := time.Now().UTC().Add(-time.Minute * MaxDeadMinites)
deadTimeString := deadTime.Format(FormatDateTime)
TLogger.Info("DeadWorkNodeUpdateTask start, deadTime: %v, deadTime: %s", deadTime, deadTimeString)
if !checkDeadTime(deadTimeString) {
TLogger.Warn("DeadWorkNodeUpdateTask check deadTime: %s error", deadTimeString)
return
}
logDeadWorkNode(deadTimeString)
updated, err := deadWorkNodeUpdate(deadTimeString)
if err != nil {
TLogger.Warn("update dead work node error: %v", err)
return
}
TLogger.Debug("DeadWorkNodeUpdateTask finished, %d are updated", updated)
}, time.Duration(DefaultDeadWorkNodeCheckTaskInterval)*time.Second, stopCh)
}
func checkDeadTime(deadTime string) bool {
isOk, err := regexp.MatchString(TimeFormatPattern, deadTime)
if err != nil {
TLogger.Error("CheckDeadTime error: %v", err)
return false
}
return isOk
}
func takeOverWorkNode() error {
f := dao.Filters{"State__in": []string{dao.WorkNodeStateTerminated, dao.WorkNodeStateError}}
wns, err := dao.GetWorkNodes(f)
if err != nil {
return err
}
TLogger.Warn("%d work nodes need to be take over, wns: %s", len(wns), wns)
for _, wn := range wns {
updated, err := dao.TakeOverWorkNode(wn.Id, wn.State, WorkNodeId)
if err != nil {
TLogger.Warn("work node %s try to take over wn %s error: %v", WorkNodeId, wn, err)
} else {
if updated != 1 {
TLogger.Warn("work node %s take over wn %s failed by other node", WorkNodeId, wn)
continue
}
TLogger.Debug("Try to take over wfs, WorkNode:%s", WorkNodeId)
go (func() {
if err := startTakeOverWorkFlow(wn.Id); err != nil {
logger.R.Warn("workflow take over error: %v", err)
}
})()
break
}
}
return nil
}
func updateWorkFlowNodeInfo(wfs []dao.Workflow, originWorkNodeId string) error {
wfIds := getWorkflowIds(wfs)
if err := dao.TakeOverWorkFlow(wfIds, WorkNodeId); err != nil {
return err
}
if err := dao.UpdateWorkNodeState(originWorkNodeId, dao.WorkNodeStateFinished); err != nil {
TLogger.Warn("update work node state to finished db error: %v", err)
}
return nil
}
func getWorkflowIds(wfs []dao.Workflow) []string {
wfIds := make([]string, 0)
for _, wf := range wfs {
wfIds = append(wfIds, wf.Id)
}
return wfIds
}
func startTakeOverWorkFlow(workNodeId string) error {
filter := dao.Filters{
"State__in": []string{dao.WorkflowStateRollbacking, dao.WorkflowStateRunning, dao.WorkflowStateCreate},
"WorkNodeId": workNodeId,
}
wfs, err := dao.GetAllWorkflows(filter)
if err != nil {
return err
}
TLogger.Info("Try to take over wfs:%+v, WorkNode:%s", wfs, WorkNodeId)
if err := updateWorkFlowNodeInfo(wfs, workNodeId); err != nil {
return err
}
for _, wf := range wfs {
tmp, err := workflow.LoadWorkflow(wf.Id)
if err != nil {
TLogger.Error("load workflow error: %v, try to update to error", err)
if err = workflow.StartWorkflowFailed(&wf); err != nil {
TLogger.Error("change workflow to error failed error: %v, try to ignore", err)
}
continue
}
tmp.Run()
}
return nil
}
func StartElection(stopCh <-chan struct{}) {
go wait.Until(func() {
if !getDistributedLock() {
return
}
fss := dao.GetFleetServerSessionStorage()
logger.R.Info("[express data cleaner] start to clean fleet server session")
fss.CleanFleetServerSession(setting.FleetServerSessionBackupDays)
}, time.Duration(TryOrLeaseInterval)*time.Second, stopCh)
}
func deadWorkNodeUpdate(deadTime string) (int64, error) {
return dao.UpdateDeadWorkNodeState(deadTime)
}
func logDeadWorkNode(deadTime string) {
wns, err := dao.QueryDeadWorkNode(deadTime)
if err != nil {
TLogger.Error("QueryDeadWorkNode db error: %v", err)
return
}
TLogger.Info("QueryDeadWorkNode work node: %s", wns)
}
func heartBeat() error {
return dao.HeartBeat(WorkNodeId)
}
func stopWorkNode(stopCh <-chan struct{}) error {
for {
select {
case <-stopCh:
return dao.TerminateWorkNode(WorkNodeId)
}
}
}
func getDistributedLock() bool {
lockKey := DistributedLockKey
lockValue := WorkNodeId
TLogger.Info("work node [%s] trying to get distributed lock", lockValue)
RedisClient := redis.GetRedisClient()
defer redis.CloseRedisClient(RedisClient)
_, err := RedisClient.SetNX(lockKey, lockValue, time.Second*LockExpiredSecond).Result()
if err != nil {
TLogger.Error("setnx to redis error:%+v", err)
return false
}
curValue, err := RedisClient.Get(lockKey).Result()
if err != nil {
TLogger.Error("get redis value error:%+v", err)
return false
}
if curValue != lockValue {
TLogger.Info("Distributed lock holder is work node %s, but local node is %s", curValue, lockValue)
return false
}
TLogger.Info("work node %s get distributed lock success", lockValue)
extendLockTime(lockKey)
return true
}
func extendLockTime(lockKey string) {
RedisClient := redis.GetRedisClient()
defer redis.CloseRedisClient(RedisClient)
_, err := RedisClient.Expire(lockKey, time.Second*LockExpiredSecond).Result()
if err != nil {
TLogger.Error("set expire time of lock to redis error:%+v", err)
return
}
}