Create a cache of group membership to speed up membership tests
This way we can more efficiently examine the groups that an account is a member of by storing it in a hash within the JVM. Group queries should be fairly infrequent, so we use a secondary database connection if we need to perform the lookup. Very busy sites may need a larger cache, but 4096 users should be sufficient for most servers and yet not require too much memory within the JVM. Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
@@ -79,6 +79,7 @@ public class GerritServer {
|
|||||||
private final SignedToken xsrf;
|
private final SignedToken xsrf;
|
||||||
private final SignedToken account;
|
private final SignedToken account;
|
||||||
private final RepositoryCache repositories;
|
private final RepositoryCache repositories;
|
||||||
|
private final GroupCache groupCache;
|
||||||
|
|
||||||
private GerritServer() throws OrmException, XsrfException {
|
private GerritServer() throws OrmException, XsrfException {
|
||||||
db = createDatabase();
|
db = createDatabase();
|
||||||
@@ -95,6 +96,8 @@ public class GerritServer {
|
|||||||
} else {
|
} else {
|
||||||
repositories = null;
|
repositories = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupCache = new GroupCache(db, sConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Database<ReviewDb> createDatabase() throws OrmException {
|
private Database<ReviewDb> createDatabase() throws OrmException {
|
||||||
@@ -305,8 +308,8 @@ public class GerritServer {
|
|||||||
return repositories;
|
return repositories;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the group whose members have full access to manage the site. */
|
/** Get the group membership cache. */
|
||||||
public AccountGroup.Id getAdminGroupId() {
|
public GroupCache getGroupCache() {
|
||||||
return sConfig.adminGroupId;
|
return groupCache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -40,18 +40,18 @@ import java.util.Set;
|
|||||||
|
|
||||||
public class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
public class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
||||||
GroupAdminService {
|
GroupAdminService {
|
||||||
private final AccountGroup.Id adminId;
|
private final GroupCache groupCache;
|
||||||
|
|
||||||
public GroupAdminServiceImpl(final GerritServer server) {
|
public GroupAdminServiceImpl(final GerritServer server) {
|
||||||
super(server.getDatabase());
|
super(server.getDatabase());
|
||||||
adminId = server.getAdminGroupId();
|
groupCache = server.getGroupCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ownedGroups(final AsyncCallback<List<AccountGroup>> callback) {
|
public void ownedGroups(final AsyncCallback<List<AccountGroup>> callback) {
|
||||||
run(callback, new Action<List<AccountGroup>>() {
|
run(callback, new Action<List<AccountGroup>>() {
|
||||||
public List<AccountGroup> run(ReviewDb db) throws OrmException {
|
public List<AccountGroup> run(ReviewDb db) throws OrmException {
|
||||||
final List<AccountGroup> result;
|
final List<AccountGroup> result;
|
||||||
if (amAdmin(db)) {
|
if (groupCache.isAdministrator(RpcUtil.getAccountId())) {
|
||||||
result = db.accountGroups().all().toList();
|
result = db.accountGroups().all().toList();
|
||||||
} else {
|
} else {
|
||||||
result = myOwnedGroups(db);
|
result = myOwnedGroups(db);
|
||||||
@@ -90,6 +90,8 @@ public class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
|||||||
db.accountGroups().insert(Collections.singleton(group), txn);
|
db.accountGroups().insert(Collections.singleton(group), txn);
|
||||||
db.accountGroupMembers().insert(Collections.singleton(m), txn);
|
db.accountGroupMembers().insert(Collections.singleton(m), txn);
|
||||||
txn.commit();
|
txn.commit();
|
||||||
|
groupCache.notifyGroupAdd(m);
|
||||||
|
|
||||||
return group.getId();
|
return group.getId();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -188,6 +190,8 @@ public class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
|||||||
|
|
||||||
final AccountGroupMember m = new AccountGroupMember(key);
|
final AccountGroupMember m = new AccountGroupMember(key);
|
||||||
db.accountGroupMembers().insert(Collections.singleton(m));
|
db.accountGroupMembers().insert(Collections.singleton(m));
|
||||||
|
groupCache.notifyGroupAdd(m);
|
||||||
|
|
||||||
final AccountGroupDetail d = new AccountGroupDetail();
|
final AccountGroupDetail d = new AccountGroupDetail();
|
||||||
d.loadOneMember(db, a, m);
|
d.loadOneMember(db, a, m);
|
||||||
return d;
|
return d;
|
||||||
@@ -204,7 +208,7 @@ public class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
|||||||
for (final AccountGroupMember.Key k : keys) {
|
for (final AccountGroupMember.Key k : keys) {
|
||||||
if (!owned.contains(k.getAccountGroupId())) {
|
if (!owned.contains(k.getAccountGroupId())) {
|
||||||
if (amAdmin == null) {
|
if (amAdmin == null) {
|
||||||
amAdmin = amAdmin(db);
|
amAdmin = groupCache.isAdministrator(RpcUtil.getAccountId());
|
||||||
}
|
}
|
||||||
if (!amAdmin) {
|
if (!amAdmin) {
|
||||||
throw new Failure(new NoSuchEntityException());
|
throw new Failure(new NoSuchEntityException());
|
||||||
@@ -215,6 +219,7 @@ public class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
|||||||
final AccountGroupMember m = db.accountGroupMembers().get(k);
|
final AccountGroupMember m = db.accountGroupMembers().get(k);
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
db.accountGroupMembers().delete(Collections.singleton(m));
|
db.accountGroupMembers().delete(Collections.singleton(m));
|
||||||
|
groupCache.notifyGroupDelete(m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return VoidResult.INSTANCE;
|
return VoidResult.INSTANCE;
|
||||||
@@ -222,23 +227,15 @@ public class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean amInGroup(final ReviewDb db,
|
|
||||||
final AccountGroup.Id groupId) throws OrmException {
|
|
||||||
return db.accountGroupMembers().get(
|
|
||||||
new AccountGroupMember.Key(RpcUtil.getAccountId(), groupId)) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean amAdmin(final ReviewDb db) throws OrmException {
|
|
||||||
return adminId != null && amInGroup(db, adminId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertAmGroupOwner(final ReviewDb db,
|
private void assertAmGroupOwner(final ReviewDb db,
|
||||||
final AccountGroup.Id groupId) throws OrmException, Failure {
|
final AccountGroup.Id groupId) throws OrmException, Failure {
|
||||||
final AccountGroup group = db.accountGroups().get(groupId);
|
final AccountGroup group = db.accountGroups().get(groupId);
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
throw new Failure(new NoSuchEntityException());
|
throw new Failure(new NoSuchEntityException());
|
||||||
}
|
}
|
||||||
if (!amInGroup(db, group.getOwnerGroupId()) && !amAdmin(db)) {
|
final Account.Id me = RpcUtil.getAccountId();
|
||||||
|
if (!groupCache.isInGroup(me, group.getOwnerGroupId())
|
||||||
|
&& !groupCache.isAdministrator(me)) {
|
||||||
throw new Failure(new NoSuchEntityException());
|
throw new Failure(new NoSuchEntityException());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,13 +249,12 @@ public class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<AccountGroup> myOwnedGroups(final ReviewDb db)
|
private List<AccountGroup> myOwnedGroups(final ReviewDb db)
|
||||||
throws OrmException {
|
throws OrmException {
|
||||||
|
final Account.Id me = RpcUtil.getAccountId();
|
||||||
final List<AccountGroup> own = new ArrayList<AccountGroup>();
|
final List<AccountGroup> own = new ArrayList<AccountGroup>();
|
||||||
for (final AccountGroupMember m : db.accountGroupMembers().byAccount(
|
for (final AccountGroup.Id groupId : groupCache.getGroups(me)) {
|
||||||
RpcUtil.getAccountId()).toList()) {
|
for (final AccountGroup g : db.accountGroups().ownedByGroup(groupId)) {
|
||||||
for (final AccountGroup g : db.accountGroups().ownedByGroup(
|
|
||||||
m.getAccountGroupId())) {
|
|
||||||
own.add(g);
|
own.add(g);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
152
appjar/src/main/java/com/google/gerrit/server/GroupCache.java
Normal file
152
appjar/src/main/java/com/google/gerrit/server/GroupCache.java
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// Copyright 2008 Google Inc.
|
||||||
|
//
|
||||||
|
// 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.server;
|
||||||
|
|
||||||
|
import com.google.gerrit.client.reviewdb.Account;
|
||||||
|
import com.google.gerrit.client.reviewdb.AccountGroup;
|
||||||
|
import com.google.gerrit.client.reviewdb.AccountGroupMember;
|
||||||
|
import com.google.gerrit.client.reviewdb.ReviewDb;
|
||||||
|
import com.google.gerrit.client.reviewdb.SystemConfig;
|
||||||
|
import com.google.gwtorm.client.OrmException;
|
||||||
|
import com.google.gwtorm.client.SchemaFactory;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Cache of group information, including account memberships. */
|
||||||
|
public class GroupCache {
|
||||||
|
private final SchemaFactory<ReviewDb> schema;
|
||||||
|
private AccountGroup.Id adminGroupId;
|
||||||
|
|
||||||
|
private final LinkedHashMap<Account.Id, Set<AccountGroup.Id>> byAccount =
|
||||||
|
new LinkedHashMap<Account.Id, Set<AccountGroup.Id>>(16, 0.75f, true) {
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(
|
||||||
|
final Map.Entry<Account.Id, Set<AccountGroup.Id>> eldest) {
|
||||||
|
return 4096 <= size();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected GroupCache(final SchemaFactory<ReviewDb> rdf, final SystemConfig cfg) {
|
||||||
|
schema = rdf;
|
||||||
|
adminGroupId = cfg.adminGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the user is a member of the blessed administrator group.
|
||||||
|
*
|
||||||
|
* @param accountId the account to test for membership.
|
||||||
|
* @return true if the account is in the special blessed administration group;
|
||||||
|
* false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isAdministrator(final Account.Id accountId) {
|
||||||
|
return isInGroup(accountId, adminGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the user is a member of a specific group.
|
||||||
|
*
|
||||||
|
* @param accountId the account to test for membership.
|
||||||
|
* @param groupId the group to test for membership within.
|
||||||
|
* @return true if the account is in the group; false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isInGroup(final Account.Id accountId,
|
||||||
|
final AccountGroup.Id groupId) {
|
||||||
|
final Set<AccountGroup.Id> m = getGroups(accountId);
|
||||||
|
return m.contains(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate all cached information about a single user account.
|
||||||
|
*
|
||||||
|
* @param accountId the account to invalidate from the cache.
|
||||||
|
*/
|
||||||
|
public void invalidate(final Account.Id accountId) {
|
||||||
|
synchronized (byAccount) {
|
||||||
|
byAccount.remove(accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the cache that an account has become a member of a group.
|
||||||
|
*
|
||||||
|
* @param m the account-group pairing that was just inserted.
|
||||||
|
*/
|
||||||
|
public void notifyGroupAdd(final AccountGroupMember m) {
|
||||||
|
synchronized (byAccount) {
|
||||||
|
final Set<AccountGroup.Id> e = byAccount.get(m.getAccountId());
|
||||||
|
if (e != null) {
|
||||||
|
final Set<AccountGroup.Id> n = new HashSet<AccountGroup.Id>(e);
|
||||||
|
n.add(m.getAccountGroupId());
|
||||||
|
byAccount.put(m.getAccountId(), Collections.unmodifiableSet(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the cache that an account has been removed from a group.
|
||||||
|
*
|
||||||
|
* @param m the account-group pairing that was just deleted.
|
||||||
|
*/
|
||||||
|
public void notifyGroupDelete(final AccountGroupMember m) {
|
||||||
|
synchronized (byAccount) {
|
||||||
|
final Set<AccountGroup.Id> e = byAccount.get(m.getAccountId());
|
||||||
|
if (e != null) {
|
||||||
|
final Set<AccountGroup.Id> n = new HashSet<AccountGroup.Id>(e);
|
||||||
|
n.remove(m.getAccountGroupId());
|
||||||
|
byAccount.put(m.getAccountId(), Collections.unmodifiableSet(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the groups a specific account is a member of.
|
||||||
|
*
|
||||||
|
* @param accountId the account to obtain the group list for.
|
||||||
|
* @return unmodifiable set listing the groups the account is a member of.
|
||||||
|
*/
|
||||||
|
public Set<AccountGroup.Id> getGroups(final Account.Id accountId) {
|
||||||
|
Set<AccountGroup.Id> m;
|
||||||
|
synchronized (byAccount) {
|
||||||
|
m = byAccount.get(accountId);
|
||||||
|
}
|
||||||
|
if (m != null) {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
m = new HashSet<AccountGroup.Id>();
|
||||||
|
try {
|
||||||
|
final ReviewDb db = schema.open();
|
||||||
|
try {
|
||||||
|
for (final AccountGroupMember g : db.accountGroupMembers().byAccount(
|
||||||
|
accountId)) {
|
||||||
|
m.add(g.getAccountGroupId());
|
||||||
|
}
|
||||||
|
m = Collections.unmodifiableSet(m);
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
} catch (OrmException e) {
|
||||||
|
m = Collections.emptySet();
|
||||||
|
}
|
||||||
|
synchronized (byAccount) {
|
||||||
|
byAccount.put(accountId, m);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
@@ -16,8 +16,8 @@ package com.google.gerrit.server;
|
|||||||
|
|
||||||
import com.google.gerrit.client.admin.ProjectAdminService;
|
import com.google.gerrit.client.admin.ProjectAdminService;
|
||||||
import com.google.gerrit.client.admin.ProjectDetail;
|
import com.google.gerrit.client.admin.ProjectDetail;
|
||||||
|
import com.google.gerrit.client.reviewdb.Account;
|
||||||
import com.google.gerrit.client.reviewdb.AccountGroup;
|
import com.google.gerrit.client.reviewdb.AccountGroup;
|
||||||
import com.google.gerrit.client.reviewdb.AccountGroupMember;
|
|
||||||
import com.google.gerrit.client.reviewdb.Project;
|
import com.google.gerrit.client.reviewdb.Project;
|
||||||
import com.google.gerrit.client.reviewdb.ReviewDb;
|
import com.google.gerrit.client.reviewdb.ReviewDb;
|
||||||
import com.google.gerrit.client.rpc.BaseServiceImplementation;
|
import com.google.gerrit.client.rpc.BaseServiceImplementation;
|
||||||
@@ -34,18 +34,18 @@ import java.util.List;
|
|||||||
|
|
||||||
public class ProjectAdminServiceImpl extends BaseServiceImplementation
|
public class ProjectAdminServiceImpl extends BaseServiceImplementation
|
||||||
implements ProjectAdminService {
|
implements ProjectAdminService {
|
||||||
private final AccountGroup.Id adminId;
|
private final GroupCache groupCache;
|
||||||
|
|
||||||
public ProjectAdminServiceImpl(final GerritServer server) {
|
public ProjectAdminServiceImpl(final GerritServer server) {
|
||||||
super(server.getDatabase());
|
super(server.getDatabase());
|
||||||
adminId = server.getAdminGroupId();
|
groupCache = server.getGroupCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ownedProjects(final AsyncCallback<List<Project>> callback) {
|
public void ownedProjects(final AsyncCallback<List<Project>> callback) {
|
||||||
run(callback, new Action<List<Project>>() {
|
run(callback, new Action<List<Project>>() {
|
||||||
public List<Project> run(ReviewDb db) throws OrmException {
|
public List<Project> run(ReviewDb db) throws OrmException {
|
||||||
final List<Project> result;
|
final List<Project> result;
|
||||||
if (amAdmin(db)) {
|
if (groupCache.isAdministrator(RpcUtil.getAccountId())) {
|
||||||
result = db.projects().all().toList();
|
result = db.projects().all().toList();
|
||||||
} else {
|
} else {
|
||||||
result = myOwnedProjects(db);
|
result = myOwnedProjects(db);
|
||||||
@@ -116,33 +116,24 @@ public class ProjectAdminServiceImpl extends BaseServiceImplementation
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean amInGroup(final ReviewDb db,
|
|
||||||
final AccountGroup.Id groupId) throws OrmException {
|
|
||||||
return db.accountGroupMembers().get(
|
|
||||||
new AccountGroupMember.Key(RpcUtil.getAccountId(), groupId)) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean amAdmin(final ReviewDb db) throws OrmException {
|
|
||||||
return adminId != null && amInGroup(db, adminId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertAmProjectOwner(final ReviewDb db,
|
private void assertAmProjectOwner(final ReviewDb db,
|
||||||
final Project.Id projectId) throws OrmException, Failure {
|
final Project.Id projectId) throws OrmException, Failure {
|
||||||
final Project project = db.projects().get(projectId);
|
final Project project = db.projects().get(projectId);
|
||||||
if (project == null) {
|
if (project == null) {
|
||||||
throw new Failure(new NoSuchEntityException());
|
throw new Failure(new NoSuchEntityException());
|
||||||
}
|
}
|
||||||
if (!amInGroup(db, project.getOwnerGroupId()) && !amAdmin(db)) {
|
final Account.Id me = RpcUtil.getAccountId();
|
||||||
|
if (!groupCache.isInGroup(me, project.getOwnerGroupId())
|
||||||
|
&& !groupCache.isAdministrator(me)) {
|
||||||
throw new Failure(new NoSuchEntityException());
|
throw new Failure(new NoSuchEntityException());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Project> myOwnedProjects(final ReviewDb db)
|
private List<Project> myOwnedProjects(final ReviewDb db) throws OrmException {
|
||||||
throws OrmException {
|
final Account.Id me = RpcUtil.getAccountId();
|
||||||
final List<Project> own = new ArrayList<Project>();
|
final List<Project> own = new ArrayList<Project>();
|
||||||
for (final AccountGroupMember m : db.accountGroupMembers().byAccount(
|
for (final AccountGroup.Id groupId : groupCache.getGroups(me)) {
|
||||||
RpcUtil.getAccountId()).toList()) {
|
for (final Project g : db.projects().ownedByGroup(groupId)) {
|
||||||
for (final Project g : db.projects().ownedByGroup(m.getAccountGroupId())) {
|
|
||||||
own.add(g);
|
own.add(g);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user