Skip to content

Commit 176b213

Browse files
committed
Add a way to skip wait for R53 sync on delete; documentation update
1 parent 5ed9ab7 commit 176b213

File tree

5 files changed

+137
-21
lines changed

5 files changed

+137
-21
lines changed

BREAKING.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Breaking Changes
22

3-
## Version 1.6 (Upcoming)
3+
## Version 1.6
44

55
### libdns 1.0 Compatibility
66

@@ -82,33 +82,38 @@ provider := &route53.Provider{
8282

8383
**Rationale:** The new name clearly indicates this waits for Route53's internal synchronization, not worldwide DNS propagation (which can take hours depending on TTL values).
8484

85-
### JSON Configuration
85+
### Removed Deprecated Fields
8686

87-
If you're using JSON configuration, update your field names:
87+
Two deprecated fields have been removed in v1.6:
8888

89-
**Old (pre-v1.6):**
90-
```json
91-
{
92-
"max_wait_dur": 60,
93-
"wait_for_propagation": true
89+
- **`AWSProfile`** → Use `Profile` instead
90+
- **`Token`** → Use `SessionToken` instead
91+
92+
These fields were deprecated several versions ago and have identical functionality to their replacements.
93+
94+
```go
95+
// Old (removed in v1.6)
96+
provider := &route53.Provider{
97+
AWSProfile: "my-profile",
98+
Token: "my-session-token",
9499
}
95-
```
96100

97-
**New (v1.6+):**
98-
```json
99-
{
100-
"route53_max_wait": "2m",
101-
"wait_for_route53_sync": true
101+
// New (v1.6+)
102+
provider := &route53.Provider{
103+
Profile: "my-profile",
104+
SessionToken: "my-session-token",
102105
}
103106
```
104107

105-
Note: The old `max_wait_dur` field accepted a numeric value (seconds), while the new `route53_max_wait` field uses standard Go duration strings like `"2m"`, `"120s"`, etc.
108+
**JSON Configuration:** If using JSON config, update field names: `aws_profile``profile`, `token``session_token`
106109

107110
## Migration Checklist
108111

109112
- [ ] Update to libdns v1.0+ (see libdns documentation for typed records)
110113
- [ ] Rename `MaxWaitDur` to `Route53MaxWait` in your code
111114
- [ ] Change from plain integer (e.g., `60`) to proper `time.Duration` (e.g., `60 * time.Second`)
112115
- [ ] Rename `WaitForPropagation` to `WaitForRoute53Sync` in your code
116+
- [ ] Replace `AWSProfile` with `Profile` (if using)
117+
- [ ] Replace `Token` with `SessionToken` (if using)
113118
- [ ] Update JSON/YAML configuration files with new field names
114119
- [ ] Test your code thoroughly after migration

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ When you update records in AWS Route53, changes first propagate internally acros
111111

112112
See [Change Propagation to Route 53 DNS Servers](https://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets.html#API_ChangeResourceRecordSets_RequestSyntax:~:text=Change%20Propagation%20to%20Route%2053%20DNS%20Servers).
113113

114+
### Performance optimization for delete operations
115+
116+
By default, when `WaitForRoute53Sync` is enabled, the provider waits for synchronization on all operations, including deletes. For bulk delete operations where immediate consistency is not required, you can skip the wait on deletes by setting `SkipRoute53SyncOnDelete` to `true`:
117+
118+
```go
119+
provider := &route53.Provider{
120+
WaitForRoute53Sync: true, // Wait for sync on create/update
121+
SkipRoute53SyncOnDelete: true, // Skip wait on delete for better performance
122+
}
123+
```
124+
125+
This can significantly speed up bulk delete operations while still maintaining consistency guarantees for create and update operations.
126+
114127
## Contributing
115128

116129
Contributions are welcome! Please ensure that:

client.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import (
1717
"github.com/libdns/libdns"
1818
)
1919

20+
type contextKey int
21+
22+
const (
23+
contextKeyIsDeleteOperation contextKey = iota
24+
)
25+
2026
const (
2127
// defaultTTL is the default TTL for DNS records in seconds.
2228
defaultTTL = 300
@@ -392,8 +398,17 @@ func (p *Provider) applyChange(ctx context.Context, input *r53.ChangeResourceRec
392398
return err
393399
}
394400

395-
// Waiting for propagation if it's set in the provider config.
396-
if p.WaitForRoute53Sync {
401+
// Check if we should skip waiting for synchronization
402+
shouldWait := p.WaitForRoute53Sync
403+
if shouldWait && p.SkipRoute53SyncOnDelete {
404+
// Check if this is a delete operation
405+
if isDelete, ok := ctx.Value(contextKeyIsDeleteOperation).(bool); ok && isDelete {
406+
shouldWait = false
407+
}
408+
}
409+
410+
// Wait for propagation if enabled and not skipped
411+
if shouldWait {
397412
changeInput := &r53.GetChangeInput{
398413
Id: changeResult.ChangeInfo.Id,
399414
}

libdnstest/route53_test.go

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
package route53_libdnstest_test
22

33
import (
4+
"context"
5+
"net/netip"
46
"os"
57
"strings"
68
"testing"
9+
"time"
710

11+
"github.com/libdns/libdns"
812
"github.com/libdns/libdns/libdnstest"
913
"github.com/libdns/route53"
1014
)
1115

1216
func TestRoute53Provider(t *testing.T) {
13-
// Get credentials from environment
17+
// get credentials from environment
1418
accessKeyId := os.Getenv("AWS_ACCESS_KEY_ID")
1519
secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
1620
sessionToken := os.Getenv("AWS_SESSION_TOKEN")
1721
region := os.Getenv("AWS_REGION")
1822
profile := os.Getenv("AWS_PROFILE")
1923
testZone := os.Getenv("ROUTE53_TEST_ZONE")
2024

21-
// Check required environment variables
25+
// check required environment variables
2226
if testZone == "" {
2327
t.Skip("Skipping Route53 provider tests: ROUTE53_TEST_ZONE environment variable must be set")
2428
}
@@ -27,7 +31,7 @@ func TestRoute53Provider(t *testing.T) {
2731
t.Fatal("We expect the test zone to have trailing dot")
2832
}
2933

30-
// Create provider with available credentials
34+
// create provider with available credentials
3135
provider := &route53.Provider{
3236
Region: region,
3337
Profile: profile,
@@ -38,4 +42,75 @@ func TestRoute53Provider(t *testing.T) {
3842

3943
suite := libdnstest.NewTestSuite(libdnstest.WrapNoZoneLister(provider), testZone)
4044
suite.RunTests(t)
41-
}
45+
}
46+
47+
func TestSkipRoute53SyncOnDelete_Performance(t *testing.T) {
48+
// get credentials from environment
49+
accessKeyId := os.Getenv("AWS_ACCESS_KEY_ID")
50+
secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
51+
sessionToken := os.Getenv("AWS_SESSION_TOKEN")
52+
region := os.Getenv("AWS_REGION")
53+
profile := os.Getenv("AWS_PROFILE")
54+
testZone := os.Getenv("ROUTE53_TEST_ZONE")
55+
56+
// check required environment variables
57+
if testZone == "" {
58+
t.Skip("Skipping Route53 provider tests: ROUTE53_TEST_ZONE environment variable must be set")
59+
}
60+
61+
if !strings.HasSuffix(testZone, ".") {
62+
t.Fatal("We expect the test zone to have trailing dot")
63+
}
64+
65+
// create provider with waiting enabled but skip on delete operations
66+
provider := &route53.Provider{
67+
Region: region,
68+
Profile: profile,
69+
AccessKeyId: accessKeyId,
70+
SecretAccessKey: secretAccessKey,
71+
SessionToken: sessionToken,
72+
WaitForRoute53Sync: true,
73+
SkipRoute53SyncOnDelete: true,
74+
}
75+
76+
ctx := context.Background()
77+
testRecord := &libdns.Address{
78+
Name: "test-append-record-r53-sync",
79+
TTL: 300 * time.Second,
80+
IP: netip.MustParseAddr("192.0.2.1"),
81+
}
82+
83+
// clean up first - delete the record if it exists (fast because we skip sync on delete)
84+
t.Log("Cleaning up any existing test record...")
85+
_, _ = provider.DeleteRecords(ctx, testZone, []libdns.Record{testRecord})
86+
87+
// append the record - this should take longer because we wait for sync
88+
t.Log("Appending record with WaitForRoute53Sync=true (should be slow)...")
89+
appendStart := time.Now()
90+
_, err := provider.AppendRecords(ctx, testZone, []libdns.Record{testRecord})
91+
appendDuration := time.Since(appendStart)
92+
if err != nil {
93+
t.Fatalf("Failed to append record: %v", err)
94+
}
95+
t.Logf("Append took %v", appendDuration)
96+
97+
// delete the record - this should be fast because we skip sync on delete
98+
t.Log("Deleting record with SkipRoute53SyncOnDelete=true (should be fast)...")
99+
deleteStart := time.Now()
100+
_, err = provider.DeleteRecords(ctx, testZone, []libdns.Record{testRecord})
101+
deleteDuration := time.Since(deleteStart)
102+
if err != nil {
103+
t.Fatalf("Failed to delete record: %v", err)
104+
}
105+
t.Logf("Delete took %v", deleteDuration)
106+
107+
// verify that delete was significantly faster than append
108+
// append should wait for sync (typically 5-60 seconds)
109+
// delete should skip sync (typically <2 seconds)
110+
t.Logf("Performance comparison: append=%v, delete=%v, ratio=%.2fx",
111+
appendDuration, deleteDuration, float64(appendDuration)/float64(deleteDuration))
112+
113+
if deleteDuration >= appendDuration {
114+
t.Errorf("Delete operation took longer than append, expected delete to be faster due to SkipRoute53SyncOnDelete")
115+
}
116+
}

provider.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ type Provider struct {
4848
// to DNS propagation, that could take much longer.
4949
WaitForRoute53Sync bool `json:"wait_for_route53_sync,omitempty"`
5050

51+
// SkipRoute53SyncOnDelete if set to true, it will skip waiting for Route53
52+
// synchronization when deleting records, even if WaitForRoute53Sync is true.
53+
// This can speed up bulk delete operations where waiting is not necessary.
54+
SkipRoute53SyncOnDelete bool `json:"skip_route53_sync_on_delete,omitempty"`
55+
5156
// HostedZoneID is the ID of the hosted zone to use. If not set, it will
5257
// be discovered from the zone name.
5358
//
@@ -161,6 +166,9 @@ type recordSetKey struct {
161166
func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
162167
p.init(ctx)
163168

169+
// mark this context as a delete operation
170+
ctx = context.WithValue(ctx, contextKeyIsDeleteOperation, true)
171+
164172
zoneID, err := p.getZoneID(ctx, zone)
165173
if err != nil {
166174
return nil, err

0 commit comments

Comments
 (0)