langfun 0.1.2.dev202509220805__py3-none-any.whl → 0.1.2.dev202509240805__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 +0 -1
- langfun/env/base_environment.py +126 -66
- langfun/env/base_feature.py +79 -40
- langfun/env/base_sandbox.py +630 -124
- langfun/env/base_test.py +1283 -474
- langfun/env/interface.py +462 -334
- langfun/env/load_balancers.py +4 -4
- langfun/env/load_balancers_test.py +32 -57
- {langfun-0.1.2.dev202509220805.dist-info → langfun-0.1.2.dev202509240805.dist-info}/METADATA +1 -1
- {langfun-0.1.2.dev202509220805.dist-info → langfun-0.1.2.dev202509240805.dist-info}/RECORD +13 -13
- {langfun-0.1.2.dev202509220805.dist-info → langfun-0.1.2.dev202509240805.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202509220805.dist-info → langfun-0.1.2.dev202509240805.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202509220805.dist-info → langfun-0.1.2.dev202509240805.dist-info}/top_level.txt +0 -0
langfun/env/interface.py
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
import abc
|
|
17
17
|
import contextlib
|
|
18
18
|
import dataclasses
|
|
19
|
+
import enum
|
|
19
20
|
import os
|
|
20
|
-
from typing import Annotated, Any,
|
|
21
|
-
import uuid
|
|
21
|
+
from typing import Annotated, Any, ContextManager, ClassVar, Iterator, Optional
|
|
22
22
|
|
|
23
23
|
import pyglove as pg
|
|
24
24
|
|
|
@@ -165,107 +165,193 @@ class SandboxStateError(SandboxError):
|
|
|
165
165
|
super().__init__(message or default_message, *args, **kwargs)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
class FeatureTeardownError(SandboxError):
|
|
169
|
+
"""Base class for feature errors."""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
message: str | None = None,
|
|
174
|
+
*args,
|
|
175
|
+
errors: dict[str, BaseException],
|
|
176
|
+
**kwargs
|
|
177
|
+
):
|
|
178
|
+
self.errors = errors
|
|
179
|
+
super().__init__(
|
|
180
|
+
(message or
|
|
181
|
+
f'Feature teardown failed with user-defined errors: {errors}.'),
|
|
182
|
+
*args,
|
|
183
|
+
**kwargs
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def has_non_sandbox_state_error(self) -> bool:
|
|
188
|
+
"""Returns True if the feature teardown error has non-sandbox state error."""
|
|
189
|
+
return any(
|
|
190
|
+
not isinstance(e, SandboxStateError) for e in self.errors.values()
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class SessionTeardownError(SandboxError):
|
|
195
|
+
"""Base class for session errors."""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
message: str | None = None,
|
|
200
|
+
*args,
|
|
201
|
+
errors: dict[str, BaseException],
|
|
202
|
+
**kwargs
|
|
203
|
+
):
|
|
204
|
+
self.errors = errors
|
|
205
|
+
super().__init__(
|
|
206
|
+
(message or
|
|
207
|
+
f'Session teardown failed with user-defined errors: {errors}.'),
|
|
208
|
+
*args,
|
|
209
|
+
**kwargs
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def has_non_sandbox_state_error(self) -> bool:
|
|
214
|
+
"""Returns True if the feature teardown error has non-sandbox state error."""
|
|
215
|
+
return any(
|
|
216
|
+
not isinstance(e, SandboxStateError) for e in self.errors.values()
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
168
220
|
#
|
|
169
221
|
# Event handler.
|
|
170
222
|
#
|
|
171
223
|
|
|
172
224
|
|
|
173
|
-
class
|
|
174
|
-
"""Base class for
|
|
225
|
+
class SessionEventHandler:
|
|
226
|
+
"""Base class for session event handlers."""
|
|
175
227
|
|
|
176
|
-
def
|
|
228
|
+
def on_session_start(
|
|
177
229
|
self,
|
|
178
230
|
environment: 'Environment',
|
|
179
|
-
|
|
231
|
+
sandbox: 'Sandbox',
|
|
232
|
+
session_id: str,
|
|
233
|
+
error: BaseException | None
|
|
180
234
|
) -> None:
|
|
181
|
-
"""Called when
|
|
235
|
+
"""Called when a sandbox session starts."""
|
|
182
236
|
|
|
183
|
-
def
|
|
237
|
+
def on_session_activity(
|
|
184
238
|
self,
|
|
239
|
+
session_id: str,
|
|
240
|
+
name: str,
|
|
185
241
|
environment: 'Environment',
|
|
186
|
-
|
|
242
|
+
sandbox: 'Sandbox',
|
|
243
|
+
feature: Optional['Feature'],
|
|
244
|
+
error: BaseException | None,
|
|
245
|
+
**kwargs
|
|
187
246
|
) -> None:
|
|
188
|
-
"""Called when
|
|
247
|
+
"""Called when a sandbox activity is performed."""
|
|
189
248
|
|
|
190
|
-
def
|
|
249
|
+
def on_session_end(
|
|
191
250
|
self,
|
|
192
251
|
environment: 'Environment',
|
|
193
252
|
sandbox: 'Sandbox',
|
|
194
|
-
|
|
253
|
+
session_id: str,
|
|
254
|
+
error: BaseException | None
|
|
195
255
|
) -> None:
|
|
196
|
-
"""Called when a sandbox
|
|
256
|
+
"""Called when a sandbox session ends."""
|
|
197
257
|
|
|
198
|
-
|
|
258
|
+
|
|
259
|
+
class FeatureEventHandler:
|
|
260
|
+
"""Base class for feature event handlers."""
|
|
261
|
+
|
|
262
|
+
def on_feature_setup(
|
|
199
263
|
self,
|
|
200
264
|
environment: 'Environment',
|
|
201
265
|
sandbox: 'Sandbox',
|
|
202
|
-
|
|
266
|
+
feature: 'Feature',
|
|
267
|
+
error: BaseException | None
|
|
203
268
|
) -> None:
|
|
204
|
-
"""Called when a sandbox is
|
|
269
|
+
"""Called when a sandbox feature is setup."""
|
|
205
270
|
|
|
206
|
-
def
|
|
271
|
+
def on_feature_teardown(
|
|
207
272
|
self,
|
|
208
273
|
environment: 'Environment',
|
|
209
274
|
sandbox: 'Sandbox',
|
|
210
275
|
feature: 'Feature',
|
|
211
|
-
error:
|
|
276
|
+
error: BaseException | None
|
|
212
277
|
) -> None:
|
|
213
|
-
"""Called when a sandbox feature is
|
|
278
|
+
"""Called when a sandbox feature is teardown."""
|
|
214
279
|
|
|
215
|
-
def
|
|
280
|
+
def on_feature_teardown_session(
|
|
216
281
|
self,
|
|
217
282
|
environment: 'Environment',
|
|
218
283
|
sandbox: 'Sandbox',
|
|
219
284
|
feature: 'Feature',
|
|
220
|
-
|
|
285
|
+
session_id: str,
|
|
286
|
+
error: BaseException | None
|
|
221
287
|
) -> None:
|
|
222
|
-
"""Called when a
|
|
288
|
+
"""Called when a feature is teardown with a session."""
|
|
223
289
|
|
|
224
|
-
def
|
|
290
|
+
def on_feature_setup_session(
|
|
225
291
|
self,
|
|
226
292
|
environment: 'Environment',
|
|
227
293
|
sandbox: 'Sandbox',
|
|
228
294
|
feature: 'Feature',
|
|
229
|
-
|
|
295
|
+
session_id: str | None,
|
|
296
|
+
error: BaseException | None
|
|
230
297
|
) -> None:
|
|
231
|
-
"""Called when a
|
|
298
|
+
"""Called when a feature is setup with a session."""
|
|
232
299
|
|
|
233
|
-
def
|
|
300
|
+
def on_feature_housekeep(
|
|
234
301
|
self,
|
|
235
302
|
environment: 'Environment',
|
|
236
303
|
sandbox: 'Sandbox',
|
|
237
|
-
|
|
238
|
-
error:
|
|
304
|
+
feature: 'Feature',
|
|
305
|
+
error: BaseException | None
|
|
239
306
|
) -> None:
|
|
240
|
-
"""Called when a sandbox
|
|
307
|
+
"""Called when a sandbox feature is housekeeping."""
|
|
241
308
|
|
|
242
|
-
|
|
309
|
+
|
|
310
|
+
class SandboxEventHandler(FeatureEventHandler, SessionEventHandler):
|
|
311
|
+
"""Base class for sandbox event handlers."""
|
|
312
|
+
|
|
313
|
+
def on_sandbox_start(
|
|
243
314
|
self,
|
|
244
315
|
environment: 'Environment',
|
|
245
316
|
sandbox: 'Sandbox',
|
|
246
|
-
|
|
247
|
-
feature: Optional['Feature'],
|
|
248
|
-
error: Exception | None,
|
|
249
|
-
**kwargs
|
|
317
|
+
error: BaseException | None
|
|
250
318
|
) -> None:
|
|
251
|
-
"""Called when a sandbox
|
|
319
|
+
"""Called when a sandbox is started."""
|
|
252
320
|
|
|
253
|
-
def
|
|
321
|
+
def on_sandbox_status_change(
|
|
254
322
|
self,
|
|
255
323
|
environment: 'Environment',
|
|
256
324
|
sandbox: 'Sandbox',
|
|
257
|
-
|
|
258
|
-
|
|
325
|
+
old_status: 'Sandbox.Status',
|
|
326
|
+
new_status: 'Sandbox.Status',
|
|
259
327
|
) -> None:
|
|
260
|
-
"""Called when a sandbox
|
|
328
|
+
"""Called when a sandbox status changes."""
|
|
261
329
|
|
|
262
|
-
def
|
|
330
|
+
def on_sandbox_shutdown(
|
|
263
331
|
self,
|
|
264
332
|
environment: 'Environment',
|
|
265
333
|
sandbox: 'Sandbox',
|
|
266
|
-
error:
|
|
334
|
+
error: BaseException | None
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Called when a sandbox is shutdown."""
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class EnvironmentEventHandler(SandboxEventHandler):
|
|
340
|
+
"""Base class for environment event handlers."""
|
|
341
|
+
|
|
342
|
+
def on_environment_start(
|
|
343
|
+
self,
|
|
344
|
+
environment: 'Environment',
|
|
345
|
+
error: BaseException | None
|
|
267
346
|
) -> None:
|
|
268
|
-
"""Called when
|
|
347
|
+
"""Called when the environment is started."""
|
|
348
|
+
|
|
349
|
+
def on_environment_shutdown(
|
|
350
|
+
self,
|
|
351
|
+
environment: 'Environment',
|
|
352
|
+
error: BaseException | None
|
|
353
|
+
) -> None:
|
|
354
|
+
"""Called when the environment is shutdown."""
|
|
269
355
|
|
|
270
356
|
|
|
271
357
|
#
|
|
@@ -276,17 +362,34 @@ class EnvironmentEventHandler:
|
|
|
276
362
|
class Environment(pg.Object):
|
|
277
363
|
"""Base class for an environment."""
|
|
278
364
|
|
|
365
|
+
class Status(enum.Enum):
|
|
366
|
+
"""Environment state.
|
|
367
|
+
|
|
368
|
+
State transitions:
|
|
369
|
+
|
|
370
|
+
+---------------+ +-----------+
|
|
371
|
+
| <CREATED> | --(start)---> | ONLINE | -(shutdown or outage detected)
|
|
372
|
+
+---------------+ | +-----------+ |
|
|
373
|
+
| +-----------+ |
|
|
374
|
+
+------> | <OFFLINE> | <------------+
|
|
375
|
+
+-----------+
|
|
376
|
+
"""
|
|
377
|
+
CREATED = 'created'
|
|
378
|
+
ONLINE = 'online'
|
|
379
|
+
SHUTTING_DOWN = 'shutting_down'
|
|
380
|
+
OFFLINE = 'offline'
|
|
381
|
+
|
|
279
382
|
features: Annotated[
|
|
280
383
|
dict[str, 'Feature'],
|
|
281
384
|
'Features to be exposed by the environment.'
|
|
282
385
|
] = {}
|
|
283
386
|
|
|
284
|
-
|
|
285
|
-
EnvironmentEventHandler
|
|
387
|
+
event_handlers: Annotated[
|
|
388
|
+
list[EnvironmentEventHandler],
|
|
286
389
|
(
|
|
287
390
|
'User handler for the environment events.'
|
|
288
391
|
)
|
|
289
|
-
] =
|
|
392
|
+
] = []
|
|
290
393
|
|
|
291
394
|
_ENV_STACK: Annotated[
|
|
292
395
|
ClassVar[list['Environment']],
|
|
@@ -302,14 +405,14 @@ class Environment(pg.Object):
|
|
|
302
405
|
def id(self) -> EnvironmentId:
|
|
303
406
|
"""Returns the identifier for the environment."""
|
|
304
407
|
|
|
408
|
+
@property
|
|
305
409
|
@abc.abstractmethod
|
|
306
|
-
def
|
|
307
|
-
"""Returns the
|
|
410
|
+
def status(self) -> Status:
|
|
411
|
+
"""Returns the status of the environment."""
|
|
308
412
|
|
|
309
|
-
@property
|
|
310
413
|
@abc.abstractmethod
|
|
311
|
-
def
|
|
312
|
-
"""Returns
|
|
414
|
+
def stats(self) -> dict[str, Any]:
|
|
415
|
+
"""Returns the stats of the environment."""
|
|
313
416
|
|
|
314
417
|
@abc.abstractmethod
|
|
315
418
|
def start(self) -> None:
|
|
@@ -323,8 +426,7 @@ class Environment(pg.Object):
|
|
|
323
426
|
def shutdown(self) -> None:
|
|
324
427
|
"""Shuts down the environment.
|
|
325
428
|
|
|
326
|
-
|
|
327
|
-
EnvironmentError: If the environment is not available.
|
|
429
|
+
IMPORTANT: This method shall not raise any exceptions.
|
|
328
430
|
"""
|
|
329
431
|
|
|
330
432
|
@abc.abstractmethod
|
|
@@ -339,10 +441,19 @@ class Environment(pg.Object):
|
|
|
339
441
|
EnvironmentOverloadError: If the environment is overloaded.
|
|
340
442
|
"""
|
|
341
443
|
|
|
444
|
+
@abc.abstractmethod
|
|
445
|
+
def new_session_id(self) -> str:
|
|
446
|
+
"""Generates a new session ID."""
|
|
447
|
+
|
|
342
448
|
#
|
|
343
449
|
# Environment lifecycle.
|
|
344
450
|
#
|
|
345
451
|
|
|
452
|
+
@property
|
|
453
|
+
def is_online(self) -> bool:
|
|
454
|
+
"""Returns True if the environment is alive."""
|
|
455
|
+
return self.status == self.Status.ONLINE
|
|
456
|
+
|
|
346
457
|
def __enter__(self) -> 'Environment':
|
|
347
458
|
"""Enters the environment and sets it as the current environment."""
|
|
348
459
|
self.start()
|
|
@@ -394,123 +505,83 @@ class Environment(pg.Object):
|
|
|
394
505
|
return self.acquire().features[name]
|
|
395
506
|
raise AttributeError(name)
|
|
396
507
|
|
|
397
|
-
#
|
|
398
|
-
# Event handlers subclasses can override.
|
|
399
|
-
#
|
|
400
508
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if self.event_handler:
|
|
404
|
-
self.event_handler.on_environment_start(self, error)
|
|
509
|
+
class Sandbox(pg.Object):
|
|
510
|
+
"""Interface for sandboxes."""
|
|
405
511
|
|
|
406
|
-
|
|
407
|
-
"""
|
|
408
|
-
|
|
409
|
-
|
|
512
|
+
class Status(enum.Enum):
|
|
513
|
+
"""Sandbox state.
|
|
514
|
+
|
|
515
|
+
State transitions:
|
|
516
|
+
|
|
517
|
+
+---------------+ +---------------+
|
|
518
|
+
| <OFFLINE> | <------ | SHUTTING_DOWN |
|
|
519
|
+
+---------------+ +---------------+
|
|
520
|
+
^ ^
|
|
521
|
+
| |
|
|
522
|
+
(shutdown)| +------------------------+
|
|
523
|
+
| |
|
|
524
|
+
+-----------+ (call start) +------------+ |
|
|
525
|
+
| <CREATED> | -------------> | SETTING_UP | <----------------+ |
|
|
526
|
+
+-----------+ +------------+ | |
|
|
527
|
+
| | |
|
|
528
|
+
| (start succeeded) | |
|
|
529
|
+
| OR (_setup_session) | |
|
|
530
|
+
v | |
|
|
531
|
+
+---------+ | |
|
|
532
|
+
| READY | | |
|
|
533
|
+
+---------+ | |
|
|
534
|
+
| | |
|
|
535
|
+
| (set_acquired) | |
|
|
536
|
+
v | |
|
|
537
|
+
+----------+ | |
|
|
538
|
+
| ACQUIRED | | |
|
|
539
|
+
+----------+ | |
|
|
540
|
+
| | |
|
|
541
|
+
| (call start_session) | |
|
|
542
|
+
+------------+ | |
|
|
543
|
+
| SETTING_UP | --(failed) -----------+
|
|
544
|
+
+------------+ | |
|
|
545
|
+
| | |
|
|
546
|
+
v (start_session succeeded)| |
|
|
547
|
+
+--------------+ | |
|
|
548
|
+
| IN_SESSION |--(end_session)--+ |
|
|
549
|
+
+--------------+ |
|
|
550
|
+
| |
|
|
551
|
+
+------(shutdown)---------------+
|
|
552
|
+
"""
|
|
410
553
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
sandbox: 'Sandbox',
|
|
414
|
-
error: Exception | None = None
|
|
415
|
-
) -> None:
|
|
416
|
-
"""Called when a sandbox is started."""
|
|
417
|
-
if self.event_handler:
|
|
418
|
-
self.event_handler.on_sandbox_start(self, sandbox, error)
|
|
554
|
+
# The sandbox is created, but not yet started.
|
|
555
|
+
CREATED = 'created'
|
|
419
556
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
sandbox: 'Sandbox',
|
|
423
|
-
error: Exception | None = None
|
|
424
|
-
) -> None:
|
|
425
|
-
"""Called when a sandbox is shutdown."""
|
|
426
|
-
if self.event_handler:
|
|
427
|
-
self.event_handler.on_sandbox_shutdown(self, sandbox, error)
|
|
557
|
+
# The sandbox is being setting up to serve user sessions.
|
|
558
|
+
SETTING_UP = 'setting_up'
|
|
428
559
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
sandbox: 'Sandbox',
|
|
432
|
-
feature: 'Feature',
|
|
433
|
-
error: Exception | None = None
|
|
434
|
-
) -> None:
|
|
435
|
-
"""Called when a sandbox feature is setup."""
|
|
436
|
-
if self.event_handler:
|
|
437
|
-
self.event_handler.on_sandbox_feature_setup(self, sandbox, feature, error)
|
|
560
|
+
# The sandbox is set up and free to be acquired by the user.
|
|
561
|
+
READY = 'ready'
|
|
438
562
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
sandbox: 'Sandbox',
|
|
442
|
-
feature: 'Feature',
|
|
443
|
-
error: Exception | None = None
|
|
444
|
-
) -> None:
|
|
445
|
-
"""Called when a sandbox feature is teardown."""
|
|
446
|
-
if self.event_handler:
|
|
447
|
-
self.event_handler.on_sandbox_feature_teardown(
|
|
448
|
-
self, sandbox, feature, error
|
|
449
|
-
)
|
|
563
|
+
# The sandbox is acquired by a thread, but not yet in a user session.
|
|
564
|
+
ACQUIRED = 'acquired'
|
|
450
565
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
sandbox: 'Sandbox',
|
|
454
|
-
feature: 'Feature',
|
|
455
|
-
error: Exception | None = None
|
|
456
|
-
) -> None:
|
|
457
|
-
"""Called when a sandbox feature is housekeeping."""
|
|
458
|
-
if self.event_handler:
|
|
459
|
-
self.event_handler.on_sandbox_feature_housekeep(
|
|
460
|
-
self, sandbox, feature, error
|
|
461
|
-
)
|
|
566
|
+
# The sandbox is in a user session.
|
|
567
|
+
IN_SESSION = 'in_session'
|
|
462
568
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
sandbox: 'Sandbox',
|
|
466
|
-
session_id: str,
|
|
467
|
-
error: Exception | None = None
|
|
468
|
-
) -> None:
|
|
469
|
-
"""Called when a sandbox session starts."""
|
|
470
|
-
if self.event_handler:
|
|
471
|
-
self.event_handler.on_sandbox_session_start(
|
|
472
|
-
self, sandbox, session_id, error
|
|
473
|
-
)
|
|
569
|
+
# The sandbox is being shut down.
|
|
570
|
+
SHUTTING_DOWN = 'shutting_down'
|
|
474
571
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
sandbox: 'Sandbox',
|
|
478
|
-
session_id: str,
|
|
479
|
-
feature: Optional['Feature'] = None,
|
|
480
|
-
error: Exception | None = None,
|
|
481
|
-
**kwargs
|
|
482
|
-
) -> None:
|
|
483
|
-
"""Called when a sandbox activity is performed."""
|
|
484
|
-
if self.event_handler:
|
|
485
|
-
self.event_handler.on_sandbox_session_activity(
|
|
486
|
-
self, sandbox, feature, session_id, error, **kwargs
|
|
487
|
-
)
|
|
572
|
+
# The sandbox is offline.
|
|
573
|
+
OFFLINE = 'offline'
|
|
488
574
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
sandbox
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
self.event_handler.on_sandbox_session_end(
|
|
498
|
-
self, sandbox, session_id, error
|
|
575
|
+
@property
|
|
576
|
+
def is_online(self) -> bool:
|
|
577
|
+
"""Returns True if the sandbox is online."""
|
|
578
|
+
return self in (
|
|
579
|
+
Sandbox.Status.SETTING_UP,
|
|
580
|
+
Sandbox.Status.READY,
|
|
581
|
+
Sandbox.Status.ACQUIRED,
|
|
582
|
+
Sandbox.Status.IN_SESSION,
|
|
499
583
|
)
|
|
500
584
|
|
|
501
|
-
def on_sandbox_ping(
|
|
502
|
-
self,
|
|
503
|
-
sandbox: 'Sandbox',
|
|
504
|
-
error: Exception | None = None
|
|
505
|
-
) -> None:
|
|
506
|
-
"""Called when a sandbox is pinged."""
|
|
507
|
-
if self.event_handler:
|
|
508
|
-
self.event_handler.on_sandbox_ping(self, sandbox, error)
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
class Sandbox(pg.Object):
|
|
512
|
-
"""Interface for sandboxes."""
|
|
513
|
-
|
|
514
585
|
@property
|
|
515
586
|
@abc.abstractmethod
|
|
516
587
|
def id(self) -> SandboxId:
|
|
@@ -528,62 +599,179 @@ class Sandbox(pg.Object):
|
|
|
528
599
|
|
|
529
600
|
@property
|
|
530
601
|
@abc.abstractmethod
|
|
531
|
-
def
|
|
532
|
-
"""Returns
|
|
602
|
+
def status(self) -> Status:
|
|
603
|
+
"""Returns the status of the sandbox."""
|
|
533
604
|
|
|
534
605
|
@property
|
|
606
|
+
def is_online(self) -> bool:
|
|
607
|
+
"""Returns True if the sandbox is online."""
|
|
608
|
+
return self.status.is_online
|
|
609
|
+
|
|
535
610
|
@abc.abstractmethod
|
|
536
|
-
def
|
|
537
|
-
"""
|
|
611
|
+
def set_acquired(self) -> None:
|
|
612
|
+
"""Marks the sandbox as acquired."""
|
|
613
|
+
|
|
614
|
+
@abc.abstractmethod
|
|
615
|
+
def add_event_handler(
|
|
616
|
+
self,
|
|
617
|
+
event_handler: EnvironmentEventHandler
|
|
618
|
+
) -> None:
|
|
619
|
+
"""Sets the status of the sandbox."""
|
|
538
620
|
|
|
539
621
|
@abc.abstractmethod
|
|
540
|
-
def
|
|
541
|
-
|
|
622
|
+
def remove_event_handler(
|
|
623
|
+
self,
|
|
624
|
+
event_handler: EnvironmentEventHandler
|
|
625
|
+
) -> None:
|
|
626
|
+
"""Removes the status of the sandbox."""
|
|
542
627
|
|
|
543
628
|
@property
|
|
544
629
|
@abc.abstractmethod
|
|
545
|
-
def
|
|
546
|
-
"""Returns
|
|
630
|
+
def state_errors(self) -> list[SandboxStateError]:
|
|
631
|
+
"""Returns state errors encountered during sandbox's lifecycle."""
|
|
547
632
|
|
|
548
633
|
@abc.abstractmethod
|
|
549
634
|
def start(self) -> None:
|
|
550
635
|
"""Starts the sandbox.
|
|
551
636
|
|
|
637
|
+
State transitions:
|
|
638
|
+
CREATED -> SETTING_UP -> READY: When all sandbox and feature setup
|
|
639
|
+
succeeds.
|
|
640
|
+
CREATED -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When sandbox or feature
|
|
641
|
+
setup fails.
|
|
642
|
+
|
|
643
|
+
`start` and `shutdown` should be called in pairs, even when the sandbox
|
|
644
|
+
fails to start. This ensures proper cleanup.
|
|
645
|
+
|
|
646
|
+
Start may fail with two sources of errors:
|
|
647
|
+
|
|
648
|
+
1. SandboxStateError: If sandbox or feature setup fail due to enviroment
|
|
649
|
+
outage or sandbox state errors.
|
|
650
|
+
2. BaseException: If feature setup failed with user-defined errors, this
|
|
651
|
+
could happen when there is bug in the user code or non-environment code
|
|
652
|
+
failure.
|
|
653
|
+
|
|
654
|
+
In both cases, the sandbox will be shutdown automatically, and the error
|
|
655
|
+
will be add to `errors`. The sandbox is considered dead and will not be
|
|
656
|
+
further used.
|
|
657
|
+
|
|
552
658
|
Raises:
|
|
553
659
|
SandboxStateError: If the sandbox is in a bad state.
|
|
660
|
+
BaseException: If feature setup failed with user-defined errors.
|
|
554
661
|
"""
|
|
555
662
|
|
|
556
663
|
@abc.abstractmethod
|
|
557
664
|
def shutdown(self) -> None:
|
|
558
665
|
"""Shuts down the sandbox.
|
|
559
666
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
667
|
+
State transitions:
|
|
668
|
+
SHUTTING_DOWN -> SHUTTING_DOWN: No operation.
|
|
669
|
+
OFFLINE -> OFFLINE: No operation.
|
|
670
|
+
SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When sandbox and feature
|
|
671
|
+
setup fails.
|
|
672
|
+
IN_SESSION -> SHUTTING_DOWN -> OFFLINE: When user session exits while
|
|
673
|
+
sandbox is set not to reuse, or session teardown fails.
|
|
674
|
+
FREE -> SHUTTING_DOWN -> OFFLINE: When sandbox is shutdown when the
|
|
675
|
+
environment is shutting down, or housekeeping loop shuts down the
|
|
676
|
+
sandbox due to housekeeping failures.
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
Please be aware that `shutdown` will be called whenever an operation on the
|
|
680
|
+
sandbox encounters a critical error. This means, `shutdown` should not make
|
|
681
|
+
the assumption that the sandbox is in a healthy state, even `start` could
|
|
682
|
+
fail. As a result, `shutdown` must allow re-entry and be thread-safe with
|
|
683
|
+
other sandbox operations.
|
|
684
|
+
|
|
685
|
+
Shutdown may fail with two sources of errors:
|
|
686
|
+
|
|
687
|
+
1. SandboxStateError: If the sandbox is in a bad state, and feature teardown
|
|
688
|
+
logic depending on a healthy sandbox may fail. In such case, we do not
|
|
689
|
+
raise error to the user as the user session is considered completed. The
|
|
690
|
+
sandbox is abandoned and new user sessions will be served on other
|
|
691
|
+
sandboxes.
|
|
692
|
+
|
|
693
|
+
2. BaseException: The sandbox is in good state, but user code raises error
|
|
694
|
+
due to bug or non-environment code failure. In such case, errors will be
|
|
695
|
+
raised to the user so the error could be surfaced and handled properly.
|
|
696
|
+
The sandbox is treated as shutdown and will not be further used.
|
|
567
697
|
|
|
568
698
|
Raises:
|
|
569
|
-
|
|
699
|
+
BaseException: If feature teardown failed with user-defined errors.
|
|
570
700
|
"""
|
|
571
701
|
|
|
572
702
|
@abc.abstractmethod
|
|
573
|
-
def start_session(
|
|
703
|
+
def start_session(
|
|
704
|
+
self,
|
|
705
|
+
session_id: str,
|
|
706
|
+
) -> None:
|
|
574
707
|
"""Begins a user session with the sandbox.
|
|
575
708
|
|
|
709
|
+
State transitions:
|
|
710
|
+
ACQUIRED -> SETTING_UP -> IN_SESSION: When session setup succeeds.
|
|
711
|
+
ACQUIRED -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When session setup
|
|
712
|
+
fails.
|
|
713
|
+
|
|
714
|
+
A session is a sequence of stateful interactions with the sandbox.
|
|
715
|
+
Across different sessions the sandbox are considered stateless.
|
|
716
|
+
`start_session` and `end_session` should always be called in pairs, even
|
|
717
|
+
when the session fails to start. `Sandbox.new_session` context manager is
|
|
718
|
+
the recommended way to use `start_session` and `end_session` in pairs.
|
|
719
|
+
|
|
720
|
+
Starting a session may fail with two sources of errors:
|
|
721
|
+
|
|
722
|
+
1. SandboxStateError: If the sandbox is in a bad state or session setup
|
|
723
|
+
failed.
|
|
724
|
+
|
|
725
|
+
2. BaseException: If session setup failed with user-defined errors.
|
|
726
|
+
|
|
727
|
+
In both cases, the sandbox will be shutdown automatically and the
|
|
728
|
+
session will be considered ended. The error will be added to `errors`.
|
|
729
|
+
Future session will be served on other sandboxes.
|
|
730
|
+
|
|
576
731
|
Args:
|
|
577
732
|
session_id: The identifier for the user session.
|
|
578
733
|
|
|
579
734
|
Raises:
|
|
580
|
-
|
|
581
|
-
|
|
735
|
+
SandboxStateError: If the sandbox is already in a bad state or session
|
|
736
|
+
setup failed.
|
|
737
|
+
BaseException: If session setup failed with user-defined errors.
|
|
582
738
|
"""
|
|
583
739
|
|
|
584
740
|
@abc.abstractmethod
|
|
585
741
|
def end_session(self) -> None:
|
|
586
|
-
"""Ends the user session with the sandbox.
|
|
742
|
+
"""Ends the user session with the sandbox.
|
|
743
|
+
|
|
744
|
+
State transitions:
|
|
745
|
+
IN_SESSION -> READY: When user session exits normally, and sandbox is set
|
|
746
|
+
to reuse.
|
|
747
|
+
IN_SESSION -> SHUTTING_DOWN -> OFFLINE: When user session exits while
|
|
748
|
+
sandbox is set not to reuse, or session teardown fails.
|
|
749
|
+
IN_SESSION -> SETTING_UP -> READY: When user session exits normally, and
|
|
750
|
+
sandbox is set to reuse, and proactive session setup is enabled.
|
|
751
|
+
IN_SESSION -> SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When user session
|
|
752
|
+
exits normally, and proactive session setup is enabled but fails.
|
|
753
|
+
not IN_SESSION -> same state: No operation
|
|
754
|
+
|
|
755
|
+
`end_session` should always be called for each `start_session` call, even
|
|
756
|
+
when the session fails to start, to ensure proper cleanup.
|
|
757
|
+
|
|
758
|
+
`end_session` may fail with two sources of errors:
|
|
759
|
+
|
|
760
|
+
1. SandboxStateError: If the sandbox is in a bad state or session teardown
|
|
761
|
+
failed.
|
|
762
|
+
|
|
763
|
+
2. BaseException: If session teardown failed with user-defined errors.
|
|
764
|
+
|
|
765
|
+
In both cases, the sandbox will be shutdown automatically and the
|
|
766
|
+
session will be considered ended. The error will be added to `errors`.
|
|
767
|
+
Future session will be served on other sandboxes.
|
|
768
|
+
|
|
769
|
+
However, SandboxStateError encountered during `end_session` will NOT be
|
|
770
|
+
raised to the user as the user session is considered completed.
|
|
771
|
+
|
|
772
|
+
Raises:
|
|
773
|
+
BaseException: If session teardown failed with user-defined errors.
|
|
774
|
+
"""
|
|
587
775
|
|
|
588
776
|
@property
|
|
589
777
|
@abc.abstractmethod
|
|
@@ -598,10 +786,30 @@ class Sandbox(pg.Object):
|
|
|
598
786
|
#
|
|
599
787
|
|
|
600
788
|
@contextlib.contextmanager
|
|
601
|
-
def new_session(
|
|
602
|
-
|
|
789
|
+
def new_session(
|
|
790
|
+
self,
|
|
791
|
+
session_id: str | None = None,
|
|
792
|
+
) -> Iterator['Sandbox']:
|
|
793
|
+
"""Context manager for obtaining a sandbox for a user session.
|
|
794
|
+
|
|
795
|
+
State transitions:
|
|
796
|
+
ACQUIRED -> IN_SESSION -> READY: When session setup and teardown succeed.
|
|
797
|
+
ACQUIRED -> IN_SESSINO -> OFFLINE: When session setup or teardown fails.
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
session_id: The identifier for the user session. If not provided, a random
|
|
801
|
+
ID will be generated.
|
|
802
|
+
|
|
803
|
+
Yields:
|
|
804
|
+
The sandbox for the user session.
|
|
805
|
+
|
|
806
|
+
Raises:
|
|
807
|
+
SandboxStateError: If a session cannot be started on the sandbox.
|
|
808
|
+
BaseException: If session setup or teardown failed with user-defined
|
|
809
|
+
errors.
|
|
810
|
+
"""
|
|
603
811
|
if session_id is None:
|
|
604
|
-
session_id =
|
|
812
|
+
session_id = self.environment.new_session_id()
|
|
605
813
|
self.start_session(session_id)
|
|
606
814
|
try:
|
|
607
815
|
yield self
|
|
@@ -630,84 +838,6 @@ class Sandbox(pg.Object):
|
|
|
630
838
|
return self.features[name]
|
|
631
839
|
raise AttributeError(name)
|
|
632
840
|
|
|
633
|
-
#
|
|
634
|
-
# Event handlers subclasses can override.
|
|
635
|
-
#
|
|
636
|
-
|
|
637
|
-
def on_start(self, error: Exception | None = None) -> None:
|
|
638
|
-
"""Called when the sandbox is started."""
|
|
639
|
-
self.environment.on_sandbox_start(self, error)
|
|
640
|
-
|
|
641
|
-
def on_shutdown(self, error: Exception | None = None) -> None:
|
|
642
|
-
"""Called when the sandbox is shutdown."""
|
|
643
|
-
self.environment.on_sandbox_shutdown(self, error)
|
|
644
|
-
|
|
645
|
-
def on_ping(self, error: Exception | None = None) -> None:
|
|
646
|
-
"""Called when the sandbox is pinged."""
|
|
647
|
-
self.environment.on_sandbox_ping(self, error)
|
|
648
|
-
|
|
649
|
-
def on_feature_setup(
|
|
650
|
-
self,
|
|
651
|
-
feature: 'Feature',
|
|
652
|
-
error: Exception | None = None
|
|
653
|
-
) -> None:
|
|
654
|
-
"""Called when a feature is setup."""
|
|
655
|
-
self.environment.on_sandbox_feature_setup(
|
|
656
|
-
self, feature, error
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
def on_feature_teardown(
|
|
660
|
-
self,
|
|
661
|
-
feature: 'Feature',
|
|
662
|
-
error: Exception | None = None
|
|
663
|
-
) -> None:
|
|
664
|
-
"""Called when a feature is teardown."""
|
|
665
|
-
self.environment.on_sandbox_feature_teardown(
|
|
666
|
-
self, feature, error
|
|
667
|
-
)
|
|
668
|
-
|
|
669
|
-
def on_feature_housekeep(
|
|
670
|
-
self,
|
|
671
|
-
feature: 'Feature',
|
|
672
|
-
error: Exception | None = None
|
|
673
|
-
) -> None:
|
|
674
|
-
"""Called when a feature is housekeeping."""
|
|
675
|
-
self.environment.on_sandbox_feature_housekeep(
|
|
676
|
-
self, feature, error
|
|
677
|
-
)
|
|
678
|
-
|
|
679
|
-
def on_session_start(
|
|
680
|
-
self,
|
|
681
|
-
session_id: str,
|
|
682
|
-
error: Exception | None = None
|
|
683
|
-
) -> None:
|
|
684
|
-
"""Called when the user session starts."""
|
|
685
|
-
self.environment.on_sandbox_session_start(self, session_id, error)
|
|
686
|
-
|
|
687
|
-
def on_session_activity(
|
|
688
|
-
self,
|
|
689
|
-
session_id: str,
|
|
690
|
-
feature: Optional['Feature'] = None,
|
|
691
|
-
error: Exception | None = None,
|
|
692
|
-
**kwargs
|
|
693
|
-
) -> None:
|
|
694
|
-
"""Called when a sandbox activity is performed."""
|
|
695
|
-
self.environment.on_sandbox_session_activity(
|
|
696
|
-
sandbox=self,
|
|
697
|
-
feature=feature,
|
|
698
|
-
session_id=session_id,
|
|
699
|
-
error=error,
|
|
700
|
-
**kwargs
|
|
701
|
-
)
|
|
702
|
-
|
|
703
|
-
def on_session_end(
|
|
704
|
-
self,
|
|
705
|
-
session_id: str,
|
|
706
|
-
error: Exception | None = None
|
|
707
|
-
) -> None:
|
|
708
|
-
"""Called when the user session ends."""
|
|
709
|
-
self.environment.on_sandbox_session_end(self, session_id, error)
|
|
710
|
-
|
|
711
841
|
|
|
712
842
|
class Feature(pg.Object):
|
|
713
843
|
"""Interface for sandbox features."""
|
|
@@ -719,40 +849,107 @@ class Feature(pg.Object):
|
|
|
719
849
|
|
|
720
850
|
@property
|
|
721
851
|
@abc.abstractmethod
|
|
722
|
-
def sandbox(self) -> Sandbox
|
|
723
|
-
"""Returns the sandbox that the feature is running in.
|
|
852
|
+
def sandbox(self) -> Sandbox:
|
|
853
|
+
"""Returns the sandbox that the feature is running in.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
The sandbox that the feature is running in.
|
|
857
|
+
|
|
858
|
+
Raises:
|
|
859
|
+
AssertError: If the feature is not set up with a sandbox yet.
|
|
860
|
+
"""
|
|
724
861
|
|
|
725
862
|
@abc.abstractmethod
|
|
726
863
|
def setup(self, sandbox: Sandbox) -> None:
|
|
727
864
|
"""Sets up the feature, which is called once when the sandbox is up.
|
|
728
865
|
|
|
866
|
+
State transitions:
|
|
867
|
+
SETTING_UP -> READY: When setup succeeds.
|
|
868
|
+
SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When setup fails.
|
|
869
|
+
|
|
870
|
+
`setup` is called when a sandbox is started for the first time. When a
|
|
871
|
+
feature's `setup` is called, its `teardown` is guaranteed to be called.
|
|
872
|
+
|
|
729
873
|
Args:
|
|
730
874
|
sandbox: The sandbox that the feature is running in.
|
|
731
875
|
|
|
732
876
|
Raises:
|
|
733
|
-
SandboxStateError: If
|
|
877
|
+
SandboxStateError: If setup failed due to sandbox state errors.
|
|
878
|
+
BaseException: If setup failed with user-defined errors.
|
|
734
879
|
"""
|
|
735
880
|
|
|
736
881
|
@abc.abstractmethod
|
|
737
882
|
def teardown(self) -> None:
|
|
738
883
|
"""Teardowns the feature, which is called once when the sandbox is down.
|
|
739
884
|
|
|
885
|
+
State transitions:
|
|
886
|
+
SHUTTING_DOWN -> OFFLINE: When teardown succeeds or fails.
|
|
887
|
+
|
|
740
888
|
Raises:
|
|
741
889
|
SandboxStateError: If the sandbox is in a bad state.
|
|
742
890
|
"""
|
|
743
891
|
|
|
744
892
|
@abc.abstractmethod
|
|
745
|
-
def setup_session(self
|
|
746
|
-
"""Sets up the feature for
|
|
893
|
+
def setup_session(self) -> None:
|
|
894
|
+
"""Sets up the feature for the upcoming user session.
|
|
895
|
+
|
|
896
|
+
State transitions:
|
|
897
|
+
SETTING_UP -> READY: When session setup succeeds.
|
|
898
|
+
SETTING_UP -> SHUTTING_DOWN -> OFFLINE: When sessino setup fails.
|
|
899
|
+
|
|
900
|
+
`setup_session` is called when a new user session starts for per-session
|
|
901
|
+
setup. `setup_session` and `teardown_session` will be called in pairs, even
|
|
902
|
+
`setup_session` fails.
|
|
903
|
+
|
|
904
|
+
`setup_session` may fail with two sources of errors:
|
|
905
|
+
|
|
906
|
+
1. SandboxStateError: If the sandbox is in a bad state or session setup
|
|
907
|
+
failed.
|
|
908
|
+
2. BaseException: If session setup failed with user-defined errors.
|
|
909
|
+
|
|
910
|
+
In both cases, the error will be raised to the user and the session will be
|
|
911
|
+
ended. The sandbox will shutdown automatically and will not be further used.
|
|
912
|
+
|
|
913
|
+
Raises:
|
|
914
|
+
SandboxStateError: If the sandbox is in a bad state or session setup
|
|
915
|
+
failed.
|
|
916
|
+
BaseException: If session setup failed with user-defined errors.
|
|
917
|
+
"""
|
|
747
918
|
|
|
748
919
|
@abc.abstractmethod
|
|
749
|
-
def teardown_session(self
|
|
750
|
-
"""Teardowns the feature for
|
|
920
|
+
def teardown_session(self) -> None:
|
|
921
|
+
"""Teardowns the feature for an ending user session.
|
|
922
|
+
|
|
923
|
+
State transitions:
|
|
924
|
+
SHUTTING_DOWN -> OFFLINE: When session teardown succeeds or fails.
|
|
925
|
+
|
|
926
|
+
`teardown_session` is called when a user session ends for per-
|
|
927
|
+
session teardown. `teardown_session` will always be called upon a feature
|
|
928
|
+
whose `setup_session` is called.
|
|
929
|
+
|
|
930
|
+
`teardown_session` may fail with two sources of errors:
|
|
931
|
+
|
|
932
|
+
1. SandboxStateError: If the sandbox is in a bad state or session teardown
|
|
933
|
+
failed.
|
|
934
|
+
2. BaseException: If session teardown failed with user-defined errors.
|
|
935
|
+
|
|
936
|
+
In both cases, the session will be closed and the sandbox will be shutdown.
|
|
937
|
+
However, SandboxStateError encountered during `teardown_session` will NOT be
|
|
938
|
+
raised to the user as the user session is considered completed. Other errors
|
|
939
|
+
will be raised to the user for proper error handling.
|
|
940
|
+
|
|
941
|
+
Raises:
|
|
942
|
+
BaseException: If session teardown failed with user-defined errors.
|
|
943
|
+
"""
|
|
751
944
|
|
|
752
945
|
@abc.abstractmethod
|
|
753
946
|
def housekeep(self) -> None:
|
|
754
947
|
"""Performs housekeeping for the feature.
|
|
755
948
|
|
|
949
|
+
State transitions:
|
|
950
|
+
(no state change): When housekeeping succeeds.
|
|
951
|
+
original state -> SHUTTING_DOWN -> OFFLINE: When housekeeping fails.
|
|
952
|
+
|
|
756
953
|
Raises:
|
|
757
954
|
SandboxStateError: If the sandbox is in a bad state.
|
|
758
955
|
"""
|
|
@@ -772,72 +969,3 @@ class Feature(pg.Object):
|
|
|
772
969
|
"""Returns the current user session identifier."""
|
|
773
970
|
assert self.sandbox is not None
|
|
774
971
|
return self.sandbox.session_id
|
|
775
|
-
|
|
776
|
-
#
|
|
777
|
-
# Event handlers subclasses can override.
|
|
778
|
-
#
|
|
779
|
-
|
|
780
|
-
def on_setup(
|
|
781
|
-
self,
|
|
782
|
-
error: BaseException | None = None
|
|
783
|
-
) -> None:
|
|
784
|
-
"""Called when the feature is setup."""
|
|
785
|
-
self.sandbox.on_feature_setup(self, error)
|
|
786
|
-
|
|
787
|
-
def on_teardown(
|
|
788
|
-
self,
|
|
789
|
-
error: BaseException | None = None
|
|
790
|
-
) -> None:
|
|
791
|
-
"""Called when the feature is teardown."""
|
|
792
|
-
self.sandbox.on_feature_teardown(self, error)
|
|
793
|
-
|
|
794
|
-
def on_housekeep(
|
|
795
|
-
self,
|
|
796
|
-
error: BaseException | None = None
|
|
797
|
-
) -> None:
|
|
798
|
-
"""Called when the feature has done housekeeping."""
|
|
799
|
-
self.sandbox.on_feature_housekeep(self, error)
|
|
800
|
-
|
|
801
|
-
def on_session_setup(
|
|
802
|
-
self,
|
|
803
|
-
session_id: str,
|
|
804
|
-
error: BaseException | None = None,
|
|
805
|
-
) -> None:
|
|
806
|
-
"""Called when the feature is setup for a user session."""
|
|
807
|
-
|
|
808
|
-
def on_session_activity(
|
|
809
|
-
self,
|
|
810
|
-
session_id: str,
|
|
811
|
-
error: Exception | None,
|
|
812
|
-
**kwargs
|
|
813
|
-
) -> None:
|
|
814
|
-
"""Called when a sandbox activity is performed."""
|
|
815
|
-
self.sandbox.on_session_activity(
|
|
816
|
-
feature=self, session_id=session_id, error=error, **kwargs
|
|
817
|
-
)
|
|
818
|
-
|
|
819
|
-
def on_session_teardown(
|
|
820
|
-
self,
|
|
821
|
-
session_id: str,
|
|
822
|
-
error: BaseException | None = None,
|
|
823
|
-
) -> None:
|
|
824
|
-
"""Called when the feature is teardown for a user session."""
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
def call_with_event(
|
|
828
|
-
action: Callable[[], None],
|
|
829
|
-
event_handler: Callable[..., None],
|
|
830
|
-
action_kwargs: dict[str, Any] | None = None,
|
|
831
|
-
event_handler_kwargs: dict[str, Any] | None = None,
|
|
832
|
-
) -> None:
|
|
833
|
-
"""Triggers an event handler."""
|
|
834
|
-
action_kwargs = action_kwargs or {}
|
|
835
|
-
event_handler_kwargs = event_handler_kwargs or {}
|
|
836
|
-
error = None
|
|
837
|
-
try:
|
|
838
|
-
action(**action_kwargs)
|
|
839
|
-
except BaseException as e: # pylint: disable=broad-except
|
|
840
|
-
error = e
|
|
841
|
-
raise
|
|
842
|
-
finally:
|
|
843
|
-
event_handler(error=error, **event_handler_kwargs)
|