diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java new file mode 100644 index 0000000000..86d37814da --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java @@ -0,0 +1,43 @@ +package sonia.scm.repository.spi; + +import java.time.Instant; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS; +import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONOUS; + +public class SyncAsyncExecutor { + + private final Executor executor; + private final Instant switchToAsyncTime; + private boolean executedAllSynchronously = true; + + SyncAsyncExecutor(Executor executor, Instant switchToAsyncTime) { + this.executor = executor; + this.switchToAsyncTime = switchToAsyncTime; + } + + public ExecutionType execute(Runnable runnable) { + return execute(ignored -> runnable.run()); + } + + public ExecutionType execute(Consumer runnable) { + if (Instant.now().isAfter(switchToAsyncTime)) { + executor.execute(() -> runnable.accept(ASYNCHRONOUS)); + executedAllSynchronously = false; + return ASYNCHRONOUS; + } else { + runnable.accept(SYNCHRONOUS); + return SYNCHRONOUS; + } + } + + public boolean hasExecutedAllSynchronously() { + return executedAllSynchronously; + } + + public enum ExecutionType { + SYNCHRONOUS, ASYNCHRONOUS + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java new file mode 100644 index 0000000000..2c576f814f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutorProvider.java @@ -0,0 +1,29 @@ +package sonia.scm.repository.spi; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class SyncAsyncExecutorProvider { + + private static final int DEFAULT_TIMEOUT_SECONDS = 2; + + private final Executor executor; + + public SyncAsyncExecutorProvider() { + this(Executors.newFixedThreadPool(4)); + } + + public SyncAsyncExecutorProvider(Executor executor) { + this.executor = executor; + } + + public SyncAsyncExecutor createExecutorWithDefaultTimeout() { + return createExecutorWithSecondsToTimeout(DEFAULT_TIMEOUT_SECONDS); + } + + public SyncAsyncExecutor createExecutorWithSecondsToTimeout(int seconds) { + return new SyncAsyncExecutor(executor, Instant.now().plus(seconds, ChronoUnit.SECONDS)); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index a82a592497..670b43dd5c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -70,9 +70,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import static java.util.Optional.empty; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -98,55 +95,46 @@ public class GitBrowseCommand extends AbstractGitCommand LoggerFactory.getLogger(GitBrowseCommand.class); private final LfsBlobStoreFactory lfsBlobStoreFactory; - private ExecutorService executorService; + private final SyncAsyncExecutor executor; //~--- constructors --------------------------------------------------------- /** * Constructs ... - * @param context + * @param context * @param repository * @param lfsBlobStoreFactory + * @param executor */ - public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) + public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory, SyncAsyncExecutor executor) { super(context, repository); this.lfsBlobStoreFactory = lfsBlobStoreFactory; + this.executor = executor; } //~--- get methods ---------------------------------------------------------- @Override - @SuppressWarnings("unchecked") public BrowserResult getBrowserResult(BrowseCommandRequest request) throws IOException { - executorService = Executors.newSingleThreadExecutor(); - try { - logger.debug("try to create browse result for {}", request); + logger.debug("try to create browse result for {}", request); - org.eclipse.jgit.lib.Repository repo = open(); - ObjectId revId = computeRevIdToBrowse(request, repo); + org.eclipse.jgit.lib.Repository repo = open(); + ObjectId revId = computeRevIdToBrowse(request, repo); - if (revId != null) { - BrowserResult browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); - executorService.execute(() -> { + if (revId != null) { + BrowserResult browserResult = new BrowserResult(revId.getName(), request.getRevision(), getEntry(repo, request, revId)); + executor.execute(executionType -> { + if (executionType == SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS) { request.updateCache(browserResult); logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); - }); - return browserResult; - } else { - logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); - return new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot()); - } - } finally { - executorService.shutdown(); - try { - if (!executorService.awaitTermination(request.getComputationTimeoutMilliSeconds(), TimeUnit.MILLISECONDS)) { - logger.info("lookup of all commits aborted after {}ms in repo {}", request.getComputationTimeoutMilliSeconds(), repository.getNamespaceAndName()); } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } + }); + return browserResult; + } else { + logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName()); + return new BrowserResult(Constants.HEAD, request.getRevision(), createEmtpyRoot()); } } @@ -219,7 +207,7 @@ public class GitBrowseCommand extends AbstractGitCommand // don't show message and date for directories to improve performance if (!file.isDirectory() &&!request.isDisableLastCommit()) { - executorService.execute(() -> { + executor.execute(() -> { logger.trace("fetch last commit for {} at {}", path, revId.getName()); RevCommit commit = getLatestCommit(repo, revId, path); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index e1ae58ada5..fa54ca6007 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -80,12 +80,13 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- - public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { + public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) { this.handler = handler; this.repository = repository; this.lfsBlobStoreFactory = lfsBlobStoreFactory; this.hookContextFactory = hookContextFactory; this.eventBus = eventBus; + this.executorProvider = executorProvider; this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); } @@ -150,7 +151,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public BrowseCommand getBrowseCommand() { - return new GitBrowseCommand(context, repository, lfsBlobStoreFactory); + return new GitBrowseCommand(context, repository, lfsBlobStoreFactory, executorProvider.createExecutorWithDefaultTimeout()); } /** @@ -301,4 +302,6 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider private final HookContextFactory hookContextFactory; private final ScmEventBus eventBus; + + private final SyncAsyncExecutorProvider executorProvider; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java index 7fc5fb27c4..1bb7e84b92 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java @@ -55,14 +55,16 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { private final LfsBlobStoreFactory lfsBlobStoreFactory; private final HookContextFactory hookContextFactory; private final ScmEventBus eventBus; + private final SyncAsyncExecutorProvider executorProvider; @Inject - public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { + public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus, SyncAsyncExecutorProvider executorProvider) { this.handler = handler; this.storeProvider = storeProvider; this.lfsBlobStoreFactory = lfsBlobStoreFactory; this.hookContextFactory = hookContextFactory; this.eventBus = eventBus; + this.executorProvider = executorProvider; } @Override @@ -70,7 +72,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { GitRepositoryServiceProvider provider = null; if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus); + provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus, executorProvider); } return provider; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 61d877bd4a..40dfdf2159 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -44,6 +44,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static sonia.scm.repository.spi.SyncAsyncExecutors.synchronousExecutor; /** * Unit tests for {@link GitBrowseCommand}. @@ -171,6 +172,6 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } private GitBrowseCommand createCommand() { - return new GitBrowseCommand(createContext(), repository, null); + return new GitBrowseCommand(createContext(), repository, null, synchronousExecutor()); } } diff --git a/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java new file mode 100644 index 0000000000..9f64a39474 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java @@ -0,0 +1,10 @@ +package sonia.scm.repository.spi; + +import java.time.Instant; + +public final class SyncAsyncExecutors { + + public static SyncAsyncExecutor synchronousExecutor() { + return new SyncAsyncExecutor(Runnable::run, Instant.MAX); + } +}