@mark1russell7/harness 0.14.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ // client-kafka BOOTSTRAP --topic NAME [--writers N] [--interval-ms N] [--max-s N] [--set name=value]...
2
+ //
3
+ // The client of a Kafka cell: the producer of kafka-clients 4.3.2, on Java 21. Each event is a JSON
4
+ // line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds (System.nanoTime, which the JVM
5
+ // reads from it on Linux). The prober stamps the records of the kernel on the same clock, and the
6
+ // injector stamps each fault on it.
7
+ //
8
+ // Each writer is one thread with one write at a time. It sends the next sequence number to
9
+ // partition 0 of the topic, and it waits for the reply of that write. It then waits --interval-ms.
10
+ // The producer keeps its own retries: a write that the client sees as a fail is a write that the
11
+ // producer gave up on.
12
+ //
13
+ // The lines:
14
+ // - start: the versions of the client and of Java, the topic, the writers and the settings.
15
+ // - ready: the producer has the metadata of the topic. `at` is the address of the leader.
16
+ // - sent: a write goes to the producer, with its sequence number.
17
+ // - ack: the reply of a write: its offset, and `at`, the address of the leader in the metadata of the
18
+ // producer at that reply.
19
+ // - fail: the error of a write, with its sequence number.
20
+ // - done, deadline: the end of the client.
21
+ //
22
+ // Each --set is a setting of the producer. A cell gives each setting that it changes, and the
23
+ // producer gives the others.
24
+
25
+ import java.io.FileDescriptor;
26
+ import java.io.FileOutputStream;
27
+ import java.io.PrintStream;
28
+ import java.net.InetAddress;
29
+ import java.net.UnknownHostException;
30
+ import java.nio.charset.StandardCharsets;
31
+ import java.nio.file.Files;
32
+ import java.nio.file.Path;
33
+ import java.util.ArrayList;
34
+ import java.util.LinkedHashMap;
35
+ import java.util.List;
36
+ import java.util.Map;
37
+ import java.util.Properties;
38
+ import java.util.concurrent.ConcurrentHashMap;
39
+ import java.util.concurrent.ExecutionException;
40
+ import java.util.concurrent.atomic.AtomicInteger;
41
+ import org.apache.kafka.clients.producer.KafkaProducer;
42
+ import org.apache.kafka.clients.producer.Producer;
43
+ import org.apache.kafka.clients.producer.ProducerConfig;
44
+ import org.apache.kafka.clients.producer.ProducerRecord;
45
+ import org.apache.kafka.clients.producer.RecordMetadata;
46
+ import org.apache.kafka.common.Node;
47
+ import org.apache.kafka.common.PartitionInfo;
48
+ import org.apache.kafka.common.serialization.StringSerializer;
49
+ import org.apache.kafka.common.utils.AppInfoParser;
50
+
51
+ public final class ClientKafka {
52
+ private static final PrintStream OUT = new PrintStream(new FileOutputStream(FileDescriptor.out), false, StandardCharsets.UTF_8);
53
+ private static final Object LOCK = new Object();
54
+ private static final AtomicInteger NEXT = new AtomicInteger(1);
55
+ private static final Map<String, String> ADDRESSES = new ConcurrentHashMap<>();
56
+ private static volatile boolean stopped = false;
57
+ private static volatile String leader = null;
58
+
59
+ private ClientKafka() {}
60
+
61
+ private static long now() {
62
+ return System.nanoTime();
63
+ }
64
+
65
+ /** This function writes one line: its event, its stamp, and the JSON fields after them. */
66
+ private static void emit(String ev, long t, String fields) {
67
+ String line = "{\"ev\":" + q(ev) + ",\"t\":" + t + (fields.isEmpty() ? "" : "," + fields) + "}\n";
68
+ synchronized (LOCK) {
69
+ OUT.print(line);
70
+ OUT.flush();
71
+ }
72
+ }
73
+
74
+ private static void emit(String ev, String fields) {
75
+ emit(ev, now(), fields);
76
+ }
77
+
78
+ /** This function gives a text as a JSON string, or null. */
79
+ private static String q(String text) {
80
+ if (text == null) return "null";
81
+ StringBuilder out = new StringBuilder("\"");
82
+ for (char c : text.toCharArray()) {
83
+ switch (c) {
84
+ case '"' -> out.append("\\\"");
85
+ case '\\' -> out.append("\\\\");
86
+ case '\n' -> out.append("\\n");
87
+ case '\r' -> out.append("\\r");
88
+ case '\t' -> out.append("\\t");
89
+ default -> {
90
+ if (c < 0x20) out.append(String.format("\\u%04x", (int) c));
91
+ else out.append(c);
92
+ }
93
+ }
94
+ }
95
+ return out.append('"').toString();
96
+ }
97
+
98
+ private static String flag(List<String> argv, String name, String otherwise) {
99
+ int at = argv.indexOf(name);
100
+ return at == -1 || at + 1 >= argv.size() ? otherwise : argv.get(at + 1);
101
+ }
102
+
103
+ /** This function gives each value of a flag that repeats, in sequence. */
104
+ private static List<String> flags(List<String> argv, String name) {
105
+ List<String> out = new ArrayList<>();
106
+ for (int at = 0; at + 1 < argv.size(); at += 1) if (argv.get(at).equals(name)) out.add(argv.get(at + 1));
107
+ return out;
108
+ }
109
+
110
+ /** This function gives the address of a broker: `ip:port`. A lookup happens once for each host. */
111
+ private static String addressOf(Node node) {
112
+ String key = node.host() + ":" + node.port();
113
+ return ADDRESSES.computeIfAbsent(key, (absent) -> {
114
+ try {
115
+ return InetAddress.getByName(node.host()).getHostAddress() + ":" + node.port();
116
+ } catch (UnknownHostException error) {
117
+ return key;
118
+ }
119
+ });
120
+ }
121
+
122
+ /**
123
+ * This function gives the address of the leader of the partition in the metadata of the producer.
124
+ * The metadata has the topic after the ready line, thus partitionsFor does not wait. A partition
125
+ * with no leader keeps the last address.
126
+ */
127
+ private static String leaderOf(Producer<String, String> producer, String topic) {
128
+ try {
129
+ List<PartitionInfo> partitions = producer.partitionsFor(topic);
130
+ Node node = partitions.isEmpty() ? null : partitions.get(0).leader();
131
+ if (node != null) leader = addressOf(node);
132
+ } catch (RuntimeException absent) {
133
+ // the last address stays
134
+ }
135
+ return leader;
136
+ }
137
+
138
+ private static String failure(Throwable error) {
139
+ Throwable cause = error.getCause() == null ? error : error.getCause();
140
+ return "\"name\":" + q(cause.getClass().getSimpleName()) + ",\"message\":" + q(cause.getMessage());
141
+ }
142
+
143
+ /** This function is one writer: one write at a time, until the client stops. */
144
+ private static void write(Producer<String, String> producer, String topic, long interval) {
145
+ while (!stopped) {
146
+ int n = NEXT.getAndIncrement();
147
+ emit("sent", "\"n\":" + n);
148
+ try {
149
+ RecordMetadata record = producer.send(new ProducerRecord<>(topic, 0, null, String.valueOf(n))).get();
150
+ emit("ack", "\"n\":" + n + ",\"offset\":" + record.offset() + ",\"at\":" + q(leaderOf(producer, topic)));
151
+ } catch (ExecutionException | RuntimeException error) {
152
+ emit("fail", "\"n\":" + n + "," + failure(error));
153
+ } catch (InterruptedException stop) {
154
+ return;
155
+ }
156
+ try {
157
+ Thread.sleep(interval);
158
+ } catch (InterruptedException stop) {
159
+ return;
160
+ }
161
+ }
162
+ }
163
+
164
+ public static void main(String[] args) throws Exception {
165
+ List<String> argv = List.of(args);
166
+ String topic = flag(argv, "--topic", null);
167
+ if (args.length < 1 || topic == null) {
168
+ emit("fail", "\"why\":\"client-kafka BOOTSTRAP --topic NAME\"");
169
+ Runtime.getRuntime().halt(1);
170
+ }
171
+ int writers = Integer.parseInt(flag(argv, "--writers", "1"));
172
+ long interval = Long.parseLong(flag(argv, "--interval-ms", "50"));
173
+ long max = Long.parseLong(flag(argv, "--max-s", "120")) * 1000;
174
+
175
+ Map<String, String> settings = new LinkedHashMap<>();
176
+ for (String each : flags(argv, "--set")) {
177
+ int at = each.indexOf('=');
178
+ if (at == -1) throw new IllegalArgumentException("a setting is name=value: " + each);
179
+ settings.put(each.substring(0, at), each.substring(at + 1));
180
+ }
181
+ String bootId;
182
+ try {
183
+ bootId = Files.readString(Path.of("/proc/sys/kernel/random/boot_id")).trim();
184
+ } catch (Exception absent) {
185
+ bootId = null;
186
+ }
187
+ StringBuilder given = new StringBuilder("{");
188
+ for (Map.Entry<String, String> each : settings.entrySet()) {
189
+ if (given.length() > 1) given.append(',');
190
+ given.append(q(each.getKey())).append(':').append(q(each.getValue()));
191
+ }
192
+ given.append('}');
193
+ emit("start", "\"boot_id\":" + q(bootId)
194
+ + ",\"client\":\"kafka\",\"driver\":" + q(AppInfoParser.getVersion())
195
+ + ",\"java\":" + q(System.getProperty("java.version"))
196
+ + ",\"topic\":" + q(topic) + ",\"writers\":" + writers + ",\"settings\":" + given);
197
+
198
+ Properties properties = new Properties();
199
+ properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, args[0]);
200
+ properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
201
+ properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
202
+ for (Map.Entry<String, String> each : settings.entrySet()) properties.put(each.getKey(), each.getValue());
203
+ Producer<String, String> producer = new KafkaProducer<>(properties);
204
+
205
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
206
+ // SIGTERM: the end of the client, with exit code 0, as the other clients give
207
+ synchronized (LOCK) {
208
+ if (stopped) return;
209
+ stopped = true;
210
+ }
211
+ emit("done", "");
212
+ Runtime.getRuntime().halt(0);
213
+ }, "sigterm"));
214
+ Thread deadline = new Thread(() -> {
215
+ try {
216
+ Thread.sleep(max);
217
+ } catch (InterruptedException stop) {
218
+ return;
219
+ }
220
+ emit("deadline", "");
221
+ stopped = true;
222
+ emit("done", "");
223
+ Runtime.getRuntime().halt(0);
224
+ }, "deadline");
225
+ deadline.setDaemon(true);
226
+ deadline.start();
227
+
228
+ emit("ready", "\"at\":" + q(leaderOf(producer, topic)));
229
+ for (int at = 0; at < writers; at += 1) {
230
+ Thread writer = new Thread(() -> write(producer, topic, interval), "writer-" + at);
231
+ writer.start();
232
+ }
233
+ }
234
+ }
@@ -0,0 +1,359 @@
1
+ // consumer-kafka BOOTSTRAP --topic NAME --group NAME --protocol consumer|classic --process-ms N
2
+ // --after-pause commit|rejoin [--max-s N] [--set name=value]...
3
+ //
4
+ // One member of a group of a Kafka cell: the consumer of kafka-clients 4.3.2, on Java 21. Each event
5
+ // is a JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds (System.nanoTime, which
6
+ // the JVM reads from it on Linux). The injector stamps each fault on the same clock.
7
+ //
8
+ // The member polls, holds the records for --process-ms, and commits the next offset of each partition
9
+ // with an explicit map. A commitSync with no map uses the positions of the partitions that the
10
+ // consumer holds. After a fence it holds none, thus that commit succeeds and tells nothing.
11
+ //
12
+ // The member sees a stop of its process as a gap in its own clock. It then does one of two things:
13
+ // - commit: it commits the map of the batch at once, before any poll.
14
+ // - rejoin: it polls until its epoch changes, and it then commits the same map. It then reads the
15
+ // committed offsets back.
16
+ //
17
+ // The lines:
18
+ // - start: the versions, the group, the settings, and `wall`, the clock of the wall next to `t`.
19
+ // The broker logs on the clock of the wall, thus this pair converts a log line.
20
+ // - assigned, revoked, lost: a call of the rebalance listener: `partitions`, `held` (each partition
21
+ // after the call), `epoch` and `member`.
22
+ // - read: the records of one partition in one poll: `partition`, `first`, `last` and `count`.
23
+ // - processing: the member holds a batch from this instant.
24
+ // - pause: a gap in the clock of the member: `gap` in nanoseconds, and `in`, the part of its loop.
25
+ // - rejoined: the epoch changed after a pause: `before` and `epoch`.
26
+ // - rejoin-timeout: the epoch did not change in REJOIN_MS.
27
+ // - commit: the broker acknowledged a commit: `offsets` and `epoch`.
28
+ // - commit-fail: the error of a commit: `name`, `message`, `offsets` and `epoch`.
29
+ // - committed: the committed offsets that the member reads back: `offsets`.
30
+ // - poll-fail, read-fail: the error of a poll or of the read back.
31
+ // - done, deadline: the end of the member.
32
+ //
33
+ // A member with a rebalance listener runs the callback of a lost partition in its next poll. Thus a
34
+ // member with no poll stays fenced (ConsumerMembershipManager.java:367-374). The listener is always
35
+ // present, as in an application that keeps state for each partition.
36
+
37
+ import java.io.FileDescriptor;
38
+ import java.io.FileOutputStream;
39
+ import java.io.PrintStream;
40
+ import java.nio.charset.StandardCharsets;
41
+ import java.time.Duration;
42
+ import java.util.ArrayList;
43
+ import java.util.Collection;
44
+ import java.util.LinkedHashMap;
45
+ import java.util.List;
46
+ import java.util.Map;
47
+ import java.util.Properties;
48
+ import java.util.Set;
49
+ import java.util.TreeSet;
50
+ import org.apache.kafka.clients.consumer.ConsumerConfig;
51
+ import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
52
+ import org.apache.kafka.clients.consumer.ConsumerRecord;
53
+ import org.apache.kafka.clients.consumer.ConsumerRecords;
54
+ import org.apache.kafka.clients.consumer.KafkaConsumer;
55
+ import org.apache.kafka.clients.consumer.OffsetAndMetadata;
56
+ import org.apache.kafka.common.TopicPartition;
57
+ import org.apache.kafka.common.serialization.StringDeserializer;
58
+ import org.apache.kafka.common.utils.AppInfoParser;
59
+
60
+ public final class ConsumerKafka {
61
+ private static final PrintStream OUT = new PrintStream(new FileOutputStream(FileDescriptor.out), false, StandardCharsets.UTF_8);
62
+ private static final Object LOCK = new Object();
63
+ /** This time is the gap in the clock of the member that tells a stop, in ns. It has no source: it is far above a tick of the loop. */
64
+ private static final long GAP_NS = 1_000_000_000L;
65
+ /** This time is the tick of the processing, in ms. It has no source. */
66
+ private static final long TICK_MS = 10;
67
+ /** This time is the poll of the loop, in ms. It has no source. */
68
+ private static final long POLL_MS = 100;
69
+ /** This time is the poll of the wait for a new epoch, in ms. It has no source. */
70
+ private static final long REJOIN_POLL_MS = 20;
71
+ /** This time bounds the wait for a new epoch, in ms. It has no source: it is far above one heartbeat. */
72
+ private static final long REJOIN_MS = 20_000;
73
+ /** This time bounds a commit and a read back, in s. It has no source: it is far above a round trip of the group. */
74
+ private static final long CALL_S = 20;
75
+
76
+ private static final Set<Integer> HELD = new TreeSet<>();
77
+ private static volatile boolean stopped = false;
78
+
79
+ private ConsumerKafka() {}
80
+
81
+ private static long now() {
82
+ return System.nanoTime();
83
+ }
84
+
85
+ /** This function writes one line: its event, its stamp, and the JSON fields after them. */
86
+ private static void emit(String ev, long t, String fields) {
87
+ String line = "{\"ev\":" + q(ev) + ",\"t\":" + t + (fields.isEmpty() ? "" : "," + fields) + "}\n";
88
+ synchronized (LOCK) {
89
+ OUT.print(line);
90
+ OUT.flush();
91
+ }
92
+ }
93
+
94
+ private static void emit(String ev, String fields) {
95
+ emit(ev, now(), fields);
96
+ }
97
+
98
+ /** This function gives a text as a JSON string, or null. */
99
+ private static String q(String text) {
100
+ if (text == null) return "null";
101
+ StringBuilder out = new StringBuilder("\"");
102
+ for (char c : text.toCharArray()) {
103
+ switch (c) {
104
+ case '"' -> out.append("\\\"");
105
+ case '\\' -> out.append("\\\\");
106
+ case '\n' -> out.append("\\n");
107
+ case '\r' -> out.append("\\r");
108
+ case '\t' -> out.append("\\t");
109
+ default -> {
110
+ if (c < 0x20) out.append(String.format("\\u%04x", (int) c));
111
+ else out.append(c);
112
+ }
113
+ }
114
+ }
115
+ return out.append('"').toString();
116
+ }
117
+
118
+ private static String flag(List<String> argv, String name, String otherwise) {
119
+ int at = argv.indexOf(name);
120
+ return at == -1 || at + 1 >= argv.size() ? otherwise : argv.get(at + 1);
121
+ }
122
+
123
+ /** This function gives each value of a flag that repeats, in sequence. */
124
+ private static List<String> flags(List<String> argv, String name) {
125
+ List<String> out = new ArrayList<>();
126
+ for (int at = 0; at + 1 < argv.size(); at += 1) if (argv.get(at).equals(name)) out.add(argv.get(at + 1));
127
+ return out;
128
+ }
129
+
130
+ private static String numbers(Collection<Integer> values) {
131
+ List<String> out = new ArrayList<>();
132
+ for (int value : values) out.add(String.valueOf(value));
133
+ return "[" + String.join(",", out) + "]";
134
+ }
135
+
136
+ /** This function gives the offsets of a map as a JSON object, by partition. */
137
+ private static String offsetsOf(Map<TopicPartition, OffsetAndMetadata> offsets) {
138
+ List<String> out = new ArrayList<>();
139
+ for (Map.Entry<TopicPartition, OffsetAndMetadata> each : offsets.entrySet()) {
140
+ out.add(q(String.valueOf(each.getKey().partition())) + ":" + (each.getValue() == null ? "null" : String.valueOf(each.getValue().offset())));
141
+ }
142
+ return "{" + String.join(",", out) + "}";
143
+ }
144
+
145
+ private static String failure(Throwable error) {
146
+ return "\"name\":" + q(error.getClass().getSimpleName()) + ",\"message\":" + q(error.getMessage());
147
+ }
148
+
149
+ private static final class Member {
150
+ private final KafkaConsumer<String, String> consumer;
151
+ private final String topic;
152
+ private final long processMs;
153
+ private final boolean rejoin;
154
+ private boolean rejoined = false;
155
+
156
+ Member(KafkaConsumer<String, String> consumer, String topic, long processMs, boolean rejoin) {
157
+ this.consumer = consumer;
158
+ this.topic = topic;
159
+ this.processMs = processMs;
160
+ this.rejoin = rejoin;
161
+ }
162
+
163
+ private int epoch() {
164
+ return consumer.groupMetadata().generationId();
165
+ }
166
+
167
+ /** This function writes the line of one call of the rebalance listener. */
168
+ private void change(String ev, Collection<TopicPartition> partitions, boolean add) {
169
+ List<Integer> named = new ArrayList<>();
170
+ for (TopicPartition each : partitions) {
171
+ named.add(each.partition());
172
+ if (add) HELD.add(each.partition());
173
+ else HELD.remove(each.partition());
174
+ }
175
+ emit(ev, "\"partitions\":" + numbers(named) + ",\"held\":" + numbers(HELD) + ",\"epoch\":" + epoch()
176
+ + ",\"member\":" + q(consumer.groupMetadata().memberId()));
177
+ }
178
+
179
+ void subscribe() {
180
+ consumer.subscribe(List.of(topic), new ConsumerRebalanceListener() {
181
+ @Override
182
+ public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
183
+ change("revoked", partitions, false);
184
+ }
185
+
186
+ @Override
187
+ public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
188
+ change("assigned", partitions, true);
189
+ }
190
+
191
+ @Override
192
+ public void onPartitionsLost(Collection<TopicPartition> partitions) {
193
+ change("lost", partitions, false);
194
+ }
195
+ });
196
+ }
197
+
198
+ /** This function gives the records of one poll, with a line for each partition. A failure of the poll gives no records. */
199
+ private ConsumerRecords<String, String> poll(long ms) {
200
+ try {
201
+ ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(ms));
202
+ for (TopicPartition partition : records.partitions()) {
203
+ List<ConsumerRecord<String, String>> each = records.records(partition);
204
+ emit("read", "\"partition\":" + partition.partition() + ",\"first\":" + each.get(0).offset()
205
+ + ",\"last\":" + each.get(each.size() - 1).offset() + ",\"count\":" + each.size());
206
+ }
207
+ return records;
208
+ } catch (RuntimeException error) {
209
+ emit("poll-fail", failure(error));
210
+ return ConsumerRecords.empty();
211
+ }
212
+ }
213
+
214
+ /** This function holds a batch for the time of the processing, and it tells if a gap came in that time. */
215
+ private boolean process() throws InterruptedException {
216
+ emit("processing", "");
217
+ long begun = now();
218
+ long last = begun;
219
+ long end = begun + processMs * 1_000_000L;
220
+ while (last < end) {
221
+ Thread.sleep(TICK_MS);
222
+ long tick = now();
223
+ if (tick - last > GAP_NS) {
224
+ emit("pause", tick, "\"gap\":" + (tick - last) + ",\"in\":\"process\"");
225
+ return true;
226
+ }
227
+ last = tick;
228
+ }
229
+ return false;
230
+ }
231
+
232
+ private void commit(Map<TopicPartition, OffsetAndMetadata> offsets) {
233
+ try {
234
+ consumer.commitSync(offsets, Duration.ofSeconds(CALL_S));
235
+ emit("commit", "\"offsets\":" + offsetsOf(offsets) + ",\"epoch\":" + epoch());
236
+ } catch (RuntimeException error) {
237
+ emit("commit-fail", failure(error) + ",\"offsets\":" + offsetsOf(offsets) + ",\"epoch\":" + epoch());
238
+ }
239
+ }
240
+
241
+ /** This function polls until the epoch changes, commits the old map, and reads the committed offsets back. */
242
+ private void rejoinThenCommit(Map<TopicPartition, OffsetAndMetadata> offsets) {
243
+ int before = epoch();
244
+ long begun = System.currentTimeMillis();
245
+ while (epoch() == before) {
246
+ if (System.currentTimeMillis() - begun > REJOIN_MS) {
247
+ emit("rejoin-timeout", "\"before\":" + before);
248
+ return;
249
+ }
250
+ poll(REJOIN_POLL_MS);
251
+ }
252
+ emit("rejoined", "\"before\":" + before + ",\"epoch\":" + epoch());
253
+ commit(offsets);
254
+ try {
255
+ emit("committed", "\"offsets\":" + offsetsOf(consumer.committed(offsets.keySet(), Duration.ofSeconds(CALL_S))));
256
+ } catch (RuntimeException error) {
257
+ emit("read-fail", failure(error));
258
+ }
259
+ }
260
+
261
+ void run() throws InterruptedException {
262
+ long before = now();
263
+ while (!stopped) {
264
+ ConsumerRecords<String, String> records = poll(POLL_MS);
265
+ long after = now();
266
+ if (after - before > POLL_MS * 1_000_000L + GAP_NS) emit("pause", after, "\"gap\":" + (after - before) + ",\"in\":\"poll\"");
267
+ if (records.isEmpty()) {
268
+ before = now();
269
+ continue;
270
+ }
271
+ Map<TopicPartition, OffsetAndMetadata> offsets = new LinkedHashMap<>();
272
+ for (TopicPartition partition : records.partitions()) {
273
+ List<ConsumerRecord<String, String>> each = records.records(partition);
274
+ offsets.put(partition, new OffsetAndMetadata(each.get(each.size() - 1).offset() + 1));
275
+ }
276
+ boolean paused = process();
277
+ if (paused && rejoin && !rejoined) {
278
+ rejoined = true;
279
+ rejoinThenCommit(offsets);
280
+ } else {
281
+ commit(offsets);
282
+ }
283
+ before = now();
284
+ }
285
+ }
286
+ }
287
+
288
+ public static void main(String[] args) throws Exception {
289
+ List<String> argv = List.of(args);
290
+ String topic = flag(argv, "--topic", null);
291
+ String group = flag(argv, "--group", null);
292
+ if (args.length < 1 || topic == null || group == null) {
293
+ emit("fail", "\"why\":\"consumer-kafka BOOTSTRAP --topic NAME --group NAME\"");
294
+ Runtime.getRuntime().halt(1);
295
+ }
296
+ String protocol = flag(argv, "--protocol", "consumer");
297
+ long processMs = Long.parseLong(flag(argv, "--process-ms", "0"));
298
+ String afterPause = flag(argv, "--after-pause", "commit");
299
+ long max = Long.parseLong(flag(argv, "--max-s", "300")) * 1000;
300
+
301
+ Map<String, String> settings = new LinkedHashMap<>();
302
+ for (String each : flags(argv, "--set")) {
303
+ int at = each.indexOf('=');
304
+ if (at == -1) throw new IllegalArgumentException("a setting is name=value: " + each);
305
+ settings.put(each.substring(0, at), each.substring(at + 1));
306
+ }
307
+ StringBuilder given = new StringBuilder("{");
308
+ for (Map.Entry<String, String> each : settings.entrySet()) {
309
+ if (given.length() > 1) given.append(',');
310
+ given.append(q(each.getKey())).append(':').append(q(each.getValue()));
311
+ }
312
+ given.append('}');
313
+ long t = now();
314
+ long wall = System.currentTimeMillis();
315
+ emit("start", t, "\"wall\":" + wall
316
+ + ",\"client\":\"kafka-consumer\",\"driver\":" + q(AppInfoParser.getVersion())
317
+ + ",\"java\":" + q(System.getProperty("java.version"))
318
+ + ",\"topic\":" + q(topic) + ",\"group\":" + q(group) + ",\"protocol\":" + q(protocol)
319
+ + ",\"process_ms\":" + processMs + ",\"after_pause\":" + q(afterPause) + ",\"settings\":" + given);
320
+
321
+ Properties properties = new Properties();
322
+ properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, args[0]);
323
+ properties.put(ConsumerConfig.GROUP_ID_CONFIG, group);
324
+ properties.put(ConsumerConfig.GROUP_PROTOCOL_CONFIG, protocol);
325
+ properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
326
+ properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
327
+ properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
328
+ properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
329
+ for (Map.Entry<String, String> each : settings.entrySet()) properties.put(each.getKey(), each.getValue());
330
+
331
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
332
+ // SIGTERM: the end of the member, with exit code 0, as the other clients give
333
+ synchronized (LOCK) {
334
+ if (stopped) return;
335
+ stopped = true;
336
+ }
337
+ emit("done", "");
338
+ Runtime.getRuntime().halt(0);
339
+ }, "sigterm"));
340
+ Thread deadline = new Thread(() -> {
341
+ try {
342
+ Thread.sleep(max);
343
+ } catch (InterruptedException stop) {
344
+ return;
345
+ }
346
+ emit("deadline", "");
347
+ stopped = true;
348
+ emit("done", "");
349
+ Runtime.getRuntime().halt(0);
350
+ }, "deadline");
351
+ deadline.setDaemon(true);
352
+ deadline.start();
353
+
354
+ KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
355
+ Member member = new Member(consumer, topic, processMs, afterPause.equals("rejoin"));
356
+ member.subscribe();
357
+ member.run();
358
+ }
359
+ }
@@ -0,0 +1,23 @@
1
+ # The harness's client of Apache Kafka: kafka-clients 4.3.2 on Temurin 21.0.12.1. It runs as a
2
+ # container in the client's network namespace, as the harness's own TCP client does. It prints the
3
+ # same JSON lines. AdminKafka is the admin of a lab: it makes each topic, reads the state of a
4
+ # partition, and reads the whole partition after a run.
5
+ # lab.ts stages the build context: this folder, under harness/image/kafka. The build gets each jar
6
+ # from Maven Central and checks its SHA-256 (jars.sha256), thus a build cannot get another jar. The
7
+ # jars are kafka-clients and each runtime dependency of its POM.
8
+ FROM eclipse-temurin:21.0.12.1_1-jdk-alpine@sha256:0bfc69a4758a86710e5c474032d28400a8bd00874766f9e8b1642ac2fd293159 AS build
9
+ WORKDIR /build
10
+ COPY harness/image/kafka/jars.sha256 ./
11
+ RUN wget -q https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/4.3.2/kafka-clients-4.3.2.jar \
12
+ && wget -q https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar \
13
+ && wget -q https://repo1.maven.org/maven2/com/github/luben/zstd-jni/1.5.6-10/zstd-jni-1.5.6-10.jar \
14
+ && wget -q https://repo1.maven.org/maven2/at/yawk/lz4/lz4-java/1.11.4/lz4-java-1.11.4.jar \
15
+ && wget -q https://repo1.maven.org/maven2/org/xerial/snappy/snappy-java/1.1.10.11/snappy-java-1.1.10.11.jar \
16
+ && sha256sum -c jars.sha256
17
+ COPY harness/image/kafka/ClientKafka.java harness/image/kafka/AdminKafka.java harness/image/kafka/ConsumerKafka.java ./
18
+ RUN javac --release 21 -cp "kafka-clients-4.3.2.jar:slf4j-api-1.7.36.jar" -d classes ClientKafka.java AdminKafka.java ConsumerKafka.java
19
+
20
+ FROM eclipse-temurin:21.0.12.1_1-jre-alpine@sha256:51ab5e3302e7141ce665ca3ea85e8b5cd648eafbc3c0c90dd79d6537684e4555
21
+ WORKDIR /opt/harness/kafka
22
+ COPY --from=build /build/kafka-clients-4.3.2.jar /build/slf4j-api-1.7.36.jar /build/zstd-jni-1.5.6-10.jar /build/lz4-java-1.11.4.jar /build/snappy-java-1.1.10.11.jar ./lib/
23
+ COPY --from=build /build/classes ./classes
@@ -0,0 +1,5 @@
1
+ edaf0818414a61a98d1d2e58d6966be8c9f780cef6ded2ffbab10dd841b2bfb8 kafka-clients-4.3.2.jar
2
+ d3ef575e3e4979678dc01bf1dcce51021493b4d11fb7f1be8ad982877c16a1c0 slf4j-api-1.7.36.jar
3
+ b6c3237e24b8252a5a9c3a0e1ae30ef8f323412e74bd358548b7a9e527d676df zstd-jni-1.5.6-10.jar
4
+ 58c8e0b813960d2a248e050c353baea73139b48a2ffc55382d42c69707e17325 lz4-java-1.11.4.jar
5
+ 58d245ec749fc608ecb175e934a16f2840442c5742aed91002cfd4cb9c6c7aee snappy-java-1.1.10.11.jar
@@ -3,7 +3,7 @@ name = "harness-native"
3
3
  version = "0.1.0"
4
4
  edition = "2021"
5
5
  publish = false
6
- description = "The harness's native side: the cell client and the injector"
6
+ description = "The harness's native side: the cell client, the injector and the HTTP load driver"
7
7
 
8
8
  [dependencies]
9
9
  # the clock, the quantities and the errno names, without the collector