ChangeScreen2: Poll for updates in the background

Poll every 30 seconds for updates made to the currently open
change. If a modification is detected show a small butter
bar in the bottom right corner advising the user they are
viewing a stale version of the change.

To be efficient this relies on the recently added support to
reply "304 Not Modified" when there are no updates.

change.updateDelay can be configured by the site administrator
to manage the polling frequency.

Change-Id: Ib3f3acf513193bf1f1f92c472cd586999f3cad5d
This commit is contained in:
Shawn Pearce 2013-07-19 19:45:25 -07:00
parent cd5035d179
commit b9ebb668f6
12 changed files with 555 additions and 32 deletions

View File

@ -705,6 +705,28 @@ link:cmd-flush-caches.html[gerrit flush-caches].
+
Default is 5 minutes.
[[change]]Section change
~~~~~~~~~~~~~~~~~~~~~~~~
[[change.updateDelay]]change.updateDelay::
+
How often in seconds the web interface should poll for updates to the
currently open change. The poller relies on the client's browser
cache to use If-Modified-Since and respect `304 Not Modified` HTTP
reponses. This allows for fast polls, often under 8 milliseconds.
+
With a configured 30 second delay a server with 4900 active users will
typically need to dedicate 1 CPU to the update check. 4900 users
divided by an average delay of 30 seconds is 163 requests arriving per
second. If requests are served at ~6 ms response time, 1 CPU is
necessary to keep up with the update request traffic. On a smaller
user base of 500 active users, the default 30 second delay is only 17
requests per second and requires ~10% CPU.
+
If 0 the update polling is disabled.
+
Default is 30 seconds.
[[changeMerge]]Section changeMerge
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -46,6 +46,7 @@ public class GerritConfig implements Cloneable {
protected boolean testChangeMerge;
protected String anonymousCowardName;
protected int suggestFrom;
protected int changeUpdateDelay;
public String getRegisterUrl() {
return registerUrl;
@ -225,4 +226,12 @@ public class GerritConfig implements Cloneable {
}
return true;
}
public int getChangeUpdateDelay() {
return changeUpdateDelay;
}
public void setChangeUpdateDelay(int seconds) {
changeUpdateDelay = seconds;
}
}

View File

@ -18,16 +18,20 @@ import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.event.dom.client.HasKeyPressHandlers;
import com.google.gwt.event.dom.client.HasMouseMoveHandlers;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
class DocWidget extends Widget implements HasKeyPressHandlers {
public class DocWidget extends Widget
implements HasKeyPressHandlers, HasMouseMoveHandlers {
private static DocWidget me;
static DocWidget get() {
public static DocWidget get() {
if (me == null) {
me = new DocWidget();
}
@ -45,6 +49,11 @@ class DocWidget extends Widget implements HasKeyPressHandlers {
return addDomHandler(handler, KeyPressEvent.getType());
}
@Override
public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
return addDomHandler(handler, MouseMoveEvent.getType());
}
private static Node docnode() {
return Document.get();
}

View File

@ -261,6 +261,10 @@ public class Gerrit implements EntryPoint {
return topMenu.isVisible();
}
public static RootPanel getBottomMenu() {
return bottomMenu;
}
/** Get the public configuration data used by this Gerrit instance. */
public static GerritConfig getConfig() {
return myConfig;

View File

@ -25,6 +25,7 @@ import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
import com.google.gerrit.client.changes.ChangeInfo.MergeableInfo;
import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.changes.ChangeList;
import com.google.gerrit.client.changes.StarredChanges;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.diff.DiffApi;
@ -36,10 +37,12 @@ import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.client.ui.UserActivityMonitor;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.changes.ListChangesOption;
import com.google.gerrit.reviewdb.client.Change;
@ -54,6 +57,8 @@ import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.CssResource;
@ -65,6 +70,7 @@ import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.ToggleButton;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwtexpui.clippy.client.CopyableLabel;
@ -73,6 +79,7 @@ import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.globalkey.client.KeyCommandSet;
import com.google.gwtorm.client.KeyUtil;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -97,11 +104,15 @@ public class ChangeScreen2 extends Screen {
private final Change.Id changeId;
private String revision;
private ChangeInfo changeInfo;
private CommentLinkProcessor commentLinkProcessor;
private KeyCommandSet keysNavigation;
private KeyCommandSet keysAction;
private List<HandlerRegistration> keys = new ArrayList<HandlerRegistration>(2);
private List<HandlerRegistration> handlers = new ArrayList<HandlerRegistration>(4);
private UpdateCheckTimer updateCheck;
private Timestamp lastDisplayedUpdate;
private UpdateAvailableBar updateAvailable;
@UiField Style style;
@UiField ToggleButton star;
@ -143,25 +154,40 @@ public class ChangeScreen2 extends Screen {
@Override
protected void onLoad() {
super.onLoad();
ChangeApi.detail(changeId.get(),
EnumSet.of(
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_ACTIONS),
new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo info) {
info.init();
loadConfigInfo(info);
}
});
loadChangeInfo(true, new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo info) {
info.init();
loadConfigInfo(info);
}
});
}
void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
RestApi call = ChangeApi.detail(changeId.get());
ChangeList.addOptions(call, EnumSet.of(
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_ACTIONS));
if (!fg) {
call.background();
}
call.get(cb);
}
@Override
protected void onUnload() {
for (HandlerRegistration h : keys) {
if (updateAvailable != null) {
updateAvailable.hide(true);
updateAvailable = null;
}
if (updateCheck != null) {
updateCheck.cancel();
updateCheck = null;
}
for (HandlerRegistration h : handlers) {
h.removeHandler();
}
keys.clear();
handlers.clear();
super.onUnload();
}
@ -207,8 +233,8 @@ public class ChangeScreen2 extends Screen {
@Override
public void registerKeys() {
super.registerKeys();
keys.add(GlobalKey.add(this, keysNavigation));
keys.add(GlobalKey.add(this, keysAction));
handlers.add(GlobalKey.add(this, keysNavigation));
handlers.add(GlobalKey.add(this, keysAction));
files.registerKeys();
}
@ -220,6 +246,7 @@ public class ChangeScreen2 extends Screen {
if (prior != null && prior.startsWith("/c/")) {
scrollToPath(prior.substring(3));
}
startPoller();
}
private void scrollToPath(String token) {
@ -405,6 +432,8 @@ public class ChangeScreen2 extends Screen {
}
private void renderChangeInfo(ChangeInfo info) {
changeInfo = info;
lastDisplayedUpdate = info.updated();
statusText.setInnerText(Util.toLongString(info.status()));
boolean current = info.status().isOpen()
&& revision.equals(info.current_revision());
@ -539,4 +568,53 @@ public class ChangeScreen2 extends Screen {
}
}
}
void showUpdates(ChangeInfo newInfo) {
if (!isAttached() || newInfo.updated().equals(lastDisplayedUpdate)) {
return;
}
JsArray<MessageInfo> om = changeInfo.messages();
JsArray<MessageInfo> nm = newInfo.messages();
if (om == null) {
om = JsArray.createArray().cast();
}
if (nm == null) {
nm = JsArray.createArray().cast();
}
if (updateAvailable == null) {
updateAvailable = new UpdateAvailableBar() {
@Override
void onShow() {
reload.reload();
}
void onIgnore(Timestamp newTime) {
lastDisplayedUpdate = newTime;
}
};
updateAvailable.addCloseHandler(new CloseHandler<PopupPanel>() {
@Override
public void onClose(CloseEvent<PopupPanel> event) {
updateAvailable = null;
}
});
}
updateAvailable.set(
Natives.asList(nm).subList(om.length(), nm.length()),
newInfo.updated());
if (!updateAvailable.isShowing()) {
updateAvailable.popup();
}
}
private void startPoller() {
if (Gerrit.isSignedIn() && 0 < Gerrit.getConfig().getChangeUpdateDelay()) {
updateCheck = new UpdateCheckTimer(this);
updateCheck.schedule();
handlers.add(UserActivityMonitor.addValueChangeHandler(updateCheck));
}
}
}

View File

@ -89,7 +89,7 @@ class Message extends Composite {
}
}
private static String authorName(MessageInfo info) {
static String authorName(MessageInfo info) {
if (info.author() != null) {
if (info.author().name() != null) {
return info.author().name();

View File

@ -0,0 +1,151 @@
// Copyright (C) 2013 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.client.change;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ScrollEvent;
import com.google.gwt.user.client.Window.ScrollHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import java.sql.Timestamp;
import java.util.HashSet;
import java.util.List;
/** Displays the "New Message From ..." panel in bottom right on updates. */
abstract class UpdateAvailableBar extends PopupPanel {
interface Binder extends UiBinder<HTMLPanel, UpdateAvailableBar> {}
private static Binder uiBinder = GWT.create(Binder.class);
static interface Style extends CssResource {
String popup();
}
private Timestamp updated;
private HandlerRegistration resizer;
private HandlerRegistration scroller;
@UiField Style style;
@UiField Element author;
@UiField Anchor show;
@UiField Anchor ignore;
UpdateAvailableBar() {
super(/* autoHide = */ false, /* modal = */ false);
add(uiBinder.createAndBindUi(this));
setStyleName(style.popup());
}
void set(List<MessageInfo> newMessages, Timestamp newTime) {
HashSet<Integer> seen = new HashSet<Integer>();
StringBuilder r = new StringBuilder();
for (MessageInfo m : newMessages) {
int a = m.author() != null ? m.author()._account_id() : 0;
if (seen.add(a)) {
if (r.length() > 0) {
r.append(", ");
}
r.append(Message.authorName(m));
}
}
author.setInnerText(r.toString());
updated = newTime;
if (isShowing()) {
setPopupPosition(
Window.getScrollLeft() + Window.getClientWidth() - getOffsetWidth(),
Window.getScrollTop() + Window.getClientHeight() - getOffsetHeight());
}
}
void popup() {
setPopupPositionAndShow(new PositionCallback() {
@Override
public void setPosition(int w, int h) {
w += 7; // Initial information is wrong, adjust with some slop.
h += 19;
setPopupPosition(
Window.getScrollLeft() + Window.getClientWidth() - w,
Window.getScrollTop() + Window.getClientHeight() - h);
}
});
if (resizer == null) {
resizer = Window.addResizeHandler(new ResizeHandler() {
@Override
public void onResize(ResizeEvent event) {
setPopupPosition(
Window.getScrollLeft() + event.getWidth() - getOffsetWidth(),
Window.getScrollTop() + event.getHeight() - getOffsetHeight());
}
});
}
if (scroller == null) {
scroller = Window.addWindowScrollHandler(new ScrollHandler() {
@Override
public void onWindowScroll(ScrollEvent event) {
RootPanel b = Gerrit.getBottomMenu();
int br = b.getAbsoluteLeft() + b.getOffsetWidth();
int bp = b.getAbsoluteTop() + b.getOffsetHeight();
int wr = event.getScrollLeft() + Window.getClientWidth();
int wp = event.getScrollTop() + Window.getClientHeight();
setPopupPosition(
Math.min(br, wr) - getOffsetWidth(),
Math.min(bp, wp) - getOffsetHeight());
}
});
}
}
@Override
public void hide() {
if (resizer != null) {
resizer.removeHandler();
resizer = null;
}
if (scroller != null) {
scroller.removeHandler();
scroller = null;
}
super.hide();
}
@UiHandler("show")
void onShow(ClickEvent e) {
onShow();
}
@UiHandler("ignore")
void onIgnore(ClickEvent e) {
onIgnore(updated);
hide();
}
abstract void onShow();
abstract void onIgnore(Timestamp newTime);
}

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2013 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.
-->
<ui:UiBinder
xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
xmlns:g='urn:import:com.google.gwt.user.client.ui'>
<ui:style type='com.google.gerrit.client.change.UpdateAvailableBar.Style'>
.popup {
padding: 5px;
}
.bar {
background-color: #fff1a8;
border: 1px solid #ccc;
padding: 5px 10px;
font-size: 80%;
color: #222;
white-space: nowrap;
width: auto;
height: auto;
}
.action {
color: #222;
display: inline-block;
margin-left: 0.5em;
}
</ui:style>
<g:HTMLPanel styleName='{style.bar}'>
<ui:msg>Update from <span ui:field='author'/></ui:msg>
<g:Anchor ui:field='show' styleName='{style.action}'>
<ui:msg>Show</ui:msg>
</g:Anchor>
<g:Anchor ui:field='ignore' styleName='{style.action}'>
<ui:msg>Ignore</ui:msg>
</g:Anchor>
</g:HTMLPanel>
</ui:UiBinder>

View File

@ -0,0 +1,95 @@
// Copyright (C) 2013 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.client.change;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.ui.UserActivityMonitor;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
class UpdateCheckTimer extends Timer implements ValueChangeHandler<Boolean> {
private static final int MAX_PERIOD = 3 * 60 * 1000;
private static final int IDLE_PERIOD = 2 * 3600 * 1000;
private static final int POLL_PERIOD =
Gerrit.getConfig().getChangeUpdateDelay() * 1000;
private final ChangeScreen2 screen;
private int delay;
private boolean running;
UpdateCheckTimer(ChangeScreen2 screen) {
this.screen = screen;
this.delay = POLL_PERIOD;
}
void schedule() {
scheduleRepeating(delay);
}
@Override
public void run() {
if (!screen.isAttached()) {
// screen should have cancelled this timer.
cancel();
return;
} else if (running) {
return;
}
running = true;
screen.loadChangeInfo(false, new AsyncCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo info) {
running = false;
screen.showUpdates(info);
int d = UserActivityMonitor.isActive()
? POLL_PERIOD
: IDLE_PERIOD;
if (d != delay) {
delay = d;
schedule();
}
}
@Override
public void onFailure(Throwable caught) {
// On failures increase the delay time and try again,
// but place an upper bound on the delay.
running = false;
delay = (int) Math.max(
delay * (1.5 + Math.random()),
UserActivityMonitor.isActive()
? MAX_PERIOD
: IDLE_PERIOD + MAX_PERIOD);
schedule();
}
});
}
@Override
public void onValueChange(ValueChangeEvent<Boolean> event) {
if (event.getValue()) {
delay = POLL_PERIOD;
run();
} else {
delay = IDLE_PERIOD;
}
schedule();
}
}

View File

@ -16,13 +16,10 @@ package com.google.gerrit.client.changes;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.common.changes.ListChangesOption;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.rpc.AsyncCallback;
import java.util.EnumSet;
/**
* A collection of static methods which work on the Gerrit REST API for specific
* changes.
@ -68,16 +65,7 @@ public class ChangeApi {
detail(id).get(cb);
}
public static void detail(int id, EnumSet<ListChangesOption> options,
AsyncCallback<ChangeInfo> cb) {
RestApi call = detail(id);
if (!options.isEmpty()) {
ChangeList.addOptions(call, options);
}
call.get(cb);
}
private static RestApi detail(int id) {
public static RestApi detail(int id) {
return call(id, "detail");
}

View File

@ -0,0 +1,113 @@
// Copyright (C) 2013 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.client.ui;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.SimpleEventBus;
import com.google.gwt.user.client.History;
import com.google.gwtexpui.globalkey.client.DocWidget;
/** Checks for user keyboard and mouse activity. */
public class UserActivityMonitor {
private static final long TIMEOUT = 10 * 60 * 1000;
private static final MonitorImpl impl;
/**
* @return true if there has been keyboard and/or mouse activity in recent
* enough history to believe a user is still controlling this session.
*/
public static boolean isActive() {
return impl.active || impl.recent;
}
public static HandlerRegistration addValueChangeHandler(
ValueChangeHandler<Boolean> handler) {
return impl.addValueChangeHandler(handler);
}
static {
impl = new MonitorImpl();
DocWidget.get().addKeyPressHandler(impl);
DocWidget.get().addMouseMoveHandler(impl);
History.addValueChangeHandler(impl);
Scheduler.get().scheduleFixedDelay(impl, 60 * 1000);
}
private UserActivityMonitor() {
}
private static class MonitorImpl implements RepeatingCommand,
KeyPressHandler, MouseMoveHandler, ValueChangeHandler<String>,
HasValueChangeHandlers<Boolean> {
private final EventBus bus = new SimpleEventBus();
private boolean recent = true;
private boolean active = true;
private long last = System.currentTimeMillis();
@Override
public void onKeyPress(KeyPressEvent event) {
recent = true;
}
@Override
public void onMouseMove(MouseMoveEvent event) {
recent = true;
}
@Override
public void onValueChange(ValueChangeEvent<String> event) {
recent = true;
}
@Override
public boolean execute() {
long now = System.currentTimeMillis();
if (recent) {
if (!active) {
ValueChangeEvent.fire(this, active);
}
recent = false;
active = true;
last = now;
} else if (active && (now - last) > TIMEOUT) {
active = false;
ValueChangeEvent.fire(this, false);
}
return true;
}
@Override
public HandlerRegistration addValueChangeHandler(
ValueChangeHandler<Boolean> handler) {
return bus.addHandler(ValueChangeEvent.getType(), handler);
}
@Override
public void fireEvent(GwtEvent<?> event) {
bus.fireEvent(event);
}
}
}

View File

@ -21,6 +21,7 @@ import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.DownloadConfig;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.contact.ContactStore;
@ -35,6 +36,7 @@ import org.eclipse.jgit.lib.Config;
import java.net.MalformedURLException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext;
@ -115,6 +117,8 @@ class GerritConfigProvider implements Provider<GerritConfig> {
"test", false));
config.setAnonymousCowardName(anonymousCowardName);
config.setSuggestFrom(cfg.getInt("suggest", "from", 0));
config.setChangeUpdateDelay((int) ConfigUtil.getTimeUnit(
cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS));
config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
if (config.getReportBugUrl() == null) {