Repository: ray-project/ray

Issue:
No proper error message when num_return_vals is negative or not integral 
<!--Please include [tune], [rllib], [autoscaler] etc. in the issue title if relevant-->

### What is the problem?
*Ray version and other system information (Python version, TensorFlow version, OS):*
    *Ubuntu 18.04.4 LTS*
    *Python 3.7.7*
    *ray-0.9.0.dev0-cp37-cp37m-manylinux1_x86_64.whl*

* For non integral values of `x`, eg. `x = 3.2` in the code below, ray doesn't give a proper error message. Instead the worker dies. This also happens when `x` is not equal to the true num return values, eg. `x = 5`.
`
ray.exceptions.RayWorkerError: The worker died unexpectedly while executing this task.
`
* For negative values of `x`, eg. `x = -2` the kernel dies altogether. 
` Aborted (core dumped)`

Although one would not expect the user to enter such values. But still. 

### Reproduction
```
import ray

ray.init()

def f(a):
    return a, 2

x = <some_value>
g = ray.remote(num_return_vals = x)(f)

print(ray.get(g.remote(3)))
```

- [x] I have verified my script runs in a clean environment and reproduces the issue.
- [x] I have verified the issue also occurs with the [latest wheels](https://docs.ray.io/en/latest/installation.html).



Discussion:
@vinamrabenara thanks for catching this!

Resolving patch:
diff --git a/python/ray/worker.py b/python/ray/worker.py
--- a/python/ray/worker.py
+++ b/python/ray/worker.py
@@ -1806,7 +1806,21 @@ def decorator(function_or_class):
             if max_task_retries is not None:
                 raise ValueError("The keyword 'max_task_retries' is not "
                                  "allowed for remote functions.")
-
+            if num_return_vals is not None and (not isinstance(
+                    num_return_vals, int) or num_return_vals < 0):
+                raise ValueError(
+                    "The keyword 'num_return_vals' only accepts 0 or a"
+                    " positive integer")
+            if max_retries is not None and (not isinstance(max_retries, int)
+                                            or max_retries < -1):
+                raise ValueError(
+                    "The keyword 'max_retries' only accepts 0, -1 or a"
+                    " positive integer")
+            if max_calls is not None and (not isinstance(max_calls, int)
+                                          or max_calls < 0):
+                raise ValueError(
+                    "The keyword 'max_calls' only accepts 0 or a positive"
+                    " integer")
             return ray.remote_function.RemoteFunction(
                 Language.PYTHON, function_or_class, None, num_cpus, num_gpus,
                 memory, object_store_memory, resources, num_return_vals,
@@ -1820,7 +1834,16 @@ def decorator(function_or_class):
             if max_calls is not None:
                 raise TypeError("The keyword 'max_calls' is not "
                                 "allowed for actors.")
-
+            if max_restarts is not None and (not isinstance(max_restarts, int)
+                                             or max_restarts < -1):
+                raise ValueError(
+                    "The keyword 'max_restarts' only accepts -1, 0 or a"
+                    " positive integer")
+            if max_task_retries is not None and (not isinstance(
+                    max_task_retries, int) or max_task_retries < -1):
+                raise ValueError(
+                    "The keyword 'max_task_retries' only accepts -1, 0 or a"
+                    " positive integer")
             return ray.actor.make_actor(function_or_class, num_cpus, num_gpus,
                                         memory, object_store_memory, resources,
                                         max_restarts, max_task_retries)


Test patch:
diff --git a/python/ray/tests/test_basic.py b/python/ray/tests/test_basic.py
--- a/python/ray/tests/test_basic.py
+++ b/python/ray/tests/test_basic.py
@@ -111,6 +111,60 @@ def method(self):
     assert ray.get([id1, id2, id3, id4]) == [0, 1, "test", 2]
 
 
+def test_invalid_arguments(shutdown_only):
+    ray.init(num_cpus=2)
+
+    for opt in [np.random.randint(-100, -1), np.random.uniform(0, 1)]:
+        with pytest.raises(
+                ValueError,
+                match="The keyword 'num_return_vals' only accepts 0 or a"
+                " positive integer"):
+
+            @ray.remote(num_return_vals=opt)
+            def g1():
+                return 1
+
+    for opt in [np.random.randint(-100, -2), np.random.uniform(0, 1)]:
+        with pytest.raises(
+                ValueError,
+                match="The keyword 'max_retries' only accepts 0, -1 or a"
+                " positive integer"):
+
+            @ray.remote(max_retries=opt)
+            def g2():
+                return 1
+
+    for opt in [np.random.randint(-100, -1), np.random.uniform(0, 1)]:
+        with pytest.raises(
+                ValueError,
+                match="The keyword 'max_calls' only accepts 0 or a positive"
+                " integer"):
+
+            @ray.remote(max_calls=opt)
+            def g3():
+                return 1
+
+    for opt in [np.random.randint(-100, -2), np.random.uniform(0, 1)]:
+        with pytest.raises(
+                ValueError,
+                match="The keyword 'max_restarts' only accepts -1, 0 or a"
+                " positive integer"):
+
+            @ray.remote(max_restarts=opt)
+            class A1:
+                x = 1
+
+    for opt in [np.random.randint(-100, -2), np.random.uniform(0, 1)]:
+        with pytest.raises(
+                ValueError,
+                match="The keyword 'max_task_retries' only accepts -1, 0 or a"
+                " positive integer"):
+
+            @ray.remote(max_task_retries=opt)
+            class A2:
+                x = 1
+
+
 def test_many_fractional_resources(shutdown_only):
     ray.init(num_cpus=2, num_gpus=2, resources={"Custom": 2})