- // 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"
-
- "github.com/tendermint/tendermint/libs/service"
- )
-
- //---------------------------------------------------------------------------------------
-
- 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 {
- service.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 = *service.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))))
- }
|