Connecting a remote service#
The orchestrator runs most OrangeQS Juice services and manages their life-cycle. A remote service is different. It runs somewhere else — on a lab machine, or on hardware that you supervise by hand. It opens an outbound connection to the Juice server. Juice never connects to a remote service. So the remote does not need to accept connections from Juice.
A remote service is a plain Python program that subclasses the RemoteService base class.
You register handlers for the task types it answers, connect it to your installation, and run it.
After Juice acknowledges the connection, the remote behaves like any other service: you send it tasks by name.
This guide covers the full path:
Declaring the remote. Declare the remote’s name so Juice will accept it.
Writing the remote service. Subclass
RemoteServiceand register a task handler.Running and connecting. Run the program on the remote machine and connect to the installation.
Sending tasks to the remote. Send tasks to the remote from a notebook.
Declaring the remote#
Juice accepts only the remotes that you declare.
This is an operator step. You declare a remote’s name in the remote-services configuration. Juice rejects any connection whose announced name is not declared, or collides with a managed service. The remote must also present a valid edge token when it connects (see Writing the remote service). Juice permits the connection only when both the token and the name pass.
Add the name to a remote-services.toml file in your installation’s configuration.
In a lab repository this file lives under the package’s config folder. A system administrator can also place it in /etc/juice/config/.
# remote-services.toml
remotes = [
"lab-instrument",
]
The name is the task routing target. It must be unique across the entire service namespace. Managed and remote services share one namespace.
Juice reads the declared list fresh on every connection attempt. So Juice accepts a newly added remote without a restart. This does not affect existing connections.
Writing the remote service#
Install the Juice package on the remote machine, then subclass RemoteService.
In the constructor, forward the name, edge_url, and api_token to the base class and register a handler for each task type the remote should answer.
A handler is a function that takes a task instance and returns its result.
It can be synchronous or async.
The example below answers the Ping task. It returns the same message.
# lab_instrument.py
import os
from dotenv import load_dotenv
from orangeqs.juice.schemas.tasks import Ping
from orangeqs.juice.service import RemoteService
class InstrumentRemote(RemoteService):
"""A remote service that answers Ping."""
def __init__(self, name: str, edge_url: str, api_token: str) -> None:
super().__init__(name, edge_url, api_token)
self.register_handler(Ping, self._on_ping)
async def _on_ping(self, task: Ping) -> str:
return task.message
if __name__ == "__main__":
load_dotenv()
os.environ.get("JUICE_REMOTE_EDGE_TOKEN") or exit(
"Set the JUICE_REMOTE_EDGE_TOKEN environment variable first."
)
InstrumentRemote(
name="lab-instrument",
edge_url="ws://juice.example.com:8888",
api_token=os.environ["JUICE_REMOTE_EDGE_TOKEN"],
).run()
The name must match the name you declared in Declaring the remote.
The edge_url is the base WebSocket URL of your installation’s edge. This is the JupyterHub port 8888. The remote appends the /hub/tasks path itself.
If the operator uses a secured connection before JupyterHub, use wss:// instead of ws://.
The api_token is the edge token the remote presents. Generate it on the token page of your installation (/hub/token). You can also use a service token that the operator creates. Juice validates this token on every connection.
Keep the token secret and do not commit it.
Call load_dotenv() to read a local .env file when the file exists.
Install python-dotenv on the remote machine before you run this program.
Running and connecting#
Run the program on the remote machine:
Create a
.envfile in the same directory aslab_instrument.py.Add this line to the file:
JUICE_REMOTE_EDGE_TOKEN=<your-jupyterhub-token>
Run the program:
python lab_instrument.py
The run() method opens the outbound connection, presents the edge token, announces the remote’s name, and waits for Juice to acknowledge it.
After acceptance the remote serves tasks and stays connected. It handles each task that Juice pushes down the channel.
Acknowledgement is connection-scoped. If the connection drops, the remote is gone. When the remote reconnects, it runs the same flow again. Juice does not need to re-accept it. A remote whose connection has dropped fails at routing time, exactly like a crashed managed service.
Warning
Juice rejects the connection for three reasons:
an invalid or missing edge token
an undeclared name
a name that collides with a managed service
In each case run() raises RemoteAcknowledgementError with the reason.
Check that the token is valid. Check that the name in your program matches the declared remote-services entry exactly.
Sending tasks to the remote#
After Juice acknowledges the remote, you reach it by name like any other service.
Use execute() from a notebook, with the remote’s declared name as the target:
from orangeqs.juice import Client
from orangeqs.juice.schemas.tasks import Ping
client = Client()
result = await client.execute(
"lab-instrument",
Ping(message="hello"),
)
print(result.result) # <- "hello"