diff --git a/server.go b/server.go index 5c77d6607279841d8d1994231bf3165bb94086dd..79d25a48dcfcc70a77669d444a4eddbd5d22df98 100644 --- a/server.go +++ b/server.go @@ -15,6 +15,7 @@ import ( "io" "log" "path/filepath" + "sync" "time" "git.autistici.org/ai3/go-common/clientutil" @@ -126,11 +127,19 @@ var ( errorRetryTimeout = 10 * time.Minute ) -func (m *Manager) updateAllCerts(ctx context.Context) { - for _, certInfo := range m.certs { +func (m *Manager) updateAllCerts(ctx context.Context, certs []*certInfo) { + for _, certInfo := range certs { if certInfo.retryDeadline.After(time.Now()) { continue } + + // Abort the loop if our context is canceled. + select { + case <-ctx.Done(): + return + default: + } + uctx, cancel := context.WithTimeout(ctx, renewalTimeout) err := m.updateCert(uctx, certInfo) cancel() @@ -174,7 +183,7 @@ func (m *Manager) updateCert(ctx context.Context, certInfo *certInfo) error { } // Replace the current configuration. -func (m *Manager) loadConfig(certDomains [][]string) { +func (m *Manager) loadConfig(certDomains [][]string) []*certInfo { var certs []*certInfo for _, domains := range certDomains { cn := domains[0] @@ -197,7 +206,7 @@ func (m *Manager) loadConfig(certDomains [][]string) { } certs = append(certs, certInfo) } - m.certs = certs + return certs } // This channel is used by the testing code to trigger an update, @@ -205,15 +214,43 @@ func (m *Manager) loadConfig(certDomains [][]string) { var testUpdateCh = make(chan bool) func (m *Manager) loop(ctx context.Context) { + // Updates are long-term jobs, so they should be + // interruptible. We run updates in a separate goroutine, and + // cancel them when the configuration is reloaded or on exit. + var upCancel context.CancelFunc + var wg sync.WaitGroup + + startUpdate := func(certs []*certInfo) context.CancelFunc { + // Ensure the previous update has finished. + wg.Wait() + + upCtx, cancel := context.WithCancel(ctx) + wg.Add(1) + go func() { + m.updateAllCerts(upCtx, certs) + wg.Done() + }() + return cancel + } + + cancelUpdate := func() { + if upCancel != nil { + upCancel() + } + wg.Wait() + } + defer cancelUpdate() + tick := time.NewTicker(5 * time.Minute) for { select { case <-tick.C: - m.updateAllCerts(ctx) + upCancel = startUpdate(m.certs) case <-testUpdateCh: - m.updateAllCerts(ctx) + upCancel = startUpdate(m.certs) case certDomains := <-m.configCh: - m.loadConfig(certDomains) + cancelUpdate() + m.certs = m.loadConfig(certDomains) case <-m.stopCh: return case <-ctx.Done():