- 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
149 lines
3.3 KiB
Go
149 lines
3.3 KiB
Go
package elog
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"bunny-bot/internal/mylog"
|
|
)
|
|
|
|
// acquire returns the chat's lock with this goroutine holding a reference.
|
|
func acquire(chatID int64) *chatLock {
|
|
registryMu.Lock()
|
|
|
|
l, ok := locks[chatID]
|
|
if !ok {
|
|
l = &chatLock{}
|
|
locks[chatID] = l
|
|
}
|
|
|
|
l.refs++
|
|
l.lastUsed = time.Now()
|
|
|
|
evictIdleLocked()
|
|
|
|
registryMu.Unlock()
|
|
|
|
l.mu.Lock()
|
|
|
|
return l
|
|
}
|
|
|
|
// release drops this goroutine's reference and unlocks the chat.
|
|
func release(l *chatLock) {
|
|
l.mu.Unlock()
|
|
|
|
registryMu.Lock()
|
|
|
|
l.refs--
|
|
l.lastUsed = time.Now()
|
|
|
|
registryMu.Unlock()
|
|
}
|
|
|
|
// evictIdleLocked drops registry entries nobody is holding and that have been
|
|
// idle for longer than chatLockIdleTimeout.
|
|
//
|
|
// Safety rests on refs: it is only ever changed while registryMu is held, so
|
|
// observing refs == 0 here is a proof that no goroutine holds the entry, not a
|
|
// guess. Evicting a held entry would let a second writer take a *different*
|
|
// mutex for the same chat, which is precisely the interleaving the lock exists
|
|
// to prevent. The caller must hold registryMu.
|
|
func evictIdleLocked() {
|
|
cutoff := time.Now().Add(-chatLockIdleTimeout)
|
|
|
|
for id, l := range locks {
|
|
if l.refs == 0 && l.lastUsed.Before(cutoff) {
|
|
delete(locks, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// appendLine writes one line to the chat's log for the day containing now.
|
|
//
|
|
// The whole protocol is set_mutex -> MkdirAll -> open|create -> write -> close,
|
|
// with unset_mutex deferred. Nothing is held open between lines.
|
|
func appendLine(chatID int64, line string) {
|
|
// One read of both the root and whether logging is on, so a Close racing
|
|
// this call can never leave us with an empty root and a relative path.
|
|
dir, on := chatLogRoot()
|
|
if !on {
|
|
return
|
|
}
|
|
|
|
lock := acquire(chatID)
|
|
|
|
defer release(lock)
|
|
|
|
// One clock for both the path and the line: using Message.Date for one and
|
|
// time.Now() for the other would let a line carry a date its own path
|
|
// disagrees with.
|
|
now := nowUTC()
|
|
|
|
dayDir := filepath.Join(dir, itoa(chatID), now.Format("2006"), now.Format("01"))
|
|
path := filepath.Join(dayDir, now.Format("02")+".txt")
|
|
|
|
// MkdirAll creates every missing parent, so this covers the chat directory
|
|
// and the year too. No need to pre-create anything.
|
|
if err := os.MkdirAll(dayDir, logDirPerm); err != nil {
|
|
reportFailure(err)
|
|
|
|
return
|
|
}
|
|
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, logFilePerm)
|
|
if err != nil {
|
|
reportFailure(err)
|
|
|
|
return
|
|
}
|
|
|
|
_, writeErr := f.WriteString(line + "\n")
|
|
closeErr := f.Close()
|
|
|
|
if writeErr != nil {
|
|
reportFailure(writeErr)
|
|
|
|
return
|
|
}
|
|
|
|
if closeErr != nil {
|
|
reportFailure(closeErr)
|
|
|
|
return
|
|
}
|
|
|
|
reportRecovered()
|
|
}
|
|
|
|
// reportFailure sends a write failure to the main log, but only the first one
|
|
// until a write succeeds again. A full disk would otherwise produce one error
|
|
// per chat event and drown the very log meant to explain it.
|
|
func reportFailure(err error) {
|
|
stateMu.Lock()
|
|
already := failing
|
|
failing = true
|
|
stateMu.Unlock()
|
|
|
|
if already {
|
|
return
|
|
}
|
|
|
|
mylog.Errorf("CHAT LOG WRITE FAILED, further failures suppressed until a write succeeds: %s", err)
|
|
}
|
|
|
|
// reportRecovered clears the suppression after a successful write.
|
|
func reportRecovered() {
|
|
stateMu.Lock()
|
|
was := failing
|
|
failing = false
|
|
stateMu.Unlock()
|
|
|
|
if was {
|
|
mylog.Info("Chat logging recovered")
|
|
}
|
|
}
|
|
|
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|