-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Fix Mutiny schedulers context propagation bug #26242
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,71 @@ | ||
package io.quarkus.mutiny.runtime; | ||
|
||
import java.util.concurrent.Delayed; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.concurrent.RunnableScheduledFuture; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.TimeoutException; | ||
|
||
import org.jboss.threads.ContextHandler; | ||
|
||
class ContextualRunnableScheduledFuture<V> implements RunnableScheduledFuture<V> { | ||
private final RunnableScheduledFuture<V> runnable; | ||
private final Object context; | ||
private final ContextHandler<Object> contextHandler; | ||
|
||
public ContextualRunnableScheduledFuture(ContextHandler<Object> contextHandler, Object context, | ||
RunnableScheduledFuture<V> runnable) { | ||
this.contextHandler = contextHandler; | ||
this.context = context; | ||
this.runnable = runnable; | ||
} | ||
|
||
@Override | ||
public boolean isPeriodic() { | ||
return runnable.isPeriodic(); | ||
} | ||
|
||
@Override | ||
public long getDelay(TimeUnit unit) { | ||
return runnable.getDelay(unit); | ||
} | ||
|
||
@Override | ||
public int compareTo(Delayed o) { | ||
return runnable.compareTo(o); | ||
} | ||
|
||
@Override | ||
public void run() { | ||
if (contextHandler != null) { | ||
contextHandler.runWith(runnable, context); | ||
} else { | ||
runnable.run(); | ||
} | ||
} | ||
|
||
@Override | ||
public boolean cancel(boolean mayInterruptIfRunning) { | ||
return runnable.cancel(mayInterruptIfRunning); | ||
} | ||
|
||
@Override | ||
public boolean isCancelled() { | ||
return runnable.isCancelled(); | ||
} | ||
|
||
@Override | ||
public boolean isDone() { | ||
return runnable.isDone(); | ||
} | ||
|
||
@Override | ||
public V get() throws InterruptedException, ExecutionException { | ||
return runnable.get(); | ||
} | ||
|
||
@Override | ||
public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { | ||
return runnable.get(timeout, unit); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -546,14 +546,15 @@ public Object captureContext() { | |
|
||
@Override | ||
public void runWith(Runnable task, Object context) { | ||
if (context != null) { | ||
ContextInternal currentContext = (ContextInternal) Vertx.currentContext(); | ||
if (context != null && context != currentContext) { | ||
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. And now the philosophical question: should we distinguish root context and duplicated context? I do not believe so, but I think we should consider the question. 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. Duplicated contexts do not share the context-local data, so I would say we are running with a different context. 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. Those context are mostly duplicated contexts. When you duplicate a root context, their local data will also by propagated to the duplicate context, the only thing is that changing this local data won't change the root local data. |
||
// Only do context handling if it's non-null | ||
final ContextInternal vertxContext = (ContextInternal) context; | ||
vertxContext.beginDispatch(); | ||
try { | ||
task.run(); | ||
} finally { | ||
vertxContext.endDispatch(null); | ||
vertxContext.endDispatch(currentContext); | ||
} | ||
} else { | ||
task.run(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package io.quarkus.it.resteasy.mutiny.regression.bug25818; | ||
|
||
import javax.enterprise.context.ApplicationScoped; | ||
|
||
import io.vertx.core.Context; | ||
import io.vertx.core.Vertx; | ||
|
||
@ApplicationScoped | ||
public class BlockingService { | ||
|
||
public String getBlocking() { | ||
try { | ||
Thread.sleep(250); | ||
} catch (InterruptedException e) { | ||
throw new RuntimeException(e); | ||
} | ||
Context context = Vertx.currentContext(); | ||
if (context == null) { | ||
return "~~ context is null ~~"; | ||
} else { | ||
return "hello-" + context.getLocal("hello-target"); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package io.quarkus.it.resteasy.mutiny.regression.bug25818; | ||
|
||
import java.util.concurrent.TimeUnit; | ||
|
||
import javax.inject.Inject; | ||
import javax.ws.rs.GET; | ||
import javax.ws.rs.Path; | ||
import javax.ws.rs.Produces; | ||
import javax.ws.rs.core.MediaType; | ||
|
||
import org.jboss.logging.Logger; | ||
|
||
import io.smallrye.mutiny.Uni; | ||
import io.smallrye.mutiny.infrastructure.Infrastructure; | ||
import io.vertx.core.Context; | ||
import io.vertx.core.Vertx; | ||
|
||
@Path("/reproducer/25818") | ||
public class ReproducerResource { | ||
|
||
private final Logger logger = Logger.getLogger(ReproducerResource.class); | ||
|
||
@Inject | ||
BlockingService service; | ||
|
||
private void addToContext() { | ||
Vertx.currentContext().putLocal("hello-target", "you"); | ||
} | ||
|
||
@GET | ||
@Path("/worker-pool") | ||
@Produces(MediaType.TEXT_PLAIN) | ||
public Uni<String> workerPool() { | ||
logger.info("worker pool endpoint"); | ||
addToContext(); | ||
return Uni.createFrom() | ||
.item(service::getBlocking) | ||
.runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); | ||
} | ||
|
||
@GET | ||
@Path("/default-executor") | ||
@Produces(MediaType.TEXT_PLAIN) | ||
public Uni<String> defaultExecutor() { | ||
logger.info("default executor endpoint"); | ||
addToContext(); | ||
return Uni.createFrom() | ||
.item(service::getBlocking) | ||
.runSubscriptionOn(Infrastructure.getDefaultExecutor()); | ||
} | ||
|
||
@GET | ||
@Path("/worker-pool-submit") | ||
public Uni<String> workerPoolSubmit() { | ||
Vertx.currentContext().putLocal("yolo", "yolo"); | ||
return Uni.createFrom().emitter(emitter -> { | ||
Infrastructure.getDefaultWorkerPool().submit(() -> { | ||
Context ctx = Vertx.currentContext(); | ||
if (ctx != null) { | ||
emitter.complete("yolo -> " + ctx.getLocal("yolo")); | ||
} else { | ||
emitter.complete("Context was null"); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
@GET | ||
@Path("/worker-pool-schedule") | ||
public Uni<String> workerPoolSchedule() { | ||
Vertx.currentContext().putLocal("yolo", "yolo"); | ||
return Uni.createFrom().emitter(emitter -> { | ||
Infrastructure.getDefaultWorkerPool().schedule(() -> { | ||
Context ctx = Vertx.currentContext(); | ||
if (ctx != null) { | ||
emitter.complete("yolo -> " + ctx.getLocal("yolo")); | ||
} else { | ||
emitter.complete("Context was null"); | ||
} | ||
}, 25, TimeUnit.MILLISECONDS); | ||
}); | ||
} | ||
} |
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.
If we schedule something, inside a route handler the Vert.x context won't be propagated, just when
execute
is called, that would mean schedule it to run right away... If someone use theInfrastructure.getDefaultWorkerPool
and call any of the schedule methods it won't propagate the vert.x context.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.
I'm trying a variation of your code (we won't add another parameter to
Infrastructure
in Mutiny) but I get an error testing the Mutiny extension:Other than that I have added test cases calling
submit
andschedule
, context does propagate with your changes to the Vert.x extension.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.
Wrapping in an
Optional<...>
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.
Yup, in the mutiny processor the runtimeInit needs to take an
Optional<ContextHandlerBuildItem> contextHandlerBuildItem
and need to update the code when there is no context handlerThere 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.
Making it runtimeInit accept optional:
Creating the right MutinyScheduler if context handler is not null:
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.
Pushed it to my branch, if you are basing on something there: main...luneo7:mutiny-executors-simple
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.
I already have something like this locally 👍
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.
I just let a
null
handler being forwarded, and just callRunnable::run()
whennull
inCRSF