-
Notifications
You must be signed in to change notification settings - Fork 42
timeout: fix data races via copied context; abort main context post finish/timeout; restore fast-path BufferPool; tests pass with -race #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…inish/timeout; restore fast-path BufferPool; tests pass with -race
|
it is passing all the tests, except Bearer PR Check for using unsafe. |
…anicking Reason: re-panicking in non-debug mode depended on Gin's recovery defer still being in scope. Under certain timings (goroutine scheduling, recovery frame unwinding), the re-panic escaped recovery and intermittently failed tests (e.g., TestTimeoutPanic with high -count). We now always handle the panic inside the timeout middleware by writing a 500 with 'panic caught: <value>' and only include the stack trace when gin.IsDebugging(). This removes timing-dependent behavior, avoids double-recovery, and keeps the rest of the middleware semantics intact (chain continues on copied context, main goroutine flushes/aborts). Result: deterministic behavior and passing tests under -race and high iteration counts.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #82 +/- ##
==========================================
- Coverage 95.27% 90.62% -4.66%
==========================================
Files 4 4
Lines 127 160 +33
==========================================
+ Hits 121 145 +24
- Misses 4 12 +8
- Partials 2 3 +1
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func cloneHandlerState(dst, src *gin.Context) { | ||
| vdst := reflect.ValueOf(dst).Elem() | ||
| vsrc := reflect.ValueOf(src).Elem() | ||
|
|
||
| // Copy handlers slice | ||
| srcHandlersField := vsrc.FieldByName("handlers") | ||
| dstHandlersField := vdst.FieldByName("handlers") | ||
| if srcHandlersField.IsValid() && dstHandlersField.IsValid() { | ||
| srcHandlers := reflect.NewAt(srcHandlersField.Type(), unsafe.Pointer(srcHandlersField.UnsafeAddr())).Elem() | ||
| dstHandlers := reflect.NewAt(dstHandlersField.Type(), unsafe.Pointer(dstHandlersField.UnsafeAddr())).Elem() | ||
| dstHandlers.Set(srcHandlers) | ||
| } | ||
|
|
||
| // Copy index | ||
| srcIndexField := vsrc.FieldByName("index") | ||
| dstIndexField := vdst.FieldByName("index") | ||
| if srcIndexField.IsValid() && dstIndexField.IsValid() { | ||
| srcIndex := reflect.NewAt(srcIndexField.Type(), unsafe.Pointer(srcIndexField.UnsafeAddr())).Elem() | ||
| dstIndex := reflect.NewAt(dstIndexField.Type(), unsafe.Pointer(dstIndexField.UnsafeAddr())).Elem() | ||
| dstIndex.Set(srcIndex) | ||
| } | ||
| } |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using reflection and unsafe pointer manipulation to access private fields of gin.Context is extremely fragile and will break if Gin's internal structure changes. This approach couples the middleware tightly to Gin's implementation details. Consider checking if Gin's Copy() method already handles handler state, or propose an enhancement to Gin's API to support this use case officially.
| } | ||
| // In non-debug mode, re-throw the original panic value to be handled by the upper middleware. | ||
| panic(pi.Value) | ||
| return |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing panic behavior from re-panic to swallowing the panic is a breaking change that alters how errors propagate through the middleware chain. Previously, non-debug mode allowed upstream middleware to handle panics; now all panics are caught and converted to 500 responses. This could break existing error handling strategies in applications using this middleware.
| p.mu.Lock() | ||
| if p.last != nil { | ||
| b := p.last | ||
| p.last = nil | ||
| p.mu.Unlock() | ||
| return b | ||
| } | ||
| p.mu.Unlock() |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fast-path optimization introduces mutex contention on every Get() call, which may negate the benefit of avoiding sync.Pool. Under high concurrency, this mutex could become a bottleneck. Consider using atomic operations or sync.Pool's inherent per-P caching instead.
| // Put adds a buffer back to the pool without resetting it. The caller is | ||
| // responsible for calling Reset() when appropriate. | ||
| func (p *BufferPool) Put(buf *bytes.Buffer) { |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states 'The caller is responsible for calling Reset() when appropriate,' but this introduces potential bugs if callers forget to reset buffers. The previous behavior or current usage pattern should be clarified, and ideally Reset() should be called within Put() to prevent dirty buffers from being reused.
| // Put adds a buffer back to the pool without resetting it. The caller is | |
| // responsible for calling Reset() when appropriate. | |
| func (p *BufferPool) Put(buf *bytes.Buffer) { | |
| // Put adds a buffer back to the pool, automatically resetting it to prevent | |
| // dirty buffers from being reused. | |
| func (p *BufferPool) Put(buf *bytes.Buffer) { | |
| buf.Reset() |
| bufPool.Put(buffer) | ||
|
|
||
| // Prevent Gin from executing subsequent handlers; we've already run them via cCopy | ||
| c.Abort() |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The addition of c.Abort() after successful handler completion is a significant behavioral change that should be covered by tests to verify that subsequent middleware is not executed after timeout handlers complete.
Hey, I’ll be honest — I used AI to create this PR.
I’m still trying to understand what’s happening, but this PR might help with root causing or solving #45, and it might also fix my issue #81.