Skip to content

Commit

Permalink
feat: expose deployment to serve Executors (jina-ai#5563)
Browse files Browse the repository at this point in the history
Co-authored-by: Jina Dev Bot <[email protected]>
Co-authored-by: AnneY <[email protected]>
Co-authored-by: Alex Cureton-Griffiths <[email protected]>
  • Loading branch information
4 people authored Jan 10, 2023
1 parent 210d75f commit 8c0fe7d
Show file tree
Hide file tree
Showing 23 changed files with 891 additions and 452 deletions.
2 changes: 1 addition & 1 deletion docs/concepts/executor/dynamic-batching.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Then, in your `config.yaml` file, you can enable dynamic batching on the `/bar`
``` yaml
jtype: MyExecutor
py_modules:
- my_executor.yaml
- my_executor.py
dynamic_batching:
/bar:
preferred_batch_size: 10
Expand Down
188 changes: 76 additions & 112 deletions docs/concepts/executor/serve.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
(serve-executor-standalone)=
# Serve

{class}`~jina.Executor`s can be served - and remotely accessed - directly, without instantiating a Flow manually.
This is especially useful when debugging an Executor in a remote setting. It can also be used to run external/shared Executors to be used in multiple Flows.
{class}`~jina.Executor`s can be served and accessed over gRPC, allowing you to use them to create a gRPC-based service for various tasks such as model inference, data processing, generative AI, and search services.

There are different options for deploying and running a standalone Executor:
* Run the Executor directly from Python with the `.serve()` class method
* Run the static {meth}`~jina.serve.executors.BaseExecutor.to_kubernetes_yaml()` method to generate K8s deployment configuration files
* Run the Executor directly from Python with the {class}`~jina.orchestrate.deployments.Deployment` class
* Run the static {meth}`~jina.serve.executors.BaseExecutor.to_kubernetes_yaml()` method to generate Kubernetes deployment configuration files
* Run the static {meth}`~jina.serve.executors.BaseExecutor.to_docker_compose_yaml()` method to generate a Docker Compose service file



```{seealso}
Executors can also be combined to form a pipeline of microservices. We will see in a later step how
to achieve this with the {ref}`Flow <flow-cookbook>`
```

````{admonition} Served vs. shared Executor
:class: hint
In Jina there are two ways of running standalone Executors: *Served Executors* and *shared Executors*.
- A **served Executor** is launched by one of the following methods: `.serve()`, `to_kubernetes_yaml()`, or `to_docker_compose_yaml()`.
- A **served Executor** is launched by one of the following methods: {class}`~jina.orchestrate.deployments.Deployment`, `to_kubernetes_yaml()`, or `to_docker_compose_yaml()`.
It resides behind a {ref}`Gateway <architecture-overview>` and can thus be directly accessed by a {ref}`Client <client>`.
It can also be used as part of a Flow.
Expand All @@ -28,27 +34,84 @@ Both served and shared Executors can be used as part of a Flow, by adding them a
````

## Serve directly
An {class}`~jina.Executor` can be served using the {meth}`~jina.serve.executors.BaseExecutor.serve` method:
An {class}`~jina.Executor` can be served using the {class}`~jina.orchestrate.deployments.Deployment` class.

The {class}`~jina.orchestrate.deployments.Deployment` class aims to separate the deployment configuration from the serving logic.
In other words:
* the Executor cares about defining the logic to serve, which endpoints to define and what data to accept.
* the Deployment layer cares about how to orchestrate this service, how many replicas or shards, etc.

This separation also aims to enhance the reusability of Executors: the same implementation of an Executor can be
served in multiple ways/configurations using Deployment.

````{tab} Serve Executor
Serve the Executor:
````{tab} Python class
```python
from docarray import DocumentArray, Document
from jina import Executor, requests
from jina import Executor, requests, Deployment
class MyExec(Executor):
@requests
def foo(self, docs: DocumentArray, **kwargs):
docs[0] = 'executed MyExec' # custom logic goes here
docs[0].text = 'executed MyExec' # custom logic goes here
with Deployment(uses=MyExec, port=12345, replicas=2) as dep:
dep.block()
```
````

````{tab} YAML configuration
`executor.yaml`:
```
jtype: MyExec
py_modules:
- executor.py
```
```python
from jina import Deployment
MyExec.serve(port=12345)
with Deployment(uses='executor.yaml', port=12345, replicas=2) as dep:
dep.block()
```
````

````{tab} Hub Executor
```python
from jina import Deployment
with Deployment(uses='jinaai://my-username/MyExec/', port=12345, replicas=2) as dep:
dep.block()
```
````

````{tab} Access served Executor
````{tab} Docker image
```python
from jina import Deployment
with Deployment(uses='docker://my-executor-image', port=12345, replicas=2) as dep:
dep.block()
```
````

```text
─────────────────────── 🎉 Deployment is ready to serve! ───────────────────────
╭────────────── 🔗 Endpoint ────────────────╮
│ ⛓ Protocol GRPC │
│ 🏠 Local 0.0.0.0:12345 │
│ 🔒 Private 192.168.3.147:12345 │
│ 🌍 Public 87.191.159.105:12345 │
╰───────────────────────────────────────────╯
```

Access the served Executor:

```python
from jina import Client, DocumentArray, Document
Expand All @@ -60,11 +123,9 @@ print(Client(port=12345).post(inputs=DocumentArray.empty(1), on='/foo').texts)
['executed MyExec']
```

````

Internally, the {meth}`~jina.serve.executors.BaseExecutor.serve` method creates and starts a {class}`~jina.Flow`. Therefore, it can take all associated parameters:
`uses_with`, `uses_metas`, `uses_requests` are passed to the internal {meth}`~jina.Flow.add` call, `stop_event` stops
the Executor, and `**kwargs` is passed to the internal {meth}`~jina.Flow` initialisation call.
The {class}`~jina.orchestrate.deployments.Deployment` class accepts configuration options similar to
{ref}`Executor configuration with Flows <flow-configure-executors>`.

````{admonition} See Also
:class: seealso
Expand Down Expand Up @@ -128,100 +189,3 @@ The above example runs the `DummyHubExecutor` from Executor Hub locally on your
The Executor you use needs to be already containerized and stored in an accessible registry. We recommend Executor Hub for this.
````

(reload-executor)=
## Reload Executor

While developing your Executor, it can be useful to have the Executor be refreshed from the source code while you are working on it, without needing to restart the complete server.

For this you can use the Executor's `reload` argument so that it watches changes in the source code and ensures changes are applied live to the served Executor.

The Executor will keep track in changes inside the Executor source file, every file passed in `py_modules` argument from {meth}`~jina.Flow.add` and all Python files in the folder (and its subfolders) where the Executor class is defined.

````{admonition} Caution
:class: caution
This feature aims to let developers iterate faster while developing or improving the Executor, but is not intended to be used in production.
````

````{admonition} Note
:class: note
This feature requires watchfiles>=0.18 package to be installed.
````

To see how this works, let's define an Executor in a file `my_executor.py`:
```python
from jina import Executor, requests


class MyExecutor(Executor):
@requests
def foo(self, docs, **kwargs):
for doc in docs:
doc.text = 'I am coming from the first version of MyExecutor'
```

Build a Flow and expose it:

```python
import os
from jina import Flow

from my_executor import MyExecutor

os.environ['JINA_LOG_LEVEL'] = 'DEBUG'


f = Flow(port=12345).add(uses=MyExecutor, reload=True)

with f:
f.block()
```

You can see that the Executor is successfully serving:

```python
from jina import Client, DocumentArray

c = Client(port=12345)

print(c.post(on='/', inputs=DocumentArray.empty(1))[0].text)
```

```text
I am coming from the first version of MyExecutor
```

You can edit the Executor file and save the changes:

```python
from jina import Executor, requests


class MyExecutor(Executor):
@requests
def foo(self, docs, **kwargs):
for doc in docs:
doc.text = 'I am coming from a new version of MyExecutor'
```

You should see in the logs of the serving Executor

```text
INFO executor0/rep-0@11606 detected changes in: ['XXX/XXX/XXX/my_executor.py']. Refreshing the Executor
```

And after this, the Executor will start serving with the renewed code.

```python
from jina import Client, DocumentArray

c = Client(port=12345)

print(c.post(on='/', inputs=DocumentArray.empty(1))[0].text)
```

```text
'I am coming from a new version of MyExecutor'
```



2 changes: 1 addition & 1 deletion docs/concepts/flow/add-executors.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ f = Flow(extra_search_paths=['../executor']).add(uses='config1.yml').add(uses='c
````



(flow-configure-executors)=
## Configure Executors
You can set and override {class}`~jina.Executor` configuration when adding them to a {class}`~jina.Flow`.

Expand Down
3 changes: 3 additions & 0 deletions jina/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ def _set_nofile(nofile_atleast=4096):
# Flow
from jina.orchestrate.flow.base import Flow

# Deployment
from jina.orchestrate.deployments import Deployment

# Executor
from jina.serve.executors import BaseExecutor as Executor
from jina.serve.executors.decorators import dynamic_batching, monitor, requests
Expand Down
2 changes: 1 addition & 1 deletion jina/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class NetworkChecker:
"""Check if a BaseDeployment is running or not."""
"""Check if a Deployment is running or not."""

def __init__(self, args: 'argparse.Namespace'):
"""
Expand Down
Loading

0 comments on commit 8c0fe7d

Please sign in to comment.