micronaut-projects / micronaut-core

Micronaut Application Framework
http://micronaut.io
Apache License 2.0
6k stars 1.04k forks source link

Ability to await for execution of all @Async methods #7858

Open musketyr opened 1 year ago

musketyr commented 1 year ago

Feature description

We are trying to move non-business related code into async event listeners but when we are running AWS functions then some of the methods annotated with @Async are not executed because the Lambda is put into sleep mode before the code is finished. We we able to make this work with a following snippet:

@Named(TaskExecutors.SCHEDULED) @Inject ExecutorService executorService

executorService.shutdown()
executorService.awaitTermination(5, TimeUnit.SECONDS)

This is pretty primitive approach and I wonder if there could be some bean which would provide such an interface which is more reliable (e.g. will work out of box with any other custom ExecutorService). I'm also bit worried what will happen if the Lambda is reused.

dstepanov commented 1 year ago

Shutting down the executor service will break the runtime when the lambda is reused. You might want to create a custom ExecutorService that will collect all the tasks and await their completion before the lambda is done.

musketyr commented 1 year ago

Thanks for fast response. Is this something which can be done directly in Micronaut libraries, ideally somewhere inside AsyncInterceptor? I believe this would be helpful for every Micronaut user.

dstepanov commented 1 year ago

Maybe somewhere in the Lambda project, pretty much you just need a custom scheduler:

@Singleton
@Named("LambdaScheduler")
class LambdaSchedulerScheduler implements ExecutorService {
   ///...

   void awaitTasksCompletion() { ... }

}

@Async("LambdaScheduler")
void myAsyncMethod() {...}

void someLambdaWork() {
  //...
  lambdaSchedulerScheduler.awaitTasksCompletion();
}

I don't know much about Lamdas implementation, but assuming the context is reused. I haven't found any events that would be automatically triggered at the end of the invocation, those would have been helpful to do the await.

musketyr commented 1 year ago

Yes, the context is reused and you were right about the shutdown. I have created a test function and it looks like your suggestion with registering the futures works. So the question is if you would like to add something like this directly inside micronaut-function or should I create our own standalone library.

https://github.com/musketyr/micronaut-function-async-support/tree/master/src/main/java/com/example/async

dstepanov commented 1 year ago

It might be a good idea to have it first as a part of micronaut-aws and then maybe to move it to micronaut-function. WDYT @graemerocher Also, it would be nice to introduce a new event being triggered after the lambda function invocation, and this new scheduler can have a listener and do the awaiting automatically.

BTW: you can use CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) in the scheduler and CompletableFuture<Void> allOf to implement await.

musketyr commented 1 year ago

Also, it would be nice to introduce a new event being triggered after the lambda function invocation, and this new scheduler can have a listener and do the awaiting automatically.

If we stick directly to the io.micronaut.function.aws.MicronautRequestHandler and not more generic io.micronaut.function.executor.FunctionInitializer (which we are historically using - I believe one time it was a favorite way how to write handlers) then yes, it would be good if a sync event was fired at the end of execution of handleRequest method.

    @Override
    public final O handleRequest(I input, Context context) {
        HandlerUtils.configureWithContext(this, context);
        if (!inputType.isInstance(input)) {
            input = convertInput(input);
        }
        O output = this.execute(input);
        publishExecutionFinishedEvent(input, output);
        return output;
    }

The same applies for io.micronaut.function.aws.MicronautRequestStreamHandler. It should not be a big deal for us to migrate our functions. Once there is a simple way for post-execute hooks then we can simplify a lot of our code, including for example Sentry Integration where we need to wait for flushing the events.

BTW: you can use CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) in the scheduler and CompletableFuture<Void> allOf to implement await.

Thanks for a tip!

musketyr commented 1 year ago

I have started the work on the event here https://github.com/micronaut-projects/micronaut-aws/pull/1433

musketyr commented 1 year ago

Just a few more details why we would like to keep @Async even in Lambda environment. We simply have a services which are shared between Micronaut applications and Micronaut functions and we don't want to create pairs of duplicates for sync/async invocations.

dstepanov commented 1 year ago

In that case, you might want to wrap all the executors in the Lamba environment using a bean listener like this https://github.com/micronaut-projects/micronaut-core/blob/3.6.x/context/src/main/java/io/micronaut/scheduling/instrument/ExecutorServiceInstrumenter.java and await the completion.

musketyr commented 1 year ago

Thanks for the link! It may be really handly!

musketyr commented 1 year ago

Just one radical idea. Would it be possible to ignore @Async annotation/interceptor and make publishEventAsync call synchronous when AWS Lambda function environment is detected?

cprashantgupta commented 11 months ago

👍

Any way this work can be progressed.

We are facing same issue with Application hosted as Lambda Functions where is the service response stops lambda execution and do not wait for ASYNC methods to finish.