Skip to content

Commit 0404e54

Browse files
authored
Merge pull request #2016 from pbiering/server-delay
Server delay
2 parents 28f0bf9 + 7fd6cde commit 0404e54

File tree

7 files changed

+34
-3
lines changed

7 files changed

+34
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Cleanup: deprecate config option 'ldap_use_ssl' for good
88
* Performance: improve `path_to_filesystem()`
99
* Performance: preload access rights from file
10+
* Add: [server] delay_on_error option
1011

1112
## 3.6.1
1213

DOCUMENTATION.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,14 @@ The maximum number of parallel connections. Set to `0` to disable the limit.
831831

832832
Default: `8`
833833

834+
##### delay_on_error
835+
836+
_(>= 3.7.0)_
837+
838+
Base delay in case of error response (seconds)
839+
840+
Default: `0.01`
841+
834842
##### max_content_length
835843

836844
The maximum size of the request body. (bytes)

config

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
# Max parallel connections
2222
#max_connections = 8
2323

24+
# Base delay in case of error response (seconds)
25+
#delay_on_error = 0.01
26+
2427
# Max size of request body (bytes), default: 100 Mbyte
2528
# In case of using a reverse proxy in front of check also there related option
2629
#max_content_length = 100000000

radicale/app/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Copyright © 2008 Pascal Halter
44
# Copyright © 2008-2017 Guillaume Ayoub
55
# Copyright © 2017-2019 Unrud <unrud@outlook.com>
6-
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
6+
# Copyright © 2024-2026 Peter Bieringer <pb@bieringer.de>
77
#
88
# This library is free software: you can redistribute it and/or modify
99
# it under the terms of the GNU General Public License as published by
@@ -72,6 +72,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
7272

7373
_mask_passwords: bool
7474
_auth_delay: float
75+
_delay_on_error: float
7576
_internal_server: bool
7677
_max_content_length: int
7778
_max_resource_size: int
@@ -97,6 +98,8 @@ def __init__(self, configuration: config.Configuration) -> None:
9798
"""
9899
super().__init__(configuration)
99100
self._mask_passwords = configuration.get("logging", "mask_passwords")
101+
self._delay_on_error = configuration.get("server", "delay_on_error")
102+
logger.info("delay_on_error set to: %.3f seconds", self._delay_on_error)
100103
self._max_content_length = configuration.get("server", "max_content_length")
101104
self._max_resource_size = configuration.get("server", "max_resource_size")
102105
logger.info("max_content_length set to: %d bytes (%sbytes)", self._max_content_length, utils.format_unit(self._max_content_length, binary=True))
@@ -105,7 +108,7 @@ def __init__(self, configuration: config.Configuration) -> None:
105108
logger.warning("max_resource_size set to: %d bytes (%sbytes) (capped from %d to 80%% of max_content_length)", max_resource_size_limited, utils.format_unit(max_resource_size_limited, binary=True), self._max_resource_size)
106109
self._max_resource_size = max_resource_size_limited
107110
else:
108-
logger.info("max_resource_size set to: %d bytes (%sbytes)", self._max_resource_size, utils.format_unit(self._max_resource_size, binary=True))
111+
logger.info("max_resource_size set to: %d bytes (%sbytes)", self._max_resource_size, utils.format_unit(self._max_resource_size, binary=True))
109112
self._bad_put_request_content = configuration.get("logging", "bad_put_request_content")
110113
logger.info("log bad put request content: %s", self._bad_put_request_content)
111114
self._request_header_on_debug = configuration.get("logging", "request_header_on_debug")
@@ -304,6 +307,15 @@ def response(status: int, headers: types.WSGIResponseHeaders,
304307
flags_text = " (" + " ".join(flags) + ")"
305308
else:
306309
flags_text = ""
310+
# delay on error
311+
if status >= 400:
312+
if self._delay_on_error > 0:
313+
random_delay = self._delay_on_error * (1 + random.random())
314+
if status >= 500:
315+
random_delay = 2 * random_delay
316+
if logger.isEnabledFor(logging.DEBUG):
317+
logger.debug("Response delay triggered by result code: %d -> %0.3f seconds", status, random_delay)
318+
time.sleep(random_delay)
307319
if answer is not None:
308320
logger.info("%s response status for %r%s in %.3f seconds %s %s bytes%s: %s",
309321
request_method, unsafe_path, depthinfo,

radicale/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ def json_str(value: Any) -> dict:
171171
"value": "100000000",
172172
"help": "maximum size of request body in bytes (default: 100 Mbyte)",
173173
"type": positive_int}),
174+
("delay_on_error", {
175+
"value": "0.01",
176+
"help": "base delay in case of error response (seconds)",
177+
"type": positive_float}),
174178
("max_resource_size", {
175179
"value": "10000000",
176180
"help": "maximum size of resource (default: 10 Mbyte)",

radicale/tests/test_auth.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ def _test_htpasswd(self, htpasswd_encryption: str, htpasswd_content: str,
6969
with open(htpasswd_file_path, "w", encoding=encoding) as f:
7070
f.write(htpasswd_content)
7171
self.configure({"auth": {"type": "htpasswd",
72+
"delay": 0,
7273
"htpasswd_filename": htpasswd_file_path,
73-
"htpasswd_encryption": htpasswd_encryption}})
74+
"htpasswd_encryption": htpasswd_encryption},
75+
"server": {"delay_on_error": 0}})
7476
if test_matrix == "ascii":
7577
test_matrix = (("tmp", "bepo", True), ("tmp", "tmp", False),
7678
("tmp", "", False), ("unk", "unk", False),

radicale/tests/test_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def setup_method(self) -> None:
7373
permissions: RrWw""")
7474
self.configure({"rights": {"file": rights_file_path,
7575
"type": "from_file"},
76+
"server": {"delay_on_error": 0},
7677
"logging": {"request_header_on_debug": "True",
7778
"request_content_on_debug": "True",
7879
"response_header_on_debug": "True",

0 commit comments

Comments
 (0)