// 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.server.git; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import com.google.gerrit.reviewdb.Branch; import com.google.gerrit.reviewdb.Project; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.RemotePeer; import com.google.gerrit.server.RequestCleanup; import com.google.gerrit.server.config.GerritRequestModule; import com.google.gerrit.server.ssh.SshInfo; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.OutOfScopeException; import com.google.inject.Provider; import com.google.inject.Scope; import com.google.inject.servlet.RequestScoped; import com.jcraft.jsch.HostKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.SocketAddress; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; public class ChangeMergeQueue implements MergeQueue { private static final Logger log = LoggerFactory.getLogger(ChangeMergeQueue.class); private final Map<Branch.NameKey, MergeEntry> active = new HashMap<Branch.NameKey, MergeEntry>(); private final Map<Branch.NameKey, RecheckJob> recheck = new HashMap<Branch.NameKey, RecheckJob>(); private final WorkQueue workQueue; private final Provider<MergeOp.Factory> bgFactory; @Inject ChangeMergeQueue(final WorkQueue wq, Injector parent) { workQueue = wq; Injector child = parent.createChildInjector(new AbstractModule() { @Override protected void configure() { bindScope(RequestScoped.class, MyScope.REQUEST); install(new GerritRequestModule()); bind(CurrentUser.class).to(IdentifiedUser.class); bind(IdentifiedUser.class).toProvider(new Provider<IdentifiedUser>() { @Override public IdentifiedUser get() { throw new OutOfScopeException("No user on merge thread"); } }); bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider( new Provider<SocketAddress>() { @Override public SocketAddress get() { throw new OutOfScopeException("No remote peer on merge thread"); } }); bind(SshInfo.class).toInstance(new SshInfo() { @Override public List<HostKey> getHostKeys() { return Collections.emptyList(); } }); } }); bgFactory = child.getProvider(MergeOp.Factory.class); } @Override public void merge(MergeOp.Factory mof, Branch.NameKey branch) { if (start(branch)) { mergeImpl(mof, branch); } } private synchronized boolean start(final Branch.NameKey branch) { final MergeEntry e = active.get(branch); if (e == null) { // Let the caller attempt this merge, its the only one interested // in processing this branch right now. // active.put(branch, new MergeEntry(branch)); return true; } else { // Request that the job queue handle this merge later. // e.needMerge = true; return false; } } @Override public synchronized void schedule(final Branch.NameKey branch) { MergeEntry e = active.get(branch); if (e == null) { e = new MergeEntry(branch); active.put(branch, e); } e.needMerge = true; scheduleJob(e); } @Override public synchronized void recheckAfter(final Branch.NameKey branch, final long delay, final TimeUnit delayUnit) { final long now = System.currentTimeMillis(); final long at = now + MILLISECONDS.convert(delay, delayUnit); RecheckJob e = recheck.get(branch); if (e == null) { e = new RecheckJob(branch); workQueue.getDefaultQueue().schedule(e, now - at, MILLISECONDS); recheck.put(branch, e); } e.recheckAt = Math.max(at, e.recheckAt); } private synchronized void finish(final Branch.NameKey branch) { final MergeEntry e = active.get(branch); if (e == null) { // Not registered? Shouldn't happen but ignore it. // return; } if (!e.needMerge) { // No additional merges are in progress, we can delete it. // active.remove(branch); return; } scheduleJob(e); } private void scheduleJob(final MergeEntry e) { if (!e.jobScheduled) { // No job has been scheduled to execute this branch, but it needs // to run a merge again. // e.jobScheduled = true; workQueue.getDefaultQueue().schedule(e, 0, TimeUnit.SECONDS); } } private synchronized void unschedule(final MergeEntry e) { e.jobScheduled = false; e.needMerge = false; } private void mergeImpl(MergeOp.Factory opFactory, Branch.NameKey branch) { try { opFactory.create(branch).merge(); } catch (Throwable e) { log.error("Merge attempt for " + branch + " failed", e); } finally { finish(branch); } } private void mergeImpl(Branch.NameKey branch) { try { MyScope ctx = new MyScope(); MyScope old = MyScope.set(ctx); try { try { bgFactory.get().create(branch).merge(); } finally { ctx.cleanup.run(); } } finally { MyScope.set(old); } } catch (Throwable e) { log.error("Merge attempt for " + branch + " failed", e); } finally { finish(branch); } } private synchronized void recheck(final RecheckJob e) { final long remainingDelay = e.recheckAt - System.currentTimeMillis(); if (MILLISECONDS.convert(10, SECONDS) < remainingDelay) { // Woke up too early, the job deadline was pushed back. // Reschedule for the new deadline. We allow for a small // amount of fuzz due to multiple reschedule attempts in // a short period of time being caused by MergeOp. // workQueue.getDefaultQueue().schedule(e, remainingDelay, MILLISECONDS); } else { // Schedule a merge attempt on this branch to see if we can // actually complete it this time. // schedule(e.dest); } } private class MergeEntry implements Runnable { final Branch.NameKey dest; boolean needMerge; boolean jobScheduled; MergeEntry(final Branch.NameKey d) { dest = d; } public void run() { unschedule(this); mergeImpl(dest); } @Override public String toString() { final Project.NameKey project = dest.getParentKey(); return "submit " + project.get() + " " + dest.getShortName(); } } private class RecheckJob implements Runnable { final Branch.NameKey dest; long recheckAt; RecheckJob(final Branch.NameKey d) { dest = d; } @Override public void run() { recheck(this); } @Override public String toString() { final Project.NameKey project = dest.getParentKey(); return "recheck " + project.get() + " " + dest.getShortName(); } } private static class MyScope { private static final ThreadLocal<MyScope> current = new ThreadLocal<MyScope>(); private static MyScope getContext() { final MyScope ctx = current.get(); if (ctx == null) { throw new OutOfScopeException("Not in command/request"); } return ctx; } static MyScope set(MyScope ctx) { MyScope old = current.get(); current.set(ctx); return old; } static final Scope REQUEST = new Scope() { public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { return new Provider<T>() { public T get() { return getContext().get(key, creator); } @Override public String toString() { return String.format("%s[%s]", creator, REQUEST); } }; } @Override public String toString() { return "MergeQueue.REQUEST"; } }; private static final Key<RequestCleanup> RC_KEY = Key.get(RequestCleanup.class); private final RequestCleanup cleanup; private final Map<Key<?>, Object> map; MyScope() { cleanup = new RequestCleanup(); map = new HashMap<Key<?>, Object>(); map.put(RC_KEY, cleanup); } synchronized <T> T get(Key<T> key, Provider<T> creator) { @SuppressWarnings("unchecked") T t = (T) map.get(key); if (t == null) { t = creator.get(); map.put(key, t); } return t; } } }