Skip to content

Commit a81a529

Browse files
smirautkuozdemir
andcommitted
fix: ignore correctly bootstrapped duplicates in WatchKind
Previous implementation had a bug when it might ignore an event coming for a resource which was deleted and then recreated under the same resource version. Co-authored-by: Utku Ozdemir <[email protected]> Signed-off-by: Andrey Smirnov <[email protected]>
1 parent bfd872f commit a81a529

File tree

4 files changed

+126
-23
lines changed

4 files changed

+126
-23
lines changed

pkg/state/impl/etcd/controller_runtime_test.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,28 @@ import (
1313
"github.com/cosi-project/runtime/pkg/resource/protobuf"
1414
"github.com/cosi-project/runtime/pkg/state"
1515
"github.com/cosi-project/runtime/pkg/state/impl/store"
16-
"github.com/stretchr/testify/require"
1716
suiterunner "github.com/stretchr/testify/suite"
1817
clientv3 "go.etcd.io/etcd/client/v3"
1918

2019
"github.com/cosi-project/state-etcd/pkg/state/impl/etcd"
2120
"github.com/cosi-project/state-etcd/pkg/util/testhelpers"
2221
)
2322

23+
func must(err error) {
24+
if err != nil {
25+
panic(err)
26+
}
27+
}
28+
29+
func init() {
30+
must(protobuf.RegisterResource(conformance.IntResourceType, &conformance.IntResource{}))
31+
must(protobuf.RegisterResource(conformance.StrResourceType, &conformance.StrResource{}))
32+
must(protobuf.RegisterResource(conformance.SentenceResourceType, &conformance.SentenceResource{}))
33+
}
34+
2435
func TestRuntimeConformance(t *testing.T) {
2536
t.Parallel()
2637

27-
require.NoError(t, protobuf.RegisterResource(conformance.IntResourceType, &conformance.IntResource{}))
28-
require.NoError(t, protobuf.RegisterResource(conformance.StrResourceType, &conformance.StrResource{}))
29-
require.NoError(t, protobuf.RegisterResource(conformance.SentenceResourceType, &conformance.SentenceResource{}))
30-
3138
testhelpers.WithEtcd(t, func(cli *clientv3.Client) {
3239
suite := &conformance.RuntimeSuite{}
3340
suite.SetupRuntime = func() {

pkg/state/impl/etcd/etcd.go

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"fmt"
1111
"sort"
1212
"strconv"
13-
"strings"
1413
"time"
1514

1615
"github.com/cosi-project/runtime/pkg/resource"
@@ -113,7 +112,7 @@ func (st *State) List(ctx context.Context, resourceKind resource.Kind, opts ...s
113112
}
114113

115114
sort.Slice(resources, func(i, j int) bool {
116-
return strings.Compare(resources[i].Metadata().String(), resources[j].Metadata().String()) < 0
115+
return resources[i].Metadata().ID() < resources[j].Metadata().ID()
117116
})
118117

119118
return resource.List{
@@ -507,8 +506,6 @@ func (st *State) WatchKind(ctx context.Context, resourceKind resource.Kind, ch c
507506
go func() {
508507
defer cancel()
509508

510-
sentBootstrapEventSet := make(map[string]struct{}, len(bootstrapList))
511-
512509
// send initial contents if they were captured
513510
for _, res := range bootstrapList {
514511
if !channel.SendWithContext(ctx, ch,
@@ -519,8 +516,6 @@ func (st *State) WatchKind(ctx context.Context, resourceKind resource.Kind, ch c
519516
) {
520517
return
521518
}
522-
523-
sentBootstrapEventSet[res.Metadata().String()] = struct{}{}
524519
}
525520

526521
bootstrapList = nil
@@ -567,6 +562,11 @@ func (st *State) WatchKind(ctx context.Context, resourceKind resource.Kind, ch c
567562
}
568563

569564
for _, etcdEvent := range watchResponse.Events {
565+
// watch event might come for a revision which was already sent in the bootstrapped set, ignore it
566+
if etcdEvent.Kv != nil && etcdEvent.Kv.ModRevision <= revision {
567+
continue
568+
}
569+
570570
event, err := st.convertEvent(etcdEvent)
571571
if err != nil {
572572
channel.SendWithContext(ctx, ch,
@@ -607,13 +607,6 @@ func (st *State) WatchKind(ctx context.Context, resourceKind resource.Kind, ch c
607607
panic("should never be reached")
608608
}
609609

610-
if !(event.Type == state.Destroyed) {
611-
_, alreadySent := sentBootstrapEventSet[event.Resource.Metadata().String()]
612-
if alreadySent {
613-
continue
614-
}
615-
}
616-
617610
if !channel.SendWithContext(ctx, ch, event) {
618611
return
619612
}

pkg/state/impl/etcd/etcd_test.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package etcd_test
66

77
import (
88
"context"
9-
"log"
109
"testing"
1110
"time"
1211

@@ -23,10 +22,7 @@ import (
2322
)
2423

2524
func init() {
26-
err := protobuf.RegisterResource(conformance.PathResourceType, &conformance.PathResource{})
27-
if err != nil {
28-
log.Fatalf("failed to register resource: %v", err)
29-
}
25+
must(protobuf.RegisterResource(conformance.PathResourceType, &conformance.PathResource{}))
3026
}
3127

3228
func TestPreserveCreated(t *testing.T) {

pkg/state/impl/etcd/watch_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package etcd_test
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"testing"
11+
"time"
12+
13+
"github.com/cosi-project/runtime/pkg/safe"
14+
"github.com/cosi-project/runtime/pkg/state"
15+
"github.com/cosi-project/runtime/pkg/state/conformance"
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
func TestWatchKindWithBootstrap(t *testing.T) {
21+
t.Parallel()
22+
23+
for _, test := range []struct {
24+
name string
25+
destroyIsTheLastOperation bool
26+
}{
27+
{
28+
name: "put is last",
29+
destroyIsTheLastOperation: false,
30+
},
31+
{
32+
name: "delete is last",
33+
destroyIsTheLastOperation: true,
34+
},
35+
} {
36+
test := test
37+
38+
t.Run(test.name, func(t *testing.T) {
39+
t.Parallel()
40+
41+
withEtcd(t, func(s state.State) {
42+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
43+
defer cancel()
44+
45+
for i := 0; i < 3; i++ {
46+
require.NoError(t, s.Create(ctx, conformance.NewPathResource("default", fmt.Sprintf("path-%d", i))))
47+
}
48+
49+
if test.destroyIsTheLastOperation {
50+
require.NoError(t, s.Create(ctx, conformance.NewPathResource("default", "path-3")))
51+
require.NoError(t, s.Destroy(ctx, conformance.NewPathResource("default", "path-3").Metadata()))
52+
}
53+
54+
watchCh := make(chan state.Event)
55+
56+
require.NoError(t, s.WatchKind(ctx, conformance.NewPathResource("default", "").Metadata(), watchCh, state.WithBootstrapContents(true)))
57+
58+
for i := 0; i < 3; i++ {
59+
select {
60+
case <-time.After(time.Second):
61+
t.Fatal("timeout waiting for event")
62+
case ev := <-watchCh:
63+
assert.Equal(t, state.Created, ev.Type)
64+
assert.Equal(t, fmt.Sprintf("path-%d", i), ev.Resource.Metadata().ID())
65+
assert.IsType(t, &conformance.PathResource{}, ev.Resource)
66+
}
67+
}
68+
69+
select {
70+
case <-time.After(time.Second):
71+
t.Fatal("timeout waiting for event")
72+
case ev := <-watchCh:
73+
assert.Equal(t, state.Bootstrapped, ev.Type)
74+
}
75+
76+
require.NoError(t, s.Destroy(ctx, conformance.NewPathResource("default", "path-0").Metadata()))
77+
78+
select {
79+
case <-time.After(time.Second):
80+
t.Fatal("timeout waiting for event")
81+
case ev := <-watchCh:
82+
assert.Equal(t, state.Destroyed, ev.Type, "event %s %s", ev.Type, ev.Resource)
83+
assert.Equal(t, "path-0", ev.Resource.Metadata().ID())
84+
assert.IsType(t, &conformance.PathResource{}, ev.Resource)
85+
}
86+
87+
newR, err := safe.StateUpdateWithConflicts(ctx, s, conformance.NewPathResource("default", "path-1").Metadata(), func(r *conformance.PathResource) error {
88+
r.Metadata().Finalizers().Add("foo")
89+
90+
return nil
91+
})
92+
require.NoError(t, err)
93+
94+
select {
95+
case <-time.After(time.Second):
96+
t.Fatal("timeout waiting for event")
97+
case ev := <-watchCh:
98+
assert.Equal(t, state.Updated, ev.Type, "event %s %s", ev.Type, ev.Resource)
99+
assert.Equal(t, "path-1", ev.Resource.Metadata().ID())
100+
assert.Equal(t, newR.Metadata().Finalizers(), ev.Resource.Metadata().Finalizers())
101+
assert.Equal(t, newR.Metadata().Version(), ev.Resource.Metadata().Version())
102+
assert.IsType(t, &conformance.PathResource{}, ev.Resource)
103+
}
104+
})
105+
})
106+
}
107+
}

0 commit comments

Comments
 (0)