fix: reconcile logged_in against connected peers every 120s

After a server restart, Clients.logged_in stayed 1 for peers no longer
connected, letting the monitor authenticate disconnected hotspots via
login-by-ip. Replace the hourly clean_tbl (24h idle sweep) with a
reconcile in the existing 120s lst_seen loop: connected peers get
logged_in=1, the rest 0. The lst_seen loop now starts with now=True so
its first tick at boot clears all stale flags immediately.
pull/30/head
Rodrigo Pérez 3 months ago
parent 2ec900b260
commit ab63a8ceb1

@ -363,8 +363,8 @@ class ProxySelfServiceStore(ABC):
...
@abstractmethod
def clean_tbl(self) -> Any:
"""Returns Deferred."""
def reconcile_logged_in(self, connected_peer_ids: list[bytes]) -> Any:
"""logged_in=1 for connected peers, 0 for the rest. Returns Deferred."""
class DynamicTgStore(ABC):

@ -67,7 +67,7 @@ class NullProxySelfServiceStore(ProxySelfServiceStore):
def updt_lstseen(self, dmrid_list: list[tuple[bytes, ...]]) -> None:
pass
def clean_tbl(self) -> Any:
def reconcile_logged_in(self, connected_peer_ids: list[bytes]) -> Any:
from twisted.internet.defer import succeed
return succeed(None)

@ -141,8 +141,28 @@ class ProxySelfServiceRepository(ProxySelfServiceStore):
)
)
@inlineCallbacks
def clean_tbl(self) -> Any:
yield self._pool.runOperation(
"UPDATE Clients SET logged_in=0 WHERE logged_in=1 AND last_seen < UNIX_TIMESTAMP() - 86400"
).addErrback(lambda f: logger.error("(SELF_SERVICE) clean_tbl: %s", f.getTraceback()))
def reconcile_logged_in(self, connected_peer_ids: list[bytes]) -> Any:
if connected_peer_ids:
ph = ",".join(["%s"] * len(connected_peer_ids))
self._pool.runOperation(
f"UPDATE Clients SET logged_in=1 WHERE dmr_id IN ({ph})",
tuple(connected_peer_ids),
).addErrback(
lambda f: logger.error(
"(SELF_SERVICE) reconcile set 1: %s", f.getTraceback()
)
)
self._pool.runOperation(
f"UPDATE Clients SET logged_in=0 WHERE dmr_id NOT IN ({ph})",
tuple(connected_peer_ids),
).addErrback(
lambda f: logger.error(
"(SELF_SERVICE) reconcile set 0: %s", f.getTraceback()
)
)
else:
self._pool.runOperation("UPDATE Clients SET logged_in=0").addErrback(
lambda f: logger.error(
"(SELF_SERVICE) reconcile clear: %s", f.getTraceback()
)
)

@ -81,18 +81,17 @@ class ProxySelfServiceBridge:
self._mysql_option_peers: set[bytes] = set()
def start_loops(self) -> None:
"""Legacy timers: send_opts 10s, lst_seen 120s, clean_tbl 3600s."""
for interval, fn in (
(10.0, self.send_opts),
(120.0, self.lst_seen),
(3600.0, self._clean_tbl),
"""Timers: send_opts 10s, lst_seen+reconcile 120s (now=True for startup clean slate)."""
for interval, fn, now in (
(10.0, self.send_opts, False),
(120.0, self.lst_seen, True),
):
call = LoopingCall(fn)
call.start(interval, now=False)
call.start(interval, now=now)
self._loop_calls.append(call)
self._log.info(
"(SELF_SERVICE) DB options on PASS= (immediate), send_opts every 10s, "
"clean_tbl every 1h, lst_seen every 2min"
"lst_seen + reconcile_logged_in every 2min"
)
def stop_loops(self) -> None:
@ -349,9 +348,8 @@ class ProxySelfServiceBridge:
self._log.warning("(SELF_SERVICE) send_opts error: %s", err)
def lst_seen(self) -> None:
dmrid_list = [(slot.peer_id,) for slot in self._use_cases.list_slots()]
slots = self._use_cases.list_slots()
dmrid_list = [(slot.peer_id,) for slot in slots]
if dmrid_list:
self._store.updt_lstseen(dmrid_list)
def _clean_tbl(self) -> None:
self._store.clean_tbl()
self._store.reconcile_logged_in([slot.peer_id for slot in slots])

@ -22,6 +22,8 @@
from __future__ import annotations
from typing import Any
from twisted.internet.defer import Deferred
from adn_server.application.proxy import ProxyUseCases
@ -56,6 +58,7 @@ class _FakeStore(NullProxySelfServiceStore):
self.actions: list[tuple[str, bytes]] = []
self.options_by_peer: dict[bytes, str] = {}
self.pending_modified: list[tuple[bytes, str]] = []
self.reconcile_calls: list[list[bytes]] = []
def ins_conf(
self,
@ -89,6 +92,15 @@ class _FakeStore(NullProxySelfServiceStore):
return succeed([(pid, opt) for pid, opt in self.pending_modified])
def updt_lstseen(self, dmrid_list: list[tuple[bytes, ...]]) -> None:
self.actions.append(("updt_lstseen", b""))
def reconcile_logged_in(self, connected_peer_ids: list[bytes]) -> Any:
from twisted.internet.defer import succeed
self.reconcile_calls.append(list(connected_peer_ids))
return succeed(None)
def _bridge() -> tuple[ProxySelfServiceBridge, _RecordingSink, _FakeStore, _RecordingSender]:
store = _FakeStore()
@ -302,6 +314,33 @@ def test_session_expired_logs_out() -> None:
assert ("log_out", peer) in store.actions
def test_lst_seen_reconciles_connected_peers() -> None:
"""lst_seen calls reconcile_logged_in with the connected peer IDs."""
bridge, _sink, store, _sender = _bridge()
peer = bytes_4(7300444)
bridge.lst_seen()
assert store.reconcile_calls == [[peer]]
def test_lst_seen_reconciles_empty_when_no_peers() -> None:
"""lst_seen with no slots calls reconcile_logged_in([]) — startup clean slate."""
from adn_server.application.proxy import ProxyUseCases
from adn_server.infrastructure.proxy.rpto_queue import InMemoryPendingRptoQueue
from adn_server.infrastructure.proxy.slot_store import InMemoryProxySlotStore
store = _FakeStore()
sink = _RecordingSink()
sender = _RecordingSender()
use_cases = ProxyUseCases(
InMemoryProxySlotStore(), InMemoryPendingRptoQueue(), max_peers=4
)
bridge = ProxySelfServiceBridge(
store, use_cases, sink, sender, pbkdf2_salt="ADN", pbkdf2_iterations=2000
)
bridge.lst_seen()
assert store.reconcile_calls == [[]]
def test_yaml_loader_preserves_self_service_block(tmp_path) -> None:
"""SELF_SERVICE from adn-server.yaml must reach runtime (not stripped at load)."""
from adn_server.infrastructure.config_loader import YamlConfigLoader

@ -0,0 +1,72 @@
# ADN DMR Peer Server - tests infrastructure proxy self service repository
#
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
#
###############################################################################
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
"""ProxySelfServiceRepository SQL for reconcile_logged_in (mock pool)."""
from __future__ import annotations
from unittest.mock import MagicMock
from twisted.internet.defer import succeed
from adn_server.infrastructure.proxy.persistence.proxy_repository import (
ProxySelfServiceRepository,
)
def _repo() -> ProxySelfServiceRepository:
pool = MagicMock()
pool.runOperation.return_value = succeed(None)
return ProxySelfServiceRepository(pool)
def test_reconcile_empty_clears_all_logged_in() -> None:
repo = _repo()
repo.reconcile_logged_in([])
sql = repo._pool.runOperation.call_args[0][0] # noqa: SLF001
assert sql == "UPDATE Clients SET logged_in=0"
def test_reconcile_with_peers_sets_in_and_not_in() -> None:
repo = _repo()
peer_a = b"\x00\x70\x22\x34"
peer_b = b"\x00\x70\x22\x35"
repo.reconcile_logged_in([peer_a, peer_b])
calls = repo._pool.runOperation.call_args_list # noqa: SLF001
assert len(calls) == 2
set_1_sql, set_1_args = calls[0][0]
set_0_sql, set_0_args = calls[1][0]
assert "logged_in=1" in set_1_sql
assert "IN (%s,%s)" in set_1_sql
assert set_1_args == (peer_a, peer_b)
assert "logged_in=0" in set_0_sql
assert "NOT IN (%s,%s)" in set_0_sql
assert set_0_args == (peer_a, peer_b)
def test_reconcile_single_peer_uses_one_placeholder() -> None:
repo = _repo()
peer = b"\x00\x70\x22\x34"
repo.reconcile_logged_in([peer])
calls = repo._pool.runOperation.call_args_list # noqa: SLF001
assert len(calls) == 2
set_1_sql, set_1_args = calls[0][0]
assert "IN (%s)" in set_1_sql
assert set_1_args == (peer,)
Loading…
Cancel
Save

Powered by TurnKey Linux.