|
50 | 50 | compression | |
51 | 51 | drop_if_highmem | |
52 | 52 | max_batch_age | |
| 53 | + max_retry | |
53 | 54 | telemetry_meta_data | |
54 | 55 | max_partitions. |
55 | 56 |
|
|
67 | 68 | compression => kpro:compress_option(), |
68 | 69 | drop_if_highmem => boolean(), |
69 | 70 | max_batch_age => timeout(), |
| 71 | + max_retry => infinity | non_neg_integer(), |
70 | 72 | telemetry_meta_data => map(), |
71 | 73 | max_partitions => pos_integer() |
72 | 74 | }. |
|
84 | 86 | compression => kpro:compress_option(), |
85 | 87 | drop_if_highmem => boolean(), |
86 | 88 | max_batch_age => timeout(), |
| 89 | + max_retry => infinity | non_neg_integer(), |
87 | 90 | telemetry_meta_data => map(), |
88 | 91 | max_partitions => pos_integer() |
89 | 92 | }. |
|
170 | 173 | %% on reconnect). Each dropped message has its ack callback evaluated with |
171 | 174 | %% reason `message_expired' and bumps the `dropped' and `dropped_expired' |
172 | 175 | %% counters. Set to `infinity' to disable (never drop by age). |
| 176 | +%% * `max_retry': default `infinity'. Maximum number of times a batch is retried |
| 177 | +%% after a Kafka error response (e.g. `not_leader_for_partition') before it is |
| 178 | +%% dropped. A batch is dropped once its attempt counter reaches |
| 179 | +%% `max_retry + 1' (i.e. the initial send plus `max_retry' retries have all |
| 180 | +%% failed). Each dropped message has its ack callback evaluated with reason |
| 181 | +%% `max_retry_exceeded' and bumps the `dropped' counter. `max_retry = 0' drops |
| 182 | +%% on the first error; `infinity' (the default) retries forever. Note this |
| 183 | +%% counts Kafka error responses only; resends triggered purely by connection |
| 184 | +%% loss are bounded by `max_batch_age', not by `max_retry'. |
173 | 185 | -spec start_link(wolff:client_id(), topic(), partition(), pid() | ?conn_down(any()), config_in()) -> |
174 | 186 | {ok, pid()} | {error, any()}. |
175 | 187 | start_link(ClientId, Topic, Partition, MaybeConnPid, Config) -> |
@@ -437,6 +449,7 @@ use_defaults(Config) -> |
437 | 449 | {max_send_ahead, 0}, |
438 | 450 | {compression, no_compression}, |
439 | 451 | {max_batch_age, infinity}, |
| 452 | + {max_retry, infinity}, |
440 | 453 | {reconnect_delay_ms, 2000} |
441 | 454 | ]). |
442 | 455 |
|
@@ -787,20 +800,45 @@ do_handle_kafka_ack(EC, _BaseOffset, St) when EC =:= ?message_too_large orelse E |
787 | 800 | end; |
788 | 801 | do_handle_kafka_ack(ErrorCode, _BaseOffset, St) -> |
789 | 802 | %% Other errors, such as not_leader_for_partition |
790 | | - #{sent_reqs := SentReqs} = St, |
791 | | - {value, #{attempts := Attempts, q_items := Items}} = queue:peek(SentReqs), |
792 | | - #{topic := Topic, |
| 803 | + #{sent_reqs := SentReqs, |
| 804 | + topic := Topic, |
793 | 805 | partition := Partition, |
794 | 806 | config := Config |
795 | 807 | } = St, |
| 808 | + {value, #{attempts := Attempts, q_items := Items}} = queue:peek(SentReqs), |
796 | 809 | NrOfCalls = count_calls(Items), |
797 | | - inc_sent_failed(Config, NrOfCalls, Attempts), |
798 | | - log_warn(Topic, Partition, "error_in_produce_response", |
799 | | - #{error_code => ErrorCode, |
800 | | - batch_size => count_msgs(Items), |
801 | | - attempts => Attempts}), |
802 | | - St1 = increment_attempt(St), |
803 | | - erlang:throw({kafka_error, ErrorCode, St1}). |
| 810 | + MaxRetry = maps:get(max_retry, Config, infinity), |
| 811 | + case is_max_retry_reached(Attempts, MaxRetry) of |
| 812 | + true -> |
| 813 | + %% Give up: drop the front batch instead of retrying it again. |
| 814 | + log_error(Topic, Partition, "dropped_produce_request_reached_max_retry", |
| 815 | + #{error_code => ErrorCode, |
| 816 | + batch_size => count_msgs(Items), |
| 817 | + attempts => Attempts, |
| 818 | + max_retry => MaxRetry}), |
| 819 | + wolff_metrics:dropped_inc(Config, NrOfCalls), |
| 820 | + %% clear_sent_and_ack_callers/3 pops the front request, bumps the |
| 821 | + %% failed/retried_failed counter, and acks the callers with the reason. |
| 822 | + St1 = clear_sent_and_ack_callers(ErrorCode, ?max_retry_exceeded, St), |
| 823 | + %% Still reconnect: the error (e.g. not_leader_for_partition) means the |
| 824 | + %% current connection is stale for the remaining in-flight requests. |
| 825 | + erlang:throw({kafka_error, ErrorCode, St1}); |
| 826 | + false -> |
| 827 | + inc_sent_failed(Config, NrOfCalls, Attempts), |
| 828 | + log_warn(Topic, Partition, "error_in_produce_response", |
| 829 | + #{error_code => ErrorCode, |
| 830 | + batch_size => count_msgs(Items), |
| 831 | + attempts => Attempts}), |
| 832 | + St1 = increment_attempt(St), |
| 833 | + erlang:throw({kafka_error, ErrorCode, St1}) |
| 834 | + end. |
| 835 | + |
| 836 | +%% A batch is dropped once it has been attempted `max_retry + 1' times, i.e. the |
| 837 | +%% initial send plus `max_retry' retries have all failed. |
| 838 | +is_max_retry_reached(_Attempts, infinity) -> |
| 839 | + false; |
| 840 | +is_max_retry_reached(Attempts, MaxRetry) -> |
| 841 | + Attempts >= MaxRetry + 1. |
804 | 842 |
|
805 | 843 | %% Since we only handle Kafka ack for the first sent request in the sent queue |
806 | 844 | %% we only bump the first itme with attempt counter |
@@ -1329,6 +1367,15 @@ is_batch_expired_test_() -> |
1329 | 1367 | ?_assert(is_batch_expired(Sent([Item(50)]), 50, Now))} |
1330 | 1368 | ]. |
1331 | 1369 |
|
| 1370 | +is_max_retry_reached_test_() -> |
| 1371 | + [ {"infinity never reached", ?_assertNot(is_max_retry_reached(1000000, infinity))} |
| 1372 | + %% max_retry = 0 -> drop on the first attempt's failure |
| 1373 | + , {"0: first attempt reached", ?_assert(is_max_retry_reached(1, 0))} |
| 1374 | + %% max_retry = 3 -> allow attempts 1..4, drop when attempts reaches 4 |
| 1375 | + , {"3: attempt 3 not reached", ?_assertNot(is_max_retry_reached(3, 3))} |
| 1376 | + , {"3: attempt 4 reached", ?_assert(is_max_retry_reached(4, 3))} |
| 1377 | + ]. |
| 1378 | + |
1332 | 1379 | maybe_log_discard_test_() -> |
1333 | 1380 | [ {"no-increment", fun() -> maybe_log_discard(undefined, 0, ?DISCARD_OVERFLOW) end} |
1334 | 1381 | , {"fake-last-old", |
|
0 commit comments