diff --git a/CHANGELOG.md b/CHANGELOG.md index 23b32fce..5718b76b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Valid flag on node [\#105](https://github.com/NodeFactoryIo/vedran/pull/105) ([mpetrun5](https://github.com/mpetrun5)) - Passing SSL certificates [\#112](https://github.com/NodeFactoryIo/vedran/pull/112) ([mpetrun5](https://github.com/mpetrun5)) - Expose stats endpoints [\#114](https://github.com/NodeFactoryIo/vedran/pull/114) ([MakMuftic](https://github.com/MakMuftic)) +- Calculating reward distribution [\#124](https://github.com/NodeFactoryIo/vedran/pull/124) ([MakMuftic](https://github.com/MakMuftic)) ### Fix diff --git a/internal/controllers/stats.go b/internal/controllers/stats.go index 4f029dd3..8101a96a 100644 --- a/internal/controllers/stats.go +++ b/internal/controllers/stats.go @@ -2,19 +2,25 @@ package controllers import ( "encoding/json" - "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/configuration" + "github.com/NodeFactoryIo/vedran/internal/payout" "github.com/NodeFactoryIo/vedran/internal/stats" muxhelpper "github.com/gorilla/mux" log "github.com/sirupsen/logrus" "net/http" + "time" ) type StatsResponse struct { - Stats map[string]models.NodeStatsDetails `json:"stats"` + Stats map[string]payout.NodePayoutDetails `json:"stats"` + Fee float32 `json:"fee"` } +var getNow = time.Now + func (c *ApiController) StatisticsHandlerAllStats(w http.ResponseWriter, r *http.Request) { - statisticsForPayout, err := stats.CalculateStatisticsFromLastPayout(c.repositories) + // should check for signature in body and only then record payout + payoutStatistics, err := payout.GetStatsForPayout(c.repositories, getNow(), false) if err != nil { log.Errorf("Failed to calculate statistics, because %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -23,7 +29,8 @@ func (c *ApiController) StatisticsHandlerAllStats(w http.ResponseWriter, r *http w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(StatsResponse{ - Stats: statisticsForPayout, + Stats: payoutStatistics, + Fee: configuration.Config.Fee, }) } @@ -36,7 +43,7 @@ func (c *ApiController) StatisticsHandlerStatsForNode(w http.ResponseWriter, r * return } - nodeStatisticsFromLastPayout, err := stats.CalculateNodeStatisticsFromLastPayout(c.repositories, nodeId) + nodeStatisticsFromLastPayout, err := stats.CalculateNodeStatisticsFromLastPayout(c.repositories, nodeId, getNow()) if err != nil { log.Errorf("Failed to calculate statistics for node %s, because %v", nodeId, err) if err.Error() == "not found" { diff --git a/internal/controllers/stats_test.go b/internal/controllers/stats_test.go index 3d42d1ce..f93b3548 100644 --- a/internal/controllers/stats_test.go +++ b/internal/controllers/stats_test.go @@ -20,6 +20,9 @@ import ( func TestApiController_StatisticsHandlerAllStats(t *testing.T) { now := time.Now() + getNow = func() time.Time { + return now + } tests := []struct { name string httpStatus int @@ -86,6 +89,10 @@ func TestApiController_StatisticsHandlerAllStats(t *testing.T) { nodeRepoMock.On("GetAll").Return( test.nodeRepoGetAllReturns, test.nodeRepoGetAllError, ) + nodeRepoMock.On("FindByID", test.nodeId).Return(&models.Node{ + ID: test.nodeId, + PayoutAddress: "0xtest-address", + }, nil) recordRepoMock := mocks.RecordRepository{} recordRepoMock.On("FindSuccessfulRecordsInsideInterval", test.nodeId, mock.Anything, mock.Anything, @@ -114,6 +121,7 @@ func TestApiController_StatisticsHandlerAllStats(t *testing.T) { test.payoutRepoFindLatestPayoutReturns, test.payoutRepoFindLatestPayoutError, ) + payoutRepoMock.On("Save", mock.Anything).Return(nil) apiController := NewApiController(false, repositories.Repos{ NodeRepo: &nodeRepoMock, PingRepo: &pingRepoMock, @@ -135,8 +143,9 @@ func TestApiController_StatisticsHandlerAllStats(t *testing.T) { var statsResponse StatsResponse if rr.Code == http.StatusOK { _ = json.Unmarshal(rr.Body.Bytes(), &statsResponse) - assert.LessOrEqual(t, test.nodeNumberOfPings, statsResponse.Stats[test.nodeId].TotalPings) - assert.Equal(t, test.nodeNumberOfRequests, statsResponse.Stats[test.nodeId].TotalRequests) + assert.LessOrEqual(t, test.nodeNumberOfPings, statsResponse.Stats[test.nodeId].Stats.TotalPings) + assert.Equal(t, test.nodeNumberOfRequests, statsResponse.Stats[test.nodeId].Stats.TotalRequests) + assert.Equal(t, "0xtest-address", statsResponse.Stats[test.nodeId].PayoutAddress) } }) } @@ -144,6 +153,9 @@ func TestApiController_StatisticsHandlerAllStats(t *testing.T) { func TestApiController_StatisticsHandlerStatsForNode(t *testing.T) { now := time.Now() + getNow = func() time.Time { + return now + } tests := []struct { name string httpStatus int diff --git a/internal/loadbalancer/server.go b/internal/loadbalancer/server.go index a8404ea5..47971505 100644 --- a/internal/loadbalancer/server.go +++ b/internal/loadbalancer/server.go @@ -2,16 +2,17 @@ package loadbalancer import ( "fmt" - "net/http" - "github.com/NodeFactoryIo/vedran/internal/auth" "github.com/NodeFactoryIo/vedran/internal/configuration" + "github.com/NodeFactoryIo/vedran/internal/models" "github.com/NodeFactoryIo/vedran/internal/repositories" "github.com/NodeFactoryIo/vedran/internal/router" "github.com/NodeFactoryIo/vedran/internal/schedule/checkactive" "github.com/NodeFactoryIo/vedran/internal/schedule/penalize" "github.com/asdine/storm/v3" log "github.com/sirupsen/logrus" + "net/http" + "time" ) func StartLoadBalancerServer(props configuration.Configuration) { @@ -45,6 +46,21 @@ func StartLoadBalancerServer(props configuration.Configuration) { log.Fatalf("Failed reseting pings because of: %v", err) } + // save initial payout if there isn't any saved payouts + p, err := repos.PayoutRepo.GetAll() + if err != nil { + log.Fatalf("Failed creating initial payout because of: %v", err) + } else if len(*p) == 0 { + err := repos.PayoutRepo.Save(&models.Payout{ + ID: "1", + Timestamp: time.Now(), + PaymentDetails: nil, + }) + if err != nil { + log.Fatalf("Failed creating initial payout because of: %v", err) + } + } + penalizedNodes, err := repos.NodeRepo.GetPenalizedNodes() if err != nil { log.Fatalf("Failed fetching penalized nodes because of: %v", err) diff --git a/internal/models/payout.go b/internal/models/payout.go index 85f5e696..89efdc35 100644 --- a/internal/models/payout.go +++ b/internal/models/payout.go @@ -3,6 +3,7 @@ package models import "time" type Payout struct { + ID string `storm:"id"` Timestamp time.Time `json:"timestamp"` PaymentDetails map[string]NodeStatsDetails } diff --git a/internal/payout/distribution.go b/internal/payout/distribution.go new file mode 100644 index 00000000..447b5430 --- /dev/null +++ b/internal/payout/distribution.go @@ -0,0 +1,62 @@ +package payout + +import ( + "github.com/NodeFactoryIo/vedran/internal/models" + "math" + "math/big" +) + +const ( + livelinessRewardPercentage = 0.1 + requestsRewardPercentage = 0.9 +) + +func CalculatePayoutDistributionByNode( + payoutDetails map[string]models.NodeStatsDetails, + totalReward float64, + loadBalancerFeePercentage float64, +) map[string]big.Int { + var rewardPool = totalReward + + loadbalancerReward := rewardPool * loadBalancerFeePercentage + rewardPool -= loadbalancerReward + + livelinessRewardPool := rewardPool * livelinessRewardPercentage + requestsRewardPool := rewardPool * requestsRewardPercentage + + var totalNumberOfPings = float64(0) + var totalNumberOfRequests = float64(0) + for _, node := range payoutDetails { + totalNumberOfPings += node.TotalPings + totalNumberOfRequests += node.TotalRequests + } + + totalDistributedLivelinessRewards := float64(0) + totalDistributedRequestsRewards := float64(0) + payoutAmountDistributionByNodes := make(map[string]big.Int, len(payoutDetails)) + + for nodeId, nodeStatsDetails := range payoutDetails { + // liveliness rewards + livelinessReward := float64(0) + if totalNumberOfPings != 0 && nodeStatsDetails.TotalPings != 0 { + nodeLivelinessRewardPercentage := nodeStatsDetails.TotalPings / totalNumberOfPings + livelinessReward = livelinessRewardPool * nodeLivelinessRewardPercentage + livelinessReward = math.Floor(livelinessReward) + totalDistributedLivelinessRewards += livelinessReward + } + // requests rewards + requestsReward := float64(0) + if totalNumberOfRequests != 0 && nodeStatsDetails.TotalRequests != 0 { + nodeRequestsRewardPercentage := nodeStatsDetails.TotalRequests / totalNumberOfRequests + requestsReward = requestsRewardPool * nodeRequestsRewardPercentage + requestsReward = math.Floor(requestsReward) + totalDistributedRequestsRewards += requestsReward + } + + totalNodeReward := livelinessReward + requestsReward + totalNodeRewardAsInt, _ := big.NewFloat(totalNodeReward).Int(nil) + payoutAmountDistributionByNodes[nodeId] = *totalNodeRewardAsInt + } + + return payoutAmountDistributionByNodes +} diff --git a/internal/payout/distribution_test.go b/internal/payout/distribution_test.go new file mode 100644 index 00000000..5f0cca1a --- /dev/null +++ b/internal/payout/distribution_test.go @@ -0,0 +1,75 @@ +package payout + +import ( + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/stretchr/testify/assert" + "math/big" + "testing" +) + +func Test_CalculatePayoutDistributionByNode(t *testing.T) { + tests := []struct { + name string + payoutDetails map[string]models.NodeStatsDetails + totalReward float64 + loadBalancerFee float64 + resultDistribution map[string]big.Int + }{ + { // this test is set for 10/90 split between liveliness and requests + name: "test distribution", + payoutDetails: map[string]models.NodeStatsDetails{ + "1": { + TotalPings: 100, + TotalRequests: 10, + }, + "2": { + TotalPings: 100, + TotalRequests: 5, + }, + "3": { + TotalPings: 90, + TotalRequests: 10, + }, + "4": { + TotalPings: 90, + TotalRequests: 5, + }, + "5": { + TotalPings: 50, + TotalRequests: 2, + }, + "6": { + TotalPings: 40, + TotalRequests: 0, + }, + }, + totalReward: 100000000, + loadBalancerFee: 0.1, + resultDistribution: map[string]big.Int{ + "1": *big.NewInt(27227393), // 27227393.617021276 // 100P 10R + "2": *big.NewInt(14571143), // 14571143.617021276 // 100P 5R + "3": *big.NewInt(27035904), // 27035904.255319147 // 90P 10R + "4": *big.NewInt(14379654), // 14379654.25531915 // 90P 5R + "5": *big.NewInt(6019946), // 6019946.808510638 // 50P 2R + "6": *big.NewInt(765957), // 765957.4468085106 // 40P 0R + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + distributionByNode := CalculatePayoutDistributionByNode( + test.payoutDetails, test.totalReward, test.loadBalancerFee, + ) + assert.Equal(t, test.resultDistribution, distributionByNode) + totalDistributed := big.NewInt(0) + for _, amount := range distributionByNode { + totalDistributed.Add(totalDistributed, &amount) + } + totalShoudBeDistributed := test.totalReward * (float64(1) - test.loadBalancerFee) + + totalShouldBeDistributedRounded, _ := big.NewFloat(totalShoudBeDistributed).Int(nil) + delta := big.NewInt(0).Sub(totalShouldBeDistributedRounded, totalDistributed) + assert.GreaterOrEqual(t, delta.Int64(), int64(0)) + }) + } +} diff --git a/internal/payout/payout.go b/internal/payout/payout.go new file mode 100644 index 00000000..e74ad68d --- /dev/null +++ b/internal/payout/payout.go @@ -0,0 +1,48 @@ +package payout + +import ( + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/repositories" + "github.com/NodeFactoryIo/vedran/internal/stats" + log "github.com/sirupsen/logrus" + "time" +) + +type NodePayoutDetails struct { + Stats models.NodeStatsDetails `json:"stats"` + PayoutAddress string `json:"payout_address"` +} + +func GetStatsForPayout( + repos repositories.Repos, + intervalEnd time.Time, + recordPayout bool, +) (map[string]NodePayoutDetails, error) { + + statistics, err := stats.CalculateStatisticsFromLastPayout(repos, intervalEnd) + if err != nil { + return nil, err + } + + payoutStatistics := make(map[string]NodePayoutDetails, len(statistics)) + for nodeId, statsDetails := range statistics { + node, _ := repos.NodeRepo.FindByID(nodeId) + payoutStatistics[nodeId] = NodePayoutDetails{ + Stats: statsDetails, + PayoutAddress: node.PayoutAddress, + } + } + + if recordPayout { + err = repos.PayoutRepo.Save(&models.Payout{ + Timestamp: intervalEnd, + PaymentDetails: statistics, + }) + if err != nil { + log.Errorf("Unable to save payout information to database, because of: %v", err) + return nil, err + } + } + + return payoutStatistics, nil +} diff --git a/internal/payout/payout_test.go b/internal/payout/payout_test.go new file mode 100644 index 00000000..d05cb44f --- /dev/null +++ b/internal/payout/payout_test.go @@ -0,0 +1,255 @@ +package payout + +import ( + "errors" + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/repositories" + mocks "github.com/NodeFactoryIo/vedran/mocks/repositories" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "testing" + "time" +) + +func Test_GetStatsForPayout(t *testing.T) { + now := time.Now() + aDayAgo := now.Add(-24 * time.Hour) + tests := []struct { + name string + nodeId string + shouldRecordPayout bool + // NodeRepo.GetAll + nodeRepoGetAllReturns *[]models.Node + nodeRepoGetAllError error + // NodeRepo.FindByID + nodeRepoFindByIDReturns *models.Node + nodeRepoFindByIDError error + // RecordRepo.FindSuccessfulRecordsInsideInterval + recordRepoFindSuccessfulRecordsInsideIntervalReturns []models.Record + recordRepoFindSuccessfulRecordsInsideIntervalError error + // DowntimeRepo.FindDowntimesInsideInterval + downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime + downtimeRepoFindDowntimesInsideIntervalError error + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration time.Duration + pingRepoCalculateDowntimeError error + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns *models.Payout + payoutRepoFindLatestPayoutError error + // PayoutRepo.Save + payoutRepoSaveError error + payoutRepoSaveNumOfCalls int + // GetStatsForPayout + getStatsForPayoutReturns map[string]NodePayoutDetails + getStatsForPayoutError error + }{ + { + name: "calculate stats and save payout record", + nodeId: "1", + shouldRecordPayout: true, + // NodeRepo.GetAll + nodeRepoGetAllReturns: &[]models.Node{ + { + ID: "1", + }, + }, + nodeRepoGetAllError: nil, + // NodeRepo.FindByID + nodeRepoFindByIDReturns: &models.Node{ + ID: "1", + PayoutAddress: "0xpayout-address", + }, + nodeRepoFindByIDError: nil, + // RecordRepo.FindSuccessfulRecordsInsideInterval + recordRepoFindSuccessfulRecordsInsideIntervalReturns: []models.Record{ + {ID: 1, NodeId: "1", Status: "successful", Timestamp: now.Add(-12 * time.Hour)}, + {ID: 2, NodeId: "1", Status: "successful", Timestamp: now.Add(-10 * time.Hour)}, + {ID: 3, NodeId: "1", Status: "successful", Timestamp: now.Add(-8 * time.Hour)}, + }, + recordRepoFindSuccessfulRecordsInsideIntervalError: nil, + // DowntimeRepo.FindDowntimesInsideInterval + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns: &models.Payout{ + Timestamp: aDayAgo, + PaymentDetails: nil, + }, + payoutRepoFindLatestPayoutError: nil, + // PayoutRepo.Save + payoutRepoSaveError: nil, + payoutRepoSaveNumOfCalls: 1, + // GetStatsForPayout + getStatsForPayoutError: nil, + getStatsForPayoutReturns: map[string]NodePayoutDetails{ + "1": { + Stats: models.NodeStatsDetails{ + TotalPings: 17280, + TotalRequests: 3, + }, + PayoutAddress: "0xpayout-address", + }, + }, + }, + { + name: "calculate stats and don't save payout record", + nodeId: "1", + shouldRecordPayout: false, + // NodeRepo.GetAll + nodeRepoGetAllReturns: &[]models.Node{ + { + ID: "1", + }, + }, + nodeRepoGetAllError: nil, + // NodeRepo.FindByID + nodeRepoFindByIDReturns: &models.Node{ + ID: "1", + PayoutAddress: "0xpayout-address", + }, + nodeRepoFindByIDError: nil, + // RecordRepo.FindSuccessfulRecordsInsideInterval + recordRepoFindSuccessfulRecordsInsideIntervalReturns: []models.Record{ + {ID: 1, NodeId: "1", Status: "successful", Timestamp: now.Add(-12 * time.Hour)}, + {ID: 2, NodeId: "1", Status: "successful", Timestamp: now.Add(-10 * time.Hour)}, + {ID: 3, NodeId: "1", Status: "successful", Timestamp: now.Add(-8 * time.Hour)}, + }, + recordRepoFindSuccessfulRecordsInsideIntervalError: nil, + // DowntimeRepo.FindDowntimesInsideInterval + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns: &models.Payout{ + Timestamp: aDayAgo, + PaymentDetails: nil, + }, + payoutRepoFindLatestPayoutError: nil, + // PayoutRepo.Save + payoutRepoSaveError: nil, + payoutRepoSaveNumOfCalls: 0, + // GetStatsForPayout + getStatsForPayoutError: nil, + getStatsForPayoutReturns: map[string]NodePayoutDetails{ + "1": { + Stats: models.NodeStatsDetails{ + TotalPings: 17280, + TotalRequests: 3, + }, + PayoutAddress: "0xpayout-address", + }, + }, + }, + { + name: "fail on fetching from database", + nodeId: "1", + shouldRecordPayout: false, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutError: errors.New("db error"), + // GetStatsForPayout + getStatsForPayoutError: errors.New("db error"), + getStatsForPayoutReturns: nil, + }, + { + name: "fail on saving to database", + nodeId: "1", + shouldRecordPayout: true, + // NodeRepo.GetAll + nodeRepoGetAllReturns: &[]models.Node{ + { + ID: "1", + }, + }, + nodeRepoGetAllError: nil, + // NodeRepo.FindByID + nodeRepoFindByIDReturns: &models.Node{ + ID: "1", + PayoutAddress: "0xpayout-address", + }, + nodeRepoFindByIDError: nil, + // RecordRepo.FindSuccessfulRecordsInsideInterval + recordRepoFindSuccessfulRecordsInsideIntervalReturns: []models.Record{ + {ID: 1, NodeId: "1", Status: "successful", Timestamp: now.Add(-12 * time.Hour)}, + {ID: 2, NodeId: "1", Status: "successful", Timestamp: now.Add(-10 * time.Hour)}, + {ID: 3, NodeId: "1", Status: "successful", Timestamp: now.Add(-8 * time.Hour)}, + }, + recordRepoFindSuccessfulRecordsInsideIntervalError: nil, + // DowntimeRepo.FindDowntimesInsideInterval + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns: &models.Payout{ + Timestamp: aDayAgo, + PaymentDetails: nil, + }, + payoutRepoFindLatestPayoutError: nil, + // PayoutRepo.Save + payoutRepoSaveError: errors.New("db error"), + payoutRepoSaveNumOfCalls: 1, + // GetStatsForPayout + getStatsForPayoutError: errors.New("db error"), + getStatsForPayoutReturns: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + nodeRepoMock := mocks.NodeRepository{} + nodeRepoMock.On("GetAll").Return( + test.nodeRepoGetAllReturns, test.nodeRepoGetAllError, + ) + nodeRepoMock.On("FindByID", test.nodeId).Return( + test.nodeRepoFindByIDReturns, test.nodeRepoFindByIDError, + ) + recordRepoMock := mocks.RecordRepository{} + recordRepoMock.On("FindSuccessfulRecordsInsideInterval", + test.nodeId, mock.Anything, mock.Anything, + ).Return( + test.recordRepoFindSuccessfulRecordsInsideIntervalReturns, + test.recordRepoFindSuccessfulRecordsInsideIntervalError, + ) + pingRepoMock := mocks.PingRepository{} + pingRepoMock.On("CalculateDowntime", + test.nodeId, mock.Anything, + ).Return( + time.Now(), + test.pingRepoCalculateDowntimeReturnDuration, + test.pingRepoCalculateDowntimeError, + ) + downtimeRepoMock := mocks.DowntimeRepository{} + downtimeRepoMock.On("FindDowntimesInsideInterval", + test.nodeId, mock.Anything, mock.Anything, + ).Return( + test.downtimeRepoFindDowntimesInsideIntervalReturns, + test.downtimeRepoFindDowntimesInsideIntervalError, + ) + payoutRepoMock := mocks.PayoutRepository{} + payoutRepoMock.On("FindLatestPayout").Return( + test.payoutRepoFindLatestPayoutReturns, + test.payoutRepoFindLatestPayoutError, + ) + payoutRepoMock.On("Save", mock.Anything).Return(test.payoutRepoSaveError) + + repos := repositories.Repos{ + NodeRepo: &nodeRepoMock, + PingRepo: &pingRepoMock, + RecordRepo: &recordRepoMock, + DowntimeRepo: &downtimeRepoMock, + PayoutRepo: &payoutRepoMock, + } + + statsForPayout, err := GetStatsForPayout(repos, now, test.shouldRecordPayout) + assert.Equal(t, test.getStatsForPayoutReturns, statsForPayout) + assert.Equal(t, test.getStatsForPayoutError, err) + // check if payout saved + payoutRepoMock.AssertNumberOfCalls(t, "Save", test.payoutRepoSaveNumOfCalls) + }) + } +} diff --git a/internal/repositories/downtime.go b/internal/repositories/downtime.go index a11cd4e3..1b24aa11 100644 --- a/internal/repositories/downtime.go +++ b/internal/repositories/downtime.go @@ -31,7 +31,7 @@ func (r *DowntimeRepo) Save(downtime *models.Downtime) error { func (r *DowntimeRepo) FindDowntimesInsideInterval(nodeID string, from time.Time, to time.Time) ([]models.Downtime, error) { var downtimes []models.Downtime err := r.db.Select(q.And( - q.Eq("id", nodeID), + q.Eq("NodeId", nodeID), q.Or( q.And( // start inside interval q.Gte("start", from), @@ -42,7 +42,6 @@ func (r *DowntimeRepo) FindDowntimesInsideInterval(nodeID string, from time.Time q.Lte("end", to), ), ), - )).Find(downtimes) + )).Find(&downtimes) return downtimes, err } - diff --git a/internal/repositories/record.go b/internal/repositories/record.go index 7e594023..4b577775 100644 --- a/internal/repositories/record.go +++ b/internal/repositories/record.go @@ -31,10 +31,10 @@ func (r *recordRepo) Save(record *models.Record) error { func (r *recordRepo) FindSuccessfulRecordsInsideInterval(nodeID string, from time.Time, to time.Time) ([]models.Record, error) { var records []models.Record err := r.db.Select(q.And( - q.Eq("id", nodeID), - q.Gte("timestamp", from), - q.Lte("timestamp", to), - q.Eq("status", "successful"), + q.Eq("NodeId", nodeID), + q.Gte("Timestamp", from), + q.Lte("Timestamp", to), + q.Eq("Status", "successful"), )).Find(&records) return records, err } diff --git a/internal/stats/const.go b/internal/stats/const.go index 2a8e5b78..0f216881 100644 --- a/internal/stats/const.go +++ b/internal/stats/const.go @@ -3,4 +3,4 @@ package stats const ( // PingIntervalInSeconds PingIntervalInSeconds = 5 -) \ No newline at end of file +) diff --git a/internal/stats/interval.go b/internal/stats/interval.go index 1c9fdab8..7c60913f 100644 --- a/internal/stats/interval.go +++ b/internal/stats/interval.go @@ -5,17 +5,13 @@ import ( "time" ) -var nowFunc = time.Now - // GetIntervalFromLastPayout returns interval from last recorded payout until now as (intervalStart, intervalEnd, err) -func GetIntervalFromLastPayout(repos repositories.Repos) (*time.Time, *time.Time, error) { +func GetIntervalFromLastPayout(repos repositories.Repos) (*time.Time, error) { latestPayout, err := repos.PayoutRepo.FindLatestPayout() if err != nil { - return nil, nil, err + return nil, err } intervalStart := latestPayout.Timestamp - intervalEnd := nowFunc() - - return &intervalStart, &intervalEnd, err -} \ No newline at end of file + return &intervalStart, err +} diff --git a/internal/stats/interval_test.go b/internal/stats/interval_test.go index 0c964a18..75dd1fb4 100644 --- a/internal/stats/interval_test.go +++ b/internal/stats/interval_test.go @@ -11,38 +11,31 @@ import ( ) func Test_GetIntervalFromLastPayout(t *testing.T) { - now := time.Now() - hourAgo := now.Add(-24 * time.Hour) - nowFunc = func() time.Time { - return now - } + hourAgo := time.Now().Add(-24 * time.Hour) tests := []struct { - name string - payoutRepoFindLatestPayoutReturns *models.Payout - payoutRepoFindLatestPayoutError error + name string + payoutRepoFindLatestPayoutReturns *models.Payout + payoutRepoFindLatestPayoutError error getIntervalFromLastPayoutIntervalStart *time.Time - getIntervalFromLastPayoutIntervalEnd *time.Time - getIntervalFromLastPayoutError error + getIntervalFromLastPayoutError error }{ { name: "calculate interval from existing last payment to now", payoutRepoFindLatestPayoutReturns: &models.Payout{ - Timestamp: now.Add(-24 * time.Hour), + Timestamp: hourAgo, PaymentDetails: nil, }, - payoutRepoFindLatestPayoutError: nil, + payoutRepoFindLatestPayoutError: nil, getIntervalFromLastPayoutIntervalStart: &hourAgo, - getIntervalFromLastPayoutIntervalEnd: &now, - getIntervalFromLastPayoutError: nil, + getIntervalFromLastPayoutError: nil, }, { - name: "calculate interval from existing last payment to now", - payoutRepoFindLatestPayoutReturns: nil, - payoutRepoFindLatestPayoutError: errors.New("db error"), + name: "calculate interval from existing last payment to now", + payoutRepoFindLatestPayoutReturns: nil, + payoutRepoFindLatestPayoutError: errors.New("db error"), getIntervalFromLastPayoutIntervalStart: nil, - getIntervalFromLastPayoutIntervalEnd: nil, - getIntervalFromLastPayoutError: errors.New("db error"), + getIntervalFromLastPayoutError: errors.New("db error"), }, } for _, test := range tests { @@ -53,15 +46,12 @@ func Test_GetIntervalFromLastPayout(t *testing.T) { test.payoutRepoFindLatestPayoutError, ) repos := repositories.Repos{ - PayoutRepo: &payoutRepoMock, + PayoutRepo: &payoutRepoMock, } - intervalStart, intervalEnd, err := GetIntervalFromLastPayout(repos) + intervalStart, err := GetIntervalFromLastPayout(repos) assert.Equal(t, test.getIntervalFromLastPayoutIntervalStart, intervalStart) - assert.Equal(t, test.getIntervalFromLastPayoutIntervalEnd, intervalEnd) assert.Equal(t, test.getIntervalFromLastPayoutError, err) }) } - - nowFunc = time.Now } diff --git a/internal/stats/stats.go b/internal/stats/stats.go index 1412c4d3..96f1ac2c 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -9,22 +9,22 @@ import ( // CalculateStatisticsFromLastPayout calculates stats for all nodes for interval, that starts from last recorded payout // until now, as map[string]models.NodeStatsDetails where keys represent node id-s -func CalculateStatisticsFromLastPayout(repos repositories.Repos) (map[string]models.NodeStatsDetails, error) { - intervalStart, intervalEnd, err := GetIntervalFromLastPayout(repos) +func CalculateStatisticsFromLastPayout(repos repositories.Repos, intervalEnd time.Time) (map[string]models.NodeStatsDetails, error) { + intervalStart, err := GetIntervalFromLastPayout(repos) if err != nil { return nil, err } - return CalculateStatisticsForInterval(repos, *intervalStart, *intervalEnd) + return CalculateStatisticsForInterval(repos, *intervalStart, intervalEnd) } // CalculateNodeStatisticsFromLastPayout calculates stats for specific node for interval, that starts from last recorded payout // until now, as models.NodeStatsDetails where node is specified with argument nodeId -func CalculateNodeStatisticsFromLastPayout(repos repositories.Repos, nodeId string) (*models.NodeStatsDetails, error) { - intervalStart, intervalEnd, err := GetIntervalFromLastPayout(repos) +func CalculateNodeStatisticsFromLastPayout(repos repositories.Repos, nodeId string, intervalEnd time.Time) (*models.NodeStatsDetails, error) { + intervalStart, err := GetIntervalFromLastPayout(repos) if err != nil { return nil, err } - return CalculateNodeStatisticsForInterval(repos, nodeId, *intervalStart, *intervalEnd) + return CalculateNodeStatisticsForInterval(repos, nodeId, *intervalStart, intervalEnd) } // CalculateStatisticsForInterval calculates stats for all nodes for interval, specified with arguments @@ -83,5 +83,3 @@ func CalculateNodeStatisticsForInterval( TotalRequests: float64(len(recordsInInterval)), }, nil } - - diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go index a4bbd45a..2958f440 100644 --- a/internal/stats/stats_test.go +++ b/internal/stats/stats_test.go @@ -12,9 +12,7 @@ import ( func Test_CalculateNodeStatisticsFromLastPayout(t *testing.T) { now := time.Now() - nowFunc = func() time.Time { - return now - } + tests := []struct { name string nodeID string @@ -122,7 +120,7 @@ func Test_CalculateNodeStatisticsFromLastPayout(t *testing.T) { PayoutRepo: &payoutRepoMock, } - statisticsForPayout, err := CalculateNodeStatisticsFromLastPayout(repos, test.nodeID) + statisticsForPayout, err := CalculateNodeStatisticsFromLastPayout(repos, test.nodeID, test.intervalEnd) assert.Equal(t, test.calculateNodeStatisticsFromLastPayoutError, err) assert.Equal(t, test.calculateNodeStatisticsFromLastPayoutReturns, statisticsForPayout) @@ -145,14 +143,10 @@ func Test_CalculateNodeStatisticsFromLastPayout(t *testing.T) { ) }) } - nowFunc = time.Now } func Test_CalculateStatisticsFromLastPayout(t *testing.T) { now := time.Now() - nowFunc = func() time.Time { - return now - } tests := []struct { name string @@ -232,12 +226,12 @@ func Test_CalculateStatisticsFromLastPayout(t *testing.T) { intervalStart: now.Add(-24 * time.Hour), intervalEnd: now, // PayoutRepo.FindLatestPayout - payoutRepoFindLatestPayoutReturns: nil, + payoutRepoFindLatestPayoutReturns: nil, payoutRepoFindLatestPayoutError: errors.New("db error"), payoutRepoFindLatestPayoutNumOfCalls: 1, // CalculateNodeStatisticsForInterval calculateStatisticsFromLastPayoutReturns: nil, - calculateStatisticsFromLastPayoutError: errors.New("db error"), + calculateStatisticsFromLastPayoutError: errors.New("db error"), }, } @@ -283,7 +277,7 @@ func Test_CalculateStatisticsFromLastPayout(t *testing.T) { PayoutRepo: &payoutRepoMock, } - statisticsForPayout, err := CalculateStatisticsFromLastPayout(repos) + statisticsForPayout, err := CalculateStatisticsFromLastPayout(repos, test.intervalEnd) assert.Equal(t, test.calculateStatisticsFromLastPayoutError, err) assert.Equal(t, test.calculateStatisticsFromLastPayoutReturns, statisticsForPayout) @@ -306,8 +300,6 @@ func Test_CalculateStatisticsFromLastPayout(t *testing.T) { ) }) } - - nowFunc = time.Now } func Test_CalculateNodeStatisticsForInterval(t *testing.T) {