Skip to content

Commit e11e82a

Browse files
Bernd Warmuthwarber
authored andcommitted
fix: make document::Create more resilient if subsequent patch call receives 404
We call PATCH immediately after POST when creating a document in order to make it public. However, it can be the case that the subsequent call to the PATCH endpoint happens to early. In this situation the API returns 404 and the call to Create fails. To make this more resilient, a retry mechanism was added. Signed-off-by: Bernd Warmuth <[email protected]>
1 parent ccc0481 commit e11e82a

File tree

2 files changed

+62
-4
lines changed

2 files changed

+62
-4
lines changed

clients/documents/documents.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"net/http"
2929
"net/url"
3030
"strings"
31+
"time"
3132

3233
"github.com/dynatrace/dynatrace-configuration-as-code-core/api"
3334
"github.com/dynatrace/dynatrace-configuration-as-code-core/api/clients/documents"
@@ -215,14 +216,15 @@ func (c Client) Create(ctx context.Context, name string, isPrivate bool, externa
215216
return api.Response{}, err
216217
}
217218

218-
r, err := c.patch(ctx, md.ID, md.Version, d)
219+
r, err := c.patchWithRetry(ctx, md.ID, md.Version, d)
219220
if err != nil {
220-
if _, err1 := c.delete(ctx, md.ID, md.Version); err1 != nil {
221-
return api.Response{}, errors.Join(err, err1)
221+
if !isNotFoundError(err) {
222+
if _, err1 := c.delete(ctx, md.ID, md.Version); err1 != nil {
223+
return api.Response{}, errors.Join(err, err1)
224+
}
222225
}
223226
return api.Response{}, err
224227
}
225-
226228
return r, nil
227229
}
228230

@@ -275,6 +277,24 @@ func (c Client) create(ctx context.Context, d documents.Document) (api.Response,
275277
return processHttpResponse(c.client.Create(ctx, d))
276278
}
277279

280+
func (c Client) patchWithRetry(ctx context.Context, id string, version int, d documents.Document) (resp api.Response, err error) {
281+
const maxRetries = 5
282+
const retryDelay = 200 * time.Millisecond
283+
for r := 0; r < maxRetries; r++ {
284+
if resp, err = c.patch(ctx, id, version, d); isNotFoundError(err) {
285+
time.Sleep(retryDelay)
286+
continue
287+
}
288+
break
289+
}
290+
return
291+
}
292+
293+
func isNotFoundError(err error) bool {
294+
var apiErr api.APIError
295+
return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound
296+
}
297+
278298
func (c Client) patch(ctx context.Context, id string, version int, d documents.Document) (api.Response, error) {
279299
resp, err := processHttpResponse(c.client.Patch(ctx, id, version, d))
280300
if err != nil {

clients/documents/documents_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,44 @@ func TestDocumentClient_Create(t *testing.T) {
230230
assert.ErrorContains(t, err, "some internal error")
231231
})
232232

233+
t.Run("patch call returns 404 - retry succeeds", func(t *testing.T) {
234+
ctx := testutils.ContextWithLogger(t)
235+
mockClient := documents.NewMockclient(gomock.NewController(t))
236+
mockClient.EXPECT().Create(ctx, givenDoc).
237+
Return(&http.Response{Status: http.StatusText(http.StatusCreated), StatusCode: http.StatusCreated, Body: io.NopCloser(strings.NewReader(respCreate)), Request: &http.Request{Method: http.MethodGet, URL: &url.URL{}}}, nil)
238+
mockClient.EXPECT().Patch(ctx, "f6e26fdd-1451-4655-b6ab-1240a00c1fba", 1, givenDoc).Times(4).
239+
Return(&http.Response{Status: http.StatusText(http.StatusNotFound), StatusCode: http.StatusNotFound, Body: io.NopCloser(strings.NewReader("some internal error")), Request: &http.Request{Method: http.MethodPatch, URL: &url.URL{}}}, nil)
240+
mockClient.EXPECT().Patch(ctx, "f6e26fdd-1451-4655-b6ab-1240a00c1fba", 1, givenDoc).
241+
Return(&http.Response{Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(respPatch)), Request: &http.Request{Method: http.MethodPatch, URL: &url.URL{}}}, nil)
242+
243+
docClient := documents.NewTestClient(mockClient)
244+
245+
res, err := docClient.Create(ctx, "name", false, "extID", []byte("this is the content"), documents.Notebook)
246+
247+
require.NoError(t, err)
248+
assert.JSONEq(t, expected, string(res.Data))
249+
})
250+
251+
t.Run("patch call returns 404 - retry fails", func(t *testing.T) {
252+
ctx := testutils.ContextWithLogger(t)
253+
mockClient := documents.NewMockclient(gomock.NewController(t))
254+
mockClient.EXPECT().Create(ctx, givenDoc).
255+
Return(&http.Response{Status: http.StatusText(http.StatusCreated), StatusCode: http.StatusCreated, Body: io.NopCloser(strings.NewReader(respCreate)), Request: &http.Request{Method: http.MethodGet, URL: &url.URL{}}}, nil)
256+
mockClient.EXPECT().Patch(ctx, "f6e26fdd-1451-4655-b6ab-1240a00c1fba", 1, givenDoc).Times(5).
257+
Return(&http.Response{Status: http.StatusText(http.StatusNotFound), StatusCode: http.StatusNotFound, Body: io.NopCloser(strings.NewReader("some internal error")), Request: &http.Request{Method: http.MethodPatch, URL: &url.URL{}}}, nil)
258+
259+
docClient := documents.NewTestClient(mockClient)
260+
261+
res, err := docClient.Create(ctx, "name", false, "extID", []byte("this is the content"), documents.Notebook)
262+
263+
require.Empty(t, res)
264+
require.Error(t, err)
265+
266+
// var apiErr api.APIError
267+
assert.ErrorAs(t, err, &api.APIError{})
268+
assert.ErrorContains(t, err, "API request HTTP PATCH failed with status code 404")
269+
})
270+
233271
t.Run("patch call returns non successful response; rollback fails", func(t *testing.T) {
234272
ctx := testutils.ContextWithLogger(t)
235273
mockClient := documents.NewMockclient(gomock.NewController(t))

0 commit comments

Comments
 (0)