|
19 | 19 |
|
20 | 20 | -define(KEY, key(?FUNCTION_NAME)). |
21 | 21 | -define(HOSTS, [{"localhost", 9092}]). |
| 22 | +%% Allowed measurement slack when asserting that at least |
| 23 | +%% max_linger_ms has elapsed (timer precision, scheduling delays). |
| 24 | +-define(LINGER_SLACK_MS, 100). |
22 | 25 | -define(assert_eq_optional_tail(EXPR,EXPECTED), assert_eq_optional_tail(fun() -> EXPR end, EXPECTED)). |
23 | 26 |
|
24 | 27 | %% Kafka v2 batch consists of below fields: |
@@ -791,6 +794,189 @@ replayq_highmem_overflow_test() -> |
791 | 794 | ets:delete(CntrEventsTable), |
792 | 795 | deinstall_event_logging(?FUNCTION_NAME). |
793 | 796 |
|
| 797 | +%% In memory mode, the producer should linger before popping the queue: |
| 798 | +%% messages arriving one-by-one (each handled in its own mailbox message) |
| 799 | +%% are collected into a single produce request which is |
| 800 | +%% sent when max_linger_ms expires since the first message. |
| 801 | +pop_linger_batch_test_() -> |
| 802 | + {timeout, 30, fun pop_linger_batch/0}. |
| 803 | + |
| 804 | +pop_linger_batch() -> |
| 805 | + ClientCfg = client_config(), |
| 806 | + {ok, Client} = start_client(<<"client-1">>, ?HOSTS, ClientCfg), |
| 807 | + LingerMs = 500, |
| 808 | + ProducerCfg = #{partitioner => fun(_, _) -> 0 end, |
| 809 | + required_acks => all_isr, |
| 810 | + max_linger_ms => LingerMs |
| 811 | + }, |
| 812 | + {ok, Producers} = wolff:start_producers(Client, <<"test-topic">>, ProducerCfg), |
| 813 | + TesterPid = self(), |
| 814 | + ok = meck:new(kpro, [no_history, no_link, passthrough]), |
| 815 | + meck:expect(kpro, send, |
| 816 | + fun(Conn, Req) -> |
| 817 | + Payload = iolist_to_binary(lists:last(tuple_to_list(Req))), |
| 818 | + TesterPid ! {sent_to_kafka, Payload}, |
| 819 | + meck:passthrough([Conn, Req]) |
| 820 | + end), |
| 821 | + Values = [<<"pop-linger-a">>, <<"pop-linger-b">>, <<"pop-linger-c">>], |
| 822 | + AckFun = fun(_Partition, _BaseOffset) -> TesterPid ! ack, ok end, |
| 823 | + T0 = erlang:monotonic_time(millisecond), |
| 824 | + lists:foreach( |
| 825 | + fun(V) -> |
| 826 | + _ = wolff:send(Producers, [#{key => <<>>, value => V}], AckFun), |
| 827 | + timer:sleep(50) %% ensure calls are not collected from mailbox at once |
| 828 | + end, Values), |
| 829 | + try |
| 830 | + %% expect one single produce request holding all 3 values, |
| 831 | + %% sent only after the linger time expired |
| 832 | + receive |
| 833 | + {sent_to_kafka, Payload} -> |
| 834 | + Elapsed = erlang:monotonic_time(millisecond) - T0, |
| 835 | + ?assert(Elapsed >= LingerMs - ?LINGER_SLACK_MS, #{elapsed => Elapsed}), |
| 836 | + lists:foreach( |
| 837 | + fun(V) -> ?assertNotEqual(nomatch, binary:match(Payload, V)) end, |
| 838 | + Values) |
| 839 | + after |
| 840 | + 3000 -> error(timeout) |
| 841 | + end, |
| 842 | + %% all 3 calls are acked from the single produce request |
| 843 | + ok = wait_for_acks(length(Values)), |
| 844 | + %% and there is no other produce request |
| 845 | + receive |
| 846 | + {sent_to_kafka, _} = Extra -> error({unexpected, Extra}) |
| 847 | + after |
| 848 | + 200 -> ok |
| 849 | + end, |
| 850 | + %% linger timer is cleared after the pop |
| 851 | + Pid = wolff_producers:lookup_producer(Producers, 0), |
| 852 | + ?assertMatch(#{pop_linger_timer := false}, sys:get_state(Pid)) |
| 853 | + after |
| 854 | + meck:unload(kpro), |
| 855 | + ok = wolff:stop_producers(Producers), |
| 856 | + ok = stop_client(Client) |
| 857 | + end. |
| 858 | + |
| 859 | +%% The pop-linger must not delay sending when the queue already holds |
| 860 | +%% min(max_linger_bytes, max_batch_bytes) worth of bytes. |
| 861 | +pop_linger_full_batch_no_wait_test_() -> |
| 862 | + {timeout, 30, fun pop_linger_full_batch_no_wait/0}. |
| 863 | + |
| 864 | +pop_linger_full_batch_no_wait() -> |
| 865 | + ClientCfg = client_config(), |
| 866 | + {ok, Client} = start_client(<<"client-1">>, ?HOSTS, ClientCfg), |
| 867 | + LingerMs = 5000, |
| 868 | + Msg = #{key => <<>>, value => <<"pop-linger-full-batch">>}, |
| 869 | + MsgBytes = wolff_producer:batch_bytes([Msg]), |
| 870 | + ProducerCfg = #{partitioner => fun(_, _) -> 0 end, |
| 871 | + required_acks => all_isr, |
| 872 | + max_linger_ms => LingerMs, |
| 873 | + %% the 2nd message accumulates enough bytes to flush |
| 874 | + max_linger_bytes => MsgBytes + 1 |
| 875 | + }, |
| 876 | + {ok, Producers} = wolff:start_producers(Client, <<"test-topic">>, ProducerCfg), |
| 877 | + TesterPid = self(), |
| 878 | + ok = meck:new(kpro, [no_history, no_link, passthrough]), |
| 879 | + meck:expect(kpro, send, |
| 880 | + fun(Conn, Req) -> |
| 881 | + Payload = iolist_to_binary(lists:last(tuple_to_list(Req))), |
| 882 | + TesterPid ! {sent_to_kafka, Payload}, |
| 883 | + meck:passthrough([Conn, Req]) |
| 884 | + end), |
| 885 | + AckFun = fun(_Partition, _BaseOffset) -> TesterPid ! ack, ok end, |
| 886 | + T0 = erlang:monotonic_time(millisecond), |
| 887 | + _ = wolff:send(Producers, [Msg], AckFun), |
| 888 | + _ = wolff:send(Producers, [Msg], AckFun), |
| 889 | + try |
| 890 | + %% both messages are flushed in one produce request, |
| 891 | + %% well before the linger time expires |
| 892 | + receive |
| 893 | + {sent_to_kafka, Payload} -> |
| 894 | + Elapsed = erlang:monotonic_time(millisecond) - T0, |
| 895 | + ?assert(Elapsed < LingerMs div 2, #{elapsed => Elapsed}), |
| 896 | + ?assertEqual(2, length(binary:matches(Payload, <<"pop-linger-full-batch">>))) |
| 897 | + after |
| 898 | + 3000 -> error(timeout) |
| 899 | + end, |
| 900 | + ok = wait_for_acks(2) |
| 901 | + after |
| 902 | + meck:unload(kpro), |
| 903 | + ok = wolff:stop_producers(Producers), |
| 904 | + ok = stop_client(Client) |
| 905 | + end. |
| 906 | + |
| 907 | +%% A lone under-sized message is delivered within max_linger_ms, never stalls. |
| 908 | +pop_linger_lone_message_test_() -> |
| 909 | + {timeout, 30, fun pop_linger_lone_message/0}. |
| 910 | + |
| 911 | +pop_linger_lone_message() -> |
| 912 | + ClientCfg = client_config(), |
| 913 | + {ok, Client} = start_client(<<"client-1">>, ?HOSTS, ClientCfg), |
| 914 | + LingerMs = 300, |
| 915 | + ProducerCfg = #{partitioner => fun(_, _) -> 0 end, |
| 916 | + required_acks => all_isr, |
| 917 | + max_linger_ms => LingerMs |
| 918 | + }, |
| 919 | + {ok, Producers} = wolff:start_producers(Client, <<"test-topic">>, ProducerCfg), |
| 920 | + TesterPid = self(), |
| 921 | + AckFun = fun(_Partition, _BaseOffset) -> TesterPid ! ack, ok end, |
| 922 | + T0 = erlang:monotonic_time(millisecond), |
| 923 | + _ = wolff:send(Producers, [#{key => <<>>, value => <<"pop-linger-lone">>}], AckFun), |
| 924 | + try |
| 925 | + ok = wait_for_acks(1), |
| 926 | + Elapsed = erlang:monotonic_time(millisecond) - T0, |
| 927 | + ?assert(Elapsed >= LingerMs - ?LINGER_SLACK_MS, #{elapsed => Elapsed}) |
| 928 | + after |
| 929 | + ok = wolff:stop_producers(Producers), |
| 930 | + ok = stop_client(Client) |
| 931 | + end. |
| 932 | + |
| 933 | +%% When the pop-linger expires while the connection is down, the message |
| 934 | +%% stays queued, and is flushed upon reconnect without lingering another time. |
| 935 | +pop_linger_expire_while_disconnected_test_() -> |
| 936 | + {timeout, 30, fun pop_linger_expire_while_disconnected/0}. |
| 937 | + |
| 938 | +pop_linger_expire_while_disconnected() -> |
| 939 | + ClientCfg = client_config(), |
| 940 | + {ok, Client} = start_client(<<"client-1">>, ?HOSTS, ClientCfg), |
| 941 | + LingerMs = 300, |
| 942 | + ProducerCfg = #{partitioner => fun(_, _) -> 0 end, |
| 943 | + required_acks => all_isr, |
| 944 | + max_linger_ms => LingerMs, |
| 945 | + reconnect_delay_ms => 1000 |
| 946 | + }, |
| 947 | + {ok, Producers} = wolff:start_producers(Client, <<"test-topic">>, ProducerCfg), |
| 948 | + Pid = wolff_producers:lookup_producer(Producers, 0), |
| 949 | + #{conn := Conn} = sys:get_state(Pid), |
| 950 | + ?assert(is_pid(Conn)), |
| 951 | + TesterPid = self(), |
| 952 | + AckFun = fun(_Partition, _BaseOffset) -> TesterPid ! ack, ok end, |
| 953 | + %% an under-sized message starts the pop-linger timer |
| 954 | + _ = wolff:send(Producers, [#{key => <<>>, value => <<"pop-linger-disc">>}], AckFun), |
| 955 | + exit(Conn, kill), |
| 956 | + %% wait for the linger to expire while disconnected |
| 957 | + timer:sleep(LingerMs * 2), |
| 958 | + try |
| 959 | + %% linger expired, but still disconnected: the message is still queued |
| 960 | + #{conn := Conn2, replayq := Q} = sys:get_state(Pid), |
| 961 | + ?assertNot(is_pid(Conn2)), |
| 962 | + ?assertEqual(1, replayq:count(Q)), |
| 963 | + %% flushed upon reconnect |
| 964 | + ok = wait_for_acks(1), |
| 965 | + ?assertMatch(#{pop_linger_timer := false}, sys:get_state(Pid)) |
| 966 | + after |
| 967 | + ok = wolff:stop_producers(Producers), |
| 968 | + ok = stop_client(Client) |
| 969 | + end. |
| 970 | + |
| 971 | +wait_for_acks(0) -> |
| 972 | + ok; |
| 973 | +wait_for_acks(N) -> |
| 974 | + receive |
| 975 | + ack -> wait_for_acks(N - 1) |
| 976 | + after |
| 977 | + 5000 -> error(timeout) |
| 978 | + end. |
| 979 | + |
794 | 980 | mem_only_replayq_test() -> |
795 | 981 | CntrEventsTable = ets:new(cntr_events, [public]), |
796 | 982 | install_event_logging(?FUNCTION_NAME, CntrEventsTable, false), |
|
0 commit comments