modAL 1.4.3.dev23__py3-none-any.whl → 1.4.3.dev24__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.
modal/_functions.py CHANGED
@@ -549,6 +549,7 @@ class FunctionStats:
549
549
  backlog: int
550
550
  num_total_runners: int
551
551
  num_running_inputs: int
552
+ input_headroom: int
552
553
 
553
554
 
554
555
  def _parse_retries(
@@ -1851,7 +1852,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1851
1852
  retry=Retry(total_timeout=10.0),
1852
1853
  )
1853
1854
  return FunctionStats(
1854
- backlog=resp.backlog, num_total_runners=resp.num_total_tasks, num_running_inputs=resp.num_running_inputs
1855
+ backlog=resp.backlog,
1856
+ num_total_runners=resp.num_total_tasks,
1857
+ num_running_inputs=resp.num_running_inputs,
1858
+ input_headroom=resp.input_headroom,
1855
1859
  )
1856
1860
 
1857
1861
  @live_method
@@ -403,7 +403,7 @@ async def _retry_transient_errors(
403
403
  and isinstance(exc, GRPCError)
404
404
  and (server_retry_policy := get_server_retry_policy(exc))
405
405
  ):
406
- server_delay = server_retry_policy.retry_after_secs
406
+ server_delay = max(server_retry_policy.retry_after_secs, 0.1)
407
407
 
408
408
  now = time.time()
409
409
 
@@ -417,6 +417,7 @@ async def _retry_transient_errors(
417
417
  )
418
418
  final_attempt = total_timeout_will_be_reached or max_throttle_will_be_reached
419
419
 
420
+ elapsed_time = time.monotonic() - attempt_started_at
420
421
  with suppress_tb_frame():
421
422
  process_exception_before_retry(
422
423
  exc,
@@ -425,20 +426,23 @@ async def _retry_transient_errors(
425
426
  n_retries,
426
427
  server_delay,
427
428
  idempotency_key,
428
- time.monotonic() - attempt_started_at,
429
+ elapsed_time,
429
430
  )
430
431
 
431
- now = time.time()
432
432
  if last_server_retry_warning_time is None or (
433
433
  now - last_server_retry_warning_time >= SERVER_RETRY_WARNING_TIME_INTERVAL
434
434
  ):
435
435
  last_server_retry_warning_time = now
436
436
  logger.warning(
437
- f"Warning: Received {exc.status}{os.linesep}"
438
- f"{exc.message}{os.linesep}"
439
- f"Will retry in {server_delay:0.2f} seconds."
437
+ f"Warning: Received {exc.status}{os.linesep}{exc.message}{os.linesep}"
438
+ f"for {fn.name} ({idempotency_key[:8]}). Retrying..."
440
439
  )
441
440
 
441
+ logger.debug(
442
+ f"Server-side retry triggered with {repr(exc)} {n_throttled_retries=} {server_delay=} "
443
+ f"{elapsed_time=} for {fn.name} ({idempotency_key[:8]})."
444
+ )
445
+
442
446
  n_throttled_retries += 1
443
447
  await asyncio.sleep(server_delay)
444
448
  continue
@@ -16,7 +16,7 @@ from grpclib import GRPCError, Status
16
16
  from grpclib.exceptions import StreamTerminatedError
17
17
 
18
18
  from modal.config import logger
19
- from modal.exception import ConflictError, ExecTimeoutError
19
+ from modal.exception import ExecTimeoutError
20
20
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
21
21
  from modal_proto.task_command_router_grpc import TaskCommandRouterStub
22
22
 
@@ -196,21 +196,12 @@ class TaskCommandRouterClient:
196
196
  return cls(server_client, task_id, url, jwt, channel, loop, jwt_refresh_lock, sandbox_id=sandbox_id)
197
197
 
198
198
  @classmethod
199
- async def try_init(
199
+ async def init(
200
200
  cls,
201
201
  server_client,
202
202
  task_id: str,
203
- ) -> Optional["TaskCommandRouterClient"]:
204
- """Attempt to initialize a TaskCommandRouterClient by fetching direct access.
205
-
206
- Returns None if command router access is not enabled (FAILED_PRECONDITION).
207
- """
208
- try:
209
- resp = await fetch_command_router_access(server_client, task_id)
210
- except ConflictError:
211
- logger.debug(f"Command router access is not enabled for task {task_id}")
212
- return None
213
-
203
+ ) -> "TaskCommandRouterClient":
204
+ resp = await fetch_command_router_access(server_client, task_id)
214
205
  logger.debug(f"Using command router access for task {task_id}")
215
206
  return await cls._connect(server_client, task_id, resp.url, resp.jwt)
216
207
 
@@ -241,7 +232,7 @@ class TaskCommandRouterClient:
241
232
  stream_stdio_retry_delay_factor: float = 2,
242
233
  stream_stdio_max_retries: int = 10,
243
234
  ) -> None:
244
- """Callers should not use this directly. Use TaskCommandRouterClient.try_init() instead."""
235
+ """Callers should not use this directly. Use TaskCommandRouterClient.init() instead."""
245
236
  # Record the loop this instance is bound to so __del__ can safely schedule cleanup
246
237
  # even if finalization happens from a different thread (e.g. via synchronicity).
247
238
  self._loop = loop
modal/cli/cluster.py CHANGED
@@ -16,7 +16,6 @@ from modal.cli.utils import display_table, env_option, is_tty
16
16
  from modal.client import _Client
17
17
  from modal.config import config
18
18
  from modal.container_process import _ContainerProcess
19
- from modal.exception import InvalidError
20
19
  from modal.output import OutputManager
21
20
  from modal.stream_type import StreamType
22
21
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
@@ -79,9 +78,7 @@ async def shell(cluster_id: str, rank: int = 0):
79
78
 
80
79
  pty = is_tty()
81
80
 
82
- command_router_client = await TaskCommandRouterClient.try_init(client, task_id)
83
- if command_router_client is None:
84
- raise InvalidError(f"Command router access is not available for container {task_id}")
81
+ command_router_client = await TaskCommandRouterClient.init(client, task_id)
85
82
 
86
83
  process_id = str(uuid.uuid4())
87
84
 
modal/cli/container.py CHANGED
@@ -278,9 +278,7 @@ async def _exec_impl(
278
278
 
279
279
  client = await _Client.from_env()
280
280
 
281
- command_router_client = await TaskCommandRouterClient.try_init(client, container_id)
282
- if command_router_client is None:
283
- raise InvalidError(f"Command router access is not available for container {container_id}")
281
+ command_router_client = await TaskCommandRouterClient.init(client, container_id)
284
282
 
285
283
  process_id = str(uuid.uuid4())
286
284
 
modal/client.pyi CHANGED
@@ -35,7 +35,7 @@ class _Client:
35
35
  server_url: str,
36
36
  client_type: int,
37
37
  credentials: typing.Optional[tuple[str, str]],
38
- version: str = "1.4.3.dev23",
38
+ version: str = "1.4.3.dev24",
39
39
  ):
40
40
  """mdmd:hidden
41
41
  The Modal client object is not intended to be instantiated directly by users.
@@ -175,7 +175,7 @@ class Client:
175
175
  server_url: str,
176
176
  client_type: int,
177
177
  credentials: typing.Optional[tuple[str, str]],
178
- version: str = "1.4.3.dev23",
178
+ version: str = "1.4.3.dev24",
179
179
  ):
180
180
  """mdmd:hidden
181
181
  The Modal client object is not intended to be instantiated directly by users.
@@ -1,7 +1,6 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import asyncio
3
3
  import platform
4
- import time
5
4
  from typing import Generic, Optional, TypeVar
6
5
 
7
6
  from modal_proto import api_pb2
@@ -12,194 +11,17 @@ from ._utils.task_command_router_client import TaskCommandRouterClient
12
11
  from .client import _Client
13
12
  from .config import logger
14
13
  from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
15
- from .io_streams import _StreamReader, _StreamWriter
14
+ from .io_streams import (
15
+ _StreamReader,
16
+ _StreamReaderThroughCommandRouterParams,
17
+ _StreamWriter,
18
+ _StreamWriterThroughCommandRouterParams,
19
+ )
16
20
  from .stream_type import StreamType
17
21
 
18
22
  T = TypeVar("T", str, bytes)
19
23
 
20
24
 
21
- class _ContainerProcessThroughServer(Generic[T]):
22
- _process_id: Optional[str] = None
23
- _stdout: _StreamReader[T]
24
- _stderr: _StreamReader[T]
25
- _stdin: _StreamWriter
26
- _exec_deadline: Optional[float] = None
27
- _text: bool
28
- _by_line: bool
29
- _returncode: Optional[int] = None
30
-
31
- def __init__(
32
- self,
33
- process_id: str,
34
- task_id: str,
35
- client: _Client,
36
- stdout: StreamType = StreamType.PIPE,
37
- stderr: StreamType = StreamType.PIPE,
38
- exec_deadline: Optional[float] = None,
39
- text: bool = True,
40
- by_line: bool = False,
41
- ) -> None:
42
- self._process_id = process_id
43
- self._client = client
44
- self._exec_deadline = exec_deadline
45
- self._text = text
46
- self._by_line = by_line
47
- self._stdout = _StreamReader[T](
48
- api_pb2.FILE_DESCRIPTOR_STDOUT,
49
- process_id,
50
- "container_process",
51
- self._client,
52
- stream_type=stdout,
53
- text=text,
54
- by_line=by_line,
55
- deadline=exec_deadline,
56
- task_id=task_id,
57
- )
58
- self._stderr = _StreamReader[T](
59
- api_pb2.FILE_DESCRIPTOR_STDERR,
60
- process_id,
61
- "container_process",
62
- self._client,
63
- stream_type=stderr,
64
- text=text,
65
- by_line=by_line,
66
- deadline=exec_deadline,
67
- task_id=task_id,
68
- )
69
- self._stdin = _StreamWriter(process_id, "container_process", self._client)
70
-
71
- def __repr__(self) -> str:
72
- return f"ContainerProcess(process_id={self._process_id!r})"
73
-
74
- @property
75
- def stdout(self) -> _StreamReader[T]:
76
- """StreamReader for the container process's stdout stream."""
77
- return self._stdout
78
-
79
- @property
80
- def stderr(self) -> _StreamReader[T]:
81
- """StreamReader for the container process's stderr stream."""
82
- return self._stderr
83
-
84
- @property
85
- def stdin(self) -> _StreamWriter:
86
- """StreamWriter for the container process's stdin stream."""
87
- return self._stdin
88
-
89
- @property
90
- def returncode(self) -> int:
91
- if self._returncode is None:
92
- raise InvalidError(
93
- "You must call wait() before accessing the returncode. "
94
- "To poll for the status of a running process, use poll() instead."
95
- )
96
- return self._returncode
97
-
98
- async def poll(self) -> Optional[int]:
99
- """Check if the container process has finished running.
100
-
101
- Returns `None` if the process is still running, else returns the exit code.
102
- """
103
- assert self._process_id
104
- if self._returncode is not None:
105
- return self._returncode
106
- if self._exec_deadline and time.monotonic() >= self._exec_deadline:
107
- # TODO(matt): In the future, it would be nice to raise a ContainerExecTimeoutError to make it
108
- # clear to the user that their sandbox terminated due to a timeout
109
- self._returncode = -1
110
- return self._returncode
111
-
112
- req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=0)
113
- resp = await self._client.stub.ContainerExecWait(req)
114
-
115
- if resp.completed:
116
- self._returncode = resp.exit_code
117
- return self._returncode
118
-
119
- return None
120
-
121
- async def _wait_for_completion(self) -> int:
122
- assert self._process_id
123
- while True:
124
- req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=10)
125
- resp = await self._client.stub.ContainerExecWait(req)
126
- if resp.completed:
127
- return resp.exit_code
128
-
129
- async def wait(self) -> int:
130
- """Wait for the container process to finish running. Returns the exit code."""
131
- if self._returncode is not None:
132
- return self._returncode
133
-
134
- try:
135
- timeout = None
136
- if self._exec_deadline:
137
- timeout = self._exec_deadline - time.monotonic()
138
- if timeout <= 0:
139
- raise TimeoutError()
140
- self._returncode = await asyncio.wait_for(self._wait_for_completion(), timeout=timeout)
141
- except (asyncio.TimeoutError, TimeoutError):
142
- self._returncode = -1
143
- logger.debug(f"ContainerProcess {self._process_id} wait completed with returncode {self._returncode}")
144
- return self._returncode
145
-
146
- async def attach(self):
147
- """mdmd:hidden"""
148
- if platform.system() == "Windows":
149
- print("interactive exec is not currently supported on Windows.") # noqa: T201
150
- return
151
-
152
- from .output import OutputManager
153
-
154
- output = OutputManager.get()
155
- connecting_status = output.status("Connecting...")
156
- connecting_status.start()
157
- on_connect = asyncio.Event()
158
-
159
- async def _write_to_fd_loop(stream: _StreamReader):
160
- # This is required to make modal shell to an existing task work,
161
- # since that uses ContainerExec RPCs directly, but this is hacky.
162
- #
163
- # TODO(saltzm): Once we use the new exec path for that use case, this code can all be removed.
164
- from .io_streams import _StreamReaderThroughServer
165
-
166
- assert isinstance(stream._impl, _StreamReaderThroughServer)
167
- stream_impl = stream._impl
168
- # Don't skip empty messages so we can detect when the process has booted.
169
- async for chunk in stream_impl._get_logs(skip_empty_messages=False):
170
- if not on_connect.is_set():
171
- connecting_status.stop()
172
- on_connect.set()
173
-
174
- await write_to_fd(stream.file_descriptor, chunk)
175
-
176
- async def _handle_input(data: bytes, message_index: int):
177
- self.stdin.write(data)
178
- await self.stdin.drain()
179
-
180
- async with TaskContext() as tc:
181
- stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
182
- stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
183
-
184
- try:
185
- # time out if we can't connect to the server fast enough
186
- await asyncio.wait_for(on_connect.wait(), timeout=60)
187
-
188
- async with stream_from_stdin(_handle_input, use_raw_terminal=True):
189
- await stdout_task
190
- await stderr_task
191
-
192
- # TODO: this doesn't work right now.
193
- # if exit_status != 0:
194
- # raise ExecutionError(f"Process exited with status code {exit_status}")
195
-
196
- except (asyncio.TimeoutError, TimeoutError):
197
- connecting_status.stop()
198
- stdout_task.cancel()
199
- stderr_task.cancel()
200
- raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
201
-
202
-
203
25
  async def _iter_stream_as_bytes(stream: _StreamReader[T]):
204
26
  """Yield raw bytes from a StreamReader regardless of text mode/backend."""
205
27
  async for part in stream:
@@ -209,19 +31,19 @@ async def _iter_stream_as_bytes(stream: _StreamReader[T]):
209
31
  yield part
210
32
 
211
33
 
212
- class _ContainerProcessThroughCommandRouter(Generic[T]):
213
- """
214
- Container process implementation that works via direct communication with
34
+ class _ContainerProcess(Generic[T]):
35
+ """Represents a running process in a container.
36
+
37
+ Container processes communicate via direct communication with
215
38
  the Modal worker where the container is running.
216
39
  """
217
40
 
218
41
  def __init__(
219
42
  self,
220
43
  process_id: str,
44
+ task_id: str,
221
45
  client: _Client,
222
46
  command_router_client: TaskCommandRouterClient,
223
- task_id: str,
224
- *,
225
47
  stdout: StreamType = StreamType.PIPE,
226
48
  stderr: StreamType = StreamType.PIPE,
227
49
  exec_deadline: Optional[float] = None,
@@ -236,35 +58,35 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
236
58
  self._by_line = by_line
237
59
  self._task_id = task_id
238
60
  self._stdout = _StreamReader[T](
239
- api_pb2.FILE_DESCRIPTOR_STDOUT,
240
- process_id,
241
- "container_process",
242
- self._client,
61
+ _StreamReaderThroughCommandRouterParams(
62
+ file_descriptor=api_pb2.FILE_DESCRIPTOR_STDOUT,
63
+ task_id=self._task_id,
64
+ object_id=process_id,
65
+ command_router_client=self._command_router_client,
66
+ deadline=exec_deadline,
67
+ ),
243
68
  stream_type=stdout,
244
69
  text=text,
245
70
  by_line=by_line,
246
- deadline=exec_deadline,
247
- command_router_client=self._command_router_client,
248
- task_id=self._task_id,
249
71
  )
250
72
  self._stderr = _StreamReader[T](
251
- api_pb2.FILE_DESCRIPTOR_STDERR,
252
- process_id,
253
- "container_process",
254
- self._client,
73
+ _StreamReaderThroughCommandRouterParams(
74
+ file_descriptor=api_pb2.FILE_DESCRIPTOR_STDERR,
75
+ task_id=self._task_id,
76
+ object_id=process_id,
77
+ command_router_client=self._command_router_client,
78
+ deadline=exec_deadline,
79
+ ),
255
80
  stream_type=stderr,
256
81
  text=text,
257
82
  by_line=by_line,
258
- deadline=exec_deadline,
259
- command_router_client=self._command_router_client,
260
- task_id=self._task_id,
261
83
  )
262
84
  self._stdin = _StreamWriter(
263
- process_id,
264
- "container_process",
265
- self._client,
266
- command_router_client=self._command_router_client,
267
- task_id=self._task_id,
85
+ _StreamWriterThroughCommandRouterParams(
86
+ task_id=self._task_id,
87
+ object_id=process_id,
88
+ command_router_client=self._command_router_client,
89
+ )
268
90
  )
269
91
  self._returncode = None
270
92
 
@@ -273,14 +95,17 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
273
95
 
274
96
  @property
275
97
  def stdout(self) -> _StreamReader[T]:
98
+ """StreamReader for the container process's stdout stream."""
276
99
  return self._stdout
277
100
 
278
101
  @property
279
102
  def stderr(self) -> _StreamReader[T]:
103
+ """StreamReader for the container process's stderr stream."""
280
104
  return self._stderr
281
105
 
282
106
  @property
283
107
  def stdin(self) -> _StreamWriter:
108
+ """StreamWriter for the container process's stdin stream."""
284
109
  return self._stdin
285
110
 
286
111
  @property
@@ -293,6 +118,10 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
293
118
  return self._returncode
294
119
 
295
120
  async def poll(self) -> Optional[int]:
121
+ """Check if the container process has finished running.
122
+
123
+ Returns `None` if the process is still running, else returns the exit code.
124
+ """
296
125
  if self._returncode is not None:
297
126
  return self._returncode
298
127
  try:
@@ -322,6 +151,7 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
322
151
  raise
323
152
 
324
153
  async def wait(self) -> int:
154
+ """Wait for the container process to finish running. Returns the exit code."""
325
155
  if self._returncode is not None:
326
156
  return self._returncode
327
157
 
@@ -345,6 +175,7 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
345
175
  return self._returncode
346
176
 
347
177
  async def attach(self):
178
+ """mdmd:hidden"""
348
179
  if platform.system() == "Windows":
349
180
  print("interactive exec is not currently supported on Windows.") # noqa: T201
350
181
  return
@@ -390,81 +221,4 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
390
221
  raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
391
222
 
392
223
 
393
- class _ContainerProcess(Generic[T]):
394
- """Represents a running process in a container."""
395
-
396
- def __init__(
397
- self,
398
- process_id: str,
399
- task_id: str,
400
- client: _Client,
401
- stdout: StreamType = StreamType.PIPE,
402
- stderr: StreamType = StreamType.PIPE,
403
- exec_deadline: Optional[float] = None,
404
- text: bool = True,
405
- by_line: bool = False,
406
- command_router_client: Optional[TaskCommandRouterClient] = None,
407
- ) -> None:
408
- if command_router_client is None:
409
- self._impl = _ContainerProcessThroughServer(
410
- process_id,
411
- task_id,
412
- client,
413
- stdout=stdout,
414
- stderr=stderr,
415
- exec_deadline=exec_deadline,
416
- text=text,
417
- by_line=by_line,
418
- )
419
- else:
420
- self._impl = _ContainerProcessThroughCommandRouter(
421
- process_id,
422
- client,
423
- command_router_client,
424
- task_id,
425
- stdout=stdout,
426
- stderr=stderr,
427
- exec_deadline=exec_deadline,
428
- text=text,
429
- by_line=by_line,
430
- )
431
-
432
- def __repr__(self) -> str:
433
- return self._impl.__repr__()
434
-
435
- @property
436
- def stdout(self) -> _StreamReader[T]:
437
- """StreamReader for the container process's stdout stream."""
438
- return self._impl.stdout
439
-
440
- @property
441
- def stderr(self) -> _StreamReader[T]:
442
- """StreamReader for the container process's stderr stream."""
443
- return self._impl.stderr
444
-
445
- @property
446
- def stdin(self) -> _StreamWriter:
447
- """StreamWriter for the container process's stdin stream."""
448
- return self._impl.stdin
449
-
450
- @property
451
- def returncode(self) -> int:
452
- return self._impl.returncode
453
-
454
- async def poll(self) -> Optional[int]:
455
- """Check if the container process has finished running.
456
-
457
- Returns `None` if the process is still running, else returns the exit code.
458
- """
459
- return await self._impl.poll()
460
-
461
- async def wait(self) -> int:
462
- """Wait for the container process to finish running. Returns the exit code."""
463
- return await self._impl.wait()
464
-
465
- async def attach(self):
466
- """mdmd:hidden"""
467
- await self._impl.attach()
468
-
469
-
470
224
  ContainerProcess = synchronize_api(_ContainerProcess)