[Fix #1611] Improving service loaders performance - #1615
Conversation
…ormance Signed-off-by: Francisco Javier Tirado Sarti <ftirados@ibm.com>
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Improves executor “service loader” performance by caching ServiceLoader results instead of repeatedly scanning the classpath on each builder/executor creation.
Changes:
- Cache
HttpRequestDecoratorlists per application id inHttpExecutorBuilder. - Eagerly load/sort
RunnableTaskBuilderimplementations once inRunTaskExecutor. - Eagerly load/sort
ScriptRunnerimplementations once inRunScriptExecutorBuilder.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java | Adds per-application caching of request decorators to avoid repeated ServiceLoader scans. |
| impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunTaskExecutor.java | Preloads/sorts runnable task builders once to reduce repeated provider instantiation/sorting. |
| impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutorBuilder.java | Preloads/sorts script runners once to avoid repeated ServiceLoader scans in build(). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…en application is created Signed-off-by: Francisco Javier Tirado Sarti <ftirados@ibm.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Suppressed comments (3)
impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java:723
ServiceLoader.load(clazz)uses the thread context classloader (TCCL). With the new caching, whichever TCCL is present on the first call effectively becomes 'sticky' for the lifetime of the application cache, which can lead to missing/incorrect service discovery in container/plugin environments. Consider using a stable classloader (e.g.,ServiceLoader.load(clazz, WorkflowApplication.class.getClassLoader())or a classloader stored onWorkflowApplication) so discovery is deterministic across threads.
@SuppressWarnings("unchecked")
public <T> List<T> serviceLoadedClasses(Class<T> clazz) {
return (List<T>)
serviceLoadedClasses.computeIfAbsent(
clazz,
c ->
ServiceLoader.load(clazz).stream()
.map(ServiceLoader.Provider::get)
.sorted()
.toList());
}
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java:38
requestDecoratorswas changed fromfinalto non-final, but in this diff it’s still assigned only in the constructor. If the builder doesn’t reassign the list elsewhere, keeping itfinal(build the list in a local variable, then assign once) helps preserve immutability guarantees and reduces accidental reassignment risk.
private List<HttpRequestDecorator> requestDecorators;
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java:50
requestDecoratorswas changed fromfinalto non-final, but in this diff it’s still assigned only in the constructor. If the builder doesn’t reassign the list elsewhere, keeping itfinal(build the list in a local variable, then assign once) helps preserve immutability guarantees and reduces accidental reassignment risk.
this.requestDecorators =
new ArrayList<>(definition.application().serviceLoadedClasses(HttpRequestDecorator.class));
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java:38
requestDecoratorsno longer appears to be reassigned after construction (it’s still set once in the constructor). Keeping itfinalwould better communicate immutability/intent and prevent accidental reassignment later.
private List<HttpRequestDecorator> requestDecorators;
Signed-off-by: Francisco Javier Tirado Sarti <ftirados@ibm.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
impl/core/src/main/java/io/serverlessworkflow/impl/auth/CommonOAuthProvider.java:99
- In the secret-based resolver,
app.serviceLoadedClass(...)is executed every time theWorkflowValueResolveris evaluated. Even with caching, this adds repeated lookups and repeats the empty-list check + exception path setup on every call. Prefer resolvingAccessTokenProviderFactoryandJWTConverteronce (outside the returned lambda) and capturing them in the closure so the resolver only performs the secret parsing and token-provider build.
String secretName, AuthRequestBuilder authBuilder, WorkflowApplication app) {
return (w, t, m) -> {
Map<String, Object> secret = secret(w, secretName);
String issuers = (String) secret.get("issuers");
return app.serviceLoadedClass(AccessTokenProviderFactory.class)
.build(
authBuilder.apply(secret),
issuers != null ? Arrays.asList(issuers.split(",")) : null,
app.serviceLoadedClass(JWTConverter.class));
};
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java:38
- If
requestDecoratorsis only assigned in the constructor (as shown here), it should remainfinalto preserve immutability and make the builder state easier to reason about. Suggested fix: restoreprivate final List<HttpRequestDecorator> requestDecorators;and keep the current initialization.
private List<HttpRequestDecorator> requestDecorators;
impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java:50
- If
requestDecoratorsis only assigned in the constructor (as shown here), it should remainfinalto preserve immutability and make the builder state easier to reason about. Suggested fix: restoreprivate final List<HttpRequestDecorator> requestDecorators;and keep the current initialization.
this.requestDecorators =
new ArrayList<>(definition.application().serviceLoadedClasses(HttpRequestDecorator.class));
impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java:714
- The
T extends Comparable<?>bound forces every SPI type loaded through these helpers to implementComparable, even when ordering is not semantically required (e.g., singleton helpers whereserviceLoadedClass(...)just returns the first). This makes the API more restrictive than necessary and can block future SPI extensions. Consider removing the bound and either (a) not sorting by default, (b) sorting only whenT instanceof Comparable, or (c) providing an overload that accepts aComparator<T>for the cases that require deterministic ordering.
public <T extends Comparable<?>> List<T> serviceLoadedClasses(Class<T> clazz) {
impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java:725
- The
T extends Comparable<?>bound forces every SPI type loaded through these helpers to implementComparable, even when ordering is not semantically required (e.g., singleton helpers whereserviceLoadedClass(...)just returns the first). This makes the API more restrictive than necessary and can block future SPI extensions. Consider removing the bound and either (a) not sorting by default, (b) sorting only whenT instanceof Comparable, or (c) providing an overload that accepts aComparator<T>for the cases that require deterministic ordering.
public <T extends Comparable<?>> T serviceLoadedClass(Class<T> serviceClass) {
impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java:719
- Inside
computeIfAbsent, the lambda parameter iscbut the code uses the outer variableclazz. Usingcwould make it clearer that the load is based on the computed key and avoids accidental capture if this code is refactored. Suggested fix: replaceServiceLoader.load(clazz)withServiceLoader.load(c).
serviceLoadedClasses.computeIfAbsent(
clazz,
c ->
ServiceLoader.load(clazz).stream()
Fix #1611