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