389 lines
9.2 KiB
Go
389 lines
9.2 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package submatview_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"net"
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sync/errgroup"
|
|
"google.golang.org/grpc"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/cache"
|
|
"github.com/hashicorp/consul/agent/consul/state"
|
|
"github.com/hashicorp/consul/agent/consul/stream"
|
|
"github.com/hashicorp/consul/agent/grpc-internal/services/subscribe"
|
|
"github.com/hashicorp/consul/agent/rpcclient/health"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/agent/submatview"
|
|
"github.com/hashicorp/consul/proto/private/pbsubscribe"
|
|
)
|
|
|
|
func TestStore_IntegrationWithBackend(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
var maxIndex uint64 = 200
|
|
count := &counter{latest: 3}
|
|
producers := map[string]*eventProducer{
|
|
state.EventSubjectService{Key: "srv1"}.String(): newEventProducer(pbsubscribe.Topic_ServiceHealth, "srv1", count, maxIndex),
|
|
state.EventSubjectService{Key: "srv2"}.String(): newEventProducer(pbsubscribe.Topic_ServiceHealth, "srv2", count, maxIndex),
|
|
state.EventSubjectService{Key: "srv3"}.String(): newEventProducer(pbsubscribe.Topic_ServiceHealth, "srv3", count, maxIndex),
|
|
}
|
|
|
|
sh := snapshotHandler{producers: producers}
|
|
pub := stream.NewEventPublisher(10 * time.Millisecond)
|
|
pub.RegisterHandler(pbsubscribe.Topic_ServiceHealth, sh.Snapshot, false)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go pub.Run(ctx)
|
|
|
|
store := submatview.NewStore(hclog.New(nil))
|
|
go store.Run(ctx)
|
|
|
|
addr := runServer(t, pub)
|
|
|
|
consumers := []*consumer{
|
|
newConsumer(t, addr, store, "srv1"),
|
|
newConsumer(t, addr, store, "srv1"),
|
|
newConsumer(t, addr, store, "srv1"),
|
|
newConsumer(t, addr, store, "srv2"),
|
|
newConsumer(t, addr, store, "srv2"),
|
|
newConsumer(t, addr, store, "srv2"),
|
|
}
|
|
|
|
group, gctx := errgroup.WithContext(ctx)
|
|
for i := range producers {
|
|
producer := producers[i]
|
|
group.Go(func() error {
|
|
producer.Produce(gctx, pub)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
for i := range consumers {
|
|
consumer := consumers[i]
|
|
group.Go(func() error {
|
|
return consumer.Consume(gctx, maxIndex)
|
|
})
|
|
}
|
|
|
|
_ = group.Wait()
|
|
|
|
for i, consumer := range consumers {
|
|
t.Run(fmt.Sprintf("consumer %d", i), func(t *testing.T) {
|
|
require.True(t, len(consumer.states) > 2, "expected more than %d events", len(consumer.states))
|
|
|
|
expected := producers[state.EventSubjectService{Key: consumer.srvName}.String()].nodesByIndex
|
|
for idx, nodes := range consumer.states {
|
|
assertDeepEqual(t, idx, expected[idx], nodes)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertDeepEqual(t *testing.T, idx uint64, x, y interface{}) {
|
|
t.Helper()
|
|
if diff := cmp.Diff(x, y, cmpopts.EquateEmpty()); diff != "" {
|
|
t.Fatalf("assertion failed: values at index %d are not equal\n--- expected\n+++ actual\n%v", idx, diff)
|
|
}
|
|
}
|
|
|
|
func stateFromUpdates(u cache.UpdateEvent) []string {
|
|
var result []string
|
|
for _, node := range u.Result.(*structs.IndexedCheckServiceNodes).Nodes {
|
|
result = append(result, node.Node.Node)
|
|
}
|
|
|
|
sort.Strings(result)
|
|
return result
|
|
}
|
|
|
|
func runServer(t *testing.T, pub *stream.EventPublisher) net.Addr {
|
|
subSrv := &subscribe.Server{
|
|
Backend: backend{pub: pub},
|
|
Logger: hclog.New(nil),
|
|
}
|
|
srv := grpc.NewServer()
|
|
pbsubscribe.RegisterStateChangeSubscriptionServer(srv, subSrv)
|
|
|
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
var g errgroup.Group
|
|
g.Go(func() error {
|
|
return srv.Serve(lis)
|
|
})
|
|
t.Cleanup(func() {
|
|
srv.Stop()
|
|
if err := g.Wait(); err != nil {
|
|
t.Log(err.Error())
|
|
}
|
|
})
|
|
|
|
return lis.Addr()
|
|
}
|
|
|
|
type backend struct {
|
|
pub *stream.EventPublisher
|
|
}
|
|
|
|
func (b backend) ResolveTokenAndDefaultMeta(string, *acl.EnterpriseMeta, *acl.AuthorizerContext) (acl.Authorizer, error) {
|
|
return acl.AllowAll(), nil
|
|
}
|
|
|
|
func (b backend) Forward(structs.RPCInfo, func(*grpc.ClientConn) error) (handled bool, err error) {
|
|
return false, nil
|
|
}
|
|
|
|
func (b backend) Subscribe(req *stream.SubscribeRequest) (*stream.Subscription, error) {
|
|
return b.pub.Subscribe(req)
|
|
}
|
|
|
|
var _ subscribe.Backend = (*backend)(nil)
|
|
|
|
type eventProducer struct {
|
|
rand *rand.Rand
|
|
counter *counter
|
|
topic stream.Topic
|
|
srvName string
|
|
nodesByIndex map[uint64][]string
|
|
nodesLock sync.Mutex
|
|
maxIndex uint64
|
|
}
|
|
|
|
func newEventProducer(
|
|
topic stream.Topic,
|
|
srvName string,
|
|
counter *counter,
|
|
maxIndex uint64,
|
|
) *eventProducer {
|
|
return &eventProducer{
|
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
|
counter: counter,
|
|
nodesByIndex: map[uint64][]string{},
|
|
topic: topic,
|
|
srvName: srvName,
|
|
maxIndex: maxIndex,
|
|
}
|
|
}
|
|
|
|
var minEventDelay = 5 * time.Millisecond
|
|
|
|
func (e *eventProducer) Produce(ctx context.Context, pub *stream.EventPublisher) {
|
|
var nodes []string
|
|
var nextID int
|
|
|
|
for ctx.Err() == nil {
|
|
var event stream.Event
|
|
|
|
action := e.rand.Intn(3)
|
|
if len(nodes) == 0 {
|
|
action = 1
|
|
}
|
|
|
|
idx := e.counter.Next()
|
|
switch action {
|
|
|
|
case 0: // Deregister
|
|
nodeIdx := e.rand.Intn(len(nodes))
|
|
node := nodes[nodeIdx]
|
|
nodes = append(nodes[:nodeIdx], nodes[nodeIdx+1:]...)
|
|
|
|
event = stream.Event{
|
|
Topic: e.topic,
|
|
Index: idx,
|
|
Payload: state.EventPayloadCheckServiceNode{
|
|
Op: pbsubscribe.CatalogOp_Deregister,
|
|
Value: &structs.CheckServiceNode{
|
|
Node: &structs.Node{Node: node},
|
|
Service: &structs.NodeService{
|
|
ID: e.srvName,
|
|
Service: e.srvName,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
case 1: // Register new
|
|
node := nodeName(nextID)
|
|
nodes = append(nodes, node)
|
|
nextID++
|
|
|
|
event = stream.Event{
|
|
Topic: e.topic,
|
|
Index: idx,
|
|
Payload: state.EventPayloadCheckServiceNode{
|
|
Op: pbsubscribe.CatalogOp_Register,
|
|
Value: &structs.CheckServiceNode{
|
|
Node: &structs.Node{Node: node},
|
|
Service: &structs.NodeService{
|
|
ID: e.srvName,
|
|
Service: e.srvName,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
case 2: // Register update
|
|
node := nodes[e.rand.Intn(len(nodes))]
|
|
event = stream.Event{
|
|
Topic: e.topic,
|
|
Index: idx,
|
|
Payload: state.EventPayloadCheckServiceNode{
|
|
Op: pbsubscribe.CatalogOp_Register,
|
|
Value: &structs.CheckServiceNode{
|
|
Node: &structs.Node{Node: node},
|
|
Service: &structs.NodeService{
|
|
ID: e.srvName,
|
|
Service: e.srvName,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
e.nodesLock.Lock()
|
|
pub.Publish([]stream.Event{event})
|
|
e.nodesByIndex[idx] = copyNodeList(nodes)
|
|
e.nodesLock.Unlock()
|
|
|
|
if idx > e.maxIndex {
|
|
return
|
|
}
|
|
|
|
delay := time.Duration(rand.Intn(25)) * time.Millisecond
|
|
time.Sleep(minEventDelay + delay)
|
|
}
|
|
}
|
|
|
|
func nodeName(i int) string {
|
|
return fmt.Sprintf("node-%d", i)
|
|
}
|
|
|
|
func copyNodeList(nodes []string) []string {
|
|
result := make([]string, len(nodes))
|
|
copy(result, nodes)
|
|
sort.Strings(result)
|
|
return result
|
|
}
|
|
|
|
type counter struct {
|
|
latest uint64
|
|
}
|
|
|
|
func (c *counter) Next() uint64 {
|
|
return atomic.AddUint64(&c.latest, 1)
|
|
}
|
|
|
|
type consumer struct {
|
|
healthClient *health.Client
|
|
states map[uint64][]string
|
|
srvName string
|
|
}
|
|
|
|
func newConsumer(t *testing.T, addr net.Addr, store *submatview.Store, srv string) *consumer {
|
|
//nolint:staticcheck
|
|
conn, err := grpc.Dial(addr.String(), grpc.WithInsecure())
|
|
require.NoError(t, err)
|
|
|
|
c := &health.Client{
|
|
UseStreamingBackend: true,
|
|
ViewStore: store,
|
|
MaterializerDeps: health.MaterializerDeps{
|
|
Conn: conn,
|
|
Logger: hclog.New(nil),
|
|
},
|
|
}
|
|
|
|
return &consumer{
|
|
healthClient: c,
|
|
states: make(map[uint64][]string),
|
|
srvName: srv,
|
|
}
|
|
}
|
|
|
|
func (c *consumer) Consume(ctx context.Context, maxIndex uint64) error {
|
|
req := structs.ServiceSpecificRequest{ServiceName: c.srvName}
|
|
updateCh := make(chan cache.UpdateEvent, 10)
|
|
|
|
group, cctx := errgroup.WithContext(ctx)
|
|
group.Go(func() error {
|
|
return c.healthClient.Notify(cctx, req, "", func(ctx context.Context, event cache.UpdateEvent) {
|
|
select {
|
|
case updateCh <- event:
|
|
case <-ctx.Done():
|
|
}
|
|
})
|
|
})
|
|
group.Go(func() error {
|
|
var idx uint64
|
|
for {
|
|
if idx >= maxIndex {
|
|
return nil
|
|
}
|
|
select {
|
|
case u := <-updateCh:
|
|
idx = u.Meta.Index
|
|
c.states[u.Meta.Index] = stateFromUpdates(u)
|
|
case <-cctx.Done():
|
|
return nil
|
|
}
|
|
}
|
|
})
|
|
return group.Wait()
|
|
}
|
|
|
|
type snapshotHandler struct {
|
|
producers map[string]*eventProducer
|
|
}
|
|
|
|
func (s *snapshotHandler) Snapshot(req stream.SubscribeRequest, buf stream.SnapshotAppender) (index uint64, err error) {
|
|
producer := s.producers[req.Subject.String()]
|
|
|
|
producer.nodesLock.Lock()
|
|
defer producer.nodesLock.Unlock()
|
|
idx := atomic.LoadUint64(&producer.counter.latest)
|
|
|
|
// look backwards for an index that was used by the producer
|
|
nodes, ok := producer.nodesByIndex[idx]
|
|
for !ok && idx > 0 {
|
|
idx--
|
|
nodes, ok = producer.nodesByIndex[idx]
|
|
}
|
|
|
|
for _, node := range nodes {
|
|
event := stream.Event{
|
|
Topic: producer.topic,
|
|
Index: idx,
|
|
Payload: state.EventPayloadCheckServiceNode{
|
|
Op: pbsubscribe.CatalogOp_Register,
|
|
Value: &structs.CheckServiceNode{
|
|
Node: &structs.Node{Node: node},
|
|
Service: &structs.NodeService{
|
|
ID: producer.srvName,
|
|
Service: producer.srvName,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
buf.Append([]stream.Event{event})
|
|
}
|
|
return idx, nil
|
|
}
|