Skip to content

Commit 101232a

Browse files
authored
Merge branch 'main' into feat/extract_archive
2 parents 8b54de7 + 08a386c commit 101232a

16 files changed

Lines changed: 884 additions & 272 deletions

File tree

alchemiscale/cli.py

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import click
88
import yaml
99
import json
10-
import signal
1110

1211
from .security.models import (
1312
CredentialedEntity,
@@ -382,31 +381,23 @@ def synchronous(config_file, name, compute_manager_id):
382381
from alchemiscale.models import Scope
383382
from alchemiscale.compute.service import SynchronousComputeService
384383
from alchemiscale.compute.settings import ComputeServiceSettings
384+
from alchemiscale.compute.signals import install_stop_handlers
385385

386386
params = yaml.safe_load(config_file)
387387

388-
params_init = params.get("init", {})
389-
params_start = params.get("start", {})
390-
391388
if name is not None:
392-
params_init["name"] = name
389+
params["name"] = name
393390

394391
if compute_manager_id is not None:
395-
params_init["compute_manager_id"] = compute_manager_id
396-
397-
service = SynchronousComputeService(ComputeServiceSettings(**params_init))
398-
399-
# add signal handling
400-
for signame in {"SIGHUP", "SIGINT", "SIGTERM"}:
392+
params["compute_manager_id"] = compute_manager_id
401393

402-
def stop(*args, **kwargs):
403-
service.stop()
404-
raise KeyboardInterrupt()
394+
service = SynchronousComputeService(ComputeServiceSettings(**params))
405395

406-
signal.signal(getattr(signal, signame), stop)
396+
# install handlers so SIGHUP/SIGINT/SIGTERM stop the service cleanly
397+
install_stop_handlers(service)
407398

408399
try:
409-
service.start(**params_start)
400+
service.start()
410401
except KeyboardInterrupt:
411402
pass
412403

@@ -551,6 +542,7 @@ def strategist(config_file):
551542
from alchemiscale.models import Scope
552543
from alchemiscale.strategist.service import StrategistService
553544
from alchemiscale.strategist.settings import StrategistSettings
545+
from alchemiscale.compute.signals import install_stop_handlers
554546

555547
params = yaml.safe_load(config_file)
556548

@@ -559,18 +551,12 @@ def strategist(config_file):
559551

560552
service = StrategistService(StrategistSettings(**params))
561553

562-
# add signal handling
563-
for signame in {"SIGHUP", "SIGINT", "SIGTERM"}:
564-
565-
def stop(*args, **kwargs):
566-
service.stop()
554+
# install handlers so SIGHUP/SIGINT/SIGTERM stop the service cleanly.
555+
# do *not* raise KeyboardInterrupt --- the strategist's stop() triggers a
556+
# ProcessPoolExecutor shutdown that must not be interrupted partway through.
557+
install_stop_handlers(service, raise_keyboard_interrupt=False)
567558

568-
signal.signal(getattr(signal, signame), stop)
569-
570-
try:
571-
service.start()
572-
except KeyboardInterrupt:
573-
pass
559+
service.start()
574560

575561

576562
@cli.group(help="Subcommands for managing identities")

alchemiscale/compute/manager.py

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
from abc import abstractmethod
8+
from contextlib import contextmanager
89
import logging
910
import time
1011

@@ -18,6 +19,7 @@
1819
AlchemiscaleComputeManagerClientError,
1920
)
2021
from .settings import ComputeManagerSettings, ComputeServiceSettings
22+
from ..sleep import InterruptableSleep, SleepInterrupted
2123

2224

2325
class ComputeManager:
@@ -44,6 +46,7 @@ def __init__(
4446
)
4547

4648
self._stop = False
49+
self.int_sleep = InterruptableSleep()
4750

4851
logger = logging.getLogger("AlchemiscaleComputeManager")
4952
logger.setLevel(self.settings.loglevel)
@@ -75,34 +78,73 @@ def _register(self, steal=False):
7578
def _deregister(self):
7679
self.client.deregister(self.compute_manager_id)
7780

78-
def start(self, max_cycles: int | None = None, steal=False):
79-
self.logger.info(f"Starting up compute manager '{self.settings.name}'")
80-
self._register(steal=steal)
81-
self.logger.info(f"Registered compute manager '{self.compute_manager_id}'")
82-
self._stop = False
81+
@contextmanager
82+
def _running(self, steal=False):
83+
"""Register this compute manager for the lifetime of the context.
84+
85+
This guarantees that if anything interrupts startup after registration,
86+
including int_sleep.clear(), the manager is still deregistered.
87+
"""
88+
registered = False
89+
8390
try:
84-
count = 0
85-
self.logger.info("Starting main loop")
86-
while self.cycle():
87-
count += 1
88-
if max_cycles and count >= max_cycles:
89-
self.logger.info("Reached maximum number of cycles")
90-
break
91-
self.logger.info(f"Sleeping for {self.settings.sleep_interval} seconds")
92-
time.sleep(self.settings.sleep_interval)
93-
except Exception as e:
94-
self.logger.error(f"Unknown exception raised: '{str(e)}'")
95-
self.logger.info(f"Updating manager status to 'ERROR'")
96-
self.client.update_status(
97-
self.compute_manager_id, ComputeManagerStatus.ERROR, detail=repr(e)
98-
)
99-
raise e
100-
except KeyboardInterrupt:
101-
self.logger.info("Caught SIGINT/Keyboard interrupt.")
91+
self._register(steal=steal)
92+
registered = True
93+
94+
self.logger.info(f"Registered compute manager '{self.compute_manager_id}'")
95+
96+
self._stop = False
97+
self.int_sleep.clear()
98+
99+
yield
100+
102101
finally:
103-
self.logger.info(f"Deregistering '{self.compute_manager_id}'")
104-
self._deregister()
105-
self.logger.info(f"Deregistration successful")
102+
if registered:
103+
self.logger.info(f"Deregistering '{self.compute_manager_id}'")
104+
105+
# kept here in case we add additional cleanup to stop later, such as other threads
106+
self.stop()
107+
108+
self._deregister()
109+
self.logger.info("Deregistration successful")
110+
111+
def start(self, max_cycles: int | None = None, steal=False):
112+
self.logger.info(f"Starting up compute manager '{self.settings.name}'")
113+
114+
with self._running(steal=steal):
115+
try:
116+
count = 0
117+
self.logger.info("Starting main loop")
118+
119+
while self.cycle():
120+
count += 1
121+
122+
if max_cycles and count >= max_cycles:
123+
self.logger.info("Reached maximum number of cycles")
124+
break
125+
126+
self.logger.info(
127+
f"Sleeping for {self.settings.sleep_interval} seconds"
128+
)
129+
self.int_sleep(self.settings.sleep_interval)
130+
131+
except SleepInterrupted:
132+
self.logger.info("Compute manager stopping.")
133+
134+
except KeyboardInterrupt:
135+
self.logger.info("Caught SIGINT/Keyboard interrupt.")
136+
137+
except Exception as e:
138+
self.logger.error(f"Unknown exception raised: '{str(e)}'")
139+
self.logger.info("Updating manager status to 'ERROR'")
140+
141+
self.client.update_status(
142+
self.compute_manager_id,
143+
ComputeManagerStatus.ERROR,
144+
detail=repr(e),
145+
)
146+
147+
raise
106148

107149
@abstractmethod
108150
def create_compute_services(self, data: dict, target: int) -> int:
@@ -189,6 +231,7 @@ def _compute_jobs_to_create(self, num_tasks: int, num_active_services: int) -> i
189231
return jobs or 1
190232

191233
def stop(self):
234+
self.int_sleep.interrupt()
192235
self._stop = True
193236

194237
def cycle(self) -> bool:

0 commit comments

Comments
 (0)