diff --git a/scm-core/src/main/java/sonia/scm/repository/FileObject.java b/scm-core/src/main/java/sonia/scm/repository/FileObject.java index 2f3241bf40..acb7559094 100644 --- a/scm-core/src/main/java/sonia/scm/repository/FileObject.java +++ b/scm-core/src/main/java/sonia/scm/repository/FileObject.java @@ -226,6 +226,10 @@ public class FileObject implements LastModifiedAware, Serializable return partialResult; } + public boolean isComputationAborted() { + return computationAborted; + } + //~--- set methods ---------------------------------------------------------- /** @@ -310,6 +314,10 @@ public class FileObject implements LastModifiedAware, Serializable this.partialResult = partialResult; } + public void setComputationAborted(boolean computationAborted) { + this.computationAborted = computationAborted; + } + public Collection getChildren() { return unmodifiableCollection(children); } @@ -348,6 +356,8 @@ public class FileObject implements LastModifiedAware, Serializable private boolean partialResult = false; + private boolean computationAborted = false; + /** sub repository informations */ @XmlElement(name = "subrepository") private SubRepository subRepository; 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 index 55fdbcacdb..0cd3200736 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutor.java @@ -5,10 +5,14 @@ import java.util.function.Consumer; public interface SyncAsyncExecutor { default ExecutionType execute(Runnable runnable) { - return execute(ignored -> runnable.run()); + return execute(ignored -> runnable.run(), () -> {}); } - ExecutionType execute(Consumer runnable); + default ExecutionType execute(Runnable runnable, Runnable abortionFallback) { + return execute(ignored -> runnable.run(), abortionFallback); + } + + ExecutionType execute(Consumer runnable, Runnable abortionFallback); boolean hasExecutedAllSynchronously(); 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 06b724d820..de479a4250 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 @@ -133,7 +133,7 @@ public class GitBrowseCommand extends AbstractGitCommand 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()); @@ -248,6 +248,11 @@ public class GitBrowseCommand extends AbstractGitCommand logger.warn("could not find latest commit for {} on {}", path, revId); } + }, () -> { + file.setPartialResult(false); + file.setComputationAborted(true); + request.updateCache(browserResult); + logger.info("updated browser result for repository {}", repository.getNamespaceAndName()); }); } } 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 index aa7e7bb954..6c24e72b69 100644 --- a/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java +++ b/scm-test/src/main/java/sonia/scm/repository/spi/SyncAsyncExecutors.java @@ -14,7 +14,7 @@ public final class SyncAsyncExecutors { public static SyncAsyncExecutor synchronousExecutor() { return new SyncAsyncExecutor() { @Override - public ExecutionType execute(Consumer runnable) { + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { runnable.accept(SYNCHRONOUS); return SYNCHRONOUS; } @@ -32,7 +32,7 @@ public final class SyncAsyncExecutors { return new SyncAsyncExecutor() { @Override - public ExecutionType execute(Consumer runnable) { + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { executor.execute(() -> runnable.accept(ASYNCHRONOUS)); return ASYNCHRONOUS; } @@ -45,12 +45,13 @@ public final class SyncAsyncExecutors { } public static AsyncExecutorStepper stepperAsynchronousExecutor() { - - Executor executor = Executors.newSingleThreadExecutor(); - Semaphore enterSemaphore = new Semaphore(0); - Semaphore exitSemaphore = new Semaphore(0); - return new AsyncExecutorStepper() { + + Executor executor = Executors.newSingleThreadExecutor(); + Semaphore enterSemaphore = new Semaphore(0); + Semaphore exitSemaphore = new Semaphore(0); + boolean timedOut = false; + @Override public void close() { enterSemaphore.release(Integer.MAX_VALUE/2); @@ -58,15 +59,19 @@ public final class SyncAsyncExecutors { } @Override - public ExecutionType execute(Consumer runnable) { + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { executor.execute(() -> { try { enterSemaphore.acquire(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - runnable.accept(ASYNCHRONOUS); - exitSemaphore.release(); + if (timedOut) { + abortionFallback.run(); + } else { + runnable.accept(ASYNCHRONOUS); + exitSemaphore.release(); + } }); return ASYNCHRONOUS; } @@ -81,6 +86,12 @@ public final class SyncAsyncExecutors { } } + @Override + public void timeout() { + timedOut = true; + close(); + } + @Override public boolean hasExecutedAllSynchronously() { return true; @@ -90,5 +101,7 @@ public final class SyncAsyncExecutors { public interface AsyncExecutorStepper extends SyncAsyncExecutor, Closeable { void next(); + + void timeout(); } } diff --git a/scm-ui/ui-types/src/Sources.ts b/scm-ui/ui-types/src/Sources.ts index 7b80b9df94..f5fca71d65 100644 --- a/scm-ui/ui-types/src/Sources.ts +++ b/scm-ui/ui-types/src/Sources.ts @@ -17,6 +17,7 @@ export type File = { lastModified?: string; subRepository?: SubRepository; // TODO partialResult: boolean; + computationAborted: boolean; _links: Links; _embedded: { children: File[] | null | undefined; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index df6c534ece..5a357efd95 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -101,7 +101,9 @@ "length": "Größe", "lastModified": "Zuletzt bearbeitet", "description": "Beschreibung", - "branch": "Branch" + "branch": "Branch", + "notYetComputed": "Noch nicht berechnet; Der Wert wird in Kürze aktualisiert", + "computationAborted": "Die Berechnung dauert zu lange und wurde abgebrochen" }, "content": { "historyButton": "History", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 103e30b825..c1e7a835cd 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -101,7 +101,9 @@ "length": "Length", "lastModified": "Last modified", "description": "Description", - "branch": "Branch" + "branch": "Branch", + "notYetComputed": "Not yet computed, will be updated in a short while", + "computationAborted": "The computation took too long and was aborted" }, "content": { "historyButton": "History", diff --git a/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx index 961d65c043..037d8f7c45 100644 --- a/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/components/FileTreeLeaf.tsx @@ -4,10 +4,12 @@ import classNames from "classnames"; import styled from "styled-components"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { File } from "@scm-manager/ui-types"; -import { DateFromNow, FileSize } from "@scm-manager/ui-components"; +import { DateFromNow, FileSize, Tooltip } from "@scm-manager/ui-components"; import FileIcon from "./FileIcon"; +import { Icon } from "@scm-manager/ui-components/src"; +import { WithTranslation, withTranslation } from "react-i18next"; -type Props = { +type Props = WithTranslation & { file: File; baseUrl: string; }; @@ -31,7 +33,7 @@ export function createLink(base: string, file: File) { return link; } -export default class FileTreeLeaf extends React.Component { +class FileTreeLeaf extends React.Component { createLink = (file: File) => { return createLink(this.props.baseUrl, file); }; @@ -58,6 +60,25 @@ export default class FileTreeLeaf extends React.Component { return {file.name}; }; + contentIfPresent = (file: File, content: any) => { + const { t } = this.props; + if (file.computationAborted) { + return ( + + + + ); + } else if (file.partialResult) { + return ( + + + + ); + } else { + return content; + } + }; + render() { const { file } = this.props; @@ -68,10 +89,10 @@ export default class FileTreeLeaf extends React.Component { {this.createFileIcon(file)} {this.createFileName(file)} {fileSize} - - - - {file.description} + {this.contentIfPresent(file, )} + + {this.contentIfPresent(file, file.description)} + {binder.hasExtension("repos.sources.tree.row.right") && ( {!file.directory && ( @@ -89,3 +110,5 @@ export default class FileTreeLeaf extends React.Component { ); } } + +export default withTranslation("repos")(FileTreeLeaf); diff --git a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts index b989e18b34..7d2b8c94a7 100644 --- a/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts +++ b/scm-ui/ui-webapp/src/repos/sources/modules/sources.ts @@ -29,7 +29,7 @@ export function fetchSourcesWithoutOptionalLoadingState( .then((sources: File) => { dispatch(fetchSourcesSuccess(repository, revision, path, sources)); if (sources._embedded.children && sources._embedded.children.find(c => c.partialResult)) { - setTimeout(() => dispatch(fetchSourcesWithoutOptionalLoadingState(repository, revision, path, false)), 1000); + setTimeout(() => dispatch(fetchSourcesWithoutOptionalLoadingState(repository, revision, path, false)), 3000); } }) .catch(err => { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java index 626af4bac5..4676a0fb03 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectDto.java @@ -28,6 +28,7 @@ public class FileObjectDto extends HalRepresentation { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String revision; private boolean partialResult; + private boolean computationAborted; public FileObjectDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java index 326b329218..6bdfa2d39f 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutor.java @@ -4,6 +4,7 @@ import sonia.scm.repository.spi.SyncAsyncExecutor; import java.time.Instant; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS; @@ -11,18 +12,35 @@ import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONO public class DefaultSyncAsyncExecutor implements SyncAsyncExecutor { + public static final long DEFAULT_MAX_ASYNC_RUNTIME = 60 * 1000L; + private final Executor executor; private final Instant switchToAsyncTime; + private final long maxAsyncRuntime; + private AtomicLong asyncRuntime = new AtomicLong(0L); private boolean executedAllSynchronously = true; DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime) { - this.executor = executor; - this.switchToAsyncTime = switchToAsyncTime; + this(executor, switchToAsyncTime, DEFAULT_MAX_ASYNC_RUNTIME); } - public ExecutionType execute(Consumer runnable) { + DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime, long maxAsyncRuntime) { + this.executor = executor; + this.switchToAsyncTime = switchToAsyncTime; + this.maxAsyncRuntime = maxAsyncRuntime; + } + + public ExecutionType execute(Consumer runnable, Runnable abortionFallback) { if (Instant.now().isAfter(switchToAsyncTime)) { - executor.execute(() -> runnable.accept(ASYNCHRONOUS)); + executor.execute(() -> { + if (asyncRuntime.get() < maxAsyncRuntime) { + long chunkStartTime = System.currentTimeMillis(); + runnable.accept(ASYNCHRONOUS); + asyncRuntime.addAndGet(System.currentTimeMillis() - chunkStartTime); + } else { + abortionFallback.run(); + } + }); executedAllSynchronously = false; return ASYNCHRONOUS; } else { diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java index bd4148606b..6baca6204e 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultSyncAsyncExecutorTest.java @@ -13,26 +13,40 @@ import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONO class DefaultSyncAsyncExecutorTest { ExecutionType calledWithType = null; + boolean aborted = false; @Test void shouldExecuteSynchronouslyBeforeTimeout() { DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.MAX); - ExecutionType result = executor.execute(type -> calledWithType = type); + ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); assertThat(result).isEqualTo(SYNCHRONOUS); assertThat(calledWithType).isEqualTo(SYNCHRONOUS); assertThat(executor.hasExecutedAllSynchronously()).isTrue(); + assertThat(aborted).isFalse(); } @Test void shouldExecuteAsynchronouslyAfterTimeout() { DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS)); - ExecutionType result = executor.execute(type -> calledWithType = type); + ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); assertThat(result).isEqualTo(ASYNCHRONOUS); assertThat(calledWithType).isEqualTo(ASYNCHRONOUS); assertThat(executor.hasExecutedAllSynchronously()).isFalse(); + assertThat(aborted).isFalse(); + } + + @Test + void shouldCallFallbackAfterAbortion() { + DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS), 0L); + + ExecutionType result = executor.execute(type -> calledWithType = type, () -> aborted = true); + + assertThat(result).isEqualTo(ASYNCHRONOUS); + assertThat(calledWithType).isNull(); + assertThat(aborted).isTrue(); } }