langfun 0.1.2.dev202511010804__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/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,12 @@ 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
+
241
416
  @property
242
417
  @abc.abstractmethod
243
418
  def event_handler(self) -> 'EventHandler':
@@ -324,11 +499,13 @@ class Environment(pg.Object):
324
499
 
325
500
  def sandbox(
326
501
  self,
327
- image_id: str | None = None,
328
502
  session_id: str | None = None,
503
+ image_id: str | None = None,
329
504
  ) -> ContextManager['Sandbox']:
330
505
  """Gets a sandbox from the environment and starts a new user session."""
331
- return self.acquire(image_id=image_id).new_session(session_id)
506
+ return self.acquire(image_id=image_id).new_session(
507
+ session_id or self.new_session_id()
508
+ )
332
509
 
333
510
  def __getattr__(self, name: str) -> Any:
334
511
  """Gets a feature session from a free sandbox from the environment.
@@ -356,13 +533,14 @@ class Environment(pg.Object):
356
533
 
357
534
 
358
535
  @contextlib.contextmanager
359
- def _session_for_feature(
536
+ def _sandbox_session_for_feature(
360
537
  environment: Environment,
361
538
  feature: 'Feature',
362
539
  image_id: str | None = None,
363
540
  session_id: str | None = None,
364
541
  ) -> Iterator['Feature']:
365
542
  """Returns a context manager for a session for a feature."""
543
+ assert feature.is_sandbox_based
366
544
  if image_id is None:
367
545
  image_id = environment.image_id_for(feature)
368
546
  elif not feature.is_applicable(image_id):
@@ -370,14 +548,25 @@ def _session_for_feature(
370
548
  f'Feature {feature.name!r} is not applicable to image {image_id!r}.'
371
549
  )
372
550
  sandbox = environment.acquire(image_id=image_id)
373
- with sandbox.new_session(session_id=session_id, feature_hint=feature.name):
551
+ with sandbox.new_session(
552
+ session_id=session_id or environment.new_session_id(feature.name)
553
+ ):
374
554
  yield sandbox.features[feature.name]
375
555
 
376
556
 
377
557
  def _feature_session_creator(environment: Environment, feature: 'Feature'):
378
558
  """Returns a callable that returns a context manager for a feature session."""
379
- def fn(image_id: str | None = None, session_id: str | None = None):
380
- return _session_for_feature(environment, feature, image_id, session_id)
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
+ )
381
570
  return fn
382
571
 
383
572
 
@@ -386,7 +575,12 @@ pg.typing.register_converter(str, Environment.Id, Environment.Id)
386
575
 
387
576
 
388
577
  class Sandbox(pg.Object):
389
- """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
+ """
390
584
 
391
585
  # Disable symbolic comparison and hashing for sandbox objects.
392
586
  use_symbolic_comparison = False
@@ -695,14 +889,12 @@ class Sandbox(pg.Object):
695
889
  def track_activity(
696
890
  self,
697
891
  name: str,
698
- feature: Optional['Feature'] = None,
699
892
  **kwargs: Any
700
893
  ) -> ContextManager[None]:
701
894
  """Context manager that tracks a sandbox activity.
702
895
 
703
896
  Args:
704
897
  name: The name of the activity.
705
- feature: The feature that the activity is associated with.
706
898
  **kwargs: Additional keyword arguments to pass to the activity handler.
707
899
 
708
900
  Returns:
@@ -717,12 +909,7 @@ class Sandbox(pg.Object):
717
909
  #
718
910
 
719
911
  @contextlib.contextmanager
720
- def new_session(
721
- self,
722
- session_id: str | None = None,
723
- *,
724
- feature_hint: str | None = None,
725
- ) -> Iterator['Sandbox']:
912
+ def new_session(self, session_id: str) -> Iterator['Sandbox']:
726
913
  """Context manager for obtaining a sandbox for a user session.
727
914
 
728
915
  State transitions:
@@ -730,11 +917,7 @@ class Sandbox(pg.Object):
730
917
  ACQUIRED -> IN_SESSINO -> OFFLINE: When session setup or teardown fails.
731
918
 
732
919
  Args:
733
- session_id: The identifier for the user session. If not provided, a random
734
- ID will be generated.
735
- feature_hint: A hint of which feature is the main user intent when
736
- starting the session. This is used for generating the session id if
737
- `session_id` is not provided.
920
+ session_id: The identifier for the user session.
738
921
 
739
922
  Yields:
740
923
  The sandbox for the user session.
@@ -744,8 +927,6 @@ class Sandbox(pg.Object):
744
927
  BaseException: If session setup or teardown failed with user-defined
745
928
  errors.
746
929
  """
747
- if session_id is None:
748
- session_id = self.environment.new_session_id(feature_hint)
749
930
  self.start_session(session_id)
750
931
  try:
751
932
  yield self
@@ -779,7 +960,61 @@ class Sandbox(pg.Object):
779
960
 
780
961
 
781
962
  class Feature(pg.Object):
782
- """Interface for sandbox features."""
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
+ )
783
1018
 
784
1019
  # Disable symbolic comparison and hashing for sandbox objects.
785
1020
  allow_symbolic_comparison = False
@@ -789,6 +1024,13 @@ class Feature(pg.Object):
789
1024
  def name(self) -> str:
790
1025
  """Name of the feature, which will be used as key to access the feature."""
791
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
+
792
1034
  @property
793
1035
  @abc.abstractmethod
794
1036
  def environment(self) -> Environment:
@@ -796,14 +1038,17 @@ class Feature(pg.Object):
796
1038
 
797
1039
  @property
798
1040
  @abc.abstractmethod
799
- def sandbox(self) -> Sandbox:
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:
800
1047
  """Returns the sandbox that the feature is running in.
801
1048
 
802
1049
  Returns:
803
- The sandbox that the feature is running in.
804
-
805
- Raises:
806
- 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.
807
1052
  """
808
1053
 
809
1054
  @abc.abstractmethod
@@ -811,8 +1056,17 @@ class Feature(pg.Object):
811
1056
  """Returns True if the feature is applicable to the given image."""
812
1057
 
813
1058
  @abc.abstractmethod
814
- def setup(self, sandbox: Sandbox) -> None:
815
- """Sets up the feature, which is called once when the sandbox is up.
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.
816
1070
 
817
1071
  State transitions:
818
1072
  SETTING_UP -> READY: When setup succeeds.
@@ -915,17 +1169,54 @@ class Feature(pg.Object):
915
1169
  will not be housekeeping.
916
1170
  """
917
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
+
918
1188
  @property
919
1189
  def session_id(self) -> str | None:
920
1190
  """Returns the current user session identifier."""
921
- assert self.sandbox is not None
922
- return self.sandbox.session_id
1191
+ if self.is_sandbox_based:
1192
+ return self.sandbox.session_id
1193
+ return self._non_sandbox_based_session_id
923
1194
 
924
- def track_activity(self, name: str, **kwargs: Any) -> ContextManager[None]:
925
- """Context manager that tracks a feature activity."""
926
- return self.sandbox.track_activity(
927
- f'{self.name}.{name}', feature=self, **kwargs
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.'
928
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
929
1220
 
930
1221
 
931
1222
  def _make_path_compatible(id_str: str) -> str:
@@ -982,8 +1273,8 @@ def treat_as_sandbox_state_error(
982
1273
  return decorator
983
1274
 
984
1275
 
985
- def log_sandbox_activity(name: str | None = None):
986
- """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."""
987
1278
 
988
1279
  def decorator(func):
989
1280
  signature = pg.typing.get_signature(func)
@@ -1013,167 +1304,89 @@ def log_sandbox_activity(name: str | None = None):
1013
1304
 
1014
1305
 
1015
1306
  #
1016
- # Interface for Environment event handlers.
1307
+ # Interface for event handlers.
1017
1308
  #
1018
1309
 
1019
1310
 
1020
- class _SessionEventHandler:
1021
- """Base class for session event handlers."""
1311
+ class _EnvironmentEventHandler:
1312
+ """Base class for event handlers of an environment."""
1022
1313
 
1023
- def on_session_start(
1024
- self,
1025
- environment: Environment,
1026
- sandbox: Sandbox,
1027
- session_id: str,
1028
- duration: float,
1029
- error: BaseException | None
1030
- ) -> None:
1031
- """Called when a sandbox session starts.
1314
+ def on_environment_starting(self, environment: Environment) -> None:
1315
+ """Called when the environment is getting started.
1032
1316
 
1033
1317
  Args:
1034
1318
  environment: The environment.
1035
- sandbox: The sandbox.
1036
- session_id: The session ID.
1037
- duration: The time spent on starting the session.
1038
- error: The error that caused the session to start. If None, the session
1039
- started normally.
1040
1319
  """
1041
1320
 
1042
- def on_session_end(
1321
+ def on_environment_start(
1043
1322
  self,
1044
1323
  environment: Environment,
1045
- sandbox: Sandbox,
1046
- session_id: str,
1047
1324
  duration: float,
1048
- lifetime: float,
1049
1325
  error: BaseException | None
1050
1326
  ) -> None:
1051
- """Called when a sandbox session ends.
1327
+ """Called when the environment is started.
1052
1328
 
1053
1329
  Args:
1054
1330
  environment: The environment.
1055
- sandbox: The sandbox.
1056
- session_id: The session ID.
1057
- duration: The time spent on ending the session.
1058
- lifetime: The session lifetime in seconds.
1059
- error: The error that caused the session to end. If None, the session
1060
- ended normally.
1331
+ duration: The environment start duration in seconds.
1332
+ error: The error that failed the environment start. If None, the
1333
+ environment started normally.
1061
1334
  """
1062
1335
 
1063
-
1064
- class _FeatureEventHandler:
1065
- """Base class for feature event handlers."""
1066
-
1067
- def on_feature_setup(
1336
+ def on_environment_housekeep(
1068
1337
  self,
1069
1338
  environment: Environment,
1070
- sandbox: Sandbox,
1071
- feature: Feature,
1339
+ counter: int,
1072
1340
  duration: float,
1073
- error: BaseException | None
1341
+ error: BaseException | None,
1342
+ **kwargs
1074
1343
  ) -> None:
1075
- """Called when a sandbox feature is setup.
1344
+ """Called when the environment finishes a round of housekeeping.
1076
1345
 
1077
1346
  Args:
1078
1347
  environment: The environment.
1079
- sandbox: The sandbox.
1080
- feature: The feature.
1081
- duration: The feature setup duration in seconds.
1082
- error: The error happened during the feature setup. If None,
1083
- the feature setup performed normally.
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.
1084
1353
  """
1085
1354
 
1086
- def on_feature_teardown(
1355
+ def on_environment_shutting_down(
1087
1356
  self,
1088
1357
  environment: Environment,
1089
- sandbox: Sandbox,
1090
- feature: Feature,
1091
- duration: float,
1092
- error: BaseException | None
1358
+ offline_duration: float,
1093
1359
  ) -> None:
1094
- """Called when a sandbox feature is teardown.
1360
+ """Called when the environment is shutting down.
1095
1361
 
1096
1362
  Args:
1097
1363
  environment: The environment.
1098
- sandbox: The sandbox.
1099
- feature: The feature.
1100
- duration: The feature teardown duration in seconds.
1101
- error: The error happened during the feature teardown. If None,
1102
- the feature teardown performed normally.
1364
+ offline_duration: The environment offline duration in seconds.
1103
1365
  """
1104
1366
 
1105
- def on_feature_teardown_session(
1367
+ def on_environment_shutdown(
1106
1368
  self,
1107
1369
  environment: Environment,
1108
- sandbox: Sandbox,
1109
- feature: Feature,
1110
- session_id: str,
1111
1370
  duration: float,
1371
+ lifetime: float,
1112
1372
  error: BaseException | None
1113
1373
  ) -> None:
1114
- """Called when a feature is teardown with a session.
1115
-
1116
- Args:
1117
- environment: The environment.
1118
- sandbox: The sandbox.
1119
- feature: The feature.
1120
- session_id: The session ID.
1121
- duration: The feature teardown session duration in seconds.
1122
- error: The error happened during the feature teardown session. If
1123
- None, the feature teardown session performed normally.
1124
- """
1125
-
1126
- def on_feature_setup_session(
1127
- self,
1128
- environment: Environment,
1129
- sandbox: Sandbox,
1130
- feature: Feature,
1131
- session_id: str | None,
1132
- duration: float,
1133
- error: BaseException | None,
1134
- ) -> None:
1135
- """Called when a feature is setup with a session.
1136
-
1137
- Args:
1138
- environment: The environment.
1139
- sandbox: The sandbox.
1140
- feature: The feature.
1141
- session_id: The session ID.
1142
- duration: The feature setup session duration in seconds.
1143
- error: The error happened during the feature setup session. If
1144
- None, the feature setup session performed normally.
1145
- """
1146
-
1147
- def on_feature_housekeep(
1148
- self,
1149
- environment: Environment,
1150
- sandbox: Sandbox,
1151
- feature: Feature,
1152
- counter: int,
1153
- duration: float,
1154
- error: BaseException | None,
1155
- **kwargs,
1156
- ) -> None:
1157
- """Called when a sandbox feature is housekeeping.
1374
+ """Called when the environment is shutdown.
1158
1375
 
1159
1376
  Args:
1160
1377
  environment: The environment.
1161
- sandbox: The sandbox.
1162
- feature: The feature.
1163
- counter: Zero-based counter of the housekeeping round.
1164
- duration: The feature housekeeping duration in seconds.
1165
- error: The error happened during the feature housekeeping. If None, the
1166
- feature housekeeping normally.
1167
- **kwargs: Feature-specific properties computed during housekeeping.
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.
1168
1382
  """
1169
1383
 
1170
1384
 
1171
- class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1385
+ class _SandboxEventHandler:
1172
1386
  """Base class for sandbox event handlers."""
1173
1387
 
1174
1388
  def on_sandbox_start(
1175
1389
  self,
1176
- environment: Environment,
1177
1390
  sandbox: Sandbox,
1178
1391
  duration: float,
1179
1392
  error: BaseException | None
@@ -1181,7 +1394,6 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1181
1394
  """Called when a sandbox is started.
1182
1395
 
1183
1396
  Args:
1184
- environment: The environment.
1185
1397
  sandbox: The sandbox.
1186
1398
  duration: The time spent on starting the sandbox.
1187
1399
  error: The error that caused the sandbox to start. If None, the sandbox
@@ -1190,7 +1402,6 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1190
1402
 
1191
1403
  def on_sandbox_status_change(
1192
1404
  self,
1193
- environment: Environment,
1194
1405
  sandbox: Sandbox,
1195
1406
  old_status: 'Sandbox.Status',
1196
1407
  new_status: 'Sandbox.Status',
@@ -1199,7 +1410,6 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1199
1410
  """Called when a sandbox status changes.
1200
1411
 
1201
1412
  Args:
1202
- environment: The environment.
1203
1413
  sandbox: The sandbox.
1204
1414
  old_status: The old sandbox status.
1205
1415
  new_status: The new sandbox status.
@@ -1208,7 +1418,6 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1208
1418
 
1209
1419
  def on_sandbox_shutdown(
1210
1420
  self,
1211
- environment: Environment,
1212
1421
  sandbox: Sandbox,
1213
1422
  duration: float,
1214
1423
  lifetime: float,
@@ -1217,7 +1426,6 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1217
1426
  """Called when a sandbox is shutdown.
1218
1427
 
1219
1428
  Args:
1220
- environment: The environment.
1221
1429
  sandbox: The sandbox.
1222
1430
  duration: The time spent on shutting down the sandbox.
1223
1431
  lifetime: The sandbox lifetime in seconds.
@@ -1225,12 +1433,46 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1225
1433
  sandbox shutdown normally.
1226
1434
  """
1227
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
+
1228
1472
  def on_sandbox_activity(
1229
1473
  self,
1230
1474
  name: str,
1231
- environment: Environment,
1232
1475
  sandbox: Sandbox,
1233
- feature: Feature | None,
1234
1476
  session_id: str | None,
1235
1477
  duration: float,
1236
1478
  error: BaseException | None,
@@ -1240,9 +1482,7 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1240
1482
 
1241
1483
  Args:
1242
1484
  name: The name of the sandbox activity.
1243
- environment: The environment.
1244
1485
  sandbox: The sandbox.
1245
- feature: The feature that is associated with the sandbox activity.
1246
1486
  session_id: The session ID.
1247
1487
  duration: The sandbox activity duration in seconds.
1248
1488
  error: The error that caused the sandbox activity to perform. If None,
@@ -1252,7 +1492,6 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1252
1492
 
1253
1493
  def on_sandbox_housekeep(
1254
1494
  self,
1255
- environment: Environment,
1256
1495
  sandbox: Sandbox,
1257
1496
  counter: int,
1258
1497
  duration: float,
@@ -1262,7 +1501,6 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1262
1501
  """Called when a sandbox finishes a round of housekeeping.
1263
1502
 
1264
1503
  Args:
1265
- environment: The environment.
1266
1504
  sandbox: The sandbox.
1267
1505
  counter: Zero-based counter of the housekeeping round.
1268
1506
  duration: The sandbox housekeeping duration in seconds.
@@ -1272,75 +1510,131 @@ class _SandboxEventHandler(_FeatureEventHandler, _SessionEventHandler):
1272
1510
  """
1273
1511
 
1274
1512
 
1275
- class EventHandler(_SandboxEventHandler):
1276
- """Base class for event handlers of an environment."""
1513
+ class _FeatureEventHandler:
1514
+ """Base class for feature event handlers."""
1277
1515
 
1278
- def on_environment_starting(self, environment: Environment) -> None:
1279
- """Called when the environment is getting started.
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.
1280
1525
 
1281
1526
  Args:
1282
- environment: The environment.
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.
1283
1531
  """
1284
1532
 
1285
- def on_environment_start(
1533
+ def on_feature_teardown(
1286
1534
  self,
1287
- environment: Environment,
1535
+ feature: Feature,
1288
1536
  duration: float,
1289
1537
  error: BaseException | None
1290
1538
  ) -> None:
1291
- """Called when the environment is started.
1539
+ """Called when a sandbox feature is teardown.
1540
+
1541
+ Applicable to both sandbox-based and non-sandbox-based features.
1292
1542
 
1293
1543
  Args:
1294
- environment: The environment.
1295
- duration: The environment start duration in seconds.
1296
- error: The error that failed the environment start. If None, the
1297
- environment started normally.
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.
1298
1548
  """
1299
1549
 
1300
- def on_environment_housekeep(
1550
+ def on_feature_teardown_session(
1301
1551
  self,
1302
- environment: Environment,
1303
- counter: int,
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,
1304
1573
  duration: float,
1305
1574
  error: BaseException | None,
1306
- **kwargs
1307
1575
  ) -> None:
1308
- """Called when the environment finishes a round of housekeeping.
1576
+ """Called when a feature is setup with a session.
1577
+
1578
+ Applicable to both sandbox-based and non-sandbox-based features.
1309
1579
 
1310
1580
  Args:
1311
- environment: The environment.
1312
- counter: Zero-based counter of the housekeeping round.
1313
- duration: The environment start duration in seconds.
1314
- error: The error that failed the housekeeping. If None, the
1315
- housekeeping succeeded.
1316
- **kwargs: Environment-specific properties computed during housekeeping.
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.
1317
1586
  """
1318
1587
 
1319
- def on_environment_shutting_down(
1588
+ def on_feature_activity(
1320
1589
  self,
1321
- environment: Environment,
1322
- offline_duration: float,
1590
+ name: str,
1591
+ feature: Feature,
1592
+ session_id: str | None,
1593
+ duration: float,
1594
+ error: BaseException | None,
1595
+ **kwargs
1323
1596
  ) -> None:
1324
- """Called when the environment is shutting down.
1597
+ """Called when a feature activity is performed.
1598
+
1599
+ Applicable to both sandbox-based and non-sandbox-based features.
1325
1600
 
1326
1601
  Args:
1327
- environment: The environment.
1328
- offline_duration: The environment offline duration in seconds.
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.
1329
1611
  """
1330
1612
 
1331
- def on_environment_shutdown(
1613
+ def on_feature_housekeep(
1332
1614
  self,
1333
- environment: Environment,
1615
+ feature: Feature,
1616
+ counter: int,
1334
1617
  duration: float,
1335
- lifetime: float,
1336
- error: BaseException | None
1618
+ error: BaseException | None,
1619
+ **kwargs,
1337
1620
  ) -> None:
1338
- """Called when the environment is shutdown.
1621
+ """Called when a sandbox feature is housekeeping.
1622
+
1623
+ Applicable to both sandbox-based and non-sandbox-based features.
1339
1624
 
1340
1625
  Args:
1341
- environment: The environment.
1342
- duration: The environment shutdown duration in seconds.
1343
- lifetime: The environment lifetime in seconds.
1344
- error: The error that caused the environment to shutdown. If None, the
1345
- environment shutdown normally.
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.
1346
1632
  """
1633
+
1634
+
1635
+ class EventHandler(
1636
+ _EnvironmentEventHandler,
1637
+ _SandboxEventHandler,
1638
+ _FeatureEventHandler,
1639
+ ):
1640
+ """Base class for langfun/env handlers."""