/* Copyright 2015 The Kubernetes 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 queue import ( "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" ) const ( tolerance = 100 * time.Millisecond // go time delays aren't perfect, this is our tolerance for errors WRT expected timeouts ) func timedPriority(t time.Time) Priority { return Priority{ts: t} } func TestPQ(t *testing.T) { t.Parallel() var pq priorityQueue if pq.Len() != 0 { t.Fatalf("pq should be empty") } now := timedPriority(time.Now()) now2 := timedPriority(now.ts.Add(2 * time.Second)) pq.Push(&qitem{priority: now2}) if pq.Len() != 1 { t.Fatalf("pq.len should be 1") } x := pq.Pop() if x == nil { t.Fatalf("x is nil") } if pq.Len() != 0 { t.Fatalf("pq should be empty") } item := x.(*qitem) if !item.priority.Equal(now2) { t.Fatalf("item.priority != now2") } pq.Push(&qitem{priority: now2}) pq.Push(&qitem{priority: now2}) pq.Push(&qitem{priority: now2}) pq.Push(&qitem{priority: now2}) pq.Push(&qitem{priority: now2}) pq.Pop() pq.Pop() pq.Pop() pq.Pop() pq.Pop() if pq.Len() != 0 { t.Fatalf("pq should be empty") } now4 := timedPriority(now.ts.Add(4 * time.Second)) now6 := timedPriority(now.ts.Add(4 * time.Second)) pq.Push(&qitem{priority: now2}) pq.Push(&qitem{priority: now4}) pq.Push(&qitem{priority: now6}) pq.Swap(0, 2) if !pq[0].priority.Equal(now6) || !pq[2].priority.Equal(now2) { t.Fatalf("swap failed") } if pq.Less(1, 2) { t.Fatalf("now4 < now2") } } func TestPopEmptyPQ(t *testing.T) { t.Parallel() defer func() { if r := recover(); r == nil { t.Fatalf("Expected panic from popping an empty PQ") } }() var pq priorityQueue pq.Pop() } type testjob struct { d time.Duration t time.Time deadline *time.Time uid string instance int } func (j *testjob) GetDelay() time.Duration { return j.d } func (j testjob) GetUID() string { return j.uid } func (td *testjob) Deadline() (deadline time.Time, ok bool) { if td.deadline != nil { return *td.deadline, true } else { return time.Now(), false } } func TestDQ_sanity_check(t *testing.T) { t.Parallel() dq := NewDelayQueue() delay := 2 * time.Second dq.Add(&testjob{d: delay}) before := time.Now() x := dq.Pop() now := time.Now() waitPeriod := now.Sub(before) if waitPeriod+tolerance < delay { t.Fatalf("delay too short: %v, expected: %v", waitPeriod, delay) } if x == nil { t.Fatalf("x is nil") } item := x.(*testjob) if item.d != delay { t.Fatalf("d != delay") } } func TestDQ_Offer(t *testing.T) { t.Parallel() assert := assert.New(t) dq := NewDelayQueue() delay := time.Second added := dq.Offer(&testjob{}) if added { t.Fatalf("DelayQueue should not add offered job without deadline") } deadline := time.Now().Add(delay) added = dq.Offer(&testjob{deadline: &deadline}) if !added { t.Fatalf("DelayQueue should add offered job with deadline") } before := time.Now() x := dq.Pop() now := time.Now() waitPeriod := now.Sub(before) if waitPeriod+tolerance < delay { t.Fatalf("delay too short: %v, expected: %v", waitPeriod, delay) } assert.NotNil(x) assert.Equal(x.(*testjob).deadline, &deadline) } func TestDQ_ordered_add_pop(t *testing.T) { t.Parallel() dq := NewDelayQueue() dq.Add(&testjob{d: 2 * time.Second}) dq.Add(&testjob{d: 1 * time.Second}) dq.Add(&testjob{d: 3 * time.Second}) var finished [3]*testjob before := time.Now() idx := int32(-1) ch := make(chan bool, 3) //TODO: replace with `for range finished` once Go 1.3 support is dropped for n := 0; n < len(finished); n++ { go func() { var ok bool x := dq.Pop() i := atomic.AddInt32(&idx, 1) if finished[i], ok = x.(*testjob); !ok { t.Fatalf("expected a *testjob, not %v", x) } finished[i].t = time.Now() ch <- true }() } <-ch <-ch <-ch after := time.Now() totalDelay := after.Sub(before) if totalDelay+tolerance < (3 * time.Second) { t.Fatalf("totalDelay < 3s: %v", totalDelay) } for i, v := range finished { if v == nil { t.Fatalf("task %d was nil", i) } expected := time.Duration(i+1) * time.Second if v.d != expected { t.Fatalf("task %d had delay-priority %v, expected %v", i, v.d, expected) } actualDelay := v.t.Sub(before) if actualDelay+tolerance < v.d { t.Fatalf("task %d had actual-delay %v < expected delay %v", i, actualDelay, v.d) } } } func TestDQ_always_pop_earliest_deadline(t *testing.T) { t.Skip("disabled due to flakiness; see #11857") t.Parallel() // add a testjob with delay of 2s // spawn a func f1 that attempts to Pop() and wait for f1 to begin // add a testjob with a delay of 1s // check that the func f1 actually popped the 1s task (not the 2s task) dq := NewDelayQueue() dq.Add(&testjob{d: 2 * time.Second}) ch := make(chan *testjob) started := make(chan bool) go func() { started <- true x := dq.Pop() job := x.(*testjob) job.t = time.Now() ch <- job }() <-started time.Sleep(500 * time.Millisecond) // give plently of time for Pop() to enter expected := 1 * time.Second dq.Add(&testjob{d: expected}) job := <-ch if expected != job.d { t.Fatalf("Expected delay-prority of %v got instead got %v", expected, job.d) } job = dq.Pop().(*testjob) expected = 2 * time.Second if expected != job.d { t.Fatalf("Expected delay-prority of %v got instead got %v", expected, job.d) } } func TestDQ_always_pop_earliest_deadline_multi(t *testing.T) { t.Skip("disabled due to flakiness; see #11821") t.Parallel() dq := NewDelayQueue() dq.Add(&testjob{d: 2 * time.Second}) ch := make(chan *testjob) multi := 10 started := make(chan bool, multi) go func() { started <- true for i := 0; i < multi; i++ { x := dq.Pop() job := x.(*testjob) job.t = time.Now() ch <- job } }() <-started time.Sleep(500 * time.Millisecond) // give plently of time for Pop() to enter expected := 1 * time.Second for i := 0; i < multi; i++ { dq.Add(&testjob{d: expected}) } for i := 0; i < multi; i++ { job := <-ch if expected != job.d { t.Fatalf("Expected delay-prority of %v got instead got %v", expected, job.d) } } job := dq.Pop().(*testjob) expected = 2 * time.Second if expected != job.d { t.Fatalf("Expected delay-prority of %v got instead got %v", expected, job.d) } } func TestDQ_negative_delay(t *testing.T) { t.Parallel() dq := NewDelayQueue() delay := -2 * time.Second dq.Add(&testjob{d: delay}) before := time.Now() x := dq.Pop() now := time.Now() waitPeriod := now.Sub(before) if waitPeriod > tolerance { t.Fatalf("delay too long: %v, expected something less than: %v", waitPeriod, tolerance) } if x == nil { t.Fatalf("x is nil") } item := x.(*testjob) if item.d != delay { t.Fatalf("d != delay") } } func TestDFIFO_sanity_check(t *testing.T) { t.Parallel() assert := assert.New(t) df := NewDelayFIFO() delay := 2 * time.Second df.Add(&testjob{d: delay, uid: "a", instance: 1}, ReplaceExisting) assert.True(df.ContainedIDs().Has("a")) // re-add by ReplaceExisting df.Add(&testjob{d: delay, uid: "a", instance: 2}, ReplaceExisting) assert.True(df.ContainedIDs().Has("a")) a, ok := df.Get("a") assert.True(ok) assert.Equal(a.(*testjob).instance, 2) // re-add by KeepExisting df.Add(&testjob{d: delay, uid: "a", instance: 3}, KeepExisting) assert.True(df.ContainedIDs().Has("a")) a, ok = df.Get("a") assert.True(ok) assert.Equal(a.(*testjob).instance, 2) // pop last before := time.Now() x := df.Pop(WithoutCancel()) assert.Equal(a.(*testjob).instance, 2) now := time.Now() waitPeriod := now.Sub(before) if waitPeriod+tolerance < delay { t.Fatalf("delay too short: %v, expected: %v", waitPeriod, delay) } if x == nil { t.Fatalf("x is nil") } item := x.(*testjob) if item.d != delay { t.Fatalf("d != delay") } } func TestDFIFO_Offer(t *testing.T) { t.Parallel() assert := assert.New(t) dq := NewDelayFIFO() delay := time.Second added := dq.Offer(&testjob{instance: 1}, ReplaceExisting) if added { t.Fatalf("DelayFIFO should not add offered job without deadline") } deadline := time.Now().Add(delay) added = dq.Offer(&testjob{deadline: &deadline, instance: 2}, ReplaceExisting) if !added { t.Fatalf("DelayFIFO should add offered job with deadline") } before := time.Now() x := dq.Pop(WithoutCancel()) now := time.Now() waitPeriod := now.Sub(before) if waitPeriod+tolerance < delay { t.Fatalf("delay too short: %v, expected: %v", waitPeriod, delay) } assert.NotNil(x) assert.Equal(x.(*testjob).instance, 2) }