Skip to content

Commit 1d8c0e5

Browse files
authored
Merge pull request #424 from hairyhum/gen_consumer_callback_stop
Allow GenConsumer callbacks to return :stop replies
2 parents 301c483 + 6523b1f commit 1d8c0e5

File tree

2 files changed

+163
-49
lines changed

2 files changed

+163
-49
lines changed

lib/kafka_ex/gen_consumer.ex

Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ defmodule KafkaEx.GenConsumer do
232232
"""
233233
@callback init(topic :: binary, partition :: non_neg_integer) ::
234234
{:ok, state :: term}
235+
| {:stop, reason :: term}
235236

236237
@doc """
237238
Invoked when the server is started. `start_link/5` will block until it
@@ -255,7 +256,7 @@ defmodule KafkaEx.GenConsumer do
255256
topic :: binary,
256257
partition :: non_neg_integer,
257258
extra_args :: map()
258-
) :: {:ok, state :: term}
259+
) :: {:ok, state :: term} | {:stop, reason :: term}
259260

260261
@doc """
261262
Invoked for each message set consumed from a Kafka topic partition.
@@ -287,6 +288,8 @@ defmodule KafkaEx.GenConsumer do
287288
"""
288289
@callback handle_call(call :: term, from :: GenServer.from(), state :: term) ::
289290
{:reply, reply_value :: term, new_state :: term}
291+
| {:stop, reason :: term, reply_value :: term, new_state :: term}
292+
| {:stop, reason :: term, new_state :: term}
290293

291294
@doc """
292295
Invoked by `KafkaEx.GenConsumer.cast/2`.
@@ -296,6 +299,7 @@ defmodule KafkaEx.GenConsumer do
296299
"""
297300
@callback handle_cast(cast :: term, state :: term) ::
298301
{:noreply, new_state :: term}
302+
| {:stop, reason :: term, new_state :: term}
299303

300304
@doc """
301305
Invoked by sending messages to the consumer.
@@ -305,6 +309,7 @@ defmodule KafkaEx.GenConsumer do
305309
"""
306310
@callback handle_info(info :: term, state :: term) ::
307311
{:noreply, new_state :: term}
312+
| {:stop, reason :: term, new_state :: term}
308313

309314
defmacro __using__(_opts) do
310315
quote do
@@ -541,44 +546,49 @@ defmodule KafkaEx.GenConsumer do
541546
api_versions = Keyword.get(opts, :api_versions, %{})
542547
api_versions = Map.merge(default_api_versions, api_versions)
543548

544-
{:ok, consumer_state} =
545-
consumer_module.init(topic, partition, extra_consumer_args)
549+
case consumer_module.init(topic, partition, extra_consumer_args) do
550+
{:ok, consumer_state} ->
551+
worker_opts = Keyword.take(opts, [:uris, :use_ssl, :ssl_options])
546552

547-
worker_opts = Keyword.take(opts, [:uris, :use_ssl, :ssl_options])
553+
{:ok, worker_name} =
554+
KafkaEx.create_worker(
555+
:no_name,
556+
[consumer_group: group_name] ++ worker_opts
557+
)
548558

549-
{:ok, worker_name} =
550-
KafkaEx.create_worker(
551-
:no_name,
552-
[consumer_group: group_name] ++ worker_opts
553-
)
559+
default_fetch_options = [
560+
auto_commit: false,
561+
worker_name: worker_name
562+
]
554563

555-
default_fetch_options = [
556-
auto_commit: false,
557-
worker_name: worker_name
558-
]
564+
given_fetch_options = Keyword.get(opts, :fetch_options, [])
559565

560-
given_fetch_options = Keyword.get(opts, :fetch_options, [])
561-
fetch_options = Keyword.merge(default_fetch_options, given_fetch_options)
562-
563-
state = %State{
564-
consumer_module: consumer_module,
565-
consumer_state: consumer_state,
566-
commit_interval: commit_interval,
567-
commit_threshold: commit_threshold,
568-
auto_offset_reset: auto_offset_reset,
569-
worker_name: worker_name,
570-
group: group_name,
571-
topic: topic,
572-
partition: partition,
573-
generation_id: generation_id,
574-
member_id: member_id,
575-
fetch_options: fetch_options,
576-
api_versions: api_versions
577-
}
566+
fetch_options =
567+
Keyword.merge(default_fetch_options, given_fetch_options)
568+
569+
state = %State{
570+
consumer_module: consumer_module,
571+
consumer_state: consumer_state,
572+
commit_interval: commit_interval,
573+
commit_threshold: commit_threshold,
574+
auto_offset_reset: auto_offset_reset,
575+
worker_name: worker_name,
576+
group: group_name,
577+
topic: topic,
578+
partition: partition,
579+
generation_id: generation_id,
580+
member_id: member_id,
581+
fetch_options: fetch_options,
582+
api_versions: api_versions
583+
}
578584

579-
Process.flag(:trap_exit, true)
585+
Process.flag(:trap_exit, true)
580586

581-
{:ok, state, 0}
587+
{:ok, state, 0}
588+
589+
{:stop, reason} ->
590+
{:stop, reason}
591+
end
582592
end
583593

584594
def handle_call(:partition, _from, state) do
@@ -597,14 +607,23 @@ defmodule KafkaEx.GenConsumer do
597607
# which we turn into a timeout = 0 clause so that we continue to consume.
598608
# any other GenServer flow control could have unintended consequences,
599609
# so we leave that for later consideration
600-
{:reply, reply, new_consumer_state} =
610+
consumer_reply =
601611
consumer_module.handle_call(
602612
message,
603613
from,
604614
consumer_state
605615
)
606616

607-
{:reply, reply, %{state | consumer_state: new_consumer_state}, 0}
617+
case consumer_reply do
618+
{:reply, reply, new_consumer_state} ->
619+
{:reply, reply, %{state | consumer_state: new_consumer_state}, 0}
620+
621+
{:stop, reason, new_consumer_state} ->
622+
{:stop, reason, %{state | consumer_state: new_consumer_state}}
623+
624+
{:stop, reason, reply, new_consumer_state} ->
625+
{:stop, reason, reply, %{state | consumer_state: new_consumer_state}}
626+
end
608627
end
609628

610629
def handle_cast(
@@ -618,13 +637,19 @@ defmodule KafkaEx.GenConsumer do
618637
# which we turn into a timeout = 0 clause so that we continue to consume.
619638
# any other GenServer flow control could have unintended consequences,
620639
# so we leave that for later consideration
621-
{:noreply, new_consumer_state} =
640+
consumer_reply =
622641
consumer_module.handle_cast(
623642
message,
624643
consumer_state
625644
)
626645

627-
{:noreply, %{state | consumer_state: new_consumer_state}, 0}
646+
case consumer_reply do
647+
{:noreply, new_consumer_state} ->
648+
{:noreply, %{state | consumer_state: new_consumer_state}, 0}
649+
650+
{:stop, reason, new_consumer_state} ->
651+
{:stop, reason, %{state | consumer_state: new_consumer_state}}
652+
end
628653
end
629654

630655
def handle_info(
@@ -660,13 +685,19 @@ defmodule KafkaEx.GenConsumer do
660685
# which we turn into a timeout = 0 clause so that we continue to consume.
661686
# any other GenServer flow control could have unintended consequences,
662687
# so we leave that for later consideration
663-
{:noreply, new_consumer_state} =
688+
consumer_reply =
664689
consumer_module.handle_info(
665690
message,
666691
consumer_state
667692
)
668693

669-
{:noreply, %{state | consumer_state: new_consumer_state}, 0}
694+
case consumer_reply do
695+
{:noreply, new_consumer_state} ->
696+
{:noreply, %{state | consumer_state: new_consumer_state}, 0}
697+
698+
{:stop, reason, new_consumer_state} ->
699+
{:stop, reason, %{state | consumer_state: new_consumer_state}}
700+
end
670701
end
671702

672703
def terminate(_reason, %State{} = state) do
@@ -689,7 +720,8 @@ defmodule KafkaEx.GenConsumer do
689720
KafkaEx.fetch(
690721
topic,
691722
partition,
692-
Keyword.merge(fetch_options,
723+
Keyword.merge(
724+
fetch_options,
693725
offset: offset,
694726
api_version: Map.fetch!(state.api_versions, :fetch)
695727
)
@@ -850,9 +882,12 @@ defmodule KafkaEx.GenConsumer do
850882
# one of these needs to match, depending on which client
851883
case partition_response do
852884
# old client
853-
^partition -> :ok
885+
^partition ->
886+
:ok
887+
854888
# new client
855-
%{error_code: :no_error, partition: ^partition} -> :ok
889+
%{error_code: :no_error, partition: ^partition} ->
890+
:ok
856891
end
857892

858893
Logger.debug(fn ->

test/integration/consumer_group_implementation_test.exs

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule KafkaEx.ConsumerGroupImplementationTest do
2-
use ExUnit.Case
2+
use ExUnit.Case, async: false
33

44
alias KafkaEx.ConsumerGroup
55
alias KafkaEx.GenConsumer
@@ -75,14 +75,30 @@ defmodule KafkaEx.ConsumerGroupImplementationTest do
7575
{:reply, Map.get(state, key), state}
7676
end
7777

78+
def handle_call({:stop, msg}, _from, state) do
79+
{:stop, :test_stop, msg, state}
80+
end
81+
82+
def handle_call(:stop, _from, state) do
83+
{:stop, :test_stop, state}
84+
end
85+
7886
def handle_cast({:set, key, value}, state) do
7987
{:noreply, Map.put_new(state, key, value)}
8088
end
8189

90+
def handle_cast(:stop, state) do
91+
{:stop, :test_stop, state}
92+
end
93+
8294
def handle_info({:set, key, value}, state) do
8395
{:noreply, Map.put_new(state, key, value)}
8496
end
8597

98+
def handle_info(:stop, state) do
99+
{:stop, :test_stop, state}
100+
end
101+
86102
def handle_message_set(message_set, state) do
87103
Logger.debug(fn ->
88104
"Consumer #{inspect(self())} handled message set #{inspect(message_set)}"
@@ -130,14 +146,14 @@ defmodule KafkaEx.ConsumerGroupImplementationTest do
130146
|> length
131147
end
132148

133-
setup do
149+
setup context do
134150
ports_before = num_open_ports()
135151
{:ok, test_partitioner_pid} = TestPartitioner.start_link()
136152

137153
{:ok, consumer_group_pid1} =
138154
ConsumerGroup.start_link(
139155
TestConsumer,
140-
@consumer_group_name,
156+
consumer_group_name(context),
141157
[@topic_name],
142158
heartbeat_interval: 100,
143159
partition_assignment_callback: &TestPartitioner.assign_partitions/2,
@@ -147,7 +163,7 @@ defmodule KafkaEx.ConsumerGroupImplementationTest do
147163
{:ok, consumer_group_pid2} =
148164
ConsumerGroup.start_link(
149165
TestConsumer,
150-
@consumer_group_name,
166+
consumer_group_name(context),
151167
[@topic_name],
152168
heartbeat_interval: 100,
153169
partition_assignment_callback: &TestPartitioner.assign_partitions/2,
@@ -183,7 +199,9 @@ defmodule KafkaEx.ConsumerGroupImplementationTest do
183199
generation_id2 = ConsumerGroup.generation_id(context[:consumer_group_pid2])
184200
assert generation_id1 == generation_id2
185201

186-
assert @consumer_group_name ==
202+
consumer_group_name = consumer_group_name(context)
203+
204+
assert consumer_group_name ==
187205
ConsumerGroup.group_name(context[:consumer_group_pid1])
188206

189207
member1 = ConsumerGroup.member_id(context[:consumer_group_pid1])
@@ -289,7 +307,11 @@ defmodule KafkaEx.ConsumerGroupImplementationTest do
289307
for px <- partition_range do
290308
wait_for(fn ->
291309
ending_offset =
292-
latest_consumer_offset_number(@topic_name, px, @consumer_group_name)
310+
latest_consumer_offset_number(
311+
@topic_name,
312+
px,
313+
consumer_group_name(context)
314+
)
293315

294316
last_offset = Map.get(last_offsets, px)
295317
ending_offset == last_offset + 1
@@ -318,7 +340,7 @@ defmodule KafkaEx.ConsumerGroupImplementationTest do
318340
{:ok, consumer_group_pid3} =
319341
ConsumerGroup.start_link(
320342
TestConsumer,
321-
@consumer_group_name,
343+
consumer_group_name(context),
322344
[@topic_name],
323345
heartbeat_interval: 100,
324346
partition_assignment_callback: &TestPartitioner.assign_partitions/2
@@ -374,4 +396,61 @@ defmodule KafkaEx.ConsumerGroupImplementationTest do
374396
assert :value == TestConsumer.get(consumer_pid, :test_info)
375397
end
376398
end
399+
400+
test "handle call stop returns from callbacks", context do
401+
consumer_group_pid =
402+
ConsumerGroup.consumer_supervisor_pid(context[:consumer_group_pid1])
403+
404+
[c1, c2] = GenConsumer.Supervisor.child_pids(consumer_group_pid)
405+
assert :foo = GenConsumer.call(c1, {:stop, :foo})
406+
407+
try do
408+
GenConsumer.call(c2, :stop)
409+
catch
410+
_, err ->
411+
assert {:test_stop, _} = err
412+
end
413+
414+
assert nil == Process.info(c1)
415+
assert nil == Process.info(c2)
416+
end
417+
418+
test "handle cast stop returns from callbacks", context do
419+
consumer_group_pid =
420+
ConsumerGroup.consumer_supervisor_pid(context[:consumer_group_pid1])
421+
422+
[c1, _c2] = GenConsumer.Supervisor.child_pids(consumer_group_pid)
423+
GenConsumer.cast(c1, :stop)
424+
425+
try do
426+
:sys.get_state(c1)
427+
catch
428+
_, err ->
429+
assert {:test_stop, _} = err
430+
end
431+
432+
assert nil == Process.info(c1)
433+
end
434+
435+
test "handle info stop returns from callbacks", context do
436+
consumer_group_pid =
437+
ConsumerGroup.consumer_supervisor_pid(context[:consumer_group_pid1])
438+
439+
[c1, _c2] = GenConsumer.Supervisor.child_pids(consumer_group_pid)
440+
send(c1, :stop)
441+
442+
try do
443+
:sys.get_state(c1)
444+
catch
445+
_, err ->
446+
assert {:test_stop, _} = err
447+
end
448+
449+
assert nil == Process.info(c1)
450+
end
451+
452+
def consumer_group_name(context) do
453+
test_name = context[:test] |> to_string() |> String.replace(" ", "_")
454+
@consumer_group_name <> test_name
455+
end
377456
end

0 commit comments

Comments
 (0)