Skip to content

Commit

Permalink
Add server poll (#53)
Browse files Browse the repository at this point in the history
Rename "poll" to "get_event_update" since there were confusion with
the other "poll" that asks the server to poll the source of a specific
event for updates.

* Improve UpdateHandler
* More docs
* Easier to use, hopefully
* Add support for polling the server, by asking the server to check for updates for a specific event.
* Ensure events are removed consistently and safely
* Improve ZinoError renaming
  • Loading branch information
hmpf authored Apr 2, 2024
1 parent 3ac8081 commit 2dabbb7
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 18 deletions.
2 changes: 1 addition & 1 deletion src/zinolib/controllers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def _set_event(self, event: Event):

def remove_event(self, event_or_id: EventOrId):
event_id = self._get_event_id(event_or_id)
self.events.pop(event_id)
self.events.pop(event_id, None)
self.removed_ids.add(event_id)

def _verify_session(self, quiet=False):
Expand Down
84 changes: 70 additions & 14 deletions src/zinolib/controllers/zino1.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@
For updates, either regularly use ``get_events()`` or utilize the UpdateHandler::
> updater = UpdateHandler(event_manager)
> updated = updater.poll()
> updated = updater.get_event_update()
``updater.poll()`` updates ``event_manager.events`` and
``event_manager.removed_ids`` and returns ``True`` on any change, falsey
otherwise.
The updater is unique per authenticated user and is only available after login.
``updater.get_event_update()`` updates ``event_manager.events`` and
``event_manager.removed_ids`` and returns the id of a changed event on any
change, falsey otherwise. Check the id against the removed_id's set to see if
it has been removed from the server.
To get history for a specific event::
Expand Down Expand Up @@ -72,7 +75,6 @@
from .base import EventManager, EventOrId
from ..compat import StrEnum
from ..event_types import EventType, Event, HistoryEntry, LogEntry, AdmState
from ..event_types import PortStateEvent
from ..ritz import ZinoError, ProtocolError, ritz, notifier
from ..utils import log_exception_with_params

Expand Down Expand Up @@ -135,51 +137,89 @@ def __init__(self, manager, autoremove=False):
self.events = manager.events
self.autoremove = autoremove

def poll(self):
def get_event_update(self):
"""
Fetches one update for a single event and runs the appropriate handler
Attributes on the update object:
id: event id
type: update type, triggers the correct handler
info: a type-specific string with the actual change
Run in a loop/every N seconds for a lightweight way to update the event
list
"""
update = self.manager.session.push.poll()
if not update:
return False
return self.handle(update)
return self.handle_event_update(update)

def update(self, event_id: int):
"Refresh an event from the server, refreshing everything"
event = self.manager.get_updated_event_for_id(event_id)
self.manager._set_event(event)
LOG.debug("Updated event #%i", event_id)

def remove(self, event_id: int):
"Remove an event from our local copy of the events list"
self.manager.remove_event(event_id)
LOG.debug("Removed event #%i", event_id)

def handle(self, update):
def handle_event_update(self, update):
"""Call the right handle on the update object depending on type
If the update is about a locally unknown id and the update type is not
UpdateType.STATE it is a new, incomplete event so we return nothing.
Otherwise call the right handler on the update data.
"""
if update.id not in self.events and update.type != self.UpdateType.STATE:
# new event that still don't have a state
# unknown event that don't have a state (yet), wait for new update
return None
if update.type in tuple(self.UpdateType):
method = getattr(self, f"cmd_{update.type}")
return method(update)
return self.fallback(update)

def cmd_state(self, update):
"""State has been changed
Removes a now closed state if the setting "autoremove" is True,
otherwise refreshes the event from the server.
"""
states = update.info.split(" ")
if states[1] == "closed" and self.autoremove:
LOG.debug('Autoremoving "%s"', update.id)
self.remove(update.id)
else:
self.update(update.id)
return True
return update.id

def cmd_attr(self, update):
"""Attributes has been changed
Refresh the event from the server.
"""
self.update(update.id)
return True
return update.id

cmd_history = cmd_attr
cmd_log = cmd_attr

def cmd_scavenged(self, update):
"""The event has been removed from the server
Remove it from our local copy of the events list.
"""
self.remove(update.id)
return True
return update.id

def fallback(self, update):
"""There's an unknown update type
Log it and do nothing.
"""
LOG.warning('Unknown update type: "%s" for id %s' % (update.type, update.id))
return False

Expand Down Expand Up @@ -302,6 +342,13 @@ def get_event_ids(request):
except ProtocolError as e:
raise RetryError('Zino 1 failed to send a correct response header, retry') from e

@staticmethod
def poll(request, event: EventType) -> bool:
if event.type == Event.Type.PORTSTATE:
return request.poll_interface(event.router, event.if_index)
else:
return request.poll_router(event.router)


class HistoryAdapter:
SYSTEM_USER = "monitor"
Expand Down Expand Up @@ -432,7 +479,7 @@ def rename_exception(self, function, *args):
try:
return function(*args)
except ZinoError as e:
raise self.ManagerException(e)
raise self.ManagerException(e) from e

def _verify_session(self, quiet=False):
if not getattr(self.session, 'request', None):
Expand Down Expand Up @@ -475,13 +522,22 @@ def clear_flapping(self, event_or_id: EventOrId):
return self.session.request.clear_flapping(event.router, event.if_index)
return None

def poll(self, event_or_id: EventOrId):
"""Ask the server to refresh data for the event
If there are any changes they will be available through the update
handler in a bit
"""
event = self._get_event(event_or_id)
return self._event_adapter.poll(self.session.request, event)

def get_events(self):
self._verify_session()
for event_id in self._event_adapter.get_event_ids(self.session.request):
try:
event = self.create_event_from_id(event_id)
except self.ManagerException:
self.removed_ids.add(event_id)
self.remove_event(event_id)
continue
self.events[event_id] = event

Expand Down
6 changes: 3 additions & 3 deletions tests/test_zinolib_controllers_zino1.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def test_handle_new_stateless_event_is_very_special(self):
zino1 = self.init_manager()
updates = UpdateHandler(zino1)
update = NotifierResponse(1337, "", "")
result = updates.handle(update)
result = updates.handle_event_update(update)
self.assertEqual(result, None)

def test_handle_known_type(self):
Expand All @@ -250,7 +250,7 @@ def test_handle_known_type(self):
old_events[raw_event_id].priority = 500
updates = UpdateHandler(zino1)
update = NotifierResponse(raw_event_id, updates.UpdateType.LOG, "")
ok = updates.handle(update) # will refetch events
ok = updates.handle_event_update(update) # will refetch events
self.assertTrue(ok)
self.assertNotEqual(zino1.events[raw_event_id].priority, old_events[raw_event_id].priority)

Expand All @@ -260,5 +260,5 @@ def test_handle_unknown_type(self):
updates = UpdateHandler(zino1)
update = NotifierResponse(raw_event_id, "trout", "")
with self.assertLogs('zinolib.controllers.zino1', level='WARNING'):
ok = updates.handle(update) # will run fallback
ok = updates.handle_event_update(update) # will run fallback
self.assertFalse(ok)

0 comments on commit 2dabbb7

Please sign in to comment.