Skip to content

Commit

Permalink
366 add cli commands to inspectrestart the environment (#409)
Browse files Browse the repository at this point in the history
Co-authored-by: David Perl <david.perl@diamond.ac.uk>
Co-authored-by: Callum Forrester <callum.forrester@diamond.ac.uk>
  • Loading branch information
3 people authored Jun 20, 2024
1 parent 31c94c5 commit 6a9efe1
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 29 deletions.
3 changes: 2 additions & 1 deletion docs/reference/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ paths:
'200':
content:
application/json:
schema: {}
schema:
$ref: '#/components/schemas/EnvironmentResponse'
description: Successful Response
summary: Delete Environment
get:
Expand Down
17 changes: 11 additions & 6 deletions docs/tutorials/dev-run.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
# Run/Debug in a Developer Environment
# Run/Debug in a Developer Environment

Assuming you have setup a developer environment, you can run a development version of the bluesky worker.


## Start Bluesky Worker

Ensure you are inside your virtual environment:
```

```
source venv/bin/activate
```

You will need to follow the instructions for setting up ActiveMQ as in [run cli instructions](../how-to/run-cli.md).

You will need to follow the instructions for setting up ActiveMQ as in [run cli instructions](../how-to/run-cli.md).

The worker will be available from the command line (`blueapi serve`), but can be started from vscode with additional
The worker will be available from the command line (`blueapi serve`), but can be started from vscode with additional
debugging capabilities.

1. Navigate to "Run and Debug" in the left hand menu.
2. Select "Worker Service" from the debug configuration.
3. Click the green "Run Button"

[debug in vscode](../images/debug-vscode.png)

## Develop devices

When you select the 'scratch directory' option - where you have devices (dodal) and plans (BLxx-beamline) in a place like `/dls_sw/BLXX/software/blueapi/scratch`, then the list of devices available will refresh without interfacing with the K8S cluster. Just run the command `blueapi env -r` or `blueapi env --reload`.

With this setup you get a developer loop: "write devices - write plans - test them with blueapi".
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dev = [
"mypy",
"pytest-cov",
"pytest-asyncio",
"responses",
"ruff",
"sphinx-autobuild==2024.2.4", # Later versions have a clash with fastapi<0.99, remove pin when fastapi is a higher version
"sphinx-copybutton",
Expand Down
59 changes: 57 additions & 2 deletions src/blueapi/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from functools import wraps
from pathlib import Path
from pprint import pprint
from time import sleep

import click
from pydantic import ValidationError
Expand Down Expand Up @@ -181,8 +182,9 @@ def run_plan(
if config.stomp is not None:
_message_template = StompMessagingTemplate.autoconfigured(config.stomp)
else:
pprint("ERROR: Cannot run plans without Stomp configuration to track progress")
return
raise RuntimeError(
"Cannot run plans without Stomp configuration to track progress"
)
event_bus_client = EventBusClient(_message_template)
finished_event: deque[WorkerEvent] = deque()

Expand Down Expand Up @@ -278,6 +280,59 @@ def stop(obj: dict) -> None:
pprint(client.cancel_current_task(state=WorkerState.STOPPING))


@controller.command(name="env")
@check_connection
@click.option(
"-r",
"--reload",
is_flag=True,
type=bool,
help="Reload the current environment",
default=False,
)
@click.pass_obj
def env(obj: dict, reload: bool | None) -> None:
"""
Inspect or restart the environment
"""

assert isinstance(client := obj["rest_client"], BlueapiRestClient)
if reload:
# Reload the environment if needed
print("Reloading the environment...")
try:
deserialized = client.reload_environment()
print(deserialized)

except BlueskyRemoteError as e:
raise BlueskyRemoteError("Failed to reload the environment") from e

# Initialize a variable to keep track of the environment status
environment_initialized = False
polling_count = 0
max_polling_count = 10
# Use a while loop to keep checking until the environment is initialized
while not environment_initialized and polling_count < max_polling_count:
# Fetch the current environment status
environment_status = client.get_environment()

# Check if the environment is initialized
if environment_status.initialized:
print("Environment is initialized.")
environment_initialized = True
else:
print("Waiting for environment to initialize...")
polling_count += 1
sleep(1) # Wait for 1 seconds before checking again
if polling_count == max_polling_count:
raise TimeoutError("Environment initialization timed out.")

# Once out of the loop, print the initialized environment status
print(environment_status)
else:
print(client.get_environment())


# helper function
def process_event_after_finished(event: WorkerEvent, logger: logging.Logger):
if event.is_error():
Expand Down
20 changes: 14 additions & 6 deletions src/blueapi/cli/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from blueapi.service.model import (
DeviceModel,
DeviceResponse,
EnvironmentResponse,
PlanModel,
PlanResponse,
TaskResponse,
Expand Down Expand Up @@ -115,16 +116,23 @@ def _request_and_deserialize(
raise_if: Callable[[requests.Response], bool] = _is_exception,
) -> T:
url = self._url(suffix)
response = requests.request(method, url, json=data)
if data:
response = requests.request(method, url, json=data)
else:
response = requests.request(method, url)
if raise_if(response):
message = get_status_message(response.status_code)
error_message = f"""Response failed with text: {response.text},
with error code: {response.status_code}
which corresponds to {message}"""
raise BlueskyRemoteError(error_message)
raise BlueskyRemoteError(str(response))
deserialized = parse_obj_as(target_type, response.json())
return deserialized

def _url(self, suffix: str) -> str:
base_url = f"{self._config.protocol}://{self._config.host}:{self._config.port}"
return f"{base_url}{suffix}"

def get_environment(self) -> EnvironmentResponse:
return self._request_and_deserialize("/environment", EnvironmentResponse)

def reload_environment(self) -> EnvironmentResponse:
return self._request_and_deserialize(
"/environment", EnvironmentResponse, method="DELETE"
)
10 changes: 7 additions & 3 deletions src/blueapi/service/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,18 @@ def get_environment(
return EnvironmentResponse(initialized=handler.initialized)


@app.delete("/environment")
@app.delete("/environment", response_model=EnvironmentResponse)
async def delete_environment(
background_tasks: BackgroundTasks,
handler: BlueskyHandler = Depends(get_handler),
):
) -> EnvironmentResponse:
def restart_handler(handler: BlueskyHandler):
handler.stop()
handler.start()

if handler.initialized:
background_tasks.add_task(restart_handler, handler)
return EnvironmentResponse(initialized=False)


@app.get("/plans", response_model=PlanResponse)
Expand Down Expand Up @@ -134,6 +135,9 @@ def get_device_by_name(name: str, handler: BlueskyHandler = Depends(get_handler)
return handler.get_device(name)


example_task = Task(name="count", params={"detectors": ["x"]})


@app.post(
"/tasks",
response_model=TaskResponse,
Expand All @@ -142,7 +146,7 @@ def get_device_by_name(name: str, handler: BlueskyHandler = Depends(get_handler)
def submit_task(
request: Request,
response: Response,
task: Task = Body(..., example=Task(name="count", params={"detectors": ["x"]})), # noqa: B008
task: Task = Body(..., example=example_task),
handler: BlueskyHandler = Depends(get_handler),
):
"""Submit a task to the worker."""
Expand Down
Loading

0 comments on commit 6a9efe1

Please sign in to comment.