Skip to content

Fix: remove close(l.buffer) to prevent send-on-closed-channel panic i…#92

Merged
SantiagoDePolonia merged 4 commits intomainfrom
fix/concurrency-error-fix
Feb 25, 2026
Merged

Fix: remove close(l.buffer) to prevent send-on-closed-channel panic i…#92
SantiagoDePolonia merged 4 commits intomainfrom
fix/concurrency-error-fix

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

…n async loggers

A race existed between Write() and flushLoop() during shutdown: a goroutine could pass the l.closed check, get preempted, and resume after close(l.buffer), causing a panic. Closing the buffer channel is unnecessary since flushLoop exits via l.done; the GC reclaims the channel. Drain now uses a non-blocking select loop instead of range.

…n async loggers

A race existed between Write() and flushLoop() during shutdown: a goroutine
could pass the l.closed check, get preempted, and resume after close(l.buffer),
causing a panic. Closing the buffer channel is unnecessary since flushLoop exits
via l.done; the GC reclaims the channel. Drain now uses a non-blocking select
loop instead of range.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes a race condition in async loggers that could cause a "send on closed channel" panic during shutdown. The issue occurred when Write() goroutines passed the l.closed check but then got preempted before sending on the buffer channel, allowing flushLoop to close the channel. The fix removes the channel close operation and replaces the range-based drain with a non-blocking select loop.

Changes:

  • Removed close(l.buffer) calls to prevent send-on-closed-channel panics
  • Replaced blocking range drain with non-blocking select loop in shutdown path
  • Updated comments to explain the synchronization strategy

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
internal/usage/logger.go Fixed race condition by removing channel close and using non-blocking drain loop; maintains existing writes.Wait() synchronization
internal/auditlog/logger.go Applied similar fix but missing the writes WaitGroup synchronization pattern that usage/logger.go has

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 122 to 134
// Shutdown: mark as closed so Write() stops sending
l.closed.Store(true)
close(l.buffer)
// Drain remaining entries from buffer
for entry := range l.buffer {
batch = append(batch, entry)
// Drain remaining entries from buffer using non-blocking loop.
// We do NOT close(l.buffer) — closing is unnecessary since flushLoop
// exits via l.done, and closing creates a race with concurrent Write() calls.
for {
select {
case entry := <-l.buffer:
batch = append(batch, entry)
default:
goto drainComplete
}
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The auditlog/logger.go implementation is missing the synchronization pattern that usage/logger.go uses to prevent race conditions during shutdown. Specifically: (1) auditlog/logger.go lacks a 'writes sync.WaitGroup' field to track in-flight Write() calls, (2) the Close() method doesn't set l.closed before closing l.done, and (3) Close() doesn't wait for in-flight writes to complete. This creates a critical race where Write() can send to l.buffer after the drain starts but before it exits via the default case, potentially losing entries. Setting l.closed here (line 123) is ineffective because it happens after Close() has already closed l.done without synchronization. To fix this properly: add a writes WaitGroup field, modify Write() to track calls like usage/logger.go does (lines 63-64, 102), update Close() to set l.closed and call writes.Wait() before closing l.done, and remove the Store call from flushLoop.

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 25, 2026

Warning

Rate limit exceeded

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between e034f66 and 8de8757.

📒 Files selected for processing (6)
  • internal/auditlog/logger.go
  • internal/storage/sqlite.go
  • internal/usage/logger.go
  • internal/usage/reader_mongodb.go
  • internal/usage/reader_postgresql.go
  • internal/usage/reader_sqlite.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/concurrency-error-fix

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.

The single-connection pool (MaxOpenConns=1) serialized all DB access,
negating WAL mode's native support for concurrent readers. Dashboard
read queries blocked on background flush writes and vice versa.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@SantiagoDePolonia
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

SantiagoDePolonia and others added 2 commits February 25, 2026 13:47
Mirrors the synchronization pattern already in usage/logger.go:
- writes WaitGroup tracks in-flight Write() calls so Close() waits
  for them before signaling flushLoop to stop
- Double-check pattern in Write() closes the race window between
  the first closed.Load() and writes.Add(1)
- Close() uses closed.Swap(true) for idempotency — safe to call
  multiple times during shutdown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore sqliteDateRangeConditions(), pgDateRangeConditions(), and
mongoDateRangeFilter() helper functions. The inlining was an unrelated
refactoring regression that duplicated identical date-range logic across
12 call sites with no behavioral change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@SantiagoDePolonia SantiagoDePolonia merged commit cf5e6ff into main Feb 25, 2026
12 checks passed
@SantiagoDePolonia SantiagoDePolonia deleted the fix/concurrency-error-fix branch March 22, 2026 14:24
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.

2 participants