737d7c7dfc
The ApplyUpdates() method on the reputation.DB interface acts like the similar Update() method, but can allow for applying the changes from multiple audit events, instead of only one. This will be necessary for the reputation write cache, which will batch up changes to each node's reputation in order to flush them periodically. Refs: https://github.com/storj/storj/issues/4601 Change-Id: I44cc47767ea2d9423166bb8fed080c8a11182041
302 lines
11 KiB
Go
302 lines
11 KiB
Go
// Copyright (C) 2021 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package reputation_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"storj.io/common/pb"
|
|
"storj.io/storj/satellite/reputation"
|
|
)
|
|
|
|
func TestAddAuditToHistory(t *testing.T) {
|
|
config := reputation.AuditHistoryConfig{
|
|
WindowSize: time.Hour,
|
|
TrackingPeriod: 2 * time.Hour,
|
|
GracePeriod: time.Hour,
|
|
OfflineThreshold: 0.6,
|
|
OfflineDQEnabled: true,
|
|
OfflineSuspensionEnabled: true,
|
|
}
|
|
|
|
startingWindow := time.Now().Truncate(time.Hour)
|
|
windowsInTrackingPeriod := int(config.TrackingPeriod.Seconds() / config.WindowSize.Seconds())
|
|
currentWindow := startingWindow
|
|
|
|
history := &pb.AuditHistory{}
|
|
|
|
// online score should be 1 until the first window is finished
|
|
err := reputation.AddAuditToHistory(history, false, currentWindow.Add(2*time.Minute), config)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, history.Score)
|
|
|
|
err = reputation.AddAuditToHistory(history, true, currentWindow.Add(20*time.Minute), config)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, history.Score)
|
|
|
|
// move to next window
|
|
currentWindow = currentWindow.Add(time.Hour)
|
|
|
|
// online score should be now be 0.5 since the first window is complete with one online audit and one offline audit
|
|
err = reputation.AddAuditToHistory(history, false, currentWindow.Add(2*time.Minute), config)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 0.5, history.Score)
|
|
|
|
err = reputation.AddAuditToHistory(history, true, currentWindow.Add(20*time.Minute), config)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 0.5, history.Score)
|
|
|
|
// move to next window
|
|
currentWindow = currentWindow.Add(time.Hour)
|
|
|
|
// try to add an audit for an old window, expect error
|
|
err = reputation.AddAuditToHistory(history, true, startingWindow, config)
|
|
require.Error(t, err)
|
|
|
|
// add another online audit for the latest window; score should still be 0.5
|
|
err = reputation.AddAuditToHistory(history, true, currentWindow, config)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 0.5, history.Score)
|
|
// add another online audit for the latest window; score should still be 0.5
|
|
err = reputation.AddAuditToHistory(history, true, currentWindow.Add(45*time.Minute), config)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 0.5, history.Score)
|
|
|
|
currentWindow = currentWindow.Add(time.Hour)
|
|
// in the current state, there are windowsInTrackingPeriod windows with a score of 0.5
|
|
// and one window with a score of 1.0. The Math below calculates the new score when the latest
|
|
// window gets included in the tracking period, and the earliest 0.5 window gets dropped.
|
|
expectedScore := (0.5*float64(windowsInTrackingPeriod-1) + 1) / float64(windowsInTrackingPeriod)
|
|
// add online audit for next window; score should now be expectedScore
|
|
err = reputation.AddAuditToHistory(history, true, currentWindow.Add(time.Minute), config)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, expectedScore, history.Score)
|
|
}
|
|
|
|
func TestMergeAuditHistoriesWithSingleAudit(t *testing.T) {
|
|
config := reputation.AuditHistoryConfig{
|
|
WindowSize: time.Hour,
|
|
TrackingPeriod: 2 * time.Hour,
|
|
GracePeriod: time.Hour,
|
|
OfflineThreshold: 0.6,
|
|
OfflineDQEnabled: true,
|
|
OfflineSuspensionEnabled: true,
|
|
}
|
|
|
|
startingWindow := time.Now().Truncate(time.Hour)
|
|
windowsInTrackingPeriod := int(config.TrackingPeriod.Seconds() / config.WindowSize.Seconds())
|
|
currentWindow := startingWindow
|
|
|
|
history := &pb.AuditHistory{}
|
|
|
|
// online score should be 1 until the first window is finished
|
|
trackingPeriodFull := testMergeAuditHistories(history, false, currentWindow.Add(2*time.Minute), config)
|
|
require.EqualValues(t, 1, history.Score)
|
|
require.False(t, trackingPeriodFull)
|
|
|
|
trackingPeriodFull = testMergeAuditHistories(history, true, currentWindow.Add(20*time.Minute), config)
|
|
require.EqualValues(t, 1, history.Score)
|
|
require.False(t, trackingPeriodFull)
|
|
|
|
// move to next window
|
|
currentWindow = currentWindow.Add(time.Hour)
|
|
|
|
// online score should be now be 0.5 since the first window is complete with one online audit and one offline audit
|
|
trackingPeriodFull = testMergeAuditHistories(history, false, currentWindow.Add(2*time.Minute), config)
|
|
require.EqualValues(t, 0.5, history.Score)
|
|
require.False(t, trackingPeriodFull)
|
|
|
|
trackingPeriodFull = testMergeAuditHistories(history, true, currentWindow.Add(20*time.Minute), config)
|
|
require.EqualValues(t, 0.5, history.Score)
|
|
require.False(t, trackingPeriodFull)
|
|
|
|
// move to next window
|
|
currentWindow = currentWindow.Add(time.Hour)
|
|
|
|
// add another online audit for the latest window; score should still be 0.5
|
|
trackingPeriodFull = testMergeAuditHistories(history, true, currentWindow, config)
|
|
require.EqualValues(t, 0.5, history.Score)
|
|
// now that we have two full windows other than the current one, tracking period should be considered full.
|
|
require.True(t, trackingPeriodFull)
|
|
// add another online audit for the latest window; score should still be 0.5
|
|
trackingPeriodFull = testMergeAuditHistories(history, true, currentWindow.Add(45*time.Minute), config)
|
|
require.EqualValues(t, 0.5, history.Score)
|
|
require.True(t, trackingPeriodFull)
|
|
|
|
currentWindow = currentWindow.Add(time.Hour)
|
|
// in the current state, there are windowsInTrackingPeriod windows with a score of 0.5
|
|
// and one window with a score of 1.0. The Math below calculates the new score when the latest
|
|
// window gets included in the tracking period, and the earliest 0.5 window gets dropped.
|
|
expectedScore := (0.5*float64(windowsInTrackingPeriod-1) + 1) / float64(windowsInTrackingPeriod)
|
|
// add online audit for next window; score should now be expectedScore
|
|
trackingPeriodFull = testMergeAuditHistories(history, true, currentWindow.Add(time.Minute), config)
|
|
require.EqualValues(t, expectedScore, history.Score)
|
|
require.True(t, trackingPeriodFull)
|
|
}
|
|
|
|
func testMergeAuditHistories(history *pb.AuditHistory, online bool, auditTime time.Time, config reputation.AuditHistoryConfig) bool {
|
|
onlineCount := int32(0)
|
|
if online {
|
|
onlineCount = 1
|
|
}
|
|
windows := []*pb.AuditWindow{{
|
|
WindowStart: auditTime.Truncate(config.WindowSize),
|
|
OnlineCount: onlineCount,
|
|
TotalCount: 1,
|
|
}}
|
|
return reputation.MergeAuditHistories(history, windows, config)
|
|
}
|
|
|
|
type hist struct {
|
|
online bool
|
|
startAt time.Time
|
|
}
|
|
|
|
func TestMergeAuditHistoriesWithMultipleAudits(t *testing.T) {
|
|
config := reputation.AuditHistoryConfig{
|
|
WindowSize: 10 * time.Minute,
|
|
TrackingPeriod: 1 * time.Hour,
|
|
}
|
|
startTime := time.Now().Truncate(time.Hour).Add(-time.Hour)
|
|
|
|
t.Parallel()
|
|
|
|
t.Run("normal-merge", func(t *testing.T) {
|
|
history := makeHistory([]hist{
|
|
// first window: half online
|
|
{true, startTime},
|
|
{false, startTime.Add(1 * time.Minute)},
|
|
{true, startTime.Add(5 * time.Minute)},
|
|
{false, startTime.Add(8 * time.Minute)},
|
|
// second window: all online
|
|
{true, startTime.Add(10 * time.Minute)},
|
|
{true, startTime.Add(11 * time.Minute)},
|
|
{true, startTime.Add(20*time.Minute - time.Second)},
|
|
// third window: all online
|
|
{true, startTime.Add(20 * time.Minute)},
|
|
// fourth window: all online
|
|
{true, startTime.Add(30 * time.Minute)},
|
|
// fifth window; won't be included in score
|
|
{false, startTime.Add(40 * time.Minute)},
|
|
}, config)
|
|
require.Equal(t, float64(0.875), history.Score) // 3.5/4; chosen to be exact in floating point
|
|
|
|
// make the second, third, and fourth windows go from all-online to half-online
|
|
addHistory := makeHistory([]hist{
|
|
// fits in second window
|
|
{false, startTime.Add(12 * time.Minute)},
|
|
{false, startTime.Add(13 * time.Minute)},
|
|
{false, startTime.Add(14 * time.Minute)},
|
|
// fits in third window
|
|
{false, startTime.Add(20*time.Minute + time.Microsecond)},
|
|
// fits in fourth window
|
|
{false, startTime.Add(40*time.Minute - time.Microsecond)},
|
|
}, config)
|
|
require.Equal(t, float64(0), addHistory.Score)
|
|
|
|
periodFull := reputation.MergeAuditHistories(history, addHistory.Windows, config)
|
|
|
|
require.False(t, periodFull)
|
|
require.Equal(t, 5, len(history.Windows))
|
|
require.Equal(t, float64(0.5), history.Score) // all windows at 50% online
|
|
})
|
|
|
|
t.Run("trim-old-windows", func(t *testing.T) {
|
|
history := makeHistory([]hist{
|
|
// this window is too old
|
|
{true, startTime.Add(-2 * time.Minute)},
|
|
{true, startTime.Add(-1 * time.Minute)},
|
|
// oldest window
|
|
{false, startTime.Add(0)},
|
|
// newest window (not included in score)
|
|
{true, startTime.Add(1 * time.Hour)},
|
|
}, config)
|
|
require.Equal(t, float64(0.5), history.Score) // the too-old window is still included in the score here
|
|
|
|
addHistory := makeHistory([]hist{
|
|
// this window is too old
|
|
{true, startTime.Add(-10 * time.Minute)},
|
|
// oldest window
|
|
{false, startTime.Add(9 * time.Minute)},
|
|
// a window entirely not present in the other history
|
|
{true, startTime.Add(10 * time.Minute)},
|
|
}, config)
|
|
require.Equal(t, float64(0.5), addHistory.Score) // the latest window is not included (yet)
|
|
|
|
periodFull := reputation.MergeAuditHistories(history, addHistory.Windows, config)
|
|
|
|
require.False(t, periodFull)
|
|
require.Equal(t, 3, len(history.Windows))
|
|
// oldest window = 0/2, second window = 1/1, third window not counted
|
|
require.Equal(t, float64(0.5), history.Score)
|
|
})
|
|
|
|
t.Run("merge-with-empty", func(t *testing.T) {
|
|
history := makeHistory([]hist{}, config)
|
|
require.Equal(t, float64(1), history.Score)
|
|
|
|
addHistory := makeHistory([]hist{
|
|
{true, startTime.Add(0)},
|
|
{false, startTime.Add(10 * time.Minute)},
|
|
{false, startTime.Add(59 * time.Minute)},
|
|
}, config)
|
|
require.Equal(t, float64(0.5), addHistory.Score)
|
|
|
|
periodFull := reputation.MergeAuditHistories(history, addHistory.Windows, config)
|
|
|
|
require.False(t, periodFull)
|
|
require.Equal(t, 3, len(history.Windows))
|
|
require.Equal(t, float64(0.5), history.Score)
|
|
|
|
// now merge with an empty addHistory instead
|
|
addHistory = makeHistory([]hist{}, config)
|
|
require.Equal(t, float64(1), addHistory.Score)
|
|
|
|
periodFull = reputation.MergeAuditHistories(history, addHistory.Windows, config)
|
|
|
|
require.False(t, periodFull)
|
|
require.Equal(t, 3, len(history.Windows))
|
|
require.Equal(t, float64(0.5), history.Score)
|
|
|
|
// and finally, merge two empty histories with each other
|
|
history = makeHistory([]hist{}, config)
|
|
addHistory = makeHistory([]hist{}, config)
|
|
|
|
periodFull = reputation.MergeAuditHistories(history, addHistory.Windows, config)
|
|
|
|
require.False(t, periodFull)
|
|
require.Equal(t, 0, len(history.Windows))
|
|
require.Equal(t, float64(1), history.Score)
|
|
})
|
|
}
|
|
|
|
func makeHistory(histWindows []hist, config reputation.AuditHistoryConfig) *pb.AuditHistory {
|
|
windows := make([]*pb.AuditWindow, 0, len(histWindows))
|
|
for _, histWindow := range histWindows {
|
|
onlineCount := int32(0)
|
|
if histWindow.online {
|
|
onlineCount = 1
|
|
}
|
|
startAt := histWindow.startAt.Truncate(config.WindowSize)
|
|
if len(windows) > 0 && startAt == windows[len(windows)-1].WindowStart {
|
|
windows[len(windows)-1].OnlineCount += onlineCount
|
|
windows[len(windows)-1].TotalCount++
|
|
} else {
|
|
windows = append(windows, &pb.AuditWindow{
|
|
OnlineCount: onlineCount,
|
|
TotalCount: 1,
|
|
WindowStart: startAt,
|
|
})
|
|
}
|
|
}
|
|
baseHistory := &pb.AuditHistory{
|
|
Windows: windows,
|
|
}
|
|
reputation.RecalculateScore(baseHistory)
|
|
return baseHistory
|
|
}
|