Skip to content

Commit f330774

Browse files
Merge pull request #237 from ArangoDB-Community/support-overload-control
Support overload control
2 parents 8008f86 + 8b09e07 commit f330774

File tree

10 files changed

+293
-9
lines changed

10 files changed

+293
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
main
22
----
33

4+
* Added OverloadControlDatabase, enabling the client to react effectively to potential server overloads.
5+
46
* The db.version() now has a new optional parameter "details" that can be used to return additional information about
57
the server version. The default is still false, so the old behavior is preserved.
68

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ database natively supporting documents, graphs and search.
1414

1515
## Requirements
1616

17-
- ArangoDB version 3.7+
17+
- ArangoDB version 3.9+
1818
- Python version 3.8+
1919

2020
## Installation

arango/database.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
__all__ = ["StandardDatabase", "AsyncDatabase", "BatchDatabase", "TransactionDatabase"]
1+
__all__ = [
2+
"StandardDatabase",
3+
"AsyncDatabase",
4+
"BatchDatabase",
5+
"OverloadControlDatabase",
6+
"TransactionDatabase",
7+
]
28

39
from datetime import datetime
410
from numbers import Number
@@ -75,6 +81,7 @@
7581
AsyncApiExecutor,
7682
BatchApiExecutor,
7783
DefaultApiExecutor,
84+
OverloadControlApiExecutor,
7885
TransactionApiExecutor,
7986
)
8087
from arango.formatter import (
@@ -2518,6 +2525,19 @@ def begin_transaction(
25182525
max_size=max_size,
25192526
)
25202527

2528+
def begin_controlled_execution(
2529+
self, max_queue_time_seconds: Optional[float] = None
2530+
) -> "OverloadControlDatabase":
2531+
"""Begin a controlled connection, with options to handle server-side overload.
2532+
2533+
:param max_queue_time_seconds: Maximum time in seconds a request can be queued
2534+
on the server-side. If set to 0 or None, the server ignores this setting.
2535+
:type max_queue_time_seconds: Optional[float]
2536+
:return: Database API wrapper object specifically for queue bounded execution.
2537+
:rtype: arango.database.OverloadControlDatabase
2538+
"""
2539+
return OverloadControlDatabase(self._conn, max_queue_time_seconds)
2540+
25212541

25222542
class AsyncDatabase(Database):
25232543
"""Database API wrapper tailored specifically for async execution.
@@ -2688,3 +2708,55 @@ def abort_transaction(self) -> bool:
26882708
:raise arango.exceptions.TransactionAbortError: If abort fails.
26892709
"""
26902710
return self._executor.abort()
2711+
2712+
2713+
class OverloadControlDatabase(Database):
2714+
"""Database API wrapper tailored to gracefully handle server overload scenarios.
2715+
2716+
See :func:`arango.database.StandardDatabase.begin_controlled_execution`.
2717+
2718+
:param connection: HTTP connection.
2719+
:param max_queue_time_seconds: Maximum server-side queuing time in seconds.
2720+
If the server-side queuing time exceeds the client's specified limit,
2721+
the request will be rejected.
2722+
:type max_queue_time_seconds: Optional[float]
2723+
"""
2724+
2725+
def __init__(
2726+
self, connection: Connection, max_queue_time_seconds: Optional[float] = None
2727+
) -> None:
2728+
self._executor: OverloadControlApiExecutor
2729+
super().__init__(
2730+
connection=connection,
2731+
executor=OverloadControlApiExecutor(connection, max_queue_time_seconds),
2732+
)
2733+
2734+
def __repr__(self) -> str: # pragma: no cover
2735+
return f"<OverloadControlDatabase {self.name}>"
2736+
2737+
@property
2738+
def last_queue_time(self) -> float:
2739+
"""Return the most recently recorded server-side queuing time in seconds.
2740+
2741+
:return: Server-side queuing time in seconds.
2742+
:rtype: float
2743+
"""
2744+
return self._executor.queue_time_seconds
2745+
2746+
@property
2747+
def max_queue_time(self) -> Optional[float]:
2748+
"""Return the maximum server-side queuing time in seconds.
2749+
2750+
:return: Maximum server-side queuing time in seconds.
2751+
:rtype: Optional[float]
2752+
"""
2753+
return self._executor.max_queue_time_seconds
2754+
2755+
def adjust_max_queue_time(self, max_queue_time_seconds: Optional[float]) -> None:
2756+
"""Adjust the maximum server-side queuing time in seconds.
2757+
2758+
:param max_queue_time_seconds: New maximum server-side queuing time
2759+
in seconds. Setting it to None disables the limit.
2760+
:type max_queue_time_seconds: Optional[float]
2761+
"""
2762+
self._executor.max_queue_time_seconds = max_queue_time_seconds

arango/exceptions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,15 @@ class BatchExecuteError(ArangoServerError):
232232
"""Failed to execute batch API request."""
233233

234234

235+
#########################################
236+
# Overload Control Execution Exceptions #
237+
#########################################
238+
239+
240+
class OverloadControlExecutorError(ArangoServerError):
241+
"""Failed to execute overload controlled API request."""
242+
243+
235244
#########################
236245
# Collection Exceptions #
237246
#########################

arango/executor.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"AsyncApiExecutor",
55
"BatchApiExecutor",
66
"TransactionApiExecutor",
7+
"OverloadControlApiExecutor",
78
]
89

910
from collections import OrderedDict
@@ -16,6 +17,7 @@
1617
AsyncExecuteError,
1718
BatchExecuteError,
1819
BatchStateError,
20+
OverloadControlExecutorError,
1921
TransactionAbortError,
2022
TransactionCommitError,
2123
TransactionInitError,
@@ -32,6 +34,7 @@
3234
"AsyncApiExecutor",
3335
"BatchApiExecutor",
3436
"TransactionApiExecutor",
37+
"OverloadControlApiExecutor",
3538
]
3639

3740
T = TypeVar("T")
@@ -428,3 +431,84 @@ def abort(self) -> bool:
428431
if resp.is_success:
429432
return True
430433
raise TransactionAbortError(resp, request)
434+
435+
436+
class OverloadControlApiExecutor:
437+
"""Allows setting the maximum acceptable server-side queuing time
438+
for client requests.
439+
440+
:param connection: HTTP connection.
441+
:type connection: arango.connection.BasicConnection |
442+
arango.connection.JwtConnection | arango.connection.JwtSuperuserConnection
443+
:param max_queue_time_seconds: Maximum server-side queuing time in seconds.
444+
:type max_queue_time_seconds: float
445+
"""
446+
447+
def __init__(
448+
self, connection: Connection, max_queue_time_seconds: Optional[float] = None
449+
) -> None:
450+
self._conn = connection
451+
self._max_queue_time_seconds = max_queue_time_seconds
452+
self._queue_time_seconds = 0.0
453+
454+
@property
455+
def context(self) -> str: # pragma: no cover
456+
return "overload-control"
457+
458+
@property
459+
def queue_time_seconds(self) -> float:
460+
"""Return the most recent request queuing/de-queuing time.
461+
Defaults to 0 if no request has been sent.
462+
463+
:return: Server-side queuing time in seconds.
464+
:rtype: float
465+
"""
466+
return self._queue_time_seconds
467+
468+
@property
469+
def max_queue_time_seconds(self) -> Optional[float]:
470+
"""Return the maximum server-side queuing time.
471+
472+
:return: Maximum server-side queuing time in seconds.
473+
:rtype: Optional[float]
474+
"""
475+
return self._max_queue_time_seconds
476+
477+
@max_queue_time_seconds.setter
478+
def max_queue_time_seconds(self, value: Optional[float]) -> None:
479+
"""Set the maximum server-side queuing time.
480+
Setting it to None disables the feature.
481+
482+
:param value: Maximum server-side queuing time in seconds.
483+
:type value: Optional[float]
484+
"""
485+
self._max_queue_time_seconds = value
486+
487+
def execute(
488+
self,
489+
request: Request,
490+
response_handler: Callable[[Response], T],
491+
) -> T:
492+
"""Execute an API request and return the result.
493+
494+
:param request: HTTP request.
495+
:type request: arango.request.Request
496+
:param response_handler: HTTP response handler.
497+
:type response_handler: callable
498+
:return: API execution result.
499+
"""
500+
if self._max_queue_time_seconds is not None:
501+
request.headers["x-arango-queue-time-seconds"] = str(
502+
self._max_queue_time_seconds
503+
)
504+
resp = self._conn.send_request(request)
505+
506+
if not resp.is_success:
507+
raise OverloadControlExecutorError(resp, request)
508+
509+
if "X-Arango-Queue-Time-Seconds" in resp.headers:
510+
self._queue_time_seconds = float(
511+
resp.headers["X-Arango-Queue-Time-Seconds"]
512+
)
513+
514+
return response_handler(resp)

docs/contributing.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ To ensure PEP8_ compliance, run flake8_:
4040
.. code-block:: bash
4141
4242
~$ pip install flake8
43-
~$ git clone https://github.com/joowani/python-arango.git
43+
~$ git clone https://github.com/ArangoDB-Community/python-arango.git
4444
~$ cd python-arango
4545
~$ flake8
4646
@@ -57,7 +57,7 @@ To run the test suite (use your own host, port and root password):
5757
.. code-block:: bash
5858
5959
~$ pip install pytest
60-
~$ git clone https://github.com/joowani/python-arango.git
60+
~$ git clone https://github.com/ArangoDB-Community/python-arango.git
6161
~$ cd python-arango
6262
~$ py.test --complete --host=127.0.0.1 --port=8529 --passwd=passwd
6363
@@ -66,7 +66,7 @@ To run the test suite with coverage report:
6666
.. code-block:: bash
6767
6868
~$ pip install coverage pytest pytest-cov
69-
~$ git clone https://github.com/joowani/python-arango.git
69+
~$ git clone https://github.com/ArangoDB-Community/python-arango.git
7070
~$ cd python-arango
7171
~$ py.test --complete --host=127.0.0.1 --port=8529 --passwd=passwd --cov=kq
7272
@@ -82,9 +82,9 @@ Sphinx_. To build an HTML version on your local machine:
8282
.. code-block:: bash
8383
8484
~$ pip install sphinx sphinx_rtd_theme
85-
~$ git clone https://github.com/joowani/python-arango.git
86-
~$ cd python-arango/docs
87-
~$ sphinx-build . build # Open build/index.html in a browser
85+
~$ git clone https://github.com/ArangoDB-Community/python-arango.git
86+
~$ cd python-arango
87+
~$ python -m sphinx -b html -W docs docs/_build/ # Open build/index.html in a browser
8888
8989
As always, thank you for your contribution!
9090

docs/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Welcome to the documentation for **python-arango**, a Python driver for ArangoDB
1010
Requirements
1111
=============
1212

13-
- ArangoDB version 3.7+
13+
- ArangoDB version 3.9+
1414
- Python version 3.8+
1515

1616
Installation
@@ -38,6 +38,7 @@ Contents
3838
cursor
3939
async
4040
batch
41+
overload
4142
transaction
4243
admin
4344
user

docs/overload.rst

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
Overload API Execution
2+
----------------------
3+
:ref:`OverloadControlDatabase` is designed to handle time-bound requests. It allows setting a maximum server-side
4+
queuing time for client requests via the *max_queue_time_seconds* parameter. If the server's queueing time for a
5+
request surpasses this defined limit, the request will be rejected. This mechanism provides you with more control over
6+
request handling, enabling your application to react effectively to potential server overloads.
7+
8+
Additionally, the response from ArangoDB will always include the most recent request queuing/dequeuing time from the
9+
server's perspective. This can be accessed via the :attr:`~.OverloadControlDatabase.last_queue_time` property.
10+
11+
**Example:**
12+
13+
.. testcode::
14+
15+
from arango import errno
16+
from arango import ArangoClient
17+
from arango.exceptions import OverloadControlExecutorError
18+
19+
# Initialize the ArangoDB client.
20+
client = ArangoClient()
21+
22+
# Connect to "test" database as root user.
23+
db = client.db('test', username='root', password='passwd')
24+
25+
# Begin controlled execution.
26+
controlled_db = db.begin_controlled_execution(max_queue_time_seconds=7.5)
27+
28+
# All requests surpassing the specified limit will be rejected.
29+
controlled_aql = controlled_db.aql
30+
controlled_col = controlled_db.collection('students')
31+
32+
# On API execution, the last_queue_time property gets updated.
33+
controlled_col.insert({'_key': 'Neal'})
34+
35+
# Retrieve the last recorded queue time.
36+
assert controlled_db.last_queue_time >= 0
37+
38+
try:
39+
controlled_aql.execute('RETURN 100000')
40+
except OverloadControlExecutorError as err:
41+
assert err.http_code == errno.HTTP_PRECONDITION_FAILED
42+
assert err.error_code == errno.QUEUE_TIME_REQUIREMENT_VIOLATED
43+
44+
# Retrieve the maximum allowed queue time.
45+
assert controlled_db.max_queue_time == 7.5
46+
47+
# Adjust the maximum allowed queue time.
48+
controlled_db.adjust_max_queue_time(0.0001)
49+
50+
# Disable the maximum allowed queue time.
51+
controlled_db.adjust_max_queue_time(None)
52+
53+
.. note::
54+
Setting *max_queue_time_seconds* to 0 or a non-numeric value will cause ArangoDB to ignore the header.
55+
56+
See :ref:`OverloadControlDatabase` for API specification.
57+
See the `official documentation <https://www.arangodb.com/docs/stable/http/general.html#overload-control>`_ for
58+
details on ArangoDB's overload control options.

docs/specs.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ HTTPClient
135135
.. autoclass:: arango.http.HTTPClient
136136
:members:
137137

138+
.. _OverloadControlDatabase:
139+
140+
OverloadControlDatabase
141+
=======================
142+
143+
.. autoclass:: arango.database.OverloadControlDatabase
144+
:inherited-members:
145+
:members:
146+
138147
.. _Pregel:
139148

140149
Pregel

0 commit comments

Comments
 (0)