blob: 1095cb08b941c9fef7bf341f004d43bf8eca751b [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tsmon
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/tsmon"
"go.chromium.org/luci/common/tsmon/metric"
"go.chromium.org/luci/common/tsmon/monitor"
"go.chromium.org/luci/common/tsmon/store"
"go.chromium.org/luci/common/tsmon/target"
"go.chromium.org/luci/server/router"
. "github.com/smartystreets/goconvey/convey"
)
var testMetric = metric.NewCounter("test_metric", "", nil)
func TestMiddleware(t *testing.T) {
t.Parallel()
runMiddlware := func(c context.Context, state *State, cb func(*router.Context)) {
rec := httptest.NewRecorder()
router.RunMiddleware(
&router.Context{Context: c, Writer: rec, Request: &http.Request{}},
router.NewMiddlewareChain(state.Middleware),
cb,
)
So(rec.Code, ShouldEqual, http.StatusOK)
}
incrMetric := func(c *router.Context) {
So(store.IsNilStore(tsmon.Store(c.Context)), ShouldBeFalse)
tsmon.Store(c.Context).Incr(c.Context, testMetric, time.Time{}, []interface{}{}, int64(1))
}
readMetric := func(c context.Context) interface{} {
return tsmon.Store(c).Get(c, testMetric, time.Time{}, []interface{}{})
}
Convey("With fakes", t, func() {
c, clock := buildTestContext()
state, monitor, allocator := buildTestState()
Convey("Pings TaskNumAllocator and waits for number", func() {
So(allocator.instanceIDs, ShouldHaveLength, 0)
allocator.taskNum = -1 // no number yet
// Do the flush.
state.nextFlush = clock.Now()
runMiddlware(c, state, incrMetric)
// Called the allocator.
So(allocator.instanceIDs, ShouldHaveLength, 1)
// Shouldn't flush since the instance entity doesn't have a task number yet.
So(monitor.Cells, ShouldHaveLength, 0)
// Wait until next expected flush.
clock.Add(time.Minute)
allocator.taskNum = 0 // got the number!
// Do the flush again.
state.nextFlush = clock.Now()
runMiddlware(c, state, incrMetric)
// Flushed stuff this time.
So(monitor.Cells, ShouldHaveLength, 1)
// The value should still be set.
So(readMetric(c), ShouldEqual, int64(2))
})
Convey("Resets cumulative metrics", func() {
// Do the flush.
state.nextFlush = clock.Now()
runMiddlware(c, state, incrMetric)
// Flushed stuff.
So(monitor.Cells, ShouldHaveLength, 1)
monitor.Cells = nil
// The value is set.
So(readMetric(c), ShouldEqual, int64(1))
// Lost the task number.
allocator.taskNum = -1
// Do the flush again.
state.nextFlush = clock.Now()
runMiddlware(c, state, incrMetric)
// No stuff is sent, and cumulative metrics are reset.
So(monitor.Cells, ShouldHaveLength, 0)
So(readMetric(c), ShouldEqual, nil)
})
Convey("Dynamic enable and disable works", func() {
// Enabled. Store is not nil.
runMiddlware(c, state, func(c *router.Context) {
So(store.IsNilStore(tsmon.Store(c.Context)), ShouldBeFalse)
})
// Disabled. Store is nil.
state.Settings.Enabled = false
runMiddlware(c, state, func(c *router.Context) {
So(store.IsNilStore(tsmon.Store(c.Context)), ShouldBeTrue)
})
})
Convey("Check error backoff", func() {
// Flush now.
state.nextFlush = clock.Now()
runMiddlware(c, state, incrMetric)
// Break the monitor.
monitor.SendErr = http.ErrServerClosed
clock.Add(time.Minute)
flushTime := clock.Now()
lastRetry := state.flushRetry
state.nextFlush = flushTime
runMiddlware(c, state, incrMetric)
// lastFlush should not have changed.
So(state.lastFlush, ShouldNotEqual, flushTime)
So(state.flushRetry, ShouldEqual, lastRetry*2)
clock.Add(time.Minute)
runMiddlware(c, state, incrMetric)
// Check retry is doubling.
So(state.flushRetry, ShouldEqual, lastRetry*4)
})
})
}
////////////////////////////////////////////////////////////////////////////////
func buildTestContext() (context.Context, testclock.TestClock) {
c := context.Background()
c, clock := testclock.UseTime(c, testclock.TestTimeUTC)
c, _, _ = tsmon.WithFakes(c)
return c, clock
}
func buildTestState() (*State, *monitor.Fake, *fakeNumAllocator) {
mon := &monitor.Fake{}
allocator := &fakeNumAllocator{}
return &State{
Target: func(c context.Context) target.Task {
return target.Task{
DataCenter: "datacenter",
ServiceName: "app-id",
JobName: "service-name",
HostName: "12345-version",
}
},
InstanceID: func(c context.Context) string { return "some.id" },
TaskNumAllocator: allocator,
FlushInMiddleware: true,
CustomMonitor: mon,
Settings: &Settings{
Enabled: true,
FlushIntervalSec: 60,
},
}, mon, allocator
}
type fakeNumAllocator struct {
taskNum int
instanceIDs []string
}
func (a *fakeNumAllocator) NotifyTaskIsAlive(c context.Context, task *target.Task, instanceID string) (int, error) {
a.instanceIDs = append(a.instanceIDs, instanceID)
if a.taskNum == -1 {
return 0, ErrNoTaskNumber
}
return a.taskNum, nil
}