Adiciona mecanismo de expiração periódica para jobs que ficam presos nos status received ou processing além de um threshold configurável. - models: novo status StatusTimeout - store: migration v2 com índice em jobs(status); método ExpireStuck que faz UPDATE atômico para os status alvo anteriores ao threshold - expirer: novo pacote com goroutine periódica que chama ExpireStuck e loga WARN quando algum job é expirado - config: campos JobTimeoutMinutes (default 120) e ExpirerIntervalMinutes (default 10), lidos via env vars JOB_TIMEOUT_MINUTES e EXPIRER_INTERVAL_MINUTES - main: instancia e inicia o Expirer no startup - dashboard: card Timeout com contagem; badge neutral na tabela de jobs
90 lines
2.1 KiB
Go
90 lines
2.1 KiB
Go
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/fs"
|
|
"log/slog"
|
|
"sync"
|
|
)
|
|
|
|
type Renderer struct {
|
|
fs fs.FS
|
|
tmpl *template.Template
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func NewRenderer(templatesFS fs.FS) *Renderer {
|
|
r := &Renderer{fs: templatesFS}
|
|
if !isDev {
|
|
tmpl, err := r.parseAll()
|
|
if err != nil {
|
|
panic("failed to parse templates: " + err.Error())
|
|
}
|
|
r.tmpl = tmpl
|
|
}
|
|
return r
|
|
}
|
|
|
|
func (r *Renderer) Render(w io.Writer, name string, data any) error {
|
|
if isDev {
|
|
tmpl, err := r.parseAll()
|
|
if err != nil {
|
|
slog.Error("failed to parse templates (dev)", "error", err)
|
|
return err
|
|
}
|
|
return tmpl.ExecuteTemplate(w, name, data)
|
|
}
|
|
|
|
r.mu.Lock()
|
|
tmpl := r.tmpl
|
|
r.mu.Unlock()
|
|
return tmpl.ExecuteTemplate(w, name, data)
|
|
}
|
|
|
|
func (r *Renderer) parseAll() (*template.Template, error) {
|
|
funcs := template.FuncMap{
|
|
"formatDuration": formatDuration,
|
|
"statusBadge": statusBadge,
|
|
"add": func(a, b int) int { return a + b },
|
|
"sub": func(a, b int) int { return a - b },
|
|
"mul": func(a, b int) int { return a * b },
|
|
"gt": func(a, b int) bool { return a > b },
|
|
}
|
|
tmpl := template.New("").Funcs(funcs)
|
|
return tmpl.ParseFS(r.fs, "*.html", "partials/*.html")
|
|
}
|
|
|
|
func formatDuration(ms int64) string {
|
|
if ms < 1000 {
|
|
return "0s"
|
|
}
|
|
s := ms / 1000
|
|
if s < 60 {
|
|
return fmt.Sprintf("%ds", s)
|
|
}
|
|
m := s / 60
|
|
s = s % 60
|
|
return fmt.Sprintf("%dm %ds", m, s)
|
|
}
|
|
|
|
func statusBadge(level string) template.HTML {
|
|
switch level {
|
|
case "DEBUG":
|
|
return template.HTML(`<span class="badge badge-neutral">` + level + `</span>`)
|
|
case "INFO", "done":
|
|
return template.HTML(`<span class="badge badge-success">` + level + `</span>`)
|
|
case "WARN", "processing":
|
|
return template.HTML(`<span class="badge badge-warning">` + level + `</span>`)
|
|
case "ERROR", "error":
|
|
return template.HTML(`<span class="badge badge-error">` + level + `</span>`)
|
|
case "received":
|
|
return template.HTML(`<span class="badge badge-info">` + level + `</span>`)
|
|
case "timeout":
|
|
return template.HTML(`<span class="badge badge-neutral">` + level + `</span>`)
|
|
default:
|
|
return template.HTML(level)
|
|
}
|
|
}
|