1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

Implementing API retries when receiving 429 (TooManyRequests) HTTP Error (#2506)

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
Alexander Kabenin
2023-08-17 08:37:36 -07:00
committed by GitHub
parent 3c33ee2212
commit f13ae54858

View File

@@ -31,6 +31,9 @@ var docNotes = providers.DocumentationNotes{
providers.DocOfficiallySupported: providers.Cannot(),
}
// Number of retries for API backend requests in case of StatusTooManyRequests responses
const CLIENT_RETRIES = 10
func init() {
fns := providers.DspFuncs{
Initializer: newProvider,
@@ -48,27 +51,53 @@ func newProvider(creds map[string]string, meta json.RawMessage) (providers.DNSSe
if creds["api_token"] == "" {
return nil, fmt.Errorf("api_token required for ns1")
}
return &nsone{rest.NewClient(http.DefaultClient, rest.SetAPIKey(creds["api_token"]))}, nil
// Enable Sleep API Rate limit strategy - it will sleep until new tokens are available
// see https://help.ns1.com/hc/en-us/articles/360020250573-About-API-rate-limiting
// this strategy would imply the least sleep time for non-parallel client requests
return &nsone{rest.NewClient(
http.DefaultClient,
rest.SetAPIKey(creds["api_token"]),
func(c *rest.Client) {
c.RateLimitStrategySleep()
},
)}, nil
}
// A wrapper around rest.Client's Zones.Get() implementing retries
// no explicit sleep is needed, it is implemented in NS1 client's RateLimitStrategy we used
func (n *nsone) GetZone(domain string) (*dns.Zone, error) {
for rtr := 0; ; rtr++ {
z, httpResp, err := n.Zones.Get(domain)
if httpResp.StatusCode == http.StatusTooManyRequests && rtr < CLIENT_RETRIES {
continue
}
return z, err
}
}
func (n *nsone) EnsureZoneExists(domain string) error {
// This enables the create-domains subcommand
zone := dns.NewZone(domain)
_, err := n.Zones.Create(zone)
if err == rest.ErrZoneExists {
// if domain exists already, just return nil, nothing to do here.
return nil
for rtr := 0; ; rtr++ {
httpResp, err := n.Zones.Create(zone)
if err == rest.ErrZoneExists {
// if domain exists already, just return nil, nothing to do here.
return nil
}
// too many requests - retry w/out waiting. We specified rate limit strategy creating the client
if httpResp.StatusCode == http.StatusTooManyRequests && rtr < CLIENT_RETRIES {
continue
}
return err
}
return err
}
func (n *nsone) GetNameservers(domain string) ([]*models.Nameserver, error) {
var nservers []string
z, _, err := n.Zones.Get(domain, true)
z, _, err := n.Zones.Get(domain, true)
if err != nil {
return nil, err
}
@@ -112,20 +141,23 @@ func (n *nsone) GetZoneRecords(domain string, meta map[string]string) (models.Re
// 2. DNSSEC is disabled (returns false)
// 3. some error state (return false plus the error)
func (n *nsone) GetZoneDNSSEC(domain string) (bool, error) {
_, _, err := n.DNSSEC.Get(domain)
for rtr := 0; ; rtr++ {
_, httpResp, err := n.DNSSEC.Get(domain)
// rest.ErrDNSECNotEnabled is our "disabled" state
if err != nil && err == rest.ErrDNSECNotEnabled {
return false, nil
}
if httpResp.StatusCode == http.StatusTooManyRequests && rtr < CLIENT_RETRIES {
continue
}
// any other errors not expected, let's surface them
if err != nil {
return false, err
}
// rest.ErrDNSECNotEnabled is our "disabled" state
if err != nil && err == rest.ErrDNSECNotEnabled {
return false, nil
// no errors returned, we assume DNSSEC is enabled
return true, nil
}
// any other errors not expected, let's surface them
if err != nil {
return false, err
}
// no errors returned, we assume DNSSEC is enabled
return true, nil
}
// getDomainCorrectionsDNSSEC creates DNSSEC zone corrections based on current state and preference
@@ -244,8 +276,13 @@ func (n *nsone) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecor
}
func (n *nsone) add(recs models.Records, domain string) error {
_, err := n.Records.Create(buildRecord(recs, domain, ""))
return err
for rtr := 0; ; rtr++ {
httpResp, err := n.Records.Create(buildRecord(recs, domain, ""))
if httpResp.StatusCode == http.StatusTooManyRequests && rtr < CLIENT_RETRIES {
continue
}
return err
}
}
func (n *nsone) remove(key models.RecordKey, domain string) error {
@@ -253,13 +290,23 @@ func (n *nsone) remove(key models.RecordKey, domain string) error {
key.Type = "URLFWD"
}
_, err := n.Records.Delete(domain, key.NameFQDN, key.Type)
return err
for rtr := 0; ; rtr++ {
httpResp, err := n.Records.Delete(domain, key.NameFQDN, key.Type)
if httpResp.StatusCode == http.StatusTooManyRequests && rtr < CLIENT_RETRIES {
continue
}
return err
}
}
func (n *nsone) modify(recs models.Records, domain string) error {
_, err := n.Records.Update(buildRecord(recs, domain, ""))
return err
for rtr := 0; ; rtr++ {
httpResp, err := n.Records.Update(buildRecord(recs, domain, ""))
if httpResp.StatusCode == http.StatusTooManyRequests && rtr < CLIENT_RETRIES {
continue
}
return err
}
}
// configureDNSSEC configures DNSSEC for a zone. Set 'enabled' to true to enable, false to disable.
@@ -276,8 +323,13 @@ func (n *nsone) configureDNSSEC(domain string, enabled bool) error {
return err
}
z.DNSSEC = &enabled
_, err = n.Zones.Update(z)
return err
for rtr := 0; ; rtr++ {
httpResp, err := n.Zones.Update(z)
if httpResp.StatusCode == http.StatusTooManyRequests && rtr < CLIENT_RETRIES {
continue
}
return err
}
}
func buildRecord(recs models.Records, domain string, id string) *dns.Record {