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

Support for multiple HealthChecks that resolve to a single health check status #117

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ Thumbs.db
*/target
/build
*/build

/bin
*/bin
# IntelliJ specific files/directories
out
.idea
Expand Down
1 change: 1 addition & 0 deletions karyon-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ tasks.withType(Javadoc).each {
dependencies {
dependencies {
compile "com.netflix.rxnetty:rx-netty-contexts:${rxnetty_version}"
compile "com.netflix.governator:governator:1.2.20"
Copy link
Contributor

Choose a reason for hiding this comment

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

governator dependency should not be added in core. karyon-governator is the module for governator integration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. This was done so ImplementedBy could be used. At minimum this should be changed to a dependency on Guice instead of Governator. We could remove ImplementedBy but that would require the user to install a guice module.

}
}

Expand Down
108 changes: 108 additions & 0 deletions karyon-core/src/main/java/com/netflix/karyon/health/HealthCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.netflix.karyon.health;

/**
* SPI for a component implementation an application status condition.
* Status conditions can indicate one of three scenarios
* 1. Component is not ready (i.e. starting or initializing)
* 2. Component is ready and functioning propertly
* 3. Component has either failed to start or failed during runtime
*
* There can be multiple status conditions registered for a single application.
* The status conditions are ultimately resolved to a single application up/down
* status.
*
* @author elandau
*/
public interface HealthCheck {
/**
* The status can have one of three states
*
* 1. Not ready status = false, error = null
* 2. Ready status = true, error = null
* 3. Error state status = false, error ! =null
*
Copy link

Choose a reason for hiding this comment

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

Since we are moving away from single boolean, why not introduce a more complete service status model with { Starting, PartiallyReady, Ready, Down, Error } states?

There is different semantic associated with each:

  • Starting - some time in the future I will be ready
  • PartiallyReady - I am providing limited service, possibly due to problem with communicating with backend services
  • Read - complete set of services provided
  • Down - state after service/library shutdown call was made
  • Error - any other error condition (with associated cause)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With this solution I'm trying to minimize the different states being tracked and instead opt to have multiple conditions. The above states can be modeled using the following conditions

  1. Starting
  2. Healthcheck
  3. Terminating

Each of these conditions would have a boolean ok/bad as well as an optional error. So a failure to start would have 'starting' set to false with error.

* @author elandau
*
*/
public static class Status {
private final HealthCheck healthCheck;
private final Throwable error;
private final boolean status;

private Status(HealthCheck healthCheck, boolean status, Throwable error) {
this.status = status;
this.error = error;
this.healthCheck = healthCheck;
}

/**
* Component is ready to be used and functioning properly
* @return
*/
public boolean isReady() {
return status && error == null;
}

/**
* Component is either not ready or failed
*/
public boolean isNotReady() {
return !isReady();
}

/**
* There was an error starting or running a component
*/
public boolean hasError() {
return error != null;
}

public Throwable getError() {
return error;
}

/**
* @return Name of component(s) being checked by this StatusCheck
*/
public String getName() {
return healthCheck.getName();
}

public static Status error(HealthCheck healthCheck, Throwable error) {
return new Status(healthCheck, false, error);
}

public static Status ready(HealthCheck healthCheck) {
return new Status(healthCheck, true, null);
}

public static Status notReady(HealthCheck healthCheck) {
return new Status(healthCheck, false, null);
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Status [")
.append("healthCheck=").append(healthCheck.getName())
.append(", status=") .append(status);

if (error != null) {
sb.append(", error=" + error.getMessage());
}
sb.append("]");
return sb.toString();
}
}

/**
* Run the status check and return the current status
* @return
*/
public abstract Status check();

/**
* @return Get name for health check
*/
public abstract String getName();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.netflix.karyon.health;


/**
* This is an extension to the <a href="https://github.com/Netflix/eureka/blob/master/eureka-client/src/main/java/com/netflix/appinfo/HealthCheckCallback.java">callback handler </a>
* in <a href="https://github.com/Netflix/eureka/">eureka</a> to provide a
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.netflix.karyon.health;

import java.util.Collection;
import java.util.List;

import com.google.inject.ImplementedBy;

/**
* Strategy for invoking the actual healthcheck operations
*
* @author elandau
*
*/
@ImplementedBy(InlineHealthCheckInvoker.class)
public interface HealthCheckInvoker {
public List<HealthCheck.Status> invoke(Collection<HealthCheck> healthChecks);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.netflix.karyon.health;

import java.util.List;

/**
* Registry of all status checkers. The actual health check is performed
* by an implementation of {@link HealthCheckInvoker}
*
* @author elandau
*/
public interface HealthCheckRegistry {
/**
* @return Return list of all registered HealthCheck conditions
*/
List<HealthCheck> getHealthChecks();

/**
* Add a HealthCheck to the registry
*
* @param handler
*/
void registerHealthCheck(HealthCheck handler);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.netflix.karyon.health;

import java.util.Collection;
import java.util.List;

import javax.inject.Singleton;

import com.google.common.collect.Lists;
import com.netflix.karyon.health.HealthCheck.Status;

/**
* Invoke all health checks in the context of the calling thread
* @author elandau
*
*/
@Singleton
public class InlineHealthCheckInvoker implements HealthCheckInvoker {
@Override
public List<Status> invoke(Collection<HealthCheck> healthChecks) {
List<Status> response = Lists.newArrayList();
for (HealthCheck hc : healthChecks) {
response.add(hc.check());
}
return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.netflix.karyon.health;

/**
* Implementation of HealthCheck that may be set manually.
*
* @author elandau
*/
public class ManualHealthCheck implements HealthCheck {
private volatile Status status;
private final String name;

public ManualHealthCheck() {
this("manual");
}

public ManualHealthCheck(String name) {
this.name = name;
this.status = Status.error(this, null);
}

public void unhealthy(Throwable error) {
this.status = Status.error(this, error);
}

public void healthy() {
this.status = Status.ready(this);
}

@Override
public String getName() {
return name;
}

@Override
public Status check() {
return status;
}

@Override
public String toString() {
return "ManualHealthCheck [status=" + status + ", name=" + name + "]";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.netflix.karyon.eureka;

import javax.inject.Singleton;

import com.google.inject.Inject;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.karyon.health.HealthCheck;

/**
* StatusCheck based on the ApplicationInfoManager tracked status.
*
* @author elandau
*/
@Singleton
public class ApplicationInfoManagerHealthCheck implements HealthCheck {
ApplicationInfoManager manager;

@Deprecated
public ApplicationInfoManagerHealthCheck() {
manager = ApplicationInfoManager.getInstance();
}

@Inject
public ApplicationInfoManagerHealthCheck(ApplicationInfoManager manager) {
this.manager = manager;
}

@Override
public Status check() {
InstanceStatus status = manager.getInstanceStatus();
if (status != null) {
switch (status) {
case UP:
return Status.ready(this);
case STARTING:
return Status.error(this, null);
case DOWN:
return Status.error(this, new Exception("Application is DOWN"));
default:
return Status.error(this, new Exception("Invalid state : " + status));
}
}
else {
return Status.error(this, new Exception("Invalid status <null>"));
}
}

@Override
public String getName() {
return "ApplicationInfoManager";
}

@Override
public String toString() {
return "ApplicationInfoManagerHealthCheck []";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/**
* @author Nitesh Kant
*/
@Deprecated
public class DefaultEurekaKaryonStatusBridge implements EurekaKaryonStatusBridge {

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.netflix.karyon.eureka;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.inject.Provider;
import javax.inject.Singleton;

import com.google.inject.Inject;
import com.google.inject.util.Providers;
import com.netflix.karyon.health.HealthCheckHandler;
import com.netflix.karyon.health.HealthCheck;
import com.netflix.karyon.health.HealthCheckRegistry;

/**
* Simple health check registry that supports the following HealthChecks
* 1. Bridge for ApplicationInfoManager's manual HealthCheck status
* 2. Bridge for HealthCheckHandler
* 3. Set<HealthCheck> creating using guice's multibinding
*
* Note that all healthchecks are injected lazily using Providers to ensure there is no
* circular dependency for components that depend on DiscoveryClient.
*
* @author elandau
*/
@Singleton
public class DefaultHealthCheckRegistry implements HealthCheckRegistry {
private final CopyOnWriteArrayList<Provider<? extends HealthCheck>> healthChecks = new CopyOnWriteArrayList<Provider<? extends HealthCheck>>();

public static class OptionalArgs {
@Inject(optional=true)
Provider<HealthCheckHandler> handler;

@Inject(optional=true)
Set<Provider<HealthCheck>> healthChecks;
}

@Inject
DefaultHealthCheckRegistry(Provider<ApplicationInfoManagerHealthCheck> manager, OptionalArgs args) {
this(args.healthChecks, manager, args.handler);
}

public DefaultHealthCheckRegistry(
final Set<Provider<HealthCheck>> healthChecks,
final Provider<ApplicationInfoManagerHealthCheck> manager,
final Provider<HealthCheckHandler> handler) {

if (manager != null) {
this.healthChecks.add(manager);
}

if (handler != null) {
this.healthChecks.add(Providers.of(new HealthCheckHandlerToHealthCheckAdapter(handler, "legacy")));
}

if (healthChecks != null) {
this.healthChecks.addAll(healthChecks);
}
}

/**
* Return a list of ALL registered handlers
*/
@Override
public List<HealthCheck> getHealthChecks() {
List<HealthCheck> statuses = new ArrayList<HealthCheck>();
for (Provider<? extends HealthCheck> provider : healthChecks) {
HealthCheck hc = provider.get();
if (hc != null) {
statuses.add(provider.get());
}
}
return statuses;
}

@Override
public void registerHealthCheck(HealthCheck handler) {
this.healthChecks.add(Providers.of(handler));
}
}
Loading