langfun 0.1.2.dev202509240805__py3-none-any.whl → 0.1.2.dev202509250804__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.

@@ -92,10 +92,12 @@ class BaseSandbox(interface.Sandbox):
92
92
  assert self._status != status, (self._status, status)
93
93
  self.on_status_change(self._status, status)
94
94
  self._status = status
95
+ self._status_start_time = time.time()
95
96
 
96
- def _maybe_report_state_error(self, e: BaseException | None) -> None:
97
+ def report_maybe_state_error(self, e: BaseException | None) -> None:
97
98
  """Reports sandbox state errors."""
98
- if isinstance(e, interface.SandboxStateError):
99
+ if (isinstance(e, interface.SandboxStateError)
100
+ and e not in self._state_errors):
99
101
  self._state_errors.append(e)
100
102
 
101
103
  def _setup_features(self) -> None:
@@ -133,7 +135,7 @@ class BaseSandbox(interface.Sandbox):
133
135
  try:
134
136
  feature.teardown()
135
137
  except BaseException as e: # pylint: disable=broad-except
136
- self._maybe_report_state_error(e)
138
+ self.report_maybe_state_error(e)
137
139
  errors[feature.name] = e
138
140
  if errors:
139
141
  return interface.FeatureTeardownError(sandbox=self, errors=errors)
@@ -167,7 +169,7 @@ class BaseSandbox(interface.Sandbox):
167
169
  try:
168
170
  feature.teardown_session()
169
171
  except BaseException as e: # pylint: disable=broad-except
170
- self._maybe_report_state_error(e)
172
+ self.report_maybe_state_error(e)
171
173
  feature_teardown_errors[name] = e
172
174
 
173
175
  return interface.SessionTeardownError(
@@ -190,7 +192,6 @@ class BaseSandbox(interface.Sandbox):
190
192
  for name, feature in self.environment.features.items()
191
193
  })
192
194
  self._event_handlers = []
193
-
194
195
  self._enable_pre_session_setup = (
195
196
  self.reusable and self.proactive_session_setup
196
197
  )
@@ -202,18 +203,24 @@ class BaseSandbox(interface.Sandbox):
202
203
  )
203
204
  )
204
205
  self._housekeep_thread = None
205
- self._housekeep_count = 0
206
+ self._housekeep_counter = 0
206
207
 
207
208
  # Runtime state.
208
209
  self._status = self.Status.CREATED
210
+ self._status_start_time = time.time()
211
+
209
212
  self._start_time = None
210
213
  self._state_errors = []
214
+
211
215
  self._features_with_setup_called = set()
212
216
  self._features_with_setup_session_called = set()
213
217
 
214
218
  self._session_id = None
215
219
  self._session_start_time = None
216
220
 
221
+ # Thread local state for this sandbox.
222
+ self._tls_state = threading.local()
223
+
217
224
  @functools.cached_property
218
225
  def working_dir(self) -> str | None:
219
226
  """Returns the working directory for the sandbox."""
@@ -228,6 +235,11 @@ class BaseSandbox(interface.Sandbox):
228
235
  """Marks the sandbox as acquired."""
229
236
  self._set_status(self.Status.ACQUIRED)
230
237
 
238
+ @property
239
+ def housekeep_counter(self) -> int:
240
+ """Returns the housekeeping counter."""
241
+ return self._housekeep_counter
242
+
231
243
  def add_event_handler(
232
244
  self,
233
245
  event_handler: interface.EnvironmentEventHandler | None
@@ -247,11 +259,40 @@ class BaseSandbox(interface.Sandbox):
247
259
  """Returns all errors encountered during sandbox lifecycle."""
248
260
  return self._state_errors
249
261
 
262
+ @property
263
+ def is_shutting_down(self) -> bool:
264
+ """Returns True if the sandbox is shutting down."""
265
+ return self._status == self.Status.SHUTTING_DOWN or (
266
+ self._state_errors and self._status == self.Status.EXITING_SESSION
267
+ )
268
+
250
269
  @property
251
270
  def features(self) -> dict[str, interface.Feature]:
252
271
  """Returns the features in the sandbox."""
253
272
  return self._features
254
273
 
274
+ def _enter_service_call(self) -> bool:
275
+ """Enters a service call.
276
+
277
+ Returns:
278
+ True if the service call is at the top of the call stack.
279
+ """
280
+ v = getattr(self._tls_state, 'service_call_depth', None)
281
+ if v is None:
282
+ v = 0
283
+ setattr(self._tls_state, 'service_call_depth', v + 1)
284
+ return v == 0
285
+
286
+ def _exit_service_call(self) -> bool:
287
+ """Exits a service call.
288
+
289
+ Returns:
290
+ True if the service call is at the top of the call stack.
291
+ """
292
+ v = getattr(self._tls_state, 'service_call_depth')
293
+ setattr(self._tls_state, 'service_call_depth', v - 1)
294
+ return v == 1
295
+
255
296
  #
256
297
  # Sandbox start/shutdown.
257
298
  #
@@ -289,7 +330,7 @@ class BaseSandbox(interface.Sandbox):
289
330
  f'it is in {self._status} status.'
290
331
  )
291
332
 
292
- t = time.time()
333
+ starting_time = time.time()
293
334
  self._state = self.Status.SETTING_UP
294
335
 
295
336
  try:
@@ -314,18 +355,20 @@ class BaseSandbox(interface.Sandbox):
314
355
  # Mark the sandbox as ready when all setup succeeds.
315
356
  self._set_status(self.Status.READY)
316
357
 
317
- self.on_start()
358
+ duration = time.time() - starting_time
359
+ self.on_start(duration)
318
360
  pg.logging.info(
319
361
  '[%s]: Sandbox started in %.2f seconds.',
320
- self.id, time.time() - t
362
+ self.id, duration
321
363
  )
322
364
  except BaseException as e: # pylint: disable=broad-except
365
+ duration = time.time() - starting_time
323
366
  pg.logging.error(
324
- '[%s]: Sandbox failed to start: %s',
325
- self.id, e
367
+ '[%s]: Sandbox failed to start in %.2f seconds: %s',
368
+ self.id, duration, e
326
369
  )
327
- self._maybe_report_state_error(e)
328
- self.on_start(e)
370
+ self.report_maybe_state_error(e)
371
+ self.on_start(duration, e)
329
372
  self.shutdown()
330
373
  raise e
331
374
 
@@ -403,7 +446,7 @@ class BaseSandbox(interface.Sandbox):
403
446
  shutdown_error = None
404
447
  except BaseException as e: # pylint: disable=broad-except
405
448
  shutdown_error = e
406
- self._maybe_report_state_error(e)
449
+ self.report_maybe_state_error(e)
407
450
  self._set_status(interface.Sandbox.Status.OFFLINE)
408
451
  pg.logging.error(
409
452
  '[%s]: Sandbox shutdown with error: %s',
@@ -496,10 +539,12 @@ class BaseSandbox(interface.Sandbox):
496
539
  try:
497
540
  self._start_session()
498
541
  self._set_status(self.Status.IN_SESSION)
499
- self.on_session_start(session_id)
542
+ self.on_session_start(session_id, time.time() - self._session_start_time)
500
543
  except BaseException as e: # pylint: disable=broad-except
501
- self._maybe_report_state_error(e)
502
- self.on_session_start(session_id, e)
544
+ self.report_maybe_state_error(e)
545
+ self.on_session_start(
546
+ session_id, time.time() - self._session_start_time, e
547
+ )
503
548
  self.shutdown()
504
549
  raise e
505
550
 
@@ -507,15 +552,19 @@ class BaseSandbox(interface.Sandbox):
507
552
  """Ends the user session with the sandbox.
508
553
 
509
554
  State transitions:
510
- IN_SESSION -> READY: When user session exits normally, and sandbox is set
511
- to reuse.
512
- IN_SESSION -> SHUTTING_DOWN -> OFFLINE: When user session exits while
555
+ IN_SESSION -> EXITING_SESSION -> READY: When user session exits normally,
556
+ and sandbox is set to reuse.
557
+ IN_SESSION -> EXITING_SESSION -> SHUTTING_DOWN -> OFFLINE: When user
558
+ session exits while
513
559
  sandbox is set not to reuse, or session teardown fails.
514
- IN_SESSION -> SETTING_UP -> READY: When user session exits normally, and
515
- sandbox is set to reuse, and proactive session setup is enabled.
516
- IN_SESSION -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When user session
517
- exits normally, and proactive session setup is enabled but fails.
518
- not (IN_SESSION) -> same state: No operation
560
+ IN_SESSION -> EXITING_SESSION -> SETTING_UP -> READY: When user session
561
+ exits normally, and sandbox is set to reuse, and proactive session setup
562
+ is enabled.
563
+ IN_SESSION -> EXITING_SESSION -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE:
564
+ When user session exits normally, and proactive session setup is enabled
565
+ but fails.
566
+ EXITING_SESSION -> EXITING_SESSION: No operation.
567
+ not IN_SESSION -> same state: No operation
519
568
 
520
569
  `end_session` should always be called for each `start_session` call, even
521
570
  when the session fails to start, to ensure proper cleanup.
@@ -541,6 +590,9 @@ class BaseSandbox(interface.Sandbox):
541
590
  Raises:
542
591
  BaseException: If session teardown failed with user-defined errors.
543
592
  """
593
+ if self._status == self.Status.EXITING_SESSION:
594
+ return
595
+
544
596
  if self._status not in (
545
597
  self.Status.IN_SESSION,
546
598
  ):
@@ -549,6 +601,8 @@ class BaseSandbox(interface.Sandbox):
549
601
  assert self._session_id is not None, (
550
602
  'No user session is active for this sandbox'
551
603
  )
604
+ # Set sandbox status to EXITING_SESSION to avoid re-entry.
605
+ self._set_status(self.Status.EXITING_SESSION)
552
606
  shutdown_sandbox = shutdown_sandbox or not self.reusable
553
607
 
554
608
  # Teardown features for the current session.
@@ -572,7 +626,7 @@ class BaseSandbox(interface.Sandbox):
572
626
  self.id,
573
627
  e
574
628
  )
575
- self._maybe_report_state_error(e)
629
+ self.report_maybe_state_error(e)
576
630
  self.shutdown()
577
631
 
578
632
  # End session before setting up the next session.
@@ -634,6 +688,7 @@ class BaseSandbox(interface.Sandbox):
634
688
  last_housekeep_time = {name: now for name in self._features.keys()}
635
689
 
636
690
  while self._status not in (self.Status.SHUTTING_DOWN, self.Status.OFFLINE):
691
+ housekeep_start = time.time()
637
692
  if self.keepalive_interval is not None:
638
693
  if time.time() - last_ping > self.keepalive_interval:
639
694
  try:
@@ -645,8 +700,9 @@ class BaseSandbox(interface.Sandbox):
645
700
  self.id,
646
701
  str(e)
647
702
  )
648
- self._housekeep_count += 1
649
- self._maybe_report_state_error(e)
703
+ self._housekeep_counter += 1
704
+ self.report_maybe_state_error(e)
705
+ self.on_housekeep(time.time() - housekeep_start, e)
650
706
  self.shutdown()
651
707
  break
652
708
  last_ping = time.time()
@@ -667,20 +723,28 @@ class BaseSandbox(interface.Sandbox):
667
723
  feature.name,
668
724
  e,
669
725
  )
670
- self._maybe_report_state_error(e)
726
+ self.report_maybe_state_error(e)
727
+ self._housekeep_counter += 1
728
+ self.on_housekeep(time.time() - housekeep_start, e)
671
729
  self.shutdown()
672
730
  break
673
- self._housekeep_count += 1
731
+
732
+ self._housekeep_counter += 1
733
+ self.on_housekeep(time.time() - housekeep_start)
674
734
  time.sleep(1)
675
735
 
676
736
  #
677
737
  # Event handlers subclasses can override.
678
738
  #
679
739
 
680
- def on_start(self, error: BaseException | None = None) -> None:
740
+ def on_start(
741
+ self,
742
+ duration: float,
743
+ error: BaseException | None = None
744
+ ) -> None:
681
745
  """Called when the sandbox is started."""
682
746
  for handler in self._event_handlers:
683
- handler.on_sandbox_start(self.environment, self, error)
747
+ handler.on_sandbox_start(self.environment, self, duration, error)
684
748
 
685
749
  def on_status_change(
686
750
  self,
@@ -690,96 +754,124 @@ class BaseSandbox(interface.Sandbox):
690
754
  """Called when the sandbox status changes."""
691
755
  for handler in self._event_handlers:
692
756
  handler.on_sandbox_status_change(
693
- self.environment, self, old_status, new_status
757
+ self.environment,
758
+ self,
759
+ old_status,
760
+ new_status,
761
+ time.time() - self._status_start_time
694
762
  )
695
763
 
696
764
  def on_shutdown(self, error: BaseException | None = None) -> None:
697
765
  """Called when the sandbox is shutdown."""
766
+ if self._start_time is None:
767
+ lifetime = 0.0
768
+ else:
769
+ lifetime = time.time() - self._start_time
770
+ for handler in self._event_handlers:
771
+ handler.on_sandbox_shutdown(self.environment, self, lifetime, error)
772
+
773
+ def on_housekeep(
774
+ self,
775
+ duration: float,
776
+ error: BaseException | None = None
777
+ ) -> None:
778
+ """Called when the sandbox finishes a round of housekeeping."""
779
+ counter = self._housekeep_counter
698
780
  for handler in self._event_handlers:
699
- handler.on_sandbox_shutdown(self.environment, self, error)
781
+ handler.on_sandbox_housekeep(
782
+ self.environment, self, counter, duration, error
783
+ )
700
784
 
701
785
  def on_feature_setup(
702
786
  self,
703
787
  feature: interface.Feature,
788
+ duration: float,
704
789
  error: BaseException | None = None
705
790
  ) -> None:
706
791
  """Called when a feature is setup."""
707
792
  for handler in self._event_handlers:
708
793
  handler.on_feature_setup(
709
- self.environment, self, feature, error
794
+ self.environment, self, feature, duration, error
710
795
  )
711
796
 
712
797
  def on_feature_teardown(
713
798
  self,
714
799
  feature: interface.Feature,
800
+ duration: float,
715
801
  error: BaseException | None = None
716
802
  ) -> None:
717
803
  """Called when a feature is teardown."""
718
804
  for handler in self._event_handlers:
719
805
  handler.on_feature_teardown(
720
- self.environment, self, feature, error
806
+ self.environment, self, feature, duration, error
721
807
  )
722
808
 
723
809
  def on_feature_setup_session(
724
810
  self,
725
811
  feature: interface.Feature,
812
+ duration: float,
726
813
  error: BaseException | None = None
727
814
  ) -> None:
728
815
  """Called when a feature is setup for a user session."""
729
816
  for handler in self._event_handlers:
730
817
  handler.on_feature_setup_session(
731
- self.environment, self, feature, self.session_id, error
818
+ self.environment, self, feature, self.session_id, duration, error
732
819
  )
733
820
 
734
821
  def on_feature_teardown_session(
735
822
  self,
736
823
  feature: interface.Feature,
824
+ duration: float,
737
825
  error: BaseException | None = None
738
826
  ) -> None:
739
827
  """Called when a feature is teardown for a user session."""
740
828
  for handler in self._event_handlers:
741
829
  handler.on_feature_teardown_session(
742
- self.environment, self, feature, self.session_id, error
830
+ self.environment, self, feature, self.session_id, duration, error
743
831
  )
744
832
 
745
833
  def on_feature_housekeep(
746
834
  self,
747
835
  feature: interface.Feature,
836
+ counter: int,
837
+ duration: float,
748
838
  error: BaseException | None = None
749
839
  ) -> None:
750
840
  """Called when a feature is housekeeping."""
751
841
  for handler in self._event_handlers:
752
842
  handler.on_feature_housekeep(
753
- self.environment, self, feature, error
843
+ self.environment, self, feature, counter, duration, error
754
844
  )
755
845
 
756
846
  def on_session_start(
757
847
  self,
758
848
  session_id: str,
849
+ duration: float,
759
850
  error: BaseException | None = None
760
851
  ) -> None:
761
852
  """Called when the user session starts."""
762
853
  for handler in self._event_handlers:
763
854
  handler.on_session_start(
764
- self.environment, self, session_id, error
855
+ self.environment, self, session_id, duration, error
765
856
  )
766
857
 
767
- def on_session_activity(
858
+ def on_activity(
768
859
  self,
769
- session_id: str,
770
860
  name: str,
861
+ duration: float,
771
862
  feature: interface.Feature | None = None,
772
863
  error: BaseException | None = None,
773
864
  **kwargs
774
865
  ) -> None:
775
866
  """Called when a sandbox activity is performed."""
776
867
  for handler in self._event_handlers:
777
- handler.on_session_activity(
778
- session_id=session_id,
868
+ handler.on_sandbox_activity(
779
869
  name=name,
780
870
  environment=self.environment,
781
871
  sandbox=self,
782
872
  feature=feature,
873
+ session_id=self.session_id,
874
+ duration=duration,
783
875
  error=error,
784
876
  **kwargs
785
877
  )
@@ -790,9 +882,10 @@ class BaseSandbox(interface.Sandbox):
790
882
  error: BaseException | None = None
791
883
  ) -> None:
792
884
  """Called when the user session ends."""
885
+ lifetime = time.time() - self._session_start_time
793
886
  for handler in self._event_handlers:
794
887
  handler.on_session_end(
795
- self.environment, self, session_id, error
888
+ self.environment, self, session_id, lifetime, error
796
889
  )
797
890
 
798
891
 
@@ -876,9 +969,14 @@ def sandbox_service(
876
969
  @functools.wraps(func)
877
970
  def method_wrapper(self, *args, **kwargs) -> Any:
878
971
  """Helper function to safely execute logics in the sandbox."""
972
+
879
973
  assert isinstance(self, (BaseSandbox, interface.Feature)), self
880
974
  sandbox = self.sandbox if isinstance(self, interface.Feature) else self
881
975
 
976
+ # We count the service call stack depth so we could shutdown the sandbox
977
+ # at the top upon sandbox state error.
978
+ sandbox._enter_service_call() # pylint: disable=protected-access
979
+
882
980
  # When a capability is directly accessed from the environment,
883
981
  # we create a new session for the capability call. This
884
982
  # prevents the sandbox from being reused for other feature calls.
@@ -895,70 +993,102 @@ def sandbox_service(
895
993
  new_session = False
896
994
 
897
995
  kwargs.pop('session_id', None)
898
- session_id = sandbox.session_id
899
996
  result = None
900
- state_error = None
901
997
  error = None
998
+ start_time = time.time()
902
999
 
903
1000
  try:
904
1001
  # Execute the service function.
905
1002
  result = func(self, *args, **kwargs)
906
1003
 
907
- # If the result is a context manager, use it and end the session
908
- # afterwards.
909
- if new_session and isinstance(
910
- result, contextlib.AbstractContextManager
911
- ):
912
- return _end_session_when_exit(result, sandbox)
1004
+ # If the result is a context manager, wrap it with a context manager
1005
+ # to end the session when exiting.
1006
+ if isinstance(result, contextlib.AbstractContextManager):
1007
+ return _service_context_manager_wrapper(
1008
+ service=result,
1009
+ sandbox_or_feature=self,
1010
+ sandbox=sandbox,
1011
+ name=func.__name__,
1012
+ kwargs=to_kwargs(*args, **kwargs),
1013
+ start_time=start_time,
1014
+ new_session=new_session
1015
+ )
913
1016
 
914
1017
  # Otherwise, return the result and end the session in the finally block.
915
1018
  return result
916
- except interface.SandboxStateError as e:
917
- sandbox._maybe_report_state_error(e) # pylint: disable=protected-access
918
- state_error = e
919
- error = e
920
- raise
921
1019
  except BaseException as e:
922
1020
  error = e
1021
+ sandbox.report_maybe_state_error(e)
923
1022
  if pg.match_error(e, critical_errors):
924
1023
  state_error = interface.SandboxStateError(
925
1024
  'Sandbox encountered an unexpected error executing '
926
1025
  f'`{func.__name__}` (args={args!r}, kwargs={kwargs!r}): {e}',
927
1026
  sandbox=self
928
1027
  )
929
- sandbox._maybe_report_state_error(state_error) # pylint: disable=protected-access
1028
+ sandbox.report_maybe_state_error(state_error)
930
1029
  raise state_error from e
931
1030
  raise
932
1031
  finally:
933
- if session_id is not None:
934
- self.on_session_activity(
1032
+ is_topmost_call = sandbox._exit_service_call() # pylint: disable=protected-access
1033
+ if not isinstance(result, contextlib.AbstractContextManager):
1034
+ self.on_activity(
935
1035
  name=func.__name__,
936
- session_id=session_id,
1036
+ duration=time.time() - start_time,
937
1037
  error=error,
938
1038
  **to_kwargs(*args, **kwargs),
939
1039
  )
940
-
941
- if state_error is not None:
1040
+ if new_session:
1041
+ assert is_topmost_call
1042
+
1043
+ # End the session if it's from a feature method and the result
1044
+ # is not a context manager.
1045
+ sandbox.end_session()
1046
+
1047
+ # Shutdown the sandbox if it is at the top of the service call stack and
1048
+ # has state errors.
1049
+ if (is_topmost_call
1050
+ and sandbox.state_errors
1051
+ # Sandbox service method might be called during shutting down, in
1052
+ # that case we don't want to shutdown the sandbox again.
1053
+ and not sandbox.is_shutting_down):
942
1054
  sandbox.shutdown()
943
- elif (new_session
944
- and not isinstance(result, contextlib.AbstractContextManager)):
945
- # End the session if it's from a feature method and the result is not
946
- # a context manager.
947
- sandbox.end_session(
948
- shutdown_sandbox=isinstance(error, interface.SandboxStateError)
949
- )
1055
+
950
1056
  return method_wrapper
951
1057
  return decorator
952
1058
 
953
1059
 
954
1060
  @contextlib.contextmanager
955
- def _end_session_when_exit(
1061
+ def _service_context_manager_wrapper(
956
1062
  service: contextlib.AbstractContextManager[Any],
957
- sandbox: interface.Sandbox
1063
+ sandbox_or_feature: BaseSandbox | interface.Feature,
1064
+ sandbox: interface.Sandbox,
1065
+ name: str,
1066
+ kwargs: dict[str, Any],
1067
+ new_session: bool,
1068
+ start_time: float,
958
1069
  ) -> Iterator[Any]:
959
1070
  """Context manager wrapper for ending a sandbox session when exiting."""
1071
+ error = None
1072
+ sandbox._enter_service_call() # pylint: disable=protected-access
1073
+
960
1074
  try:
961
1075
  with service as result:
962
1076
  yield result
1077
+ except BaseException as e:
1078
+ error = e
1079
+ sandbox.report_maybe_state_error(error)
1080
+ raise
963
1081
  finally:
964
- sandbox.end_session()
1082
+ sandbox_or_feature.on_activity(
1083
+ name=name,
1084
+ error=error,
1085
+ duration=time.time() - start_time,
1086
+ **kwargs,
1087
+ )
1088
+ is_topmost_call = sandbox._exit_service_call() # pylint: disable=protected-access
1089
+
1090
+ if new_session:
1091
+ assert is_topmost_call
1092
+ sandbox.end_session()
1093
+ elif isinstance(error, interface.SandboxStateError):
1094
+ sandbox.shutdown()