🐛 bug: synchronize view reloads with template rendering#4288
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR introduces per-Views synchronization to serialize view template operations. It adds a reflection-based lock store that maps each Views instance to a *sync.RWMutex, integrates write locking in ReloadViews() around Views.Load(), read locking in DefaultRes.Render() around Views.Render(), and provides tests for concurrent blocking and panic-recovery scenarios across mounted apps. ChangesView Engine Thread Safety
Sequence Diagram(s)sequenceDiagram
participant Reload as ReloadViews()
participant Render as DefaultRes.Render()
participant Lock as *sync.RWMutex
participant Views as Views.Load/Render
Reload->>Lock: Lock() (write)
Render->>Lock: RLock() (blocked)
Reload->>Views: Load()
Views-->>Reload: complete
Reload->>Lock: Unlock()
Lock-->>Render: RLock acquired
Render->>Views: Render()
Views-->>Render: complete
Render->>Lock: RUnlock()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" 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. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4288 +/- ##
==========================================
+ Coverage 91.27% 91.34% +0.07%
==========================================
Files 130 132 +2
Lines 12797 12969 +172
==========================================
+ Hits 11680 11847 +167
- Misses 705 708 +3
- Partials 412 414 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds synchronization around view engine reloads and template rendering to prevent concurrent Load/Render access from racing at runtime.
Changes:
- Adds an
App.viewsMutexto coordinate view engine access. - Locks
ReloadViews()with an exclusive lock and rendering with a read lock. - Adds a regression test verifying renders wait for an in-flight reload.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
app.go |
Adds the view mutex and uses it around Views.Load() in ReloadViews(). |
res.go |
Uses the view mutex read lock around Views.Render(). |
app_test.go |
Adds a blocking view test for reload/render synchronization. |
There was a problem hiding this comment.
Code Review
This pull request introduces a viewsMutex to the App struct to synchronize view reloading and rendering, ensuring thread safety and preventing rendering during a reload. A new test case was added to verify this blocking behavior. The review feedback suggests improving the robustness of the locking mechanism by using defer within anonymous functions to prevent potential mutex leakage or deadlocks if the view engine panics during Load or Render operations.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@app.go`:
- Around line 778-783: The manual Lock/Unlock around targetApp.viewsMutex in
ReloadViews is not panic-safe; change it to acquire the lock inside an anonymous
closure (or scope) and call defer targetApp.viewsMutex.Unlock() immediately
after Lock(), so that if targetApp.config.Views.Load() panics or returns an
error the mutex is always released; follow the same pattern used for app.mutex
in this function to ensure unlock happens even on panic.
In `@res.go`:
- Around line 694-699: The read lock on app.viewsMutex is acquired with
app.viewsMutex.RLock() but manually unlocked later, so a panic in
app.config.Views.Render would skip the RUnlock and deadlock ReloadViews; change
to the panic-safe pattern by calling app.viewsMutex.RLock() then immediately
defer app.viewsMutex.RUnlock(), then call app.config.Views.Render(buf, name,
bind, layouts...), and remove the manual RUnlock() before returns; keep the
existing error handling (return fmt.Errorf(...)) so the deferred unlock always
runs even on errors or panics.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: fdc869f8-bab0-4792-8d01-a04bf23efb43
📒 Files selected for processing (3)
app.goapp_test.gores.go
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/57c33969-f896-4fe2-adab-8b430f077fad Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Addressed in 82e2cad. Both |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app_test.go (1)
2128-2132: 💤 Low valueTiming-based assertion could be flaky.
The 20ms wait to verify render hasn't entered is susceptible to flakiness under CI load. Consider increasing this timeout or adding a more deterministic synchronization mechanism if flakiness is observed.
🤖 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 `@app_test.go` around lines 2128 - 2132, The timing-based assertion using a 20ms timeout on select (checking view.renderEntered) is flaky; increase the timeout (e.g., to 200ms) or, better, replace the ad-hoc timing with deterministic synchronization: have the test control when ReloadViews.Load proceeds (for example by adding a blocking channel or sync.WaitGroup used by the mocked ReloadViews.Load) and then assert that view.renderEntered remains empty until you release that blocker, referencing the test's select that reads view.renderEntered and the ReloadViews.Load mock to implement the deterministic block/unblock.
🤖 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 `@app_test.go`:
- Around line 2128-2132: The timing-based assertion using a 20ms timeout on
select (checking view.renderEntered) is flaky; increase the timeout (e.g., to
200ms) or, better, replace the ad-hoc timing with deterministic synchronization:
have the test control when ReloadViews.Load proceeds (for example by adding a
blocking channel or sync.WaitGroup used by the mocked ReloadViews.Load) and then
assert that view.renderEntered remains empty until you release that blocker,
referencing the test's select that reads view.renderEntered and the
ReloadViews.Load mock to implement the deterministic block/unblock.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a01adc5a-37e3-42c8-b1b3-f847924e03a1
📒 Files selected for processing (3)
app.goapp_test.gores.go
🚧 Files skipped from review as they are similar to previous changes (2)
- app.go
- res.go
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/fa606f2b-0b57-4627-8c5e-ebdad7638102 Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Addressed in e68e1e9. The view lock is now keyed by the |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
app_test.go:2370
- This mounted-app regression also uses
app.Testwhile the reload is blocked. Duringapp.Test,startupProcesslocks mounted sub-apps viaensureAutoHeadRoutes, so the render request can be waiting on the reloading sub-app'sapp.mutexrather than on the shared view-engine lock. Pre-start the mounted app and exercise the request handler without the test startup path so the test actually fails when the renderRLockis absent.
go func() {
_, err := app.Test(httptest.NewRequest(MethodGet, "/b/render", http.NoBody))
renderDone <- err
| if viewValue.Type().Comparable() { | ||
| key.view = views | ||
| } else { | ||
| switch viewValue.Kind() { | ||
| case reflect.Pointer, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice, reflect.UnsafePointer: | ||
| key.ptr = viewValue.Pointer() | ||
| default: |
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/08cdb605-d96c-4207-884e-783f5e267982 Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Addressed in 2d88eb5. Shared view locks are now scoped to the mounted app tree instead of a global cache, and the blocking regressions now prebuild the handler and invoke it directly so they exercise the view lock path rather than |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
mount.go (1)
48-49: ⚖️ Poor tradeoff
setViewsLockStorecalled without synchronization.
setViewsLockStoreiteratessubApp.mountFields.appListwithout holding any mutex. IfsubAppis being modified concurrently (unlikely during typical app setup), the map iteration could cause a runtime panic.The read of
app.viewsLockStoreis also unsynchronized.In practice, mount operations occur during single-threaded app setup before
Listen(), so this is unlikely to cause issues. Flagging for awareness if concurrent mount patterns are ever supported.🤖 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 `@mount.go` around lines 48 - 49, The call to subApp.setViewsLockStore(app.viewsLockStore) accesses subApp.mountFields.appList and reads app.viewsLockStore without synchronization; update setViewsLockStore (and any callers) to acquire the appropriate mutex(es) before iterating subApp.mountFields.appList and before reading app.viewsLockStore to prevent concurrent map access panics. Specifically, inside setViewsLockStore (or a wrapper) lock subApp.mountFields' mutex (e.g., mountFields.mu) while iterating appList, and ensure any read of app.viewsLockStore is performed under app's read-lock or by copying the reference under app's lock before releasing it. Ensure locks are held only for the minimal duration to avoid contention.app.go (1)
118-161: 💤 Low valueEdge case in keying logic for non-comparable struct Views.
If a
Viewsimplementation is a non-comparable struct (e.g., contains a slice or map field) but is not itself a pointer/chan/func/map/slice, the key falls through to thedefaultcase and onlytypis populated. This means all instances of such a type would share a single lock.This is still safe (no data races), but could cause unnecessary serialization when different view engine instances of the same non-comparable struct type are used.
In practice, most
Viewsimplementations are pointers, so this edge case is unlikely to occur. Flagging for awareness.Possible fix: use unsafe pointer for any addressable non-comparable value
} else { switch viewValue.Kind() { case reflect.Pointer, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice, reflect.UnsafePointer: key.ptr = viewValue.Pointer() - default: + default: + if viewValue.CanAddr() { + key.ptr = viewValue.Addr().Pointer() + } } }Note: Interface values passed directly are typically not addressable, so this may not fully resolve the edge case. Consider documenting this limitation.
🤖 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 `@app.go` around lines 118 - 161, The keying logic in viewsLockStore.get can collapse distinct non-comparable struct instances of the same type into one lock because for non-comparable values that are not pointer/chan/func/map/slice it only uses typ; fix by populating key.ptr for addressable non-comparable values: in viewsLockStore.get (function get, types viewsLockKey, viewsLockStore, variable viewValue/Views) check viewValue.CanAddr() and if true use viewValue.UnsafeAddr() (or create an addressable copy with reflect.New(viewValue.Type()) and set it) to set key.ptr so different instances get different keys; leave the existing pointer/chan/func/map/slice handling and document the remaining limitation for non-addressable interface values.
🤖 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 `@app.go`:
- Around line 118-161: The keying logic in viewsLockStore.get can collapse
distinct non-comparable struct instances of the same type into one lock because
for non-comparable values that are not pointer/chan/func/map/slice it only uses
typ; fix by populating key.ptr for addressable non-comparable values: in
viewsLockStore.get (function get, types viewsLockKey, viewsLockStore, variable
viewValue/Views) check viewValue.CanAddr() and if true use
viewValue.UnsafeAddr() (or create an addressable copy with
reflect.New(viewValue.Type()) and set it) to set key.ptr so different instances
get different keys; leave the existing pointer/chan/func/map/slice handling and
document the remaining limitation for non-addressable interface values.
In `@mount.go`:
- Around line 48-49: The call to subApp.setViewsLockStore(app.viewsLockStore)
accesses subApp.mountFields.appList and reads app.viewsLockStore without
synchronization; update setViewsLockStore (and any callers) to acquire the
appropriate mutex(es) before iterating subApp.mountFields.appList and before
reading app.viewsLockStore to prevent concurrent map access panics.
Specifically, inside setViewsLockStore (or a wrapper) lock subApp.mountFields'
mutex (e.g., mountFields.mu) while iterating appList, and ensure any read of
app.viewsLockStore is performed under app's read-lock or by copying the
reference under app's lock before releasing it. Ensure locks are held only for
the minimal duration to avoid contention.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 9ffc95b5-6b4b-407a-8aec-f1485d3cfdce
📒 Files selected for processing (5)
app.goapp_test.godomain.gomount.gores.go
🚧 Files skipped from review as they are similar to previous changes (2)
- res.go
- app_test.go
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/69619338-e670-428d-8376-e8cb95fae210 Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Addressed in 45a3593. Shared view locking is now resolved globally by engine identity without mutating mounted app state, and I added a reusable-mount regression so remounting the same sub-app can’t break reload/render synchronization in the original parent. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app.go (1)
141-148: 💤 Low valueNon-pointer, non-comparable Views instances of the same type will share a single lock.
When
viewValue.Kind()is not pointer-like andviewValue.Type().Comparable()is false, neitherkey.ptrnorkey.viewis set. This causes all distinct instances of the same non-comparable value type to share one lock, which may lead to unintended cross-instance blocking.This is an edge case since most
Viewsimplementations are pointers, but consider adding a comment documenting this limitation or handling it explicitly (e.g., by storing the address of the interface data if possible).🤖 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 `@app.go` around lines 141 - 148, The current switch in app.go leaves key.ptr and key.view unset for non-pointer, non-comparable Views values causing distinct instances to share a lock; update the logic in the switch handling for viewValue.Kind() (and the code that constructs the lock key) to explicitly handle this case: if viewValue is not pointer-like and not Comparable(), obtain a unique identifier for the concrete instance (for example use reflect.Value.CanAddr()/Addr() or fallback to using unsafe.Pointer to the interface data) and store that address into key.ptr (or another dedicated key field) so each distinct non-comparable value gets its own lock; alternatively add a clear comment near viewValue, key.ptr and key.view documenting the limitation if you decide not to change behavior.
🤖 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 `@app.go`:
- Around line 141-148: The current switch in app.go leaves key.ptr and key.view
unset for non-pointer, non-comparable Views values causing distinct instances to
share a lock; update the logic in the switch handling for viewValue.Kind() (and
the code that constructs the lock key) to explicitly handle this case: if
viewValue is not pointer-like and not Comparable(), obtain a unique identifier
for the concrete instance (for example use reflect.Value.CanAddr()/Addr() or
fallback to using unsafe.Pointer to the interface data) and store that address
into key.ptr (or another dedicated key field) so each distinct non-comparable
value gets its own lock; alternatively add a clear comment near viewValue,
key.ptr and key.view documenting the limitation if you decide not to change
behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 829acd38-d75c-4678-a4d4-55d8e0769327
📒 Files selected for processing (3)
app.goapp_test.gores.go
Motivation
Views.Load()while requests may concurrently callViews.Render(), causing data races and crashes such as concurrent map read/write.Load/Renderraces without changing view-engine APIs or behavior during startup.Description
viewsMutex sync.RWMutextoAppto coordinate engine access across reload and render paths inapp.go.ReloadViews()to acquire an exclusiveviewsMutexlock for each target app before callingViews.Load()and release it after the call inapp.go.DefaultRes.Render()to acquire a read lock (RLock) aroundViews.Render()so rendering and reloads are properly synchronized inres.go.Test_App_ReloadViews_BlocksRenderUntilLoadCompletesinapp_test.gothat verifies a render blocks until an in-flightReloadViews()finishes, and the test usest.Parallel()as required.