Skip to content

Commit

Permalink
Make max idle time lower bound user-configurable
Browse files Browse the repository at this point in the history
Make max idle time lower bound configurable using the pafd
configuration file.

This patch also renames the upper bound configuration variable.

The max idle time limits are now put into a YAML dictionary "idle",
with optional two keys "min" and "max".

The semantics are the same as libpaf's PAF_IDLE_MIN and PAF_IDLE_MAX
environment variables.

For backward compatibility, the "max_idle_time" domain
dictionary-level parameter is also allowed.

Resolves #50.

Signed-off-by: Mattias Rönnblom <mattias.ronnblom@ericsson.com>
  • Loading branch information
m-ronnblom committed Apr 6, 2024
1 parent fe82acd commit f82596e
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 72 deletions.
3 changes: 3 additions & 0 deletions conf/pafd-example.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
# the effects of a server failure.
domains:
- name: domain0 # The name is optional, and used only for logging.
idle: # Maximum idle time limits (v3 clients only)
min: 10 # Lower limit.
max: 60 # Upper limit.
sockets: # A list of server sockets for this domain. Having
# multiple sockets (using different transport protocol)
# may be useful is some client are local, and some
Expand Down
24 changes: 18 additions & 6 deletions doc/man/pafd.8.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,18 @@ the *name* key. This domain name is only used for logging and
documentation and is not seen in the by clients to the Pathfinder
server.

The default max client idle time (in seconds) may by configured by
adding a *max_idle_time* key to the domain dictionary. The default is
30 seconds. The actual maximum idle time may be lower (e.g., if a
client has published services with a low time-to-live [TTL]).
The max idle time for Pathfinder Protocol v3 clients may be controlled
on a per-domain basis by configuring a dictionary *idle* with
either/both of the *max* and *min* keys.

*max* represents an upper bound for the maximum idle time (in seconds)
that will ever be employed for any client. This maximum value is also
used as the initial value for clients connecting to that domain. The
actual maximum idle time may be lower (e.g., if a client has published
services with a low time-to-live [TTL]). *max* default is 30 seconds.

*min* represents a lower bound for the maximum idle time. The default
value is 4 seconds. *min* may not be set lower than 1 second.

Each element of the *sockets* list is a socket. A socket is either the
address in the form of a string (in XCM format), or a dictionary,
Expand Down Expand Up @@ -176,10 +184,14 @@ command-line options for details.
Configuration file example:

domains:
- addrs: # Domain which may be access via two server sockets
- name: domain0
addrs: # Domain which may be access via two server sockets
- tls:*:4711
- ux:local
- addrs: # Second domain, only available at one socket
- name: domain1
idle:
max: 15
addrs: # Second domain, only available at one socket
- tls:192.168.1.59:5711

resources:
Expand Down
75 changes: 56 additions & 19 deletions paf/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
DEFAULT_LOG_SYSLOG = True
DEFAULT_LOG_FACILITY = logging.handlers.SysLogHandler.LOG_DAEMON
DEFAULT_LOG_FILTER = logging.INFO
DEFAULT_MAX_IDLE_TIME = 30
DEFAULT_IDLE_MIN = 4
DEFAULT_IDLE_MAX = 30


class Error(Exception):
Expand All @@ -26,6 +27,12 @@ def __init__(self, dict_path, dict_key):
path(dict_path, dict_key))


class DuplicateFieldError(Error):
def __init__(self, dict_path, dict_key):
Error.__init__(self, "parameter '%s' was used in combination "
"with one of its aliases" % path(dict_path, dict_key))


class FormatError(Error):
def __init__(self, field_name, illegal_value, valid_values=None):
message = "invalid %s: '%s'" % (field_name, illegal_value)
Expand Down Expand Up @@ -185,21 +192,26 @@ def __repr__(self):


class DomainConf:
def __init__(self, name, max_idle_time):
def __init__(self, name, idle_limit):
self.name = name
self.max_idle_time = max_idle_time
self.idle_limit = idle_limit
self.sockets = []

def add_socket(self, addr, tls_attrs={}):
self.sockets.append(SocketConf(addr, tls_attrs))

def __str__(self):
s = {}

if self.name is not None:
s["name"] = self.name

s["sockets"] = self.sockets

s["max_idle_time"] = self.max_idle_time
s["idle"] = {
"min": self.idle_limit.idle_min,
"max": self.idle_limit.idle_max
}

return str(s)

Expand All @@ -210,8 +222,10 @@ def __init__(self):
self.domains = []
self.resources = ResourcesConf()

def add_domain(self, name=None, max_idle_time=DEFAULT_MAX_IDLE_TIME):
domain_conf = DomainConf(name, max_idle_time)
def add_domain(self, name=None,
idle_limit=sd.IdleLimit(DEFAULT_IDLE_MIN,
DEFAULT_IDLE_MAX)):
domain_conf = DomainConf(name, idle_limit)
self.domains.append(domain_conf)
return domain_conf

Expand Down Expand Up @@ -241,9 +255,19 @@ def assure_type(value, value_type, path):
"'%s')" % (path, type(value), value_type))


def dict_lookup(dict_value, dict_key, value_type, dict_path, required=False,
def dict_lookup(dict_value, dict_keys, value_type, dict_path, required=False,
default=None):
value = dict_value.get(dict_key)
if (isinstance(dict_keys, str)):
dict_keys = [dict_keys]

value = None

for dict_key in dict_keys:
if dict_key in dict_value:
if value is not None:
raise DuplicateFieldError(dict_path, dict_key)
value = dict_value.get(dict_key)

if value is None:
if required:
assert default is None
Expand Down Expand Up @@ -291,20 +315,33 @@ def domains_populate(conf, domains, path):

name = dict_lookup(domain, "name", str, domain_path, required=False)

max_idle_time = dict_lookup(domain, "max_idle_time", int,
domain_path, default=DEFAULT_MAX_IDLE_TIME,
required=False)
idle_min = DEFAULT_IDLE_MIN

# 'max_idle_time' is a legacy name for 'idle_max'
idle_max = dict_lookup(domain, "max_idle_time", int,
domain_path, default=DEFAULT_IDLE_MAX,
required=False)

idle = domain.get("idle")
if idle is not None:
idle_path = "%s.idle" % domain_path

idle_min = dict_lookup(idle, "min", int, idle_path,
default=DEFAULT_IDLE_MIN, required=False)

if "max_idle_time" in domain and "max" in idle:
raise DuplicateFieldError(domain_path, "max_idle_time")

idle_max = dict_lookup(idle, "max", int, idle_path,
default=DEFAULT_IDLE_MAX, required=False)

# 'addrs' is an alternative name, supported for backward
# compatibility reasons
sockets = dict_lookup(domain, "addrs", list, domain_path,
required=False)
# 'addrs' is a legacy name for 'sockets'
sockets = dict_lookup(domain, ["sockets", "addrs"], list, domain_path,
required=True)

if sockets is None:
sockets = dict_lookup(domain, "sockets", list, domain_path,
required=True)
idle_limit = sd.IdleLimit(idle_min, idle_max)

domain_conf = conf.add_domain(name, max_idle_time)
domain_conf = conf.add_domain(name, idle_limit)

for socket_num, socket in enumerate(sockets):
socket_path = "%s.sockets[%d]" % (domain_path, socket_num)
Expand Down
2 changes: 1 addition & 1 deletion paf/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def run(conf, hook=None):
user = conf.resources.user.resources
total = conf.resources.total.resources
server = paf.server.create(domain.name, domain.sockets, user,
total, domain.max_idle_time, event_loop)
total, domain.idle_limit, event_loop)
servers.append(server)

if hook is not None:
Expand Down
58 changes: 38 additions & 20 deletions paf/sd.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,27 @@ def transfer(self, from_user_id, to_user_id, resource_type):
raise e


MIN_IDLE_MIN = 1


class IdleLimit:
def __init__(self, idle_min, idle_max):
if idle_min < MIN_IDLE_MIN:
raise ValueError("Lower bound for max idle time must be set "
">= %d" % MIN_IDLE_MIN)
if idle_min > idle_max:
raise ValueError("Max idle time lower bound must be equal to "
"or lower than the upper bound")
self.idle_min = idle_min
self.idle_max = idle_max

def limit(self, value):
return min(self.idle_max, max(self.idle_min, value))

def idle_default(self):
return self.idle_max


class Subscription:
def __init__(self, sub_id, filter, client_id, user_id, match_cb):
self.sub_id = sub_id
Expand Down Expand Up @@ -291,7 +312,6 @@ def assure_not_connected(fun):

WARNING_THRESHOLD = 0.5
WARNING_JITTER = 0.1
MIN_IDLE_TIME = 4


class IdleState(enum.Enum):
Expand All @@ -306,10 +326,10 @@ def jitter(base, max_jitter):


class Connection:
def __init__(self, client, timer_manager, max_idle_time, idle_cb):
def __init__(self, client, timer_manager, idle_limit, idle_cb):
self.client = client
self.timer_manager = timer_manager
self.max_idle_time = max_idle_time
self.idle_limit = idle_limit
self.idle_cb = idle_cb
self.subscriptions = {}
self.services = {}
Expand All @@ -318,7 +338,7 @@ def __init__(self, client, timer_manager, max_idle_time, idle_cb):
self.idle_state = IdleState.ACTIVE
self.idle_timer = None
self.last_seen = time.time()
if max_idle_time is not None:
if idle_limit is not None:
self.install_idle_warning_timer()

def client_id(self):
Expand Down Expand Up @@ -374,38 +394,37 @@ def active(self):
if self.idle_state != IdleState.TIMED_OUT:
self.idle_state = IdleState.ACTIVE

if self.max_idle_time is not None:
if self.idle_limit is not None:
self.install_idle_warning_timer()

self.last_seen = time.time()

def check_idle(self):
assert self.max_idle_time is not None
assert self.idle_limit is not None

if self.idle_state == IdleState.ACTIVE:
self.issue_idle_warning()

def install_idle_timer(self, t):
assert self.max_idle_time is not None
assert self.idle_limit is not None
self.uninstall_idle_timer()
self.idle_timer = \
self.timer_manager.add(self.idle_timer_fired, t, relative=True)

def idle_time(self):
if self.max_idle_time is not None:
t = self.max_idle_time
def max_idle_time(self):
if self.idle_limit is not None:
if len(self.services) > 0:
t = min(t, self.get_lowest_ttl())
t = max(t, MIN_IDLE_TIME)
return t
return self.idle_limit.limit(self.get_lowest_ttl())
else:
return self.idle_limit.idle_default()

def install_idle_warning_timer(self):
warning_time = jitter(WARNING_THRESHOLD * self.idle_time(),
warning_time = jitter(WARNING_THRESHOLD * self.max_idle_time(),
WARNING_JITTER)
self.install_idle_timer(warning_time)

def install_idle_timeout_timer(self):
self.install_idle_timer((1 - WARNING_THRESHOLD) * self.idle_time())
self.install_idle_timer((1 - WARNING_THRESHOLD) * self.max_idle_time())

def uninstall_idle_timer(self):
if self.idle_timer is not None:
Expand Down Expand Up @@ -507,7 +526,7 @@ def is_stale(self):
return False
return True

def connect(self, user_id, max_idle_time, conn_idle_cb):
def connect(self, user_id, idle_limit, conn_idle_cb):
if self.is_connected():
# The active connection may be down but this has not yet
# noticed by the server.
Expand All @@ -522,7 +541,7 @@ def connect(self, user_id, max_idle_time, conn_idle_cb):
self.db.add_client(self)

self.active_connection = \
Connection(self, self.timer_manager, max_idle_time, conn_idle_cb)
Connection(self, self.timer_manager, idle_limit, conn_idle_cb)

@assure_connected
def disconnect(self):
Expand Down Expand Up @@ -737,15 +756,14 @@ def __init__(self, name, timer_manager, max_user_resources,
self.db = DB()
self.orphan_timers = {}

def client_connect(self, client_id, user_id, max_idle_time,
conn_idle_cb):
def client_connect(self, client_id, user_id, idle_limit, conn_idle_cb):
client = self.db.get_client(client_id)

if client is None:
client = Client(client_id, user_id, self.db, self.resource_manager,
self.timer_manager)

client.connect(user_id, max_idle_time, conn_idle_cb)
client.connect(user_id, idle_limit, conn_idle_cb)

def client_disconnect(self, client_id):
client = self._get_connected_client(client_id)
Expand Down
Loading

0 comments on commit f82596e

Please sign in to comment.