jvm-jersey-env (#1677)

A new Jersey framework based JVM environment which is lightweight and simpler.
This commit is contained in:
Sahil Lakhwani
2020-09-08 21:24:25 +05:30
committed by GitHub
parent 975286c2ac
commit 54b384efec
28 changed files with 1049 additions and 25 deletions
+2
View File
@@ -0,0 +1,2 @@
target/
bin/
+15
View File
@@ -0,0 +1,15 @@
FROM maven:3.5-jdk-8 as BUILD
WORKDIR /usr/src/myapp/
# To reuse the build cache, here we split maven dependency
# download and package into two RUN commands to avoid cache invalidation.
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src /usr/src/myapp/src/
RUN mvn package
FROM openjdk:8-jre-alpine
COPY --from=BUILD /usr/src/myapp/target/env-jvm-jersey-0.0.1.jar /app.jar
ENTRYPOINT java ${JVM_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar 8888
EXPOSE 8888
+15
View File
@@ -0,0 +1,15 @@
FROM maven:3.6-jdk-11 as BUILD
WORKDIR /usr/src/myapp/
# To reuse the build cache, here we split maven dependency
# download and package into two RUN commands to avoid cache invalidation.
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src /usr/src/myapp/src/
RUN mvn package
FROM openjdk:11-jre
COPY --from=BUILD /usr/src/myapp/target/env-jvm-jersey-0.0.1.jar /app.jar
ENTRYPOINT java ${JVM_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /app.jar --server.port=8888
EXPOSE 8888
+68
View File
@@ -0,0 +1,68 @@
# Fission: Java and JVM-Jersey Environment
This is the JVM (Jersey based) environment for Fission.
It's a Docker image containing a OpenJDK8 runtime, along with a
dynamic loader. A few dependencies are included in the
pom.xml file.
Unlike the other [JVM environment](../jvm) which is based on the Spring framework, this environment uses Jersey.
Looking for ready-to-run examples? See the [JVM examples directory](../../examples/jvm-jersey).
## Customizing this image
To add package dependencies, edit pom.xml to add what you
need, and rebuild this image (instructions below).
## Rebuilding and pushing the image
You'll need access to a Docker registry to push the image: you can
sign up for Docker hub at hub.docker.com, or use registries from
gcr.io, quay.io, etc. Let's assume you're using a docker hub account
called USER. Build and push the image to the the registry:
```
docker build -t USER/jvm-jersey-env . && docker push USER/jvm-jersey-env
```
You can also create environment image based on JVM 11 using Dockerfile-11 in this directory.
## Using the image in fission
You can add this customized image to fission with "fission env
create":
```
fission env create --name jvm --image USER/jvm-jersey-env
```
Or, if you already have an environment, you can update its image:
```
fission env update --name jvm --image USER/jvm-jersey-env
```
After this, fission functions that have the env parameter set to the
same environment name as this command will use this environment.
## Web Server Framework
JVM Jersey environment uses an embedded Jetty HTTP server by default, as can be seen in the pom.xml file.
```
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.0.4.v20130625</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.0.4.v20130625</version>
</dependency>
```
## Java and JVM builder
There are two JVM environment builder based on OpenJDK8 and OpenJDK 11 and using Maven 3.5.4. The default build command runs `mvn clean package` and uses the target/*with-dependencies.jar file for function. The default build command can be overridden as long as the uber jar file is copied to ${DEPLOY_PKG}.
@@ -0,0 +1,48 @@
## Fission builder base image
ARG BUILDER_IMAGE=fission/builder:latest
FROM ${BUILDER_IMAGE}
## Section copied from the openjdk:8-jdk-alpine Dockerfile - (https://github.com/docker-library/openjdk/blob/47a6539cd18023dafb45db9013455136cc0bca07/8/jdk/alpine/Dockerfile)
ENV LANG C.UTF-8
RUN { \
echo '#!/bin/sh'; \
echo 'set -e'; \
echo; \
echo 'dirname "$(dirname "$(readlink -f "$(which javac || which java)")")"'; \
} > /usr/local/bin/docker-java-home \
&& chmod +x /usr/local/bin/docker-java-home
ENV JAVA_HOME /usr/lib/jvm/java-1.8-openjdk
ENV PATH $PATH:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/bin
ENV JAVA_VERSION 8u181
## Use "fuzzy" version matching to pin the version to a major/minor release
ENV JAVA_ALPINE_VERSION "~8"
RUN set -x \
&& apk add --no-cache \
openjdk8="$JAVA_ALPINE_VERSION" \
&& [ "$JAVA_HOME" = "$(docker-java-home)" ]
## Section copied from the Maven Dockerfile
RUN apk add --no-cache curl tar bash procps
ARG MAVEN_VERSION=3.5.4
ARG USER_HOME_DIR="/root"
ARG SHA=ce50b1c91364cb77efe3776f756a6d92b76d9038b0a0782f7d53acf1e997a14d
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries
RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
&& curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
&& echo "${SHA} /tmp/apache-maven.tar.gz" | sha256sum -c - \
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
&& rm -f /tmp/apache-maven.tar.gz \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
## Fission builder specific section
ADD build.sh /usr/local/bin/build
EXPOSE 8001
@@ -0,0 +1,43 @@
## Fission builder base image
ARG BUILDER_IMAGE=fission/builder:latest
FROM ${BUILDER_IMAGE}
## Section referred from the openjdk:8-jdk-alpine Dockerfile - (https://github.com/docker-library/openjdk/blob/47a6539cd18023dafb45db9013455136cc0bca07/8/jdk/alpine/Dockerfile)
ENV LANG C.UTF-8
RUN { \
echo '#!/bin/sh'; \
echo 'set -e'; \
echo; \
echo 'dirname "$(dirname "$(readlink -f "$(which javac || which java)")")"'; \
} > /usr/local/bin/docker-java-home \
&& chmod +x /usr/local/bin/docker-java-home
ENV JAVA_HOME /usr/lib/jvm/java-11-openjdk
ENV PATH $PATH:/usr/lib/jvm/java-11-openjdk/jre/bin:/usr/lib/jvm/java-11-openjdk/bin
RUN set -x \
&& apk add --no-cache openjdk11 \
&& [ "$JAVA_HOME" = "$(docker-java-home)" ]
## Section copied from the Maven Dockerfile
RUN apk add --no-cache curl tar bash procps
ARG MAVEN_VERSION=3.5.4
ARG USER_HOME_DIR="/root"
ARG SHA=ce50b1c91364cb77efe3776f756a6d92b76d9038b0a0782f7d53acf1e997a14d
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries
RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
&& curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
&& echo "${SHA} /tmp/apache-maven.tar.gz" | sha256sum -c - \
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
&& rm -f /tmp/apache-maven.tar.gz \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
## Fission builder specific section
ADD build.sh /usr/local/bin/build
EXPOSE 8001
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
set -eou pipefail
mvn clean package
cp ${SRC_PKG}/target/*with-dependencies.jar ${DEPLOY_PKG}
+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.fission</groupId>
<artifactId>env-jvm-jersey</artifactId>
<version>0.0.1</version>
<properties>
<java.source.level>1.6</java.source.level>
<java.target.level>1.6</java.target.level>
</properties>
<dependencies>
<dependency>
<groupId>io.fission</groupId>
<artifactId>fission-jvm-jersey</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.0.4.v20130625</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.0.4.v20130625</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.source.level}</source>
<target>${java.target.level}</target>
<encoding>UTF-8</encoding>
<showDeprecation>true</showDeprecation>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>io.fission.Main</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,35 @@
package io.fission;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class FunctionLoadRequest {
private String filepath;
private String functionName;
private String url;
String getFilepath() {
return filepath;
}
void setFilepath(String filepath) {
this.filepath = filepath;
}
String getUrl() {
return url;
}
void setUrl(String url) {
this.url = url;
}
public String getFunctionName() {
return functionName;
}
public void setFunctionName(String functionName) {
this.functionName = functionName;
}
}
@@ -0,0 +1,147 @@
package io.fission;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.DELETE;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Context;
import javax.ws.rs.container.ContainerRequestContext;
import io.fission.Function;
@Path("/")
public class JerseyServer {
private static Function<ContainerRequestContext,Response> fn;
private static final int CLASS_LENGTH = 6;
private static Logger logger = Logger.getGlobal();
@GET
public Response home(@Context ContainerRequestContext request) {
return callUserFunction(request);
}
@POST
public Response homePost(@Context ContainerRequestContext request) {
return callUserFunction(request);
}
@PUT
public Response homePut(@Context ContainerRequestContext request) {
return callUserFunction(request);
}
@DELETE
public Response homeDelete(@Context ContainerRequestContext request) {
return callUserFunction(request);
}
@Path("v2/specialize")
@POST
public Response specialize(FunctionLoadRequest req) {
long startTime = System.nanoTime();
File file = new File(req.getFilepath());
if (!file.exists()) {
return Response.status(Response.Status.BAD_REQUEST).entity("/userfunc/usernot found").build();
}
String entryPoint = req.getFunctionName();
logger.log(Level.INFO, "Entrypoint class:" + entryPoint);
if (entryPoint == null) {
return Response.status(Response.Status.BAD_REQUEST).entity("Entrypoint class is missing in the JAR or the name is incorrect")
.build();
}
JarFile jarFile = null;
ClassLoader cl = null;
try {
jarFile = new JarFile(file);
Enumeration<JarEntry> e = jarFile.entries();
URL[] urls = { new URL("jar:file:" + file + "!/") };
// TODO Check if the classloading can be improved for ex. use something like:
// Thread.currentThread().setContextClassLoader(cl);
if (this.getClass().getClassLoader() == null) {
cl = URLClassLoader.newInstance(urls);
} else {
cl = URLClassLoader.newInstance(urls, this.getClass().getClassLoader());
}
if (cl == null) {
return Response.status(Response.Status.BAD_REQUEST).entity("Failed to initialize the classloader")
.build();
}
// Load all dependent classes from libraries etc.
while (e.hasMoreElements()) {
JarEntry je = e.nextElement();
if (je.isDirectory() || !je.getName().endsWith(".class")) {
continue;
}
String className = je.getName().substring(0, je.getName().length() - CLASS_LENGTH);
className = className.replace('/', '.');
cl.loadClass(className);
}
// Instantiate the function class
fn = (Function) cl.loadClass(entryPoint).newInstance();
} catch (MalformedURLException e) {
e.printStackTrace();
return Response.status(Response.Status.BAD_REQUEST).entity("Entrypoint class is missing in the function")
.build();
} catch (ClassNotFoundException e) {
e.printStackTrace();
return Response.status(Response.Status.BAD_REQUEST).entity("Error loading Function or dependent class")
.build();
} catch (InstantiationException e) {
e.printStackTrace();
return Response.status(Response.Status.BAD_REQUEST)
.entity("Error creating a new instance of function class").build();
} catch (IllegalAccessException e) {
e.printStackTrace();
return Response.status(Response.Status.BAD_REQUEST)
.entity("Error creating a new instance of function class").build();
} catch (IOException e) {
e.printStackTrace();
return Response.status(Response.Status.BAD_REQUEST).entity("Error reading the JAR file").build();
} finally {
try {
jarFile.close();
} catch (IOException e) {
e.printStackTrace();
return Response.status(Response.Status.BAD_REQUEST)
.entity("Error closing the file while loading the class").build();
}
}
long elapsedTime = System.nanoTime() - startTime;
logger.log(Level.INFO, "Specialize call done in: " + elapsedTime / 1000000 + " ms");
return Response.status(Response.Status.OK).entity("Done").build();
}
private Response callUserFunction(ContainerRequestContext httpRequest) {
if (fn == null) {
return Response.status(Response.Status.BAD_REQUEST).entity("Container not specialized").build();
} else {
return fn.call(httpRequest, null);
}
}
}
@@ -0,0 +1,50 @@
package io.fission;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
public class Main {
private static final int DEFAULT_PORT = 8888;
private int serverPort;
public Main(int serverPort) throws Exception {
this.serverPort = serverPort;
Server server = configureServer();
server.start();
server.join();
}
private Server configureServer() {
ResourceConfig resourceConfig = new ResourceConfig();
resourceConfig.packages(JerseyServer.class.getPackage().getName());
resourceConfig.register(JacksonFeature.class);
ServletContainer servletContainer = new ServletContainer(resourceConfig);
ServletHolder sh = new ServletHolder(servletContainer);
Server server = new Server(serverPort);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
context.addServlet(sh, "/*");
server.setHandler(context);
return server;
}
public static void main(String[] args) throws Exception {
int serverPort = DEFAULT_PORT;
if(args.length >= 1) {
try {
serverPort = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
new Main(serverPort);
}
}