def test_deploy_from_yaml(self, serve_instance):
        config_file_name = os.path.join(
            os.path.dirname(__file__), "test_config_files", "two_deployments.yaml"
        )

        # Check if yaml string and yaml file both produce the same Application
        with open(config_file_name, "r") as f:
            app1 = Application.from_yaml(f)
        with open(config_file_name, "r") as f:
            yaml_str = f.read()
        app2 = Application.from_yaml(yaml_str)
        compare_specified_options(app1.to_dict(), app2.to_dict())

        # Check that deployment works
        app1.deploy()
        assert (
            requests.get("http://localhost:8000/shallow").text == "Hello shallow world!"
        )
        assert requests.get("http://localhost:8000/one").text == "2"

        # Check if yaml string output is same as the Application
        recreated_app = Application.from_yaml(app1.to_yaml())
        compare_specified_options(recreated_app.to_dict(), app1.to_dict())

        # Check if yaml file output is same as the Application
        with tempfile.TemporaryFile(mode="w+") as tmp:
            app1.to_yaml(tmp)
            tmp.seek(0)
            compare_specified_options(
                Application.from_yaml(tmp).to_dict(), app1.to_dict()
            )
def test_immutable_deployment_list(serve_instance):
    app = Application([DecoratedClass, decorated_func])
    assert len(app.deployments.values()) == 2

    for name in app.deployments.keys():
        with pytest.raises(RuntimeError):
            app.deployments[name] = app.deployments[name].options(
                name="sneaky")
    def test_add_deployment_valid(self):
        app = Application()
        app.add_deployment(self.f)
        app.add_deployment(self.C)

        assert len(app) == 2
        assert "f" in app
        assert "C" in app
Exemple #4
0
    async def get_all_deployments(self, req: Request) -> Response:
        from ray.serve.api import list_deployments
        from ray.serve.application import Application

        app = Application(list(list_deployments().values()))
        return Response(
            text=json.dumps(app.to_dict()),
            content_type="application/json",
        )
Exemple #5
0
    def test_convert_to_import_path(self, serve_instance):
        f = decorated_func.options(name="f")
        C = DecoratedClass.options(name="C")
        app = Application([f, C])

        reconstructed_app = Application.from_yaml(app.to_yaml())

        serve.run(reconstructed_app)
        assert requests.get(
            "http://localhost:8000/f").text == "got decorated func"
        assert requests.get(
            "http://localhost:8000/C").text == "got decorated class"
    def deploy_and_check_responses(
        self, deployments, responses, blocking=True, client=None
    ):
        """
        Helper function that deploys the list of deployments, calls them with
        their handles, and checks whether they return the objects in responses.
        If blocking is False, this function uses a non-blocking deploy and uses
        the client to wait until the deployments finish deploying.
        """

        Application(deployments).deploy(blocking=blocking)

        def check_all_deployed():
            try:
                for deployment, response in zip(deployments, responses):
                    if ray.get(deployment.get_handle().remote()) != response:
                        return False
            except Exception:
                return False

            return True

        if blocking:
            # If blocking, this should be guaranteed to pass immediately.
            assert check_all_deployed()
        else:
            # If non-blocking, this should pass eventually.
            wait_for_condition(check_all_deployed)
Exemple #7
0
    def test_valid_deployments(self):
        app = Application([self.f, self.C])

        assert len(app.deployments) == 2
        app_deployment_names = {d.name for d in app.deployments.values()}
        assert "f" in app_deployment_names
        assert "C" in app_deployment_names
def maybe_build(
    node: DeploymentNode, use_build: bool
) -> Union[Application, DeploymentNode]:
    if use_build:
        return Application.from_dict(build_app(node).to_dict())
    else:
        return node
Exemple #9
0
def build(target: Union[ClassNode, FunctionNode]) -> Application:
    """Builds a Serve application into a static application.

    Takes in a ClassNode or FunctionNode and converts it to a
    Serve application consisting of one or more deployments. This is intended
    to be used for production scenarios and deployed via the Serve REST API or
    CLI, so there are some restrictions placed on the deployments:
        1) All of the deployments must be importable. That is, they cannot be
           defined in __main__ or inline defined. The deployments will be
           imported in production using the same import path they were here.
        2) All arguments bound to the deployment must be JSON-serializable.

    The returned Application object can be exported to a dictionary or YAML
    config.
    """

    if in_interactive_shell():
        raise RuntimeError(
            "build cannot be called from an interactive shell like "
            "IPython or Jupyter because it requires all deployments to be "
            "importable to run the app after building.")

    # TODO(edoakes): this should accept host and port, but we don't
    # currently support them in the REST API.
    return Application(pipeline_build(target))
Exemple #10
0
    async def put_all_deployments(self, req: Request) -> Response:
        from ray import serve
        from ray.serve.application import Application

        app = Application.from_dict(await req.json())
        serve.run(app, _blocking=False)

        return Response()
Exemple #11
0
    def test_invalid_input(self, serve_instance):
        """
        Checks Application's deploy behavior when deployment group contains
        non-Deployment objects.
        """

        with pytest.raises(TypeError):
            Application([self.f, self.C,
                         "not a Deployment object"]).deploy(blocking=True)
    def test_add_deployment_repeat_name(self):
        with pytest.raises(ValueError):
            app = Application()
            app.add_deployment(self.f)
            app.add_deployment(self.C.options(name="f"))

        with pytest.raises(ValueError):
            Application([self.C, self.f.options(name="C")])
Exemple #13
0
def test_run_get_ingress_app(serve_instance):
    """Check that serve.run() with an app returns the ingress."""
    @serve.deployment(route_prefix=None)
    def f():
        return "got f"

    @serve.deployment(route_prefix="/g")
    def g():
        return "got g"

    app = Application([f, g])
    ingress_handle = serve.run(app)

    assert ray.get(ingress_handle.remote()) == "got g"
    serve_instance.delete_deployments(["f", "g"])

    no_ingress_app = Application([f.options(route_prefix="/f"), g])
    ingress_handle = serve.run(no_ingress_app)
    assert ingress_handle is None
def test_get_set_item(serve_instance):
    config_file_name = os.path.join(
        os.path.dirname(__file__), "test_config_files", "two_deployments.yaml"
    )

    with open(config_file_name, "r") as f:
        app = Application.from_yaml(f)
    app["shallow"].deploy()
    app["one"].deploy()

    assert requests.get("http://localhost:8000/shallow").text == "Hello shallow world!"
    assert requests.get("http://localhost:8000/one").text == "2"
Exemple #15
0
    async def put_all_deployments(self, req: Request) -> Response:
        app = Application.from_dict(await req.json())
        app.deploy(blocking=False)

        new_names = set()
        for deployment in app:
            new_names.add(deployment.name)

        all_deployments = serve.list_deployments()
        all_names = set(all_deployments.keys())
        names_to_delete = all_names.difference(new_names)
        for name in names_to_delete:
            all_deployments[name].delete()

        return Response()
Exemple #16
0
def run(
    config_or_import_path: str,
    runtime_env: str,
    runtime_env_json: str,
    working_dir: str,
    app_dir: str,
    address: str,
    host: str,
    port: int,
    blocking: bool,
):
    sys.path.insert(0, app_dir)

    final_runtime_env = parse_runtime_env_args(
        runtime_env=runtime_env,
        runtime_env_json=runtime_env_json,
        working_dir=working_dir,
    )

    app_or_node = None
    if pathlib.Path(config_or_import_path).is_file():
        config_path = config_or_import_path
        cli_logger.print(f"Deploying from config file: '{config_path}'.")
        with open(config_path, "r") as config_file:
            app_or_node = Application.from_yaml(config_file)
    else:
        import_path = config_or_import_path
        cli_logger.print(f"Deploying from import path: '{import_path}'.")
        app_or_node = import_attr(import_path)

    # Setting the runtime_env here will set defaults for the deployments.
    ray.init(address=address, namespace="serve", runtime_env=final_runtime_env)

    try:
        serve.run(app_or_node, host=host, port=port)
        cli_logger.success("Deployed successfully.")

        if blocking:
            while True:
                # Block, letting Ray print logs to the terminal.
                time.sleep(10)

    except KeyboardInterrupt:
        cli_logger.info("Got KeyboardInterrupt, shutting down...")
        serve.shutdown()
        sys.exit()
Exemple #17
0
    async def put_all_deployments(self, req: Request) -> Response:
        from ray import serve
        from ray.serve.context import get_global_client
        from ray.serve.schema import ServeApplicationSchema
        from ray.serve.application import Application

        config = ServeApplicationSchema.parse_obj(await req.json())

        if config.import_path is not None:
            client = get_global_client(_override_controller_namespace="serve")
            client.deploy_app(config)
        else:
            # TODO (shrekris-anyscale): Remove this conditional path
            app = Application.from_dict(await req.json())
            serve.run(app, _blocking=False)

        return Response()
Exemple #18
0
    def test_deploy_from_dict(self, serve_instance):
        config_file_name = os.path.join(os.path.dirname(__file__),
                                        "test_config_files",
                                        "two_deployments.yaml")

        with open(config_file_name, "r") as config_file:
            config_dict = yaml.safe_load(config_file)

        app = Application.from_dict(config_dict)
        app_dict = app.to_dict()

        compare_specified_options(config_dict, app_dict)

        serve.run(app.from_dict(app_dict))

        assert (requests.get("http://localhost:8000/shallow").text ==
                "Hello shallow world!")
        assert requests.get("http://localhost:8000/one").text == "2"
Exemple #19
0
    async def put_all_deployments(self, req: Request) -> Response:
        from ray import serve
        from ray.serve.context import get_global_client
        from ray.serve.application import Application

        app = Application.from_dict(await req.json())
        serve.run(app, _blocking=False)

        new_names = set()
        for deployment in app.deployments.values():
            new_names.add(deployment.name)

        all_deployments = serve.list_deployments()
        all_names = set(all_deployments.keys())
        names_to_delete = all_names.difference(new_names)
        get_global_client().delete_deployments(names_to_delete)

        return Response()
Exemple #20
0
def test_immutable_deployment_list(serve_instance):
    config_file_name = os.path.join(os.path.dirname(__file__),
                                    "test_config_files",
                                    "two_deployments.yaml")

    with open(config_file_name, "r") as f:
        app = Application.from_yaml(f)

    assert len(app.deployments.values()) == 2

    for name in app.deployments.keys():
        with pytest.raises(RuntimeError):
            app.deployments[name] = app.deployments[name].options(
                name="sneaky")

    for deployment in app.deployments.values():
        deployment.deploy()

    assert requests.get(
        "http://localhost:8000/shallow").text == "Hello shallow world!"
    assert requests.get("http://localhost:8000/one").text == "2"
    def test_mutual_handles(self, serve_instance):
        """
        Atomically deploys a group of deployments that get handles to other
        deployments in the group inside their __init__ functions. The handle
        references should fail in a non-atomic deployment. Checks whether the
        deployments deploy correctly.
        """

        @serve.deployment
        class MutualHandles:
            async def __init__(self, handle_name):
                self.handle = serve.get_deployment(handle_name).get_handle()

            async def __call__(self, echo: str):
                return await self.handle.request_echo.remote(echo)

            async def request_echo(self, echo: str):
                return echo

        names = []
        for i in range(10):
            names.append("a" * i)

        deployments = []
        for idx in range(len(names)):
            # Each deployment will hold a ServeHandle with the next name in
            # the list
            deployment_name = names[idx]
            handle_name = names[(idx + 1) % len(names)]

            deployments.append(
                MutualHandles.options(name=deployment_name, init_args=(handle_name,))
            )

        Application(deployments).deploy(blocking=True)

        for deployment in deployments:
            assert (ray.get(deployment.get_handle().remote("hello"))) == "hello"
Exemple #22
0
    # Check idempotence
    for _ in range(2):
        subprocess.check_output(["serve", "deploy", config_file_name])
        wait_for_condition(lambda: get_num_deployments() == 2, timeout=35)

        subprocess.check_output(["serve", "delete", "-y"])
        wait_for_condition(lambda: get_num_deployments() == 0, timeout=35)


@serve.deployment
def parrot(request):
    return request.query_params["sound"]


parrot_app = Application([parrot])


@pytest.mark.skipif(sys.platform == "win32",
                    reason="File path incorrect on Windows.")
def test_run_application(ray_start_stop):
    # Deploys valid config file and import path via serve run

    # Deploy via config file
    config_file_name = os.path.join(os.path.dirname(__file__),
                                    "test_config_files",
                                    "two_deployments.yaml")

    p = subprocess.Popen(["serve", "run", "--address=auto", config_file_name])
    wait_for_condition(lambda: ping_endpoint("one") == "2", timeout=10)
    wait_for_condition(
Exemple #23
0
def run(
    config_or_import_path: str,
    args_and_kwargs: Tuple[str],
    runtime_env: str,
    runtime_env_json: str,
    working_dir: str,
    address: str,
):

    # Check if path provided is for config or import
    is_config = pathlib.Path(config_or_import_path).is_file()
    args, kwargs = _process_args_and_kwargs(args_and_kwargs)

    # Calculate deployments' runtime env updates requested via args
    runtime_env_updates = parse_runtime_env_args(
        runtime_env=runtime_env,
        runtime_env_json=runtime_env_json,
        working_dir=working_dir,
    )

    # Create ray.init()'s runtime_env
    if "working_dir" in runtime_env_updates:
        ray_runtime_env = {
            "working_dir": runtime_env_updates.pop("working_dir")
        }
    else:
        ray_runtime_env = {}

    if is_config:
        config_path = config_or_import_path
        # Delay serve.start() to catch invalid inputs without waiting
        if len(args) + len(kwargs) > 0:
            raise ValueError(
                "ARGS_AND_KWARGS cannot be defined for a "
                "config file deployment. Please specify the "
                "init_args and init_kwargs inside the config file.")

        cli_logger.print("Deploying application in config file at "
                         f"{config_path}.")
        with open(config_path, "r") as config_file:
            app = Application.from_yaml(config_file)

    else:
        import_path = config_or_import_path
        if "." not in import_path:
            raise ValueError(
                "Import paths must be of the form "
                '"module.submodule_1...submodule_n.MyClassOrFunction".')

        cli_logger.print(
            f'Deploying function or class imported from "{import_path}".')

        deployment_name = import_path[import_path.rfind(".") + 1:]
        deployment = serve.deployment(name=deployment_name)(import_path)

        app = Application(
            [deployment.options(init_args=args, init_kwargs=kwargs)])

    ray.init(address=address, namespace="serve", runtime_env=ray_runtime_env)

    for deployment in app:
        _configure_runtime_env(deployment, runtime_env_updates)

    app.run(logger=cli_logger)
Exemple #24
0
    def test_repeated_deployment_names(self):
        with pytest.raises(ValueError):
            Application([self.f, self.C.options(name="f")])

        with pytest.raises(ValueError):
            Application([self.C, self.f.options(name="C")])
Exemple #25
0
 def test_non_deployments(self):
     with pytest.raises(TypeError):
         Application([self.f, 5, "hello"])