langfun 0.1.2.dev202510230805__py3-none-any.whl → 0.1.2.dev202511160804__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.
Potentially problematic release.
This version of langfun might be problematic. Click here for more details.
- langfun/core/__init__.py +1 -0
- langfun/core/agentic/action.py +107 -12
- langfun/core/agentic/action_eval.py +9 -2
- langfun/core/agentic/action_test.py +25 -0
- langfun/core/async_support.py +32 -3
- langfun/core/coding/python/correction.py +19 -9
- langfun/core/coding/python/execution.py +14 -12
- langfun/core/coding/python/generation.py +21 -16
- langfun/core/coding/python/sandboxing.py +23 -3
- langfun/core/component.py +42 -3
- langfun/core/concurrent.py +70 -6
- langfun/core/concurrent_test.py +1 -0
- langfun/core/console.py +1 -1
- langfun/core/data/conversion/anthropic.py +12 -3
- langfun/core/data/conversion/anthropic_test.py +8 -6
- langfun/core/data/conversion/gemini.py +9 -2
- langfun/core/data/conversion/gemini_test.py +12 -9
- langfun/core/data/conversion/openai.py +145 -31
- langfun/core/data/conversion/openai_test.py +161 -17
- langfun/core/eval/base.py +47 -43
- langfun/core/eval/base_test.py +4 -4
- langfun/core/eval/matching.py +5 -2
- langfun/core/eval/patching.py +3 -3
- langfun/core/eval/scoring.py +4 -3
- langfun/core/eval/v2/__init__.py +1 -0
- langfun/core/eval/v2/checkpointing.py +39 -5
- langfun/core/eval/v2/checkpointing_test.py +1 -1
- langfun/core/eval/v2/eval_test_helper.py +96 -0
- langfun/core/eval/v2/evaluation.py +87 -15
- langfun/core/eval/v2/evaluation_test.py +9 -3
- langfun/core/eval/v2/example.py +45 -39
- langfun/core/eval/v2/example_test.py +3 -3
- langfun/core/eval/v2/experiment.py +51 -8
- langfun/core/eval/v2/metric_values.py +31 -3
- langfun/core/eval/v2/metric_values_test.py +32 -0
- langfun/core/eval/v2/metrics.py +157 -44
- langfun/core/eval/v2/metrics_test.py +39 -18
- langfun/core/eval/v2/progress.py +30 -1
- langfun/core/eval/v2/progress_test.py +27 -0
- langfun/core/eval/v2/progress_tracking_test.py +3 -0
- langfun/core/eval/v2/reporting.py +90 -71
- langfun/core/eval/v2/reporting_test.py +20 -6
- langfun/core/eval/v2/runners/__init__.py +26 -0
- langfun/core/eval/v2/{runners.py → runners/base.py} +22 -124
- langfun/core/eval/v2/runners/debug.py +40 -0
- langfun/core/eval/v2/runners/debug_test.py +79 -0
- langfun/core/eval/v2/runners/parallel.py +100 -0
- langfun/core/eval/v2/runners/parallel_test.py +98 -0
- langfun/core/eval/v2/runners/sequential.py +47 -0
- langfun/core/eval/v2/runners/sequential_test.py +175 -0
- langfun/core/langfunc.py +45 -130
- langfun/core/langfunc_test.py +6 -4
- langfun/core/language_model.py +103 -16
- langfun/core/language_model_test.py +9 -3
- langfun/core/llms/__init__.py +7 -1
- langfun/core/llms/anthropic.py +157 -2
- langfun/core/llms/azure_openai.py +29 -17
- langfun/core/llms/cache/base.py +25 -3
- langfun/core/llms/cache/in_memory.py +48 -7
- langfun/core/llms/cache/in_memory_test.py +14 -4
- langfun/core/llms/compositional.py +25 -1
- langfun/core/llms/deepseek.py +30 -2
- langfun/core/llms/fake.py +32 -1
- langfun/core/llms/gemini.py +14 -9
- langfun/core/llms/google_genai.py +29 -1
- langfun/core/llms/groq.py +28 -3
- langfun/core/llms/llama_cpp.py +23 -4
- langfun/core/llms/openai.py +36 -3
- langfun/core/llms/openai_compatible.py +148 -27
- langfun/core/llms/openai_compatible_test.py +207 -20
- langfun/core/llms/openai_test.py +0 -2
- langfun/core/llms/rest.py +12 -1
- langfun/core/llms/vertexai.py +51 -8
- langfun/core/logging.py +1 -1
- langfun/core/mcp/client.py +77 -22
- langfun/core/mcp/client_test.py +8 -35
- langfun/core/mcp/session.py +94 -29
- langfun/core/mcp/session_test.py +54 -0
- langfun/core/mcp/tool.py +151 -22
- langfun/core/mcp/tool_test.py +197 -0
- langfun/core/memory.py +1 -0
- langfun/core/message.py +160 -55
- langfun/core/message_test.py +65 -81
- langfun/core/modalities/__init__.py +8 -0
- langfun/core/modalities/audio.py +21 -1
- langfun/core/modalities/image.py +19 -1
- langfun/core/modalities/mime.py +62 -3
- langfun/core/modalities/pdf.py +19 -1
- langfun/core/modalities/video.py +21 -1
- langfun/core/modality.py +167 -29
- langfun/core/modality_test.py +42 -12
- langfun/core/natural_language.py +1 -1
- langfun/core/sampling.py +4 -4
- langfun/core/sampling_test.py +20 -4
- langfun/core/structured/__init__.py +2 -24
- langfun/core/structured/completion.py +34 -44
- langfun/core/structured/completion_test.py +23 -43
- langfun/core/structured/description.py +54 -50
- langfun/core/structured/function_generation.py +29 -12
- langfun/core/structured/mapping.py +81 -37
- langfun/core/structured/parsing.py +95 -79
- langfun/core/structured/parsing_test.py +0 -3
- langfun/core/structured/querying.py +215 -142
- langfun/core/structured/querying_test.py +65 -29
- langfun/core/structured/schema/__init__.py +48 -0
- langfun/core/structured/schema/base.py +664 -0
- langfun/core/structured/schema/base_test.py +531 -0
- langfun/core/structured/schema/json.py +174 -0
- langfun/core/structured/schema/json_test.py +121 -0
- langfun/core/structured/schema/python.py +316 -0
- langfun/core/structured/schema/python_test.py +410 -0
- langfun/core/structured/schema_generation.py +33 -14
- langfun/core/structured/scoring.py +47 -36
- langfun/core/structured/tokenization.py +26 -11
- langfun/core/subscription.py +2 -2
- langfun/core/template.py +174 -49
- langfun/core/template_test.py +123 -17
- langfun/env/__init__.py +8 -2
- langfun/env/base_environment.py +320 -128
- langfun/env/base_environment_test.py +473 -0
- langfun/env/base_feature.py +92 -15
- langfun/env/base_feature_test.py +228 -0
- langfun/env/base_sandbox.py +84 -361
- langfun/env/base_sandbox_test.py +1235 -0
- langfun/env/event_handlers/__init__.py +1 -1
- langfun/env/event_handlers/chain.py +233 -0
- langfun/env/event_handlers/chain_test.py +253 -0
- langfun/env/event_handlers/event_logger.py +95 -98
- langfun/env/event_handlers/event_logger_test.py +21 -21
- langfun/env/event_handlers/metric_writer.py +225 -140
- langfun/env/event_handlers/metric_writer_test.py +23 -6
- langfun/env/interface.py +854 -40
- langfun/env/interface_test.py +112 -2
- langfun/env/load_balancers_test.py +23 -2
- langfun/env/test_utils.py +126 -84
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/METADATA +1 -1
- langfun-0.1.2.dev202511160804.dist-info/RECORD +211 -0
- langfun/core/eval/v2/runners_test.py +0 -343
- langfun/core/structured/schema.py +0 -987
- langfun/core/structured/schema_test.py +0 -982
- langfun/env/base_test.py +0 -1481
- langfun/env/event_handlers/base.py +0 -350
- langfun-0.1.2.dev202510230805.dist-info/RECORD +0 -195
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/top_level.txt +0 -0
langfun/env/base_environment.py
CHANGED
|
@@ -23,8 +23,10 @@ Note that:
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
import abc
|
|
26
|
+
import collections
|
|
26
27
|
import functools
|
|
27
28
|
import random
|
|
29
|
+
import re
|
|
28
30
|
import threading
|
|
29
31
|
import time
|
|
30
32
|
from typing import Annotated, Any
|
|
@@ -34,7 +36,6 @@ import langfun.core as lf
|
|
|
34
36
|
from langfun.env import base_sandbox
|
|
35
37
|
from langfun.env import interface
|
|
36
38
|
from langfun.env import load_balancers
|
|
37
|
-
from langfun.env.event_handlers import base as event_handler_base
|
|
38
39
|
import pyglove as pg
|
|
39
40
|
|
|
40
41
|
|
|
@@ -46,6 +47,23 @@ class BaseEnvironment(interface.Environment):
|
|
|
46
47
|
maintenance.
|
|
47
48
|
"""
|
|
48
49
|
|
|
50
|
+
image_ids: Annotated[
|
|
51
|
+
list[str],
|
|
52
|
+
(
|
|
53
|
+
'A list of static image IDs served by the environment. '
|
|
54
|
+
)
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
supports_dynamic_image_loading: Annotated[
|
|
58
|
+
bool,
|
|
59
|
+
(
|
|
60
|
+
'Whether the environment supports dynamic loading of images which is '
|
|
61
|
+
'not included in the `image_ids`. `image_ids` could coexist with '
|
|
62
|
+
'dynamic image loading, which allows users to specify an image id '
|
|
63
|
+
'that is not included in the `image_ids`.'
|
|
64
|
+
)
|
|
65
|
+
] = False
|
|
66
|
+
|
|
49
67
|
root_dir: Annotated[
|
|
50
68
|
str | None,
|
|
51
69
|
(
|
|
@@ -55,11 +73,15 @@ class BaseEnvironment(interface.Environment):
|
|
|
55
73
|
] = None
|
|
56
74
|
|
|
57
75
|
pool_size: Annotated[
|
|
58
|
-
int | tuple[int, int],
|
|
76
|
+
int | tuple[int, int] | dict[str, int | tuple[int, int]],
|
|
59
77
|
(
|
|
60
78
|
'The (min_size, max_size) of the sandbox pool. If an integer, it '
|
|
61
|
-
'will be used as both min and max size. If 0, sandboxes will be '
|
|
62
|
-
'created on demand and shutdown when user session ends.'
|
|
79
|
+
'will be used as both min and max size. If 0, all sandboxes will be '
|
|
80
|
+
'created on demand and shutdown when user session ends. If a dict, '
|
|
81
|
+
'users could configure the pool size based on image IDs. The keys '
|
|
82
|
+
'are regular expressions for image IDs, and the values are '
|
|
83
|
+
'(min_size, max_size) tuples. For dynamic image IDs, min_size will '
|
|
84
|
+
'ignored while max_size will be honored.'
|
|
63
85
|
)
|
|
64
86
|
] = (0, 256)
|
|
65
87
|
|
|
@@ -90,12 +112,13 @@ class BaseEnvironment(interface.Environment):
|
|
|
90
112
|
)
|
|
91
113
|
] = True
|
|
92
114
|
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
event_handler: Annotated[
|
|
116
|
+
interface.EventHandler,
|
|
95
117
|
(
|
|
96
118
|
'User handler for the environment events.'
|
|
119
|
+
'By default, the no-op event handler is used.'
|
|
97
120
|
)
|
|
98
|
-
] =
|
|
121
|
+
] = interface.EventHandler()
|
|
99
122
|
|
|
100
123
|
outage_grace_period: Annotated[
|
|
101
124
|
float,
|
|
@@ -146,14 +169,39 @@ class BaseEnvironment(interface.Environment):
|
|
|
146
169
|
|
|
147
170
|
self._status = self.Status.CREATED
|
|
148
171
|
self._start_time = None
|
|
149
|
-
|
|
150
|
-
self.
|
|
172
|
+
|
|
173
|
+
self._sandbox_pool: dict[str, list[base_sandbox.BaseSandbox]] = (
|
|
174
|
+
collections.defaultdict(list)
|
|
175
|
+
)
|
|
176
|
+
self._next_sandbox_id: dict[str, int] = collections.defaultdict(int)
|
|
151
177
|
self._random = (
|
|
152
178
|
random if self.random_seed is None else random.Random(self.random_seed)
|
|
153
179
|
)
|
|
154
|
-
|
|
155
180
|
self._housekeep_thread = None
|
|
156
181
|
self._offline_start_time = None
|
|
182
|
+
self._non_sandbox_based_features_with_setup_called = set()
|
|
183
|
+
|
|
184
|
+
# Check image IDs and feature requirements.
|
|
185
|
+
self._check_image_ids()
|
|
186
|
+
self._check_feature_requirements()
|
|
187
|
+
|
|
188
|
+
def _check_image_ids(self) -> None:
|
|
189
|
+
"""Checks image ids. Subclass could override this method."""
|
|
190
|
+
|
|
191
|
+
def _check_feature_requirements(self) -> None:
|
|
192
|
+
"""Checks if the image ID is supported by the feature."""
|
|
193
|
+
if self.supports_dynamic_image_loading:
|
|
194
|
+
return
|
|
195
|
+
for name, feature in self.features.items():
|
|
196
|
+
if not feature.is_sandbox_based or any(
|
|
197
|
+
feature.is_applicable(image_id) for image_id in self.image_ids
|
|
198
|
+
):
|
|
199
|
+
continue
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f'Feature {name!r} is not applicable to all available images: '
|
|
202
|
+
f'{self.image_ids!r}. '
|
|
203
|
+
f'Applicable images: {feature.applicable_images}.'
|
|
204
|
+
)
|
|
157
205
|
|
|
158
206
|
#
|
|
159
207
|
# Subclasses must implement:
|
|
@@ -162,6 +210,7 @@ class BaseEnvironment(interface.Environment):
|
|
|
162
210
|
@abc.abstractmethod
|
|
163
211
|
def _create_sandbox(
|
|
164
212
|
self,
|
|
213
|
+
image_id: str,
|
|
165
214
|
sandbox_id: str,
|
|
166
215
|
reusable: bool,
|
|
167
216
|
proactive_session_setup: bool,
|
|
@@ -170,6 +219,7 @@ class BaseEnvironment(interface.Environment):
|
|
|
170
219
|
"""Creates a sandbox with the given identifier.
|
|
171
220
|
|
|
172
221
|
Args:
|
|
222
|
+
image_id: The image ID to use for the sandbox.
|
|
173
223
|
sandbox_id: The identifier for the sandbox.
|
|
174
224
|
reusable: Whether the sandbox is reusable across user sessions.
|
|
175
225
|
proactive_session_setup: Whether the sandbox performs session setup work
|
|
@@ -185,13 +235,13 @@ class BaseEnvironment(interface.Environment):
|
|
|
185
235
|
interface.SandboxStateError: If sandbox cannot be started.
|
|
186
236
|
"""
|
|
187
237
|
|
|
188
|
-
def new_session_id(self) -> str:
|
|
238
|
+
def new_session_id(self, feature_hint: str | None = None) -> str:
|
|
189
239
|
"""Generates a random session ID."""
|
|
190
240
|
suffix = uuid.UUID(
|
|
191
241
|
bytes=bytes(bytes(self._random.getrandbits(8) for _ in range(16))),
|
|
192
242
|
version=4
|
|
193
243
|
).hex[:7]
|
|
194
|
-
return f'session-{suffix}'
|
|
244
|
+
return f'{feature_hint or "unknown"}-session-{suffix}'
|
|
195
245
|
|
|
196
246
|
@property
|
|
197
247
|
def housekeep_counter(self) -> int:
|
|
@@ -204,42 +254,66 @@ class BaseEnvironment(interface.Environment):
|
|
|
204
254
|
|
|
205
255
|
def stats(self) -> dict[str, Any]:
|
|
206
256
|
"""Returns the stats of the environment."""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
257
|
+
stats_by_image_id = {}
|
|
258
|
+
for image_id, sandboxes in self._sandbox_pool.items():
|
|
259
|
+
stats_dict = {
|
|
260
|
+
status.value: 0
|
|
261
|
+
for status in interface.Sandbox.Status
|
|
262
|
+
}
|
|
263
|
+
for sandbox in sandboxes:
|
|
264
|
+
stats_dict[sandbox.status.value] += 1
|
|
265
|
+
stats_by_image_id[image_id] = stats_dict
|
|
213
266
|
return {
|
|
214
|
-
'sandbox':
|
|
267
|
+
'sandbox': stats_by_image_id,
|
|
215
268
|
}
|
|
216
269
|
|
|
217
270
|
def _start(self) -> None:
|
|
218
271
|
"""Implementation of starting the environment."""
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
self._sandbox_pool[
|
|
234
|
-
|
|
235
|
-
|
|
272
|
+
sandbox_startup_infos = []
|
|
273
|
+
self._non_sandbox_based_features_with_setup_called.clear()
|
|
274
|
+
# Setup all non-sandbox-based features.
|
|
275
|
+
for feature in self.non_sandbox_based_features():
|
|
276
|
+
self._non_sandbox_based_features_with_setup_called.add(feature.name)
|
|
277
|
+
feature.setup(sandbox=None)
|
|
278
|
+
|
|
279
|
+
# Setup sandbox pools.
|
|
280
|
+
for image_id in self.image_ids:
|
|
281
|
+
next_sandbox_id = 0
|
|
282
|
+
if self.enable_pooling(image_id):
|
|
283
|
+
min_pool_size = self.min_pool_size(image_id)
|
|
284
|
+
for i in range(min_pool_size):
|
|
285
|
+
sandbox_startup_infos.append((image_id, i))
|
|
286
|
+
self._sandbox_pool[image_id] = [None] * min_pool_size
|
|
287
|
+
next_sandbox_id = min_pool_size
|
|
288
|
+
self._next_sandbox_id[image_id] = next_sandbox_id
|
|
289
|
+
|
|
290
|
+
def _start_sandbox(sandbox_startup_info) -> None:
|
|
291
|
+
image_id, index = sandbox_startup_info
|
|
292
|
+
self._sandbox_pool[image_id][index] = self._bring_up_sandbox_with_retry(
|
|
293
|
+
image_id=image_id,
|
|
294
|
+
sandbox_id=f'{index}:0',
|
|
295
|
+
shutdown_env_upon_outage=False
|
|
296
|
+
)
|
|
236
297
|
|
|
237
|
-
if
|
|
238
|
-
|
|
239
|
-
|
|
298
|
+
if sandbox_startup_infos:
|
|
299
|
+
# Pre-allocate the sandbox pool before usage.
|
|
300
|
+
_ = list(
|
|
301
|
+
lf.concurrent_map(
|
|
302
|
+
_start_sandbox,
|
|
303
|
+
sandbox_startup_infos,
|
|
304
|
+
silence_on_errors=None,
|
|
305
|
+
max_workers=min(
|
|
306
|
+
self.pool_operation_max_parallelism,
|
|
307
|
+
len(sandbox_startup_infos)
|
|
308
|
+
),
|
|
309
|
+
)
|
|
240
310
|
)
|
|
241
|
-
|
|
242
|
-
|
|
311
|
+
|
|
312
|
+
self._housekeep_thread = threading.Thread(
|
|
313
|
+
target=self._housekeep_loop, daemon=True
|
|
314
|
+
)
|
|
315
|
+
self._housekeep_counter = 0
|
|
316
|
+
self._housekeep_thread.start()
|
|
243
317
|
|
|
244
318
|
def _shutdown(self) -> None:
|
|
245
319
|
"""Implementation of shutting down the environment."""
|
|
@@ -248,30 +322,44 @@ class BaseEnvironment(interface.Environment):
|
|
|
248
322
|
self._housekeep_thread.join()
|
|
249
323
|
self._housekeep_thread = None
|
|
250
324
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
325
|
+
# Teardown all non-sandbox-based features.
|
|
326
|
+
for feature in self.non_sandbox_based_features():
|
|
327
|
+
if feature.name in self._non_sandbox_based_features_with_setup_called:
|
|
328
|
+
try:
|
|
329
|
+
feature.teardown()
|
|
330
|
+
except BaseException: # pylint: disable=broad-except
|
|
331
|
+
pass
|
|
254
332
|
|
|
333
|
+
# Shutdown sandbox pools.
|
|
255
334
|
if self._sandbox_pool:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
335
|
+
sandboxes = []
|
|
336
|
+
for sandbox in self._sandbox_pool.values():
|
|
337
|
+
sandboxes.extend(sandbox)
|
|
338
|
+
self._sandbox_pool = {}
|
|
339
|
+
|
|
340
|
+
if sandboxes:
|
|
341
|
+
def _shutdown_sandbox(sandbox: base_sandbox.BaseSandbox) -> None:
|
|
342
|
+
if sandbox is not None:
|
|
343
|
+
sandbox.shutdown()
|
|
344
|
+
|
|
345
|
+
_ = list(
|
|
346
|
+
lf.concurrent_map(
|
|
347
|
+
_shutdown_sandbox,
|
|
348
|
+
sandboxes,
|
|
349
|
+
silence_on_errors=None,
|
|
350
|
+
max_workers=min(
|
|
351
|
+
self.pool_operation_max_parallelism,
|
|
352
|
+
len(sandboxes)
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
)
|
|
268
356
|
|
|
269
357
|
#
|
|
270
358
|
# Environment basics.
|
|
271
359
|
#
|
|
272
360
|
|
|
273
361
|
@property
|
|
274
|
-
def sandbox_pool(self) -> list[base_sandbox.BaseSandbox]:
|
|
362
|
+
def sandbox_pool(self) -> dict[str, list[base_sandbox.BaseSandbox]]:
|
|
275
363
|
"""Returns the sandbox pool."""
|
|
276
364
|
return self._sandbox_pool
|
|
277
365
|
|
|
@@ -280,11 +368,6 @@ class BaseEnvironment(interface.Environment):
|
|
|
280
368
|
"""Returns the working directory for the environment."""
|
|
281
369
|
return self.id.working_dir(self.root_dir)
|
|
282
370
|
|
|
283
|
-
@property
|
|
284
|
-
def enable_pooling(self) -> bool:
|
|
285
|
-
"""Returns whether the environment enables pooling."""
|
|
286
|
-
return self.max_pool_size > 0
|
|
287
|
-
|
|
288
371
|
@property
|
|
289
372
|
def status(self) -> interface.Environment.Status:
|
|
290
373
|
"""Returns whether the environment is online."""
|
|
@@ -294,19 +377,39 @@ class BaseEnvironment(interface.Environment):
|
|
|
294
377
|
"""Sets the status of the environment."""
|
|
295
378
|
self._status = status
|
|
296
379
|
|
|
297
|
-
|
|
298
|
-
|
|
380
|
+
def enable_pooling(self, image_id: str) -> bool:
|
|
381
|
+
"""Returns whether the environment enables pooling."""
|
|
382
|
+
return self.max_pool_size(image_id) > 0
|
|
383
|
+
|
|
384
|
+
def min_pool_size(self, image_id: str) -> int:
|
|
299
385
|
"""Returns the minimum size of the sandbox pool."""
|
|
300
|
-
|
|
301
|
-
return self.pool_size
|
|
302
|
-
return self.pool_size[0]
|
|
386
|
+
return self._pool_size(image_id)[0]
|
|
303
387
|
|
|
304
|
-
|
|
305
|
-
def max_pool_size(self) -> int:
|
|
388
|
+
def max_pool_size(self, image_id: str) -> int:
|
|
306
389
|
"""Returns the maximum size of the sandbox pool."""
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
390
|
+
return self._pool_size(image_id)[1]
|
|
391
|
+
|
|
392
|
+
def _pool_size(self, image_id: str) -> tuple[int, int]:
|
|
393
|
+
"""Returns the minimum and maximum size of the sandbox pool."""
|
|
394
|
+
if isinstance(self.pool_size, dict):
|
|
395
|
+
if image_id in self.pool_size:
|
|
396
|
+
pool_size = self.pool_size[image_id]
|
|
397
|
+
else:
|
|
398
|
+
for k, v in self.pool_size.items():
|
|
399
|
+
if re.match(k, image_id):
|
|
400
|
+
pool_size = v
|
|
401
|
+
break
|
|
402
|
+
else:
|
|
403
|
+
# Default pool size is 0 and 256.
|
|
404
|
+
pool_size = (0, 256)
|
|
405
|
+
else:
|
|
406
|
+
pool_size = self.pool_size
|
|
407
|
+
|
|
408
|
+
if isinstance(pool_size, int):
|
|
409
|
+
return pool_size, pool_size
|
|
410
|
+
else:
|
|
411
|
+
assert isinstance(pool_size, tuple) and len(pool_size) == 2
|
|
412
|
+
return pool_size
|
|
310
413
|
|
|
311
414
|
@property
|
|
312
415
|
def start_time(self) -> float | None:
|
|
@@ -373,9 +476,16 @@ class BaseEnvironment(interface.Environment):
|
|
|
373
476
|
# Environment operations.
|
|
374
477
|
#
|
|
375
478
|
|
|
376
|
-
def acquire(
|
|
479
|
+
def acquire(
|
|
480
|
+
self,
|
|
481
|
+
image_id: str | None = None
|
|
482
|
+
) -> base_sandbox.BaseSandbox:
|
|
377
483
|
"""Acquires a sandbox from the environment.
|
|
378
484
|
|
|
485
|
+
Args:
|
|
486
|
+
image_id: The image ID to use for the sandbox. If None, it will be
|
|
487
|
+
automatically determined by the environment.
|
|
488
|
+
|
|
379
489
|
Returns:
|
|
380
490
|
The acquired sandbox.
|
|
381
491
|
|
|
@@ -385,28 +495,50 @@ class BaseEnvironment(interface.Environment):
|
|
|
385
495
|
interface.EnvironmentOverloadError: If the max pool size is reached and
|
|
386
496
|
the grace period has passed.
|
|
387
497
|
"""
|
|
388
|
-
|
|
389
498
|
if not self.is_online:
|
|
390
499
|
raise interface.EnvironmentOutageError(
|
|
391
500
|
f'Environment {self.id} is not alive.',
|
|
392
501
|
environment=self,
|
|
393
502
|
offline_duration=self.offline_duration,
|
|
394
503
|
)
|
|
504
|
+
if image_id is None:
|
|
505
|
+
if not self.image_ids:
|
|
506
|
+
raise ValueError(
|
|
507
|
+
f'Environment {self.id} does not have a default image ID. '
|
|
508
|
+
'Please specify the image ID explicitly.'
|
|
509
|
+
)
|
|
510
|
+
image_id = self.image_ids[0]
|
|
511
|
+
elif (image_id not in self.image_ids
|
|
512
|
+
and not self.supports_dynamic_image_loading):
|
|
513
|
+
raise ValueError(
|
|
514
|
+
f'Environment {self.id} does not serve image ID {image_id!r}. '
|
|
515
|
+
f'Please use one of the following image IDs: {self.image_ids!r} or '
|
|
516
|
+
f'set `{self.__class__.__name__}.supports_dynamic_image_loading` '
|
|
517
|
+
'to True if dynamic image loading is supported.'
|
|
518
|
+
)
|
|
519
|
+
return self._acquire(image_id)
|
|
395
520
|
|
|
396
|
-
|
|
521
|
+
def _acquire(
|
|
522
|
+
self,
|
|
523
|
+
image_id: str | None = None
|
|
524
|
+
) -> base_sandbox.BaseSandbox:
|
|
525
|
+
"""Acquires a sandbox from the environment."""
|
|
526
|
+
if not self.enable_pooling(image_id):
|
|
397
527
|
return self._bring_up_sandbox_with_retry(
|
|
398
|
-
|
|
528
|
+
image_id=image_id,
|
|
529
|
+
sandbox_id=str(self._increment_sandbox_id(image_id)),
|
|
399
530
|
set_acquired=True,
|
|
400
531
|
)
|
|
401
532
|
|
|
402
533
|
allocation_start_time = time.time()
|
|
534
|
+
sandbox_pool = self._sandbox_pool[image_id]
|
|
403
535
|
while True:
|
|
404
536
|
try:
|
|
405
537
|
# We only append or replace items in the sandbox pool, therefore
|
|
406
538
|
# there is no need to lock the pool.
|
|
407
|
-
return self.load_balancer.acquire(
|
|
539
|
+
return self.load_balancer.acquire(sandbox_pool)
|
|
408
540
|
except IndexError:
|
|
409
|
-
if len(
|
|
541
|
+
if len(sandbox_pool) == self.max_pool_size(image_id):
|
|
410
542
|
if time.time() - allocation_start_time > self.outage_grace_period:
|
|
411
543
|
raise interface.EnvironmentOverloadError( # pylint: disable=raise-missing-from
|
|
412
544
|
environment=self
|
|
@@ -415,11 +547,12 @@ class BaseEnvironment(interface.Environment):
|
|
|
415
547
|
else:
|
|
416
548
|
try:
|
|
417
549
|
sandbox = self._bring_up_sandbox(
|
|
418
|
-
|
|
550
|
+
image_id=image_id,
|
|
551
|
+
sandbox_id=f'{self._increment_sandbox_id(image_id)}:0',
|
|
419
552
|
set_acquired=True,
|
|
420
553
|
)
|
|
421
554
|
# Append is atomic and does not require locking.
|
|
422
|
-
|
|
555
|
+
sandbox_pool.append(sandbox)
|
|
423
556
|
return sandbox
|
|
424
557
|
except (
|
|
425
558
|
interface.EnvironmentError, interface.SandboxStateError
|
|
@@ -428,6 +561,7 @@ class BaseEnvironment(interface.Environment):
|
|
|
428
561
|
|
|
429
562
|
def _bring_up_sandbox(
|
|
430
563
|
self,
|
|
564
|
+
image_id: str,
|
|
431
565
|
sandbox_id: str,
|
|
432
566
|
set_acquired: bool = False,
|
|
433
567
|
) -> base_sandbox.BaseSandbox:
|
|
@@ -435,13 +569,12 @@ class BaseEnvironment(interface.Environment):
|
|
|
435
569
|
env_error = None
|
|
436
570
|
try:
|
|
437
571
|
sandbox = self._create_sandbox(
|
|
572
|
+
image_id=image_id,
|
|
438
573
|
sandbox_id=sandbox_id,
|
|
439
|
-
reusable=self.enable_pooling,
|
|
574
|
+
reusable=self.enable_pooling(image_id),
|
|
440
575
|
proactive_session_setup=self.proactive_session_setup,
|
|
441
576
|
keepalive_interval=self.sandbox_keepalive_interval,
|
|
442
577
|
)
|
|
443
|
-
for handler in self.event_handlers:
|
|
444
|
-
sandbox.add_event_handler(handler)
|
|
445
578
|
sandbox.start()
|
|
446
579
|
if set_acquired:
|
|
447
580
|
sandbox.set_acquired()
|
|
@@ -457,6 +590,7 @@ class BaseEnvironment(interface.Environment):
|
|
|
457
590
|
|
|
458
591
|
def _bring_up_sandbox_with_retry(
|
|
459
592
|
self,
|
|
593
|
+
image_id: str,
|
|
460
594
|
sandbox_id: str,
|
|
461
595
|
set_acquired: bool = False,
|
|
462
596
|
shutdown_env_upon_outage: bool = True,
|
|
@@ -464,6 +598,7 @@ class BaseEnvironment(interface.Environment):
|
|
|
464
598
|
"""Brings up a new sandbox with retry until grace period is passed.
|
|
465
599
|
|
|
466
600
|
Args:
|
|
601
|
+
image_id: The image ID to use for the sandbox.
|
|
467
602
|
sandbox_id: The ID of the sandbox to bring up.
|
|
468
603
|
set_acquired: If True, the sandbox will be marked as acquired.
|
|
469
604
|
shutdown_env_upon_outage: Whether to shutdown the environment when the
|
|
@@ -479,15 +614,15 @@ class BaseEnvironment(interface.Environment):
|
|
|
479
614
|
while True:
|
|
480
615
|
try:
|
|
481
616
|
return self._bring_up_sandbox(
|
|
482
|
-
sandbox_id=sandbox_id, set_acquired=set_acquired
|
|
617
|
+
image_id=image_id, sandbox_id=sandbox_id, set_acquired=set_acquired
|
|
483
618
|
)
|
|
484
619
|
except (interface.EnvironmentError, interface.SandboxStateError) as e:
|
|
485
620
|
self._report_outage_or_wait(e, shutdown_env_upon_outage)
|
|
486
621
|
|
|
487
|
-
def _increment_sandbox_id(self) -> int:
|
|
622
|
+
def _increment_sandbox_id(self, image_id: str) -> int:
|
|
488
623
|
"""Returns the next pooled sandbox ID."""
|
|
489
|
-
x = self._next_sandbox_id
|
|
490
|
-
self._next_sandbox_id += 1
|
|
624
|
+
x = self._next_sandbox_id[image_id]
|
|
625
|
+
self._next_sandbox_id[image_id] += 1
|
|
491
626
|
return x
|
|
492
627
|
|
|
493
628
|
def _report_outage_or_wait(
|
|
@@ -511,33 +646,83 @@ class BaseEnvironment(interface.Environment):
|
|
|
511
646
|
|
|
512
647
|
def _housekeep_loop(self) -> None:
|
|
513
648
|
"""Housekeeping loop for the environment."""
|
|
649
|
+
def _indices_by_image_id(
|
|
650
|
+
entries: list[tuple[str, int, Any]]
|
|
651
|
+
) -> dict[str, list[int]]:
|
|
652
|
+
indices_by_image_id = collections.defaultdict(list)
|
|
653
|
+
for image_id, i, _ in entries:
|
|
654
|
+
indices_by_image_id[image_id].append(i)
|
|
655
|
+
return indices_by_image_id
|
|
656
|
+
|
|
657
|
+
last_housekeep_time = {
|
|
658
|
+
f.name: time.time() for f in self.non_sandbox_based_features()
|
|
659
|
+
}
|
|
660
|
+
|
|
514
661
|
while self._status not in (self.Status.SHUTTING_DOWN, self.Status.OFFLINE):
|
|
515
662
|
housekeep_start_time = time.time()
|
|
663
|
+
feature_housekeep_successes = []
|
|
664
|
+
feature_housekeep_failures = []
|
|
665
|
+
|
|
666
|
+
# Housekeeping non-sandbox-based features.
|
|
667
|
+
for feature in self.non_sandbox_based_features():
|
|
668
|
+
if feature.housekeep_interval is None:
|
|
669
|
+
continue
|
|
670
|
+
if (last_housekeep_time[feature.name]
|
|
671
|
+
+ feature.housekeep_interval < time.time()):
|
|
672
|
+
try:
|
|
673
|
+
feature.housekeep()
|
|
674
|
+
last_housekeep_time[feature.name] = time.time()
|
|
675
|
+
feature_housekeep_successes.append(feature.name)
|
|
676
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
677
|
+
pg.logging.error(
|
|
678
|
+
'[%s/%s]: Feature housekeeping failed with error: %s.'
|
|
679
|
+
'Shutting down environment...',
|
|
680
|
+
self.id,
|
|
681
|
+
feature.name,
|
|
682
|
+
e,
|
|
683
|
+
)
|
|
684
|
+
feature_housekeep_failures.append(feature.name)
|
|
685
|
+
self._housekeep_counter += 1
|
|
686
|
+
self.on_housekeep(
|
|
687
|
+
duration=time.time() - housekeep_start_time,
|
|
688
|
+
error=e,
|
|
689
|
+
feature_housekeep_successes=feature_housekeep_successes,
|
|
690
|
+
feature_housekeep_failures=feature_housekeep_failures,
|
|
691
|
+
)
|
|
692
|
+
self.shutdown()
|
|
693
|
+
return
|
|
516
694
|
|
|
695
|
+
# Replace dead sandboxes.
|
|
517
696
|
is_online = True
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
697
|
+
dead_sandbox_entries = []
|
|
698
|
+
for image_id, sandboxes in self._sandbox_pool.items():
|
|
699
|
+
for i, sandbox in enumerate(sandboxes):
|
|
700
|
+
if sandbox.status == interface.Sandbox.Status.OFFLINE:
|
|
701
|
+
dead_sandbox_entries.append((image_id, i, sandbox))
|
|
702
|
+
|
|
703
|
+
replaced_indices_by_image_id = {}
|
|
704
|
+
|
|
705
|
+
if dead_sandbox_entries:
|
|
706
|
+
replaced_indices_by_image_id = self._replace_dead_sandboxes(
|
|
707
|
+
dead_sandbox_entries
|
|
708
|
+
)
|
|
709
|
+
if not replaced_indices_by_image_id:
|
|
527
710
|
is_online = self.offline_duration < self.outage_grace_period
|
|
528
711
|
|
|
529
712
|
self._housekeep_counter += 1
|
|
530
713
|
duration = time.time() - housekeep_start_time
|
|
714
|
+
|
|
531
715
|
kwargs = dict(
|
|
532
|
-
|
|
533
|
-
|
|
716
|
+
feature_housekeep_successes=feature_housekeep_successes,
|
|
717
|
+
feature_housekeep_failures=feature_housekeep_failures,
|
|
718
|
+
dead_sandboxes=_indices_by_image_id(dead_sandbox_entries),
|
|
719
|
+
replaced_sandboxes=replaced_indices_by_image_id,
|
|
534
720
|
offline_duration=self.offline_duration,
|
|
535
721
|
)
|
|
536
722
|
if is_online:
|
|
537
723
|
self.on_housekeep(duration, **kwargs)
|
|
538
724
|
time.sleep(self.housekeep_interval)
|
|
539
725
|
else:
|
|
540
|
-
self.shutdown()
|
|
541
726
|
self.on_housekeep(
|
|
542
727
|
duration,
|
|
543
728
|
interface.EnvironmentOutageError(
|
|
@@ -545,50 +730,63 @@ class BaseEnvironment(interface.Environment):
|
|
|
545
730
|
),
|
|
546
731
|
**kwargs
|
|
547
732
|
)
|
|
733
|
+
self.shutdown()
|
|
548
734
|
|
|
549
|
-
def _replace_dead_sandboxes(
|
|
735
|
+
def _replace_dead_sandboxes(
|
|
736
|
+
self,
|
|
737
|
+
dead_sandbox_entries: list[tuple[str, int, base_sandbox.BaseSandbox]]
|
|
738
|
+
) -> dict[str, list[int]]:
|
|
550
739
|
"""Replaces a dead sandbox with a new one.
|
|
551
740
|
|
|
552
741
|
Args:
|
|
553
|
-
|
|
742
|
+
dead_sandbox_entries: A list of tuples (image_id, index, sandbox) of
|
|
743
|
+
dead sandboxes to replace.
|
|
554
744
|
|
|
555
745
|
Returns:
|
|
556
|
-
Successfully replaced
|
|
746
|
+
Successfully replaced sandboxes in a dict of image ID to a list of
|
|
747
|
+
indices.
|
|
557
748
|
"""
|
|
558
749
|
pg.logging.warning(
|
|
559
750
|
'[%s]: %s maintenance: '
|
|
560
751
|
'Replacing %d dead sandbox(es) with new ones...',
|
|
561
752
|
self.id,
|
|
562
753
|
self.__class__.__name__,
|
|
563
|
-
len(
|
|
754
|
+
len(dead_sandbox_entries),
|
|
564
755
|
)
|
|
565
|
-
def _replace(
|
|
566
|
-
|
|
567
|
-
|
|
756
|
+
def _replace(sandbox_entry: tuple[str, int, base_sandbox.BaseSandbox]):
|
|
757
|
+
image_id, i, sandbox = sandbox_entry
|
|
758
|
+
generation = int(sandbox.id.sandbox_id.split(':')[-1])
|
|
759
|
+
replaced_sandbox = self._bring_up_sandbox(
|
|
760
|
+
image_id=image_id,
|
|
761
|
+
sandbox_id=f'{i}:{generation + 1}'
|
|
762
|
+
)
|
|
763
|
+
self._sandbox_pool[image_id][i] = replaced_sandbox
|
|
568
764
|
|
|
569
765
|
# TODO(daiyip): Consider to loose the condition to allow some dead
|
|
570
766
|
# sandboxes to be replaced successfully.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
767
|
+
replaced_indices_by_image_id = collections.defaultdict(list)
|
|
768
|
+
num_replaced = 0
|
|
769
|
+
for (image_id, index, _), _, error in lf.concurrent_map(
|
|
770
|
+
_replace, dead_sandbox_entries,
|
|
574
771
|
max_workers=min(
|
|
575
772
|
self.pool_operation_max_parallelism,
|
|
576
|
-
len(
|
|
773
|
+
len(dead_sandbox_entries)
|
|
577
774
|
),
|
|
578
775
|
):
|
|
579
776
|
if error is None:
|
|
580
|
-
|
|
777
|
+
replaced_indices_by_image_id[image_id].append(index)
|
|
778
|
+
num_replaced += 1
|
|
581
779
|
|
|
582
780
|
pg.logging.warning(
|
|
583
781
|
'[%s]: %s maintenance: '
|
|
584
782
|
'%d/%d dead sandbox(es) have been replaced with new ones. (slots=%s)',
|
|
585
783
|
self.id,
|
|
586
784
|
self.__class__.__name__,
|
|
587
|
-
|
|
588
|
-
len(
|
|
589
|
-
|
|
785
|
+
num_replaced,
|
|
786
|
+
len(dead_sandbox_entries),
|
|
787
|
+
replaced_indices_by_image_id,
|
|
590
788
|
)
|
|
591
|
-
return
|
|
789
|
+
return replaced_indices_by_image_id
|
|
592
790
|
|
|
593
791
|
#
|
|
594
792
|
# Event handlers subclasses can override.
|
|
@@ -596,16 +794,14 @@ class BaseEnvironment(interface.Environment):
|
|
|
596
794
|
|
|
597
795
|
def on_starting(self) -> None:
|
|
598
796
|
"""Called when the environment is getting started."""
|
|
599
|
-
|
|
600
|
-
handler.on_environment_starting(self)
|
|
797
|
+
self.event_handler.on_environment_starting(self)
|
|
601
798
|
|
|
602
799
|
def on_start(
|
|
603
800
|
self,
|
|
604
801
|
duration: float, error: BaseException | None = None
|
|
605
802
|
) -> None:
|
|
606
803
|
"""Called when the environment is started."""
|
|
607
|
-
|
|
608
|
-
handler.on_environment_start(self, duration, error)
|
|
804
|
+
self.event_handler.on_environment_start(self, duration, error)
|
|
609
805
|
|
|
610
806
|
def on_housekeep(
|
|
611
807
|
self,
|
|
@@ -614,16 +810,13 @@ class BaseEnvironment(interface.Environment):
|
|
|
614
810
|
**kwargs
|
|
615
811
|
) -> None:
|
|
616
812
|
"""Called when the environment finishes a round of housekeeping."""
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
self, housekeep_counter, duration, error, **kwargs
|
|
621
|
-
)
|
|
813
|
+
self.event_handler.on_environment_housekeep(
|
|
814
|
+
self, self.housekeep_counter, duration, error, **kwargs
|
|
815
|
+
)
|
|
622
816
|
|
|
623
817
|
def on_shutting_down(self) -> None:
|
|
624
818
|
"""Called when the environment is shutting down."""
|
|
625
|
-
|
|
626
|
-
handler.on_environment_shutting_down(self, self.offline_duration)
|
|
819
|
+
self.event_handler.on_environment_shutting_down(self, self.offline_duration)
|
|
627
820
|
|
|
628
821
|
def on_shutdown(
|
|
629
822
|
self,
|
|
@@ -631,5 +824,4 @@ class BaseEnvironment(interface.Environment):
|
|
|
631
824
|
error: BaseException | None = None) -> None:
|
|
632
825
|
"""Called when the environment is shutdown."""
|
|
633
826
|
lifetime = (time.time() - self.start_time) if self.start_time else 0.0
|
|
634
|
-
|
|
635
|
-
handler.on_environment_shutdown(self, duration, lifetime, error)
|
|
827
|
+
self.event_handler.on_environment_shutdown(self, duration, lifetime, error)
|