oklahomer/go-sarah

View on GitHub
status.go

Summary

Maintainability
A
0 mins
Test Coverage
package sarah

import (
    "errors"
    "github.com/oklahomer/go-kasumi/logger"
    "sync"
)

var runnerStatus = &status{}

// ErrRunnerAlreadyRunning indicates that Run is already called and the process is running.
// The second or later initiations are prevented by returning this error so the initially activated process is protected.
var ErrRunnerAlreadyRunning = errors.New("go-sarah's process is already running")

// CurrentStatus returns the current status of go-sarah.
// This can still be called even when Run is not called, yet.
// So developers can safely run two different goroutines:
//
//   - One that sets up the bot configuration and calls Run.
//   - Another that periodically calls CurrentStatus and monitors status.
//     When Status.Running is false and Status.Bots field is empty, then the bot is not initiated yet.
func CurrentStatus() Status {
    return runnerStatus.snapshot()
}

// Status represents the current status of Sarah and all registered Bots.
type Status struct {
    // Running indicates if Sarah is currently "running."
    // Sarah is considered running when Run is called and at least one of its belonging Bot is actively running.
    Running bool

    // Bots holds a list of BotStatus values where each value represents its corresponding Bot's status.
    Bots []BotStatus
}

// BotStatus represents the current status of a Bot.
type BotStatus struct {
    // Type represents a BotType the corresponding Bot.BotType returns.
    Type BotType

    // Running indicates if the Bot is currently "running."
    // The Bot is considered running when Bot.Run is already called and its process is context.Context is not yet canceled.
    // When this returns false, the state is final and the Bot is never recovered unless the process is rebooted.
    // In other words, a Bot is "running" even if the connection with the chat service is unstable and recovery is in progress.
    Running bool
}

type status struct {
    bots     []*botStatus
    finished chan struct{}
    mutex    sync.RWMutex
}

func (s *status) running() bool {
    s.mutex.RLock()
    defer s.mutex.RUnlock()

    finished := s.finished
    if finished == nil {
        // This status instance is created but Run is not called yet.
        // This channel field is populated when status.start is called via Run.
        return false
    }

    select {
    case <-finished:
        return false

    default:
        return true

    }
}

func (s *status) start() error {
    s.mutex.Lock()
    defer s.mutex.Unlock()

    if s.finished != nil {
        return ErrRunnerAlreadyRunning
    }

    s.finished = make(chan struct{})
    return nil
}

func (s *status) addBot(bot Bot) {
    s.mutex.Lock()
    defer s.mutex.Unlock()

    botStatus := &botStatus{
        botType:  bot.BotType(),
        finished: make(chan struct{}),
    }
    s.bots = append(s.bots, botStatus)
}

func (s *status) stopBot(bot Bot) {
    s.mutex.RLock()
    defer s.mutex.RUnlock()

    for _, bs := range s.bots {
        if bs.botType == bot.BotType() {
            bs.stop()
        }
    }
}

func (s *status) snapshot() Status {
    s.mutex.RLock()
    defer s.mutex.RUnlock()

    var bots []BotStatus
    for _, botStatus := range s.bots {
        bs := BotStatus{
            Type:    botStatus.botType,
            Running: botStatus.running(),
        }
        bots = append(bots, bs)
    }
    return Status{
        Running: s.running(),
        Bots:    bots,
    }
}

func (s *status) stop() {
    defer func() {
        if recover() != nil {
            // O.K.
            // Comes here when channel is already closed.
            // This method is not expected to be called multiple times,
            // but recover here to avoid panic.
            logger.Warn("Multiple status.stop() calls occurred.")
        }
    }()

    close(s.finished)
}

type botStatus struct {
    botType  BotType
    finished chan struct{}
}

func (bs *botStatus) running() bool {
    select {
    case <-bs.finished:
        return false

    default:
        return true

    }
}

func (bs *botStatus) stop() {
    defer func() {
        if recover() != nil {
            // O.K.
            // Comes here when channel is already closed.
            // This method is not expected to be called multiple times,
            // but recover here to avoid panic.
            logger.Warnf("Multiple botStatus.stop() calls for %s occurred.", bs.botType)
        }
    }()

    close(bs.finished)
}