-
Notifications
You must be signed in to change notification settings - Fork 3.6k
fix: scope offloaded node status query to the page. Fixes #16611 #16618
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| //go:build !windows | ||
|
|
||
| package sqldb | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| wfv1 "github.com/argoproj/argo-workflows/v4/pkg/apis/workflow/v1alpha1" | ||
| "github.com/argoproj/argo-workflows/v4/util/logging" | ||
| usqldb "github.com/argoproj/argo-workflows/v4/util/sqldb" | ||
| ) | ||
|
|
||
| // setupMySQLOffloadTest starts a MySQL or MariaDB container and returns an offload repository. | ||
| func setupMySQLOffloadTest(ctx context.Context, t *testing.T, v usqldb.MySQLVariant) OffloadNodeStatusRepo { | ||
| t.Helper() | ||
| repo, err := NewOffloadNodeStatusRepo(ctx, logging.RequireLoggerFromContext(ctx), setupMySQLTest(ctx, t, v), "test", "argo_workflows") | ||
| require.NoError(t, err) | ||
| return repo | ||
| } | ||
|
|
||
| // saveOffload writes a node status and returns the version it was stored under. | ||
| func saveOffload(ctx context.Context, t *testing.T, repo OffloadNodeStatusRepo, uid, nodeName string) string { | ||
| t.Helper() | ||
| version, err := repo.Save(ctx, uid, "argo", wfv1.Nodes{nodeName: wfv1.NodeStatus{ID: nodeName}}) | ||
| require.NoError(t, err) | ||
| return version | ||
| } | ||
|
|
||
| // TestMySQLListOnlyReturnsRequestedKeys covers the behaviour the list path depends on: the | ||
| // query matches whole (uid, version) pairs, so a caller that needs one page of workflows does | ||
| // not pull every offloaded blob in the namespace. | ||
| // | ||
| // Save deliberately leaves superseded rows behind for the garbage collector, so uid-a and uid-b | ||
| // each have two versions here and only one version of each is requested. | ||
| // | ||
| // The version is a hash of the node contents, so two workflows only share a version value when | ||
| // they store identical nodes. That is arranged deliberately: without it every version value is | ||
| // unique to one uid, and a `uid IN (...) AND version IN (...)` cross-product happens to return | ||
| // the right rows anyway. With shared version values the cross-product matches all four rows, | ||
| // while matching whole pairs returns two. | ||
| func TestMySQLListOnlyReturnsRequestedKeys(t *testing.T) { | ||
| for name, variant := range usqldb.MySQLVariants { | ||
| t.Run(name, func(t *testing.T) { | ||
| ctx := logging.TestContext(t.Context()) | ||
| repo := setupMySQLOffloadTest(ctx, t, variant) | ||
|
|
||
| versionX := saveOffload(ctx, t, repo, "uid-a", "node-x") | ||
| versionY := saveOffload(ctx, t, repo, "uid-a", "node-y") | ||
| require.NotEqual(t, versionX, versionY, "different nodes must produce different versions") | ||
| require.Equal(t, versionX, saveOffload(ctx, t, repo, "uid-b", "node-x"), "identical nodes must share a version") | ||
| require.Equal(t, versionY, saveOffload(ctx, t, repo, "uid-b", "node-y"), "identical nodes must share a version") | ||
| unwanted := UUIDVersion{UID: "uid-c", Version: saveOffload(ctx, t, repo, "uid-c", "node-c")} | ||
|
|
||
| wantedA := UUIDVersion{UID: "uid-a", Version: versionX} | ||
| wantedB := UUIDVersion{UID: "uid-b", Version: versionY} | ||
|
|
||
| got, err := repo.List(ctx, "argo", []UUIDVersion{wantedA, wantedB}) | ||
| require.NoError(t, err) | ||
|
|
||
| assert.Len(t, got, 2) | ||
| assert.Contains(t, got, wantedA) | ||
| assert.Contains(t, got, wantedB) | ||
| assert.NotContains(t, got, UUIDVersion{UID: "uid-a", Version: versionY}, "a version that was not asked for must not come back") | ||
| assert.NotContains(t, got, UUIDVersion{UID: "uid-b", Version: versionX}, "a version that was not asked for must not come back") | ||
| assert.NotContains(t, got, unwanted, "a uid that was not asked for must not come back") | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ import ( | |
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "maps" | ||
| "slices" | ||
| "sort" | ||
| "sync" | ||
| "time" | ||
|
|
@@ -285,19 +287,22 @@ func (s *workflowServer) ListWorkflows(ctx context.Context, req *workflowpkg.Wor | |
| } | ||
|
|
||
| cleaner := fields.NewCleaner(req.Fields) | ||
| logger := logging.RequireLoggerFromContext(ctx) | ||
| if s.offloadNodeStatusRepo.IsEnabled() && !cleaner.WillExclude("items.status.nodes") { | ||
| offloadedNodes, err := s.offloadNodeStatusRepo.List(ctx, req.Namespace) | ||
| if err != nil { | ||
| return nil, sutils.ToStatusError(err, codes.Internal) | ||
| } | ||
| // This page is already resolved, so we know exactly which offloaded rows we need. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing the unreachable
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deleted the const. |
||
| offloaded := map[int]sqldb.UUIDVersion{} | ||
| for i, wf := range wfs { | ||
| if wf.Status.IsOffloadNodeStatus() { | ||
| if s.offloadNodeStatusRepo.IsEnabled() { | ||
| wfs[i].Status.Nodes = offloadedNodes[sqldb.UUIDVersion{UID: string(wf.UID), Version: wf.GetOffloadNodeStatusVersion()}] | ||
| } else { | ||
| logger.WithFields(logging.Fields{"namespace": wf.Namespace, "name": wf.Name}).Warn(ctx, sqldb.OffloadNodeStatusDisabled) | ||
| } | ||
| offloaded[i] = sqldb.UUIDVersion{UID: string(wf.UID), Version: wf.GetOffloadNodeStatusVersion()} | ||
| } | ||
| } | ||
| // Nothing on this page is offloaded, so there is nothing to fetch. | ||
| if len(offloaded) > 0 { | ||
| offloadedNodes, err := s.offloadNodeStatusRepo.List(ctx, req.Namespace, slices.Collect(maps.Values(offloaded))) | ||
| if err != nil { | ||
| return nil, sutils.ToStatusError(err, codes.Internal) | ||
| } | ||
| for i, key := range offloaded { | ||
| wfs[i].Status.Nodes = offloadedNodes[key] | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These fixtures can't distinguish OR-of-ANDs from a broken cross-product: each uid has exactly one stored version, so
uid IN (...) AND version IN (...)would return the same rows and pass. SinceSavedeliberately leaves superseded rows behind, mismatched pairs genuinely exist in production. A fixture with uid-a at v1+v2 and uid-b at v1+v2, requesting (uid-a,v1) and (uid-b,v2), would pin the exact propertyuuidVersionInexists to provide.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both
uidsnow have the same two data payloads. Because the version number is based on the data, both uids now share the exact same version numbers.Before I made this change, every version number was unique to its
uid. Because of that, a bad database query (uid IN (...) AND version IN (...)) would accidentally return the correct rows. I checked this: my first test passed even though the query logic was wrong.Now, by sharing the version numbers, the bad query returns four rows and correctly fails the test.
Because this updated test now catches the problem, we don't need
TestMySQLListExcludesSupersededVersionsanymore, so I have deleted it.