Skip to content

Commit 5f9d703

Browse files
committed
Retry telemetry registration quickly instead of hourly
TelemetryService only retried every iter_min_period (up to 1 hour), but exordos-bootstrap sets the VS variables it depends on with no systemd ordering guarantee relative to ec-gservice, so the very first attempt can silently skip (variables not set yet). With a 1-hour retry period, a managed realm's ecosystem-side provisioning_timeout (also defaulting to 1 hour) can fire before the next attempt, permanently erroring the realm. Retry starting at 30s until registration is confirmed, then relax to the caller's steady-state period immediately. Stands with no network access at all would otherwise retry every 30s forever, so each failed attempt doubles the retry period, capped at the steady-state period, until registration succeeds. Also bump the "variables not configured" log from DEBUG to INFO so this condition is visible in normal logs. Signed-off-by: George Melikov <mail@gmelikov.ru>
1 parent 61b6760 commit 5f9d703

2 files changed

Lines changed: 49 additions & 6 deletions

File tree

exordos_core/telemetry/service.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@
4040
TELEMETRY_TIMEOUT = 30
4141
TELEMETRY_POOL_SIZE = 1
4242

43+
# Initial retry cadence used until the stand has successfully registered
44+
# with the ecosystem at least once. exordos-bootstrap (which sets the VS
45+
# variables this service depends on) has no systemd ordering guarantee
46+
# relative to ec-gservice, so the very first iteration can race ahead of
47+
# the variables being set. Falling back to the full iter_min_period (can be
48+
# as long as an hour) in that case would leave the stand stuck in
49+
# PROVISIONING for a long time, so we start retrying quickly.
50+
#
51+
# Stands with no network access at all would otherwise keep retrying every
52+
# TELEMETRY_RETRY_PERIOD forever, so on every failed attempt the period is
53+
# doubled (TELEMETRY_RETRY_BACKOFF_FACTOR), up to the caller's steady-state
54+
# period. It resets back to TELEMETRY_RETRY_PERIOD once registration
55+
# succeeds, then relaxes to the steady-state period.
56+
TELEMETRY_RETRY_PERIOD = 30
57+
TELEMETRY_RETRY_BACKOFF_FACTOR = 2
58+
4359

4460
class TelemetryService(basic.BasicService):
4561
"""Periodically collects and sends telemetry data to the ecosystem."""
@@ -49,6 +65,13 @@ def __init__(self, *args, **kwargs):
4965
self._client = bazooka.Client(default_timeout=TELEMETRY_TIMEOUT)
5066
self._executor = futures.ThreadPoolExecutor(max_workers=TELEMETRY_POOL_SIZE)
5167
self._pending_future = None
68+
# Steady-state period requested by the caller (e.g. 1 hour). Until
69+
# the stand registers successfully we back off from
70+
# TELEMETRY_RETRY_PERIOD towards this value instead; see the
71+
# comment on TELEMETRY_RETRY_PERIOD above.
72+
self._steady_period = self._iter_min_period
73+
self._registered = False
74+
self._iter_min_period = TELEMETRY_RETRY_PERIOD
5275

5376
def _get_variable_value(self, var_uuid):
5477
"""Read variable value from ValuesStore by UUID."""
@@ -227,6 +250,20 @@ def _register_stand(self, endpoint, realm_uuid, realm_secret):
227250
)
228251
LOG.info("Stand registered in ecosystem successfully")
229252

253+
def _mark_registered(self):
254+
"""Relax the retry cadence once the stand is confirmed registered."""
255+
self._registered = True
256+
self._iter_min_period = self._steady_period
257+
258+
def _backoff_retry(self):
259+
"""Grow the retry cadence towards the steady-state period on failure."""
260+
if self._registered:
261+
return
262+
self._iter_min_period = min(
263+
self._iter_min_period * TELEMETRY_RETRY_BACKOFF_FACTOR,
264+
self._steady_period,
265+
)
266+
230267
def _send_telemetry(self, endpoint, realm_uuid, realm_secret, data):
231268
"""Send telemetry data to the ecosystem endpoint."""
232269
url = f"{endpoint}/api/ecosystem/v1/realms/{realm_uuid}/actions/push_telemetry/invoke"
@@ -240,6 +277,7 @@ def _send_telemetry(self, endpoint, realm_uuid, realm_secret, data):
240277
auth=auth,
241278
)
242279
LOG.debug("Telemetry sent successfully")
280+
self._mark_registered()
243281
except bazooka_exc.ForbiddenError:
244282
LOG.warning("Stand is not registered, attempting registration")
245283
try:
@@ -251,10 +289,13 @@ def _send_telemetry(self, endpoint, realm_uuid, realm_secret, data):
251289
auth=auth,
252290
)
253291
LOG.debug("Telemetry sent successfully after registration")
292+
self._mark_registered()
254293
except Exception:
255294
LOG.exception("Failed to register stand or send telemetry")
295+
self._backoff_retry()
256296
except Exception:
257297
LOG.exception("Failed to send telemetry")
298+
self._backoff_retry()
258299

259300
def _check_pending_future(self):
260301
"""Clear completed future to allow the next submission."""
@@ -281,7 +322,11 @@ def _iteration(self):
281322
realm_secret = self._get_variable_value(c.VAR_REALM_SECRET_UUID)
282323

283324
if not all([ecosystem_endpoint, realm_uuid, realm_secret]):
284-
LOG.debug("Telemetry variables are not configured, skipping")
325+
self._backoff_retry()
326+
LOG.info(
327+
"Telemetry variables are not configured yet, will retry in %ss",
328+
self._iter_min_period,
329+
)
285330
return
286331

287332
# Collect telemetry data synchronously, send asynchronously

exordos_core/user_api/iam/api/controllers.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@
1717
import errno
1818
import logging
1919
import mimetypes
20-
import uuid as sys_uuid
2120
from os import path as os_path
2221
import re
2322
import string
2423
import typing as tp
2524
from urllib import parse as urllib_parse
25+
import uuid as sys_uuid
2626

2727
from authlib.integrations import requests_client
28+
from gcl_iam import exceptions as gcl_iam_e
2829
from gcl_iam import rules
2930
from gcl_iam.api import controllers as iam_controllers
3031
from gcl_iam.api import field_perms as iam_fp
31-
from gcl_iam import exceptions as gcl_iam_e
3232
import jinja2
3333
import pyotp
3434
from restalchemy.api import actions
@@ -310,9 +310,7 @@ def _get_request_iam_client(self) -> models.IamClient | None:
310310
# a client token; attribute it to the default client.
311311
return models.IamClient.objects.get_one_or_none(
312312
filters={
313-
"uuid": ra_filters.EQ(
314-
sys_uuid.UUID(common_c.DEFAULT_CLIENT_UUID)
315-
)
313+
"uuid": ra_filters.EQ(sys_uuid.UUID(common_c.DEFAULT_CLIENT_UUID))
316314
}
317315
)
318316

0 commit comments

Comments
 (0)