Gitlab Community Edition Instance

Commit 28ace6ab authored by Marcel Hellkamp's avatar Marcel Hellkamp Committed by mhellka
Browse files

Added keepalive functionality

parent 4d7c47ad
import os import os
import urllib.parse import urllib.parse
from threading import Timer, Thread, Event
PATH_TYPES = (str,) PATH_TYPES = (str,)
if hasattr(os, "PathLike"): if hasattr(os, "PathLike"):
...@@ -32,3 +33,30 @@ def url_split_auth(url: str): ...@@ -32,3 +33,30 @@ def url_split_auth(url: str):
netloc += ":" + str(split.port) netloc += ":" + str(split.port)
url = urllib.parse.urlunsplit(split._replace(netloc=netloc)) url = urllib.parse.urlunsplit(split._replace(netloc=netloc))
return url, username, password return url, username, password
class IntervalTimer(Thread):
""" A thread that runs a function over and over until stopped.
t = IntervalTimer(30.0, f, args=None, kwargs=None)
t.start()
t.cancel()
"""
def __init__(self, interval, function, *args, **kwargs):
Thread.__init__(self)
self.interval = interval
self.function = function
self.args = args
self.kwargs = kwargs
self.finished = Event()
def set_interval(self, interval):
self.interval = interval
def cancel(self):
self.finished.set()
def run(self):
while not self.finished.wait(self.interval):
self.function(*self.args, **self.kwargs)
...@@ -3,11 +3,12 @@ Client api implementation. Usually imported directly from :mod:`pycdstar` and no ...@@ -3,11 +3,12 @@ Client api implementation. Usually imported directly from :mod:`pycdstar` and no
""" """
import os import os
import threading
import typing import typing
import requests import requests
from pycdstar3._utils import PATH_TYPES, url_split_auth from pycdstar3._utils import PATH_TYPES, url_split_auth, IntervalTimer
from pycdstar3.model import ApiError, JsonObject, FileDownload, FormUpdate from pycdstar3.model import ApiError, JsonObject, FileDownload, FormUpdate
__all__ = "CDStar", "CDStarVault", "FormUpdate", "ApiError" __all__ = "CDStar", "CDStarVault", "FormUpdate", "ApiError"
...@@ -43,6 +44,7 @@ class CDStar: ...@@ -43,6 +44,7 @@ class CDStar:
self._session = _session or requests.Session() self._session = _session or requests.Session()
self._tx = None self._tx = None
self._autocommit = False self._autocommit = False
self._keepalive_timer = None
def clone(self): def clone(self):
""" Return an independent instance with the same settings. """ Return an independent instance with the same settings.
...@@ -79,20 +81,24 @@ class CDStar: ...@@ -79,20 +81,24 @@ class CDStar:
raise ApiError(rs) raise ApiError(rs)
def rest(self, method, *path, expect_status=None, **options) -> JsonObject: def rest(self, method, *path, expect_status=None, **options) -> typing.Optional[JsonObject]:
""" Just like `raw()`, but expects the response to be JSON and returns """ Just like `raw()`, but expects the response to be JSON and returns
the parsed result instead of the raw response. Non-JSON responses the parsed result instead of the raw response. Non-JSON responses
are errors. are errors. Empty (204) responses return None.
Disclaimer: Avoid using this method if there is a more specific implementation available. Disclaimer: Avoid using this method if there is a more specific implementation available.
If you find a feature missing from this class, please submit a feature request instead of If you find a feature missing from this class, please submit a feature request instead of
over-using this method. over-using this method.
""" """
# TODO: Expect json errors or non-json responses and throw a better error message # TODO: Expect json errors or non-json responses and
return self.raw(method, *path, expect_status=expect_status, **options).json(object_hook=JsonObject) # throw a better error message
rs = self.raw(method, *path, expect_status=expect_status, **options)
if rs.status_code == 204:
return None
return rs.json(object_hook=JsonObject)
def begin(self, autocommit=False, readonly=False): def begin(self, autocommit=False, readonly=False, keepalive=False):
""" Start a new transaction and return self. """ Start a new transaction and return self.
Transactions are used to group multiple operations into a single atomic action. After begin(), you have to Transactions are used to group multiple operations into a single atomic action. After begin(), you have to
...@@ -108,23 +114,28 @@ class CDStar: ...@@ -108,23 +114,28 @@ class CDStar:
closed automatically. If autocommit is true and no exception was raised, it is committed. Otherwise, it is closed automatically. If autocommit is true and no exception was raised, it is committed. Otherwise, it is
rolled back. rolled back.
:param autocommit: Commit this transaction automatically after the with-block without errors? (default: false) :param autocommit: Commit this transaction if the with-block ended without errors. (default: False)
:param readonly: Create a (cheap) read-only transaction. :param readonly: Create a (cheaper) read-only transaction. (default: False)
:param keepalive: Automatically call :meth:`keepalive` from a separate thread. This is only
required when waiting for user input for a long time. (default: False)
""" """
self.rollback() self.rollback()
self._autocommit = autocommit self._autocommit = autocommit
self._tx = self.rest("POST", "_tx", data={"readonly": readonly}) self._tx = self.rest("POST", "_tx", data={"readonly": readonly})
if keepalive:
self._auto_keepalive_start()
return self return self
@property @property
def tx(self) -> JsonObject: def tx(self) -> JsonObject:
""" Return the current transaction handle, or None if no transaction is running. """ """ Return the transaction handle, or None if no transaction is running. """
return self._tx return self._tx
def commit(self): def commit(self):
""" Commit the current transaction. """ """ Commit the current transaction. """
if not self._tx: if not self._tx:
raise RuntimeError("No transaction running") raise RuntimeError("No transaction running")
self._auto_keepalive_stop()
try: try:
self.raw("POST", "_tx", self._tx.id) self.raw("POST", "_tx", self._tx.id)
...@@ -135,17 +146,45 @@ class CDStar: ...@@ -135,17 +146,45 @@ class CDStar:
def rollback(self): def rollback(self):
""" Rollback the current transaction, if any. Do nothing otherwise. """ """ Rollback the current transaction, if any. Do nothing otherwise. """
self._auto_keepalive_stop()
try: try:
if self._tx: if self._tx:
self.raw("DELETE", "_tx", self._tx.id) self.raw("DELETE", "_tx", self._tx["id"])
finally: finally:
self._tx = None self._tx = None
def keepalive(self): def keepalive(self):
""" If a transaction is running, keep it alive. Otherwise, do nothing. """ """ Keep the current transaction alive (reset the timeout). """
if not self._tx: if not self._tx:
raise RuntimeError("No transaction running") raise RuntimeError("No transaction running")
self._tx = self.rest("GET", "_tx", self._tx.id) self._tx = self.rest("POST", "_tx", self._tx["id"], params={"renew": True})
def _auto_keepalive_start(self):
self._auto_keepalive_stop()
# leeway: 10% or 2 seconds, but sleep for at least a second
interval = max(1, min(self.tx['ttl'] * 0.9, self.tx['ttl'] - 2))
self._keepalive_timer = IntervalTimer(interval, self._auto_keepalive, self.tx)
self._keepalive_timer.daemon = True
self._keepalive_timer.start()
def _auto_keepalive(self, tx):
timer = self._keepalive_timer
if timer != threading.current_thread():
raise RuntimeError()
if not self.tx or self.tx["id"] != tx["id"]:
raise RuntimeError()
self.keepalive()
# leeway: 10% or 2 seconds, but sleep for at least a second
timer.set_interval(max(1, min(self.tx['ttl'] * 0.9, self.tx['ttl'] - 2)))
def _auto_keepalive_stop(self):
""" Stop the current keepalive timer, if any. """
if self._keepalive_timer:
self._keepalive_timer.cancel()
self._keepalive_timer = None
def __enter__(self): def __enter__(self):
""" Expect a transaction to be already running. """ """ Expect a transaction to be already running. """
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment