|
2 | 2 | # Licensed under the MIT License. |
3 | 3 |
|
4 | 4 | import inspect |
| 5 | +import random |
5 | 6 | import threading |
6 | 7 |
|
7 | 8 | import grpc |
|
20 | 21 | SandboxWorkerProfileImageOptions, |
21 | 22 | ) |
22 | 23 | from durabletask.azuremanaged.preview.sandboxes.profile_builder import ( |
| 24 | + _ActivityOwnerIndex, |
23 | 25 | _build_sandbox_worker_profile, |
24 | 26 | build_sandbox_worker_profiles, |
25 | 27 | ) |
|
28 | 30 | build_sandbox_worker_start, |
29 | 31 | ) |
30 | 32 | from durabletask.azuremanaged.preview.sandboxes.helpers import resolve_activities |
| 33 | +from durabletask.azuremanaged.preview.sandboxes.helpers import activities_overlap |
31 | 34 | from durabletask.azuremanaged.preview.sandboxes.helpers import SandboxActivity |
32 | 35 | from durabletask.azuremanaged.internal import sandbox_service_pb2 as pb |
33 | 36 | from durabletask.azuremanaged.internal import sandbox_service_pb2_grpc as stubs |
@@ -206,6 +209,9 @@ def configure(self, options: SandboxWorkerProfileOptions) -> None: |
206 | 209 | try: |
207 | 210 | build_sandbox_worker_profiles() |
208 | 211 | except ValueError as ex: |
| 212 | + assert str(ex) == ( |
| 213 | + "Sandbox activity 'pytestoverlapremotehello' is assigned to both worker profile " |
| 214 | + "'pytest-overlap-profile-a' and 'pytest-overlap-profile-b'.") |
209 | 215 | assert "pytestoverlapremotehello" in str(ex) |
210 | 216 | assert "pytest-overlap-profile-a" in str(ex) |
211 | 217 | assert "pytest-overlap-profile-b" in str(ex) |
@@ -252,6 +258,128 @@ def configure(self, options: SandboxWorkerProfileOptions) -> None: |
252 | 258 | sandbox_worker_profiles._worker_profiles.pop("pytest-version-profile-b", None) |
253 | 259 |
|
254 | 260 |
|
| 261 | +def test_build_sandbox_worker_profiles_rejects_versioned_activity_overlapping_unversioned_owner() -> None: |
| 262 | + @sandbox_worker_profile("pytest-unversioned-owner-profile-a") |
| 263 | + class PytestUnversionedOwnerProfileA(SandboxWorkerProfile): |
| 264 | + def configure(self, options: SandboxWorkerProfileOptions) -> None: |
| 265 | + options.image.image_ref = "example.azurecr.io/python-worker-a:v1" |
| 266 | + options.image.managed_identity_client_id = "image-pull-client-id" |
| 267 | + options.scheduler_managed_identity_client_id = "scheduler-client-id" |
| 268 | + options.add_activity("PytestUnversionedOwner", version=None) |
| 269 | + |
| 270 | + @sandbox_worker_profile("pytest-unversioned-owner-profile-b") |
| 271 | + class PytestUnversionedOwnerProfileB(SandboxWorkerProfile): |
| 272 | + def configure(self, options: SandboxWorkerProfileOptions) -> None: |
| 273 | + options.image.image_ref = "example.azurecr.io/python-worker-b:v1" |
| 274 | + options.image.managed_identity_client_id = "image-pull-client-id" |
| 275 | + options.scheduler_managed_identity_client_id = "scheduler-client-id" |
| 276 | + options.add_activity("pytestunversionedowner", version="v9") |
| 277 | + |
| 278 | + try: |
| 279 | + try: |
| 280 | + build_sandbox_worker_profiles() |
| 281 | + except ValueError as ex: |
| 282 | + assert str(ex) == ( |
| 283 | + "Sandbox activity 'pytestunversionedowner@v9' is assigned to both worker profile " |
| 284 | + "'pytest-unversioned-owner-profile-a' and 'pytest-unversioned-owner-profile-b'.") |
| 285 | + else: |
| 286 | + raise AssertionError( |
| 287 | + "Expected an unversioned sandbox activity to overlap every version of the same name.") |
| 288 | + finally: |
| 289 | + sandbox_worker_profiles._worker_profiles.pop("pytest-unversioned-owner-profile-a", None) |
| 290 | + sandbox_worker_profiles._worker_profiles.pop("pytest-unversioned-owner-profile-b", None) |
| 291 | + |
| 292 | + |
| 293 | +def test_build_sandbox_worker_profiles_rejects_unversioned_activity_overlapping_versioned_owner() -> None: |
| 294 | + @sandbox_worker_profile("pytest-versioned-owner-profile-a") |
| 295 | + class PytestVersionedOwnerProfileA(SandboxWorkerProfile): |
| 296 | + def configure(self, options: SandboxWorkerProfileOptions) -> None: |
| 297 | + options.image.image_ref = "example.azurecr.io/python-worker-a:v1" |
| 298 | + options.image.managed_identity_client_id = "image-pull-client-id" |
| 299 | + options.scheduler_managed_identity_client_id = "scheduler-client-id" |
| 300 | + options.add_activity("PytestVersionedOwner", version="v9") |
| 301 | + |
| 302 | + @sandbox_worker_profile("pytest-versioned-owner-profile-b") |
| 303 | + class PytestVersionedOwnerProfileB(SandboxWorkerProfile): |
| 304 | + def configure(self, options: SandboxWorkerProfileOptions) -> None: |
| 305 | + options.image.image_ref = "example.azurecr.io/python-worker-b:v1" |
| 306 | + options.image.managed_identity_client_id = "image-pull-client-id" |
| 307 | + options.scheduler_managed_identity_client_id = "scheduler-client-id" |
| 308 | + options.add_activity("pytestversionedowner", version=None) |
| 309 | + |
| 310 | + try: |
| 311 | + try: |
| 312 | + build_sandbox_worker_profiles() |
| 313 | + except ValueError as ex: |
| 314 | + assert str(ex) == ( |
| 315 | + "Sandbox activity 'pytestversionedowner' is assigned to both worker profile " |
| 316 | + "'pytest-versioned-owner-profile-a' and 'pytest-versioned-owner-profile-b'.") |
| 317 | + else: |
| 318 | + raise AssertionError( |
| 319 | + "Expected an unversioned sandbox activity to overlap an existing versioned owner.") |
| 320 | + finally: |
| 321 | + sandbox_worker_profiles._worker_profiles.pop("pytest-versioned-owner-profile-a", None) |
| 322 | + sandbox_worker_profiles._worker_profiles.pop("pytest-versioned-owner-profile-b", None) |
| 323 | + |
| 324 | + |
| 325 | +def test_build_sandbox_worker_profiles_rejects_identical_activity_versions() -> None: |
| 326 | + @sandbox_worker_profile("pytest-same-version-profile-a") |
| 327 | + class PytestSameVersionProfileA(SandboxWorkerProfile): |
| 328 | + def configure(self, options: SandboxWorkerProfileOptions) -> None: |
| 329 | + options.image.image_ref = "example.azurecr.io/python-worker-a:v1" |
| 330 | + options.image.managed_identity_client_id = "image-pull-client-id" |
| 331 | + options.scheduler_managed_identity_client_id = "scheduler-client-id" |
| 332 | + options.add_activity("PytestSameVersionActivity", version="v3") |
| 333 | + |
| 334 | + @sandbox_worker_profile("pytest-same-version-profile-b") |
| 335 | + class PytestSameVersionProfileB(SandboxWorkerProfile): |
| 336 | + def configure(self, options: SandboxWorkerProfileOptions) -> None: |
| 337 | + options.image.image_ref = "example.azurecr.io/python-worker-b:v1" |
| 338 | + options.image.managed_identity_client_id = "image-pull-client-id" |
| 339 | + options.scheduler_managed_identity_client_id = "scheduler-client-id" |
| 340 | + options.add_activity("pytestsameversionactivity", version="v3") |
| 341 | + |
| 342 | + try: |
| 343 | + try: |
| 344 | + build_sandbox_worker_profiles() |
| 345 | + except ValueError as ex: |
| 346 | + assert str(ex) == ( |
| 347 | + "Sandbox activity 'pytestsameversionactivity@v3' is assigned to both worker profile " |
| 348 | + "'pytest-same-version-profile-a' and 'pytest-same-version-profile-b'.") |
| 349 | + else: |
| 350 | + raise AssertionError("Expected identical sandbox activity versions to overlap.") |
| 351 | + finally: |
| 352 | + sandbox_worker_profiles._worker_profiles.pop("pytest-same-version-profile-a", None) |
| 353 | + sandbox_worker_profiles._worker_profiles.pop("pytest-same-version-profile-b", None) |
| 354 | + |
| 355 | + |
| 356 | +def test_activity_owner_index_matches_linear_overlap_scan() -> None: |
| 357 | + registrations = [ |
| 358 | + (SandboxActivity(name, version), worker_profile_id) |
| 359 | + for name in ("Alpha", "alpha", "BETA", "Gamma") |
| 360 | + for version in (None, "v1", "v2", "V1") |
| 361 | + for worker_profile_id in ("profile-a", "profile-b", "profile-c") |
| 362 | + ] |
| 363 | + |
| 364 | + for seed in range(25): |
| 365 | + shuffled = list(registrations) |
| 366 | + random.Random(seed).shuffle(shuffled) |
| 367 | + |
| 368 | + index = _ActivityOwnerIndex() |
| 369 | + owners: list[tuple[SandboxActivity, str]] = [] |
| 370 | + for activity, worker_profile_id in shuffled: |
| 371 | + expected = next( |
| 372 | + (owner_profile for owner_activity, owner_profile in owners |
| 373 | + if activities_overlap(owner_activity, activity) |
| 374 | + and owner_profile != worker_profile_id), |
| 375 | + None) |
| 376 | + |
| 377 | + assert index.find_conflicting_profile(activity, worker_profile_id) == expected |
| 378 | + |
| 379 | + owners.append((activity, worker_profile_id)) |
| 380 | + index.add(activity, worker_profile_id) |
| 381 | + |
| 382 | + |
255 | 383 | def test_profile_options_add_activity_accepts_callable() -> None: |
256 | 384 | def pytest_callable_remote_hello(_ctx, value): |
257 | 385 | return value |
|
0 commit comments