langfun 0.1.2.dev202509220805__py3-none-any.whl → 0.1.2.dev202509240805__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
@@ -16,9 +16,9 @@
16
16
  import abc
17
17
  import contextlib
18
18
  import dataclasses
19
+ import enum
19
20
  import os
20
- from typing import Annotated, Any, Callable, ContextManager, ClassVar, Iterator, Optional
21
- import uuid
21
+ from typing import Annotated, Any, ContextManager, ClassVar, Iterator, Optional
22
22
 
23
23
  import pyglove as pg
24
24
 
@@ -165,107 +165,193 @@ class SandboxStateError(SandboxError):
165
165
  super().__init__(message or default_message, *args, **kwargs)
166
166
 
167
167
 
168
+ class FeatureTeardownError(SandboxError):
169
+ """Base class for feature errors."""
170
+
171
+ def __init__(
172
+ self,
173
+ message: str | None = None,
174
+ *args,
175
+ errors: dict[str, BaseException],
176
+ **kwargs
177
+ ):
178
+ self.errors = errors
179
+ super().__init__(
180
+ (message or
181
+ f'Feature teardown failed with user-defined errors: {errors}.'),
182
+ *args,
183
+ **kwargs
184
+ )
185
+
186
+ @property
187
+ def has_non_sandbox_state_error(self) -> bool:
188
+ """Returns True if the feature teardown error has non-sandbox state error."""
189
+ return any(
190
+ not isinstance(e, SandboxStateError) for e in self.errors.values()
191
+ )
192
+
193
+
194
+ class SessionTeardownError(SandboxError):
195
+ """Base class for session errors."""
196
+
197
+ def __init__(
198
+ self,
199
+ message: str | None = None,
200
+ *args,
201
+ errors: dict[str, BaseException],
202
+ **kwargs
203
+ ):
204
+ self.errors = errors
205
+ super().__init__(
206
+ (message or
207
+ f'Session teardown failed with user-defined errors: {errors}.'),
208
+ *args,
209
+ **kwargs
210
+ )
211
+
212
+ @property
213
+ def has_non_sandbox_state_error(self) -> bool:
214
+ """Returns True if the feature teardown error has non-sandbox state error."""
215
+ return any(
216
+ not isinstance(e, SandboxStateError) for e in self.errors.values()
217
+ )
218
+
219
+
168
220
  #
169
221
  # Event handler.
170
222
  #
171
223
 
172
224
 
173
- class EnvironmentEventHandler:
174
- """Base class for environment event handlers."""
225
+ class SessionEventHandler:
226
+ """Base class for session event handlers."""
175
227
 
176
- def on_environment_start(
228
+ def on_session_start(
177
229
  self,
178
230
  environment: 'Environment',
179
- error: Exception | None
231
+ sandbox: 'Sandbox',
232
+ session_id: str,
233
+ error: BaseException | None
180
234
  ) -> None:
181
- """Called when the environment is started."""
235
+ """Called when a sandbox session starts."""
182
236
 
183
- def on_environment_shutdown(
237
+ def on_session_activity(
184
238
  self,
239
+ session_id: str,
240
+ name: str,
185
241
  environment: 'Environment',
186
- error: Exception | None
242
+ sandbox: 'Sandbox',
243
+ feature: Optional['Feature'],
244
+ error: BaseException | None,
245
+ **kwargs
187
246
  ) -> None:
188
- """Called when the environment is shutdown."""
247
+ """Called when a sandbox activity is performed."""
189
248
 
190
- def on_sandbox_start(
249
+ def on_session_end(
191
250
  self,
192
251
  environment: 'Environment',
193
252
  sandbox: 'Sandbox',
194
- error: Exception | None
253
+ session_id: str,
254
+ error: BaseException | None
195
255
  ) -> None:
196
- """Called when a sandbox is started."""
256
+ """Called when a sandbox session ends."""
197
257
 
198
- def on_sandbox_shutdown(
258
+
259
+ class FeatureEventHandler:
260
+ """Base class for feature event handlers."""
261
+
262
+ def on_feature_setup(
199
263
  self,
200
264
  environment: 'Environment',
201
265
  sandbox: 'Sandbox',
202
- error: Exception | None
266
+ feature: 'Feature',
267
+ error: BaseException | None
203
268
  ) -> None:
204
- """Called when a sandbox is shutdown."""
269
+ """Called when a sandbox feature is setup."""
205
270
 
206
- def on_sandbox_feature_setup(
271
+ def on_feature_teardown(
207
272
  self,
208
273
  environment: 'Environment',
209
274
  sandbox: 'Sandbox',
210
275
  feature: 'Feature',
211
- error: Exception | None
276
+ error: BaseException | None
212
277
  ) -> None:
213
- """Called when a sandbox feature is setup."""
278
+ """Called when a sandbox feature is teardown."""
214
279
 
215
- def on_sandbox_feature_teardown(
280
+ def on_feature_teardown_session(
216
281
  self,
217
282
  environment: 'Environment',
218
283
  sandbox: 'Sandbox',
219
284
  feature: 'Feature',
220
- error: Exception | None
285
+ session_id: str,
286
+ error: BaseException | None
221
287
  ) -> None:
222
- """Called when a sandbox feature is teardown."""
288
+ """Called when a feature is teardown with a session."""
223
289
 
224
- def on_sandbox_feature_housekeep(
290
+ def on_feature_setup_session(
225
291
  self,
226
292
  environment: 'Environment',
227
293
  sandbox: 'Sandbox',
228
294
  feature: 'Feature',
229
- error: Exception | None
295
+ session_id: str | None,
296
+ error: BaseException | None
230
297
  ) -> None:
231
- """Called when a sandbox feature is housekeeping."""
298
+ """Called when a feature is setup with a session."""
232
299
 
233
- def on_sandbox_session_start(
300
+ def on_feature_housekeep(
234
301
  self,
235
302
  environment: 'Environment',
236
303
  sandbox: 'Sandbox',
237
- session_id: str,
238
- error: Exception | None
304
+ feature: 'Feature',
305
+ error: BaseException | None
239
306
  ) -> None:
240
- """Called when a sandbox session starts."""
307
+ """Called when a sandbox feature is housekeeping."""
241
308
 
242
- def on_sandbox_session_activity(
309
+
310
+ class SandboxEventHandler(FeatureEventHandler, SessionEventHandler):
311
+ """Base class for sandbox event handlers."""
312
+
313
+ def on_sandbox_start(
243
314
  self,
244
315
  environment: 'Environment',
245
316
  sandbox: 'Sandbox',
246
- session_id: str,
247
- feature: Optional['Feature'],
248
- error: Exception | None,
249
- **kwargs
317
+ error: BaseException | None
250
318
  ) -> None:
251
- """Called when a sandbox activity is performed."""
319
+ """Called when a sandbox is started."""
252
320
 
253
- def on_sandbox_session_end(
321
+ def on_sandbox_status_change(
254
322
  self,
255
323
  environment: 'Environment',
256
324
  sandbox: 'Sandbox',
257
- session_id: str,
258
- error: Exception | None
325
+ old_status: 'Sandbox.Status',
326
+ new_status: 'Sandbox.Status',
259
327
  ) -> None:
260
- """Called when a sandbox session ends."""
328
+ """Called when a sandbox status changes."""
261
329
 
262
- def on_sandbox_ping(
330
+ def on_sandbox_shutdown(
263
331
  self,
264
332
  environment: 'Environment',
265
333
  sandbox: 'Sandbox',
266
- error: Exception | None
334
+ error: BaseException | None
335
+ ) -> None:
336
+ """Called when a sandbox is shutdown."""
337
+
338
+
339
+ class EnvironmentEventHandler(SandboxEventHandler):
340
+ """Base class for environment event handlers."""
341
+
342
+ def on_environment_start(
343
+ self,
344
+ environment: 'Environment',
345
+ error: BaseException | None
267
346
  ) -> None:
268
- """Called when a sandbox is pinged."""
347
+ """Called when the environment is started."""
348
+
349
+ def on_environment_shutdown(
350
+ self,
351
+ environment: 'Environment',
352
+ error: BaseException | None
353
+ ) -> None:
354
+ """Called when the environment is shutdown."""
269
355
 
270
356
 
271
357
  #
@@ -276,17 +362,34 @@ class EnvironmentEventHandler:
276
362
  class Environment(pg.Object):
277
363
  """Base class for an environment."""
278
364
 
365
+ class Status(enum.Enum):
366
+ """Environment state.
367
+
368
+ State transitions:
369
+
370
+ +---------------+ +-----------+
371
+ | <CREATED> | --(start)---> | ONLINE | -(shutdown or outage detected)
372
+ +---------------+ | +-----------+ |
373
+ | +-----------+ |
374
+ +------> | <OFFLINE> | <------------+
375
+ +-----------+
376
+ """
377
+ CREATED = 'created'
378
+ ONLINE = 'online'
379
+ SHUTTING_DOWN = 'shutting_down'
380
+ OFFLINE = 'offline'
381
+
279
382
  features: Annotated[
280
383
  dict[str, 'Feature'],
281
384
  'Features to be exposed by the environment.'
282
385
  ] = {}
283
386
 
284
- event_handler: Annotated[
285
- EnvironmentEventHandler | None,
387
+ event_handlers: Annotated[
388
+ list[EnvironmentEventHandler],
286
389
  (
287
390
  'User handler for the environment events.'
288
391
  )
289
- ] = None
392
+ ] = []
290
393
 
291
394
  _ENV_STACK: Annotated[
292
395
  ClassVar[list['Environment']],
@@ -302,14 +405,14 @@ class Environment(pg.Object):
302
405
  def id(self) -> EnvironmentId:
303
406
  """Returns the identifier for the environment."""
304
407
 
408
+ @property
305
409
  @abc.abstractmethod
306
- def stats(self) -> dict[str, Any]:
307
- """Returns the stats of the environment."""
410
+ def status(self) -> Status:
411
+ """Returns the status of the environment."""
308
412
 
309
- @property
310
413
  @abc.abstractmethod
311
- def is_alive(self) -> bool:
312
- """Returns True if the environment is alive."""
414
+ def stats(self) -> dict[str, Any]:
415
+ """Returns the stats of the environment."""
313
416
 
314
417
  @abc.abstractmethod
315
418
  def start(self) -> None:
@@ -323,8 +426,7 @@ class Environment(pg.Object):
323
426
  def shutdown(self) -> None:
324
427
  """Shuts down the environment.
325
428
 
326
- Raises:
327
- EnvironmentError: If the environment is not available.
429
+ IMPORTANT: This method shall not raise any exceptions.
328
430
  """
329
431
 
330
432
  @abc.abstractmethod
@@ -339,10 +441,19 @@ class Environment(pg.Object):
339
441
  EnvironmentOverloadError: If the environment is overloaded.
340
442
  """
341
443
 
444
+ @abc.abstractmethod
445
+ def new_session_id(self) -> str:
446
+ """Generates a new session ID."""
447
+
342
448
  #
343
449
  # Environment lifecycle.
344
450
  #
345
451
 
452
+ @property
453
+ def is_online(self) -> bool:
454
+ """Returns True if the environment is alive."""
455
+ return self.status == self.Status.ONLINE
456
+
346
457
  def __enter__(self) -> 'Environment':
347
458
  """Enters the environment and sets it as the current environment."""
348
459
  self.start()
@@ -394,123 +505,83 @@ class Environment(pg.Object):
394
505
  return self.acquire().features[name]
395
506
  raise AttributeError(name)
396
507
 
397
- #
398
- # Event handlers subclasses can override.
399
- #
400
508
 
401
- def on_start(self, error: Exception | None = None) -> None:
402
- """Called when the environment is started."""
403
- if self.event_handler:
404
- self.event_handler.on_environment_start(self, error)
509
+ class Sandbox(pg.Object):
510
+ """Interface for sandboxes."""
405
511
 
406
- def on_shutdown(self, error: Exception | None = None) -> None:
407
- """Called when the environment is shutdown."""
408
- if self.event_handler:
409
- self.event_handler.on_environment_shutdown(self, error)
512
+ class Status(enum.Enum):
513
+ """Sandbox state.
514
+
515
+ State transitions:
516
+
517
+ +---------------+ +---------------+
518
+ | <OFFLINE> | <------ | SHUTTING_DOWN |
519
+ +---------------+ +---------------+
520
+ ^ ^
521
+ | |
522
+ (shutdown)| +------------------------+
523
+ | |
524
+ +-----------+ (call start) +------------+ |
525
+ | <CREATED> | -------------> | SETTING_UP | <----------------+ |
526
+ +-----------+ +------------+ | |
527
+ | | |
528
+ | (start succeeded) | |
529
+ | OR (_setup_session) | |
530
+ v | |
531
+ +---------+ | |
532
+ | READY | | |
533
+ +---------+ | |
534
+ | | |
535
+ | (set_acquired) | |
536
+ v | |
537
+ +----------+ | |
538
+ | ACQUIRED | | |
539
+ +----------+ | |
540
+ | | |
541
+ | (call start_session) | |
542
+ +------------+ | |
543
+ | SETTING_UP | --(failed) -----------+
544
+ +------------+ | |
545
+ | | |
546
+ v (start_session succeeded)| |
547
+ +--------------+ | |
548
+ | IN_SESSION |--(end_session)--+ |
549
+ +--------------+ |
550
+ | |
551
+ +------(shutdown)---------------+
552
+ """
410
553
 
411
- def on_sandbox_start(
412
- self,
413
- sandbox: 'Sandbox',
414
- error: Exception | None = None
415
- ) -> None:
416
- """Called when a sandbox is started."""
417
- if self.event_handler:
418
- self.event_handler.on_sandbox_start(self, sandbox, error)
554
+ # The sandbox is created, but not yet started.
555
+ CREATED = 'created'
419
556
 
420
- def on_sandbox_shutdown(
421
- self,
422
- sandbox: 'Sandbox',
423
- error: Exception | None = None
424
- ) -> None:
425
- """Called when a sandbox is shutdown."""
426
- if self.event_handler:
427
- self.event_handler.on_sandbox_shutdown(self, sandbox, error)
557
+ # The sandbox is being setting up to serve user sessions.
558
+ SETTING_UP = 'setting_up'
428
559
 
429
- def on_sandbox_feature_setup(
430
- self,
431
- sandbox: 'Sandbox',
432
- feature: 'Feature',
433
- error: Exception | None = None
434
- ) -> None:
435
- """Called when a sandbox feature is setup."""
436
- if self.event_handler:
437
- self.event_handler.on_sandbox_feature_setup(self, sandbox, feature, error)
560
+ # The sandbox is set up and free to be acquired by the user.
561
+ READY = 'ready'
438
562
 
439
- def on_sandbox_feature_teardown(
440
- self,
441
- sandbox: 'Sandbox',
442
- feature: 'Feature',
443
- error: Exception | None = None
444
- ) -> None:
445
- """Called when a sandbox feature is teardown."""
446
- if self.event_handler:
447
- self.event_handler.on_sandbox_feature_teardown(
448
- self, sandbox, feature, error
449
- )
563
+ # The sandbox is acquired by a thread, but not yet in a user session.
564
+ ACQUIRED = 'acquired'
450
565
 
451
- def on_sandbox_feature_housekeep(
452
- self,
453
- sandbox: 'Sandbox',
454
- feature: 'Feature',
455
- error: Exception | None = None
456
- ) -> None:
457
- """Called when a sandbox feature is housekeeping."""
458
- if self.event_handler:
459
- self.event_handler.on_sandbox_feature_housekeep(
460
- self, sandbox, feature, error
461
- )
566
+ # The sandbox is in a user session.
567
+ IN_SESSION = 'in_session'
462
568
 
463
- def on_sandbox_session_start(
464
- self,
465
- sandbox: 'Sandbox',
466
- session_id: str,
467
- error: Exception | None = None
468
- ) -> None:
469
- """Called when a sandbox session starts."""
470
- if self.event_handler:
471
- self.event_handler.on_sandbox_session_start(
472
- self, sandbox, session_id, error
473
- )
569
+ # The sandbox is being shut down.
570
+ SHUTTING_DOWN = 'shutting_down'
474
571
 
475
- def on_sandbox_session_activity(
476
- self,
477
- sandbox: 'Sandbox',
478
- session_id: str,
479
- feature: Optional['Feature'] = None,
480
- error: Exception | None = None,
481
- **kwargs
482
- ) -> None:
483
- """Called when a sandbox activity is performed."""
484
- if self.event_handler:
485
- self.event_handler.on_sandbox_session_activity(
486
- self, sandbox, feature, session_id, error, **kwargs
487
- )
572
+ # The sandbox is offline.
573
+ OFFLINE = 'offline'
488
574
 
489
- def on_sandbox_session_end(
490
- self,
491
- sandbox: 'Sandbox',
492
- session_id: str,
493
- error: Exception | None = None
494
- ) -> None:
495
- """Called when a sandbox session ends."""
496
- if self.event_handler:
497
- self.event_handler.on_sandbox_session_end(
498
- self, sandbox, session_id, error
575
+ @property
576
+ def is_online(self) -> bool:
577
+ """Returns True if the sandbox is online."""
578
+ return self in (
579
+ Sandbox.Status.SETTING_UP,
580
+ Sandbox.Status.READY,
581
+ Sandbox.Status.ACQUIRED,
582
+ Sandbox.Status.IN_SESSION,
499
583
  )
500
584
 
501
- def on_sandbox_ping(
502
- self,
503
- sandbox: 'Sandbox',
504
- error: Exception | None = None
505
- ) -> None:
506
- """Called when a sandbox is pinged."""
507
- if self.event_handler:
508
- self.event_handler.on_sandbox_ping(self, sandbox, error)
509
-
510
-
511
- class Sandbox(pg.Object):
512
- """Interface for sandboxes."""
513
-
514
585
  @property
515
586
  @abc.abstractmethod
516
587
  def id(self) -> SandboxId:
@@ -528,62 +599,179 @@ class Sandbox(pg.Object):
528
599
 
529
600
  @property
530
601
  @abc.abstractmethod
531
- def is_alive(self) -> bool:
532
- """Returns True if the sandbox is alive."""
602
+ def status(self) -> Status:
603
+ """Returns the status of the sandbox."""
533
604
 
534
605
  @property
606
+ def is_online(self) -> bool:
607
+ """Returns True if the sandbox is online."""
608
+ return self.status.is_online
609
+
535
610
  @abc.abstractmethod
536
- def is_busy(self) -> bool:
537
- """Returns whether the sandbox is busy."""
611
+ def set_acquired(self) -> None:
612
+ """Marks the sandbox as acquired."""
613
+
614
+ @abc.abstractmethod
615
+ def add_event_handler(
616
+ self,
617
+ event_handler: EnvironmentEventHandler
618
+ ) -> None:
619
+ """Sets the status of the sandbox."""
538
620
 
539
621
  @abc.abstractmethod
540
- def set_pending(self) -> None:
541
- """Marks the sandbox pending after acquisition but before ready for use."""
622
+ def remove_event_handler(
623
+ self,
624
+ event_handler: EnvironmentEventHandler
625
+ ) -> None:
626
+ """Removes the status of the sandbox."""
542
627
 
543
628
  @property
544
629
  @abc.abstractmethod
545
- def is_pending(self) -> bool:
546
- """Returns whether the sandbox is pending."""
630
+ def state_errors(self) -> list[SandboxStateError]:
631
+ """Returns state errors encountered during sandbox's lifecycle."""
547
632
 
548
633
  @abc.abstractmethod
549
634
  def start(self) -> None:
550
635
  """Starts the sandbox.
551
636
 
637
+ State transitions:
638
+ CREATED -> SETTING_UP -> READY: When all sandbox and feature setup
639
+ succeeds.
640
+ CREATED -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When sandbox or feature
641
+ setup fails.
642
+
643
+ `start` and `shutdown` should be called in pairs, even when the sandbox
644
+ fails to start. This ensures proper cleanup.
645
+
646
+ Start may fail with two sources of errors:
647
+
648
+ 1. SandboxStateError: If sandbox or feature setup fail due to enviroment
649
+ outage or sandbox state errors.
650
+ 2. BaseException: If feature setup failed with user-defined errors, this
651
+ could happen when there is bug in the user code or non-environment code
652
+ failure.
653
+
654
+ In both cases, the sandbox will be shutdown automatically, and the error
655
+ will be add to `errors`. The sandbox is considered dead and will not be
656
+ further used.
657
+
552
658
  Raises:
553
659
  SandboxStateError: If the sandbox is in a bad state.
660
+ BaseException: If feature setup failed with user-defined errors.
554
661
  """
555
662
 
556
663
  @abc.abstractmethod
557
664
  def shutdown(self) -> None:
558
665
  """Shuts down the sandbox.
559
666
 
560
- Raises:
561
- SandboxStateError: If the sandbox is in a bad state.
562
- """
563
-
564
- @abc.abstractmethod
565
- def ping(self) -> None:
566
- """Ping the sandbox to check if it is alive.
667
+ State transitions:
668
+ SHUTTING_DOWN -> SHUTTING_DOWN: No operation.
669
+ OFFLINE -> OFFLINE: No operation.
670
+ SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When sandbox and feature
671
+ setup fails.
672
+ IN_SESSION -> SHUTTING_DOWN -> OFFLINE: When user session exits while
673
+ sandbox is set not to reuse, or session teardown fails.
674
+ FREE -> SHUTTING_DOWN -> OFFLINE: When sandbox is shutdown when the
675
+ environment is shutting down, or housekeeping loop shuts down the
676
+ sandbox due to housekeeping failures.
677
+
678
+
679
+ Please be aware that `shutdown` will be called whenever an operation on the
680
+ sandbox encounters a critical error. This means, `shutdown` should not make
681
+ the assumption that the sandbox is in a healthy state, even `start` could
682
+ fail. As a result, `shutdown` must allow re-entry and be thread-safe with
683
+ other sandbox operations.
684
+
685
+ Shutdown may fail with two sources of errors:
686
+
687
+ 1. SandboxStateError: If the sandbox is in a bad state, and feature teardown
688
+ logic depending on a healthy sandbox may fail. In such case, we do not
689
+ raise error to the user as the user session is considered completed. The
690
+ sandbox is abandoned and new user sessions will be served on other
691
+ sandboxes.
692
+
693
+ 2. BaseException: The sandbox is in good state, but user code raises error
694
+ due to bug or non-environment code failure. In such case, errors will be
695
+ raised to the user so the error could be surfaced and handled properly.
696
+ The sandbox is treated as shutdown and will not be further used.
567
697
 
568
698
  Raises:
569
- SandboxStateError: If the sandbox is in a bad state.
699
+ BaseException: If feature teardown failed with user-defined errors.
570
700
  """
571
701
 
572
702
  @abc.abstractmethod
573
- def start_session(self, session_id: str) -> None:
703
+ def start_session(
704
+ self,
705
+ session_id: str,
706
+ ) -> None:
574
707
  """Begins a user session with the sandbox.
575
708
 
709
+ State transitions:
710
+ ACQUIRED -> SETTING_UP -> IN_SESSION: When session setup succeeds.
711
+ ACQUIRED -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When session setup
712
+ fails.
713
+
714
+ A session is a sequence of stateful interactions with the sandbox.
715
+ Across different sessions the sandbox are considered stateless.
716
+ `start_session` and `end_session` should always be called in pairs, even
717
+ when the session fails to start. `Sandbox.new_session` context manager is
718
+ the recommended way to use `start_session` and `end_session` in pairs.
719
+
720
+ Starting a session may fail with two sources of errors:
721
+
722
+ 1. SandboxStateError: If the sandbox is in a bad state or session setup
723
+ failed.
724
+
725
+ 2. BaseException: If session setup failed with user-defined errors.
726
+
727
+ In both cases, the sandbox will be shutdown automatically and the
728
+ session will be considered ended. The error will be added to `errors`.
729
+ Future session will be served on other sandboxes.
730
+
576
731
  Args:
577
732
  session_id: The identifier for the user session.
578
733
 
579
734
  Raises:
580
- SandboxError: If the sandbox already has a user session
581
- or the session cannot be started.
735
+ SandboxStateError: If the sandbox is already in a bad state or session
736
+ setup failed.
737
+ BaseException: If session setup failed with user-defined errors.
582
738
  """
583
739
 
584
740
  @abc.abstractmethod
585
741
  def end_session(self) -> None:
586
- """Ends the user session with the sandbox."""
742
+ """Ends the user session with the sandbox.
743
+
744
+ State transitions:
745
+ IN_SESSION -> READY: When user session exits normally, and sandbox is set
746
+ to reuse.
747
+ IN_SESSION -> SHUTTING_DOWN -> OFFLINE: When user session exits while
748
+ sandbox is set not to reuse, or session teardown fails.
749
+ IN_SESSION -> SETTING_UP -> READY: When user session exits normally, and
750
+ sandbox is set to reuse, and proactive session setup is enabled.
751
+ IN_SESSION -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When user session
752
+ exits normally, and proactive session setup is enabled but fails.
753
+ not IN_SESSION -> same state: No operation
754
+
755
+ `end_session` should always be called for each `start_session` call, even
756
+ when the session fails to start, to ensure proper cleanup.
757
+
758
+ `end_session` may fail with two sources of errors:
759
+
760
+ 1. SandboxStateError: If the sandbox is in a bad state or session teardown
761
+ failed.
762
+
763
+ 2. BaseException: If session teardown failed with user-defined errors.
764
+
765
+ In both cases, the sandbox will be shutdown automatically and the
766
+ session will be considered ended. The error will be added to `errors`.
767
+ Future session will be served on other sandboxes.
768
+
769
+ However, SandboxStateError encountered during `end_session` will NOT be
770
+ raised to the user as the user session is considered completed.
771
+
772
+ Raises:
773
+ BaseException: If session teardown failed with user-defined errors.
774
+ """
587
775
 
588
776
  @property
589
777
  @abc.abstractmethod
@@ -598,10 +786,30 @@ class Sandbox(pg.Object):
598
786
  #
599
787
 
600
788
  @contextlib.contextmanager
601
- def new_session(self, session_id: str | None = None) -> Iterator['Sandbox']:
602
- """Context manager for obtaining a sandbox for a user session."""
789
+ def new_session(
790
+ self,
791
+ session_id: str | None = None,
792
+ ) -> Iterator['Sandbox']:
793
+ """Context manager for obtaining a sandbox for a user session.
794
+
795
+ State transitions:
796
+ ACQUIRED -> IN_SESSION -> READY: When session setup and teardown succeed.
797
+ ACQUIRED -> IN_SESSINO -> OFFLINE: When session setup or teardown fails.
798
+
799
+ Args:
800
+ session_id: The identifier for the user session. If not provided, a random
801
+ ID will be generated.
802
+
803
+ Yields:
804
+ The sandbox for the user session.
805
+
806
+ Raises:
807
+ SandboxStateError: If a session cannot be started on the sandbox.
808
+ BaseException: If session setup or teardown failed with user-defined
809
+ errors.
810
+ """
603
811
  if session_id is None:
604
- session_id = f'session-{uuid.uuid4().hex[:7]}'
812
+ session_id = self.environment.new_session_id()
605
813
  self.start_session(session_id)
606
814
  try:
607
815
  yield self
@@ -630,84 +838,6 @@ class Sandbox(pg.Object):
630
838
  return self.features[name]
631
839
  raise AttributeError(name)
632
840
 
633
- #
634
- # Event handlers subclasses can override.
635
- #
636
-
637
- def on_start(self, error: Exception | None = None) -> None:
638
- """Called when the sandbox is started."""
639
- self.environment.on_sandbox_start(self, error)
640
-
641
- def on_shutdown(self, error: Exception | None = None) -> None:
642
- """Called when the sandbox is shutdown."""
643
- self.environment.on_sandbox_shutdown(self, error)
644
-
645
- def on_ping(self, error: Exception | None = None) -> None:
646
- """Called when the sandbox is pinged."""
647
- self.environment.on_sandbox_ping(self, error)
648
-
649
- def on_feature_setup(
650
- self,
651
- feature: 'Feature',
652
- error: Exception | None = None
653
- ) -> None:
654
- """Called when a feature is setup."""
655
- self.environment.on_sandbox_feature_setup(
656
- self, feature, error
657
- )
658
-
659
- def on_feature_teardown(
660
- self,
661
- feature: 'Feature',
662
- error: Exception | None = None
663
- ) -> None:
664
- """Called when a feature is teardown."""
665
- self.environment.on_sandbox_feature_teardown(
666
- self, feature, error
667
- )
668
-
669
- def on_feature_housekeep(
670
- self,
671
- feature: 'Feature',
672
- error: Exception | None = None
673
- ) -> None:
674
- """Called when a feature is housekeeping."""
675
- self.environment.on_sandbox_feature_housekeep(
676
- self, feature, error
677
- )
678
-
679
- def on_session_start(
680
- self,
681
- session_id: str,
682
- error: Exception | None = None
683
- ) -> None:
684
- """Called when the user session starts."""
685
- self.environment.on_sandbox_session_start(self, session_id, error)
686
-
687
- def on_session_activity(
688
- self,
689
- session_id: str,
690
- feature: Optional['Feature'] = None,
691
- error: Exception | None = None,
692
- **kwargs
693
- ) -> None:
694
- """Called when a sandbox activity is performed."""
695
- self.environment.on_sandbox_session_activity(
696
- sandbox=self,
697
- feature=feature,
698
- session_id=session_id,
699
- error=error,
700
- **kwargs
701
- )
702
-
703
- def on_session_end(
704
- self,
705
- session_id: str,
706
- error: Exception | None = None
707
- ) -> None:
708
- """Called when the user session ends."""
709
- self.environment.on_sandbox_session_end(self, session_id, error)
710
-
711
841
 
712
842
  class Feature(pg.Object):
713
843
  """Interface for sandbox features."""
@@ -719,40 +849,107 @@ class Feature(pg.Object):
719
849
 
720
850
  @property
721
851
  @abc.abstractmethod
722
- def sandbox(self) -> Sandbox | None:
723
- """Returns the sandbox that the feature is running in."""
852
+ def sandbox(self) -> Sandbox:
853
+ """Returns the sandbox that the feature is running in.
854
+
855
+ Returns:
856
+ The sandbox that the feature is running in.
857
+
858
+ Raises:
859
+ AssertError: If the feature is not set up with a sandbox yet.
860
+ """
724
861
 
725
862
  @abc.abstractmethod
726
863
  def setup(self, sandbox: Sandbox) -> None:
727
864
  """Sets up the feature, which is called once when the sandbox is up.
728
865
 
866
+ State transitions:
867
+ SETTING_UP -> READY: When setup succeeds.
868
+ SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When setup fails.
869
+
870
+ `setup` is called when a sandbox is started for the first time. When a
871
+ feature's `setup` is called, its `teardown` is guaranteed to be called.
872
+
729
873
  Args:
730
874
  sandbox: The sandbox that the feature is running in.
731
875
 
732
876
  Raises:
733
- SandboxStateError: If the sandbox is in a bad state.
877
+ SandboxStateError: If setup failed due to sandbox state errors.
878
+ BaseException: If setup failed with user-defined errors.
734
879
  """
735
880
 
736
881
  @abc.abstractmethod
737
882
  def teardown(self) -> None:
738
883
  """Teardowns the feature, which is called once when the sandbox is down.
739
884
 
885
+ State transitions:
886
+ SHUTTING_DOWN -> OFFLINE: When teardown succeeds or fails.
887
+
740
888
  Raises:
741
889
  SandboxStateError: If the sandbox is in a bad state.
742
890
  """
743
891
 
744
892
  @abc.abstractmethod
745
- def setup_session(self, session_id: str) -> None:
746
- """Sets up the feature for a user session."""
893
+ def setup_session(self) -> None:
894
+ """Sets up the feature for the upcoming user session.
895
+
896
+ State transitions:
897
+ SETTING_UP -> READY: When session setup succeeds.
898
+ SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When sessino setup fails.
899
+
900
+ `setup_session` is called when a new user session starts for per-session
901
+ setup. `setup_session` and `teardown_session` will be called in pairs, even
902
+ `setup_session` fails.
903
+
904
+ `setup_session` may fail with two sources of errors:
905
+
906
+ 1. SandboxStateError: If the sandbox is in a bad state or session setup
907
+ failed.
908
+ 2. BaseException: If session setup failed with user-defined errors.
909
+
910
+ In both cases, the error will be raised to the user and the session will be
911
+ ended. The sandbox will shutdown automatically and will not be further used.
912
+
913
+ Raises:
914
+ SandboxStateError: If the sandbox is in a bad state or session setup
915
+ failed.
916
+ BaseException: If session setup failed with user-defined errors.
917
+ """
747
918
 
748
919
  @abc.abstractmethod
749
- def teardown_session(self, session_id: str) -> None:
750
- """Teardowns the feature for a user session."""
920
+ def teardown_session(self) -> None:
921
+ """Teardowns the feature for an ending user session.
922
+
923
+ State transitions:
924
+ SHUTTING_DOWN -> OFFLINE: When session teardown succeeds or fails.
925
+
926
+ `teardown_session` is called when a user session ends for per-
927
+ session teardown. `teardown_session` will always be called upon a feature
928
+ whose `setup_session` is called.
929
+
930
+ `teardown_session` may fail with two sources of errors:
931
+
932
+ 1. SandboxStateError: If the sandbox is in a bad state or session teardown
933
+ failed.
934
+ 2. BaseException: If session teardown failed with user-defined errors.
935
+
936
+ In both cases, the session will be closed and the sandbox will be shutdown.
937
+ However, SandboxStateError encountered during `teardown_session` will NOT be
938
+ raised to the user as the user session is considered completed. Other errors
939
+ will be raised to the user for proper error handling.
940
+
941
+ Raises:
942
+ BaseException: If session teardown failed with user-defined errors.
943
+ """
751
944
 
752
945
  @abc.abstractmethod
753
946
  def housekeep(self) -> None:
754
947
  """Performs housekeeping for the feature.
755
948
 
949
+ State transitions:
950
+ (no state change): When housekeeping succeeds.
951
+ original state -> SHUTTING_DOWN -> OFFLINE: When housekeeping fails.
952
+
756
953
  Raises:
757
954
  SandboxStateError: If the sandbox is in a bad state.
758
955
  """
@@ -772,72 +969,3 @@ class Feature(pg.Object):
772
969
  """Returns the current user session identifier."""
773
970
  assert self.sandbox is not None
774
971
  return self.sandbox.session_id
775
-
776
- #
777
- # Event handlers subclasses can override.
778
- #
779
-
780
- def on_setup(
781
- self,
782
- error: BaseException | None = None
783
- ) -> None:
784
- """Called when the feature is setup."""
785
- self.sandbox.on_feature_setup(self, error)
786
-
787
- def on_teardown(
788
- self,
789
- error: BaseException | None = None
790
- ) -> None:
791
- """Called when the feature is teardown."""
792
- self.sandbox.on_feature_teardown(self, error)
793
-
794
- def on_housekeep(
795
- self,
796
- error: BaseException | None = None
797
- ) -> None:
798
- """Called when the feature has done housekeeping."""
799
- self.sandbox.on_feature_housekeep(self, error)
800
-
801
- def on_session_setup(
802
- self,
803
- session_id: str,
804
- error: BaseException | None = None,
805
- ) -> None:
806
- """Called when the feature is setup for a user session."""
807
-
808
- def on_session_activity(
809
- self,
810
- session_id: str,
811
- error: Exception | None,
812
- **kwargs
813
- ) -> None:
814
- """Called when a sandbox activity is performed."""
815
- self.sandbox.on_session_activity(
816
- feature=self, session_id=session_id, error=error, **kwargs
817
- )
818
-
819
- def on_session_teardown(
820
- self,
821
- session_id: str,
822
- error: BaseException | None = None,
823
- ) -> None:
824
- """Called when the feature is teardown for a user session."""
825
-
826
-
827
- def call_with_event(
828
- action: Callable[[], None],
829
- event_handler: Callable[..., None],
830
- action_kwargs: dict[str, Any] | None = None,
831
- event_handler_kwargs: dict[str, Any] | None = None,
832
- ) -> None:
833
- """Triggers an event handler."""
834
- action_kwargs = action_kwargs or {}
835
- event_handler_kwargs = event_handler_kwargs or {}
836
- error = None
837
- try:
838
- action(**action_kwargs)
839
- except BaseException as e: # pylint: disable=broad-except
840
- error = e
841
- raise
842
- finally:
843
- event_handler(error=error, **event_handler_kwargs)