Skip to content

Commit dc9d6a6

Browse files
committed
fix: manage config changes even when actor disabled
1 parent 10d60b4 commit dc9d6a6

4 files changed

Lines changed: 80 additions & 23 deletions

File tree

bec_server/bec_server/actors/builtin_actor_manager.py

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from threading import Event, Thread
2+
from typing import TypeVar
23

34
from bec_lib.client import BECClient, ServiceConfig
45
from bec_lib.connector import MessageObject
@@ -7,12 +8,29 @@
78
from bec_lib.messages import (
89
BuiltinActorStateChangeNotification,
910
BuiltinActorStateUpdatedNotification,
11+
ScanInterlockModifyStateTableMessage,
12+
ScanInterlockStateTableContent,
1013
)
1114
from bec_server.actors.actor import ActorBase
1215
from bec_server.actors.scan_interlock import ScanInterlockActor
1316

1417
logger = bec_logger.logger
1518

19+
ActorType = TypeVar("ActorType", bound=ActorBase)
20+
21+
22+
class ActorDict(dict):
23+
def __setitem__(self, key: type[ActorType], value: tuple[ActorType, Thread, Event], /) -> None:
24+
return super().__setitem__(key, value)
25+
26+
def __getitem__(self, key: type[ActorType], /) -> tuple[ActorType, Thread, Event]:
27+
return super().__getitem__(key)
28+
29+
def get( # type: ignore
30+
self, key: type[ActorType], default: tuple[ActorType, Thread, Event] | None = None
31+
) -> tuple[ActorType, Thread, Event]:
32+
return super().get(key) # type: ignore
33+
1634

1735
class BuiltinActorManager:
1836
"""A simple manager for builtin actors which are always available - only handles enabling and
@@ -25,12 +43,15 @@ def __init__(self, bootstrap_server: str) -> None:
2543
name="BuiltinActors",
2644
)
2745
self._client.start()
28-
self._actors_threads_and_stops: dict[str, tuple[ActorBase, Thread, Event]] = {}
46+
self._actors_threads_and_stops = ActorDict()
2947
self._builtin_actors = {cls.__name__: cls for cls in (ScanInterlockActor,)}
3048
self._start_all()
3149
self._client.connector.register(
3250
MessageEndpoints.builtin_actor_update_req_notif(), cb=self._on_state_changed
3351
)
52+
self._client.connector.register(
53+
MessageEndpoints.modify_interlock_table(), cb=self._modify_interlock_table
54+
)
3455

3556
def _ping_clients(self, actor_name: str):
3657
self._client.connector.send(
@@ -51,33 +72,68 @@ def _on_state_changed(self, msg_obj: MessageObject):
5172
self._ping_clients(msg.actor_name)
5273

5374
def _start_all(self):
54-
for actor_class in self._builtin_actors.values():
55-
if self._client.builtin_actors.check_enabled(actor_class.__name__):
56-
self._start_actor(actor_class)
75+
for actor_class_name in self._builtin_actors:
76+
if self._client.builtin_actors.check_enabled(actor_class_name):
77+
self._start_actor(self._builtin_actors[actor_class_name])
5778

5879
def _start_actor(self, actor_class: type[ActorBase]):
5980
name = actor_class.__name__
6081
logger.info(f"Starting {name}")
61-
if name in self._actors_threads_and_stops:
82+
if actor_class in self._actors_threads_and_stops:
6283
logger.warning(f"Actor {name} is already active!")
6384
return
6485
actor = actor_class(self._client, name=name, exec_id=name)
6586
t = Thread(target=actor.run)
66-
self._actors_threads_and_stops[name] = (actor, t, actor.stop_event)
87+
self._actors_threads_and_stops[actor_class] = (actor, t, actor.stop_event)
6788
t.start()
6889

6990
def _stop_actor(self, actor_name: str):
7091
logger.info(f"Stopping {actor_name}")
71-
if (entry := self._actors_threads_and_stops.get(actor_name)) is None:
92+
actor_class = self._builtin_actors.get(actor_name)
93+
if (entry := self._actors_threads_and_stops.get(actor_class)) is None:
7294
logger.warning(f"Actor {actor_name} is not active!")
7395
return
7496
actor, t, event = entry
7597
event.set()
7698
t.join()
77-
del self._actors_threads_and_stops[actor_name]
99+
del self._actors_threads_and_stops[actor_class]
78100
del actor
79101

80102
def shutdown(self):
81103
for actor in self._actors_threads_and_stops:
82104
self._stop_actor(actor)
83105
self._client.shutdown()
106+
107+
# Actor specific management methods:
108+
def _modify_interlock_table(self, msg_dict):
109+
"""Update the watched states for ScanInterlockActor - handled by the actor itself if it is
110+
active, otherwise just the config in redis is updated."""
111+
msg: ScanInterlockModifyStateTableMessage = msg_dict["data"]
112+
if (ats := self._actors_threads_and_stops.get(ScanInterlockActor)) is not None:
113+
actor, _, _ = ats
114+
actor._on_state_modification(msg)
115+
else:
116+
states: ScanInterlockStateTableContent | None = self._client.connector.get(
117+
MessageEndpoints.scan_interlock_states()
118+
)
119+
current_watched = states.states_watched if states is not None else {}
120+
if msg.action == "add":
121+
logger.info(f"Adding {msg.state_name} to the scan interlock actor")
122+
current_watched[msg.state_name] = msg.status
123+
self._client.connector.set(
124+
MessageEndpoints.scan_interlock_states(),
125+
ScanInterlockStateTableContent(states_watched=current_watched),
126+
)
127+
elif msg.action == "remove_all":
128+
self._client.connector.set(
129+
MessageEndpoints.scan_interlock_states(),
130+
ScanInterlockStateTableContent(states_watched={}),
131+
)
132+
else:
133+
logger.info(f"Removing {msg.state_name} from the scan interlock actor")
134+
current_watched.pop(msg.state_name, None)
135+
self._client.connector.set(
136+
MessageEndpoints.scan_interlock_states(),
137+
ScanInterlockStateTableContent(states_watched=current_watched),
138+
)
139+
self._ping_clients("ScanInterlockActor")

bec_server/bec_server/actors/scan_interlock.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ def __init__(self, client: BECClient, name: str, exec_id: str):
2626
self.state_table = {}
2727

2828
super().__init__(client, name, exec_id)
29-
self.client.connector.register(
30-
MessageEndpoints.modify_interlock_table(), cb=self._on_state_modification
31-
)
3229

3330
def _ping_clients(self):
3431
logger.warning(self.name)
@@ -44,8 +41,7 @@ def _update_watched_states_in_redis(self):
4441
)
4542
self._ping_clients()
4643

47-
def _on_state_modification(self, msg_dict: dict):
48-
msg: ScanInterlockModifyStateTableMessage = msg_dict["data"]
44+
def _on_state_modification(self, msg: ScanInterlockModifyStateTableMessage):
4945
with self.state_table_lock:
5046
if msg.action == "add":
5147
logger.info(f"Adding {msg.state_name} to the scan interlock actor")

bec_server/tests/tests_scan_server/test_builtin_actor_manager.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,25 @@ def mocked_manager():
2929
mock_client_cls.return_value = mock_client
3030

3131
manager = BuiltinActorManager("localhost:6379")
32-
yield manager, mock_client
32+
with patch.object(manager, "_builtin_actors", {"DummyActor": DummyActor}):
33+
yield manager, mock_client
3334

3435

3536
def test_init_registers_callback(mocked_manager):
3637
manager, mock_client = mocked_manager
3738

3839
mock_client.start.assert_called_once()
3940

40-
mock_client.connector.register.assert_called_once()
41-
args, kwargs = mock_client.connector.register.call_args
41+
assert mock_client.connector.register.call_count == 2
4242

43+
kwargs = mock_client.connector.register.call_args_list[0].kwargs
4344
assert "cb" in kwargs
4445
assert kwargs["cb"] == manager._on_state_changed
4546

47+
kwargs = mock_client.connector.register.call_args_list[1].kwargs
48+
assert "cb" in kwargs
49+
assert kwargs["cb"] == manager._modify_interlock_table
50+
4651

4752
def test_start_actor_starts_thread(mocked_manager):
4853
manager, _ = mocked_manager
@@ -53,9 +58,9 @@ def test_start_actor_starts_thread(mocked_manager):
5358

5459
manager._start_actor(DummyActor)
5560

56-
assert "DummyActor" in manager._actors_threads_and_stops
61+
assert DummyActor in manager._actors_threads_and_stops
5762

58-
actor, thread, stop_event = manager._actors_threads_and_stops["DummyActor"]
63+
actor, thread, stop_event = manager._actors_threads_and_stops[DummyActor]
5964

6065
assert isinstance(actor, DummyActor)
6166
assert thread == mock_thread
@@ -80,7 +85,7 @@ def test_stop_actor_sets_event_and_joins(mocked_manager):
8085
actor = DummyActor(None, "DummyActor", "DummyActor")
8186
mock_thread = MagicMock()
8287

83-
manager._actors_threads_and_stops["DummyActor"] = (actor, mock_thread, actor.stop_event)
88+
manager._actors_threads_and_stops[DummyActor] = (actor, mock_thread, actor.stop_event)
8489

8590
manager._stop_actor("DummyActor")
8691

bec_server/tests/tests_scan_server/test_scan_interlock.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def test_on_state_modification_remove_all(self, actor, mock_client):
5252
msg.status = None
5353

5454
with patch("bec_server.actors.actor.BlStateActor.evaluate"):
55-
actor._on_state_modification({"data": msg})
55+
actor._on_state_modification(msg)
5656

5757
assert actor.state_table == {}
5858
assert actor.state_cache == {}
@@ -63,7 +63,7 @@ def test_on_state_modification_add(self, actor, mock_client):
6363
actor._update_cache = MagicMock()
6464
mock_client.connector.register.reset_mock()
6565
msg = MagicMock(action="add", state_name="beam_ok", status="valid")
66-
actor._on_state_modification({"data": msg})
66+
actor._on_state_modification(msg)
6767
assert actor.state_table["beam_ok"] == "valid"
6868
mock_client.connector.register.assert_called_once()
6969
actor._update_cache.assert_called_once()
@@ -76,7 +76,7 @@ def test_on_state_modification_remove(self, actor, mock_client):
7676
msg = MagicMock(action="remove", state_name="beam_ok", status=None)
7777

7878
with patch("bec_server.actors.actor.BlStateActor.evaluate"):
79-
actor._on_state_modification({"data": msg})
79+
actor._on_state_modification(msg)
8080

8181
assert "beam_ok" not in actor.state_table
8282
assert "beam_ok" not in actor.state_cache
@@ -88,7 +88,7 @@ def test_on_state_modification_remove_missing(self, actor, mock_client):
8888
msg = MagicMock(action="remove", state_name="missing", status=None)
8989

9090
with patch("bec_server.actors.actor.BlStateActor.evaluate"):
91-
actor._on_state_modification({"data": msg})
91+
actor._on_state_modification(msg)
9292

9393
mock_client.connector.unregister.assert_not_called()
9494

0 commit comments

Comments
 (0)