Skip to content

Commit

Permalink
HETZNER: enable concurrency capability (#3234)
Browse files Browse the repository at this point in the history
Signed-off-by: Jakob Ackermann <das7pad@outlook.com>
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
  • Loading branch information
das7pad and tlimoncelli committed Dec 12, 2024
1 parent 0e966d9 commit aef00df
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 22 deletions.
2 changes: 1 addition & 1 deletion documentation/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ If a feature is definitively not supported for whatever reason, we would also li
| [`GCLOUD`](provider/gcloud.md) ||||||||||||||||||||||||
| [`GCORE`](provider/gcore.md) ||||||||||||||||||||||||
| [`HEDNS`](provider/hedns.md) ||||||||||||||||||||||||
| [`HETZNER`](provider/hetzner.md) |||| ||||||||||||||||||||
| [`HETZNER`](provider/hetzner.md) |||| ||||||||||||||||||||
| [`HEXONET`](provider/hexonet.md) ||||||||||||||||||||||||
| [`HOSTINGDE`](provider/hostingde.md) ||||||||||||||||||||||||
| [`HUAWEICLOUD`](provider/huaweicloud.md) ||||||||||||||||||||||||
Expand Down
46 changes: 34 additions & 12 deletions providers/hetzner/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"net/http"
"strconv"
"sync"
"time"

"github.com/StackExchange/dnscontrol/v4/pkg/printer"
Expand All @@ -18,7 +19,8 @@ const (

type hetznerProvider struct {
apiKey string
zones map[string]zone
mu sync.Mutex
cachedZones map[string]zone
requestRateLimiter requestRateLimiter
}

Expand Down Expand Up @@ -103,9 +105,17 @@ func (api *hetznerProvider) getAllRecords(domain string) ([]record, error) {
return records, nil
}

func (api *hetznerProvider) getAllZones() error {
if api.zones != nil {
return nil
func (api *hetznerProvider) resetZoneCache() {
api.mu.Lock()
defer api.mu.Unlock()
api.cachedZones = nil
}

func (api *hetznerProvider) getAllZones() (map[string]zone, error) {
api.mu.Lock()
defer api.mu.Unlock()
if api.cachedZones != nil {
return api.cachedZones, nil
}
var zones map[string]zone
page := 1
Expand All @@ -124,7 +134,7 @@ func (api *hetznerProvider) getAllZones() error {
response := getAllZonesResponse{}
url := fmt.Sprintf("/zones?per_page=100&page=%d", page)
if err := api.request(url, "GET", nil, &response, statusOK); err != nil {
return fmt.Errorf("failed fetching zones: %w", err)
return nil, fmt.Errorf("failed fetching zones: %w", err)
}
if zones == nil {
zones = make(map[string]zone, response.Meta.Pagination.TotalEntries)
Expand All @@ -138,15 +148,16 @@ func (api *hetznerProvider) getAllZones() error {
}
page++
}
api.zones = zones
return nil
api.cachedZones = zones
return zones, nil
}

func (api *hetznerProvider) getZone(name string) (*zone, error) {
if err := api.getAllZones(); err != nil {
zones, err := api.getAllZones()
if err != nil {
return nil, err
}
z, ok := api.zones[name]
z, ok := zones[name]
if !ok {
return nil, fmt.Errorf("%q is not a zone in this HETZNER account", name)
}
Expand Down Expand Up @@ -213,18 +224,28 @@ func (api *hetznerProvider) request(endpoint string, method string, request inte
}

type requestRateLimiter struct {
mu sync.Mutex
delay time.Duration
lastRequest time.Time
resetAt time.Time
}

func (rrl *requestRateLimiter) delayRequest() {
time.Sleep(time.Until(rrl.lastRequest.Add(rrl.delay)))

rrl.mu.Lock()
// When not rate-limited, include network/server latency in delay.
rrl.lastRequest = time.Now()
next := rrl.lastRequest.Add(rrl.delay)
if next.After(rrl.resetAt) {
// Do not stack delays past the reset point.
next = rrl.resetAt
}
rrl.lastRequest = next
rrl.mu.Unlock()
time.Sleep(time.Until(next))
}

func (rrl *requestRateLimiter) handleResponse(resp *http.Response) (bool, error) {
rrl.mu.Lock()
defer rrl.mu.Unlock()
if resp.StatusCode == http.StatusTooManyRequests {
printer.Printf("Rate-Limited. Consider contacting the Hetzner Support for raising your quota. URL: %q, Headers: %q\n", resp.Request.URL, resp.Header)

Expand Down Expand Up @@ -264,5 +285,6 @@ func (rrl *requestRateLimiter) handleResponse(resp *http.Response) (bool, error)
// ... then spread requests evenly throughout the window.
rrl.delay = reset / time.Duration(remaining+1)
}
rrl.resetAt = time.Now().Add(reset)
return false, nil
}
21 changes: 12 additions & 9 deletions providers/hetzner/hetznerProvider.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var features = providers.DocumentationNotes{
// See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanGetZones: providers.Can(),
providers.CanConcur: providers.Cannot(),
providers.CanConcur: providers.Can(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Can(),
Expand Down Expand Up @@ -68,9 +68,11 @@ func (api *hetznerProvider) EnsureZoneExists(domain string) error {
}
}

// reset zone cache
api.zones = nil
return api.createZone(domain)
if err = api.createZone(domain); err != nil {
return err
}
api.resetZoneCache()
return nil
}

// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
Expand Down Expand Up @@ -167,12 +169,13 @@ func (api *hetznerProvider) GetZoneRecords(domain string, meta map[string]string

// ListZones lists the zones on this account.
func (api *hetznerProvider) ListZones() ([]string, error) {
if err := api.getAllZones(); err != nil {
zones, err := api.getAllZones()
if err != nil {
return nil, err
}
zones := make([]string, 0, len(api.zones))
for domain := range api.zones {
zones = append(zones, domain)
domains := make([]string, 0, len(zones))
for domain := range zones {
domains = append(domains, domain)
}
return zones, nil
return domains, nil
}

0 comments on commit aef00df

Please # to comment.