Skip to content

fix(glide): prevent stale failure callbacks across request swaps#943

Open
frozenstar80 wants to merge 1 commit into
skydoves:mainfrom
frozenstar80:fix/glide-stale-failure-state
Open

fix(glide): prevent stale failure callbacks across request swaps#943
frozenstar80 wants to merge 1 commit into
skydoves:mainfrom
frozenstar80:fix/glide-stale-failure-state

Conversation

@frozenstar80

@frozenstar80 frozenstar80 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

What

This PR fixes a stale-state issue in the Glide integration where a failure callback from an earlier request could affect the currently active request state.

Changes

  • Emit failure state directly from FlowRequestListener.onLoadFailed(...).
  • Close the active flow channel in the same request-scoped failure path.
  • Return true from Glide onLoadFailed(...) to prevent duplicate/late target-level failure routing.
  • Remove target-side failure backchannel (failException / updateFailException) from FlowCustomTarget.
  • Add a regression instrumentation test:
    • staleFailure_fromPreviousRequest_doesNotOverrideLatestSuccess
    • file: glide/src/androidTest/kotlin/com/skydoves/landscapist/glide/GlideImageTest.kt

Why

FlowCustomTarget can be reused across recompositions. When failure propagation depends on target-side mutable state, late callbacks from previous requests can leak into the active request flow.
By handling failures in FlowRequestListener (request-scoped), terminal states stay aligned to the current request.

Validation

Executed successfully:

  • ./gradlew :glide:compileDebugKotlin --no-daemon
  • ./gradlew :glide:testDebugUnitTest --no-daemon
  • ./gradlew :glide:compileDebugAndroidTestKotlin --no-daemon
  • ./gradlew spotlessApply --no-daemon
  • ./gradlew apiDump --no-daemon

Risk

Low-to-medium. Scope is limited to Glide failure-path state propagation and includes regression coverage.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where an earlier failed image request could override a later successful result.
    • Improved image loading state handling so the latest image selection is reflected correctly, even when previous requests fail.

@frozenstar80 frozenstar80 requested a review from skydoves as a code owner July 4, 2026 08:03
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Refactors Glide failure propagation: FlowRequestListener now emits ImageLoadState.Failure directly through a producerScope instead of via a failException callback, and FlowCustomTarget removes its failException storage and no longer emits failure states, only closing the channel. GlideImage wiring updated accordingly. A new instrumentation test validates that stale failures don't override the latest success.

Changes

Glide stale failure fix

Layer / File(s) Summary
FlowRequestListener failure emission via producerScope
glide/src/main/kotlin/com/skydoves/landscapist/glide/FlowRequestListener.kt
Constructor replaces failException callback with a producerScope; onLoadFailed emits ImageLoadState.Failure via trySendBlocking, closes the channel, and returns true.
FlowCustomTarget removes failException tracking
glide/src/main/kotlin/com/skydoves/landscapist/glide/FlowCustomTarget.kt
onLoadFailed only closes the producerScope channel instead of emitting a Failure state; failException field and updateFailException method removed.
GlideImage listener wiring update
glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt
FlowRequestListener is instantiated without the prior failure lambda forwarding errors to target.updateFailException.
Regression test for stale failure vs latest success
glide/src/androidTest/kotlin/com/skydoves/landscapist/glide/GlideImageTest.kt
Adds Compose state imports and a test switching imageModel from a failing URL to a success drawable, asserting the latest terminal state is Success with no Failure recorded for the latest model.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Compose as GlideImage (Compose)
    participant Listener as FlowRequestListener
    participant Target as FlowCustomTarget
    participant Scope as producerScope

    Compose->>Listener: request image load (model)
    alt load fails
        Listener->>Scope: trySendBlocking(Failure(data=null, reason=e))
        Listener->>Scope: close channel
        Listener-->>Compose: return true
    else load fails at target
        Target->>Scope: close channel (no Failure emitted)
    end
    Compose->>Scope: switch to new model (success)
    Scope-->>Compose: emit Success state
Loading

Suggested reviewers: skydoves

Poem

A rabbit hopped through failing scenes,
Chasing stale errors from routines,
Now channels close, no ghost of dread,
Success shines through where failure fled. 🐇✨
Hop, hop — the latest state prevails!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers what, changes, why, validation, and risk, but it omits the template's Goal, Implementation details, examples, and review prep sections. Add the missing template sections: 🎯 Goal, 🛠 Implementation details, ✍️ Explain examples, review-prep commands, and Code reviews guidance.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: preventing stale Glide failure callbacks across request swaps.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
glide/src/androidTest/kotlin/com/skydoves/landscapist/glide/GlideImageTest.kt (1)

218-236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assertions may run before a stale, in-flight failure has time to arrive.

waitUntil stops as soon as the first terminal state is recorded. Since the new model here is a local drawable (near-instant, no network I/O) while the stale request is a real network URL (needs DNS/connection failure to resolve), the Success is very likely to land well before the earlier failure would ever complete — so assertions may pass even if the stale-failure race were reintroduced, undermining the purpose of this regression test.

Consider adding a short grace period after the first terminal state is observed, before asserting, to give a late/stale callback a realistic chance to surface.

♻️ Proposed adjustment
     composeTestRule.waitUntil(10_000) {
       terminalStatesForLatestModel.isNotEmpty()
     }
+
+    // Give any stale/delayed callback from the superseded request a chance to arrive
+    // before asserting, so a regression would actually be caught here.
+    Thread.sleep(2_000)
+    composeTestRule.waitForIdle()

     composeTestRule.runOnIdle {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@glide/src/androidTest/kotlin/com/skydoves/landscapist/glide/GlideImageTest.kt`
around lines 218 - 236, The regression test in GlideImageTest is asserting
immediately after the first terminal state, which can miss a late stale failure
from the previous request. Update the test around imageModel, waitUntil, and
terminalStatesForLatestModel to add a short grace period after the first
terminal state is observed before running the Success/Failure assertions, so a
delayed callback has time to surface and the race is actually validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@glide/src/androidTest/kotlin/com/skydoves/landscapist/glide/GlideImageTest.kt`:
- Around line 218-236: The regression test in GlideImageTest is asserting
immediately after the first terminal state, which can miss a late stale failure
from the previous request. Update the test around imageModel, waitUntil, and
terminalStatesForLatestModel to add a short grace period after the first
terminal state is observed before running the Success/Failure assertions, so a
delayed callback has time to surface and the race is actually validated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a20512d-f182-4ae3-ad12-dd6c4b1d8fe0

📥 Commits

Reviewing files that changed from the base of the PR and between e6abbdb and 891d901.

📒 Files selected for processing (4)
  • glide/src/androidTest/kotlin/com/skydoves/landscapist/glide/GlideImageTest.kt
  • glide/src/main/kotlin/com/skydoves/landscapist/glide/FlowCustomTarget.kt
  • glide/src/main/kotlin/com/skydoves/landscapist/glide/FlowRequestListener.kt
  • glide/src/main/kotlin/com/skydoves/landscapist/glide/GlideImage.kt

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.

1 participant