langfun 0.1.2.dev202510310805__py3-none-any.whl → 0.1.2.dev202511010804__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 +4 -2
- langfun/env/base_environment.py +12 -19
- langfun/env/base_environment_test.py +473 -0
- langfun/env/base_feature.py +52 -8
- langfun/env/base_sandbox.py +43 -121
- langfun/env/{base_test.py → base_sandbox_test.py} +21 -484
- langfun/env/event_handlers/__init__.py +1 -1
- langfun/env/event_handlers/chain.py +255 -0
- langfun/env/event_handlers/chain_test.py +281 -0
- langfun/env/event_handlers/event_logger.py +39 -39
- langfun/env/event_handlers/event_logger_test.py +1 -1
- langfun/env/event_handlers/metric_writer.py +38 -38
- langfun/env/event_handlers/metric_writer_test.py +1 -1
- langfun/env/interface.py +344 -8
- langfun/env/test_utils.py +1 -4
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511010804.dist-info}/METADATA +1 -1
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511010804.dist-info}/RECORD +20 -18
- langfun/env/event_handlers/base.py +0 -350
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511010804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511010804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511010804.dist-info}/top_level.txt +0 -0
langfun/env/__init__.py
CHANGED
|
@@ -24,6 +24,8 @@ from langfun.env.interface import Environment
|
|
|
24
24
|
from langfun.env.interface import Sandbox
|
|
25
25
|
from langfun.env.interface import Feature
|
|
26
26
|
|
|
27
|
+
from langfun.env.interface import EventHandler
|
|
28
|
+
|
|
27
29
|
# Decorators for sandbox/feature methods.
|
|
28
30
|
from langfun.env.interface import treat_as_sandbox_state_error
|
|
29
31
|
from langfun.env.interface import log_sandbox_activity
|
|
@@ -35,7 +37,7 @@ from langfun.env.base_feature import BaseFeature
|
|
|
35
37
|
from langfun.env import load_balancers
|
|
36
38
|
from langfun.env.load_balancers import LoadBalancer
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
# Import all event handlers.
|
|
41
|
+
from langfun.env.event_handlers import *
|
|
40
42
|
|
|
41
43
|
# Google-internal imports.
|
langfun/env/base_environment.py
CHANGED
|
@@ -36,7 +36,6 @@ import langfun.core as lf
|
|
|
36
36
|
from langfun.env import base_sandbox
|
|
37
37
|
from langfun.env import interface
|
|
38
38
|
from langfun.env import load_balancers
|
|
39
|
-
from langfun.env.event_handlers import base as event_handler_base
|
|
40
39
|
import pyglove as pg
|
|
41
40
|
|
|
42
41
|
|
|
@@ -113,12 +112,13 @@ class BaseEnvironment(interface.Environment):
|
|
|
113
112
|
)
|
|
114
113
|
] = True
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
event_handler: Annotated[
|
|
116
|
+
interface.EventHandler,
|
|
118
117
|
(
|
|
119
118
|
'User handler for the environment events.'
|
|
119
|
+
'By default, the no-op event handler is used.'
|
|
120
120
|
)
|
|
121
|
-
] =
|
|
121
|
+
] = interface.EventHandler()
|
|
122
122
|
|
|
123
123
|
outage_grace_period: Annotated[
|
|
124
124
|
float,
|
|
@@ -169,6 +169,7 @@ class BaseEnvironment(interface.Environment):
|
|
|
169
169
|
|
|
170
170
|
self._status = self.Status.CREATED
|
|
171
171
|
self._start_time = None
|
|
172
|
+
|
|
172
173
|
self._sandbox_pool: dict[str, list[base_sandbox.BaseSandbox]] = (
|
|
173
174
|
collections.defaultdict(list)
|
|
174
175
|
)
|
|
@@ -555,8 +556,6 @@ class BaseEnvironment(interface.Environment):
|
|
|
555
556
|
proactive_session_setup=self.proactive_session_setup,
|
|
556
557
|
keepalive_interval=self.sandbox_keepalive_interval,
|
|
557
558
|
)
|
|
558
|
-
for handler in self.event_handlers:
|
|
559
|
-
sandbox.add_event_handler(handler)
|
|
560
559
|
sandbox.start()
|
|
561
560
|
if set_acquired:
|
|
562
561
|
sandbox.set_acquired()
|
|
@@ -738,16 +737,14 @@ class BaseEnvironment(interface.Environment):
|
|
|
738
737
|
|
|
739
738
|
def on_starting(self) -> None:
|
|
740
739
|
"""Called when the environment is getting started."""
|
|
741
|
-
|
|
742
|
-
handler.on_environment_starting(self)
|
|
740
|
+
self.event_handler.on_environment_starting(self)
|
|
743
741
|
|
|
744
742
|
def on_start(
|
|
745
743
|
self,
|
|
746
744
|
duration: float, error: BaseException | None = None
|
|
747
745
|
) -> None:
|
|
748
746
|
"""Called when the environment is started."""
|
|
749
|
-
|
|
750
|
-
handler.on_environment_start(self, duration, error)
|
|
747
|
+
self.event_handler.on_environment_start(self, duration, error)
|
|
751
748
|
|
|
752
749
|
def on_housekeep(
|
|
753
750
|
self,
|
|
@@ -756,16 +753,13 @@ class BaseEnvironment(interface.Environment):
|
|
|
756
753
|
**kwargs
|
|
757
754
|
) -> None:
|
|
758
755
|
"""Called when the environment finishes a round of housekeeping."""
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
self, housekeep_counter, duration, error, **kwargs
|
|
763
|
-
)
|
|
756
|
+
self.event_handler.on_environment_housekeep(
|
|
757
|
+
self, self.housekeep_counter, duration, error, **kwargs
|
|
758
|
+
)
|
|
764
759
|
|
|
765
760
|
def on_shutting_down(self) -> None:
|
|
766
761
|
"""Called when the environment is shutting down."""
|
|
767
|
-
|
|
768
|
-
handler.on_environment_shutting_down(self, self.offline_duration)
|
|
762
|
+
self.event_handler.on_environment_shutting_down(self, self.offline_duration)
|
|
769
763
|
|
|
770
764
|
def on_shutdown(
|
|
771
765
|
self,
|
|
@@ -773,5 +767,4 @@ class BaseEnvironment(interface.Environment):
|
|
|
773
767
|
error: BaseException | None = None) -> None:
|
|
774
768
|
"""Called when the environment is shutdown."""
|
|
775
769
|
lifetime = (time.time() - self.start_time) if self.start_time else 0.0
|
|
776
|
-
|
|
777
|
-
handler.on_environment_shutdown(self, duration, lifetime, error)
|
|
770
|
+
self.event_handler.on_environment_shutdown(self, duration, lifetime, error)
|
|
@@ -0,0 +1,473 @@
|
|
|
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
|
+
import time
|
|
15
|
+
import unittest
|
|
16
|
+
|
|
17
|
+
from langfun.env import interface
|
|
18
|
+
from langfun.env import test_utils
|
|
19
|
+
|
|
20
|
+
TestingEnvironment = test_utils.TestingEnvironment
|
|
21
|
+
TestingSandbox = test_utils.TestingSandbox
|
|
22
|
+
TestingFeature = test_utils.TestingFeature
|
|
23
|
+
TestingEventHandler = test_utils.TestingEventHandler
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseEnvironmentTests(unittest.TestCase):
|
|
27
|
+
|
|
28
|
+
def test_basics(self):
|
|
29
|
+
env = TestingEnvironment(
|
|
30
|
+
image_ids=['test_image'],
|
|
31
|
+
root_dir='/tmp',
|
|
32
|
+
pool_size=0,
|
|
33
|
+
features={'test_feature': TestingFeature()},
|
|
34
|
+
outage_grace_period=1,
|
|
35
|
+
outage_retry_interval=0,
|
|
36
|
+
)
|
|
37
|
+
self.assertIsNone(interface.Environment.current())
|
|
38
|
+
self.assertEqual(env.image_ids, ['test_image'])
|
|
39
|
+
self.assertFalse(env.supports_dynamic_image_loading)
|
|
40
|
+
self.assertEqual(env.status, interface.Environment.Status.CREATED)
|
|
41
|
+
self.assertFalse(env.is_online)
|
|
42
|
+
self.assertEqual(env.min_pool_size('test_image'), 0)
|
|
43
|
+
self.assertEqual(env.max_pool_size('test_image'), 0)
|
|
44
|
+
self.assertEqual(env.sandbox_pool, {})
|
|
45
|
+
self.assertEqual(env.id, interface.Environment.Id('testing-env'))
|
|
46
|
+
self.assertEqual(env.outage_grace_period, 1)
|
|
47
|
+
self.assertEqual(env.features['test_feature'].name, 'test_feature')
|
|
48
|
+
|
|
49
|
+
self.assertIsNone(env.start_time)
|
|
50
|
+
|
|
51
|
+
with env:
|
|
52
|
+
self.assertEqual(env.status, interface.Environment.Status.ONLINE)
|
|
53
|
+
self.assertIs(interface.Environment.current(), env)
|
|
54
|
+
self.assertTrue(env.is_online)
|
|
55
|
+
self.assertIsNotNone(env.start_time)
|
|
56
|
+
self.assertEqual(env.offline_duration, 0.0)
|
|
57
|
+
self.assertEqual(env.sandbox_pool, {})
|
|
58
|
+
self.assertEqual(env.working_dir, '/tmp/testing-env')
|
|
59
|
+
|
|
60
|
+
with env.sandbox(session_id='session1') as sb:
|
|
61
|
+
self.assertEqual(
|
|
62
|
+
sb.id, interface.Sandbox.Id(
|
|
63
|
+
environment_id=env.id,
|
|
64
|
+
image_id=sb.image_id,
|
|
65
|
+
sandbox_id='0')
|
|
66
|
+
)
|
|
67
|
+
self.assertEqual(sb.session_id, 'session1')
|
|
68
|
+
self.assertEqual(sb.working_dir, '/tmp/testing-env/test_image/0')
|
|
69
|
+
self.assertTrue(sb.is_online)
|
|
70
|
+
self.assertIs(sb.test_feature, sb.features['test_feature'])
|
|
71
|
+
self.assertEqual(
|
|
72
|
+
sb.test_feature.working_dir,
|
|
73
|
+
'/tmp/testing-env/test_image/0/test_feature'
|
|
74
|
+
)
|
|
75
|
+
with self.assertRaises(AttributeError):
|
|
76
|
+
_ = sb.test_feature2
|
|
77
|
+
self.assertFalse(sb.is_online)
|
|
78
|
+
|
|
79
|
+
with self.assertRaisesRegex(
|
|
80
|
+
ValueError, 'Environment .* does not serve image ID .*'
|
|
81
|
+
):
|
|
82
|
+
env.sandbox('test_image2')
|
|
83
|
+
|
|
84
|
+
with env.test_feature() as feature:
|
|
85
|
+
self.assertIsInstance(feature, TestingFeature)
|
|
86
|
+
self.assertEqual(
|
|
87
|
+
feature.sandbox.status, interface.Sandbox.Status.IN_SESSION
|
|
88
|
+
)
|
|
89
|
+
self.assertTrue(
|
|
90
|
+
feature.sandbox.session_id.startswith('test_feature-session')
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
with self.assertRaises(AttributeError):
|
|
94
|
+
_ = env.test_feature2
|
|
95
|
+
|
|
96
|
+
def test_dynamic_image_loading(self):
|
|
97
|
+
env = TestingEnvironment(
|
|
98
|
+
image_ids=[],
|
|
99
|
+
supports_dynamic_image_loading=True,
|
|
100
|
+
pool_size=0,
|
|
101
|
+
features={'test_feature': TestingFeature()},
|
|
102
|
+
outage_grace_period=1,
|
|
103
|
+
outage_retry_interval=0,
|
|
104
|
+
)
|
|
105
|
+
with env:
|
|
106
|
+
with env.sandbox(image_id='test_image2') as sb:
|
|
107
|
+
self.assertEqual(sb.image_id, 'test_image2')
|
|
108
|
+
|
|
109
|
+
with self.assertRaisesRegex(
|
|
110
|
+
ValueError, 'Environment .* does not have a default image ID.'
|
|
111
|
+
):
|
|
112
|
+
env.sandbox()
|
|
113
|
+
|
|
114
|
+
def test_dynamic_image_loading_with_pooling(self):
|
|
115
|
+
env = TestingEnvironment(
|
|
116
|
+
image_ids=[],
|
|
117
|
+
supports_dynamic_image_loading=True,
|
|
118
|
+
pool_size=2,
|
|
119
|
+
features={'test_feature': TestingFeature()},
|
|
120
|
+
outage_grace_period=1,
|
|
121
|
+
outage_retry_interval=0,
|
|
122
|
+
)
|
|
123
|
+
with env:
|
|
124
|
+
with env.sandbox(image_id='test_image'):
|
|
125
|
+
self.assertEqual(len(env.sandbox_pool['test_image']), 1)
|
|
126
|
+
|
|
127
|
+
with env.sandbox(image_id='test_image'):
|
|
128
|
+
self.assertEqual(len(env.sandbox_pool['test_image']), 2)
|
|
129
|
+
|
|
130
|
+
with self.assertRaises(interface.EnvironmentOverloadError):
|
|
131
|
+
with env.sandbox(image_id='test_image'):
|
|
132
|
+
pass
|
|
133
|
+
self.assertEqual(len(env.sandbox_pool['test_image']), 2)
|
|
134
|
+
|
|
135
|
+
with env.sandbox(image_id='test_image'):
|
|
136
|
+
self.assertEqual(len(env.sandbox_pool['test_image']), 2)
|
|
137
|
+
|
|
138
|
+
def test_image_feature_mappings(self):
|
|
139
|
+
env = TestingEnvironment(
|
|
140
|
+
image_ids=[
|
|
141
|
+
'test_image1',
|
|
142
|
+
'test_image2',
|
|
143
|
+
],
|
|
144
|
+
features={
|
|
145
|
+
'test_feature': TestingFeature(
|
|
146
|
+
applicable_images=['test_image1.*']
|
|
147
|
+
),
|
|
148
|
+
'test_feature2': TestingFeature(
|
|
149
|
+
applicable_images=['test_image2.*']
|
|
150
|
+
),
|
|
151
|
+
'test_feature3': TestingFeature(
|
|
152
|
+
applicable_images=['test_image.*']
|
|
153
|
+
),
|
|
154
|
+
},
|
|
155
|
+
pool_size=0,
|
|
156
|
+
outage_grace_period=1,
|
|
157
|
+
outage_retry_interval=0,
|
|
158
|
+
sandbox_keepalive_interval=0,
|
|
159
|
+
)
|
|
160
|
+
with env:
|
|
161
|
+
with env.sandbox(image_id='test_image1') as sb:
|
|
162
|
+
self.assertIn('test_feature', sb.features)
|
|
163
|
+
self.assertNotIn('test_feature2', sb.features)
|
|
164
|
+
self.assertIn('test_feature3', sb.features)
|
|
165
|
+
|
|
166
|
+
with env.sandbox(image_id='test_image2') as sb:
|
|
167
|
+
self.assertNotIn('test_feature', sb.features)
|
|
168
|
+
self.assertIn('test_feature2', sb.features)
|
|
169
|
+
self.assertIn('test_feature3', sb.features)
|
|
170
|
+
|
|
171
|
+
with env.test_feature() as feature:
|
|
172
|
+
self.assertEqual(feature.sandbox.image_id, 'test_image1')
|
|
173
|
+
|
|
174
|
+
with self.assertRaisesRegex(
|
|
175
|
+
ValueError, 'Feature .* is not applicable to .*'
|
|
176
|
+
):
|
|
177
|
+
with env.test_feature('test_image2'):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
with env.test_feature2() as feature:
|
|
181
|
+
self.assertEqual(feature.sandbox.image_id, 'test_image2')
|
|
182
|
+
|
|
183
|
+
with env.test_feature3() as feature:
|
|
184
|
+
self.assertEqual(feature.sandbox.image_id, 'test_image1')
|
|
185
|
+
|
|
186
|
+
with env.test_feature3('test_image2') as feature:
|
|
187
|
+
self.assertEqual(feature.sandbox.image_id, 'test_image2')
|
|
188
|
+
|
|
189
|
+
def test_feature_applicability_check(self):
|
|
190
|
+
with self.assertRaisesRegex(
|
|
191
|
+
ValueError, 'Feature .* is not applicable to .*'
|
|
192
|
+
):
|
|
193
|
+
TestingEnvironment(
|
|
194
|
+
image_ids=[
|
|
195
|
+
'test_image1',
|
|
196
|
+
],
|
|
197
|
+
features={
|
|
198
|
+
'test_feature2': TestingFeature(
|
|
199
|
+
applicable_images=['test_image2.*']
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
env = TestingEnvironment(
|
|
204
|
+
image_ids=[],
|
|
205
|
+
supports_dynamic_image_loading=True,
|
|
206
|
+
features={
|
|
207
|
+
'test_feature2': TestingFeature(
|
|
208
|
+
applicable_images=['test_image2.*']
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
pool_size=0
|
|
212
|
+
)
|
|
213
|
+
with env:
|
|
214
|
+
with self.assertRaisesRegex(
|
|
215
|
+
ValueError, 'No image ID found for feature .*'
|
|
216
|
+
):
|
|
217
|
+
with env.test_feature2():
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
# Dynamically loaded IDs.
|
|
221
|
+
with env.test_feature2('test_image2') as feature:
|
|
222
|
+
self.assertEqual(feature.sandbox.image_id, 'test_image2')
|
|
223
|
+
|
|
224
|
+
def test_pool_size(self):
|
|
225
|
+
env = TestingEnvironment(
|
|
226
|
+
image_ids=['test_image'],
|
|
227
|
+
pool_size=1,
|
|
228
|
+
outage_grace_period=1,
|
|
229
|
+
outage_retry_interval=0,
|
|
230
|
+
)
|
|
231
|
+
self.assertEqual(env.min_pool_size('test_image'), 1)
|
|
232
|
+
self.assertEqual(env.max_pool_size('test_image'), 1)
|
|
233
|
+
|
|
234
|
+
env = TestingEnvironment(
|
|
235
|
+
image_ids=['test_image'],
|
|
236
|
+
pool_size=(0, 256),
|
|
237
|
+
outage_grace_period=1,
|
|
238
|
+
outage_retry_interval=0,
|
|
239
|
+
)
|
|
240
|
+
self.assertEqual(env.min_pool_size('test_image'), 0)
|
|
241
|
+
self.assertEqual(env.max_pool_size('test_image'), 256)
|
|
242
|
+
|
|
243
|
+
env = TestingEnvironment(
|
|
244
|
+
image_ids=['test_image'],
|
|
245
|
+
pool_size={
|
|
246
|
+
'test_.*': (0, 128),
|
|
247
|
+
'my.*': (5, 64),
|
|
248
|
+
'exact_image_name': 10,
|
|
249
|
+
},
|
|
250
|
+
outage_grace_period=1,
|
|
251
|
+
outage_retry_interval=0,
|
|
252
|
+
)
|
|
253
|
+
self.assertEqual(env.min_pool_size('test_image'), 0)
|
|
254
|
+
self.assertEqual(env.max_pool_size('test_image'), 128)
|
|
255
|
+
self.assertEqual(env.min_pool_size('my_image'), 5)
|
|
256
|
+
self.assertEqual(env.max_pool_size('my_image'), 64)
|
|
257
|
+
self.assertEqual(env.min_pool_size('exact_image_name'), 10)
|
|
258
|
+
self.assertEqual(env.max_pool_size('exact_image_name'), 10)
|
|
259
|
+
self.assertEqual(env.min_pool_size('some_image'), 0) # default
|
|
260
|
+
self.assertEqual(env.max_pool_size('some_image'), 256) # default
|
|
261
|
+
|
|
262
|
+
def test_del(self):
|
|
263
|
+
env = TestingEnvironment(
|
|
264
|
+
features={'test_feature': TestingFeature()},
|
|
265
|
+
pool_size=0,
|
|
266
|
+
outage_grace_period=1,
|
|
267
|
+
outage_retry_interval=0,
|
|
268
|
+
sandbox_keepalive_interval=0,
|
|
269
|
+
)
|
|
270
|
+
env.start()
|
|
271
|
+
sb = env.acquire()
|
|
272
|
+
del sb
|
|
273
|
+
del env
|
|
274
|
+
|
|
275
|
+
def test_acquire_env_offline(self):
|
|
276
|
+
env = TestingEnvironment(
|
|
277
|
+
features={'test_feature': TestingFeature()},
|
|
278
|
+
pool_size=0,
|
|
279
|
+
outage_grace_period=1,
|
|
280
|
+
outage_retry_interval=0,
|
|
281
|
+
sandbox_keepalive_interval=0,
|
|
282
|
+
)
|
|
283
|
+
with self.assertRaises(interface.EnvironmentOutageError):
|
|
284
|
+
env.acquire()
|
|
285
|
+
|
|
286
|
+
def test_acquire_no_pooling(self):
|
|
287
|
+
env = TestingEnvironment(
|
|
288
|
+
features={'test_feature': TestingFeature()},
|
|
289
|
+
pool_size=0,
|
|
290
|
+
outage_grace_period=1,
|
|
291
|
+
outage_retry_interval=0,
|
|
292
|
+
sandbox_keepalive_interval=0,
|
|
293
|
+
)
|
|
294
|
+
with env:
|
|
295
|
+
sb = env.acquire()
|
|
296
|
+
self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
|
|
297
|
+
self.assertIsNone(env.working_dir)
|
|
298
|
+
self.assertIsNone(sb.working_dir)
|
|
299
|
+
self.assertIsNone(sb.test_feature.working_dir)
|
|
300
|
+
|
|
301
|
+
def test_acquire_no_pooling_with_error(self):
|
|
302
|
+
env = TestingEnvironment(
|
|
303
|
+
features={
|
|
304
|
+
'test_feature': TestingFeature(
|
|
305
|
+
simulate_setup_error=interface.SandboxStateError
|
|
306
|
+
)
|
|
307
|
+
},
|
|
308
|
+
pool_size=0,
|
|
309
|
+
outage_grace_period=1,
|
|
310
|
+
outage_retry_interval=0,
|
|
311
|
+
sandbox_keepalive_interval=0,
|
|
312
|
+
)
|
|
313
|
+
with env:
|
|
314
|
+
with self.assertRaises(interface.EnvironmentOutageError):
|
|
315
|
+
env.acquire()
|
|
316
|
+
|
|
317
|
+
def test_acquire_with_pooling(self):
|
|
318
|
+
env = TestingEnvironment(
|
|
319
|
+
features={'test_feature': TestingFeature()},
|
|
320
|
+
pool_size=1,
|
|
321
|
+
outage_grace_period=1,
|
|
322
|
+
outage_retry_interval=0,
|
|
323
|
+
sandbox_keepalive_interval=0,
|
|
324
|
+
)
|
|
325
|
+
with env:
|
|
326
|
+
sb = env.acquire()
|
|
327
|
+
self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
|
|
328
|
+
|
|
329
|
+
def test_acquire_with_pooling_overload(self):
|
|
330
|
+
env = TestingEnvironment(
|
|
331
|
+
features={'test_feature': TestingFeature()},
|
|
332
|
+
pool_size=1,
|
|
333
|
+
outage_grace_period=1,
|
|
334
|
+
outage_retry_interval=0,
|
|
335
|
+
sandbox_keepalive_interval=0,
|
|
336
|
+
)
|
|
337
|
+
with env:
|
|
338
|
+
sb = env.acquire()
|
|
339
|
+
self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
|
|
340
|
+
with self.assertRaises(interface.EnvironmentOverloadError):
|
|
341
|
+
env.acquire()
|
|
342
|
+
|
|
343
|
+
def test_acquire_with_growing_pool(self):
|
|
344
|
+
env = TestingEnvironment(
|
|
345
|
+
features={'test_feature': TestingFeature()},
|
|
346
|
+
pool_size=(1, 3),
|
|
347
|
+
outage_grace_period=1,
|
|
348
|
+
outage_retry_interval=0,
|
|
349
|
+
sandbox_keepalive_interval=0,
|
|
350
|
+
)
|
|
351
|
+
with env:
|
|
352
|
+
self.assertEqual(len(env.sandbox_pool['test_image']), 1)
|
|
353
|
+
self.assertEqual(
|
|
354
|
+
env.stats(),
|
|
355
|
+
{
|
|
356
|
+
'sandbox': {
|
|
357
|
+
'test_image': {
|
|
358
|
+
'created': 0,
|
|
359
|
+
'setting_up': 0,
|
|
360
|
+
'ready': 1,
|
|
361
|
+
'acquired': 0,
|
|
362
|
+
'in_session': 0,
|
|
363
|
+
'exiting_session': 0,
|
|
364
|
+
'shutting_down': 0,
|
|
365
|
+
'offline': 0,
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
sb = env.acquire()
|
|
371
|
+
self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
|
|
372
|
+
self.assertEqual(
|
|
373
|
+
env.stats(),
|
|
374
|
+
{
|
|
375
|
+
'sandbox': {
|
|
376
|
+
'test_image': {
|
|
377
|
+
'created': 0,
|
|
378
|
+
'setting_up': 0,
|
|
379
|
+
'ready': 0,
|
|
380
|
+
'acquired': 1,
|
|
381
|
+
'in_session': 0,
|
|
382
|
+
'exiting_session': 0,
|
|
383
|
+
'shutting_down': 0,
|
|
384
|
+
'offline': 0,
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
self.assertEqual(len(env.sandbox_pool['test_image']), 1)
|
|
390
|
+
sb2 = env.acquire()
|
|
391
|
+
self.assertEqual(sb2.status, interface.Sandbox.Status.ACQUIRED)
|
|
392
|
+
self.assertEqual(len(env.sandbox_pool['test_image']), 2)
|
|
393
|
+
self.assertEqual(
|
|
394
|
+
env.stats(),
|
|
395
|
+
{
|
|
396
|
+
'sandbox': {
|
|
397
|
+
'test_image': {
|
|
398
|
+
'created': 0,
|
|
399
|
+
'setting_up': 0,
|
|
400
|
+
'ready': 0,
|
|
401
|
+
'acquired': 2,
|
|
402
|
+
'in_session': 0,
|
|
403
|
+
'exiting_session': 0,
|
|
404
|
+
'shutting_down': 0,
|
|
405
|
+
'offline': 0,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
self.assertEqual(
|
|
411
|
+
env.stats(),
|
|
412
|
+
{
|
|
413
|
+
'sandbox': {}
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def test_acquire_with_growing_pool_failure(self):
|
|
418
|
+
env = TestingEnvironment(
|
|
419
|
+
features={'test_feature': TestingFeature()},
|
|
420
|
+
pool_size=(1, 3),
|
|
421
|
+
outage_grace_period=1,
|
|
422
|
+
outage_retry_interval=0,
|
|
423
|
+
sandbox_keepalive_interval=0,
|
|
424
|
+
)
|
|
425
|
+
with env:
|
|
426
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
427
|
+
sb = env.acquire()
|
|
428
|
+
self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
|
|
429
|
+
|
|
430
|
+
# Make future sandbox setup to fail.
|
|
431
|
+
env.features.test_feature.rebind(
|
|
432
|
+
simulate_setup_error=interface.SandboxStateError,
|
|
433
|
+
skip_notification=True
|
|
434
|
+
)
|
|
435
|
+
with self.assertRaises(interface.EnvironmentOutageError):
|
|
436
|
+
env.acquire()
|
|
437
|
+
|
|
438
|
+
def test_housekeep_error(self):
|
|
439
|
+
env = TestingEnvironment(
|
|
440
|
+
features={'test_feature': TestingFeature()},
|
|
441
|
+
pool_size=1,
|
|
442
|
+
proactive_session_setup=True,
|
|
443
|
+
outage_grace_period=1,
|
|
444
|
+
outage_retry_interval=0,
|
|
445
|
+
sandbox_keepalive_interval=0,
|
|
446
|
+
)
|
|
447
|
+
with env:
|
|
448
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
449
|
+
self.assertIn('test_image', env.sandbox_pool)
|
|
450
|
+
self.assertEqual(
|
|
451
|
+
env.sandbox_pool['test_image'][0].status,
|
|
452
|
+
interface.Sandbox.Status.READY
|
|
453
|
+
)
|
|
454
|
+
# Make future sandbox setup to fail.
|
|
455
|
+
env.features.test_feature.rebind(
|
|
456
|
+
simulate_setup_error=interface.SandboxStateError,
|
|
457
|
+
skip_notification=True
|
|
458
|
+
)
|
|
459
|
+
with self.assertRaises(interface.SandboxStateError):
|
|
460
|
+
with env.sandbox() as sb:
|
|
461
|
+
sb.shell('bad command', raise_error=interface.SandboxStateError)
|
|
462
|
+
self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
|
|
463
|
+
self.assertEqual(len(sb.state_errors), 1)
|
|
464
|
+
sb_offline_time = time.time()
|
|
465
|
+
while time.time() - sb_offline_time < 10:
|
|
466
|
+
if not env.is_online:
|
|
467
|
+
break
|
|
468
|
+
time.sleep(0.5)
|
|
469
|
+
self.assertFalse(env.is_online)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
if __name__ == '__main__':
|
|
473
|
+
unittest.main()
|
langfun/env/base_feature.py
CHANGED
|
@@ -110,6 +110,15 @@ class BaseFeature(interface.Feature):
|
|
|
110
110
|
super()._on_parent_change(old_parent, new_parent)
|
|
111
111
|
self.__dict__.pop('name', None)
|
|
112
112
|
|
|
113
|
+
@functools.cached_property
|
|
114
|
+
def environment(self) -> interface.Environment:
|
|
115
|
+
"""Returns the environment that the feature is running in."""
|
|
116
|
+
if self._sandbox is not None:
|
|
117
|
+
return self._sandbox.environment
|
|
118
|
+
env = self.sym_ancestor(lambda v: isinstance(v, interface.Environment))
|
|
119
|
+
assert env is not None, 'Feature is not put into an environment.'
|
|
120
|
+
return env
|
|
121
|
+
|
|
113
122
|
@property
|
|
114
123
|
def sandbox(self) -> interface.Sandbox:
|
|
115
124
|
"""Returns the sandbox that the feature is running in."""
|
|
@@ -188,7 +197,13 @@ class BaseFeature(interface.Feature):
|
|
|
188
197
|
error: BaseException | None = None
|
|
189
198
|
) -> None:
|
|
190
199
|
"""Called when the feature is setup."""
|
|
191
|
-
self.
|
|
200
|
+
self.environment.event_handler.on_feature_setup(
|
|
201
|
+
environment=self.environment,
|
|
202
|
+
sandbox=self.sandbox,
|
|
203
|
+
feature=self,
|
|
204
|
+
duration=duration,
|
|
205
|
+
error=error
|
|
206
|
+
)
|
|
192
207
|
|
|
193
208
|
def on_teardown(
|
|
194
209
|
self,
|
|
@@ -196,7 +211,13 @@ class BaseFeature(interface.Feature):
|
|
|
196
211
|
error: BaseException | None = None
|
|
197
212
|
) -> None:
|
|
198
213
|
"""Called when the feature is teardown."""
|
|
199
|
-
self.
|
|
214
|
+
self.environment.event_handler.on_feature_teardown(
|
|
215
|
+
environment=self.environment,
|
|
216
|
+
sandbox=self.sandbox,
|
|
217
|
+
feature=self,
|
|
218
|
+
duration=duration,
|
|
219
|
+
error=error
|
|
220
|
+
)
|
|
200
221
|
|
|
201
222
|
def on_housekeep(
|
|
202
223
|
self,
|
|
@@ -205,8 +226,14 @@ class BaseFeature(interface.Feature):
|
|
|
205
226
|
**kwargs
|
|
206
227
|
) -> None:
|
|
207
228
|
"""Called when the feature has done housekeeping."""
|
|
208
|
-
self.
|
|
209
|
-
self
|
|
229
|
+
self.environment.event_handler.on_feature_housekeep(
|
|
230
|
+
environment=self.environment,
|
|
231
|
+
sandbox=self.sandbox,
|
|
232
|
+
feature=self,
|
|
233
|
+
counter=self._housekeep_counter,
|
|
234
|
+
duration=duration,
|
|
235
|
+
error=error,
|
|
236
|
+
**kwargs
|
|
210
237
|
)
|
|
211
238
|
|
|
212
239
|
def on_setup_session(
|
|
@@ -215,7 +242,14 @@ class BaseFeature(interface.Feature):
|
|
|
215
242
|
error: BaseException | None = None,
|
|
216
243
|
) -> None:
|
|
217
244
|
"""Called when the feature is setup for a user session."""
|
|
218
|
-
self.
|
|
245
|
+
self.environment.event_handler.on_feature_setup_session(
|
|
246
|
+
environment=self.environment,
|
|
247
|
+
sandbox=self.sandbox,
|
|
248
|
+
feature=self,
|
|
249
|
+
session_id=self.session_id,
|
|
250
|
+
duration=duration,
|
|
251
|
+
error=error
|
|
252
|
+
)
|
|
219
253
|
|
|
220
254
|
def on_teardown_session(
|
|
221
255
|
self,
|
|
@@ -223,7 +257,14 @@ class BaseFeature(interface.Feature):
|
|
|
223
257
|
error: BaseException | None = None,
|
|
224
258
|
) -> None:
|
|
225
259
|
"""Called when the feature is teardown for a user session."""
|
|
226
|
-
self.
|
|
260
|
+
self.environment.event_handler.on_feature_teardown_session(
|
|
261
|
+
environment=self.environment,
|
|
262
|
+
sandbox=self.sandbox,
|
|
263
|
+
feature=self,
|
|
264
|
+
session_id=self.session_id,
|
|
265
|
+
duration=duration,
|
|
266
|
+
error=error
|
|
267
|
+
)
|
|
227
268
|
|
|
228
269
|
def on_activity(
|
|
229
270
|
self,
|
|
@@ -233,10 +274,13 @@ class BaseFeature(interface.Feature):
|
|
|
233
274
|
**kwargs
|
|
234
275
|
) -> None:
|
|
235
276
|
"""Called when a sandbox activity is performed."""
|
|
236
|
-
self.
|
|
277
|
+
self.environment.event_handler.on_sandbox_activity(
|
|
237
278
|
name=f'{self.name}.{name}',
|
|
279
|
+
environment=self.environment,
|
|
280
|
+
sandbox=self.sandbox,
|
|
238
281
|
feature=self,
|
|
239
|
-
|
|
282
|
+
session_id=self.session_id,
|
|
240
283
|
duration=duration,
|
|
284
|
+
error=error,
|
|
241
285
|
**kwargs
|
|
242
286
|
)
|