From f2b69dbab8f368299edeafb0d440f099a93db361 Mon Sep 17 00:00:00 2001 From: "Jose D. Gomez R." Date: Mon, 22 Sep 2025 17:45:11 +0200 Subject: [PATCH 1/2] feat: Optional keepalive for the http connection Long running connections and strict firewall policies (one can argue that are not RFC compliant but that's besides the point) can incur in connections getting stalled forever. Upon connecting to the OBS backend, every so minutes (5 min) to "keep the line up". --- oscfs/fs.py | 46 ++++++++++++++++++++++++++++++++++------------ oscfs/obs.py | 11 +++++++++++ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/oscfs/fs.py b/oscfs/fs.py index 8e73ef9..ba7b0a2 100644 --- a/oscfs/fs.py +++ b/oscfs/fs.py @@ -2,6 +2,9 @@ import errno import os import sys +import threading +import time +import contextlib # third party modules import fuse @@ -139,8 +142,26 @@ def _setupLogfile(self): sys.stdout = lf sys.stderr = lf - def run(self): + def _keepalive(self): + while self._keepalive: + time.sleep(5 * 60) + # send a request to keep the connection alive + self.m_obs.about() + + @contextlib.contextmanager + def optional_keepalive(self): + if "--no-urlopen-wrapper" in sys.argv: + yield + return + self._timer = threading.Thread(target=self._keepalive) + self._keepalive = True + self._timer.start() + yield + self._keepalive = False + self._timer.join() + + def run(self): self.m_args = self.m_parser.parse_args() self._setupLogfile() self.m_obs.configure(self.m_args.apiurl) @@ -152,17 +173,18 @@ def run(self): self._checkAuth() - fuse.FUSE( - self, - self.m_args.mountpoint, - foreground=self.m_args.f, - nothreads=True, - # direct_io is necessary in our use case to avoid - # caching in the kernel and support dynamically - # determined file contents - direct_io=True, - nonempty=True - ) + with self.optional_keepalive(): + fuse.FUSE( + self, + self.m_args.mountpoint, + foreground=self.m_args.f, + nothreads=True, + # direct_io is necessary in our use case to avoid + # caching in the kernel and support dynamically + # determined file contents + direct_io=True, + nonempty=True + ) def init(self, path): """This is called upon file system initialization.""" diff --git a/oscfs/obs.py b/oscfs/obs.py index 8194637..38ca24c 100644 --- a/oscfs/obs.py +++ b/oscfs/obs.py @@ -198,6 +198,17 @@ def _download(self, urlcomps, query=dict()): return f.read() + @transparent_retry() + def about(self): + url = osc.core.makeurl( + self.m_apiurl, + ['about'] + ) + + f = osc.core.http_GET(url) + + return f.read() + @transparent_retry() def _getPackageRevisions(self, project, package, fmt): """Returns the list of revisions for the given project/package From a63a571c3828c8b421fc7430142f6d7a02e5419a Mon Sep 17 00:00:00 2001 From: "Jose D. Gomez R" <1josegomezr@gmail.com> Date: Thu, 25 Sep 2025 21:20:18 +0200 Subject: [PATCH 2/2] fixup: Use FUSE init method and remove the contextlib implementation --- oscfs/fs.py | 56 ++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/oscfs/fs.py b/oscfs/fs.py index ba7b0a2..20c152c 100644 --- a/oscfs/fs.py +++ b/oscfs/fs.py @@ -4,7 +4,6 @@ import sys import threading import time -import contextlib # third party modules import fuse @@ -35,6 +34,7 @@ def __init__(self): # unallocated file handles self.m_free_handles = list(range(1024)) self._setupParser() + self._keepalive_timer = threading.Thread(target=self._keepalive_thread) def _setupParser(self): @@ -87,6 +87,14 @@ def _setupParser(self): seconds. Set to zero to disable caching.""" ) + self.m_parser.add_argument( + "--keepalive-interval", type=int, + default=0, + help=f"""Specifies the time in seconds a keepalive request will be + issued towards the OBS backend. Default: 0 + seconds. Set to zero to disable keepalive.""" + ) + def _checkAuth(self): """Check for correct authentication at the remote server.""" # simply fetch the root entries, this will also benefit the @@ -142,25 +150,12 @@ def _setupLogfile(self): sys.stdout = lf sys.stderr = lf - def _keepalive(self): + def _keepalive_thread(self): while self._keepalive: - time.sleep(5 * 60) + time.sleep(self.m_args.keepalive_interval) # send a request to keep the connection alive self.m_obs.about() - @contextlib.contextmanager - def optional_keepalive(self): - if "--no-urlopen-wrapper" in sys.argv: - yield - return - - self._timer = threading.Thread(target=self._keepalive) - self._keepalive = True - self._timer.start() - yield - self._keepalive = False - self._timer.join() - def run(self): self.m_args = self.m_parser.parse_args() self._setupLogfile() @@ -173,18 +168,20 @@ def run(self): self._checkAuth() - with self.optional_keepalive(): - fuse.FUSE( - self, - self.m_args.mountpoint, - foreground=self.m_args.f, - nothreads=True, - # direct_io is necessary in our use case to avoid - # caching in the kernel and support dynamically - # determined file contents - direct_io=True, - nonempty=True - ) + fuse.FUSE( + self, + self.m_args.mountpoint, + foreground=self.m_args.f, + nothreads=True, + # direct_io is necessary in our use case to avoid + # caching in the kernel and support dynamically + # determined file contents + direct_io=True, + nonempty=True + ) + + self._keepalive = False + self._keepalive_timer.join() def init(self, path): """This is called upon file system initialization.""" @@ -195,6 +192,9 @@ def init(self, path): print("file system initialized") sys.stdout.flush() + self._keepalive = self.m_args.keepalive_interval > 0 + self._keepalive_timer.start() + # global file system methods def getattr(self, path, fh=None):