Skip to content

fix(socket): guard against missing operator during store patch#2962

Open
grolu wants to merge 4 commits into
masterfrom
fix/socket-event-handler-guards
Open

fix(socket): guard against missing operator during store patch#2962
grolu wants to merge 4 commits into
masterfrom
fix/socket-event-handler-guards

Conversation

@grolu
Copy link
Copy Markdown
Member

@grolu grolu commented May 12, 2026

/area quality
/kind bug

What this PR does / why we need it:

Guards the socket event handler against a missing operator when the
store has not been initialized yet. Events arriving before store
initialization are deferred and requeued. If the operator cannot be
created for an already-initialized store (permanent misconfiguration),
an error is logged instead of silently swallowing events.

Which issue(s) this PR fixes:
Fixes #

Special notes for your reviewer:

The default operator returns undefined when state.list is not an
array. Stores initialize list as null and set it to an array after
fetching. The guard distinguishes these two cases to avoid hiding real
failures behind a debug log.

Release note:

Fixed socket event handler crashing when events arrive before store initialization.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Socket events are now deferred and re-queued when the local store isn’t initialized, preventing lost or incorrect updates; visibility changes now gate when queued events are flushed.
    • Improved error handling: synchronization failures are logged and roll back without causing repeated retries.
  • Tests

    • Added unit tests covering deferred event handling, re-queueing on reset/failure, visibility gating, and operator error behavior.

Review Change Stack

Defer socket events when the store is uninitialized (list is null)
and log an error when the operator cannot be created for an
already-initialized store, distinguishing transient startup
timing from permanent misconfiguration.
@gardener-prow gardener-prow Bot added area/quality Output qualification (tests, checks, scans, automation in general, etc.) related kind/bug Bug labels May 12, 2026
@gardener-prow
Copy link
Copy Markdown

gardener-prow Bot commented May 12, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign klocke-io for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@gardener-prow gardener-prow Bot added size/S Denotes a PR that changes 10-29 lines, ignoring generated files. cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. labels May 12, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Warning

Rate limit exceeded

@petersutter has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 50 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd164b74-89c7-42ee-abee-e2015ce8c175

📥 Commits

Reviewing files that changed from the base of the PR and between dc9bac0 and fc8b523.

📒 Files selected for processing (1)
  • frontend/src/composables/useSocketEventHandler.js
📝 Walkthrough

Walkthrough

The handler treats an uninitialized list store as an explicit error, adds restore/flush helpers to re-queue buffered socket events, gates flushing on store initialization and document visibility, and wires a watcher to flush when the store becomes ready. Tests validate queuing, requeue-on-reset, immediate sync when initialized, and operator error behavior.

Changes

Socket event handler refactor

Layer / File(s) Summary
Store error type and operator validation
frontend/src/composables/useSocketEventHandler.js
Adds StoreNotInitializedError and makes createDefaultOperator throw when state.list is null, delegating non-null validation to createListOperator.
Event handling, restoreEvents and flushEvents
frontend/src/composables/useSocketEventHandler.js
Introduces isStoreInitialized, restoreEvents, and flushEvents. handleEvents buffers/clears events, early-exits when uninitialized, clears the buffer before syncing, and rolls back via restoreEvents on socketStore.synchronize errors while preserving Status handling.
Error handling and lifecycle watcher
frontend/src/composables/useSocketEventHandler.js
Catches StoreNotInitializedError to restore buffered events and logs other operator errors. start() sets up a watcher to call flushEvents when the store becomes initialized; stop() clears state and stops the watcher.
Event listener and visibility wiring
frontend/src/composables/useSocketEventHandler.js
Event listener and document visibility watcher now invoke flushEvents(useStore()) when the handler is active and when visibility transitions to visible.
Unit tests for buffered/requeued synchronization
frontend/__tests__/composables/useSocketEventHandler.spec.js
Adds tests covering queuing until initialization, ignoring sync after reset, requeue and eventual apply after reset during apply, immediate sync when initialized, operator failure logging without retry, and createListOperator(null) throwing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled at events while the list slept tight,
Queued them by moonlight, held them till right,
When the store woke and shone like spring,
I let them hop in—what joy they bring! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: guarding the socket event handler against missing operators during store initialization.
Description check ✅ Passed The description covers all required template sections with clear explanations of what the PR does, why it's needed, and includes proper categorization and release notes.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/socket-event-handler-guards

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 and usage tips.

@gardener-prow gardener-prow Bot added cla: no Indicates the PR's author has not signed the cla-assistant.io CLA. cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. and removed cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. cla: no Indicates the PR's author has not signed the cla-assistant.io CLA. labels May 12, 2026
Socket events arriving before the initial list fetch now queue
until the store transitions out of its null/initial state. Events
are restored (not dropped) when the store resets mid-synchronize.
Operator failures surface as errors instead of being masked as
transient initialization delays.
@gardener-prow gardener-prow Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/S Denotes a PR that changes 10-29 lines, ignoring generated files. labels May 13, 2026
Copy link
Copy Markdown

@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

🤖 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.

Inline comments:
In `@frontend/src/composables/useSocketEventHandler.js`:
- Around line 119-132: Before awaiting socketStore.synchronize(pluralName, uids)
capture the store's current initialization epoch/token (e.g., call
isInitialized(store) or read an initEpoch property) and store it in a local
variable; after the await and before applying patches/using items, re-check the
store's current epoch and only proceed if it matches the captured epoch,
otherwise call restoreEvents(events) and return. Update the logic around
socketStore.synchronize, isInitialized(store), restoreEvents(events), and any
code that applies items to the store to use this epoch comparison so transient
reset→reinit cycles won’t patch stale results.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 06e75b63-ed22-45bc-a476-e3772877bce7

📥 Commits

Reviewing files that changed from the base of the PR and between cdc0fdd and dc9bac0.

📒 Files selected for processing (2)
  • frontend/__tests__/composables/useSocketEventHandler.spec.js
  • frontend/src/composables/useSocketEventHandler.js

Comment on lines +119 to +132
items = await socketStore.synchronize(pluralName, uids)
} catch (err) {
if (isTooManyRequestsError(err)) {
logger.info('Skipped synchronization of modified %s: %s', pluralName, err.message)
} else {
logger.error('Failed to synchronize modified %s: %s', pluralName, err.message)
}
// Synchronization failed -> Rollback events
restoreEvents(events)
return
}
if (!isInitialized(store)) {
restoreEvents(events)
return
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 13, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Track store resets across the async gap.

This only checks isInitialized(store) after socketStore.synchronize(...) resolves. If the store briefly resets to its initial state and reinitializes before the await finishes, this branch will still patch stale results into the new store state instead of requeueing them. Please capture a reset/init epoch before the await and compare it again before applying the patch, so transient reset→reinit cycles are treated the same as the slower reset case.

🤖 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 `@frontend/src/composables/useSocketEventHandler.js` around lines 119 - 132,
Before awaiting socketStore.synchronize(pluralName, uids) capture the store's
current initialization epoch/token (e.g., call isInitialized(store) or read an
initEpoch property) and store it in a local variable; after the await and before
applying patches/using items, re-check the store's current epoch and only
proceed if it matches the captured epoch, otherwise call restoreEvents(events)
and return. Update the logic around socketStore.synchronize,
isInitialized(store), restoreEvents(events), and any code that applies items to
the store to use this epoch comparison so transient reset→reinit cycles won’t
patch stale results.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Isn't it the case that if the store resets during an active synchronize(), the connection was lost.. which triggers a disconnect/reconnect cycle that initiates a fresh synchronize with the new store state anyway? The stale patch would never survive because the entire sync restarts.

Wouldn't adding an epoch counter introduce complexity for a race condition that already self-heals through the existing reconnect mechanism?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@petersutter, that's a well-reasoned architectural argument. You're right that in practice:

  1. A store reset during an in-flight synchronize() is driven by a connection loss.
  2. The disconnect/reconnect cycle triggers a fresh full synchronization, which overwrites any stale patch that may have briefly landed on the reinitialized store.
  3. The existing post-await isInitialized(store) check already covers the simpler case where the store is still in reset state when synchronize() resolves.

Given the self-healing guarantee from the reconnect mechanism, adding an epoch counter would be extra complexity for a race window that the system architecture already handles. I'll withdraw this concern.

(´・ω・`)ノ


✏️ Learnings added
Learnt from: petersutter
Repo: gardener/dashboard PR: 2962
File: frontend/src/composables/useSocketEventHandler.js:119-132
Timestamp: 2026-05-13T16:56:42.824Z
Learning: In `frontend/src/composables/useSocketEventHandler.js` (gardener/dashboard), a store reset during an in-flight `socketStore.synchronize()` is always tied to a connection loss and a subsequent disconnect/reconnect cycle. The reconnect cycle triggers a fresh full synchronization that overwrites any stale patch, so the post-await `isInitialized(store)` check is sufficient; adding an epoch counter for the transient reset→reinit race is unnecessary complexity.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

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

Labels

area/quality Output qualification (tests, checks, scans, automation in general, etc.) related cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. kind/bug Bug size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants