Skip to content

Conversation

@akshayutture-augment
Copy link

@akshayutture-augment akshayutture-augment commented Nov 14, 2025

Test 6

Summary by CodeRabbit

Release Notes

  • New Features

    • Added list functionality for dual-write mode 3 operations to support reading from storage.
  • Tests

    • Enhanced test coverage for dual-write mode 3 with comprehensive test cases across Create, Get, List, Delete, and Update operations.
    • Added new integration tests for dual-write mode 3 across multiple storage backends.

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/grafana-coderabbit#6

* 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. <[email protected]>

* 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 <[email protected]>

* remove test assertion

* Use mode log when dual writer is initiated

---------

Co-authored-by: Arati R. <[email protected]>
Co-authored-by: gonvee <[email protected]>
Co-authored-by: maicon <[email protected]>
@akshayutture-augment
Copy link
Author

@coderabbitai review

@akshayutture-augment
Copy link
Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

Walkthrough

This pull request adds a List() method to DualWriterMode3, refactors dualwriter_mode3_test with comprehensive table-driven test coverage for CRUD operations, cleans up a Prometheus registry declaration in dualwriter_mode1_test, and extends playlist integration tests to exercise Mode3 dual writes across file, unified, and etcd storage backends.

Changes

Cohort / File(s) Summary
DualWriterMode3 implementation
pkg/apiserver/rest/dualwriter_mode3.go
Added public List() method that reads from Storage with logging context and duration tracking.
DualWriterMode3 test suite
pkg/apiserver/rest/dualwriter_mode3_test.go
Replaced incomplete scaffold with comprehensive table-driven test suite covering Create, Get, List, Delete, DeleteCollection, and Update operations; introduced mock storage/legacy storage with error scenarios and testify assertions.
Mode1 test cleanup
pkg/apiserver/rest/dualwriter_mode1_test.go
Removed locally declared Prometheus registry; now uses package-level registry.
Playlist integration tests
pkg/tests/apis/playlist/playlist_test.go
Added new test subtests for Mode3 dual writes with file storage, unified storage, and etcd storage backends; included etcd collection cleanup.

Sequence Diagram(s)

sequenceDiagram
    participant Test
    participant DualWriterMode3
    participant Storage
    participant Legacy

    Test->>DualWriterMode3: List(ctx, options)
    DualWriterMode3->>Storage: List from unified storage
    Storage-->>DualWriterMode3: runtime.Object
    DualWriterMode3-->>Test: runtime.Object, error
    
    Note over DualWriterMode3: Logs context & tracks duration
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • dualwriter_mode3.go: New List() method implementation requires verification of correct Storage interaction, logging, and duration tracking semantics.
  • dualwriter_mode3_test.go: Comprehensive test suite with mocked storage layers and multiple operation paths (CRUD) demands careful review of mock configurations and assertion logic across all test cases.
  • playlist_test.go: Integration test additions spanning multiple storage backends require validation of test setup consistency and proper cleanup logic (especially etcd collection deletion).

Poem

🐰 A List() arrives in Mode3's land,
With tests so grand, both mocked and planned,
From file to etcd, storage flows free,
Table-driven tales of CRUD's decree! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Dual Storage Architecture' is vague and generic, failing to convey the specific changes in the PR (Mode3 List method, test suite expansion, and integration tests). Use a more descriptive title that captures the primary changes, such as 'Add DualWriterMode3.List method and comprehensive test suite' or 'Implement Mode3 dual write testing across storage backends'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai 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: 1

🧹 Nitpick comments (2)
pkg/tests/apis/playlist/playlist_test.go (1)

132-144: Mode3 playlist integration coverage for file/unified/etcd backends mirrors existing modes

The three new subtests:

  • "with dual write (file, mode 3)",
  • "with dual write (unified storage, mode 3)", and
  • "with dual write (etcd, mode 3)"

all correctly:

  • Set the expected APIServerStorageType ("file", "unified", "etcd").
  • Enable featuremgmt.FlagKubernetesPlaylists, plus featuremgmt.FlagUnifiedStorage for unified.
  • Configure DualWriterDesiredModes to grafanarest.Mode3 for playlistv0alpha1.GROUPRESOURCE.
  • Reuse doPlaylistTests so Mode3 flows get the same CRUD and cross-API assertions as modes 0–2.

This is a straightforward and consistent extension of coverage. If duplication across modes becomes painful, you could later switch this whole group to a table-driven pattern, but it’s not necessary for this change.

Also applies to: 191-204, 287-312

pkg/apiserver/rest/dualwriter_mode3_test.go (1)

17-78: Mode3 unit tests provide good CRUD/List coverage; consider tighter backend-specific assertions later

The new table-driven tests for Mode3:

  • Cover Create, Get, List, Delete, DeleteCollection, and Update success and error paths.
  • Use legacyStoreMock / storageMock wrappers to plug a single *mock.Mock into both Legacy and Storage.
  • Verify:
    • Returned objects vs exampleObj / anotherObj.
    • List outputs vs exampleList / anotherList.
    • Error vs non-error flows based on test cases.

This is solid baseline coverage for the new List method and Mode3’s “write to both, read from Storage” semantics.

If you later need stricter guarantees, two incremental improvements to consider:

  1. Differentiate legacy vs storage interactions
    Using separate mocks for Legacy and Storage (or asserting call counts per backend) would let you express expectations like “Legacy is not called when Storage fails on Create” or “DeleteCollection always triggers the async legacy delete”. Today the shared *mock.Mock makes those distinctions harder to encode.

  2. Optionally assert metrics side-effects
    Once metrics wiring in dualwriter_mode3.go is corrected, focused tests around dualWriterMetrics would help catch regressions in storage vs legacy duration labeling.

None of this blocks the current PR; the existing tests already add meaningful coverage over Mode3 behavior.

Also applies to: 80-133, 135-187, 189-241, 243-295, 297-356

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3a5b3 and 67b74e1.

⛔ Files ignored due to path filters (1)
  • go.work.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • pkg/apiserver/rest/dualwriter_mode1_test.go (0 hunks)
  • pkg/apiserver/rest/dualwriter_mode3.go (2 hunks)
  • pkg/apiserver/rest/dualwriter_mode3_test.go (1 hunks)
  • pkg/tests/apis/playlist/playlist_test.go (3 hunks)
💤 Files with no reviewable changes (1)
  • pkg/apiserver/rest/dualwriter_mode1_test.go
🔇 Additional comments (2)
pkg/apiserver/rest/dualwriter_mode3.go (2)

24-26: Constructor and mode constant look consistent with other dual-writer modes

newDualWriterMode3 wiring (legacy, storage, metrics, and a per-instance logger labeled with mode3Str) is coherent and should integrate cleanly with existing dual-writer infrastructure.

Also applies to: 33-34


62-76: Get/List behavior for Mode3 reads from Storage looks correct and consistent

  • Get and the new List method both:
    • Attach a per-call logger with useful fields (kind, name or resourceVersion, method).
    • Measure Storage latency via recordStorageDuration(err != nil, ...).
    • Read exclusively from Storage, which matches Mode3’s “read from new storage” semantics.
  • Asynchronous legacy writes are intentionally absent from List, which is appropriate since Mode3 reads-only behavior doesn’t require legacy interaction on list.

No functional issues spotted here.

Also applies to: 78-92

Comment on lines 36 to 60
func (d *DualWriterMode3) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
log := klog.FromContext(ctx)
var method = "create"
log := d.Log.WithValues("kind", options.Kind, "method", method)
ctx = klog.NewContext(ctx, log)

startStorage := time.Now()
created, err := d.Storage.Create(ctx, obj, createValidation, options)
if err != nil {
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)

if _, err := d.Legacy.Create(ctx, obj, createValidation, options); err != nil {
log.WithValues("object", created).Error(err, "unable to create object in legacy storage")
}
return created, nil
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)
}()

return created, err
}
Copy link

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

Metrics helpers mis-wired and one logging-context inconsistency in Mode3 write paths

There are a few subtle but important inconsistencies in how metrics and logging are wired:

  • Create error path (Lines 43–46)
    On storage failure you record a legacy duration using the storage start time and never record a storage failure metric:

    • d.recordLegacyDuration(true, mode3Str, options.Kind, method, startStorage)
      This should be a storage metric to accurately reflect that the error originates from Storage.Create.
  • Delete success path label mismatch (Line 106)
    You record storage duration using name instead of options.Kind for the “kind” label:

    • d.recordStorageDuration(false, mode3Str, name, method, startStorage)
      All other methods use options.Kind in this position, so this will pollute metrics with object names instead of resource kinds.
  • Delete logging context (Line 97)
    You build an enriched logger as log := d.Log.WithValues("name", name, "kind", options.Kind, "method", method) but then attach the base logger to the context:

    • ctx = klog.NewContext(ctx, d.Log)
      This is inconsistent with Create/Get/List/DeleteCollection/Update and drops the per-call fields from the context logger.
  • Update error path (Lines 127–130)
    On storage failure you again record a legacy duration with the storage start time:

    • d.recordLegacyDuration(true, mode3Str, options.Kind, method, startStorage)
      This should be a storage failure metric.
  • DeleteCollection legacy path (Lines 161–167)
    In the legacy goroutine you call d.recordStorageDuration instead of d.recordLegacyDuration:

    • d.recordStorageDuration(err != nil, mode3Str, options.Kind, method, startLegacy)
      This double-counts storage metrics and never records legacy DeleteCollection timings.

These don’t break functional behavior but will significantly skew observability and make it harder to reason about Mode3 traffic across backends.

A minimal, consistent fix would look like:

@@ func (d *DualWriterMode3) Create(...
-   if err != nil {
-       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)
+   if err != nil {
+       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)
@@ func (d *DualWriterMode3) Delete(...
-   log := d.Log.WithValues("name", name, "kind", options.Kind, "method", method)
-   ctx = klog.NewContext(ctx, d.Log)
+   log := d.Log.WithValues("name", name, "kind", options.Kind, "method", method)
+   ctx = klog.NewContext(ctx, log)
@@
-   d.recordStorageDuration(false, mode3Str, name, method, startStorage)
+   d.recordStorageDuration(false, mode3Str, options.Kind, method, startStorage)
@@ func (d *DualWriterMode3) Update(...
-   if err != nil {
-       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)
+   if err != nil {
+       log.Error(err, "unable to update in storage")
+       d.recordStorageDuration(true, mode3Str, options.Kind, method, startStorage)
+       return res, async, err
+   }
+   d.recordStorageDuration(false, mode3Str, options.Kind, method, startStorage)
@@ func (d *DualWriterMode3) DeleteCollection(...
-       _, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
-       d.recordStorageDuration(err != nil, mode3Str, options.Kind, method, startLegacy)
+       _, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
+       d.recordLegacyDuration(err != nil, mode3Str, options.Kind, method, startLegacy)

Also applies to: 94-117, 120-144, 147-170


I'll verify the review comment by examining the actual code in the repository. Let me retrieve the file and check each claim.


🏁 Script executed:

#!/bin/bash
# Extract the full dualwriter_mode3.go file to verify all claims
cat pkg/apiserver/rest/dualwriter_mode3.go

Length of output: 7431


🏁 Script executed:

#!/bin/bash
# Alternative: use rg to find the specific methods mentioned
rg -n "func \(d \*DualWriterMode3\) (Create|Delete|Update|DeleteCollection)" pkg/apiserver/rest/dualwriter_mode3.go -A 50

Length of output: 6805


All five issues confirmed as accurate; critical metrics and logging inconsistencies need fixing.

Verification of the actual code confirms every claim in the review comment:

  1. Create error path (line 45): Correctly identified—recordLegacyDuration is used on Storage.Create failure instead of recordStorageDuration.

  2. Delete label mismatch (line 106): Correctly identified—Delete is the only method using name instead of options.Kind for the "kind" metric label; all other methods (Create, Get, List, Update, DeleteCollection) use options.Kind.

  3. Delete logging context (line 97): Correctly identified—Delete attaches base d.Log to context instead of the enriched log created on line 96; all other methods attach the enriched logger.

  4. Update error path (line 129): Correctly identified—recordLegacyDuration is used on Storage.Update failure instead of recordStorageDuration.

  5. DeleteCollection legacy path (line 166): Correctly identified—the legacy goroutine calls recordStorageDuration with startLegacy timing instead of recordLegacyDuration, double-counting storage metrics.

The suggested fixes in the review are appropriate. These inconsistencies will indeed cause observability issues and misattribution of metrics to the wrong backend paths.

🤖 Prompt for AI Agents
pkg/apiserver/rest/dualwriter_mode3.go lines 36-60 (and related locations noted
in review): multiple metric/logging mis-attributions need correction—1) at
Storage.Create error path (around line 45) replace the incorrect call to
recordLegacyDuration with recordStorageDuration and keep the rest of the error
handling intact; 2) align Delete metric "kind" label (around line 106) to use
options.Kind instead of name so it matches other methods; 3) fix Delete logging
context (around line 97) to attach the enriched log (log) to the context instead
of the base d.Log; 4) at Storage.Update error path (around line 129) replace the
incorrect recordLegacyDuration call with recordStorageDuration; 5) in
DeleteCollection legacy goroutine (around line 166) replace the incorrect
recordStorageDuration call with recordLegacyDuration and pass the correct
startLegacy timing so storage and legacy durations aren’t double-counted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants