Repository: bentoml/BentoML

Issue:
When I built a H2O-based image, the following error occurred
**Describe the bug**
When I built a H2O-based image, the following error occurred. From the Dockerfile generated by BentoML, the base image is bentoml/model-server:0.8.1, and JRE is indeed not installed


I used service.py to build a bentoML bundle
```python
import bentoml
from bentoml.adapters import DataframeInput
from bentoml.artifact import H2oModelArtifact
from customize import preprocessing, postprocessing


@bentoml.env(auto_pip_dependencies=True)
@bentoml.artifacts([H2oModelArtifact('model')])
class LoanDefaultPrediction(bentoml.BentoService):
    @bentoml.api(input=DataframeInput())
    def predict(self, df):
        model_input = preprocessing(df)
        model_output = self.artifacts.model.predict(model_input)
        return postprocessing(model_output)


if __name__ == "__main__":
    import h2o
    h2o.init(strict_version_check=False)
    model = h2o.load_model('H2O_AutoML_20200720_153457')

    model_service = LoanDefaultPrediction()
    model_service.pack("model", model)

    saved_path = model_service.save()
```
And another customize.py to pre process
```python
import h2o

def preprocessing(model_input):
    return h2o.H2OFrame(model_input, na_strings=['NaN'])


def postprocessing(model_output):
    return model_output.as_data_frame()
```

Finally, After I build service image with this bundle, I run this docker image with below log:
```
Traceback (most recent call last):
  File "/opt/conda/lib/python3.7/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
    worker.init_process()
  File "/opt/conda/lib/python3.7/site-packages/gunicorn/workers/base.py", line 119, in init_process
    self.load_wsgi()
  File "/opt/conda/lib/python3.7/site-packages/gunicorn/workers/base.py", line 144, in load_wsgi
    self.wsgi = self.app.wsgi()
  File "/opt/conda/lib/python3.7/site-packages/gunicorn/app/base.py", line 67, in wsgi
    self.callable = self.load()
  File "/opt/conda/lib/python3.7/site-packages/bentoml/server/gunicorn_server.py", line 92, in load
    bento_service = load(self.bento_service_bundle_path)
  File "/opt/conda/lib/python3.7/site-packages/bentoml/saved_bundle/loader.py", line 179, in load
    svc = svc_cls()
  File "/opt/conda/lib/python3.7/site-packages/bentoml/service.py", line 526, in __init__
    self._load_artifacts(self._bento_service_bundle_path)
  File "/opt/conda/lib/python3.7/site-packages/bentoml/service.py", line 734, in _load_artifacts
    packed_artifact = artifact.load(artifacts_path)
  File "/opt/conda/lib/python3.7/site-packages/bentoml/artifact/h2o_model_artifact.py", line 79, in load
    h2o.init()
  File "/opt/conda/lib/python3.7/site-packages/h2o/h2o.py", line 307, in init
    bind_to_localhost=bind_to_localhost)
  File "/opt/conda/lib/python3.7/site-packages/h2o/backend/server.py", line 143, in start
    bind_to_localhost=bind_to_localhost, log_dir=log_dir, log_level=log_level, max_log_file_size=max_log_file_size)
  File "/opt/conda/lib/python3.7/site-packages/h2o/backend/server.py", line 276, in _launch_server
    java = self._find_java()
  File "/opt/conda/lib/python3.7/site-packages/h2o/backend/server.py", line 441, in _find_java
    raise H2OStartupError("Cannot find Java. Please install the latest JRE from\n"
h2o.exceptions.H2OStartupError: Cannot find Java. Please install the latest JRE from
http://www.oracle.com/technetwork/java/javase/downloads/index.html
```


Discussion:
Hi @hechangqin - you will need to add h2o conda dependency for the docker build to install h2o properly. You may refer to the h2o example notebooks found in the BentoML gallery: https://github.com/bentoml/gallery#h2o

e.g.:
```python
import h2o

from bentoml import api, env, artifacts, BentoService
from bentoml.artifact import H2oModelArtifact
from bentoml.adapters import DataframeInput

@env(
    pip_dependencies=['h2o==3.24.0.2', 'pandas'],
    conda_channels=['h2oai'],
    conda_dependencies=['h2o==3.24.0.2']
)
@artifacts([H2oModelArtifact('model')])
class LoanPrediction(BentoService):
    
    @api(input=DataframeInput())
    def predict(self, df):
        h2o_frame = h2o.H2OFrame(df, na_strings=['NaN'])
        predictions = self.artifacts.model.predict(h2o_frame)
        return predictions.as_data_frame()
```

Related documentation on conda dependencies:
https://docs.bentoml.org/en/latest/concepts.html#conda-packages
@parano I add h2o conda dependency for the docker build and the problem still exists. The Dockerfile generated by BentoML as below. I think it is caused by the absence of jdk in the **bentoml/model-server:0.8.1** base image.
```
FROM bentoml/model-server:0.8.1

# copy over model files
COPY . /bento
WORKDIR /bento

# Configuring PyPI index
ARG PIP_INDEX_URL=https://pypi.python.org/simple/
ARG PIP_TRUSTED_HOST=pypi.python.org
ENV PIP_INDEX_URL $PIP_INDEX_URL
ENV PIP_TRUSTED_HOST $PIP_TRUSTED_HOST

# Install conda, pip dependencies and run user defined setup script
RUN if [ -f /bento/bentoml-init.sh ]; then bash -c /bento/bentoml-init.sh; fi

# the env var $PORT is required by heroku container runtime
ENV PORT 5000
EXPOSE $PORT

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT [ "docker-entrypoint.sh" ]
CMD ["bentoml", "serve-gunicorn", "/bento"]

```
Error log as below:
```
[2020-07-22 02:04:07 +0000] [14] [ERROR] Exception in worker process
Traceback (most recent call last):
  File "/opt/conda/lib/python3.7/site-packages/h2o/h2o.py", line 266, in init
    _msgs=("Checking whether there is an H2O instance running at {url} ",
  File "/opt/conda/lib/python3.7/site-packages/h2o/backend/connection.py", line 323, in open
    conn._cluster = conn._test_connection(retries, messages=_msgs)
  File "/opt/conda/lib/python3.7/site-packages/h2o/backend/connection.py", line 603, in _test_connection
    % (self._base_url, max_retries, "\n".join(errors)))
h2o.exceptions.H2OConnectionError: Could not establish link to the H2O cloud http://localhost:54321 after 5 retries
[04:06.36] H2OConnectionError: Unexpected HTTP error: HTTPConnectionPool(host='localhost', port=54321): Max retries exceeded with url: /3/Cloud (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7fa02c5d9850>: Failed to establish a new connection: [Errno 111] Connection refused'))
[04:06.56] H2OConnectionError: Unexpected HTTP error: HTTPConnectionPool(host='localhost', port=54321): Max retries exceeded with url: /3/Cloud (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7fa02c5d9610>: Failed to establish a new connection: [Errno 111] Connection refused'))
[04:06.76] H2OConnectionError: Unexpected HTTP error: HTTPConnectionPool(host='localhost', port=54321): Max retries exceeded with url: /3/Cloud (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7fa02c291bd0>: Failed to establish a new connection: [Errno 111] Connection refused'))
[04:06.97] H2OConnectionError: Unexpected HTTP error: HTTPConnectionPool(host='localhost', port=54321): Max retries exceeded with url: /3/Cloud (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7fa02c2bb510>: Failed to establish a new connection: [Errno 111] Connection refused'))
[04:07.17] H2OConnectionError: Unexpected HTTP error: HTTPConnectionPool(host='localhost', port=54321): Max retries exceeded with url: /3/Cloud (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7fa02c242310>: Failed to establish a new connection: [Errno 111] Connection refused'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/conda/lib/python3.7/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
    worker.init_process()
  File "/opt/conda/lib/python3.7/site-packages/gunicorn/workers/base.py", line 119, in init_process
    self.load_wsgi()
  File "/opt/conda/lib/python3.7/site-packages/gunicorn/workers/base.py", line 144, in load_wsgi
    self.wsgi = self.app.wsgi()
  File "/opt/conda/lib/python3.7/site-packages/gunicorn/app/base.py", line 67, in wsgi
    self.callable = self.load()
  File "/opt/conda/lib/python3.7/site-packages/bentoml/server/gunicorn_server.py", line 92, in load
    bento_service = load(self.bento_service_bundle_path)
  File "/opt/conda/lib/python3.7/site-packages/bentoml/saved_bundle/loader.py", line 179, in load
    svc = svc_cls()
  File "/opt/conda/lib/python3.7/site-packages/bentoml/service.py", line 526, in __init__
    self._load_artifacts(self._bento_service_bundle_path)
  File "/opt/conda/lib/python3.7/site-packages/bentoml/service.py", line 734, in _load_artifacts
    packed_artifact = artifact.load(artifacts_path)
  File "/opt/conda/lib/python3.7/site-packages/bentoml/artifact/h2o_model_artifact.py", line 79, in load
    h2o.init()
  File "/opt/conda/lib/python3.7/site-packages/h2o/h2o.py", line 279, in init
    bind_to_localhost=bind_to_localhost)
  File "/opt/conda/lib/python3.7/site-packages/h2o/backend/server.py", line 138, in start
    bind_to_localhost=bind_to_localhost, log_dir=log_dir, log_level=log_level)
  File "/opt/conda/lib/python3.7/site-packages/h2o/backend/server.py", line 271, in _launch_server
    java = self._find_java()
  File "/opt/conda/lib/python3.7/site-packages/h2o/backend/server.py", line 410, in _find_java
    raise H2OStartupError("Cannot find Java. Please install the latest JRE from\n"
h2o.exceptions.H2OStartupError: Cannot find Java. Please install the latest JRE from
http://www.oracle.com/technetwork/java/javase/downloads/index.html
```
@hechangqin could you share the `environment.yml` file in your saved bundle directory?
@parano As below
```
name: bentoml-LoanDefaultPrediction
channels:
- defaults
- h2oai
dependencies:
- python=3.7.6
- pip
- h2o==3.24.0.2

```
@hechangqin I had a chance to look into this today.  We need to add `openjdk` as part of the `condo_dependencies`.  I will update the h2o notebook to correct this issue.

@yubozhao OK. Thank you very much. If you have done this job, please share me to know.
@yubozhao any updates on this?

Btw, with the new `set_dependencies` API in `BentoArtifact` class, we should be able to add the conda dependencies for the user by default, when using H2O artifact.
@parano  ~~I can't wait for the new `set_dependencies` API, it would make H2o Artifact a lot more user friendly to use.~~

~~For now, the quickest work around is user manually add `openjdk` as part of the `conda_dependencies` when they write the BentoService definition.~~

I am also going to create a PR to add this in

Resolving patch:
diff --git a/bentoml/artifact/h2o_model_artifact.py b/bentoml/artifact/h2o_model_artifact.py
--- a/bentoml/artifact/h2o_model_artifact.py
+++ b/bentoml/artifact/h2o_model_artifact.py
@@ -66,6 +66,7 @@ def __init__(self, name):
 
     def set_dependencies(self, env: BentoServiceEnv):
         env.add_pip_dependencies_if_missing(['h2o'])
+        env.add_conda_dependencies(['openjdk'])
 
     def _model_file_path(self, base_path):
         return os.path.join(base_path, self.name)


Test patch:
diff --git a/tests/bento_service_examples/h2o_service.py b/tests/bento_service_examples/h2o_service.py
new file mode 100644
--- /dev/null
+++ b/tests/bento_service_examples/h2o_service.py
@@ -0,0 +1,16 @@
+import h2o  # pylint: disable=import-error
+
+import bentoml
+from bentoml.artifact import H2oModelArtifact
+from bentoml.adapters import DataframeInput
+
+
+@bentoml.env(auto_pip_dependencies=True)
+@bentoml.artifacts([H2oModelArtifact('model')])
+class H2oExampleBentoService(bentoml.BentoService):
+    @bentoml.api(input=DataframeInput())
+    def predict(self, df):
+        hf = h2o.H2OFrame(df)
+        predictions = self.artifacts.model.predict(hf)
+        result = predictions.as_data_frame().to_json(orient='records')
+        return result
diff --git a/tests/integration/test_h2o_model_artifact.py b/tests/integration/test_h2o_model_artifact.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/test_h2o_model_artifact.py
@@ -0,0 +1,115 @@
+import time
+import json
+import pytest
+
+import bentoml
+from tests.bento_service_examples.h2o_service import H2oExampleBentoService
+from tests.integration.api_server.conftest import _wait_until_ready
+
+test_data = {
+    "TemperatureCelcius": {"0": 21.6},
+    "ExhaustVacuumHg": {"0": 62.52},
+    "AmbientPressureMillibar": {"0": 1017.23},
+    "RelativeHumidity": {"0": 67.87},
+}
+
+
+@pytest.fixture(scope="module")
+def h2o_svc():
+    import h2o
+    from h2o.automl import H2OAutoML
+
+    h2o.init()
+
+    df = h2o.import_file(
+        "https://github.com/yubozhao/bentoml-h2o-data-for-testing/raw/master/"
+        "powerplant_output.csv"
+    )
+    splits = df.split_frame(ratios=[0.8], seed=1)
+    train = splits[0]
+    test = splits[1]
+
+    aml = H2OAutoML(max_runtime_secs=60, seed=1, project_name="powerplant_lb_frame")
+    aml.train(y="HourlyEnergyOutputMW", training_frame=train, leaderboard_frame=test)
+
+    svc = H2oExampleBentoService()
+    svc.pack('model', aml.leader)
+
+    return svc
+
+
+@pytest.fixture(scope="module")
+def h2o_svc_saved_dir(tmp_path_factory, h2o_svc):
+    tmpdir = str(tmp_path_factory.mktemp("h2o_svc"))
+    h2o_svc.save_to_dir(tmpdir)
+    return tmpdir
+
+
+@pytest.fixture()
+def h2o_svc_loaded(h2o_svc_saved_dir):
+    return bentoml.load(h2o_svc_saved_dir)
+
+
+def test_h2o_artifact(h2o_svc_loaded):
+    import pandas as pd
+
+    test_df = pd.read_json(json.dumps(test_data))
+    result = h2o_svc_loaded.predict(test_df)
+    result_json = json.loads(result)
+    inference_result = result_json[0]['predict']
+    assert (
+        inference_result > 448 and inference_result < 453
+    ), 'Prediction on the saved h2o artifact does not match expect result'
+
+
+@pytest.fixture()
+def h2o_image(h2o_svc_saved_dir):
+    import docker
+
+    client = docker.from_env()
+    image = client.images.build(
+        path=h2o_svc_saved_dir, rm=True, tag='h2o_example_service',
+    )[0]
+    yield image
+    client.images.remove(image.id)
+
+
+@pytest.fixture()
+def h2o_docker_host(h2o_image):
+    import docker
+
+    client = docker.from_env()
+    with bentoml.utils.reserve_free_port() as port:
+        pass
+    command = "bentoml serve-gunicorn /bento --workers 1"
+    try:
+        container = client.containers.run(
+            command=command,
+            image=h2o_image.id,
+            auto_remove=True,
+            tty=True,
+            ports={'5000/tcp': port},
+            detach=True,
+        )
+        _host = f"127.0.0.1:{port}"
+        _wait_until_ready(_host, 500)
+        yield _host
+    finally:
+        container.stop()
+        time.sleep(1)
+
+
+def test_h2o_artifact_with_docker(h2o_docker_host):
+    import requests
+
+    result = requests.post(
+        f'http://{h2o_docker_host}/predict',
+        json=test_data,
+        headers={'Content-Type': 'application/json'},
+    )
+    assert result.status_code == 200, 'Failed to make successful request'
+    result_json = json.loads(result.json())
+    inference_result = result_json[0]['predict']
+    assert (
+        inference_result > 448 and inference_result < 453
+    ), 'Prediction on the saved h2o artifact does not match expect result'
diff --git a/travis/h2o_integration_tests.sh b/travis/h2o_integration_tests.sh
new file mode 100755
--- /dev/null
+++ b/travis/h2o_integration_tests.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+set -x
+
+# https://stackoverflow.com/questions/42218009/how-to-tell-if-any-command-in-bash-script-failed-non-zero-exit-status/42219754#42219754
+# Set err to 1 if pytest failed.
+error=0
+trap 'error=1' ERR
+
+GIT_ROOT=$(git rev-parse --show-toplevel)
+cd "$GIT_ROOT" || exit
+
+
+# Install required packages for h2o model artifacts test
+pip install h2o
+
+pytest "$GIT_ROOT"/tests/integration/test_h2o_model_artifact.py --cov=bentoml --cov-config=.coveragerc
+
+test $error = 0 # Return non-zero if pytest failed