Skip to content
Merged
Show file tree
Hide file tree
Changes from 46 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
134 changes: 133 additions & 1 deletion 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,14 +106,24 @@ 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
).manager_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)
Comment thread
dotsdl marked this conversation as resolved.
Outdated
Expand Down Expand Up @@ -392,6 +406,124 @@ 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 ValueError as e:
raise HTTPException(
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
details=str(e),
)
except Exception as e:
raise HTTPException(
status_code=http_status.HTTP_500_INTERNAL_SERVER_ERROR,
details=str(e),
)
Comment thread
dotsdl marked this conversation as resolved.

return compute_manager_id


@router.post("/computemanager/{compute_manager_id}/register")
def computemanager_register(
Comment thread
dotsdl marked this conversation as resolved.
Outdated
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(
manager_name=compute_manager_id.manager_name,
uuid=compute_manager_id.uuid,
registered=now,
last_status_update=now,
status=ComputeManagerStatus.OK,
detail="",
saturation=0,
)

compute_manager_id_ = n4js.register_computemanager(cm_registration)
Comment thread
dotsdl marked this conversation as resolved.
Outdated
return compute_manager_id_


@router.post("/computemanager/{compute_manager_id}/deregister")
def computemanager_deregister(
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 computemanager_get_instruction(
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.from_str("*-*-*")]
Comment thread
ianmkenney marked this conversation as resolved.
Outdated
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 computemanager_update_status(
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_ERRORED
)
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),
Comment thread
dotsdl marked this conversation as resolved.
Outdated
)
except ValueError as e:
raise HTTPException(
status_code=http_status.HTTP_400_BAD_REQUEST,
details=str(e),
)
except Exception as e:
raise HTTPException(
status_code=http_status.HTTP_500_INTERNAL_SERVER_ERROR,
details=str(e),
)
Comment thread
dotsdl marked this conversation as resolved.
Outdated

return compute_manager_id


### add router

app.include_router(router)
81 changes: 78 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,64 @@ 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 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 get_instruction(
Comment thread
dotsdl marked this conversation as resolved.
Outdated
self, compute_manager_id: ComputeManagerID
) -> tuple[ComputeManagerInstruction, dict]:
instruction_data = self._post_resource(
f"/computemanager/{compute_manager_id}/instruction",
{"scopes": []},
Comment thread
dotsdl marked this conversation as resolved.
Outdated
)

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}"
)
125 changes: 125 additions & 0 deletions alchemiscale/compute/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""
:mod:`alchemiscale.compute.manager` --- compute manager for creating compute services
=====================================================================================

"""

from abc import abstractmethod
import logging
import time

from ..storage.models import (
ComputeManagerID,
ComputeManagerInstruction,
ComputeManagerStatus,
)
from .client import AlchemiscaleComputeManagerClient
from .settings import ComputeManagerSettings


class ComputeManager:
Comment thread
dotsdl marked this conversation as resolved.

compute_manager_id: ComputeManagerID
client: AlchemiscaleComputeManagerClient
service_settings_template: bytes
manager_settings: ComputeManagerSettings
Comment thread
dotsdl marked this conversation as resolved.
Outdated

def __init__(self, settings: ComputeManagerSettings):
self.settings = settings
self.compute_manager_id = ComputeManagerID.new_from_manager_name(
self.settings.name
)
self.client = AlchemiscaleComputeManagerClient(
api_url=self.settings.api_url,
identifier=self.settings.identifier,
key=self.settings.key,
)

logger = logging.getLogger("AlchemiscaleComputeManager")
logger.setLevel(self.settings.loglevel)

extra = {"compute_manager_id": self.compute_manager_id}
formatter = logging.Formatter(
"[%(asctime)s] [%(compute_manager_id)s] [%(levelname)s] %(message)s"
)
formatter.converter = time.gmtime # use utc time for logging timestamps

sh = logging.StreamHandler()
sh.setFormatter(formatter)
logger.addHandler(sh)

if self.settings.logfile is not None:
fh = logging.FileHandler(self.settings.logfile)
fh.setFormatter(formatter)
logger.addHandler(fh)

self.logger = logging.LoggerAdapter(logger, extra)

def _register(self):
self.client.register(self.compute_manager_id)

def _deregister(self):
self.client.deregister(self.compute_manager_id)

def start(self, max_cycles: int | None = None):
Comment thread
dotsdl marked this conversation as resolved.
self._register()
try:
count = 0
while True:
self.cycle()
count += 1
if max_cycles and count >= max_cycles:
break
time.sleep(self.settings.sleep_interval)
except Exception as e:
self.client.update_status(
self.compute_manager_id, ComputeManagerStatus.ERRORED, detail=repr(e)
Comment thread
dotsdl marked this conversation as resolved.
Outdated
)
raise e
except KeyboardInterrupt:
self.logger.info("Caught SIGINT/Keyboard interrupt.")
finally:
self._deregister()

@abstractmethod
def create_compute_services(self, data) -> int:
"""Method responsible for creating compute services based on
data returned with an OK ComputeManagerInstruction. This must
return the number of compute services started.
"""
raise NotImplementedError

def cycle(self):
instruction, data = self.client.get_instruction(self.compute_manager_id)
Comment thread
dotsdl marked this conversation as resolved.
Outdated
match instruction:
case ComputeManagerInstruction.OK:
total_services = len(data["compute_service_ids"])
num_tasks = data["num_tasks"]
if (
total_services < self.settings.max_compute_services
and num_tasks > 0
):
new_services = self.create_compute_services(data)
total_services += new_services
if new_services:
self.logger.info(
f"Created {new_services} new compute service(s)"
)
else:
self.logger.info(f"No new compute services created")
case ComputeManagerInstruction.SKIP:
total_services = len(data["compute_service_ids"])
self.logger.info(f"Received skip instruction")
case ComputeManagerInstruction.SHUTDOWN:
shutdown_message = data["message"]
self.logger.info(f'Received shutdown message: "{shutdown_message}"')
return
Comment thread
dotsdl marked this conversation as resolved.
Outdated
self.client.update_status(
self.compute_manager_id,
ComputeManagerStatus.OK,
saturation=total_services / self.settings.max_compute_services,
)

@abstractmethod
def create_compute_service(self):
raise NotImplementedError
Comment thread
ianmkenney marked this conversation as resolved.
Outdated
Loading