Karyon3 is a integration framework for writting services using netflix OSS, Archaius, Eureka and RxNetty. Karyon3 makes use of dependency injection (specifically using Google Guice) with additional support for context based conditional module loading to transparently load contextual bindings and configurations for the environment in which the service is running. Karyon3 is broken up into sub-projects on functional and dependency boundaries to reduce pulling in excessive dependencies.
Core features
- Minimize dependencies
- Context based auto-binding
- Dynamic configuration
- Health check
- Admin console
- Integration with core Netflix OSS
Note that Karyon3 is meant to be container agnostic and can run inside Tomcat, spawn a Jetty or RxNetty server.
Karyon is currently available as a release candidate
compile "com.netflix.karyon:karyon3-core:${karyon-version}"Set karyon-version to the latest 3.0.1-rc.+ available on maven central
A Karyon3 based main should normally consist of a simple block of code to create the injector via Karyon given a configuration and a set of modules, plus block on the application to terminate (this can be made event drive). Karyon encourages a clear separation between the injector creation, binding specification (via Guice modules) and code which should only use the JSR330 annotations.
public class HelloWorld {
public static void main(String[] args) {
Karyon.forApplication("MyService")
.addModules(
// Add any guice module
new ApplicationModule())
)
.start())
// Block until the application terminates
.awaitTermination();
}
}To run in tomcat simply extend Karyon's KaryonServletContextListener and create the injector just as you would a standalone application.
First, add a dependency on karyon-servlet
compile "com.netflix.karyon:karyon3-servlet:${karyon-version}"Next, write your ContextListener
package com.example;
public class MyContextListener extends KaryonServletContextListener {
@Override
protected Injector createInjector() {
return Karyon
.create()
.addModules(
new ServletModule() {
...
}
// ... more modules
)
.start()
));
}Finally, reference the context listener in web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<filter>
<filter-name>guiceFilter</filter-name>
<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>guiceFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>com.example.MyContextListener</listener-class>
</listener>
</web-app>
public class HelloWorld {
public static void main(String[] args) {
Karyon.forApplication("MyService")
.addModules(
// Add any guice module
new ApplicationModule(),
// To enable embedded Jetty (no need for web.xml)
new JettyModule()
)
.start()
// Block until the application terminates
.awaitTermination();
}
}Applications and libraries frequently need different bindings based on the context in which the application is running. Conditional bindings may be specified in a Guice module using @ProvidesConditionally. This features is syntactic sugar instead of having to write a complex Provider, put conditional logic in a Guice module or write complex conditional code when determining which Guice modules to install.
Karyon.create()
.addModules(new AbstractModule() {
@Override
protected void configure() {
}
@ProvidesConditionally(isDefault=true)
@ConditionalOnProfile("local")
protected Foo getFooWhenRunningLocally() {
return new FooForLocalDevelopment();
}
@ProvidesConditionally
@ConditionalOnEc2
protected Foo getFooWhenRunningInEc2() {
return new FooForEc2();
}
})
.start()Note the following restrictions when using conditional bindings
- ALL bindings for the type must be conditional otherwise Guice will fail with duplicate bindings errors
- No more than 1 conditional may be true otherwise Guice will fail with duplicate bindings errors
- If no conditions are met the @ProvidesConditionally with isDefault=true will be used. Only one @ProvidesConditional may be default.
- If no conditions are met and no isDefault is set Guice will throw a ProvisionException
Karyon3 uses Archaius2 to manage the application configuration. Archaius provides a simple override structure through which configuration may be loaded and overwritten. The configuration is fully DI'd and is therefore injectable into any code.
Archaius encourages the user of java interfaces to model configuration for a class as opposed to depending on a specific configuration API (such as apache commons) or mapping to Pojo setters (makes it difficult to have final fields). To simplify the use of interfaces Archaius provides a mechanism to bind a proxied implementation to the configuration. This approach has several benefits,
- Typed configuration mapping
- Decouple configuration representation from configuration format
- Mockable configuration
- Decouple configuration from executable code
A configuration interface looks like this,
@Configuration(prefix="foo") // Optional prefix
@ConfigurationSource("foo") // Will load foo.properties (and cascade overrides)
public interface FooConfig {
@DefaultValue("50")
int getTimeout(); // Will bind to property foo.timeout
}The configuration interfaces is used like this,
public class Foo {
@Inject
Foo(FooConfig config) {
}
}To create the proxied configuration
new AbstractModule() {
@Provides
@Singleton
FooConfig getFooConfiguration(ConfigProxyFactory factory) {
return factory.newProxy(FooConfig.class);
}
}For more complex configurations that can't be modeled as an interface it is still possible to just inject archaius's Config and access properties manually.
public class Foo {
@Inject
Foo(Config config) {
config.getInteger("foo.timeout", 50);
}
}TODO
TODO
TODO
TODO
The admin console provides invaluable insight into the internal state of a running instance. For simplicity and to avoid requiring additional dependencies to write and run the admin pages Karyon admin resources are written as simple Pojos with method names corresponding to service actions. Method should have the following signature
ResponseType methodName(RequestType request);A default HTTP server implementation is provided using the JDK built in web server as well as Jackson for serialization. (Note that in the future this default implementation may be changes to use gRPC).
To enable the Admin Console REST server (default port 8076)
import com.netflix.karyon.admin.rest.AdminServerModule;
...
install(new AdminServerModule());To enable the simple UI server (default port 8078)
import com.netflix.karyon.admin.ui.AdminUIServerModule;
...
install(new AdminUIServerModule());The REST and Admin ports are completely decoupled so that the UI may be hosted (and independently modified) remotely. By default, browsing the root path of port 8076 will redirect to the internal port 8078. Set 'karyon.server.admin.remoteServer' to re-direct browsers to a remotely hosted server. For example,
karyon.server.admin.remoteServer=http://org.example.adminserver:80/index.html#/${@publicHostname}:8076/")For now we ask that only the internally provided Admin endpoints be used as the API may change in the future.
Use of v2 admin pages is discouraged as they requires a large number of depdendencies and use the outdated mode of server side templating using FreekMarker. However, for existing applications it may be necessary to enable these pages for backwards compatibility.
dependencies {
compile 'com.netflix.karyon:karyon2-admin:2.7.4'
compile 'com.netflix.karyon:karyon2-admin-web:2.7.4'
compile 'com.netflix.karyon:karyon2-admin-eureka-plugin:2.7.4'
}In a guice module, or Karyon.addModule()
install(new KaryonAdminModule());Instance health is an important aspect of any cloud ready application. It is used for service discovery as well as bad instance termination. Through Karyon's HealthCheck API an application can expose a REST endpoint for external monitoring to ping for health status or integrate with Eureka for service discovery registration based on instance health state. An instance can be in one of 4 lifecycle states: Starting, Running, Stopping and Stopped. HealthCheck state varies slightly in that it combines these application lifecycle states with the instance health to provide the following states: Starting, Healthy, Unhealthy or OutOfService.
- Starting - the application is healthy but not done bootstrapping
- Healthy - the application finished bootstrapping and is functioning properly
- Unhealthy - the application either failed bootstrapping or is not functioning properly
- OutOfService - the application has been shut down
The HealthCheck API is broken up into several abstractions to allow for maximum customization. It's important to understand these abstractions.
- HealthIndicator - boolean health indicator for a specific application features/aspect. For example, CPU usage.
- HealthIndicatorRegistry - registry of all health indicators to consider for health check. The default HealthIndicatorRegistry ANDs all bound HealthIndicator (MapBinding, Multibinding, Qualified Binding). Altenatively and application may manually construct a HealthIndicatorRegistry from a curated set of HealthIndicators.
- HealthCheck - combines application lifecycle + indicators to derive a meaningful health state
@Path("/health")
public class HealthCheckResource {
@Inject
public HealthCheckResource(HealthCheck healthCheck) {
this.healthCheck = healthCheck;
}
@GET
public HealthCheckStatus doCheck() {
return healthCheck.check().get();
}
}To create a custom health indicator simply implement HealthIndicator, inject any objects that are needed to determine the health state, and implement you logic in check(). Note that check returns a future so that the healthcheck system can implement a timeout. The check() implementation is therefore expected to be well behaved and NOT block.
public class MyHealthIndicator extends AbstractHealthIndicator {
@Inject
public MyHealthIndicator(MyService service) {
this.service = service;
}
@Override
public CompletableFuture<HealthIndicatorStatus> check() {
if (service.getErrorRate() > 0.1) {
return CompletableFuture.completedFuture(healthy(getName()));
}
else {
return CompletableFuture.completedFuture(healthy(getName()));
}
}
@Override
public String getName() {
return "MyService";
}
}To enable the HealthIndicator simply register it as a set binding. It will automatically be picked up by the default HealthIndicatorRegistry
Multbindings.newSetBinder(binder()).addBinding().to(MyHealthIndicator.class);TBD
TBD
First, add the following dependency
compile 'com.netflix.karyon:karyon3-eureka:{karyon_version}'Next add the EurekaModule from OSS eureka-client
Karyon.create()
.addModule(
new EurekaModule()
)
.start()
...Using to conditional loading the Karyon will auto-install a module that will bridge Karyon's health check with Eureka's V2 health check.
TODO: manually mark instance as UP
Karyon3 doesn't offer any specific Jersey integration other the then existing Jersey Guice integration. To add Jersey support,
First, add the following dependency
compile 'com.sun.jersey.contribs:jersey-guice:1.19'Then simply add a JerseyServletModule implementation to the list of modules passed to Karyon
Karyon.create()
.addModules(
new JerseyServletModule() {
@Override
protected void configureServlets() {
serve("/*").with(GuiceContainer.class);
bind(GuiceContainer.class).asEagerSingleton();
bind(SomeJerseyClass.class).asEagerSingleton();
}
})
.start();NOTE: Karyon currently uses RxNetty 0.4.x until 0.5.x is released.
Karyon provides a mechanism to define and configure multiple RxNetty servers within a single application with servlet style request routing similar to ServletModule. Based on these bindings Karyon will auto-start the servers as the injector is created.
To add RxNetty support
compile 'com.netflix.karyon:karyon3-rxnetty:{karyon_version}'To specify basic URL routes for an RxNetty Server
Karyon.create()
.addModules(
new RxNettyServerModule() {
@Override
protected void configureEndpoints() {
serve("/hello").with(HelloWorldRequestHandler.class);
}
}
).start()HelloWorldRequestHandler is a standard RxNetty request handler
@Singleton
public class HelloWorldRequestHandler implements RequestHandler<ByteBuf, ByteBuf> {
@Override
public Observable<Void> handle(
HttpServerRequest<ByteBuf> request,
HttpServerResponse<ByteBuf> response) {
return response.writeStringAndFlush("Hello World!");
}
}Karyon will auto-create a default binding for ServerConfig. However, an alternate ServerConfig may be provided by specifying the binding to ServerConfig.
karyon.httpserver.serverPort=7001Qualified RxNetty servers makes it possible to expose services (such as admin) over other ports.
Karyon
.create()
.addModules(
new RxNettyServerModule() {
@Override
protected void configureEndpoints() {
serve(FooServer.class, "/foo").with(FooRequestHandler.class);
}
},
...Where the port number is defined in the property
karyon.httpserver.serverPort=7001If not interested in the built in routing an RxNetty server may be constructed manually using a simple @Provides method.
new AbstractModule() {
@Provides
@Singleton
HttpServer<ByteBuf, ByteBuf> getShutdownServer() {
return RxNetty.newHttpServerBuilder(
80,
new FooRequestHandler()
)
.build();
}
}TODO
To add JUnit support
compile 'com.netflix.karyon:karyon3-junit:{karyon_version}'Use KaryonRule to simplify testing and provide auto injector shutdown after the unit test completes. For example,
public class MyUnitTest {
{@literal @}Rule
public KaryonRule karyon = new KaryonRule(this);
{@literal @}Inject
SomeClassBeingTested obj;
{@literal @}Test
public void someTest() {
// Configuration the KaryonRule just like you would Karyon
karyon.addModules(someModules).start();
// Once start is called field's of MyUnitTest will have been injected
Assert.assertTrue(obj.someTestCondition());
}
}