Let background computations abort for browse command

This commit is contained in:
Rene Pfeuffer
2019-12-12 11:47:03 +01:00
parent f7dc89ee81
commit 8df43e7b4e
12 changed files with 122 additions and 29 deletions

View File

@@ -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<FileObject> 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;

View File

@@ -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<ExecutionType> runnable);
default ExecutionType execute(Runnable runnable, Runnable abortionFallback) {
return execute(ignored -> runnable.run(), abortionFallback);
}
ExecutionType execute(Consumer<ExecutionType> runnable, Runnable abortionFallback);
boolean hasExecutedAllSynchronously();

View File

@@ -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());
});
}
}

View File

@@ -14,7 +14,7 @@ public final class SyncAsyncExecutors {
public static SyncAsyncExecutor synchronousExecutor() {
return new SyncAsyncExecutor() {
@Override
public ExecutionType execute(Consumer<ExecutionType> runnable) {
public ExecutionType execute(Consumer<ExecutionType> runnable, Runnable abortionFallback) {
runnable.accept(SYNCHRONOUS);
return SYNCHRONOUS;
}
@@ -32,7 +32,7 @@ public final class SyncAsyncExecutors {
return new SyncAsyncExecutor() {
@Override
public ExecutionType execute(Consumer<ExecutionType> runnable) {
public ExecutionType execute(Consumer<ExecutionType> runnable, Runnable abortionFallback) {
executor.execute(() -> runnable.accept(ASYNCHRONOUS));
return ASYNCHRONOUS;
}
@@ -45,12 +45,13 @@ public final class SyncAsyncExecutors {
}
public static AsyncExecutorStepper stepperAsynchronousExecutor() {
return new AsyncExecutorStepper() {
Executor executor = Executors.newSingleThreadExecutor();
Semaphore enterSemaphore = new Semaphore(0);
Semaphore exitSemaphore = new Semaphore(0);
boolean timedOut = false;
return new AsyncExecutorStepper() {
@Override
public void close() {
enterSemaphore.release(Integer.MAX_VALUE/2);
@@ -58,15 +59,19 @@ public final class SyncAsyncExecutors {
}
@Override
public ExecutionType execute(Consumer<ExecutionType> runnable) {
public ExecutionType execute(Consumer<ExecutionType> runnable, Runnable abortionFallback) {
executor.execute(() -> {
try {
enterSemaphore.acquire();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
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();
}
}

View File

@@ -17,6 +17,7 @@ export type File = {
lastModified?: string;
subRepository?: SubRepository; // TODO
partialResult: boolean;
computationAborted: boolean;
_links: Links;
_embedded: {
children: File[] | null | undefined;

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<Props> {
class FileTreeLeaf extends React.Component<Props> {
createLink = (file: File) => {
return createLink(this.props.baseUrl, file);
};
@@ -58,6 +60,25 @@ export default class FileTreeLeaf extends React.Component<Props> {
return <Link to={this.createLink(file)}>{file.name}</Link>;
};
contentIfPresent = (file: File, content: any) => {
const { t } = this.props;
if (file.computationAborted) {
return (
<Tooltip location="top" message={t("sources.file-tree.computationAborted")}>
<Icon name={"question-circle"} />
</Tooltip>
);
} else if (file.partialResult) {
return (
<Tooltip location="top" message={t("sources.file-tree.notYetComputed")}>
<Icon name={"hourglass"} />
</Tooltip>
);
} else {
return content;
}
};
render() {
const { file } = this.props;
@@ -68,10 +89,10 @@ export default class FileTreeLeaf extends React.Component<Props> {
<td>{this.createFileIcon(file)}</td>
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
<td className="is-hidden-mobile">{fileSize}</td>
<td className="is-hidden-mobile">
<DateFromNow date={file.lastModified} />
</td>
<MinWidthTd className={classNames("is-word-break", "is-hidden-mobile")}>{file.description}</MinWidthTd>
<td className="is-hidden-mobile">{this.contentIfPresent(file, <DateFromNow date={file.lastModified} />)}</td>
<MinWidthTd className={classNames("is-word-break", "is-hidden-mobile")}>
{this.contentIfPresent(file, file.description)}
</MinWidthTd>
{binder.hasExtension("repos.sources.tree.row.right") && (
<td className="is-hidden-mobile">
{!file.directory && (
@@ -89,3 +110,5 @@ export default class FileTreeLeaf extends React.Component<Props> {
);
}
}
export default withTranslation("repos")(FileTreeLeaf);

View File

@@ -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 => {

View File

@@ -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);

View File

@@ -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<ExecutionType> runnable) {
DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime, long maxAsyncRuntime) {
this.executor = executor;
this.switchToAsyncTime = switchToAsyncTime;
this.maxAsyncRuntime = maxAsyncRuntime;
}
public ExecutionType execute(Consumer<ExecutionType> 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 {

View File

@@ -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();
}
}