2023-04-10 15:36:59 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2018-03-08 23:08:23 +00:00
|
|
|
package drainer
|
2018-03-03 01:15:38 +00:00
|
|
|
|
|
|
|
import (
|
2023-03-23 18:07:09 +00:00
|
|
|
"context"
|
2018-03-03 01:15:38 +00:00
|
|
|
"sync"
|
2023-03-23 18:07:09 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
2018-03-03 01:15:38 +00:00
|
|
|
|
2023-03-23 18:07:09 +00:00
|
|
|
"golang.org/x/time/rate"
|
|
|
|
|
|
|
|
"github.com/hashicorp/nomad/helper/testlog"
|
|
|
|
"github.com/hashicorp/nomad/nomad/state"
|
2018-03-03 01:15:38 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
|
|
)
|
|
|
|
|
2023-03-23 18:07:09 +00:00
|
|
|
// This file contains helpers for testing. The raft shims make it hard to test
|
|
|
|
// the whole package behavior of the drainer. See also nomad/drainer_int_test.go
|
|
|
|
// for integration tests.
|
2018-03-03 01:15:38 +00:00
|
|
|
|
2023-03-23 18:07:09 +00:00
|
|
|
type MockJobWatcher struct {
|
|
|
|
drainCh chan *DrainRequest
|
|
|
|
migratedCh chan []*structs.Allocation
|
|
|
|
jobs map[structs.NamespacedID]struct{}
|
2018-03-03 01:15:38 +00:00
|
|
|
sync.Mutex
|
|
|
|
}
|
|
|
|
|
2023-03-23 18:07:09 +00:00
|
|
|
// RegisterJobs marks the job as being watched
|
|
|
|
func (m *MockJobWatcher) RegisterJobs(jobs []structs.NamespacedID) {
|
|
|
|
m.Lock()
|
|
|
|
defer m.Unlock()
|
|
|
|
for _, job := range jobs {
|
|
|
|
m.jobs[job] = struct{}{}
|
2018-03-03 01:15:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-23 18:07:09 +00:00
|
|
|
// Drain returns the DrainRequest channel. Tests can send on this channel to
|
|
|
|
// simulate steps through the NodeDrainer watch loop. (Sending on this channel
|
|
|
|
// will block anywhere else.)
|
|
|
|
func (m *MockJobWatcher) Drain() <-chan *DrainRequest {
|
|
|
|
return m.drainCh
|
2018-03-03 01:15:38 +00:00
|
|
|
}
|
|
|
|
|
2023-03-23 18:07:09 +00:00
|
|
|
// Migrated returns the channel of migrated allocations. Tests can send on this
|
|
|
|
// channel to simulate steps through the NodeDrainer watch loop. (Sending on
|
|
|
|
// this channel will block anywhere else.)
|
|
|
|
func (m *MockJobWatcher) Migrated() <-chan []*structs.Allocation {
|
|
|
|
return m.migratedCh
|
2018-03-03 01:15:38 +00:00
|
|
|
}
|
|
|
|
|
2023-03-23 18:07:09 +00:00
|
|
|
type MockDeadlineNotifier struct {
|
|
|
|
expiredCh <-chan []string
|
|
|
|
nodes map[string]struct{}
|
|
|
|
sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
// NextBatch returns the channel of expired nodes. Tests can send on this
|
|
|
|
// channel to simulate timer events in the NodeDrainer watch loop. (Sending on
|
|
|
|
// this channel will block anywhere else.)
|
|
|
|
func (m *MockDeadlineNotifier) NextBatch() <-chan []string {
|
|
|
|
return m.expiredCh
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove removes the given node from being tracked for a deadline.
|
|
|
|
func (m *MockDeadlineNotifier) Remove(nodeID string) {
|
2018-03-03 01:15:38 +00:00
|
|
|
m.Lock()
|
|
|
|
defer m.Unlock()
|
2023-03-23 18:07:09 +00:00
|
|
|
delete(m.nodes, nodeID)
|
2018-03-03 01:15:38 +00:00
|
|
|
}
|
2019-05-21 18:35:23 +00:00
|
|
|
|
2023-03-23 18:07:09 +00:00
|
|
|
// Watch marks the node as being watched; this mock throws out the timer in lieu
|
|
|
|
// of manully sending on the channel to avoid racy tests.
|
|
|
|
func (m *MockDeadlineNotifier) Watch(nodeID string, _ time.Time) {
|
2019-05-21 18:35:23 +00:00
|
|
|
m.Lock()
|
|
|
|
defer m.Unlock()
|
2023-03-23 18:07:09 +00:00
|
|
|
m.nodes[nodeID] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
type MockRaftApplierShim struct {
|
|
|
|
lock sync.Mutex
|
|
|
|
state *state.StateStore
|
|
|
|
}
|
|
|
|
|
|
|
|
// AllocUpdateDesiredTransition mocks a write to raft as a state store update
|
|
|
|
func (m *MockRaftApplierShim) AllocUpdateDesiredTransition(
|
|
|
|
allocs map[string]*structs.DesiredTransition, evals []*structs.Evaluation) (uint64, error) {
|
|
|
|
|
|
|
|
m.lock.Lock()
|
|
|
|
defer m.lock.Unlock()
|
|
|
|
|
|
|
|
index, _ := m.state.LatestIndex()
|
|
|
|
index++
|
|
|
|
err := m.state.UpdateAllocsDesiredTransitions(structs.MsgTypeTestSetup, index, allocs, evals)
|
|
|
|
return index, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// NodesDrainComplete mocks a write to raft as a state store update
|
|
|
|
func (m *MockRaftApplierShim) NodesDrainComplete(
|
|
|
|
nodes []string, event *structs.NodeEvent) (uint64, error) {
|
|
|
|
|
|
|
|
m.lock.Lock()
|
|
|
|
defer m.lock.Unlock()
|
|
|
|
|
|
|
|
index, _ := m.state.LatestIndex()
|
|
|
|
index++
|
|
|
|
|
|
|
|
updates := make(map[string]*structs.DrainUpdate, len(nodes))
|
|
|
|
nodeEvents := make(map[string]*structs.NodeEvent, len(nodes))
|
|
|
|
update := &structs.DrainUpdate{}
|
|
|
|
for _, node := range nodes {
|
|
|
|
updates[node] = update
|
|
|
|
if event != nil {
|
|
|
|
nodeEvents[node] = event
|
|
|
|
}
|
|
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
|
|
|
|
|
|
err := m.state.BatchUpdateNodeDrain(structs.MsgTypeTestSetup, index, now,
|
|
|
|
updates, nodeEvents)
|
|
|
|
|
|
|
|
return index, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func testNodeDrainWatcher(t *testing.T) (*nodeDrainWatcher, *state.StateStore, *NodeDrainer) {
|
|
|
|
t.Helper()
|
|
|
|
store := state.TestStateStore(t)
|
|
|
|
limiter := rate.NewLimiter(100.0, 100)
|
|
|
|
logger := testlog.HCLogger(t)
|
|
|
|
|
|
|
|
drainer := &NodeDrainer{
|
|
|
|
enabled: false,
|
|
|
|
logger: logger,
|
|
|
|
nodes: map[string]*drainingNode{},
|
|
|
|
jobWatcher: &MockJobWatcher{jobs: map[structs.NamespacedID]struct{}{}},
|
|
|
|
deadlineNotifier: &MockDeadlineNotifier{nodes: map[string]struct{}{}},
|
|
|
|
state: store,
|
|
|
|
queryLimiter: limiter,
|
|
|
|
raft: &MockRaftApplierShim{state: store},
|
|
|
|
batcher: allocMigrateBatcher{},
|
|
|
|
}
|
2019-05-21 18:35:23 +00:00
|
|
|
|
2023-03-23 18:07:09 +00:00
|
|
|
w := NewNodeDrainWatcher(context.Background(), limiter, store, logger, drainer)
|
|
|
|
drainer.nodeWatcher = w
|
|
|
|
return w, store, drainer
|
2019-05-21 18:35:23 +00:00
|
|
|
}
|