forked from rook/rook
236 lines
6.2 KiB
Go
236 lines
6.2 KiB
Go
/*
|
|
Copyright 2016 The Rook Authors. All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
package clusterd
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
etcd "github.com/coreos/etcd/client"
|
|
|
|
"github.com/rook/rook/pkg/clusterd/inventory"
|
|
"github.com/rook/rook/pkg/util"
|
|
)
|
|
|
|
const (
|
|
refreshDelaySeconds = 5
|
|
periodicRefreshMinutes = 10
|
|
)
|
|
|
|
var (
|
|
refreshDelayInterval = time.Second * refreshDelaySeconds
|
|
periodicRefreshInterval = time.Minute * periodicRefreshMinutes
|
|
)
|
|
|
|
// RefreshEvent type
|
|
type RefreshEvent struct {
|
|
Context *Context
|
|
NodesAdded *util.Set
|
|
NodesRemoved *util.Set
|
|
NodesChanged *util.Set
|
|
NodesUnhealthy map[string]*UnhealthyNode
|
|
}
|
|
|
|
// UnhealthyNode type
|
|
type UnhealthyNode struct {
|
|
ID string
|
|
AgeSeconds int
|
|
}
|
|
|
|
// RefreshKey type
|
|
type RefreshKey struct {
|
|
Path string
|
|
Triggered func(response *etcd.Response, refresher *ClusterRefresher)
|
|
}
|
|
|
|
// ClusterRefresher type
|
|
type ClusterRefresher struct {
|
|
nextEvent *RefreshEvent
|
|
leader *servicesLeader
|
|
refreshMutex sync.RWMutex
|
|
changes bool
|
|
closed chan (bool)
|
|
triggered chan (bool)
|
|
}
|
|
|
|
// Create a new cluster refresher
|
|
func NewClusterRefresher() *ClusterRefresher {
|
|
return &ClusterRefresher{
|
|
nextEvent: NewRefreshEvent(),
|
|
closed: make(chan bool),
|
|
triggered: make(chan bool),
|
|
}
|
|
}
|
|
|
|
// Create a new refresh event
|
|
func NewRefreshEvent() *RefreshEvent {
|
|
return &RefreshEvent{
|
|
NodesAdded: util.NewSet(),
|
|
NodesRemoved: util.NewSet(),
|
|
NodesChanged: util.NewSet(),
|
|
NodesUnhealthy: make(map[string]*UnhealthyNode),
|
|
}
|
|
}
|
|
|
|
// Start consuming refresh events
|
|
func (c *ClusterRefresher) Start() {
|
|
go c.eventLoop()
|
|
|
|
// wait for the event loop to start
|
|
<-time.After(time.Millisecond)
|
|
}
|
|
|
|
// Stop consuming refresh events
|
|
func (c *ClusterRefresher) Stop() {
|
|
c.closed <- true
|
|
}
|
|
|
|
// Trigger a general refresh of the cluster
|
|
func (c *ClusterRefresher) TriggerRefresh() bool {
|
|
c.refreshMutex.Lock()
|
|
c.changes = true
|
|
c.refreshMutex.Unlock()
|
|
|
|
return c.triggerRefresh()
|
|
}
|
|
|
|
// Trigger an event for a device being added or removed to a node
|
|
func (c *ClusterRefresher) TriggerDevicesChanged(nodeID string) bool {
|
|
c.refreshMutex.Lock()
|
|
c.changes = true
|
|
c.nextEvent.NodesChanged.Add(nodeID)
|
|
c.refreshMutex.Unlock()
|
|
|
|
return c.triggerRefresh()
|
|
}
|
|
|
|
// Trigger an event for a node being added to the cluster
|
|
func (c *ClusterRefresher) triggerNodeAdded(nodeID string) bool {
|
|
c.refreshMutex.Lock()
|
|
c.changes = true
|
|
c.nextEvent.NodesAdded.Add(nodeID)
|
|
c.refreshMutex.Unlock()
|
|
|
|
return c.triggerRefresh()
|
|
}
|
|
|
|
// Trigger an event for detecting unhealthy nodes
|
|
func (c *ClusterRefresher) triggerNodeUnhealthy(nodes []*UnhealthyNode) bool {
|
|
c.refreshMutex.Lock()
|
|
c.changes = true
|
|
for _, node := range nodes {
|
|
c.nextEvent.NodesUnhealthy[node.ID] = node
|
|
}
|
|
c.refreshMutex.Unlock()
|
|
|
|
return c.triggerRefresh()
|
|
}
|
|
|
|
// Produce a refresh event.
|
|
// The pattern is a non-blocking producer with a blocking consumer.
|
|
func (c *ClusterRefresher) triggerRefresh() bool {
|
|
|
|
// Only start the orchestration if currently elected leader
|
|
if !c.leader.parent.isLeader {
|
|
return false
|
|
}
|
|
|
|
// Trigger the refresh, but only if it is not busy.
|
|
// If the orchestrator is already busy with a refresh, the new refresh
|
|
// will be handled in processEvent() immediately after the first orchestration completes.
|
|
select {
|
|
case c.triggered <- true:
|
|
logger.Debugf("The refresh was idle when triggered")
|
|
return true
|
|
default:
|
|
logger.Debugf("The refresh was busy when triggered")
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Consume the refresh events
|
|
func (c *ClusterRefresher) eventLoop() {
|
|
for {
|
|
select {
|
|
case <-c.closed:
|
|
logger.Debugf("refresh event loop closed")
|
|
return
|
|
case <-c.triggered:
|
|
c.processEvent()
|
|
case <-time.After(periodicRefreshInterval):
|
|
// periodically check if any refresh is needed
|
|
if c.refreshNeeded() {
|
|
c.processEvent()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process an individual refresh. If another refresh is triggered after this one has already started,
|
|
// process the new refresh immediately after the first one completes.
|
|
func (c *ClusterRefresher) processEvent() {
|
|
// Wait a few seconds in case multiple machines are coming online at the same time.
|
|
logger.Infof("triggering a refresh in %.1fs", refreshDelayInterval.Seconds())
|
|
<-time.After(refreshDelayInterval)
|
|
|
|
// Double check that we're still the leader
|
|
if !c.leader.parent.isLeader {
|
|
logger.Infof("not leader anymore. skipping refresh.")
|
|
return
|
|
}
|
|
|
|
// Process each of the refresh events until no more are raised during the current refresh
|
|
for {
|
|
c.refreshMutex.Lock()
|
|
c.changes = false
|
|
event := c.nextEvent
|
|
c.nextEvent = NewRefreshEvent()
|
|
c.refreshMutex.Unlock()
|
|
|
|
// Update the node inventory for the event
|
|
var err error
|
|
event.Context = copyContext(c.leader.context)
|
|
event.Context.Inventory, err = inventory.LoadDiscoveredNodes(event.Context.EtcdClient)
|
|
if err != nil {
|
|
logger.Errorf("failed to load node info. err=%v", err)
|
|
return
|
|
}
|
|
|
|
// Sequentially update the services. We may consider running them in parallel in the future.
|
|
for _, s := range event.Context.Services {
|
|
s.Leader.HandleRefresh(event)
|
|
}
|
|
|
|
// stop refreshing if no more refresh events were raised during this refresh
|
|
if !c.refreshNeeded() {
|
|
logger.Infof("Done with the orchestration. Waiting for the next event signal.")
|
|
break
|
|
}
|
|
|
|
// There is a small window where a refresh event could be raised and we will miss processing
|
|
// the refresh since we are not listening on the triggered channel yet. This will be caught
|
|
// by the periodic check for refreshNeeded() in the consumer select statement.
|
|
logger.Infof("triggering events that were queued during an active orchestration")
|
|
}
|
|
}
|
|
|
|
// check if a refresh is needed
|
|
func (c *ClusterRefresher) refreshNeeded() bool {
|
|
c.refreshMutex.Lock()
|
|
defer c.refreshMutex.Unlock()
|
|
return c.changes
|
|
}
|