Merge branch 'improve-conf'
diff --git a/resources/adaptorlib/static/dashboard.js b/resources/adaptorlib/static/dashboard.js
index 1eec058..e3c9bd6 100755
--- a/resources/adaptorlib/static/dashboard.js
+++ b/resources/adaptorlib/static/dashboard.js
@@ -242,6 +242,27 @@
});
}
+function checkConfig() {
+ var sending = $('#gaf-check-config-sending');
+ sending.show();
+ rpc('checkForUpdatedConfig', null, function(result, error) {
+ sending.hide();
+ if (result === null) {
+ throw "Invalid response from server";
+ }
+ var notificationSpan = result ? $('#gaf-check-config-updated')
+ : $('#gaf-check-config-not-updated');
+ notificationSpan.show();
+ window.setTimeout(function() {
+ notificationSpan.fadeOut();
+ }, 5000);
+ if (result) {
+ // Config was updated; auto-update displayed config.
+ rpc('getConfig', null, getConfigCallback);
+ }
+ });
+}
+
function getLogCallback(result, error) {
if (result === null) {
throw error;
@@ -257,6 +278,7 @@
throw error;
} else {
var configTable = $('#gaf-config-table');
+ configTable.empty();
var keys = [];
var key;
for (key in result) {
@@ -327,4 +349,5 @@
rpc('getConfig', null, getConfigCallback);
rpc('getLog', null, getLogCallback);
$('#gaf-start-feed-push').click(startFeedPush);
+ $('#gaf-check-config').click(checkConfig);
});
diff --git a/resources/adaptorlib/static/index.html b/resources/adaptorlib/static/index.html
index 1723929..8454907 100755
--- a/resources/adaptorlib/static/index.html
+++ b/resources/adaptorlib/static/index.html
@@ -25,6 +25,13 @@
<span id="gaf-start-feed-push-error"></span>
</div>
+ <div>
+ <button id="gaf-check-config">Apply Updated Config</button>
+ <span id="gaf-check-config-sending" style="display: none">Sending request...</span>
+ <span id="gaf-check-config-updated" style="display: none">Configuration updated</span>
+ <span id="gaf-check-config-not-updated" style="display: none">No configuration changes noticed; nothing updated.</span>
+ </div>
+
<h2>Status</h2>
<table id="gaf-status-table"></table>
diff --git a/src/adaptorlib/Config.java b/src/adaptorlib/Config.java
index 2b2b7b1..fb6cc03 100644
--- a/src/adaptorlib/Config.java
+++ b/src/adaptorlib/Config.java
@@ -21,6 +21,7 @@
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.*;
/**
@@ -37,14 +38,19 @@
protected final Set<String> noDefaultConfig = new HashSet<String>();
/** Default configuration values. */
protected final Properties defaultConfig = new Properties();
- /** Overriding configuration values loaded from file. */
- protected Properties configFileProperties = new Properties(defaultConfig);
/** Overriding configuration values loaded from command line. */
- protected Properties config = new Properties(configFileProperties);
- protected File configFile = new File(DEFAULT_CONFIG_FILE);
+ // Reads require no additional locks, but modifications require lock on 'this'
+ // to prevent lost updates.
+ protected volatile Properties config = new Properties(defaultConfig);
+ /** Default configuration to use in {@link #loadDefaultConfigFile}. */
+ protected File defaultConfigFile = new File(DEFAULT_CONFIG_FILE);
+ /**
+ * The actual config file in use, or {@code null} if none have been loaded.
+ */
+ protected File configFile;
protected long configFileLastModified;
protected List<ConfigModificationListener> modificationListeners
- = new LinkedList<ConfigModificationListener>();
+ = new CopyOnWriteArrayList<ConfigModificationListener>();
public Config() {
String hostname = null;
@@ -79,8 +85,6 @@
addKey("adaptor.fullListingSchedule", "0 3 * * *");
// 15 minutes.
addKey("adaptor.incrementalPollPeriodSecs", "900");
- // In seconds.
- addKey("config.pollPeriodSecs", "30");
addKey("transform.pipeline", "");
// 1 MiB.
addKey("transform.maxDocumentBytes", "1048576");
@@ -302,13 +306,6 @@
}
/**
- * Period in milliseconds between checks for updated configuration.
- */
- public long getConfigPollPeriodMillis() {
- return Long.parseLong(getValue("config.pollPeriodSecs")) * 1000;
- }
-
- /**
* Returns a list of maps correspending to each transform in the pipeline.
* Each map is the configuration entries for that transform. The 'name'
* configuration entry is added in each map based on the name provided by the
@@ -391,76 +388,63 @@
}
/**
- * Load user-provided configuration file.
+ * Load user-provided configuration file, replacing any previously loaded file
+ * configuration.
*/
private void load(Reader configFile) throws IOException {
- configFileProperties.load(configFile);
+ Properties newConfigFileProperties = new Properties(defaultConfig);
+ newConfigFileProperties.load(configFile);
+
+ Config fakeOldConfig;
+ Set<String> differentKeys;
+ synchronized (this) {
+ // Create replacement config.
+ Properties newConfig = new Properties(newConfigFileProperties);
+ for (Object o : config.keySet()) {
+ newConfig.put(o, config.get(o));
+ }
+
+ // Find differences.
+ differentKeys = findDifferences(config, newConfig);
+
+ if (differentKeys.isEmpty()) {
+ log.info("No configuration changes found");
+ return;
+ }
+
+ validate(newConfig);
+
+ fakeOldConfig = new Config();
+ fakeOldConfig.config = config;
+ this.config = newConfig;
+ }
+ log.info("New configuration file loaded");
+ fireConfigModificationEvent(fakeOldConfig, differentKeys);
}
- Reader createReader(File file) throws IOException {
+ Reader createReader(File configFile) throws IOException {
return new InputStreamReader(new BufferedInputStream(
new FileInputStream(configFile)), Charset.forName("UTF-8"));
}
- public synchronized void ensureLatestConfigLoaded() throws IOException {
- if (!configFile.exists() || !configFile.isFile()) {
- return;
- }
- // Check for modifications.
- long newLastModified = configFile.lastModified();
- if (configFileLastModified == newLastModified || newLastModified == 0) {
- return;
- }
- log.info("Noticed modified configuration file");
- // Go ahead and update the modification time now, to prevent constantly
- // trying to load the configuration file, in case of errors.
- configFileLastModified = newLastModified;
-
- // Load freshly-modified file.
- Properties newConfigFileProperties = new Properties(defaultConfig);
- Properties newConfig = new Properties(newConfigFileProperties);
- Reader reader = createReader(configFile);
- try {
- newConfigFileProperties.load(reader);
- } finally {
- reader.close();
- }
-
- for (Object o : config.keySet()) {
- newConfig.put(o, config.get(o));
- }
-
- // Find differences.
- Set<String> differentKeys = findDifferences(config, newConfig);
-
- // Only allow adaptor.fullListingSchedule to be updated at the moment. No
- // other code can handle updates. Since the Dashboard will show the current
- // values, we don't want the Dashboard showing new values and the code using
- // old values. TODO(ejona): Once more things support modification of
- // configuration, this should be removed.
- for (String name : new ArrayList<String>(differentKeys)) {
- if (!"adaptor.fullListingSchedule".equals(name)) {
- differentKeys.remove(name);
- log.log(Level.INFO,
- "Ignoring modified key {0}, since it is not white-listed", name);
- newConfigFileProperties.setProperty(name, config.getProperty(name));
+ /**
+ * @return {@code true} if configuration file was modified.
+ */
+ public boolean ensureLatestConfigLoaded() throws IOException {
+ synchronized (this) {
+ if (configFile == null || !configFile.exists() || !configFile.isFile()) {
+ return false;
}
+ // Check for modifications.
+ long newLastModified = configFile.lastModified();
+ if (configFileLastModified == newLastModified || newLastModified == 0) {
+ return false;
+ }
+ log.info("Noticed modified configuration file");
+
+ load(configFile);
}
-
- if (differentKeys.isEmpty()) {
- log.info("No configuration changes found");
- return;
- }
-
- validate(newConfig);
-
- Config fakeOldConfig = new Config();
- fakeOldConfig.configFileProperties = configFileProperties;
- fakeOldConfig.config = config;
- this.configFileProperties = newConfigFileProperties;
- this.config = newConfig;
- log.info("New configuration file loaded");
- fireConfigModificationEvent(fakeOldConfig, differentKeys);
+ return true;
}
private Set<String> findDifferences(Properties config, Properties newConfig) {
@@ -486,6 +470,7 @@
* error handling, since this is typically non-fatal.
*/
public void loadDefaultConfigFile() {
+ configFile = defaultConfigFile;
if (configFile.exists() && configFile.isFile()) {
try {
load(configFile);
@@ -519,7 +504,6 @@
* @throws IllegalStateException when not all configuration keys have values
*/
public String[] autoConfig(String[] args) {
- loadDefaultConfigFile();
int i;
for (i = 0; i < args.length; i++) {
if (!args[i].startsWith("-D")) {
@@ -532,6 +516,7 @@
}
setValue(parts[0], parts[1]);
}
+ loadDefaultConfigFile();
validate();
if (i == 0) {
return args;
@@ -545,7 +530,7 @@
*
* @throws IllegalStateException if {@code key} has no value
*/
- public synchronized String getValue(String key) {
+ public String getValue(String key) {
String value = config.getProperty(key);
if (value == null) {
throw new IllegalStateException(MessageFormat.format(
@@ -612,30 +597,24 @@
public void addConfigModificationListener(
ConfigModificationListener listener) {
- synchronized (modificationListeners) {
- modificationListeners.add(listener);
- }
+ modificationListeners.add(listener);
}
public void removeConfigModificationListener(
ConfigModificationListener listener) {
- synchronized (modificationListeners) {
- modificationListeners.remove(listener);
- }
+ modificationListeners.remove(listener);
}
private void fireConfigModificationEvent(Config oldConfig,
Set<String> modifiedKeys) {
ConfigModificationEvent ev
= new ConfigModificationEvent(this, oldConfig, modifiedKeys);
- synchronized (modificationListeners) {
- for (ConfigModificationListener listener : modificationListeners) {
- try {
- listener.configModified(ev);
- } catch (Exception ex) {
- log.log(Level.WARNING,
- "Unexpected exception. Consider filing a bug.", ex);
- }
+ for (ConfigModificationListener listener : modificationListeners) {
+ try {
+ listener.configModified(ev);
+ } catch (Exception ex) {
+ log.log(Level.WARNING,
+ "Unexpected exception. Consider filing a bug.", ex);
}
}
}
diff --git a/src/adaptorlib/Dashboard.java b/src/adaptorlib/Dashboard.java
index 33d2eed..ffdd4c9 100644
--- a/src/adaptorlib/Dashboard.java
+++ b/src/adaptorlib/Dashboard.java
@@ -68,10 +68,12 @@
= new HttpsConfigurator(SSLContext.getDefault());
((HttpsServer) dashboardServer).setHttpsConfigurator(httpsConf);
}
- // If the port is zero, then the OS chose a port for us. This is mainly
- // useful during testing.
- dashboardPort = dashboardServer.getAddress().getPort();
- config.setValue("server.dashboardPort", "" + dashboardPort);
+ if (dashboardPort == 0) {
+ // If the port is zero, then the OS chose a port for us. This is mainly
+ // useful during testing.
+ dashboardPort = dashboardServer.getAddress().getPort();
+ config.setValue("server.dashboardPort", "" + dashboardPort);
+ }
// Use separate Executor for Dashboard to allow the administrator to
// investigate why things are going wrong without waiting on the normal work
// queue.
@@ -131,6 +133,8 @@
rpcHandler.registerRpcMethod("getConfig", new ConfigRpcMethod(config));
rpcHandler.registerRpcMethod("getStats", new StatRpcMethod(journal));
rpcHandler.registerRpcMethod("getStatuses", new StatusRpcMethod(monitor));
+ rpcHandler.registerRpcMethod("checkForUpdatedConfig",
+ new CheckForUpdatedConfigRpcMethod(gsaCommHandler));
return rpcHandler;
}
@@ -208,6 +212,19 @@
}
}
+ static class CheckForUpdatedConfigRpcMethod implements RpcHandler.RpcMethod {
+ private final GsaCommunicationHandler gsaComm;
+
+ public CheckForUpdatedConfigRpcMethod(GsaCommunicationHandler gsaComm) {
+ this.gsaComm = gsaComm;
+ }
+
+ @Override
+ public Object run(List request) {
+ return gsaComm.ensureLatestConfigLoaded();
+ }
+ }
+
static class LastPushStatusSource implements StatusSource {
private final Journal journal;
diff --git a/src/adaptorlib/GsaCommunicationHandler.java b/src/adaptorlib/GsaCommunicationHandler.java
index cef5b09..cfa90a4 100644
--- a/src/adaptorlib/GsaCommunicationHandler.java
+++ b/src/adaptorlib/GsaCommunicationHandler.java
@@ -54,14 +54,14 @@
* permits one invocation at a time. If multiple simultaneous invocations
* occur, all but the first will log a warning and return immediately.
*/
- private final OneAtATimeRunnable docIdFullPusher;
+ private final OneAtATimeRunnable docIdFullPusher = new OneAtATimeRunnable(
+ new PushRunnable(), new AlreadyRunningRunnable());
/**
* Schedule identifier for {@link #sendDocIds}.
*/
private String sendDocIdsSchedId;
private HttpServer server;
private Thread shutdownHook;
- private Timer configWatcherTimer = new Timer("configWatcher", true);
private IncrementalAdaptorPoller incrementalAdaptorPoller;
private final DocIdCodec docIdCodec;
private final DocIdSender docIdSender;
@@ -73,13 +73,10 @@
dashboard = new Dashboard(config, this, journal);
docIdCodec = new DocIdCodec(config);
- GsaFeedFileSender fileSender = new GsaFeedFileSender(
- config.getGsaCharacterEncoding(), config.isServerSecure());
+ GsaFeedFileSender fileSender = new GsaFeedFileSender(config);
GsaFeedFileMaker fileMaker = new GsaFeedFileMaker(docIdCodec);
docIdSender
= new DocIdSender(fileMaker, fileSender, journal, config, adaptor);
- docIdFullPusher = new OneAtATimeRunnable(
- new PushRunnable(), new AlreadyRunningRunnable());
}
/** Starts listening for communications from GSA. */
@@ -111,10 +108,12 @@
throw new RuntimeException(ex);
}
}
- // If the port is zero, then the OS chose a port for us. This is mainly
- // useful during testing.
- port = server.getAddress().getPort();
- config.setValue("server.port", "" + port);
+ if (port == 0) {
+ // If the port is zero, then the OS chose a port for us. This is mainly
+ // useful during testing.
+ port = server.getAddress().getPort();
+ config.setValue("server.port", "" + port);
+ }
int maxThreads = config.getServerMaxWorkerThreads();
int queueCapacity = config.getServerQueueCapacity();
BlockingQueue<Runnable> blockingQueue
@@ -168,7 +167,6 @@
Runtime.getRuntime().addShutdownHook(shutdownHook);
config.addConfigModificationListener(new GsaConfigModListener());
- TimerTask configWatcher = new ConfigWatcher(config);
long sleepDurationMillis = 1000;
// An hour.
@@ -182,7 +180,7 @@
Thread.sleep(sleepDurationMillis);
sleepDurationMillis
= Math.min(sleepDurationMillis * 2, maxSleepDurationMillis);
- configWatcher.run();
+ ensureLatestConfigLoaded();
}
}
@@ -203,9 +201,6 @@
scheduler.start();
sendDocIdsSchedId = scheduler.schedule(
config.getAdaptorFullListingSchedule(), docIdFullPusher);
-
- long period = config.getConfigPollPeriodMillis();
- configWatcherTimer.schedule(configWatcher, period, period);
}
private TransformPipeline createTransformPipeline() {
@@ -316,6 +311,16 @@
return docIdFullPusher.runInNewThread() != null;
}
+ boolean ensureLatestConfigLoaded() {
+ try {
+ return config.ensureLatestConfigLoaded();
+ } catch (Exception ex) {
+ log.log(Level.WARNING, "Error while trying to reload configuration",
+ ex);
+ return false;
+ }
+ }
+
/**
* Runnable that calls {@link DocIdSender#pushDocIds}.
*/
@@ -378,23 +383,25 @@
}
}
}
- }
- }
- private static class ConfigWatcher extends TimerTask {
- private Config config;
-
- public ConfigWatcher(Config config) {
- this.config = config;
- }
-
- @Override
- public void run() {
- try {
- config.ensureLatestConfigLoaded();
- } catch (Exception ex) {
- log.log(Level.WARNING, "Error while trying to reload configuration",
- ex);
+ // List of "safe" keys that can be updated without a restart.
+ List<String> safeKeys = Arrays.asList("adaptor.fullListingSchedule");
+ // Set of "unsafe" keys that have been modified.
+ Set<String> modifiedKeysRequiringRestart
+ = new HashSet<String>(modifiedKeys);
+ modifiedKeysRequiringRestart.removeAll(safeKeys);
+ // If there are modified "unsafe" keys, then we restart things to make
+ // sure all the code is up-to-date with the new values.
+ if (!modifiedKeysRequiringRestart.isEmpty()) {
+ log.warning("Unsafe configuration keys modified. To ensure a sane "
+ + "state, the adaptor is restarting.");
+ stop(3);
+ try {
+ start();
+ } catch (Exception ex) {
+ log.log(Level.SEVERE, "Automatic restart failed", ex);
+ throw new RuntimeException(ex);
+ }
}
}
}
diff --git a/src/adaptorlib/GsaFeedFileSender.java b/src/adaptorlib/GsaFeedFileSender.java
index e8b9772..19fd634 100644
--- a/src/adaptorlib/GsaFeedFileSender.java
+++ b/src/adaptorlib/GsaFeedFileSender.java
@@ -18,7 +18,6 @@
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
-import java.nio.charset.Charset;
import java.util.logging.Logger;
import java.util.zip.GZIPOutputStream;
@@ -50,11 +49,8 @@
// TODO(pjo): Add corrective tips.
}
- // All communications are expected to be tailored to GSA.
- private final Charset encoding;
-
- /** Whether to use HTTP or HTTPS to talk to the feedergate. */
- private final boolean useHttps;
+ /** Configuration for GSA's encoding and whether to use HTTPS. */
+ private final Config config;
// Feed file XML will not contain "<<".
private static final String BOUNDARY = "<<";
@@ -62,14 +58,13 @@
// Another frequently used constant of sent message.
private static final String CRLF = "\r\n";
- public GsaFeedFileSender(Charset encoding, boolean useHttps) {
- this.encoding = encoding;
- this.useHttps = useHttps;
+ public GsaFeedFileSender(Config config) {
+ this.config = config;
}
// Get bytes of string in communication's encoding.
private byte[] toEncodedBytes(String s) {
- return s.getBytes(encoding);
+ return s.getBytes(config.getGsaCharacterEncoding());
}
/** Helper method for creating a multipart/form-data HTTP post.
@@ -98,7 +93,7 @@
boolean useCompression)
throws MalformedURLException, IOException {
URL feedUrl;
- if (useHttps) {
+ if (config.isServerSecure()) {
feedUrl = new URL("https://" + gsaHost + ":19902/xmlfeed");
} else {
feedUrl = new URL("http://" + gsaHost + ":19900/xmlfeed");
@@ -136,7 +131,8 @@
BufferedReader br = null;
try {
InputStream inputStream = uc.getInputStream();
- br = new BufferedReader(new InputStreamReader(inputStream, encoding));
+ br = new BufferedReader(new InputStreamReader(
+ inputStream, config.getGsaCharacterEncoding()));
String line;
while ((line = br.readLine()) != null) {
buf.append(line);
diff --git a/test/adaptorlib/ConfigTest.java b/test/adaptorlib/ConfigTest.java
index 7801a41..cbc5dc6 100644
--- a/test/adaptorlib/ConfigTest.java
+++ b/test/adaptorlib/ConfigTest.java
@@ -149,6 +149,7 @@
+ "transform.pipeline.trans2.key2=value2\n"
+ "transform.pipeline.trans2.key3=value3\n"
);
+ config.setValue("gsa.hostname", "notreal");
config.load(configFile);
assertEquals(golden, config.getTransformPipelineSpec());
}
@@ -163,6 +164,7 @@
@Test
public void testGetTransformPipelineSpecInValid() throws Exception {
configFile.setFileContents("transform.pipeline=name1, ,name3\n");
+ config.setValue("gsa.hostname", "notreal");
config.load(configFile);
thrown.expect(RuntimeException.class);
config.getTransformPipelineSpec();
@@ -170,7 +172,7 @@
private static class ModifiedConfig extends Config {
public ModifiedConfig(MockFile file) {
- this.configFile = file;
+ this.defaultConfigFile = file;
}
@Override
diff --git a/test/adaptorlib/DocIdSenderTest.java b/test/adaptorlib/DocIdSenderTest.java
index df12e33..5142b12 100644
--- a/test/adaptorlib/DocIdSenderTest.java
+++ b/test/adaptorlib/DocIdSenderTest.java
@@ -240,7 +240,7 @@
List<String> xmlStrings = new ArrayList<String>();
public MockGsaFeedFileSender() {
- super(null, false);
+ super(new Config());
}
@Override