Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

try to add a better localhost resolver. See issue #44 #1

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.aphyr</groupId>
<artifactId>riemann-java-client</artifactId>
<version>0.2.12-SNAPSHOT</version>
<version>0.2.13-SNAPSHOT</version>
<packaging>bundle</packaging>
<name>Riemann Java Client</name>
<description>Java client for http://aphyr.github.com/riemann/</description>
Expand Down
20 changes: 11 additions & 9 deletions riemann-java-client.iml
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these changes related to this feature?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not, possibly new intellij version did this when importing the maven project.
otherwise it wouldn't recognize imports etc.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. so please consider reverting these file changes

<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/target/generated-sources/com" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/generated-sources/test-annotations" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/target/generated-sources/annotations" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/generated-sources" isTestSource="false" generated="true" />
<excludeFolder url="file://$MODULE_DIR$/target/antrun" />
<excludeFolder url="file://$MODULE_DIR$/target/apidocs" />
<excludeFolder url="file://$MODULE_DIR$/target/classes" />
<excludeFolder url="file://$MODULE_DIR$/target/maven-archiver" />
<excludeFolder url="file://$MODULE_DIR$/target/javadoc-bundle-options" />
<excludeFolder url="file://$MODULE_DIR$/target/surefire" />
<excludeFolder url="file://$MODULE_DIR$/target/surefire-reports" />
<excludeFolder url="file://$MODULE_DIR$/target/test-classes" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: com.google.protobuf:protobuf-java:2.3.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit-dep:4.10" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.1" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: org.codehaus.jsr166-mirror:jsr166y:1.7.0" level="project" />
<orderEntry type="library" name="Maven: com.google.protobuf:protobuf-java:2.5.0" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty:3.6.1.Final" level="project" />
<orderEntry type="library" name="Maven: com.yammer.metrics:metrics-core:2.1.2" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.6.4" level="project" />
<orderEntry type="library" name="Maven: com.codahale.metrics:metrics-core:3.0.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit-dep:4.10" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-all:1.9.5" level="project" />
</component>
</module>

</module>
8 changes: 2 additions & 6 deletions src/main/java/com/aphyr/riemann/client/EventDSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,8 @@ public class EventDSL {
public EventDSL(AbstractRiemannClient client) {
this.client = client;
this.builder = Event.newBuilder();
try {
this.builder.setHost(java.net.InetAddress.getLocalHost().getHostName());
} catch (java.net.UnknownHostException e) {
// If we can't get the local host, a null host is perfectly
// acceptable. Caller will know soon enough. :)
}

this.builder.setHost(LocalhostResolver.getResolvedHostname());
}

public EventDSL host(String host) {
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/com/aphyr/riemann/client/LocalhostResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.aphyr.riemann.client;

import java.net.UnknownHostException;

/**
* A "Smarter" localhost resolver
* see issue: https://github.com/aphyr/riemann-java-client/issues/44
* Trying to avoid a lot of calls to java.net.InetAddress.getLocalHost()
* which under AWS trigger DNS resolving and have relatively high latency *per event*
* usually, the hostname doesn't change so often to warrant a real query.
*
* A real call to java.net.InetAddress.getLocalHost().getHostName()
* is made only if:
* 1) the refresh interval has passed (=result is stale)
* AND
* 2) no env vars that identify the hostname are found
*/
public class LocalhostResolver {

// default hostname env var names on Win/Nix
public static final String COMPUTERNAME = "COMPUTERNAME"; // Windows
public static final String HOSTNAME = "HOSTNAME"; // Nix

// how often should we refresh the cached hostname
public static long RefreshIntervalMillis = 60 * 1000;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

casing (first letter lowecase)

// enables setting a custom env var used for resolving
public static String CustomEnvVarName = null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too much customization. Would you ever need this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought for testing/staging or in cases where you couldn't change the usually reserved env vars.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do that with custom injection points in the dev code. Anyway, I think it's not a requirement now.


// cached hostname result
private static String hostname;

// update (refresh) time management
private static long lastUpdate = 0;
private static long lastNetUpdate = 0;
public static long getLastUpdateTime() { return lastUpdate; }
public static long getLastNetUpdateTime() { return lastNetUpdate; }
public static void resetUpdateTimes() {
lastUpdate = 0;
lastNetUpdate = 0;
}

/**
* get resolved hostname.
* encapsulates all lookup and caching logic.
*
* @return the hostname
*/
public static String getResolvedHostname() {
long now = System.currentTimeMillis();
if(now - RefreshIntervalMillis > lastUpdate) {
refreshResolve();
}

return hostname;
}

/**
* forces a new resolve even if refresh interval has not passed yet
*/
public static void refreshResolve() {
try {
hostname = resolveByEnv();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't be here. There is no need to resolve the environment variable again and again. There is also no need to call System.currentTimeMillis() each time in this case.

if(hostname == null || hostname.isEmpty()) {
hostname = java.net.InetAddress.getLocalHost().getHostName();
lastNetUpdate = System.currentTimeMillis();
}
} catch (UnknownHostException e) {
//e.printStackTrace();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace comment with // fallthrough

}
finally {
lastUpdate = System.currentTimeMillis();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need this variable.

}
}

/**
* try to resolve the hostname by env vars
*
* @return
*/
private static String resolveByEnv() {
if(CustomEnvVarName != null) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not really needed.

return System.getenv(CustomEnvVarName);
}

if(System.getProperty("os.name").startsWith("Windows")) {
return System.getenv(COMPUTERNAME);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use getenv instead of System.getenv which inludes an override for testing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean, I assume it relates to setEnv. Let's talk about the approach.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we talked, you suggested that adding an injection point in the dev code is not needed since we have the test code that can inject directly into env variables.

}

return System.getenv(HOSTNAME);
}
}
146 changes: 146 additions & 0 deletions src/test/java/riemann/java/client/tests/LocalhostResolveTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package riemann.java.client.tests;

import com.aphyr.riemann.client.LocalhostResolver;
import junit.framework.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import java.lang.reflect.Field;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class LocalhostResolveTest {

protected Map<String, String> env = new HashMap<String, String>();

protected static final String OS_NAME_PROP = "os.name";
protected static final String ExpectedWinHostname = "LocalHostResolverTestWin";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first letter must be lowercase

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change everything to private. no need for protected

protected static final String ExpectedNixHostname = "LocalHostResolverTestNix";

@BeforeClass
public static void oneTimeSetUp() {
LocalhostResolver.RefreshIntervalMillis = 1000;
}

@Before
public void setup() {
env = new HashMap<String, String>(System.getenv());
env.put(LocalhostResolver.HOSTNAME, ExpectedNixHostname);
env.put(LocalhostResolver.COMPUTERNAME, ExpectedWinHostname);
setEnv(env);

LocalhostResolver.CustomEnvVarName = null;
LocalhostResolver.resetUpdateTimes();
Assert.assertEquals(0, LocalhostResolver.getLastUpdateTime());
Assert.assertEquals(0, LocalhostResolver.getLastNetUpdateTime());
}

@Test
public void testUpdateInterval() {
Assert.assertEquals(0, LocalhostResolver.getLastUpdateTime());
String hostname = LocalhostResolver.getResolvedHostname();
long lastUpdateTime = LocalhostResolver.getLastUpdateTime();
Assert.assertNotNull(hostname);
try {
Thread.sleep(1500);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you shouldn't use sleep in unit tests. You can expose get/setLastNetUpdateTime() for testing.
You can have a unit test to check that it actually changed the first time called, but did not change the second time called. Then inject a bogus lastNetUpdateTime call again and check that it was updated again.

} catch (InterruptedException e) {
e.printStackTrace();
}

Assert.assertNotSame(lastUpdateTime, LocalhostResolver.getLastUpdateTime());
}

@Test
public void testNoEnvVars() throws UnknownHostException {
env.remove(LocalhostResolver.HOSTNAME);
env.remove(LocalhostResolver.COMPUTERNAME);
setEnv(env);

String hostname = LocalhostResolver.getResolvedHostname();
Assert.assertNotNull(hostname);

try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}

// ensure queried hostname without env vars
Assert.assertEquals(LocalhostResolver.getLastUpdateTime(), LocalhostResolver.getLastNetUpdateTime());
Assert.assertEquals(java.net.InetAddress.getLocalHost().getHostName(), hostname);
}

@Test
public void testEnvVarWindows() {
System.getProperties().put(OS_NAME_PROP, "Windows7");

String hostname = LocalhostResolver.getResolvedHostname();
Assert.assertEquals(ExpectedWinHostname, hostname);
Assert.assertEquals(0, LocalhostResolver.getLastNetUpdateTime());
}

@Test
public void testEnvVarNix() {
System.getProperties().put(OS_NAME_PROP, "Linux");
String hostname = LocalhostResolver.getResolvedHostname();
Assert.assertEquals(ExpectedNixHostname, hostname);
Assert.assertEquals(0, LocalhostResolver.getLastNetUpdateTime());
}

@Test
public void testCustomEnvVar() {
final String customHostnameEnvVar = "AWS_HOST";
final String customHostname = "EC2-LocalHostResolverTest";
env.put(customHostnameEnvVar, customHostname);
setEnv(env);

LocalhostResolver.CustomEnvVarName = customHostnameEnvVar;
String hostname = LocalhostResolver.getResolvedHostname();
Assert.assertEquals(customHostname, hostname);
Assert.assertEquals(0, LocalhostResolver.getLastNetUpdateTime());
}


/**
* evil hack for testing (only!) with env var in-memory modification
* see: http://stackoverflow.com/a/7201825/1469004
*
* @param newEnv - to set in memory
*/
protected static void setEnv(Map<String, String> newEnv) {
try {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why??? Just add setEnv() in the production code and document it's a testing hook (which overrides getEnv()).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's talk about this, not sure why you prefer this approach.
I like to restrict hacks for testing to the test code and not actual code as much as possible.

Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
theEnvironmentField.setAccessible(true);
Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
env.putAll(newEnv);
Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
theCaseInsensitiveEnvironmentField.setAccessible(true);
Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
cienv.putAll(newEnv);
}
catch (NoSuchFieldException e) {
try {
Class[] classes = Collections.class.getDeclaredClasses();
Map<String, String> env = System.getenv();
for(Class cl : classes) {
if("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
Field field = cl.getDeclaredField("m");
field.setAccessible(true);
Object obj = field.get(env);
Map<String, String> map = (Map<String, String>) obj;
map.clear();
map.putAll(newEnv);
}
}
} catch (Exception e2) {
e2.printStackTrace();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw. do not swallow exceptions

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but then I have to make every test method declare throw (which I prefer not to here, since it's a hacky test method anyway).
Can I use a RuntimeException?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have Guava in the pom use throw Throwables.propagate(e2) otherwise use throw new RuntimeException(e2)

}
} catch (Exception e1) {
e1.printStackTrace();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw. do not swallow exceptions

}
}
}