Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
c6bf42d
Add unimplemented classes and functions for compute manager implement…
ianmkenney Jul 14, 2025
8ae35b3
Add unimplemented statestore methods for the compute manager
ianmkenney Jul 14, 2025
0720596
Validate ComputeManagerID
ianmkenney Jul 15, 2025
6836edf
First implementation of update status
ianmkenney Jul 15, 2025
d87d848
Improved handling of compute manager ids, migration of code from api …
ianmkenney Jul 16, 2025
b3742d8
Merge remote-tracking branch 'origin/main' into autoscaling
ianmkenney Jul 16, 2025
a898fdf
Implement instruction and fix naming issues
ianmkenney Jul 16, 2025
b63c63d
Reorganize compute manager related models
ianmkenney Jul 16, 2025
febf7a6
Moved ComputeManager specific models to storage models
ianmkenney Jul 18, 2025
c71a133
Use manager_id instead of name
ianmkenney Jul 18, 2025
ebe1111
Simplify deregistration and handle everything in cypher
ianmkenney Jul 18, 2025
273accf
Remove "force" from compute manager deregistration
ianmkenney Jul 21, 2025
eccf567
Implement tests for compute manager client
ianmkenney Jul 22, 2025
11471a8
Correct name for compute manager in test
ianmkenney Jul 23, 2025
3108d97
Implement instruction testing for compute manager client
ianmkenney Jul 23, 2025
a780135
Begin implementing tests for statestore methods
ianmkenney Jul 28, 2025
83df0d3
Change manager_id to manager_name, remove STALLED, implement status test
ianmkenney Jul 29, 2025
80572cc
Allow manager registration to reattach to compute services and test e…
ianmkenney Jul 31, 2025
9375a90
Rework structure of returned ComputeManagerInstruction and its data
ianmkenney Aug 4, 2025
0235436
Docstrings for statestore and minor refactoring
ianmkenney Aug 4, 2025
2de6a76
Implement a bulk version of compute_service_can_claim
ianmkenney Aug 4, 2025
30456f7
Revert "Rework structure of returned ComputeManagerInstruction and it…
ianmkenney Aug 5, 2025
00df011
Implement tests for manager base class
ianmkenney Aug 5, 2025
89998bf
Remove use of value from StrEnum instances
ianmkenney Aug 5, 2025
3ba652d
Count available tasks
ianmkenney Aug 5, 2025
96f3726
Collect unique sets of tasks
ianmkenney Aug 6, 2025
8b7ebb9
Fix FastAPI parameters
ianmkenney Aug 7, 2025
4157219
Corrected checking for number of tasks
ianmkenney Aug 8, 2025
2eb45b6
Add saturation to compute manager update status protocol
ianmkenney Aug 12, 2025
aff6bf1
Test saturation is correctly set from status update
ianmkenney Aug 12, 2025
47720a3
Add compute manager ID to compute service settings
ianmkenney Aug 13, 2025
ec07e6d
Add saturation to compute manager client status update method
ianmkenney Aug 13, 2025
0aa3d78
Implement creation of compute services in compute manager tests
ianmkenney Aug 13, 2025
896853f
Properly terminate compute service processes
ianmkenney Aug 13, 2025
cc702eb
Consider the number of unclaimed tasks when creating compute services
ianmkenney Aug 18, 2025
8a8e610
Fix test with correct number of expected compute services started
ianmkenney Aug 18, 2025
768ced6
Merge remote-tracking branch 'upstream/main' into autoscaling
ianmkenney Aug 19, 2025
ad6b235
Improve testing for ComputeManagerID
ianmkenney Aug 19, 2025
0eb792b
Merge branch 'main' into autoscaling
dotsdl Aug 22, 2025
a5043b8
Include basic implementation for the compute manager base class
ianmkenney Aug 26, 2025
784eec3
Attempt to remove expired compute managers on all status updates
ianmkenney Aug 26, 2025
fc6fe0a
Add minimal docs about compute managers to the compute docs
ianmkenney Aug 26, 2025
c0ace2f
Add more tests to ComputeManager implementation
ianmkenney Aug 26, 2025
2ea4a37
Remove exception_to_raise parameter in _create_compute_service
ianmkenney Aug 26, 2025
aedd190
Improve logging and test coverage
ianmkenney Aug 27, 2025
152b59f
Allow creating multiple compute services at once
ianmkenney Aug 27, 2025
aa6b192
Formatting adjustments.
dotsdl Sep 1, 2025
b2e952a
Black format and address quick fixes from review
ianmkenney Sep 2, 2025
0cf6353
Rename compute manager API points
ianmkenney Sep 2, 2025
4da3546
Rename ERRORED to ERROR
ianmkenney Sep 2, 2025
71f1144
Merge remote-tracking branch 'origin/main' into autoscaling
ianmkenney Sep 2, 2025
73faeb0
Managers now use `name` instead of `manager_name` when appropriate
ianmkenney Sep 4, 2025
424a039
Make signs clear for time arithmetic, improve logging, simplify query
ianmkenney Sep 9, 2025
bcb2492
ERROR ComputeManager clearing
ianmkenney Sep 10, 2025
7eb9e15
Registration failure flow control
ianmkenney Sep 10, 2025
c76f5ea
Removed unnecessary exception handling
ianmkenney Sep 11, 2025
8d6a1a8
Pass scopes along to client get_instruction requests
ianmkenney Sep 11, 2025
7edb01a
ComputeManager now uses service settings as input
ianmkenney Sep 11, 2025
2a36107
Change logger.fatal to logger.error
ianmkenney Sep 11, 2025
4d7edf8
Test providing a missing compute manager to a compute service
ianmkenney Sep 11, 2025
5a708a0
Drop try-except on register for compute managers in client
dotsdl Sep 15, 2025
6007bb4
Modifications from review
dotsdl Sep 16, 2025
207ae1e
Add news entry for compute managers
ianmkenney Sep 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 150 additions & 2 deletions alchemiscale/compute/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from datetime import timedelta
import random

from fastapi import FastAPI, APIRouter, Body, Depends, Request
from fastapi import FastAPI, APIRouter, Body, Depends, HTTPException, Request
from fastapi import status as http_status
from fastapi.middleware.gzip import GZipMiddleware
from gufe.tokenization import JSON_HANDLER
from gufe.protocols import ProtocolDAGResult
Expand Down Expand Up @@ -40,7 +41,10 @@
from ..storage.models import (
ProtocolDAGResultRef,
ComputeServiceID,
ComputeManagerID,
ComputeManagerRegistration,
ComputeServiceRegistration,
ComputeManagerStatus,
)
from ..models import Scope, ScopedKey
from ..security.models import (
Expand Down Expand Up @@ -102,17 +106,31 @@ def list_scopes(
@router.post("/computeservice/{compute_service_id}/register")
def register_computeservice(
compute_service_id,
*,
compute_manager_id: str | None = Body(None, embed=True),
n4js: Neo4jStore = Depends(get_n4js_depends),
):
now = datetime.datetime.now(tz=datetime.UTC)
if compute_manager_id:
manager_name = process_compute_manager_id_string(compute_manager_id).name
else:
manager_name = None

csreg = ComputeServiceRegistration(
identifier=ComputeServiceID(compute_service_id),
registered=now,
heartbeat=now,
failure_times=[],
manager_name=manager_name,
)

compute_service_id_ = n4js.register_computeservice(csreg)
try:
compute_service_id_ = n4js.register_computeservice(csreg)
except ValueError as e:
raise HTTPException(
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(e),
)

return compute_service_id_

Expand Down Expand Up @@ -392,6 +410,136 @@ async def set_task_result(
return result_sk


def process_compute_manager_id_string(
compute_manager_id_string: str,
) -> ComputeManagerID:
"""Try creating a ComputeManagerID from a string representation. Raise HTTPException."""
try:
compute_manager_id = ComputeManagerID(compute_manager_id_string)
except Exception as e:
raise HTTPException(
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(e),
)

return compute_manager_id


@router.post("/computemanager/{compute_manager_id}/register")
def register_computemanager(
compute_manager_id,
n4js: Neo4jStore = Depends(get_n4js_depends),
):

compute_manager_id = process_compute_manager_id_string(compute_manager_id)

now = datetime.datetime.now(tz=datetime.UTC)
cm_registration = ComputeManagerRegistration(
name=compute_manager_id.name,
uuid=compute_manager_id.uuid,
registered=now,
last_status_update=now,
status=ComputeManagerStatus.OK,
detail="",
saturation=0,
)

try:
compute_manager_id_ = n4js.register_computemanager(cm_registration)
except ValueError as e:
raise HTTPException(
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(e),
)

return compute_manager_id_


@router.post("/computemanager/{compute_manager_id}/deregister")
def deregister_computemanager(
compute_manager_id,
n4js: Neo4jStore = Depends(get_n4js_depends),
):
compute_manager_id = process_compute_manager_id_string(compute_manager_id)
n4js.deregister_computemanager(compute_manager_id)
return compute_manager_id


@router.post("/computemanager/{compute_manager_id}/instruction")
def get_instruction_computemanager(
compute_manager_id,
*,
scopes: list[Scope] = Body([], embed=True),
n4js: Neo4jStore = Depends(get_n4js_depends),
settings: ComputeAPISettings = Depends(get_base_api_settings),
token: TokenData = Depends(get_token_data_depends),
):
scopes = scopes or [Scope()]
scopes_reduced = minimize_scope_space(scopes)
query_scopes = []
for scope in scopes_reduced:
query_scopes.extend(validate_scopes_query(scope, token))

compute_manager_id = process_compute_manager_id_string(compute_manager_id)
now = datetime.datetime.now(tz=datetime.UTC)
instruction, payload = n4js.get_computemanager_instruction(
compute_manager_id,
now - timedelta(seconds=settings.ALCHEMISCALE_COMPUTE_API_FORGIVE_TIME_SECONDS),
settings.ALCHEMISCALE_COMPUTE_API_MAX_FAILURES,
query_scopes,
)
payload["instruction"] = str(instruction)
return payload


@router.post("/computemanager/{compute_manager_id}/status")
def update_status_computemanager(
compute_manager_id,
*,
status: str = Body(),
detail: str | None = Body(None),
saturation: float | None = Body(None),
n4js: Neo4jStore = Depends(get_n4js_depends),
settings: ComputeAPISettings = Depends(get_base_api_settings),
):
expire_seconds = settings.ALCHEMISCALE_COMPUTE_API_MANAGER_EXPIRE_SECONDS
expire_seconds_errored = (
settings.ALCHEMISCALE_COMPUTE_API_MANAGER_EXPIRE_SECONDS_ERROR
)
compute_manager_id = process_compute_manager_id_string(compute_manager_id)
try:
n4js.update_compute_manager_status(
compute_manager_id, status, detail, saturation
)
now = datetime.datetime.now(tz=datetime.UTC)
n4js.expire_computemanager_registrations(
now - timedelta(seconds=expire_seconds),
now - timedelta(seconds=expire_seconds_errored),
)
except ValueError as e:
raise HTTPException(
status_code=http_status.HTTP_400_BAD_REQUEST,
detail=str(e),
)

return compute_manager_id


@router.post("/computemanager/{compute_manager_name}/clear_error")
def clear_error_computemanager(
compute_manager_name: str,
n4js: Neo4jStore = Depends(get_n4js_depends),
):
if not compute_manager_name.isalnum():
raise ValueError("Provided manager name is not alphanumeric")

with n4js.transaction() as tx:
compute_manager_id = n4js.get_compute_manager_id(
name=compute_manager_name, tx=tx
)
n4js.clear_errored_computemanager(compute_manager_id, tx=tx)


### add router

app.include_router(router)
89 changes: 86 additions & 3 deletions alchemiscale/compute/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@
)
from ..compression import compress_gufe_zstd, decompress_gufe_zstd
from ..models import Scope, ScopedKey
from ..storage.models import TaskHub, Task, ComputeServiceID
from ..storage.models import (
TaskHub,
Task,
ComputeServiceID,
ComputeManagerID,
ComputeManagerInstruction,
ComputeManagerStatus,
)


class AlchemiscaleComputeClientError(AlchemiscaleBaseClientError): ...
Expand All @@ -28,8 +35,15 @@ class AlchemiscaleComputeClient(AlchemiscaleBaseClient):

_exception = AlchemiscaleComputeClientError

def register(self, compute_service_id: ComputeServiceID):
res = self._post_resource(f"/computeservice/{compute_service_id}/register", {})
def register(
self,
compute_service_id: ComputeServiceID,
compute_manager_id: ComputeManagerID | None = None,
):
res = self._post_resource(
f"/computeservice/{compute_service_id}/register",
{"compute_manager_id": compute_manager_id},
)
return ComputeServiceID(res)

def deregister(self, compute_service_id: ComputeServiceID):
Expand Down Expand Up @@ -144,3 +158,72 @@ def set_task_result(
pdr_sk = self._post_resource(f"/tasks/{task}/results", data)

return ScopedKey.from_dict(pdr_sk)


class AlchemiscaleComputeManagerClientError(AlchemiscaleBaseClientError): ...


class AlchemiscaleComputeManagerClient(AlchemiscaleBaseClient):

_exception = AlchemiscaleComputeManagerClientError

def register(self, compute_manager_id: ComputeManagerID) -> ComputeManagerID:
Comment thread
dotsdl marked this conversation as resolved.
res = self._post_resource(f"/computemanager/{compute_manager_id}/register", {})
return ComputeManagerID(res)

def deregister(self, compute_manager_id: ComputeManagerID) -> ComputeManagerID:
res = self._post_resource(
f"/computemanager/{compute_manager_id}/deregister", {}
)
return ComputeManagerID(res)

def get_instruction(
self,
scopes: list[Scope],
compute_manager_id: ComputeManagerID,
) -> tuple[ComputeManagerInstruction, dict]:
instruction_data = self._post_resource(
f"/computemanager/{compute_manager_id}/instruction",
{"scopes": [scope.to_dict() for scope in scopes]},
)

match instruction_data:
case {
"instruction": "OK",
"compute_service_ids": ids,
"num_tasks": num_tasks,
}:
return ComputeManagerInstruction.OK, {
"compute_service_ids": ids,
"num_tasks": num_tasks,
}
case {"instruction": "SKIP", "compute_service_ids": ids}:
return ComputeManagerInstruction.SKIP, {"compute_service_ids": ids}
case {"instruction": "SHUTDOWN", "message": message}:
return ComputeManagerInstruction.SHUTDOWN, {"message": message}
case _:
raise self._exception(
f"Received unknown instruction pattern: {instruction_data}"
)

def update_status(
self,
compute_manager_id: ComputeManagerID,
status: ComputeManagerStatus,
*,
detail: str | None = None,
saturation: float | None = None,
) -> ComputeManagerID:
payload = {"detail": detail, "saturation": saturation, "status": str(status)}
res = self._post_resource(
f"/computemanager/{compute_manager_id}/status",
payload,
)

return ComputeManagerID(res)

def clear_error(self, compute_manager_name: str):
res = self._post_resource(
f"/computemanager/{compute_manager_name}/clear_error",
{},
)
Loading