Skip to content

Commit 156e37d

Browse files
authored
Consumer decorator protocol (#19)
* Consumer decorator protocol
1 parent 862c79f commit 156e37d

File tree

8 files changed

+110
-56
lines changed

8 files changed

+110
-56
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [[2.0.0]] (https://github.com/AppsFlyer/ketu/pull/19) - 2024-09-03
9+
### Changed
10+
- consumer decorator API breaking change - use ConsumerDecorator protocol instead of `consumer-decorator` function.
11+
812
## [[1.1.0]](https://github.com/AppsFlyer/ketu/pull/18) - 2024-07-29
913

1014
### Added

README.md

+17-13
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
A Clojure Apache Kafka client with core.async api
99

1010
```clojure
11-
[com.appsflyer/ketu "1.1.0"]
11+
[com.appsflyer/ketu "2.0.0"]
1212
```
1313

1414
## Features
@@ -78,12 +78,11 @@ Note: `int` is used for brevity but can also mean `long`. Don't worry about it.
7878
| :internal-config | map | optional | A map of the underlying java client properties, for any extra lower level config |
7979

8080
#### Consumer-source options
81-
82-
| Key | Type | Req? | Notes |
83-
|---------------------------------|-----------------------------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
84-
| :group-id | string | required | |
85-
| :shape | `:value:`, `[:vector <fields>]`,`[:map <fields>]`, or an arity-1 function of `ConsumerRecord` | optional | If unspecified, channel will contain ConsumerRecord objects. [Examples](#data-shapes) |
86-
| :ketu.source/consumer-decorator | `fn [consumer-context poll-fn] -> Iterable<ConsumerRecord>` | optional | Decorates the internal poll function. when provided the decorator will be called with the following params:<br/>consumer-context: {:ketu.source/consumer consumer}<br/>pool-fn: fn [] -> Iterable<ConsumerRecord> <br/>Returns an iterable collection of consumerRecord.<br/>The decorator should call the poll-fn on behalf of the consumer source.<br/> |
81+
| Key | Type | Req? | Notes |
82+
|---------------------------------|-----------------------------------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------|
83+
| :group-id | string | required | |
84+
| :shape | `:value:`, `[:vector <fields>]`,`[:map <fields>]`, or an arity-1 function of `ConsumerRecord` | optional | If unspecified, channel will contain ConsumerRecord objects. [Examples](#data-shapes) |
85+
| :ketu.source/consumer-decorator | `ConsumerDecorator` | optional | [Protocol](#ketu.decorators.consumer.protocol) |
8786

8887
#### Producer-sink options
8988

@@ -151,6 +150,7 @@ The decorator processes all immediately available commands in the commands-chan,
151150
(ns consumer-decorator-example
152151
(:require [clojure.core.async :as async]
153152
[ketu.async.source :as source]
153+
[ketu.decorators.consumer.protocol :refer [ConsumerDecorator]]
154154
[ketu.async.sink :as sink]))
155155

156156
(let [commands-chan (async/chan 10)
@@ -161,12 +161,16 @@ The decorator processes all immediately available commands in the commands-chan,
161161
:group-id "example"
162162
:value-type :string
163163
:shape :value
164-
:ketu.source/consumer-decorator (fn [consumer-ctx poll-fn]
165-
(loop []
166-
(when-let [command (async/poll! commands-chan)]
167-
(command consumer-ctx)
168-
(recur)))
169-
(poll-fn))}
164+
:ketu.source/consumer-decorator (reify ConsumerDecorator
165+
(poll! [consumer-ctx poll-fn]
166+
(loop []
167+
(when-let [command (async/poll! commands-chan)]
168+
(command consumer-ctx)
169+
(recur)))
170+
(poll-fn))
171+
(validate [this opts]
172+
;custom validation logic of the consumer options can be added here
173+
true))}
170174
source (source/source consumer-chan consumer-opts)
171175

172176
producer-chan (async/chan 10)

project.clj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(defproject com.appsflyer/ketu "1.1.0"
1+
(defproject com.appsflyer/ketu "2.0.0-SNAPSHOT"
22
:description "Clojure Apache Kafka client with core.async api"
33
:url "https://github.com/AppsFlyer/ketu"
44
:license {:name "Apache License, Version 2.0"

src/ketu/async/source.clj

+3-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[ketu.clients.consumer :as consumer]
55
[ketu.shape.consumer :as shape]
66
[ketu.spec]
7+
[ketu.decorators.consumer.decorator :as consumer-decorator]
78
[ketu.util.log :as log])
89
(:import (java.time Duration)
910
(org.apache.kafka.clients.consumer Consumer)
@@ -104,16 +105,12 @@
104105
^long close-consumer? (:ketu.source/close-consumer? opts)
105106
consumer-close-timeout-ms (:ketu.source/consumer-close-timeout-ms opts)
106107
should-poll? (volatile! true)
107-
decorator-fn (some-> (:ketu.source/consumer-decorator opts)
108-
(partial {:ketu.source/consumer consumer}))
109-
110108
abort-pending-put (async/chan)
111109
done-putting (async/chan)
112-
113110
subscribe! (or (subscribe-fn opts) (assign-fn opts))
114111
poll-impl (poll-fn consumer should-poll? opts)
115-
poll! (if (some? decorator-fn)
116-
(partial decorator-fn poll-impl)
112+
poll! (if (some? (:ketu.source/consumer-decorator opts))
113+
(consumer-decorator/decorate-poll-fn {:ketu.source/consumer consumer} poll-impl opts)
117114
poll-impl)
118115
->data (->data-fn opts)
119116
put! (fn [record] (put-or-abort-pending! out-chan (->data record) abort-pending-put))
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
(ns ketu.decorators.consumer.decorator
2+
(:require [ketu.decorators.consumer.protocol :as cdp]))
3+
4+
(defn- valid? [consumer-decorator consumer-opts]
5+
(when (not (cdp/valid? consumer-decorator consumer-opts))
6+
(throw (Exception. "Consumer decorator validation failed"))))
7+
8+
(defn decorate-poll-fn
9+
[consumer-ctx poll-fn {:keys [ketu.source/consumer-decorator] :as consumer-opts}]
10+
(valid? consumer-decorator consumer-opts)
11+
#(cdp/poll! consumer-decorator consumer-ctx poll-fn))
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
(ns ketu.decorators.consumer.protocol)
2+
3+
(defprotocol ConsumerDecorator
4+
"Consumer decorator provides a way to extend the consumer source functionality.
5+
The decorator runs in the context of the polling thread and allows custom control on the internal consumer instance"
6+
(poll! [this consumer-ctx poll-fn]
7+
"Decorates the internal consumer poll loop.
8+
- Parameters:
9+
- `consumer-ctx`: A map containing the consumer context, typically {:ketu.source/consumer consumer}.
10+
- `poll-fn`: A function with no arguments that returns an Iterable of ConsumerRecord.
11+
- Returns: An iterable collection of ConsumerRecord.
12+
- The decorator should call the `poll-fn` on behalf of the consumer source.")
13+
(valid? [this consumer-opts]
14+
"Validates the consumer options according to the decorator logic.
15+
- Parameters:
16+
- `consumer-opts`: A map of consumer options to be validated.
17+
- Returns: true if the consumer options are valid according to the decorator logic, false otherwise."))

src/ketu/spec.clj

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
(:require [clojure.set]
33
[clojure.spec.alpha :as s]
44
[clojure.string]
5-
[expound.alpha :as expound]
6-
[clojure.core.async.impl.protocols])
5+
[ketu.decorators.consumer.protocol]
6+
[expound.alpha :as expound])
77
(:import (java.util.regex Pattern)
8+
(ketu.decorators.consumer.protocol ConsumerDecorator)
89
(org.apache.kafka.clients.producer Callback)
910
(org.apache.kafka.common.serialization Deserializer Serializer)))
1011

@@ -28,7 +29,7 @@
2829
(s/def :ketu.source/close-out-chan? boolean?)
2930
(s/def :ketu.source/close-consumer? boolean?)
3031
(s/def :ketu.source/create-rebalance-listener-obj fn?)
31-
(s/def :ketu.source/consumer-decorator fn?)
32+
(s/def :ketu.source/consumer-decorator #(instance? ConsumerDecorator %))
3233
(s/def :ketu.source.assign/topic :ketu/topic)
3334
(s/def :ketu.source.assign/partition-nums (s/coll-of nat-int?))
3435
(s/def :ketu.source/assign-single-topic-partitions

test/ketu/async/integration_test.clj

+53-33
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[clojure.core.async :as async]
77
[ketu.clients.consumer :as consumer]
88
[ketu.clients.producer :as producer]
9+
[ketu.decorators.consumer.protocol :refer [ConsumerDecorator]]
910
[ketu.async.source :as source]
1011
[ketu.async.sink :as sink]
1112
[ketu.test.kafka-setup :as kafka-setup])
@@ -213,37 +214,56 @@
213214
(.close ^AdminClient admin-client)))))
214215

215216
(deftest consumer-decorator
216-
(let [consumer-chan (async/chan 10)
217-
result-chan (async/chan 100)
218-
clicks-consumer-opts {:name "clicks-consumer"
219-
:brokers (kafka-setup/get-bootstrap-servers)
220-
:topic "clicks"
221-
:group-id "clicks-test-consumer"
222-
:auto-offset-reset "earliest"
223-
:shape :value
224-
:ketu.source/consumer-decorator (fn [{_consumer :ketu.source/consumer} poll-fn]
225-
(let [records (poll-fn)]
226-
(doseq [^ConsumerRecord record records]
227-
(async/>!! result-chan (String. ^"[B" (.value record))))
228-
records))}
229-
source (source/source consumer-chan clicks-consumer-opts)
230-
clicks-producer-opts {:name "clicks-producer"
231-
:brokers (kafka-setup/get-bootstrap-servers)
232-
:topic "clicks"
233-
:key-type :string
234-
:internal-config {"value.serializer" "org.apache.kafka.common.serialization.StringSerializer"}
235-
:shape [:vector :key :value]}
236-
producer-chan (async/chan 10)
237-
sink (sink/sink producer-chan clicks-producer-opts)
238-
input-values #{"1" "2" "3"}]
239-
(try
240-
(doseq [value input-values]
241-
(async/>!! producer-chan ["1" value]))
242-
(is (= input-values (into #{} (repeatedly 3 #(u/try-take! result-chan)))))
243-
(is (= input-values (into #{} (map #(String. ^"[B" %)) (repeatedly 3 #(u/try-take! consumer-chan)))))
244-
(finally
245-
(Thread/sleep 2000)
246-
(source/stop! source)
247-
(async/close! producer-chan)
248-
(sink/stop! sink)))))
217+
(testing "consumer decorator functionality"
218+
(let [consumer-chan (async/chan 10)
219+
result-chan (async/chan 100)
220+
clicks-consumer-opts {:name "clicks-consumer"
221+
:brokers (kafka-setup/get-bootstrap-servers)
222+
:topic "clicks"
223+
:group-id "clicks-test-consumer"
224+
:auto-offset-reset "earliest"
225+
:shape :value
226+
:ketu.source/consumer-decorator (reify ConsumerDecorator
227+
(poll! [_ {_consumer :ketu.source/consumer} poll-fn]
228+
(let [records (poll-fn)]
229+
(doseq [^ConsumerRecord record records]
230+
(async/>!! result-chan (String. ^"[B" (.value record))))
231+
records))
232+
(valid? [_ _]
233+
true))}
234+
source (source/source consumer-chan clicks-consumer-opts)
235+
clicks-producer-opts {:name "clicks-producer"
236+
:brokers (kafka-setup/get-bootstrap-servers)
237+
:topic "clicks"
238+
:key-type :string
239+
:internal-config {"value.serializer" "org.apache.kafka.common.serialization.StringSerializer"}
240+
:shape [:vector :key :value]}
241+
producer-chan (async/chan 10)
242+
sink (sink/sink producer-chan clicks-producer-opts)
243+
input-values #{"1" "2" "3"}]
244+
(try
245+
(doseq [value input-values]
246+
(async/>!! producer-chan ["1" value]))
247+
(is (= input-values (into #{} (repeatedly 3 #(u/try-take! result-chan)))))
248+
(is (= input-values (into #{} (map #(String. ^"[B" %)) (repeatedly 3 #(u/try-take! consumer-chan)))))
249+
(finally
250+
(Thread/sleep 2000)
251+
(source/stop! source)
252+
(async/close! producer-chan)
253+
(sink/stop! sink))))
249254

255+
(testing "consumer decorator validation failure"
256+
(let [consumer-chan (async/chan 10)
257+
clicks-consumer-opts {:name "clicks-consumer"
258+
:brokers (kafka-setup/get-bootstrap-servers)
259+
:topic "clicks"
260+
:group-id "clicks-test-consumer"
261+
:auto-offset-reset "earliest"
262+
:shape :value
263+
:ketu.source/consumer-decorator (reify ConsumerDecorator
264+
(poll! [_ _ _]
265+
nil)
266+
(valid? [_ _]
267+
false))}]
268+
(is (thrown-with-msg? Exception #"Consumer decorator validation failed"
269+
(source/source consumer-chan clicks-consumer-opts)))))))

0 commit comments

Comments
 (0)