Skip to content

Commit b769b3f

Browse files
ianmkenneydotsdl
andauthored
Autoscaling support in alchemiscale (#424)
Co-authored-by: David L. Dotson <dotsdl@gmail.com>
1 parent 92389ac commit b769b3f

16 files changed

Lines changed: 1960 additions & 14 deletions

alchemiscale/compute/api.py

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from datetime import timedelta
1010
import random
1111

12-
from fastapi import FastAPI, APIRouter, Body, Depends, Request
12+
from fastapi import FastAPI, APIRouter, Body, Depends, HTTPException, Request
13+
from fastapi import status as http_status
1314
from fastapi.middleware.gzip import GZipMiddleware
1415
from gufe.tokenization import JSON_HANDLER
1516
from gufe.protocols import ProtocolDAGResult
@@ -40,7 +41,10 @@
4041
from ..storage.models import (
4142
ProtocolDAGResultRef,
4243
ComputeServiceID,
44+
ComputeManagerID,
45+
ComputeManagerRegistration,
4346
ComputeServiceRegistration,
47+
ComputeManagerStatus,
4448
)
4549
from ..models import Scope, ScopedKey
4650
from ..security.models import (
@@ -102,17 +106,31 @@ def list_scopes(
102106
@router.post("/computeservice/{compute_service_id}/register")
103107
def register_computeservice(
104108
compute_service_id,
109+
*,
110+
compute_manager_id: str | None = Body(None, embed=True),
105111
n4js: Neo4jStore = Depends(get_n4js_depends),
106112
):
107113
now = datetime.datetime.now(tz=datetime.UTC)
114+
if compute_manager_id:
115+
manager_name = process_compute_manager_id_string(compute_manager_id).name
116+
else:
117+
manager_name = None
118+
108119
csreg = ComputeServiceRegistration(
109120
identifier=ComputeServiceID(compute_service_id),
110121
registered=now,
111122
heartbeat=now,
112123
failure_times=[],
124+
manager_name=manager_name,
113125
)
114126

115-
compute_service_id_ = n4js.register_computeservice(csreg)
127+
try:
128+
compute_service_id_ = n4js.register_computeservice(csreg)
129+
except ValueError as e:
130+
raise HTTPException(
131+
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
132+
detail=str(e),
133+
)
116134

117135
return compute_service_id_
118136

@@ -392,6 +410,136 @@ async def set_task_result(
392410
return result_sk
393411

394412

413+
def process_compute_manager_id_string(
414+
compute_manager_id_string: str,
415+
) -> ComputeManagerID:
416+
"""Try creating a ComputeManagerID from a string representation. Raise HTTPException."""
417+
try:
418+
compute_manager_id = ComputeManagerID(compute_manager_id_string)
419+
except Exception as e:
420+
raise HTTPException(
421+
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
422+
detail=str(e),
423+
)
424+
425+
return compute_manager_id
426+
427+
428+
@router.post("/computemanager/{compute_manager_id}/register")
429+
def register_computemanager(
430+
compute_manager_id,
431+
n4js: Neo4jStore = Depends(get_n4js_depends),
432+
):
433+
434+
compute_manager_id = process_compute_manager_id_string(compute_manager_id)
435+
436+
now = datetime.datetime.now(tz=datetime.UTC)
437+
cm_registration = ComputeManagerRegistration(
438+
name=compute_manager_id.name,
439+
uuid=compute_manager_id.uuid,
440+
registered=now,
441+
last_status_update=now,
442+
status=ComputeManagerStatus.OK,
443+
detail="",
444+
saturation=0,
445+
)
446+
447+
try:
448+
compute_manager_id_ = n4js.register_computemanager(cm_registration)
449+
except ValueError as e:
450+
raise HTTPException(
451+
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
452+
detail=str(e),
453+
)
454+
455+
return compute_manager_id_
456+
457+
458+
@router.post("/computemanager/{compute_manager_id}/deregister")
459+
def deregister_computemanager(
460+
compute_manager_id,
461+
n4js: Neo4jStore = Depends(get_n4js_depends),
462+
):
463+
compute_manager_id = process_compute_manager_id_string(compute_manager_id)
464+
n4js.deregister_computemanager(compute_manager_id)
465+
return compute_manager_id
466+
467+
468+
@router.post("/computemanager/{compute_manager_id}/instruction")
469+
def get_instruction_computemanager(
470+
compute_manager_id,
471+
*,
472+
scopes: list[Scope] = Body([], embed=True),
473+
n4js: Neo4jStore = Depends(get_n4js_depends),
474+
settings: ComputeAPISettings = Depends(get_base_api_settings),
475+
token: TokenData = Depends(get_token_data_depends),
476+
):
477+
scopes = scopes or [Scope()]
478+
scopes_reduced = minimize_scope_space(scopes)
479+
query_scopes = []
480+
for scope in scopes_reduced:
481+
query_scopes.extend(validate_scopes_query(scope, token))
482+
483+
compute_manager_id = process_compute_manager_id_string(compute_manager_id)
484+
now = datetime.datetime.now(tz=datetime.UTC)
485+
instruction, payload = n4js.get_computemanager_instruction(
486+
compute_manager_id,
487+
now - timedelta(seconds=settings.ALCHEMISCALE_COMPUTE_API_FORGIVE_TIME_SECONDS),
488+
settings.ALCHEMISCALE_COMPUTE_API_MAX_FAILURES,
489+
query_scopes,
490+
)
491+
payload["instruction"] = str(instruction)
492+
return payload
493+
494+
495+
@router.post("/computemanager/{compute_manager_id}/status")
496+
def update_status_computemanager(
497+
compute_manager_id,
498+
*,
499+
status: str = Body(),
500+
detail: str | None = Body(None),
501+
saturation: float | None = Body(None),
502+
n4js: Neo4jStore = Depends(get_n4js_depends),
503+
settings: ComputeAPISettings = Depends(get_base_api_settings),
504+
):
505+
expire_seconds = settings.ALCHEMISCALE_COMPUTE_API_MANAGER_EXPIRE_SECONDS
506+
expire_seconds_errored = (
507+
settings.ALCHEMISCALE_COMPUTE_API_MANAGER_EXPIRE_SECONDS_ERROR
508+
)
509+
compute_manager_id = process_compute_manager_id_string(compute_manager_id)
510+
try:
511+
n4js.update_compute_manager_status(
512+
compute_manager_id, status, detail, saturation
513+
)
514+
now = datetime.datetime.now(tz=datetime.UTC)
515+
n4js.expire_computemanager_registrations(
516+
now - timedelta(seconds=expire_seconds),
517+
now - timedelta(seconds=expire_seconds_errored),
518+
)
519+
except ValueError as e:
520+
raise HTTPException(
521+
status_code=http_status.HTTP_400_BAD_REQUEST,
522+
detail=str(e),
523+
)
524+
525+
return compute_manager_id
526+
527+
528+
@router.post("/computemanager/{compute_manager_name}/clear_error")
529+
def clear_error_computemanager(
530+
compute_manager_name: str,
531+
n4js: Neo4jStore = Depends(get_n4js_depends),
532+
):
533+
if not compute_manager_name.isalnum():
534+
raise ValueError("Provided manager name is not alphanumeric")
535+
536+
with n4js.transaction() as tx:
537+
compute_manager_id = n4js.get_compute_manager_id(
538+
name=compute_manager_name, tx=tx
539+
)
540+
n4js.clear_errored_computemanager(compute_manager_id, tx=tx)
541+
542+
395543
### add router
396544

397545
app.include_router(router)

alchemiscale/compute/client.py

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@
1717
)
1818
from ..compression import compress_gufe_zstd, decompress_gufe_zstd
1919
from ..models import Scope, ScopedKey
20-
from ..storage.models import TaskHub, Task, ComputeServiceID
20+
from ..storage.models import (
21+
TaskHub,
22+
Task,
23+
ComputeServiceID,
24+
ComputeManagerID,
25+
ComputeManagerInstruction,
26+
ComputeManagerStatus,
27+
)
2128

2229

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

2936
_exception = AlchemiscaleComputeClientError
3037

31-
def register(self, compute_service_id: ComputeServiceID):
32-
res = self._post_resource(f"/computeservice/{compute_service_id}/register", {})
38+
def register(
39+
self,
40+
compute_service_id: ComputeServiceID,
41+
compute_manager_id: ComputeManagerID | None = None,
42+
):
43+
res = self._post_resource(
44+
f"/computeservice/{compute_service_id}/register",
45+
{"compute_manager_id": compute_manager_id},
46+
)
3347
return ComputeServiceID(res)
3448

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

146160
return ScopedKey.from_dict(pdr_sk)
161+
162+
163+
class AlchemiscaleComputeManagerClientError(AlchemiscaleBaseClientError): ...
164+
165+
166+
class AlchemiscaleComputeManagerClient(AlchemiscaleBaseClient):
167+
168+
_exception = AlchemiscaleComputeManagerClientError
169+
170+
def register(self, compute_manager_id: ComputeManagerID) -> ComputeManagerID:
171+
res = self._post_resource(f"/computemanager/{compute_manager_id}/register", {})
172+
return ComputeManagerID(res)
173+
174+
def deregister(self, compute_manager_id: ComputeManagerID) -> ComputeManagerID:
175+
res = self._post_resource(
176+
f"/computemanager/{compute_manager_id}/deregister", {}
177+
)
178+
return ComputeManagerID(res)
179+
180+
def get_instruction(
181+
self,
182+
scopes: list[Scope],
183+
compute_manager_id: ComputeManagerID,
184+
) -> tuple[ComputeManagerInstruction, dict]:
185+
instruction_data = self._post_resource(
186+
f"/computemanager/{compute_manager_id}/instruction",
187+
{"scopes": [scope.to_dict() for scope in scopes]},
188+
)
189+
190+
match instruction_data:
191+
case {
192+
"instruction": "OK",
193+
"compute_service_ids": ids,
194+
"num_tasks": num_tasks,
195+
}:
196+
return ComputeManagerInstruction.OK, {
197+
"compute_service_ids": ids,
198+
"num_tasks": num_tasks,
199+
}
200+
case {"instruction": "SKIP", "compute_service_ids": ids}:
201+
return ComputeManagerInstruction.SKIP, {"compute_service_ids": ids}
202+
case {"instruction": "SHUTDOWN", "message": message}:
203+
return ComputeManagerInstruction.SHUTDOWN, {"message": message}
204+
case _:
205+
raise self._exception(
206+
f"Received unknown instruction pattern: {instruction_data}"
207+
)
208+
209+
def update_status(
210+
self,
211+
compute_manager_id: ComputeManagerID,
212+
status: ComputeManagerStatus,
213+
*,
214+
detail: str | None = None,
215+
saturation: float | None = None,
216+
) -> ComputeManagerID:
217+
payload = {"detail": detail, "saturation": saturation, "status": str(status)}
218+
res = self._post_resource(
219+
f"/computemanager/{compute_manager_id}/status",
220+
payload,
221+
)
222+
223+
return ComputeManagerID(res)
224+
225+
def clear_error(self, compute_manager_name: str):
226+
res = self._post_resource(
227+
f"/computemanager/{compute_manager_name}/clear_error",
228+
{},
229+
)

0 commit comments

Comments
 (0)