mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Add configurable Nostr retry backoff
This commit is contained in:
@@ -9,9 +9,11 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------
|
||||
# Nostr Relay Connection Settings
|
||||
# -----------------------------------
|
||||
# Retry fewer times with a shorter wait by default
|
||||
MAX_RETRIES = 2 # Maximum number of retries for relay connections
|
||||
RETRY_DELAY = 1 # Seconds to wait before retrying a failed connection
|
||||
# Retry fewer times with a shorter wait by default. These values
|
||||
# act as defaults that can be overridden via ``ConfigManager``
|
||||
# entries ``nostr_max_retries`` and ``nostr_retry_delay``.
|
||||
MAX_RETRIES = 2 # Default maximum number of retry attempts
|
||||
RETRY_DELAY = 1 # Default seconds to wait before retrying
|
||||
MIN_HEALTHY_RELAYS = 2 # Minimum relays that should return data on startup
|
||||
|
||||
# -----------------------------------
|
||||
|
@@ -313,8 +313,7 @@ class NostrClient:
|
||||
|
||||
self.connect()
|
||||
self.last_error = None
|
||||
attempt = 0
|
||||
while True:
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
result = asyncio.run(self._retrieve_json_from_nostr())
|
||||
if result is not None:
|
||||
@@ -322,10 +321,9 @@ class NostrClient:
|
||||
except Exception as e:
|
||||
self.last_error = str(e)
|
||||
logger.error("Failed to retrieve events from Nostr: %s", e)
|
||||
if attempt >= retries:
|
||||
break
|
||||
attempt += 1
|
||||
time.sleep(delay)
|
||||
if attempt < retries - 1:
|
||||
sleep_time = delay * (2**attempt)
|
||||
time.sleep(sleep_time)
|
||||
return None
|
||||
|
||||
async def _retrieve_json_from_nostr(self) -> Optional[bytes]:
|
||||
@@ -434,11 +432,24 @@ class NostrClient:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if self.config_manager is None:
|
||||
from seedpass.core.config_manager import ConfigManager
|
||||
from seedpass.core.vault import Vault
|
||||
|
||||
cfg_mgr = ConfigManager(
|
||||
Vault(self.encryption_manager, self.fingerprint_dir),
|
||||
self.fingerprint_dir,
|
||||
)
|
||||
else:
|
||||
cfg_mgr = self.config_manager
|
||||
cfg = cfg_mgr.load_config(require_pin=False)
|
||||
max_retries = int(cfg.get("nostr_max_retries", MAX_RETRIES))
|
||||
delay = float(cfg.get("nostr_retry_delay", RETRY_DELAY))
|
||||
|
||||
chunks: list[bytes] = []
|
||||
for meta in manifest.chunks:
|
||||
attempt = 0
|
||||
chunk_bytes: bytes | None = None
|
||||
while attempt < MAX_RETRIES:
|
||||
for attempt in range(max_retries):
|
||||
cf = Filter().author(pubkey).kind(Kind(KIND_SNAPSHOT_CHUNK))
|
||||
if meta.event_id:
|
||||
cf = cf.id(EventId.parse(meta.event_id))
|
||||
@@ -451,9 +462,8 @@ class NostrClient:
|
||||
if hashlib.sha256(candidate).hexdigest() == meta.hash:
|
||||
chunk_bytes = candidate
|
||||
break
|
||||
attempt += 1
|
||||
if attempt < MAX_RETRIES:
|
||||
await asyncio.sleep(RETRY_DELAY)
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(delay * (2**attempt))
|
||||
if chunk_bytes is None:
|
||||
return None
|
||||
chunks.append(chunk_bytes)
|
||||
|
@@ -13,7 +13,7 @@ import bcrypt
|
||||
from .vault import Vault
|
||||
from nostr.client import DEFAULT_RELAYS as DEFAULT_NOSTR_RELAYS
|
||||
|
||||
from constants import INACTIVITY_TIMEOUT
|
||||
from constants import INACTIVITY_TIMEOUT, MAX_RETRIES, RETRY_DELAY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -52,8 +52,8 @@ class ConfigManager:
|
||||
"secret_mode_enabled": False,
|
||||
"clipboard_clear_delay": 45,
|
||||
"quick_unlock": False,
|
||||
"nostr_max_retries": 2,
|
||||
"nostr_retry_delay": 1.0,
|
||||
"nostr_max_retries": MAX_RETRIES,
|
||||
"nostr_retry_delay": float(RETRY_DELAY),
|
||||
"min_uppercase": 2,
|
||||
"min_lowercase": 2,
|
||||
"min_digits": 2,
|
||||
@@ -77,8 +77,8 @@ class ConfigManager:
|
||||
data.setdefault("secret_mode_enabled", False)
|
||||
data.setdefault("clipboard_clear_delay", 45)
|
||||
data.setdefault("quick_unlock", False)
|
||||
data.setdefault("nostr_max_retries", 2)
|
||||
data.setdefault("nostr_retry_delay", 1.0)
|
||||
data.setdefault("nostr_max_retries", MAX_RETRIES)
|
||||
data.setdefault("nostr_retry_delay", float(RETRY_DELAY))
|
||||
data.setdefault("min_uppercase", 2)
|
||||
data.setdefault("min_lowercase", 2)
|
||||
data.setdefault("min_digits", 2)
|
||||
@@ -303,7 +303,7 @@ class ConfigManager:
|
||||
def get_nostr_max_retries(self) -> int:
|
||||
"""Retrieve the configured Nostr retry count."""
|
||||
cfg = self.load_config(require_pin=False)
|
||||
return int(cfg.get("nostr_max_retries", 2))
|
||||
return int(cfg.get("nostr_max_retries", MAX_RETRIES))
|
||||
|
||||
def set_nostr_retry_delay(self, delay: float) -> None:
|
||||
"""Persist the delay between Nostr retry attempts."""
|
||||
@@ -316,7 +316,7 @@ class ConfigManager:
|
||||
def get_nostr_retry_delay(self) -> float:
|
||||
"""Retrieve the delay in seconds between Nostr retries."""
|
||||
cfg = self.load_config(require_pin=False)
|
||||
return float(cfg.get("nostr_retry_delay", 1.0))
|
||||
return float(cfg.get("nostr_retry_delay", float(RETRY_DELAY)))
|
||||
|
||||
def set_verbose_timing(self, enabled: bool) -> None:
|
||||
cfg = self.load_config(require_pin=False)
|
||||
|
@@ -12,6 +12,7 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
from seedpass.core.encryption import EncryptionManager
|
||||
from nostr.client import NostrClient
|
||||
import nostr.client as nostr_client
|
||||
import constants
|
||||
|
||||
|
||||
def test_nostr_client_uses_custom_relays():
|
||||
@@ -151,3 +152,31 @@ def test_update_relays_reinitializes_pool(tmp_path, monkeypatch):
|
||||
assert called["ran"] is True
|
||||
assert isinstance(client.client, FakeAddRelaysClient)
|
||||
assert client.relays == new_relays
|
||||
|
||||
|
||||
def test_retrieve_json_sync_backoff(tmp_path, monkeypatch):
|
||||
client = _setup_client(tmp_path, FakeAddRelayClient)
|
||||
|
||||
monkeypatch.setattr("nostr.client.MAX_RETRIES", 3)
|
||||
monkeypatch.setattr("nostr.client.RETRY_DELAY", 1)
|
||||
monkeypatch.setattr("constants.MAX_RETRIES", 3)
|
||||
monkeypatch.setattr("constants.RETRY_DELAY", 1)
|
||||
monkeypatch.setattr("seedpass.core.config_manager.MAX_RETRIES", 3)
|
||||
monkeypatch.setattr("seedpass.core.config_manager.RETRY_DELAY", 1)
|
||||
|
||||
sleeps: list[float] = []
|
||||
|
||||
def fake_sleep(d):
|
||||
sleeps.append(d)
|
||||
|
||||
monkeypatch.setattr(nostr_client.time, "sleep", fake_sleep)
|
||||
|
||||
async def fake_async(self):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(NostrClient, "_retrieve_json_from_nostr", fake_async)
|
||||
|
||||
result = client.retrieve_json_from_nostr_sync()
|
||||
|
||||
assert result is None
|
||||
assert sleeps == [1, 2]
|
||||
|
@@ -8,6 +8,7 @@ from seedpass.core.backup import BackupManager
|
||||
from seedpass.core.config_manager import ConfigManager
|
||||
from nostr.client import prepare_snapshot
|
||||
from nostr.backup_models import KIND_SNAPSHOT_CHUNK
|
||||
import constants
|
||||
|
||||
|
||||
def test_manifest_generation(tmp_path):
|
||||
@@ -73,7 +74,17 @@ def test_fetch_snapshot_fallback_on_missing_chunk(dummy_nostr_client, monkeypatc
|
||||
|
||||
client, relay = dummy_nostr_client
|
||||
monkeypatch.setattr("nostr.client.MAX_RETRIES", 3)
|
||||
monkeypatch.setattr("nostr.client.RETRY_DELAY", 0)
|
||||
monkeypatch.setattr("nostr.client.RETRY_DELAY", 1)
|
||||
monkeypatch.setattr("constants.MAX_RETRIES", 3)
|
||||
monkeypatch.setattr("constants.RETRY_DELAY", 1)
|
||||
monkeypatch.setattr("seedpass.core.config_manager.MAX_RETRIES", 3)
|
||||
monkeypatch.setattr("seedpass.core.config_manager.RETRY_DELAY", 1)
|
||||
delays: list[float] = []
|
||||
|
||||
async def fake_sleep(d):
|
||||
delays.append(d)
|
||||
|
||||
monkeypatch.setattr("nostr.client.asyncio.sleep", fake_sleep)
|
||||
|
||||
data1 = os.urandom(60000)
|
||||
manifest1, _ = asyncio.run(client.publish_snapshot(data1))
|
||||
@@ -102,6 +113,7 @@ def test_fetch_snapshot_fallback_on_missing_chunk(dummy_nostr_client, monkeypatc
|
||||
)
|
||||
)
|
||||
assert attempts == 3
|
||||
assert delays == [1, 2]
|
||||
|
||||
|
||||
def test_fetch_snapshot_uses_event_ids(dummy_nostr_client):
|
||||
|
Reference in New Issue
Block a user