github.com/m-lab/locate@v0.17.6/heartbeat/heartbeat_test.go (about)

     1  package heartbeat
     2  
     3  import (
     4  	"errors"
     5  	"reflect"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/m-lab/locate/static"
    10  
    11  	"github.com/go-test/deep"
    12  	"github.com/m-lab/go/testingx"
    13  	v2 "github.com/m-lab/locate/api/v2"
    14  	"github.com/m-lab/locate/connection/testdata"
    15  	"github.com/m-lab/locate/heartbeat/heartbeattest"
    16  	"github.com/m-lab/locate/metrics"
    17  	prometheus "github.com/prometheus/client_model/go"
    18  )
    19  
    20  var (
    21  	fakeDC       = &heartbeattest.FakeMemorystoreClient
    22  	fakeErrDC    = &heartbeattest.FakeErrorMemorystoreClient
    23  	testMachine  = "mlab1-lga00.mlab-sandbox.measurement-lab.org"
    24  	testHostname = "ndt-" + testMachine
    25  )
    26  
    27  func TestRegisterInstance_PutError(t *testing.T) {
    28  	h := NewHeartbeatStatusTracker(fakeErrDC)
    29  	defer h.StopImport()
    30  
    31  	err := h.RegisterInstance(*testdata.FakeRegistration.Registration)
    32  
    33  	if !errors.Is(err, heartbeattest.FakeError) {
    34  		t.Errorf("RegisterInstance() error: %+v, want: %+v", err, heartbeattest.FakeError)
    35  	}
    36  }
    37  
    38  func TestRegisterInstance_Success(t *testing.T) {
    39  	h := NewHeartbeatStatusTracker(fakeDC)
    40  	defer h.StopImport()
    41  
    42  	hbm := testdata.FakeRegistration
    43  	err := h.RegisterInstance(*hbm.Registration)
    44  
    45  	if err != nil {
    46  		t.Errorf("RegisterInstance() error: %+v, want: nil", err)
    47  	}
    48  
    49  	if diff := deep.Equal(h.instances[hbm.Registration.Hostname], hbm); diff != nil {
    50  		t.Errorf("RegisterInstance() failed to register; got: %+v. want: %+v",
    51  			h.instances[hbm.Registration.Hostname], &hbm)
    52  	}
    53  }
    54  
    55  func TestRegisterInstanceTwice(t *testing.T) {
    56  	h := NewHeartbeatStatusTracker(fakeDC)
    57  	defer h.StopImport()
    58  
    59  	// Register once.
    60  	reg := testdata.FakeRegistration.Registration
    61  	err := h.RegisterInstance(*reg)
    62  	testingx.Must(t, err, "failed to register instance")
    63  
    64  	// Set health.
    65  	err = h.UpdateHealth(testdata.FakeHostname, v2.Health{Score: 1.0})
    66  	testingx.Must(t, err, "failed to update health")
    67  
    68  	// Re-register.
    69  	newReg := v2.Registration(*testdata.FakeRegistration.Registration)
    70  	newReg.Site = "foo"
    71  	err = h.RegisterInstance(newReg)
    72  
    73  	got := h.instances[reg.Hostname]
    74  	if got.Registration.Site != "foo" {
    75  		t.Errorf("RegisterInstance() failed to re-register; got: %+v, want: %+v", got.Registration.Site, "foo")
    76  	}
    77  
    78  	if got.Health.Score != 1.0 {
    79  		t.Errorf("RegisterInstance() changed health; got: %+v, want: %+v", got.Health.Score, "foo")
    80  	}
    81  }
    82  
    83  func TestUpdateHealth_UpdateError(t *testing.T) {
    84  	h := NewHeartbeatStatusTracker(fakeErrDC)
    85  	defer h.StopImport()
    86  
    87  	hm := testdata.FakeHealth.Health
    88  	err := h.UpdateHealth(testdata.FakeHostname, *hm)
    89  
    90  	if !errors.Is(err, heartbeattest.FakeError) {
    91  		t.Errorf("UpdateHealth() error: %+v, want: %+v", err, heartbeattest.FakeError)
    92  	}
    93  }
    94  
    95  func TestUpdateHealth_LocalError(t *testing.T) {
    96  	h := NewHeartbeatStatusTracker(fakeDC)
    97  	defer h.StopImport()
    98  
    99  	hm := testdata.FakeHealth.Health
   100  	err := h.UpdateHealth(testdata.FakeHostname, *hm)
   101  
   102  	if err == nil {
   103  		t.Error("UpdateHealth() error: nil, want: !nil")
   104  	}
   105  }
   106  
   107  func TestUpdateHealth_Success(t *testing.T) {
   108  	h := NewHeartbeatStatusTracker(fakeDC)
   109  	defer h.StopImport()
   110  
   111  	err := h.RegisterInstance(*testdata.FakeRegistration.Registration)
   112  	testingx.Must(t, err, "failed to register instance")
   113  
   114  	hm := testdata.FakeHealth.Health
   115  	err = h.UpdateHealth(testdata.FakeHostname, *hm)
   116  
   117  	if err != nil {
   118  		t.Errorf("UpdateHealth() error: %+v, want: !nil", err)
   119  	}
   120  
   121  	if diff := deep.Equal(h.instances[testdata.FakeHostname].Health, hm); diff != nil {
   122  		t.Errorf("UpdateHealth() failed to update health; got: %+v, want: %+v",
   123  			h.instances[testdata.FakeHostname].Health, hm)
   124  	}
   125  }
   126  
   127  func TestUpdatePrometheus_PutError(t *testing.T) {
   128  	h := heartbeatStatusTracker{
   129  		MemorystoreClient: fakeErrDC,
   130  		instances: map[string]v2.HeartbeatMessage{
   131  			testHostname: {
   132  				Registration: &v2.Registration{
   133  					Hostname: testHostname,
   134  				},
   135  			},
   136  		},
   137  	}
   138  	hostnames := map[string]bool{testHostname: true}
   139  	machines := map[string]bool{testMachine: true}
   140  
   141  	err := h.UpdatePrometheus(hostnames, machines)
   142  
   143  	if !errors.Is(err, errPrometheus) {
   144  		t.Errorf("UpdatePrometheus() err: %v, want: %v", err, errPrometheus)
   145  	}
   146  }
   147  
   148  func TestUpdatePrometheus_Success(t *testing.T) {
   149  	h := heartbeatStatusTracker{
   150  		MemorystoreClient: fakeDC,
   151  		instances: map[string]v2.HeartbeatMessage{
   152  			testHostname: {
   153  				Registration: &v2.Registration{
   154  					Hostname: testHostname,
   155  				},
   156  			},
   157  		},
   158  	}
   159  	hostnames := map[string]bool{testHostname: true}
   160  	machines := map[string]bool{testMachine: true}
   161  
   162  	err := h.UpdatePrometheus(hostnames, machines)
   163  
   164  	if err != nil {
   165  		t.Errorf("UpdatePrometheus() err: %v, want: nil", err)
   166  	}
   167  }
   168  
   169  func TestInstances(t *testing.T) {
   170  	h := NewHeartbeatStatusTracker(fakeDC)
   171  	h.StopImport()
   172  
   173  	hbm := testdata.FakeRegistration
   174  	h.RegisterInstance(*hbm.Registration)
   175  
   176  	instances := h.Instances()
   177  	expected := map[string]v2.HeartbeatMessage{testdata.FakeHostname: testdata.FakeRegistration}
   178  	if diff := deep.Equal(instances, expected); diff != nil {
   179  		t.Errorf("Instances() got: %+v, want: %+v", instances, expected)
   180  	}
   181  
   182  }
   183  
   184  func TestInstancesCopy(t *testing.T) {
   185  	h := NewHeartbeatStatusTracker(fakeDC)
   186  	h.StopImport()
   187  
   188  	// Add a new instance with nil v2.Health.
   189  	hbm := testdata.FakeRegistration
   190  	h.RegisterInstance(*hbm.Registration)
   191  
   192  	// Get copy of instances and verify that v2.Health field is nil.
   193  	instances := h.Instances()
   194  	if instances[testdata.FakeHostname].Health != nil {
   195  		t.Errorf("Instances() got: %+v, want: nil", instances[testdata.FakeHostname].Health)
   196  	}
   197  
   198  	// Update v2.Health for the instance in the tracker.
   199  	h.UpdateHealth(testdata.FakeHostname, *testdata.FakeHealth.Health)
   200  	instancesWithUpdate := h.Instances()
   201  	if instancesWithUpdate[testdata.FakeHostname].Health == nil {
   202  		t.Errorf("Instances() got: nil, want: %+v", instancesWithUpdate[testdata.FakeHostname].Health)
   203  	}
   204  
   205  	// Verify original copy of instances did not get updated.
   206  	if instances[testdata.FakeHostname].Health != nil {
   207  		t.Errorf("Instances() got: %+v, want: nil", instances[testdata.FakeHostname].Health)
   208  	}
   209  }
   210  
   211  func TestImportMemorystore(t *testing.T) {
   212  	fdc := &heartbeattest.FakeMemorystoreClient
   213  	h := NewHeartbeatStatusTracker(fdc)
   214  	if h.Ready() {
   215  		t.Errorf("importMemorystore() Ready too soon; got %s, want over: %s", time.Since(h.lastUpdate), 2*static.MemorystoreExportPeriod)
   216  	}
   217  	defer h.StopImport()
   218  
   219  	fdc.FakeAdd(testdata.FakeHostname, testdata.FakeRegistration)
   220  	h.importMemorystore()
   221  
   222  	expected := map[string]v2.HeartbeatMessage{testdata.FakeHostname: testdata.FakeRegistration}
   223  	if diff := deep.Equal(h.instances, expected); diff != nil {
   224  		t.Errorf("importMemorystore() failed to import; got: %+v, want: %+v", h.instances,
   225  			expected)
   226  	}
   227  
   228  	if !h.Ready() {
   229  		t.Errorf("importMemorystore() not Ready; got %s, want under: %s", time.Since(h.lastUpdate), 2*static.MemorystoreExportPeriod)
   230  	}
   231  }
   232  
   233  func TestUpdateMetrics(t *testing.T) {
   234  	tests := []struct {
   235  		name       string
   236  		instances  map[string]v2.HeartbeatMessage
   237  		experiment string
   238  		want       float64
   239  	}{
   240  		{
   241  			name: "success",
   242  			instances: map[string]v2.HeartbeatMessage{
   243  				testdata.FakeHostname: {
   244  					Registration: testdata.FakeRegistration.Registration,
   245  					Health:       testdata.FakeHealth.Health,
   246  				},
   247  			},
   248  			experiment: testdata.FakeRegistration.Registration.Experiment,
   249  			want:       1,
   250  		},
   251  		{
   252  			name:       "no-metrics",
   253  			instances:  map[string]v2.HeartbeatMessage{},
   254  			experiment: "",
   255  			want:       0,
   256  		},
   257  	}
   258  
   259  	for _, tt := range tests {
   260  		t.Run(tt.name, func(t *testing.T) {
   261  			h := heartbeatStatusTracker{
   262  				instances: tt.instances,
   263  			}
   264  
   265  			metrics.LocateHealthStatus.Reset()
   266  			h.updateMetrics()
   267  
   268  			metric := &prometheus.Metric{}
   269  			gauge := metrics.LocateHealthStatus.With(map[string]string{"experiment": tt.experiment})
   270  			gauge.Write(metric)
   271  			got := metric.GetGauge().GetValue()
   272  
   273  			if got != tt.want {
   274  				t.Errorf("updateMetrics() failed; got: %f want %f", got, tt.want)
   275  			}
   276  		})
   277  	}
   278  }
   279  
   280  func TestGetPrometheusMessage(t *testing.T) {
   281  	tests := []struct {
   282  		name      string
   283  		hostnames map[string]bool
   284  		machines  map[string]bool
   285  		reg       *v2.Registration
   286  		want      *v2.Prometheus
   287  	}{
   288  		{
   289  			name:      "nil-registration",
   290  			hostnames: map[string]bool{testHostname: true},
   291  			machines:  map[string]bool{testMachine: true},
   292  			reg:       nil,
   293  			want:      nil,
   294  		},
   295  		{
   296  			name:      "both-empty",
   297  			hostnames: map[string]bool{},
   298  			machines:  map[string]bool{},
   299  			reg: &v2.Registration{
   300  				Hostname: testHostname,
   301  			},
   302  			want: nil,
   303  		},
   304  		{
   305  			name:      "only-hostnames",
   306  			hostnames: map[string]bool{testHostname: true},
   307  			machines:  map[string]bool{},
   308  			reg: &v2.Registration{
   309  				Hostname: testHostname,
   310  			},
   311  			want: &v2.Prometheus{Health: true},
   312  		},
   313  		{
   314  			name:      "only-machines",
   315  			hostnames: map[string]bool{},
   316  			machines:  map[string]bool{testMachine: true},
   317  			reg: &v2.Registration{
   318  				Hostname: testHostname,
   319  			},
   320  			want: &v2.Prometheus{Health: true},
   321  		},
   322  		{
   323  			name:      "both-unhealthy",
   324  			hostnames: map[string]bool{testHostname: false},
   325  			machines:  map[string]bool{testMachine: false},
   326  			reg: &v2.Registration{
   327  				Hostname: testHostname,
   328  			},
   329  			want: &v2.Prometheus{Health: false},
   330  		},
   331  		{
   332  			name:      "only-hostname-unhealthy",
   333  			hostnames: map[string]bool{testHostname: false},
   334  			machines:  map[string]bool{testMachine: true},
   335  			reg: &v2.Registration{
   336  				Hostname: testHostname,
   337  			},
   338  			want: &v2.Prometheus{Health: false},
   339  		},
   340  		{
   341  			name:      "only-machine-unhealthy",
   342  			hostnames: map[string]bool{testHostname: true},
   343  			machines:  map[string]bool{testMachine: false},
   344  			reg: &v2.Registration{
   345  				Hostname: testHostname,
   346  			},
   347  			want: &v2.Prometheus{Health: false},
   348  		},
   349  		{
   350  			name:      "both-healthy",
   351  			hostnames: map[string]bool{testHostname: true},
   352  			machines:  map[string]bool{testMachine: true},
   353  			reg: &v2.Registration{
   354  				Hostname: testHostname,
   355  			},
   356  			want: &v2.Prometheus{Health: true},
   357  		},
   358  	}
   359  
   360  	for _, tt := range tests {
   361  		t.Run(tt.name, func(t *testing.T) {
   362  			i := v2.HeartbeatMessage{Registration: tt.reg}
   363  			pm := constructPrometheusMessage(i, tt.hostnames, tt.machines)
   364  
   365  			if !reflect.DeepEqual(pm, tt.want) {
   366  				t.Errorf("getPrometheusMessage() got: %v, want: %v", pm, tt.want)
   367  			}
   368  		})
   369  	}
   370  }