langfun 0.1.2.dev202509150805__py3-none-any.whl → 0.1.2.dev202509170804__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 +37 -0
- langfun/env/base_environment.py +491 -0
- langfun/env/base_feature.py +158 -0
- langfun/env/base_sandbox.py +444 -0
- langfun/env/base_test.py +795 -0
- langfun/env/interface.py +843 -0
- langfun/env/interface_test.py +43 -0
- langfun/env/load_balancers.py +59 -0
- langfun/env/load_balancers_test.py +157 -0
- {langfun-0.1.2.dev202509150805.dist-info → langfun-0.1.2.dev202509170804.dist-info}/METADATA +1 -1
- {langfun-0.1.2.dev202509150805.dist-info → langfun-0.1.2.dev202509170804.dist-info}/RECORD +14 -5
- {langfun-0.1.2.dev202509150805.dist-info → langfun-0.1.2.dev202509170804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202509150805.dist-info → langfun-0.1.2.dev202509170804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202509150805.dist-info → langfun-0.1.2.dev202509170804.dist-info}/top_level.txt +0 -0
langfun/env/base_test.py
ADDED
|
@@ -0,0 +1,795 @@
|
|
|
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 concurrent.futures
|
|
15
|
+
import contextlib
|
|
16
|
+
import time
|
|
17
|
+
from typing import Iterator
|
|
18
|
+
import unittest
|
|
19
|
+
|
|
20
|
+
from langfun.env import base_environment
|
|
21
|
+
from langfun.env import base_feature
|
|
22
|
+
from langfun.env import base_sandbox
|
|
23
|
+
from langfun.env import interface
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestingEnvironment(base_environment.BaseEnvironment):
|
|
27
|
+
|
|
28
|
+
ping_simulate_error: bool = False
|
|
29
|
+
keepalive_interval: float | None = 60.0
|
|
30
|
+
offline: bool = False
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def id(self) -> interface.EnvironmentId:
|
|
34
|
+
return interface.EnvironmentId('testing-env')
|
|
35
|
+
|
|
36
|
+
def set_offline(self, offline: bool) -> None:
|
|
37
|
+
self.rebind(
|
|
38
|
+
offline=offline, skip_notification=True, raise_on_no_change=False
|
|
39
|
+
)
|
|
40
|
+
for sandbox in self._sandbox_pool:
|
|
41
|
+
sandbox.rebind(
|
|
42
|
+
ping_simulate_error=offline,
|
|
43
|
+
skip_notification=True,
|
|
44
|
+
raise_on_no_change=False
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def _create_sandbox(
|
|
48
|
+
self,
|
|
49
|
+
sandbox_id: str,
|
|
50
|
+
reusable: bool
|
|
51
|
+
) -> interface.Sandbox:
|
|
52
|
+
if self.offline:
|
|
53
|
+
raise interface.EnvironmentError(
|
|
54
|
+
'Unknown environment error.',
|
|
55
|
+
environment=self,
|
|
56
|
+
)
|
|
57
|
+
return TestingSandbox(
|
|
58
|
+
environment=self,
|
|
59
|
+
id=interface.SandboxId(
|
|
60
|
+
environment_id=self.id,
|
|
61
|
+
sandbox_id=sandbox_id
|
|
62
|
+
),
|
|
63
|
+
reusable=reusable,
|
|
64
|
+
ping_simulate_error=self.ping_simulate_error,
|
|
65
|
+
keepalive_interval=self.keepalive_interval,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestingSandbox(base_sandbox.BaseSandbox):
|
|
70
|
+
|
|
71
|
+
ping_simulate_error: bool = False
|
|
72
|
+
|
|
73
|
+
def _on_bound(self) -> None:
|
|
74
|
+
super()._on_bound()
|
|
75
|
+
self._shell_history = []
|
|
76
|
+
self._ping_history = []
|
|
77
|
+
|
|
78
|
+
@base_sandbox.sandbox_service(critical_errors=(RuntimeError,))
|
|
79
|
+
def shell(
|
|
80
|
+
self,
|
|
81
|
+
code: str,
|
|
82
|
+
must_succeed: bool = True,
|
|
83
|
+
) -> str:
|
|
84
|
+
self._shell_history.append(code)
|
|
85
|
+
if 'bad' not in code:
|
|
86
|
+
return f'shell {len(self._shell_history)} succeeded'
|
|
87
|
+
|
|
88
|
+
message = f'shell {len(self._shell_history)} failed'
|
|
89
|
+
if must_succeed:
|
|
90
|
+
raise RuntimeError(message)
|
|
91
|
+
raise ValueError(message)
|
|
92
|
+
|
|
93
|
+
def _ping(self) -> None:
|
|
94
|
+
self._ping_history.append(not self.ping_simulate_error)
|
|
95
|
+
if self.ping_simulate_error:
|
|
96
|
+
raise interface.SandboxStateError(sandbox=self, code='ping')
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestingFeature(base_feature.BaseFeature):
|
|
100
|
+
housekeep_interval = 0
|
|
101
|
+
simulate_housekeep_error: bool = False
|
|
102
|
+
|
|
103
|
+
class Service:
|
|
104
|
+
"""Sandbox."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, sandbox: interface.Sandbox):
|
|
107
|
+
self._sandbox = sandbox
|
|
108
|
+
|
|
109
|
+
def do(self, code: str):
|
|
110
|
+
self._sandbox.shell(code)
|
|
111
|
+
|
|
112
|
+
def _setup(self) -> None:
|
|
113
|
+
self.sandbox.shell(f'echo "setup {self.name}"')
|
|
114
|
+
|
|
115
|
+
def _teardown(self) -> None:
|
|
116
|
+
self.sandbox.shell(f'echo "teardown {self.name}"')
|
|
117
|
+
|
|
118
|
+
@base_sandbox.sandbox_service()
|
|
119
|
+
def num_shell_calls(self) -> None:
|
|
120
|
+
return len(self.sandbox._shell_history)
|
|
121
|
+
|
|
122
|
+
@base_sandbox.sandbox_service()
|
|
123
|
+
def bad_shell_call(self) -> None:
|
|
124
|
+
self.sandbox.shell('bad command')
|
|
125
|
+
|
|
126
|
+
@base_sandbox.sandbox_service()
|
|
127
|
+
def show_session_id(self):
|
|
128
|
+
return self.session_id
|
|
129
|
+
|
|
130
|
+
def _on_bound(self) -> None:
|
|
131
|
+
super()._on_bound()
|
|
132
|
+
self._service = None
|
|
133
|
+
|
|
134
|
+
@base_sandbox.sandbox_service()
|
|
135
|
+
@contextlib.contextmanager
|
|
136
|
+
def my_service(self) -> Iterator[Service]:
|
|
137
|
+
try:
|
|
138
|
+
self._service = TestingFeature.Service(sandbox=self.sandbox)
|
|
139
|
+
yield self._service
|
|
140
|
+
finally:
|
|
141
|
+
self._service = None
|
|
142
|
+
|
|
143
|
+
def _housekeep(self) -> None:
|
|
144
|
+
if self.simulate_housekeep_error:
|
|
145
|
+
raise interface.SandboxStateError(
|
|
146
|
+
'House keeping error', sandbox=self.sandbox
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestingEnvironmentEventHandler(interface.EnvironmentEventHandler):
|
|
151
|
+
|
|
152
|
+
def __init__(self):
|
|
153
|
+
self.history = []
|
|
154
|
+
|
|
155
|
+
def _add_message(self, message: str, error: Exception | None) -> None:
|
|
156
|
+
"""Adds a message to the history."""
|
|
157
|
+
if error is None:
|
|
158
|
+
self.history.append(message)
|
|
159
|
+
else:
|
|
160
|
+
self.history.append(f'{message} with error: {error}')
|
|
161
|
+
|
|
162
|
+
def on_environment_start(
|
|
163
|
+
self,
|
|
164
|
+
environment: 'Environment',
|
|
165
|
+
error: Exception | None
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Called when the environment is started."""
|
|
168
|
+
self._add_message(f'[{environment.id}] environment started', error)
|
|
169
|
+
|
|
170
|
+
def on_environment_shutdown(
|
|
171
|
+
self,
|
|
172
|
+
environment: 'Environment',
|
|
173
|
+
error: Exception | None
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Called when the environment is shutdown."""
|
|
176
|
+
self._add_message(f'[{environment.id}] environment shutdown', error)
|
|
177
|
+
|
|
178
|
+
def on_sandbox_start(
|
|
179
|
+
self,
|
|
180
|
+
environment: interface.Environment,
|
|
181
|
+
sandbox: interface.Sandbox,
|
|
182
|
+
error: Exception | None
|
|
183
|
+
) -> None:
|
|
184
|
+
del environment
|
|
185
|
+
self._add_message(f'[{sandbox.id}] sandbox started', error)
|
|
186
|
+
|
|
187
|
+
def on_sandbox_shutdown(
|
|
188
|
+
self,
|
|
189
|
+
environment: interface.Environment,
|
|
190
|
+
sandbox: interface.Sandbox,
|
|
191
|
+
error: Exception | None
|
|
192
|
+
) -> None:
|
|
193
|
+
self._add_message(f'[{sandbox.id}] sandbox shutdown', error)
|
|
194
|
+
|
|
195
|
+
def on_sandbox_feature_setup(
|
|
196
|
+
self,
|
|
197
|
+
environment: interface.Environment,
|
|
198
|
+
sandbox: interface.Sandbox,
|
|
199
|
+
feature: interface.Feature,
|
|
200
|
+
error: Exception | None
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Called when a sandbox feature is setup."""
|
|
203
|
+
self._add_message(
|
|
204
|
+
f'[{sandbox.id}/{feature.name}] feature setup', error
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def on_sandbox_feature_teardown(
|
|
208
|
+
self,
|
|
209
|
+
environment: 'Environment',
|
|
210
|
+
sandbox: 'Sandbox',
|
|
211
|
+
feature: 'Feature',
|
|
212
|
+
error: Exception | None
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Called when a sandbox feature is teardown."""
|
|
215
|
+
self._add_message(
|
|
216
|
+
f'[{sandbox.id}/{feature.name}] feature teardown', error
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def on_sandbox_session_start(
|
|
220
|
+
self,
|
|
221
|
+
environment: 'Environment',
|
|
222
|
+
sandbox: 'Sandbox',
|
|
223
|
+
session_id: str,
|
|
224
|
+
error: Exception | None
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Called when a sandbox session starts."""
|
|
227
|
+
self._add_message(
|
|
228
|
+
f'[{sandbox.id}] session {session_id!r} started', error
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def on_sandbox_session_end(
|
|
232
|
+
self,
|
|
233
|
+
environment: 'Environment',
|
|
234
|
+
sandbox: 'Sandbox',
|
|
235
|
+
session_id: str,
|
|
236
|
+
error: Exception | None
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Called when a sandbox session ends."""
|
|
239
|
+
self._add_message(
|
|
240
|
+
f'[{sandbox.id}] session {session_id!r} ended', error
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class EnvironmentTest(unittest.TestCase):
|
|
245
|
+
|
|
246
|
+
def test_environment_no_pooling_normal(self):
|
|
247
|
+
event_handler = TestingEnvironmentEventHandler()
|
|
248
|
+
env = TestingEnvironment(
|
|
249
|
+
pool_size=0,
|
|
250
|
+
features={'test_feature': TestingFeature()},
|
|
251
|
+
outage_grace_period=1,
|
|
252
|
+
event_handler=event_handler,
|
|
253
|
+
outage_retry_interval=0,
|
|
254
|
+
)
|
|
255
|
+
self.assertFalse(env.is_alive)
|
|
256
|
+
self.assertEqual(env.min_pool_size, 0)
|
|
257
|
+
self.assertEqual(env.max_pool_size, 0)
|
|
258
|
+
self.assertEqual(env.sandbox_pool, [])
|
|
259
|
+
self.assertEqual(env.id, interface.EnvironmentId('testing-env'))
|
|
260
|
+
self.assertEqual(env.outage_grace_period, 1)
|
|
261
|
+
self.assertEqual(env.stats_report_interval, 60)
|
|
262
|
+
self.assertEqual(env.features['test_feature'].name, 'test_feature')
|
|
263
|
+
|
|
264
|
+
self.assertIsNone(env.start_time)
|
|
265
|
+
|
|
266
|
+
with env:
|
|
267
|
+
self.assertEqual(
|
|
268
|
+
event_handler.history,
|
|
269
|
+
[
|
|
270
|
+
'[testing-env] environment started',
|
|
271
|
+
]
|
|
272
|
+
)
|
|
273
|
+
self.assertIs(interface.Environment.current(), env)
|
|
274
|
+
self.assertTrue(env.is_alive)
|
|
275
|
+
self.assertIsNotNone(env.start_time)
|
|
276
|
+
self.assertEqual(env.offline_duration, 0.0)
|
|
277
|
+
self.assertEqual(env.sandbox_pool, [])
|
|
278
|
+
self.assertIsNone(env.working_dir)
|
|
279
|
+
|
|
280
|
+
with env.sandbox('session1') as sb:
|
|
281
|
+
self.assertEqual(env.sandbox_pool, [])
|
|
282
|
+
self.assertTrue(sb.is_alive)
|
|
283
|
+
self.assertTrue(sb.is_busy)
|
|
284
|
+
self.assertEqual(sb.session_id, 'session1')
|
|
285
|
+
self.assertIsNone(sb.working_dir)
|
|
286
|
+
self.assertEqual(
|
|
287
|
+
sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
|
|
288
|
+
)
|
|
289
|
+
sb.shell('echo "foo"')
|
|
290
|
+
self.assertIsInstance(sb.test_feature, TestingFeature)
|
|
291
|
+
self.assertEqual(sb.test_feature.session_id, 'session1')
|
|
292
|
+
with self.assertRaises(AttributeError):
|
|
293
|
+
_ = sb.test_feature2
|
|
294
|
+
self.assertIsNone(sb.test_feature._service)
|
|
295
|
+
with sb.test_feature.my_service() as service:
|
|
296
|
+
self.assertIs(sb.test_feature._service, service)
|
|
297
|
+
self.assertIsNone(sb.test_feature._service)
|
|
298
|
+
self.assertEqual(sb.session_id, 'session1')
|
|
299
|
+
|
|
300
|
+
with env.sandbox('session2') as sb:
|
|
301
|
+
self.assertEqual(env.sandbox_pool, [])
|
|
302
|
+
self.assertTrue(sb.is_alive)
|
|
303
|
+
self.assertTrue(sb.is_busy)
|
|
304
|
+
self.assertEqual(sb.session_id, 'session2')
|
|
305
|
+
self.assertEqual(
|
|
306
|
+
sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='1')
|
|
307
|
+
)
|
|
308
|
+
sb.shell('echo "bar"')
|
|
309
|
+
|
|
310
|
+
# Access the feature from the environment without obtaining a sandbox.
|
|
311
|
+
self.assertEqual(
|
|
312
|
+
env.test_feature.num_shell_calls(session_id='num_shell_session'), 1
|
|
313
|
+
)
|
|
314
|
+
with self.assertRaises(AttributeError):
|
|
315
|
+
_ = env.test_feature2
|
|
316
|
+
# Access the feature from the environment without obtaining a sandbox.
|
|
317
|
+
with env.test_feature.my_service(
|
|
318
|
+
session_id='my_service_session'
|
|
319
|
+
) as service:
|
|
320
|
+
service.do('echo "baz"')
|
|
321
|
+
|
|
322
|
+
self.assertFalse(env.is_alive)
|
|
323
|
+
self.assertIsNone(interface.Environment.current())
|
|
324
|
+
self.assertEqual(
|
|
325
|
+
event_handler.history,
|
|
326
|
+
[
|
|
327
|
+
'[testing-env] environment started',
|
|
328
|
+
'[testing-env/0/test_feature] feature setup',
|
|
329
|
+
'[testing-env/0] sandbox started',
|
|
330
|
+
'[testing-env/0] session \'session1\' started',
|
|
331
|
+
'[testing-env/0] session \'session1\' ended',
|
|
332
|
+
'[testing-env/0/test_feature] feature teardown',
|
|
333
|
+
'[testing-env/0] sandbox shutdown',
|
|
334
|
+
'[testing-env/1/test_feature] feature setup',
|
|
335
|
+
'[testing-env/1] sandbox started',
|
|
336
|
+
'[testing-env/1] session \'session2\' started',
|
|
337
|
+
'[testing-env/1] session \'session2\' ended',
|
|
338
|
+
'[testing-env/1/test_feature] feature teardown',
|
|
339
|
+
'[testing-env/1] sandbox shutdown',
|
|
340
|
+
'[testing-env/2/test_feature] feature setup',
|
|
341
|
+
'[testing-env/2] sandbox started',
|
|
342
|
+
'[testing-env/2] session \'num_shell_session\' started',
|
|
343
|
+
'[testing-env/2] session \'num_shell_session\' ended',
|
|
344
|
+
'[testing-env/2/test_feature] feature teardown',
|
|
345
|
+
'[testing-env/2] sandbox shutdown',
|
|
346
|
+
'[testing-env/3/test_feature] feature setup',
|
|
347
|
+
'[testing-env/3] sandbox started',
|
|
348
|
+
'[testing-env/3] session \'my_service_session\' started',
|
|
349
|
+
'[testing-env/3] session \'my_service_session\' ended',
|
|
350
|
+
'[testing-env/3/test_feature] feature teardown',
|
|
351
|
+
'[testing-env/3] sandbox shutdown',
|
|
352
|
+
'[testing-env] environment shutdown',
|
|
353
|
+
]
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def test_environment_no_pooling_state_error(self):
|
|
357
|
+
event_handler = TestingEnvironmentEventHandler()
|
|
358
|
+
env = TestingEnvironment(
|
|
359
|
+
root_dir='/tmp',
|
|
360
|
+
pool_size=0,
|
|
361
|
+
features={'test_feature': TestingFeature()},
|
|
362
|
+
outage_grace_period=1,
|
|
363
|
+
outage_retry_interval=0,
|
|
364
|
+
event_handler=event_handler,
|
|
365
|
+
)
|
|
366
|
+
self.assertEqual(env.working_dir, '/tmp/testing-env')
|
|
367
|
+
with env:
|
|
368
|
+
with env.sandbox('session1') as sb:
|
|
369
|
+
self.assertEqual(env.sandbox_pool, [])
|
|
370
|
+
self.assertTrue(sb.is_alive)
|
|
371
|
+
self.assertTrue(sb.is_busy)
|
|
372
|
+
self.assertFalse(sb.is_pending)
|
|
373
|
+
self.assertEqual(sb.session_id, 'session1')
|
|
374
|
+
self.assertEqual(sb.working_dir, '/tmp/testing-env/0')
|
|
375
|
+
self.assertEqual(
|
|
376
|
+
sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
|
|
377
|
+
)
|
|
378
|
+
# Non-critical error.
|
|
379
|
+
with self.assertRaises(ValueError):
|
|
380
|
+
sb.shell('bad command', must_succeed=False)
|
|
381
|
+
|
|
382
|
+
# Critical error.
|
|
383
|
+
with sb.test_feature.my_service() as service:
|
|
384
|
+
with self.assertRaises(interface.SandboxStateError):
|
|
385
|
+
service.do('bad command')
|
|
386
|
+
self.assertFalse(sb.is_alive)
|
|
387
|
+
|
|
388
|
+
with self.assertRaises(interface.SandboxStateError):
|
|
389
|
+
env.test_feature.bad_shell_call(session_id='bad_shell_session')
|
|
390
|
+
|
|
391
|
+
# Access the feature from the environment without obtaining a sandbox.
|
|
392
|
+
with env.test_feature.my_service(
|
|
393
|
+
session_id='my_service_session'
|
|
394
|
+
) as service:
|
|
395
|
+
with self.assertRaises(interface.SandboxStateError):
|
|
396
|
+
service.do('bad command')
|
|
397
|
+
|
|
398
|
+
self.assertFalse(env.is_alive)
|
|
399
|
+
self.assertIsNone(interface.Environment.current())
|
|
400
|
+
self.assertEqual(
|
|
401
|
+
event_handler.history,
|
|
402
|
+
[
|
|
403
|
+
'[testing-env] environment started',
|
|
404
|
+
'[testing-env/0/test_feature] feature setup',
|
|
405
|
+
'[testing-env/0] sandbox started',
|
|
406
|
+
'[testing-env/0] session \'session1\' started',
|
|
407
|
+
# Sandbox shutdown is triggered by the SandboxStateError before
|
|
408
|
+
# the session end event is triggered.
|
|
409
|
+
'[testing-env/0/test_feature] feature teardown',
|
|
410
|
+
'[testing-env/0] sandbox shutdown',
|
|
411
|
+
'[testing-env/0] session \'session1\' ended',
|
|
412
|
+
'[testing-env/1/test_feature] feature setup',
|
|
413
|
+
'[testing-env/1] sandbox started',
|
|
414
|
+
"[testing-env/1] session 'bad_shell_session' started",
|
|
415
|
+
'[testing-env/1/test_feature] feature teardown',
|
|
416
|
+
'[testing-env/1] sandbox shutdown',
|
|
417
|
+
"[testing-env/1] session 'bad_shell_session' ended",
|
|
418
|
+
'[testing-env/2/test_feature] feature setup',
|
|
419
|
+
'[testing-env/2] sandbox started',
|
|
420
|
+
"[testing-env/2] session 'my_service_session' started",
|
|
421
|
+
'[testing-env/2/test_feature] feature teardown',
|
|
422
|
+
'[testing-env/2] sandbox shutdown',
|
|
423
|
+
"[testing-env/2] session 'my_service_session' ended",
|
|
424
|
+
'[testing-env] environment shutdown',
|
|
425
|
+
]
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def test_environment_with_pooling_normal(self):
|
|
429
|
+
event_handler = TestingEnvironmentEventHandler()
|
|
430
|
+
env = TestingEnvironment(
|
|
431
|
+
pool_size=(1, 2),
|
|
432
|
+
features={'test_feature': TestingFeature()},
|
|
433
|
+
event_handler=event_handler,
|
|
434
|
+
# To make pool operation deterministic in the test.
|
|
435
|
+
pool_operation_max_parallelism=1,
|
|
436
|
+
outage_grace_period=1,
|
|
437
|
+
outage_retry_interval=0,
|
|
438
|
+
)
|
|
439
|
+
self.assertFalse(env.is_alive)
|
|
440
|
+
self.assertEqual(env.min_pool_size, 1)
|
|
441
|
+
self.assertEqual(env.max_pool_size, 2)
|
|
442
|
+
self.assertEqual(env.sandbox_pool, [])
|
|
443
|
+
self.assertEqual(env.id, interface.EnvironmentId('testing-env'))
|
|
444
|
+
self.assertEqual(env.outage_grace_period, 1)
|
|
445
|
+
self.assertEqual(env.stats_report_interval, 60)
|
|
446
|
+
self.assertEqual(env.features['test_feature'].name, 'test_feature')
|
|
447
|
+
|
|
448
|
+
self.assertIsNone(env.start_time)
|
|
449
|
+
|
|
450
|
+
with env:
|
|
451
|
+
self.assertEqual(
|
|
452
|
+
event_handler.history,
|
|
453
|
+
[
|
|
454
|
+
'[testing-env/0/test_feature] feature setup',
|
|
455
|
+
'[testing-env/0] sandbox started',
|
|
456
|
+
'[testing-env] environment started',
|
|
457
|
+
]
|
|
458
|
+
)
|
|
459
|
+
self.assertIs(interface.Environment.current(), env)
|
|
460
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
461
|
+
self.assertTrue(env.sandbox_pool[0].is_alive)
|
|
462
|
+
self.assertFalse(env.sandbox_pool[0].is_busy)
|
|
463
|
+
self.assertFalse(env.sandbox_pool[0].is_pending)
|
|
464
|
+
|
|
465
|
+
# Use the pooled server.
|
|
466
|
+
with env.sandbox('session1') as sb:
|
|
467
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
468
|
+
self.assertTrue(sb.is_alive)
|
|
469
|
+
self.assertFalse(sb.is_pending)
|
|
470
|
+
self.assertTrue(sb.is_busy)
|
|
471
|
+
self.assertEqual(sb.session_id, 'session1')
|
|
472
|
+
self.assertEqual(
|
|
473
|
+
sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
|
|
474
|
+
)
|
|
475
|
+
sb.shell('echo "foo"')
|
|
476
|
+
|
|
477
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
478
|
+
|
|
479
|
+
# Reuse the pooled server.
|
|
480
|
+
with env.sandbox('session2') as sb2:
|
|
481
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
482
|
+
self.assertTrue(sb2.is_alive)
|
|
483
|
+
self.assertTrue(sb2.is_busy)
|
|
484
|
+
self.assertEqual(sb2.session_id, 'session2')
|
|
485
|
+
self.assertEqual(
|
|
486
|
+
sb2.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
|
|
487
|
+
)
|
|
488
|
+
sb2.shell('echo "bar"')
|
|
489
|
+
|
|
490
|
+
# Dynamically bring up a new server in the pool.
|
|
491
|
+
with env.sandbox('session3') as sb3:
|
|
492
|
+
self.assertEqual(len(env.sandbox_pool), 2)
|
|
493
|
+
self.assertTrue(sb3.is_alive)
|
|
494
|
+
self.assertFalse(sb3.is_pending)
|
|
495
|
+
self.assertTrue(sb3.is_busy)
|
|
496
|
+
self.assertEqual(sb3.session_id, 'session3')
|
|
497
|
+
self.assertEqual(
|
|
498
|
+
sb3.id, interface.SandboxId(environment_id=env.id, sandbox_id='1')
|
|
499
|
+
)
|
|
500
|
+
sb3.shell('echo "baz"')
|
|
501
|
+
|
|
502
|
+
self.assertEqual(
|
|
503
|
+
env.stats(),
|
|
504
|
+
dict(
|
|
505
|
+
sandbox=dict(
|
|
506
|
+
num_total=2,
|
|
507
|
+
num_busy=2,
|
|
508
|
+
num_free=0,
|
|
509
|
+
num_dead=0,
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Environment overloaded as all pooled servers are busy, and there
|
|
515
|
+
# is no more quota to bring up new servers.
|
|
516
|
+
with self.assertRaises(interface.EnvironmentOverloadError):
|
|
517
|
+
with env.sandbox('session4'):
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
self.assertEqual(
|
|
521
|
+
env.stats(),
|
|
522
|
+
dict(
|
|
523
|
+
sandbox=dict(
|
|
524
|
+
num_total=2,
|
|
525
|
+
num_busy=1,
|
|
526
|
+
num_free=1,
|
|
527
|
+
num_dead=0,
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Access the feature from the environment without obtaining a sandbox.
|
|
533
|
+
self.assertEqual(
|
|
534
|
+
env.test_feature.num_shell_calls(session_id='num_shell_session'), 2
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
self.assertFalse(env.is_alive)
|
|
538
|
+
self.assertEqual(env.sandbox_pool, [])
|
|
539
|
+
self.assertIsNone(interface.Environment.current())
|
|
540
|
+
self.assertEqual(
|
|
541
|
+
event_handler.history,
|
|
542
|
+
[
|
|
543
|
+
'[testing-env/0/test_feature] feature setup',
|
|
544
|
+
'[testing-env/0] sandbox started',
|
|
545
|
+
# Environment ready is after the first sandbox is started.
|
|
546
|
+
'[testing-env] environment started',
|
|
547
|
+
'[testing-env/0] session \'session1\' started',
|
|
548
|
+
'[testing-env/0] session \'session1\' ended',
|
|
549
|
+
'[testing-env/0] session \'session2\' started',
|
|
550
|
+
'[testing-env/1/test_feature] feature setup',
|
|
551
|
+
'[testing-env/1] sandbox started',
|
|
552
|
+
'[testing-env/1] session \'session3\' started',
|
|
553
|
+
'[testing-env/1] session \'session3\' ended',
|
|
554
|
+
'[testing-env/1] session \'num_shell_session\' started',
|
|
555
|
+
'[testing-env/1] session \'num_shell_session\' ended',
|
|
556
|
+
'[testing-env/0] session \'session2\' ended',
|
|
557
|
+
'[testing-env/0/test_feature] feature teardown',
|
|
558
|
+
'[testing-env/0] sandbox shutdown',
|
|
559
|
+
'[testing-env/1/test_feature] feature teardown',
|
|
560
|
+
'[testing-env/1] sandbox shutdown',
|
|
561
|
+
'[testing-env] environment shutdown',
|
|
562
|
+
]
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def test_session_id(self):
|
|
566
|
+
event_handler = TestingEnvironmentEventHandler()
|
|
567
|
+
env = TestingEnvironment(
|
|
568
|
+
features={'test_feature': TestingFeature()},
|
|
569
|
+
event_handler=event_handler,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
with env:
|
|
573
|
+
with env.sandbox() as sb:
|
|
574
|
+
self.assertRegex(sb.session_id, r'session-[0-9a-f]{7}')
|
|
575
|
+
|
|
576
|
+
self.assertEqual(
|
|
577
|
+
env.test_feature.show_session_id(session_id='session1'),
|
|
578
|
+
'session1'
|
|
579
|
+
)
|
|
580
|
+
self.assertRegex(
|
|
581
|
+
env.test_feature.show_session_id(),
|
|
582
|
+
r'session-[0-9a-f]{7}'
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
with self.assertRaisesRegex(ValueError, '`session_id` should not be used'):
|
|
586
|
+
@base_sandbox.sandbox_service()
|
|
587
|
+
def foo(session_id: str):
|
|
588
|
+
del session_id
|
|
589
|
+
|
|
590
|
+
def test_environment_with_pooling_state_error(self):
|
|
591
|
+
event_handler = TestingEnvironmentEventHandler()
|
|
592
|
+
env = TestingEnvironment(
|
|
593
|
+
root_dir='/tmp',
|
|
594
|
+
pool_size=(1, 2),
|
|
595
|
+
outage_grace_period=1,
|
|
596
|
+
outage_retry_interval=0,
|
|
597
|
+
event_handler=event_handler,
|
|
598
|
+
)
|
|
599
|
+
self.assertEqual(env.working_dir, '/tmp/testing-env')
|
|
600
|
+
with env:
|
|
601
|
+
with env.sandbox('session1') as sb:
|
|
602
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
603
|
+
self.assertTrue(sb.is_alive)
|
|
604
|
+
self.assertTrue(sb.is_busy)
|
|
605
|
+
self.assertEqual(sb.session_id, 'session1')
|
|
606
|
+
self.assertEqual(
|
|
607
|
+
sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Non-critical error.
|
|
611
|
+
with self.assertRaises(ValueError):
|
|
612
|
+
sb.shell('bad command', must_succeed=False)
|
|
613
|
+
|
|
614
|
+
# Critical error.
|
|
615
|
+
with self.assertRaises(interface.SandboxStateError):
|
|
616
|
+
sb.shell('bad command')
|
|
617
|
+
maintenance_count = env._maintenance_count
|
|
618
|
+
self.assertFalse(sb.is_alive)
|
|
619
|
+
|
|
620
|
+
self.assertTrue(env.is_alive)
|
|
621
|
+
|
|
622
|
+
# Wait dead sandbox to be replaced.
|
|
623
|
+
while env._maintenance_count == maintenance_count:
|
|
624
|
+
time.sleep(0.5)
|
|
625
|
+
|
|
626
|
+
self.assertTrue(env.sandbox_pool[0].is_alive)
|
|
627
|
+
self.assertEqual(
|
|
628
|
+
env.stats(),
|
|
629
|
+
dict(
|
|
630
|
+
sandbox=dict(
|
|
631
|
+
num_total=1,
|
|
632
|
+
num_busy=0,
|
|
633
|
+
num_free=1,
|
|
634
|
+
num_dead=0,
|
|
635
|
+
)
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
self.assertFalse(env.is_alive)
|
|
640
|
+
self.assertIsNone(interface.Environment.current())
|
|
641
|
+
self.assertEqual(
|
|
642
|
+
event_handler.history,
|
|
643
|
+
[
|
|
644
|
+
'[testing-env/0] sandbox started',
|
|
645
|
+
'[testing-env] environment started',
|
|
646
|
+
'[testing-env/0] session \'session1\' started',
|
|
647
|
+
# Sandbox shutdown is triggered by the SandboxStateError before
|
|
648
|
+
# the session end event is triggered.
|
|
649
|
+
'[testing-env/0] sandbox shutdown',
|
|
650
|
+
'[testing-env/0] session \'session1\' ended',
|
|
651
|
+
# The maintenance loop will replace the dead sandbox and start a new
|
|
652
|
+
# sandbox.
|
|
653
|
+
'[testing-env/0] sandbox started',
|
|
654
|
+
'[testing-env/0] sandbox shutdown',
|
|
655
|
+
'[testing-env] environment shutdown',
|
|
656
|
+
]
|
|
657
|
+
)
|
|
658
|
+
self.assertEqual(
|
|
659
|
+
env.stats(),
|
|
660
|
+
dict(
|
|
661
|
+
sandbox=dict(
|
|
662
|
+
num_total=0,
|
|
663
|
+
num_busy=0,
|
|
664
|
+
num_free=0,
|
|
665
|
+
num_dead=0,
|
|
666
|
+
)
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
def test_environment_outage_with_pooling(self):
|
|
671
|
+
event_handler = TestingEnvironmentEventHandler()
|
|
672
|
+
env = TestingEnvironment(
|
|
673
|
+
features={'test_feature': TestingFeature()},
|
|
674
|
+
pool_size=(1, 2),
|
|
675
|
+
outage_grace_period=0,
|
|
676
|
+
outage_retry_interval=0,
|
|
677
|
+
keepalive_interval=0,
|
|
678
|
+
event_handler=event_handler,
|
|
679
|
+
stats_report_interval=1,
|
|
680
|
+
)
|
|
681
|
+
with env:
|
|
682
|
+
with env.sandbox('session1') as sb:
|
|
683
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
684
|
+
self.assertTrue(sb.is_alive)
|
|
685
|
+
self.assertTrue(sb.is_busy)
|
|
686
|
+
self.assertEqual(sb.session_id, 'session1')
|
|
687
|
+
env.set_offline(True)
|
|
688
|
+
housekeep_count = sb._housekeep_count
|
|
689
|
+
while sb._housekeep_count == housekeep_count:
|
|
690
|
+
time.sleep(1.0)
|
|
691
|
+
self.assertFalse(sb.is_alive)
|
|
692
|
+
maintenance_count = env._maintenance_count
|
|
693
|
+
while env._maintenance_count == maintenance_count:
|
|
694
|
+
time.sleep(1.0)
|
|
695
|
+
self.assertFalse(env.is_alive)
|
|
696
|
+
|
|
697
|
+
with self.assertRaises(interface.EnvironmentOutageError):
|
|
698
|
+
with env.sandbox('session2'):
|
|
699
|
+
pass
|
|
700
|
+
|
|
701
|
+
with self.assertRaises(interface.EnvironmentOutageError):
|
|
702
|
+
with env.test_feature.my_service():
|
|
703
|
+
pass
|
|
704
|
+
self.assertGreater(env.offline_duration, 0)
|
|
705
|
+
|
|
706
|
+
def test_environment_outage_during_acquire(self):
|
|
707
|
+
event_handler = TestingEnvironmentEventHandler()
|
|
708
|
+
env = TestingEnvironment(
|
|
709
|
+
features={'test_feature': TestingFeature()},
|
|
710
|
+
pool_size=(2, 3),
|
|
711
|
+
outage_grace_period=0,
|
|
712
|
+
outage_retry_interval=0,
|
|
713
|
+
keepalive_interval=0,
|
|
714
|
+
event_handler=event_handler,
|
|
715
|
+
stats_report_interval=1,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
def _thread_func(i) -> bool:
|
|
719
|
+
time.sleep(0.8 * i)
|
|
720
|
+
with env.sandbox(f'session{i}') as sb:
|
|
721
|
+
return sb.shell('echo "foo"')
|
|
722
|
+
|
|
723
|
+
with env:
|
|
724
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
|
|
725
|
+
fs = [
|
|
726
|
+
executor.submit(_thread_func, i) for i in range(5)
|
|
727
|
+
]
|
|
728
|
+
time.sleep(1.0)
|
|
729
|
+
env.set_offline(True)
|
|
730
|
+
vs = []
|
|
731
|
+
for f in fs:
|
|
732
|
+
try:
|
|
733
|
+
vs.append(f.result())
|
|
734
|
+
except interface.EnvironmentError as e:
|
|
735
|
+
vs.append(e)
|
|
736
|
+
|
|
737
|
+
def test_environment_outage_during_acquire_pool_not_full(self):
|
|
738
|
+
event_handler = TestingEnvironmentEventHandler()
|
|
739
|
+
env = TestingEnvironment(
|
|
740
|
+
features={
|
|
741
|
+
'test_feature': TestingFeature(simulate_housekeep_error=True)
|
|
742
|
+
},
|
|
743
|
+
pool_size=(1, 3),
|
|
744
|
+
outage_grace_period=1,
|
|
745
|
+
outage_retry_interval=0,
|
|
746
|
+
keepalive_interval=0,
|
|
747
|
+
event_handler=event_handler,
|
|
748
|
+
stats_report_interval=1,
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
def _thread_func() -> bool:
|
|
752
|
+
with env.sandbox('session1') as sb:
|
|
753
|
+
return sb.shell('echo "foo"')
|
|
754
|
+
|
|
755
|
+
with env:
|
|
756
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
757
|
+
self.assertFalse(env.sandbox_pool[0].is_alive)
|
|
758
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
759
|
+
env.set_offline(True)
|
|
760
|
+
f = executor.submit(_thread_func)
|
|
761
|
+
with self.assertRaises(interface.EnvironmentOutageError):
|
|
762
|
+
f.result()
|
|
763
|
+
|
|
764
|
+
def test_housekeep_error(self):
|
|
765
|
+
event_handler = TestingEnvironmentEventHandler()
|
|
766
|
+
env = TestingEnvironment(
|
|
767
|
+
features={
|
|
768
|
+
'test_feature': TestingFeature(
|
|
769
|
+
housekeep_interval=0
|
|
770
|
+
)
|
|
771
|
+
},
|
|
772
|
+
pool_size=1,
|
|
773
|
+
outage_grace_period=0,
|
|
774
|
+
outage_retry_interval=0,
|
|
775
|
+
keepalive_interval=0,
|
|
776
|
+
event_handler=event_handler,
|
|
777
|
+
stats_report_interval=1,
|
|
778
|
+
)
|
|
779
|
+
with env:
|
|
780
|
+
with env.sandbox('session1') as sb:
|
|
781
|
+
self.assertEqual(len(env.sandbox_pool), 1)
|
|
782
|
+
self.assertTrue(sb.is_alive)
|
|
783
|
+
self.assertTrue(sb.is_busy)
|
|
784
|
+
self.assertEqual(sb.session_id, 'session1')
|
|
785
|
+
housekeep_count = sb._housekeep_count
|
|
786
|
+
sb.test_feature.rebind(
|
|
787
|
+
simulate_housekeep_error=True, skip_notification=True
|
|
788
|
+
)
|
|
789
|
+
while sb._housekeep_count == housekeep_count or sb.is_alive:
|
|
790
|
+
time.sleep(1.0)
|
|
791
|
+
self.assertFalse(sb.is_alive)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
if __name__ == '__main__':
|
|
795
|
+
unittest.main()
|