Make ReviewDb available in the server on a per-thread basis

Allow types bound in the system injector to access the per-thread
ReviewDb if they are invoked from within a request context, without
caring about which entry they were invoked from. This allows email
sending and some other change update action code to be bound at the
global level and work in both SSH and HTTP invocations.

Change-Id: I411ded5196ae6920573233fb0cde48f35ce7ad01
This commit is contained in:
Shawn O. Pearce
2012-11-13 16:09:21 -08:00
parent 3d45206d1c
commit 401440f975
17 changed files with 187 additions and 72 deletions

View File

@@ -14,20 +14,31 @@
package com.google.gerrit.httpd;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
import com.google.gerrit.server.util.RequestContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
class HttpRequestContext implements RequestContext {
private final WebSession session;
private final RequestScopedReviewDbProvider provider;
@Inject
HttpRequestContext(final WebSession session) {
HttpRequestContext(WebSession session,
RequestScopedReviewDbProvider provider) {
this.session = session;
this.provider = provider;
}
@Override
public CurrentUser getCurrentUser() {
return session.getCurrentUser();
}
@Override
public Provider<ReviewDb> getReviewDbProvider() {
return provider;
}
}

View File

@@ -34,6 +34,7 @@ import com.google.gerrit.server.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.account.AccountByEmailCacheImpl;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountVisibility;
import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.CapabilityControl;
@@ -60,6 +61,7 @@ import com.google.gerrit.server.git.ReloadSubmitQueueOp;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.mail.EmailModule;
import com.google.gerrit.server.mail.FromAddressGenerator;
import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
import com.google.gerrit.server.mail.VelocityRuntimeProvider;
@@ -73,6 +75,8 @@ import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectNode;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -130,12 +134,17 @@ public class GerritGlobalModule extends FactoryModule {
install(ChangeCache.module());
install(new AccessControlModule());
install(new EmailModule());
install(new GitModule());
install(new PrologModule());
install(ThreadLocalRequestContext.module());
bind(AccountResolver.class);
bind(ChangeQueryRewriter.class);
factory(AccountInfoCacheFactory.Factory.class);
factory(CapabilityControl.Factory.class);
factory(ChangeQueryBuilder.Factory.class);
factory(GroupInfoCacheFactory.Factory.class);
factory(InternalUser.Factory.class);
factory(ProjectNode.Factory.class);

View File

@@ -16,12 +16,10 @@ package com.google.gerrit.server.config;
import static com.google.inject.Scopes.SINGLETON;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestCleanup;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.GroupDetailFactory;
import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.account.PerformCreateGroup;
@@ -39,7 +37,6 @@ import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.NotesBranchUtil;
import com.google.gerrit.server.git.SubmoduleOp;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.AddReviewerSender;
import com.google.gerrit.server.mail.CommentSender;
import com.google.gerrit.server.mail.CreateChangeSender;
@@ -58,8 +55,6 @@ import com.google.gerrit.server.project.ListProjects;
import com.google.gerrit.server.project.PerRequestProjectControlCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.SuggestParentCandidates;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.inject.servlet.RequestScoped;
/** Bindings for {@link RequestScoped} entities. */
@@ -67,12 +62,9 @@ public class GerritRequestModule extends FactoryModule {
@Override
protected void configure() {
bind(RequestCleanup.class).in(RequestScoped.class);
bind(ReviewDb.class).toProvider(RequestScopedReviewDbProvider.class).in(
RequestScoped.class);
bind(RequestScopedReviewDbProvider.class);
bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
bind(MetaDataUpdate.User.class).in(RequestScoped.class);
bind(AccountResolver.class);
bind(ChangeQueryRewriter.class);
bind(ListProjects.class);
bind(ListDashboards.class);
bind(ApprovalsUtil.class);
@@ -82,7 +74,6 @@ public class GerritRequestModule extends FactoryModule {
bind(ProjectControl.Factory.class).in(SINGLETON);
bind(AccountControl.Factory.class).in(SINGLETON);
factory(ChangeQueryBuilder.Factory.class);
factory(SubmoduleOp.Factory.class);
factory(MergeOp.Factory.class);
factory(CreateCodeReviewNotes.Factory.class);
@@ -101,7 +92,6 @@ public class GerritRequestModule extends FactoryModule {
factory(RebaseChange.Factory.class);
factory(ReplacePatchSetSender.Factory.class);
factory(RebasedPatchSetSender.Factory.class);
factory(AbandonedSender.Factory.class);
factory(RemoveReviewer.Factory.class);
factory(RestoredSender.Factory.class);
factory(RevertedSender.Factory.class);

View File

@@ -21,16 +21,17 @@ import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import com.google.inject.servlet.RequestScoped;
/** Provides {@link ReviewDb} database handle live only for this request. */
@Singleton
final class RequestScopedReviewDbProvider implements Provider<ReviewDb> {
@RequestScoped
public class RequestScopedReviewDbProvider implements Provider<ReviewDb> {
private final SchemaFactory<ReviewDb> schema;
private final Provider<RequestCleanup> cleanup;
private ReviewDb db;
@Inject
RequestScopedReviewDbProvider(final SchemaFactory<ReviewDb> schema,
public RequestScopedReviewDbProvider(final SchemaFactory<ReviewDb> schema,
final Provider<RequestCleanup> cleanup) {
this.schema = schema;
this.cleanup = cleanup;
@@ -38,6 +39,7 @@ final class RequestScopedReviewDbProvider implements Provider<ReviewDb> {
@Override
public ReviewDb get() {
if (db == null) {
final ReviewDb c;
try {
c = schema.open();
@@ -49,15 +51,15 @@ final class RequestScopedReviewDbProvider implements Provider<ReviewDb> {
@Override
public void run() {
c.close();
db = null;
}
});
return c;
} catch (Error e) {
} catch (Throwable e) {
c.close();
throw e;
} catch (RuntimeException e) {
c.close();
throw e;
}
throw new ProvisionException("Cannot defer cleanup of ReviewDb", e);
}
db = c;
}
return db;
}
}

View File

@@ -19,9 +19,11 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.RequestScopePropagator;
@@ -91,12 +93,18 @@ public class ChangeMergeQueue implements MergeQueue {
@Provides
public PerThreadRequestScope.Scoper provideScoper(
final PerThreadRequestScope.Propagator propagator) {
final PerThreadRequestScope.Propagator propagator,
final Provider<RequestScopedReviewDbProvider> dbProvider) {
final RequestContext requestContext = new RequestContext() {
@Override
public CurrentUser getCurrentUser() {
throw new OutOfScopeException("No user on merge thread");
}
@Override
public Provider<ReviewDb> getReviewDbProvider() {
return dbProvider.get();
}
};
return new PerThreadRequestScope.Scoper() {
@Override

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.server.git;
import com.google.common.collect.Maps;
import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
@@ -52,8 +53,9 @@ public class PerThreadRequestScope {
public static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
@Inject
Propagator(ThreadLocalRequestContext local) {
super(REQUEST, current, local);
Propagator(ThreadLocalRequestContext local,
Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
super(REQUEST, current, local, dbProviderProvider);
}
@Override

View File

@@ -0,0 +1,24 @@
// Copyright (C) 2012 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.mail;
import com.google.gerrit.server.config.FactoryModule;
public class EmailModule extends FactoryModule {
@Override
protected void configure() {
factory(AbandonedSender.Factory.class);
}
}

View File

@@ -14,9 +14,12 @@
package com.google.gerrit.server.util;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
/**
@@ -37,4 +40,15 @@ public class FallbackRequestContext implements RequestContext {
public CurrentUser getCurrentUser() {
return user;
}
@Override
public Provider<ReviewDb> getReviewDbProvider() {
return new Provider<ReviewDb>() {
@Override
public ReviewDb get() {
throw new ProvisionException(
"Automatic ReviewDb only available in request scope");
}
};
}
}

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.server.util;
import com.google.common.collect.Maps;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.Provider;
@@ -42,8 +43,9 @@ public class GuiceRequestScopePropagator extends RequestScopePropagator {
GuiceRequestScopePropagator(
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
@RemotePeer Provider<SocketAddress> remotePeerProvider,
ThreadLocalRequestContext local) {
super(ServletScopes.REQUEST, local);
ThreadLocalRequestContext local,
Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
super(ServletScopes.REQUEST, local, dbProviderProvider);
this.url = urlProvider != null ? urlProvider.get() : null;
this.peer = remotePeerProvider.get();
}

View File

@@ -14,13 +14,15 @@
package com.google.gerrit.server.util;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.inject.Provider;
/**
* The RequestContext is an interface exposing the fields that are needed
* by the GerritGlobalModule scope.
*/
public interface RequestContext {
CurrentUser getCurrentUser();
Provider<ReviewDb> getReviewDbProvider();
}

View File

@@ -14,8 +14,12 @@
package com.google.gerrit.server.util;
import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.common.base.Throwables;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.RequestCleanup;
import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
import com.google.gerrit.server.git.ProjectRunnable;
import com.google.inject.Key;
import com.google.inject.Provider;
@@ -43,11 +47,14 @@ public abstract class RequestScopePropagator {
private final Scope scope;
private final ThreadLocalRequestContext local;
private final Provider<RequestScopedReviewDbProvider> dbProviderProvider;
protected RequestScopePropagator(Scope scope,
ThreadLocalRequestContext local) {
ThreadLocalRequestContext local,
Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
this.scope = scope;
this.local = local;
this.dbProviderProvider = dbProviderProvider;
}
/**
@@ -111,15 +118,14 @@ public abstract class RequestScopePropagator {
public void run() {
try {
wrapped.call();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
Throwables.propagateIfPossible(e);
throw new RuntimeException(e); // Not possible.
}
}
@Override
public NameKey getProjectNameKey() {
public Project.NameKey getProjectNameKey() {
return ((ProjectRunnable) runnable).getProjectNameKey();
}
@@ -164,12 +170,23 @@ public abstract class RequestScopePropagator {
*/
protected abstract <T> Callable<T> wrapImpl(final Callable<T> callable);
protected <T> Callable<T> context(final RequestContext context,
protected <T> Callable<T> context(RequestContext context,
final Callable<T> callable) {
final CurrentUser user = context.getCurrentUser();
return new Callable<T>() {
@Override
public T call() throws Exception {
RequestContext old = local.setContext(context);
RequestContext old = local.setContext(new RequestContext() {
@Override
public CurrentUser getCurrentUser() {
return user;
}
@Override
public Provider<ReviewDb> getReviewDbProvider() {
return dbProviderProvider.get();
}
});
try {
return callable.call();
} finally {
@@ -191,7 +208,6 @@ public abstract class RequestScopePropagator {
return new RequestCleanup();
}
}).get();
try {
return callable.call();
} finally {

View File

@@ -16,6 +16,7 @@ package com.google.gerrit.server.util;
import com.google.common.base.Objects;
import com.google.gerrit.common.errors.NotSignedInException;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.AbstractModule;
@@ -64,6 +65,11 @@ public class ThreadLocalRequestContext {
throw new ProvisionException(NotSignedInException.MESSAGE,
new NotSignedInException());
}
@Provides
ReviewDb provideReviewDb(RequestContext ctx) {
return ctx.getReviewDbProvider().get();
}
};
}

View File

@@ -14,7 +14,9 @@
package com.google.gerrit.server.util;
import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.Scope;
import java.util.concurrent.Callable;
@@ -31,8 +33,10 @@ public abstract class ThreadLocalRequestScopePropagator<C>
private final ThreadLocal<C> threadLocal;
protected ThreadLocalRequestScopePropagator(Scope scope,
ThreadLocal<C> threadLocal, ThreadLocalRequestContext local) {
super(scope, local);
ThreadLocal<C> threadLocal,
ThreadLocalRequestContext local,
Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
super(scope, local, dbProviderProvider);
this.threadLocal = threadLocal;
}

View File

@@ -16,9 +16,11 @@ package com.google.gerrit.sshd;
import com.google.common.util.concurrent.Atomics;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.sshd.SshScope.Context;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -56,15 +58,17 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
private final SshScope sshScope;
private final ScheduledExecutorService startExecutor;
private final Executor destroyExecutor;
private final SchemaFactory<ReviewDb> schemaFactory;
@Inject
CommandFactoryProvider(
@CommandName(Commands.ROOT) final DispatchCommandProvider d,
@GerritServerConfig final Config cfg, final WorkQueue workQueue,
final SshLog l, final SshScope s) {
final SshLog l, final SshScope s, SchemaFactory<ReviewDb> sf) {
dispatcher = d;
log = l;
sshScope = s;
schemaFactory = sf;
int threads = cfg.getInt("sshd","commandStartThreads", 2);
startExecutor = workQueue.createQueue(threads, "SshCommandStart");
@@ -122,7 +126,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
public void setSession(final ServerSession session) {
final SshSession s = session.getAttribute(SshSession.KEY);
this.ctx = sshScope.newContext(s, commandLine);
this.ctx = sshScope.newContext(schemaFactory, s, commandLine);
}
public void start(final Environment env) throws IOException {

View File

@@ -170,7 +170,7 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
// session, record a login event in the log and add
// a close listener to record a logout event.
//
Context ctx = sshScope.newContext(sd, null);
Context ctx = sshScope.newContext(null, sd, null);
Context old = sshScope.set(ctx);
try {
sshLog.onLogin();
@@ -182,7 +182,7 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
new IoFutureListener<IoFuture>() {
@Override
public void operationComplete(IoFuture future) {
final Context ctx = sshScope.newContext(sd, null);
final Context ctx = sshScope.newContext(null, sd, null);
final Context old = sshScope.set(ctx);
try {
sshLog.onLogout();

View File

@@ -15,10 +15,12 @@
package com.google.gerrit.sshd;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gerrit.sshd.SshScope.Context;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -58,6 +60,7 @@ class NoShell implements Factory<Command> {
static class SendMessage implements Command, SessionAware {
private final Provider<MessageFactory> messageFactory;
private final SchemaFactory<ReviewDb> schemaFactory;
private final SshScope sshScope;
private InputStream in;
@@ -67,8 +70,10 @@ class NoShell implements Factory<Command> {
private Context context;
@Inject
SendMessage(Provider<MessageFactory> messageFactory, SshScope sshScope) {
SendMessage(Provider<MessageFactory> messageFactory,
SchemaFactory<ReviewDb> sf, SshScope sshScope) {
this.messageFactory = messageFactory;
this.schemaFactory = sf;
this.sshScope = sshScope;
}
@@ -89,7 +94,8 @@ class NoShell implements Factory<Command> {
}
public void setSession(final ServerSession session) {
this.context = sshScope.newContext(session.getAttribute(SshSession.KEY), "");
SshSession s = session.getAttribute(SshSession.KEY);
this.context = sshScope.newContext(schemaFactory, s, "");
}
public void start(final Environment env) throws IOException {

View File

@@ -14,19 +14,23 @@
package com.google.gerrit.sshd;
import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestCleanup;
import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.util.Providers;
import java.util.HashMap;
import java.util.Map;
/** Guice scopes for state during an SSH connection. */
@@ -34,29 +38,34 @@ class SshScope {
private static final Key<RequestCleanup> RC_KEY =
Key.get(RequestCleanup.class);
private static final Key<RequestScopedReviewDbProvider> DB_KEY =
Key.get(RequestScopedReviewDbProvider.class);
class Context implements RequestContext {
private final RequestCleanup cleanup;
private final RequestCleanup cleanup = new RequestCleanup();
private final Map<Key<?>, Object> map = Maps.newHashMap();
private final SchemaFactory<ReviewDb> schemaFactory;
private final SshSession session;
private final String commandLine;
private final Map<Key<?>, Object> map;
final long created;
volatile long started;
volatile long finished;
private Context(final SshSession s, final String c, final long at) {
cleanup = new RequestCleanup();
private Context(SchemaFactory<ReviewDb> sf, final SshSession s,
final String c, final long at) {
schemaFactory = sf;
session = s;
commandLine = c;
map = new HashMap<Key<?>, Object>();
map.put(RC_KEY, cleanup);
created = started = finished = at;
map.put(RC_KEY, cleanup);
map.put(DB_KEY, new RequestScopedReviewDbProvider(
schemaFactory,
Providers.of(cleanup)));
}
private Context(Context p, SshSession s, String c) {
this(s, c, p.created);
this(p.schemaFactory, s, c, p.created);
started = p.started;
finished = p.finished;
}
@@ -78,6 +87,11 @@ class SshScope {
return user;
}
@Override
public Provider<ReviewDb> getReviewDbProvider() {
return (RequestScopedReviewDbProvider) map.get(DB_KEY);
}
synchronized <T> T get(Key<T> key, Provider<T> creator) {
@SuppressWarnings("unchecked")
T t = (T) map.get(key);
@@ -113,8 +127,9 @@ class SshScope {
private final SshScope sshScope;
@Inject
Propagator(SshScope sshScope, ThreadLocalRequestContext local) {
super(REQUEST, current, local);
Propagator(SshScope sshScope, ThreadLocalRequestContext local,
Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
super(REQUEST, current, local, dbProviderProvider);
this.sshScope = sshScope;
}
@@ -147,8 +162,8 @@ class SshScope {
this.userFactory = userFactory;
}
Context newContext(SshSession session, String commandLine) {
return new Context(session, commandLine, System.currentTimeMillis());
Context newContext(SchemaFactory<ReviewDb> sf, SshSession s, String cmd) {
return new Context(sf, s, cmd, System.currentTimeMillis());
}
private Context newContinuingContext(Context ctx) {