Automatically check for refs/meta/config changes
Periodically check a project's refs/meta/config for modifications made outside of Gerrit Code Review. This ensures slave servers will eventually pick up new access controls or project settings without requiring administrators to flush the "projects" cache over SSH. Checks are done only every cache.projects.checkFrequency period, as a local disk check requires at least one stat() call to examine the loose reference's last modified time. This is relatively inexpensive for a single project request like git clone, but not feasible for multiple project lookups like a query results page or user dashboard. To prevent many calls to System.currentTimeMillis() a background thread (managed by Executors.newScheduledThreadPool) is used to update a generation flag every checkFrequency period. During a cache get the ProjectState rechecks its refs/meta/config if the generation does not match, and gets replaced if there were changes. Bug: issue 962 Change-Id: I9ad4db27329968e2993b4dd142d1325446190065 Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
parent
cb115b6d24
commit
b8e4e35949
|
@ -485,6 +485,21 @@ configuration.
|
|||
+
|
||||
Default is true, enabled.
|
||||
|
||||
cache.projects.checkFrequency::
|
||||
+
|
||||
How often project configuration should be checked for update from Git.
|
||||
Gerrit Code Review caches project access rules and configuration in
|
||||
memory, checking the refs/meta/config branch every checkFrequency
|
||||
minutes to see if a new revision should be loaded and used for future
|
||||
access. Values can be specified using standard time unit abbreviations
|
||||
('ms', 'sec', 'min', etc.).
|
||||
+
|
||||
If set to 0, checks occur every time, which may slow down operations.
|
||||
Administrators may force the cache to flush with
|
||||
link:cmd-flush-caches.html[gerrit flush-caches].
|
||||
+
|
||||
Default is 5 minutes.
|
||||
|
||||
|
||||
[[commentlink]]Section commentlink
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -19,6 +19,8 @@ import com.google.gerrit.reviewdb.ReviewDb;
|
|||
import com.google.gerrit.server.cache.Cache;
|
||||
import com.google.gerrit.server.cache.CacheModule;
|
||||
import com.google.gerrit.server.cache.EntryCreator;
|
||||
import com.google.gerrit.server.config.ConfigUtil;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.ProjectConfig;
|
||||
import com.google.gwtorm.client.SchemaFactory;
|
||||
|
@ -29,6 +31,7 @@ import com.google.inject.TypeLiteral;
|
|||
import com.google.inject.name.Named;
|
||||
|
||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
|
||||
import java.util.Collections;
|
||||
|
@ -36,6 +39,8 @@ import java.util.Iterator;
|
|||
import java.util.NoSuchElementException;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
|
@ -66,14 +71,37 @@ public class ProjectCacheImpl implements ProjectCache {
|
|||
private final Cache<Project.NameKey, ProjectState> byName;
|
||||
private final Cache<ListKey,SortedSet<Project.NameKey>> list;
|
||||
private final Lock listLock;
|
||||
private volatile long generation;
|
||||
|
||||
@Inject
|
||||
ProjectCacheImpl(
|
||||
@Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName,
|
||||
@Named(CACHE_LIST) final Cache<ListKey, SortedSet<Project.NameKey>> list) {
|
||||
@Named(CACHE_LIST) final Cache<ListKey, SortedSet<Project.NameKey>> list,
|
||||
@GerritServerConfig final Config serverConfig) {
|
||||
this.byName = byName;
|
||||
this.list = list;
|
||||
this.listLock = new ReentrantLock(true /* fair */);
|
||||
|
||||
long checkFrequencyMillis = TimeUnit.MILLISECONDS.convert(
|
||||
ConfigUtil.getTimeUnit(serverConfig,
|
||||
"cache", "projects", "checkFrequency",
|
||||
5, TimeUnit.MINUTES), TimeUnit.MINUTES);
|
||||
if (10 < checkFrequencyMillis) {
|
||||
// Start with generation 1 (to avoid magic 0 below).
|
||||
generation = 1;
|
||||
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// This is not exactly thread-safe, but is OK for our use.
|
||||
// The only thread that writes the volatile is this task.
|
||||
generation = generation + 1;
|
||||
}
|
||||
}, checkFrequencyMillis, checkFrequencyMillis, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
// Magic generation 0 triggers ProjectState to always
|
||||
// check on each needsRefresh() request we make to it.
|
||||
generation = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,7 +111,12 @@ public class ProjectCacheImpl implements ProjectCache {
|
|||
* @return the cached data; null if no such project exists.
|
||||
*/
|
||||
public ProjectState get(final Project.NameKey projectName) {
|
||||
return byName.get(projectName);
|
||||
ProjectState state = byName.get(projectName);
|
||||
if (state != null && state.needsRefresh(generation)) {
|
||||
byName.remove(projectName);
|
||||
state = byName.get(projectName);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/** Invalidate the cached information about the given project. */
|
||||
|
|
|
@ -23,10 +23,15 @@ import com.google.gerrit.reviewdb.Project;
|
|||
import com.google.gerrit.server.AnonymousUser;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.config.WildProjectName;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.ProjectConfig;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -44,21 +49,28 @@ public class ProjectState {
|
|||
private final Project.NameKey wildProject;
|
||||
private final ProjectCache projectCache;
|
||||
private final ProjectControl.AssistedFactory projectControlFactory;
|
||||
private final GitRepositoryManager gitMgr;
|
||||
|
||||
private final ProjectConfig config;
|
||||
private final Set<AccountGroup.UUID> localOwners;
|
||||
|
||||
/** Last system time the configuration's revision was examined. */
|
||||
private transient long lastCheckTime;
|
||||
|
||||
@Inject
|
||||
protected ProjectState(final AnonymousUser anonymousUser,
|
||||
final ProjectCache projectCache,
|
||||
@WildProjectName final Project.NameKey wildProject,
|
||||
final ProjectControl.AssistedFactory projectControlFactory,
|
||||
final GitRepositoryManager gitMgr,
|
||||
@Assisted final ProjectConfig config) {
|
||||
this.anonymousUser = anonymousUser;
|
||||
this.projectCache = projectCache;
|
||||
this.wildProject = wildProject;
|
||||
this.projectControlFactory = projectControlFactory;
|
||||
this.gitMgr = gitMgr;
|
||||
this.config = config;
|
||||
this.lastCheckTime = System.currentTimeMillis();
|
||||
|
||||
HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
|
||||
AccessSection all = config.getAccessSection(AccessSection.ALL);
|
||||
|
@ -76,6 +88,34 @@ public class ProjectState {
|
|||
localOwners = Collections.unmodifiableSet(groups);
|
||||
}
|
||||
|
||||
boolean needsRefresh(long generation) {
|
||||
if (generation <= 0) {
|
||||
return isRevisionOutOfDate();
|
||||
}
|
||||
if (lastCheckTime != generation) {
|
||||
lastCheckTime = generation;
|
||||
return isRevisionOutOfDate();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isRevisionOutOfDate() {
|
||||
try {
|
||||
Repository git = gitMgr.openRepository(getProject().getNameKey());
|
||||
try {
|
||||
Ref ref = git.getRef(GitRepositoryManager.REF_CONFIG);
|
||||
if (ref == null || ref.getObjectId() == null) {
|
||||
return true;
|
||||
}
|
||||
return !ref.getObjectId().equals(config.getRevision());
|
||||
} finally {
|
||||
git.close();
|
||||
}
|
||||
} catch (IOException gone) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public Project getProject() {
|
||||
return getConfig().getProject();
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import com.google.gerrit.server.AnonymousUser;
|
|||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.config.AuthConfig;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.ProjectConfig;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Guice;
|
||||
|
@ -316,12 +317,13 @@ public class RefControlTest extends TestCase {
|
|||
}
|
||||
};
|
||||
|
||||
GitRepositoryManager mgr = null;
|
||||
Project.NameKey wildProject = new Project.NameKey("-- All Projects --");
|
||||
ProjectControl.AssistedFactory projectControlFactory = null;
|
||||
all.put(local.getProject().getNameKey(), new ProjectState(anonymousUser,
|
||||
projectCache, wildProject, projectControlFactory, local));
|
||||
projectCache, wildProject, projectControlFactory, mgr, local));
|
||||
all.put(parent.getProject().getNameKey(), new ProjectState(anonymousUser,
|
||||
projectCache, wildProject, projectControlFactory, parent));
|
||||
projectCache, wildProject, projectControlFactory, mgr, parent));
|
||||
return all.get(local.getProject().getNameKey());
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue