-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
core/scheduler: trigger duty at slot offset #516
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Copyright © 2022 Obol Labs Inc. | ||
// | ||
// This program is free software: you can redistribute it and/or modify it | ||
// under the terms of the GNU General Public License as published by the Free | ||
// Software Foundation, either version 3 of the License, or (at your option) | ||
// any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, but WITHOUT | ||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
// more details. | ||
// | ||
// You should have received a copy of the GNU General Public License along with | ||
// this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
package scheduler | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/obolnetwork/charon/core" | ||
) | ||
|
||
// slotOffsets defines the offsets | ||
// at which duties should be triggered. | ||
var slotOffsets = map[core.DutyType]func(time.Duration) time.Duration{ | ||
core.DutyAttester: fraction(1, 3), // 1/3 slot duration | ||
// TODO(corver): Add more duties | ||
} | ||
|
||
// fraction returns a function that calculates slot offset | ||
// based on the fraction x/y of total slot duration. | ||
func fraction(x, y int64) func(time.Duration) time.Duration { | ||
return func(total time.Duration) time.Duration { | ||
return (total * time.Duration(x)) / time.Duration(y) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,8 @@ import ( | |
"encoding/json" | ||
"flag" | ||
"os" | ||
"sort" | ||
"sync" | ||
"testing" | ||
"time" | ||
|
||
|
@@ -159,7 +161,7 @@ func TestSchedulerWait(t *testing.T) { | |
}, err | ||
} | ||
|
||
sched := scheduler.NewForT(t, clock, nil, eth2Cl) | ||
sched := scheduler.NewForT(t, clock, new(delayer).Delay, nil, eth2Cl) | ||
sched.Stop() // Just run wait functions, then quit. | ||
require.NoError(t, sched.Run()) | ||
require.EqualValues(t, test.WaitSecs, clock.Since(t0).Seconds()) | ||
|
@@ -175,29 +177,35 @@ func TestSchedulerDuties(t *testing.T) { | |
Name string | ||
Factor int // Determines how duties are spread per epoch | ||
PropErrs int | ||
Results int | ||
}{ | ||
{ | ||
// All duties grouped in first slot of epoch | ||
Name: "grouped", | ||
Factor: 0, | ||
Name: "grouped", | ||
Factor: 0, | ||
Results: 2, | ||
}, | ||
{ | ||
// All duties spread in first N slots of epoch (N is number of validators) | ||
Name: "spread", | ||
Factor: 1, | ||
Name: "spread", | ||
Factor: 1, | ||
Results: 6, | ||
}, | ||
{ | ||
// All duties spread in first N slots of epoch (except first proposer errors) | ||
Name: "spread_errors", | ||
Factor: 1, | ||
PropErrs: 1, | ||
Results: 5, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.Name, func(t *testing.T) { | ||
// Configure beacon mock | ||
var t0 time.Time | ||
t0 = t0.Add(time.Minute * 8) // Nice round slot numbers. | ||
|
||
valSet := beaconmock.ValidatorSetA | ||
eth2Cl, err := beaconmock.New( | ||
beaconmock.WithValidatorSet(valSet), | ||
|
@@ -224,23 +232,28 @@ func TestSchedulerDuties(t *testing.T) { | |
|
||
// Construct scheduler | ||
clock := newTestClock(t0) | ||
sched := scheduler.NewForT(t, clock, pubkeys, eth2Cl) | ||
delayer := new(delayer) | ||
sched := scheduler.NewForT(t, clock, delayer.Delay, pubkeys, eth2Cl) | ||
|
||
// Only test scheduler output for first N slots, so Stop scheduler (and slotTicker) after that. | ||
const stopAfter = 3 | ||
slotDuration, err := eth2Cl.SlotDuration(context.Background()) | ||
require.NoError(t, err) | ||
clock.CallbackAfter(t0.Add(time.Duration(stopAfter)*slotDuration), func() { | ||
sched.Stop() | ||
time.Sleep(time.Hour) // Do not let the slot ticker tick anymore. | ||
}) | ||
|
||
// Collect results | ||
type result struct { | ||
Duty string | ||
Time string | ||
DutyStr string `json:"duty"` | ||
Duty core.Duty `json:"-"` | ||
DutyArgSet map[core.PubKey]string | ||
} | ||
var results []result | ||
var ( | ||
results []result | ||
mu sync.Mutex | ||
) | ||
sched.Subscribe(func(ctx context.Context, duty core.Duty, set core.FetchArgSet) error { | ||
// Make result human-readable | ||
resultSet := make(map[core.PubKey]string) | ||
|
@@ -249,17 +262,39 @@ func TestSchedulerDuties(t *testing.T) { | |
} | ||
|
||
// Add result | ||
mu.Lock() | ||
defer mu.Unlock() | ||
|
||
results = append(results, result{ | ||
Duty: duty.String(), | ||
Duty: duty, | ||
DutyStr: duty.String(), | ||
DutyArgSet: resultSet, | ||
}) | ||
|
||
if len(results) == test.Results { | ||
sched.Stop() | ||
} | ||
|
||
return nil | ||
}) | ||
|
||
// Run scheduler | ||
require.NoError(t, sched.Run()) | ||
|
||
// Add deadlines to results | ||
deadlines := delayer.Get() | ||
for i := 0; i < len(results); i++ { | ||
results[i].Time = deadlines[results[i].Duty].Format("04:05.000") | ||
} | ||
// Make result order deterministic | ||
sort.Slice(results, func(i, j int) bool { | ||
if results[i].Duty.Slot == results[j].Duty.Slot { | ||
return results[i].Duty.Type < results[j].Duty.Type | ||
} | ||
|
||
return results[i].Duty.Slot < results[j].Duty.Slot | ||
}) | ||
|
||
// Assert results | ||
testutil.RequireGoldenJSON(t, results) | ||
}) | ||
|
@@ -283,7 +318,7 @@ func TestScheduler_GetDuty(t *testing.T) { | |
|
||
// Construct scheduler | ||
clock := newTestClock(t0) | ||
sched := scheduler.NewForT(t, clock, pubkeys, eth2Cl) | ||
sched := scheduler.NewForT(t, clock, new(delayer).Delay, pubkeys, eth2Cl) | ||
|
||
_, err = sched.GetDuty(context.Background(), core.Duty{Slot: 0, Type: core.DutyAttester}) | ||
// due to current design we will return an error if we request the duty of a slot that has not been resolved | ||
|
@@ -312,6 +347,34 @@ func TestScheduler_GetDuty(t *testing.T) { | |
require.NoError(t, sched.Run()) | ||
} | ||
|
||
// delayer implements scheduler.delayFunc and records the deadline and returns it immediately. | ||
type delayer struct { | ||
mu sync.Mutex | ||
deadlines map[core.Duty]time.Time | ||
} | ||
|
||
func (d *delayer) Get() map[core.Duty]time.Time { | ||
d.mu.Lock() | ||
defer d.mu.Unlock() | ||
|
||
return d.deadlines | ||
} | ||
|
||
// Delay implements scheduler.delayFunc and records the deadline and returns it immediately. | ||
func (d *delayer) Delay(duty core.Duty, deadline time.Time) <-chan time.Time { | ||
d.mu.Lock() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this method used or aimed to be used outside the package? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see comment above There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok made it unexported. |
||
defer d.mu.Unlock() | ||
if d.deadlines == nil { | ||
d.deadlines = make(map[core.Duty]time.Time) | ||
} | ||
d.deadlines[duty] = deadline | ||
|
||
resp := make(chan time.Time, 1) | ||
resp <- deadline | ||
|
||
return resp | ||
} | ||
|
||
func newTestClock(now time.Time) *testClock { | ||
return &testClock{ | ||
now: now, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this exported method used or aimed to used outside the package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed before, the type
delayer
isn't exported, so nothing is leaked. But the functionGet
is used by other things in this package. So then the method is capitalised. I prefer using private methods/fields for internal use within in a type only.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also since this is a test, it is impossible to use it from outside the package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hence, making it public does not give us an adventage, making it private does not requires more complex code but it keeps encapsulation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you make a method public you are documenting that the method is to be comsumed by other packages
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public methods in private structs does not provide encapsulation in Go as shown in the code https://github.com/leolara/privateExperiment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are in a loop.
These are facts, not subjective at all:
This is a common rule in Software Engineering:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here there is an example of code: https://willhaley.com/blog/private-and-public-visibility-with-go-packages/
a private struct with public method and private method, they both have a place in Go code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've documented my approach here: https://github.com/corverroos/go-visibilty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok made it unexported.