Speeding up health change detection by separating it from catalog services check.
This commit is contained in:
parent
9cb07d026f
commit
db1baf80a9
2 changed files with 240 additions and 23 deletions
|
@ -76,18 +76,25 @@ func (a nodeSorter) Less(i int, j int) bool {
|
||||||
return lentr.Service.Port < rentr.Service.Port
|
return lentr.Service.Port < rentr.Service.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CatalogProvider) watchServices(stopCh <-chan struct{}) <-chan map[string][]string {
|
func getChangedKeys(currState map[string][]string, prevState map[string][]string) ([]string, []string) {
|
||||||
watchCh := make(chan map[string][]string)
|
currKeySet := fun.Set(fun.Keys(currState).([]string)).(map[string]bool)
|
||||||
|
prevKeySet := fun.Set(fun.Keys(prevState).([]string)).(map[string]bool)
|
||||||
|
|
||||||
catalog := p.client.Catalog()
|
addedKeys := fun.Difference(currKeySet, prevKeySet).(map[string]bool)
|
||||||
|
removedKeys := fun.Difference(prevKeySet, currKeySet).(map[string]bool)
|
||||||
|
|
||||||
|
return fun.Keys(addedKeys).([]string), fun.Keys(removedKeys).([]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogProvider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[string][]string) {
|
||||||
health := p.client.Health()
|
health := p.client.Health()
|
||||||
var lastHealthIndex uint64
|
catalog := p.client.Catalog()
|
||||||
|
|
||||||
safe.Go(func() {
|
safe.Go(func() {
|
||||||
defer close(watchCh)
|
// variable to hold previous state
|
||||||
|
var flashback map[string][]string
|
||||||
|
|
||||||
catalogOptions := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
|
options := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
|
||||||
healthOptions := &api.QueryOptions{}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -96,16 +103,10 @@ func (p *CatalogProvider) watchServices(stopCh <-chan struct{}) <-chan map[strin
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
data, catalogMeta, err := catalog.Services(catalogOptions)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("Failed to list services")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listening to changes that leads to `passing` state or degrades from it.
|
// Listening to changes that leads to `passing` state or degrades from it.
|
||||||
// The call is used just as a trigger for further actions
|
// The call is used just as a trigger for further actions
|
||||||
// (intentionally there is no interest in the received data).
|
// (intentionally there is no interest in the received data).
|
||||||
_, healthMeta, err := health.State("passing", healthOptions)
|
_, meta, err := health.State("passing", options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("Failed to retrieve health checks")
|
log.WithError(err).Error("Failed to retrieve health checks")
|
||||||
return
|
return
|
||||||
|
@ -113,21 +114,89 @@ func (p *CatalogProvider) watchServices(stopCh <-chan struct{}) <-chan map[strin
|
||||||
|
|
||||||
// If LastIndex didn't change then it means `Get` returned
|
// If LastIndex didn't change then it means `Get` returned
|
||||||
// because of the WaitTime and the key didn't changed.
|
// because of the WaitTime and the key didn't changed.
|
||||||
sameServiceAmount := catalogOptions.WaitIndex == catalogMeta.LastIndex
|
if options.WaitIndex == meta.LastIndex {
|
||||||
sameServiceHealth := lastHealthIndex == healthMeta.LastIndex
|
|
||||||
if sameServiceAmount && sameServiceHealth {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
catalogOptions.WaitIndex = catalogMeta.LastIndex
|
|
||||||
lastHealthIndex = healthMeta.LastIndex
|
options.WaitIndex = meta.LastIndex
|
||||||
|
|
||||||
|
// The response should be unified with watchCatalogServices
|
||||||
|
data, _, err := catalog.Services(&api.QueryOptions{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to list services: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if data != nil {
|
if data != nil {
|
||||||
watchCh <- data
|
// A critical note is that the return of a blocking request is no guarantee of a change.
|
||||||
|
// It is possible that there was an idempotent write that does not affect the result of the query.
|
||||||
|
// Thus it is required to do extra check for changes...
|
||||||
|
addedKeys, removedKeys := getChangedKeys(data, flashback)
|
||||||
|
|
||||||
|
if len(addedKeys) > 0 {
|
||||||
|
log.WithField("DiscoveredServices", addedKeys).Debug("Health State change detected.")
|
||||||
|
watchCh <- data
|
||||||
|
flashback = data
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(removedKeys) > 0 {
|
||||||
|
log.WithField("MissingServices", removedKeys).Debug("Health State change detected.")
|
||||||
|
watchCh <- data
|
||||||
|
flashback = data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return watchCh
|
func (p *CatalogProvider) watchCatalogServices(stopCh <-chan struct{}, watchCh chan<- map[string][]string) {
|
||||||
|
catalog := p.client.Catalog()
|
||||||
|
|
||||||
|
safe.Go(func() {
|
||||||
|
// variable to hold previous state
|
||||||
|
var flashback map[string][]string
|
||||||
|
|
||||||
|
options := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stopCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
data, meta, err := catalog.Services(options)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to list services: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.WaitIndex == meta.LastIndex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
options.WaitIndex = meta.LastIndex
|
||||||
|
|
||||||
|
if data != nil {
|
||||||
|
// A critical note is that the return of a blocking request is no guarantee of a change.
|
||||||
|
// It is possible that there was an idempotent write that does not affect the result of the query.
|
||||||
|
// Thus it is required to do extra check for changes...
|
||||||
|
addedKeys, removedKeys := getChangedKeys(data, flashback)
|
||||||
|
|
||||||
|
if len(addedKeys) > 0 {
|
||||||
|
log.WithField("DiscoveredServices", addedKeys).Debug("Catalog Services change detected.")
|
||||||
|
watchCh <- data
|
||||||
|
flashback = data
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(removedKeys) > 0 {
|
||||||
|
log.WithField("MissingServices", removedKeys).Debug("Catalog Services change detected.")
|
||||||
|
watchCh <- data
|
||||||
|
flashback = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) {
|
func (p *CatalogProvider) healthyNodes(service string) (catalogUpdate, error) {
|
||||||
|
@ -357,15 +426,19 @@ func (p *CatalogProvider) getNodes(index map[string][]string) ([]catalogUpdate,
|
||||||
|
|
||||||
func (p *CatalogProvider) watch(configurationChan chan<- types.ConfigMessage, stop chan bool) error {
|
func (p *CatalogProvider) watch(configurationChan chan<- types.ConfigMessage, stop chan bool) error {
|
||||||
stopCh := make(chan struct{})
|
stopCh := make(chan struct{})
|
||||||
serviceCatalog := p.watchServices(stopCh)
|
watchCh := make(chan map[string][]string)
|
||||||
|
|
||||||
|
p.watchHealthState(stopCh, watchCh)
|
||||||
|
p.watchCatalogServices(stopCh, watchCh)
|
||||||
|
|
||||||
defer close(stopCh)
|
defer close(stopCh)
|
||||||
|
defer close(watchCh)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-stop:
|
case <-stop:
|
||||||
return nil
|
return nil
|
||||||
case index, ok := <-serviceCatalog:
|
case index, ok := <-watchCh:
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("Consul service list nil")
|
return errors.New("Consul service list nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/ty/fun"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
)
|
)
|
||||||
|
@ -606,3 +607,146 @@ func TestConsulCatalogNodeSorter(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConsulCatalogGetChangedKeys(t *testing.T) {
|
||||||
|
type Input struct {
|
||||||
|
currState map[string][]string
|
||||||
|
prevState map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Output struct {
|
||||||
|
addedKeys []string
|
||||||
|
removedKeys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
input Input
|
||||||
|
output Output
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: Input{
|
||||||
|
currState: map[string][]string{
|
||||||
|
"foo-service": {"v1"},
|
||||||
|
"bar-service": {"v1"},
|
||||||
|
"baz-service": {"v1"},
|
||||||
|
"qux-service": {"v1"},
|
||||||
|
"quux-service": {"v1"},
|
||||||
|
"quuz-service": {"v1"},
|
||||||
|
"corge-service": {"v1"},
|
||||||
|
"grault-service": {"v1"},
|
||||||
|
"garply-service": {"v1"},
|
||||||
|
"waldo-service": {"v1"},
|
||||||
|
"fred-service": {"v1"},
|
||||||
|
"plugh-service": {"v1"},
|
||||||
|
"xyzzy-service": {"v1"},
|
||||||
|
"thud-service": {"v1"},
|
||||||
|
},
|
||||||
|
prevState: map[string][]string{
|
||||||
|
"foo-service": {"v1"},
|
||||||
|
"bar-service": {"v1"},
|
||||||
|
"baz-service": {"v1"},
|
||||||
|
"qux-service": {"v1"},
|
||||||
|
"quux-service": {"v1"},
|
||||||
|
"quuz-service": {"v1"},
|
||||||
|
"corge-service": {"v1"},
|
||||||
|
"grault-service": {"v1"},
|
||||||
|
"garply-service": {"v1"},
|
||||||
|
"waldo-service": {"v1"},
|
||||||
|
"fred-service": {"v1"},
|
||||||
|
"plugh-service": {"v1"},
|
||||||
|
"xyzzy-service": {"v1"},
|
||||||
|
"thud-service": {"v1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: Output{
|
||||||
|
addedKeys: []string{},
|
||||||
|
removedKeys: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: Input{
|
||||||
|
currState: map[string][]string{
|
||||||
|
"foo-service": {"v1"},
|
||||||
|
"bar-service": {"v1"},
|
||||||
|
"baz-service": {"v1"},
|
||||||
|
"qux-service": {"v1"},
|
||||||
|
"quux-service": {"v1"},
|
||||||
|
"quuz-service": {"v1"},
|
||||||
|
"corge-service": {"v1"},
|
||||||
|
"grault-service": {"v1"},
|
||||||
|
"garply-service": {"v1"},
|
||||||
|
"waldo-service": {"v1"},
|
||||||
|
"fred-service": {"v1"},
|
||||||
|
"plugh-service": {"v1"},
|
||||||
|
"xyzzy-service": {"v1"},
|
||||||
|
"thud-service": {"v1"},
|
||||||
|
},
|
||||||
|
prevState: map[string][]string{
|
||||||
|
"foo-service": {"v1"},
|
||||||
|
"bar-service": {"v1"},
|
||||||
|
"baz-service": {"v1"},
|
||||||
|
"corge-service": {"v1"},
|
||||||
|
"grault-service": {"v1"},
|
||||||
|
"garply-service": {"v1"},
|
||||||
|
"waldo-service": {"v1"},
|
||||||
|
"fred-service": {"v1"},
|
||||||
|
"plugh-service": {"v1"},
|
||||||
|
"xyzzy-service": {"v1"},
|
||||||
|
"thud-service": {"v1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: Output{
|
||||||
|
addedKeys: []string{"qux-service", "quux-service", "quuz-service"},
|
||||||
|
removedKeys: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: Input{
|
||||||
|
currState: map[string][]string{
|
||||||
|
"foo-service": {"v1"},
|
||||||
|
"qux-service": {"v1"},
|
||||||
|
"quux-service": {"v1"},
|
||||||
|
"quuz-service": {"v1"},
|
||||||
|
"corge-service": {"v1"},
|
||||||
|
"grault-service": {"v1"},
|
||||||
|
"garply-service": {"v1"},
|
||||||
|
"waldo-service": {"v1"},
|
||||||
|
"fred-service": {"v1"},
|
||||||
|
"plugh-service": {"v1"},
|
||||||
|
"xyzzy-service": {"v1"},
|
||||||
|
"thud-service": {"v1"},
|
||||||
|
},
|
||||||
|
prevState: map[string][]string{
|
||||||
|
"foo-service": {"v1"},
|
||||||
|
"bar-service": {"v1"},
|
||||||
|
"baz-service": {"v1"},
|
||||||
|
"qux-service": {"v1"},
|
||||||
|
"quux-service": {"v1"},
|
||||||
|
"quuz-service": {"v1"},
|
||||||
|
"corge-service": {"v1"},
|
||||||
|
"waldo-service": {"v1"},
|
||||||
|
"fred-service": {"v1"},
|
||||||
|
"plugh-service": {"v1"},
|
||||||
|
"xyzzy-service": {"v1"},
|
||||||
|
"thud-service": {"v1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: Output{
|
||||||
|
addedKeys: []string{"grault-service", "garply-service"},
|
||||||
|
removedKeys: []string{"bar-service", "baz-service"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
addedKeys, removedKeys := getChangedKeys(c.input.currState, c.input.prevState)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(fun.Set(addedKeys), fun.Set(c.output.addedKeys)) {
|
||||||
|
t.Fatalf("Added keys comparison results: got %q, want %q", addedKeys, c.output.addedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(fun.Set(removedKeys), fun.Set(c.output.removedKeys)) {
|
||||||
|
t.Fatalf("Removed keys comparison results: got %q, want %q", removedKeys, c.output.removedKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue