From 71fbaceddf5d7b37810f20db2fbe06273014cf06 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 2 Feb 2017 18:39:59 -0800 Subject: [PATCH 01/15] Add support for GRPC API to H2O-3 --- .gitignore | 17 ++---- build.gradle | 55 ++++++++++------- gradle/scala.gradle | 6 +- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 68 ++++++++++++--------- gradlew.bat | 14 ++--- h2o-bindings/bin/bindings.py | 2 + h2o-core/build.gradle | 53 +++++++++++++++- .../java/ai/h2o/api/protos/core/JobService.java | 62 +++++++++++++++++++ h2o-core/src/main/java/water/H2O.java | 66 ++++++++++---------- h2o-core/src/main/java/water/api/JobsHandler.java | 30 +++++++++ .../src/main/java/water/api/RegisterGrpcApi.java | 14 +++++ .../src/main/java/water/api/RegisterV4Api.java | 3 + .../main/java/water/api/schemas4/input/JobIV4.java | 15 +++++ .../main/java/water/api/schemas4/output/JobV4.java | 26 ++++---- h2o-core/src/main/java/water/init/NetworkInit.java | 45 +++++--------- h2o-core/src/main/proto/core/job.proto | 47 ++++++++++++++ h2o-py/build.gradle | 46 +++++++++++--- h2o-py/h2o/h2o.py | 2 +- 20 files changed, 406 insertions(+), 169 deletions(-) create mode 100644 h2o-core/src/main/java/ai/h2o/api/protos/core/JobService.java create mode 100644 h2o-core/src/main/java/water/api/RegisterGrpcApi.java create mode 100644 h2o-core/src/main/java/water/api/schemas4/input/JobIV4.java create mode 100644 h2o-core/src/main/proto/core/job.proto diff --git a/.gitignore b/.gitignore index 7528e0ce722..626e798d54d 100644 --- a/.gitignore +++ b/.gitignore @@ -75,25 +75,18 @@ target h2o-test-accuracy/testng.xml # stuff created by sphinx-build (python docs) -./h2o-py/docs/docs/_static -./h2o-py/docs/docs/_sources/ -./h2o-py/docs/docs/searchindex.js -./h2o-py/docs/docs/search* -./h2o-py/docs/docs/py-modindex.html -./h2o-py/docs/docs/*html -./h2o-py/docs/docs/*js -./h2o-py/docs/docs/.buildinfo -./h2o-py/docs/docs/.doctrees/ -./h2o-py/docs/docs/_modules/ -./h2o-py/docs/docs/objects.inv +h2o-py/docs/_build/ git_private_jenkins.sh make-java6.sh # Ignore generated code -src-gen/ +h2o-bindings/src-gen/ +h2o-core/proto-gen/ h2o-3-DESCRIPTION gradle/buildnumber.properties +*_pb2.py +*_pb2_grpc.py # Doc stuff .Rapp.history diff --git a/build.gradle b/build.gradle index c26a47cd709..22c029b0249 100644 --- a/build.gradle +++ b/build.gradle @@ -1,35 +1,16 @@ -// -// The top-level h2o-3 project does not have any java pieces itself, but -// apply from the standard java.gradle so that 'gradle idea' generates IDE -// files with the right settings. -// -// The top-level jar file that gets produced is empty and not usable -// for anything. Use the jar file produced by the h2o-assembly subproject. -// -apply from: 'gradle/java.gradle' - -// For multiproject setup we have to apply release plugin here (we share same release number cross all modules) -if (project.hasProperty("doRelease")) { - apply from: 'gradle/release.gradle' -} - -// Print out time taken for each task so we find things that are slow. -apply from: 'gradle/timing.gradle' - // The build script settings to fetch plugins and put them on // classpath buildscript { repositories { - maven { - url 'https://plugins.gradle.org/m2/' - } + maven { url 'https://plugins.gradle.org/m2/' } mavenCentral() jcenter() } + //noinspection GroovyAssignabilityCheck dependencies { classpath 'org.ow2.asm:asm:5.1' - classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2' + classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.3' classpath 'org.gradle.api.plugins:gradle-nexus-plugin:0.7.1' classpath 'com.github.townsfolk:gradle-release:1.2' classpath 'de.undercouch:gradle-download-task:2.1.0' @@ -42,6 +23,29 @@ buildscript { } } +plugins { + id "java" + id "com.google.protobuf" version "0.8.0" +} + +// +// The top-level h2o-3 project does not have any java pieces itself, but +// apply from the standard java.gradle so that 'gradle idea' generates IDE +// files with the right settings. +// +// The top-level jar file that gets produced is empty and not usable +// for anything. Use the jar file produced by the h2o-assembly subproject. +// +apply from: 'gradle/java.gradle' + +// For multiproject setup we have to apply release plugin here (we share same release number cross all modules) +if (project.hasProperty("doRelease")) { + apply from: 'gradle/release.gradle' +} + +// Print out time taken for each task so we find things that are slow. +apply from: 'gradle/timing.gradle' + // // Common configuration // @@ -107,10 +111,15 @@ ext { // // Versions of libraries shared cross all projects + // The version of protoc must match protobuf-java. If you don't depend on + // protobuf-java directly, you will be transitively depending on the + // protobuf-java version that grpc depends on. // junitVersion = '4.12' jets3tVersion = '0.7.1' awsJavaSdkVersion = '1.8.3' + protocVersion = '3.0.2' + grpcVersion = '1.0.3' // // H2O's REST API version @@ -237,7 +246,7 @@ subprojects { } task wrapper(type: Wrapper) { - gradleVersion = '2.9' + gradleVersion = '3.3' } // diff --git a/gradle/scala.gradle b/gradle/scala.gradle index 2b904af29ee..1fd3ad445ba 100644 --- a/gradle/scala.gradle +++ b/gradle/scala.gradle @@ -21,9 +21,9 @@ sourceSets { // Activate Zinc compiler and configure scalac tasks.withType(ScalaCompile) { - scalaCompileOptions.useCompileDaemon = false - scalaCompileOptions.useAnt = false - scalaCompileOptions.additionalParameters = ['-target:jvm-1.6'] +// scalaCompileOptions.useCompileDaemon = false +// scalaCompileOptions.useAnt = false +// scalaCompileOptions.additionalParameters = ['-target:jvm-1.6'] } // Create jar diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..26deb5f9b8b6d4fc7706eae168cd9aaee8f8a7b4 100644 GIT binary patch delta 24984 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 BeVzaS delta 24409 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 g@%w%UXe%pPd*|9 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730b4e..e95643d6a2c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/h2o-bindings/bin/bindings.py b/h2o-bindings/bin/bindings.py index 8713791dc4a..346c238cfcd 100644 --- a/h2o-bindings/bin/bindings.py +++ b/h2o-bindings/bin/bindings.py @@ -243,6 +243,8 @@ def gen_rich_route(): mm = classname_pattern.match(path) assert mm, "Cannot determine class name in URL " + path e["class_name"] = mm.group(1) + if e["class_name"].islower(): + e["class_name"] = e["class_name"].capitalize() # Resolve input/output schemas into actual objects assert e["input_schema"] in schmap, "Encountered unknown schema %s in %s" % (e["input_schema"], path) diff --git a/h2o-core/build.gradle b/h2o-core/build.gradle index a9edb13dd69..a997f518670 100644 --- a/h2o-core/build.gradle +++ b/h2o-core/build.gradle @@ -1,3 +1,6 @@ +apply plugin: 'java' +apply plugin: 'com.google.protobuf' + // // H2O Core Module // @@ -15,7 +18,11 @@ dependencies { compile "ai.h2o:google-analytics-java:1.1.2-H2O-CUSTOM" compile "org.eclipse.jetty.aggregate:jetty-servlet:8.1.17.v20150415" compile "org.eclipse.jetty:jetty-plus:8.1.17.v20150415" - compile "com.github.rwl:jtransforms:2.4.0" + compile "com.github.rwl:jtransforms:2.4.0" + compile 'com.google.protobuf:protobuf-java:3.0.0' + compile "io.grpc:grpc-netty:${grpcVersion}" + compile "io.grpc:grpc-protobuf:${grpcVersion}" + compile "io.grpc:grpc-stub:${grpcVersion}" compile("log4j:log4j:1.2.15") { exclude module: "activation" @@ -55,6 +62,36 @@ jar { } } +protobuf { + // Configure the protoc executable (for compiling the *.proto files) + protoc { + artifact = 'com.google.protobuf:protoc:3.0.2' + } + + // Configure the codegen plugins + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.0.3' + } + } + + generateProtoTasks { + ofSourceSet('main').each { task -> + task.builtins { + java {} + } + task.plugins { + // Add grpc output without any option. grpc must have been defined in the + // protobuf.plugins block. + grpc {} + } + } + } + + generatedFilesBaseDir = "${projectDir}/proto-gen" +} + + // Run a single small JVM under heavy memory load, and confirm spilling works task testOOM(type: Exec) { dependsOn cpLibs, jar, testJar @@ -103,6 +140,20 @@ task cleanBuildVersionJava(type: Delete) { delete buildVersionFile } +task cleanGeneratedProtoClasses(type: Delete) { + delete protobuf.generatedFilesBaseDir +} + +clean.dependsOn cleanGeneratedProtoClasses clean.dependsOn cleanBuildVersionJava apply from: '../gradle/javaIgnoreSymbolFile.gradle' + +idea { + module { + sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/java"); + sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/grpc"); + sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/python"); + } +} + diff --git a/h2o-core/src/main/java/ai/h2o/api/protos/core/JobService.java b/h2o-core/src/main/java/ai/h2o/api/protos/core/JobService.java new file mode 100644 index 00000000000..4099cb78775 --- /dev/null +++ b/h2o-core/src/main/java/ai/h2o/api/protos/core/JobService.java @@ -0,0 +1,62 @@ +package ai.h2o.api.protos.core; + +import io.grpc.stub.StreamObserver; +import water.*; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import static ai.h2o.api.protos.core.JobInfo.Status.*; + + +/** + */ +public class JobService extends JobGrpc.JobImplBase { + + @Override + public void poll(JobId request, StreamObserver responseObserver) { + Key key = Key.make(request.getId()); + Value val = DKV.get(key); + if (val == null) + throw new IllegalArgumentException("Job " + request.getId() + " is missing"); + Iced iced = val.get(); + if (!(iced instanceof water.Job)) + throw new IllegalArgumentException("Id " + request.getId() + " references a " + iced.getClass() + " not a Job"); + + water.Job job = (water.Job) iced; + responseObserver.onNext(collectJobInfo(job)); + responseObserver.onCompleted(); + } + + + private JobInfo collectJobInfo(water.Job job) { + JobInfo.Builder jb = JobInfo.newBuilder(); + jb.setId(job._key.toString()) + .setProgress(job.progress()) + .setMessage(job.progress_msg()) + .setDuration(job.msec()); + + if (job.isRunning()) { + jb.setStatus(job.stop_requested()? STOPPING : RUNNING); + } else { + jb.setStatus(job.stop_requested()? CANCELLED : DONE); + } + + Throwable ex = job.ex(); + if (ex != null) { + StringWriter sw = new StringWriter(); + ex.printStackTrace(new PrintWriter(sw)); + jb.setStatus(FAILED) + .setException(ex.toString()) + .setStacktrace(sw.toString()); + } + + if (job._result != null && !job.readyForView()) + jb.setTargetId(job._result.toString()); + + String ttype = TypeMap.theFreezable(job._typeid).getClass().getSimpleName(); + jb.setTargetType(JobInfo.TargetType.valueOf(ttype)); + + return jb.build(); + } +} diff --git a/h2o-core/src/main/java/water/H2O.java b/h2o-core/src/main/java/water/H2O.java index c4c4e5d89e4..02b0c2dfe84 100644 --- a/h2o-core/src/main/java/water/H2O.java +++ b/h2o-core/src/main/java/water/H2O.java @@ -2,14 +2,24 @@ import com.brsanthu.googleanalytics.DefaultRequest; import com.brsanthu.googleanalytics.GoogleAnalytics; - import jsr166y.CountedCompleter; import jsr166y.ForkJoinPool; import jsr166y.ForkJoinWorkerThread; - import org.apache.log4j.LogManager; import org.apache.log4j.PropertyConfigurator; import org.reflections.Reflections; +import water.UDPRebooted.ShutdownTsk; +import water.api.RegisterGrpcApi; +import water.api.RequestServer; +import water.exceptions.H2OFailException; +import water.exceptions.H2OIllegalArgumentException; +import water.init.*; +import water.nbhm.NonBlockingHashMap; +import water.parser.ParserService; +import water.persist.PersistManager; +import water.util.*; +import io.grpc.Server; +import io.grpc.ServerBuilder; import java.io.File; import java.io.IOException; @@ -17,42 +27,12 @@ import java.lang.management.RuntimeMXBean; import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.MulticastSocket; -import java.net.NetworkInterface; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.net.*; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; -import water.UDPRebooted.ShutdownTsk; -import water.api.RequestServer; -import water.exceptions.H2OFailException; -import water.exceptions.H2OIllegalArgumentException; -import water.init.AbstractBuildVersion; -import water.init.AbstractEmbeddedH2OConfig; -import water.init.JarHash; -import water.init.NetworkInit; -import water.init.NodePersistentStorage; -import water.nbhm.NonBlockingHashMap; -import water.parser.ParserService; -import water.persist.PersistManager; -import water.util.GAUtils; -import water.util.Log; -import water.util.NetworkUtils; -import water.util.OSUtils; -import water.util.PrettyPrint; - /** * Start point for creating or joining an H2O Cloud. * @@ -242,6 +222,9 @@ public static void printHelp() { /** -user_name=user_name; Set user name */ public String user_name = System.getProperty("user.name"); + /** -run_grpc; launch GRPC/Protobuf service on {@code port + 2} */ + public boolean run_grpc = false; + //----------------------------------------------------------------------------------- // Node configuration //----------------------------------------------------------------------------------- @@ -448,6 +431,9 @@ else if (s.matches("user_name")) { i = s.incrementAndCheck(i, args); ARGS.user_name = args[i]; } + else if (s.matches("run_grpc")) { + ARGS.run_grpc = true; + } else if (s.matches("ice_root")) { i = s.incrementAndCheck(i, args); ARGS.ice_root = args[i]; @@ -1275,6 +1261,7 @@ public H2OCallback(){} public static int H2O_PORT; // Both TCP & UDP cluster ports public static int API_PORT; // RequestServer and the API HTTP port + public static int GRPC_PORT; // GRPC/Protobuf interface port /** * @return String of the form ipaddress:port @@ -1373,6 +1360,17 @@ public static JettyHTTPD getJetty() { return jetty; } + private static Server netty; + public static Server getNetty(int port) { + if (netty == null) { + ServerBuilder sb = ServerBuilder.forPort(port); + RegisterGrpcApi.registerWithServer(sb); + netty = sb.build(); + Log.info("Starting GRPC server on 127.0.0.1:" + port); + } + return netty; + } + /** If logging has not been setup yet, then Log.info will only print to * stdout. This allows for early processing of the '-version' option * without unpacking the jar file and other startup stuff. */ diff --git a/h2o-core/src/main/java/water/api/JobsHandler.java b/h2o-core/src/main/java/water/api/JobsHandler.java index cd444fbe18c..63f0d9dd87b 100644 --- a/h2o-core/src/main/java/water/api/JobsHandler.java +++ b/h2o-core/src/main/java/water/api/JobsHandler.java @@ -3,6 +3,8 @@ import water.*; import water.api.schemas3.JobV3; import water.api.schemas3.JobsV3; +import water.api.schemas4.input.JobIV4; +import water.api.schemas4.output.JobV4; import water.exceptions.H2ONotFoundArgumentException; public class JobsHandler extends Handler { @@ -51,4 +53,32 @@ public JobsV3 cancel(int version, JobsV3 c) { j.stop(); // Request Job stop return c; } + + + public static class FetchJob extends RestApiHandler { + + @Override public String name() { + return "getJob4"; + } + + @Override public String help() { + return "Retrieve information about the current state of a job."; + } + + @Override + public JobV4 exec(int ignored, JobIV4 input) { + Key key = Key.make(input.job_id); + Value val = DKV.get(key); + if (val == null) + throw new IllegalArgumentException("Job " + input.job_id + " is missing"); + Iced iced = val.get(); + if (!(iced instanceof Job)) + throw new IllegalArgumentException("Id " + input.job_id + " references a " + iced.getClass() + " not a Job"); + + Job job = (Job) iced; + JobV4 out = new JobV4(); + out.fillFromImpl(job); + return out; + } + } } diff --git a/h2o-core/src/main/java/water/api/RegisterGrpcApi.java b/h2o-core/src/main/java/water/api/RegisterGrpcApi.java new file mode 100644 index 00000000000..970f29445ed --- /dev/null +++ b/h2o-core/src/main/java/water/api/RegisterGrpcApi.java @@ -0,0 +1,14 @@ +package water.api; + +import ai.h2o.api.protos.core.JobService; +import io.grpc.ServerBuilder; + + +/** + */ +public abstract class RegisterGrpcApi { + + public static void registerWithServer(ServerBuilder sb) { + sb.addService(new JobService()); + } +} diff --git a/h2o-core/src/main/java/water/api/RegisterV4Api.java b/h2o-core/src/main/java/water/api/RegisterV4Api.java index 4ecffeeac45..83684f6f651 100644 --- a/h2o-core/src/main/java/water/api/RegisterV4Api.java +++ b/h2o-core/src/main/java/water/api/RegisterV4Api.java @@ -38,5 +38,8 @@ public void register(String relativeResourcePath) { //------------ Frames ---------------------------------------------------------------------------------------------- registerEndpoint("POST /4/Frames/$simple", CreateFrameHandler.CreateSimpleFrame.class); + + //------------ Jobs ------------------------------------------------------------------------------------------------ + registerEndpoint("GET /4/jobs/{job_id}", JobsHandler.FetchJob.class); } } diff --git a/h2o-core/src/main/java/water/api/schemas4/input/JobIV4.java b/h2o-core/src/main/java/water/api/schemas4/input/JobIV4.java new file mode 100644 index 00000000000..f46104e4281 --- /dev/null +++ b/h2o-core/src/main/java/water/api/schemas4/input/JobIV4.java @@ -0,0 +1,15 @@ +package water.api.schemas4.input; + +import water.Iced; +import water.api.API; +import water.api.schemas4.InputSchemaV4; + +/** + * Input schema for the {@code "GET /4/jobs/{job_id}"} endpoint. + */ +public class JobIV4 extends InputSchemaV4 { + + @API(help="Id of the job to fetch.") + public String job_id; + +} diff --git a/h2o-core/src/main/java/water/api/schemas4/output/JobV4.java b/h2o-core/src/main/java/water/api/schemas4/output/JobV4.java index c380ccf25f9..6669232fe30 100644 --- a/h2o-core/src/main/java/water/api/schemas4/output/JobV4.java +++ b/h2o-core/src/main/java/water/api/schemas4/output/JobV4.java @@ -1,10 +1,8 @@ package water.api.schemas4.output; import water.Job; -import water.Keyed; import water.TypeMap; import water.api.API; -import water.api.schemas3.KeyV3; import water.api.schemas4.OutputSchemaV4; import java.io.PrintWriter; @@ -14,10 +12,8 @@ /** Schema for a single Job. */ public class JobV4 extends OutputSchemaV4, JobV4> { - // TODO: replace all KeyV3's with KeyV4's - - @API(help="Job key") - public KeyV3.JobKeyV3 key; + @API(help="Job id") + public String job_id; @API(help="Job status", values={"RUNNING", "DONE", "STOPPING", "CANCELLED", "FAILED"}) public Status status; @@ -34,8 +30,11 @@ @API(help="Runtime in milliseconds") public long duration; - @API(help="Key of the target object (being created by this Job)") - public KeyV3 dest; + @API(help="Id of the target object (being created by this Job)") + public String target_id; + + @API(help="Type of the target: Frame, Model, etc.") + public String target_type; @API(help="Exception message, if an exception occurred") public String exception; @@ -43,9 +42,6 @@ @API(help="Stacktrace") public String stacktrace; - @API(help="ready for view") - public boolean ready_for_view; - public enum Status { RUNNING, DONE, STOPPING, CANCELLED, FAILED } @@ -54,11 +50,10 @@ @Override public JobV4 fillFromImpl(Job job) { if (job == null) return this; - key = new KeyV3.JobKeyV3(job._key); + job_id = job._key.toString(); progress = job.progress(); progress_msg = job.progress_msg(); duration = job.msec(); - ready_for_view = job.readyForView(); if (job.isRunning()) { status = job.stop_requested()? Status.STOPPING : Status.RUNNING; @@ -74,8 +69,9 @@ stacktrace = sw.toString(); } - Keyed dest_type = (Keyed) TypeMap.theFreezable(job._typeid); - dest = job._result == null ? null : KeyV3.make(dest_type.makeSchema(), job._result); + target_id = job._result == null || !job.readyForView()? null : job._result.toString(); + target_type = TypeMap.theFreezable(job._typeid).getClass().getSimpleName(); + return this; } diff --git a/h2o-core/src/main/java/water/init/NetworkInit.java b/h2o-core/src/main/java/water/init/NetworkInit.java index b7b3ae461ca..c3efd4cbbe0 100644 --- a/h2o-core/src/main/java/water/init/NetworkInit.java +++ b/h2o-core/src/main/java/water/init/NetworkInit.java @@ -1,43 +1,21 @@ package water.init; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.DatagramPacket; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.MulticastSocket; -import java.net.NetworkInterface; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.nio.channels.DatagramChannel; -import java.nio.channels.ServerSocketChannel; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import water.H2O; import water.H2ONode; import water.JettyHTTPD; -import water.Paxos; import water.util.Log; import water.util.NetworkUtils; import water.util.OSUtils; +import java.io.*; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.ServerSocketChannel; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Data structure for holding network info specified by the user on the command line. */ @@ -457,6 +435,11 @@ public static void initializeNetworkSockets( ) { apiSocket.close(); H2O.getJetty().start(H2O.ARGS.web_ip, H2O.API_PORT); } + + if (H2O.ARGS.run_grpc) { + H2O.GRPC_PORT = H2O.API_PORT + 2; + H2O.getNetty(H2O.GRPC_PORT); + } break; } catch (Exception e) { Log.trace("Cannot allocate API port " + H2O.API_PORT + " because of following exception: ", e); diff --git a/h2o-core/src/main/proto/core/job.proto b/h2o-core/src/main/proto/core/job.proto new file mode 100644 index 00000000000..b476684b913 --- /dev/null +++ b/h2o-core/src/main/proto/core/job.proto @@ -0,0 +1,47 @@ + +syntax = "proto3"; + +option java_package = "ai.h2o.api.protos.core"; +//option java_outer_classname = "JobProto"; +option java_multiple_files = true; + +package core; + + +service Job { + rpc poll (JobId) returns (JobInfo); +} + + +message JobId { + string id = 1; +} + +message JobInfo { + + enum Status { + RUNNING = 0; + STOPPING = 1; + DONE = 2; + CANCELLED = 3; + FAILED = 4; + } + + enum TargetType { + FRAME = 0; + MODEL = 1; + } + + string id = 1; + Status status = 2; + float progress = 3; + string message = 4; + string target_id = 5; + TargetType target_type = 6; + + int64 start_time = 7; + int64 duration = 8; + string exception = 9; + string stacktrace = 10; + repeated string warning = 11; +} diff --git a/h2o-py/build.gradle b/h2o-py/build.gradle index e2195237f99..3d120b316e6 100644 --- a/h2o-py/build.gradle +++ b/h2o-py/build.gradle @@ -1,4 +1,3 @@ -import org.apache.commons.io.output.ByteArrayOutputStream import org.apache.tools.ant.taskdefs.condition.Os import java.nio.file.FileSystems import java.nio.file.Files @@ -8,18 +7,16 @@ import static java.nio.file.StandardCopyOption.*; defaultTasks 'build_python' description = "H2O Python Package" +def protoDir = "${projectDir}/h2o/backend/proto/core" dependencies { compile project(":h2o-assembly") } -def getOS() { - String os = [Os.FAMILY_WINDOWS, Os.FAMILY_MAC, Os.FAMILY_UNIX].find {String family -> Os.isFamily(family) } - return os +def getOsSpecificCommandLine(args) { + return Os.isFamily(Os.FAMILY_WINDOWS) ? [ 'cmd', '/c' ] + args : args } -def getOsSpecificCommandLine(args) { return Os.isFamily(Os.FAMILY_WINDOWS) ? [ 'cmd', '/c' ] + args : args } - def bv = new H2OBuildVersion(rootDir, version) ext { @@ -28,13 +25,21 @@ ext { } task makeOutputDir(type: Exec) { - if(Os.isFamily(Os.FAMILY_WINDOWS)){ + if (Os.isFamily(Os.FAMILY_WINDOWS)) { commandLine getOsSpecificCommandLine(['if not exist "build\\tmp" mkdir', 'build\\tmp']) } else { commandLine getOsSpecificCommandLine(['mkdir', '-p', 'build/tmp']) } } +task makeProtoOutputDir(type: Exec) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine getOsSpecificCommandLine(['mkdir', protoDir]) + } else { + commandLine getOsSpecificCommandLine(['mkdir', '-p', protoDir]) + } +} + task setProjectVersion << { File INIT = new File([T, "h2o", "__init__.py"].join(File.separator)) println " INIT.path = " + INIT.path @@ -53,6 +58,13 @@ task resetProjectVersion << { INIT.write(txt) } +task installPythonDependencies(type: Exec) { + commandLine getOsSpecificCommandLine([ + "pip", "install", "tabulate>=0.7.5", "requests>=2.10", "colorama>=0.3.7", + "future>=0.15.2", "grpcio-tools>=1.1.0" + ]) +} + task upgradeOrInstallTabulate(type: Exec) { commandLine getOsSpecificCommandLine(["pip", "install", "tabulate", "--user", "--upgrade"]) } @@ -61,7 +73,24 @@ task upgradeOrInstallWheel(type: Exec) { commandLine getOsSpecificCommandLine(["pip", "install", "wheel", "--user", "--upgrade", "--ignore-installed"]) } -task buildDist(type: Exec, dependsOn: makeOutputDir) { +task generateProtoFiles(type: Exec) { + dependsOn makeProtoOutputDir + commandLine getOsSpecificCommandLine([ + "python", "-m", "grpc_tools.protoc", + "-I${rootDir}/h2o-core/src/main/proto/core", + "--python_out=${protoDir}", + "--grpc_python_out=${protoDir}", + // TODO: auto-detect the list of files and process them all at once + "${rootDir}/h2o-core/src/main/proto/core/job.proto" + ]) +} + +task buildDist(type: Exec) { + dependsOn installPythonDependencies + dependsOn setProjectVersion + dependsOn makeOutputDir + dependsOn generateProtoFiles + doFirst { standardOutput = new FileOutputStream("build/tmp/h2o-py_buildDist.out") } @@ -103,7 +132,6 @@ clean.dependsOn cleaner, cleanUpSmokeTest upgradeOrInstallWheel.dependsOn cleaner // buildDist.dependsOn upgradeOrInstallTabulate // buildDist.dependsOn upgradeOrInstallWheel -buildDist.dependsOn setProjectVersion resetProjectVersion.dependsOn buildDist task build_python(dependsOn: resetProjectVersion) build.dependsOn build_python diff --git a/h2o-py/h2o/h2o.py b/h2o-py/h2o/h2o.py index 84fa11b0bce..117f6ba590f 100644 --- a/h2o-py/h2o/h2o.py +++ b/h2o-py/h2o/h2o.py @@ -105,11 +105,11 @@ def connection(): def version_check(): """Used to verify that h2o-python module and the H2O server are compatible with each other.""" + from .__init__ import __version__ as ver_pkg ci = h2oconn.cluster if not ci: raise H2OConnectionError("Connection not initialized. Did you run h2o.connect()?") ver_h2o = ci.version - from .__init__ import __version__ as ver_pkg if ver_pkg == "SUBST_PROJECT_VERSION": ver_pkg = "UNKNOWN" if str(ver_h2o) != str(ver_pkg): branch_name_h2o = ci.branch_name From fada78440abea5c01acf01333361634cdc14de7f Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 3 Feb 2017 02:31:16 -0800 Subject: [PATCH 02/15] add task dependency for py:generateProtoFiles --- h2o-py/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/h2o-py/build.gradle b/h2o-py/build.gradle index 3d120b316e6..dd719dfe6e1 100644 --- a/h2o-py/build.gradle +++ b/h2o-py/build.gradle @@ -74,6 +74,7 @@ task upgradeOrInstallWheel(type: Exec) { } task generateProtoFiles(type: Exec) { + dependsOn installPythonDependencies dependsOn makeProtoOutputDir commandLine getOsSpecificCommandLine([ "python", "-m", "grpc_tools.protoc", From afcb2a5d84f6f602928a12fd87404055bd19887f Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 3 Feb 2017 10:21:27 -0800 Subject: [PATCH 03/15] Make sure the GRPC server actually starts, and register a shutdown hook --- h2o-core/src/main/java/water/H2O.java | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/h2o-core/src/main/java/water/H2O.java b/h2o-core/src/main/java/water/H2O.java index 02b0c2dfe84..e3b4b38dc94 100644 --- a/h2o-core/src/main/java/water/H2O.java +++ b/h2o-core/src/main/java/water/H2O.java @@ -1366,7 +1366,26 @@ public static Server getNetty(int port) { ServerBuilder sb = ServerBuilder.forPort(port); RegisterGrpcApi.registerWithServer(sb); netty = sb.build(); - Log.info("Starting GRPC server on 127.0.0.1:" + port); + try { + Log.info("Starting GRPC server on 127.0.0.1:" + port); + netty.start(); + } catch (IOException e) { + netty = null; + Log.err("Failed to start the GRPC server:"); + Log.err(e); + throw H2O.fail(); + } + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + if (netty != null) { + // Use stderr here since the logger may have been reset by its JVM shutdown hook. + System.err.println("*** shutting down gRPC server since JVM is shutting down"); + netty.shutdown(); + System.err.println("*** server shut down"); + } + } + }); } return netty; } From 66b9637a99020c1375ea13ac1573c3bc74140792 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Sat, 4 Feb 2017 10:07:47 -0800 Subject: [PATCH 04/15] Add rpc Job.cancel() call; attempt to force gradle to use virtualenv python/pip always --- .../h2o/api/{protos => proto}/core/JobService.java | 18 ++++++++++++- .../src/main/java/water/api/RegisterGrpcApi.java | 2 +- h2o-core/src/main/proto/core/common.proto | 10 +++++++ h2o-core/src/main/proto/core/job.proto | 4 +++ h2o-py/build.gradle | 31 +++++++++++++++------- h2o-py/conda/h2o/meta.yaml | 2 ++ h2o-py/setup.py | 2 +- 7 files changed, 56 insertions(+), 13 deletions(-) rename h2o-core/src/main/java/ai/h2o/api/{protos => proto}/core/JobService.java (76%) create mode 100644 h2o-core/src/main/proto/core/common.proto diff --git a/h2o-core/src/main/java/ai/h2o/api/protos/core/JobService.java b/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java similarity index 76% rename from h2o-core/src/main/java/ai/h2o/api/protos/core/JobService.java rename to h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java index 4099cb78775..f88c343b400 100644 --- a/h2o-core/src/main/java/ai/h2o/api/protos/core/JobService.java +++ b/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java @@ -1,5 +1,8 @@ -package ai.h2o.api.protos.core; +package ai.h2o.api.proto.core; +import ai.h2o.api.protos.core.JobGrpc; +import ai.h2o.api.protos.core.JobId; +import ai.h2o.api.protos.core.JobInfo; import io.grpc.stub.StreamObserver; import water.*; @@ -28,6 +31,19 @@ public void poll(JobId request, StreamObserver responseObserver) { responseObserver.onCompleted(); } + @Override + public void cancel(JobId request, StreamObserver responseObserver) { + Key key = Key.make(request.getId()); + Job job = DKV.getGet(key); + if (job == null) { + throw new IllegalArgumentException("No job with key " + key); + } + job.stop(); // Request Job stop + + responseObserver.onNext(collectJobInfo(job)); + responseObserver.onCompleted(); + } + private JobInfo collectJobInfo(water.Job job) { JobInfo.Builder jb = JobInfo.newBuilder(); diff --git a/h2o-core/src/main/java/water/api/RegisterGrpcApi.java b/h2o-core/src/main/java/water/api/RegisterGrpcApi.java index 970f29445ed..20f88f50b13 100644 --- a/h2o-core/src/main/java/water/api/RegisterGrpcApi.java +++ b/h2o-core/src/main/java/water/api/RegisterGrpcApi.java @@ -1,6 +1,6 @@ package water.api; -import ai.h2o.api.protos.core.JobService; +import ai.h2o.api.proto.core.JobService; import io.grpc.ServerBuilder; diff --git a/h2o-core/src/main/proto/core/common.proto b/h2o-core/src/main/proto/core/common.proto new file mode 100644 index 00000000000..032948007bc --- /dev/null +++ b/h2o-core/src/main/proto/core/common.proto @@ -0,0 +1,10 @@ + +syntax = "proto3"; + +package core; + +message empty { + // Empty message that can be reused for any RPC that takes / returns + // no values +} + diff --git a/h2o-core/src/main/proto/core/job.proto b/h2o-core/src/main/proto/core/job.proto index b476684b913..bcb9816df33 100644 --- a/h2o-core/src/main/proto/core/job.proto +++ b/h2o-core/src/main/proto/core/job.proto @@ -1,6 +1,8 @@ syntax = "proto3"; +// import "core/common.proto"; + option java_package = "ai.h2o.api.protos.core"; //option java_outer_classname = "JobProto"; option java_multiple_files = true; @@ -10,6 +12,8 @@ package core; service Job { rpc poll (JobId) returns (JobInfo); + + rpc cancel(JobId) returns (JobInfo); } diff --git a/h2o-py/build.gradle b/h2o-py/build.gradle index dd719dfe6e1..d2a5544572a 100644 --- a/h2o-py/build.gradle +++ b/h2o-py/build.gradle @@ -9,11 +9,18 @@ defaultTasks 'build_python' description = "H2O Python Package" def protoDir = "${projectDir}/h2o/backend/proto/core" +java.lang.String pythonexe = "python" +java.lang.String pipexe = "pip" +if (System.env.VIRTUAL_ENV) { + pythonexe = "${System.env.VIRTUAL_ENV}/bin/python".toString() + pipexe = "${System.env.VIRTUAL_ENV}/bin/pip".toString() +} + dependencies { compile project(":h2o-assembly") } -def getOsSpecificCommandLine(args) { +static List getOsSpecificCommandLine(List args) { return Os.isFamily(Os.FAMILY_WINDOWS) ? [ 'cmd', '/c' ] + args : args } @@ -24,6 +31,7 @@ ext { T = getProjectDir().toString() } + task makeOutputDir(type: Exec) { if (Os.isFamily(Os.FAMILY_WINDOWS)) { commandLine getOsSpecificCommandLine(['if not exist "build\\tmp" mkdir', 'build\\tmp']) @@ -43,8 +51,7 @@ task makeProtoOutputDir(type: Exec) { task setProjectVersion << { File INIT = new File([T, "h2o", "__init__.py"].join(File.separator)) println " INIT.path = " + INIT.path - def txt="" - txt = INIT.text + def txt = INIT.text txt = txt.replaceAll("SUBST_PROJECT_VERSION", PROJECT_VERSION) INIT.write(txt) } @@ -52,16 +59,19 @@ task setProjectVersion << { task resetProjectVersion << { File INIT = new File([T, "h2o", "__init__.py"].join(File.separator)) println " INIT.path = " + INIT.path - def txt="" - txt = INIT.text + def txt = INIT.text txt = txt.replaceAll(PROJECT_VERSION, "SUBST_PROJECT_VERSION") INIT.write(txt) } task installPythonDependencies(type: Exec) { commandLine getOsSpecificCommandLine([ - "pip", "install", "tabulate>=0.7.5", "requests>=2.10", "colorama>=0.3.7", - "future>=0.15.2", "grpcio-tools>=1.1.0" + pipexe, "install", + "tabulate>=0.7.5", + "requests>=2.10", + "colorama>=0.3.7", + "future>=0.15.2", + "grpcio-tools>=1.1.0" ]) } @@ -77,11 +87,12 @@ task generateProtoFiles(type: Exec) { dependsOn installPythonDependencies dependsOn makeProtoOutputDir commandLine getOsSpecificCommandLine([ - "python", "-m", "grpc_tools.protoc", + pythonexe, "-m", "grpc_tools.protoc", "-I${rootDir}/h2o-core/src/main/proto/core", "--python_out=${protoDir}", "--grpc_python_out=${protoDir}", // TODO: auto-detect the list of files and process them all at once + "${rootDir}/h2o-core/src/main/proto/core/common.proto", "${rootDir}/h2o-core/src/main/proto/core/job.proto" ]) } @@ -98,14 +109,14 @@ task buildDist(type: Exec) { commandLine getOsSpecificCommandLine(["python", "setup.py", "bdist_wheel"]) } -def testsPath = new File("./tests") +File testsPath = new File("./tests") task smokeTest(type: Exec) { dependsOn build println "PyUnit smoke test (run.py --wipeall --testsize s)..." workingDir testsPath commandLine 'pwd' - def args = ['python', '../../scripts/run.py', '--wipeall', '--testsize', 's'] + List args = [pythonexe, '../../scripts/run.py', '--wipeall', '--testsize', 's'] if (project.hasProperty("jacocoCoverage")) { args << '--jacoco' } diff --git a/h2o-py/conda/h2o/meta.yaml b/h2o-py/conda/h2o/meta.yaml index 522813f4d2d..4ae145f6417 100644 --- a/h2o-py/conda/h2o/meta.yaml +++ b/h2o-py/conda/h2o/meta.yaml @@ -15,6 +15,7 @@ requirements: - future >=0.15.2 - tabulate >=0.7.5 - requests >=2.10 + - grpcio-tools >= 1.1.0 run: - python >=2.7,<3|>=3.5 @@ -22,6 +23,7 @@ requirements: - future >=0.15.2 - tabulate >=0.7.5 - requests >=2.10 + - grpcio >= 1.1.0 about: home: https://github.com/h2oai/h2o-3.git diff --git a/h2o-py/setup.py b/h2o-py/setup.py index 22424241995..c2646522d41 100644 --- a/h2o-py/setup.py +++ b/h2o-py/setup.py @@ -91,5 +91,5 @@ ]}, # run-time dependencies - install_requires=["requests", "tabulate", "future", "colorama"], + install_requires=["requests", "tabulate", "future", "colorama", "grpcio-tools"], ) From 0755e2bd0176ab2472a728f032ca78a50c628378 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Sat, 4 Feb 2017 20:50:11 -0800 Subject: [PATCH 05/15] Make sure that the latest version of pip installed in the virtual environment --- scripts/jenkins/PR_py3unit_small.sh | 1 + scripts/jenkins/PR_pybooklets.sh | 1 + scripts/jenkins/PR_python_demos.sh | 1 + scripts/jenkins/PR_pyunit_medium_large.sh | 1 + scripts/jenkins/PR_pyunit_small.sh | 1 + scripts/jenkins/PR_pyunit_small_J6.sh | 1 + scripts/jenkins/PR_smoke_pyunit.sh | 1 + scripts/jenkins/PR_startup_checks.sh | 3 +++ 8 files changed, 10 insertions(+) diff --git a/scripts/jenkins/PR_py3unit_small.sh b/scripts/jenkins/PR_py3unit_small.sh index 893533402f1..4d22d619046 100644 --- a/scripts/jenkins/PR_py3unit_small.sh +++ b/scripts/jenkins/PR_py3unit_small.sh @@ -50,6 +50,7 @@ echo "* Activating Python virtualenv" echo "*********************************************" echo "" source $WORKSPACE/../h2o_venv3/bin/activate +pip install --upgrade pip # Use the Jenkins-user shared R library; already sync'd no need to sync again export R_LIBS_USER=${WORKSPACE}/../Rlibrary diff --git a/scripts/jenkins/PR_pybooklets.sh b/scripts/jenkins/PR_pybooklets.sh index 6300f9eb921..69c271587ac 100644 --- a/scripts/jenkins/PR_pybooklets.sh +++ b/scripts/jenkins/PR_pybooklets.sh @@ -49,6 +49,7 @@ echo "* Activating Python virtualenv" echo "*********************************************" echo "" source $WORKSPACE/../h2o_venv/bin/activate +pip install --upgrade pip # Use the Jenkins-user shared R library; already sync'd no need to sync again export R_LIBS_USER=${WORKSPACE}/../Rlibrary diff --git a/scripts/jenkins/PR_python_demos.sh b/scripts/jenkins/PR_python_demos.sh index 1c9ec523e69..7cddf1a58af 100644 --- a/scripts/jenkins/PR_python_demos.sh +++ b/scripts/jenkins/PR_python_demos.sh @@ -49,6 +49,7 @@ echo "* Activating Python virtualenv" echo "*********************************************" echo "" source $WORKSPACE/../h2o_venv/bin/activate +pip install --upgrade pip python --version diff --git a/scripts/jenkins/PR_pyunit_medium_large.sh b/scripts/jenkins/PR_pyunit_medium_large.sh index 68f9303aea6..fc11e68f4b6 100644 --- a/scripts/jenkins/PR_pyunit_medium_large.sh +++ b/scripts/jenkins/PR_pyunit_medium_large.sh @@ -49,6 +49,7 @@ echo "* Activating Python virtualenv" echo "*********************************************" echo "" source $WORKSPACE/../h2o_venv/bin/activate +pip install --upgrade pip # Use the Jenkins-user shared R library; already sync'd no need to sync again export R_LIBS_USER=${WORKSPACE}/../Rlibrary diff --git a/scripts/jenkins/PR_pyunit_small.sh b/scripts/jenkins/PR_pyunit_small.sh index c3bb7f47bf3..da0d49d45ad 100644 --- a/scripts/jenkins/PR_pyunit_small.sh +++ b/scripts/jenkins/PR_pyunit_small.sh @@ -53,6 +53,7 @@ echo "* Activating Python virtualenv" echo "*********************************************" echo "" source $WORKSPACE/../h2o_venv/bin/activate +pip install --upgrade pip which python which pip diff --git a/scripts/jenkins/PR_pyunit_small_J6.sh b/scripts/jenkins/PR_pyunit_small_J6.sh index fd45510c9ee..2e6a4a0dd72 100644 --- a/scripts/jenkins/PR_pyunit_small_J6.sh +++ b/scripts/jenkins/PR_pyunit_small_J6.sh @@ -53,6 +53,7 @@ echo "* Activating Python virtualenv" echo "*********************************************" echo "" source $WORKSPACE/../h2o_venv/bin/activate +pip install --upgrade pip # Use the Jenkins-user shared R library; already sync'd no need to sync again export R_LIBS_USER=${WORKSPACE}/../Rlibrary diff --git a/scripts/jenkins/PR_smoke_pyunit.sh b/scripts/jenkins/PR_smoke_pyunit.sh index 903dcba1c0c..204069233a8 100644 --- a/scripts/jenkins/PR_smoke_pyunit.sh +++ b/scripts/jenkins/PR_smoke_pyunit.sh @@ -50,6 +50,7 @@ echo "* Activating Python virtualenv" echo "*********************************************" echo "" source $WORKSPACE/../h2o_venv/bin/activate +pip install --upgrade pip # Use the Jenkins-user shared R library; already sync'd no need to sync again export R_LIBS_USER=${WORKSPACE}/../Rlibrary diff --git a/scripts/jenkins/PR_startup_checks.sh b/scripts/jenkins/PR_startup_checks.sh index f2b64eef616..a41865eee76 100644 --- a/scripts/jenkins/PR_startup_checks.sh +++ b/scripts/jenkins/PR_startup_checks.sh @@ -51,6 +51,9 @@ echo "*********************************************" echo "" virtualenv $WORKSPACE/h2o_venv --python=python2.7 source $WORKSPACE/h2o_venv/bin/activate +pip install --upgrade pip + +# This should be done in gradle... pip install numpy --upgrade pip install scipy --upgrade pip install -r h2o-py/requirements.txt From 51fd9f5670200c0242f3596257fd873417a466b6 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Mon, 6 Feb 2017 11:15:37 -0800 Subject: [PATCH 06/15] Refactor logic for retrieving a job from its id --- .../java/ai/h2o/api/proto/core/JobService.java | 47 +++++++++++++--------- h2o-core/src/main/proto/core/job.proto | 4 +- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java b/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java index f88c343b400..aaa6967a26f 100644 --- a/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java +++ b/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java @@ -18,36 +18,43 @@ @Override public void poll(JobId request, StreamObserver responseObserver) { - Key key = Key.make(request.getId()); - Value val = DKV.get(key); - if (val == null) - throw new IllegalArgumentException("Job " + request.getId() + " is missing"); - Iced iced = val.get(); - if (!(iced instanceof water.Job)) - throw new IllegalArgumentException("Id " + request.getId() + " references a " + iced.getClass() + " not a Job"); - - water.Job job = (water.Job) iced; - responseObserver.onNext(collectJobInfo(job)); + water.Job job = resolveJob(request); + responseObserver.onNext(fillJobInfo(job)); responseObserver.onCompleted(); } @Override public void cancel(JobId request, StreamObserver responseObserver) { - Key key = Key.make(request.getId()); - Job job = DKV.getGet(key); - if (job == null) { - throw new IllegalArgumentException("No job with key " + key); - } - job.stop(); // Request Job stop - - responseObserver.onNext(collectJobInfo(job)); + water.Job job = resolveJob(request); + job.stop(); + responseObserver.onNext(fillJobInfo(job)); responseObserver.onCompleted(); } - private JobInfo collectJobInfo(water.Job job) { + + //-------------------------------------------------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------------------------------------------------- + + private water.Job resolveJob(JobId request) { + String strId = request.getJobId(); + Value val = DKV.get(Key.make(strId)); + if (val == null) { + throw new IllegalArgumentException("Job " + strId + " not found in the DKV"); + } + Iced iced = val.get(); + if (iced instanceof Job) { + return (water.Job) iced; + } else { + throw new IllegalArgumentException("Id " + strId + " does not reference a Job but a " + iced.getClass()); + } + } + + + private JobInfo fillJobInfo(water.Job job) { JobInfo.Builder jb = JobInfo.newBuilder(); - jb.setId(job._key.toString()) + jb.setJobId(job._key.toString()) .setProgress(job.progress()) .setMessage(job.progress_msg()) .setDuration(job.msec()); diff --git a/h2o-core/src/main/proto/core/job.proto b/h2o-core/src/main/proto/core/job.proto index bcb9816df33..11e8fb5daac 100644 --- a/h2o-core/src/main/proto/core/job.proto +++ b/h2o-core/src/main/proto/core/job.proto @@ -18,7 +18,7 @@ service Job { message JobId { - string id = 1; + string job_id = 1; } message JobInfo { @@ -36,7 +36,7 @@ message JobInfo { MODEL = 1; } - string id = 1; + string job_id = 1; Status status = 2; float progress = 3; string message = 4; From a65f0ac6fafaf3274d5020e48fafbe5ef6acaf90 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Mon, 6 Feb 2017 11:42:52 -0800 Subject: [PATCH 07/15] Use correct python (from virtualenv) when building py distribution --- h2o-py/build.gradle | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/h2o-py/build.gradle b/h2o-py/build.gradle index d2a5544572a..3f105f8a14a 100644 --- a/h2o-py/build.gradle +++ b/h2o-py/build.gradle @@ -75,13 +75,6 @@ task installPythonDependencies(type: Exec) { ]) } -task upgradeOrInstallTabulate(type: Exec) { - commandLine getOsSpecificCommandLine(["pip", "install", "tabulate", "--user", "--upgrade"]) -} - -task upgradeOrInstallWheel(type: Exec) { - commandLine getOsSpecificCommandLine(["pip", "install", "wheel", "--user", "--upgrade", "--ignore-installed"]) -} task generateProtoFiles(type: Exec) { dependsOn installPythonDependencies @@ -106,7 +99,7 @@ task buildDist(type: Exec) { doFirst { standardOutput = new FileOutputStream("build/tmp/h2o-py_buildDist.out") } - commandLine getOsSpecificCommandLine(["python", "setup.py", "bdist_wheel"]) + commandLine getOsSpecificCommandLine([pythonexe, "setup.py", "bdist_wheel"]) } File testsPath = new File("./tests") @@ -141,9 +134,6 @@ task cleaner << { } clean.dependsOn cleaner, cleanUpSmokeTest -upgradeOrInstallWheel.dependsOn cleaner -// buildDist.dependsOn upgradeOrInstallTabulate -// buildDist.dependsOn upgradeOrInstallWheel resetProjectVersion.dependsOn buildDist task build_python(dependsOn: resetProjectVersion) build.dependsOn build_python From 803dc36a2748efaedab05d0be7d8fff7c9cc099b Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Mon, 6 Feb 2017 14:53:56 -0800 Subject: [PATCH 08/15] Implement error propagation mechanism for GRPC protocol --- .../java/ai/h2o/api/proto/core/GrpcCommon.java | 43 ++++++++++++++++++++++ .../java/ai/h2o/api/proto/core/JobService.java | 35 +++++++++--------- h2o-core/src/main/proto/core/common.proto | 14 ++++++- h2o-core/src/main/proto/core/job.proto | 28 +++++++------- h2o-py/build.gradle | 4 +- 5 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 h2o-core/src/main/java/ai/h2o/api/proto/core/GrpcCommon.java diff --git a/h2o-core/src/main/java/ai/h2o/api/proto/core/GrpcCommon.java b/h2o-core/src/main/java/ai/h2o/api/proto/core/GrpcCommon.java new file mode 100644 index 00000000000..23c744cabd8 --- /dev/null +++ b/h2o-core/src/main/java/ai/h2o/api/proto/core/GrpcCommon.java @@ -0,0 +1,43 @@ +package ai.h2o.api.proto.core; + +import com.google.protobuf.Message; +import io.grpc.stub.StreamObserver; + +import java.lang.reflect.Method; + +/** + * + */ +public abstract class GrpcCommon { + + public static void sendError(Throwable e, StreamObserver responseObserver, Class clz) { + try { + Method m = clz.getDeclaredMethod("newBuilder"); + Message.Builder builder = (Message.Builder) m.invoke(null); + + Method em = builder.getClass().getDeclaredMethod("setError", Error.class); + em.invoke(builder, buildError(e, 0)); + + responseObserver.onNext((T) builder.build()); + responseObserver.onCompleted(); + } catch (Exception e2) { + throw new RuntimeException(e2.getMessage(), e); + } + } + + + public static Error buildError(Throwable e, int depth) { + Error.Builder eb = Error.newBuilder(); + eb.setMessage(e.getMessage()); + StringBuilder sb = new StringBuilder(); + for (StackTraceElement st: e.getStackTrace()) { + sb.append(st.toString()).append("\n"); + } + eb.setStacktrace(sb.toString()); + if (e.getCause() != null && depth < 3) { + eb.setCause(buildError(e.getCause(), depth + 1)); + } + return eb.build(); + } + +} diff --git a/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java b/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java index aaa6967a26f..54c37b7f4c4 100644 --- a/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java +++ b/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java @@ -1,15 +1,9 @@ package ai.h2o.api.proto.core; -import ai.h2o.api.protos.core.JobGrpc; -import ai.h2o.api.protos.core.JobId; -import ai.h2o.api.protos.core.JobInfo; import io.grpc.stub.StreamObserver; import water.*; -import java.io.PrintWriter; -import java.io.StringWriter; - -import static ai.h2o.api.protos.core.JobInfo.Status.*; +import static ai.h2o.api.proto.core.JobInfo.Status.*; /** @@ -18,17 +12,25 @@ @Override public void poll(JobId request, StreamObserver responseObserver) { - water.Job job = resolveJob(request); - responseObserver.onNext(fillJobInfo(job)); - responseObserver.onCompleted(); + try { + water.Job job = resolveJob(request); + responseObserver.onNext(fillJobInfo(job)); + responseObserver.onCompleted(); + } catch (Throwable ex) { + GrpcCommon.sendError(ex, responseObserver, JobInfo.class); + } } @Override public void cancel(JobId request, StreamObserver responseObserver) { - water.Job job = resolveJob(request); - job.stop(); - responseObserver.onNext(fillJobInfo(job)); - responseObserver.onCompleted(); + try { + water.Job job = resolveJob(request); + job.stop(); + responseObserver.onNext(fillJobInfo(job)); + responseObserver.onCompleted(); + } catch (Throwable ex) { + GrpcCommon.sendError(ex, responseObserver, JobInfo.class); + } } @@ -67,11 +69,8 @@ private JobInfo fillJobInfo(water.Job job) { Throwable ex = job.ex(); if (ex != null) { - StringWriter sw = new StringWriter(); - ex.printStackTrace(new PrintWriter(sw)); jb.setStatus(FAILED) - .setException(ex.toString()) - .setStacktrace(sw.toString()); + .setError(GrpcCommon.buildError(ex, 0)); } if (job._result != null && !job.readyForView()) diff --git a/h2o-core/src/main/proto/core/common.proto b/h2o-core/src/main/proto/core/common.proto index 032948007bc..1341724efbb 100644 --- a/h2o-core/src/main/proto/core/common.proto +++ b/h2o-core/src/main/proto/core/common.proto @@ -1,10 +1,20 @@ - syntax = "proto3"; package core; +option java_package = "ai.h2o.api.proto.core"; +option java_multiple_files = true; + -message empty { +message Empty { // Empty message that can be reused for any RPC that takes / returns // no values } + +message Error { + // + string message = 1; + string stacktrace = 2; + Error cause = 3; +} + diff --git a/h2o-core/src/main/proto/core/job.proto b/h2o-core/src/main/proto/core/job.proto index 11e8fb5daac..faee5db8849 100644 --- a/h2o-core/src/main/proto/core/job.proto +++ b/h2o-core/src/main/proto/core/job.proto @@ -1,9 +1,8 @@ - syntax = "proto3"; -// import "core/common.proto"; +import "core/common.proto"; -option java_package = "ai.h2o.api.protos.core"; +option java_package = "ai.h2o.api.proto.core"; //option java_outer_classname = "JobProto"; option java_multiple_files = true; @@ -22,6 +21,7 @@ message JobId { } message JobInfo { + Error error = 1; enum Status { RUNNING = 0; @@ -36,16 +36,14 @@ message JobInfo { MODEL = 1; } - string job_id = 1; - Status status = 2; - float progress = 3; - string message = 4; - string target_id = 5; - TargetType target_type = 6; - - int64 start_time = 7; - int64 duration = 8; - string exception = 9; - string stacktrace = 10; - repeated string warning = 11; + string job_id = 2; + Status status = 3; + float progress = 4; + string message = 5; + string target_id = 6; + TargetType target_type = 7; + + int64 start_time = 8; + int64 duration = 9; + repeated string warning = 10; } diff --git a/h2o-py/build.gradle b/h2o-py/build.gradle index 3f105f8a14a..4dea2167f29 100644 --- a/h2o-py/build.gradle +++ b/h2o-py/build.gradle @@ -7,7 +7,7 @@ import static java.nio.file.StandardCopyOption.*; defaultTasks 'build_python' description = "H2O Python Package" -def protoDir = "${projectDir}/h2o/backend/proto/core" +GString protoDir = "${projectDir}/h2o/backend/proto" java.lang.String pythonexe = "python" java.lang.String pipexe = "pip" @@ -81,7 +81,7 @@ task generateProtoFiles(type: Exec) { dependsOn makeProtoOutputDir commandLine getOsSpecificCommandLine([ pythonexe, "-m", "grpc_tools.protoc", - "-I${rootDir}/h2o-core/src/main/proto/core", + "-I${rootDir}/h2o-core/src/main/proto", "--python_out=${protoDir}", "--grpc_python_out=${protoDir}", // TODO: auto-detect the list of files and process them all at once From 42e42d93aa897dd982274117baafe9e7fa13a1a5 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Tue, 7 Feb 2017 13:35:12 -0800 Subject: [PATCH 09/15] Relocate GRPC subsystem into a separate folder h2o-grpc, and convert it into a plugin for H2O --- .gitignore | 2 +- h2o-assembly/build.gradle | 1 + h2o-core/build.gradle | 43 -------- .../src/main/java/water/AbstractH2OExtension.java | 16 ++- h2o-core/src/main/java/water/H2O.java | 64 ++--------- h2o-core/src/main/java/water/init/NetworkInit.java | 4 - h2o-grpc/build.gradle | 118 +++++++++++++++++++++ .../src/main/java/ai/h2o/api/GrpcExtension.java | 81 ++++++++++++++ .../src/main/java/ai/h2o}/api/RegisterGrpcApi.java | 6 +- .../java/ai/h2o/api/proto/core/GrpcCommon.java | 0 .../java/ai/h2o/api/proto/core/JobService.java | 0 .../src/main/proto/core/common.proto | 0 .../src/main/proto/core/job.proto | 0 .../META-INF/services/water.AbstractH2OExtension | 1 + h2o-py/build.gradle | 28 +---- settings.gradle | 1 + 16 files changed, 230 insertions(+), 135 deletions(-) create mode 100644 h2o-grpc/build.gradle create mode 100644 h2o-grpc/src/main/java/ai/h2o/api/GrpcExtension.java rename {h2o-core/src/main/java/water => h2o-grpc/src/main/java/ai/h2o}/api/RegisterGrpcApi.java (51%) rename {h2o-core => h2o-grpc}/src/main/java/ai/h2o/api/proto/core/GrpcCommon.java (100%) rename {h2o-core => h2o-grpc}/src/main/java/ai/h2o/api/proto/core/JobService.java (100%) rename {h2o-core => h2o-grpc}/src/main/proto/core/common.proto (100%) rename {h2o-core => h2o-grpc}/src/main/proto/core/job.proto (100%) create mode 100644 h2o-grpc/src/main/resources/META-INF/services/water.AbstractH2OExtension diff --git a/.gitignore b/.gitignore index 626e798d54d..e1b7cf62568 100644 --- a/.gitignore +++ b/.gitignore @@ -82,7 +82,7 @@ make-java6.sh # Ignore generated code h2o-bindings/src-gen/ -h2o-core/proto-gen/ +h2o-grpc/proto-gen/ h2o-3-DESCRIPTION gradle/buildnumber.properties *_pb2.py diff --git a/h2o-assembly/build.gradle b/h2o-assembly/build.gradle index 7e231771bb3..041c3486710 100644 --- a/h2o-assembly/build.gradle +++ b/h2o-assembly/build.gradle @@ -14,6 +14,7 @@ configurations { // Dependencies dependencies { compile project(":h2o-app") + compile project(":h2o-grpc") compile project(":h2o-persist-s3") compile project(":h2o-persist-hdfs") if (project.hasProperty("doIncludeOrc") && project.doIncludeOrc == "true") { diff --git a/h2o-core/build.gradle b/h2o-core/build.gradle index a997f518670..a6525bd2425 100644 --- a/h2o-core/build.gradle +++ b/h2o-core/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'java' -apply plugin: 'com.google.protobuf' // // H2O Core Module @@ -19,10 +18,6 @@ dependencies { compile "org.eclipse.jetty.aggregate:jetty-servlet:8.1.17.v20150415" compile "org.eclipse.jetty:jetty-plus:8.1.17.v20150415" compile "com.github.rwl:jtransforms:2.4.0" - compile 'com.google.protobuf:protobuf-java:3.0.0' - compile "io.grpc:grpc-netty:${grpcVersion}" - compile "io.grpc:grpc-protobuf:${grpcVersion}" - compile "io.grpc:grpc-stub:${grpcVersion}" compile("log4j:log4j:1.2.15") { exclude module: "activation" @@ -62,35 +57,6 @@ jar { } } -protobuf { - // Configure the protoc executable (for compiling the *.proto files) - protoc { - artifact = 'com.google.protobuf:protoc:3.0.2' - } - - // Configure the codegen plugins - plugins { - grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.0.3' - } - } - - generateProtoTasks { - ofSourceSet('main').each { task -> - task.builtins { - java {} - } - task.plugins { - // Add grpc output without any option. grpc must have been defined in the - // protobuf.plugins block. - grpc {} - } - } - } - - generatedFilesBaseDir = "${projectDir}/proto-gen" -} - // Run a single small JVM under heavy memory load, and confirm spilling works task testOOM(type: Exec) { @@ -148,12 +114,3 @@ clean.dependsOn cleanGeneratedProtoClasses clean.dependsOn cleanBuildVersionJava apply from: '../gradle/javaIgnoreSymbolFile.gradle' - -idea { - module { - sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/java"); - sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/grpc"); - sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/python"); - } -} - diff --git a/h2o-core/src/main/java/water/AbstractH2OExtension.java b/h2o-core/src/main/java/water/AbstractH2OExtension.java index 1b7be598df0..5f150a6a6bf 100644 --- a/h2o-core/src/main/java/water/AbstractH2OExtension.java +++ b/h2o-core/src/main/java/water/AbstractH2OExtension.java @@ -12,13 +12,19 @@ /** * Any up-front initialization that needs to happen before H2O is started. - * This is called in H2OApp before H2O.main() is called. + * This is called in {@code H2OApp} before {@code H2O.main()} is called. */ public void init() {} + /** - * Print stuff for - * java -jar h2o.jar -help + * Called during the start up process of {@code H2OApp}, after the local + * network connections are opened. + */ + public void onLocalNodeStarted() {} + + /** + * Print stuff (into System.out) for {@code java -jar h2o.jar -help} */ public void printHelp() {} @@ -51,7 +57,9 @@ public void validateArguments() {} * * @return build information. */ - public abstract AbstractBuildVersion getBuildVersion(); + public AbstractBuildVersion getBuildVersion() { + return AbstractBuildVersion.UNKNOWN_VERSION; + } /** * Print a short message when the extension finishes initializing. diff --git a/h2o-core/src/main/java/water/H2O.java b/h2o-core/src/main/java/water/H2O.java index e3b4b38dc94..e0bb8f9e94a 100644 --- a/h2o-core/src/main/java/water/H2O.java +++ b/h2o-core/src/main/java/water/H2O.java @@ -9,7 +9,6 @@ import org.apache.log4j.PropertyConfigurator; import org.reflections.Reflections; import water.UDPRebooted.ShutdownTsk; -import water.api.RegisterGrpcApi; import water.api.RequestServer; import water.exceptions.H2OFailException; import water.exceptions.H2OIllegalArgumentException; @@ -18,8 +17,6 @@ import water.parser.ParserService; import water.persist.PersistManager; import water.util.*; -import io.grpc.Server; -import io.grpc.ServerBuilder; import java.io.File; import java.io.IOException; @@ -222,8 +219,6 @@ public static void printHelp() { /** -user_name=user_name; Set user name */ public String user_name = System.getProperty("user.name"); - /** -run_grpc; launch GRPC/Protobuf service on {@code port + 2} */ - public boolean run_grpc = false; //----------------------------------------------------------------------------------- // Node configuration @@ -431,9 +426,6 @@ else if (s.matches("user_name")) { i = s.incrementAndCheck(i, args); ARGS.user_name = args[i]; } - else if (s.matches("run_grpc")) { - ARGS.run_grpc = true; - } else if (s.matches("ice_root")) { i = s.incrementAndCheck(i, args); ARGS.ice_root = args[i]; @@ -732,24 +724,10 @@ public static void registerExtensions() { // Disallow schemas whose parent is in another package because it takes ~4s to do the getSubTypesOf call. String[] packages = new String[]{"water", "hex"}; - - for (String pkg : packages) { - Reflections reflections = new Reflections(pkg); - for (Class registerClass : reflections.getSubTypesOf(water.AbstractH2OExtension.class)) { - if (!Modifier.isAbstract(registerClass.getModifiers())) { - try { - Object instance = registerClass.newInstance(); - water.AbstractH2OExtension e = (water.AbstractH2OExtension) instance; - H2O.addExtension(e); - } catch (Exception e) { - throw H2O.fail(e.toString()); - } - } - } - } - - for (AbstractH2OExtension e : H2O.getExtensions()) { + ServiceLoader extensionsLoader = ServiceLoader.load(AbstractH2OExtension.class); + for (AbstractH2OExtension e : extensionsLoader) { e.init(); + extensions.add(e); } extensionsRegistered = true; @@ -1261,7 +1239,6 @@ public H2OCallback(){} public static int H2O_PORT; // Both TCP & UDP cluster ports public static int API_PORT; // RequestServer and the API HTTP port - public static int GRPC_PORT; // GRPC/Protobuf interface port /** * @return String of the form ipaddress:port @@ -1360,36 +1337,6 @@ public static JettyHTTPD getJetty() { return jetty; } - private static Server netty; - public static Server getNetty(int port) { - if (netty == null) { - ServerBuilder sb = ServerBuilder.forPort(port); - RegisterGrpcApi.registerWithServer(sb); - netty = sb.build(); - try { - Log.info("Starting GRPC server on 127.0.0.1:" + port); - netty.start(); - } catch (IOException e) { - netty = null; - Log.err("Failed to start the GRPC server:"); - Log.err(e); - throw H2O.fail(); - } - Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { - if (netty != null) { - // Use stderr here since the logger may have been reset by its JVM shutdown hook. - System.err.println("*** shutting down gRPC server since JVM is shutting down"); - netty.shutdown(); - System.err.println("*** server shut down"); - } - } - }); - } - return netty; - } - /** If logging has not been setup yet, then Log.info will only print to * stdout. This allows for early processing of the '-version' option * without unpacking the jar file and other startup stuff. */ @@ -1884,6 +1831,11 @@ public static void main( String[] args ) { // Start the local node. Needed before starting logging. startLocalNode(); + // Allow extensions to perform initialization that requires the network. + for (AbstractH2OExtension ext: extensions) { + ext.onLocalNodeStarted(); + } + try { String logDir = Log.getLogDir(); Log.info("Log dir: '" + logDir + "'"); diff --git a/h2o-core/src/main/java/water/init/NetworkInit.java b/h2o-core/src/main/java/water/init/NetworkInit.java index c3efd4cbbe0..e9a41205601 100644 --- a/h2o-core/src/main/java/water/init/NetworkInit.java +++ b/h2o-core/src/main/java/water/init/NetworkInit.java @@ -436,10 +436,6 @@ public static void initializeNetworkSockets( ) { H2O.getJetty().start(H2O.ARGS.web_ip, H2O.API_PORT); } - if (H2O.ARGS.run_grpc) { - H2O.GRPC_PORT = H2O.API_PORT + 2; - H2O.getNetty(H2O.GRPC_PORT); - } break; } catch (Exception e) { Log.trace("Cannot allocate API port " + H2O.API_PORT + " because of following exception: ", e); diff --git a/h2o-grpc/build.gradle b/h2o-grpc/build.gradle new file mode 100644 index 00000000000..9fd87811983 --- /dev/null +++ b/h2o-grpc/build.gradle @@ -0,0 +1,118 @@ +import org.apache.tools.ant.taskdefs.condition.Os + + +apply plugin: 'java' +apply plugin: 'com.google.protobuf' + +repositories { + mavenCentral() +} + +def grpcVersion = '1.0.3' + +// Detect the correct version of python executable to use +def pythonexe = "python" +def pipexe = "pip" +if (System.env.VIRTUAL_ENV) { + pythonexe = "${System.env.VIRTUAL_ENV}/bin/python" + pipexe = "${System.env.VIRTUAL_ENV}/bin/pip" +} + + +dependencies { + compile project(":h2o-core") + compile 'com.google.protobuf:protobuf-java:3.0.0' + compile "io.grpc:grpc-netty:${grpcVersion}" + compile "io.grpc:grpc-protobuf:${grpcVersion}" + compile "io.grpc:grpc-stub:${grpcVersion}" +} + + +//---------------------------------------------------------------------------------------------------------------------- +// Compile Java Protobuf+GRPC objects +//---------------------------------------------------------------------------------------------------------------------- + +protobuf { + // Configure the protoc executable (for compiling the *.proto files) + protoc { + artifact = 'com.google.protobuf:protoc:3.0.2' + } + + // Configure the codegen plugins + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" + } + } + + generateProtoTasks { + ofSourceSet('main').each { task -> + task.builtins { + java {} + } + task.plugins { + // Add grpc output without any option. grpc must have been defined in the + // protobuf.plugins block. + grpc {} + } + } + } + + generatedFilesBaseDir = "${projectDir}/proto-gen" +} + + +//---------------------------------------------------------------------------------------------------------------------- +// Compile Python Protobuf+GRPC classes +//---------------------------------------------------------------------------------------------------------------------- +def pyProtoDir = "${projectDir}/proto-gen/main/python" + +static List getOsSpecificCommandLine(List args) { + return Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c'] + args : args +} + +task installGrpciotoolsModule(type: Exec) { + doFirst { + standardOutput = new FileOutputStream("build/tmp/h2o-grpc_installGrpciotoolsModule.out") + } + commandLine getOsSpecificCommandLine([pipexe, "install", "grpcio-tools>=1.1.0"]) +} + +task makeProtoOutputDir(type: Exec) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'cmd', '/c', 'mkdir', pyProtoDir + } else { + commandLine 'mkdir', '-p', pyProtoDir + } +} + +task generatePyProtoFiles(type: Exec) { + dependsOn installGrpciotoolsModule + dependsOn makeProtoOutputDir + + commandLine getOsSpecificCommandLine([ + pythonexe, "-m", "grpc_tools.protoc", + "-I${projectDir}/src/main/proto", + "--python_out=${pyProtoDir}", + "--grpc_python_out=${pyProtoDir}", + // TODO: auto-detect the list of files and process them all at once + "${projectDir}/src/main/proto/core/common.proto", + "${projectDir}/src/main/proto/core/job.proto" + ]) +} + +build.dependsOn generatePyProtoFiles + + +//---------------------------------------------------------------------------------------------------------------------- +// Instruct IDEA to use the generated files as sources +//---------------------------------------------------------------------------------------------------------------------- + +idea { + module { + sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/java"); + sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/grpc"); + sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/python"); + } +} + diff --git a/h2o-grpc/src/main/java/ai/h2o/api/GrpcExtension.java b/h2o-grpc/src/main/java/ai/h2o/api/GrpcExtension.java new file mode 100644 index 00000000000..989102129e2 --- /dev/null +++ b/h2o-grpc/src/main/java/ai/h2o/api/GrpcExtension.java @@ -0,0 +1,81 @@ +package ai.h2o.api; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import water.AbstractH2OExtension; +import water.H2O; +import water.util.Log; + +import java.io.IOException; + +/** + */ +public class GrpcExtension extends AbstractH2OExtension { + private int grpcPort = 0; // 0 means that GRPC service should not start + private Server netty; + + @Override + public String getExtensionName() { + return "GRPC"; + } + + @Override + public void printHelp() { + System.out.println( + "\nGRPC extension:\n" + + " -grpc_port\n" + + " Port number on which to start the GRPC service. If not \n" + + " specified, GRPC service will not be started." + ); + } + + @Override + public String[] parseArguments(String[] args) { + for (int i = 0; i < args.length - 1; i++) { + H2O.OptString s = new H2O.OptString(args[i]); + if (s.matches("grpc_port")) { + grpcPort = s.parseInt(args[i + 1]); + String[] new_args = new String[args.length - 2]; + System.arraycopy(args, 0, new_args, 0, i); + System.arraycopy(args, i + 2, new_args, i, args.length - (i + 2)); + return new_args; + } + } + return args; + } + + @Override + public void validateArguments() { + if (grpcPort < 0 || grpcPort >= 65536) { + H2O.parseFailed("Invalid port number: " + grpcPort); + } + } + + @Override + public void onLocalNodeStarted() { + if (grpcPort != 0) { + ServerBuilder sb = ServerBuilder.forPort(grpcPort); + RegisterGrpcApi.registerWithServer(sb); + netty = sb.build(); + try { + Log.info("Starting GRPC server on 127.0.0.1:" + grpcPort); + netty.start(); + } catch (IOException e) { + netty = null; + throw new RuntimeException("Failed to start the GRPC server on port " + grpcPort); + } + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + if (netty != null) { + // Use stderr here since the logger may have been reset by its JVM shutdown hook. + System.err.println("*** shutting down gRPC server since JVM is shutting down"); + netty.shutdown(); + System.err.println("*** server shut down"); + } + } + }); + } + } + +} diff --git a/h2o-core/src/main/java/water/api/RegisterGrpcApi.java b/h2o-grpc/src/main/java/ai/h2o/api/RegisterGrpcApi.java similarity index 51% rename from h2o-core/src/main/java/water/api/RegisterGrpcApi.java rename to h2o-grpc/src/main/java/ai/h2o/api/RegisterGrpcApi.java index 20f88f50b13..1210f09f61c 100644 --- a/h2o-core/src/main/java/water/api/RegisterGrpcApi.java +++ b/h2o-grpc/src/main/java/ai/h2o/api/RegisterGrpcApi.java @@ -1,4 +1,4 @@ -package water.api; +package ai.h2o.api; import ai.h2o.api.proto.core.JobService; import io.grpc.ServerBuilder; @@ -6,9 +6,9 @@ /** */ -public abstract class RegisterGrpcApi { +abstract class RegisterGrpcApi { - public static void registerWithServer(ServerBuilder sb) { + static void registerWithServer(ServerBuilder sb) { sb.addService(new JobService()); } } diff --git a/h2o-core/src/main/java/ai/h2o/api/proto/core/GrpcCommon.java b/h2o-grpc/src/main/java/ai/h2o/api/proto/core/GrpcCommon.java similarity index 100% rename from h2o-core/src/main/java/ai/h2o/api/proto/core/GrpcCommon.java rename to h2o-grpc/src/main/java/ai/h2o/api/proto/core/GrpcCommon.java diff --git a/h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java b/h2o-grpc/src/main/java/ai/h2o/api/proto/core/JobService.java similarity index 100% rename from h2o-core/src/main/java/ai/h2o/api/proto/core/JobService.java rename to h2o-grpc/src/main/java/ai/h2o/api/proto/core/JobService.java diff --git a/h2o-core/src/main/proto/core/common.proto b/h2o-grpc/src/main/proto/core/common.proto similarity index 100% rename from h2o-core/src/main/proto/core/common.proto rename to h2o-grpc/src/main/proto/core/common.proto diff --git a/h2o-core/src/main/proto/core/job.proto b/h2o-grpc/src/main/proto/core/job.proto similarity index 100% rename from h2o-core/src/main/proto/core/job.proto rename to h2o-grpc/src/main/proto/core/job.proto diff --git a/h2o-grpc/src/main/resources/META-INF/services/water.AbstractH2OExtension b/h2o-grpc/src/main/resources/META-INF/services/water.AbstractH2OExtension new file mode 100644 index 00000000000..9da5cadc194 --- /dev/null +++ b/h2o-grpc/src/main/resources/META-INF/services/water.AbstractH2OExtension @@ -0,0 +1 @@ +ai.h2o.api.GrpcExtension diff --git a/h2o-py/build.gradle b/h2o-py/build.gradle index 4dea2167f29..ed7e8378210 100644 --- a/h2o-py/build.gradle +++ b/h2o-py/build.gradle @@ -7,7 +7,6 @@ import static java.nio.file.StandardCopyOption.*; defaultTasks 'build_python' description = "H2O Python Package" -GString protoDir = "${projectDir}/h2o/backend/proto" java.lang.String pythonexe = "python" java.lang.String pipexe = "pip" @@ -40,13 +39,6 @@ task makeOutputDir(type: Exec) { } } -task makeProtoOutputDir(type: Exec) { - if (Os.isFamily(Os.FAMILY_WINDOWS)) { - commandLine getOsSpecificCommandLine(['mkdir', protoDir]) - } else { - commandLine getOsSpecificCommandLine(['mkdir', '-p', protoDir]) - } -} task setProjectVersion << { File INIT = new File([T, "h2o", "__init__.py"].join(File.separator)) @@ -65,36 +57,24 @@ task resetProjectVersion << { } task installPythonDependencies(type: Exec) { + doFirst { + standardOutput = new FileOutputStream("build/tmp/h2o-py_installPythonDependencies.out") + } commandLine getOsSpecificCommandLine([ pipexe, "install", "tabulate>=0.7.5", "requests>=2.10", "colorama>=0.3.7", - "future>=0.15.2", - "grpcio-tools>=1.1.0" + "future>=0.15.2" ]) } -task generateProtoFiles(type: Exec) { - dependsOn installPythonDependencies - dependsOn makeProtoOutputDir - commandLine getOsSpecificCommandLine([ - pythonexe, "-m", "grpc_tools.protoc", - "-I${rootDir}/h2o-core/src/main/proto", - "--python_out=${protoDir}", - "--grpc_python_out=${protoDir}", - // TODO: auto-detect the list of files and process them all at once - "${rootDir}/h2o-core/src/main/proto/core/common.proto", - "${rootDir}/h2o-core/src/main/proto/core/job.proto" - ]) -} task buildDist(type: Exec) { dependsOn installPythonDependencies dependsOn setProjectVersion dependsOn makeOutputDir - dependsOn generateProtoFiles doFirst { standardOutput = new FileOutputStream("build/tmp/h2o-py_buildDist.out") diff --git a/settings.gradle b/settings.gradle index 64589daa7c3..be7b2aebed9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,7 @@ include 'h2o-test-accuracy' include 'h2o-avro-parser' include 'h2o-orc-parser' include 'h2o-parquet-parser' +include 'h2o-grpc' // Reconfigure scala projects to support cross compilation // The following code will create two projects for each included item: From ab6e913128b2b4a48b6857c6acc6bfebbbdff6ef Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Wed, 8 Feb 2017 12:03:19 -0800 Subject: [PATCH 10/15] Add a proto for CreateFrame/simple --- h2o-grpc/src/main/proto/frames/create_frame.proto | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 h2o-grpc/src/main/proto/frames/create_frame.proto diff --git a/h2o-grpc/src/main/proto/frames/create_frame.proto b/h2o-grpc/src/main/proto/frames/create_frame.proto new file mode 100644 index 00000000000..efb798cfe26 --- /dev/null +++ b/h2o-grpc/src/main/proto/frames/create_frame.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +import "core/job.proto"; + +option java_package = "ai.h2o.api.proto.frames"; +option java_multiple_files = true; + +package frames; + + +service CreateFrame { + rpc simple (CreateFrameSimpleSpec) returns (JobInfo); +} + + +message CreateFrameSimpleSpec { + enum ResponseType { + NONE = 0; + REAL = 1; + INT = 2; + BOOL = 3; + ENUM = 4; + TIME = 5; + } + + // Id for the frame to be created + string target_id = 0; + + // Seed for the random number generator. Providing same seed should produce exactly same frames + int64 seed = 1; + + // Number of rows + int32 nrows = 2; + + // Number of real-valued columns. Values in these columns will be uniformly distributed between + // real_lb and real_ub + int32 ncols_real = 3; + + // Number of integer columns + int32 ncols_int = 4; + + // Number of categorical (enum) columns + int32 ncols_enum = 5; + + // Number of binary (boolean) columns + int32 ncols_bool = 6; + + // Number of string columns + int32 ncols_str = 7; + + // Number of columns of "time" type + int32 ncols_time = 8; + + double real_lb = 9; + double real_ub = 10; + int32 int_lb = 11; + int32 int_ub = 12; + int32 enum_nlevels = 13; + float bool_p = 14; + int64 time_lb = 15; + int64 time_ub = 16; + int32 str_length = 17; + + // Fraction of missing values + float missing_fraction = 18; + + // Type of the response column to add + ResponseType response_type = 19; + + // Lower bound for the response variable (real/int/time types) + double response_lb = 20; + + // Upper bound for the response variable (real/int/time types) + double response_ub = 21; + + // Frequency of 1s for the bool (binary) response column + double response_p = 22; + + // Number of categorical levels for the enum response column + int32 response_nlevels = 23; +} From cdbd71a4b692b6280624539f2050fbb61459d2c9 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Wed, 8 Feb 2017 14:21:03 -0800 Subject: [PATCH 11/15] Do not bundle GRPC plugin with the main repository, instead put it into a separate assembly --- build.gradle | 2 +- gradle/jacoco.gradle | 4 +- {h2o-assembly => h2o-assemblies/main}/README.md | 0 {h2o-assembly => h2o-assemblies/main}/build.gradle | 5 +- h2o-assemblies/py2o/build.gradle | 58 ++++++++++++++++++++++ h2o-bindings/build.gradle | 2 +- h2o-docs/build.gradle | 2 +- .../main/proto/{frames => core}/create_frame.proto | 52 +++++++++---------- h2o-py/build.gradle | 2 +- h2o-r/build.gradle | 2 +- h2o-test-integ/build.gradle | 2 +- settings.gradle | 3 +- 12 files changed, 96 insertions(+), 38 deletions(-) rename {h2o-assembly => h2o-assemblies/main}/README.md (100%) rename {h2o-assembly => h2o-assemblies/main}/build.gradle (94%) create mode 100644 h2o-assemblies/py2o/build.gradle rename h2o-grpc/src/main/proto/{frames => core}/create_frame.proto (65%) diff --git a/build.gradle b/build.gradle index 22c029b0249..3bdb9d6a379 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ plugins { // files with the right settings. // // The top-level jar file that gets produced is empty and not usable -// for anything. Use the jar file produced by the h2o-assembly subproject. +// for anything. Use the jar file produced by the :h2o-assemblies:main subproject. // apply from: 'gradle/java.gradle' diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index d49ef228d3f..16f7e886576 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -57,7 +57,7 @@ jacocoTestReport { } // Collect class files - FileTree classLocation = fileTree(dir: "$rootDir", include: "**/build/libs/**/*.jar", excludes: ["build/", "h2o-assembly/"]) + FileTree classLocation = fileTree(dir: "$rootDir", include: "**/build/libs/**/*.jar", excludes: ["build/", "h2o-assemblies/"]) classDirectories = classLocation reports { @@ -75,4 +75,4 @@ task cleanCoverageData (type: Delete) { delete file("${buildDir}/reports/jacoco/report.exec") } -jacocoTestReport.dependsOn jacocoMergeExecs \ No newline at end of file +jacocoTestReport.dependsOn jacocoMergeExecs diff --git a/h2o-assembly/README.md b/h2o-assemblies/main/README.md similarity index 100% rename from h2o-assembly/README.md rename to h2o-assemblies/main/README.md diff --git a/h2o-assembly/build.gradle b/h2o-assemblies/main/build.gradle similarity index 94% rename from h2o-assembly/build.gradle rename to h2o-assemblies/main/build.gradle index 041c3486710..eae2d8b358a 100644 --- a/h2o-assembly/build.gradle +++ b/h2o-assemblies/main/build.gradle @@ -14,7 +14,6 @@ configurations { // Dependencies dependencies { compile project(":h2o-app") - compile project(":h2o-grpc") compile project(":h2o-persist-s3") compile project(":h2o-persist-hdfs") if (project.hasProperty("doIncludeOrc") && project.doIncludeOrc == "true") { @@ -52,14 +51,14 @@ artifacts { // project build directory // -def assembly = "h2o-assembly.jar" +def assembly = "main.jar" def allInOne = "h2o.jar" task copyJar(type: Copy) { from ("${project.buildDir}/libs"){ include assembly } - into "${project.parent.buildDir}" + into "${project.parent.parent.buildDir}" rename { it.replace(assembly, allInOne) } } // Execute always copyJar diff --git a/h2o-assemblies/py2o/build.gradle b/h2o-assemblies/py2o/build.gradle new file mode 100644 index 00000000000..3a66c09debe --- /dev/null +++ b/h2o-assemblies/py2o/build.gradle @@ -0,0 +1,58 @@ +apply plugin: 'java' +apply plugin: 'com.github.johnrengelman.shadow' + +description = "H2O Core Java library for use with python" + +// Exclude unwanted dependencies +configurations { + compile.exclude module: 'junit' + compile.exclude module: 'mockito-all' + compile.exclude module: 'zookeeper' + compile.exclude module: "javax.mail.glassfish" +} + +// Dependencies +dependencies { + compile project(":h2o-app") + compile project(":h2o-grpc") + compile "org.slf4j:slf4j-log4j12:1.7.5" +} + +shadowJar { + mergeServiceFiles() + classifier = '' + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.SF' + exclude 'synchronize.properties' + exclude 'uploader.properties' + exclude 'test.properties' + exclude 'cockpitlite.properties' + exclude 'devpay_products.properties' + manifest { + attributes 'Main-Class': 'water.H2OApp' + } +} + +artifacts { + archives shadowJar +} + +// +// Support make infrastructure by copying the resulting assembly into parent +// project build directory +// + +def assembly = "py2o.jar" +def allInOne = "h2o-py.jar" + +task copyJar(type: Copy) { + from ("${project.buildDir}/libs"){ + include assembly + } + into "${project.parent.parent.buildDir}" + rename { it.replace(assembly, allInOne) } +} +// Execute always copyJar +shadowJar.finalizedBy copyJar +// Run shadowJar as par of build +jar.finalizedBy shadowJar diff --git a/h2o-bindings/build.gradle b/h2o-bindings/build.gradle index 04cbcc0627e..703a2091e1a 100644 --- a/h2o-bindings/build.gradle +++ b/h2o-bindings/build.gradle @@ -20,7 +20,7 @@ dependencies { testCompile "junit:junit:${junitVersion}" // Generator dependencies - srcGenCompile project( path: ":h2o-assembly", configuration: "shadow") + srcGenCompile project(path: ":h2o-assemblies:main", configuration: "shadow") } // Configure idea import diff --git a/h2o-docs/build.gradle b/h2o-docs/build.gradle index e5a79460e71..591b941f1d8 100644 --- a/h2o-docs/build.gradle +++ b/h2o-docs/build.gradle @@ -3,7 +3,7 @@ description = "H2O Documentation" apply plugin: 'java' dependencies { - compile project(":h2o-assembly") + compile project(":h2o-assemblies:main") } diff --git a/h2o-grpc/src/main/proto/frames/create_frame.proto b/h2o-grpc/src/main/proto/core/create_frame.proto similarity index 65% rename from h2o-grpc/src/main/proto/frames/create_frame.proto rename to h2o-grpc/src/main/proto/core/create_frame.proto index efb798cfe26..69ad8a91ce1 100644 --- a/h2o-grpc/src/main/proto/frames/create_frame.proto +++ b/h2o-grpc/src/main/proto/core/create_frame.proto @@ -5,7 +5,7 @@ import "core/job.proto"; option java_package = "ai.h2o.api.proto.frames"; option java_multiple_files = true; -package frames; +package core; service CreateFrame { @@ -24,58 +24,58 @@ message CreateFrameSimpleSpec { } // Id for the frame to be created - string target_id = 0; + string target_id = 1; // Seed for the random number generator. Providing same seed should produce exactly same frames - int64 seed = 1; + int64 seed = 2; // Number of rows - int32 nrows = 2; + int32 nrows = 3; // Number of real-valued columns. Values in these columns will be uniformly distributed between // real_lb and real_ub - int32 ncols_real = 3; + int32 ncols_real = 4; // Number of integer columns - int32 ncols_int = 4; + int32 ncols_int = 5; // Number of categorical (enum) columns - int32 ncols_enum = 5; + int32 ncols_enum = 6; // Number of binary (boolean) columns - int32 ncols_bool = 6; + int32 ncols_bool = 7; // Number of string columns - int32 ncols_str = 7; + int32 ncols_str = 8; // Number of columns of "time" type - int32 ncols_time = 8; - - double real_lb = 9; - double real_ub = 10; - int32 int_lb = 11; - int32 int_ub = 12; - int32 enum_nlevels = 13; - float bool_p = 14; - int64 time_lb = 15; - int64 time_ub = 16; - int32 str_length = 17; + int32 ncols_time = 9; + + double real_lb = 10; + double real_ub = 11; + int32 int_lb = 12; + int32 int_ub = 13; + int32 enum_nlevels = 14; + float bool_p = 15; + int64 time_lb = 16; + int64 time_ub = 17; + int32 str_length = 18; // Fraction of missing values - float missing_fraction = 18; + float missing_fraction = 19; // Type of the response column to add - ResponseType response_type = 19; + ResponseType response_type = 20; // Lower bound for the response variable (real/int/time types) - double response_lb = 20; + double response_lb = 21; // Upper bound for the response variable (real/int/time types) - double response_ub = 21; + double response_ub = 22; // Frequency of 1s for the bool (binary) response column - double response_p = 22; + double response_p = 23; // Number of categorical levels for the enum response column - int32 response_nlevels = 23; + int32 response_nlevels = 24; } diff --git a/h2o-py/build.gradle b/h2o-py/build.gradle index ed7e8378210..e8932b0d4e4 100644 --- a/h2o-py/build.gradle +++ b/h2o-py/build.gradle @@ -16,7 +16,7 @@ if (System.env.VIRTUAL_ENV) { } dependencies { - compile project(":h2o-assembly") + compile project(":h2o-assemblies:main") } static List getOsSpecificCommandLine(List args) { diff --git a/h2o-r/build.gradle b/h2o-r/build.gradle index 1f7d629378a..594bb7e4970 100644 --- a/h2o-r/build.gradle +++ b/h2o-r/build.gradle @@ -12,7 +12,7 @@ description = "H2O R Package" //apply plugin: 'water.gradle.plugins.manageLocalClouds' dependencies { - compile project(":h2o-assembly") + compile project(":h2o-assemblies:main") } def getOS() { diff --git a/h2o-test-integ/build.gradle b/h2o-test-integ/build.gradle index b640254db7e..b0c84fb6e62 100644 --- a/h2o-test-integ/build.gradle +++ b/h2o-test-integ/build.gradle @@ -1,7 +1,7 @@ description = "H2O Integration Testing" dependencies { - compile project(":h2o-assembly") + compile project(":h2o-assemblies:main") } def runner = new File("$rootDir/scripts/run.py").canonicalPath diff --git a/settings.gradle b/settings.gradle index be7b2aebed9..732b19020c7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,8 @@ include 'h2o-web' include 'h2o-app' include 'h2o-r' include 'h2o-py' -include 'h2o-assembly' +include 'h2o-assemblies:main' +include 'h2o-assemblies:py2o' include 'h2o-persist-hdfs' include 'h2o-persist-s3' include 'h2o-docs' From 81ec6fbd4d33ddbd8f12992f579cf8a1ab1d52da Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Wed, 8 Feb 2017 14:41:22 -0800 Subject: [PATCH 12/15] Do not include Flow / Avro parser with the py h2o assembly --- h2o-app/build.gradle | 4 +--- h2o-assemblies/main/build.gradle | 18 ++++++++++-------- h2o-bindings/build.gradle | 2 ++ h2o-hadoop/assemblyjar.gradle | 2 ++ h2o-hadoop/driverjar.gradle | 2 ++ h2o-hadoop/h2o-mapreduce-generic/build.gradle | 2 ++ h2o-hadoop/h2o-yarn-generic/build.gradle | 2 ++ 7 files changed, 21 insertions(+), 11 deletions(-) diff --git a/h2o-app/build.gradle b/h2o-app/build.gradle index 263a15f3d06..2fa3c8944b0 100644 --- a/h2o-app/build.gradle +++ b/h2o-app/build.gradle @@ -4,12 +4,10 @@ description = "H2O Application Runner" dependencies { - compile project(":h2o-web") compile project(":h2o-algos") compile project(":h2o-core") compile project(":h2o-genmodel") - compile project(":h2o-avro-parser") - // Note: orc parser is included at the assembly level for each + // Note: orc parser is included at the assembly level for each // Hadoop distribution } diff --git a/h2o-assemblies/main/build.gradle b/h2o-assemblies/main/build.gradle index eae2d8b358a..3ac36cba7c8 100644 --- a/h2o-assemblies/main/build.gradle +++ b/h2o-assemblies/main/build.gradle @@ -13,14 +13,16 @@ configurations { // Dependencies dependencies { - compile project(":h2o-app") - compile project(":h2o-persist-s3") - compile project(":h2o-persist-hdfs") - if (project.hasProperty("doIncludeOrc") && project.doIncludeOrc == "true") { - compile project(":h2o-orc-parser") - } - compile project(":h2o-parquet-parser") - compile "org.slf4j:slf4j-log4j12:1.7.5" + compile project(":h2o-app") + compile project(":h2o-web") + compile project(":h2o-avro-parser") + compile project(":h2o-persist-s3") + compile project(":h2o-persist-hdfs") + if (project.hasProperty("doIncludeOrc") && project.doIncludeOrc == "true") { + compile project(":h2o-orc-parser") + } + compile project(":h2o-parquet-parser") + compile "org.slf4j:slf4j-log4j12:1.7.5" } shadowJar { diff --git a/h2o-bindings/build.gradle b/h2o-bindings/build.gradle index 703a2091e1a..61e2abcb184 100644 --- a/h2o-bindings/build.gradle +++ b/h2o-bindings/build.gradle @@ -17,6 +17,8 @@ dependencies { compile 'com.squareup.okio:okio:1.8.0' testCompile project(":h2o-app") + testCompile project(":h2o-web") + testCompile project(":h2o-avro-parser") testCompile "junit:junit:${junitVersion}" // Generator dependencies diff --git a/h2o-hadoop/assemblyjar.gradle b/h2o-hadoop/assemblyjar.gradle index ae5ee01d426..d47bef8910e 100644 --- a/h2o-hadoop/assemblyjar.gradle +++ b/h2o-hadoop/assemblyjar.gradle @@ -15,6 +15,8 @@ dependencies { } compile project(':h2o-hadoop:h2o-' + hadoopVersion) compile project(':h2o-app') + compile project(":h2o-web") + compile project(":h2o-avro-parser") // Include S3 persist layer compile(project(":h2o-persist-s3")) // Include HDFS persist layer diff --git a/h2o-hadoop/driverjar.gradle b/h2o-hadoop/driverjar.gradle index 21d00efc295..560bda5ec54 100644 --- a/h2o-hadoop/driverjar.gradle +++ b/h2o-hadoop/driverjar.gradle @@ -30,5 +30,7 @@ dependencies { compile('org.apache.hadoop:hadoop-client:' + hadoopMavenArtifactVersion) } compile project(':h2o-app') + compile project(":h2o-web") + compile project(":h2o-avro-parser") } diff --git a/h2o-hadoop/h2o-mapreduce-generic/build.gradle b/h2o-hadoop/h2o-mapreduce-generic/build.gradle index da5b7e9bbc2..d561dc64667 100644 --- a/h2o-hadoop/h2o-mapreduce-generic/build.gradle +++ b/h2o-hadoop/h2o-mapreduce-generic/build.gradle @@ -17,4 +17,6 @@ compileJava { dependencies { compile('org.apache.hadoop:hadoop-client:' + hadoopMavenArtifactVersion) compile project(':h2o-app') + compile project(":h2o-web") + compile project(":h2o-avro-parser") } diff --git a/h2o-hadoop/h2o-yarn-generic/build.gradle b/h2o-hadoop/h2o-yarn-generic/build.gradle index c438e459407..2e663683dd1 100644 --- a/h2o-hadoop/h2o-yarn-generic/build.gradle +++ b/h2o-hadoop/h2o-yarn-generic/build.gradle @@ -17,4 +17,6 @@ compileJava { dependencies { compile('org.apache.hadoop:hadoop-client:' + hadoopMavenArtifactVersion) compile project(':h2o-app') + compile project(":h2o-web") + compile project(":h2o-avro-parser") } From dbd90cdcc73376bcb047c969640da968cb8f92bd Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Wed, 8 Feb 2017 15:12:57 -0800 Subject: [PATCH 13/15] Localize protobuf build dependency to h2o-grpc submodule; h2o-py does not depend on grpcio yet --- build.gradle | 1 - h2o-core/build.gradle | 5 ----- h2o-grpc/build.gradle | 13 +++++++++++++ h2o-py/conda/h2o/meta.yaml | 2 -- h2o-py/setup.py | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 3bdb9d6a379..159f32cbcee 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,6 @@ buildscript { plugins { id "java" - id "com.google.protobuf" version "0.8.0" } // diff --git a/h2o-core/build.gradle b/h2o-core/build.gradle index a6525bd2425..d16274249b9 100644 --- a/h2o-core/build.gradle +++ b/h2o-core/build.gradle @@ -106,11 +106,6 @@ task cleanBuildVersionJava(type: Delete) { delete buildVersionFile } -task cleanGeneratedProtoClasses(type: Delete) { - delete protobuf.generatedFilesBaseDir -} - -clean.dependsOn cleanGeneratedProtoClasses clean.dependsOn cleanBuildVersionJava apply from: '../gradle/javaIgnoreSymbolFile.gradle' diff --git a/h2o-grpc/build.gradle b/h2o-grpc/build.gradle index 9fd87811983..d34bdd4f2e8 100644 --- a/h2o-grpc/build.gradle +++ b/h2o-grpc/build.gradle @@ -1,5 +1,8 @@ import org.apache.tools.ant.taskdefs.condition.Os +plugins { + id "com.google.protobuf" version "0.8.0" +} apply plugin: 'java' apply plugin: 'com.google.protobuf' @@ -116,3 +119,13 @@ idea { } } + +//---------------------------------------------------------------------------------------------------------------------- +// Clean up +//---------------------------------------------------------------------------------------------------------------------- + +task cleanGeneratedProtoClasses(type: Delete) { + delete protobuf.generatedFilesBaseDir +} + +clean.dependsOn cleanGeneratedProtoClasses diff --git a/h2o-py/conda/h2o/meta.yaml b/h2o-py/conda/h2o/meta.yaml index 4ae145f6417..522813f4d2d 100644 --- a/h2o-py/conda/h2o/meta.yaml +++ b/h2o-py/conda/h2o/meta.yaml @@ -15,7 +15,6 @@ requirements: - future >=0.15.2 - tabulate >=0.7.5 - requests >=2.10 - - grpcio-tools >= 1.1.0 run: - python >=2.7,<3|>=3.5 @@ -23,7 +22,6 @@ requirements: - future >=0.15.2 - tabulate >=0.7.5 - requests >=2.10 - - grpcio >= 1.1.0 about: home: https://github.com/h2oai/h2o-3.git diff --git a/h2o-py/setup.py b/h2o-py/setup.py index c2646522d41..22424241995 100644 --- a/h2o-py/setup.py +++ b/h2o-py/setup.py @@ -91,5 +91,5 @@ ]}, # run-time dependencies - install_requires=["requests", "tabulate", "future", "colorama", "grpcio-tools"], + install_requires=["requests", "tabulate", "future", "colorama"], ) From 7e51beda6f85e69cf92314d8080843c9a16d3bd1 Mon Sep 17 00:00:00 2001 From: mmalohlava Date: Wed, 8 Feb 2017 16:50:24 -0800 Subject: [PATCH 14/15] Fix README to point to right Gradle task. --- h2o-assemblies/main/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h2o-assemblies/main/README.md b/h2o-assemblies/main/README.md index ee1edd49cfa..e752ee05d42 100644 --- a/h2o-assemblies/main/README.md +++ b/h2o-assemblies/main/README.md @@ -7,6 +7,6 @@ with H2O. ## Building ``` -./gradlew :h2o-assembly:build +./gradlew :h2o-assemblies:main:build ``` From d5d7e62cd1e29ffd480cef9367c065884870d7b8 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Wed, 8 Feb 2017 17:21:10 -0800 Subject: [PATCH 15/15] Implement CreateFrame grpc endpoint in java --- .../java/hex/createframe/CreateFrameRecipe.java | 4 +- .../java/ai/h2o/api/proto/core/JobService.java | 6 +-- .../h2o/api/proto/frames/CreateFrameService.java | 55 ++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 h2o-grpc/src/main/java/ai/h2o/api/proto/frames/CreateFrameService.java diff --git a/h2o-core/src/main/java/hex/createframe/CreateFrameRecipe.java b/h2o-core/src/main/java/hex/createframe/CreateFrameRecipe.java index f14f2bea291..d28af6c1262 100644 --- a/h2o-core/src/main/java/hex/createframe/CreateFrameRecipe.java +++ b/h2o-core/src/main/java/hex/createframe/CreateFrameRecipe.java @@ -13,8 +13,8 @@ * Base class for all frame creation recipes. */ public abstract class CreateFrameRecipe> extends Iced { - protected Key dest; - protected long seed = -1; + public Key dest; + public long seed = -1; //-------------------------------------------------------------------------------------------------------------------- // Inheritance interface diff --git a/h2o-grpc/src/main/java/ai/h2o/api/proto/core/JobService.java b/h2o-grpc/src/main/java/ai/h2o/api/proto/core/JobService.java index 54c37b7f4c4..682215391a6 100644 --- a/h2o-grpc/src/main/java/ai/h2o/api/proto/core/JobService.java +++ b/h2o-grpc/src/main/java/ai/h2o/api/proto/core/JobService.java @@ -36,10 +36,10 @@ public void cancel(JobId request, StreamObserver responseObserver) { //-------------------------------------------------------------------------------------------------------------------- - // Private + // Helpers //-------------------------------------------------------------------------------------------------------------------- - private water.Job resolveJob(JobId request) { + private static water.Job resolveJob(JobId request) { String strId = request.getJobId(); Value val = DKV.get(Key.make(strId)); if (val == null) { @@ -54,7 +54,7 @@ public void cancel(JobId request, StreamObserver responseObserver) { } - private JobInfo fillJobInfo(water.Job job) { + public static JobInfo fillJobInfo(water.Job job) { JobInfo.Builder jb = JobInfo.newBuilder(); jb.setJobId(job._key.toString()) .setProgress(job.progress()) diff --git a/h2o-grpc/src/main/java/ai/h2o/api/proto/frames/CreateFrameService.java b/h2o-grpc/src/main/java/ai/h2o/api/proto/frames/CreateFrameService.java new file mode 100644 index 00000000000..9aa00e80043 --- /dev/null +++ b/h2o-grpc/src/main/java/ai/h2o/api/proto/frames/CreateFrameService.java @@ -0,0 +1,55 @@ +package ai.h2o.api.proto.frames; + + +import ai.h2o.api.proto.core.GrpcCommon; +import ai.h2o.api.proto.core.JobInfo; +import ai.h2o.api.proto.core.JobService; +import hex.createframe.recipes.SimpleCreateFrameRecipe; +import io.grpc.stub.StreamObserver; +import water.Job; +import water.Key; +import water.fvec.Frame; + +/** + */ +public class CreateFrameService extends CreateFrameGrpc.CreateFrameImplBase { + + @Override + public void simple(CreateFrameSimpleSpec request, StreamObserver responseObserver) { + try { + SimpleCreateFrameRecipe cf = new SimpleCreateFrameRecipe(); + cf.dest = Key.make(request.getTargetId()); + cf.seed = request.getSeed(); + cf.nrows = request.getNrows(); + cf.ncols_real = request.getNcolsReal(); + cf.ncols_int = request.getNcolsInt(); + cf.ncols_enum = request.getNcolsEnum(); + cf.ncols_bool = request.getNcolsBool(); + cf.ncols_str = request.getNcolsStr(); + cf.ncols_time = request.getNcolsTime(); + cf.real_lb = request.getRealLb(); + cf.real_ub = request.getRealUb(); + cf.int_lb = request.getIntLb(); + cf.int_ub = request.getIntUb(); + cf.enum_nlevels = request.getEnumNlevels(); + cf.bool_p = request.getBoolP(); + cf.time_lb = request.getTimeLb(); + cf.time_ub = request.getTimeUb(); + cf.str_length = request.getStrLength(); + cf.missing_fraction = request.getMissingFraction(); + cf.response_type = SimpleCreateFrameRecipe.ResponseType.valueOf(request.getResponseType().toString()); + cf.response_lb = request.getResponseLb(); + cf.response_ub = request.getResponseUb(); + cf.response_p = request.getResponseP(); + cf.response_nlevels = request.getResponseNlevels(); + + Job job = cf.exec(); + responseObserver.onNext(JobService.fillJobInfo(job)); + responseObserver.onCompleted(); + + } catch (Throwable ex) { + GrpcCommon.sendError(ex, responseObserver, JobInfo.class); + } + } + +}