langfun 0.1.2.dev202510310805__py3-none-any.whl → 0.1.2.dev202511020804__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/eval/v2/runners_test.py +3 -0
- langfun/env/__init__.py +5 -3
- langfun/env/base_environment.py +74 -24
- langfun/env/base_environment_test.py +473 -0
- langfun/env/base_feature.py +77 -15
- langfun/env/base_feature_test.py +228 -0
- langfun/env/base_sandbox.py +40 -124
- langfun/env/{base_test.py → base_sandbox_test.py} +274 -730
- 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 +19 -19
- langfun/env/event_handlers/metric_writer.py +193 -157
- langfun/env/event_handlers/metric_writer_test.py +1 -1
- langfun/env/interface.py +677 -47
- langfun/env/interface_test.py +30 -1
- langfun/env/test_utils.py +111 -82
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/METADATA +1 -1
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/RECORD +23 -20
- langfun/env/event_handlers/base.py +0 -350
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/top_level.txt +0 -0
langfun/env/interface.py
CHANGED
|
@@ -169,7 +169,176 @@ class SessionTeardownError(SandboxError):
|
|
|
169
169
|
|
|
170
170
|
|
|
171
171
|
class Environment(pg.Object):
|
|
172
|
-
"""Base class for an environment.
|
|
172
|
+
"""Base class for an environment.
|
|
173
|
+
|
|
174
|
+
An **Environment** is the central component for managing sandboxes and
|
|
175
|
+
**Features**. It acts as an abstraction layer, hiding the implementation
|
|
176
|
+
details of the underlying container/sandboxing system
|
|
177
|
+
(e.g., Docker, virtual machines).
|
|
178
|
+
|
|
179
|
+
The core goal is to enable the development of features that are **agnostic**
|
|
180
|
+
to how or where they are executed (sandboxed or not).
|
|
181
|
+
|
|
182
|
+
-----------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
## Core Concepts
|
|
185
|
+
|
|
186
|
+
1. **Sandbox-Based Features:** Features that require isolated execution
|
|
187
|
+
contexts (sandboxes). They become available as properties of the sandbox
|
|
188
|
+
object (e.g., `sandbox.feature1`). Applicability is determined by an image
|
|
189
|
+
ID regex.
|
|
190
|
+
2. **Non-Sandbox-Based Features:** Features that run directly within the host
|
|
191
|
+
process or manage their own execution context outside of the Environment's
|
|
192
|
+
managed sandboxes. They are accessed directly via the Environment (e.g.,
|
|
193
|
+
`env.feature3()`).
|
|
194
|
+
|
|
195
|
+
How to Use:
|
|
196
|
+
|
|
197
|
+
The primary usage patterns are creating a **sandbox session** or directly
|
|
198
|
+
accessing a specific **Feature**, which transparently handles sandbox creation
|
|
199
|
+
if needed.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
env = MyEnvironment(
|
|
203
|
+
image_ids=['image1', 'image2'],
|
|
204
|
+
features={
|
|
205
|
+
'feature1': Feature1(applicable_images=['image1.*']),
|
|
206
|
+
'feature2': Feature2(applicable_images=['image2.*']),
|
|
207
|
+
'feature3': Feature3(is_sandbox_based=False)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
# Context manager for the Environment lifetime.
|
|
211
|
+
with env:
|
|
212
|
+
|
|
213
|
+
# 1. Access a specific sandbox directly.
|
|
214
|
+
# Upon exiting the context, the sandbox will be shutdown or returned to
|
|
215
|
+
# the pool for reuse.
|
|
216
|
+
with env.sandbox(image_id='image1') as sandbox:
|
|
217
|
+
# Execute a shell command inside the sandbox.
|
|
218
|
+
sandbox.shell('echo "hello world"')
|
|
219
|
+
|
|
220
|
+
# Access a sandbox-based feature (feature1 is applicable to image1).
|
|
221
|
+
sandbox.feature1.feature_method()
|
|
222
|
+
|
|
223
|
+
# Attempts to access inapplicable features will raise AttributeError:
|
|
224
|
+
# sandbox.feature2 # Not applicable to image1.
|
|
225
|
+
# sandbox.feature3 # Not sandbox-based.
|
|
226
|
+
|
|
227
|
+
# 2. Access a sandbox-based feature and let the Environment manage the
|
|
228
|
+
# sandbox. A suitable sandbox (e.g., one built from an image matching
|
|
229
|
+
# 'image1.*') will be provisioned, and the feature instance will be yielded.
|
|
230
|
+
with env.feature1() as feature1:
|
|
231
|
+
feature1.feature_method()
|
|
232
|
+
|
|
233
|
+
# 3. Access a non-sandbox-based feature.
|
|
234
|
+
with env.feature3() as feature3:
|
|
235
|
+
feature3.feature_method()
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
-----------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
## Multi-tenancy and Pooling
|
|
241
|
+
|
|
242
|
+
The Environment supports multi-tenancy (working with multiple image types) and
|
|
243
|
+
pooling (reusing sandboxes) to amortize setup costs across different user
|
|
244
|
+
requests. Pooling is configured via the pool_size parameter.
|
|
245
|
+
|
|
246
|
+
| pool_size Value | Behavior |
|
|
247
|
+
|---------------------------|------------------------------------------------|
|
|
248
|
+
| 0 or (0, 0) | No pooling. Sandboxes are created and shut down|
|
|
249
|
+
| | on demand (useful for local development). |
|
|
250
|
+
|---------------------------|------------------------------------------------|
|
|
251
|
+
| (MIN, MAX) tuple | Global Pool: Applies the same minimum and |
|
|
252
|
+
| | maximum pool size to sandboxes created from all|
|
|
253
|
+
| | specified images. |
|
|
254
|
+
|---------------------------|------------------------------------------------|
|
|
255
|
+
| {image_regex: (MIN, MAX)} | Per-Image Pool: Allows customizing pool |
|
|
256
|
+
| | settings based on image ID regular expressions.|
|
|
257
|
+
| | (e.g., 'image1.*': (64, 256), '.*': (0, 256)). |
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
**Example 1: No Pooling (pool_size=0)**
|
|
261
|
+
Sandboxes are created and shutdown immediately upon session end.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
env = MyEnvironment(image_ids=['image1', 'image2'], pool_size=0)
|
|
265
|
+
|
|
266
|
+
# Sandbox created and shutdown on demand.
|
|
267
|
+
with env.sandbox(image_id='image1') as sandbox1:
|
|
268
|
+
...
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Exaxmple 2: Global Pooling (pool_size=(0, 256))**
|
|
272
|
+
Up to 256 sandboxes will be created and pooled across both images as needed.
|
|
273
|
+
None are created initially.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
env = MyEnvironment(
|
|
277
|
+
image_ids=['image1', 'image2'],
|
|
278
|
+
pool_size=(0, 256)
|
|
279
|
+
)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Example 3: Per-Image Custom Pooling**:
|
|
283
|
+
For images matching 'image1.*': 64 sandboxes are pre-created (MIN=64) and
|
|
284
|
+
pooled, up to a MAX of 256.
|
|
285
|
+
For all other images ('.*'): Sandboxes are created and pooled on demand
|
|
286
|
+
(MIN=0), up to a MAX of 256.
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
env = MyEnvironment(
|
|
290
|
+
image_ids=['image1', 'image2'],
|
|
291
|
+
pool_size={
|
|
292
|
+
'image1.*': (64, 256),
|
|
293
|
+
'.*': (0, 256),
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Handling Sandbox Failures
|
|
299
|
+
|
|
300
|
+
Sandboxes often run in distributed, ephemeral environments and must be treated
|
|
301
|
+
as fault-tolerant. Langfun provides a protocol for handling unexpected sandbox
|
|
302
|
+
state issues.
|
|
303
|
+
|
|
304
|
+
### Communicating Errors
|
|
305
|
+
If a feature encounters an unexpected state in its sandbox (e.g., a process
|
|
306
|
+
died), it should raise `lf.env.SandboxStateError`.
|
|
307
|
+
|
|
308
|
+
The sandbox will be automatically shut down when its context manager exits.
|
|
309
|
+
The Environment handles replacement and ensures future requests are routed to
|
|
310
|
+
healthy sandboxes.
|
|
311
|
+
|
|
312
|
+
For example:
|
|
313
|
+
```
|
|
314
|
+
with env:
|
|
315
|
+
with env.sandbox() as sb:
|
|
316
|
+
# If SandboxStateError is raised within this block, the sandbox
|
|
317
|
+
# will be forcefully shut down upon block exit.
|
|
318
|
+
sb.shell('echo hi')
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Robust User Code
|
|
322
|
+
A simple strategy for robust user code is to wrap critical operations in a
|
|
323
|
+
retry loop:
|
|
324
|
+
```
|
|
325
|
+
while True:
|
|
326
|
+
try:
|
|
327
|
+
result = do_something_that_involves_sandbox()
|
|
328
|
+
break # Success!
|
|
329
|
+
except lf.env.SandboxStateError:
|
|
330
|
+
# The sandbox failed; a new, healthy one will be provisioned on the next
|
|
331
|
+
# iteration.
|
|
332
|
+
# Wait briefly to avoid resource thrashing.
|
|
333
|
+
time.sleep(1)
|
|
334
|
+
except lf.env.EnvironmentOutageError:
|
|
335
|
+
# If the Environment is down for too long
|
|
336
|
+
# (past BaseEnvironment.outage_grace_period)
|
|
337
|
+
# and cannot provision a healthy replacement, this error is raised.
|
|
338
|
+
# The retry loop should be broken or an outer failure reported.
|
|
339
|
+
raise
|
|
340
|
+
```
|
|
341
|
+
"""
|
|
173
342
|
|
|
174
343
|
# Disable symbolic comparison and hashing for environment objects.
|
|
175
344
|
use_symbolic_comparison = False
|
|
@@ -238,6 +407,17 @@ class Environment(pg.Object):
|
|
|
238
407
|
f'No image ID found for feature {feature.name} in {self.image_ids}.'
|
|
239
408
|
)
|
|
240
409
|
|
|
410
|
+
def non_sandbox_based_features(self) -> Iterator['Feature']:
|
|
411
|
+
"""Returns non-sandbox-based features."""
|
|
412
|
+
for feature in self.features.values():
|
|
413
|
+
if not feature.is_sandbox_based:
|
|
414
|
+
yield feature
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
@abc.abstractmethod
|
|
418
|
+
def event_handler(self) -> 'EventHandler':
|
|
419
|
+
"""Returns the event handler for the environment."""
|
|
420
|
+
|
|
241
421
|
@property
|
|
242
422
|
@abc.abstractmethod
|
|
243
423
|
def status(self) -> Status:
|
|
@@ -306,10 +486,6 @@ class Environment(pg.Object):
|
|
|
306
486
|
Environment._ENV_STACK.pop()
|
|
307
487
|
self.shutdown()
|
|
308
488
|
|
|
309
|
-
def __del__(self):
|
|
310
|
-
"""Deletes the environment."""
|
|
311
|
-
self.shutdown()
|
|
312
|
-
|
|
313
489
|
@classmethod
|
|
314
490
|
def current(cls) -> Optional['Environment']:
|
|
315
491
|
"""Returns the current environment."""
|
|
@@ -323,11 +499,13 @@ class Environment(pg.Object):
|
|
|
323
499
|
|
|
324
500
|
def sandbox(
|
|
325
501
|
self,
|
|
326
|
-
image_id: str | None = None,
|
|
327
502
|
session_id: str | None = None,
|
|
503
|
+
image_id: str | None = None,
|
|
328
504
|
) -> ContextManager['Sandbox']:
|
|
329
505
|
"""Gets a sandbox from the environment and starts a new user session."""
|
|
330
|
-
return self.acquire(image_id=image_id).new_session(
|
|
506
|
+
return self.acquire(image_id=image_id).new_session(
|
|
507
|
+
session_id or self.new_session_id()
|
|
508
|
+
)
|
|
331
509
|
|
|
332
510
|
def __getattr__(self, name: str) -> Any:
|
|
333
511
|
"""Gets a feature session from a free sandbox from the environment.
|
|
@@ -355,13 +533,14 @@ class Environment(pg.Object):
|
|
|
355
533
|
|
|
356
534
|
|
|
357
535
|
@contextlib.contextmanager
|
|
358
|
-
def
|
|
536
|
+
def _sandbox_session_for_feature(
|
|
359
537
|
environment: Environment,
|
|
360
538
|
feature: 'Feature',
|
|
361
539
|
image_id: str | None = None,
|
|
362
540
|
session_id: str | None = None,
|
|
363
541
|
) -> Iterator['Feature']:
|
|
364
542
|
"""Returns a context manager for a session for a feature."""
|
|
543
|
+
assert feature.is_sandbox_based
|
|
365
544
|
if image_id is None:
|
|
366
545
|
image_id = environment.image_id_for(feature)
|
|
367
546
|
elif not feature.is_applicable(image_id):
|
|
@@ -369,14 +548,25 @@ def _session_for_feature(
|
|
|
369
548
|
f'Feature {feature.name!r} is not applicable to image {image_id!r}.'
|
|
370
549
|
)
|
|
371
550
|
sandbox = environment.acquire(image_id=image_id)
|
|
372
|
-
with sandbox.new_session(
|
|
551
|
+
with sandbox.new_session(
|
|
552
|
+
session_id=session_id or environment.new_session_id(feature.name)
|
|
553
|
+
):
|
|
373
554
|
yield sandbox.features[feature.name]
|
|
374
555
|
|
|
375
556
|
|
|
376
557
|
def _feature_session_creator(environment: Environment, feature: 'Feature'):
|
|
377
558
|
"""Returns a callable that returns a context manager for a feature session."""
|
|
378
|
-
def fn(
|
|
379
|
-
|
|
559
|
+
def fn(session_id: str | None = None, image_id: str | None = None):
|
|
560
|
+
if feature.is_sandbox_based:
|
|
561
|
+
return _sandbox_session_for_feature(
|
|
562
|
+
environment, feature, image_id, session_id
|
|
563
|
+
)
|
|
564
|
+
assert image_id is None, (
|
|
565
|
+
'Non-sandbox based feature does not support image ID.'
|
|
566
|
+
)
|
|
567
|
+
return feature.new_session(
|
|
568
|
+
session_id or environment.new_session_id(feature.name)
|
|
569
|
+
)
|
|
380
570
|
return fn
|
|
381
571
|
|
|
382
572
|
|
|
@@ -385,7 +575,12 @@ pg.typing.register_converter(str, Environment.Id, Environment.Id)
|
|
|
385
575
|
|
|
386
576
|
|
|
387
577
|
class Sandbox(pg.Object):
|
|
388
|
-
"""Interface for sandboxes.
|
|
578
|
+
"""Interface for sandboxes.
|
|
579
|
+
|
|
580
|
+
A sandbox is a container that runs a single image with a set of features.
|
|
581
|
+
It will be brought up by the environment, setup the features, fullfill user
|
|
582
|
+
requests, and then tear down features and finally the sandbox itself.
|
|
583
|
+
"""
|
|
389
584
|
|
|
390
585
|
# Disable symbolic comparison and hashing for sandbox objects.
|
|
391
586
|
use_symbolic_comparison = False
|
|
@@ -694,14 +889,12 @@ class Sandbox(pg.Object):
|
|
|
694
889
|
def track_activity(
|
|
695
890
|
self,
|
|
696
891
|
name: str,
|
|
697
|
-
feature: Optional['Feature'] = None,
|
|
698
892
|
**kwargs: Any
|
|
699
893
|
) -> ContextManager[None]:
|
|
700
894
|
"""Context manager that tracks a sandbox activity.
|
|
701
895
|
|
|
702
896
|
Args:
|
|
703
897
|
name: The name of the activity.
|
|
704
|
-
feature: The feature that the activity is associated with.
|
|
705
898
|
**kwargs: Additional keyword arguments to pass to the activity handler.
|
|
706
899
|
|
|
707
900
|
Returns:
|
|
@@ -716,12 +909,7 @@ class Sandbox(pg.Object):
|
|
|
716
909
|
#
|
|
717
910
|
|
|
718
911
|
@contextlib.contextmanager
|
|
719
|
-
def new_session(
|
|
720
|
-
self,
|
|
721
|
-
session_id: str | None = None,
|
|
722
|
-
*,
|
|
723
|
-
feature_hint: str | None = None,
|
|
724
|
-
) -> Iterator['Sandbox']:
|
|
912
|
+
def new_session(self, session_id: str) -> Iterator['Sandbox']:
|
|
725
913
|
"""Context manager for obtaining a sandbox for a user session.
|
|
726
914
|
|
|
727
915
|
State transitions:
|
|
@@ -729,11 +917,7 @@ class Sandbox(pg.Object):
|
|
|
729
917
|
ACQUIRED -> IN_SESSINO -> OFFLINE: When session setup or teardown fails.
|
|
730
918
|
|
|
731
919
|
Args:
|
|
732
|
-
session_id: The identifier for the user session.
|
|
733
|
-
ID will be generated.
|
|
734
|
-
feature_hint: A hint of which feature is the main user intent when
|
|
735
|
-
starting the session. This is used for generating the session id if
|
|
736
|
-
`session_id` is not provided.
|
|
920
|
+
session_id: The identifier for the user session.
|
|
737
921
|
|
|
738
922
|
Yields:
|
|
739
923
|
The sandbox for the user session.
|
|
@@ -743,8 +927,6 @@ class Sandbox(pg.Object):
|
|
|
743
927
|
BaseException: If session setup or teardown failed with user-defined
|
|
744
928
|
errors.
|
|
745
929
|
"""
|
|
746
|
-
if session_id is None:
|
|
747
|
-
session_id = self.environment.new_session_id(feature_hint)
|
|
748
930
|
self.start_session(session_id)
|
|
749
931
|
try:
|
|
750
932
|
yield self
|
|
@@ -776,13 +958,63 @@ class Sandbox(pg.Object):
|
|
|
776
958
|
return self.features[name]
|
|
777
959
|
raise AttributeError(name)
|
|
778
960
|
|
|
779
|
-
def __del__(self):
|
|
780
|
-
"""Deletes the sandbox."""
|
|
781
|
-
self.shutdown()
|
|
782
|
-
|
|
783
961
|
|
|
784
962
|
class Feature(pg.Object):
|
|
785
|
-
"""Interface for
|
|
963
|
+
"""Interface for features that run in a Langfun environment.
|
|
964
|
+
|
|
965
|
+
There are two type of features: sandbox-based and non-sandbox-based.
|
|
966
|
+
Sandbox-based features run in a sandbox, which is emulated in a separate
|
|
967
|
+
process. Non-sandbox-based features do not run in a sandbox.
|
|
968
|
+
|
|
969
|
+
Features can be directly accessed through the environment, for example:
|
|
970
|
+
|
|
971
|
+
```python
|
|
972
|
+
env = MyEnvironment(
|
|
973
|
+
feature={
|
|
974
|
+
'feature1': SandboxBasedFeature(),
|
|
975
|
+
'feature2': NonSandboxBasedFeature(),
|
|
976
|
+
}
|
|
977
|
+
)
|
|
978
|
+
# Start the environment.
|
|
979
|
+
with env:
|
|
980
|
+
# Access feature1, which involves acquiring a sandbox and return the feature
|
|
981
|
+
# associated with the sandbox.
|
|
982
|
+
with env.feature1() as f1:
|
|
983
|
+
f1.feature_method()
|
|
984
|
+
|
|
985
|
+
# Access feature2, which does not involve acquiring a sandbox.
|
|
986
|
+
with env.feature2() as f2:
|
|
987
|
+
f2.feature_method()
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
Sandbox-based features can also be accessed through the sandbox, for example:
|
|
991
|
+
|
|
992
|
+
```python
|
|
993
|
+
with env.sandbox('session1') as sb:
|
|
994
|
+
# Access feature1 within the sandbox.
|
|
995
|
+
sb.feature1.feature_method()
|
|
996
|
+
|
|
997
|
+
# Attribute error.
|
|
998
|
+
sb.feature2
|
|
999
|
+
```
|
|
1000
|
+
"""
|
|
1001
|
+
|
|
1002
|
+
@dataclasses.dataclass
|
|
1003
|
+
class Id:
|
|
1004
|
+
container_id: Environment.Id | Sandbox.Id
|
|
1005
|
+
feature_name: str
|
|
1006
|
+
|
|
1007
|
+
def __str__(self) -> str:
|
|
1008
|
+
return f'{self.container_id}/{self.feature_name}'
|
|
1009
|
+
|
|
1010
|
+
def working_dir(self, root_dir: str | None) -> str | None:
|
|
1011
|
+
"""Returns the working directory for the feature."""
|
|
1012
|
+
if root_dir is None:
|
|
1013
|
+
return None
|
|
1014
|
+
return os.path.join(
|
|
1015
|
+
self.container_id.working_dir(root_dir),
|
|
1016
|
+
_make_path_compatible(self.feature_name)
|
|
1017
|
+
)
|
|
786
1018
|
|
|
787
1019
|
# Disable symbolic comparison and hashing for sandbox objects.
|
|
788
1020
|
allow_symbolic_comparison = False
|
|
@@ -792,16 +1024,31 @@ class Feature(pg.Object):
|
|
|
792
1024
|
def name(self) -> str:
|
|
793
1025
|
"""Name of the feature, which will be used as key to access the feature."""
|
|
794
1026
|
|
|
1027
|
+
@functools.cached_property
|
|
1028
|
+
def id(self) -> Id:
|
|
1029
|
+
"""Returns the identifier of the feature."""
|
|
1030
|
+
if self.is_sandbox_based:
|
|
1031
|
+
return Feature.Id(self.sandbox.id, self.name)
|
|
1032
|
+
return Feature.Id(self.environment.id, self.name)
|
|
1033
|
+
|
|
795
1034
|
@property
|
|
796
1035
|
@abc.abstractmethod
|
|
797
|
-
def
|
|
1036
|
+
def environment(self) -> Environment:
|
|
1037
|
+
"""Returns the environment that the feature is running in."""
|
|
1038
|
+
|
|
1039
|
+
@property
|
|
1040
|
+
@abc.abstractmethod
|
|
1041
|
+
def is_sandbox_based(self) -> bool:
|
|
1042
|
+
"""Returns True if the feature is sandbox-based."""
|
|
1043
|
+
|
|
1044
|
+
@property
|
|
1045
|
+
@abc.abstractmethod
|
|
1046
|
+
def sandbox(self) -> Sandbox | None:
|
|
798
1047
|
"""Returns the sandbox that the feature is running in.
|
|
799
1048
|
|
|
800
1049
|
Returns:
|
|
801
|
-
The sandbox that the feature is running in.
|
|
802
|
-
|
|
803
|
-
Raises:
|
|
804
|
-
AssertError: If the feature is not set up with a sandbox yet.
|
|
1050
|
+
The sandbox that the feature is running in. None if the feature is not
|
|
1051
|
+
sandbox-based or not yet bound with a sandbox.
|
|
805
1052
|
"""
|
|
806
1053
|
|
|
807
1054
|
@abc.abstractmethod
|
|
@@ -809,8 +1056,17 @@ class Feature(pg.Object):
|
|
|
809
1056
|
"""Returns True if the feature is applicable to the given image."""
|
|
810
1057
|
|
|
811
1058
|
@abc.abstractmethod
|
|
812
|
-
def setup(self, sandbox: Sandbox) -> None:
|
|
813
|
-
"""Sets up the feature
|
|
1059
|
+
def setup(self, sandbox: Sandbox | None = None) -> None:
|
|
1060
|
+
"""Sets up the feature.
|
|
1061
|
+
|
|
1062
|
+
For sandbox-based features, the setup will be called when a sandbox is
|
|
1063
|
+
started for the first time.
|
|
1064
|
+
|
|
1065
|
+
For non-sandbox-based features, the setup will be called when the
|
|
1066
|
+
environment starts.
|
|
1067
|
+
|
|
1068
|
+
When a feature's `setup` is called, its `teardown` is guaranteed to be
|
|
1069
|
+
called.
|
|
814
1070
|
|
|
815
1071
|
State transitions:
|
|
816
1072
|
SETTING_UP -> READY: When setup succeeds.
|
|
@@ -913,17 +1169,54 @@ class Feature(pg.Object):
|
|
|
913
1169
|
will not be housekeeping.
|
|
914
1170
|
"""
|
|
915
1171
|
|
|
1172
|
+
@abc.abstractmethod
|
|
1173
|
+
def track_activity(
|
|
1174
|
+
self,
|
|
1175
|
+
name: str,
|
|
1176
|
+
**kwargs: Any
|
|
1177
|
+
) -> ContextManager[None]:
|
|
1178
|
+
"""Context manager that tracks a feature activity.
|
|
1179
|
+
|
|
1180
|
+
Args:
|
|
1181
|
+
name: The name of the activity.
|
|
1182
|
+
**kwargs: Additional keyword arguments to pass to the activity handler.
|
|
1183
|
+
|
|
1184
|
+
Returns:
|
|
1185
|
+
A context manager that tracks the activity, including duration and error.
|
|
1186
|
+
"""
|
|
1187
|
+
|
|
916
1188
|
@property
|
|
917
1189
|
def session_id(self) -> str | None:
|
|
918
1190
|
"""Returns the current user session identifier."""
|
|
919
|
-
|
|
920
|
-
|
|
1191
|
+
if self.is_sandbox_based:
|
|
1192
|
+
return self.sandbox.session_id
|
|
1193
|
+
return self._non_sandbox_based_session_id
|
|
921
1194
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1195
|
+
@contextlib.contextmanager
|
|
1196
|
+
def new_session(self, session_id: str) -> Iterator['Feature']:
|
|
1197
|
+
"""Context manager for obtaining a non-sandbox-based feature session."""
|
|
1198
|
+
assert not self.is_sandbox_based, (
|
|
1199
|
+
'Applicable only to non-sandbox-based features. '
|
|
1200
|
+
'For sandbox-based features, use `Sandbox.new_session` instead.'
|
|
926
1201
|
)
|
|
1202
|
+
try:
|
|
1203
|
+
self._non_sandbox_based_session_id = session_id
|
|
1204
|
+
self.setup_session()
|
|
1205
|
+
yield self
|
|
1206
|
+
finally:
|
|
1207
|
+
try:
|
|
1208
|
+
# Since the session is ended, we don't want to raise any errors during
|
|
1209
|
+
# session teardown to the user. So we catch all exceptions here.
|
|
1210
|
+
# However the event handler will still be notified and log the error.
|
|
1211
|
+
self.teardown_session()
|
|
1212
|
+
except BaseException: # pylint: disable=broad-except
|
|
1213
|
+
pass
|
|
1214
|
+
self._non_sandbox_based_session_id = None
|
|
1215
|
+
|
|
1216
|
+
def _on_bound(self) -> None:
|
|
1217
|
+
"""Called when the feature is bound to a sandbox."""
|
|
1218
|
+
super()._on_bound()
|
|
1219
|
+
self._non_sandbox_based_session_id = None
|
|
927
1220
|
|
|
928
1221
|
|
|
929
1222
|
def _make_path_compatible(id_str: str) -> str:
|
|
@@ -980,8 +1273,8 @@ def treat_as_sandbox_state_error(
|
|
|
980
1273
|
return decorator
|
|
981
1274
|
|
|
982
1275
|
|
|
983
|
-
def
|
|
984
|
-
"""Decorator for Sandbox/Feature methods to log sandbox activity."""
|
|
1276
|
+
def log_activity(name: str | None = None):
|
|
1277
|
+
"""Decorator for Sandbox/Feature methods to log sandbox/feature activity."""
|
|
985
1278
|
|
|
986
1279
|
def decorator(func):
|
|
987
1280
|
signature = pg.typing.get_signature(func)
|
|
@@ -1008,3 +1301,340 @@ def log_sandbox_activity(name: str | None = None):
|
|
|
1008
1301
|
return func(self, *args, **kwargs)
|
|
1009
1302
|
return method_wrapper
|
|
1010
1303
|
return decorator
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
#
|
|
1307
|
+
# Interface for event handlers.
|
|
1308
|
+
#
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
class _EnvironmentEventHandler:
|
|
1312
|
+
"""Base class for event handlers of an environment."""
|
|
1313
|
+
|
|
1314
|
+
def on_environment_starting(self, environment: Environment) -> None:
|
|
1315
|
+
"""Called when the environment is getting started.
|
|
1316
|
+
|
|
1317
|
+
Args:
|
|
1318
|
+
environment: The environment.
|
|
1319
|
+
"""
|
|
1320
|
+
|
|
1321
|
+
def on_environment_start(
|
|
1322
|
+
self,
|
|
1323
|
+
environment: Environment,
|
|
1324
|
+
duration: float,
|
|
1325
|
+
error: BaseException | None
|
|
1326
|
+
) -> None:
|
|
1327
|
+
"""Called when the environment is started.
|
|
1328
|
+
|
|
1329
|
+
Args:
|
|
1330
|
+
environment: The environment.
|
|
1331
|
+
duration: The environment start duration in seconds.
|
|
1332
|
+
error: The error that failed the environment start. If None, the
|
|
1333
|
+
environment started normally.
|
|
1334
|
+
"""
|
|
1335
|
+
|
|
1336
|
+
def on_environment_housekeep(
|
|
1337
|
+
self,
|
|
1338
|
+
environment: Environment,
|
|
1339
|
+
counter: int,
|
|
1340
|
+
duration: float,
|
|
1341
|
+
error: BaseException | None,
|
|
1342
|
+
**kwargs
|
|
1343
|
+
) -> None:
|
|
1344
|
+
"""Called when the environment finishes a round of housekeeping.
|
|
1345
|
+
|
|
1346
|
+
Args:
|
|
1347
|
+
environment: The environment.
|
|
1348
|
+
counter: Zero-based counter of the housekeeping round.
|
|
1349
|
+
duration: The environment start duration in seconds.
|
|
1350
|
+
error: The error that failed the housekeeping. If None, the
|
|
1351
|
+
housekeeping succeeded.
|
|
1352
|
+
**kwargs: Environment-specific properties computed during housekeeping.
|
|
1353
|
+
"""
|
|
1354
|
+
|
|
1355
|
+
def on_environment_shutting_down(
|
|
1356
|
+
self,
|
|
1357
|
+
environment: Environment,
|
|
1358
|
+
offline_duration: float,
|
|
1359
|
+
) -> None:
|
|
1360
|
+
"""Called when the environment is shutting down.
|
|
1361
|
+
|
|
1362
|
+
Args:
|
|
1363
|
+
environment: The environment.
|
|
1364
|
+
offline_duration: The environment offline duration in seconds.
|
|
1365
|
+
"""
|
|
1366
|
+
|
|
1367
|
+
def on_environment_shutdown(
|
|
1368
|
+
self,
|
|
1369
|
+
environment: Environment,
|
|
1370
|
+
duration: float,
|
|
1371
|
+
lifetime: float,
|
|
1372
|
+
error: BaseException | None
|
|
1373
|
+
) -> None:
|
|
1374
|
+
"""Called when the environment is shutdown.
|
|
1375
|
+
|
|
1376
|
+
Args:
|
|
1377
|
+
environment: The environment.
|
|
1378
|
+
duration: The environment shutdown duration in seconds.
|
|
1379
|
+
lifetime: The environment lifetime in seconds.
|
|
1380
|
+
error: The error that caused the environment to shutdown. If None, the
|
|
1381
|
+
environment shutdown normally.
|
|
1382
|
+
"""
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
class _SandboxEventHandler:
|
|
1386
|
+
"""Base class for sandbox event handlers."""
|
|
1387
|
+
|
|
1388
|
+
def on_sandbox_start(
|
|
1389
|
+
self,
|
|
1390
|
+
sandbox: Sandbox,
|
|
1391
|
+
duration: float,
|
|
1392
|
+
error: BaseException | None
|
|
1393
|
+
) -> None:
|
|
1394
|
+
"""Called when a sandbox is started.
|
|
1395
|
+
|
|
1396
|
+
Args:
|
|
1397
|
+
sandbox: The sandbox.
|
|
1398
|
+
duration: The time spent on starting the sandbox.
|
|
1399
|
+
error: The error that caused the sandbox to start. If None, the sandbox
|
|
1400
|
+
started normally.
|
|
1401
|
+
"""
|
|
1402
|
+
|
|
1403
|
+
def on_sandbox_status_change(
|
|
1404
|
+
self,
|
|
1405
|
+
sandbox: Sandbox,
|
|
1406
|
+
old_status: 'Sandbox.Status',
|
|
1407
|
+
new_status: 'Sandbox.Status',
|
|
1408
|
+
span: float,
|
|
1409
|
+
) -> None:
|
|
1410
|
+
"""Called when a sandbox status changes.
|
|
1411
|
+
|
|
1412
|
+
Args:
|
|
1413
|
+
sandbox: The sandbox.
|
|
1414
|
+
old_status: The old sandbox status.
|
|
1415
|
+
new_status: The new sandbox status.
|
|
1416
|
+
span: Time spent on the old status in seconds.
|
|
1417
|
+
"""
|
|
1418
|
+
|
|
1419
|
+
def on_sandbox_shutdown(
|
|
1420
|
+
self,
|
|
1421
|
+
sandbox: Sandbox,
|
|
1422
|
+
duration: float,
|
|
1423
|
+
lifetime: float,
|
|
1424
|
+
error: BaseException | None
|
|
1425
|
+
) -> None:
|
|
1426
|
+
"""Called when a sandbox is shutdown.
|
|
1427
|
+
|
|
1428
|
+
Args:
|
|
1429
|
+
sandbox: The sandbox.
|
|
1430
|
+
duration: The time spent on shutting down the sandbox.
|
|
1431
|
+
lifetime: The sandbox lifetime in seconds.
|
|
1432
|
+
error: The error that caused the sandbox to shutdown. If None, the
|
|
1433
|
+
sandbox shutdown normally.
|
|
1434
|
+
"""
|
|
1435
|
+
|
|
1436
|
+
def on_sandbox_session_start(
|
|
1437
|
+
self,
|
|
1438
|
+
sandbox: Sandbox,
|
|
1439
|
+
session_id: str,
|
|
1440
|
+
duration: float,
|
|
1441
|
+
error: BaseException | None
|
|
1442
|
+
) -> None:
|
|
1443
|
+
"""Called when a sandbox session starts.
|
|
1444
|
+
|
|
1445
|
+
Args:
|
|
1446
|
+
sandbox: The sandbox.
|
|
1447
|
+
session_id: The session ID.
|
|
1448
|
+
duration: The time spent on starting the session.
|
|
1449
|
+
error: The error that caused the session to start. If None, the session
|
|
1450
|
+
started normally.
|
|
1451
|
+
"""
|
|
1452
|
+
|
|
1453
|
+
def on_sandbox_session_end(
|
|
1454
|
+
self,
|
|
1455
|
+
sandbox: Sandbox,
|
|
1456
|
+
session_id: str,
|
|
1457
|
+
duration: float,
|
|
1458
|
+
lifetime: float,
|
|
1459
|
+
error: BaseException | None
|
|
1460
|
+
) -> None:
|
|
1461
|
+
"""Called when a sandbox session ends.
|
|
1462
|
+
|
|
1463
|
+
Args:
|
|
1464
|
+
sandbox: The sandbox.
|
|
1465
|
+
session_id: The session ID.
|
|
1466
|
+
duration: The time spent on ending the session.
|
|
1467
|
+
lifetime: The session lifetime in seconds.
|
|
1468
|
+
error: The error that caused the session to end. If None, the session
|
|
1469
|
+
ended normally.
|
|
1470
|
+
"""
|
|
1471
|
+
|
|
1472
|
+
def on_sandbox_activity(
|
|
1473
|
+
self,
|
|
1474
|
+
name: str,
|
|
1475
|
+
sandbox: Sandbox,
|
|
1476
|
+
session_id: str | None,
|
|
1477
|
+
duration: float,
|
|
1478
|
+
error: BaseException | None,
|
|
1479
|
+
**kwargs
|
|
1480
|
+
) -> None:
|
|
1481
|
+
"""Called when a sandbox activity is performed.
|
|
1482
|
+
|
|
1483
|
+
Args:
|
|
1484
|
+
name: The name of the sandbox activity.
|
|
1485
|
+
sandbox: The sandbox.
|
|
1486
|
+
session_id: The session ID.
|
|
1487
|
+
duration: The sandbox activity duration in seconds.
|
|
1488
|
+
error: The error that caused the sandbox activity to perform. If None,
|
|
1489
|
+
the sandbox activity performed normally.
|
|
1490
|
+
**kwargs: The keyword arguments of the sandbox activity.
|
|
1491
|
+
"""
|
|
1492
|
+
|
|
1493
|
+
def on_sandbox_housekeep(
|
|
1494
|
+
self,
|
|
1495
|
+
sandbox: Sandbox,
|
|
1496
|
+
counter: int,
|
|
1497
|
+
duration: float,
|
|
1498
|
+
error: BaseException | None,
|
|
1499
|
+
**kwargs
|
|
1500
|
+
) -> None:
|
|
1501
|
+
"""Called when a sandbox finishes a round of housekeeping.
|
|
1502
|
+
|
|
1503
|
+
Args:
|
|
1504
|
+
sandbox: The sandbox.
|
|
1505
|
+
counter: Zero-based counter of the housekeeping round.
|
|
1506
|
+
duration: The sandbox housekeeping duration in seconds.
|
|
1507
|
+
error: The error that caused the sandbox to housekeeping. If None, the
|
|
1508
|
+
sandbox housekeeping normally.
|
|
1509
|
+
**kwargs: Sandbox-specific properties computed during housekeeping.
|
|
1510
|
+
"""
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
class _FeatureEventHandler:
|
|
1514
|
+
"""Base class for feature event handlers."""
|
|
1515
|
+
|
|
1516
|
+
def on_feature_setup(
|
|
1517
|
+
self,
|
|
1518
|
+
feature: Feature,
|
|
1519
|
+
duration: float,
|
|
1520
|
+
error: BaseException | None
|
|
1521
|
+
) -> None:
|
|
1522
|
+
"""Called when a sandbox feature is setup.
|
|
1523
|
+
|
|
1524
|
+
Applicable to both sandbox-based and non-sandbox-based features.
|
|
1525
|
+
|
|
1526
|
+
Args:
|
|
1527
|
+
feature: The feature.
|
|
1528
|
+
duration: The feature setup duration in seconds.
|
|
1529
|
+
error: The error happened during the feature setup. If None,
|
|
1530
|
+
the feature setup performed normally.
|
|
1531
|
+
"""
|
|
1532
|
+
|
|
1533
|
+
def on_feature_teardown(
|
|
1534
|
+
self,
|
|
1535
|
+
feature: Feature,
|
|
1536
|
+
duration: float,
|
|
1537
|
+
error: BaseException | None
|
|
1538
|
+
) -> None:
|
|
1539
|
+
"""Called when a sandbox feature is teardown.
|
|
1540
|
+
|
|
1541
|
+
Applicable to both sandbox-based and non-sandbox-based features.
|
|
1542
|
+
|
|
1543
|
+
Args:
|
|
1544
|
+
feature: The feature.
|
|
1545
|
+
duration: The feature teardown duration in seconds.
|
|
1546
|
+
error: The error happened during the feature teardown. If None,
|
|
1547
|
+
the feature teardown performed normally.
|
|
1548
|
+
"""
|
|
1549
|
+
|
|
1550
|
+
def on_feature_teardown_session(
|
|
1551
|
+
self,
|
|
1552
|
+
feature: Feature,
|
|
1553
|
+
session_id: str,
|
|
1554
|
+
duration: float,
|
|
1555
|
+
error: BaseException | None
|
|
1556
|
+
) -> None:
|
|
1557
|
+
"""Called when a feature is teardown with a session.
|
|
1558
|
+
|
|
1559
|
+
Applicable to both sandbox-based and non-sandbox-based features.
|
|
1560
|
+
|
|
1561
|
+
Args:
|
|
1562
|
+
feature: The feature.
|
|
1563
|
+
session_id: The session ID.
|
|
1564
|
+
duration: The feature teardown session duration in seconds.
|
|
1565
|
+
error: The error happened during the feature teardown session. If
|
|
1566
|
+
None, the feature teardown session performed normally.
|
|
1567
|
+
"""
|
|
1568
|
+
|
|
1569
|
+
def on_feature_setup_session(
|
|
1570
|
+
self,
|
|
1571
|
+
feature: Feature,
|
|
1572
|
+
session_id: str | None,
|
|
1573
|
+
duration: float,
|
|
1574
|
+
error: BaseException | None,
|
|
1575
|
+
) -> None:
|
|
1576
|
+
"""Called when a feature is setup with a session.
|
|
1577
|
+
|
|
1578
|
+
Applicable to both sandbox-based and non-sandbox-based features.
|
|
1579
|
+
|
|
1580
|
+
Args:
|
|
1581
|
+
feature: The feature.
|
|
1582
|
+
session_id: The session ID.
|
|
1583
|
+
duration: The feature setup session duration in seconds.
|
|
1584
|
+
error: The error happened during the feature setup session. If
|
|
1585
|
+
None, the feature setup session performed normally.
|
|
1586
|
+
"""
|
|
1587
|
+
|
|
1588
|
+
def on_feature_activity(
|
|
1589
|
+
self,
|
|
1590
|
+
name: str,
|
|
1591
|
+
feature: Feature,
|
|
1592
|
+
session_id: str | None,
|
|
1593
|
+
duration: float,
|
|
1594
|
+
error: BaseException | None,
|
|
1595
|
+
**kwargs
|
|
1596
|
+
) -> None:
|
|
1597
|
+
"""Called when a feature activity is performed.
|
|
1598
|
+
|
|
1599
|
+
Applicable to both sandbox-based and non-sandbox-based features.
|
|
1600
|
+
|
|
1601
|
+
Args:
|
|
1602
|
+
name: The name of the feature activity.
|
|
1603
|
+
feature: The feature.
|
|
1604
|
+
session_id: The session ID. Session ID could be None if a feature
|
|
1605
|
+
activity is performed when setting up a session
|
|
1606
|
+
(e.g. BaseEnvironment.proactive_session_setup is on)
|
|
1607
|
+
duration: The feature activity duration in seconds.
|
|
1608
|
+
error: The error happened during the feature activity. If None,
|
|
1609
|
+
the feature activity performed normally.
|
|
1610
|
+
**kwargs: The keyword arguments of the feature activity.
|
|
1611
|
+
"""
|
|
1612
|
+
|
|
1613
|
+
def on_feature_housekeep(
|
|
1614
|
+
self,
|
|
1615
|
+
feature: Feature,
|
|
1616
|
+
counter: int,
|
|
1617
|
+
duration: float,
|
|
1618
|
+
error: BaseException | None,
|
|
1619
|
+
**kwargs,
|
|
1620
|
+
) -> None:
|
|
1621
|
+
"""Called when a sandbox feature is housekeeping.
|
|
1622
|
+
|
|
1623
|
+
Applicable to both sandbox-based and non-sandbox-based features.
|
|
1624
|
+
|
|
1625
|
+
Args:
|
|
1626
|
+
feature: The feature.
|
|
1627
|
+
counter: Zero-based counter of the housekeeping round.
|
|
1628
|
+
duration: The feature housekeeping duration in seconds.
|
|
1629
|
+
error: The error happened during the feature housekeeping. If None, the
|
|
1630
|
+
feature housekeeping normally.
|
|
1631
|
+
**kwargs: Feature-specific properties computed during housekeeping.
|
|
1632
|
+
"""
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
class EventHandler(
|
|
1636
|
+
_EnvironmentEventHandler,
|
|
1637
|
+
_SandboxEventHandler,
|
|
1638
|
+
_FeatureEventHandler,
|
|
1639
|
+
):
|
|
1640
|
+
"""Base class for langfun/env handlers."""
|