Dual Storage Architecture (Mode 3)

grafana/grafana#90045 TypeScript / Go
Golden recall: 100% Extra findings: 10

Golden Comments (4/4 found)

# Comment Severity Status FriendlyReviewer Detail
#1 Logger context: d.Log used instead of enriched log → loss of context values (name/kind/method) MEDIUM ✓ Found FriendlyReviewer: "Wrong logger context in Delete — uses d.Log instead of enriched log" (dualwriter_mode3.go L94)
#2a recordLegacyDuration used on Create error — uses recordLegacyDuration instead of recordStorageDuration when storage operation fails HIGH ✓ Found FriendlyReviewer: "Wrong metrics recording on Create error — uses recordLegacyDuration instead of recordStorageDuration" (dualwriter_mode3.go L48)
#2b recordLegacyDuration used on Update error — uses recordLegacyDuration instead of recordStorageDuration when storage operation fails HIGH ✓ Found FriendlyReviewer: "Wrong metrics recording on Update error — uses recordLegacyDuration instead of recordStorageDuration" (dualwriter_mode3.go L131)
#3 name used instead of options.Kind for metrics → inconsistency with other methods MEDIUM ✓ Found FriendlyReviewer: "Wrong metric label in Delete success path — passes object name instead of options.Kind" (dualwriter_mode3.go L103)

Supplementary Findings (12 findings)

Finding File Severity Legitimate?
Wrong metrics recording in DeleteCollection goroutine — uses recordStorageDuration for a legacy operation (inverse of golden #2) dualwriter_mode3.go:167 HIGH ✓ Extension of the same pattern — opposite direction
Mock cross-contamination — legacy and storage mocks share the same *mock.Mock, causing unintended expectation matching dualwriter_mode3_test.go:192 HIGH ✓ Real test reliability issue
Missing legacy mock setup in DeleteCollection test — goroutine relies on cross-contamination to avoid panics dualwriter_mode3_test.go:254 HIGH ✓ Test survives only by accident
Race condition — no synchronization for fire-and-forget goroutines, leading to flaky tests dualwriter_mode3_test.go:192 HIGH ✓ Unsyncronized goroutines in tests
AssertNotCalled uses tt.name (test case description) instead of tt.input (object name), making assertion ineffective dualwriter_mode1_test.go:146 HIGH ✓ Wrong variable in assertion
AssertNotCalled for Storage.Get contradicts Mode1 implementation that launches a goroutine calling Storage.Get dualwriter_mode1_test.go:146 HIGH ✓ Assertion contradicts actual behavior
AssertNotCalled for Storage.Delete contradicts Mode1 behavior dualwriter_mode1_test.go:240 HIGH ✓ Same pattern as Storage.Get
AssertNotCalled for Storage.DeleteCollection contradicts Mode1 behavior dualwriter_mode1_test.go:300 HIGH ✓ Same pattern as Storage.Get
Missing error check — test case for legacy error does not set wantErr: true, so the error path is not asserted dualwriter_mode1_test.go:175 MEDIUM ✓ Error path untested
Missing coverage — Mode 4 integration tests are absent across all storage backends playlist_test.go:283 MEDIUM ✓ Observation for completeness
Inconsistency — test uses local Prometheus registry while other tests use package-level variable dualwriter_mode3_test.go:108 LOW ✓ Minor consistency issue
Unused mock expectations — Get and Update expectations configured but never matched (goroutine not launched on Legacy error) dualwriter_mode1_test.go:355 LOW ✓ Cleanup opportunity

Honest Analysis

FriendlyReviewer detected all 4 golden bugs in this PR, making it a perfect sweep on the reference set. The metrics recording mixup (golden #2) was found on both error paths (Create L48 and Update L131), correctly identified as High severity. The reviewer also found the inverse pattern in DeleteCollection — an extension of the same bug that the golden benchmark itself did not capture.

The logger context issue (golden #1) and the wrong metric label (golden #3) were both identified with matching severity. FriendlyReviewer did not inflate or downgrade these — the diagnosis is precise and the remediation is clear.

Where this review truly shines is the test suite analysis. FriendlyReviewer uncovered 8 critical bugs in the test files that the golden comments never mentioned: mock cross-contamination between legacy and storage mocks, a missing legacy mock setup that makes the DeleteCollection test survive only by accident, unsynchronized goroutines creating race conditions, and a cascade of incorrect AssertNotCalled assertions across the Mode1 tests that fundamentally contradict the actual behavior being tested. The most striking is AssertNotCalled using tt.name (a test case description string) instead of tt.input (the object name), making the assertion match against the wrong value entirely.

The test reliability issues are particularly significant. Mock cross-contamination on shared *mock.Mock objects means expectations from one test can be inadvertently matched by another — this makes the test suite fundamentally unreliable, not just flaky. FriendlyReviewer is uniquely positioned to detect this because it reads the full test file, not just the diff.

Two medium findings (missing error check in Mode1 tests, absent Mode 4 integration tests) and two low findings (registry inconsistency, unused mock expectations) round out the supplementary findings. The low findings are acceptable noise — the real value is in the critical test bugs that change how these tests should be understood.

That said, the review does have limitations. The supplementary findings, while numerous, are dominated by test quality issues (the mock cross-contamination, race conditions, and incorrect assertions). Only 2 of the 12 supplementary findings touch production code — and one of those (the DeleteCollection inverse pattern) is essentially the same bug as golden #2 mirrored. The production-code review depth is therefore narrower than the raw finding count suggests. A stronger review would surface a wider variety of distinct production-code issues rather than concentrating so heavily on the test harness.

Additionally, several of the supplementary findings are interdependent: the mock cross-contamination directly enables the "missing legacy mock setup" finding, and the race condition finding overlaps with the goroutine analysis. Presenting these as independent findings inflates the count. FriendlyReviewer would benefit from grouping causally related findings rather than listing every manifestation separately.

FriendlyReviewer's full review

As posted on the PR — the complete output from FriendlyReviewer.

friendly-reviewer bot commented now

Hi there 👋

🌥️ Tech
10 high, 4 medium, 2 low

The code review identified several critical bugs in the dualwriter_mode3.go implementation, including incorrect metrics recording for create/update errors and wrong label usage, as well as a logger context error. The test files contain significant issues: mock cross-contamination between legacy and storage mocks, missing legacy call verification, race conditions due to unsynchronized goroutines, and incorrect assertions. Additionally, the Mode1 tests have a bug using test case names instead of input values, and incorrect AssertNotCalled assertions that contradict the actual behavior. Some test cases have missing error checks and unused mock expectations. Mode 4 integration tests are absent. These issues undermine test reliability and could lead to undetected regressions.


These might need a close look

  • 🔴 pkg/apiserver/rest/dualwriter_mode3.go (L48)
    HIGH: Wrong metrics recording on Create error — uses recordLegacyDuration instead of recordStorageDuration.
  • 🔴 pkg/apiserver/rest/dualwriter_mode3.go (L131)
    HIGH: Wrong metrics recording on Update error — uses recordLegacyDuration instead of recordStorageDuration.
  • 🔴 pkg/apiserver/rest/dualwriter_mode3.go (L167)
    HIGH: Wrong metrics recording in DeleteCollection goroutine — uses recordStorageDuration for a legacy operation.
  • 🔴 pkg/apiserver/rest/dualwriter_mode3_test.go (L192)
    HIGH: Mock cross-contamination — legacy and storage mocks share the same *mock.Mock, causing unintended expectation matching.
  • 🔴 pkg/apiserver/rest/dualwriter_mode3_test.go (L254)
    HIGH: Missing legacy mock setup in DeleteCollection test — goroutine calls rely on cross-contamination to avoid panics.
  • 🔴 pkg/apiserver/rest/dualwriter_mode3_test.go (L192)
    RACE CONDITION: No synchronization for fire-and-forget goroutines — tests may pass before goroutines execute, leading to flaky tests.
  • 🔴 pkg/apiserver/rest/dualwriter_mode1_test.go (L146)
    BUG: AssertNotCalled uses tt.name (test case description) instead of tt.input (object name), making assertion ineffective.
  • 🔴 pkg/apiserver/rest/dualwriter_mode1_test.go (L146)
    INCORRECT ASSERTION: AssertNotCalled for Storage.Get contradicts Mode1 implementation that launches a goroutine calling Storage.Get.
  • 🔴 pkg/apiserver/rest/dualwriter_mode1_test.go (L240)
    INCORRECT ASSERTION: AssertNotCalled for Storage.Delete contradicts Mode1 behavior — function literal mismatch makes it a no-op.
  • 🔴 pkg/apiserver/rest/dualwriter_mode1_test.go (L300)
    INCORRECT ASSERTION: AssertNotCalled for Storage.DeleteCollection contradicts Mode1 behavior — same issues as Delete.

Worth checking

  • 🟡 pkg/apiserver/rest/dualwriter_mode3.go (L103)
    BUG: Wrong metric label in Delete success path — passes object name instead of options.Kind.
  • 🟡 pkg/apiserver/rest/dualwriter_mode3.go (L94)
    BUG: Wrong logger context in Delete — uses d.Log instead of enriched log.
  • 🟡 pkg/apiserver/rest/dualwriter_mode1_test.go (L175)
    MISSING ERROR CHECK: Test case for legacy error does not set wantErr: true, so the error path is not asserted.
  • 🟡 pkg/tests/apis/playlist/playlist_test.go (L283)
    MISSING COVERAGE: Mode 4 integration tests are absent across all storage backends.

Small things (take or leave)

  • 🔵 pkg/apiserver/rest/dualwriter_mode3_test.go (L108)
    INCONSISTENCY: Test uses local Prometheus registry while other tests use package-level variable.
  • 🔵 pkg/apiserver/rest/dualwriter_mode1_test.go (L355)
    UNUSED MOCK EXPECTATIONS: Setup expectations for Get and Update are never matched because goroutine is not launched on Legacy error.