Skip to content

fix: serialize websocket write deadlines with message writes - #157

Merged
AlexStocks merged 2 commits into
AlexStocks:masterfrom
beatenevo:fix/issue-153-ws-write-deadline
Oct 23, 2026
Merged

AlexStocks merged 2 commits into
AlexStocks:masterfrom
beatenevo:fix/issue-153-ws-write-deadline

Conversation

@beatenevo

@beatenevo beatenevo commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does:

gettyWSConn.Send, writePing, writePong and CloseConn each called updateWriteDeadline() before entering threadSafeWriteMessage(). The write lock covered WriteMessage, but not the deadline update.

That is not safe under concurrency:

  • Session.Send takes packetLock.RLock(), which is shared by design, so multiple Send calls can enter gettyWSConn.Send at the same time.
  • Heartbeat writePing does not take packetLock at all, writePong runs on the read goroutine, and CloseConn can run from the session teardown goroutine.
  • Gorilla v1.5.3's Conn.SetWriteDeadline is just c.writeDeadline = t, and every frame flush reads that field. Two deadline updates are therefore a write-write race, and a deadline update concurrent with an in-flight write is a write-read race. go test -race reports it inside websocket.(*Conn).SetWriteDeadline.

This PR makes the deadline update part of the same critical section as the frame write:

func (w *gettyWSConn) threadSafeWriteMessage(messageType int, data []byte) error {
    w.writeLock.Lock()
    defer w.writeLock.Unlock()
    w.streamStarted.Store(true)

    if err := w.updateWriteDeadline(); err != nil {
        log.Warnf("failed to update write deadline: %+v", err)
    }
    return w.conn.WriteMessage(messageType, data)
}

The four callers no longer update the deadline themselves. All WebSocket write paths — Send, Ping, Pong and the close frame — now hold the same per-connection writeLock across both the deadline update and the message write.

updateWriteDeadline also snapshots wTimeout once instead of loading it twice. The guard and the value passed to SetWriteDeadline can no longer disagree if SetWriteTimeout runs concurrently. The write behavior itself is unchanged: each message still gets a fresh deadline, and a deadline error is still logged before the write is attempted.

Which issue(s) this PR fixes:

Refs #153 — this PR only addresses item 4 (concurrent WebSocket sends racing on the write deadline). The other items in that issue are separate fixes.

Special notes for your reviewer:

  • writeLock is a field on gettyWSConn, so it is per connection: writers on one connection serialize, while different connections stay independent.
  • There is no bypass. w.conn.WriteMessage is only called from threadSafeWriteMessage; w.conn.SetWriteDeadline is only called from updateWriteDeadline; and updateWriteDeadline is only called from threadSafeWriteMessage.
  • CloseConn writes the close frame under the lock. The subsequent transport Close remains outside the lock; Gorilla permits Close concurrently with other methods, and after the close frame is written Gorilla sets writeErr to ErrCloseSent, so waiting writers fail before sending a data frame. The close=true test case covers this.
  • SetCompressType already uses the same writeLock to keep compression configuration from racing with writers; this change is consistent with that.
  • The concurrent-close test calls CloseConn(-1) instead of CloseConn(0). CloseConn forwards its argument to SetLinger; 0 enables SO_LINGER with a zero timeout, which sends an RST and can discard the just-written close frame, making the peer observe 1006 instead of a clean 1000 close. -1 restores the OS-default close. This is test-only; the library's CloseConn behavior is unchanged.
  • No exported API change.

Testing:

  • New TestWSConcurrentWriteDeadlines in transport/connection_ws_test.go runs two Session.Send goroutines plus writePing and writePong, each for 100 iterations, with an optional concurrent CloseConn.
  • The test has close=false and close=true modes. It validates the peer's received messages and control frames, so it checks correctness as well as the absence of the data race.
  • Against the pre-fix connection.go, the test fails with WARNING: DATA RACE between two SetWriteDeadline calls. With the fix it passes, including -race -count=100, which was run to stress the close path.
  • go test ./transport -count=1 and go test -race ./transport -count=1 pass locally, as do gofmt, go vet and git diff --check.

Does this PR introduce a user-facing change?:

WebSocket writes now update the Gorilla write deadline and send the frame under the same per-connection lock. Concurrent Send/Ping/Pong/Close calls no longer race on the deadline field. Each write still gets a fresh deadline, and the existing write-timeout behavior is unchanged.

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of WebSocket message, ping, pong, and close operations when they occur concurrently.
    • WebSocket writes now proceed when the configured write timeout is disabled or a deadline cannot be set. Failed message writes report zero bytes sent.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8459d1d1-b25c-46b6-8759-ee14befbf86c
📥 Commits

Reviewing files that changed from the base of the PR and between 64d7136 and 3f52458.

📒 Files selected for processing: 2
  • transport/connection.go
  • transport/connection_ws_test.go
🚧 Files skipped from review as they are similar to previous changes: 2
  • transport/connection_ws_test.go
  • transport/connection.go

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.

📝 Walkthrough

WebSocket write-deadline updates now occur inside the shared write lock before message writes. A new test covers concurrent data and control-frame writes, both with and without a concurrent close.

Changes: WebSocket write deadline serialization

Layer / File(s) Summary
Serialize deadline updates with WebSocket writes
transport/connection.go, transport/connection_ws_test.go
threadSafeWriteMessage updates the deadline while holding writeLock. Send, ping, pong, and close use the shared helper. Deadline-update errors are logged, but do not stop the message write. The new test checks concurrent writes and close behavior.

Priority: ➖ Normal

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

Change: Bug fix

Possibly related issues

  • AlexStocks/getty issue 153: The issue describes a data race when concurrent WebSocket writes update the write deadline before acquiring writeLock. This change moves deadline updates into the shared write lock and adds a regression test.
Merge Risk: ⚪ Minimal · up to 3f524

The WebSocket deadline fix is ready to merge after normal checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: serializing WebSocket write-deadline updates with message writes.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files.
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 a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Add docstrings to the remaining WebSocket write-related helpers and the test helper/closure touched by the deadline fix, so the docstring coverage check sees the changed functions documented. No behavior change.
@AlexStocks
AlexStocks requested a balanced review from Copilot October 23, 2026 07:24

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@AlexStocks AlexStocks left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

工业级 review 结论:Approve。

验证范围:issue #153 缺陷④「并发 WS 发送 deadline 数据竞争」。

验证结果:

  • 基线上 SetWriteDeadline 在 Send/writePing/writePong/CloseConn 中各自调用,未被 writeLock 保护;-race 检测到多个 goroutine 并发设置 deadline 的 DATA RACE。
  • 本 PR 将 updateWriteDeadline 移入 threadSafeWriteMessage 内部,在持有 writeLock 期间统一设置 deadline 并立即写入,消除竞态。
  • 移除各调用点重复的 updateWriteDeadline 调用,简化调用路径。
  • 新增 TestWSConcurrentWriteDeadlines 覆盖并发数据帧/ping/pong 写入及并发 close,-race 下通过。

代码观察:

  • updateWriteDeadline 现在要求调用方已持有 writeLock,注释已明确说明,符合 gorilla/websocket 对单写者的要求。

未处理项:issue #153 缺陷⑥不在本 PR 范围内,需后续跟进。

@AlexStocks
AlexStocks merged commit 7c5be5e into AlexStocks:master Oct 23, 2026
10 checks passed
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.

3 participants