You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

412 lines
11 KiB

// 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 (
"math"
"sync"
"time"
cmn "github.com/tendermint/tendermint/libs/common"
)
//---------------------------------------------------------------------------------------
const (
// The weight applied to the derivative when current behavior is >= previous behavior
defaultDerivativeGamma1 = 0
// The weight applied to the derivative when current behavior is less than previous behavior
defaultDerivativeGamma2 = 1.0
// The weight applied to history data values when calculating the history value
defaultHistoryDataWeight = 0.8
)
// MetricHistoryJSON - history data necessary to save the trust metric
type MetricHistoryJSON struct {
NumIntervals int `json:"intervals"`
History []float64 `json:"history"`
}
// Metric - keeps track of peer reliability
// See tendermint/docs/architecture/adr-006-trust-metric.md for details
type Metric struct {
cmn.BaseService
// Mutex that protects the metric from concurrent access
mtx sync.Mutex
// Determines the percentage given to current behavior
proportionalWeight float64
// Determines the percentage given to prior behavior
integralWeight float64
// Count of how many time intervals this metric has been tracking
numIntervals int
// Size of the time interval window for this trust metric
maxIntervals int
// The time duration for a single time interval
intervalLen time.Duration
// Stores the trust history data for this metric
history []float64
// Weights applied to the history data when calculating the history value
historyWeights []float64
// The sum of the history weights used when calculating the history value
historyWeightSum float64
// The current number of history data elements
historySize int
// The maximum number of history data elements
historyMaxSize int
// The calculated history value for the current time interval
historyValue float64
// 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
// Used during testing in order to control the passing of time intervals
testTicker MetricTicker
}
// NewMetric returns a trust metric with the default configuration.
// Use Start to begin tracking the quality of peer behavior over time
func NewMetric() *Metric {
return NewMetricWithConfig(DefaultConfig())
}
// NewMetricWithConfig returns a trust metric with a custom configuration.
// Use Start to begin tracking the quality of peer behavior over time
func NewMetricWithConfig(tmc MetricConfig) *Metric {
tm := new(Metric)
config := customConfig(tmc)
// Setup using the configuration values
tm.proportionalWeight = config.ProportionalWeight
tm.integralWeight = config.IntegralWeight
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
tm.historyMaxSize = intervalToHistoryOffset(tm.maxIntervals) + 1
// This metric has a perfect history so far
tm.historyValue = 1.0
tm.BaseService = *cmn.NewBaseService(nil, "Metric", tm)
return tm
}
// OnStart implements Service
func (tm *Metric) OnStart() error {
if err := tm.BaseService.OnStart(); err != nil {
return err
}
go tm.processRequests()
return nil
}
// OnStop implements Service
// Nothing to do since the goroutine shuts down by itself via BaseService.Quit()
func (tm *Metric) OnStop() {}
// Returns a snapshot of the trust metric history data
func (tm *Metric) HistoryJSON() MetricHistoryJSON {
tm.mtx.Lock()
defer tm.mtx.Unlock()
return MetricHistoryJSON{
NumIntervals: tm.numIntervals,
History: tm.history,
}
}
// Instantiates a trust metric by loading the history data for a single peer.
// This is called only once and only right after creation, which is why the
// lock is not held while accessing the trust metric struct members
func (tm *Metric) Init(hist MetricHistoryJSON) {
// Restore the number of time intervals we have previously tracked
if hist.NumIntervals > tm.maxIntervals {
hist.NumIntervals = tm.maxIntervals
}
tm.numIntervals = hist.NumIntervals
// Restore the history and its current size
if len(hist.History) > tm.historyMaxSize {
// Keep the history no larger than historyMaxSize
last := len(hist.History) - tm.historyMaxSize
hist.History = hist.History[last:]
}
tm.history = hist.History
tm.historySize = len(tm.history)
// Create the history weight values and weight sum
for i := 1; i <= tm.numIntervals; i++ {
x := math.Pow(defaultHistoryDataWeight, float64(i)) // Optimistic weight
tm.historyWeights = append(tm.historyWeights, x)
}
for _, v := range tm.historyWeights {
tm.historyWeightSum += v
}
// Calculate the history value based on the loaded history data
tm.historyValue = tm.calcHistoryValue()
}
// Pause tells the metric to pause recording data over time intervals.
// All method calls that indicate events will unpause the metric
func (tm *Metric) Pause() {
tm.mtx.Lock()
defer tm.mtx.Unlock()
// Pause the metric for now
tm.paused = true
}
// BadEvents indicates that an undesirable event(s) took place
func (tm *Metric) BadEvents(num int) {
tm.mtx.Lock()
defer tm.mtx.Unlock()
tm.unpause()
tm.bad += float64(num)
}
// GoodEvents indicates that a desirable event(s) took place
func (tm *Metric) GoodEvents(num int) {
tm.mtx.Lock()
defer tm.mtx.Unlock()
tm.unpause()
tm.good += float64(num)
}
// TrustValue gets the dependable trust value; always between 0 and 1
func (tm *Metric) TrustValue() float64 {
tm.mtx.Lock()
defer tm.mtx.Unlock()
return tm.calcTrustValue()
}
// TrustScore gets a score based on the trust value always between 0 and 100
func (tm *Metric) TrustScore() int {
score := tm.TrustValue() * 100
return int(math.Floor(score))
}
// NextTimeInterval saves current time interval data and prepares for the following interval
func (tm *Metric) NextTimeInterval() {
tm.mtx.Lock()
defer tm.mtx.Unlock()
if tm.paused {
// Do not prepare for the next time interval while paused
return
}
// Add the current trust value to the history data
newHist := tm.calcTrustValue()
tm.history = append(tm.history, newHist)
// Update history and interval counters
if tm.historySize < tm.historyMaxSize {
tm.historySize++
} else {
// Keep the history no larger than historyMaxSize
last := len(tm.history) - tm.historyMaxSize
tm.history = tm.history[last:]
}
if tm.numIntervals < tm.maxIntervals {
tm.numIntervals++
// Add the optimistic weight for the new time interval
wk := math.Pow(defaultHistoryDataWeight, float64(tm.numIntervals))
tm.historyWeights = append(tm.historyWeights, wk)
tm.historyWeightSum += wk
}
// 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
}
// SetTicker allows a TestTicker to be provided that will manually control
// the passing of time from the perspective of the Metric.
// The ticker must be set before Start is called on the metric
func (tm *Metric) SetTicker(ticker MetricTicker) {
tm.mtx.Lock()
defer tm.mtx.Unlock()
tm.testTicker = ticker
}
// Copy returns a new trust metric with members containing the same values
func (tm *Metric) Copy() *Metric {
if tm == nil {
return nil
}
tm.mtx.Lock()
defer tm.mtx.Unlock()
return &Metric{
proportionalWeight: tm.proportionalWeight,
integralWeight: tm.integralWeight,
numIntervals: tm.numIntervals,
maxIntervals: tm.maxIntervals,
intervalLen: tm.intervalLen,
history: tm.history,
historyWeights: tm.historyWeights,
historyWeightSum: tm.historyWeightSum,
historySize: tm.historySize,
historyMaxSize: tm.historyMaxSize,
historyValue: tm.historyValue,
good: tm.good,
bad: tm.bad,
paused: tm.paused,
}
}
/* Private methods */
// This method is for a goroutine that handles all requests on the metric
func (tm *Metric) processRequests() {
t := tm.testTicker
if t == nil {
// No test ticker was provided, so we create a normal ticker
t = NewTicker(tm.intervalLen)
}
defer t.Stop()
// Obtain the raw channel
tick := t.GetChannel()
loop:
for {
select {
case <-tick:
tm.NextTimeInterval()
case <-tm.Quit():
// Stop all further tracking for this metric
break loop
}
}
}
// Wakes the trust metric up if it is currently paused
// This method needs to be called with the mutex locked
func (tm *Metric) unpause() {
// Check if this is the first experience with
// 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
}
}
// Calculates the trust value for the request processing
func (tm *Metric) calcTrustValue() float64 {
weightedP := tm.proportionalWeight * tm.proportionalValue()
weightedI := tm.integralWeight * tm.historyValue
weightedD := tm.weightedDerivative()
tv := weightedP + weightedI + weightedD
// Do not return a negative value.
if tv < 0 {
tv = 0
}
return tv
}
// Calculates the current score for good/bad experiences
func (tm *Metric) proportionalValue() float64 {
value := 1.0
total := tm.good + tm.bad
if total > 0 {
value = tm.good / total
}
return value
}
// Strengthens the derivative component when the change is negative
func (tm *Metric) weightedDerivative() float64 {
var weight float64 = defaultDerivativeGamma1
d := tm.derivativeValue()
if d < 0 {
weight = defaultDerivativeGamma2
}
return weight * d
}
// Calculates the derivative component
func (tm *Metric) derivativeValue() float64 {
return tm.proportionalValue() - tm.historyValue
}
// Calculates the integral (history) component of the trust value
func (tm *Metric) calcHistoryValue() float64 {
var hv float64
for i := 0; i < tm.numIntervals; i++ {
hv += tm.fadedMemoryValue(i) * tm.historyWeights[i]
}
return hv / tm.historyWeightSum
}
// Retrieves the actual history data value that represents the requested time interval
func (tm *Metric) fadedMemoryValue(interval int) float64 {
first := tm.historySize - 1
if interval == 0 {
// Base case
return tm.history[first]
}
offset := intervalToHistoryOffset(interval)
return tm.history[first-offset]
}
// Performs the update for our Faded Memories process, which allows the
// trust metric tracking window to be large while maintaining a small
// number of history data values
func (tm *Metric) updateFadedMemory() {
if tm.historySize < 2 {
return
}
end := tm.historySize - 1
// Keep the most recent history element
for count := 1; count < tm.historySize; count++ {
i := end - count
// The older the data is, the more we spread it out
x := math.Pow(2, float64(count))
// Two history data values are merged into a single value
tm.history[i] = ((tm.history[i] * (x - 1)) + tm.history[i+1]) / x
}
}
// Map the interval value down to an offset from the beginning of history
func intervalToHistoryOffset(interval int) int {
// The system maintains 2^m interval values in the form of m history
// data values. Therefore, we access the ith interval by obtaining
// the history data index = the floor of log2(i)
return int(math.Floor(math.Log2(float64(interval))))
}