Repository: mlflow/mlflow

Issue:
Tracking UI cannot show experiment if a run contains an empty parameter value
### System information
- **Have I written custom code (as opposed to using a stock example script provided in MLflow)**: Yes
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: mlflow server ubuntu 16.04, client Win 7
- **MLflow installed from (source or binary)**: binary
- **MLflow version (run ``mlflow --version``)**: 0.8.2
- **Python version**: mlflow server 3.5.2, client 3.5.4
- **npm version (if running the dev UI):
- **Exact command to reproduce**:

### Describe the problem
From my client python program, I attempt to log a parameter whose value is the empty string. (This was not intentional, but it happened.) The client eventually times out with `failed to return code 200 after 3 tries` (see traceback below). This is probably expected behavior.

The problem is that then viewing the experiment for the run is not possible. The server log shows `Param 'sys.env.CUDA_VISIBLE_DEVICES' is malformed. No data found` (see traceback below). Upon inspecting the filesystem, there is an empty file for this parameter. So there seems to be a mismatch between the client API validation and the UI validation. Only the one experiment is affected; other experiments are still viewable. Removing the empty parameter file on the server restores expected behavior.

### Source code / logs

*Client program*
Simple invocation of tracking API:
```
mlflow.log_param('sys.env.CUDA_VISIBLE_DEVICES', '')
```

```
Traceback (most recent call last):
...
    mlflow.log_param(key, value)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\tracking\fluent.py", line 179, in log_param
    MlflowClient().log_param(run_id, key, value)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\tracking\client.py", line 144, in log_param
    self.store.log_param(run_id, param)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\store\rest_store.py", line 173, in log_param
    self._call_endpoint(LogParam, req_body)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\store\rest_store.py", line 58, in _call_endpoint
    host_creds=host_creds, endpoint=endpoint, method=method, json=json_body)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\utils\rest_utils.py", line 73, in http_request_safe
    response = http_request(host_creds=host_creds, endpoint=endpoint, **kwargs)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\utils\rest_utils.py", line 58, in http_request
    (url, retries))
mlflow.exceptions.MlflowException: API request to http://10.53.36.24:5000/api/2.0/preview/mlflow/runs/log-parameter failed to return code 200 after 3 tries
Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\tracking\fluent.py", line 157, in end_run
    MlflowClient().set_terminated(_active_run_stack[-1].info.run_uuid, status)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\tracking\client.py", line 213, in set_terminated
    end_time=end_time)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\store\rest_store.py", line 130, in update_run_info
    response_proto = self._call_endpoint(UpdateRun, req_body)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\store\rest_store.py", line 58, in _call_endpoint
    host_creds=host_creds, endpoint=endpoint, method=method, json=json_body)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\utils\rest_utils.py", line 73, in http_request_safe
    response = http_request(host_creds=host_creds, endpoint=endpoint, **kwargs)
  File "C:\Users\dstine\.virtualenvs\labs_nlp_topic_model-C2Cdqh84\lib\site-packages\mlflow\utils\rest_utils.py", line 58, in http_request
    (url, retries))
mlflow.exceptions.MlflowException: API request to http://10.53.36.24:5000/api/2.0/preview/mlflow/runs/update failed to return code 200 after 3 tries
```

*Server log*
```
[2019-02-10 05:07:24,010] ERROR in app: Exception on /ajax-api/2.0/preview/mlflow/runs/search [POST]
Traceback (most recent call last):
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/mlflow/server/handlers.py", line 68, in wrapper
    return func(*args, **kwargs)
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/mlflow/server/handlers.py", line 272, in _search_runs
    run_view_type)
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/mlflow/store/file_store.py", line 519, in search_runs
    runs.extend(self.get_run(r.run_uuid) for r in run_infos)
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/mlflow/store/file_store.py", line 519, in <genexpr>
    runs.extend(self.get_run(r.run_uuid) for r in run_infos)
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/mlflow/store/file_store.py", line 355, in get_run
    params = self.get_all_params(run_uuid)
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/mlflow/store/file_store.py", line 474, in get_all_params
    params.append(self._get_param_from_file(parent_path, param_file))
  File "/home/ubuntu/.local/share/virtualenvs/mlflow-exp-KEZxtoN-/lib/python3.5/site-packages/mlflow/store/file_store.py", line 449, in _get_param_from_file
    raise Exception("Param '%s' is malformed. No data found." % param_name)
Exception: Param 'sys.env.CUDA_VISIBLE_DEVICES' is malformed. No data found.
```


Discussion:
@dbczumar Seems a reasonable use case for me. At this [line](https://github.com/mlflow/mlflow/blob/56d080707853fedcb0ddc5febf4d30ff3e47000e/mlflow/store/file_store.py#L441), `read_file_lines()` would return an empty list when the value of the parameter is an empty string. I can perhaps do something about it.

Resolving patch:
diff --git a/mlflow/store/file_store.py b/mlflow/store/file_store.py
--- a/mlflow/store/file_store.py
+++ b/mlflow/store/file_store.py
@@ -439,12 +439,13 @@ def get_metric_history(self, run_uuid, metric_key):
     def _get_param_from_file(parent_path, param_name):
         _validate_param_name(param_name)
         param_data = read_file_lines(parent_path, param_name)
-        if len(param_data) == 0:
-            raise Exception("Param '%s' is malformed. No data found." % param_name)
         if len(param_data) > 1:
             raise Exception("Unexpected data for param '%s'. Param recorded more than once"
                             % param_name)
-        return Param(param_name, str(param_data[0].strip()))
+        # The only cause for param_data's length to be zero is the param's
+        # value is an empty string
+        value = '' if len(param_data) == 0 else str(param_data[0].strip())
+        return Param(param_name, value)
 
     @staticmethod
     def _get_tag_from_file(parent_path, tag_name):


Test patch:
diff --git a/tests/store/test_file_store.py b/tests/store/test_file_store.py
--- a/tests/store/test_file_store.py
+++ b/tests/store/test_file_store.py
@@ -337,6 +337,18 @@ def test_weird_param_names(self):
         assert param.key == WEIRD_PARAM_NAME
         assert param.value == "Value"
 
+    def test_log_empty_str(self):
+        PARAM_NAME = "new param"
+        fs = FileStore(self.test_root)
+        run_uuid = self.exp_data[0]["runs"][0]
+        fs.log_param(run_uuid, Param(PARAM_NAME, ""))
+        run = fs.get_run(run_uuid)
+        my_params = [p for p in run.data.params if p.key == PARAM_NAME]
+        assert len(my_params) == 1
+        param = my_params[0]
+        assert param.key == PARAM_NAME
+        assert param.value == ""
+
     def test_weird_metric_names(self):
         WEIRD_METRIC_NAME = "this is/a weird/but valid metric"
         fs = FileStore(self.test_root)
diff --git a/tests/store/test_sqlalchemy_store.py b/tests/store/test_sqlalchemy_store.py
--- a/tests/store/test_sqlalchemy_store.py
+++ b/tests/store/test_sqlalchemy_store.py
@@ -466,6 +466,31 @@ def test_log_param_uniqueness(self):
             self.store.log_param(run.run_uuid, param2)
         self.assertIn("Changing param value is not allowed. Param with key=", e.exception.message)
 
+    def test_log_empty_str(self):
+        run = self._run_factory()
+
+        self.session.commit()
+
+        tkey = 'blahmetric'
+        tval = ''
+        param = entities.Param(tkey, tval)
+        param2 = entities.Param('new param', 'new key')
+        self.store.log_param(run.run_uuid, param)
+        self.store.log_param(run.run_uuid, param2)
+
+        actual = self.session.query(models.SqlParam).filter_by(key=tkey, value=tval)
+        self.assertIsNotNone(actual)
+
+        run = self.store.get_run(run.run_uuid)
+        self.assertEqual(2, len(run.data.params))
+
+        found = False
+        for m in run.data.params:
+            if m.key == tkey and m.value == tval:
+                found = True
+
+        self.assertTrue(found)
+
     def test_log_null_param(self):
         run = self._run_factory()