1818import grpc .aio
1919from frequenz .channels import Broadcast , Receiver , select , selected_from
2020from frequenz .channels .timer import SkipMissedAndResync , Timer
21+ from frequenz .client .base .streaming import (
22+ StreamFatalError ,
23+ StreamRetrying ,
24+ StreamStarted ,
25+ )
26+ from frequenz .client .common .microgrid import MicrogridId
2127from frequenz .client .dispatch import DispatchApiClient
22- from frequenz .client .dispatch .types import Event
28+ from frequenz .client .dispatch .types import DispatchEvent as ApiDispatchEvent
29+ from frequenz .client .dispatch .types import DispatchId , Event
2330from frequenz .sdk .actor import BackgroundService
2431
32+ from ._actor_dispatcher import DispatchActorId
2533from ._dispatch import Dispatch
2634from ._event import Created , Deleted , DispatchEvent , Updated
2735
@@ -33,11 +41,13 @@ class MergeStrategy(ABC):
3341 """Base class for strategies to merge running intervals."""
3442
3543 @abstractmethod
36- def identity (self , dispatch : Dispatch ) -> int :
44+ def identity (self , dispatch : Dispatch ) -> DispatchActorId :
3745 """Identity function for the merge criteria."""
3846
3947 @abstractmethod
40- def filter (self , dispatches : Mapping [int , Dispatch ], dispatch : Dispatch ) -> bool :
48+ def filter (
49+ self , dispatches : Mapping [DispatchId , Dispatch ], dispatch : Dispatch
50+ ) -> bool :
4151 """Filter dispatches based on the strategy.
4252
4353 Args:
@@ -75,7 +85,7 @@ class QueueItem:
7585 to consider the start event when deciding whether to execute the
7686 stop event.
7787 """
78- dispatch_id : int
88+ dispatch_id : DispatchId
7989 dispatch : Dispatch = field (compare = False )
8090
8191 def __init__ (
@@ -90,7 +100,7 @@ def __init__(
90100 # pylint: disable=too-many-arguments
91101 def __init__ (
92102 self ,
93- microgrid_id : int ,
103+ microgrid_id : MicrogridId ,
94104 client : DispatchApiClient ,
95105 ) -> None :
96106 """Initialize the background service.
@@ -102,7 +112,7 @@ def __init__(
102112 super ().__init__ (name = "dispatch" )
103113
104114 self ._client = client
105- self ._dispatches : dict [int , Dispatch ] = {}
115+ self ._dispatches : dict [DispatchId , Dispatch ] = {}
106116 self ._microgrid_id = microgrid_id
107117
108118 self ._lifecycle_events_channel = Broadcast [DispatchEvent ](
@@ -230,8 +240,21 @@ async def _run(self) -> None:
230240 ) as next_event_timer :
231241 # Initial fetch
232242 await self ._fetch (next_event_timer )
233- stream = self ._client .stream (microgrid_id = self ._microgrid_id )
234243
244+ # pylint: disable-next=protected-access
245+ streamer = self ._client ._get_stream (microgrid_id = self ._microgrid_id )
246+ stream = streamer .new_receiver (include_events = True )
247+
248+ # We track stream start events linked to retries to avoid re-fetching
249+ # dispatches that were already retrieved during an initial stream start.
250+ # The initial fetch gets all dispatches, and the StreamStarted event
251+ # isn't always reliable due to parallel receiver creation and stream
252+ # task initiation.
253+ # This way we get a deterministic behavior where we only fetch
254+ # dispatches once initially and then only when the stream is restarted.
255+ is_retry_attempt = False
256+
257+ # Streaming updates
235258 async for selected in select (next_event_timer , stream ):
236259 if selected_from (selected , next_event_timer ):
237260 if not self ._scheduled_events :
@@ -240,36 +263,54 @@ async def _run(self) -> None:
240263 heappop (self ._scheduled_events ).dispatch , next_event_timer
241264 )
242265 elif selected_from (selected , stream ):
243- _logger .debug ("Received dispatch event: %s" , selected .message )
244- dispatch = Dispatch (selected .message .dispatch )
245- match selected .message .event :
246- case Event .CREATED :
247- self ._dispatches [dispatch .id ] = dispatch
248- await self ._update_dispatch_schedule_and_notify (
249- dispatch , None , next_event_timer
250- )
251- await self ._lifecycle_events_tx .send (
252- Created (dispatch = dispatch )
253- )
254- case Event .UPDATED :
255- await self ._update_dispatch_schedule_and_notify (
256- dispatch ,
257- self ._dispatches [dispatch .id ],
258- next_event_timer ,
259- )
260- self ._dispatches [dispatch .id ] = dispatch
261- await self ._lifecycle_events_tx .send (
262- Updated (dispatch = dispatch )
263- )
264- case Event .DELETED :
265- self ._dispatches .pop (dispatch .id )
266- await self ._update_dispatch_schedule_and_notify (
267- None , dispatch , next_event_timer
268- )
269-
270- await self ._lifecycle_events_tx .send (
271- Deleted (dispatch = dispatch )
266+ match selected .message :
267+ case ApiDispatchEvent ():
268+ _logger .debug (
269+ "Received dispatch event: %s" , selected .message
272270 )
271+ dispatch = Dispatch (selected .message .dispatch )
272+ match selected .message .event :
273+ case Event .CREATED :
274+ self ._dispatches [dispatch .id ] = dispatch
275+ await self ._update_dispatch_schedule_and_notify (
276+ dispatch , None , next_event_timer
277+ )
278+ await self ._lifecycle_events_tx .send (
279+ Created (dispatch = dispatch )
280+ )
281+ case Event .UPDATED :
282+ await self ._update_dispatch_schedule_and_notify (
283+ dispatch ,
284+ self ._dispatches [dispatch .id ],
285+ next_event_timer ,
286+ )
287+ self ._dispatches [dispatch .id ] = dispatch
288+ await self ._lifecycle_events_tx .send (
289+ Updated (dispatch = dispatch )
290+ )
291+ case Event .DELETED :
292+ self ._dispatches .pop (dispatch .id )
293+ await self ._update_dispatch_schedule_and_notify (
294+ None , dispatch , next_event_timer
295+ )
296+
297+ await self ._lifecycle_events_tx .send (
298+ Deleted (dispatch = dispatch )
299+ )
300+
301+ case StreamRetrying ():
302+ is_retry_attempt = True
303+
304+ case StreamStarted ():
305+ if is_retry_attempt :
306+ _logger .info (
307+ "Dispatch stream restarted, getting dispatches"
308+ )
309+ await self ._fetch (next_event_timer )
310+ is_retry_attempt = False
311+
312+ case StreamFatalError ():
313+ pass
273314
274315 async def _execute_scheduled_event (self , dispatch : Dispatch , timer : Timer ) -> None :
275316 """Execute a scheduled event.
0 commit comments