[tl;dr] https://twitter.com/seinecle/status/1748615007363829827
Background: how is this part of the app architectured
This (free, open source) app helps users analyze their data with text mining and network analysis. Different operations are possible, they are called "functions".
Functions typically take 1 second to a few minutes to run. The function called "Cowo" is the one where I experiment new ways to make the wait and the experience as good to the user as I can, while the function is running. When I'll get it right, I'll model the other functions in the same way.
The architecture is quite exotic, but I stand by it. Here it is:
-
the user uploads one or several files containing the text they want to analyze. The text content is persisted on disk as a single file, the name of the file is a "data persistence id" which is unique to this dataset.
-
the user then navigates to the cowo.html page, where they can set a few options to fine tune how the function will run. On this page, they click on a CommandButton
"compute" which executes the runAnalysis()
method in CowoBean, which is a Session Scoped Bean hosting the fields and methods related to this function.
-
the method runAnalysis()
launches two methods in succession, each designed to return quickly, so the runAnalysis()
method returns almost immediately:
public void runAnalysis() {
progress = 0;
runButtonDisabled = true;
gexfHasArrived = false;
sendCallToCowoFunction();
getTopNodes();
}
sendCallToCowoFunction()
sends a GET request to the API endpoint (locally hosted) which runs the actual function Cowo. The API endpoint executes the function asynchronously, so the GET request returns almost immediately.
The results of the function are stored on disk with a file.
getTopNodes()
is used to extract a sample of the results returned by Cowo. The method creates a new thread and executes all its code in this new thread, so the method returns immediately.
Communication and orchestration between the front-end and the different parts of the backend
How are all these different steps orchestrated, and how is the user alerted that the end results have arrived?
Things that are weird (but I am ok with them)
- using the file system (instead of db) as a way to store intermediary results and share them between processes
- having functions located in different sub-systems sending messages through http calls, and having an Application Scoped "WatchTower" Bean centralizing these messages.
I see it as a way to avoid using frameworks that would professionally handle of all of this. I have a taste for keeping things as framework-free as a I can.
Things that are not satisfying (and finally, the question!)
- I don't like polling from the front-end to wait for results and execute things when they arrive. This is largely a matter of taste. I would prefer the backend to execute the navigation by itself when it has finished running the function.
- and this is where I have a question: spinning a new thread running an asynch method, it would be able to return to the original Session Scoped when the results have arrived, with a callback. And the Session Scoped bean would then do a page redirect!. But the callback function cannot execute a page navigation because somehow, the state of the session is not recognized/ maintained / reconciled? Here is:
@Stateless
public class LongRunningProcessBean {
@Asynchronous
public void executeLongRunningOperation(Consumer<String> callback) {
// retrieve topNodes...
// Invoke the callback when top nodes have bee retrieved
callback.accept("success!");
}
}
@Named
@SessionScoped
public class CowoBean {
@EJB
private LongRunningProcessBean longRunningProcessBean;
public void startLongRunningOperation() {
longRunningProcessBean.executeLongRunningOperation(this::onOperationComplete);
}
private void onOperationComplete(Strint result) {
FacesContext context = FacesContext.getCurrentInstance(); // **error, not able to retrieve Context**
context.getApplication().getNavigationHandler().handleNavigation(context, null, "/cowo/results.xhtml?faces-redirect=true");
}
}
If that would work, that would be more elegant than a polling or a while () loop running and waiting for a result, right?
But FacesContext context = FacesContext.getCurrentInstance();
returns an error because the multi-thread design doesn't go well with the single thread model of JSF (or is it JakartaEE?). As a solution, it might be possible to:
- pass on an httpsession object to the asynch method
startLongRunningOperation()
- having this same httpsession object passed back to the callback
- using this httpsession object to retrieve the original, correct FacesContext.getCurrentInstance(); and hence enable a navigation to whatever page, maintaining the state of this user session.
But I don't have a clear view on how doing that. Thanks for your help!