
When we start the Prolog environment through RulesCache, we create a bare machine (=interpreter) that will later be used by PrologEnvironment, a subclass of BufferingPrologControl. The reduction limit is applied through PrologEnvironment when terms are evaluated. The problem with that is that even the bare machine creation might run into the reduction limit by interpreting the provided facts. Before this commit, the reduction limit in the bare machine was always 1M. This commit makes it so that we apply the same limit that we have configured in Gerrit to the bare machine. This commit is obviously lacking a test, but we see this bug occuring in production right now and don't want to block the fix on providing a test. The effect was validated in a local Gerrit test site. Change-Id: I688f4abd8c02b635edb1c0cab9c921448ce6ac0a
282 lines
11 KiB
Java
282 lines
11 KiB
Java
// Copyright (C) 2011 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.server.rules;
|
|
|
|
import static com.googlecode.prolog_cafe.lang.PrologMachineCopy.save;
|
|
|
|
import com.google.common.base.Joiner;
|
|
import com.google.common.cache.Cache;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.gerrit.entities.Project;
|
|
import com.google.gerrit.entities.RefNames;
|
|
import com.google.gerrit.server.cache.CacheModule;
|
|
import com.google.gerrit.server.config.GerritServerConfig;
|
|
import com.google.gerrit.server.config.SitePaths;
|
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
|
import com.google.gerrit.server.plugincontext.PluginSetContext;
|
|
import com.google.gerrit.server.project.ProjectCacheImpl;
|
|
import com.google.inject.Inject;
|
|
import com.google.inject.Singleton;
|
|
import com.google.inject.name.Named;
|
|
import com.googlecode.prolog_cafe.exceptions.CompileException;
|
|
import com.googlecode.prolog_cafe.exceptions.SyntaxException;
|
|
import com.googlecode.prolog_cafe.exceptions.TermException;
|
|
import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
|
|
import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
|
|
import com.googlecode.prolog_cafe.lang.ListTerm;
|
|
import com.googlecode.prolog_cafe.lang.Prolog;
|
|
import com.googlecode.prolog_cafe.lang.PrologClassLoader;
|
|
import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
|
|
import com.googlecode.prolog_cafe.lang.StructureTerm;
|
|
import com.googlecode.prolog_cafe.lang.SymbolTerm;
|
|
import com.googlecode.prolog_cafe.lang.Term;
|
|
import java.io.IOException;
|
|
import java.io.PushbackReader;
|
|
import java.io.Reader;
|
|
import java.io.StringReader;
|
|
import java.net.MalformedURLException;
|
|
import java.net.URL;
|
|
import java.net.URLClassLoader;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
import java.util.EnumSet;
|
|
import java.util.List;
|
|
import java.util.concurrent.ExecutionException;
|
|
import org.eclipse.jgit.errors.LargeObjectException;
|
|
import org.eclipse.jgit.lib.Config;
|
|
import org.eclipse.jgit.lib.Constants;
|
|
import org.eclipse.jgit.lib.ObjectId;
|
|
import org.eclipse.jgit.lib.ObjectLoader;
|
|
import org.eclipse.jgit.lib.Repository;
|
|
import org.eclipse.jgit.util.RawParseUtils;
|
|
|
|
/**
|
|
* Manages a cache of compiled Prolog rules.
|
|
*
|
|
* <p>Rules are loaded from the {@code site_path/cache/rules/rules-SHA1.jar}, where {@code SHA1} is
|
|
* the SHA1 of the Prolog {@code rules.pl} in a project's {@link RefNames#REFS_CONFIG} branch.
|
|
*/
|
|
@Singleton
|
|
public class RulesCache {
|
|
public static class Module extends CacheModule {
|
|
@Override
|
|
protected void configure() {
|
|
cache(RulesCache.CACHE_NAME, ObjectId.class, PrologMachineCopy.class)
|
|
// This cache is auxiliary to the project cache, so size it the same.
|
|
.configKey(ProjectCacheImpl.CACHE_NAME);
|
|
}
|
|
}
|
|
|
|
private static final ImmutableList<String> PACKAGE_LIST =
|
|
ImmutableList.of(Prolog.BUILTIN, "gerrit");
|
|
|
|
static final String CACHE_NAME = "prolog_rules";
|
|
|
|
private final boolean enableProjectRules;
|
|
private final int maxDbSize;
|
|
private final int compileReductionLimit;
|
|
private final int maxSrcBytes;
|
|
private final Path cacheDir;
|
|
private final Path rulesDir;
|
|
private final GitRepositoryManager gitMgr;
|
|
private final PluginSetContext<PredicateProvider> predicateProviders;
|
|
private final ClassLoader systemLoader;
|
|
private final PrologMachineCopy defaultMachine;
|
|
private final Cache<ObjectId, PrologMachineCopy> machineCache;
|
|
|
|
@Inject
|
|
protected RulesCache(
|
|
@GerritServerConfig Config config,
|
|
SitePaths site,
|
|
GitRepositoryManager gm,
|
|
PluginSetContext<PredicateProvider> predicateProviders,
|
|
@Named(CACHE_NAME) Cache<ObjectId, PrologMachineCopy> machineCache) {
|
|
maxDbSize = config.getInt("rules", null, "maxPrologDatabaseSize", 256);
|
|
compileReductionLimit = RuleUtil.compileReductionLimit(config);
|
|
maxSrcBytes = config.getInt("rules", null, "maxSourceBytes", 128 << 10);
|
|
enableProjectRules = config.getBoolean("rules", null, "enable", true) && maxSrcBytes > 0;
|
|
cacheDir = site.resolve(config.getString("cache", null, "directory"));
|
|
rulesDir = cacheDir != null ? cacheDir.resolve("rules") : null;
|
|
gitMgr = gm;
|
|
this.predicateProviders = predicateProviders;
|
|
this.machineCache = machineCache;
|
|
|
|
systemLoader = getClass().getClassLoader();
|
|
defaultMachine = save(newEmptyMachine(systemLoader));
|
|
}
|
|
|
|
public boolean isProjectRulesEnabled() {
|
|
return enableProjectRules;
|
|
}
|
|
|
|
/**
|
|
* Locate a cached Prolog machine state, or create one if not available.
|
|
*
|
|
* @return a Prolog machine, after loading the specified rules.
|
|
* @throws CompileException the machine cannot be created.
|
|
*/
|
|
public synchronized PrologMachineCopy loadMachine(Project.NameKey project, ObjectId rulesId)
|
|
throws CompileException {
|
|
if (!enableProjectRules || project == null || rulesId == null) {
|
|
return defaultMachine;
|
|
}
|
|
|
|
try {
|
|
return machineCache.get(rulesId, () -> createMachine(project, rulesId));
|
|
} catch (ExecutionException e) {
|
|
if (e.getCause() instanceof CompileException) {
|
|
throw new CompileException(e.getCause().getMessage(), e);
|
|
}
|
|
throw new CompileException("Error while consulting rules from " + project, e);
|
|
}
|
|
}
|
|
|
|
public PrologMachineCopy loadMachine(String name, Reader in) throws CompileException {
|
|
PrologMachineCopy pmc = consultRules(name, in);
|
|
if (pmc == null) {
|
|
throw new CompileException("Cannot consult rules from the stream " + name);
|
|
}
|
|
return pmc;
|
|
}
|
|
|
|
private PrologMachineCopy createMachine(Project.NameKey project, ObjectId rulesId)
|
|
throws CompileException {
|
|
// If the rules are available as a complied JAR on local disk, prefer
|
|
// that over dynamic consult as the bytecode will be faster.
|
|
//
|
|
if (rulesDir != null) {
|
|
Path jarPath = rulesDir.resolve("rules-" + rulesId.getName() + ".jar");
|
|
if (Files.isRegularFile(jarPath)) {
|
|
URL[] cp = new URL[] {toURL(jarPath)};
|
|
return save(newEmptyMachine(URLClassLoader.newInstance(cp, systemLoader)));
|
|
}
|
|
}
|
|
|
|
// Dynamically consult the rules into the machine's internal database.
|
|
//
|
|
String rules = read(project, rulesId);
|
|
PrologMachineCopy pmc = consultRules("rules.pl", new StringReader(rules));
|
|
if (pmc == null) {
|
|
throw new CompileException("Cannot consult rules of " + project);
|
|
}
|
|
return pmc;
|
|
}
|
|
|
|
private PrologMachineCopy consultRules(String name, Reader rules) throws CompileException {
|
|
BufferingPrologControl ctl = newEmptyMachine(systemLoader);
|
|
PushbackReader in = new PushbackReader(rules, Prolog.PUSHBACK_SIZE);
|
|
try {
|
|
if (!ctl.execute(
|
|
Prolog.BUILTIN, "consult_stream", SymbolTerm.intern(name), new JavaObjectTerm(in))) {
|
|
return null;
|
|
}
|
|
} catch (SyntaxException e) {
|
|
throw new CompileException(e.toString(), e);
|
|
} catch (TermException e) {
|
|
Term m = e.getMessageTerm();
|
|
if (m instanceof StructureTerm && "syntax_error".equals(m.name()) && m.arity() >= 1) {
|
|
StringBuilder msg = new StringBuilder();
|
|
if (m.arg(0) instanceof ListTerm) {
|
|
msg.append(Joiner.on(' ').join(((ListTerm) m.arg(0)).toJava()));
|
|
} else {
|
|
msg.append(m.arg(0).toString());
|
|
}
|
|
if (m.arity() == 2 && m.arg(1) instanceof StructureTerm && "at".equals(m.arg(1).name())) {
|
|
Term at = m.arg(1).arg(0).dereference();
|
|
if (at instanceof ListTerm) {
|
|
msg.append(" at: ");
|
|
msg.append(prettyProlog(at));
|
|
}
|
|
}
|
|
throw new CompileException(msg.toString(), e);
|
|
}
|
|
throw new CompileException("Error while consulting rules from " + name, e);
|
|
} catch (RuntimeException e) {
|
|
throw new CompileException("Error while consulting rules from " + name, e);
|
|
}
|
|
return save(ctl);
|
|
}
|
|
|
|
private static String prettyProlog(Term at) {
|
|
StringBuilder b = new StringBuilder();
|
|
for (Object o : ((ListTerm) at).toJava()) {
|
|
if (o instanceof Term) {
|
|
Term t = (Term) o;
|
|
if (!(t instanceof StructureTerm)) {
|
|
b.append(t.toString()).append(' ');
|
|
continue;
|
|
}
|
|
switch (t.name()) {
|
|
case "atom":
|
|
SymbolTerm atom = (SymbolTerm) t.arg(0);
|
|
b.append(atom.toString());
|
|
break;
|
|
case "var":
|
|
b.append(t.arg(0).toString());
|
|
break;
|
|
}
|
|
} else {
|
|
b.append(o);
|
|
}
|
|
}
|
|
return b.toString().trim();
|
|
}
|
|
|
|
private String read(Project.NameKey project, ObjectId rulesId) throws CompileException {
|
|
try (Repository git = gitMgr.openRepository(project)) {
|
|
try {
|
|
ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
|
|
byte[] raw = ldr.getCachedBytes(maxSrcBytes);
|
|
return RawParseUtils.decode(raw);
|
|
} catch (LargeObjectException e) {
|
|
throw new CompileException("rules of " + project + " are too large", e);
|
|
} catch (RuntimeException | IOException e) {
|
|
throw new CompileException("Cannot load rules of " + project, e);
|
|
}
|
|
} catch (IOException e) {
|
|
throw new CompileException("Cannot open repository " + project, e);
|
|
}
|
|
}
|
|
|
|
private BufferingPrologControl newEmptyMachine(ClassLoader cl) {
|
|
BufferingPrologControl ctl = new BufferingPrologControl();
|
|
ctl.setMaxDatabaseSize(maxDbSize);
|
|
// Use the compiled reduction limit because the first term evaluation is done with
|
|
// consult_stream - an internal, combined Prolog term.
|
|
ctl.setReductionLimit(compileReductionLimit);
|
|
ctl.setPrologClassLoader(
|
|
new PrologClassLoader(new PredicateClassLoader(predicateProviders, cl)));
|
|
ctl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
|
|
|
|
List<String> packages = new ArrayList<>();
|
|
packages.addAll(PACKAGE_LIST);
|
|
predicateProviders.runEach(
|
|
predicateProvider -> packages.addAll(predicateProvider.getPackages()));
|
|
|
|
// Bootstrap the interpreter and ensure there is clean state.
|
|
ctl.initialize(packages.toArray(new String[packages.size()]));
|
|
return ctl;
|
|
}
|
|
|
|
private static URL toURL(Path jarPath) throws CompileException {
|
|
try {
|
|
return jarPath.toUri().toURL();
|
|
} catch (MalformedURLException e) {
|
|
throw new CompileException("Cannot create URL for " + jarPath, e);
|
|
}
|
|
}
|
|
}
|