From a3c6d039c25a22692e9b1503a00aa18599339c0c Mon Sep 17 00:00:00 2001 From: Edwin Kempin Date: Wed, 28 May 2014 14:46:44 +0200 Subject: [PATCH] Support flushing a set of caches at once via REST Change-Id: I2e51e3138eea15b305c3fc5029e91aa6b2f0b4cb Signed-off-by: Edwin Kempin --- Documentation/rest-api-config.txt | 38 +++++- .../rest/config/CacheOperationsIT.java | 112 +++++++++++++++++- .../gerrit/server/config/CacheResource.java | 9 ++ .../gerrit/server/config/PostCaches.java | 73 ++++++++++-- .../gerrit/sshd/commands/CacheCommand.java | 33 ------ .../gerrit/sshd/commands/FlushCaches.java | 52 ++------ 6 files changed, 225 insertions(+), 92 deletions(-) delete mode 100644 gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt index 6f9ef46a55..8d9b34c708 100644 --- a/Documentation/rest-api-config.txt +++ b/Documentation/rest-api-config.txt @@ -366,6 +366,28 @@ link:#cache-operation-input[CacheOperationInput] entity. HTTP/1.1 200 OK ---- +[[flush-several-caches]] +==== Flush Several Caches At Once + +.Request +---- + POST /config/server/caches/ HTTP/1.0 + Content-Type: application/json;charset=UTF-8 + + { + "operation": "FLUSH" + "caches": [ + "projects", + "project_list" + ] + } +---- + +.Response +---- + HTTP/1.1 200 OK +---- + [[get-cache]] === Get Cache -- @@ -621,14 +643,20 @@ The `CapabilityInfo` entity contains information about a capability. The `CacheOperationInput` entity contains information about an operation that should be executed on caches. -[options="header",width="50%",cols="1,6"] -|================================= -|Field Name |Description -|`operation` | +[options="header",width="50%",cols="1,^1,5"] +|================================== +|Field Name ||Description +|`operation` || The cache operation that should be executed: `FLUSH_ALL`: Flushes all caches, except the `web_sessions` cache. -|================================= + +`FLUSH`: Flushes the specified caches. +|`caches` |optional| +A list of cache names. This list defines the caches on which the +specified operation should be executed. Whether this list must be +specified depends on the operation being executed. +|================================== [[entries-info]] === EntriesInfo diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java index 0038a54e68..d2174bceed 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java @@ -14,24 +14,44 @@ package com.google.gerrit.acceptance.rest.config; +import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH; import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL; - +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.server.project.Util.allow; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.config.ListCaches.CacheInfo; +import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.PostCaches; +import com.google.gerrit.server.git.MetaDataUpdate; +import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.group.SystemGroupBackend; +import com.google.gerrit.server.project.ProjectCache; +import com.google.inject.Inject; import org.apache.http.HttpStatus; import org.junit.Test; import java.io.IOException; +import java.util.Arrays; public class CacheOperationsIT extends AbstractDaemonTest { + @Inject + private ProjectCache projectCache; + + @Inject + private AllProjectsName allProjects; + + @Inject + private MetaDataUpdate.Server metaDataUpdateFactory; + @Test public void flushAll() throws IOException { RestResponse r = adminSession.get("/config/server/caches/project_list"); @@ -53,4 +73,94 @@ public class CacheOperationsIT extends AbstractDaemonTest { new PostCaches.Input(FLUSH_ALL)); assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode()); } + + @Test + public void flushAll_BadRequest() throws IOException { + RestResponse r = adminSession.post("/config/server/caches/", + new PostCaches.Input(FLUSH_ALL, Arrays.asList("projects"))); + assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode()); + } + + @Test + public void flush() throws IOException { + RestResponse r = adminSession.get("/config/server/caches/project_list"); + CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); + assertTrue(cacheInfo.entries.mem.longValue() > 0); + + r = adminSession.get("/config/server/caches/projects"); + cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); + assertTrue(cacheInfo.entries.mem.longValue() > 1); + + r = adminSession.post("/config/server/caches/", + new PostCaches.Input(FLUSH, Arrays.asList("accounts", "project_list"))); + assertEquals(HttpStatus.SC_OK, r.getStatusCode()); + r.consume(); + + r = adminSession.get("/config/server/caches/project_list"); + cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); + assertNull(cacheInfo.entries.mem); + + r = adminSession.get("/config/server/caches/projects"); + cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); + assertTrue(cacheInfo.entries.mem.longValue() > 1); + } + + @Test + public void flush_Forbidden() throws IOException { + RestResponse r = userSession.post("/config/server/caches/", + new PostCaches.Input(FLUSH, Arrays.asList("projects"))); + assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode()); + } + + @Test + public void flush_BadRequest() throws IOException { + RestResponse r = adminSession.post("/config/server/caches/", + new PostCaches.Input(FLUSH)); + assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode()); + } + + @Test + public void flush_UnprocessableEntity() throws IOException { + RestResponse r = adminSession.get("/config/server/caches/projects"); + CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); + assertTrue(cacheInfo.entries.mem.longValue() > 0); + + r = adminSession.post("/config/server/caches/", + new PostCaches.Input(FLUSH, Arrays.asList("projects", "unprocessable"))); + assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode()); + r.consume(); + + r = adminSession.get("/config/server/caches/projects"); + cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); + assertTrue(cacheInfo.entries.mem.longValue() > 0); + } + + @Test + public void flushWebSessions_Forbidden() throws IOException { + ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig(); + AccountGroup.UUID registeredUsers = + SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); + allow(cfg, GlobalCapability.VIEW_CACHES, registeredUsers); + allow(cfg, GlobalCapability.FLUSH_CACHES, registeredUsers); + saveProjectConfig(cfg); + + RestResponse r = userSession.post("/config/server/caches/", + new PostCaches.Input(FLUSH, Arrays.asList("projects"))); + assertEquals(HttpStatus.SC_OK, r.getStatusCode()); + r.consume(); + + r = userSession.post("/config/server/caches/", + new PostCaches.Input(FLUSH, Arrays.asList("web_sessions"))); + assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode()); + } + + private void saveProjectConfig(ProjectConfig cfg) throws IOException { + MetaDataUpdate md = metaDataUpdateFactory.create(allProjects); + try { + cfg.commit(md); + } finally { + md.close(); + } + projectCache.evict(allProjects); + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java index 9019f4d1b1..afb972b6be 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java @@ -31,6 +31,15 @@ public class CacheResource extends ConfigResource { this.cacheProvider = cacheProvider; } + public CacheResource(String pluginName, String cacheName, final Cache cache) { + this(pluginName, cacheName, new Provider>() { + @Override + public Cache get() { + return cache; + } + }); + } + public String getName() { return name; } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java index 6a4856b2bb..7302ea191d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java @@ -23,26 +23,36 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.server.config.PostCaches.Input; import com.google.inject.Inject; import com.google.inject.Singleton; +import java.util.ArrayList; +import java.util.List; + @RequiresCapability(GlobalCapability.FLUSH_CACHES) @Singleton public class PostCaches implements RestModifyView { public static class Input { public Operation operation; + public List caches; public Input() { } public Input(Operation op) { + this(op, null); + } + + public Input(Operation op, List c) { operation = op; + caches = c; } } public static enum Operation { - FLUSH_ALL; + FLUSH_ALL, FLUSH; } private final DynamicMap> cacheMap; @@ -57,25 +67,68 @@ public class PostCaches implements RestModifyView { @Override public Object apply(ConfigResource rsrc, Input input) throws AuthException, - ResourceNotFoundException, BadRequestException { + ResourceNotFoundException, BadRequestException, + UnprocessableEntityException { if (input == null || input.operation == null) { throw new BadRequestException("operation must be specified"); } switch (input.operation) { case FLUSH_ALL: - for (DynamicMap.Entry> e : cacheMap) { - CacheResource cacheResource = - new CacheResource(e.getPluginName(), e.getExportName(), - e.getProvider()); - if (FlushCache.WEB_SESSIONS.equals(cacheResource.getName())) { - continue; - } - flushCache.apply(cacheResource, null); + if (input.caches != null) { + throw new BadRequestException( + "specifying caches is not allowed for operation 'FLUSH_ALL'"); } + flushAll(); + return Response.ok(""); + case FLUSH: + if (input.caches == null || input.caches.isEmpty()) { + throw new BadRequestException( + "caches must be specified for operation 'FLUSH'"); + } + flush(input.caches); return Response.ok(""); default: throw new BadRequestException("unsupported operation: " + input.operation); } } + + private void flushAll() throws AuthException { + for (DynamicMap.Entry> e : cacheMap) { + CacheResource cacheResource = + new CacheResource(e.getPluginName(), e.getExportName(), + e.getProvider()); + if (FlushCache.WEB_SESSIONS.equals(cacheResource.getName())) { + continue; + } + flushCache.apply(cacheResource, null); + } + } + + private void flush(List cacheNames) + throws UnprocessableEntityException, AuthException { + List cacheResources = new ArrayList<>(cacheNames.size()); + + for (String n : cacheNames) { + String pluginName = "gerrit"; + String cacheName = n; + int i = cacheName.lastIndexOf('-'); + if (i != -1) { + pluginName = cacheName.substring(0, i); + cacheName = cacheName.length() > i + 1 ? cacheName.substring(i + 1) : ""; + } + + Cache cache = cacheMap.get(pluginName, cacheName); + if (cache != null) { + cacheResources.add(new CacheResource(pluginName, cacheName, cache)); + } else { + throw new UnprocessableEntityException(String.format( + "cache %s not found", n)); + } + } + + for (CacheResource rsrc : cacheResources) { + flushCache.apply(rsrc, null); + } + } } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java deleted file mode 100644 index f16dcd1e30..0000000000 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2009 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.sshd.commands; - -import com.google.common.cache.Cache; -import com.google.gerrit.extensions.registration.DynamicMap; -import com.google.gerrit.sshd.SshCommand; -import com.google.inject.Inject; - -abstract class CacheCommand extends SshCommand { - @Inject - protected DynamicMap> cacheMap; - - protected String cacheNameOf(String plugin, String name) { - if ("gerrit".equals(plugin)) { - return name; - } else { - return plugin + "-" + name; - } - } -} diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java index bc6687a518..6dfcb51d16 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java @@ -14,21 +14,19 @@ package com.google.gerrit.sshd.commands; +import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH; import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL; import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE; -import com.google.common.cache.Cache; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.extensions.annotations.RequiresCapability; -import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.server.config.CacheResource; import com.google.gerrit.server.config.ConfigResource; -import com.google.gerrit.server.config.FlushCache; import com.google.gerrit.server.config.ListCaches; import com.google.gerrit.server.config.ListCaches.OutputFormat; import com.google.gerrit.server.config.PostCaches; import com.google.gerrit.sshd.CommandMetaData; +import com.google.gerrit.sshd.SshCommand; import com.google.inject.Inject; import com.google.inject.Provider; @@ -41,7 +39,7 @@ import java.util.List; @RequiresCapability(GlobalCapability.FLUSH_CACHES) @CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory", runsAt = MASTER_OR_SLAVE) -final class FlushCaches extends CacheCommand { +final class FlushCaches extends SshCommand { @Option(name = "--cache", usage = "flush named cache", metaVar = "NAME") private List caches = new ArrayList<>(); @@ -51,9 +49,6 @@ final class FlushCaches extends CacheCommand { @Option(name = "--list", usage = "list available caches") private boolean list; - @Inject - private FlushCache flushCache; - @Inject private Provider listCaches; @@ -84,54 +79,25 @@ final class FlushCaches extends CacheCommand { postCaches.apply(new ConfigResource(), new PostCaches.Input(FLUSH_ALL)); } else { - List names = cacheNames(); - for (String n : caches) { - if (!names.contains(n)) { - throw error("error: cache \"" + n + "\" not recognized"); - } - } - doBulkFlush(); + postCaches.apply(new ConfigResource(), + new PostCaches.Input(FLUSH, caches)); } } catch (RestApiException e) { throw die(e.getMessage()); } } - private static UnloggedFailure error(final String msg) { + private static UnloggedFailure error(String msg) { return new UnloggedFailure(1, msg); } @SuppressWarnings("unchecked") - private List cacheNames() { - return (List) listCaches.get().setFormat(OutputFormat.LIST) - .apply(new ConfigResource()); - } - - private void doList() throws RestApiException { - for (String name : cacheNames()) { + private void doList() { + for (String name : (List) listCaches.get() + .setFormat(OutputFormat.LIST).apply(new ConfigResource())) { stderr.print(name); stderr.print('\n'); } stderr.flush(); } - - private void doBulkFlush() { - try { - for (DynamicMap.Entry> e : cacheMap) { - String n = cacheNameOf(e.getPluginName(), e.getExportName()); - if (caches.contains(n)) { - try { - flushCache.apply( - new CacheResource(e.getPluginName(), e.getExportName(), - e.getProvider()), null); - } catch (RestApiException err) { - stderr.println("error: cannot flush cache \"" + n + "\": " - + err.getMessage()); - } - } - } - } finally { - stderr.flush(); - } - } }