added initial trust metric test routines

This commit is contained in:
caffix
2017-11-14 18:26:06 -05:00
parent 54c25ccbf5
commit 687834c99e
5 changed files with 488 additions and 173 deletions
+103 -100
View File
@@ -5,12 +5,12 @@ package trust
import (
"encoding/json"
"io/ioutil"
"math"
"sync"
"time"
cmn "github.com/tendermint/tmlibs/common"
dbm "github.com/tendermint/tmlibs/db"
)
// TrustMetricStore - Manages all trust metrics for peers
@@ -23,19 +23,19 @@ type TrustMetricStore struct {
// Mutex that protects the map and history data file
mtx sync.Mutex
// The file path where peer trust metric history data will be stored
filePath string
// The db where peer trust metric history data will be stored
db dbm.DB
// This configuration will be used when creating new TrustMetrics
config *TrustMetricConfig
config TrustMetricConfig
}
// NewTrustMetricStore returns a store that optionally saves data to
// the file path and uses the optional config when creating new trust metrics
func NewTrustMetricStore(filePath string, tmc *TrustMetricConfig) *TrustMetricStore {
// NewTrustMetricStore returns a store that saves data to the DB
// and uses the config when creating new trust metrics
func NewTrustMetricStore(db dbm.DB, tmc TrustMetricConfig) *TrustMetricStore {
tms := &TrustMetricStore{
peerMetrics: make(map[string]*TrustMetric),
filePath: filePath,
db: db,
config: tmc,
}
@@ -49,7 +49,8 @@ func (tms *TrustMetricStore) OnStart() error {
tms.mtx.Lock()
defer tms.mtx.Unlock()
tms.loadFromFile()
tms.loadFromDB()
return nil
}
@@ -63,10 +64,18 @@ func (tms *TrustMetricStore) OnStop() {
tm.Stop()
}
tms.saveToFile()
tms.saveToDB()
tms.BaseService.OnStop()
}
// Size returns the number of entries in the trust metric store
func (tms *TrustMetricStore) Size() int {
tms.mtx.Lock()
defer tms.mtx.Unlock()
return tms.size()
}
// GetPeerTrustMetric returns a trust metric by peer key
func (tms *TrustMetricStore) GetPeerTrustMetric(key string) *TrustMetric {
tms.mtx.Lock()
@@ -95,41 +104,43 @@ func (tms *TrustMetricStore) PeerDisconnected(key string) {
}
}
/* Private methods */
// size returns the number of entries in the store without acquiring the mutex
func (tms *TrustMetricStore) size() int {
return len(tms.peerMetrics)
}
/* Loading & Saving */
/* Both of these methods assume the mutex has been acquired, since they write to the map */
var trustMetricKey = []byte("trustMetricStore")
type peerHistoryJSON struct {
NumIntervals int `json:"intervals"`
History []float64 `json:"history"`
}
// Loads the history data for the Peer identified by key from the store file.
// Loads the history data for the Peer identified by key from the store DB.
// cmn.Panics if file is corrupt
func (tms *TrustMetricStore) loadFromFile() bool {
// Check that a file has been configured for use
if tms.filePath == "" {
// The trust metric store can operate without the file
func (tms *TrustMetricStore) loadFromDB() bool {
// Obtain the history data we have so far
bytes := tms.db.Get(trustMetricKey)
if bytes == nil {
return false
}
// Obtain the history data we have so far
content, err := ioutil.ReadFile(tms.filePath)
if err != nil {
cmn.PanicCrisis(cmn.Fmt("Error reading file %s: %v", tms.filePath, err))
}
peers := make(map[string]peerHistoryJSON, 0)
err = json.Unmarshal(content, &peers)
err := json.Unmarshal(bytes, &peers)
if err != nil {
cmn.PanicCrisis(cmn.Fmt("Error decoding file %s: %v", tms.filePath, err))
cmn.PanicCrisis(cmn.Fmt("Could not unmarchal Trust Metric Store DB data: %v", err))
}
// If history data exists in the file,
// load it into trust metrics and recalc
for key, p := range peers {
tm := NewMetricWithConfig(tms.config)
if tm == nil {
continue
}
// Restore the number of time intervals we have previously tracked
if p.NumIntervals > tm.maxIntervals {
p.NumIntervals = tm.maxIntervals
@@ -149,15 +160,9 @@ func (tms *TrustMetricStore) loadFromFile() bool {
return true
}
// Saves the history data for all peers to the store file
func (tms *TrustMetricStore) saveToFile() {
// Check that a file has been configured for use
if tms.filePath == "" {
// The trust metric store can operate without the file
return
}
tms.Logger.Info("Saving TrustHistory to file", "size", len(tms.peerMetrics))
// Saves the history data for all peers to the store DB
func (tms *TrustMetricStore) saveToDB() {
tms.Logger.Info("Saving TrustHistory to DB", "size", tms.size())
peers := make(map[string]peerHistoryJSON, 0)
@@ -169,20 +174,23 @@ func (tms *TrustMetricStore) saveToFile() {
}
}
// Write all the data back to the file
b, err := json.Marshal(peers)
// Write all the data back to the DB
bytes, err := json.Marshal(peers)
if err != nil {
tms.Logger.Error("Failed to encode the TrustHistory", "err", err)
return
}
err = ioutil.WriteFile(tms.filePath, b, 0644)
if err != nil {
tms.Logger.Error("Failed to save TrustHistory to file", "err", err)
}
tms.db.SetSync(trustMetricKey, bytes)
}
//---------------------------------------------------------------------------------------
// The number of event updates that can be sent on a single metric before blocking
const defaultUpdateChanCapacity = 10
// The number of trust value requests that can be made simultaneously before blocking
const defaultRequestChanCapacity = 10
// TrustMetric - keeps track of peer reliability
// See tendermint/docs/architecture/adr-006-trust-metric.md for details
type TrustMetric struct {
@@ -216,6 +224,9 @@ type TrustMetric struct {
// The number of recorded good and bad events for the current time interval
bad, good float64
// While true, history data is not modified
paused bool
// Sending true on this channel stops tracking, while false pauses tracking
stop chan bool
@@ -238,7 +249,8 @@ type reqTrustValue struct {
Resp chan float64
}
// Pause tells the metric to pause recording data over time intervals
// Pause tells the metric to pause recording data over time intervals.
// All method calls that indicate events will unpause the metric
func (tm *TrustMetric) Pause() {
tm.stop <- false
}
@@ -299,40 +311,33 @@ type TrustMetricConfig struct {
// Each interval should be short for adapability.
// Less than 30 seconds is too sensitive,
// and greater than 5 minutes will make the metric numb
IntervalLen time.Duration
IntervalLength time.Duration
}
// DefaultConfig returns a config with values that have been tested and produce desirable results
func DefaultConfig() *TrustMetricConfig {
return &TrustMetricConfig{
func DefaultConfig() TrustMetricConfig {
return TrustMetricConfig{
ProportionalWeight: 0.4,
IntegralWeight: 0.6,
TrackingWindow: (time.Minute * 60 * 24) * 14, // 14 days.
IntervalLen: 1 * time.Minute,
IntervalLength: 1 * time.Minute,
}
}
// NewMetric returns a trust metric with the default configuration
func NewMetric() *TrustMetric {
return NewMetricWithConfig(nil)
return NewMetricWithConfig(DefaultConfig())
}
// NewMetricWithConfig returns a trust metric with a custom configuration
func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric {
var config *TrustMetricConfig
if tmc == nil {
config = DefaultConfig()
} else {
config = customConfig(tmc)
}
func NewMetricWithConfig(tmc TrustMetricConfig) *TrustMetric {
tm := new(TrustMetric)
config := customConfig(tmc)
// Setup using the configuration values
tm.proportionalWeight = config.ProportionalWeight
tm.integralWeight = config.IntegralWeight
tm.intervalLen = config.IntervalLen
tm.intervalLen = config.IntervalLength
// The maximum number of time intervals is the tracking window / interval length
tm.maxIntervals = int(config.TrackingWindow / tm.intervalLen)
// The history size will be determined by the maximum number of time intervals
@@ -340,8 +345,8 @@ func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric {
// This metric has a perfect history so far
tm.historyValue = 1.0
// Setup the channels
tm.update = make(chan *updateBadGood, 10)
tm.trustValue = make(chan *reqTrustValue, 10)
tm.update = make(chan *updateBadGood, defaultUpdateChanCapacity)
tm.trustValue = make(chan *reqTrustValue, defaultRequestChanCapacity)
tm.stop = make(chan bool, 2)
go tm.processRequests()
@@ -351,25 +356,27 @@ func NewMetricWithConfig(tmc *TrustMetricConfig) *TrustMetric {
/* Private methods */
// Ensures that all configuration elements have valid values
func customConfig(tmc *TrustMetricConfig) *TrustMetricConfig {
func customConfig(tmc TrustMetricConfig) TrustMetricConfig {
config := DefaultConfig()
// Check the config for set values, and setup appropriately
if tmc.ProportionalWeight != 0 {
if tmc.ProportionalWeight > 0 {
config.ProportionalWeight = tmc.ProportionalWeight
}
if tmc.IntegralWeight != 0 {
if tmc.IntegralWeight > 0 {
config.IntegralWeight = tmc.IntegralWeight
}
if tmc.TrackingWindow != time.Duration(0) {
if tmc.IntervalLength > time.Duration(0) {
config.IntervalLength = tmc.IntervalLength
}
if tmc.TrackingWindow > time.Duration(0) &&
tmc.TrackingWindow >= config.IntervalLength {
config.TrackingWindow = tmc.TrackingWindow
}
if tmc.IntervalLen != time.Duration(0) {
config.IntervalLen = tmc.IntervalLen
}
return config
}
@@ -480,18 +487,19 @@ func (tm *TrustMetric) calcTrustValue() float64 {
// This method is for a goroutine that handles all requests on the metric
func (tm *TrustMetric) processRequests() {
var t *time.Ticker
t := time.NewTicker(tm.intervalLen)
defer t.Stop()
loop:
for {
select {
case bg := <-tm.update:
// Check if this is the first experience with
// what we are tracking since being started or paused
if t == nil {
t = time.NewTicker(tm.intervalLen)
// what we are tracking since being paused
if tm.paused {
tm.good = 0
tm.bad = 0
// New events cause us to unpause the metric
tm.paused = false
}
if bg.IsBad {
@@ -502,41 +510,36 @@ loop:
case rtv := <-tm.trustValue:
rtv.Resp <- tm.calcTrustValue()
case <-t.C:
// Add the current trust value to the history data
newHist := tm.calcTrustValue()
tm.history = append([]float64{newHist}, tm.history...)
if !tm.paused {
// Add the current trust value to the history data
newHist := tm.calcTrustValue()
tm.history = append([]float64{newHist}, tm.history...)
// Update history and interval counters
if tm.historySize < tm.historyMaxSize {
tm.historySize++
} else {
tm.history = tm.history[:tm.historyMaxSize]
// Update history and interval counters
if tm.historySize < tm.historyMaxSize {
tm.historySize++
} else {
tm.history = tm.history[:tm.historyMaxSize]
}
if tm.numIntervals < tm.maxIntervals {
tm.numIntervals++
}
// Update the history data using Faded Memories
tm.updateFadedMemory()
// Calculate the history value for the upcoming time interval
tm.historyValue = tm.calcHistoryValue()
tm.good = 0
tm.bad = 0
}
if tm.numIntervals < tm.maxIntervals {
tm.numIntervals++
}
// Update the history data using Faded Memories
tm.updateFadedMemory()
// Calculate the history value for the upcoming time interval
tm.historyValue = tm.calcHistoryValue()
tm.good = 0
tm.bad = 0
case stop := <-tm.stop:
if stop {
// Stop all further tracking for this metric
break loop
}
// Pause the metric for now by stopping the ticker
if t != nil {
t.Stop()
t = nil
}
// Pause the metric for now
tm.paused = true
}
}
if t != nil {
t.Stop()
}
}
+239
View File
@@ -0,0 +1,239 @@
// Copyright 2017 Tendermint. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
package trust
import (
"fmt"
"io/ioutil"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
dbm "github.com/tendermint/tmlibs/db"
"github.com/tendermint/tmlibs/log"
)
func getTempDir(prefix string) string {
dir, err := ioutil.TempDir("", prefix)
if err != nil {
panic(err)
}
return dir
}
func TestTrustMetricStoreSaveLoad(t *testing.T) {
dir := getTempDir("trustMetricStoreTest")
defer os.Remove(dir)
historyDB := dbm.NewDB("trusthistory", "goleveldb", dir)
config := TrustMetricConfig{
TrackingWindow: 5 * time.Minute,
IntervalLength: 50 * time.Millisecond,
}
// 0 peers saved
store := NewTrustMetricStore(historyDB, config)
store.SetLogger(log.TestingLogger())
store.saveToDB()
// Load the data from the file
store = NewTrustMetricStore(historyDB, config)
store.SetLogger(log.TestingLogger())
store.loadFromDB()
// Make sure we still have 0 entries
assert.Zero(t, store.Size())
// 100 peers
for i := 0; i < 100; i++ {
key := fmt.Sprintf("peer_%d", i)
tm := store.GetPeerTrustMetric(key)
tm.AddBadEvents(10)
tm.GoodEvent()
}
// Check that we have 100 entries and save
assert.Equal(t, 100, store.Size())
// Give the metrics time to process the history data
time.Sleep(1 * time.Second)
// Stop all the trust metrics and save
for _, tm := range store.peerMetrics {
tm.Stop()
}
store.saveToDB()
// Load the data from the DB
store = NewTrustMetricStore(historyDB, config)
store.SetLogger(log.TestingLogger())
store.loadFromDB()
// Check that we still have 100 peers with imperfect trust values
assert.Equal(t, 100, store.Size())
for _, tm := range store.peerMetrics {
assert.NotEqual(t, 1.0, tm.TrustValue())
}
// Stop all the trust metrics
for _, tm := range store.peerMetrics {
tm.Stop()
}
}
func TestTrustMetricStoreConfig(t *testing.T) {
historyDB := dbm.NewDB("", "memdb", "")
config := TrustMetricConfig{
ProportionalWeight: 0.5,
IntegralWeight: 0.5,
}
// Create a store with custom config
store := NewTrustMetricStore(historyDB, config)
store.SetLogger(log.TestingLogger())
// Have the store make us a metric with the config
tm := store.GetPeerTrustMetric("TestKey")
// Check that the options made it to the metric
assert.Equal(t, 0.5, tm.proportionalWeight)
assert.Equal(t, 0.5, tm.integralWeight)
tm.Stop()
}
func TestTrustMetricStoreLookup(t *testing.T) {
historyDB := dbm.NewDB("", "memdb", "")
store := NewTrustMetricStore(historyDB, DefaultConfig())
store.SetLogger(log.TestingLogger())
// Create 100 peers in the trust metric store
for i := 0; i < 100; i++ {
key := fmt.Sprintf("peer_%d", i)
store.GetPeerTrustMetric(key)
// Check that the trust metric was successfully entered
ktm := store.peerMetrics[key]
assert.NotNil(t, ktm, "Expected to find TrustMetric %s but wasn't there.", key)
}
// Stop all the trust metrics
for _, tm := range store.peerMetrics {
tm.Stop()
}
}
func TestTrustMetricStorePeerScore(t *testing.T) {
historyDB := dbm.NewDB("", "memdb", "")
store := NewTrustMetricStore(historyDB, DefaultConfig())
store.SetLogger(log.TestingLogger())
key := "TestKey"
tm := store.GetPeerTrustMetric(key)
// This peer is innocent so far
first := tm.TrustScore()
assert.Equal(t, 100, first)
// Add some undesirable events and disconnect
tm.BadEvent()
first = tm.TrustScore()
assert.NotEqual(t, 100, first)
tm.AddBadEvents(10)
second := tm.TrustScore()
if second > first {
t.Errorf("A greater number of bad events should lower the trust score")
}
store.PeerDisconnected(key)
// We will remember our experiences with this peer
tm = store.GetPeerTrustMetric(key)
assert.NotEqual(t, 100, tm.TrustScore())
tm.Stop()
}
func TestTrustMetricScores(t *testing.T) {
tm := NewMetric()
// Perfect score
tm.GoodEvent()
score := tm.TrustScore()
assert.Equal(t, 100, score)
// Less than perfect score
tm.AddBadEvents(10)
score = tm.TrustScore()
assert.NotEqual(t, 100, score)
tm.Stop()
}
func TestTrustMetricConfig(t *testing.T) {
// 7 days
window := time.Minute * 60 * 24 * 7
config := TrustMetricConfig{
TrackingWindow: window,
IntervalLength: 2 * time.Minute,
}
tm := NewMetricWithConfig(config)
// The max time intervals should be the TrackingWindow / IntervalLen
assert.Equal(t, int(config.TrackingWindow/config.IntervalLength), tm.maxIntervals)
dc := DefaultConfig()
// These weights should still be the default values
assert.Equal(t, dc.ProportionalWeight, tm.proportionalWeight)
assert.Equal(t, dc.IntegralWeight, tm.integralWeight)
tm.Stop()
config.ProportionalWeight = 0.3
config.IntegralWeight = 0.7
tm = NewMetricWithConfig(config)
// These weights should be equal to our custom values
assert.Equal(t, config.ProportionalWeight, tm.proportionalWeight)
assert.Equal(t, config.IntegralWeight, tm.integralWeight)
tm.Stop()
}
func TestTrustMetricStopPause(t *testing.T) {
// Cause time intervals to pass quickly
config := TrustMetricConfig{
TrackingWindow: 5 * time.Minute,
IntervalLength: 10 * time.Millisecond,
}
tm := NewMetricWithConfig(config)
// Allow some time intervals to pass and pause
time.Sleep(50 * time.Millisecond)
tm.Pause()
// Give the pause some time to take place
time.Sleep(10 * time.Millisecond)
first := tm.numIntervals
// Allow more time to pass and check the intervals are unchanged
time.Sleep(50 * time.Millisecond)
assert.Equal(t, first, tm.numIntervals)
// Get the trust metric activated again
tm.AddGoodEvents(5)
// Allow some time intervals to pass and stop
time.Sleep(50 * time.Millisecond)
tm.Stop()
// Give the stop some time to take place
time.Sleep(10 * time.Millisecond)
second := tm.numIntervals
// Allow more time to pass and check the intervals are unchanged
time.Sleep(50 * time.Millisecond)
assert.Equal(t, second, tm.numIntervals)
if first >= second {
t.Fatalf("numIntervals should always increase or stay the same over time")
}
}