Skip to content

Commit cfd2122

Browse files
authored
3.10: Read from Followers (allow dirty read) (#222)
1 parent 3824211 commit cfd2122

File tree

4 files changed

+88
-8
lines changed

4 files changed

+88
-8
lines changed

arango/aql.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ def execute(
266266
skip_inaccessible_cols: Optional[bool] = None,
267267
max_runtime: Optional[Number] = None,
268268
fill_block_cache: Optional[bool] = None,
269+
allow_dirty_read: bool = False,
269270
) -> Result[Cursor]:
270271
"""Execute the query and return the result cursor.
271272
@@ -358,6 +359,8 @@ def execute(
358359
query will not make it into the RocksDB block cache if not already
359360
in there, thus leaving more room for the actual hot set.
360361
:type fill_block_cache: bool
362+
:param allow_dirty_read: Allow reads from followers in a cluster.
363+
:type allow_dirty_read: bool | None
361364
:return: Result cursor.
362365
:rtype: arango.cursor.Cursor
363366
:raise arango.exceptions.AQLQueryExecuteError: If execute fails.
@@ -408,7 +411,12 @@ def execute(
408411
data["options"] = options
409412
data.update(options)
410413

411-
request = Request(method="post", endpoint="/_api/cursor", data=data)
414+
request = Request(
415+
method="post",
416+
endpoint="/_api/cursor",
417+
data=data,
418+
headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None,
419+
)
412420

413421
def response_handler(resp: Response) -> Cursor:
414422
if not resp.is_success:

arango/collection.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ def has(
511511
document: Union[str, Json],
512512
rev: Optional[str] = None,
513513
check_rev: bool = True,
514+
allow_dirty_read: bool = False,
514515
) -> Result[bool]:
515516
"""Check if a document exists in the collection.
516517
@@ -523,13 +524,18 @@ def has(
523524
:param check_rev: If set to True, revision of **document** (if given)
524525
is compared against the revision of target document.
525526
:type check_rev: bool
527+
:param allow_dirty_read: Allow reads from followers in a cluster.
528+
:type allow_dirty_read: bool | None
526529
:return: True if document exists, False otherwise.
527530
:rtype: bool
528531
:raise arango.exceptions.DocumentInError: If check fails.
529532
:raise arango.exceptions.DocumentRevisionError: If revisions mismatch.
530533
"""
531534
handle, body, headers = self._prep_from_doc(document, rev, check_rev)
532535

536+
if allow_dirty_read:
537+
headers["x-arango-allow-dirty-read"] = "true"
538+
533539
request = Request(
534540
method="get",
535541
endpoint=f"/_api/document/{handle}",
@@ -662,7 +668,11 @@ def response_handler(resp: Response) -> Cursor:
662668
return self._execute(request, response_handler)
663669

664670
def find_near(
665-
self, latitude: Number, longitude: Number, limit: Optional[int] = None
671+
self,
672+
latitude: Number,
673+
longitude: Number,
674+
limit: Optional[int] = None,
675+
allow_dirty_read: bool = False,
666676
) -> Result[Cursor]:
667677
"""Return documents near a given coordinate.
668678
@@ -677,6 +687,8 @@ def find_near(
677687
:type longitude: int | float
678688
:param limit: Max number of documents returned.
679689
:type limit: int | None
690+
:param allow_dirty_read: Allow reads from followers in a cluster.
691+
:type allow_dirty_read: bool | None
680692
:returns: Document cursor.
681693
:rtype: arango.cursor.Cursor
682694
:raises arango.exceptions.DocumentGetError: If retrieval fails.
@@ -705,6 +717,7 @@ def find_near(
705717
endpoint="/_api/cursor",
706718
data={"query": query, "bindVars": bind_vars, "count": True},
707719
read=self.name,
720+
headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None,
708721
)
709722

710723
def response_handler(resp: Response) -> Cursor:
@@ -721,6 +734,7 @@ def find_in_range(
721734
upper: int,
722735
skip: Optional[int] = None,
723736
limit: Optional[int] = None,
737+
allow_dirty_read: bool = False,
724738
) -> Result[Cursor]:
725739
"""Return documents within a given range in a random order.
726740
@@ -736,6 +750,8 @@ def find_in_range(
736750
:type skip: int | None
737751
:param limit: Max number of documents returned.
738752
:type limit: int | None
753+
:param allow_dirty_read: Allow reads from followers in a cluster.
754+
:type allow_dirty_read: bool | None
739755
:returns: Document cursor.
740756
:rtype: arango.cursor.Cursor
741757
:raises arango.exceptions.DocumentGetError: If retrieval fails.
@@ -764,6 +780,7 @@ def find_in_range(
764780
endpoint="/_api/cursor",
765781
data={"query": query, "bindVars": bind_vars, "count": True},
766782
read=self.name,
783+
headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None,
767784
)
768785

769786
def response_handler(resp: Response) -> Cursor:
@@ -779,6 +796,7 @@ def find_in_radius(
779796
longitude: Number,
780797
radius: Number,
781798
distance_field: Optional[str] = None,
799+
allow_dirty_read: bool = False,
782800
) -> Result[Cursor]:
783801
"""Return documents within a given radius around a coordinate.
784802
@@ -793,6 +811,8 @@ def find_in_radius(
793811
:param distance_field: Document field used to indicate the distance to
794812
the given coordinate. This parameter is ignored in transactions.
795813
:type distance_field: str
814+
:param allow_dirty_read: Allow reads from followers in a cluster.
815+
:type allow_dirty_read: bool | None
796816
:returns: Document cursor.
797817
:rtype: arango.cursor.Cursor
798818
:raises arango.exceptions.DocumentGetError: If retrieval fails.
@@ -823,6 +843,7 @@ def find_in_radius(
823843
endpoint="/_api/cursor",
824844
data={"query": query, "bindVars": bind_vars, "count": True},
825845
read=self.name,
846+
headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None,
826847
)
827848

828849
def response_handler(resp: Response) -> Cursor:
@@ -899,7 +920,11 @@ def response_handler(resp: Response) -> Cursor:
899920
return self._execute(request, response_handler)
900921

901922
def find_by_text(
902-
self, field: str, query: str, limit: Optional[int] = None
923+
self,
924+
field: str,
925+
query: str,
926+
limit: Optional[int] = None,
927+
allow_dirty_read: bool = False,
903928
) -> Result[Cursor]:
904929
"""Return documents that match the given fulltext query.
905930
@@ -909,6 +934,8 @@ def find_by_text(
909934
:type query: str
910935
:param limit: Max number of documents returned.
911936
:type limit: int | None
937+
:param allow_dirty_read: Allow reads from followers in a cluster.
938+
:type allow_dirty_read: bool | None
912939
:returns: Document cursor.
913940
:rtype: arango.cursor.Cursor
914941
:raises arango.exceptions.DocumentGetError: If retrieval fails.
@@ -935,6 +962,7 @@ def find_by_text(
935962
endpoint="/_api/cursor",
936963
data={"query": aql, "bindVars": bind_vars, "count": True},
937964
read=self.name,
965+
headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None,
938966
)
939967

940968
def response_handler(resp: Response) -> Cursor:
@@ -944,12 +972,18 @@ def response_handler(resp: Response) -> Cursor:
944972

945973
return self._execute(request, response_handler)
946974

947-
def get_many(self, documents: Sequence[Union[str, Json]]) -> Result[List[Json]]:
975+
def get_many(
976+
self,
977+
documents: Sequence[Union[str, Json]],
978+
allow_dirty_read: bool = False,
979+
) -> Result[List[Json]]:
948980
"""Return multiple documents ignoring any missing ones.
949981
950982
:param documents: List of document keys, IDs or bodies. Document bodies
951983
must contain the "_id" or "_key" fields.
952984
:type documents: [str | dict]
985+
:param allow_dirty_read: Allow reads from followers in a cluster.
986+
:type allow_dirty_read: bool | None
953987
:return: Documents. Missing ones are not included.
954988
:rtype: [dict]
955989
:raise arango.exceptions.DocumentGetError: If retrieval fails.
@@ -964,6 +998,7 @@ def get_many(self, documents: Sequence[Union[str, Json]]) -> Result[List[Json]]:
964998
params=params,
965999
data=handles,
9661000
read=self.name,
1001+
headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None,
9671002
)
9681003

9691004
def response_handler(resp: Response) -> List[Json]:
@@ -2123,6 +2158,7 @@ def get(
21232158
document: Union[str, Json],
21242159
rev: Optional[str] = None,
21252160
check_rev: bool = True,
2161+
allow_dirty_read: bool = False,
21262162
) -> Result[Optional[Json]]:
21272163
"""Return a document.
21282164
@@ -2135,13 +2171,18 @@ def get(
21352171
:param check_rev: If set to True, revision of **document** (if given)
21362172
is compared against the revision of target document.
21372173
:type check_rev: bool
2174+
:param allow_dirty_read: Allow reads from followers in a cluster.
2175+
:type allow_dirty_read: bool | None
21382176
:return: Document, or None if not found.
21392177
:rtype: dict | None
21402178
:raise arango.exceptions.DocumentGetError: If retrieval fails.
21412179
:raise arango.exceptions.DocumentRevisionError: If revisions mismatch.
21422180
"""
21432181
handle, body, headers = self._prep_from_doc(document, rev, check_rev)
21442182

2183+
if allow_dirty_read:
2184+
headers["x-arango-allow-dirty-read"] = "true"
2185+
21452186
request = Request(
21462187
method="get",
21472188
endpoint=f"/_api/document/{handle}",
@@ -3144,7 +3185,10 @@ def link(
31443185
return self.insert(edge, sync=sync, silent=silent, return_new=return_new)
31453186

31463187
def edges(
3147-
self, vertex: Union[str, Json], direction: Optional[str] = None
3188+
self,
3189+
vertex: Union[str, Json],
3190+
direction: Optional[str] = None,
3191+
allow_dirty_read: bool = False,
31483192
) -> Result[Json]:
31493193
"""Return the edge documents coming in and/or out of the vertex.
31503194
@@ -3153,6 +3197,8 @@ def edges(
31533197
:param direction: The direction of the edges. Allowed values are "in"
31543198
and "out". If not set, edges in both directions are returned.
31553199
:type direction: str
3200+
:param allow_dirty_read: Allow reads from followers in a cluster.
3201+
:type allow_dirty_read: bool | None
31563202
:return: List of edges and statistics.
31573203
:rtype: dict
31583204
:raise arango.exceptions.EdgeListError: If retrieval fails.
@@ -3166,6 +3212,7 @@ def edges(
31663212
endpoint=f"/_api/edges/{self.name}",
31673213
params=params,
31683214
read=self.name,
3215+
headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None,
31693216
)
31703217

31713218
def response_handler(resp: Response) -> Json:

arango/database.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ def execute_transaction(
224224
allow_implicit: Optional[bool] = None,
225225
intermediate_commit_count: Optional[int] = None,
226226
intermediate_commit_size: Optional[int] = None,
227+
allow_dirty_read: bool = False,
227228
) -> Result[Any]:
228229
"""Execute raw Javascript command in transaction.
229230
@@ -256,6 +257,8 @@ def execute_transaction(
256257
:param intermediate_commit_size: Max size of operations in bytes after
257258
which an intermediate commit is performed automatically.
258259
:type intermediate_commit_size: int | None
260+
:param allow_dirty_read: Allow reads from followers in a cluster.
261+
:type allow_dirty_read: bool | None
259262
:return: Return value of **command**.
260263
:rtype: Any
261264
:raise arango.exceptions.TransactionExecuteError: If execution fails.
@@ -282,7 +285,12 @@ def execute_transaction(
282285
if intermediate_commit_size is not None:
283286
data["intermediateCommitSize"] = intermediate_commit_size
284287

285-
request = Request(method="post", endpoint="/_api/transaction", data=data)
288+
request = Request(
289+
method="post",
290+
endpoint="/_api/transaction",
291+
data=data,
292+
headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None,
293+
)
286294

287295
def response_handler(resp: Response) -> Any:
288296
if not resp.is_success:

arango/executor.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ class TransactionApiExecutor:
293293
:type lock_timeout: int
294294
:param max_size: Max transaction size in bytes.
295295
:type max_size: int
296+
:param allow_dirty_read: Allow reads from followers in a cluster.
297+
:type allow_dirty_read: bool | None
296298
"""
297299

298300
def __init__(
@@ -305,6 +307,7 @@ def __init__(
305307
allow_implicit: Optional[bool] = None,
306308
lock_timeout: Optional[int] = None,
307309
max_size: Optional[int] = None,
310+
allow_dirty_read: bool = False,
308311
) -> None:
309312
self._conn = connection
310313

@@ -326,7 +329,12 @@ def __init__(
326329
if max_size is not None:
327330
data["maxTransactionSize"] = max_size
328331

329-
request = Request(method="post", endpoint="/_api/transaction/begin", data=data)
332+
request = Request(
333+
method="post",
334+
endpoint="/_api/transaction/begin",
335+
data=data,
336+
headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None,
337+
)
330338
resp = self._conn.send_request(request)
331339

332340
if not resp.is_success:
@@ -348,16 +356,25 @@ def id(self) -> str:
348356
"""
349357
return self._id
350358

351-
def execute(self, request: Request, response_handler: Callable[[Response], T]) -> T:
359+
def execute(
360+
self,
361+
request: Request,
362+
response_handler: Callable[[Response], T],
363+
allow_dirty_read: bool = False,
364+
) -> T:
352365
"""Execute API request in a transaction and return the result.
353366
354367
:param request: HTTP request.
355368
:type request: arango.request.Request
356369
:param response_handler: HTTP response handler.
357370
:type response_handler: callable
371+
:param allow_dirty_read: Allow reads from followers in a cluster.
372+
:type allow_dirty_read: bool | None
358373
:return: API execution result.
359374
"""
360375
request.headers["x-arango-trx-id"] = self._id
376+
if allow_dirty_read:
377+
request.headers["x-arango-allow-dirty-read"] = "true"
361378
resp = self._conn.send_request(request)
362379
return response_handler(resp)
363380

0 commit comments

Comments
 (0)