volcano-sdk-python 0.7.3__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,13 +54,16 @@ class LockGuard:
54
54
  *,
55
55
  ttl: int,
56
56
  started_at: float,
57
+ lease_started_at: float | None = None,
57
58
  ) -> None:
58
59
  """Track one acquired lease against its local monotonic deadline."""
59
60
  self._state_lock = threading.Lock()
60
61
  self._lease = lease
61
62
  self._ttl = ttl
62
63
  self._absolute_deadline = started_at + MAX_LOCK_LIFETIME_SECONDS
63
- self._lease_deadline = min(started_at + ttl, self._absolute_deadline)
64
+ # A successful retry renews the TTL, not the maximum ownership lifetime.
65
+ lease_started_at = started_at if lease_started_at is None else lease_started_at
66
+ self._lease_deadline = min(lease_started_at + ttl, self._absolute_deadline)
64
67
  self._failure: Exception | None = None
65
68
  self._lost = threading.Event()
66
69
 
volcano_sdk/_transport.py CHANGED
@@ -104,12 +104,17 @@ from ._generated.api.functions.invoke_function import (
104
104
  _get_kwargs as invoke_function_kwargs,
105
105
  )
106
106
  from ._generated.api.locks import (
107
- acquire_project_lock,
108
107
  force_release_project_lock,
109
108
  get_project_lock,
110
109
  release_project_lock,
111
110
  renew_project_lock,
112
111
  )
112
+ from ._generated.api.locks.acquire_project_lock import (
113
+ _build_response as build_lock_acquire_response,
114
+ )
115
+ from ._generated.api.locks.acquire_project_lock import (
116
+ _get_kwargs as lock_acquire_kwargs,
117
+ )
113
118
  from ._generated.api.logs.get_project_log_activity import (
114
119
  _build_response as build_log_activity_response,
115
120
  )
@@ -253,6 +258,7 @@ if TYPE_CHECKING:
253
258
  )
254
259
  from .models import JSONValue
255
260
 
261
+ HTTP_CREATED = 201
256
262
  HTTP_UNAUTHORIZED = 401
257
263
  HTTP_NOT_FOUND = 404
258
264
  HTTP_CONFLICT = 409
@@ -643,6 +649,7 @@ class Transport(Protocol):
643
649
  key: str,
644
650
  ttl: int,
645
651
  token: str,
652
+ request_id: str | None = None,
646
653
  ) -> TransportResponse: ...
647
654
 
648
655
  def release_project_lock(
@@ -651,6 +658,7 @@ class Transport(Protocol):
651
658
  authorization: str,
652
659
  key: str,
653
660
  token: str,
661
+ request_id: str | None = None,
654
662
  ) -> TransportResponse: ...
655
663
 
656
664
 
@@ -1735,15 +1743,19 @@ class GeneratedTransport:
1735
1743
  key: str,
1736
1744
  ttl: int,
1737
1745
  token: str,
1746
+ request_id: str | None = None,
1738
1747
  ) -> TransportResponse:
1739
1748
  with self._client(authorization) as client:
1740
- response = acquire_project_lock.sync_detailed(
1749
+ request_kwargs = lock_acquire_kwargs(
1741
1750
  key,
1742
- client=client,
1743
1751
  body=ProjectLockLeaseRequest(ttl_seconds=ttl),
1744
1752
  x_volcano_lock_token=cast("UUID", token),
1745
- x_volcano_request_id=cast("UUID", str(uuid4())),
1753
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1746
1754
  )
1755
+ raw_response = client.get_httpx_client().request(**request_kwargs)
1756
+ if raw_response.status_code != HTTP_CREATED:
1757
+ return self._raw_response(raw_response)
1758
+ response = build_lock_acquire_response(client=client, response=raw_response)
1747
1759
  return self._response(response)
1748
1760
 
1749
1761
  def get_project_lock(
@@ -1751,12 +1763,13 @@ class GeneratedTransport:
1751
1763
  *,
1752
1764
  authorization: str,
1753
1765
  key: str,
1766
+ request_id: str | None = None,
1754
1767
  ) -> TransportResponse:
1755
1768
  with self._client(authorization) as client:
1756
1769
  response = get_project_lock.sync_detailed(
1757
1770
  key,
1758
1771
  client=client,
1759
- x_volcano_request_id=cast("UUID", str(uuid4())),
1772
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1760
1773
  )
1761
1774
  return self._response(response)
1762
1775
 
@@ -1765,12 +1778,13 @@ class GeneratedTransport:
1765
1778
  *,
1766
1779
  authorization: str,
1767
1780
  key: str,
1781
+ request_id: str | None = None,
1768
1782
  ) -> TransportResponse:
1769
1783
  with self._client(authorization) as client:
1770
1784
  response = force_release_project_lock.sync_detailed(
1771
1785
  key,
1772
1786
  client=client,
1773
- x_volcano_request_id=cast("UUID", str(uuid4())),
1787
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1774
1788
  )
1775
1789
  return self._response(response)
1776
1790
 
@@ -1781,6 +1795,7 @@ class GeneratedTransport:
1781
1795
  key: str,
1782
1796
  ttl: int,
1783
1797
  token: str,
1798
+ request_id: str | None = None,
1784
1799
  ) -> TransportResponse:
1785
1800
  with self._client(authorization) as client:
1786
1801
  response = renew_project_lock.sync_detailed(
@@ -1788,7 +1803,7 @@ class GeneratedTransport:
1788
1803
  client=client,
1789
1804
  body=ProjectLockLeaseRequest(ttl_seconds=ttl),
1790
1805
  x_volcano_lock_token=cast("UUID", token),
1791
- x_volcano_request_id=cast("UUID", str(uuid4())),
1806
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1792
1807
  )
1793
1808
  return self._response(response)
1794
1809
 
@@ -1798,12 +1813,13 @@ class GeneratedTransport:
1798
1813
  authorization: str,
1799
1814
  key: str,
1800
1815
  token: str,
1816
+ request_id: str | None = None,
1801
1817
  ) -> TransportResponse:
1802
1818
  with self._client(authorization) as client:
1803
1819
  response = release_project_lock.sync_detailed(
1804
1820
  key,
1805
1821
  client=client,
1806
1822
  x_volcano_lock_token=cast("UUID", token),
1807
- x_volcano_request_id=cast("UUID", str(uuid4())),
1823
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1808
1824
  )
1809
1825
  return self._response(response)
volcano_sdk/locks.py CHANGED
@@ -5,11 +5,12 @@ from __future__ import annotations
5
5
  from contextlib import contextmanager, suppress
6
6
  from datetime import datetime
7
7
  from typing import TYPE_CHECKING, Protocol, cast
8
- from uuid import uuid4
8
+ from uuid import UUID, uuid4
9
9
 
10
10
  from ._lock_guard import LockGuard, _lease_now
11
11
  from ._lock_worker import LockRenewer
12
12
  from ._transport import Transport, invoke, response_payload
13
+ from .errors import ServerError, TransportError
13
14
  from .models import LockLease, LockState
14
15
 
15
16
  if TYPE_CHECKING:
@@ -37,6 +38,7 @@ class LockGetTransport(Protocol):
37
38
  *,
38
39
  authorization: str,
39
40
  key: str,
41
+ request_id: str | None = None,
40
42
  ) -> object:
41
43
  """Get one project-scoped lock."""
42
44
  ...
@@ -52,6 +54,7 @@ class LockRenewTransport(Protocol):
52
54
  key: str,
53
55
  ttl: int,
54
56
  token: str,
57
+ request_id: str | None = None,
55
58
  ) -> object:
56
59
  """Renew one project-scoped lock."""
57
60
  ...
@@ -65,6 +68,7 @@ class LockForceReleaseTransport(Protocol):
65
68
  *,
66
69
  authorization: str,
67
70
  key: str,
71
+ request_id: str | None = None,
68
72
  ) -> object:
69
73
  """Force release one project-scoped lock."""
70
74
  ...
@@ -85,6 +89,17 @@ def _validate_ttl(ttl: object) -> None:
85
89
  raise ValueError(_INVALID_LOCK_TTL)
86
90
 
87
91
 
92
+ def _request_uuid(value: str | None, name: str) -> str:
93
+ if value is None:
94
+ return str(uuid4())
95
+ try:
96
+ UUID(value)
97
+ except (AttributeError, ValueError) as error:
98
+ message = f"{name} must be a UUID string"
99
+ raise ValueError(message) from error
100
+ return value
101
+
102
+
88
103
  class Locks:
89
104
  """Acquire and release project-scoped distributed locks."""
90
105
 
@@ -92,13 +107,14 @@ class Locks:
92
107
  """Create a lock facade backed by a client."""
93
108
  self._client = client
94
109
 
95
- def get(self, key: str) -> LockState:
110
+ def get(self, key: str, *, request_id: str | None = None) -> LockState:
96
111
  """Return the current state of a project-scoped lock."""
97
112
  transport = cast("LockGetTransport", self._client._transport)
98
113
  response = invoke(
99
114
  transport.get_project_lock,
100
115
  authorization=self._client._service_token(),
101
116
  key=key,
117
+ request_id=_request_uuid(request_id, "request_id"),
102
118
  )
103
119
  payload = response_payload(response, 200)
104
120
  return LockState(
@@ -107,26 +123,64 @@ class Locks:
107
123
  fencing_token=payload.get("fencing_token"),
108
124
  )
109
125
 
110
- def acquire(self, key: str, *, ttl: int) -> LockLease:
111
- """Acquire a lock lease for the requested number of seconds."""
126
+ def acquire(
127
+ self,
128
+ key: str,
129
+ *,
130
+ ttl: int,
131
+ token: str | None = None,
132
+ request_id: str | None = None,
133
+ ) -> LockLease:
134
+ """Acquire with one bounded retry using the same ownership token."""
135
+ lease, _ = self._acquire_with_start(
136
+ key, ttl=ttl, token=token, request_id=request_id
137
+ )
138
+ return lease
139
+
140
+ def _acquire_with_start(
141
+ self,
142
+ key: str,
143
+ *,
144
+ ttl: int,
145
+ token: str | None,
146
+ request_id: str | None,
147
+ ) -> tuple[LockLease, float]:
112
148
  _validate_ttl(ttl)
113
- token = str(uuid4())
114
- response = invoke(
115
- self._client._transport.acquire_project_lock,
116
- authorization=self._client._service_token(),
149
+ token = _request_uuid(token, "token")
150
+ request_id = _request_uuid(request_id, "request_id")
151
+ authorization = self._client._service_token()
152
+ started_at = _lease_now()
153
+ try:
154
+ payload = self._acquire_payload(key, ttl, token, request_id, authorization)
155
+ except (TransportError, ServerError) as error:
156
+ if error.status not in (None, 503):
157
+ raise
158
+ started_at = _lease_now()
159
+ payload = self._acquire_payload(key, ttl, token, request_id, authorization)
160
+ lease = LockLease(
117
161
  key=key,
118
- ttl=ttl,
119
162
  token=token,
163
+ expires_at=_parse_datetime(payload.get("expires_at")),
164
+ fencing_token=cast("int | None", payload.get("fencing_token")),
120
165
  )
121
- payload = response_payload(response, 201)
122
- return LockLease(
166
+ return lease, started_at
167
+
168
+ def _acquire_payload(
169
+ self, key: str, ttl: int, token: str, request_id: str, authorization: str
170
+ ) -> dict[str, object]:
171
+ response = invoke(
172
+ self._client._transport.acquire_project_lock,
173
+ authorization=authorization,
123
174
  key=key,
175
+ ttl=ttl,
124
176
  token=token,
125
- expires_at=_parse_datetime(payload.get("expires_at")),
126
- fencing_token=payload.get("fencing_token"),
177
+ request_id=request_id,
127
178
  )
179
+ return cast("dict[str, object]", response_payload(response, 201))
128
180
 
129
- def renew(self, key: str, lease: LockLease, *, ttl: int) -> LockLease:
181
+ def renew(
182
+ self, key: str, lease: LockLease, *, ttl: int, request_id: str | None = None
183
+ ) -> LockLease:
130
184
  """Renew a lock lease and return its immutable replacement."""
131
185
  _validate_ttl(ttl)
132
186
  transport = cast("LockRenewTransport", self._client._transport)
@@ -134,6 +188,7 @@ class Locks:
134
188
  transport.renew_project_lock,
135
189
  authorization=self._client._service_token(),
136
190
  key=key,
191
+ request_id=_request_uuid(request_id, "request_id"),
137
192
  ttl=ttl,
138
193
  token=lease.token,
139
194
  )
@@ -145,35 +200,47 @@ class Locks:
145
200
  fencing_token=payload.get("fencing_token"),
146
201
  )
147
202
 
148
- def release(self, key: str, lease: LockLease) -> None:
203
+ def release(
204
+ self, key: str, lease: LockLease, *, request_id: str | None = None
205
+ ) -> None:
149
206
  """Release a lock lease."""
150
207
  response = invoke(
151
208
  self._client._transport.release_project_lock,
152
209
  authorization=self._client._service_token(),
153
210
  key=key,
211
+ request_id=_request_uuid(request_id, "request_id"),
154
212
  token=lease.token,
155
213
  )
156
214
  response_payload(response, 204)
157
215
 
158
- def force_release(self, key: str) -> None:
216
+ def force_release(self, key: str, *, request_id: str | None = None) -> None:
159
217
  """Release a lock regardless of which token owns it."""
160
218
  transport = cast("LockForceReleaseTransport", self._client._transport)
161
219
  response = invoke(
162
220
  transport.force_release_project_lock,
163
221
  authorization=self._client._service_token(),
164
222
  key=key,
223
+ request_id=_request_uuid(request_id, "request_id"),
165
224
  )
166
225
  response_payload(response, 204)
167
226
 
168
227
  @contextmanager
169
- def with_lock(self, key: str, *, ttl: int) -> Generator[LockGuard, None, None]:
228
+ def with_lock(
229
+ self,
230
+ key: str,
231
+ *,
232
+ ttl: int,
233
+ token: str | None = None,
234
+ request_id: str | None = None,
235
+ ) -> Generator[LockGuard, None, None]:
170
236
  """Hold and automatically renew a lock for the context's lifetime."""
171
237
  _validate_ttl(ttl)
172
238
  started_at = _lease_now()
239
+ lease, lease_started_at = self._acquire_with_start(
240
+ key, ttl=ttl, token=token, request_id=request_id
241
+ )
173
242
  guard = LockGuard(
174
- self.acquire(key, ttl=ttl),
175
- ttl=ttl,
176
- started_at=started_at,
243
+ lease, ttl=ttl, started_at=started_at, lease_started_at=lease_started_at
177
244
  )
178
245
  renewer = LockRenewer(self, key, guard, ttl=ttl)
179
246
  renewer_started = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: volcano-sdk-python
3
- Version: 0.7.3
3
+ Version: 0.8.0
4
4
  Summary: Official Python SDK for Volcano
5
5
  Project-URL: Homepage, https://volcano.dev
6
6
  Project-URL: Repository, https://github.com/Kong/volcano-sdk-python
@@ -182,6 +182,13 @@ original error. `on_progress` runs after each successful part with cumulative
182
182
  uploaded bytes and the total size.
183
183
  `upload_part()` returns immutable part metadata and can safely retry the same
184
184
  part number to replace that part.
185
+ Acquisition accepts caller-owned UUID `token` and `request_id` values and retries
186
+ an ambiguous transport failure or HTTP 503 once with the same request and credential.
187
+ Retain those IDs to recover an uncertain acquisition. Other lock methods accept
188
+ `request_id`; block-scoped helpers forward initial IDs only to acquisition.
189
+ See the [lock guide](https://github.com/Kong/volcano-sdk-python/blob/main/docs/locks.md)
190
+ for examples and fencing requirements.
191
+
185
192
  `locks.get()` returns immutable lock availability, expiry, and fencing-token
186
193
  state without acquiring the lock.
187
194
  Lock acquisition and renewal require an integer TTL from 5 seconds through 90 days.
@@ -1,17 +1,17 @@
1
1
  volcano_sdk/__init__.py,sha256=rPSFMLjFbxeQvSRkL1Ee8MsNWArIhs8fgNFgtDuMNzI,1954
2
2
  volcano_sdk/_function_resolution.py,sha256=_gC3LY6x71NtJc_o9d9eq3N27Z-T_Un3p2-XRu2-Cqs,5088
3
- volcano_sdk/_lock_guard.py,sha256=EZzcwOPTFTCNUrIWIQ0-nMg9tm_nPkaYkcocgeLbu94,5572
3
+ volcano_sdk/_lock_guard.py,sha256=VhHTgBc4e5EDj8j15vVIhdBOTp6fvD628x3hVBeF39o,5794
4
4
  volcano_sdk/_lock_renewer.py,sha256=tFo1ZeRiPKB5VSbW61uoMpNR67ZDYyMEe-dCjpAM3As,841
5
5
  volcano_sdk/_lock_worker.py,sha256=QhSmwR0WcwgT-7ueJMlbCogLqOakQ5eRAtyWV74fgV0,3057
6
6
  volcano_sdk/_realtime_fetch_worker.py,sha256=zQTgFF4cIUBeAL4J2OpWAlz_sIynv2BLfu588EogAdo,9269
7
- volcano_sdk/_transport.py,sha256=PzdO7uhqvgI2L-Ff0rwZCWeF7yuH_Ty-OvZx0F7dMpo,57509
7
+ volcano_sdk/_transport.py,sha256=AFd47C3X1WAHwq3f9V2ux0WNzwGIn4NBxrwgaIN-xl0,58296
8
8
  volcano_sdk/auth.py,sha256=Z5UCrqdvmHBS6n6ZleXya04hYV94rH4SRV1GW2yimXo,43185
9
9
  volcano_sdk/client.py,sha256=KutjBUOEJyK73OibKilrcWazpWeXKEZYUehSAH8qxUE,10366
10
10
  volcano_sdk/connection_string.py,sha256=LlTZigsrCF4IfAqQ8SDTWJv35hnnzNu1MfeVv5JW-Kk,2331
11
11
  volcano_sdk/database.py,sha256=p_5OaqlBjKAtIUfKQCflXM_CGqokrVmpOMsp6KHiJ8A,9493
12
12
  volcano_sdk/errors.py,sha256=ffs66FIN_jhyh5SMj7KdXb-zVV3DSgVK6xJEPNA5h9Y,1613
13
13
  volcano_sdk/functions.py,sha256=zDeAKyCieNnI6wzb9iZMlg9_cbjaYWXYbXHb7jzCle4,10126
14
- volcano_sdk/locks.py,sha256=iWMhdbZQDw44ZNwENaZWu7WelakjpHhjYzKzufD7SNU,6932
14
+ volcano_sdk/locks.py,sha256=eSkidfHQ_wml5-1LT2hBtHxeALP6E1cGYK0FzHjbrNk,9241
15
15
  volcano_sdk/logs.py,sha256=lxORRQBvsnJ0BdMau19MXrIeig5_mEucQSyJ4F1CZxU,4666
16
16
  volcano_sdk/models.py,sha256=43zM6LQ9eJ3Hn6q9PiVSozSmOzPSr4m4OL8G8KVb-gg,8619
17
17
  volcano_sdk/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
@@ -773,7 +773,7 @@ volcano_sdk/_generated/models/usage_data_point.py,sha256=d4U08JbVTiIMM_l2BmRILL1
773
773
  volcano_sdk/_generated/models/variable.py,sha256=tvGDOMv77gW6j5skufnfrlNAhzCzA1F7nuPCQWx79yY,5926
774
774
  volcano_sdk/_generated/models/variable_deploy_source.py,sha256=G_aCiFHzQx8WhJFs0GvYZEFclfTrfkDvd3kT1e3vty8,463
775
775
  volcano_sdk/_generated/models/variable_status.py,sha256=THSsKBSiAYG4kAtBzGA4TZqxjgx8hdL0Q4qCdZfOj00,391
776
- volcano_sdk_python-0.7.3.dist-info/METADATA,sha256=0lDdNzghgpaKsYFFqMZZrWCn0rUt9nqdaGWSg0FhU_U,33557
777
- volcano_sdk_python-0.7.3.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
778
- volcano_sdk_python-0.7.3.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
779
- volcano_sdk_python-0.7.3.dist-info/RECORD,,
776
+ volcano_sdk_python-0.8.0.dist-info/METADATA,sha256=lJ43KIGGSaMGAsRRM_EqJYI9AJXnEibMrg9CqElZU78,34010
777
+ volcano_sdk_python-0.8.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
778
+ volcano_sdk_python-0.8.0.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
779
+ volcano_sdk_python-0.8.0.dist-info/RECORD,,