Skip to content

Commit

Permalink
Add exponential backoff for Slack requests
Browse files Browse the repository at this point in the history
Adds a 30 second retry to POST requests made to Slack's API, in the
event that Slack experiences an intermittent failure.
  • Loading branch information
arbourd committed Jun 20, 2024
1 parent e98df62 commit 5cc9ddd
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 24 deletions.
3 changes: 2 additions & 1 deletion check/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"encoding/json"
"fmt"
"log"
"os"

Expand All @@ -11,6 +12,6 @@ import (
func main() {
err := json.NewEncoder(os.Stdout).Encode(concourse.CheckResponse{})
if err != nil {
log.Fatalln(err)
log.Fatalln(fmt.Errorf("error: %s", err))
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.22

require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/cenkalti/backoff/v4 v4.3.0
github.com/google/go-cmp v0.6.0
golang.org/x/oauth2 v0.21.0
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
Expand Down
3 changes: 2 additions & 1 deletion in/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"encoding/json"
"fmt"
"log"
"os"

Expand All @@ -11,6 +12,6 @@ import (
func main() {
err := json.NewEncoder(os.Stdout).Encode(concourse.InResponse{Version: concourse.Version{"ver": "static"}})
if err != nil {
log.Fatalln(err)
log.Fatalln(fmt.Errorf("error: %s", err))
}
}
5 changes: 4 additions & 1 deletion out/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"

"github.com/arbourd/concourse-slack-alert-resource/concourse"
"github.com/arbourd/concourse-slack-alert-resource/slack"
Expand Down Expand Up @@ -130,6 +131,8 @@ func previousBuildName(s string) (string, error) {
return strings.Trim(s, ".0"), nil
}

var maxElapsedTime = 30 * time.Second

func out(input *concourse.OutRequest, path string) (*concourse.OutResponse, error) {
if input.Source.URL == "" {
return nil, errors.New("slack webhook url cannot be blank")
Expand All @@ -153,7 +156,7 @@ func out(input *concourse.OutRequest, path string) (*concourse.OutResponse, erro
}

message := buildMessage(alert, metadata, path)
err := slack.Send(input.Source.URL, message)
err := slack.Send(input.Source.URL, message, maxElapsedTime)
if err != nil {
return nil, fmt.Errorf("error sending slack message: %w", err)
}
Expand Down
27 changes: 20 additions & 7 deletions slack/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/cenkalti/backoff/v4"
)

// Message represents a Slack API message
Expand Down Expand Up @@ -35,20 +38,30 @@ type Field struct {
}

// Send sends the message to the webhook URL.
func Send(url string, m *Message) error {
func Send(url string, m *Message, maxRetryTime time.Duration) error {
buf, err := json.Marshal(m)
if err != nil {
return err
}

res, err := http.Post(url, "application/json", bytes.NewReader(buf))
err = backoff.Retry(
func() error {
r, err := http.Post(url, "application/json", bytes.NewReader(buf))
if err != nil {
return err
}
defer r.Body.Close()

if r.StatusCode > 399 {
return fmt.Errorf("unexpected response status code: %d", r.StatusCode)
}
return nil
},
backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(maxRetryTime)),
)

if err != nil {
return err
}
defer res.Body.Close()

if res.StatusCode != 200 {
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
return nil
}
39 changes: 25 additions & 14 deletions slack/slack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,48 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestSend(t *testing.T) {
cases := map[string]struct {
message *Message
err bool
backoff uint8
wantErr bool
}{
"ok": {
message: &Message{Channel: "concourse"},
backoff: 0,
},
"unauthorized": {
message: &Message{},
err: true,
"retry ok": {
message: &Message{Channel: "concourse"},
backoff: 1,
},
"retry fail": {
message: &Message{Channel: "concourse"},
backoff: 255,
wantErr: true,
},
}

for name, c := range cases {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if c.err {
http.Error(w, "", http.StatusUnauthorized)
}
}))

t.Run(name, func(t *testing.T) {
err := Send(s.URL, c.message)
if err != nil && !c.err {
tries := c.backoff

s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tries > 0 {
tries--
http.Error(w, "", http.StatusUnauthorized)
}
}))
defer s.Close()

err := Send(s.URL, c.message, 2*time.Second)
if err != nil && !c.wantErr {
t.Fatalf("unexpected error from Send:\n\t(ERR): %s", err)
} else if err == nil && c.err {
} else if err == nil && c.wantErr {
t.Fatalf("expected an error from Send:\n\t(GOT): nil")
}
})
s.Close()
}
}

0 comments on commit 5cc9ddd

Please sign in to comment.