Support flushing a set of caches at once via REST

Change-Id: I2e51e3138eea15b305c3fc5029e91aa6b2f0b4cb
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2014-05-28 14:46:44 +02:00
parent a428683bc2
commit a3c6d039c2
6 changed files with 225 additions and 92 deletions

View File

@@ -366,6 +366,28 @@ link:#cache-operation-input[CacheOperationInput] entity.
HTTP/1.1 200 OK 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]]
=== Get Cache === Get Cache
-- --
@@ -621,14 +643,20 @@ The `CapabilityInfo` entity contains information about a capability.
The `CacheOperationInput` entity contains information about an The `CacheOperationInput` entity contains information about an
operation that should be executed on caches. operation that should be executed on caches.
[options="header",width="50%",cols="1,6"] [options="header",width="50%",cols="1,^1,5"]
|================================= |==================================
|Field Name |Description |Field Name ||Description
|`operation` | |`operation` ||
The cache operation that should be executed: The cache operation that should be executed:
`FLUSH_ALL`: Flushes all caches, except the `web_sessions` cache. `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]] [[entries-info]]
=== EntriesInfo === EntriesInfo

View File

@@ -14,24 +14,44 @@
package com.google.gerrit.acceptance.rest.config; 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.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.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse; 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.ListCaches.CacheInfo;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.PostCaches; 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.apache.http.HttpStatus;
import org.junit.Test; import org.junit.Test;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
public class CacheOperationsIT extends AbstractDaemonTest { public class CacheOperationsIT extends AbstractDaemonTest {
@Inject
private ProjectCache projectCache;
@Inject
private AllProjectsName allProjects;
@Inject
private MetaDataUpdate.Server metaDataUpdateFactory;
@Test @Test
public void flushAll() throws IOException { public void flushAll() throws IOException {
RestResponse r = adminSession.get("/config/server/caches/project_list"); RestResponse r = adminSession.get("/config/server/caches/project_list");
@@ -53,4 +73,94 @@ public class CacheOperationsIT extends AbstractDaemonTest {
new PostCaches.Input(FLUSH_ALL)); new PostCaches.Input(FLUSH_ALL));
assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode()); 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);
}
} }

View File

@@ -31,6 +31,15 @@ public class CacheResource extends ConfigResource {
this.cacheProvider = cacheProvider; this.cacheProvider = cacheProvider;
} }
public CacheResource(String pluginName, String cacheName, final Cache<?, ?> cache) {
this(pluginName, cacheName, new Provider<Cache<?, ?>>() {
@Override
public Cache<?, ?> get() {
return cache;
}
});
}
public String getName() { public String getName() {
return name; return name;
} }

View File

@@ -23,26 +23,36 @@ import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView; 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.gerrit.server.config.PostCaches.Input;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
@RequiresCapability(GlobalCapability.FLUSH_CACHES) @RequiresCapability(GlobalCapability.FLUSH_CACHES)
@Singleton @Singleton
public class PostCaches implements RestModifyView<ConfigResource, Input> { public class PostCaches implements RestModifyView<ConfigResource, Input> {
public static class Input { public static class Input {
public Operation operation; public Operation operation;
public List<String> caches;
public Input() { public Input() {
} }
public Input(Operation op) { public Input(Operation op) {
this(op, null);
}
public Input(Operation op, List<String> c) {
operation = op; operation = op;
caches = c;
} }
} }
public static enum Operation { public static enum Operation {
FLUSH_ALL; FLUSH_ALL, FLUSH;
} }
private final DynamicMap<Cache<?, ?>> cacheMap; private final DynamicMap<Cache<?, ?>> cacheMap;
@@ -57,25 +67,68 @@ public class PostCaches implements RestModifyView<ConfigResource, Input> {
@Override @Override
public Object apply(ConfigResource rsrc, Input input) throws AuthException, public Object apply(ConfigResource rsrc, Input input) throws AuthException,
ResourceNotFoundException, BadRequestException { ResourceNotFoundException, BadRequestException,
UnprocessableEntityException {
if (input == null || input.operation == null) { if (input == null || input.operation == null) {
throw new BadRequestException("operation must be specified"); throw new BadRequestException("operation must be specified");
} }
switch (input.operation) { switch (input.operation) {
case FLUSH_ALL: case FLUSH_ALL:
for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) { if (input.caches != null) {
CacheResource cacheResource = throw new BadRequestException(
new CacheResource(e.getPluginName(), e.getExportName(), "specifying caches is not allowed for operation 'FLUSH_ALL'");
e.getProvider());
if (FlushCache.WEB_SESSIONS.equals(cacheResource.getName())) {
continue;
}
flushCache.apply(cacheResource, null);
} }
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(""); return Response.ok("");
default: default:
throw new BadRequestException("unsupported operation: " + input.operation); throw new BadRequestException("unsupported operation: " + input.operation);
} }
} }
private void flushAll() throws AuthException {
for (DynamicMap.Entry<Cache<?, ?>> 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<String> cacheNames)
throws UnprocessableEntityException, AuthException {
List<CacheResource> 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);
}
}
} }

View File

@@ -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<Cache<?, ?>> cacheMap;
protected String cacheNameOf(String plugin, String name) {
if ("gerrit".equals(plugin)) {
return name;
} else {
return plugin + "-" + name;
}
}
}

View File

@@ -14,21 +14,19 @@
package com.google.gerrit.sshd.commands; 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.server.config.PostCaches.Operation.FLUSH_ALL;
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE; 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.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability; 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.extensions.restapi.RestApiException;
import com.google.gerrit.server.config.CacheResource;
import com.google.gerrit.server.config.ConfigResource; 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;
import com.google.gerrit.server.config.ListCaches.OutputFormat; import com.google.gerrit.server.config.ListCaches.OutputFormat;
import com.google.gerrit.server.config.PostCaches; import com.google.gerrit.server.config.PostCaches;
import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
@@ -41,7 +39,7 @@ import java.util.List;
@RequiresCapability(GlobalCapability.FLUSH_CACHES) @RequiresCapability(GlobalCapability.FLUSH_CACHES)
@CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory", @CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory",
runsAt = MASTER_OR_SLAVE) runsAt = MASTER_OR_SLAVE)
final class FlushCaches extends CacheCommand { final class FlushCaches extends SshCommand {
@Option(name = "--cache", usage = "flush named cache", metaVar = "NAME") @Option(name = "--cache", usage = "flush named cache", metaVar = "NAME")
private List<String> caches = new ArrayList<>(); private List<String> caches = new ArrayList<>();
@@ -51,9 +49,6 @@ final class FlushCaches extends CacheCommand {
@Option(name = "--list", usage = "list available caches") @Option(name = "--list", usage = "list available caches")
private boolean list; private boolean list;
@Inject
private FlushCache flushCache;
@Inject @Inject
private Provider<ListCaches> listCaches; private Provider<ListCaches> listCaches;
@@ -84,54 +79,25 @@ final class FlushCaches extends CacheCommand {
postCaches.apply(new ConfigResource(), postCaches.apply(new ConfigResource(),
new PostCaches.Input(FLUSH_ALL)); new PostCaches.Input(FLUSH_ALL));
} else { } else {
List<String> names = cacheNames(); postCaches.apply(new ConfigResource(),
for (String n : caches) { new PostCaches.Input(FLUSH, caches));
if (!names.contains(n)) {
throw error("error: cache \"" + n + "\" not recognized");
}
}
doBulkFlush();
} }
} catch (RestApiException e) { } catch (RestApiException e) {
throw die(e.getMessage()); throw die(e.getMessage());
} }
} }
private static UnloggedFailure error(final String msg) { private static UnloggedFailure error(String msg) {
return new UnloggedFailure(1, msg); return new UnloggedFailure(1, msg);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private List<String> cacheNames() { private void doList() {
return (List<String>) listCaches.get().setFormat(OutputFormat.LIST) for (String name : (List<String>) listCaches.get()
.apply(new ConfigResource()); .setFormat(OutputFormat.LIST).apply(new ConfigResource())) {
}
private void doList() throws RestApiException {
for (String name : cacheNames()) {
stderr.print(name); stderr.print(name);
stderr.print('\n'); stderr.print('\n');
} }
stderr.flush(); stderr.flush();
} }
private void doBulkFlush() {
try {
for (DynamicMap.Entry<Cache<?, ?>> 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();
}
}
} }