diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt index 48abce2587..672f025d37 100644 --- a/Documentation/rest-api-projects.txt +++ b/Documentation/rest-api-projects.txt @@ -844,6 +844,34 @@ The response is the streamed output of the garbage collection. done. ---- +==== Asynchronous Execution + +The option `async` allows to schedule a background task that asynchronously +executes a Git garbage collection. + +The `Location` header of the response refers to the link:rest-api-config.html#get-task[background task] +which allows to inspect the progress of its execution. In case of asynchronous +execution the `show_progress` option is ignored. + +.Request +---- + POST /projects/plugins%2Freplication/gc HTTP/1.0 + Content-Type: application/json;charset=UTF-8 + + { + "async": true + } +---- + +The response is empty. + +.Response +---- + HTTP/1.1 202 Accepted + Content-Disposition: attachment + Location: https:/a/config/server/tasks/383a0602 +---- + [[ban-commit]] === Ban Commit -- @@ -2212,6 +2240,8 @@ collection. Whether progress information should be shown. |`aggressive` |`false` if not set| Whether an aggressive garbage collection should be done. +|`async` |`false` if not set| +Whether the garbage collection should run asynchronously. |============================= [[head-input]] diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java index 43450764bc..803a94ac7e 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java @@ -36,6 +36,11 @@ public abstract class Response { return new Impl<>(201, value); } + /** HTTP 202 Accepted: accepted as background task. */ + public static Accepted accepted(String location) { + return new Accepted(location); + } + /** HTTP 204 No Content: typically used when the resource is deleted. */ @SuppressWarnings("unchecked") public static Response none() { @@ -168,4 +173,33 @@ public abstract class Response { return String.format("[302 Redirect] %s", location); } } + + /** Accepted as task for asynchronous execution. */ + public static final class Accepted { + private final String location; + + private Accepted(String url) { + this.location = url; + } + + public String location() { + return location; + } + + @Override + public int hashCode() { + return location.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof Accepted + && ((Accepted) o).location.equals(location); + } + + @Override + public String toString() { + return String.format("[202 Accepted] %s", location); + } + } } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java index f2ca49dc9b..cf7eff9038 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.math.RoundingMode.CEILING; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.UTF_8; +import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; import static javax.servlet.http.HttpServletResponse.SC_CREATED; @@ -347,6 +348,11 @@ public class RestApiServlet extends HttpServlet { CacheHeaders.setNotCacheable(res); res.sendRedirect(((Response.Redirect) result).location()); return; + } else if (result instanceof Response.Accepted) { + CacheHeaders.setNotCacheable(res); + res.setStatus(SC_ACCEPTED); + res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted)result).location()); + return; } else { CacheHeaders.setNotCacheable(res); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java index 03dc97c6a4..3f321aa9ee 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java @@ -20,13 +20,19 @@ import com.google.gerrit.common.data.GarbageCollectionResult; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.extensions.annotations.RequiresCapability; import com.google.gerrit.extensions.restapi.BinaryResult; +import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.git.GarbageCollection; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.LocalDiskRepositoryManager; +import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.project.GarbageCollect.Input; +import com.google.gerrit.server.util.IdGenerator; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; @@ -42,22 +48,61 @@ public class GarbageCollect implements RestModifyView, public static class Input { public boolean showProgress; public boolean aggressive; + public boolean async; } private final boolean canGC; - private GarbageCollection.Factory garbageCollectionFactory; + private final GarbageCollection.Factory garbageCollectionFactory; + private final WorkQueue workQueue; + private final Provider canonicalUrl; @Inject - GarbageCollect( - GitRepositoryManager repoManager, - GarbageCollection.Factory garbageCollectionFactory) { + GarbageCollect(GitRepositoryManager repoManager, + GarbageCollection.Factory garbageCollectionFactory, WorkQueue workQueue, + @CanonicalWebUrl Provider canonicalUrl) { + this.workQueue = workQueue; + this.canonicalUrl = canonicalUrl; this.canGC = repoManager instanceof LocalDiskRepositoryManager; this.garbageCollectionFactory = garbageCollectionFactory; } - @SuppressWarnings("resource") @Override - public BinaryResult apply(final ProjectResource rsrc, final Input input) { + public Object apply(ProjectResource rsrc, Input input) { + Project.NameKey project = rsrc.getNameKey(); + if (input.async) { + return applyAsync(project, input); + } else { + return applySync(project, input); + } + } + + private Response.Accepted applyAsync(final Project.NameKey project, final Input input) { + Runnable job = new Runnable() { + @Override + public void run() { + runGC(project, input, null); + } + + @Override + public String toString() { + return "Run " + (input.aggressive ? "aggressive " : "") + + "garbage collection on project " + project.get(); + } + }; + + @SuppressWarnings("unchecked") + WorkQueue.Task task = + (WorkQueue.Task) workQueue.getDefaultQueue().submit(job); + + String location = canonicalUrl.get() + "a/config/server/tasks/" + + IdGenerator.format(task.getTaskId()); + + return Response.accepted(location); + } + + @SuppressWarnings("resource") + private BinaryResult applySync(final Project.NameKey project, + final Input input) { return new BinaryResult() { @Override public void writeTo(OutputStream out) throws IOException { @@ -69,10 +114,8 @@ public class GarbageCollect implements RestModifyView, } }; try { - GarbageCollectionResult result = - garbageCollectionFactory.create().run( - Collections.singletonList(rsrc.getNameKey()), input.aggressive, - input.showProgress ? writer : null); + PrintWriter progressWriter = input.showProgress ? writer : null; + GarbageCollectionResult result = runGC(project, input, progressWriter); String msg = "Garbage collection completed successfully."; if (result.hasErrors()) { for (GarbageCollectionResult.Error e : result.getErrors()) { @@ -104,6 +147,13 @@ public class GarbageCollect implements RestModifyView, .disableGzip(); } + GarbageCollectionResult runGC(Project.NameKey project, + Input input, PrintWriter progressWriter) { + return garbageCollectionFactory.create().run( + Collections.singletonList(project), input.aggressive, + progressWriter); + } + @Override public UiAction.Description getDescription(ProjectResource rsrc) { return new UiAction.Description()