|
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: |
@@ -638,6 +641,189 @@ replayq_highmem_overflow_test() -> |
638 | 641 | ets:delete(CntrEventsTable), |
639 | 642 | deinstall_event_logging(?FUNCTION_NAME). |
640 | 643 |
|
| 644 | +%% In memory mode, the producer should linger before popping the queue: |
| 645 | +%% messages arriving one-by-one (each handled in its own mailbox message) |
| 646 | +%% are collected into a single produce request which is |
| 647 | +%% sent when max_linger_ms expires since the first message. |
| 648 | +pop_linger_batch_test_() -> |
| 649 | + {timeout, 30, fun pop_linger_batch/0}. |
| 650 | + |
| 651 | +pop_linger_batch() -> |
| 652 | + ClientCfg = client_config(), |
| 653 | + {ok, Client} = start_client(<<"client-1">>, ?HOSTS, ClientCfg), |
| 654 | + LingerMs = 500, |
| 655 | + ProducerCfg = #{partitioner => fun(_, _) -> 0 end, |
| 656 | + required_acks => all_isr, |
| 657 | + max_linger_ms => LingerMs |
| 658 | + }, |
| 659 | + {ok, Producers} = wolff:start_producers(Client, <<"test-topic">>, ProducerCfg), |
| 660 | + TesterPid = self(), |
| 661 | + ok = meck:new(kpro, [no_history, no_link, passthrough]), |
| 662 | + meck:expect(kpro, send, |
| 663 | + fun(Conn, Req) -> |
| 664 | + Payload = iolist_to_binary(lists:last(tuple_to_list(Req))), |
| 665 | + TesterPid ! {sent_to_kafka, Payload}, |
| 666 | + meck:passthrough([Conn, Req]) |
| 667 | + end), |
| 668 | + Values = [<<"pop-linger-a">>, <<"pop-linger-b">>, <<"pop-linger-c">>], |
| 669 | + AckFun = fun(_Partition, _BaseOffset) -> TesterPid ! ack, ok end, |
| 670 | + T0 = erlang:monotonic_time(millisecond), |
| 671 | + lists:foreach( |
| 672 | + fun(V) -> |
| 673 | + _ = wolff:send(Producers, [#{key => <<>>, value => V}], AckFun), |
| 674 | + timer:sleep(50) %% ensure calls are not collected from mailbox at once |
| 675 | + end, Values), |
| 676 | + try |
| 677 | + %% expect one single produce request holding all 3 values, |
| 678 | + %% sent only after the linger time expired |
| 679 | + receive |
| 680 | + {sent_to_kafka, Payload} -> |
| 681 | + Elapsed = erlang:monotonic_time(millisecond) - T0, |
| 682 | + ?assert(Elapsed >= LingerMs - ?LINGER_SLACK_MS, #{elapsed => Elapsed}), |
| 683 | + lists:foreach( |
| 684 | + fun(V) -> ?assertNotEqual(nomatch, binary:match(Payload, V)) end, |
| 685 | + Values) |
| 686 | + after |
| 687 | + 3000 -> error(timeout) |
| 688 | + end, |
| 689 | + %% all 3 calls are acked from the single produce request |
| 690 | + ok = wait_for_acks(length(Values)), |
| 691 | + %% and there is no other produce request |
| 692 | + receive |
| 693 | + {sent_to_kafka, _} = Extra -> error({unexpected, Extra}) |
| 694 | + after |
| 695 | + 200 -> ok |
| 696 | + end, |
| 697 | + %% linger timer is cleared after the pop |
| 698 | + Pid = wolff_producers:lookup_producer(Producers, 0), |
| 699 | + ?assertMatch(#{pop_linger_timer := false}, sys:get_state(Pid)) |
| 700 | + after |
| 701 | + meck:unload(kpro), |
| 702 | + ok = wolff:stop_producers(Producers), |
| 703 | + ok = stop_client(Client) |
| 704 | + end. |
| 705 | + |
| 706 | +%% The pop-linger must not delay sending when the queue already holds |
| 707 | +%% min(max_linger_bytes, max_batch_bytes) worth of bytes. |
| 708 | +pop_linger_full_batch_no_wait_test_() -> |
| 709 | + {timeout, 30, fun pop_linger_full_batch_no_wait/0}. |
| 710 | + |
| 711 | +pop_linger_full_batch_no_wait() -> |
| 712 | + ClientCfg = client_config(), |
| 713 | + {ok, Client} = start_client(<<"client-1">>, ?HOSTS, ClientCfg), |
| 714 | + LingerMs = 5000, |
| 715 | + Msg = #{key => <<>>, value => <<"pop-linger-full-batch">>}, |
| 716 | + MsgBytes = wolff_producer:batch_bytes([Msg]), |
| 717 | + ProducerCfg = #{partitioner => fun(_, _) -> 0 end, |
| 718 | + required_acks => all_isr, |
| 719 | + max_linger_ms => LingerMs, |
| 720 | + %% the 2nd message accumulates enough bytes to flush |
| 721 | + max_linger_bytes => MsgBytes + 1 |
| 722 | + }, |
| 723 | + {ok, Producers} = wolff:start_producers(Client, <<"test-topic">>, ProducerCfg), |
| 724 | + TesterPid = self(), |
| 725 | + ok = meck:new(kpro, [no_history, no_link, passthrough]), |
| 726 | + meck:expect(kpro, send, |
| 727 | + fun(Conn, Req) -> |
| 728 | + Payload = iolist_to_binary(lists:last(tuple_to_list(Req))), |
| 729 | + TesterPid ! {sent_to_kafka, Payload}, |
| 730 | + meck:passthrough([Conn, Req]) |
| 731 | + end), |
| 732 | + AckFun = fun(_Partition, _BaseOffset) -> TesterPid ! ack, ok end, |
| 733 | + T0 = erlang:monotonic_time(millisecond), |
| 734 | + _ = wolff:send(Producers, [Msg], AckFun), |
| 735 | + _ = wolff:send(Producers, [Msg], AckFun), |
| 736 | + try |
| 737 | + %% both messages are flushed in one produce request, |
| 738 | + %% well before the linger time expires |
| 739 | + receive |
| 740 | + {sent_to_kafka, Payload} -> |
| 741 | + Elapsed = erlang:monotonic_time(millisecond) - T0, |
| 742 | + ?assert(Elapsed < LingerMs div 2, #{elapsed => Elapsed}), |
| 743 | + ?assertEqual(2, length(binary:matches(Payload, <<"pop-linger-full-batch">>))) |
| 744 | + after |
| 745 | + 3000 -> error(timeout) |
| 746 | + end, |
| 747 | + ok = wait_for_acks(2) |
| 748 | + after |
| 749 | + meck:unload(kpro), |
| 750 | + ok = wolff:stop_producers(Producers), |
| 751 | + ok = stop_client(Client) |
| 752 | + end. |
| 753 | + |
| 754 | +%% A lone under-sized message is delivered within max_linger_ms, never stalls. |
| 755 | +pop_linger_lone_message_test_() -> |
| 756 | + {timeout, 30, fun pop_linger_lone_message/0}. |
| 757 | + |
| 758 | +pop_linger_lone_message() -> |
| 759 | + ClientCfg = client_config(), |
| 760 | + {ok, Client} = start_client(<<"client-1">>, ?HOSTS, ClientCfg), |
| 761 | + LingerMs = 300, |
| 762 | + ProducerCfg = #{partitioner => fun(_, _) -> 0 end, |
| 763 | + required_acks => all_isr, |
| 764 | + max_linger_ms => LingerMs |
| 765 | + }, |
| 766 | + {ok, Producers} = wolff:start_producers(Client, <<"test-topic">>, ProducerCfg), |
| 767 | + TesterPid = self(), |
| 768 | + AckFun = fun(_Partition, _BaseOffset) -> TesterPid ! ack, ok end, |
| 769 | + T0 = erlang:monotonic_time(millisecond), |
| 770 | + _ = wolff:send(Producers, [#{key => <<>>, value => <<"pop-linger-lone">>}], AckFun), |
| 771 | + try |
| 772 | + ok = wait_for_acks(1), |
| 773 | + Elapsed = erlang:monotonic_time(millisecond) - T0, |
| 774 | + ?assert(Elapsed >= LingerMs - ?LINGER_SLACK_MS, #{elapsed => Elapsed}) |
| 775 | + after |
| 776 | + ok = wolff:stop_producers(Producers), |
| 777 | + ok = stop_client(Client) |
| 778 | + end. |
| 779 | + |
| 780 | +%% When the pop-linger expires while the connection is down, the message |
| 781 | +%% stays queued, and is flushed upon reconnect without lingering another time. |
| 782 | +pop_linger_expire_while_disconnected_test_() -> |
| 783 | + {timeout, 30, fun pop_linger_expire_while_disconnected/0}. |
| 784 | + |
| 785 | +pop_linger_expire_while_disconnected() -> |
| 786 | + ClientCfg = client_config(), |
| 787 | + {ok, Client} = start_client(<<"client-1">>, ?HOSTS, ClientCfg), |
| 788 | + LingerMs = 300, |
| 789 | + ProducerCfg = #{partitioner => fun(_, _) -> 0 end, |
| 790 | + required_acks => all_isr, |
| 791 | + max_linger_ms => LingerMs, |
| 792 | + reconnect_delay_ms => 1000 |
| 793 | + }, |
| 794 | + {ok, Producers} = wolff:start_producers(Client, <<"test-topic">>, ProducerCfg), |
| 795 | + Pid = wolff_producers:lookup_producer(Producers, 0), |
| 796 | + #{conn := Conn} = sys:get_state(Pid), |
| 797 | + ?assert(is_pid(Conn)), |
| 798 | + TesterPid = self(), |
| 799 | + AckFun = fun(_Partition, _BaseOffset) -> TesterPid ! ack, ok end, |
| 800 | + %% an under-sized message starts the pop-linger timer |
| 801 | + _ = wolff:send(Producers, [#{key => <<>>, value => <<"pop-linger-disc">>}], AckFun), |
| 802 | + exit(Conn, kill), |
| 803 | + %% wait for the linger to expire while disconnected |
| 804 | + timer:sleep(LingerMs * 2), |
| 805 | + try |
| 806 | + %% linger expired, but still disconnected: the message is still queued |
| 807 | + #{conn := Conn2, replayq := Q} = sys:get_state(Pid), |
| 808 | + ?assertNot(is_pid(Conn2)), |
| 809 | + ?assertEqual(1, replayq:count(Q)), |
| 810 | + %% flushed upon reconnect |
| 811 | + ok = wait_for_acks(1), |
| 812 | + ?assertMatch(#{pop_linger_timer := false}, sys:get_state(Pid)) |
| 813 | + after |
| 814 | + ok = wolff:stop_producers(Producers), |
| 815 | + ok = stop_client(Client) |
| 816 | + end. |
| 817 | + |
| 818 | +wait_for_acks(0) -> |
| 819 | + ok; |
| 820 | +wait_for_acks(N) -> |
| 821 | + receive |
| 822 | + ack -> wait_for_acks(N - 1) |
| 823 | + after |
| 824 | + 5000 -> error(timeout) |
| 825 | + end. |
| 826 | + |
641 | 827 | mem_only_replayq_test() -> |
642 | 828 | CntrEventsTable = ets:new(cntr_events, [public]), |
643 | 829 | install_event_logging(?FUNCTION_NAME, CntrEventsTable, false), |
|
0 commit comments