Skip to content

Commit db70907

Browse files
tsegismontmatzew
authored andcommitted
Fix ReceiverVerticleTracingTest#traceIsPropagated
In Vert.x 4.5.1, there was a fix to use the OTel default context storage when the Vert.x context storage provider is not invoked on a Vert.x thread. See eclipse-vertx/vertx-tracing#72 Since LoomKafkaProducer invokes the tracer on a virtual thread (or a worker thread), the sending task must be wrapped with the OTel current context. Otherwise, tracing data will be lost at this point. OTel provides an ExecutorService that does just that, so this commit refactors the producer to use an executor service instead of a queue plus manual polling. Important: note that with this implementation, a virtual thread is created for each record, which is different from sending them one after the other with a single thread.
1 parent 35d547a commit db70907

File tree

2 files changed

+54
-85
lines changed

2 files changed

+54
-85
lines changed

data-plane/receiver-loom/src/main/java/dev/knative/eventing/kafka/broker/receiverloom/LoomKafkaProducer.java

+53-81
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@
1515
*/
1616
package dev.knative.eventing.kafka.broker.receiverloom;
1717

18+
import com.google.common.util.concurrent.Uninterruptibles;
1819
import dev.knative.eventing.kafka.broker.core.ReactiveKafkaProducer;
1920
import dev.knative.eventing.kafka.broker.core.tracing.kafka.ProducerTracer;
21+
import io.opentelemetry.context.Context;
2022
import io.vertx.core.Future;
2123
import io.vertx.core.Promise;
2224
import io.vertx.core.Vertx;
2325
import io.vertx.core.impl.ContextInternal;
2426
import io.vertx.core.impl.VertxInternal;
27+
import io.vertx.core.impl.future.PromiseInternal;
2528
import io.vertx.core.tracing.TracingPolicy;
2629
import java.util.Objects;
27-
import java.util.concurrent.BlockingQueue;
28-
import java.util.concurrent.LinkedBlockingQueue;
29-
import java.util.concurrent.TimeUnit;
30+
import java.util.concurrent.ExecutorService;
31+
import java.util.concurrent.Executors;
3032
import java.util.concurrent.atomic.AtomicBoolean;
3133
import org.apache.kafka.clients.producer.Producer;
3234
import org.apache.kafka.clients.producer.ProducerRecord;
@@ -41,17 +43,15 @@ public class LoomKafkaProducer<K, V> implements ReactiveKafkaProducer<K, V> {
4143

4244
private final Producer<K, V> producer;
4345

44-
private final BlockingQueue<RecordPromise<K, V>> eventQueue;
46+
private final ExecutorService executorService;
4547
private final AtomicBoolean isClosed;
4648
private final ProducerTracer<?> tracer;
4749
private final VertxInternal vertx;
48-
private final Thread sendFromQueueThread;
4950
private final Promise<Void> closePromise = Promise.promise();
5051

5152
public LoomKafkaProducer(Vertx v, Producer<K, V> producer) {
5253
Objects.requireNonNull(v, "Vertx cannot be null");
5354
this.producer = producer;
54-
this.eventQueue = new LinkedBlockingQueue<>();
5555
this.isClosed = new AtomicBoolean(false);
5656
this.vertx = (VertxInternal) v;
5757
final var ctxInt = ((ContextInternal) v.getOrCreateContext()).unwrap();
@@ -62,73 +62,54 @@ public LoomKafkaProducer(Vertx v, Producer<K, V> producer) {
6262
this.tracer = null;
6363
}
6464

65+
ExecutorService executorService;
6566
if (Boolean.parseBoolean(System.getenv("ENABLE_VIRTUAL_THREADS"))) {
66-
this.sendFromQueueThread = Thread.ofVirtual().start(this::sendFromQueue);
67+
executorService = Executors.newVirtualThreadPerTaskExecutor();
6768
} else {
68-
this.sendFromQueueThread = new Thread(this::sendFromQueue);
69-
this.sendFromQueueThread.start();
69+
executorService = Executors.newSingleThreadExecutor();
7070
}
71+
this.executorService = Context.taskWrapping(executorService);
7172
}
7273

7374
@Override
7475
public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
75-
final Promise<RecordMetadata> promise = Promise.promise();
7676
if (isClosed.get()) {
77-
promise.fail("Producer is closed");
78-
} else {
79-
eventQueue.add(new RecordPromise<>(record, this.vertx.getOrCreateContext(), promise));
77+
return Future.failedFuture("Producer is closed");
8078
}
79+
PromiseInternal<RecordMetadata> promise = vertx.promise();
80+
executorService.execute(() -> sendFromQueue(new RecordPromise<>(record, promise)));
8181
return promise.future();
8282
}
8383

84-
private void sendFromQueue() {
85-
// Process queue elements until this is closed and the tasks queue is empty
86-
while (!isClosed.get() || !eventQueue.isEmpty()) {
87-
try {
88-
final var recordPromise = eventQueue.poll(2000, TimeUnit.MILLISECONDS);
89-
if (recordPromise == null) {
90-
continue;
84+
private void sendFromQueue(RecordPromise<K, V> recordPromise) {
85+
final var startedSpan = this.tracer == null
86+
? null
87+
: this.tracer.prepareSendMessage(recordPromise.context(), recordPromise.record);
88+
89+
recordPromise
90+
.promise
91+
.future()
92+
.onComplete(v -> {
93+
if (startedSpan != null) {
94+
startedSpan.finish(recordPromise.context());
95+
}
96+
})
97+
.onFailure(cause -> {
98+
if (startedSpan != null) {
99+
startedSpan.fail(recordPromise.context(), cause);
100+
}
101+
});
102+
try {
103+
producer.send(recordPromise.record, (metadata, exception) -> {
104+
if (exception != null) {
105+
recordPromise.fail(exception);
106+
return;
91107
}
92-
93-
final var startedSpan = this.tracer == null
94-
? null
95-
: this.tracer.prepareSendMessage(recordPromise.getContext(), recordPromise.getRecord());
96-
97-
recordPromise
98-
.getPromise()
99-
.future()
100-
.onComplete(v -> {
101-
if (startedSpan != null) {
102-
startedSpan.finish(recordPromise.getContext());
103-
}
104-
})
105-
.onFailure(cause -> {
106-
if (startedSpan != null) {
107-
startedSpan.fail(recordPromise.getContext(), cause);
108-
}
109-
});
110-
try {
111-
producer.send(
112-
recordPromise.getRecord(),
113-
(metadata, exception) -> recordPromise.getContext().runOnContext(v -> {
114-
if (exception != null) {
115-
recordPromise.getPromise().fail(exception);
116-
return;
117-
}
118-
recordPromise.getPromise().complete(metadata);
119-
}));
120-
} catch (final KafkaException exception) {
121-
recordPromise
122-
.getContext()
123-
.runOnContext(v -> recordPromise.getPromise().fail(exception));
124-
}
125-
} catch (InterruptedException e) {
126-
logger.debug("Interrupted while waiting for event queue to be populated.");
127-
break;
128-
}
108+
recordPromise.complete(metadata);
109+
});
110+
} catch (final KafkaException exception) {
111+
recordPromise.fail(exception);
129112
}
130-
131-
logger.debug("Background thread completed.");
132113
}
133114

134115
@Override
@@ -141,12 +122,9 @@ public Future<Void> close() {
141122

142123
Thread.ofVirtual().start(() -> {
143124
try {
144-
while (!eventQueue.isEmpty()) {
145-
logger.debug("Waiting for the eventQueue to become empty");
146-
Thread.sleep(2000L);
147-
}
148-
logger.debug("Waiting for sendFromQueueThread thread to complete");
149-
sendFromQueueThread.join();
125+
executorService.shutdown();
126+
logger.debug("Waiting for tasks to complete");
127+
Uninterruptibles.awaitTerminationUninterruptibly(executorService);
150128
logger.debug("Closing the producer");
151129
producer.close();
152130
closePromise.complete();
@@ -178,35 +156,29 @@ public Producer<K, V> unwrap() {
178156
}
179157

180158
private static class RecordPromise<K, V> {
181-
private final ProducerRecord<K, V> record;
182-
private final ContextInternal context;
183-
private final Promise<RecordMetadata> promise;
159+
final ProducerRecord<K, V> record;
160+
final PromiseInternal<RecordMetadata> promise;
184161

185-
private RecordPromise(ProducerRecord<K, V> record, ContextInternal context, Promise<RecordMetadata> promise) {
162+
RecordPromise(ProducerRecord<K, V> record, PromiseInternal<RecordMetadata> promise) {
186163
this.record = record;
187-
this.context = context;
188164
this.promise = promise;
189165
}
190166

191-
public ProducerRecord<K, V> getRecord() {
192-
return record;
167+
ContextInternal context() {
168+
return promise.context();
193169
}
194170

195-
public Promise<RecordMetadata> getPromise() {
196-
return promise;
171+
void complete(RecordMetadata result) {
172+
promise.complete(result);
197173
}
198174

199-
public ContextInternal getContext() {
200-
return context;
175+
void fail(Throwable cause) {
176+
promise.fail(cause);
201177
}
202178
}
203179

204180
// Function needed for testing
205181
public boolean isSendFromQueueThreadAlive() {
206-
return sendFromQueueThread.isAlive();
207-
}
208-
209-
public int getEventQueueSize() {
210-
return eventQueue.size();
182+
return !executorService.isTerminated();
211183
}
212184
}

data-plane/receiver-loom/src/test/java/dev/knative/eventing/kafka/broker/receiverloom/LoomKafkaProducerTest.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
*/
1616
package dev.knative.eventing.kafka.broker.receiverloom;
1717

18-
import static org.junit.jupiter.api.Assertions.assertEquals;
19-
import static org.junit.jupiter.api.Assertions.assertFalse;
20-
import static org.junit.jupiter.api.Assertions.assertTrue;
18+
import static org.junit.jupiter.api.Assertions.*;
2119
import static org.mockito.ArgumentMatchers.any;
2220
import static org.mockito.Mockito.mock;
2321
import static org.mockito.Mockito.when;
@@ -128,7 +126,6 @@ public void testCloseIsWaitingForEmptyQueue(VertxTestContext testContext) {
128126
.onComplete(ar -> {
129127
testContext.verify(() -> {
130128
assertTrue(ar.succeeded());
131-
assertEquals(0, producer.getEventQueueSize());
132129
assertFalse(producer.isSendFromQueueThreadAlive());
133130

134131
checkpoints.flag();

0 commit comments

Comments
 (0)