* Use different pool hostnames for NTP timesource * Make govet happy * Add check for empty servers list
This commit is contained in:
parent
9e65b5a6ae
commit
bd68fa15c9
|
@ -13,13 +13,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// DefaultServer will be internally resolved to the closest available.
|
|
||||||
// also it rarely queries same server more than once.
|
|
||||||
DefaultServer = "pool.ntp.org"
|
|
||||||
|
|
||||||
// DefaultAttempts defines how many servers we will query
|
|
||||||
DefaultAttempts = 5
|
|
||||||
|
|
||||||
// DefaultMaxAllowedFailures defines how many failures will be tolerated.
|
// DefaultMaxAllowedFailures defines how many failures will be tolerated.
|
||||||
DefaultMaxAllowedFailures = 2
|
DefaultMaxAllowedFailures = 2
|
||||||
|
|
||||||
|
@ -30,6 +23,15 @@ const (
|
||||||
DefaultRPCTimeout = 2 * time.Second
|
DefaultRPCTimeout = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// defaultServers will be resolved to the closest available,
|
||||||
|
// and with high probability resolved to the different IPs
|
||||||
|
var defaultServers = []string{
|
||||||
|
"0.pool.ntp.org",
|
||||||
|
"1.pool.ntp.org",
|
||||||
|
"2.pool.ntp.org",
|
||||||
|
"3.pool.ntp.org",
|
||||||
|
}
|
||||||
|
|
||||||
type ntpQuery func(string, ntp.QueryOptions) (*ntp.Response, error)
|
type ntpQuery func(string, ntp.QueryOptions) (*ntp.Response, error)
|
||||||
|
|
||||||
type queryResponse struct {
|
type queryResponse struct {
|
||||||
|
@ -54,13 +56,13 @@ func (e multiRPCError) Error() string {
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeOffset(timeQuery ntpQuery, server string, attempts, allowedFailures int) (time.Duration, error) {
|
func computeOffset(timeQuery ntpQuery, servers []string, allowedFailures int) (time.Duration, error) {
|
||||||
if attempts == 0 {
|
if len(servers) == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
responses := make(chan queryResponse, attempts)
|
responses := make(chan queryResponse, len(servers))
|
||||||
for i := 0; i < attempts; i++ {
|
for _, server := range servers {
|
||||||
go func() {
|
go func(server string) {
|
||||||
response, err := timeQuery(server, ntp.QueryOptions{
|
response, err := timeQuery(server, ntp.QueryOptions{
|
||||||
Timeout: DefaultRPCTimeout,
|
Timeout: DefaultRPCTimeout,
|
||||||
})
|
})
|
||||||
|
@ -69,7 +71,7 @@ func computeOffset(timeQuery ntpQuery, server string, attempts, allowedFailures
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
responses <- queryResponse{Offset: response.ClockOffset}
|
responses <- queryResponse{Offset: response.ClockOffset}
|
||||||
}()
|
}(server)
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
rpcErrors multiRPCError
|
rpcErrors multiRPCError
|
||||||
|
@ -83,19 +85,19 @@ func computeOffset(timeQuery ntpQuery, server string, attempts, allowedFailures
|
||||||
offsets = append(offsets, response.Offset)
|
offsets = append(offsets, response.Offset)
|
||||||
}
|
}
|
||||||
collected++
|
collected++
|
||||||
if collected == attempts {
|
if collected == len(servers) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lth := len(rpcErrors); lth > allowedFailures {
|
if lth := len(rpcErrors); lth > allowedFailures {
|
||||||
return 0, rpcErrors
|
return 0, rpcErrors
|
||||||
} else if lth == attempts {
|
} else if lth == len(servers) {
|
||||||
return 0, rpcErrors
|
return 0, rpcErrors
|
||||||
}
|
}
|
||||||
sort.SliceStable(offsets, func(i, j int) bool {
|
sort.SliceStable(offsets, func(i, j int) bool {
|
||||||
return offsets[i] > offsets[j]
|
return offsets[i] > offsets[j]
|
||||||
})
|
})
|
||||||
mid := attempts / 2
|
mid := len(servers) / 2
|
||||||
if len(offsets)%2 == 0 {
|
if len(offsets)%2 == 0 {
|
||||||
return (offsets[mid-1] + offsets[mid]) / 2, nil
|
return (offsets[mid-1] + offsets[mid]) / 2, nil
|
||||||
}
|
}
|
||||||
|
@ -105,8 +107,7 @@ func computeOffset(timeQuery ntpQuery, server string, attempts, allowedFailures
|
||||||
// Default initializes time source with default config values.
|
// Default initializes time source with default config values.
|
||||||
func Default() *NTPTimeSource {
|
func Default() *NTPTimeSource {
|
||||||
return &NTPTimeSource{
|
return &NTPTimeSource{
|
||||||
server: DefaultServer,
|
servers: defaultServers,
|
||||||
attempts: DefaultAttempts,
|
|
||||||
allowedFailures: DefaultMaxAllowedFailures,
|
allowedFailures: DefaultMaxAllowedFailures,
|
||||||
updatePeriod: DefaultUpdatePeriod,
|
updatePeriod: DefaultUpdatePeriod,
|
||||||
timeQuery: ntp.QueryWithOptions,
|
timeQuery: ntp.QueryWithOptions,
|
||||||
|
@ -116,8 +117,7 @@ func Default() *NTPTimeSource {
|
||||||
// NTPTimeSource provides source of time that tries to be resistant to time skews.
|
// NTPTimeSource provides source of time that tries to be resistant to time skews.
|
||||||
// It does so by periodically querying time offset from ntp servers.
|
// It does so by periodically querying time offset from ntp servers.
|
||||||
type NTPTimeSource struct {
|
type NTPTimeSource struct {
|
||||||
server string
|
servers []string
|
||||||
attempts int
|
|
||||||
allowedFailures int
|
allowedFailures int
|
||||||
updatePeriod time.Duration
|
updatePeriod time.Duration
|
||||||
timeQuery ntpQuery // for ease of testing
|
timeQuery ntpQuery // for ease of testing
|
||||||
|
@ -137,7 +137,7 @@ func (s *NTPTimeSource) Now() time.Time {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NTPTimeSource) updateOffset() {
|
func (s *NTPTimeSource) updateOffset() {
|
||||||
offset, err := computeOffset(s.timeQuery, s.server, s.attempts, s.allowedFailures)
|
offset, err := computeOffset(s.timeQuery, s.servers, s.allowedFailures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to compute offset", "error", err)
|
log.Error("failed to compute offset", "error", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -15,9 +15,13 @@ const (
|
||||||
clockCompareDelta = 30 * time.Microsecond
|
clockCompareDelta = 30 * time.Microsecond
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// we don't user real servers for tests, but logic depends on
|
||||||
|
// actual number of involved NTP servers.
|
||||||
|
var mockedServers = []string{"ntp1", "ntp2", "ntp3"}
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
description string
|
description string
|
||||||
attempts int
|
servers []string
|
||||||
allowedFailures int
|
allowedFailures int
|
||||||
responses []queryResponse
|
responses []queryResponse
|
||||||
expected time.Duration
|
expected time.Duration
|
||||||
|
@ -42,7 +46,7 @@ func newTestCases() []*testCase {
|
||||||
return []*testCase{
|
return []*testCase{
|
||||||
{
|
{
|
||||||
description: "SameResponse",
|
description: "SameResponse",
|
||||||
attempts: 3,
|
servers: mockedServers,
|
||||||
responses: []queryResponse{
|
responses: []queryResponse{
|
||||||
{Offset: 10 * time.Second},
|
{Offset: 10 * time.Second},
|
||||||
{Offset: 10 * time.Second},
|
{Offset: 10 * time.Second},
|
||||||
|
@ -52,7 +56,7 @@ func newTestCases() []*testCase {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Median",
|
description: "Median",
|
||||||
attempts: 3,
|
servers: mockedServers,
|
||||||
responses: []queryResponse{
|
responses: []queryResponse{
|
||||||
{Offset: 10 * time.Second},
|
{Offset: 10 * time.Second},
|
||||||
{Offset: 20 * time.Second},
|
{Offset: 20 * time.Second},
|
||||||
|
@ -62,7 +66,7 @@ func newTestCases() []*testCase {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "EvenMedian",
|
description: "EvenMedian",
|
||||||
attempts: 2,
|
servers: mockedServers[:2],
|
||||||
responses: []queryResponse{
|
responses: []queryResponse{
|
||||||
{Offset: 10 * time.Second},
|
{Offset: 10 * time.Second},
|
||||||
{Offset: 20 * time.Second},
|
{Offset: 20 * time.Second},
|
||||||
|
@ -71,7 +75,7 @@ func newTestCases() []*testCase {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Error",
|
description: "Error",
|
||||||
attempts: 3,
|
servers: mockedServers,
|
||||||
responses: []queryResponse{
|
responses: []queryResponse{
|
||||||
{Offset: 10 * time.Second},
|
{Offset: 10 * time.Second},
|
||||||
{Error: errors.New("test")},
|
{Error: errors.New("test")},
|
||||||
|
@ -82,7 +86,7 @@ func newTestCases() []*testCase {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "MultiError",
|
description: "MultiError",
|
||||||
attempts: 3,
|
servers: mockedServers,
|
||||||
responses: []queryResponse{
|
responses: []queryResponse{
|
||||||
{Error: errors.New("test 1")},
|
{Error: errors.New("test 1")},
|
||||||
{Error: errors.New("test 2")},
|
{Error: errors.New("test 2")},
|
||||||
|
@ -93,7 +97,7 @@ func newTestCases() []*testCase {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "TolerableError",
|
description: "TolerableError",
|
||||||
attempts: 3,
|
servers: mockedServers,
|
||||||
allowedFailures: 1,
|
allowedFailures: 1,
|
||||||
responses: []queryResponse{
|
responses: []queryResponse{
|
||||||
{Offset: 10 * time.Second},
|
{Offset: 10 * time.Second},
|
||||||
|
@ -104,7 +108,7 @@ func newTestCases() []*testCase {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "NonTolerableError",
|
description: "NonTolerableError",
|
||||||
attempts: 3,
|
servers: mockedServers,
|
||||||
allowedFailures: 1,
|
allowedFailures: 1,
|
||||||
responses: []queryResponse{
|
responses: []queryResponse{
|
||||||
{Offset: 10 * time.Second},
|
{Offset: 10 * time.Second},
|
||||||
|
@ -116,7 +120,7 @@ func newTestCases() []*testCase {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "AllFailed",
|
description: "AllFailed",
|
||||||
attempts: 3,
|
servers: mockedServers,
|
||||||
allowedFailures: 3,
|
allowedFailures: 3,
|
||||||
responses: []queryResponse{
|
responses: []queryResponse{
|
||||||
{Error: errors.New("test")},
|
{Error: errors.New("test")},
|
||||||
|
@ -132,7 +136,7 @@ func newTestCases() []*testCase {
|
||||||
func TestComputeOffset(t *testing.T) {
|
func TestComputeOffset(t *testing.T) {
|
||||||
for _, tc := range newTestCases() {
|
for _, tc := range newTestCases() {
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
offset, err := computeOffset(tc.query, "", tc.attempts, tc.allowedFailures)
|
offset, err := computeOffset(tc.query, tc.servers, tc.allowedFailures)
|
||||||
if tc.expectError {
|
if tc.expectError {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -147,7 +151,7 @@ func TestNTPTimeSource(t *testing.T) {
|
||||||
for _, tc := range newTestCases() {
|
for _, tc := range newTestCases() {
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
source := &NTPTimeSource{
|
source := &NTPTimeSource{
|
||||||
attempts: tc.attempts,
|
servers: tc.servers,
|
||||||
allowedFailures: tc.allowedFailures,
|
allowedFailures: tc.allowedFailures,
|
||||||
timeQuery: tc.query,
|
timeQuery: tc.query,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue