Skip to content

Commit eaaadf0

Browse files
committed
Wait on network for remote repos
Don't try and start jobs with remote repos until the network is up. This should prevent job failures when, for instance, waking from sleep. Mac implementation is a stub currently.
1 parent f2b4274 commit eaaadf0

File tree

4 files changed

+76
-2
lines changed

4 files changed

+76
-2
lines changed

src/vorta/network_status/abc.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ def is_network_status_available(self):
2626
"""Is the network status really available, and not just a dummy implementation?"""
2727
return type(self) is not NetworkStatusMonitor
2828

29+
def is_network_active(self) -> bool:
30+
"""Is there an active network connection.
31+
32+
True signals that the network is up. The internet may still not be reachable though.
33+
"""
34+
raise NotImplementedError()
35+
2936
def is_network_metered(self) -> bool:
3037
"""Is the currently connected network a metered connection?"""
3138
raise NotImplementedError()

src/vorta/network_status/darwin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def is_network_metered(self) -> bool:
2525

2626
return is_ios_hotspot or any(is_network_metered_with_android(d) for d in get_network_devices())
2727

28+
def is_network_active(self):
29+
# Not yet implemented
30+
return True
31+
2832
def get_current_wifi(self) -> Optional[str]:
2933
"""
3034
Get current SSID or None if Wi-Fi is off.

src/vorta/network_status/network_manager.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ def is_network_metered(self) -> bool:
2525
logger.exception("Failed to check if network is metered, assuming it isn't")
2626
return False
2727

28+
def is_network_active(self):
29+
try:
30+
return self._nm.get_connectivity_state() is not NMConnectivityState.NONE
31+
except DBusException:
32+
logger.exception("Failed to check connectivity state. Assuming connected")
33+
return True
34+
2835
def get_current_wifi(self) -> Optional[str]:
2936
# Only check the primary connection. VPN over WiFi will still show the WiFi as Primary Connection.
3037
# We don't check all active connections, as NM won't disable WiFi when connecting a cable.
@@ -126,6 +133,9 @@ def isValid(self):
126133
return False
127134
return True
128135

136+
def get_connectivity_state(self) -> 'NMConnectivityState':
137+
return NMConnectivityState(read_dbus_property(self._nm, 'Connectivity'))
138+
129139
def get_primary_connection_path(self) -> Optional[str]:
130140
return read_dbus_property(self._nm, 'PrimaryConnection')
131141

@@ -186,3 +196,13 @@ class NMDeviceType(Enum):
186196
# Only the types we care about
187197
UNKNOWN = 0
188198
WIFI = 2
199+
200+
201+
class NMConnectivityState(Enum):
202+
"""https://www.networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMConnectivityState"""
203+
204+
UNKNOWN = 0
205+
NONE = 1
206+
PORTAL = 2
207+
LIMITED = 3
208+
FULL = 4

src/vorta/scheduler.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import threading
44
from datetime import datetime as dt
55
from datetime import timedelta
6-
from typing import Dict, NamedTuple, Optional, Tuple, Union
6+
from typing import Dict, List, NamedTuple, Optional, Tuple, Union
77

88
from packaging import version
99
from PyQt6 import QtCore, QtDBus
@@ -19,7 +19,7 @@
1919
from vorta.i18n import translate
2020
from vorta.notifications import VortaNotifications
2121
from vorta.store.models import BackupProfileModel, EventLogModel
22-
from vorta.utils import borg_compat
22+
from vorta.utils import borg_compat, get_network_status_monitor
2323

2424
logger = logging.getLogger(__name__)
2525

@@ -59,6 +59,13 @@ def __init__(self):
5959
self.qt_timer.setInterval(15 * 60 * 1000)
6060
self.qt_timer.start()
6161

62+
# Network backup profiles that are waiting on the network
63+
self.network_deferred_timer = QTimer()
64+
self.network_deferred_timer.timeout.connect(self.create_backup_if_net_up)
65+
self.network_deferred_timer.setInterval(5 * 1000) # Short interval for the network to come up
66+
# Don't start until its actually needed
67+
self.network_deferred_profiles: List[str] = []
68+
6269
# connect signals
6370
self.app.backup_finished_event.connect(lambda res: self.set_timer_for_profile(res['params']['profile_id']))
6471

@@ -81,6 +88,27 @@ def loginSuspendNotify(self, suspend: bool):
8188
logger.debug("Got login suspend/resume notification")
8289
self.reload_all_timers()
8390

91+
def create_backup_if_net_up(self):
92+
nm = get_network_status_monitor()
93+
if nm.is_network_active():
94+
# Cancel the timer
95+
self.network_deferred_timer.stop()
96+
logger.info("the network is active, dispatching waiting jobs")
97+
# create_backup will add to waiting_network if the network goes down again
98+
# flip ahead of time here in case that happens
99+
waiting = self.network_deferred_profiles
100+
self.network_deferred_profiles = []
101+
for profile_id in waiting:
102+
self.create_backup(profile_id)
103+
else:
104+
logger.debug("there are jobs waiting on the network, but it is not yet up")
105+
106+
def defer_backup(self, profile_id):
107+
if not self.network_deferred_profiles:
108+
# Nothing is currently waiting so start the timer
109+
self.network_deferred_timer.start()
110+
self.network_deferred_profiles.append(profile_id)
111+
84112
def tr(self, *args, **kwargs):
85113
scope = self.__class__.__name__
86114
return translate(scope, *args, **kwargs)
@@ -397,6 +425,15 @@ def create_backup(self, profile_id):
397425
logger.info('Profile not found. Maybe deleted?')
398426
return
399427

428+
if profile.repo.is_remote_repo() and not get_network_status_monitor().is_network_active():
429+
logger.info(
430+
'repo %s is remote and there is no active network connection, deferring backup for %s',
431+
profile.repo.name,
432+
profile.name,
433+
)
434+
self.defer_backup(profile_id)
435+
return
436+
400437
# Skip if a job for this profile (repo) is already in progress
401438
if self.app.jobs_manager.is_worker_running(site=profile.repo.id):
402439
logger.debug('A job for repo %s is already active.', profile.repo.id)
@@ -521,6 +558,12 @@ def post_backup_tasks(self, profile_id):
521558
)
522559

523560
def remove_job(self, profile_id):
561+
if profile_id in self.network_deferred_profiles:
562+
self.network_deferred_profiles.remove(profile_id)
563+
# If nothing is waiting cancel the timer
564+
if not self.network_deferred_profiles:
565+
self.network_deferred_timer.stop()
566+
524567
if profile_id in self.timers:
525568
qtimer = self.timers[profile_id].get('qtt')
526569
if qtimer is not None:

0 commit comments

Comments
 (0)