Skip to content

Commit 36a6bea

Browse files
authored
MONGOCRYPT-763 add in-place retry API (#1011)
1 parent c3ec2b7 commit 36a6bea

5 files changed

Lines changed: 244 additions & 27 deletions

File tree

integrating.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,16 +243,30 @@ Ensure `mongocrypt_setopt_retry_kms` is called on the `mongocrypt_t` to enable r
243243
c. Write the message from `mongocrypt_kms_ctx_message` to the
244244
> socket.
245245

246-
d. Feed the reply back with `mongocrypt_kms_ctx_feed`. Repeat
247-
> until `mongocrypt_kms_ctx_bytes_needed` returns 0.
246+
d. Feed the reply back with `mongocrypt_kms_ctx_feed` or `mongocrypt_kms_ctx_feed_with_retry`. Repeat
247+
until `mongocrypt_kms_ctx_bytes_needed` returns 0. If the `should_retry` outparam returns true,
248+
the request may be retried by feeding the new response into the same context.
248249

249250
If any step encounters a network error, call `mongocrypt_kms_ctx_fail`.
250-
If `mongocrypt_kms_ctx_fail` returns true, continue to the next KMS context.
251+
If `mongocrypt_kms_ctx_fail` returns true, retry the request by continuing to the next KMS context or by feeding the new response into the same context.
251252
If `mongocrypt_kms_ctx_fail` returns false, abort and report an error. Consider wrapping the error reported in `mongocrypt_kms_ctx_status` to include the last network error.
252253

253254
2. When done feeding all replies, call `mongocrypt_ctx_kms_done`.
254255

255-
Note, the driver MAY fan out KMS requests in parallel. More KMS requests may be added when processing responses to retry.
256+
##### Retry and Iteration
257+
258+
Call `mongocrypt_setopt_retry_kms` to enable retry behavior.
259+
260+
There are two options for retry:
261+
- Lazy retry: After processing KMS contexts, iterate again by calling `mongocrypt_ctx_next_kms_ctx`. KMS contexts
262+
needing a retry will be returned.
263+
- In-place retry: If a KMS context indicates retry, retry the KMS request and feed the new response to the same KMS
264+
context. Use `mongocrypt_kms_ctx_feed_with_retry` and check the return of `mongocrypt_kms_ctx_fail` to check if a
265+
retry is indicated.
266+
267+
The driver MAY fan out KMS requests in parallel. It is not safe to iterate KMS contexts (i.e. call
268+
`mongocrypt_ctx_next_kms_ctx`) while operating on KMS contexts (e.g. calling `mongocrypt_kms_ctx_feed`). Drivers are
269+
recommended to do an in-place retry on KMS requests.
256270

257271
**Applies to...**
258272

src/mongocrypt-kms-ctx.c

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
#include "kms_message/kms_kmip_request.h"
18+
#include "kms_message/kms_response_parser.h"
1819
#include "mongocrypt-binary-private.h"
1920
#include "mongocrypt-buffer-private.h"
2021
#include "mongocrypt-crypto-private.h"
@@ -518,6 +519,9 @@ static void set_retry(mongocrypt_kms_ctx_t *kms) {
518519
kms->should_retry = true;
519520
kms->attempts++;
520521
kms->sleep_usec = backoff_time_usec(kms->attempts);
522+
if (kms->parser) {
523+
kms_response_parser_reset(kms->parser);
524+
}
521525
}
522526

523527
/* An AWS KMS context has received full response. Parse out the result or error.
@@ -1120,6 +1124,24 @@ static bool _ctx_done_kmip_decrypt(mongocrypt_kms_ctx_t *kms_ctx) {
11201124
return ret;
11211125
}
11221126

1127+
static bool _is_retryable_req(_kms_request_type_t req_type) {
1128+
// Check if request type is retryable. Some requests are non-idempotent and cannot be safely retried.
1129+
_kms_request_type_t retryable_types[] = {MONGOCRYPT_KMS_AZURE_OAUTH,
1130+
MONGOCRYPT_KMS_GCP_OAUTH,
1131+
MONGOCRYPT_KMS_AWS_ENCRYPT,
1132+
MONGOCRYPT_KMS_AWS_DECRYPT,
1133+
MONGOCRYPT_KMS_AZURE_WRAPKEY,
1134+
MONGOCRYPT_KMS_AZURE_UNWRAPKEY,
1135+
MONGOCRYPT_KMS_GCP_ENCRYPT,
1136+
MONGOCRYPT_KMS_GCP_DECRYPT};
1137+
for (size_t i = 0; i < sizeof(retryable_types) / sizeof(retryable_types[0]); i++) {
1138+
if (retryable_types[i] == req_type) {
1139+
return true;
1140+
}
1141+
}
1142+
return false;
1143+
}
1144+
11231145
bool mongocrypt_kms_ctx_fail(mongocrypt_kms_ctx_t *kms) {
11241146
if (!kms) {
11251147
return false;
@@ -1138,37 +1160,27 @@ bool mongocrypt_kms_ctx_fail(mongocrypt_kms_ctx_t *kms) {
11381160
return false;
11391161
}
11401162

1141-
// Check if request type is retryable. Some requests are non-idempotent and cannot be safely retried.
1142-
_kms_request_type_t retryable_types[] = {MONGOCRYPT_KMS_AZURE_OAUTH,
1143-
MONGOCRYPT_KMS_GCP_OAUTH,
1144-
MONGOCRYPT_KMS_AWS_ENCRYPT,
1145-
MONGOCRYPT_KMS_AWS_DECRYPT,
1146-
MONGOCRYPT_KMS_AZURE_WRAPKEY,
1147-
MONGOCRYPT_KMS_AZURE_UNWRAPKEY,
1148-
MONGOCRYPT_KMS_GCP_ENCRYPT,
1149-
MONGOCRYPT_KMS_GCP_DECRYPT};
1150-
bool is_retryable = false;
1151-
for (size_t i = 0; i < sizeof(retryable_types) / sizeof(retryable_types[0]); i++) {
1152-
if (retryable_types[i] == kms->req_type) {
1153-
is_retryable = true;
1154-
break;
1155-
}
1156-
}
1157-
if (!is_retryable) {
1163+
if (!_is_retryable_req(kms->req_type)) {
11581164
CLIENT_ERR("KMS request failed due to network error");
11591165
return false;
11601166
}
11611167

11621168
// Mark KMS context as retryable. Return again in `mongocrypt_ctx_next_kms_ctx`.
11631169
set_retry(kms);
1164-
1165-
// Reset intermediate state of parser.
1166-
if (kms->parser) {
1167-
kms_response_parser_reset(kms->parser);
1168-
}
11691170
return true;
11701171
}
11711172

1173+
bool mongocrypt_kms_ctx_feed_with_retry(mongocrypt_kms_ctx_t *kms, mongocrypt_binary_t *bytes, bool *should_retry) {
1174+
BSON_ASSERT_PARAM(kms);
1175+
BSON_ASSERT_PARAM(bytes);
1176+
BSON_ASSERT_PARAM(should_retry);
1177+
kms->should_retry = false;
1178+
*should_retry = false;
1179+
const bool res = mongocrypt_kms_ctx_feed(kms, bytes);
1180+
*should_retry = kms->should_retry && kms->retry_enabled;
1181+
return res;
1182+
}
1183+
11721184
bool mongocrypt_kms_ctx_feed(mongocrypt_kms_ctx_t *kms, mongocrypt_binary_t *bytes) {
11731185
if (!kms) {
11741186
return false;
@@ -1178,6 +1190,10 @@ bool mongocrypt_kms_ctx_feed(mongocrypt_kms_ctx_t *kms, mongocrypt_binary_t *byt
11781190
if (!mongocrypt_status_ok(status)) {
11791191
return false;
11801192
}
1193+
if (kms->should_retry) {
1194+
CLIENT_ERR("KMS context needs retry. Call mongocrypt_kms_ctx_feed_with_retry instead");
1195+
return false;
1196+
}
11811197

11821198
if (!bytes) {
11831199
CLIENT_ERR("argument 'bytes' is required");

src/mongocrypt.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1182,7 +1182,25 @@ MONGOCRYPT_EXPORT
11821182
bool mongocrypt_kms_ctx_feed(mongocrypt_kms_ctx_t *kms, mongocrypt_binary_t *bytes);
11831183

11841184
/**
1185-
* Indicate a network-level failure.
1185+
* Feed bytes from the KMS response.
1186+
*
1187+
* Feeding more bytes than what has been returned in @ref
1188+
* mongocrypt_kms_ctx_bytes_needed is an error.
1189+
*
1190+
* @param[in] kms The @ref mongocrypt_kms_ctx_t.
1191+
* @param[in] bytes The bytes to feed. The viewed data is copied. It is valid to
1192+
* destroy @p bytes with @ref mongocrypt_binary_destroy immediately after.
1193+
* @param[out] should_retry Whether the KMS request should be retried. Retry in-place
1194+
* without calling @ref mongocrypt_kms_ctx_fail.
1195+
* @returns A boolean indicating success. If false, an error status is set.
1196+
* Retrieve it with @ref mongocrypt_kms_ctx_status
1197+
*/
1198+
MONGOCRYPT_EXPORT
1199+
bool mongocrypt_kms_ctx_feed_with_retry(mongocrypt_kms_ctx_t *kms, mongocrypt_binary_t *bytes, bool *should_retry);
1200+
1201+
/**
1202+
* Indicate a network error. Discards all data fed to this KMS context with @ref mongocrypt_kms_ctx_feed.
1203+
* The @ref mongocrypt_kms_ctx_t may be reused.
11861204
*
11871205
* @param[in] kms The @ref mongocrypt_kms_ctx_t.
11881206
* @return A boolean indicating whether the failed request may be retried.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
HTTP/1.1 200 OK
2+
x-amzn-RequestId: deeb35e5-4ecb-4bf1-9af5-84a54ff0af0e
3+
Content-Type: application/x-amz-json-1.1
4+
Content-Length: 446
5+
Connection: close
6+
7+
{"KeyId": "arn:aws:k

test/test-mongocrypt-datakey.c

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,51 @@ static void _test_create_datakey_with_retry(_mongocrypt_tester_t *tester) {
427427
mongocrypt_destroy(crypt);
428428
}
429429

430+
// Test that an HTTP error is retried in-place.
431+
{
432+
mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);
433+
mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt);
434+
bool should_retry;
435+
ASSERT_OK(
436+
mongocrypt_ctx_setopt_key_encryption_key(ctx,
437+
TEST_BSON("{'provider': 'aws', 'key': 'foo', 'region': 'bar'}")),
438+
ctx);
439+
ASSERT_OK(mongocrypt_ctx_datakey_init(ctx), ctx);
440+
ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_NEED_KMS);
441+
mongocrypt_kms_ctx_t *kms_ctx = mongocrypt_ctx_next_kms_ctx(ctx);
442+
ASSERT_OK(kms_ctx, ctx);
443+
// Expect no sleep is requested before any error.
444+
ASSERT_CMPINT64(mongocrypt_kms_ctx_usleep(kms_ctx), ==, 0);
445+
// Feed a retryable HTTP error.
446+
ASSERT_OK(mongocrypt_kms_ctx_feed_with_retry(kms_ctx,
447+
TEST_FILE("./test/data/rmd/kms-decrypt-reply-429.txt"),
448+
&should_retry),
449+
kms_ctx);
450+
// In-place retry is indicated.
451+
ASSERT(should_retry);
452+
// Feed another retryable HTTP error.
453+
ASSERT_OK(mongocrypt_kms_ctx_feed_with_retry(kms_ctx,
454+
TEST_FILE("./test/data/rmd/kms-decrypt-reply-429.txt"),
455+
&should_retry),
456+
kms_ctx);
457+
// Expect some sleep is requested
458+
ASSERT_CMPINT64(mongocrypt_kms_ctx_usleep(kms_ctx), >=, 0);
459+
// In-place retry is indicated.
460+
ASSERT(should_retry);
461+
ASSERT(kms_ctx->attempts == 2);
462+
463+
// Feed a successful response.
464+
ASSERT_OK(mongocrypt_kms_ctx_feed_with_retry(kms_ctx,
465+
TEST_FILE("./test/data/kms-aws/encrypt-response.txt"),
466+
&should_retry),
467+
kms_ctx);
468+
ASSERT(!should_retry);
469+
ASSERT_OK(mongocrypt_ctx_kms_done(ctx), ctx);
470+
_mongocrypt_tester_run_ctx_to(tester, ctx, MONGOCRYPT_CTX_DONE);
471+
mongocrypt_ctx_destroy(ctx);
472+
mongocrypt_destroy(crypt);
473+
}
474+
430475
// Test that a network error is retried.
431476
{
432477
mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);
@@ -454,6 +499,123 @@ static void _test_create_datakey_with_retry(_mongocrypt_tester_t *tester) {
454499
mongocrypt_destroy(crypt);
455500
}
456501

502+
// Test that a network error is retried in-place.
503+
{
504+
mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);
505+
mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt);
506+
bool should_retry;
507+
ASSERT_OK(
508+
mongocrypt_ctx_setopt_key_encryption_key(ctx,
509+
TEST_BSON("{'provider': 'aws', 'key': 'foo', 'region': 'bar'}")),
510+
ctx);
511+
ASSERT_OK(mongocrypt_ctx_datakey_init(ctx), ctx);
512+
ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_NEED_KMS);
513+
mongocrypt_kms_ctx_t *kms_ctx = mongocrypt_ctx_next_kms_ctx(ctx);
514+
ASSERT_OK(kms_ctx, ctx);
515+
// Expect no sleep is requested before any error.
516+
ASSERT_CMPINT64(mongocrypt_kms_ctx_usleep(kms_ctx), ==, 0);
517+
// Mark a network error.
518+
ASSERT_OK(mongocrypt_kms_ctx_fail(kms_ctx), kms_ctx);
519+
// Feed a partial response
520+
ASSERT_OK(mongocrypt_kms_ctx_feed_with_retry(kms_ctx,
521+
TEST_FILE("./test/data/kms-aws/encrypt-response-partial.txt"),
522+
&should_retry),
523+
kms_ctx);
524+
ASSERT(!should_retry);
525+
// Mark another network error.
526+
ASSERT_OK(mongocrypt_kms_ctx_fail(kms_ctx), kms_ctx);
527+
// Expect some sleep is requested
528+
ASSERT_CMPINT64(mongocrypt_kms_ctx_usleep(kms_ctx), >=, 0);
529+
ASSERT(kms_ctx->attempts == 2);
530+
// Feed a successful response.
531+
ASSERT_OK(mongocrypt_kms_ctx_feed_with_retry(kms_ctx,
532+
TEST_FILE("./test/data/kms-aws/encrypt-response.txt"),
533+
&should_retry),
534+
kms_ctx);
535+
ASSERT(!should_retry);
536+
ASSERT_OK(mongocrypt_ctx_kms_done(ctx), ctx);
537+
_mongocrypt_tester_run_ctx_to(tester, ctx, MONGOCRYPT_CTX_DONE);
538+
mongocrypt_ctx_destroy(ctx);
539+
mongocrypt_destroy(crypt);
540+
}
541+
// Test that subsequent network and HTTP errors can be retried in-place
542+
{
543+
mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);
544+
mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt);
545+
bool should_retry;
546+
ASSERT_OK(
547+
mongocrypt_ctx_setopt_key_encryption_key(ctx,
548+
TEST_BSON("{'provider': 'aws', 'key': 'foo', 'region': 'bar'}")),
549+
ctx);
550+
ASSERT_OK(mongocrypt_ctx_datakey_init(ctx), ctx);
551+
ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_NEED_KMS);
552+
mongocrypt_kms_ctx_t *kms_ctx = mongocrypt_ctx_next_kms_ctx(ctx);
553+
ASSERT_OK(kms_ctx, ctx);
554+
// Expect no sleep is requested before any error.
555+
ASSERT_CMPINT64(mongocrypt_kms_ctx_usleep(kms_ctx), ==, 0);
556+
// Mark a network error.
557+
ASSERT_OK(mongocrypt_kms_ctx_fail(kms_ctx), kms_ctx);
558+
// Feed a retryable HTTP error.
559+
ASSERT_OK(mongocrypt_kms_ctx_feed_with_retry(kms_ctx,
560+
TEST_FILE("./test/data/rmd/kms-decrypt-reply-429.txt"),
561+
&should_retry),
562+
kms_ctx);
563+
// In-place retry is indicated.
564+
ASSERT(should_retry);
565+
// Expect some sleep is requested
566+
ASSERT_CMPINT64(mongocrypt_kms_ctx_usleep(kms_ctx), >=, 0);
567+
ASSERT(kms_ctx->attempts == 2);
568+
// Feed a successful response.
569+
ASSERT_OK(mongocrypt_kms_ctx_feed_with_retry(kms_ctx,
570+
TEST_FILE("./test/data/kms-aws/encrypt-response.txt"),
571+
&should_retry),
572+
kms_ctx);
573+
ASSERT(!should_retry);
574+
ASSERT_OK(mongocrypt_ctx_kms_done(ctx), ctx);
575+
_mongocrypt_tester_run_ctx_to(tester, ctx, MONGOCRYPT_CTX_DONE);
576+
mongocrypt_ctx_destroy(ctx);
577+
mongocrypt_destroy(crypt);
578+
}
579+
580+
// Test that subsequent HTTP and network errors can be retried in-place
581+
{
582+
mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);
583+
mongocrypt_ctx_t *ctx = mongocrypt_ctx_new(crypt);
584+
bool should_retry;
585+
ASSERT_OK(
586+
mongocrypt_ctx_setopt_key_encryption_key(ctx,
587+
TEST_BSON("{'provider': 'aws', 'key': 'foo', 'region': 'bar'}")),
588+
ctx);
589+
ASSERT_OK(mongocrypt_ctx_datakey_init(ctx), ctx);
590+
ASSERT_STATE_EQUAL(mongocrypt_ctx_state(ctx), MONGOCRYPT_CTX_NEED_KMS);
591+
mongocrypt_kms_ctx_t *kms_ctx = mongocrypt_ctx_next_kms_ctx(ctx);
592+
ASSERT_OK(kms_ctx, ctx);
593+
// Expect no sleep is requested before any error.
594+
ASSERT_CMPINT64(mongocrypt_kms_ctx_usleep(kms_ctx), ==, 0);
595+
// Feed a retryable HTTP error.
596+
ASSERT_OK(mongocrypt_kms_ctx_feed_with_retry(kms_ctx,
597+
TEST_FILE("./test/data/rmd/kms-decrypt-reply-429.txt"),
598+
&should_retry),
599+
kms_ctx);
600+
// In-place retry is indicated.
601+
ASSERT(should_retry);
602+
// Mark a network error.
603+
ASSERT_OK(mongocrypt_kms_ctx_fail(kms_ctx), kms_ctx);
604+
// Expect some sleep is requested
605+
ASSERT_CMPINT64(mongocrypt_kms_ctx_usleep(kms_ctx), >=, 0);
606+
ASSERT(kms_ctx->attempts == 2);
607+
// Feed a successful response.
608+
ASSERT_OK(mongocrypt_kms_ctx_feed_with_retry(kms_ctx,
609+
TEST_FILE("./test/data/kms-aws/encrypt-response.txt"),
610+
&should_retry),
611+
kms_ctx);
612+
ASSERT(!should_retry);
613+
ASSERT_OK(mongocrypt_ctx_kms_done(ctx), ctx);
614+
_mongocrypt_tester_run_ctx_to(tester, ctx, MONGOCRYPT_CTX_DONE);
615+
mongocrypt_ctx_destroy(ctx);
616+
mongocrypt_destroy(crypt);
617+
}
618+
457619
// Test that an oauth request is retried for a network error.
458620
{
459621
mongocrypt_t *crypt = _mongocrypt_tester_mongocrypt(TESTER_MONGOCRYPT_DEFAULT);

0 commit comments

Comments
 (0)