mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
Wire in retention
- Update README - Add retention metrics - Start retention scanner if configured
This commit is contained in:
@@ -33,6 +33,7 @@ It can:
|
|||||||
* Display the source of a message (headers + body text)
|
* Display the source of a message (headers + body text)
|
||||||
* Display the HTML version of a message (in a new window)
|
* Display the HTML version of a message (in a new window)
|
||||||
* Delete a message
|
* Delete a message
|
||||||
|
* Purge messages after a configurable amount of time
|
||||||
|
|
||||||
It does not yet:
|
It does not yet:
|
||||||
|
|
||||||
|
|||||||
@@ -109,14 +109,19 @@ func LoadConfig(filename string) error {
|
|||||||
return fmt.Errorf("Failed to validate configuration")
|
return fmt.Errorf("Failed to validate configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = parseSmtpConfig()
|
if err = parseSmtpConfig(); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = parseWebConfig()
|
if err = parseWebConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
if err = parseDataStoreConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseLoggingConfig trying to catch config errors early
|
// parseLoggingConfig trying to catch config errors early
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ path=/tmp/inbucket
|
|||||||
# How many minutes after receipt should a message be stored until it's
|
# How many minutes after receipt should a message be stored until it's
|
||||||
# automatically purged. To retain messages until manually deleted, set this
|
# automatically purged. To retain messages until manually deleted, set this
|
||||||
# to 0
|
# to 0
|
||||||
retention.minutes=1
|
retention.minutes=0
|
||||||
|
|
||||||
# How many milliseconds to sleep after purging messages from a mailbox.
|
# How many milliseconds to sleep after purging messages from a mailbox.
|
||||||
# This should help reduce disk I/O when there are a large number of messages
|
# This should help reduce disk I/O when there are a large number of messages
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ type Message interface {
|
|||||||
Delete() error
|
Delete() error
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ func (s *Server) Start() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start retention scanner
|
||||||
|
StartRetentionScanner(s.dataStore)
|
||||||
|
|
||||||
|
// Handle incoming connections
|
||||||
for sid := 1; ; sid++ {
|
for sid := 1; ; sid++ {
|
||||||
if conn, err := ln.Accept(); err != nil {
|
if conn, err := ln.Accept(); err != nil {
|
||||||
// TODO Implement a max error counter before shutdown?
|
// TODO Implement a max error counter before shutdown?
|
||||||
@@ -86,6 +90,7 @@ func metricsTicker(t *time.Ticker) {
|
|||||||
expConnectsHist.Set(pushMetric(connectsHist, expConnectsTotal))
|
expConnectsHist.Set(pushMetric(connectsHist, expConnectsTotal))
|
||||||
expErrorsHist.Set(pushMetric(errorsHist, expErrorsTotal))
|
expErrorsHist.Set(pushMetric(errorsHist, expErrorsTotal))
|
||||||
expWarnsHist.Set(pushMetric(warnsHist, expWarnsTotal))
|
expWarnsHist.Set(pushMetric(warnsHist, expWarnsTotal))
|
||||||
|
expRetentionDeletesHist.Set(pushMetric(retentionDeletesHist, expRetentionDeletesTotal))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,58 @@
|
|||||||
package smtpd
|
package smtpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/list"
|
||||||
|
"expvar"
|
||||||
|
"github.com/jhillyerd/inbucket/config"
|
||||||
"github.com/jhillyerd/inbucket/log"
|
"github.com/jhillyerd/inbucket/log"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// retentionScan does a single pass of all mailboxes looking for messages that can be purged
|
var retentionScanCompleted time.Time
|
||||||
func retentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) error {
|
var retentionScanCompletedMu sync.RWMutex
|
||||||
|
|
||||||
|
var expRetentionDeletesTotal = new(expvar.Int)
|
||||||
|
|
||||||
|
// History of certain stats
|
||||||
|
var retentionDeletesHist = list.New()
|
||||||
|
|
||||||
|
// History rendered as comma delim string
|
||||||
|
var expRetentionDeletesHist = new(expvar.String)
|
||||||
|
|
||||||
|
func StartRetentionScanner(ds DataStore) {
|
||||||
|
cfg := config.GetDataStoreConfig()
|
||||||
|
if cfg.RetentionMinutes > 0 {
|
||||||
|
// Retention scanning enabled
|
||||||
|
log.Info("Retention configured for %v minutes", cfg.RetentionMinutes)
|
||||||
|
go retentionScanner(ds, time.Duration(cfg.RetentionMinutes) * time.Minute,
|
||||||
|
time.Duration(cfg.RetentionSleep) * time.Millisecond)
|
||||||
|
} else {
|
||||||
|
log.Info("Retention scanner disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func retentionScanner(ds DataStore, maxAge time.Duration, sleep time.Duration) {
|
||||||
|
start := time.Now()
|
||||||
|
for {
|
||||||
|
// Prevent scanner from running more than once a minute
|
||||||
|
since := time.Since(start)
|
||||||
|
if since < time.Minute {
|
||||||
|
dur := time.Minute - since
|
||||||
|
log.Trace("Retention scanner sleeping for %v", dur)
|
||||||
|
time.Sleep(dur)
|
||||||
|
}
|
||||||
|
start = time.Now()
|
||||||
|
|
||||||
|
// Kickoff scan
|
||||||
|
if err := doRetentionScan(ds, maxAge, sleep); err != nil {
|
||||||
|
log.Error("Error during retention scan: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRetentionScan does a single pass of all mailboxes looking for messages that can be purged
|
||||||
|
func doRetentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) error {
|
||||||
log.Trace("Starting retention scan")
|
log.Trace("Starting retention scan")
|
||||||
cutoff := time.Now().Add(-1 * maxAge)
|
cutoff := time.Now().Add(-1 * maxAge)
|
||||||
mboxes, err := ds.AllMailboxes()
|
mboxes, err := ds.AllMailboxes()
|
||||||
@@ -26,6 +72,8 @@ func retentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// Log but don't abort
|
// Log but don't abort
|
||||||
log.Error("Failed to purge message %v: %v", msg.Id(), err)
|
log.Error("Failed to purge message %v: %v", msg.Id(), err)
|
||||||
|
} else {
|
||||||
|
expRetentionDeletesTotal.Add(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,5 +81,32 @@ func retentionScan(ds DataStore, maxAge time.Duration, sleep time.Duration) erro
|
|||||||
time.Sleep(sleep)
|
time.Sleep(sleep)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRetentionScanCompleted(time.Now())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setRetentionScanCompleted(t time.Time) {
|
||||||
|
retentionScanCompletedMu.Lock()
|
||||||
|
defer retentionScanCompletedMu.Unlock()
|
||||||
|
|
||||||
|
retentionScanCompleted = t
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRetentionScanCompleted() time.Time {
|
||||||
|
retentionScanCompletedMu.RLock()
|
||||||
|
defer retentionScanCompletedMu.RUnlock()
|
||||||
|
|
||||||
|
return retentionScanCompleted
|
||||||
|
}
|
||||||
|
|
||||||
|
func secondsSinceRetentionScanCompleted() interface{} {
|
||||||
|
return time.Since(getRetentionScanCompleted()) / time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rm := expvar.NewMap("retention")
|
||||||
|
rm.Set("SecondsSinceScanCompleted", expvar.Func(secondsSinceRetentionScanCompleted))
|
||||||
|
rm.Set("DeletesHist", expRetentionDeletesHist)
|
||||||
|
rm.Set("DeletesTotal", expRetentionDeletesTotal)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRetentionScanner(t *testing.T) {
|
func TestDoRetentionScan(t *testing.T) {
|
||||||
// Create mock objects
|
// Create mock objects
|
||||||
mds := &MockDataStore{}
|
mds := &MockDataStore{}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ func TestRetentionScanner(t *testing.T) {
|
|||||||
mb3.On("GetMessages").Return([]Message{new3}, nil)
|
mb3.On("GetMessages").Return([]Message{new3}, nil)
|
||||||
|
|
||||||
// Test 4 hour retention
|
// Test 4 hour retention
|
||||||
retentionScan(mds, 4*time.Hour, 0)
|
doRetentionScan(mds, 4*time.Hour, 0)
|
||||||
|
|
||||||
// Check our assertions
|
// Check our assertions
|
||||||
mds.AssertExpectations(t)
|
mds.AssertExpectations(t)
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
function displayMetrics(data, textStatus, jqXHR) {
|
function displayMetrics(data, textStatus, jqXHR) {
|
||||||
// Non graphing
|
// Non graphing
|
||||||
metric('uptime', data.uptime, timeFilter, false)
|
metric('uptime', data.uptime, timeFilter, false)
|
||||||
|
metric('retentionScanCompleted', data.retention.SecondsSinceScanCompleted, timeFilter, false)
|
||||||
|
|
||||||
// JavaScript history
|
// JavaScript history
|
||||||
metric('memstatsSys', data.memstats.Sys, sizeFilter, true)
|
metric('memstatsSys', data.memstats.Sys, sizeFilter, true)
|
||||||
@@ -102,6 +103,8 @@
|
|||||||
setHistory('smtpWarnsTotal', data.smtp.WarnsHist)
|
setHistory('smtpWarnsTotal', data.smtp.WarnsHist)
|
||||||
metric('smtpErrorsTotal', data.smtp.ErrorsTotal, numberFilter, false)
|
metric('smtpErrorsTotal', data.smtp.ErrorsTotal, numberFilter, false)
|
||||||
setHistory('smtpErrorsTotal', data.smtp.ErrorsHist)
|
setHistory('smtpErrorsTotal', data.smtp.ErrorsHist)
|
||||||
|
metric('retentionDeletesTotal', data.retention.DeletesTotal, numberFilter, false)
|
||||||
|
setHistory('retentionDeletesTotal', data.retention.DeletesHist)
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMetrics() {
|
function loadMetrics() {
|
||||||
@@ -202,5 +205,21 @@ values over time.</p>
|
|||||||
</table>
|
</table>
|
||||||
<p class="last"> </p>
|
<p class="last"> </p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<h3>Data Store Metrics</h3>
|
||||||
|
<table class="metrics">
|
||||||
|
<tr>
|
||||||
|
<th>Retention Scan:</th>
|
||||||
|
<td colspan="3">Completed <span id="m-retentionScanCompleted">.</span> ago</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Retention Deletes:</th>
|
||||||
|
<td><span id="m-retentionDeletesTotal">.</span></td>
|
||||||
|
<td class="sparkline"><span id="s-retentionDeletesTotal"></span></td>
|
||||||
|
<td>(60s)</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p class="last"> </p>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user