Commit 5f39cbef authored by Mohammad Gufran's avatar Mohammad Gufran Committed by Matt Holt

caddytls: Extract locker into an interface (#1942)

parent 63fd2640
...@@ -39,6 +39,7 @@ type ACMEClient struct { ...@@ -39,6 +39,7 @@ type ACMEClient struct {
AllowPrompts bool AllowPrompts bool
config *Config config *Config
acmeClient *acme.Client acmeClient *acme.Client
locker Locker
} }
// newACMEClient creates a new ACMEClient given an email and whether // newACMEClient creates a new ACMEClient given an email and whether
...@@ -120,6 +121,10 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) ...@@ -120,6 +121,10 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
AllowPrompts: allowPrompts, AllowPrompts: allowPrompts,
config: config, config: config,
acmeClient: client, acmeClient: client,
locker: &syncLock{
nameLocks: make(map[string]*sync.WaitGroup),
nameLocksMu: sync.Mutex{},
},
} }
if config.DNSProvider == "" { if config.DNSProvider == "" {
...@@ -210,7 +215,7 @@ func (c *ACMEClient) Obtain(name string) error { ...@@ -210,7 +215,7 @@ func (c *ACMEClient) Obtain(name string) error {
return err return err
} }
waiter, err := storage.TryLock(name) waiter, err := c.locker.TryLock(name)
if err != nil { if err != nil {
return err return err
} }
...@@ -220,7 +225,7 @@ func (c *ACMEClient) Obtain(name string) error { ...@@ -220,7 +225,7 @@ func (c *ACMEClient) Obtain(name string) error {
return nil // we assume the process with the lock succeeded, rather than hammering this execution path again return nil // we assume the process with the lock succeeded, rather than hammering this execution path again
} }
defer func() { defer func() {
if err := storage.Unlock(name); err != nil { if err := c.locker.Unlock(name); err != nil {
log.Printf("[ERROR] Unable to unlock obtain call for %s: %v", name, err) log.Printf("[ERROR] Unable to unlock obtain call for %s: %v", name, err)
} }
}() }()
...@@ -286,7 +291,7 @@ func (c *ACMEClient) Renew(name string) error { ...@@ -286,7 +291,7 @@ func (c *ACMEClient) Renew(name string) error {
return err return err
} }
waiter, err := storage.TryLock(name) waiter, err := c.locker.TryLock(name)
if err != nil { if err != nil {
return err return err
} }
...@@ -296,7 +301,7 @@ func (c *ACMEClient) Renew(name string) error { ...@@ -296,7 +301,7 @@ func (c *ACMEClient) Renew(name string) error {
return nil // we assume the process with the lock succeeded, rather than hammering this execution path again return nil // we assume the process with the lock succeeded, rather than hammering this execution path again
} }
defer func() { defer func() {
if err := storage.Unlock(name); err != nil { if err := c.locker.Unlock(name); err != nil {
log.Printf("[ERROR] Unable to unlock renew call for %s: %v", name, err) log.Printf("[ERROR] Unable to unlock renew call for %s: %v", name, err)
} }
}() }()
......
...@@ -22,7 +22,6 @@ import ( ...@@ -22,7 +22,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"github.com/mholt/caddy" "github.com/mholt/caddy"
) )
...@@ -40,8 +39,7 @@ var storageBasePath = filepath.Join(caddy.AssetsPath(), "acme") ...@@ -40,8 +39,7 @@ var storageBasePath = filepath.Join(caddy.AssetsPath(), "acme")
// instance is guaranteed to be non-nil if there is no error. // instance is guaranteed to be non-nil if there is no error.
func NewFileStorage(caURL *url.URL) (Storage, error) { func NewFileStorage(caURL *url.URL) (Storage, error) {
return &FileStorage{ return &FileStorage{
Path: filepath.Join(storageBasePath, caURL.Host), Path: filepath.Join(storageBasePath, caURL.Host),
nameLocks: make(map[string]*sync.WaitGroup),
}, nil }, nil
} }
...@@ -49,9 +47,7 @@ func NewFileStorage(caURL *url.URL) (Storage, error) { ...@@ -49,9 +47,7 @@ func NewFileStorage(caURL *url.URL) (Storage, error) {
// directory. It is used to get file paths in a consistent, // directory. It is used to get file paths in a consistent,
// cross-platform way or persisting ACME assets on the file system. // cross-platform way or persisting ACME assets on the file system.
type FileStorage struct { type FileStorage struct {
Path string Path string
nameLocks map[string]*sync.WaitGroup
nameLocksMu sync.Mutex
} }
// sites gets the directory that stores site certificate and keys. // sites gets the directory that stores site certificate and keys.
...@@ -254,36 +250,6 @@ func (s *FileStorage) StoreUser(email string, data *UserData) error { ...@@ -254,36 +250,6 @@ func (s *FileStorage) StoreUser(email string, data *UserData) error {
return nil return nil
} }
// TryLock attempts to get a lock for name, otherwise it returns
// a Waiter value to wait until the other process is finished.
func (s *FileStorage) TryLock(name string) (Waiter, error) {
s.nameLocksMu.Lock()
defer s.nameLocksMu.Unlock()
wg, ok := s.nameLocks[name]
if ok {
// lock already obtained, let caller wait on it
return wg, nil
}
// caller gets lock
wg = new(sync.WaitGroup)
wg.Add(1)
s.nameLocks[name] = wg
return nil, nil
}
// Unlock unlocks name.
func (s *FileStorage) Unlock(name string) error {
s.nameLocksMu.Lock()
defer s.nameLocksMu.Unlock()
wg, ok := s.nameLocks[name]
if !ok {
return fmt.Errorf("FileStorage: no lock to release for %s", name)
}
wg.Done()
delete(s.nameLocks, name)
return nil
}
// MostRecentUserEmail implements Storage.MostRecentUserEmail by finding the // MostRecentUserEmail implements Storage.MostRecentUserEmail by finding the
// most recently written sub directory in the users' directory. It is named // most recently written sub directory in the users' directory. It is named
// after the email address. This corresponds to the most recent call to // after the email address. This corresponds to the most recent call to
......
...@@ -39,24 +39,9 @@ type UserData struct { ...@@ -39,24 +39,9 @@ type UserData struct {
Key []byte Key []byte
} }
// Storage is an interface abstracting all storage used by Caddy's TLS // Locker provides support for mutual exclusion
// subsystem. Implementations of this interface store both site and type Locker interface {
// user data. // TryLock will return immediatedly with or without acquiring the lock.
type Storage interface {
// SiteExists returns true if this site exists in storage.
// Site data is considered present when StoreSite has been called
// successfully (without DeleteSite having been called, of course).
SiteExists(domain string) (bool, error)
// TryLock is called before Caddy attempts to obtain or renew a
// certificate for a certain name and store it. From the perspective
// of this method and its companion Unlock, the actions of
// obtaining/renewing and then storing the certificate are atomic,
// and both should occur within a lock. This prevents multiple
// processes -- maybe distributed ones -- from stepping on each
// other's space in the same shared storage, and from spamming
// certificate providers with multiple, redundant requests.
//
// If a lock could be obtained, (nil, nil) is returned and you may // If a lock could be obtained, (nil, nil) is returned and you may
// continue normally. If not (meaning another process is already // continue normally. If not (meaning another process is already
// working on that name), a Waiter value will be returned upon // working on that name), a Waiter value will be returned upon
...@@ -75,6 +60,16 @@ type Storage interface { ...@@ -75,6 +60,16 @@ type Storage interface {
// the obtain/renew and store are finished, even if there was // the obtain/renew and store are finished, even if there was
// an error (or a timeout). // an error (or a timeout).
Unlock(name string) error Unlock(name string) error
}
// Storage is an interface abstracting all storage used by Caddy's TLS
// subsystem. Implementations of this interface store both site and
// user data.
type Storage interface {
// SiteExists returns true if this site exists in storage.
// Site data is considered present when StoreSite has been called
// successfully (without DeleteSite having been called, of course).
SiteExists(domain string) (bool, error)
// LoadSite obtains the site data from storage for the given domain and // LoadSite obtains the site data from storage for the given domain and
// returns it. If data for the domain does not exist, an error value // returns it. If data for the domain does not exist, an error value
......
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddytls
import (
"fmt"
"sync"
)
var _ Locker = &syncLock{}
type syncLock struct {
nameLocks map[string]*sync.WaitGroup
nameLocksMu sync.Mutex
}
// TryLock attempts to get a lock for name, otherwise it returns
// a Waiter value to wait until the other process is finished.
func (s *syncLock) TryLock(name string) (Waiter, error) {
s.nameLocksMu.Lock()
defer s.nameLocksMu.Unlock()
wg, ok := s.nameLocks[name]
if ok {
// lock already obtained, let caller wait on it
return wg, nil
}
// caller gets lock
wg = new(sync.WaitGroup)
wg.Add(1)
s.nameLocks[name] = wg
return nil, nil
}
// Unlock unlocks name.
func (s *syncLock) Unlock(name string) error {
s.nameLocksMu.Lock()
defer s.nameLocksMu.Unlock()
wg, ok := s.nameLocks[name]
if !ok {
return fmt.Errorf("FileStorage: no lock to release for %s", name)
}
wg.Done()
delete(s.nameLocks, name)
return nil
}
...@@ -16,7 +16,6 @@ package caddytls ...@@ -16,7 +16,6 @@ package caddytls
import ( import (
"os" "os"
"sync"
"testing" "testing"
"github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme"
...@@ -94,7 +93,7 @@ func TestQualifiesForManagedTLS(t *testing.T) { ...@@ -94,7 +93,7 @@ func TestQualifiesForManagedTLS(t *testing.T) {
} }
func TestSaveCertResource(t *testing.T) { func TestSaveCertResource(t *testing.T) {
storage := &FileStorage{Path: "./le_test_save", nameLocks: make(map[string]*sync.WaitGroup)} storage := &FileStorage{Path: "./le_test_save"}
defer func() { defer func() {
err := os.RemoveAll(storage.Path) err := os.RemoveAll(storage.Path)
if err != nil { if err != nil {
...@@ -140,7 +139,7 @@ func TestSaveCertResource(t *testing.T) { ...@@ -140,7 +139,7 @@ func TestSaveCertResource(t *testing.T) {
} }
func TestExistingCertAndKey(t *testing.T) { func TestExistingCertAndKey(t *testing.T) {
storage := &FileStorage{Path: "./le_test_existing", nameLocks: make(map[string]*sync.WaitGroup)} storage := &FileStorage{Path: "./le_test_existing"}
defer func() { defer func() {
err := os.RemoveAll(storage.Path) err := os.RemoveAll(storage.Path)
if err != nil { if err != nil {
......
...@@ -21,7 +21,6 @@ import ( ...@@ -21,7 +21,6 @@ import (
"crypto/rand" "crypto/rand"
"io" "io"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
...@@ -196,7 +195,7 @@ func TestGetEmail(t *testing.T) { ...@@ -196,7 +195,7 @@ func TestGetEmail(t *testing.T) {
} }
} }
var testStorage = &FileStorage{Path: "./testdata", nameLocks: make(map[string]*sync.WaitGroup)} var testStorage = &FileStorage{Path: "./testdata"}
func (s *FileStorage) clean() error { func (s *FileStorage) clean() error {
return os.RemoveAll(s.Path) return os.RemoveAll(s.Path)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment