[#340] Ironed out some bugs in the `alarm` integration.

- The alarm ID should be randomly generated - auto-increment IDs are
  subject to race conditions when alarms are created in separate
  processes.

- Clean up alarms that are not static and have been removed from the db.

- Better alarm shut down detection logic.
This commit is contained in:
Fabio Manganiello 2023-12-10 15:30:19 +01:00
parent ca57d3d7b3
commit 42574d054a
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
2 changed files with 24 additions and 14 deletions

View File

@ -167,6 +167,13 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
media_plugin=alarm.media_plugin or self.media_plugin,
)
# Stop and remove alarms that are not statically configured no longer
# present in the db
for name, alarm in self.alarms.copy().items():
if not alarm.static and name not in alarms:
del self.alarms[name]
alarm.stop()
def _sync_alarms(self):
with self._get_session() as session:
db_alarms = {
@ -184,15 +191,16 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
self._synced = True
def _clear_alarm(self, alarm: DbAlarm, session: Session):
self.alarms.pop(str(alarm.name), None)
alarm_obj = self.alarms.pop(str(alarm.name), None)
if alarm_obj:
alarm_obj.stop()
session.delete(alarm)
self._bus.post(EntityDeleteEvent(entity=alarm))
def _clear_expired_alarms(self, session: Session):
expired_alarms = [
alarm
for alarm in self.alarms.values()
if alarm.is_expired() and alarm.is_shut_down()
alarm for alarm in self.alarms.values() if alarm.should_stop()
]
if not expired_alarms:
@ -220,7 +228,7 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
iter(
alarm
for alarm in self.alarms.values()
if alarm.state == AlarmState.RUNNING
if alarm.state in {AlarmState.RUNNING, AlarmState.SNOOZED}
),
None,
)
@ -233,6 +241,9 @@ class AlarmPlugin(RunnablePlugin, EntityManager):
def _on_alarm_update(self, alarm: Alarm):
with self._db_lock:
if alarm.should_stop():
return
self.publish_entities([alarm])
def _add(

View File

@ -3,6 +3,7 @@ import enum
import os
import time
import threading
from random import randint
from typing import Callable, Optional, Union
import croniter
@ -40,9 +41,6 @@ class Alarm:
Alarm model and controller.
"""
_alarms_count = 0
_id_lock = threading.RLock()
def __init__(
self,
when: Union[str, int, float],
@ -59,10 +57,7 @@ class Alarm:
on_change: Optional[Callable[['Alarm'], None]] = None,
**_,
):
with self._id_lock:
self._alarms_count += 1
self.id = self._alarms_count
self.id = randint(0, 65535)
self.when = when
self.name = name or f'Alarm_{self.id}'
self.media = self._get_media_resource(media)
@ -240,10 +235,10 @@ class Alarm:
sleep_time = self._runtime_snooze_interval
else:
self.state = AlarmState.WAITING
self._on_change()
break
self._on_change()
self.wait_stop(self.poll_interval)
if self.state == AlarmState.SNOOZED:
@ -266,7 +261,11 @@ class Alarm:
self.stop_event.wait(timeout)
def should_stop(self):
return self.stop_event.is_set() or (self.is_expired() and self.is_shut_down())
return (
self.stop_event.is_set()
or (self.is_expired() and self.state == AlarmState.DISMISSED)
or self.state == AlarmState.SHUTDOWN
)
def to_dict(self) -> dict:
return {