- Remove CAS, as it no longer works for us - Remove other checks - Implement 15 minute greet timeout check - Fix chat logos - Ditch thirdparty logger, use stdlib log facility wrapper for logging
98 lines
3.1 KiB
Go
98 lines
3.1 KiB
Go
package moon
|
|
|
|
import (
|
|
"bunny-bot/internal/elog"
|
|
"bunny-bot/internal/mylog"
|
|
"fmt"
|
|
|
|
"github.com/akyoto/cache"
|
|
)
|
|
|
|
// closeCaches shuts down every cache sweeper goroutine.
|
|
//
|
|
// Safe only once the update listener and the greeting sweeper have stopped:
|
|
// cache.Close() reassigns the underlying sync.Map, which is a data race against
|
|
// any concurrent reader. shutdown() enforces that order.
|
|
func closeCaches() {
|
|
closeOnce.Do(func() {
|
|
for _, c := range []*cache.Cache{
|
|
SquashedMembers,
|
|
PendingGreetings.cache, GreetAnswers.cache,
|
|
} {
|
|
c.Close()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Shutdown performs the ordered teardown, waits for it to finish, and reports
|
|
// whether every stage of it succeeded.
|
|
//
|
|
// main calls it after Telegram returns, for two reasons. The listener returning is
|
|
// not by itself a finished shutdown — a signal handler may be halfway through the
|
|
// sequence at that moment, and main returning would end the process underneath it,
|
|
// skipping stages and closing the log twice. And if the listener stopped for any
|
|
// other reason, nobody would run the sequence at all.
|
|
//
|
|
// It is safe to call from both goroutines: sync.Once runs the body once and blocks
|
|
// every other caller until that run completes, so both callers see the same result.
|
|
func Shutdown() error {
|
|
return shutdown()
|
|
}
|
|
|
|
// shutdown stops everything in dependency order and waits for each stage.
|
|
//
|
|
// The order is not cosmetic. Every stage waits for the previous one to have
|
|
// finished, so no goroutine is ever using a resource that the next stage is
|
|
// closing:
|
|
//
|
|
// 1. the Telegram update listener — nothing else may add work;
|
|
// 2. the greeting sweeper — the last periodic user of the caches;
|
|
// 3. the caches — cache.Close() both signals the sweeper goroutines and
|
|
// reassigns the underlying sync.Map, so it must run with nothing else
|
|
// touching them;
|
|
// 4. chat logging, then 5. the main log — so the last log lines, which
|
|
// describe stages 1 to 3, still have somewhere to go.
|
|
//
|
|
// Stages 1 and 2 share one stop signal and one WaitGroup, so a worker that has
|
|
// not started costs nothing and a worker that starts late is refused rather than
|
|
// racing the wait.
|
|
//
|
|
// The main log's own failure is reported ahead of the chat log's, because it happened
|
|
// last: a chat-log failure may no longer have been writable either.
|
|
func shutdown() error {
|
|
var result error
|
|
|
|
shutdownOnce.Do(func() {
|
|
// Refuse new workers before signalling, so nothing can register between
|
|
// the signal and the wait below.
|
|
workersMu.Lock()
|
|
stopping = true
|
|
workersMu.Unlock()
|
|
|
|
// Ask every loop to return, then wait for all of them. Nothing may be
|
|
// using a cache, a log or the API once the next stages start closing.
|
|
close(stopRequested)
|
|
workers.Wait()
|
|
|
|
closeCaches()
|
|
|
|
// Chat logging first: it writes to the main log, so it must be closed
|
|
// before the main log is.
|
|
chatErr := elog.Close()
|
|
|
|
// Since we shutting down, we have noting to do with log sync.
|
|
logErr := mylog.Close()
|
|
|
|
switch {
|
|
case logErr != nil:
|
|
result = fmt.Errorf("main log: %w", logErr)
|
|
case chatErr != nil:
|
|
result = fmt.Errorf("chat log: %w", chatErr)
|
|
}
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|