@@ -44,6 +44,7 @@ import java.nio.file.Files
44
44
import java.nio.file.Paths
45
45
import java.time.Duration
46
46
import java.util.*
47
+ import java.util.concurrent.ExecutionException
47
48
import kotlin.collections.HashMap
48
49
49
50
/* *
@@ -54,13 +55,26 @@ class TestingEmbeddedKafka(config: Properties = Properties()) {
54
55
companion object {
55
56
val Log : Logger = LoggerFactory .getLogger(TestingEmbeddedKafka ::class .java)
56
57
57
- private fun getTopicNames (adminClient : Admin ): MutableSet <String > {
58
+ private fun listTopicNames (adminClient : Admin ): MutableSet <String > {
58
59
return try {
59
60
adminClient.listTopics().names().get()
60
61
} catch (e: Exception ) {
61
62
throw RuntimeException (" Failed to get topic names" , e)
62
63
}
63
64
}
65
+
66
+ private fun waitForTrue (timeout : Duration ,
67
+ time : Long = System .currentTimeMillis(),
68
+ action : () -> Boolean ): Boolean {
69
+
70
+ val timeoutMs = timeout.toMillis()
71
+ var result = false
72
+ while (System .currentTimeMillis() - time < timeoutMs && ! result) {
73
+ result = action()
74
+ }
75
+ return result
76
+ }
77
+
64
78
}
65
79
66
80
private val config: MutableMap <Any , Any > = HashMap (config)
@@ -71,15 +85,15 @@ class TestingEmbeddedKafka(config: Properties = Properties()) {
71
85
* @param securityProtocol the security protocol the returned broker list should use.
72
86
*
73
87
*/
74
- fun bootstrapServers (securityProtocol : SecurityProtocol ? = null): String {
88
+ fun bootstrapServers (securityProtocol : SecurityProtocol ? = null): Array < String > {
75
89
val port = if (securityProtocol == null ) {
76
90
val listenerName = kafka.config().advertisedListeners().apply (0 ).listenerName()
77
91
kafka.boundPort(listenerName)
78
92
}
79
93
else {
80
94
kafka.boundPort(ListenerName (securityProtocol.toString()))
81
95
}
82
- return " ${kafka.config().hostName()} :$port "
96
+ return arrayOf( " ${kafka.config().hostName()} :$port " )
83
97
}
84
98
85
99
/* *
@@ -153,21 +167,62 @@ class TestingEmbeddedKafka(config: Properties = Properties()) {
153
167
topic, partitions, replication, config
154
168
)
155
169
156
- adminClient().use {adminClient ->
157
- val newTopic = NewTopic (topic, partitions, replication.toShort())
158
- newTopic.configs(config)
170
+ adminClient().use {client ->
159
171
try {
160
- adminClient.createTopics(listOf (newTopic)).all().get()
161
- } catch (e: Exception ) {
162
- throw RuntimeException (" Failed to create topic:$topic " , e)
172
+ val newTopic = NewTopic (topic, partitions, replication.toShort())
173
+ newTopic.configs(config)
174
+ client.createTopics(listOf (newTopic)).all().get()
175
+ } catch (e : ExecutionException ) {
176
+ throw e.cause as Throwable
163
177
}
164
178
}
165
179
}
166
180
167
181
/* *
168
182
* @return the list of topics that exists on the embedded cluster.
169
183
*/
170
- fun topics (): Set <String > = adminClient().use { adminClient -> return getTopicNames(adminClient) }
184
+ fun topics (): Set <String > = adminClient().use { adminClient -> return listTopicNames(adminClient) }
185
+
186
+ /* *
187
+ * Waits for all given [topicNames] to be present on the embedded cluster until [timeout].
188
+ *
189
+ * @return {@code true} if all topics are present before reaching the timeout, {@code false} otherwise.
190
+ */
191
+ fun waitForTopicsToBePresent (vararg topicNames : String ,
192
+ timeout : Duration = Duration .ofSeconds(30)): Boolean {
193
+ val now = System .currentTimeMillis()
194
+ val required = mutableListOf (* topicNames)
195
+ return adminClient().use { client ->
196
+ waitForTrue(timeout, now) {
197
+ listTopicNames(client).containsAll(required)
198
+ }
199
+ }
200
+ }
201
+
202
+ /* *
203
+ * Waits for all given [topicNames] to be absent on the embedded cluster until [timeout].
204
+ *
205
+ * @return {@code true} if all topics are absent before reaching the timeout, {@code false} otherwise.
206
+ */
207
+ fun waitForTopicsToBeAbsent (vararg topicNames : String ,
208
+ timeout : Duration = Duration .ofSeconds(30)): Boolean {
209
+ return adminClient().use {
210
+ doWaitForTopicsToBeAbsent(topics = arrayOf(* topicNames), until = timeout, adminClient = it)
211
+ }
212
+ }
213
+
214
+ private fun doWaitForTopicsToBeAbsent (
215
+ topics : Array <String >,
216
+ until : Duration = Duration .ofMillis(Long .MAX_VALUE ),
217
+ now : Long = System .currentTimeMillis(),
218
+ adminClient : AdminClient ): Boolean {
219
+ val remaining: MutableList <String > = mutableListOf (* topics)
220
+ return waitForTrue(until, now) {
221
+ val exists = listTopicNames(adminClient)
222
+ remaining.retainAll(exists)
223
+ remaining.isEmpty()
224
+ }
225
+ }
171
226
172
227
/* *
173
228
* Creates a new admin client.
@@ -176,7 +231,7 @@ class TestingEmbeddedKafka(config: Properties = Properties()) {
176
231
*/
177
232
fun adminClient () =
178
233
AdminClient .create(mutableMapOf (
179
- Pair (AdminClientConfig .BOOTSTRAP_SERVERS_CONFIG , bootstrapServers()),
234
+ Pair (AdminClientConfig .BOOTSTRAP_SERVERS_CONFIG , bootstrapServers().joinToString() ),
180
235
Pair (AdminClientConfig .REQUEST_TIMEOUT_MS_CONFIG , 60000 )
181
236
))
182
237
@@ -187,7 +242,7 @@ class TestingEmbeddedKafka(config: Properties = Properties()) {
187
242
*/
188
243
fun producerClient (config : Map <String , Any ?> = emptyMap()): Producer <Any , Any > {
189
244
val configs = HashMap (config)
190
- configs[ProducerConfig .BOOTSTRAP_SERVERS_CONFIG ] = bootstrapServers()
245
+ configs[ProducerConfig .BOOTSTRAP_SERVERS_CONFIG ] = bootstrapServers().joinToString()
191
246
return KafkaProducer (configs)
192
247
}
193
248
@@ -200,7 +255,7 @@ class TestingEmbeddedKafka(config: Properties = Properties()) {
200
255
keyDeserializer : Deserializer <K >? = null,
201
256
valueDeserializer : Deserializer <V >? = null): Consumer <K , V > {
202
257
val configs = HashMap (config)
203
- configs[ConsumerConfig .BOOTSTRAP_SERVERS_CONFIG ] = bootstrapServers()
258
+ configs[ConsumerConfig .BOOTSTRAP_SERVERS_CONFIG ] = bootstrapServers().joinToString()
204
259
configs.putIfAbsent(ConsumerConfig .KEY_DESERIALIZER_CLASS_CONFIG , ByteArrayDeserializer ::class .java.name)
205
260
configs.putIfAbsent(ConsumerConfig .VALUE_DESERIALIZER_CLASS_CONFIG , ByteArrayDeserializer ::class .java.name)
206
261
configs.putIfAbsent(ConsumerConfig .AUTO_OFFSET_RESET_CONFIG , " earliest" )
@@ -232,18 +287,15 @@ class TestingEmbeddedKafka(config: Properties = Properties()) {
232
287
/* *
233
288
* Deletes the given [topics] from the cluster.
234
289
*/
235
- fun deleteTopics (topics : Collection <String ?>) {
290
+ fun deleteTopics (vararg topicNames : String ) {
291
+ val remaining: MutableList <String > = mutableListOf (* topicNames)
236
292
try {
237
- adminClient().use { adminClient ->
238
- adminClient.deleteTopics(topics).all().get()
239
- val remaining: MutableSet <String ?> = topics.toMutableSet()
240
- while (remaining.isNotEmpty()) {
241
- val topicNames: Set <String > = adminClient.listTopics().names().get()
242
- remaining.retainAll(topicNames)
243
- }
293
+ adminClient().use { client ->
294
+ client.deleteTopics(remaining).all().get()
295
+ doWaitForTopicsToBeAbsent(topics = arrayOf(* topicNames), adminClient = client)
244
296
}
245
297
} catch (e: Exception ) {
246
- throw RuntimeException (" Failed to delete topics: $topics " , e)
298
+ throw RuntimeException (" Failed to delete topics: $remaining " , e)
247
299
}
248
300
}
249
301
0 commit comments