New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support live reload for SmallRye Reactive Messaging #15986
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package io.quarkus.smallrye.reactivemessaging.amqp.devmode.nohttp; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.awaitility.Awaitility.await; | ||
|
||
import java.util.List; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.logging.LogRecord; | ||
import java.util.stream.Collectors; | ||
|
||
import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManagerFactory; | ||
import org.jboss.shrinkwrap.api.ShrinkWrap; | ||
import org.jboss.shrinkwrap.api.spec.JavaArchive; | ||
import org.junit.jupiter.api.AfterAll; | ||
import org.junit.jupiter.api.BeforeAll; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.RegisterExtension; | ||
|
||
import io.quarkus.smallrye.reactivemessaging.amqp.AnonymousAmqpBroker; | ||
import io.quarkus.test.QuarkusDevModeTest; | ||
|
||
public class AmqpDevModeNoHttpTest { | ||
@RegisterExtension | ||
static QuarkusDevModeTest TEST = new QuarkusDevModeTest() | ||
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) | ||
.addClasses(Producer.class, Consumer.class, | ||
AnonymousAmqpBroker.class, ProtonProtocolManagerFactory.class) | ||
.addAsResource("broker.xml") | ||
.addAsResource("application.properties")) | ||
.setLogRecordPredicate(r -> r.getLoggerName().equals(Consumer.class.getName())); | ||
|
||
@BeforeAll | ||
public static void startBroker() { | ||
AnonymousAmqpBroker.start(); | ||
} | ||
|
||
@AfterAll | ||
public static void stopBroker() { | ||
AnonymousAmqpBroker.stop(); | ||
} | ||
|
||
// For all tests below: we don't know exactly when the AMQP connector receives credits from the broker | ||
// and then requests items from the producer, so we don't know what number will be sent to the queue first. | ||
// What we do know, and hence test, is the relationship between consecutive numbers in the queue. | ||
// See also https://github.com/smallrye/smallrye-reactive-messaging/issues/1125. | ||
|
||
@Test | ||
public void testProducerUpdate() { | ||
await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { | ||
List<LogRecord> log = TEST.getLogRecords(); | ||
assertThat(log).hasSizeGreaterThanOrEqualTo(5); | ||
|
||
List<Long> nums = log.stream() | ||
.map(it -> Long.parseLong(it.getMessage())) | ||
.collect(Collectors.toList()); | ||
|
||
long last = nums.get(nums.size() - 1); | ||
assertThat(nums).containsSequence(last - 4, last - 3, last - 2, last - 1, last); | ||
}); | ||
|
||
TEST.modifySourceFile(Producer.class, s -> s.replace("* 1", "* 2")); | ||
|
||
await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { | ||
List<LogRecord> log = TEST.getLogRecords(); | ||
assertThat(log).hasSizeGreaterThanOrEqualTo(5); | ||
|
||
List<Long> nums = log.stream() | ||
.map(it -> Long.parseLong(it.getMessage())) | ||
.collect(Collectors.toList()); | ||
|
||
long last = nums.get(nums.size() - 1); | ||
assertThat(nums).containsSequence(last - 8, last - 6, last - 4, last - 2, last); | ||
}); | ||
} | ||
|
||
@Test | ||
public void testConsumerUpdate() { | ||
await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { | ||
List<LogRecord> log = TEST.getLogRecords(); | ||
assertThat(log).hasSizeGreaterThanOrEqualTo(5); | ||
|
||
List<Long> nums = log.stream() | ||
.map(it -> Long.parseLong(it.getMessage())) | ||
.collect(Collectors.toList()); | ||
|
||
long last = nums.get(nums.size() - 1); | ||
assertThat(nums).containsSequence(last - 4, last - 3, last - 2, last - 1, last); | ||
}); | ||
|
||
TEST.modifySourceFile(Consumer.class, s -> s.replace("* 1", "* 3")); | ||
|
||
await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { | ||
List<LogRecord> log = TEST.getLogRecords(); | ||
assertThat(log).hasSizeGreaterThanOrEqualTo(5); | ||
|
||
List<Long> nums = log.stream() | ||
.map(it -> Long.parseLong(it.getMessage())) | ||
.collect(Collectors.toList()); | ||
|
||
long last = nums.get(nums.size() - 1); | ||
assertThat(nums).containsSequence(last - 12, last - 9, last - 6, last - 3, last); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package io.quarkus.smallrye.reactivemessaging.amqp.devmode.nohttp; | ||
|
||
import javax.enterprise.context.ApplicationScoped; | ||
|
||
import org.eclipse.microprofile.reactive.messaging.Incoming; | ||
import org.jboss.logging.Logger; | ||
|
||
@ApplicationScoped | ||
public class Consumer { | ||
private static final Logger log = Logger.getLogger(Consumer.class); | ||
|
||
@Incoming("in") | ||
public void consume(long content) { | ||
log.info(content * 1); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package io.quarkus.smallrye.reactivemessaging.amqp.devmode.nohttp; | ||
|
||
import java.time.Duration; | ||
|
||
import javax.enterprise.context.ApplicationScoped; | ||
|
||
import org.eclipse.microprofile.reactive.messaging.Outgoing; | ||
import org.reactivestreams.Publisher; | ||
|
||
import io.smallrye.mutiny.Multi; | ||
|
||
@ApplicationScoped | ||
public class Producer { | ||
@Outgoing("source") | ||
public Publisher<Long> generate() { | ||
return Multi.createFrom().ticks().every(Duration.ofMillis(200)) | ||
.onOverflow().drop() | ||
.map(i -> i * 1); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package io.quarkus.smallrye.reactivemessaging.runtime.devmode; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
import javax.interceptor.InterceptorBinding; | ||
|
||
@InterceptorBinding | ||
@Target(ElementType.TYPE) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface DevModeSupportConnectorFactory { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package io.quarkus.smallrye.reactivemessaging.runtime.devmode; | ||
|
||
import java.util.concurrent.CompletableFuture; | ||
import java.util.function.Supplier; | ||
|
||
import javax.annotation.Priority; | ||
import javax.interceptor.AroundInvoke; | ||
import javax.interceptor.Interceptor; | ||
import javax.interceptor.InvocationContext; | ||
|
||
import org.eclipse.microprofile.reactive.messaging.Message; | ||
import org.eclipse.microprofile.reactive.streams.operators.PublisherBuilder; | ||
import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; | ||
import org.eclipse.microprofile.reactive.streams.operators.SubscriberBuilder; | ||
import org.reactivestreams.Subscriber; | ||
import org.reactivestreams.Subscription; | ||
|
||
@Interceptor | ||
@DevModeSupportConnectorFactory | ||
@Priority(Interceptor.Priority.PLATFORM_BEFORE + 10) | ||
public class DevModeSupportConnectorFactoryInterceptor { | ||
private static Supplier<CompletableFuture<Boolean>> onMessage; | ||
|
||
static void register(Supplier<CompletableFuture<Boolean>> onMessage) { | ||
DevModeSupportConnectorFactoryInterceptor.onMessage = onMessage; | ||
} | ||
|
||
@AroundInvoke | ||
public Object intercept(InvocationContext ctx) throws Exception { | ||
if (onMessage == null) { | ||
return ctx.proceed(); | ||
} | ||
|
||
if (ctx.getMethod().getName().equals("getPublisherBuilder")) { | ||
PublisherBuilder<Message<?>> result = (PublisherBuilder<Message<?>>) ctx.proceed(); | ||
return result.flatMapCompletionStage(msg -> { | ||
CompletableFuture<Message<?>> future = new CompletableFuture<>(); | ||
onMessage.get().whenComplete((restarted, error) -> { | ||
if (!restarted) { | ||
// if restarted, a new stream is already running, | ||
// no point in emitting an event to the old stream | ||
future.complete(msg); | ||
} | ||
}); | ||
return future; | ||
}); | ||
} | ||
|
||
if (ctx.getMethod().getName().equals("getSubscriberBuilder")) { | ||
SubscriberBuilder<Message<?>, Void> result = (SubscriberBuilder<Message<?>, Void>) ctx.proceed(); | ||
return ReactiveStreams.fromSubscriber(new Subscriber<Message<?>>() { | ||
private Subscriber<Message<?>> subscriber; | ||
|
||
@Override | ||
public void onSubscribe(Subscription s) { | ||
subscriber = result.build(); | ||
subscriber.onSubscribe(s); | ||
} | ||
|
||
@Override | ||
public void onNext(Message<?> o) { | ||
subscriber.onNext(o); | ||
onMessage.get().join(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't this block the IO thread? Also won't the restart happen after the message has been delivered, rather than before, so the message is handled by the old code? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hum, right - this method is going to be called on this I/O thread. Why is it require to join actually? In theory, the connectors are passing the reactive streams TCK meaning that the onNext / onError / onComplete methods cannot be called concurrently (but could be called by different threads). Also, we may want to reverse the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually did try something like Note that this is I trigger restart after submitting the message, so that the message isn't lost. Note that even if I triggered restart before submitting the message, it would still be processed by old code with full restart, because full restart takes a few seconds during which old code still runs. (At least that's what I observed.) And I wait for restart to finish so that no other message may come in and be lost. I realize blocking is a crude way to do it, but I figured that wouldn't be such a big deal in dev mode. I'm open to other ideas of course. I toyed with the idea of using the request protocol to handle this, but I really don't feel like I could do it correctly :-) |
||
} | ||
|
||
@Override | ||
public void onError(Throwable t) { | ||
subscriber.onError(t); | ||
onMessage.get().join(); | ||
} | ||
|
||
@Override | ||
public void onComplete() { | ||
subscriber.onComplete(); | ||
onMessage.get().join(); | ||
} | ||
}); | ||
} | ||
|
||
return ctx.proceed(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should have been volatile
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah that is true. I copied this pattern from other dev mode handlers, without too much thinking. I'll submit a PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done: #16263