Author SHA1 Message Date
clawbot 6a200ea639 Read the IRC client's nick under its lock in the relay
check / check (push) Successful in 3m4s
After registration each IRC connection runs a second goroutine that sends
queued messages to the client, and it read c.nick without holding c.mu.
The goroutine that reads client commands changes c.nick under c.mu when
the client sends NICK, so the race detector failed
TestIntegrationTwoClients. Every read of c.nick in relay.go now takes
c.mu. No other field that goroutine reads changes after it starts.

Model: opus-5-5
2026-09-29 10:48:48 +00:00
clawbot 6a514b565c Build the HTTP router before the server starts (closes #104)
check / check (push) Successful in 2m22s
The fx OnStart hook started a goroutine that built the router and then
began serving, so OnStart returned before the router existed. Code that
served requests through the started Server, as the internal/handlers
tests do, raced with that goroutine or hit a nil router.

OnStart now configures the server, enables Sentry and builds the router
itself, and only then starts serving in the background. Sentry is set up
first because the routes depend on whether it is enabled. Run, whose
only caller was that hook, is folded into it.

Model: opus-5-5
2026-09-29 10:40:36 +00:00
2 changed files with 48 additions and 18 deletions
+40 -8
View File
@@ -128,7 +128,11 @@ func (c *Conn) deliverIRCMessage(
default:
// Unknown command — deliver as server notice.
if text != "" {
c.sendFromServer("NOTICE", c.nick, text)
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
c.sendFromServer("NOTICE", nick, text)
}
}
}
@@ -158,8 +162,12 @@ func (c *Conn) deliverNumeric(
_ = json.Unmarshal(msg.Params, &params)
}
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
allParams := make([]string, 0, 1+len(params)+1)
allParams = append(allParams, c.nick)
allParams = append(allParams, nick)
allParams = append(allParams, params...)
if text != "" {
@@ -177,8 +185,12 @@ func (c *Conn) deliverTextMessage(
from := msg.From
target := msg.To
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
// Don't echo our own messages back.
if strings.EqualFold(from, c.nick) {
if strings.EqualFold(from, nick) {
return
}
@@ -192,9 +204,13 @@ func (c *Conn) deliverTextMessage(
// deliverJoin sends a JOIN notification.
func (c *Conn) deliverJoin(msg *db.IRCMessage) {
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
// Don't echo our own JOINs (we already sent them
// during joinChannel).
if strings.EqualFold(msg.From, c.nick) {
if strings.EqualFold(msg.From, nick) {
return
}
@@ -206,7 +222,11 @@ func (c *Conn) deliverJoin(msg *db.IRCMessage) {
// deliverPart sends a PART notification.
func (c *Conn) deliverPart(msg *db.IRCMessage, text string) {
if strings.EqualFold(msg.From, c.nick) {
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
if strings.EqualFold(msg.From, nick) {
return
}
@@ -227,7 +247,11 @@ func (c *Conn) deliverNickChange(
msg *db.IRCMessage,
newNick string,
) {
if strings.EqualFold(msg.From, c.nick) {
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
if strings.EqualFold(msg.From, nick) {
return
}
@@ -241,7 +265,11 @@ func (c *Conn) deliverQuitMsg(
msg *db.IRCMessage,
text string,
) {
if strings.EqualFold(msg.From, c.nick) {
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
if strings.EqualFold(msg.From, nick) {
return
}
@@ -302,7 +330,11 @@ func (c *Conn) deliverInviteMsg(
_ *db.IRCMessage,
text string,
) {
c.sendFromServer("NOTICE", c.nick, text)
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
c.sendFromServer("NOTICE", nick, text)
}
// deliverMode sends a MODE change notification.
+8 -10
View File
@@ -71,7 +71,14 @@ func New(
lifecycle.Append(fx.Hook{
OnStart: func(_ context.Context) error {
srv.startupTime = time.Now()
go srv.Run() //nolint:contextcheck
// Build the router before OnStart returns, so srv can
// handle requests as soon as the app has started.
srv.configure()
srv.enableSentry()
srv.SetupRoutes()
go srv.serve() //nolint:contextcheck
return nil
},
@@ -83,13 +90,6 @@ func New(
return srv, nil
}
// Run starts the server configuration, Sentry, and begins serving.
func (srv *Server) Run() {
srv.configure()
srv.enableSentry()
srv.serve()
}
// ServeHTTP delegates to the chi router.
func (srv *Server) ServeHTTP(
writer http.ResponseWriter,
@@ -202,8 +202,6 @@ func (srv *Server) serveUntilShutdown() {
Handler: srv,
}
srv.SetupRoutes()
srv.log.Info(
"http begin listen", "listenaddr", listenAddr,
)