Skip to content

Commit

Permalink
Implement retry mechanism for known retriable errors (#2)
Browse files Browse the repository at this point in the history
Wrote retry feature:
- maxRetries: 5
Fixed Typo in README:
- `go tidy` -> `go mod tidy`
  • Loading branch information
AesthicEthics authored May 30, 2024
1 parent 753d214 commit 769d008
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 561 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
terraform-provider-email
.terraform
bin

# Created by https://www.toptal.com/developers/gitignore/api/go,terraform
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Terraform provider for sending emails.
## Development

```bash
go tidy # install dependencies
go mod tidy # install dependencies
make build # build the provider
make install # install the provider
```
Expand Down
56 changes: 56 additions & 0 deletions email/provider_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package email

import (
"errors"
"fmt"
"net/smtp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/stretchr/testify/assert"
)

func TestAccEmail_basic(t *testing.T) {
Expand Down Expand Up @@ -57,6 +60,59 @@ func testAccCheckEmailExists(n string) resource.TestCheckFunc {
return nil
}
}
func mockSendMailReturn421(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
return errors.New("421 Service not available")
}

// function to test non-421 error
func mockReturn500(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
return errors.New("500 Internal Server Error")
}

// email to return no error
func mockSendMailSuccess(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
return nil
}

func TestRetryWorkflow(t *testing.T) {
// define max retries
maxRetries := 5
// write the tests
tests := []struct {
name string
sendMailFunc SendMailFunc
expectedErr error
}{
{
name: "Retry with 421 error",
sendMailFunc: mockSendMailReturn421,
expectedErr: errors.New("421 Service not available"),
},
{
name: "Success on first try",
sendMailFunc: mockSendMailSuccess,
expectedErr: nil,
},
{
name: "Other error",
sendMailFunc: mockReturn500,
expectedErr: errors.New("500 Internal Server Error"),
},
}
// execute the tests
for _, test := range tests {
// run subtests
t.Run(test.name, func(t *testing.T) {
err := sendMail(test.sendMailFunc, maxRetries, "localhost", "2525", "username", "password", "[email protected]", "[email protected]", "message")
if test.expectedErr != nil {
// assert that the errors are equal
assert.EqualError(t, err, test.expectedErr.Error())
} else {
assert.NoError(t, err)
}
})
}
}

// Requires a local SMTP server running on port 2525
// `docker run --rm -it -p 3000:80 -p 2525:25 rnwood/smtp4dev:v3`
Expand Down
56 changes: 51 additions & 5 deletions email/resource_email.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ package email

import (
"log"
"math/rand"
"net/smtp"
"regexp"
"strconv"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// define function type
type SendMailFunc func(addr string, a smtp.Auth, from string, to []string, msg []byte) error

func resourceEmail() *schema.Resource {
return &schema.Resource{
Create: resourceEmailCreate,
Expand Down Expand Up @@ -64,6 +69,17 @@ func resourceEmail() *schema.Resource {
}
}

// regex function to extract the status code
func extractStatusCode(errMsg string) string {
// Regex to find the first three-digit number, which is the SMTP status code
re := regexp.MustCompile(`\b\d{3}\b`)
matches := re.FindString(errMsg)
if matches != "" {
return matches // Returns the first match (three-digit number) if found
}
return "No status code found"
}

func resourceEmailCreate(d *schema.ResourceData, m interface{}) error {
to := d.Get("to").(string)
from := d.Get("from").(string)
Expand All @@ -83,21 +99,51 @@ func resourceEmailCreate(d *schema.ResourceData, m interface{}) error {
preamble + "\n\n" +
body

err := smtp.SendMail(smtpServer+":"+smtpPort,
smtp.PlainAuth("", smtpUsername, smtpPassword, smtpServer),
from, []string{to}, []byte(msg))

// TODO: make this tf configurable
maxRetries := 5
// send mail using exponential back-off
err := sendMail(smtp.SendMail, maxRetries, smtpServer, smtpPort, smtpUsername, smtpPassword, from, to, msg)
// log error if not cleared after retries
if err != nil {
log.Printf("smtp error: %s", err)
return err
}

// Create unique ID using current timestamp
timestamp := time.Now().Unix()
d.SetId(to + " | " + subject + " | " + strconv.FormatInt(timestamp, 10))

return resourceEmailRead(d, m)
}

func sendMail(sendEmailImpl SendMailFunc, maxRetries int, smtpServer string, smtpPort string, smtpUsername string, smtpPassword string, from string, to string, msg string) error {
// Set up a random number for exponential backoff
minRandInt := 10
maxRandInt := 150
// generate random number in that range
randomNumber := rand.Intn(maxRandInt-minRandInt) + minRandInt
var err error
for retries := 0; retries < maxRetries; retries++ {
// send smtp email
err = sendEmailImpl(smtpServer+":"+smtpPort,
smtp.PlainAuth("", smtpUsername, smtpPassword, smtpServer),
from, []string{to}, []byte(msg))

if err == nil {
break
}
// extract error code
errorCode := extractStatusCode(err.Error())
log.Printf("Extracted Error Code: %s", errorCode)
// guard statement for error 421
if errorCode != "421" {
break
}
// implement exponential back off
time.Sleep(time.Duration(randomNumber << 1))
}
return err
}

func resourceEmailRead(d *schema.ResourceData, m interface{}) error {
return nil
}
Expand Down
1 change: 1 addition & 0 deletions examples/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ terraform {

provider "email" {}


resource "email_email" "example" {
to = "[email protected]"
from = "[email protected]"
Expand Down
31 changes: 6 additions & 25 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,75 @@ module terraform-provider-email
go 1.20

require (
github.com/hashicorp/terraform-plugin-sdk v1.17.2
github.com/hashicorp/terraform-plugin-docs v0.16.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.27.0
github.com/stretchr/testify v1.8.1
)

require (
cloud.google.com/go v0.110.0 // indirect
cloud.google.com/go/compute v1.19.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.13.0 // indirect
cloud.google.com/go/storage v1.28.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/aws/aws-sdk-go v1.37.0 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
github.com/hashicorp/go-getter v1.5.3 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.4.10 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hc-install v0.5.2 // indirect
github.com/hashicorp/hcl/v2 v2.17.0 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.18.1 // indirect
github.com/hashicorp/terraform-json v0.17.1 // indirect
github.com/hashicorp/terraform-plugin-docs v0.16.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.16.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.1 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.11.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mitchellh/cli v1.1.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/russross/blackfriday v1.6.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/zclconf/go-cty v1.13.2 // indirect
github.com/zclconf/go-cty-yaml v1.0.2 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.114.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.56.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 769d008

Please sign in to comment.