ScuttleBot

perf: async mode grants in topology provisioning Mode grants (AMODE/SAMODE) now fire in a goroutine instead of blocking the provisioning loop. Provisioning 6 channels with 12 bots was taking ~5 minutes synchronously, blocking all API requests that touch the topology manager. Now provisioning returns immediately and modes are applied in the background. Also reduced the ChanServ registration wait from 600ms to 200ms.

lmata 2026-04-05 22:22 trunk
Commit 7b2b457b594374dc8a95323296f04cdf81fdc287aac80c28693a98760522ff3c
1 file changed +17 -24
--- internal/topology/topology.go
+++ internal/topology/topology.go
@@ -205,43 +205,36 @@
205205
}
206206
207207
func (m *Manager) provision(ch ChannelConfig) error {
208208
// Register with ChanServ (idempotent — fails silently if already registered).
209209
m.chanserv("REGISTER %s", ch.Name)
210
- // Give ChanServ time to process the registration before issuing follow-up
211
- // commands. Retry the sleep up to 3 times so transient load doesn't cause
212
- // TOPIC/ACCESS commands to fire before registration completes.
213
- for range 3 {
214
- time.Sleep(200 * time.Millisecond)
215
- if m.client.IsConnected() {
216
- break
217
- }
218
- }
210
+ time.Sleep(200 * time.Millisecond) // one short wait for ChanServ to process
219211
220212
if ch.Topic != "" {
221213
m.chanserv("TOPIC %s %s", ch.Name, ch.Topic)
222214
}
223215
224
- // Set persistent auto-modes. Use ChanServ AMODE when possible,
225
- // and SAMODE (oper) as immediate fallback.
226
- for _, nick := range ch.Ops {
227
- m.chanserv("AMODE %s +o %s", ch.Name, nick)
228
- if m.operPass != "" {
229
- m.client.Cmd.SendRawf("SAMODE %s +o %s", ch.Name, nick)
230
- }
231
- }
232
- for _, nick := range ch.Voice {
233
- m.chanserv("AMODE %s +v %s", ch.Name, nick)
234
- if m.operPass != "" {
235
- m.client.Cmd.SendRawf("SAMODE %s +v %s", ch.Name, nick)
236
- }
237
- }
238
-
239216
// Apply channel modes (e.g. +m for moderated).
240217
for _, mode := range ch.Modes {
241218
m.client.Cmd.Mode(ch.Name, mode)
242219
}
220
+
221
+ // Fire mode grants asynchronously — don't block provisioning.
222
+ go func(name string, ops, voice []string) {
223
+ for _, nick := range ops {
224
+ m.chanserv("AMODE %s +o %s", name, nick)
225
+ if m.operPass != "" && m.client != nil {
226
+ m.client.Cmd.SendRawf("SAMODE %s +o %s", name, nick)
227
+ }
228
+ }
229
+ for _, nick := range voice {
230
+ m.chanserv("AMODE %s +v %s", name, nick)
231
+ if m.operPass != "" && m.client != nil {
232
+ m.client.Cmd.SendRawf("SAMODE %s +v %s", name, nick)
233
+ }
234
+ }
235
+ }(ch.Name, ch.Ops, ch.Voice)
243236
244237
if len(ch.Autojoin) > 0 {
245238
m.Invite(ch.Name, ch.Autojoin)
246239
}
247240
248241
--- internal/topology/topology.go
+++ internal/topology/topology.go
@@ -205,43 +205,36 @@
205 }
206
207 func (m *Manager) provision(ch ChannelConfig) error {
208 // Register with ChanServ (idempotent — fails silently if already registered).
209 m.chanserv("REGISTER %s", ch.Name)
210 // Give ChanServ time to process the registration before issuing follow-up
211 // commands. Retry the sleep up to 3 times so transient load doesn't cause
212 // TOPIC/ACCESS commands to fire before registration completes.
213 for range 3 {
214 time.Sleep(200 * time.Millisecond)
215 if m.client.IsConnected() {
216 break
217 }
218 }
219
220 if ch.Topic != "" {
221 m.chanserv("TOPIC %s %s", ch.Name, ch.Topic)
222 }
223
224 // Set persistent auto-modes. Use ChanServ AMODE when possible,
225 // and SAMODE (oper) as immediate fallback.
226 for _, nick := range ch.Ops {
227 m.chanserv("AMODE %s +o %s", ch.Name, nick)
228 if m.operPass != "" {
229 m.client.Cmd.SendRawf("SAMODE %s +o %s", ch.Name, nick)
230 }
231 }
232 for _, nick := range ch.Voice {
233 m.chanserv("AMODE %s +v %s", ch.Name, nick)
234 if m.operPass != "" {
235 m.client.Cmd.SendRawf("SAMODE %s +v %s", ch.Name, nick)
236 }
237 }
238
239 // Apply channel modes (e.g. +m for moderated).
240 for _, mode := range ch.Modes {
241 m.client.Cmd.Mode(ch.Name, mode)
242 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
244 if len(ch.Autojoin) > 0 {
245 m.Invite(ch.Name, ch.Autojoin)
246 }
247
248
--- internal/topology/topology.go
+++ internal/topology/topology.go
@@ -205,43 +205,36 @@
205 }
206
207 func (m *Manager) provision(ch ChannelConfig) error {
208 // Register with ChanServ (idempotent — fails silently if already registered).
209 m.chanserv("REGISTER %s", ch.Name)
210 time.Sleep(200 * time.Millisecond) // one short wait for ChanServ to process
 
 
 
 
 
 
 
 
211
212 if ch.Topic != "" {
213 m.chanserv("TOPIC %s %s", ch.Name, ch.Topic)
214 }
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216 // Apply channel modes (e.g. +m for moderated).
217 for _, mode := range ch.Modes {
218 m.client.Cmd.Mode(ch.Name, mode)
219 }
220
221 // Fire mode grants asynchronously — don't block provisioning.
222 go func(name string, ops, voice []string) {
223 for _, nick := range ops {
224 m.chanserv("AMODE %s +o %s", name, nick)
225 if m.operPass != "" && m.client != nil {
226 m.client.Cmd.SendRawf("SAMODE %s +o %s", name, nick)
227 }
228 }
229 for _, nick := range voice {
230 m.chanserv("AMODE %s +v %s", name, nick)
231 if m.operPass != "" && m.client != nil {
232 m.client.Cmd.SendRawf("SAMODE %s +v %s", name, nick)
233 }
234 }
235 }(ch.Name, ch.Ops, ch.Voice)
236
237 if len(ch.Autojoin) > 0 {
238 m.Invite(ch.Name, ch.Autojoin)
239 }
240
241

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button