Skip to content

Automated Test: dual-storage-enhanced #343

Closed

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

Release Notes

  • New Features

    • DualWriter Mode 3 now performs asynchronous legacy writes with timeouts while maintaining synchronous primary storage writes for Create, Update, Delete, and DeleteCollection operations.
    • Added List operation support to DualWriter Mode 3.
    • Enhanced logging with operation-specific context and per-method duration tracking.
  • Tests

    • Added comprehensive test coverage for Mode 3 across file, unified, and etcd storage backends.

* Dual writer: mode 3

* Add integration tests for playlits in mode 3

* Remove todo

* Update pkg/apiserver/rest/dualwriter_mode3.go

Co-authored-by: Arati R. <33031346+suntala@users.noreply.github.com>

* Admin: Fixes an issue where user accounts could not be enabled (#88117)

Fix: unable to enable user

* [REVIEW] FInish mode 3 and add tests

* Improve logging

* Update dependencies

* Update pkg/apiserver/rest/dualwriter_mode3_test.go

Co-authored-by: maicon <maiconscosta@gmail.com>

* remove test assertion

* Use mode log when dual writer is initiated

---------

Co-authored-by: Arati R. <33031346+suntala@users.noreply.github.com>
Co-authored-by: gonvee <gonvee@qq.com>
Co-authored-by: maicon <maiconscosta@gmail.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This diff refactors DualWriterMode3 to implement asynchronous legacy write paths while keeping storage writes synchronous, introduces comprehensive parameterized tests covering all operations, adds a public NewDualWriter constructor, and expands integration tests with mode 3 scenarios across multiple storage backends.

Changes

Cohort / File(s) Summary
DualWriterMode3 Core Logic
pkg/apiserver/rest/dualwriter_mode3.go
Implements asynchronous legacy write paths for Create, Delete, Update, and DeleteCollection with 10-second timeouts while preserving synchronous storage writes. Introduces per-method context-based logging, separate duration recording for storage and legacy paths, and a new List method for read-only storage operations. Adds mode3Str constant and imports for error handling and timing.
DualWriterMode3 Tests
pkg/apiserver/rest/dualwriter_mode3_test.go
Complete test suite rewrite introducing parameterized table-driven tests for Create, Get, List, Delete, DeleteCollection, and Update operations. Adds public NewDualWriter constructor supporting mode-agnostic dual-writer instantiation with legacy/storage backends and Prometheus registry, replacing previous mode-specific patterns. Tests validate result propagation, error handling, and object/list integrity.
DualWriterMode1 Tests
pkg/apiserver/rest/dualwriter_mode1_test.go
Refactors TestMode1_Get to reuse package-level Prometheus registry instead of creating a new instance per test, reducing redundant registry allocations.
Playlist Integration Tests
pkg/tests/apis/playlist/playlist_test.go
Adds mode 3 dual-writer test scenarios for file, unified storage, and etcd backends, mirroring existing mode 0/1/2 patterns. Includes explicit etcd collection cleanup for mode 3 tests to ensure clean state.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DualWriterMode3
    participant Storage
    participant LegacyStorage
    
    Client->>DualWriterMode3: Create/Update/Delete(ctx, obj)
    activate DualWriterMode3
    
    DualWriterMode3->>Storage: Perform primary operation (sync)
    activate Storage
    Storage-->>DualWriterMode3: Result/Error
    deactivate Storage
    
    par Record Storage Duration
        DualWriterMode3->>DualWriterMode3: Record timing & metrics
    and Launch Async Legacy Operation
        DualWriterMode3->>DualWriterMode3: Spawn goroutine with 10s timeout
        activate DualWriterMode3
        DualWriterMode3->>LegacyStorage: Perform legacy operation (async)
        activate LegacyStorage
        LegacyStorage-->>DualWriterMode3: Result/Error
        deactivate LegacyStorage
        DualWriterMode3->>DualWriterMode3: Record legacy duration & log
        deactivate DualWriterMode3
    end
    
    DualWriterMode3-->>Client: Return Storage result immediately
    deactivate DualWriterMode3
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hops through async dreams so bright,
Storage first, then Legacy takes flight!
Ten-second timeouts guard each pending dance,
While metrics bloom in every test's advance,
Mode 3 ascends with logging's gentle grace.

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Automated Test: dual-storage-enhanced' is vague and generic, using non-descriptive phrasing that doesn't clearly convey the main changes in the changeset. Use a more specific title that summarizes the primary change, such as 'Add asynchronous legacy writes in DualWriterMode3' or 'Implement Mode3 dual-writer with async legacy operations'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dual-storage-enhanced

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@pkg/apiserver/rest/dualwriter_mode3_test.go`:
- Around line 243-295: The success case in TestMode3_DeleteCollection doesn't
set the legacy store expectation so the shared mock silently absorbs the async
Legacy.DeleteCollection call; update the success test setup to configure the
legacy mock (legacyStoreMock / mock.Mock) to expect DeleteCollection and return
exampleObj, nil (mirroring the storage mock setup), or add a separate
setupLegacyFn that calls m.On("DeleteCollection", mock.Anything, mock.Anything,
tt.input, mock.Anything).Return(exampleObj, nil) before creating NewDualWriter
so the legacy path is explicitly asserted.
- Around line 47-77: The test spawns async legacy goroutines from
NewDualWriter(Mode3, ...) and never verifies or synchronizes them, causing
unverified expectations and potential races; update the test to wait for legacy
async calls before finishing by wiring the mock (m *mock.Mock) expectations to
signal completion (e.g., via mock.On(...).Run(...) that calls a
sync.WaitGroup.Done or closes a channel) for the legacy async methods
(Create/Delete/Update/DeleteCollection) used by legacyStoreMock, then call
wg.Wait() (or block on the channel) and finally call m.AssertExpectations(t)
after dw.Create returns to ensure all async calls completed and expectations are
validated.
- Around line 189-241: The success test TestMode3_Delete is missing explicit
legacy mock expectations so Legacy.Delete is implicitly matched via the shared
*mock.Mock; update the test to either (a) add a setupLegacyFn alongside
setupStorageFn and call it to configure m.On("Delete", mock.Anything, name,
mock.Anything, mock.Anything).Return(exampleObj, false, nil) for the legacy
path, or (b) stop sharing the same mock between legacyStoreMock and storageMock
by creating two distinct *mock.Mock instances and configuring each separately
before calling NewDualWriter(Mode3, ls, us, p), ensuring Legacy.Delete and
storage Delete expectations are set independently. Ensure references to
TestMode3_Delete, NewDualWriter, Mode3, legacyStoreMock, storageMock and
Legacy.Delete are used when locating the code to change.

In `@pkg/apiserver/rest/dualwriter_mode3.go`:
- Around line 161-167: In the asynchronous legacy DeleteCollection goroutine,
replace the incorrect call to d.recordStorageDuration with
d.recordLegacyDuration so the legacy path metrics are recorded; specifically in
the anonymous goroutine that calls d.Legacy.DeleteCollection, call
d.recordLegacyDuration(err != nil, mode3Str, options.Kind, method, startLegacy)
after the DeleteCollection call to match other async legacy blocks.
- Around line 44-48: In the Create error path in dualwriter_mode3.go, replace
the incorrect call to d.recordLegacyDuration(true, mode3Str, options.Kind,
method, startStorage) with d.recordStorageDuration(true, mode3Str, options.Kind,
method, startStorage) so the storage write error records storage duration; look
for the error handling block that logs "unable to create object in storage" and
update the call to use recordStorageDuration (preserving the same args and
boolean flag).
- Around line 94-106: In Delete, replace the incorrect context/log and metric
label: when creating the klog context use the method-enriched log variable (log)
instead of d.Log, and on the success path call d.recordStorageDuration with
options.Kind (the resource kind) rather than name; update references in the
Delete function (method name Delete, variables log, d.Log, startStorage, and the
call to d.recordStorageDuration) so the context and metric label match the error
path.
- Around line 127-132: In the Update storage error path in dualwriter_mode3.go,
replace the incorrect call to d.recordLegacyDuration with
d.recordStorageDuration so the storage write error is recorded correctly;
specifically change the call that currently invokes d.recordLegacyDuration(true,
mode3Str, options.Kind, method, startStorage) to use
d.recordStorageDuration(true, mode3Str, options.Kind, method, startStorage)
(same fix applied as for Create).
- Around line 50-57: The async legacy goroutine is deriving its timeout context
from the request context (using context.WithTimeoutCause(ctx,...)) which will be
canceled when the handler returns; change the parent context to
context.WithoutCancel(ctx) and then apply the timeout (i.e., use
context.WithTimeoutCause(context.WithoutCancel(ctx), time.Second*10, ...)) so
the 10s best-effort legacy write has a chance to finish; update the Create
goroutine where d.Legacy.Create is invoked and apply the same pattern to the
other async legacy goroutines used in Delete, Update and DeleteCollection (they
also call context.WithTimeoutCause and recordLegacyDuration).
🧹 Nitpick comments (2)
pkg/apiserver/rest/dualwriter_mode3_test.go (2)

49-54: Shared mock.Mock between legacy and storage makes test setups misleading.

Using a single *mock.Mock for both legacyStoreMock and storageMock means expectations are indistinguishable between the two backends. For example, in the Create error test (lines 38-44), the expectation is set via setupLegacyFn but is actually consumed by the storage call. In Delete/DeleteCollection success tests, no legacy mock is set up but the goroutine's legacy call silently re-matches the storage expectation.

This works by accident but makes tests brittle and hard to reason about. Consider using separate *mock.Mock instances for legacy and storage.

Also applies to: 217-222, 329-334


63-63: Inconsistent Prometheus registry usage across tests.

TestMode3_Create (line 63), TestMode3_List (line 174), TestMode3_Delete (line 228), TestMode3_DeleteCollection (line 282), and TestMode3_Update (line 343) use a package-level p, while TestMode3_Get (line 119) creates a local p := prometheus.NewRegistry(). Pick one approach for consistency.

Also applies to: 119-120, 174-174, 228-228, 282-282, 343-343

Comment on lines +47 to +77
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
s := (Storage)(nil)
m := &mock.Mock{}

ls := legacyStoreMock{m, l}
us := storageMock{m, s}

if tt.setupLegacyFn != nil {
tt.setupLegacyFn(m, tt.input)
}
if tt.setupStorageFn != nil {
tt.setupStorageFn(m)
}

dw := NewDualWriter(Mode3, ls, us, p)

obj, err := dw.Create(context.Background(), tt.input, func(context.Context, runtime.Object) error { return nil }, &metav1.CreateOptions{})

if tt.wantErr {
assert.Error(t, err)
return
}

acc, err := meta.Accessor(obj)
assert.NoError(t, err)
assert.Equal(t, acc.GetResourceVersion(), "1")
assert.NotEqual(t, obj, anotherObj)
})
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Async legacy goroutines are not verified and may race with test cleanup.

The tests don't call m.AssertExpectations(t) and don't synchronize with the background goroutines spawned by the production code. This means:

  1. Legacy async calls (Create, Delete, Update, DeleteCollection) are never verified — you have no assurance they actually execute or succeed.
  2. The goroutine may run after the test function returns, causing panics or data races on the shared mock.

Consider adding m.AssertExpectations(t) with a short sleep or, better, using a sync.WaitGroup / channel-based mechanism (e.g., via mock.On(...).Run(...)) to synchronize before asserting.

🤖 Prompt for AI Agents
In `@pkg/apiserver/rest/dualwriter_mode3_test.go` around lines 47 - 77, The test
spawns async legacy goroutines from NewDualWriter(Mode3, ...) and never verifies
or synchronizes them, causing unverified expectations and potential races;
update the test to wait for legacy async calls before finishing by wiring the
mock (m *mock.Mock) expectations to signal completion (e.g., via
mock.On(...).Run(...) that calls a sync.WaitGroup.Done or closes a channel) for
the legacy async methods (Create/Delete/Update/DeleteCollection) used by
legacyStoreMock, then call wg.Wait() (or block on the channel) and finally call
m.AssertExpectations(t) after dw.Create returns to ensure all async calls
completed and expectations are validated.

Comment on lines +189 to +241
func TestMode3_Delete(t *testing.T) {
type testCase struct {
setupStorageFn func(m *mock.Mock, name string)
name string
input string
wantErr bool
}
tests :=
[]testCase{
{
name: "deleting an object in the unified store",
input: "foo",
setupStorageFn: func(m *mock.Mock, name string) {
m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
},
},
{
name: "error when deleting an object in the unified store",
input: "object-fail",
setupStorageFn: func(m *mock.Mock, name string) {
m.On("Delete", mock.Anything, name, mock.Anything, mock.Anything).Return(nil, false, errors.New("error"))
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
s := (Storage)(nil)
m := &mock.Mock{}

ls := legacyStoreMock{m, l}
us := storageMock{m, s}

if tt.setupStorageFn != nil {
tt.setupStorageFn(m, tt.input)
}

dw := NewDualWriter(Mode3, ls, us, p)

obj, _, err := dw.Delete(context.Background(), tt.input, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{})

if tt.wantErr {
assert.Error(t, err)
return
}

assert.Equal(t, obj, exampleObj)
assert.NotEqual(t, obj, anotherObj)
})
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Delete success test does not set up legacy mock expectations.

The success case (line 199) only configures setupStorageFn. In mode 3, Delete also calls d.Legacy.Delete(...) asynchronously. No setupLegacyFn is defined, so the legacy call matches the storage expectation only because both backends share the same *mock.Mock. This silently masks a missing test setup.

🤖 Prompt for AI Agents
In `@pkg/apiserver/rest/dualwriter_mode3_test.go` around lines 189 - 241, The
success test TestMode3_Delete is missing explicit legacy mock expectations so
Legacy.Delete is implicitly matched via the shared *mock.Mock; update the test
to either (a) add a setupLegacyFn alongside setupStorageFn and call it to
configure m.On("Delete", mock.Anything, name, mock.Anything,
mock.Anything).Return(exampleObj, false, nil) for the legacy path, or (b) stop
sharing the same mock between legacyStoreMock and storageMock by creating two
distinct *mock.Mock instances and configuring each separately before calling
NewDualWriter(Mode3, ls, us, p), ensuring Legacy.Delete and storage Delete
expectations are set independently. Ensure references to TestMode3_Delete,
NewDualWriter, Mode3, legacyStoreMock, storageMock and Legacy.Delete are used
when locating the code to change.

Comment on lines +243 to +295
func TestMode3_DeleteCollection(t *testing.T) {
type testCase struct {
input *metav1.DeleteOptions
setupStorageFn func(m *mock.Mock, input *metav1.DeleteOptions)
name string
wantErr bool
}
tests :=
[]testCase{
{
name: "deleting a collection in the unified store",
input: &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "foo"}},
setupStorageFn: func(m *mock.Mock, input *metav1.DeleteOptions) {
m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(exampleObj, nil)
},
},
{
name: "error deleting a collection in the unified store",
input: &metav1.DeleteOptions{TypeMeta: metav1.TypeMeta{Kind: "fail"}},
setupStorageFn: func(m *mock.Mock, input *metav1.DeleteOptions) {
m.On("DeleteCollection", mock.Anything, mock.Anything, input, mock.Anything).Return(nil, errors.New("error"))
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
s := (Storage)(nil)
m := &mock.Mock{}

ls := legacyStoreMock{m, l}
us := storageMock{m, s}

if tt.setupStorageFn != nil {
tt.setupStorageFn(m, tt.input)
}

dw := NewDualWriter(Mode3, ls, us, p)

obj, err := dw.DeleteCollection(context.Background(), func(ctx context.Context, obj runtime.Object) error { return nil }, tt.input, &metainternalversion.ListOptions{})

if tt.wantErr {
assert.Error(t, err)
return
}

assert.Equal(t, obj, exampleObj)
assert.NotEqual(t, obj, anotherObj)
})
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

DeleteCollection success test does not set up legacy mock expectations.

Same issue as Delete — the success case does not wire a legacy mock, relying on the shared mock to absorb the async d.Legacy.DeleteCollection(...) call.

🤖 Prompt for AI Agents
In `@pkg/apiserver/rest/dualwriter_mode3_test.go` around lines 243 - 295, The
success case in TestMode3_DeleteCollection doesn't set the legacy store
expectation so the shared mock silently absorbs the async
Legacy.DeleteCollection call; update the success test setup to configure the
legacy mock (legacyStoreMock / mock.Mock) to expect DeleteCollection and return
exampleObj, nil (mirroring the storage mock setup), or add a separate
setupLegacyFn that calls m.On("DeleteCollection", mock.Anything, mock.Anything,
tt.input, mock.Anything).Return(exampleObj, nil) before creating NewDualWriter
so the legacy path is explicitly asserted.

Comment on lines 44 to +48
log.Error(err, "unable to create object in storage")
d.recordLegacyDuration(true, mode3Str, options.Kind, method, startStorage)
return created, err
}
d.recordStorageDuration(false, mode3Str, options.Kind, method, startStorage)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Recording legacy duration instead of storage duration on Create error path.

Line 45 calls d.recordLegacyDuration but this is the storage write error path. It should be d.recordStorageDuration.

🐛 Proposed fix
 	if err != nil {
 		log.Error(err, "unable to create object in storage")
-		d.recordLegacyDuration(true, mode3Str, options.Kind, method, startStorage)
+		d.recordStorageDuration(true, mode3Str, options.Kind, method, startStorage)
 		return created, err
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
log.Error(err, "unable to create object in storage")
d.recordLegacyDuration(true, mode3Str, options.Kind, method, startStorage)
return created, err
}
d.recordStorageDuration(false, mode3Str, options.Kind, method, startStorage)
log.Error(err, "unable to create object in storage")
d.recordStorageDuration(true, mode3Str, options.Kind, method, startStorage)
return created, err
}
d.recordStorageDuration(false, mode3Str, options.Kind, method, startStorage)
🤖 Prompt for AI Agents
In `@pkg/apiserver/rest/dualwriter_mode3.go` around lines 44 - 48, In the Create
error path in dualwriter_mode3.go, replace the incorrect call to
d.recordLegacyDuration(true, mode3Str, options.Kind, method, startStorage) with
d.recordStorageDuration(true, mode3Str, options.Kind, method, startStorage) so
the storage write error records storage duration; look for the error handling
block that logs "unable to create object in storage" and update the call to use
recordStorageDuration (preserving the same args and boolean flag).

Comment on lines +50 to +57
go func() {
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy create timeout"))
defer cancel()

startLegacy := time.Now()
_, errObjectSt := d.Legacy.Create(ctx, obj, createValidation, options)
d.recordLegacyDuration(errObjectSt != nil, mode3Str, options.Kind, method, startLegacy)
}()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Go 1.21 context.WithoutCancel function documentation

💡 Result:

context.WithoutCancel (added in Go 1.21)

func WithoutCancel(parent context.Context) context.Context

WithoutCancel returns a derived context that still references the parent for values, but is not canceled when the parent is canceled. The returned context:

  • has no deadline (Deadline reports none),
  • returns no error (Err is nil),
  • has a nil Done channel (Done() == nil),
  • returns nil cause (context.Cause(...) == nil). (pkg.go.dev)

Go 1.21 release notes summary: it “returns a copy of a context that is not canceled when the original context is canceled.” (go.dev)

Citations:


🏁 Script executed:

# First, let's check the file and verify the code pattern
cat -n pkg/apiserver/rest/dualwriter_mode3.go | head -170

Repository: admin-coderabbit/coderabbit-grafana-mirror

Length of output: 8081


Async legacy goroutines derive context from the request context, which will be cancelled once the response is sent.

All fire-and-forget goroutines call context.WithTimeoutCause(ctx, ...) where ctx is the HTTP request context. Since the storage result is returned immediately, the HTTP handler will complete and cancel ctx, racing against the 10-second timeout. The legacy write may be aborted before it finishes.

If these legacy writes are best-effort but should still have a reasonable chance of completing, use context.WithoutCancel(ctx) (Go 1.21+) as the parent, preserving the 10-second timeout as the upper bound.

🛡️ Example fix for Create (apply similarly to Delete, Update, DeleteCollection)
 	go func() {
-		ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy create timeout"))
+		asyncCtx, cancel := context.WithTimeoutCause(context.WithoutCancel(ctx), time.Second*10, errors.New("legacy create timeout"))
 		defer cancel()
 
 		startLegacy := time.Now()
-		_, errObjectSt := d.Legacy.Create(ctx, obj, createValidation, options)
+		_, errObjectSt := d.Legacy.Create(asyncCtx, obj, createValidation, options)
 		d.recordLegacyDuration(errObjectSt != nil, mode3Str, options.Kind, method, startLegacy)
 	}()

Also applies to: 108-114, 134-141, 161-167

🤖 Prompt for AI Agents
In `@pkg/apiserver/rest/dualwriter_mode3.go` around lines 50 - 57, The async
legacy goroutine is deriving its timeout context from the request context (using
context.WithTimeoutCause(ctx,...)) which will be canceled when the handler
returns; change the parent context to context.WithoutCancel(ctx) and then apply
the timeout (i.e., use context.WithTimeoutCause(context.WithoutCancel(ctx),
time.Second*10, ...)) so the 10s best-effort legacy write has a chance to
finish; update the Create goroutine where d.Legacy.Create is invoked and apply
the same pattern to the other async legacy goroutines used in Delete, Update and
DeleteCollection (they also call context.WithTimeoutCause and
recordLegacyDuration).

Comment on lines +94 to +106
func (d *DualWriterMode3) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
var method = "delete"
log := d.Log.WithValues("name", name, "kind", options.Kind, "method", method)
ctx = klog.NewContext(ctx, d.Log)

startStorage := time.Now()
res, async, err := d.Storage.Delete(ctx, name, deleteValidation, options)
if err != nil {
log.Error(err, "unable to delete object in storage")
d.recordStorageDuration(true, mode3Str, options.Kind, method, startStorage)
return res, async, err
}
d.recordStorageDuration(false, mode3Str, name, method, startStorage)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Two bugs in Delete: wrong logger in context, wrong metric label on success path.

  1. Line 97: klog.NewContext(ctx, d.Log) passes the base d.Log instead of the method-enriched log variable. Every other method uses log.
  2. Line 106: d.recordStorageDuration(false, mode3Str, name, method, startStorage) passes name (the object name) as the kind label. The error path on line 103 correctly uses options.Kind. This will produce mismatched/garbage metric labels.
🐛 Proposed fix
 	log := d.Log.WithValues("name", name, "kind", options.Kind, "method", method)
-	ctx = klog.NewContext(ctx, d.Log)
+	ctx = klog.NewContext(ctx, log)

 	startStorage := time.Now()
 	res, async, err := d.Storage.Delete(ctx, name, deleteValidation, options)
 	if err != nil {
 		log.Error(err, "unable to delete object in storage")
 		d.recordStorageDuration(true, mode3Str, options.Kind, method, startStorage)
 		return res, async, err
 	}
-	d.recordStorageDuration(false, mode3Str, name, method, startStorage)
+	d.recordStorageDuration(false, mode3Str, options.Kind, method, startStorage)
🤖 Prompt for AI Agents
In `@pkg/apiserver/rest/dualwriter_mode3.go` around lines 94 - 106, In Delete,
replace the incorrect context/log and metric label: when creating the klog
context use the method-enriched log variable (log) instead of d.Log, and on the
success path call d.recordStorageDuration with options.Kind (the resource kind)
rather than name; update references in the Delete function (method name Delete,
variables log, d.Log, startStorage, and the call to d.recordStorageDuration) so
the context and metric label match the error path.

Comment on lines 127 to +132
if err != nil {
log.WithValues("object", updated).Error(err, "could not update or create object")
return nil, false, err
}
objInfo = &updateWrapper{
upstream: objInfo,
updated: updated,
log.Error(err, "unable to update in storage")
d.recordLegacyDuration(true, mode3Str, options.Kind, method, startStorage)
return res, async, err
}
d.recordStorageDuration(false, mode3Str, options.Kind, method, startStorage)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Recording legacy duration instead of storage duration on Update error path.

Line 129 calls d.recordLegacyDuration but this is the storage write error path — same issue as in Create.

🐛 Proposed fix
 	if err != nil {
 		log.Error(err, "unable to update in storage")
-		d.recordLegacyDuration(true, mode3Str, options.Kind, method, startStorage)
+		d.recordStorageDuration(true, mode3Str, options.Kind, method, startStorage)
 		return res, async, err
 	}
🤖 Prompt for AI Agents
In `@pkg/apiserver/rest/dualwriter_mode3.go` around lines 127 - 132, In the Update
storage error path in dualwriter_mode3.go, replace the incorrect call to
d.recordLegacyDuration with d.recordStorageDuration so the storage write error
is recorded correctly; specifically change the call that currently invokes
d.recordLegacyDuration(true, mode3Str, options.Kind, method, startStorage) to
use d.recordStorageDuration(true, mode3Str, options.Kind, method, startStorage)
(same fix applied as for Create).

Comment on lines +161 to +167
go func() {
startLegacy := time.Now()
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy deletecollection timeout"))
defer cancel()
_, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
d.recordStorageDuration(err != nil, mode3Str, options.Kind, method, startLegacy)
}()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Recording storage duration instead of legacy duration in DeleteCollection goroutine.

Line 166 calls d.recordStorageDuration inside the legacy goroutine. This should be d.recordLegacyDuration, consistent with every other async legacy block.

🐛 Proposed fix
 	go func() {
 		startLegacy := time.Now()
 		ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy deletecollection timeout"))
 		defer cancel()
 		_, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
-		d.recordStorageDuration(err != nil, mode3Str, options.Kind, method, startLegacy)
+		d.recordLegacyDuration(err != nil, mode3Str, options.Kind, method, startLegacy)
 	}()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
go func() {
startLegacy := time.Now()
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy deletecollection timeout"))
defer cancel()
_, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
d.recordStorageDuration(err != nil, mode3Str, options.Kind, method, startLegacy)
}()
go func() {
startLegacy := time.Now()
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy deletecollection timeout"))
defer cancel()
_, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
d.recordLegacyDuration(err != nil, mode3Str, options.Kind, method, startLegacy)
}()
🤖 Prompt for AI Agents
In `@pkg/apiserver/rest/dualwriter_mode3.go` around lines 161 - 167, In the
asynchronous legacy DeleteCollection goroutine, replace the incorrect call to
d.recordStorageDuration with d.recordLegacyDuration so the legacy path metrics
are recorded; specifically in the anonymous goroutine that calls
d.Legacy.DeleteCollection, call d.recordLegacyDuration(err != nil, mode3Str,
options.Kind, method, startLegacy) after the DeleteCollection call to match
other async legacy blocks.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant