1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-10 20:00:23 +00:00

Return resourceversion too old error to UI instead of logging (#667)

* Return watch error instead of logging it

The UI needs to know about watch error like `resourceversion too old` so
we need to return it.

* Sort by resourceVersion as number

The UI makes some assumption on resourceVersion. It assumes they are a
number and they are ordered by the number value. We'll want to fix this
at some point most likely but for now let's give something in a way that
UI wants.

* Remove -d suffix

After much testing, a delete of an object seems to have its own
resourceVersion so we don't need the -d suffix, we can simply use the
new resourceVersion.
This commit is contained in:
Tom Lebreux
2025-06-10 15:29:42 -06:00
committed by GitHub
parent 7db113a1fd
commit cf97607be5
4 changed files with 84 additions and 26 deletions

View File

@@ -6,13 +6,16 @@ package informer
import ( import (
"context" "context"
"errors"
"sort" "sort"
"strconv"
"time" "time"
"github.com/rancher/steve/pkg/sqlcache/db" "github.com/rancher/steve/pkg/sqlcache/db"
"github.com/rancher/steve/pkg/sqlcache/partition" "github.com/rancher/steve/pkg/sqlcache/partition"
"github.com/rancher/steve/pkg/sqlcache/sqltypes" "github.com/rancher/steve/pkg/sqlcache/sqltypes"
sqlStore "github.com/rancher/steve/pkg/sqlcache/store" sqlStore "github.com/rancher/steve/pkg/sqlcache/store"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
@@ -67,7 +70,16 @@ func NewInformer(ctx context.Context, client dynamic.ResourceInterface, fields [
a, err := client.List(ctx, options) a, err := client.List(ctx, options)
// We want the list to be consistent when there are going to be relists // We want the list to be consistent when there are going to be relists
sort.SliceStable(a.Items, func(i int, j int) bool { sort.SliceStable(a.Items, func(i int, j int) bool {
return a.Items[i].GetResourceVersion() < a.Items[j].GetResourceVersion() var err error
rvI, err1 := strconv.Atoi(a.Items[i].GetResourceVersion())
err = errors.Join(err, err1)
rvJ, err2 := strconv.Atoi(a.Items[j].GetResourceVersion())
err = errors.Join(err, err2)
if err != nil {
logrus.Debug("ResourceVersion not a number, falling back to string comparison")
return a.Items[i].GetResourceVersion() < a.Items[j].GetResourceVersion()
}
return rvI < rvJ
}) })
return a, err return a, err
}, },

View File

@@ -451,13 +451,6 @@ func (l *ListOptionIndexer) notifyEvent(eventType watch.EventType, oldObj any, o
} }
latestRV := acc.GetResourceVersion() latestRV := acc.GetResourceVersion()
// Append a -d suffix because the RV might be the same as the previous object
// in the following case:
// - Add obj1 with RV 100
// - Delete obj1 with RV 100
if eventType == watch.Deleted {
latestRV = latestRV + "-d"
}
_, err = tx.Stmt(l.upsertEventsStmt).Exec(latestRV, eventType, toBytes(obj)) _, err = tx.Stmt(l.upsertEventsStmt).Exec(latestRV, eventType, toBytes(obj))
if err != nil { if err != nil {
return &db.QueryError{QueryString: l.upsertEventsQuery, Err: err} return &db.QueryError{QueryString: l.upsertEventsQuery, Err: err}

View File

@@ -2190,13 +2190,11 @@ func TestWatchResourceVersion(t *testing.T) {
"app": "bar", "app": "bar",
}) })
barNew := &unstructured.Unstructured{} barDeleted := bar.DeepCopy()
barNew.SetResourceVersion("160") barDeleted.SetResourceVersion("160")
barNew.SetName("bar")
barNew.SetNamespace("bar") barNew := bar.DeepCopy()
barNew.SetLabels(map[string]string{ barNew.SetResourceVersion("170")
"app": "bar",
})
parentCtx := context.Background() parentCtx := context.Background()
@@ -2225,7 +2223,7 @@ func TestWatchResourceVersion(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rv3 := getRV(t) rv3 := getRV(t)
err = loi.Delete(bar) err = loi.Delete(barDeleted)
assert.NoError(t, err) assert.NoError(t, err)
rv4 := getRV(t) rv4 := getRV(t)
@@ -2246,7 +2244,7 @@ func TestWatchResourceVersion(t *testing.T) {
expectedEvents: []watch.Event{ expectedEvents: []watch.Event{
{Type: watch.Modified, Object: fooUpdated}, {Type: watch.Modified, Object: fooUpdated},
{Type: watch.Added, Object: bar}, {Type: watch.Added, Object: bar},
{Type: watch.Deleted, Object: bar}, {Type: watch.Deleted, Object: barDeleted},
{Type: watch.Added, Object: barNew}, {Type: watch.Added, Object: barNew},
}, },
}, },
@@ -2254,14 +2252,14 @@ func TestWatchResourceVersion(t *testing.T) {
rv: rv2, rv: rv2,
expectedEvents: []watch.Event{ expectedEvents: []watch.Event{
{Type: watch.Added, Object: bar}, {Type: watch.Added, Object: bar},
{Type: watch.Deleted, Object: bar}, {Type: watch.Deleted, Object: barDeleted},
{Type: watch.Added, Object: barNew}, {Type: watch.Added, Object: barNew},
}, },
}, },
{ {
rv: rv3, rv: rv3,
expectedEvents: []watch.Event{ expectedEvents: []watch.Event{
{Type: watch.Deleted, Object: bar}, {Type: watch.Deleted, Object: barDeleted},
{Type: watch.Added, Object: barNew}, {Type: watch.Added, Object: barNew},
}, },
}, },
@@ -2344,9 +2342,11 @@ func TestWatchGarbageCollection(t *testing.T) {
bar.SetResourceVersion("150") bar.SetResourceVersion("150")
bar.SetName("bar") bar.SetName("bar")
barNew := &unstructured.Unstructured{} barDeleted := bar.DeepCopy()
barNew.SetResourceVersion("160") barDeleted.SetResourceVersion("160")
barNew.SetName("bar")
barNew := bar.DeepCopy()
barNew.SetResourceVersion("170")
parentCtx := context.Background() parentCtx := context.Background()
@@ -2375,7 +2375,7 @@ func TestWatchGarbageCollection(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rv3 := getRV(t) rv3 := getRV(t)
err = loi.Delete(bar) err = loi.Delete(barDeleted)
assert.NoError(t, err) assert.NoError(t, err)
rv4 := getRV(t) rv4 := getRV(t)
@@ -2394,7 +2394,7 @@ func TestWatchGarbageCollection(t *testing.T) {
{ {
rv: rv3, rv: rv3,
expectedEvents: []watch.Event{ expectedEvents: []watch.Event{
{Type: watch.Deleted, Object: bar}, {Type: watch.Deleted, Object: barDeleted},
}, },
}, },
{ {
@@ -2449,3 +2449,54 @@ func TestWatchGarbageCollection(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
} }
func TestNonNumberResourceVersion(t *testing.T) {
ctx := context.Background()
opts := ListOptionIndexerOptions{
Fields: [][]string{{"metadata", "somefield"}},
IsNamespaced: true,
}
loi, err := makeListOptionIndexer(ctx, opts)
assert.NoError(t, err)
foo := &unstructured.Unstructured{
Object: map[string]any{
"metadata": map[string]any{
"name": "foo",
},
},
}
foo.SetResourceVersion("a")
foo2 := foo.DeepCopy()
foo2.SetResourceVersion("b")
foo2.SetLabels(map[string]string{
"hello": "world",
})
bar := &unstructured.Unstructured{
Object: map[string]any{
"metadata": map[string]any{
"name": "bar",
},
},
}
bar.SetResourceVersion("c")
err = loi.Add(foo)
assert.NoError(t, err)
err = loi.Update(foo2)
assert.NoError(t, err)
err = loi.Add(bar)
assert.NoError(t, err)
expectedUnstructured := &unstructured.Unstructured{
Object: map[string]any{
"items": []any{bar.Object, foo2.Object},
},
}
expectedList, err := expectedUnstructured.ToList()
require.NoError(t, err)
list, _, _, err := loi.ListByOptions(ctx, &sqltypes.ListOptions{}, []partition.Partition{{All: true}}, "")
assert.NoError(t, err)
assert.Equal(t, expectedList.Items, list.Items)
}

View File

@@ -564,6 +564,8 @@ func (s *Store) watch(apiOp *types.APIRequest, schema *types.APISchema, w types.
result := make(chan watch.Event) result := make(chan watch.Event)
go func() { go func() {
defer close(result)
ctx := apiOp.Context() ctx := apiOp.Context()
idNamespace, _ := kv.RSplit(w.ID, "/") idNamespace, _ := kv.RSplit(w.ID, "/")
if idNamespace == "" { if idNamespace == "" {
@@ -580,10 +582,10 @@ func (s *Store) watch(apiOp *types.APIRequest, schema *types.APISchema, w types.
} }
err := inf.ByOptionsLister.Watch(ctx, opts, result) err := inf.ByOptionsLister.Watch(ctx, opts, result)
if err != nil { if err != nil {
logrus.Error(err) returnErr(err, result)
return
} }
logrus.Debugf("closing watcher for %s", schema.ID) logrus.Debugf("closing watcher for %s", schema.ID)
close(result)
}() }()
return result, nil return result, nil
} }