Skip to content

[MRESOLVER-283] Shared executor service#213

Closed
cstamas wants to merge 3 commits intoapache:masterfrom
cstamas:shared-executors
Closed

[MRESOLVER-283] Shared executor service#213
cstamas wants to merge 3 commits intoapache:masterfrom
cstamas:shared-executors

Conversation

@cstamas
Copy link
Copy Markdown
Member

@cstamas cstamas commented Nov 2, 2022

More and more component in resolver does parallel processing (BF collector, MD resolver, basic connector), and they all create, maintain their own executor instance.

Instead of this, create one shared service component and just reuse it accross resolver.


https://issues.apache.org/jira/browse/MRESOLVER-283

@cstamas cstamas self-assigned this Nov 2, 2022
@cstamas
Copy link
Copy Markdown
Member Author

cstamas commented Nov 2, 2022

@caiwei-ebay ping

@cstamas cstamas changed the title Shared executor [MRESOLVER-283] Shared executor Nov 2, 2022
@caiwei-ebay
Copy link
Copy Markdown
Contributor

caiwei-ebay commented Nov 3, 2022

@cstamas

The PR looks good to me expect a few concerns as shown in comments. Thanks for the quick fix.
My main concern is about how to tell user pom/metadata.xml is downloaded in parallel now with BF collector.

As the special param "aether.dependencyCollector.bf.threads" has to be removed using SharedExecutor, could you put some lines to explain the parallel download behavior in param "aether.dependencyCollector.impl" where you've introduced BF and DF. With such information, user may try the BF and feedback so we can discuss whether BF could be the default based on the feedbacks.

{
command.run();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this being deleted, I think it may introduce additional cost for DF.
DF downloads pom one by one, originally it will be downloaded by main thread directly and now it will be downloaded in a thread and the main thread has to wait

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reintroduced direct strategy as well, two out of 3 users of resolver executor now relies on it.

Comment thread src/site/markdown/configuration.md
{
results.computeIfAbsent( ArtifactIdUtils.toId( artifact ),
key -> this.executorService.submit( callable ) );
key -> this.executor.submit( callable ) );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the illustration below:

https://camo.githubusercontent.com/71a9619274e478245df1c37423239b79f6b3036a3af72690d7ace08419ee21ac/68747470733a2f2f6973737565732e6170616368652e6f72672f6a6972612f7365637572652f6174746163686d656e742f31333034373931392f7265736f6c76655f646570732e706e67

As we submit a Callable here, and this callable finally resorts to RepositoryConnector which will then submit a Collection (a collection with single artifact) to the executor, 2 different tasks running in one executor and besides the 2 tasks has dependencies.

The original behavior is:

we submit a Callable here, and this callable finally resorts to RepositoryConnector which will then submit a Collection (a collection with single artifact) to the executor, but this time the executor is actually a DirectExecutor which actually runs in main thread. Please check code below.

if ( maxThreads <= 1 )
{
return DirectExecutor.INSTANCE;
}
int tasks = safe( artifacts ).size() + safe( metadatas ).size();
if ( tasks <= 1 )
{
return DirectExecutor.INSTANCE;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully I restored the original behaviour, please recheck (latest commit)

@cstamas cstamas marked this pull request as ready for review November 3, 2022 08:36
@cstamas cstamas requested review from caiwei-ebay and michael-o and removed request for caiwei-ebay November 3, 2022 08:46
@Override
public void submitOrDirect( Collection<Runnable> tasks )
{
requireNonNull( tasks );
Copy link
Copy Markdown
Contributor

@caiwei-ebay caiwei-ebay Nov 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When resolves a snapshot dependency or dependency with version range, Maven resolves metadata.xml from multiple repositories in parallel: https://github.com/apache/maven/blob/master/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java#L158

With original design, metadata.xml is using a separate pool, pom or jar can use RepositoryConnector's pool but pom downloading is in serial while jar is in parallel:

  • metadata.xml use a separate pool to download metadata.xml from multiple repositories, each downloading calls RepositoryConnector's get(null, Collections.singleList(MetadataDownload)), it actually uses DirectExecutor without leveraging RepositoryConnector's thread pool;
  • pom downloading directly calls RepositoryConnector's get(Collections.singleList(ArtifactDownload), null), so it also uses DirectExecutor without leveraging RepositoryConnector's thread pool;
  • jar downloading calls RepositoryConnector's get(List(ArtifactDownload), null), it do leverage RepositoryConnector's thread pool.

With this approach, metadata, pom and jar all share one pool:

Suppose we have multiple repositories like releases, snapshots, commercial, legacy, testing, etc. in our private Maven nexus repository and we need to resolve 5 dependencies (snapshot or with version range) in parallel.

When resolve with BFDependencyCollector, 5 tasks to resolve each dependency have used up all 5 threads, and due to resolving each dependency (let's name with task A) need to resolve metadata (task B), the metadata resolution task B will be also submitted to the same pool. Here A depends on B but B has to wait available threads. I think this could be lead to endless waiting, right?

As it is better to use separate thread pool for separate tasks, maybe the SharedExecutor here should have separate pool for separate tasks? At least, pom/jar downloading and BF collect (it also need to download pom but pom downloading actually uses directlyExecute with your recent fix, thus safe to use the same pool) can use one pool and metadata.xml downloading still use another pool? Please advice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like also add as I understand the artifact thread pools where per repo, one this is one per all repos?

I think we need a overview what executor services where created previously and which now to compare whether this could create a bottleneck.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bottleneck... or deadlock caused by thread exhaustion.

@cstamas
Copy link
Copy Markdown
Member Author

cstamas commented Nov 3, 2022

Reworked to:

  • have split pools (service can decide using service "key")
  • pools are stored in session, so each session has own pool(s)
  • this merely reflects what was before, but all is centralized.

@cstamas cstamas requested a review from caiwei-ebay November 3, 2022 17:05
Copy link
Copy Markdown
Contributor

@caiwei-ebay caiwei-ebay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR looks great to me. Thanks for tidy up the design.

@cstamas cstamas changed the title [MRESOLVER-283] Shared executor [MRESOLVER-283] Shared executor service Nov 4, 2022
@michael-o
Copy link
Copy Markdown
Member

Will try to review today

List<ChecksumAlgorithmFactory> checksumAlgorithmFactories = layout.getChecksumAlgorithmFactories();
Collection<? extends MetadataDownload> mds = safe( metadataDownloads );
Collection<? extends ArtifactDownload> ads = safe( artifactDownloads );
ArrayList<Runnable> runnable = new ArrayList<>( mds.size() + ads.size() );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runnables

this.providedChecksumsSources = providedChecksumsSources;
this.resolverExecutor = resolverExecutorService.getResolverExecutor( session, RepositoryConnector.class,
ConfigUtils.getInteger( session, CONFIG_PROP_THREADS_DEFAULT, CONFIG_PROP_THREADS,
"maven.artifact.threads" ) );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also deprecate this property since it does not reflect reality. MD is not artifacts, but this connector does both.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just remove it? We are going for 1.9 after all....

Executor executor = getExecutor( Math.min( tasks.size(), threads ) );
try
RunnableErrorForwarder errorForwarder = new RunnableErrorForwarder();
ArrayList<Runnable> runnable = new ArrayList<>( tasks.size() );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runnables

@cstamas cstamas requested a review from michael-o November 7, 2022 10:11
Copy link
Copy Markdown
Contributor

@gnodet gnodet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sharing executors must be done with caution.

Is there any way that the executor may be running out of threads to service a call ? For example, resolving multiple files in parallel, each file being resolved in its own thread, then downloading dependencies and running out of threads ?

I think using a more general ForkJoinPool which supports work stealing would be safer to avoid running into such problems.

More and more component in resolver does parallel
processing (BF collector, MD resolver, basic connector),
and they all create, maintain their own executor instance.

Instead of this, create one shared service component and
just reuse it accross resolver.

https://issues.apache.org/jira/browse/MRESOLVER-283
@cstamas
Copy link
Copy Markdown
Member Author

cstamas commented Nov 8, 2022

Sharing executors must be done with caution.

Is there any way that the executor may be running out of threads to service a call ? For example, resolving multiple files in parallel, each file being resolved in its own thread, then downloading dependencies and running out of threads ?

I think using a more general ForkJoinPool which supports work stealing would be safer to avoid running into such problems.

I kinda agree with @gnodet here, and will "tone down" this PR to NOT SHARE executors, merely will introduce this facade but instances will be handled as before (as even now, it is not 100% what it was before: collector had per arg instance, now shared). And I cannot certainly asnwer the question "Is there any way that the executor may be running out of threads to service a call".

OTOH, I disagree with forkjoinpool: all these threads will do (probably blocking, maybe inifinite) HTTP IO, not to mention the refactoring needed to use them (most of "client" code remained unchanged here, using plain Runnable and Callable).


Hence, this PR will change to merely "centralize executor creation and (probably) use ResolverExecutor shim instead directly exposing ExecutorService iface"...

@gnodet
Copy link
Copy Markdown
Contributor

gnodet commented Nov 8, 2022

OTOH, I disagree with forkjoinpool: all these threads will do (probably blocking, maybe inifinite) HTTP IO, not to mention the refactoring needed to use them (most of "client" code remained unchanged here, using plain Runnable and Callable).

ForkJoinPool is capable of handling blocking operations, just like a standard ThreadPoolExecutor. The only problem is when you don't use your own instance and reuse the system provided instance with blocking operations. The only difference between a ForkJoinPool and a ThreadPoolExecutor is that the former has more features such as thread affinity, work stealing and fork/join operations.

I think a single thread pool is a good idea, but it has to be implemented in a correct way to avoid any thread exhaustion. I.e. use a single fork/join pool and use the appropriate methods to dispatch all the work, which should ensure proper ordering of tasks execution. I've done a similar thing in apache/maven#803 (which I need to finish).

@cstamas
Copy link
Copy Markdown
Member Author

cstamas commented Nov 8, 2022

Ok, now I feel I should not push this anymore, as I agree now with use of forkJoinPool. Hence, will close this PR, as that change would be bigger, but I want 1.9.0 out as soon as possible....

@cstamas cstamas closed this Nov 8, 2022
@cstamas cstamas deleted the shared-executors branch November 8, 2022 10:35
@jira-importer
Copy link
Copy Markdown

Resolve #959

@jira-importer
Copy link
Copy Markdown

Resolve #959

@XenoAmess XenoAmess mentioned this pull request Jun 16, 2025
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants