Skip to content

Commit 3f95652

Browse files
committed
PYCBC-1894: Correct request-level timeout and scan_wait handling
Changes -------- * Encode analytics and view request timeouts in milliseconds, the unit every marshaller reads, where to_microseconds asked the core for a timeout 1000 times too long; to_microseconds is deleted with its last two callers * Read the five duration getters back from the stored milliseconds instead of slicing a unit character off a string, which raised TypeError on every request that had a timeout or scan_wait set * Convert in the N1QLQuery.scan_wait setter on every assignment rather than only when the key is already present, and pass scan_wait through _VALID_OPTS unchanged, which is how timeout already works * Build TransactionQueryOptions.scan_wait with timedelta_as_milliseconds, where dividing an uncalled total_seconds by 1000 raised TypeError on any scan_wait at all * Correct the analytics and views timeout assertions, which encoded the wrong unit, and cover the five getters, a direct scan_wait assignment and TransactionQueryOptions.scan_wait, none of which had a test Change-Id: I6962b8ef34ec623dece7368a00728eeea1220e6a Reviewed-on: https://review.couchbase.org/c/couchbase-python-client/+/252246 Reviewed-by: Sergey Avseyev <sergey.avseyev@gmail.com> Tested-by: Build Bot <build@couchbase.com>
1 parent 941a247 commit 3f95652

11 files changed

Lines changed: 86 additions & 55 deletions

File tree

couchbase/_utils.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,22 +69,6 @@ def timedelta_as_milliseconds(duration: timedelta) -> int:
6969
return int(duration.total_seconds() * 1e3 if duration else 0)
7070

7171

72-
def to_microseconds(
73-
timeout # type: Union[timedelta, float, int]
74-
) -> int:
75-
if timeout and not isinstance(timeout, (timedelta, float, int)):
76-
raise InvalidArgumentException(message=("Expected timeout to be of type "
77-
f"Union[timedelta, float, int] instead of {timeout}"))
78-
if not timeout:
79-
total_us = 0
80-
elif isinstance(timeout, timedelta):
81-
total_us = int(timeout.total_seconds() * 1e6)
82-
else:
83-
total_us = int(timeout * 1e6)
84-
85-
return total_us
86-
87-
8872
def to_milliseconds(
8973
timeout # type: Union[timedelta, float, int]
9074
) -> int:

couchbase/logic/analytics.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
Optional,
2323
Union)
2424

25-
from couchbase._utils import to_microseconds
25+
from couchbase._utils import to_milliseconds
2626
from couchbase.exceptions import ErrorMapper, InvalidArgumentException
2727
from couchbase.logic.observability import ObservableRequestHandler, SpanProtocol
2828
from couchbase.logic.options import AnalyticsOptionsBase
@@ -233,20 +233,19 @@ def params(self):
233233

234234
@property
235235
def timeout(self) -> Optional[float]:
236-
value = self._params.get('timeout', None)
237-
if not value:
236+
total_ms = self._params.get('timeout', None)
237+
if not total_ms:
238238
return None
239-
value = value[:-1]
240-
return float(value)
239+
return total_ms / 1000
241240

242241
@timeout.setter
243242
def timeout(self, value # type: Union[timedelta,float,int]
244243
) -> None:
245244
if not value:
246245
self._params.pop('timeout', 0)
247246
else:
248-
total_us = to_microseconds(value)
249-
self.set_option('timeout', total_us)
247+
total_ms = to_milliseconds(value)
248+
self.set_option('timeout', total_ms)
250249

251250
@property
252251
def metrics(self):

couchbase/logic/n1ql.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ class N1QLQuery:
329329
"query_context": {"query_context": lambda x: x},
330330
"raw": {"raw": lambda x: x},
331331
"scan_cap": {"scan_cap": lambda x: x},
332-
"scan_wait": {"scan_wait": timedelta_as_milliseconds},
332+
"scan_wait": {"scan_wait": lambda x: x},
333333
"metrics": {"metrics": lambda x: x},
334334
"flex_index": {"flex_index": lambda x: x},
335335
"preserve_expiry": {"preserve_expiry": lambda x: x},
@@ -409,11 +409,10 @@ def statement(self) -> str:
409409

410410
@property
411411
def timeout(self) -> Optional[float]:
412-
value = self._params.get('timeout', None)
413-
if not value:
412+
total_ms = self._params.get('timeout', None)
413+
if not total_ms:
414414
return None
415-
value = value[:-1]
416-
return float(value)
415+
return total_ms / 1000
417416

418417
@timeout.setter
419418
def timeout(self, value # type: Union[timedelta,float,int]
@@ -606,23 +605,18 @@ def scan_cap(self, value # type: int
606605

607606
@property
608607
def scan_wait(self) -> Optional[float]:
609-
value = self._params.get('scan_wait', None)
610-
if not value:
608+
total_ms = self._params.get('scan_wait', None)
609+
if not total_ms:
611610
return None
612-
value = value[:-1]
613-
return float(value)
611+
return total_ms / 1000
614612

615613
@scan_wait.setter
616614
def scan_wait(self, value # type: timedelta
617615
) -> None:
618616
if not value:
619617
self._params.pop('scan_wait', 0)
620618
else:
621-
# if using the setter, need to validate/transform timedelta, otherwise, just add the value
622-
if 'scan_wait' in self._params:
623-
value = timedelta_as_milliseconds(value)
624-
625-
self.set_option('scan_wait', value)
619+
self.set_option('scan_wait', timedelta_as_milliseconds(value))
626620

627621
@property
628622
def flex_index(self) -> bool:

couchbase/logic/search.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,11 +1027,10 @@ def params(self) -> Dict[str, Any]:
10271027

10281028
@property
10291029
def timeout(self) -> Optional[float]:
1030-
value = self._params.get('timeout', None)
1031-
if not value:
1030+
total_ms = self._params.get('timeout', None)
1031+
if not total_ms:
10321032
return None
1033-
value = value[:-1]
1034-
return float(value)
1033+
return total_ms / 1000
10351034

10361035
@timeout.setter
10371036
def timeout(self, value # type: Union[timedelta,float,int]

couchbase/logic/views.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
Optional,
2727
Union)
2828

29-
from couchbase._utils import to_microseconds
29+
from couchbase._utils import to_milliseconds
3030
from couchbase.exceptions import ErrorMapper, InvalidArgumentException
3131
from couchbase.logic.observability import ObservableRequestHandler, SpanProtocol
3232
from couchbase.logic.options import ViewOptionsBase
@@ -198,20 +198,19 @@ def as_encodable(self) -> Dict[str, Any]:
198198

199199
@property
200200
def timeout(self) -> Optional[float]:
201-
value = self._params.get('timeout', None)
202-
if not value:
201+
total_ms = self._params.get('timeout', None)
202+
if not total_ms:
203203
return None
204-
value = value[:-1]
205-
return float(value)
204+
return total_ms / 1000
206205

207206
@timeout.setter
208207
def timeout(self, value # type: Union[timedelta,float,int]
209208
) -> None:
210209
if not value:
211210
self._params.pop('timeout', 0)
212211
else:
213-
total_us = to_microseconds(value)
214-
self.set_option('timeout', total_us)
212+
total_ms = to_milliseconds(value)
213+
self.set_option('timeout', total_ms)
215214

216215
@property
217216
def limit(self) -> Optional[int]:

couchbase/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2009,7 +2009,7 @@ def __init__(self, # noqa: C901
20092009
kwargs["bucket_name"] = scope.bucket_name
20102010
kwargs["scope_name"] = scope.name
20112011
if kwargs.get("scan_wait", None):
2012-
kwargs["scan_wait"] = kwargs["scan_wait"].total_seconds/1000
2012+
kwargs["scan_wait"] = timedelta_as_milliseconds(kwargs["scan_wait"])
20132013
if kwargs.get("scan_consistency", None):
20142014
kwargs["scan_consistency"] = kwargs["scan_consistency"].value
20152015
if kwargs["scan_consistency"] == "at_plus":

couchbase/tests/analytics_params_t.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class AnalyticsParamTestSuite:
3434
'test_params_read_only',
3535
'test_params_serializer',
3636
'test_params_timeout',
37+
'test_params_timeout_getter',
3738
'test_status'
3839
]
3940

@@ -117,23 +118,31 @@ def test_params_timeout(self, base_opts):
117118
query = AnalyticsQuery.create_query_object(q_str, q_opts)
118119

119120
exp_opts = base_opts.copy()
120-
exp_opts['timeout'] = 120000000
121+
exp_opts['timeout'] = 120000
121122
assert query.params == exp_opts
122123

123124
q_opts = AnalyticsOptions(timeout=20)
124125
query = AnalyticsQuery.create_query_object(q_str, q_opts)
125126

126127
exp_opts = base_opts.copy()
127-
exp_opts['timeout'] = 20000000
128+
exp_opts['timeout'] = 20000
128129
assert query.params == exp_opts
129130

130131
q_opts = AnalyticsOptions(timeout=25.5)
131132
query = AnalyticsQuery.create_query_object(q_str, q_opts)
132133

133134
exp_opts = base_opts.copy()
134-
exp_opts['timeout'] = 25500000
135+
exp_opts['timeout'] = 25500
135136
assert query.params == exp_opts
136137

138+
def test_params_timeout_getter(self):
139+
q_str = 'SELECT * FROM default'
140+
query = AnalyticsQuery.create_query_object(q_str, AnalyticsOptions(timeout=timedelta(seconds=120)))
141+
assert query.timeout == 120
142+
143+
query = AnalyticsQuery.create_query_object(q_str, AnalyticsOptions())
144+
assert query.timeout is None
145+
137146
@pytest.mark.parametrize('value, expected', [(k, v) for k, v in AnalyticsStatus.__members__.items()])
138147
def test_status(self, value, expected):
139148
a_status = AnalyticsStatus[value]

couchbase/tests/query_params_t.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ class QueryParamTestSuite:
4646
'test_params_scan_cap',
4747
'test_params_scan_consistency',
4848
'test_params_scan_wait',
49+
'test_params_scan_wait_direct',
4950
'test_params_serializer',
5051
'test_params_timeout',
52+
'test_params_timeout_getter',
5153
'test_params_uninterpretable_enum_option',
5254
'test_params_use_replica',
5355
]
@@ -291,6 +293,14 @@ def test_params_scan_wait(self, base_opts):
291293
exp_opts['scan_wait'] = 30000
292294
assert query.params == exp_opts
293295

296+
def test_params_scan_wait_direct(self):
297+
# Assigning the property is a public entry point in its own right, and it has to
298+
# convert on the first assignment rather than the second.
299+
query = N1QLQuery('SELECT * FROM default')
300+
query.scan_wait = timedelta(seconds=30)
301+
assert query.params['scan_wait'] == 30000
302+
assert query.scan_wait == 30
303+
294304
def test_params_scan_consistency(self, base_opts):
295305
q_str = 'SELECT * FROM default'
296306
q_opts = QueryOptions(scan_consistency=QueryScanConsistency.REQUEST_PLUS)
@@ -345,6 +355,14 @@ def test_params_timeout(self, base_opts):
345355
exp_opts['timeout'] = 25500
346356
assert query.params == exp_opts
347357

358+
def test_params_timeout_getter(self):
359+
q_str = 'SELECT * FROM default'
360+
query = N1QLQuery.create_query_object(q_str, QueryOptions(timeout=timedelta(seconds=20)))
361+
assert query.timeout == 20
362+
363+
query = N1QLQuery.create_query_object(q_str, QueryOptions())
364+
assert query.timeout is None
365+
348366

349367
class ClassicQueryParamTests(QueryParamTestSuite):
350368
@pytest.fixture(scope='class')

couchbase/tests/search_params_t.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class SearchParamTestSuite:
7373
'test_params_sort_invalid',
7474
'test_params_sort_mixed',
7575
'test_params_timeout',
76+
'test_params_timeout_getter',
7677
'test_phrase_query',
7778
'test_prefix_query',
7879
'test_query_string_query',
@@ -1064,6 +1065,18 @@ def test_params_timeout(self, cb_env, base_query_opts):
10641065
exp_opts['timeout'] = 25500
10651066
assert search_query.params == exp_opts
10661067

1068+
def test_params_timeout_getter(self, cb_env, base_query_opts):
1069+
q, _ = base_query_opts
1070+
search_query = search.SearchQueryBuilder.create_search_query_object(
1071+
cb_env.TEST_INDEX_NAME, q, SearchOptions(timeout=timedelta(seconds=20))
1072+
)
1073+
assert search_query.timeout == 20
1074+
1075+
search_query = search.SearchQueryBuilder.create_search_query_object(
1076+
cb_env.TEST_INDEX_NAME, q, SearchOptions()
1077+
)
1078+
assert search_query.timeout is None
1079+
10671080
def test_phrase_query(self, cb_env):
10681081
exp_json = {
10691082
'query': {

couchbase/tests/transactions_t.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class TransactionTestSuite:
106106
'test_rollback',
107107
'test_rollback_eating_exceptions',
108108
'test_scan_consistency',
109+
'test_scan_wait',
109110
'test_scope_qualifier',
110111
'test_timeout',
111112
'test_transaction_config_durability',
@@ -1022,6 +1023,12 @@ def test_scan_consistency(self, cls, consistency):
10221023
assert cfg_consistency is not None
10231024
assert cfg_consistency == consistency.value
10241025

1026+
def test_scan_wait(self):
1027+
cfg = TransactionQueryOptions(scan_wait=timedelta(seconds=30))
1028+
cfg_scan_wait = cfg._base.to_dict().get('scan_wait', None)
1029+
assert cfg_scan_wait is not None
1030+
assert cfg_scan_wait == 30000
1031+
10251032
def test_scope_qualifier(self, cb_env):
10261033
pytest.skip('CBD-5091: Pending Transactions changes')
10271034
cfg = TransactionQueryOptions(scope=cb_env.collection._impl._scope)

0 commit comments

Comments
 (0)