Using dcc.Interval as watchdog

Hi,
I would like to use dcc.Interval as watchdog to monitor incoming SSE (expected every 1s) and raise alert if no SSE has been received for 10s. To do so, I would need to reset counting every time SSE is received, but I seems not to be possible to reset Interval’s counter using exposed Interval’s properties.

However I see, resetTimer() function defined in dcc internals:

So, I thought about adding dcc.Store component used as trampoline to fire clientside callback and call resetTimer there:

WATCHDOG_TIMEOUT = 10  # [s]


def watchdog(name):
    """Watchdog"""
    return html.Div(
        [
            dcc.Interval(
                id={"role": "watchdog", "name": name},
                interval=WATCHDOG_TIMEOUT * 1000,  # in milliseconds
                n_intervals=0,
            ),
            dcc.Store(
                id={"role": "watchdog_reset", "name": name},
                data=0,
            ),
            html.Div(
                id={"role": "watchdog_dummy", "name": name},
                children=[],
                hidden=True,
            ),
        ],
        id={"role": "doghouse", "name": name},
    )
    app.clientside_callback(
        """
        function(n, id) {
            var interval = document.getElementById(id);
            interval.resetTimer();
            return n;
        }
        """,
        Output({"role": "watchdog_dummy", "name": MATCH}, "children"),
        Input({"role": "watchdog_reset", "name": MATCH}, "data"),
        Input({"role": "watchdog", "name": MATCH}, "id"),
    )

But it seems that reset is not working.

Any ideas how to fix the concept above or anyhow reset dcc.Interval’s counter are appreciated. Thanks :slight_smile:

Hello @petro,

I thought you can reset the count by passing a value back to n_intervals, is this not working?

Hi @jinnyzor,

Actually, it does not look like setting n_intervals to 0 resets the counter. With interval=10s I see the following sequence

[   ERROR] 2023-10-24 10:44:50       line_status.py@  40 Watchdog fired: {'line': 'midi', 'role': 'watchdog', 'station': 'gluing'}: n_intervals: 1

[   ERROR] 2023-10-24 10:44:56       line_status.py@  36 SSE received: {'line': 'midi', 'ok_event_count': 0, 'station': 'gluing', 'status': 'nok', 'total_event_count': 2}: n_intervals: 1

[   ERROR] 2023-10-24 10:44:59       line_status.py@  36 SSE received: {'line': 'midi', 'ok_event_count': 0, 'station': 'gluing', 'status': 'nok', 'total_event_count': 1}: n_intervals: 0

[   ERROR] 2023-10-24 10:45:00       line_status.py@  40 Watchdog fired: {'line': 'midi', 'role': 'watchdog', 'station': 'gluing'}: n_intervals: 1

I reset n_intervals on SSE, but watchdog fires every 10s ignoring change of n_intervals

Hi @petro

It might be easier to simplify the example first, then add the complexity of the clientside callback and the pattern matching callbacks.

Here’s a simple example that shows how to update the n_intervals

import dash
from dash import Dash, html, dcc, callback, Input, Output, ctx

app =  Dash(__name__)

app.layout = html.Div(
    [
        dcc.Interval(id="watchdog", interval=1 * 1000),
        html.Button("Incoming request", id="incoming"),
        html.Div(id="status"),
    ]
)

@callback(
    Output("status", "children"),
    Output("watchdog","n_intervals" ),
    Input("watchdog", "n_intervals"),
    Input("incoming", "n_clicks"),
    prevent_initial_call=True
)
def update(n, clicks):
    print(n, clicks)
    if ctx.triggered_id == "incoming":
        return f"Incoming request at {n} seconds", 0
    if n < 11:
        return f"no request in {n} seconds", dash.no_update
    return "", 0
    

if __name__ == "__main__":
    app.run_server(debug=True)



2 Likes

Hi @AnnMarieW,

Thanks for your comment. I’ve started with a similar approach and it’s working.
There are two corner cases, though:

  1. Since dcc.Interval and the other trigger (button, sse…) are not synchronized, there may be jitter of max 1* interval. If interval=1s and timeout=10s, it results in an error of max. 10%, but if timeout=2s the max error is 50%. Of course, we could use shorter interval, but

  2. The update callback will be fired every interval period. I had an impression that if one callback is still running and the next one is triggered, the latter is missed. Do you know if that’s the case, i.e. are callbacks are anyhow queued in dash?

Thanks!

Btw. Another workaround could be stopping/restarting dcc.Interval on incoming SSE. (I’ve checked in the dcc source code that it should reset internal timer)

I’ve implemented that by adding dcc.Store component and:

  1. in update callback if source==incoming, set dcc.Internal’s disabled prop to True and write restart value to dcc.Store
  2. (Store, data) is an input to another callback that set dcc.Interval’s disabled prop to False
    Ofc, it requires enable_duplicates=True
1 Like