From 11eaa4f759a746ee621f85362bae0512cb96497a Mon Sep 17 00:00:00 2001 From: Jaime Martinez Date: Wed, 15 Jul 2020 17:06:12 +1000 Subject: Add simple polling to the GitLab client --- internal/source/gitlab/client/client_poll.go | 34 ++++++++ internal/source/gitlab/client/client_poll_test.go | 94 +++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 internal/source/gitlab/client/client_poll.go create mode 100644 internal/source/gitlab/client/client_poll_test.go diff --git a/internal/source/gitlab/client/client_poll.go b/internal/source/gitlab/client/client_poll.go new file mode 100644 index 00000000..bdbfb345 --- /dev/null +++ b/internal/source/gitlab/client/client_poll.go @@ -0,0 +1,34 @@ +package client + +import ( + "fmt" + "time" +) + +const ( + // DefaultPollingMaxRetries to be used by Poll + DefaultPollingMaxRetries = 30 + // DefaultPollingInterval to be used by Poll + DefaultPollingInterval = 10 * time.Second +) + +// Poll tries to call the /internal/pages/status API endpoint for +// `retries` every `interval`. +// TODO: should we consider using an exponential back-off approach? +// https://pkg.go.dev/github.com/cenkalti/backoff/v4?tab=doc#pkg-examples +func (gc *Client) Poll(retries int, interval time.Duration, errCh chan error) { + defer close(errCh) + + var err error + for i := 0; i <= retries; i++ { + err = gc.Status() + if err == nil { + // return as soon as we connect to the API + errCh <- nil + } + + time.Sleep(interval) + } + + errCh <- fmt.Errorf("polling failed after %d tries every %fs: %w", retries, interval.Seconds(), err) +} diff --git a/internal/source/gitlab/client/client_poll_test.go b/internal/source/gitlab/client/client_poll_test.go new file mode 100644 index 00000000..59da32c5 --- /dev/null +++ b/internal/source/gitlab/client/client_poll_test.go @@ -0,0 +1,94 @@ +package client + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestClient_Poll(t *testing.T) { + tests := []struct { + name string + retries int + interval time.Duration + timeout time.Duration + status int + wantErr bool + }{ + { + name: "success_with_no_retry", + retries: 0, + interval: 5 * time.Millisecond, + timeout: 5 * time.Millisecond, + status: http.StatusOK, + wantErr: false, + }, + { + name: "success_after_N_retries", + retries: 3, + interval: 5 * time.Millisecond, + timeout: 20 * time.Millisecond, + status: http.StatusOK, + wantErr: false, + }, + { + name: "fail_with_no_retries", + retries: 0, + interval: 5 * time.Millisecond, + timeout: 100 * time.Millisecond, + status: http.StatusUnauthorized, + wantErr: true, + }, + { + name: "fail_after_N_retries", + retries: 3, + interval: 5 * time.Millisecond, + timeout: 100 * time.Millisecond, + status: http.StatusUnauthorized, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var counter int + mux := http.NewServeMux() + mux.HandleFunc("/api/v4/internal/pages/status", func(w http.ResponseWriter, r *http.Request) { + if counter < tt.retries { + counter++ + // fail on purpose until we reach the max retry + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(tt.status) + }) + + server := httptest.NewServer(mux) + defer server.Close() + + client := defaultClient(t, server.URL) + errCh := make(chan error) + + go client.Poll(tt.retries, tt.interval, errCh) + + // go func() { + select { + case err := <-errCh: + if tt.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), "polling failed after") + require.Contains(t, err.Error(), ConnectionErrorMsg) + return + } + require.NoError(t, err) + case <-time.After(tt.timeout): + t.Logf("%s timed out", tt.name) + t.FailNow() + } + // }() + }) + } +} -- cgit v1.2.3