Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit c2388ab

Browse files
authored
Merge branch 'main' into client-svc-lease
2 parents 7684b0a + 9fef5c7 commit c2388ab

File tree

12 files changed

+95
-25
lines changed

12 files changed

+95
-25
lines changed

docs/source/cli/clients.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ metadata:
3232
name: john
3333
endpoint: grpc.jumpstarter.192.168.1.10.nip.io:8082
3434
token: <<token>>
35+
grpcConfig:
36+
# please refer to the https://grpc.github.io/grpc/core/group__grpc__arg__keys.html documentation
37+
grpc.keepalive_time_ms: 20000
3538
tls:
3639
ca: ''
3740
insecure: False

docs/source/introduction/exporters.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ metadata:
3030
name: demo
3131
endpoint: grpc.jumpstarter.example.com:443
3232
token: xxxxx
33+
grpcConfig:
34+
# Please refer to the https://grpc.github.io/grpc/core/group__grpc__arg__keys.html documentation
35+
grpc.keepalive_time_ms: 20000
3336
export:
3437
power:
3538
type: jumpstarter_driver_yepkit.driver.Ykush

packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,7 @@ async def test_get_exporters(_load_kube_config_mock, list_exporters_mock: AsyncM
765765

766766
EXPORTERS_DEVICES_LIST_NAME = EXPORTERS_LIST_NAME
767767

768+
768769
@pytest.mark.anyio
769770
@patch.object(ExportersV1Alpha1Api, "list_exporters")
770771
@patch.object(ExportersV1Alpha1Api, "_load_kube_config")
@@ -1151,6 +1152,7 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock):
11511152
lease.jumpstarter.dev/82a8ac0d-d7ff-4009-8948-18a3c5c607b2
11521153
"""
11531154

1155+
11541156
@pytest.mark.anyio
11551157
@patch.object(LeasesV1Alpha1Api, "list_leases")
11561158
@patch.object(LeasesV1Alpha1Api, "_load_kube_config")

packages/jumpstarter-cli-client/jumpstarter_cli_client/client_shell.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def client_shell(config, selector: str, lease_name):
2222
metadata_filter=MetadataFilter(labels=selector_to_labels(selector)), lease_name=lease_name
2323
) as lease:
2424
with lease.serve_unix() as path:
25-
exit_code = launch_shell(path, "remote", config.drivers.allow, config.drivers.unsafe)
25+
with lease.monitor():
26+
exit_code = launch_shell(path, "remote", config.drivers.allow, config.drivers.unsafe)
2627

2728
sys.exit(exit_code)

packages/jumpstarter/jumpstarter/client/grpc.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass, field
4-
from datetime import timedelta
4+
from datetime import datetime, timedelta
55

66
import yaml
77
from google.protobuf import duration_pb2, field_mask_pb2, json_format
@@ -52,6 +52,7 @@ class Lease(BaseModel):
5252
client: str
5353
exporter: str
5454
conditions: list[kubernetes_pb2.Condition]
55+
effective_begin_time: datetime | None = None
5556

5657
model_config = ConfigDict(
5758
arbitrary_types_allowed=True,
@@ -72,13 +73,20 @@ def from_protobuf(cls, data: client_pb2.Lease) -> Lease:
7273
else:
7374
exporter = ""
7475

76+
effective_begin_time = None
77+
if data.effective_begin_time:
78+
effective_begin_time = data.effective_begin_time.ToDatetime(
79+
tzinfo=datetime.now().astimezone().tzinfo,
80+
)
81+
7582
return cls(
7683
namespace=namespace,
7784
name=name,
7885
selector=data.selector,
7986
duration=data.duration.ToTimedelta(),
8087
client=client,
8188
exporter=exporter,
89+
effective_begin_time=effective_begin_time,
8290
conditions=data.conditions,
8391
)
8492

packages/jumpstarter/jumpstarter/client/lease.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
contextmanager,
88
)
99
from dataclasses import dataclass, field
10-
from datetime import timedelta
10+
from datetime import datetime, timedelta
11+
from typing import Any
1112

12-
from anyio import fail_after, sleep
13+
from anyio import create_task_group, fail_after, sleep
1314
from anyio.from_thread import BlockingPortal
1415
from grpc.aio import Channel
1516
from jumpstarter_protocol import jumpstarter_pb2, jumpstarter_pb2_grpc
@@ -39,6 +40,7 @@ class Lease(AbstractContextManager, AbstractAsyncContextManager):
3940
release: bool = True # release on contexts exit
4041
controller: jumpstarter_pb2_grpc.ControllerServiceStub = field(init=False)
4142
tls_config: TLSConfigV1Alpha1 = field(default_factory=TLSConfigV1Alpha1)
43+
grpc_options: dict[str, Any] = field(default_factory=dict)
4244

4345
def __post_init__(self):
4446
if hasattr(super(), "__post_init__"):
@@ -62,6 +64,11 @@ async def _create(self):
6264
).name
6365
logger.info("Created lease request for selector %s for duration %s", selector, duration)
6466

67+
async def get(self):
68+
with translate_grpc_exceptions():
69+
svc = ClientService(channel=self.channel, namespace=self.namespace)
70+
return await svc.GetLease(name=self.name)
71+
6572
def request(self):
6673
"""Request a lease, or verifies a lease which was already created.
6774
@@ -96,11 +103,7 @@ async def _acquire(self):
96103
with fail_after(300): # TODO: configurable timeout
97104
while True:
98105
logger.debug("Polling Lease %s", self.name)
99-
with translate_grpc_exceptions():
100-
result = await self.svc.GetLease(
101-
name=self.name,
102-
)
103-
106+
result = await self.get()
104107
# lease ready
105108
if condition_true(result.conditions, "Ready"):
106109
logger.debug("Lease %s acquired", self.name)
@@ -148,14 +151,39 @@ def __exit__(self, exc_type, exc_value, traceback):
148151
async def handle_async(self, stream):
149152
logger.debug("Connecting to Lease with name %s", self.name)
150153
response = await self.controller.Dial(jumpstarter_pb2.DialRequest(lease_name=self.name))
151-
async with connect_router_stream(response.router_endpoint, response.router_token, stream, self.tls_config):
154+
async with connect_router_stream(
155+
response.router_endpoint, response.router_token, stream, self.tls_config, self.grpc_options
156+
):
152157
pass
153158

154159
@asynccontextmanager
155160
async def serve_unix_async(self):
156161
async with TemporaryUnixListener(self.handle_async) as path:
157162
yield path
158163

164+
@asynccontextmanager
165+
async def monitor_async(self, threshold: timedelta = timedelta(minutes=5)):
166+
async def _monitor():
167+
while True:
168+
lease = await self.get()
169+
if lease.effective_begin_time:
170+
end_time = lease.effective_begin_time + lease.duration
171+
remain = end_time - datetime.now(tz=datetime.now().astimezone().tzinfo)
172+
if remain < threshold:
173+
logger.info("Lease {} ending soon in {} at {}".format(self.name, remain, end_time))
174+
await sleep(threshold.total_seconds())
175+
else:
176+
await sleep(5)
177+
else:
178+
await sleep(1)
179+
180+
async with create_task_group() as tg:
181+
tg.start_soon(_monitor)
182+
try:
183+
yield
184+
finally:
185+
tg.cancel_scope.cancel()
186+
159187
@asynccontextmanager
160188
async def connect_async(self, stack):
161189
async with self.serve_unix_async() as path:
@@ -172,3 +200,8 @@ def connect(self):
172200
def serve_unix(self):
173201
with self.portal.wrap_async_context_manager(self.serve_unix_async()) as path:
174202
yield path
203+
204+
@contextmanager
205+
def monitor(self, threshold: timedelta = timedelta(minutes=5)):
206+
with self.portal.wrap_async_context_manager(self.monitor_async(threshold)):
207+
yield

packages/jumpstarter/jumpstarter/common/grpc.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import socket
44
import ssl
55
from contextlib import contextmanager
6+
from typing import Any, Sequence, Tuple
67
from urllib.parse import urlparse
78

89
import grpc
@@ -34,20 +35,28 @@ def ssl_channel_credentials(target: str, tls_config):
3435
return grpc.ssl_channel_credentials()
3536

3637

37-
def aio_secure_channel(target: str, credentials: grpc.ChannelCredentials):
38+
def aio_secure_channel(target: str, credentials: grpc.ChannelCredentials, grpc_options: dict[str, Any] | None):
3839
return grpc.aio.secure_channel(
3940
target,
4041
credentials,
41-
options=(
42-
("grpc.lb_policy_name", "round_robin"),
43-
("grpc.keepalive_time_ms", 350000),
44-
("grpc.keepalive_timeout_ms", 5000),
45-
("grpc.http2.max_pings_without_data", 5),
46-
("grpc.keepalive_permit_without_calls", 1),
47-
),
42+
options=_override_default_grpc_options(grpc_options),
4843
)
4944

5045

46+
def _override_default_grpc_options(grpc_options: dict[str, str | int] | None) -> Sequence[Tuple[str, Any]]:
47+
defaults = (
48+
("grpc.lb_policy_name", "round_robin"),
49+
# we keep a low keepalive time to avoid idle timeouts on cloud load balancers
50+
("grpc.keepalive_time_ms", 20000),
51+
("grpc.keepalive_timeout_ms", 5000),
52+
("grpc.http2.max_pings_without_data", 5),
53+
("grpc.keepalive_permit_without_calls", 1),
54+
)
55+
options = dict(defaults)
56+
options.update(grpc_options or {})
57+
return tuple(options.items())
58+
59+
5160
def configure_grpc_env():
5261
# disable informative logs by default, i.e.:
5362
# WARNING: All log messages before absl::InitializeLog() is called are written to STDERR

packages/jumpstarter/jumpstarter/common/streams.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ class StreamRequestMetadata(BaseModel):
3232

3333

3434
@asynccontextmanager
35-
async def connect_router_stream(endpoint, token, stream, tls_config):
35+
async def connect_router_stream(endpoint, token, stream, tls_config, grpc_options):
3636
credentials = grpc.composite_channel_credentials(
3737
ssl_channel_credentials(endpoint, tls_config),
3838
grpc.access_token_call_credentials(token),
3939
)
4040

41-
async with aio_secure_channel(endpoint, credentials) as channel:
41+
async with aio_secure_channel(endpoint, credentials, grpc_options) as channel:
4242
router = router_pb2_grpc.RouterServiceStub(channel)
4343
context = router.Stream(metadata=())
4444
async with RouterStream(context=context) as s:

packages/jumpstarter/jumpstarter/config/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class ClientConfigV1Alpha1(BaseModel):
4949
endpoint: str
5050
tls: TLSConfigV1Alpha1 = Field(default_factory=TLSConfigV1Alpha1)
5151
token: str
52+
grpcOptions: dict[str, str | int] | None = Field(default_factory=dict)
5253

5354
drivers: ClientConfigV1Alpha1Drivers
5455

@@ -58,7 +59,7 @@ async def channel(self):
5859
call_credentials("Client", self.metadata, self.token),
5960
)
6061

61-
return aio_secure_channel(self.endpoint, credentials)
62+
return aio_secure_channel(self.endpoint, credentials, self.grpcOptions)
6263

6364
@contextmanager
6465
def lease(self, metadata_filter: MetadataFilter, lease_name: str | None = None):
@@ -161,6 +162,7 @@ async def request_lease_async(
161162
allow=self.drivers.allow,
162163
unsafe=self.drivers.unsafe,
163164
tls_config=self.tls,
165+
grpc_options=self.grpcOptions,
164166
)
165167
with translate_grpc_exceptions():
166168
return await lease.request_async()
@@ -204,6 +206,7 @@ async def lease_async(
204206
unsafe=self.drivers.unsafe,
205207
release=release_lease,
206208
tls_config=self.tls,
209+
grpc_options=self.grpcOptions,
207210
) as lease:
208211
yield lease
209212

packages/jumpstarter/jumpstarter/config/client_config_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def test_client_config_save(monkeypatch: pytest.MonkeyPatch):
206206
ca: ''
207207
insecure: false
208208
token: dGhpc2lzYXRva2VuLTEyMzQxMjM0MTIzNEyMzQtc2Rxd3Jxd2VycXdlcnF3ZXJxd2VyLTEyMzQxMjM0MTIz
209+
grpcOptions: {}
209210
drivers:
210211
allow:
211212
- jumpstarter.drivers.*
@@ -241,6 +242,7 @@ def test_client_config_save_explicit_path():
241242
ca: ''
242243
insecure: false
243244
token: dGhpc2lzYXRva2VuLTEyMzQxMjM0MTIzNEyMzQtc2Rxd3Jxd2VycXdlcnF3ZXJxd2VyLTEyMzQxMjM0MTIz
245+
grpcOptions: {}
244246
drivers:
245247
allow:
246248
- jumpstarter.drivers.*
@@ -274,6 +276,7 @@ def test_client_config_save_unsafe_drivers():
274276
ca: ''
275277
insecure: false
276278
token: dGhpc2lzYXRva2VuLTEyMzQxMjM0MTIzNEyMzQtc2Rxd3Jxd2VycXdlcnF3ZXJxd2VyLTEyMzQxMjM0MTIz
279+
grpcOptions: {}
277280
drivers:
278281
allow: []
279282
unsafe: true

0 commit comments

Comments
 (0)