Periodic refresh into the Key-Value Store¶
A typical use of the Scheduler + Store pair: every N seconds, the Scheduler fires a RefreshCatalog event; a manager handles it, computes a fresh value, and writes it into the Store via Set. Other managers read the cached value with Get instead of recomputing.
This pattern keeps expensive lookups out of the request path.
Events¶
# events/catalog.py
from edri.dataclass.event import Event, event
from edri.dataclass.response import Response, response
@response
class CatalogSnapshotResponse(Response):
items: list[str]
@event
class RefreshCatalog(Event):
"""Internal event — emitted by the Scheduler."""
@event
class GetCatalog(Event):
"""Request the latest cached snapshot."""
response: CatalogSnapshotResponse
RefreshCatalog has no response; it's a fire-and-forget signal.
Manager¶
The manager has two jobs:
Producer — handle
RefreshCatalogticks, recompute, write into the Store.Consumer — handle
GetCatalogrequests by asking the Store and completing the original event when the Store replies.
EDRI threads the original event through Get.data, so the response handler can fish it back out.
# managers/catalog.py
from edri.abstract import ManagerBase, request, response
from edri.events.edri.store import Get, Set
from events.catalog import GetCatalog, RefreshCatalog
CACHE_KEY = "catalog_snapshot"
class CatalogManager(ManagerBase):
@request
def refresh(self, event: RefreshCatalog) -> None:
items = self._load_items_from_source()
self.router_queue.put(Set(name=CACHE_KEY, value=items))
self.logger.debug("Catalog refreshed: %d items", len(items))
@request
def serve(self, event: GetCatalog) -> None:
# Ask the Store for the cached value. The original GetCatalog rides
# along on Get.data so we can match it up when the reply arrives.
self.router_queue.put(Get(name=CACHE_KEY, data=event))
@response
def on_store_reply(self, event: Get) -> None:
original: GetCatalog | None = event.data # type: ignore[assignment]
if original is None:
return # Not for us — some other consumer triggered this Get.
original.response.items = event.response.value or []
self.router_queue.put(original)
def _load_items_from_source(self) -> list[str]:
# Replace with the real expensive lookup.
return ["alpha", "beta", "gamma"]
Notes:
@responsedeclares a handler for replies (here, theGetreply from the Store).The Store ignores
Get.data— it only fillsresponse.value— butdatasurvives the round trip because it's a normal dataclass field.Re-emitting
originalreturns the completedGetCatalogto the router for delivery back to the API client (or whichever caller produced it).
Entry point¶
# main.py
from datetime import datetime, timedelta
from edri import EDRI
from edri.utility.manager.scheduler import Job
from events.catalog import RefreshCatalog
from managers.catalog import CatalogManager
if __name__ == "__main__":
edri = EDRI()
# Specialized managers.
edri.start_store()
edri.start_scheduler(jobs=[
Job(
event=RefreshCatalog(),
when=datetime.now(), # fire immediately on start
repeat=timedelta(seconds=30), # then every 30 s
),
])
# Application manager.
edri.add_component(CatalogManager(edri.router_queue))
edri.run()
What happens at runtime:
start_store()brings up the Key-Value Store.start_scheduler(jobs=[...])registers the recurringRefreshCatalogjob.On the first tick (and every 30 s after), the Scheduler emits
RefreshCatalog.CatalogManager.refreshcomputes a new snapshot and writes it to the Store.A consumer emits
GetCatalog;CatalogManager.serveasks the Store;on_store_replyreturns the completed event.
Scheduling jobs at runtime¶
You don't have to declare jobs at startup. From inside a manager, send a Set event to the scheduler:
from edri.events.edri.scheduler import Set as SchedulerSet
self.router_queue.put(
SchedulerSet(
event=RefreshCatalog(),
when=datetime.now() + timedelta(minutes=5),
repeat=timedelta(hours=1),
identifier="catalog-refresh",
)
)
Cancel, Update, and SetUpdate are documented in Scheduler control events.