parent
6a1f69109d
commit
9b9e0b72a4
4 changed files with 94 additions and 88 deletions
|
@ -57,8 +57,13 @@ Alarm system
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
There is a DB. The lowest level is handled by TinyDB. :class:`larigira.event.EventModel` is a thin layer on
|
There is a DB. The lowest level is handled by TinyDB. :class:`larigira.event.EventModel` is a thin layer on
|
||||||
it, providing more abstract functions. The real deal is :class:`larigira.event.EventSource`, which is a
|
it, providing more abstract functions.
|
||||||
greenlet that sends notifications about alarms in the DB. Those notifications are received by
|
|
||||||
:class:`larigira.event.Monitor`, which "runs" them; it executes the time specification, make an appropriate
|
|
||||||
"sleep" and after that runs the audiogenerator.
|
|
||||||
|
|
||||||
|
There is a :class:`Monitor <larigira.event.Monitor>`, which is something that, well, "monitors" the DB and
|
||||||
|
schedule events appropriately. It will check alarms every ``EVENT_TICK_SECS`` seconds, or when larigira received
|
||||||
|
a ``SIGALRM`` (so ``pkill -ALRM larigira`` might be useful for you).
|
||||||
|
|
||||||
|
You can view scheduled events using the web interface, at ``/view/status/running``. Please note that you will
|
||||||
|
only see *scheduled* events, which are events that will soon be added to playlist. That page will not give you
|
||||||
|
information about events that will run in more than ``2 * EVENT_TICK_SECS`` seconds (by default, this amounts
|
||||||
|
to 1 minute).
|
||||||
|
|
|
@ -23,7 +23,8 @@ def get_conf(prefix='LARIGIRA_'):
|
||||||
conf['SECRET_KEY'] = 'Please replace me!'
|
conf['SECRET_KEY'] = 'Please replace me!'
|
||||||
conf['MPD_WAIT_START'] = True
|
conf['MPD_WAIT_START'] = True
|
||||||
conf['MPD_WAIT_START_RETRYSECS'] = 5
|
conf['MPD_WAIT_START_RETRYSECS'] = 5
|
||||||
conf['CHECK_SECS'] = 20
|
conf['CHECK_SECS'] = 20 # period for checking playlist length
|
||||||
|
conf['EVENT_TICK_SECS'] = 30 # period for scheduling events
|
||||||
conf['DEBUG'] = False
|
conf['DEBUG'] = False
|
||||||
conf.update(from_envvars(prefix=prefix))
|
conf.update(from_envvars(prefix=prefix))
|
||||||
return conf
|
return conf
|
||||||
|
|
|
@ -9,7 +9,7 @@ import gevent
|
||||||
from gevent.queue import Queue
|
from gevent.queue import Queue
|
||||||
from tinydb import TinyDB
|
from tinydb import TinyDB
|
||||||
|
|
||||||
from .eventutils import ParentedLet
|
from .eventutils import ParentedLet, Timer
|
||||||
from .timegen import timegenerate
|
from .timegen import timegenerate
|
||||||
from .audiogen import audiogenerate
|
from .audiogen import audiogenerate
|
||||||
|
|
||||||
|
@ -57,53 +57,33 @@ class EventModel(object):
|
||||||
return self.alarms.update(new_fields, eids=[alarmid])
|
return self.alarms.update(new_fields, eids=[alarmid])
|
||||||
|
|
||||||
|
|
||||||
class EventSource(ParentedLet):
|
|
||||||
def __init__(self, queue, uri):
|
|
||||||
ParentedLet.__init__(self, queue)
|
|
||||||
self.log = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.log.debug('uri is %s' % uri)
|
|
||||||
self.model = EventModel(uri)
|
|
||||||
self.log.debug('opened %s' % uri)
|
|
||||||
|
|
||||||
def parent_msg(self, kind, *args):
|
|
||||||
if kind == 'add':
|
|
||||||
msg = ParentedLet.parent_msg(self, kind, *args[2:])
|
|
||||||
msg['timespec'] = args[0]
|
|
||||||
msg['audiospec'] = args[1]
|
|
||||||
else:
|
|
||||||
msg = ParentedLet.parent_msg(self, kind, *args)
|
|
||||||
return msg
|
|
||||||
|
|
||||||
def reload_id(self, alarm_id):
|
|
||||||
'''
|
|
||||||
Check if the event is still valid, and put "add" messages on queue
|
|
||||||
'''
|
|
||||||
alarm = self.model.get_alarm_by_id(alarm_id)
|
|
||||||
for action in self.model.get_actions_by_alarm(alarm):
|
|
||||||
self.send_to_parent('add', alarm, action)
|
|
||||||
|
|
||||||
def do_business(self):
|
|
||||||
for alarm, action in self.model.get_all_alarms_expanded():
|
|
||||||
self.log.debug('scheduling {}'.format(alarm))
|
|
||||||
yield ('add', alarm, action)
|
|
||||||
|
|
||||||
|
|
||||||
class Monitor(ParentedLet):
|
class Monitor(ParentedLet):
|
||||||
'''Manages timegenerators and audiogenerators'''
|
'''
|
||||||
|
Manages timegenerators and audiogenerators.
|
||||||
|
|
||||||
|
The mechanism is partially based on ticks, partially on scheduled actions.
|
||||||
|
Ticks are emitted periodically; at every tick, :func:`on_tick
|
||||||
|
<larigira.event.Monitor.on_tick>` checks if any event is "near enough". If
|
||||||
|
an event is near enough, it is ":func:`scheduled
|
||||||
|
<larigira.event.Monitor.schedule>`": a greenlet is run which will wait for
|
||||||
|
the right time, then generate the audio, then submit to Controller.
|
||||||
|
|
||||||
|
The tick mechanism allows for events to be changed on disk: if everything
|
||||||
|
was scheduled immediately, no further changes would be possible.
|
||||||
|
The scheduling mechanism allows for more precision, catching exactly the
|
||||||
|
right time. Being accurate only with ticks would have required very
|
||||||
|
frequent ticks, which is cpu-intensive.
|
||||||
|
'''
|
||||||
def __init__(self, parent_queue, conf):
|
def __init__(self, parent_queue, conf):
|
||||||
ParentedLet.__init__(self, parent_queue)
|
ParentedLet.__init__(self, parent_queue)
|
||||||
self.log = logging.getLogger(self.__class__.__name__)
|
self.log = logging.getLogger(self.__class__.__name__)
|
||||||
self.q = Queue()
|
|
||||||
self.running = {}
|
self.running = {}
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
self.source = EventSource(self.q, uri=conf['DB_URI'])
|
self.q = Queue()
|
||||||
self.source.parent_greenlet = self
|
self.model = EventModel(self.conf['DB_URI'])
|
||||||
|
self.ticker = Timer(int(self.conf['EVENT_TICK_SECS']) * 1000, self.q)
|
||||||
|
|
||||||
def add(self, timespec, audiospec):
|
def _alarm_missing_time(self, timespec):
|
||||||
'''
|
|
||||||
this is somewhat recursive: after completion calls reload_id, which
|
|
||||||
could call this method again
|
|
||||||
'''
|
|
||||||
now = datetime.now() + timedelta(seconds=self.conf['CACHING_TIME'])
|
now = datetime.now() + timedelta(seconds=self.conf['CACHING_TIME'])
|
||||||
try:
|
try:
|
||||||
when = next(timegenerate(timespec, now=now))
|
when = next(timegenerate(timespec, now=now))
|
||||||
|
@ -112,59 +92,79 @@ class Monitor(ParentedLet):
|
||||||
"an alarm from timespec {}".format(timespec))
|
"an alarm from timespec {}".format(timespec))
|
||||||
if when is None:
|
if when is None:
|
||||||
# expired
|
# expired
|
||||||
return
|
return None
|
||||||
delta = when - now
|
delta = (when - now).total_seconds()
|
||||||
assert delta.total_seconds() > 0
|
assert delta > 0
|
||||||
self.log.info('Timer<{}> will run after {} seconds, triggering <{}>'.format(
|
return delta
|
||||||
timespec.get('nick', timespec.eid),
|
|
||||||
int(delta.total_seconds()),
|
|
||||||
audiospec.get('nick', audiospec.eid)
|
|
||||||
))
|
|
||||||
|
|
||||||
audiogen = gevent.spawn_later(delta.total_seconds(), audiogenerate,
|
def on_tick(self):
|
||||||
audiospec)
|
'''
|
||||||
|
this is called every EVENT_TICK_SECS.
|
||||||
|
Checks every event in the DB (which might be slightly CPU-intensive, so
|
||||||
|
it is advisable to run it in its own greenlet); if the event is "near
|
||||||
|
enough", schedule it; if it is too far, or already expired, ignore it.
|
||||||
|
'''
|
||||||
|
for alarm, action in self.model.get_all_alarms_expanded():
|
||||||
|
if alarm.eid in self.running:
|
||||||
|
continue
|
||||||
|
delta = self._alarm_missing_time(alarm)
|
||||||
|
# why this 2*EVENT_TICK_SECS? EVENT_TICK_SECS would be enough,
|
||||||
|
# but it is "tricky"; any small delay would cause the event to be
|
||||||
|
# missed
|
||||||
|
if delta is not None and delta <= 2*self.conf['EVENT_TICK_SECS']:
|
||||||
|
self.log.debug('Scheduling event {} ({}s)'
|
||||||
|
.format(alarm.get('nick', alarm.eid),
|
||||||
|
delta))
|
||||||
|
self.schedule(alarm, action, delta)
|
||||||
|
else:
|
||||||
|
self.log.debug('Skipping event {}, too far ({}s)'
|
||||||
|
.format(alarm.get('nick', alarm.eid),
|
||||||
|
delta))
|
||||||
|
|
||||||
|
def schedule(self, timespec, audiospec, delta=None):
|
||||||
|
'''
|
||||||
|
prepare an event to be run at a specified time with a specified action;
|
||||||
|
the DB won't be read anymore after this call.
|
||||||
|
|
||||||
|
This means that this call should not be done too early, or any update
|
||||||
|
to the DB will be ignored.
|
||||||
|
'''
|
||||||
|
if delta is None:
|
||||||
|
delta = self._alarm_missing_time(timespec)
|
||||||
|
|
||||||
|
audiogen = gevent.spawn_later(delta,
|
||||||
|
self.process_action,
|
||||||
|
timespec, audiospec)
|
||||||
audiogen.parent_greenlet = self
|
audiogen.parent_greenlet = self
|
||||||
audiogen.doc = 'Will wait {} seconds, then generate audio "{}"'.format(
|
audiogen.doc = 'Will wait {} seconds, then generate audio "{}"'.format(
|
||||||
delta.total_seconds(),
|
delta,
|
||||||
audiospec.get('nick', ''))
|
audiospec.get('nick', ''))
|
||||||
self.running[timespec.eid] = {
|
self.running[timespec.eid] = {
|
||||||
'greenlet': audiogen,
|
'greenlet': audiogen,
|
||||||
'running_time': datetime.now() + timedelta(
|
'running_time': datetime.now() + timedelta(seconds=delta),
|
||||||
seconds=delta.total_seconds()),
|
|
||||||
'audiospec': audiospec
|
'audiospec': audiospec
|
||||||
}
|
}
|
||||||
gl = gevent.spawn_later(delta.total_seconds(),
|
|
||||||
self.source.reload_id,
|
def process_action(self, timespec, audiospec):
|
||||||
timespec.eid)
|
'''Generate audio and submit it to Controller'''
|
||||||
gl.parent_greenlet = self
|
if timespec.eid in self.running:
|
||||||
gl.doc = 'Will wait, then reload id {}'.format(timespec.eid)
|
del self.running[timespec.eid]
|
||||||
# FIXME: audiogen is ready in a moment between
|
else:
|
||||||
# exact_time - CACHING_TIME and the exact_time
|
self.log.warn('Timespec {} completed but not in running '
|
||||||
# atm we are just ignoring this "window", saying that any moment is
|
'registry; this is most likely a bug'.
|
||||||
# fine
|
format(timespec.get('nick', timespec.eid)))
|
||||||
# the more correct thing will be to wait till that exact moment
|
uris = audiogenerate(audiospec)
|
||||||
# adding another spawn_later
|
self.send_to_parent('add', dict(uris=uris,
|
||||||
audiogen.link_value(lambda g: self.log.info(
|
audiospec=audiospec,
|
||||||
'should play %s' % str(g.value)))
|
aid=audiospec.eid))
|
||||||
audiogen.link_exception(lambda g: self.log.exception(
|
|
||||||
'Failure in audiogen {}: {}'.format(audiospec, audiogen.exception)))
|
|
||||||
audiogen.link_value(lambda g:
|
|
||||||
self.send_to_parent('add',
|
|
||||||
dict(uris=g.value,
|
|
||||||
audiospec=audiospec,
|
|
||||||
aid=audiospec.eid
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
self.source.start()
|
self.ticker.start()
|
||||||
|
gevent.spawn(self.on_tick)
|
||||||
while True:
|
while True:
|
||||||
value = self.q.get()
|
value = self.q.get()
|
||||||
self.log.debug('<- %s' % str(value))
|
|
||||||
kind = value['kind']
|
kind = value['kind']
|
||||||
if kind == 'add':
|
if kind == 'timer':
|
||||||
self.add(value['timespec'], value['audiospec'])
|
gevent.spawn(self.on_tick)
|
||||||
elif kind == 'remove':
|
|
||||||
raise NotImplementedError()
|
|
||||||
else:
|
else:
|
||||||
self.log.warning("Unknown message: %s" % str(value))
|
self.log.warning("Unknown message: %s" % str(value))
|
||||||
|
|
|
@ -36,7 +36,7 @@ class ParentedLet(gevent.Greenlet):
|
||||||
|
|
||||||
|
|
||||||
class Timer(ParentedLet):
|
class Timer(ParentedLet):
|
||||||
'''wait some time, then send a "timer" message to parent'''
|
'''continously sleeps some time, then send a "timer" message to parent'''
|
||||||
def __init__(self, milliseconds, queue):
|
def __init__(self, milliseconds, queue):
|
||||||
ParentedLet.__init__(self, queue)
|
ParentedLet.__init__(self, queue)
|
||||||
self.ms = milliseconds
|
self.ms = milliseconds
|
||||||
|
|
Loading…
Reference in a new issue