1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-08-13 20:04:49 +02:00

Fix: Improve version polling, add thread safety and exponential backoff (#523)

Squashed commit of the following:

commit 1ed713dd8de2adb7d761e20bb8018804c2e27ea6
Author: Ralph Slooten <axllent@gmail.com>
Date:   Wed Jun 18 17:03:36 2025 +1200

    Refactor latest version caching, add console logging if update checks fails

commit bf880e583372d81a0597bc263ab22f6989e48fa9
Author: Ben Edmunds <Tigger2014@users.noreply.github.com>
Date:   Wed Jun 18 05:52:35 2025 +0100

    Fix: Improve version polling, add thread safety and exponential backoff (#523)

    * make version polling thread safe and add expo backoff

    * tidy up
This commit is contained in:
Ralph Slooten
2025-06-18 17:04:07 +12:00
parent 4b5ce0afed
commit 3aae06ff6b

View File

@@ -7,17 +7,30 @@ import (
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/updater"
)
// Stores cached version along with its expiry time and error count.
// Used to minimize repeated version lookups and track consecutive errors.
type versionCache struct {
// github version string
value string
// time to expire the cache
expiry time.Time
// count of consecutive errors
errCount int
}
var (
// to prevent hammering Github for latest version
latestVersionCache string
// Version cache storing the latest GitHub version
vCache versionCache
// StartedAt is set to the current ime when Mailpit starts
startedAt time.Time
// sync mutex to prevent race condition with simultaneous requests
mu sync.RWMutex
smtpAccepted uint64
@@ -62,6 +75,12 @@ type AppInformation struct {
}
}
// Calculates exponential backoff duration based on the error count.
func getBackoff(errCount int) time.Duration {
backoff := min(time.Duration(1<<errCount)*time.Minute, 30*time.Minute)
return backoff
}
// Load the current statistics
func Load() AppInformation {
info := AppInformation{}
@@ -78,20 +97,32 @@ func Load() AppInformation {
info.RuntimeStats.SMTPRejected = smtpRejected
info.RuntimeStats.SMTPIgnored = smtpIgnored
if latestVersionCache != "" {
info.LatestVersion = latestVersionCache
} else {
latest, _, _, err := updater.GithubLatest(config.Repo, config.RepoBinaryName)
if err == nil {
info.LatestVersion = latest
latestVersionCache = latest
mu.RLock()
cacheValid := time.Now().Before(vCache.expiry)
cacheValue := vCache.value
mu.RUnlock()
// clear latest version cache after 5 minutes
go func() {
time.Sleep(15 * time.Minute)
latestVersionCache = ""
}()
if cacheValid {
info.LatestVersion = cacheValue
} else {
mu.Lock()
// Re-check after acquiring write lock in case another goroutine refreshed it
if time.Now().Before(vCache.expiry) {
info.LatestVersion = vCache.value
} else {
latest, _, _, err := updater.GithubLatest(config.Repo, config.RepoBinaryName)
if err == nil {
vCache = versionCache{value: latest, expiry: time.Now().Add(15 * time.Minute)}
info.LatestVersion = latest
} else {
logger.Log().Errorf("Failed to fetch latest version: %v", err)
vCache.errCount++
vCache.value = ""
vCache.expiry = time.Now().Add(getBackoff(vCache.errCount))
info.LatestVersion = ""
}
}
mu.Unlock()
}
info.Database = config.Database