blob: 019daf8764ca5e89d8a6429705cccced1fd1f35f [file]
// Copyright 2025 The etcd 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 integration
import (
"context"
"errors"
"fmt"
"math/rand"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/testing/protocmp"
"go.etcd.io/etcd/api/v3/mvccpb"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
cache "go.etcd.io/etcd/cache/v3"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/pkg/v3/stringutil"
"go.etcd.io/etcd/tests/v3/framework/integration"
)
func TestCacheWithoutPrefixWatch(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
c, err := cache.New(client, "", cache.WithHistoryWindowSize(32))
if err != nil {
t.Fatalf("New(...): %v", err)
}
t.Cleanup(c.Close)
if err := c.WaitReady(t.Context()); err != nil {
t.Fatalf("cache not ready: %v", err)
}
testWatch(t, client.KV, c)
}
func TestWatch(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
testWatch(t, client.KV, client.Watcher)
}
func testWatch(t *testing.T, kv clientv3.KV, watcher Watcher) {
ctx := t.Context()
rev2PutFooA := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/a"),
Value: []byte("1"),
CreateRevision: 2,
ModRevision: 2,
Version: 1,
},
}
rev3PutFooB := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/b"),
Value: []byte("2"),
CreateRevision: 3,
ModRevision: 3,
Version: 1,
},
}
rev4DeleteFooA := &clientv3.Event{
Type: clientv3.EventTypeDelete,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/a"),
ModRevision: 4,
},
}
rev5PutFooA := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/a"),
Value: []byte("3"),
CreateRevision: 5,
ModRevision: 5,
Version: 1,
},
}
rev5DeleteFooB := &clientv3.Event{
Type: clientv3.EventTypeDelete,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/b"),
ModRevision: 5,
},
}
rev6PutFooC := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/c"),
Value: []byte("x"),
CreateRevision: 6,
ModRevision: 6,
Version: 1,
},
}
rev7PutFooBar := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/bar"),
Value: []byte("y"),
CreateRevision: 7,
ModRevision: 7,
Version: 1,
},
}
rev8PutFooBaz := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/baz"),
Value: []byte("z"),
CreateRevision: 8,
ModRevision: 8,
Version: 1,
},
}
rev9PutFooYoo := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/yoo"),
Value: []byte("1"),
CreateRevision: 9,
ModRevision: 9,
Version: 1,
},
}
rev10PutZoo := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/zoo"),
Value: []byte("1"),
CreateRevision: 10,
ModRevision: 10,
Version: 1,
},
}
rev11PutFooFuture := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/future"),
Value: []byte("42"),
CreateRevision: 11,
ModRevision: 11,
Version: 1,
},
}
rev12PutFooTx1 := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/tx1"),
Value: []byte("a"),
CreateRevision: 12,
ModRevision: 12,
Version: 1,
},
}
rev12DeleteFooFuture := &clientv3.Event{
Type: clientv3.EventTypeDelete,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/future"),
ModRevision: 12,
},
}
rev12PutFooTx2 := &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/tx2"),
Value: []byte("b"),
CreateRevision: 12,
ModRevision: 12,
Version: 1,
},
}
tcs := []struct {
name string
key string
opts []clientv3.OpOption
wantEvents []*clientv3.Event
}{
{
name: "Watch single key existing /foo/c",
key: "/foo/c",
opts: []clientv3.OpOption{clientv3.WithRev(2)},
wantEvents: []*clientv3.Event{rev6PutFooC},
},
{
name: "Watch single key non‑existent /doesnotexist",
key: "/doesnotexist",
opts: []clientv3.OpOption{clientv3.WithRev(2)},
wantEvents: nil,
},
{
name: "Watch range empty",
key: "",
opts: []clientv3.OpOption{clientv3.WithRange(""), clientv3.WithRev(2)},
wantEvents: nil,
},
{
name: "Watch range [/foo/a, /foo/b)",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/b"), clientv3.WithRev(2)},
wantEvents: []*clientv3.Event{rev2PutFooA, rev4DeleteFooA, rev5PutFooA},
},
{
name: "Watch with prefix /foo/b",
key: "/foo/b",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(2)},
wantEvents: []*clientv3.Event{rev3PutFooB, rev5DeleteFooB, rev7PutFooBar, rev8PutFooBaz},
},
{
name: "Watch with prefix non-existent /doesnotexist",
key: "/doesnotexist",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(2)},
wantEvents: nil,
},
{
name: "Watch with prefix empty string",
key: "",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(2)},
wantEvents: []*clientv3.Event{rev2PutFooA, rev3PutFooB, rev4DeleteFooA, rev5PutFooA, rev5DeleteFooB, rev6PutFooC, rev7PutFooBar, rev8PutFooBaz, rev9PutFooYoo, rev10PutZoo, rev11PutFooFuture, rev12PutFooTx1, rev12DeleteFooFuture, rev12PutFooTx2},
},
{
name: "Watch from key /foo/b",
key: "/foo/b",
opts: []clientv3.OpOption{clientv3.WithFromKey(), clientv3.WithRev(2)},
wantEvents: []*clientv3.Event{rev3PutFooB, rev5DeleteFooB, rev6PutFooC, rev7PutFooBar, rev8PutFooBaz, rev9PutFooYoo, rev10PutZoo, rev11PutFooFuture, rev12PutFooTx1, rev12DeleteFooFuture, rev12PutFooTx2},
},
{
name: "Watch from empty key",
key: "",
opts: []clientv3.OpOption{clientv3.WithFromKey(), clientv3.WithRev(2)},
wantEvents: []*clientv3.Event{rev2PutFooA, rev3PutFooB, rev4DeleteFooA, rev5PutFooA, rev5DeleteFooB, rev6PutFooC, rev7PutFooBar, rev8PutFooBaz, rev9PutFooYoo, rev10PutZoo, rev11PutFooFuture, rev12PutFooTx1, rev12DeleteFooFuture, rev12PutFooTx2},
},
{
name: "Watch from non-existent key /doesnotexist",
key: "/doesnotexist",
opts: []clientv3.OpOption{clientv3.WithFromKey(), clientv3.WithRev(2)},
wantEvents: []*clientv3.Event{rev2PutFooA, rev3PutFooB, rev4DeleteFooA, rev5PutFooA, rev5DeleteFooB, rev6PutFooC, rev7PutFooBar, rev8PutFooBaz, rev9PutFooYoo, rev10PutZoo, rev11PutFooFuture, rev12PutFooTx1, rev12DeleteFooFuture, rev12PutFooTx2},
},
{
name: "Watch from rev 4 with single key /foo/a",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRev(4)},
wantEvents: []*clientv3.Event{rev4DeleteFooA, rev5PutFooA},
},
{
name: "Watch from rev 6 with single key /foo/a",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRev(6)},
wantEvents: nil,
},
{
name: "Watch from rev 5 with prefix /foo",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(5)},
wantEvents: []*clientv3.Event{
rev5PutFooA, rev5DeleteFooB, rev6PutFooC, rev7PutFooBar, rev8PutFooBaz, rev9PutFooYoo, rev11PutFooFuture, rev12PutFooTx1, rev12DeleteFooFuture, rev12PutFooTx2,
},
},
{
name: "Watch from rev 10 with prefix /foo",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(10)},
wantEvents: []*clientv3.Event{
rev11PutFooFuture, rev12PutFooTx1, rev12DeleteFooFuture, rev12PutFooTx2,
},
},
{
name: "Watch from rev 4 with range [/foo/a, /foo/c)",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/c"), clientv3.WithRev(4)},
wantEvents: []*clientv3.Event{
rev4DeleteFooA, rev5PutFooA, rev5DeleteFooB, rev7PutFooBar, rev8PutFooBaz,
},
},
{
name: "Latest‑revision watcher for /foo",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix()},
wantEvents: []*clientv3.Event{rev11PutFooFuture, rev12PutFooTx1, rev12DeleteFooFuture, rev12PutFooTx2},
},
{
name: "Watch from rev 11 with single key /foo/future",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithRev(11), clientv3.WithPrefix()},
wantEvents: []*clientv3.Event{rev11PutFooFuture, rev12PutFooTx1, rev12DeleteFooFuture, rev12PutFooTx2},
},
{
name: "Watch from rev 12 with txn prefix /foo",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithRev(12), clientv3.WithPrefix()},
wantEvents: []*clientv3.Event{rev12PutFooTx1, rev12DeleteFooFuture, rev12PutFooTx2},
},
}
t.Log("Write the first batch of events rev 2-10")
if _, err := kv.Put(ctx, string(rev2PutFooA.Kv.Key), string(rev2PutFooA.Kv.Value)); err != nil {
t.Fatalf("Put: %v", err)
}
if _, err := kv.Put(ctx, string(rev3PutFooB.Kv.Key), string(rev3PutFooB.Kv.Value)); err != nil {
t.Fatalf("Put: %v", err)
}
if _, err := kv.Delete(ctx, string(rev4DeleteFooA.Kv.Key)); err != nil {
t.Fatalf("Delete: %v", err)
}
if _, err := kv.Txn(ctx).Then(clientv3.OpPut(string(rev5PutFooA.Kv.Key), string(rev5PutFooA.Kv.Value)), clientv3.OpDelete(string(rev5DeleteFooB.Kv.Key))).Commit(); err != nil {
t.Fatalf("Txn: %v", err)
}
if _, err := kv.Put(ctx, string(rev6PutFooC.Kv.Key), string(rev6PutFooC.Kv.Value)); err != nil {
t.Fatalf("Put: %v", err)
}
if _, err := kv.Put(ctx, string(rev7PutFooBar.Kv.Key), string(rev7PutFooBar.Kv.Value)); err != nil {
t.Fatalf("Put: %v", err)
}
if _, err := kv.Put(ctx, string(rev8PutFooBaz.Kv.Key), string(rev8PutFooBaz.Kv.Value)); err != nil {
t.Fatalf("Put: %v", err)
}
if _, err := kv.Put(ctx, string(rev9PutFooYoo.Kv.Key), string(rev9PutFooYoo.Kv.Value)); err != nil {
t.Fatalf("Put: %v", err)
}
if _, err := kv.Put(ctx, string(rev10PutZoo.Kv.Key), string(rev10PutZoo.Kv.Value)); err != nil {
t.Fatalf("Put: %v", err)
}
t.Log("Open watches")
watches := make([]clientv3.WatchChan, len(tcs))
for i, tc := range tcs {
watches[i] = watcher.Watch(ctx, tc.key, tc.opts...)
}
time.Sleep(50 * time.Millisecond)
t.Log("Write the second batch of events rev 11‑12")
if _, err := kv.Put(ctx, string(rev11PutFooFuture.Kv.Key), string(rev11PutFooFuture.Kv.Value)); err != nil {
t.Fatalf("Put /foo/future: %v", err)
}
if _, err := kv.Txn(ctx).Then(
clientv3.OpPut(string(rev12PutFooTx1.Kv.Key), string(rev12PutFooTx1.Kv.Value)),
clientv3.OpDelete(string(rev12DeleteFooFuture.Kv.Key)),
clientv3.OpPut(string(rev12PutFooTx2.Kv.Key), string(rev12PutFooTx2.Kv.Value)),
).Commit(); err != nil {
t.Fatalf("Txn rev12: %v", err)
}
t.Log("Validate")
for i, tc := range tcs {
i, tc := i, tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
events, _ := collectAndAssertAtomicEvents(t, watches[i])
if diff := cmp.Diff(tc.wantEvents, events, protocmp.Transform()); diff != "" {
t.Errorf("unexpected events (-want +got):\n%s", diff)
}
})
}
}
func TestCacheWithPrefixWatch(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
ctx := t.Context()
tests := []struct {
name string
key string
opts []clientv3.OpOption
expectCanceled bool
}{
{
name: "single key within prefix",
key: "/foo/a",
opts: nil,
expectCanceled: false,
},
{
name: "single key outside prefix returns error",
key: "/bar/a",
opts: nil,
expectCanceled: true,
},
{
name: "prefix() within cache prefix",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix()},
expectCanceled: false,
},
{
name: "prefix() outside cache prefix returns error",
key: "/bar",
opts: []clientv3.OpOption{clientv3.WithPrefix()},
expectCanceled: true,
},
{
name: "range within prefix",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/b")},
expectCanceled: false,
},
{
name: "range crosses cache prefix boundary returns error",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/zzz")},
expectCanceled: true,
},
{
name: "fromKey not allowed when cache has prefix returns error",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithFromKey()},
expectCanceled: true,
},
}
const testPutKey = "/foo/a"
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
c, err := cache.New(client, "/foo")
if err != nil {
t.Fatalf("New(...): %v", err)
}
defer c.Close()
if err := c.WaitReady(ctx); err != nil {
t.Fatal(err)
}
watchCtx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
ch := c.Watch(watchCtx, tc.key, tc.opts...)
if !tc.expectCanceled {
if _, err := client.Put(ctx, testPutKey, "val"); err != nil {
t.Fatalf("Put(%q): %v", testPutKey, err)
}
}
select {
case resp, ok := <-ch:
if tc.expectCanceled {
if !ok || !resp.Canceled {
t.Fatalf("expected canceled watch, got %+v (closed=%v)", resp, !ok)
}
return
}
if !ok || resp.Canceled {
t.Fatalf("expected active watch (not canceled), got %+v (closed=%v)", resp, !ok)
}
if len(resp.Events) == 0 {
t.Fatalf("watch returned no events, expected at least the test event")
}
if string(resp.Events[0].Kv.Key) != testPutKey {
t.Fatalf("got event for key %q, want %q", resp.Events[0].Kv.Key, testPutKey)
}
case <-watchCtx.Done():
if tc.expectCanceled {
t.Fatalf("watch did not cancel within timeout")
} else {
t.Fatalf("active watch did not deliver event within timeout")
}
}
})
}
}
func TestCacheServerRequestProgress(t *testing.T) {
if integration.ThroughProxy {
t.Skip("grpc proxy currently does not support requesting progress notifications")
}
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
ctx := t.Context()
c, err := cache.New(client, "/foo")
if err != nil {
t.Fatalf("cache.New: %v", err)
}
t.Cleanup(c.Close)
if err := c.WaitReady(ctx); err != nil {
t.Fatalf("cache.WaitReady: %v", err)
}
wctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
watchCh := c.Watch(wctx, "/foo", clientv3.WithPrefix())
var latestRev int64
for i := 0; i < 5; i++ {
resp, err := client.Put(ctx, fmt.Sprintf("/bar/out-%d", i), "v")
if err != nil {
t.Fatalf("Put(/bar/out-%d): %v", i, err)
}
latestRev = resp.Header.Revision
}
if err := client.RequestProgress(ctx); err != nil {
t.Fatalf("RequestProgress: %v", err)
}
var progressRev int64
select {
case resp, ok := <-watchCh:
if !ok || resp.Canceled {
t.Fatalf("expected active watch (not canceled), got %+v (closed=%v)", resp, !ok)
}
if len(resp.Events) != 0 {
t.Fatalf("expected a progress notification (no events), got %d event(s)", len(resp.Events))
}
if !resp.IsProgressNotify() {
t.Fatalf("expected IsProgressNotify()==true, got false (resp: %+v)", resp)
}
progressRev = resp.Header.Revision
if progressRev < latestRev {
t.Fatalf("progress revision %d < latest outside-prefix rev %d", progressRev, latestRev)
}
case <-wctx.Done():
t.Fatalf("timed out waiting for progress notification: %v", wctx.Err())
}
}
func TestCacheRequestProgress(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
ctx := t.Context()
c, err := cache.New(client, "/foo")
if err != nil {
t.Fatalf("cache.New: %v", err)
}
t.Cleanup(c.Close)
if err := c.WaitReady(ctx); err != nil {
t.Fatalf("cache.WaitReady: %v", err)
}
wctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
watchCh := c.Watch(wctx, "/foo", clientv3.WithPrefix())
// Write some keys under the watched prefix so the cache advances.
var latestRev int64
for i := 0; i < 3; i++ {
resp, err := client.Put(ctx, fmt.Sprintf("/foo/key-%d", i), "v")
if err != nil {
t.Fatalf("Put(/foo/key-%d): %v", i, err)
}
latestRev = resp.Header.Revision
}
// Drain the event responses so the watcher is caught up.
readEvents(wctx, t, watchCh, 3)
// Call RequestProgress on the cache — this should deliver a progress
// notification to the watcher with the cache's current revision.
if err := c.RequestProgress(ctx); err != nil {
t.Fatalf("RequestProgress: %v", err)
}
resp := waitForProgressNotify(wctx, t, watchCh)
if resp.Header.Revision < latestRev {
t.Fatalf("progress revision %d < latest rev %d", resp.Header.Revision, latestRev)
}
}
func TestCacheWithProgressNotify(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
ctx := t.Context()
progressInterval := 200 * time.Millisecond
c, err := cache.New(client, "/foo", cache.WithProgressNotifyInterval(progressInterval))
if err != nil {
t.Fatalf("cache.New: %v", err)
}
t.Cleanup(c.Close)
if err := c.WaitReady(ctx); err != nil {
t.Fatalf("cache.WaitReady: %v", err)
}
t.Run("progress notification fires on idle watcher", func(t *testing.T) {
wctx, cancel := context.WithTimeout(ctx, 5*progressInterval)
defer cancel()
watchCh := c.Watch(wctx, "/foo", clientv3.WithPrefix(), clientv3.WithProgressNotify())
// Write a key so the cache has a non-zero revision.
if _, err := client.Put(ctx, "/foo/a", "1"); err != nil {
t.Fatalf("Put: %v", err)
}
readEvents(wctx, t, watchCh, 1)
// Wait for a progress notification (should arrive after ~progressInterval of idle time).
resp := waitForProgressNotify(wctx, t, watchCh)
if resp.Header.Revision == 0 {
t.Fatalf("progress notification has zero revision")
}
})
t.Run("no progress notification without WithProgressNotify", func(t *testing.T) {
wctx, cancel := context.WithTimeout(ctx, 5*progressInterval)
defer cancel()
watchCh := c.Watch(wctx, "/foo", clientv3.WithPrefix())
// Write a key so the watcher gets an event.
if _, err := client.Put(ctx, "/foo/b", "2"); err != nil {
t.Fatalf("Put: %v", err)
}
readEvents(wctx, t, watchCh, 1)
// Wait longer than the progress interval — should NOT get a progress notification.
select {
case resp := <-watchCh:
if resp.IsProgressNotify() {
t.Fatalf("got unexpected progress notification on watcher without WithProgressNotify")
}
case <-time.After(3 * progressInterval):
}
})
t.Run("progress notification is elided when events are flowing", func(t *testing.T) {
wctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
watchCh := c.Watch(wctx, "/foo", clientv3.WithPrefix(), clientv3.WithProgressNotify())
// Continuously write keys faster than the progress interval.
done := make(chan struct{})
go func() {
defer close(done)
for i := 0; i < 10; i++ {
if _, err := client.Put(ctx, fmt.Sprintf("/foo/stream-%d", i), fmt.Sprintf("%d", i)); err != nil {
return
}
time.Sleep(progressInterval / 5)
}
}()
// Collect responses while writes are happening.
var gotProgress bool
eventCount := 0
collectLoop:
for {
select {
case resp, ok := <-watchCh:
if !ok {
break collectLoop
}
if resp.IsProgressNotify() {
gotProgress = true
}
eventCount += len(resp.Events)
if eventCount >= 10 {
break collectLoop
}
case <-wctx.Done():
break collectLoop
}
}
<-done
if gotProgress {
t.Logf("progress notification received during active writes (elision not guaranteed but noted)")
}
if eventCount != 10 {
t.Fatalf("expected exactly 10 events, got %d", eventCount)
}
})
}
func TestCacheWithoutPrefixGet(t *testing.T) {
if integration.ThroughProxy {
t.Skip("grpc proxy currently does not support requesting progress notifications")
}
tcs := []struct {
name string
initialEvents, followupEvents []*clientv3.Event
}{
{"watch-early (no pre-events)", nil, TestGetEvents},
{"watch-mid (partial pre-events)", filterEvents(TestGetEvents, revLessThan(4)), filterEvents(TestGetEvents, revGreaterEqual(4))},
{"watch-late (all pre-events)", TestGetEvents, nil},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client, kv := clus.Client(0), clus.Client(0).KV
testGet(t, kv, func() Getter {
c, err := cache.New(client, "")
if err != nil {
t.Fatalf("cache.New: %v", err)
}
t.Cleanup(c.Close)
if err := c.WaitReady(t.Context()); err != nil {
t.Fatalf("cache not ready: %v", err)
}
return c
}, tc.initialEvents, tc.followupEvents)
})
}
}
func TestGet(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
kv := client.KV
testGet(t, kv, func() Getter { return kv }, TestGetEvents, nil)
}
func testGet(t *testing.T, kv clientv3.KV, getReader func() Getter, initialEvents, followupEvents []*clientv3.Event) {
ctx := t.Context()
t.Log("Setup")
baseRev := applyEvents(ctx, t, kv, initialEvents)
reader := getReader()
if c, ok := reader.(*cache.Cache); ok {
if err := c.WaitForRevision(ctx, baseRev); err != nil {
t.Fatalf("cache never caught up to rev %d: %v", baseRev, err)
}
}
followupRev := applyEvents(ctx, t, kv, followupEvents)
if c, ok := reader.(*cache.Cache); ok {
if err := c.WaitForRevision(ctx, followupRev); err != nil {
t.Fatalf("cache never caught up to rev %d: %v", followupRev, err)
}
}
if followupRev > baseRev {
baseRev = followupRev
}
latestRev := baseRev
t.Log("Validate")
for _, tc := range getTestCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
latestRev += advanceRevision(t, kv)
opts := tc.opts
if tc.optsFunc != nil {
opts = append(opts, tc.optsFunc(latestRev)...)
}
op := clientv3.OpGet(tc.key, opts...)
requestedRev := op.Rev()
resp, err := reader.Get(ctx, tc.key, opts...)
if tc.expectErr != nil {
if !errors.Is(err, tc.expectErr) {
t.Fatalf("expected %v for Get %q; got %v", tc.expectErr, tc.key, err)
}
return
}
if err != nil {
if _, ok := reader.(*cache.Cache); ok && requestedRev > 0 && requestedRev < baseRev && errors.Is(err, rpctypes.ErrCompacted) {
t.Logf("expected ErrCompacted: requestedRev=%d < baseCompleteRev=%d", requestedRev, baseRev)
return
}
t.Fatalf("Get %q failed: %v", tc.key, err)
}
if diff := cmp.Diff(tc.wantKVs, resp.Kvs, protocmp.Transform()); diff != "" {
t.Fatalf("unexpected KVs (-want +got):\n%s", diff)
}
if op.IsSerializable() {
if resp.Header.Revision < baseRev {
t.Fatalf("revision: got %d, want >= %d", resp.Header.Revision, baseRev)
}
} else {
if resp.Header.Revision != latestRev {
t.Fatalf("revision: got %d, want == %d", resp.Header.Revision, latestRev)
}
}
})
}
}
var TestGetEvents = []*clientv3.Event{
Rev2PutFooA, Rev3PutFooB, Rev4PutFooC, Rev5PutFooD, Rev6DeleteFooD, Rev7TxnPutFooA, Rev7TxnPutFooB, Rev8PutFooA,
}
var (
Rev2PutFooA = &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/a"),
Value: []byte("a1"),
CreateRevision: 2,
ModRevision: 2,
Version: 1,
},
}
Rev3PutFooB = &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/b"),
Value: []byte("b1"),
CreateRevision: 3,
ModRevision: 3,
Version: 1,
},
}
Rev4PutFooC = &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/c"),
Value: []byte("c1"),
CreateRevision: 4,
ModRevision: 4,
Version: 1,
},
}
Rev5PutFooD = &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/d"),
Value: []byte("d1"),
CreateRevision: 5,
ModRevision: 5,
Version: 1,
},
}
Rev6DeleteFooD = &clientv3.Event{
Type: clientv3.EventTypeDelete,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/d"),
ModRevision: 6,
},
}
Rev7TxnPutFooA = &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/a"),
Value: []byte("a2"),
CreateRevision: 2,
ModRevision: 7,
Version: 2,
},
}
Rev7TxnPutFooB = &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/b"),
Value: []byte("b2"),
CreateRevision: 3,
ModRevision: 7,
Version: 2,
},
}
Rev8PutFooA = &clientv3.Event{
Type: clientv3.EventTypePut,
Kv: &mvccpb.KeyValue{
Key: []byte("/foo/a"),
Value: []byte("a3"),
CreateRevision: 2,
ModRevision: 8,
Version: 3,
},
}
)
type getTestCase struct {
name string
key string
opts []clientv3.OpOption
optsFunc func(latestRev int64) []clientv3.OpOption
wantKVs []*mvccpb.KeyValue
expectErr error
}
var getTestCases = []getTestCase{
{
name: "single key /foo/a",
key: "/foo/a",
opts: []clientv3.OpOption{},
wantKVs: []*mvccpb.KeyValue{Rev8PutFooA.Kv},
},
{
name: "single key /foo/a at rev=2",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRev(2)},
wantKVs: []*mvccpb.KeyValue{Rev2PutFooA.Kv},
},
{
name: "single key /foo/a at rev=7",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRev(7)},
wantKVs: []*mvccpb.KeyValue{Rev7TxnPutFooA.Kv},
},
{
name: "single key /foo/a at rev=latest",
key: "/foo/a",
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev)}
},
wantKVs: []*mvccpb.KeyValue{Rev8PutFooA.Kv},
},
{
name: "single key /foo/a at rev=latest+1 (future), returns error",
key: "/foo/a",
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
{
name: "non-existing key",
key: "/doesnotexist",
opts: []clientv3.OpOption{},
wantKVs: nil,
},
{
name: "non-existing key at rev=4",
key: "/doesnotexist",
opts: []clientv3.OpOption{clientv3.WithRev(4)},
wantKVs: nil,
},
{
name: "non-existing key at rev=latest+1 (future), returns error",
key: "/doesnotexist",
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
{
name: "prefix /foo",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix()},
wantKVs: []*mvccpb.KeyValue{Rev8PutFooA.Kv, Rev7TxnPutFooB.Kv, Rev4PutFooC.Kv},
},
{
name: "prefix /foo at rev=5",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(5)},
wantKVs: []*mvccpb.KeyValue{Rev2PutFooA.Kv, Rev3PutFooB.Kv, Rev4PutFooC.Kv, Rev5PutFooD.Kv},
},
{
name: "prefix /foo/b at rev=4",
key: "/foo/b",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(4)},
wantKVs: []*mvccpb.KeyValue{Rev3PutFooB.Kv},
},
{
name: "prefix /foo/b at rev=7",
key: "/foo/b",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(7)},
wantKVs: []*mvccpb.KeyValue{Rev7TxnPutFooB.Kv},
},
{
name: "prefix /foo at rev=latest+1 (future), returns error",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix()},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
{
name: "range [/foo/a, /foo/c)",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/c")},
wantKVs: []*mvccpb.KeyValue{Rev8PutFooA.Kv, Rev7TxnPutFooB.Kv},
},
{
name: "range [/foo/a, /foo/d) at rev=5",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/d"), clientv3.WithRev(5)},
wantKVs: []*mvccpb.KeyValue{Rev2PutFooA.Kv, Rev3PutFooB.Kv, Rev4PutFooC.Kv},
},
{
name: "range [/foo/a, /foo/c) at rev=latest+1 (future), returns error",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/c")},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
{
name: "fromKey /foo/b",
key: "/foo/b",
opts: []clientv3.OpOption{clientv3.WithFromKey()},
wantKVs: []*mvccpb.KeyValue{Rev7TxnPutFooB.Kv, Rev4PutFooC.Kv},
},
{
name: "fromKey /foo/b at rev=7",
key: "/foo/b",
opts: []clientv3.OpOption{clientv3.WithFromKey(), clientv3.WithRev(7)},
wantKVs: []*mvccpb.KeyValue{Rev7TxnPutFooB.Kv, Rev4PutFooC.Kv},
},
{
name: "fromKey /foo/b at rev=latest+1 (future), returns error",
key: "/foo/b",
opts: []clientv3.OpOption{clientv3.WithFromKey()},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
{
name: "single key /foo/a serializable",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithSerializable()},
wantKVs: []*mvccpb.KeyValue{Rev8PutFooA.Kv},
},
{
name: "single key /foo/a serializable at rev=latest+1 (future), returns error",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithSerializable()},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
{
name: "non-existing key serializable",
key: "/doesnotexist",
opts: []clientv3.OpOption{clientv3.WithSerializable()},
wantKVs: nil,
},
{
name: "non-existing key serializable at rev=latest+1 (future), returns error",
key: "/doesnotexist",
opts: []clientv3.OpOption{clientv3.WithSerializable()},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
{
name: "prefix /foo serializable",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithSerializable()},
wantKVs: []*mvccpb.KeyValue{Rev8PutFooA.Kv, Rev7TxnPutFooB.Kv, Rev4PutFooC.Kv},
},
{
name: "prefix /foo serializable at rev=latest+1 (future), returns error",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithSerializable()},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
{
name: "range [/foo/a, /foo/c) serializable",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/c"), clientv3.WithSerializable()},
wantKVs: []*mvccpb.KeyValue{Rev8PutFooA.Kv, Rev7TxnPutFooB.Kv},
},
{
name: "range [/foo/a, /foo/c) serializable at rev=latest+1 (future), returns error",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/c"), clientv3.WithSerializable()},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
{
name: "fromKey /foo/b serializable",
key: "/foo/b",
opts: []clientv3.OpOption{clientv3.WithFromKey(), clientv3.WithSerializable()},
wantKVs: []*mvccpb.KeyValue{Rev7TxnPutFooB.Kv, Rev4PutFooC.Kv},
},
{
name: "fromKey /foo/b serializable at rev=latest+1 (future), returns error",
key: "/foo/b",
opts: []clientv3.OpOption{clientv3.WithFromKey(), clientv3.WithSerializable()},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev + 1)}
},
expectErr: rpctypes.ErrFutureRev,
},
}
func TestCacheWithPrefixGetInScope(t *testing.T) {
if integration.ThroughProxy {
t.Skip("grpc proxy currently does not support requesting progress notifications")
}
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
cli := clus.Client(0)
testWithPrefixGet(t, cli, func() Getter {
c, err := cache.New(cli, "/foo")
if err != nil {
t.Fatalf("cache.New: %v", err)
}
t.Cleanup(c.Close)
if err := c.WaitReady(t.Context()); err != nil {
t.Fatalf("cache.WaitReady: %v", err)
}
return c
})
}
func TestWithPrefixGet(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
cli := clus.Client(0)
testWithPrefixGet(t, cli, func() Getter { return cli.KV })
}
func testWithPrefixGet(t *testing.T, cli *clientv3.Client, getReader func() Getter) {
ctx := t.Context()
seedResp, err := cli.Put(ctx, "/foo/a", "val1")
if err != nil {
t.Fatalf("seed put: %v", err)
}
seedRev := seedResp.Header.Revision
reader := getReader()
for i := 0; i < 5; i++ {
_, err = cli.Put(ctx, fmt.Sprintf("/bar/x%d", i), fmt.Sprintf("%d", i))
if err != nil {
t.Fatalf("advance put: %v", err)
}
}
resp, err := cli.Put(ctx, "/foo/a", "val2")
if err != nil {
t.Fatalf("second put: %v", err)
}
latestRev := resp.Header.Revision
if err := cli.RequestProgress(ctx); err != nil {
t.Fatalf("RequestProgress: %v", err)
}
if c, ok := reader.(*cache.Cache); ok {
if err := c.WaitForRevision(ctx, latestRev); err != nil {
t.Fatalf("cache didn’t observe progress to rev %d: %v", latestRev, err)
}
}
expectedFooA := &mvccpb.KeyValue{
Key: []byte("/foo/a"),
Value: []byte("val2"),
CreateRevision: seedRev,
ModRevision: latestRev,
Version: 2,
}
expectedOldFooA := &mvccpb.KeyValue{
Key: []byte("/foo/a"),
Value: []byte("val1"),
CreateRevision: seedRev,
ModRevision: seedRev,
Version: 1,
}
baseRev := latestRev
testCases := []struct {
name string
key string
opts []clientv3.OpOption
optsFunc func(latestRev int64) []clientv3.OpOption
wantKVs []*mvccpb.KeyValue
}{
{
name: "single key within cache prefix",
key: "/foo/a",
opts: []clientv3.OpOption{},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "single key within cache prefix at latest/progress rev",
key: "/foo/a",
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev)}
},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "prefix query within cache prefix",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix()},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "prefix query within cache prefix at latest/progress rev",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix()},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev)}
},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "range query within cache prefix",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/b")},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "range query within cache prefix at latest/progress rev",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/z")},
optsFunc: func(latestRev int64) []clientv3.OpOption {
return []clientv3.OpOption{clientv3.WithRev(latestRev)}
},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "single key within cache prefix serializable",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithSerializable()},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "single key within cache prefix at base rev serializable",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithRev(baseRev)},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "prefix query within cache prefix serializable",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithSerializable()},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "prefix query within cache prefix at base rev serializable",
key: "/foo",
opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithSerializable(), clientv3.WithRev(baseRev)},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "range query within cache prefix serializable",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/b"), clientv3.WithSerializable()},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "range query within cache prefix at base rev serializable",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRange("/foo/z"), clientv3.WithSerializable(), clientv3.WithRev(baseRev)},
wantKVs: []*mvccpb.KeyValue{expectedFooA},
},
{
name: "prefix query within cache prefix at intermediate rev serializable",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRev(seedRev + 1), clientv3.WithSerializable()},
wantKVs: []*mvccpb.KeyValue{expectedOldFooA},
},
{
name: "prefix query within cache prefix at intermediate rev",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithRev(seedRev + 1)},
wantKVs: []*mvccpb.KeyValue{expectedOldFooA},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
latestRev += advanceRevision(t, cli)
opts := tc.opts
if tc.optsFunc != nil {
opts = append(opts, tc.optsFunc(latestRev)...)
}
op := clientv3.OpGet(tc.key, opts...)
resp, err := reader.Get(ctx, tc.key, opts...)
if err != nil {
t.Fatalf("Get(%q): %v", tc.key, err)
}
if diff := cmp.Diff(tc.wantKVs, resp.Kvs, protocmp.Transform()); diff != "" {
t.Errorf("unexpected KVs (-want +got):\n%s", diff)
}
if op.IsSerializable() {
if resp.Header.Revision < baseRev {
t.Errorf("Header.Revision=%d; want >= %d", resp.Header.Revision, baseRev)
}
} else {
if resp.Header.Revision != latestRev {
t.Errorf("Header.Revision=%d; want: %d", resp.Header.Revision, latestRev)
}
}
})
}
}
func TestCacheWithPrefixGetOutOfScope(t *testing.T) {
if integration.ThroughProxy {
t.Skip("grpc proxy currently does not support requesting progress notifications")
}
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
cli := clus.Client(0)
c, err := cache.New(cli, "/foo")
if err != nil {
t.Fatalf("cache.New: %v", err)
}
defer c.Close()
ctx := t.Context()
if err := c.WaitReady(ctx); err != nil {
t.Fatalf("cache.WaitReady: %v", err)
}
cases := []struct {
name string
key string
opts []clientv3.OpOption
}{
{
name: "single key outside prefix",
key: "/bar/a",
opts: []clientv3.OpOption{clientv3.WithSerializable()},
},
{
name: "prefix() outside cache prefix",
key: "/bar",
opts: []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithPrefix()},
},
{
name: "range crossing cache boundary",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithRange("/zzz")},
},
{
name: "fromKey disallowed with cache prefix",
key: "/foo/a",
opts: []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithFromKey()},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
_, err := c.Get(ctx, tc.key, tc.opts...)
if !errors.Is(err, cache.ErrKeyRangeInvalid) {
t.Fatalf("expected ErrKeyRangeInvalid; got %v", err)
}
})
}
}
func TestCacheLaggingWatcher(t *testing.T) {
const prefix = "/test/"
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
tests := []struct {
name string
window int
eventCount int
wantExactEventCount int
wantAtMaxEventCount int
wantClosed bool
}{
{
name: "all event fit",
window: 10,
eventCount: 9,
wantExactEventCount: 9,
wantClosed: false,
},
{
name: "events fill window",
window: 10,
eventCount: 10,
wantExactEventCount: 10,
wantClosed: false,
},
{
name: "event fill pipeline",
window: 10,
eventCount: 11,
wantExactEventCount: 11,
wantClosed: false,
},
{
name: "pipeline overflow",
window: 10,
eventCount: 12,
wantAtMaxEventCount: 1, // Either 0 or 1.
wantClosed: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := t.Context()
c, err := cache.New(
client, prefix,
cache.WithHistoryWindowSize(tt.window),
cache.WithPerWatcherBufferSize(0),
cache.WithResyncInterval(10*time.Millisecond),
)
if err != nil {
t.Fatalf("New(...): %v", err)
}
defer c.Close()
if err := c.WaitReady(ctx); err != nil {
t.Fatalf("cache not ready: %v", err)
}
ch := c.Watch(ctx, prefix, clientv3.WithPrefix())
if err := c.WaitForNextResync(ctx); err != nil {
t.Fatalf("cache not synced: %v", err)
}
generateEvents(t, client, prefix, tt.eventCount)
if err := c.WaitForNextResync(ctx); err != nil {
t.Fatalf("cache not synced: %v", err)
}
gotEvents, ok := collectAndAssertAtomicEvents(t, ch)
closed := !ok
if tt.wantExactEventCount != 0 && tt.wantExactEventCount != len(gotEvents) {
t.Errorf("gotEvents=%v, wantEvents=%v", len(gotEvents), tt.wantExactEventCount)
}
if tt.wantAtMaxEventCount != 0 && len(gotEvents) > tt.wantAtMaxEventCount {
t.Errorf("gotEvents=%v, wantEvents<%v", len(gotEvents), tt.wantAtMaxEventCount)
}
if closed != tt.wantClosed {
t.Errorf("closed=%v, wantClosed=%v", closed, tt.wantClosed)
}
})
}
}
func TestCacheUnsupportedWatchOptions(t *testing.T) {
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
c, err := cache.New(client, "", cache.WithHistoryWindowSize(1))
if err != nil {
t.Fatalf("cache.New: %v", err)
}
defer c.Close()
if err := c.WaitReady(t.Context()); err != nil {
t.Fatalf("cache not ready: %v", err)
}
unsupported := []struct {
name string
opt clientv3.OpOption
}{
{"WithPrevKV", clientv3.WithPrevKV()},
{"WithFragment", clientv3.WithFragment()},
{"WithCreatedNotify", clientv3.WithCreatedNotify()},
{"WithFilterPut", clientv3.WithFilterPut()},
{"WithFilterDelete", clientv3.WithFilterDelete()},
}
for _, tc := range unsupported {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ch := c.Watch(t.Context(), "foo", tc.opt)
resp, ok := <-ch
if !ok {
t.Fatalf("channel closed without yielding a response")
}
if !resp.Canceled {
t.Errorf("expected Canceled=true, got %+v", resp)
}
if !strings.Contains(resp.Err().Error(), cache.ErrUnsupportedRequest.Error()) {
t.Errorf("expected ErrUnsupportedWatch text %q, got %v",
cache.ErrUnsupportedRequest.Error(), resp.Err())
}
})
}
}
func TestCacheUnsupportedGetOptions(t *testing.T) {
if integration.ThroughProxy {
t.Skip("grpc proxy currently does not support requesting progress notifications")
}
integration.BeforeTest(t)
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
t.Cleanup(func() { clus.Terminate(t) })
client := clus.Client(0)
c, err := cache.New(client, "", cache.WithHistoryWindowSize(1))
if err != nil {
t.Fatalf("cache.New: %v", err)
}
defer c.Close()
if err := c.WaitReady(t.Context()); err != nil {
t.Fatalf("cache not ready: %v", err)
}
unsupported := []struct {
name string
opts []clientv3.OpOption
}{
{"WithCountOnly", []clientv3.OpOption{clientv3.WithCountOnly()}},
{"WithLimit", []clientv3.OpOption{clientv3.WithLimit(1)}},
{"WithSort", []clientv3.OpOption{clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend)}},
{"WithPrevKV", []clientv3.OpOption{clientv3.WithPrevKV()}},
{"WithMinModRevision", []clientv3.OpOption{clientv3.WithMinModRev(2)}},
{"WithMaxModRevision", []clientv3.OpOption{clientv3.WithMaxModRev(10)}},
{"WithMinCreateRevision", []clientv3.OpOption{clientv3.WithMinCreateRev(3)}},
{"WithMaxCreateRevision", []clientv3.OpOption{clientv3.WithMaxCreateRev(5)}},
}
for _, tc := range unsupported {
tc := tc
t.Run(tc.name, func(t *testing.T) {
_, err := c.Get(t.Context(), "foo", tc.opts...)
if !errors.Is(err, cache.ErrUnsupportedRequest) {
t.Errorf("Get with %s: expected ErrUnsupportedRequest, got %v", tc.name, err)
}
})
}
}
func generateEvents(t *testing.T, client *clientv3.Client, prefix string, n int) {
t.Helper()
for i := 0; i < n; i++ {
key := fmt.Sprintf("%s%d", prefix, i)
if _, err := client.Put(t.Context(), key, fmt.Sprintf("%d", i)); err != nil {
t.Fatalf("Put(%q): %v", key, err)
}
}
}
type Watcher interface {
Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan
}
type Getter interface {
Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error)
}
func collectAndAssertAtomicEvents(t *testing.T, watch clientv3.WatchChan) (events []*clientv3.Event, ok bool) {
deadline := time.After(time.Second)
var lastRevision int64
for {
select {
case resp, ok := <-watch:
if !ok {
return events, false
}
if len(resp.Events) != 0 && resp.Events[0].Kv.ModRevision == lastRevision {
t.Fatalf("same revision found as in previous response: %d", lastRevision)
}
for _, ev := range resp.Events {
if ev.Kv.ModRevision < lastRevision {
t.Fatalf("revision went backwards: last %d, now %d", lastRevision, ev.Kv.ModRevision)
}
events = append(events, ev)
lastRevision = ev.Kv.ModRevision
}
case <-deadline:
return events, true
case <-time.After(100 * time.Millisecond):
return events, true
}
}
}
func readEvents(ctx context.Context, t *testing.T, watchCh clientv3.WatchChan, n int) {
t.Helper()
received := 0
for received < n {
select {
case resp := <-watchCh:
if resp.Canceled {
t.Fatalf("unexpected canceled response: %v", resp.CancelReason)
}
received += len(resp.Events)
case <-ctx.Done():
t.Fatalf("timed out draining events, got %d/%d", received, n)
}
}
}
func waitForProgressNotify(ctx context.Context, t *testing.T, watchCh clientv3.WatchChan) clientv3.WatchResponse {
t.Helper()
for {
select {
case resp, ok := <-watchCh:
if !ok {
t.Fatalf("watch channel closed while waiting for progress notification")
}
if resp.Canceled {
t.Fatalf("unexpected canceled: %v", resp.CancelReason)
}
if resp.IsProgressNotify() {
return resp
}
case <-ctx.Done():
t.Fatalf("timed out waiting for progress notification")
return clientv3.WatchResponse{}
}
}
}
func advanceRevision(t *testing.T, kv clientv3.KV) int64 {
t.Helper()
n := rand.Intn(3) + 3
for i := 0; i < n; i++ {
_, err := kv.Put(t.Context(), fmt.Sprintf("/bar/%d/%s", i, stringutil.RandString(10)), "v")
if err != nil {
t.Fatalf("Put: %v", err)
}
}
return int64(n)
}
func applyEvents(ctx context.Context, t *testing.T, kv clientv3.KV, evs []*clientv3.Event) int64 {
var lastRev int64
for _, batches := range batchEventsByRevision(evs) {
lastRev = applyEventBatch(ctx, t, kv, batches)
}
return lastRev
}
func batchEventsByRevision(events []*clientv3.Event) [][]*clientv3.Event {
var batches [][]*clientv3.Event
if len(events) == 0 {
return batches
}
start := 0
for end := 1; end < len(events); end++ {
if events[end].Kv.ModRevision != events[start].Kv.ModRevision {
batches = append(batches, events[start:end])
start = end
}
}
batches = append(batches, events[start:])
return batches
}
func applyEventBatch(ctx context.Context, t *testing.T, kv clientv3.KV, batch []*clientv3.Event) int64 {
ops := make([]clientv3.Op, 0, len(batch))
for _, event := range batch {
switch event.Type {
case clientv3.EventTypePut:
ops = append(ops, clientv3.OpPut(string(event.Kv.Key), string(event.Kv.Value)))
case clientv3.EventTypeDelete:
ops = append(ops, clientv3.OpDelete(string(event.Kv.Key)))
default:
t.Fatalf("unsupported event type: %v", event.Type)
}
}
resp, err := kv.Txn(ctx).Then(ops...).Commit()
if err != nil {
t.Fatalf("Txn failed: %v", err)
}
return resp.Header.Revision
}
func filterEvents(evs []*clientv3.Event, pred func(int64) bool) []*clientv3.Event {
var out []*clientv3.Event
for _, ev := range evs {
if pred(ev.Kv.ModRevision) {
out = append(out, ev)
}
}
return out
}
func revLessThan(n int64) func(int64) bool { return func(r int64) bool { return r < n } }
func revGreaterEqual(n int64) func(int64) bool { return func(r int64) bool { return r >= n } }