langfun 0.1.2.dev202509120804__py3-none-any.whl → 0.1.2.dev202512150805__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.
- langfun/__init__.py +1 -1
- langfun/core/__init__.py +7 -1
- langfun/core/agentic/__init__.py +8 -1
- langfun/core/agentic/action.py +740 -112
- langfun/core/agentic/action_eval.py +9 -2
- langfun/core/agentic/action_test.py +189 -24
- langfun/core/async_support.py +104 -5
- langfun/core/async_support_test.py +23 -0
- langfun/core/coding/python/correction.py +19 -9
- langfun/core/coding/python/execution.py +14 -12
- langfun/core/coding/python/generation.py +21 -16
- langfun/core/coding/python/sandboxing.py +23 -3
- langfun/core/component.py +42 -3
- langfun/core/concurrent.py +70 -6
- langfun/core/concurrent_test.py +9 -2
- langfun/core/console.py +1 -1
- langfun/core/data/conversion/anthropic.py +12 -3
- langfun/core/data/conversion/anthropic_test.py +8 -6
- langfun/core/data/conversion/gemini.py +11 -2
- langfun/core/data/conversion/gemini_test.py +48 -9
- langfun/core/data/conversion/openai.py +145 -31
- langfun/core/data/conversion/openai_test.py +161 -17
- langfun/core/eval/base.py +48 -44
- langfun/core/eval/base_test.py +5 -5
- langfun/core/eval/matching.py +5 -2
- langfun/core/eval/patching.py +3 -3
- langfun/core/eval/scoring.py +4 -3
- langfun/core/eval/v2/__init__.py +3 -0
- langfun/core/eval/v2/checkpointing.py +148 -46
- langfun/core/eval/v2/checkpointing_test.py +9 -2
- langfun/core/eval/v2/config_saver.py +37 -0
- langfun/core/eval/v2/config_saver_test.py +36 -0
- langfun/core/eval/v2/eval_test_helper.py +104 -3
- langfun/core/eval/v2/evaluation.py +102 -19
- langfun/core/eval/v2/evaluation_test.py +9 -3
- langfun/core/eval/v2/example.py +50 -40
- langfun/core/eval/v2/example_test.py +16 -8
- langfun/core/eval/v2/experiment.py +95 -20
- langfun/core/eval/v2/experiment_test.py +19 -0
- langfun/core/eval/v2/metric_values.py +31 -3
- langfun/core/eval/v2/metric_values_test.py +32 -0
- langfun/core/eval/v2/metrics.py +157 -44
- langfun/core/eval/v2/metrics_test.py +39 -18
- langfun/core/eval/v2/progress.py +31 -1
- langfun/core/eval/v2/progress_test.py +27 -0
- langfun/core/eval/v2/progress_tracking.py +13 -5
- langfun/core/eval/v2/progress_tracking_test.py +9 -1
- langfun/core/eval/v2/reporting.py +88 -71
- langfun/core/eval/v2/reporting_test.py +24 -6
- langfun/core/eval/v2/runners/__init__.py +30 -0
- langfun/core/eval/v2/{runners.py → runners/base.py} +73 -180
- langfun/core/eval/v2/runners/beam.py +354 -0
- langfun/core/eval/v2/runners/beam_test.py +153 -0
- langfun/core/eval/v2/runners/ckpt_monitor.py +350 -0
- langfun/core/eval/v2/runners/ckpt_monitor_test.py +213 -0
- langfun/core/eval/v2/runners/debug.py +40 -0
- langfun/core/eval/v2/runners/debug_test.py +76 -0
- langfun/core/eval/v2/runners/parallel.py +243 -0
- langfun/core/eval/v2/runners/parallel_test.py +182 -0
- langfun/core/eval/v2/runners/sequential.py +47 -0
- langfun/core/eval/v2/runners/sequential_test.py +169 -0
- langfun/core/langfunc.py +45 -130
- langfun/core/langfunc_test.py +7 -5
- langfun/core/language_model.py +189 -36
- langfun/core/language_model_test.py +54 -3
- langfun/core/llms/__init__.py +14 -1
- langfun/core/llms/anthropic.py +157 -2
- langfun/core/llms/azure_openai.py +29 -17
- langfun/core/llms/cache/base.py +25 -3
- langfun/core/llms/cache/in_memory.py +48 -7
- langfun/core/llms/cache/in_memory_test.py +14 -4
- langfun/core/llms/compositional.py +25 -1
- langfun/core/llms/deepseek.py +30 -2
- langfun/core/llms/fake.py +32 -1
- langfun/core/llms/gemini.py +90 -12
- langfun/core/llms/gemini_test.py +110 -0
- langfun/core/llms/google_genai.py +52 -1
- langfun/core/llms/groq.py +28 -3
- langfun/core/llms/llama_cpp.py +23 -4
- langfun/core/llms/openai.py +120 -3
- langfun/core/llms/openai_compatible.py +148 -27
- langfun/core/llms/openai_compatible_test.py +207 -20
- langfun/core/llms/openai_test.py +0 -2
- langfun/core/llms/rest.py +16 -1
- langfun/core/llms/vertexai.py +78 -8
- langfun/core/logging.py +1 -1
- langfun/core/mcp/__init__.py +10 -0
- langfun/core/mcp/client.py +177 -0
- langfun/core/mcp/client_test.py +71 -0
- langfun/core/mcp/session.py +241 -0
- langfun/core/mcp/session_test.py +54 -0
- langfun/core/mcp/testing/simple_mcp_client.py +33 -0
- langfun/core/mcp/testing/simple_mcp_server.py +33 -0
- langfun/core/mcp/tool.py +254 -0
- langfun/core/mcp/tool_test.py +197 -0
- langfun/core/memory.py +1 -0
- langfun/core/message.py +160 -55
- langfun/core/message_test.py +65 -81
- langfun/core/modalities/__init__.py +8 -0
- langfun/core/modalities/audio.py +21 -1
- langfun/core/modalities/image.py +73 -3
- langfun/core/modalities/image_test.py +116 -0
- langfun/core/modalities/mime.py +78 -4
- langfun/core/modalities/mime_test.py +59 -0
- langfun/core/modalities/pdf.py +19 -1
- langfun/core/modalities/video.py +21 -1
- langfun/core/modality.py +167 -29
- langfun/core/modality_test.py +42 -12
- langfun/core/natural_language.py +1 -1
- langfun/core/sampling.py +4 -4
- langfun/core/sampling_test.py +20 -4
- langfun/core/structured/__init__.py +2 -24
- langfun/core/structured/completion.py +34 -44
- langfun/core/structured/completion_test.py +23 -43
- langfun/core/structured/description.py +54 -50
- langfun/core/structured/function_generation.py +29 -12
- langfun/core/structured/mapping.py +81 -37
- langfun/core/structured/parsing.py +95 -79
- langfun/core/structured/parsing_test.py +0 -3
- langfun/core/structured/querying.py +230 -154
- langfun/core/structured/querying_test.py +69 -33
- langfun/core/structured/schema/__init__.py +49 -0
- langfun/core/structured/schema/base.py +664 -0
- langfun/core/structured/schema/base_test.py +531 -0
- langfun/core/structured/schema/json.py +174 -0
- langfun/core/structured/schema/json_test.py +121 -0
- langfun/core/structured/schema/python.py +316 -0
- langfun/core/structured/schema/python_test.py +410 -0
- langfun/core/structured/schema_generation.py +33 -14
- langfun/core/structured/scoring.py +47 -36
- langfun/core/structured/tokenization.py +26 -11
- langfun/core/subscription.py +2 -2
- langfun/core/template.py +175 -50
- langfun/core/template_test.py +123 -17
- langfun/env/__init__.py +43 -0
- langfun/env/base_environment.py +827 -0
- langfun/env/base_environment_test.py +473 -0
- langfun/env/base_feature.py +304 -0
- langfun/env/base_feature_test.py +228 -0
- langfun/env/base_sandbox.py +842 -0
- langfun/env/base_sandbox_test.py +1235 -0
- langfun/env/event_handlers/__init__.py +14 -0
- langfun/env/event_handlers/chain.py +233 -0
- langfun/env/event_handlers/chain_test.py +253 -0
- langfun/env/event_handlers/event_logger.py +472 -0
- langfun/env/event_handlers/event_logger_test.py +304 -0
- langfun/env/event_handlers/metric_writer.py +726 -0
- langfun/env/event_handlers/metric_writer_test.py +214 -0
- langfun/env/interface.py +1640 -0
- langfun/env/interface_test.py +153 -0
- langfun/env/load_balancers.py +59 -0
- langfun/env/load_balancers_test.py +141 -0
- langfun/env/test_utils.py +507 -0
- {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512150805.dist-info}/METADATA +7 -3
- langfun-0.1.2.dev202512150805.dist-info/RECORD +217 -0
- langfun/core/eval/v2/runners_test.py +0 -343
- langfun/core/structured/schema.py +0 -987
- langfun/core/structured/schema_test.py +0 -982
- langfun-0.1.2.dev202509120804.dist-info/RECORD +0 -172
- {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512150805.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512150805.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512150805.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
# Copyright 2025 The Langfun Authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Common base class for sandboxes.
|
|
15
|
+
|
|
16
|
+
This module provides an base class `BaseSandbox` for sandboxes, which takes care
|
|
17
|
+
of the sandbox lifecycle and housekeeping. It also provides a decorator for
|
|
18
|
+
sandbox service methods to handle errors and trigger shutdowns. Please note that
|
|
19
|
+
this base class is intended to provide a convenient way to implement sandboxes,
|
|
20
|
+
and not all sandbox implementations need to subclass it. Also `BaseSandbox`
|
|
21
|
+
is not coupled with `BaseEnvironment` and `BaseFeature`, and is expected to
|
|
22
|
+
work with the `Environment` and `Feature` interfaces directly.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import contextlib
|
|
26
|
+
import functools
|
|
27
|
+
import threading
|
|
28
|
+
import time
|
|
29
|
+
from typing import Annotated, Any, Iterator
|
|
30
|
+
|
|
31
|
+
from langfun.env import interface
|
|
32
|
+
import pyglove as pg
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseSandbox(interface.Sandbox):
|
|
36
|
+
"""Base class for a sandbox."""
|
|
37
|
+
|
|
38
|
+
id: Annotated[
|
|
39
|
+
interface.Sandbox.Id,
|
|
40
|
+
'The identifier for the sandbox.'
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
image_id: Annotated[
|
|
44
|
+
str,
|
|
45
|
+
'The image id for the sandbox.'
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
environment: Annotated[
|
|
49
|
+
pg.Ref[interface.Environment],
|
|
50
|
+
'The parent environment.'
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
reusable: Annotated[
|
|
54
|
+
bool,
|
|
55
|
+
(
|
|
56
|
+
'If True, the sandbox can be reused for multiple user sessions at '
|
|
57
|
+
'different times.'
|
|
58
|
+
)
|
|
59
|
+
] = False
|
|
60
|
+
|
|
61
|
+
keepalive_interval: Annotated[
|
|
62
|
+
float | None,
|
|
63
|
+
'Interval to ping the sandbox for keeping it alive..'
|
|
64
|
+
] = 60.0
|
|
65
|
+
|
|
66
|
+
proactive_session_setup: Annotated[
|
|
67
|
+
bool,
|
|
68
|
+
(
|
|
69
|
+
'If True, the sandbox will perform setup work before a user session '
|
|
70
|
+
'is started. This is useful for sandboxes that need to perform '
|
|
71
|
+
'heavy setup work, which could block the user thread for a long '
|
|
72
|
+
'time. Applicable only when `reusable` is True.'
|
|
73
|
+
)
|
|
74
|
+
] = True
|
|
75
|
+
|
|
76
|
+
#
|
|
77
|
+
# There is no required methods that subclasses must implement.
|
|
78
|
+
# Subclasses can override the following methods:
|
|
79
|
+
#
|
|
80
|
+
|
|
81
|
+
def _start(self) -> None:
|
|
82
|
+
"""Implementation of start(). Subclasses can override.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
interface.SandboxStateError: If the sandbox is in a bad state.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def _shutdown(self) -> None:
|
|
89
|
+
"""Implementation of shutdown(). Subclasses can override.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
interface.SandboxStateError: If the sandbox is in a bad state.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def _set_status(self, status: interface.Sandbox.Status) -> None:
|
|
96
|
+
"""Sets the status of the sandbox."""
|
|
97
|
+
assert self._status != status, (self._status, status)
|
|
98
|
+
self.on_status_change(self._status, status)
|
|
99
|
+
self._status = status
|
|
100
|
+
self._status_start_time = time.time()
|
|
101
|
+
|
|
102
|
+
def report_state_error(self, e: interface.SandboxStateError) -> None:
|
|
103
|
+
"""Reports sandbox state errors."""
|
|
104
|
+
if e not in self._state_errors:
|
|
105
|
+
self._state_errors.append(e)
|
|
106
|
+
|
|
107
|
+
def _setup_features(self) -> None:
|
|
108
|
+
"""Starts the features in the sandbox."""
|
|
109
|
+
# We keep track of the features that have setup called so we can teardown
|
|
110
|
+
# the features when the sandbox is shutdown.
|
|
111
|
+
self._features_with_setup_called.clear()
|
|
112
|
+
|
|
113
|
+
for feature in self._features.values():
|
|
114
|
+
self._features_with_setup_called.add(feature.name)
|
|
115
|
+
feature.setup(self)
|
|
116
|
+
|
|
117
|
+
def _setup_session(self) -> None:
|
|
118
|
+
"""Sets up a new session for the sandbox."""
|
|
119
|
+
# We keep track of the features that have setup_session called so we can
|
|
120
|
+
# teardown the session for them when the session ends.
|
|
121
|
+
self._features_with_setup_session_called.clear()
|
|
122
|
+
|
|
123
|
+
for feature in self._features.values():
|
|
124
|
+
self._features_with_setup_session_called.add(feature.name)
|
|
125
|
+
feature.setup_session()
|
|
126
|
+
|
|
127
|
+
def _teardown_features(self) -> interface.FeatureTeardownError | None:
|
|
128
|
+
"""Tears down the features in the sandbox.
|
|
129
|
+
|
|
130
|
+
IMPORTANT: This method shall not raise any exceptions.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
FeatureTeardownError: If feature teardown failed with errors.
|
|
134
|
+
Otherwise None.
|
|
135
|
+
"""
|
|
136
|
+
errors = {}
|
|
137
|
+
for feature in self._features.values():
|
|
138
|
+
if feature.name in self._features_with_setup_called:
|
|
139
|
+
try:
|
|
140
|
+
feature.teardown()
|
|
141
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
142
|
+
if isinstance(e, interface.SandboxStateError):
|
|
143
|
+
self.report_state_error(e)
|
|
144
|
+
errors[feature.name] = e
|
|
145
|
+
if errors:
|
|
146
|
+
return interface.FeatureTeardownError(sandbox=self, errors=errors)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
def _start_session(self) -> None:
|
|
150
|
+
"""Starts a user session.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
BaseException: If feature setup failed with user-defined errors.
|
|
154
|
+
SandboxStateError: If sandbox or feature setup fail due sandbox state
|
|
155
|
+
errors.
|
|
156
|
+
"""
|
|
157
|
+
# When pre-session setup is enabled, the session setup is performed
|
|
158
|
+
# before the session is started. Otherwise we setup the session here.
|
|
159
|
+
if not self._enable_pre_session_setup:
|
|
160
|
+
self._setup_session()
|
|
161
|
+
|
|
162
|
+
def _end_session(self) -> interface.SessionTeardownError | None:
|
|
163
|
+
"""Ends a user session.
|
|
164
|
+
|
|
165
|
+
IMPORTANT: This method shall not raise any exceptions.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
SessionTeardownError: If session teardown failed with errors.
|
|
169
|
+
Otherwise None.
|
|
170
|
+
"""
|
|
171
|
+
feature_teardown_errors = {}
|
|
172
|
+
for name, feature in self._features.items():
|
|
173
|
+
if name in self._features_with_setup_session_called:
|
|
174
|
+
try:
|
|
175
|
+
feature.teardown_session()
|
|
176
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
177
|
+
if isinstance(e, interface.SandboxStateError):
|
|
178
|
+
self.report_state_error(e)
|
|
179
|
+
feature_teardown_errors[name] = e
|
|
180
|
+
|
|
181
|
+
return interface.SessionTeardownError(
|
|
182
|
+
sandbox=self, errors=feature_teardown_errors
|
|
183
|
+
) if feature_teardown_errors else None
|
|
184
|
+
|
|
185
|
+
def _ping(self) -> None:
|
|
186
|
+
"""Implementation of ping for health checking."""
|
|
187
|
+
|
|
188
|
+
#
|
|
189
|
+
# Sandbox basics.
|
|
190
|
+
#
|
|
191
|
+
|
|
192
|
+
def _on_bound(self) -> None:
|
|
193
|
+
"""Called when the sandbox is bound."""
|
|
194
|
+
super()._on_bound()
|
|
195
|
+
self._features = pg.Dict({
|
|
196
|
+
name: pg.clone(feature)
|
|
197
|
+
for name, feature in self.environment.features.items()
|
|
198
|
+
if feature.is_applicable(self.image_id)
|
|
199
|
+
})
|
|
200
|
+
self._event_handler = self.environment.event_handler
|
|
201
|
+
self._enable_pre_session_setup = (
|
|
202
|
+
self.reusable and self.proactive_session_setup
|
|
203
|
+
)
|
|
204
|
+
self._enables_housekeep = (
|
|
205
|
+
self.keepalive_interval is not None
|
|
206
|
+
or any(
|
|
207
|
+
feature.housekeep_interval is not None
|
|
208
|
+
for feature in self._features.values()
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
self._housekeep_thread = None
|
|
212
|
+
self._housekeep_counter = 0
|
|
213
|
+
|
|
214
|
+
# Runtime state.
|
|
215
|
+
self._status = self.Status.CREATED
|
|
216
|
+
self._status_start_time = time.time()
|
|
217
|
+
|
|
218
|
+
self._start_time = None
|
|
219
|
+
self._state_errors = []
|
|
220
|
+
|
|
221
|
+
self._features_with_setup_called = set()
|
|
222
|
+
self._features_with_setup_session_called = set()
|
|
223
|
+
|
|
224
|
+
self._session_id = None
|
|
225
|
+
self._session_start_time = None
|
|
226
|
+
|
|
227
|
+
# Thread local state for this sandbox.
|
|
228
|
+
self._tls_state = threading.local()
|
|
229
|
+
|
|
230
|
+
@functools.cached_property
|
|
231
|
+
def working_dir(self) -> str | None:
|
|
232
|
+
"""Returns the working directory for the sandbox."""
|
|
233
|
+
return self.id.working_dir(self.environment.root_dir)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def status(self) -> interface.Sandbox.Status:
|
|
237
|
+
"""Returns the state of the sandbox."""
|
|
238
|
+
return self._status
|
|
239
|
+
|
|
240
|
+
def set_acquired(self) -> None:
|
|
241
|
+
"""Marks the sandbox as acquired."""
|
|
242
|
+
self._set_status(self.Status.ACQUIRED)
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def housekeep_counter(self) -> int:
|
|
246
|
+
"""Returns the housekeeping counter."""
|
|
247
|
+
return self._housekeep_counter
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def state_errors(self) -> list[interface.SandboxStateError]:
|
|
251
|
+
"""Returns all errors encountered during sandbox lifecycle."""
|
|
252
|
+
return self._state_errors
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def is_shutting_down(self) -> bool:
|
|
256
|
+
"""Returns True if the sandbox is shutting down."""
|
|
257
|
+
return self._status == self.Status.SHUTTING_DOWN or (
|
|
258
|
+
self._state_errors and self._status == self.Status.EXITING_SESSION
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def features(self) -> dict[str, interface.Feature]:
|
|
263
|
+
"""Returns the features in the sandbox."""
|
|
264
|
+
return self._features
|
|
265
|
+
|
|
266
|
+
#
|
|
267
|
+
# Sandbox start/shutdown.
|
|
268
|
+
#
|
|
269
|
+
|
|
270
|
+
def start(self) -> None:
|
|
271
|
+
"""Starts the sandbox.
|
|
272
|
+
|
|
273
|
+
State transitions:
|
|
274
|
+
CREATED -> SETTING_UP -> READY: When all sandbox and feature setup
|
|
275
|
+
succeeds.
|
|
276
|
+
CREATED -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When sandbox or feature
|
|
277
|
+
setup fails.
|
|
278
|
+
|
|
279
|
+
`start` and `shutdown` should be called in pairs, even when the sandbox
|
|
280
|
+
fails to start. This ensures proper cleanup.
|
|
281
|
+
|
|
282
|
+
Start may fail with two sources of errors:
|
|
283
|
+
|
|
284
|
+
1. SandboxStateError: If sandbox or feature setup fail due to enviroment
|
|
285
|
+
outage or sandbox state errors.
|
|
286
|
+
2. BaseException: If feature setup failed with user-defined errors, this
|
|
287
|
+
could happen when there is bug in the user code or non-environment code
|
|
288
|
+
failure.
|
|
289
|
+
|
|
290
|
+
In both cases, the sandbox will be shutdown automatically, and the error
|
|
291
|
+
will be added to `errors`. The sandbox is considered dead and will not be
|
|
292
|
+
further used.
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
SandboxStateError: If the sandbox is in a bad state.
|
|
296
|
+
BaseException: If feature setup failed with user-defined errors.
|
|
297
|
+
"""
|
|
298
|
+
assert self._status == self.Status.CREATED, (
|
|
299
|
+
f'Sandbox {self.id} cannot be started because '
|
|
300
|
+
f'it is in {self._status} status.'
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
starting_time = time.time()
|
|
304
|
+
self._state = self.Status.SETTING_UP
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
# Start the sandbox.
|
|
308
|
+
self._start()
|
|
309
|
+
|
|
310
|
+
# Setup the features.
|
|
311
|
+
self._setup_features()
|
|
312
|
+
|
|
313
|
+
# Setup the first session if pre-session setup is enabled.
|
|
314
|
+
if self._enable_pre_session_setup:
|
|
315
|
+
self._setup_session()
|
|
316
|
+
|
|
317
|
+
if self._enables_housekeep:
|
|
318
|
+
self._housekeep_thread = threading.Thread(
|
|
319
|
+
target=self._housekeep_loop, daemon=True
|
|
320
|
+
)
|
|
321
|
+
self._housekeep_thread.start()
|
|
322
|
+
|
|
323
|
+
self._start_time = time.time()
|
|
324
|
+
|
|
325
|
+
# Mark the sandbox as ready when all setup succeeds.
|
|
326
|
+
self._set_status(self.Status.READY)
|
|
327
|
+
|
|
328
|
+
duration = time.time() - starting_time
|
|
329
|
+
self.on_start(duration)
|
|
330
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
331
|
+
duration = time.time() - starting_time
|
|
332
|
+
pg.logging.error(
|
|
333
|
+
'[%s]: Sandbox failed to start in %.2f seconds: %s',
|
|
334
|
+
self.id, duration, e
|
|
335
|
+
)
|
|
336
|
+
if isinstance(e, interface.SandboxStateError):
|
|
337
|
+
self.report_state_error(e)
|
|
338
|
+
self.on_start(duration, e)
|
|
339
|
+
self.shutdown()
|
|
340
|
+
raise e
|
|
341
|
+
|
|
342
|
+
def shutdown(self) -> None:
|
|
343
|
+
"""Shuts down the sandbox.
|
|
344
|
+
|
|
345
|
+
State transitions:
|
|
346
|
+
SHUTTING_DOWN -> SHUTTING_DOWN: No operation.
|
|
347
|
+
OFFLINE -> OFFLINE: No operation.
|
|
348
|
+
SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When sandbox and feature
|
|
349
|
+
setup fails.
|
|
350
|
+
IN_SESSION -> SHUTTING_DOWN -> OFFLINE: When user session exits while
|
|
351
|
+
sandbox is set not to reuse, or session teardown fails.
|
|
352
|
+
FREE -> SHUTTING_DOWN -> OFFLINE: When sandbox is shutdown when the
|
|
353
|
+
environment is shutting down, or housekeeping loop shuts down the
|
|
354
|
+
sandbox due to housekeeping failures.
|
|
355
|
+
|
|
356
|
+
Please be aware that `shutdown` will be called whenever an operation on the
|
|
357
|
+
sandbox encounters a critical error. This means, `shutdown` should not make
|
|
358
|
+
the assumption that the sandbox is in a healthy state, even `start` could
|
|
359
|
+
fail. As a result, `shutdown` must allow re-entry and be thread-safe with
|
|
360
|
+
other sandbox operations.
|
|
361
|
+
|
|
362
|
+
Shutdown may fail with two sources of errors:
|
|
363
|
+
|
|
364
|
+
1. SandboxStateError: If the sandbox is in a bad state, and feature teardown
|
|
365
|
+
logic depending on a healthy sandbox may fail. In such case, we do not
|
|
366
|
+
raise error to the user as the user session is considered completed. The
|
|
367
|
+
sandbox is abandoned and new user sessions will be served on other
|
|
368
|
+
sandboxes.
|
|
369
|
+
|
|
370
|
+
2. BaseException: The sandbox is in good state, but user code raises error
|
|
371
|
+
due to bug or non-environment code failure. In such case, errors will be
|
|
372
|
+
raised to the user so the error could be surfaced and handled properly.
|
|
373
|
+
The sandbox is treated as shutdown and will not be further used.
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
BaseException: If feature teardown failed with user-defined errors.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
# Allow re-entry.
|
|
380
|
+
if self._status in (
|
|
381
|
+
interface.Sandbox.Status.SHUTTING_DOWN,
|
|
382
|
+
interface.Sandbox.Status.OFFLINE
|
|
383
|
+
):
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
# End current session and shutdown the sandbox if the sandbox is in session.
|
|
387
|
+
if self._status == self.Status.IN_SESSION:
|
|
388
|
+
self.end_session(shutdown_sandbox=True)
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
shutting_down_time = time.time()
|
|
392
|
+
self._set_status(interface.Sandbox.Status.SHUTTING_DOWN)
|
|
393
|
+
|
|
394
|
+
if (self._housekeep_thread is not None
|
|
395
|
+
and threading.current_thread() is not self._housekeep_thread):
|
|
396
|
+
self._housekeep_thread.join()
|
|
397
|
+
self._housekeep_thread = None
|
|
398
|
+
|
|
399
|
+
teardown_error = self._teardown_features()
|
|
400
|
+
try:
|
|
401
|
+
self._shutdown()
|
|
402
|
+
self._set_status(interface.Sandbox.Status.OFFLINE)
|
|
403
|
+
self.on_shutdown(
|
|
404
|
+
duration=time.time() - shutting_down_time,
|
|
405
|
+
error=teardown_error
|
|
406
|
+
)
|
|
407
|
+
shutdown_error = None
|
|
408
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
409
|
+
shutdown_error = e
|
|
410
|
+
if isinstance(e, interface.SandboxStateError):
|
|
411
|
+
self.report_state_error(e)
|
|
412
|
+
self._set_status(interface.Sandbox.Status.OFFLINE)
|
|
413
|
+
pg.logging.error(
|
|
414
|
+
'[%s]: Sandbox shutdown with error: %s',
|
|
415
|
+
self.id, e
|
|
416
|
+
)
|
|
417
|
+
self.on_shutdown(
|
|
418
|
+
duration=time.time() - shutting_down_time,
|
|
419
|
+
error=teardown_error or shutdown_error
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# We raise non-state errors to the user following timely order, so the user
|
|
423
|
+
# code could be surfaced and handled properly.
|
|
424
|
+
if (teardown_error is not None
|
|
425
|
+
and teardown_error.has_non_sandbox_state_error):
|
|
426
|
+
raise teardown_error
|
|
427
|
+
|
|
428
|
+
if shutdown_error is not None and not isinstance(
|
|
429
|
+
shutdown_error, interface.SandboxStateError
|
|
430
|
+
):
|
|
431
|
+
raise shutdown_error
|
|
432
|
+
|
|
433
|
+
def ping(self) -> None:
|
|
434
|
+
"""Pings the sandbox to check if it is alive."""
|
|
435
|
+
self._ping()
|
|
436
|
+
|
|
437
|
+
#
|
|
438
|
+
# API related to a user session.
|
|
439
|
+
# A sandbox could be reused across different user sessions.
|
|
440
|
+
# A user session is a sequence of stateful interactions with the sandbox,
|
|
441
|
+
# Across different sessions the sandbox are considered stateless.
|
|
442
|
+
#
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def session_id(self) -> str | None:
|
|
446
|
+
"""Returns the current user session identifier.
|
|
447
|
+
|
|
448
|
+
session_id is set to None when the sandbox is free, and set to 'pending'
|
|
449
|
+
when the sandbox is acquired by a thread, before a user session is started.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
The current user session identifier or None if the sandbox is not busy.
|
|
453
|
+
"""
|
|
454
|
+
return self._session_id
|
|
455
|
+
|
|
456
|
+
def start_session(
|
|
457
|
+
self,
|
|
458
|
+
session_id: str,
|
|
459
|
+
) -> None:
|
|
460
|
+
"""Begins a user session with the sandbox.
|
|
461
|
+
|
|
462
|
+
State transitions:
|
|
463
|
+
ACQUIRED -> SETTING_UP -> IN_SESSION: When session setup succeeds.
|
|
464
|
+
ACQUIRED -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When session setup
|
|
465
|
+
fails.
|
|
466
|
+
|
|
467
|
+
A session is a sequence of stateful interactions with the sandbox.
|
|
468
|
+
Across different sessions the sandbox are considered stateless.
|
|
469
|
+
`start_session` and `end_session` should always be called in pairs, even
|
|
470
|
+
when the session fails to start. `Sandbox.new_session` context manager is
|
|
471
|
+
the recommended way to use `start_session` and `end_session` in pairs.
|
|
472
|
+
|
|
473
|
+
Starting a session may fail with two sources of errors:
|
|
474
|
+
|
|
475
|
+
1. SandboxStateError: If the sandbox is in a bad state or session setup
|
|
476
|
+
failed.
|
|
477
|
+
|
|
478
|
+
2. BaseException: If session setup failed with user-defined errors.
|
|
479
|
+
|
|
480
|
+
In both cases, the sandbox will be shutdown automatically and the
|
|
481
|
+
session will be considered ended. The error will be added to `errors`.
|
|
482
|
+
Future session will be served on other sandboxes.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
session_id: The identifier for the user session.
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
SandboxStateError: If the sandbox is already in a bad state or session
|
|
489
|
+
setup failed.
|
|
490
|
+
BaseException: If session setup failed with user-defined errors.
|
|
491
|
+
"""
|
|
492
|
+
assert self._status == self.Status.ACQUIRED, (
|
|
493
|
+
f'Sandbox {self.id} is not in acquired state (status={self._status}).'
|
|
494
|
+
)
|
|
495
|
+
assert self._session_id is None, (
|
|
496
|
+
f'A user session {self._session_id} is already active '
|
|
497
|
+
f'for sandbox {self.id}.'
|
|
498
|
+
)
|
|
499
|
+
self._set_status(self.Status.SETTING_UP)
|
|
500
|
+
|
|
501
|
+
self._session_id = session_id
|
|
502
|
+
self._session_start_time = time.time()
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
self._start_session()
|
|
506
|
+
self._set_status(self.Status.IN_SESSION)
|
|
507
|
+
self.on_session_start(session_id, time.time() - self._session_start_time)
|
|
508
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
509
|
+
if isinstance(e, interface.SandboxStateError):
|
|
510
|
+
self.report_state_error(e)
|
|
511
|
+
self.on_session_start(
|
|
512
|
+
session_id, time.time() - self._session_start_time, e
|
|
513
|
+
)
|
|
514
|
+
self.shutdown()
|
|
515
|
+
raise e
|
|
516
|
+
|
|
517
|
+
def end_session(self, shutdown_sandbox: bool = False) -> None:
|
|
518
|
+
"""Ends the user session with the sandbox.
|
|
519
|
+
|
|
520
|
+
State transitions:
|
|
521
|
+
IN_SESSION -> EXITING_SESSION -> READY: When user session exits normally,
|
|
522
|
+
and sandbox is set to reuse.
|
|
523
|
+
IN_SESSION -> EXITING_SESSION -> SHUTTING_DOWN -> OFFLINE: When user
|
|
524
|
+
session exits while
|
|
525
|
+
sandbox is set not to reuse, or session teardown fails.
|
|
526
|
+
IN_SESSION -> EXITING_SESSION -> SETTING_UP -> READY: When user session
|
|
527
|
+
exits normally, and sandbox is set to reuse, and proactive session setup
|
|
528
|
+
is enabled.
|
|
529
|
+
IN_SESSION -> EXITING_SESSION -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE:
|
|
530
|
+
When user session exits normally, and proactive session setup is enabled
|
|
531
|
+
but fails.
|
|
532
|
+
EXITING_SESSION -> EXITING_SESSION: No operation.
|
|
533
|
+
not IN_SESSION -> same state: No operation
|
|
534
|
+
|
|
535
|
+
`end_session` should always be called for each `start_session` call, even
|
|
536
|
+
when the session fails to start, to ensure proper cleanup.
|
|
537
|
+
|
|
538
|
+
`end_session` may fail with two sources of errors:
|
|
539
|
+
|
|
540
|
+
1. SandboxStateError: If the sandbox is in a bad state or session teardown
|
|
541
|
+
failed.
|
|
542
|
+
|
|
543
|
+
2. BaseException: If session teardown failed with user-defined errors.
|
|
544
|
+
|
|
545
|
+
In both cases, the sandbox will be shutdown automatically and the
|
|
546
|
+
session will be considered ended. The error will be added to `errors`.
|
|
547
|
+
Future session will be served on other sandboxes.
|
|
548
|
+
|
|
549
|
+
However, SandboxStateError encountered during `end_session` will NOT be
|
|
550
|
+
raised to the user as the user session is considered completed.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
shutdown_sandbox: If True, the sandbox will be shutdown after session
|
|
554
|
+
teardown.
|
|
555
|
+
|
|
556
|
+
Raises:
|
|
557
|
+
BaseException: If session teardown failed with user-defined errors.
|
|
558
|
+
"""
|
|
559
|
+
if self._status == self.Status.EXITING_SESSION:
|
|
560
|
+
return
|
|
561
|
+
|
|
562
|
+
if self._status not in (
|
|
563
|
+
self.Status.IN_SESSION,
|
|
564
|
+
):
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
assert self._session_id is not None, (
|
|
568
|
+
'No user session is active for this sandbox'
|
|
569
|
+
)
|
|
570
|
+
# Set sandbox status to EXITING_SESSION to avoid re-entry.
|
|
571
|
+
self._set_status(self.Status.EXITING_SESSION)
|
|
572
|
+
shutdown_sandbox = shutdown_sandbox or not self.reusable
|
|
573
|
+
ending_time = time.time()
|
|
574
|
+
|
|
575
|
+
# Teardown features for the current session.
|
|
576
|
+
end_session_error = self._end_session()
|
|
577
|
+
previous_session_id = self._session_id
|
|
578
|
+
self._session_id = None
|
|
579
|
+
self._features_with_setup_session_called.clear()
|
|
580
|
+
|
|
581
|
+
# If there is no state error, and proactive session setup is enabled,
|
|
582
|
+
# set up the next session proactively.
|
|
583
|
+
if not self.state_errors:
|
|
584
|
+
if not shutdown_sandbox and self._enable_pre_session_setup:
|
|
585
|
+
def _setup_next_session():
|
|
586
|
+
try:
|
|
587
|
+
self._setup_session()
|
|
588
|
+
self._set_status(interface.Sandbox.Status.READY)
|
|
589
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
590
|
+
pg.logging.error(
|
|
591
|
+
'[%s]: Shutting down sandbox due to practively setting up '
|
|
592
|
+
'next session failed: %s',
|
|
593
|
+
self.id,
|
|
594
|
+
e
|
|
595
|
+
)
|
|
596
|
+
if isinstance(e, interface.SandboxStateError):
|
|
597
|
+
self.report_state_error(e)
|
|
598
|
+
self.shutdown()
|
|
599
|
+
|
|
600
|
+
# End session before setting up the next session.
|
|
601
|
+
self.on_session_end(
|
|
602
|
+
previous_session_id, duration=time.time() - ending_time
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Mark the sandbox as setting up to prevent it from being acquired by
|
|
606
|
+
# other threads.
|
|
607
|
+
self._set_status(interface.Sandbox.Status.SETTING_UP)
|
|
608
|
+
|
|
609
|
+
# TODO(daiyip): Consider using a thread pool to perform next session
|
|
610
|
+
# setup.
|
|
611
|
+
threading.Thread(target=_setup_next_session).start()
|
|
612
|
+
else:
|
|
613
|
+
# End session before reporting sandbox status change.
|
|
614
|
+
self.on_session_end(
|
|
615
|
+
previous_session_id, duration=time.time() - ending_time
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# If shutdown is requested, mark the sandbox as acquired to prevent it
|
|
619
|
+
# from being acquired by other threads.
|
|
620
|
+
self._set_status(
|
|
621
|
+
interface.Sandbox.Status.ACQUIRED if shutdown_sandbox else
|
|
622
|
+
interface.Sandbox.Status.READY
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Otherwise, shutdown the sandbox.
|
|
626
|
+
else:
|
|
627
|
+
self.on_session_end(
|
|
628
|
+
previous_session_id,
|
|
629
|
+
duration=time.time() - ending_time,
|
|
630
|
+
error=self.state_errors[0]
|
|
631
|
+
)
|
|
632
|
+
self._set_status(interface.Sandbox.Status.ACQUIRED)
|
|
633
|
+
shutdown_sandbox = True
|
|
634
|
+
|
|
635
|
+
self._session_start_time = None
|
|
636
|
+
|
|
637
|
+
if shutdown_sandbox:
|
|
638
|
+
self.shutdown()
|
|
639
|
+
|
|
640
|
+
# We only raise errors if teardown error contains non-sandbox-state error,
|
|
641
|
+
# meaning that the user code may have bug or other non-environment
|
|
642
|
+
# failures.
|
|
643
|
+
if (end_session_error is not None
|
|
644
|
+
and end_session_error.has_non_sandbox_state_error):
|
|
645
|
+
raise end_session_error # pylint: disable=raising-bad-type
|
|
646
|
+
|
|
647
|
+
@contextlib.contextmanager
|
|
648
|
+
def track_activity(
|
|
649
|
+
self,
|
|
650
|
+
name: str,
|
|
651
|
+
**kwargs: Any
|
|
652
|
+
) -> Iterator[None]:
|
|
653
|
+
"""Tracks an activity for the sandbox."""
|
|
654
|
+
start_time = time.time()
|
|
655
|
+
error = None
|
|
656
|
+
try:
|
|
657
|
+
yield None
|
|
658
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
659
|
+
error = e
|
|
660
|
+
raise
|
|
661
|
+
finally:
|
|
662
|
+
self.on_activity(
|
|
663
|
+
name=name,
|
|
664
|
+
duration=time.time() - start_time,
|
|
665
|
+
error=error,
|
|
666
|
+
**kwargs
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
#
|
|
670
|
+
# Housekeeping.
|
|
671
|
+
#
|
|
672
|
+
|
|
673
|
+
def _housekeep_loop(self) -> None:
|
|
674
|
+
"""Sandbox housekeeping loop."""
|
|
675
|
+
now = time.time()
|
|
676
|
+
last_ping = now
|
|
677
|
+
last_housekeep_time = {name: now for name in self._features.keys()}
|
|
678
|
+
|
|
679
|
+
def _next_housekeep_wait_time() -> float:
|
|
680
|
+
# Decide how long to sleep for the next housekeeping.
|
|
681
|
+
next_housekeep_time = None
|
|
682
|
+
if self.keepalive_interval is not None:
|
|
683
|
+
next_housekeep_time = last_ping + self.keepalive_interval
|
|
684
|
+
|
|
685
|
+
for name, feature in self._features.items():
|
|
686
|
+
if feature.housekeep_interval is None:
|
|
687
|
+
continue
|
|
688
|
+
next_feature_housekeep_time = (
|
|
689
|
+
last_housekeep_time[name] + feature.housekeep_interval
|
|
690
|
+
)
|
|
691
|
+
if (next_housekeep_time is None
|
|
692
|
+
or next_housekeep_time > next_feature_housekeep_time):
|
|
693
|
+
next_housekeep_time = next_feature_housekeep_time
|
|
694
|
+
|
|
695
|
+
# Housekeep loop is installed when at least one feature requires
|
|
696
|
+
# housekeeping or the sandbox has a keepalive interval.
|
|
697
|
+
assert next_housekeep_time is not None
|
|
698
|
+
return max(0, next_housekeep_time - time.time())
|
|
699
|
+
|
|
700
|
+
while self._status not in (self.Status.SHUTTING_DOWN, self.Status.OFFLINE):
|
|
701
|
+
housekeep_start = time.time()
|
|
702
|
+
if self.keepalive_interval is not None:
|
|
703
|
+
if time.time() - last_ping > self.keepalive_interval:
|
|
704
|
+
try:
|
|
705
|
+
self.ping()
|
|
706
|
+
except interface.SandboxStateError as e:
|
|
707
|
+
pg.logging.error(
|
|
708
|
+
'[%s]: Shutting down sandbox because ping failed '
|
|
709
|
+
'with error: %s.',
|
|
710
|
+
self.id,
|
|
711
|
+
str(e)
|
|
712
|
+
)
|
|
713
|
+
self._housekeep_counter += 1
|
|
714
|
+
self.report_state_error(e)
|
|
715
|
+
self.on_housekeep(time.time() - housekeep_start, e)
|
|
716
|
+
self.shutdown()
|
|
717
|
+
break
|
|
718
|
+
last_ping = time.time()
|
|
719
|
+
|
|
720
|
+
for name, feature in self._features.items():
|
|
721
|
+
if feature.housekeep_interval is not None and (
|
|
722
|
+
time.time() - last_housekeep_time[name]
|
|
723
|
+
> feature.housekeep_interval
|
|
724
|
+
):
|
|
725
|
+
try:
|
|
726
|
+
feature.housekeep()
|
|
727
|
+
last_housekeep_time[name] = time.time()
|
|
728
|
+
except interface.SandboxStateError as e:
|
|
729
|
+
pg.logging.error(
|
|
730
|
+
'[%s/%s]: Feature housekeeping failed with error: %s. '
|
|
731
|
+
'Shutting down sandbox...',
|
|
732
|
+
self.id,
|
|
733
|
+
feature.name,
|
|
734
|
+
e,
|
|
735
|
+
)
|
|
736
|
+
self.report_state_error(e)
|
|
737
|
+
self._housekeep_counter += 1
|
|
738
|
+
self.on_housekeep(time.time() - housekeep_start, e)
|
|
739
|
+
self.shutdown()
|
|
740
|
+
break
|
|
741
|
+
|
|
742
|
+
self._housekeep_counter += 1
|
|
743
|
+
self.on_housekeep(time.time() - housekeep_start)
|
|
744
|
+
time.sleep(_next_housekeep_wait_time())
|
|
745
|
+
|
|
746
|
+
#
|
|
747
|
+
# Event handlers subclasses can override.
|
|
748
|
+
#
|
|
749
|
+
|
|
750
|
+
def on_start(
|
|
751
|
+
self,
|
|
752
|
+
duration: float,
|
|
753
|
+
error: BaseException | None = None
|
|
754
|
+
) -> None:
|
|
755
|
+
"""Called when the sandbox is started."""
|
|
756
|
+
self._event_handler.on_sandbox_start(self, duration, error)
|
|
757
|
+
|
|
758
|
+
def on_status_change(
|
|
759
|
+
self,
|
|
760
|
+
old_status: interface.Sandbox.Status,
|
|
761
|
+
new_status: interface.Sandbox.Status,
|
|
762
|
+
) -> None:
|
|
763
|
+
"""Called when the sandbox status changes."""
|
|
764
|
+
status_duration = time.time() - self._status_start_time
|
|
765
|
+
self._event_handler.on_sandbox_status_change(
|
|
766
|
+
self, old_status, new_status, status_duration
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
def on_shutdown(
|
|
770
|
+
self,
|
|
771
|
+
duration: float,
|
|
772
|
+
error: BaseException | None = None
|
|
773
|
+
) -> None:
|
|
774
|
+
"""Called when the sandbox is shutdown."""
|
|
775
|
+
self._event_handler.on_sandbox_shutdown(
|
|
776
|
+
sandbox=self,
|
|
777
|
+
duration=duration,
|
|
778
|
+
lifetime=(0.0 if self._start_time is None
|
|
779
|
+
else (time.time() - self._start_time)),
|
|
780
|
+
error=error
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
def on_housekeep(
|
|
784
|
+
self,
|
|
785
|
+
duration: float,
|
|
786
|
+
error: BaseException | None = None,
|
|
787
|
+
**kwargs
|
|
788
|
+
) -> None:
|
|
789
|
+
"""Called when the sandbox finishes a round of housekeeping."""
|
|
790
|
+
self._event_handler.on_sandbox_housekeep(
|
|
791
|
+
sandbox=self,
|
|
792
|
+
counter=self._housekeep_counter,
|
|
793
|
+
duration=duration,
|
|
794
|
+
error=error,
|
|
795
|
+
**kwargs
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
def on_session_start(
|
|
799
|
+
self,
|
|
800
|
+
session_id: str,
|
|
801
|
+
duration: float,
|
|
802
|
+
error: BaseException | None = None
|
|
803
|
+
) -> None:
|
|
804
|
+
"""Called when the user session starts."""
|
|
805
|
+
self._event_handler.on_sandbox_session_start(
|
|
806
|
+
sandbox=self,
|
|
807
|
+
session_id=session_id,
|
|
808
|
+
duration=duration,
|
|
809
|
+
error=error
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
def on_activity(
|
|
813
|
+
self,
|
|
814
|
+
name: str,
|
|
815
|
+
duration: float,
|
|
816
|
+
error: BaseException | None = None,
|
|
817
|
+
**kwargs
|
|
818
|
+
) -> None:
|
|
819
|
+
"""Called when a sandbox activity is performed."""
|
|
820
|
+
self._event_handler.on_sandbox_activity(
|
|
821
|
+
name=name,
|
|
822
|
+
sandbox=self,
|
|
823
|
+
session_id=self.session_id,
|
|
824
|
+
duration=duration,
|
|
825
|
+
error=error,
|
|
826
|
+
**kwargs
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
def on_session_end(
|
|
830
|
+
self,
|
|
831
|
+
session_id: str,
|
|
832
|
+
duration: float,
|
|
833
|
+
error: BaseException | None = None
|
|
834
|
+
) -> None:
|
|
835
|
+
"""Called when the user session ends."""
|
|
836
|
+
self._event_handler.on_sandbox_session_end(
|
|
837
|
+
sandbox=self,
|
|
838
|
+
session_id=session_id,
|
|
839
|
+
duration=duration,
|
|
840
|
+
lifetime=time.time() - self._session_start_time,
|
|
841
|
+
error=error
|
|
842
|
+
)
|