mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 14:35:45 +01:00
Let background computations abort for browse command
This commit is contained in:
@@ -226,6 +226,10 @@ public class FileObject implements LastModifiedAware, Serializable
|
|||||||
return partialResult;
|
return partialResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isComputationAborted() {
|
||||||
|
return computationAborted;
|
||||||
|
}
|
||||||
|
|
||||||
//~--- set methods ----------------------------------------------------------
|
//~--- set methods ----------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -310,6 +314,10 @@ public class FileObject implements LastModifiedAware, Serializable
|
|||||||
this.partialResult = partialResult;
|
this.partialResult = partialResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setComputationAborted(boolean computationAborted) {
|
||||||
|
this.computationAborted = computationAborted;
|
||||||
|
}
|
||||||
|
|
||||||
public Collection<FileObject> getChildren() {
|
public Collection<FileObject> getChildren() {
|
||||||
return unmodifiableCollection(children);
|
return unmodifiableCollection(children);
|
||||||
}
|
}
|
||||||
@@ -348,6 +356,8 @@ public class FileObject implements LastModifiedAware, Serializable
|
|||||||
|
|
||||||
private boolean partialResult = false;
|
private boolean partialResult = false;
|
||||||
|
|
||||||
|
private boolean computationAborted = false;
|
||||||
|
|
||||||
/** sub repository informations */
|
/** sub repository informations */
|
||||||
@XmlElement(name = "subrepository")
|
@XmlElement(name = "subrepository")
|
||||||
private SubRepository subRepository;
|
private SubRepository subRepository;
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import java.util.function.Consumer;
|
|||||||
public interface SyncAsyncExecutor {
|
public interface SyncAsyncExecutor {
|
||||||
|
|
||||||
default ExecutionType execute(Runnable runnable) {
|
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();
|
boolean hasExecutedAllSynchronously();
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
request.updateCache(browserResult);
|
request.updateCache(browserResult);
|
||||||
logger.info("updated browser result for repository {}", repository.getNamespaceAndName());
|
logger.info("updated browser result for repository {}", repository.getNamespaceAndName());
|
||||||
}
|
}
|
||||||
});
|
}, () -> {});
|
||||||
return browserResult;
|
return browserResult;
|
||||||
} else {
|
} else {
|
||||||
logger.warn("could not find head of repository {}, empty?", repository.getNamespaceAndName());
|
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,
|
logger.warn("could not find latest commit for {} on {}", path,
|
||||||
revId);
|
revId);
|
||||||
}
|
}
|
||||||
|
}, () -> {
|
||||||
|
file.setPartialResult(false);
|
||||||
|
file.setComputationAborted(true);
|
||||||
|
request.updateCache(browserResult);
|
||||||
|
logger.info("updated browser result for repository {}", repository.getNamespaceAndName());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public final class SyncAsyncExecutors {
|
|||||||
public static SyncAsyncExecutor synchronousExecutor() {
|
public static SyncAsyncExecutor synchronousExecutor() {
|
||||||
return new SyncAsyncExecutor() {
|
return new SyncAsyncExecutor() {
|
||||||
@Override
|
@Override
|
||||||
public ExecutionType execute(Consumer<ExecutionType> runnable) {
|
public ExecutionType execute(Consumer<ExecutionType> runnable, Runnable abortionFallback) {
|
||||||
runnable.accept(SYNCHRONOUS);
|
runnable.accept(SYNCHRONOUS);
|
||||||
return SYNCHRONOUS;
|
return SYNCHRONOUS;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ public final class SyncAsyncExecutors {
|
|||||||
|
|
||||||
return new SyncAsyncExecutor() {
|
return new SyncAsyncExecutor() {
|
||||||
@Override
|
@Override
|
||||||
public ExecutionType execute(Consumer<ExecutionType> runnable) {
|
public ExecutionType execute(Consumer<ExecutionType> runnable, Runnable abortionFallback) {
|
||||||
executor.execute(() -> runnable.accept(ASYNCHRONOUS));
|
executor.execute(() -> runnable.accept(ASYNCHRONOUS));
|
||||||
return ASYNCHRONOUS;
|
return ASYNCHRONOUS;
|
||||||
}
|
}
|
||||||
@@ -45,12 +45,13 @@ public final class SyncAsyncExecutors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static AsyncExecutorStepper stepperAsynchronousExecutor() {
|
public static AsyncExecutorStepper stepperAsynchronousExecutor() {
|
||||||
|
|
||||||
Executor executor = Executors.newSingleThreadExecutor();
|
|
||||||
Semaphore enterSemaphore = new Semaphore(0);
|
|
||||||
Semaphore exitSemaphore = new Semaphore(0);
|
|
||||||
|
|
||||||
return new AsyncExecutorStepper() {
|
return new AsyncExecutorStepper() {
|
||||||
|
|
||||||
|
Executor executor = Executors.newSingleThreadExecutor();
|
||||||
|
Semaphore enterSemaphore = new Semaphore(0);
|
||||||
|
Semaphore exitSemaphore = new Semaphore(0);
|
||||||
|
boolean timedOut = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
enterSemaphore.release(Integer.MAX_VALUE/2);
|
enterSemaphore.release(Integer.MAX_VALUE/2);
|
||||||
@@ -58,15 +59,19 @@ public final class SyncAsyncExecutors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ExecutionType execute(Consumer<ExecutionType> runnable) {
|
public ExecutionType execute(Consumer<ExecutionType> runnable, Runnable abortionFallback) {
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
try {
|
try {
|
||||||
enterSemaphore.acquire();
|
enterSemaphore.acquire();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
runnable.accept(ASYNCHRONOUS);
|
if (timedOut) {
|
||||||
exitSemaphore.release();
|
abortionFallback.run();
|
||||||
|
} else {
|
||||||
|
runnable.accept(ASYNCHRONOUS);
|
||||||
|
exitSemaphore.release();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return ASYNCHRONOUS;
|
return ASYNCHRONOUS;
|
||||||
}
|
}
|
||||||
@@ -81,6 +86,12 @@ public final class SyncAsyncExecutors {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void timeout() {
|
||||||
|
timedOut = true;
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasExecutedAllSynchronously() {
|
public boolean hasExecutedAllSynchronously() {
|
||||||
return true;
|
return true;
|
||||||
@@ -90,5 +101,7 @@ public final class SyncAsyncExecutors {
|
|||||||
|
|
||||||
public interface AsyncExecutorStepper extends SyncAsyncExecutor, Closeable {
|
public interface AsyncExecutorStepper extends SyncAsyncExecutor, Closeable {
|
||||||
void next();
|
void next();
|
||||||
|
|
||||||
|
void timeout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type File = {
|
|||||||
lastModified?: string;
|
lastModified?: string;
|
||||||
subRepository?: SubRepository; // TODO
|
subRepository?: SubRepository; // TODO
|
||||||
partialResult: boolean;
|
partialResult: boolean;
|
||||||
|
computationAborted: boolean;
|
||||||
_links: Links;
|
_links: Links;
|
||||||
_embedded: {
|
_embedded: {
|
||||||
children: File[] | null | undefined;
|
children: File[] | null | undefined;
|
||||||
|
|||||||
@@ -101,7 +101,9 @@
|
|||||||
"length": "Größe",
|
"length": "Größe",
|
||||||
"lastModified": "Zuletzt bearbeitet",
|
"lastModified": "Zuletzt bearbeitet",
|
||||||
"description": "Beschreibung",
|
"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": {
|
"content": {
|
||||||
"historyButton": "History",
|
"historyButton": "History",
|
||||||
|
|||||||
@@ -101,7 +101,9 @@
|
|||||||
"length": "Length",
|
"length": "Length",
|
||||||
"lastModified": "Last modified",
|
"lastModified": "Last modified",
|
||||||
"description": "Description",
|
"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": {
|
"content": {
|
||||||
"historyButton": "History",
|
"historyButton": "History",
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import classNames from "classnames";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
import { File } from "@scm-manager/ui-types";
|
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 FileIcon from "./FileIcon";
|
||||||
|
import { Icon } from "@scm-manager/ui-components/src";
|
||||||
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = WithTranslation & {
|
||||||
file: File;
|
file: File;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
};
|
};
|
||||||
@@ -31,7 +33,7 @@ export function createLink(base: string, file: File) {
|
|||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FileTreeLeaf extends React.Component<Props> {
|
class FileTreeLeaf extends React.Component<Props> {
|
||||||
createLink = (file: File) => {
|
createLink = (file: File) => {
|
||||||
return createLink(this.props.baseUrl, 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>;
|
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() {
|
render() {
|
||||||
const { file } = this.props;
|
const { file } = this.props;
|
||||||
|
|
||||||
@@ -68,10 +89,10 @@ export default class FileTreeLeaf extends React.Component<Props> {
|
|||||||
<td>{this.createFileIcon(file)}</td>
|
<td>{this.createFileIcon(file)}</td>
|
||||||
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
|
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
|
||||||
<td className="is-hidden-mobile">{fileSize}</td>
|
<td className="is-hidden-mobile">{fileSize}</td>
|
||||||
<td className="is-hidden-mobile">
|
<td className="is-hidden-mobile">{this.contentIfPresent(file, <DateFromNow date={file.lastModified} />)}</td>
|
||||||
<DateFromNow date={file.lastModified} />
|
<MinWidthTd className={classNames("is-word-break", "is-hidden-mobile")}>
|
||||||
</td>
|
{this.contentIfPresent(file, file.description)}
|
||||||
<MinWidthTd className={classNames("is-word-break", "is-hidden-mobile")}>{file.description}</MinWidthTd>
|
</MinWidthTd>
|
||||||
{binder.hasExtension("repos.sources.tree.row.right") && (
|
{binder.hasExtension("repos.sources.tree.row.right") && (
|
||||||
<td className="is-hidden-mobile">
|
<td className="is-hidden-mobile">
|
||||||
{!file.directory && (
|
{!file.directory && (
|
||||||
@@ -89,3 +110,5 @@ export default class FileTreeLeaf extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withTranslation("repos")(FileTreeLeaf);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function fetchSourcesWithoutOptionalLoadingState(
|
|||||||
.then((sources: File) => {
|
.then((sources: File) => {
|
||||||
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
|
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
|
||||||
if (sources._embedded.children && sources._embedded.children.find(c => c.partialResult)) {
|
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 => {
|
.catch(err => {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public class FileObjectDto extends HalRepresentation {
|
|||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
private String revision;
|
private String revision;
|
||||||
private boolean partialResult;
|
private boolean partialResult;
|
||||||
|
private boolean computationAborted;
|
||||||
|
|
||||||
public FileObjectDto(Links links, Embedded embedded) {
|
public FileObjectDto(Links links, Embedded embedded) {
|
||||||
super(links, embedded);
|
super(links, embedded);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import sonia.scm.repository.spi.SyncAsyncExecutor;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.ASYNCHRONOUS;
|
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 class DefaultSyncAsyncExecutor implements SyncAsyncExecutor {
|
||||||
|
|
||||||
|
public static final long DEFAULT_MAX_ASYNC_RUNTIME = 60 * 1000L;
|
||||||
|
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final Instant switchToAsyncTime;
|
private final Instant switchToAsyncTime;
|
||||||
|
private final long maxAsyncRuntime;
|
||||||
|
private AtomicLong asyncRuntime = new AtomicLong(0L);
|
||||||
private boolean executedAllSynchronously = true;
|
private boolean executedAllSynchronously = true;
|
||||||
|
|
||||||
DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime) {
|
DefaultSyncAsyncExecutor(Executor executor, Instant switchToAsyncTime) {
|
||||||
this.executor = executor;
|
this(executor, switchToAsyncTime, DEFAULT_MAX_ASYNC_RUNTIME);
|
||||||
this.switchToAsyncTime = switchToAsyncTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
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;
|
executedAllSynchronously = false;
|
||||||
return ASYNCHRONOUS;
|
return ASYNCHRONOUS;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,26 +13,40 @@ import static sonia.scm.repository.spi.SyncAsyncExecutor.ExecutionType.SYNCHRONO
|
|||||||
class DefaultSyncAsyncExecutorTest {
|
class DefaultSyncAsyncExecutorTest {
|
||||||
|
|
||||||
ExecutionType calledWithType = null;
|
ExecutionType calledWithType = null;
|
||||||
|
boolean aborted = false;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExecuteSynchronouslyBeforeTimeout() {
|
void shouldExecuteSynchronouslyBeforeTimeout() {
|
||||||
DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.MAX);
|
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(result).isEqualTo(SYNCHRONOUS);
|
||||||
assertThat(calledWithType).isEqualTo(SYNCHRONOUS);
|
assertThat(calledWithType).isEqualTo(SYNCHRONOUS);
|
||||||
assertThat(executor.hasExecutedAllSynchronously()).isTrue();
|
assertThat(executor.hasExecutedAllSynchronously()).isTrue();
|
||||||
|
assertThat(aborted).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExecuteAsynchronouslyAfterTimeout() {
|
void shouldExecuteAsynchronouslyAfterTimeout() {
|
||||||
DefaultSyncAsyncExecutor executor = new DefaultSyncAsyncExecutor(Runnable::run, Instant.now().minus(1, MILLIS));
|
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(result).isEqualTo(ASYNCHRONOUS);
|
||||||
assertThat(calledWithType).isEqualTo(ASYNCHRONOUS);
|
assertThat(calledWithType).isEqualTo(ASYNCHRONOUS);
|
||||||
assertThat(executor.hasExecutedAllSynchronously()).isFalse();
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user