Skip to content

Commit 4e2e7c3

Browse files
committed
Refresh the dispatches after a stream restart.
To ensure we didn't miss any. Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent 2c58f8b commit 4e2e7c3

File tree

3 files changed

+72
-32
lines changed

3 files changed

+72
-32
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* The dispatcher offers two new parameters to control the client's call and stream timeout:
1414
- `call_timeout`: The maximum time to wait for a response from the client.
1515
- `stream_timeout`: The maximum time to wait before restarting a stream.
16+
* While the dispatch stream restarts we refresh our dispatch cache as well, to ensure we didn't miss any updates.
1617

1718
## Bug Fixes
1819

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ dependencies = [
3838
# Make sure to update the version for cross-referencing also in the
3939
# mkdocs.yml file when changing the version here (look for the config key
4040
# plugins.mkdocstrings.handlers.python.import)
41-
"frequenz-sdk >= 1.0.0-rc1900, < 1.0.0-rc2100",
41+
"frequenz-sdk >= 1.0.0-rc2002, < 1.0.0-rc2100",
4242
"frequenz-channels >= 1.6.1, < 2.0.0",
43-
"frequenz-client-dispatch >= 0.10.2, < 0.11.0",
43+
"frequenz-client-dispatch >= 0.11.0, < 0.12.0",
44+
"frequenz-client-common >= 0.3.2, < 0.4.0",
45+
"frequenz-client-base >= 0.11.0, < 0.12.0",
4446
]
4547
dynamic = ["version"]
4648

src/frequenz/dispatch/_bg_service.py

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@
1818
import grpc.aio
1919
from frequenz.channels import Broadcast, Receiver, select, selected_from
2020
from frequenz.channels.timer import SkipMissedAndResync, Timer
21+
from frequenz.client.base.streaming import (
22+
StreamFatalError,
23+
StreamRetrying,
24+
StreamStarted,
25+
)
2126
from frequenz.client.dispatch import DispatchApiClient
27+
from frequenz.client.dispatch.types import DispatchEvent as ApiDispatchEvent
2228
from frequenz.client.dispatch.types import Event
2329
from frequenz.sdk.actor import BackgroundService
2430

@@ -230,8 +236,21 @@ async def _run(self) -> None:
230236
) as next_event_timer:
231237
# Initial fetch
232238
await self._fetch(next_event_timer)
233-
stream = self._client.stream(microgrid_id=self._microgrid_id)
234239

240+
# pylint: disable-next=protected-access
241+
streamer = self._client._get_stream(microgrid_id=self._microgrid_id)
242+
stream = streamer.new_receiver(include_events=True)
243+
244+
# We track stream start events linked to retries to avoid re-fetching
245+
# dispatches that were already retrieved during an initial stream start.
246+
# The initial fetch gets all dispatches, and the StreamStarted event
247+
# isn't always reliable due to parallel receiver creation and stream
248+
# task initiation.
249+
# This way we get a deterministic behavior where we only fetch
250+
# dispatches once initially and then only when the stream is restarted.
251+
is_retry_attempt = False
252+
253+
# Streaming updates
235254
async for selected in select(next_event_timer, stream):
236255
if selected_from(selected, next_event_timer):
237256
if not self._scheduled_events:
@@ -240,36 +259,54 @@ async def _run(self) -> None:
240259
heappop(self._scheduled_events).dispatch, next_event_timer
241260
)
242261
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)
262+
match selected.message:
263+
case ApiDispatchEvent():
264+
_logger.debug(
265+
"Received dispatch event: %s", selected.message
272266
)
267+
dispatch = Dispatch(selected.message.dispatch)
268+
match selected.message.event:
269+
case Event.CREATED:
270+
self._dispatches[dispatch.id] = dispatch
271+
await self._update_dispatch_schedule_and_notify(
272+
dispatch, None, next_event_timer
273+
)
274+
await self._lifecycle_events_tx.send(
275+
Created(dispatch=dispatch)
276+
)
277+
case Event.UPDATED:
278+
await self._update_dispatch_schedule_and_notify(
279+
dispatch,
280+
self._dispatches[dispatch.id],
281+
next_event_timer,
282+
)
283+
self._dispatches[dispatch.id] = dispatch
284+
await self._lifecycle_events_tx.send(
285+
Updated(dispatch=dispatch)
286+
)
287+
case Event.DELETED:
288+
self._dispatches.pop(dispatch.id)
289+
await self._update_dispatch_schedule_and_notify(
290+
None, dispatch, next_event_timer
291+
)
292+
293+
await self._lifecycle_events_tx.send(
294+
Deleted(dispatch=dispatch)
295+
)
296+
297+
case StreamRetrying():
298+
is_retry_attempt = True
299+
300+
case StreamStarted():
301+
if is_retry_attempt:
302+
_logger.info(
303+
"Dispatch stream restarted, getting dispatches"
304+
)
305+
await self._fetch(next_event_timer)
306+
is_retry_attempt = False
307+
308+
case StreamFatalError():
309+
pass
273310

274311
async def _execute_scheduled_event(self, dispatch: Dispatch, timer: Timer) -> None:
275312
"""Execute a scheduled event.

0 commit comments

Comments
 (0)