| // 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 } } |