langfun 0.1.2.dev202509230805__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.
- langfun/env/__init__.py +1 -1
- langfun/env/base_environment.py +70 -39
- langfun/env/base_feature.py +25 -12
- langfun/env/base_sandbox.py +204 -74
- langfun/env/base_test.py +166 -399
- langfun/env/interface.py +167 -35
- langfun/env/test_utils.py +473 -0
- {langfun-0.1.2.dev202509230805.dist-info → langfun-0.1.2.dev202509250804.dist-info}/METADATA +1 -1
- {langfun-0.1.2.dev202509230805.dist-info → langfun-0.1.2.dev202509250804.dist-info}/RECORD +12 -11
- {langfun-0.1.2.dev202509230805.dist-info → langfun-0.1.2.dev202509250804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202509230805.dist-info → langfun-0.1.2.dev202509250804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202509230805.dist-info → langfun-0.1.2.dev202509250804.dist-info}/top_level.txt +0 -0
langfun/env/base_sandbox.py
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
502
|
-
self.on_session_start(
|
|
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,
|
|
511
|
-
to reuse.
|
|
512
|
-
IN_SESSION -> SHUTTING_DOWN -> OFFLINE: When user
|
|
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
|
|
515
|
-
sandbox is set to reuse, and proactive session setup
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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.
|
|
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.
|
|
649
|
-
self.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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.
|
|
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
|
|
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.
|
|
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,
|
|
908
|
-
#
|
|
909
|
-
if
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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.
|
|
1028
|
+
sandbox.report_maybe_state_error(state_error)
|
|
930
1029
|
raise state_error from e
|
|
931
1030
|
raise
|
|
932
1031
|
finally:
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
1036
|
+
duration=time.time() - start_time,
|
|
937
1037
|
error=error,
|
|
938
1038
|
**to_kwargs(*args, **kwargs),
|
|
939
1039
|
)
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
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
|
|
1061
|
+
def _service_context_manager_wrapper(
|
|
956
1062
|
service: contextlib.AbstractContextManager[Any],
|
|
957
|
-
|
|
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
|
-
|
|
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()
|