decoupling start and shutdown

- start is no longer a blocking call
- giving it back to users
- the Stop method now waits for the timeout before closing
This commit is contained in:
2026-06-25 16:02:47 -04:00
parent f5948eb36e
commit e6be288302
3 changed files with 51 additions and 41 deletions
+11 -2
View File
@@ -12,6 +12,8 @@ package main
import ( import (
"fmt" "fmt"
"os"
"os/signal"
"strings" "strings"
"time" "time"
@@ -43,7 +45,6 @@ things to demo functionality:
func main() { func main() {
b, err := bolt.New() b, err := bolt.New()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -99,8 +100,16 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
}
qc := make(chan os.Signal, 1)
signal.Notify(qc, os.Interrupt)
<-qc
err = b.Stop()
if err != nil {
panic(err)
}
}
``` ```
## Development ## Development
+24 -33
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"os/signal"
"slices" "slices"
"strings" "strings"
"sync" "sync"
@@ -52,6 +51,8 @@ type Bolt struct {
maxRoutines int maxRoutines int
//generic message handler func //generic message handler func
msgHandlerf Payload msgHandlerf Payload
//command timeout when closing connection
grace time.Duration
} }
// New creates a new bolt instance and applies any supplied options // New creates a new bolt instance and applies any supplied options
@@ -73,6 +74,7 @@ func New(opts ...Option) (*Bolt, error) {
indicator: DEFAULT_INDICATOR, indicator: DEFAULT_INDICATOR,
wg: sync.WaitGroup{}, wg: sync.WaitGroup{},
maxRoutines: DEFAULT_MAX_GOROUTINES, maxRoutines: DEFAULT_MAX_GOROUTINES,
grace: time.Second * 5,
} }
b.Identify.Intents = DEFAULT_INTENTS b.Identify.Intents = DEFAULT_INTENTS
@@ -87,22 +89,33 @@ func New(opts ...Option) (*Bolt, error) {
} }
// Start applies the message event handler function to the bot and opens the initial websocket connection // Start applies the message event handler function to the bot and opens the initial websocket connection
// with Discord. Start is a blocking call that also handles safe shutdown, on Interrupt, bolt will give // with Discord
// command routines a window to finish before closing the connection.
func (b *Bolt) Start() error { func (b *Bolt) Start() error {
b.AddHandler(b.msgEventHandler) b.AddHandler(b.msgEventHandler)
err := b.Open() return b.Open()
if err != nil { }
return fmt.Errorf("failed to open websocket connection with Discord: %e", err)
// AddCommands registers command handlers. Any messages that begin with the command indicator will be forwarded
// to the handler
func (b *Bolt) AddCommands(cmd ...Command) {
for _, c := range cmd {
//we can use a normal map here since writing only takes place once
b.commands[c.Trigger] = c
} }
}
sigChannel := make(chan os.Signal, 1) // AddMessageHandler registers the generic message handler, any messages that are not commands will be forwarded
signal.Notify(sigChannel, os.Interrupt) // to the handler
<-sigChannel func (b *Bolt) AddMessageHandler(p Payload) {
b.msgHandlerf = p
}
//give handler routines a 5 second window to finish processes before closing connection // Stop waits for the alloted grace period timeout before handling safe shutdown of the
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) // Discord connection
func (b *Bolt) Stop() error {
ctx, cancel := context.WithTimeout(context.Background(), b.grace)
defer cancel() defer cancel()
closeChan := make(chan struct{}) closeChan := make(chan struct{})
go func() { go func() {
b.wg.Wait() b.wg.Wait()
@@ -115,25 +128,6 @@ func (b *Bolt) Start() error {
case <-closeChan: case <-closeChan:
} }
return b.stop()
}
// AddCommands registers command handlers. Any messages that begin with the command indicator will be forwarded
// to the handler
func (b *Bolt) AddCommands(cmd ...Command) {
for _, c := range cmd {
b.commands[c.Trigger] = c
}
}
// AddMessageHandler registers the generic message handler, any messages that are not commands will be forwarded
// to the handler
func (b *Bolt) AddMessageHandler(p Payload) {
b.msgHandlerf = p
}
// stop closes the websocket connection to Discord
func (b *Bolt) stop() error {
return b.Close() return b.Close()
} }
@@ -258,7 +252,6 @@ func (b *Bolt) handleMessage(event *Message) error {
// handleCommand maps the first word of the message to the command payload, if it exists. It then forwards the message // handleCommand maps the first word of the message to the command payload, if it exists. It then forwards the message
// data to the handler after checking the timeout and role restrictions. If restrictions have not been met a generic // data to the handler after checking the timeout and role restrictions. If restrictions have not been met a generic
// response is sent to the message // response is sent to the message
// TODO: accept a string for timeout/role rejection messages to allow customization
func (b *Bolt) handleCommand(msg *Message, lg int) error { func (b *Bolt) handleCommand(msg *Message, lg int) error {
run, ok := b.commands[msg.Words[0][lg:]] run, ok := b.commands[msg.Words[0][lg:]]
if !ok { if !ok {
@@ -345,8 +338,6 @@ func (b *Bolt) remainingTimeout(timeout time.Time) string {
return fmt.Sprintf("%d%s", timeLeft, metric) return fmt.Sprintf("%d%s", timeLeft, metric)
} }
// checks if the author of msg has the correct role to run the requested command
// roleCheck loops through the provided user role ID's grabs the role "name" and ensures the role is present // roleCheck loops through the provided user role ID's grabs the role "name" and ensures the role is present
// in the commands whitelist. If the role exists in the Command whitelist a true response is returned // in the commands whitelist. If the role exists in the Command whitelist a true response is returned
func (b *Bolt) roleCheck(guild string, roles []string, s *dg.Session, run Command) (bool, error) { func (b *Bolt) roleCheck(guild string, roles []string, s *dg.Session, run Command) (bool, error) {
+10
View File
@@ -1,6 +1,8 @@
package bolt package bolt
import ( import (
"time"
dg "github.com/bwmarrin/discordgo" dg "github.com/bwmarrin/discordgo"
) )
@@ -48,3 +50,11 @@ func WithLogLevel(lvl LogLevel) Option {
b.logLvl = lvl b.logLvl = lvl
} }
} }
// WithGracePeriod gives a grace period for commands to finish prior to closing the
// websocket connection with Discord
func WithGracePeriod(t time.Duration) Option {
return func(b *Bolt) {
b.grace = t
}
}