| // 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.rules; |
| |
| import static com.googlecode.prolog_cafe.lang.PrologMachineCopy.save; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.gerrit.extensions.registration.DynamicSet; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.reviewdb.client.RefNames; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.config.SitePaths; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.inject.Inject; |
| import com.google.inject.Singleton; |
| |
| 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 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; |
| |
| import java.io.IOException; |
| import java.io.PushbackReader; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.lang.ref.Reference; |
| import java.lang.ref.ReferenceQueue; |
| import java.lang.ref.WeakReference; |
| 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.EnumSet; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * 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 { |
| /** Maximum size of a dynamic Prolog script, in bytes. */ |
| private static final int SRC_LIMIT = 128 * 1024; |
| |
| /** Default size of the internal Prolog database within each interpreter. */ |
| private static final int DB_MAX = 256; |
| |
| private static final List<String> PACKAGE_LIST = ImmutableList.of( |
| Prolog.BUILTIN, "gerrit"); |
| |
| private final Map<ObjectId, MachineRef> machineCache = new HashMap<>(); |
| |
| private final ReferenceQueue<PrologMachineCopy> dead = |
| new ReferenceQueue<>(); |
| |
| private static final class MachineRef extends WeakReference<PrologMachineCopy> { |
| final ObjectId key; |
| |
| MachineRef(ObjectId key, PrologMachineCopy pcm, |
| ReferenceQueue<PrologMachineCopy> queue) { |
| super(pcm, queue); |
| this.key = key; |
| } |
| } |
| |
| private final boolean enableProjectRules; |
| private final Path cacheDir; |
| private final Path rulesDir; |
| private final GitRepositoryManager gitMgr; |
| private final DynamicSet<PredicateProvider> predicateProviders; |
| private final ClassLoader systemLoader; |
| private final PrologMachineCopy defaultMachine; |
| |
| @Inject |
| protected RulesCache(@GerritServerConfig Config config, SitePaths site, |
| GitRepositoryManager gm, DynamicSet<PredicateProvider> predicateProviders) { |
| enableProjectRules = config.getBoolean("rules", null, "enable", true); |
| cacheDir = site.resolve(config.getString("cache", null, "directory")); |
| rulesDir = cacheDir != null ? cacheDir.resolve("rules") : null; |
| gitMgr = gm; |
| this.predicateProviders = predicateProviders; |
| |
| 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; |
| } |
| |
| Reference<? extends PrologMachineCopy> ref = machineCache.get(rulesId); |
| if (ref != null) { |
| PrologMachineCopy pmc = ref.get(); |
| if (pmc != null) { |
| return pmc; |
| } |
| |
| machineCache.remove(rulesId); |
| ref.enqueue(); |
| } |
| |
| gc(); |
| |
| PrologMachineCopy pcm = createMachine(project, rulesId); |
| MachineRef newRef = new MachineRef(rulesId, pcm, dead); |
| machineCache.put(rulesId, newRef); |
| return pcm; |
| } |
| |
| 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 void gc() { |
| Reference<?> ref; |
| while ((ref = dead.poll()) != null) { |
| ObjectId key = ((MachineRef) ref).key; |
| if (machineCache.get(key) == ref) { |
| machineCache.remove(key); |
| } |
| } |
| } |
| |
| 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(new URLClassLoader(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(SRC_LIMIT); |
| 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(DB_MAX); |
| ctl.setPrologClassLoader(new PrologClassLoader(new PredicateClassLoader( |
| predicateProviders, cl))); |
| ctl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false); |
| |
| List<String> packages = Lists.newArrayList(); |
| packages.addAll(PACKAGE_LIST); |
| for (PredicateProvider predicateProvider : predicateProviders) { |
| 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); |
| } |
| } |
| } |