langfun 0.1.2.dev202510310805__py3-none-any.whl → 0.1.2.dev202511020804__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/core/eval/v2/runners_test.py +3 -0
- langfun/env/__init__.py +5 -3
- langfun/env/base_environment.py +74 -24
- langfun/env/base_environment_test.py +473 -0
- langfun/env/base_feature.py +77 -15
- langfun/env/base_feature_test.py +228 -0
- langfun/env/base_sandbox.py +40 -124
- langfun/env/{base_test.py → base_sandbox_test.py} +274 -730
- langfun/env/event_handlers/__init__.py +1 -1
- langfun/env/event_handlers/chain.py +233 -0
- langfun/env/event_handlers/chain_test.py +253 -0
- langfun/env/event_handlers/event_logger.py +95 -98
- langfun/env/event_handlers/event_logger_test.py +19 -19
- langfun/env/event_handlers/metric_writer.py +193 -157
- langfun/env/event_handlers/metric_writer_test.py +1 -1
- langfun/env/interface.py +677 -47
- langfun/env/interface_test.py +30 -1
- langfun/env/test_utils.py +111 -82
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/METADATA +1 -1
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/RECORD +23 -20
- langfun/env/event_handlers/base.py +0 -350
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510310805.dist-info → langfun-0.1.2.dev202511020804.dist-info}/top_level.txt +0 -0
|
@@ -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('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(image_id='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(image_id='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(image_id='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(image_id='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
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
-
"""Common base class for
|
|
14
|
+
"""Common base class for environment features.
|
|
15
15
|
|
|
16
|
-
This module provides an base class `BaseFeature` for
|
|
16
|
+
This module provides an base class `BaseFeature` for environment features,
|
|
17
17
|
which provides event handlers for the feature lifecycle events, which can be
|
|
18
18
|
overridden by subclasses to provide custom behaviors. Please note that this base
|
|
19
19
|
class is intended to provide a convenient way to implement features, and not
|
|
@@ -22,18 +22,24 @@ coupled with `BaseEnvironment` and `BaseSandbox`, and is expected to work with
|
|
|
22
22
|
the `Environment` and `Sandbox` interfaces directly.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
+
import contextlib
|
|
25
26
|
import functools
|
|
26
27
|
import os
|
|
27
28
|
import re
|
|
28
29
|
import time
|
|
29
|
-
from typing import Annotated, Callable
|
|
30
|
+
from typing import Annotated, Any, Callable, Iterator
|
|
30
31
|
|
|
31
32
|
from langfun.env import interface
|
|
32
33
|
import pyglove as pg
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class BaseFeature(interface.Feature):
|
|
36
|
-
"""Common base class for
|
|
37
|
+
"""Common base class for environment features."""
|
|
38
|
+
|
|
39
|
+
is_sandbox_based: Annotated[
|
|
40
|
+
bool,
|
|
41
|
+
'Whether the feature is sandbox-based.'
|
|
42
|
+
] = True
|
|
37
43
|
|
|
38
44
|
applicable_images: Annotated[
|
|
39
45
|
list[str],
|
|
@@ -110,10 +116,21 @@ class BaseFeature(interface.Feature):
|
|
|
110
116
|
super()._on_parent_change(old_parent, new_parent)
|
|
111
117
|
self.__dict__.pop('name', None)
|
|
112
118
|
|
|
119
|
+
@functools.cached_property
|
|
120
|
+
def environment(self) -> interface.Environment:
|
|
121
|
+
"""Returns the environment that the feature is running in."""
|
|
122
|
+
if self._sandbox is not None:
|
|
123
|
+
return self._sandbox.environment
|
|
124
|
+
env = self.sym_ancestor(lambda v: isinstance(v, interface.Environment))
|
|
125
|
+
assert env is not None, 'Feature is not put into an environment.'
|
|
126
|
+
return env
|
|
127
|
+
|
|
113
128
|
@property
|
|
114
|
-
def sandbox(self) -> interface.Sandbox:
|
|
129
|
+
def sandbox(self) -> interface.Sandbox | None:
|
|
115
130
|
"""Returns the sandbox that the feature is running in."""
|
|
116
|
-
assert self._sandbox is not None
|
|
131
|
+
assert self._sandbox is not None or not self.is_sandbox_based, (
|
|
132
|
+
'Feature has not been set up yet.'
|
|
133
|
+
)
|
|
117
134
|
return self._sandbox
|
|
118
135
|
|
|
119
136
|
@property
|
|
@@ -150,7 +167,7 @@ class BaseFeature(interface.Feature):
|
|
|
150
167
|
finally:
|
|
151
168
|
event_handler(duration=time.time() - start_time, error=error)
|
|
152
169
|
|
|
153
|
-
def setup(self, sandbox: interface.Sandbox) -> None:
|
|
170
|
+
def setup(self, sandbox: interface.Sandbox | None = None) -> None:
|
|
154
171
|
"""Sets up the feature."""
|
|
155
172
|
self._sandbox = sandbox
|
|
156
173
|
self._do(self._setup, self.on_setup)
|
|
@@ -188,7 +205,11 @@ class BaseFeature(interface.Feature):
|
|
|
188
205
|
error: BaseException | None = None
|
|
189
206
|
) -> None:
|
|
190
207
|
"""Called when the feature is setup."""
|
|
191
|
-
self.
|
|
208
|
+
self.environment.event_handler.on_feature_setup(
|
|
209
|
+
feature=self,
|
|
210
|
+
duration=duration,
|
|
211
|
+
error=error
|
|
212
|
+
)
|
|
192
213
|
|
|
193
214
|
def on_teardown(
|
|
194
215
|
self,
|
|
@@ -196,7 +217,11 @@ class BaseFeature(interface.Feature):
|
|
|
196
217
|
error: BaseException | None = None
|
|
197
218
|
) -> None:
|
|
198
219
|
"""Called when the feature is teardown."""
|
|
199
|
-
self.
|
|
220
|
+
self.environment.event_handler.on_feature_teardown(
|
|
221
|
+
feature=self,
|
|
222
|
+
duration=duration,
|
|
223
|
+
error=error
|
|
224
|
+
)
|
|
200
225
|
|
|
201
226
|
def on_housekeep(
|
|
202
227
|
self,
|
|
@@ -205,8 +230,12 @@ class BaseFeature(interface.Feature):
|
|
|
205
230
|
**kwargs
|
|
206
231
|
) -> None:
|
|
207
232
|
"""Called when the feature has done housekeeping."""
|
|
208
|
-
self.
|
|
209
|
-
self,
|
|
233
|
+
self.environment.event_handler.on_feature_housekeep(
|
|
234
|
+
feature=self,
|
|
235
|
+
counter=self._housekeep_counter,
|
|
236
|
+
duration=duration,
|
|
237
|
+
error=error,
|
|
238
|
+
**kwargs
|
|
210
239
|
)
|
|
211
240
|
|
|
212
241
|
def on_setup_session(
|
|
@@ -215,7 +244,12 @@ class BaseFeature(interface.Feature):
|
|
|
215
244
|
error: BaseException | None = None,
|
|
216
245
|
) -> None:
|
|
217
246
|
"""Called when the feature is setup for a user session."""
|
|
218
|
-
self.
|
|
247
|
+
self.environment.event_handler.on_feature_setup_session(
|
|
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,12 @@ 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
|
+
feature=self,
|
|
262
|
+
session_id=self.session_id,
|
|
263
|
+
duration=duration,
|
|
264
|
+
error=error
|
|
265
|
+
)
|
|
227
266
|
|
|
228
267
|
def on_activity(
|
|
229
268
|
self,
|
|
@@ -233,10 +272,33 @@ class BaseFeature(interface.Feature):
|
|
|
233
272
|
**kwargs
|
|
234
273
|
) -> None:
|
|
235
274
|
"""Called when a sandbox activity is performed."""
|
|
236
|
-
self.
|
|
275
|
+
self.environment.event_handler.on_feature_activity(
|
|
237
276
|
name=f'{self.name}.{name}',
|
|
238
277
|
feature=self,
|
|
239
|
-
|
|
278
|
+
session_id=self.session_id,
|
|
240
279
|
duration=duration,
|
|
280
|
+
error=error,
|
|
241
281
|
**kwargs
|
|
242
282
|
)
|
|
283
|
+
|
|
284
|
+
@contextlib.contextmanager
|
|
285
|
+
def track_activity(
|
|
286
|
+
self,
|
|
287
|
+
name: str,
|
|
288
|
+
**kwargs: Any
|
|
289
|
+
) -> Iterator[None]:
|
|
290
|
+
"""Context manager that tracks a feature activity."""
|
|
291
|
+
start_time = time.time()
|
|
292
|
+
error = None
|
|
293
|
+
try:
|
|
294
|
+
yield None
|
|
295
|
+
except BaseException as e: # pylint: disable=broad-except
|
|
296
|
+
error = e
|
|
297
|
+
raise
|
|
298
|
+
finally:
|
|
299
|
+
self.on_activity(
|
|
300
|
+
name=name,
|
|
301
|
+
duration=time.time() - start_time,
|
|
302
|
+
error=error,
|
|
303
|
+
**kwargs
|
|
304
|
+
)
|