uber-compose 2.3.3__tar.gz → 3.0.0a1__tar.gz
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.
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/PKG-INFO +2 -1
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/README.md +1 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/__init__.py +22 -1
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/constants.py +11 -4
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/docker_compose.py +10 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/sequence_run_types.py +2 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/system_docker_compose.py +3 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/compose_instance_cfg.py +9 -2
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/state_waiting.py +6 -2
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/errors/up.py +4 -0
- uber_compose-3.0.0a1/uber_compose/helpers/broken_services.py +47 -0
- uber_compose-3.0.0a1/uber_compose/helpers/current_env.py +20 -0
- uber_compose-3.0.0a1/uber_compose/helpers/env_id.py +21 -0
- uber_compose-3.0.0a1/uber_compose/helpers/hooks.py +24 -0
- uber_compose-3.0.0a1/uber_compose/parallel_plugin/__init__.py +39 -0
- uber_compose-3.0.0a1/uber_compose/parallel_plugin/grouper.py +97 -0
- uber_compose-3.0.0a1/uber_compose/parallel_plugin/grouping.py +30 -0
- uber_compose-3.0.0a1/uber_compose/parallel_plugin/plugin.py +149 -0
- uber_compose-3.0.0a1/uber_compose/parallel_plugin/runner.py +205 -0
- uber_compose-3.0.0a1/uber_compose/parallel_plugin/types.py +34 -0
- uber_compose-3.0.0a1/uber_compose/parallel_plugin/worker_api.py +124 -0
- uber_compose-3.0.0a1/uber_compose/parallel_plugin/worker_filter.py +38 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/uber_compose.py +225 -5
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/base_structures/common_json_cli.py +18 -8
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/helpers/scenario_tag_processing.py +6 -6
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/plugin.py +91 -2
- uber_compose-3.0.0a1/uber_compose/version +1 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/PKG-INFO +2 -1
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/SOURCES.txt +11 -0
- uber_compose-2.3.3/uber_compose/helpers/broken_services.py +0 -29
- uber_compose-2.3.3/uber_compose/version +0 -1
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/LICENSE +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/setup.cfg +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/setup.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/docker_compose_shell/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/docker_compose_shell/interface.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/docker_compose_shell/types.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/compose_files.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/env_files.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/process_command_output.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/shell_process.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/env_description/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/env_description/env_tools.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/env_description/env_types.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/errors/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/errors/migrations.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/bytes_pickle.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/countdown_counter.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/exec_record.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/exec_result.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/health_policy.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/jobs_result.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/labels.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/singleton.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/state_keeper.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/output/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/output/console.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/output/styles.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/docker_compose_files_path.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/docker_compose_service_deps.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/search_docker_compose_files.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/services_construction.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/base_structures/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/helpers/__init__.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/helpers/scenario_ordering.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/helpers/test_env_vars_setter.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/version.py +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/dependency_links.txt +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/requires.txt +0 -0
- {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uber-compose
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0a1
|
|
4
4
|
Summary: docker compose testing env orchestrator
|
|
5
5
|
Home-page: https://github.com/ko10ok/uber-compose
|
|
6
6
|
Author: Yuriy Sagitov
|
|
@@ -231,6 +231,7 @@ This is particularly useful when:
|
|
|
231
231
|
- **[E2E Test Setup](docs/E2E_CONTAINER_SETUP.md)** - Configure test container for E2E testing
|
|
232
232
|
- **[CLI Usage Guide](docs/CLI_USAGE.md)** - Guide for using CommonJsonCli with JSON log parsing
|
|
233
233
|
- **[X-Migration](docs/X_MIGRATION.md)** - Extended service initialization and migration commands
|
|
234
|
+
- **[Lifecycle Hooks](docs/HOOKS.md)** - Register callbacks that run after the environment is up
|
|
234
235
|
|
|
235
236
|
## 🛠️ Development Guide
|
|
236
237
|
|
|
@@ -198,6 +198,7 @@ This is particularly useful when:
|
|
|
198
198
|
- **[E2E Test Setup](docs/E2E_CONTAINER_SETUP.md)** - Configure test container for E2E testing
|
|
199
199
|
- **[CLI Usage Guide](docs/CLI_USAGE.md)** - Guide for using CommonJsonCli with JSON log parsing
|
|
200
200
|
- **[X-Migration](docs/X_MIGRATION.md)** - Extended service initialization and migration commands
|
|
201
|
+
- **[Lifecycle Hooks](docs/HOOKS.md)** - Register callbacks that run after the environment is up
|
|
201
202
|
|
|
202
203
|
## 🛠️ Development Guide
|
|
203
204
|
|
|
@@ -10,6 +10,23 @@ from uber_compose.env_description.env_types import OverridenService
|
|
|
10
10
|
from uber_compose.env_description.env_types import Service
|
|
11
11
|
from uber_compose.helpers.exec_result import ExecTimeout
|
|
12
12
|
from uber_compose.helpers.health_policy import UpHealthPolicy
|
|
13
|
+
from uber_compose.helpers.hooks import UberComposeEvent
|
|
14
|
+
from uber_compose.parallel_plugin import ExecutionPlan
|
|
15
|
+
from uber_compose.parallel_plugin import VedroUberComposeParallel
|
|
16
|
+
from uber_compose.parallel_plugin import WorkerAssignment
|
|
17
|
+
from uber_compose.parallel_plugin import build_execution_plan
|
|
18
|
+
from uber_compose.parallel_plugin import build_execution_plan_from_scheduler
|
|
19
|
+
from uber_compose.parallel_plugin import env_group_key
|
|
20
|
+
from uber_compose.parallel_plugin import env_group_key_for
|
|
21
|
+
from uber_compose.parallel_plugin import get_execution_plan
|
|
22
|
+
from uber_compose.parallel_plugin import get_namespace_map
|
|
23
|
+
from uber_compose.parallel_plugin import get_worker_groups
|
|
24
|
+
from uber_compose.parallel_plugin import get_worker_id
|
|
25
|
+
from uber_compose.parallel_plugin import get_workers_total
|
|
26
|
+
from uber_compose.parallel_plugin import is_parallel_worker
|
|
27
|
+
from uber_compose.parallel_plugin import is_parallel_worker_process
|
|
28
|
+
from uber_compose.parallel_plugin import reset_execution_plan
|
|
29
|
+
from uber_compose.parallel_plugin import set_execution_plan
|
|
13
30
|
from uber_compose.uber_compose import SystemUberCompose
|
|
14
31
|
from uber_compose.uber_compose import TheUberCompose
|
|
15
32
|
from uber_compose.vedro_plugin.base_structures.common_json_cli import CommandResult
|
|
@@ -27,5 +44,9 @@ __all__ = (
|
|
|
27
44
|
'CommonJsonCli', 'CommandResult', 'ProcessExit', 'JsonParser', 'json_parser', 'ExecTimeout', 'TimeOutCheck',
|
|
28
45
|
'ExecLifeCyclePolicy',
|
|
29
46
|
'VedroUberCompose', 'UpHealthPolicy', 'DEFAULT_COMPOSE', 'ComposeConfig', 'DEFAULT_ENV_DESCRIPTION',
|
|
30
|
-
'DEFAULT_ENV_ID',
|
|
47
|
+
'DEFAULT_ENV_ID', 'UberComposeEvent',
|
|
48
|
+
'ExecutionPlan', 'WorkerAssignment', 'env_group_key', 'env_group_key_for',
|
|
49
|
+
'get_execution_plan', 'get_namespace_map', 'get_worker_groups', 'get_worker_id', 'get_workers_total',
|
|
50
|
+
'is_parallel_worker', 'is_parallel_worker_process', 'reset_execution_plan', 'set_execution_plan',
|
|
51
|
+
'VedroUberComposeParallel', 'build_execution_plan', 'build_execution_plan_from_scheduler',
|
|
31
52
|
)
|
|
@@ -6,11 +6,14 @@ class Constants:
|
|
|
6
6
|
def __init__(self,
|
|
7
7
|
docker_compose_files_scan_depth: int = 2,
|
|
8
8
|
cli_compose_util_override: str = None,
|
|
9
|
+
compose_project_name: str = None,
|
|
10
|
+
tmp_envs_path: str | Path = None,
|
|
11
|
+
in_docker_project_root_path: str | Path = None,
|
|
9
12
|
):
|
|
10
13
|
self.default_log_policy = os.environ.get('LOG_POLICY', 'DEFAULT')
|
|
11
14
|
|
|
12
|
-
self.project: str = os.environ.get('COMPOSE_PROJECT_NAME')
|
|
13
|
-
self.compose_project_name =
|
|
15
|
+
self.project: str = compose_project_name or os.environ.get('COMPOSE_PROJECT_NAME')
|
|
16
|
+
self.compose_project_name = self.project
|
|
14
17
|
assert self.compose_project_name, (
|
|
15
18
|
'COMPOSE_PROJECT_NAME environment variable is not set: - COMPOSE_PROJECT_NAME=${PWD##*/}'
|
|
16
19
|
)
|
|
@@ -19,8 +22,12 @@ class Constants:
|
|
|
19
22
|
|
|
20
23
|
# inner directories
|
|
21
24
|
self.non_stop_containers: list[str] = os.environ.get('NON_STOP_CONTAINERS', 'e2e,dockersock').split(',')
|
|
22
|
-
self.tmp_envs_path: Path = Path(
|
|
23
|
-
|
|
25
|
+
self.tmp_envs_path: Path = Path(
|
|
26
|
+
tmp_envs_path or os.environ.get('TMP_ENVS_DIRECTORY', '/tmp/uc-envs')
|
|
27
|
+
)
|
|
28
|
+
self.in_docker_project_root_path: Path = Path(
|
|
29
|
+
in_docker_project_root_path or os.environ.get('PROJECT_ROOT_DIRECTORY', '/project')
|
|
30
|
+
)
|
|
24
31
|
|
|
25
32
|
self.host_project_root_directory: Path = Path(
|
|
26
33
|
os.environ.get('HOST_PROJECT_ROOT_DIRECTORY', '__host_project_root__')
|
|
@@ -17,6 +17,7 @@ from uber_compose.env_description.env_types import EventStage
|
|
|
17
17
|
from uber_compose.errors.up import ServicesUpError
|
|
18
18
|
from uber_compose.helpers.health_policy import UpHealthPolicy
|
|
19
19
|
from uber_compose.helpers.jobs_result import JobResult
|
|
20
|
+
from uber_compose.helpers.labels import Label
|
|
20
21
|
from uber_compose.output.console import Logger
|
|
21
22
|
from uber_compose.output.styles import Style
|
|
22
23
|
|
|
@@ -206,6 +207,7 @@ class ComposeInstance:
|
|
|
206
207
|
delay_s=self.health_policy.service_up_check_delay_s,
|
|
207
208
|
logger_func=self.logger.stage_details,
|
|
208
209
|
get_compose_state=self.compose_executor.dc_state,
|
|
210
|
+
service_filter=self._is_service_in_this_namespace,
|
|
209
211
|
)
|
|
210
212
|
if check_up_result != JobResult.GOOD:
|
|
211
213
|
services_status = await self.compose_executor.dc_state()
|
|
@@ -222,6 +224,12 @@ class ComposeInstance:
|
|
|
222
224
|
migrations
|
|
223
225
|
)
|
|
224
226
|
|
|
227
|
+
def _is_service_in_this_namespace(self, service_state) -> bool:
|
|
228
|
+
return (
|
|
229
|
+
service_state.labels.get(Label.ENV_ID)
|
|
230
|
+
== self.compose_instance_files.env_config_instance.env_id
|
|
231
|
+
)
|
|
232
|
+
|
|
225
233
|
async def run(self):
|
|
226
234
|
self.compose_instance_files = await self.generate_config_files()
|
|
227
235
|
|
|
@@ -242,6 +250,7 @@ class ComposeInstance:
|
|
|
242
250
|
service
|
|
243
251
|
for service_tier_pack in services_tiers
|
|
244
252
|
for service in service_tier_pack
|
|
253
|
+
if service not in self.except_containers
|
|
245
254
|
]
|
|
246
255
|
|
|
247
256
|
await self.run_migration(
|
|
@@ -267,6 +276,7 @@ class ComposeInstance:
|
|
|
267
276
|
delay_s=self.health_policy.service_up_check_delay_s,
|
|
268
277
|
logger_func=self.logger.stage,
|
|
269
278
|
get_compose_state=self.compose_executor.dc_state,
|
|
279
|
+
service_filter=self._is_service_in_this_namespace,
|
|
270
280
|
)
|
|
271
281
|
if check_up_result != JobResult.GOOD:
|
|
272
282
|
services_status = await self.compose_executor.dc_state()
|
|
@@ -12,6 +12,8 @@ class ComposeConfig(NamedTuple):
|
|
|
12
12
|
compose_files: str
|
|
13
13
|
overridden_services: list[OverridenService] = None
|
|
14
14
|
parallel_env_limit: Union[int] = 1
|
|
15
|
+
env_parallelism: dict[str, int] | None = None
|
|
16
|
+
default_env_parallelism: int = 1
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class EnvInstanceConfig(NamedTuple):
|
|
@@ -107,5 +107,8 @@ class SystemDockerCompose:
|
|
|
107
107
|
services_state = await self.dc_shell.dc_state()
|
|
108
108
|
return [service.name for service in services_state.get_all_for()]
|
|
109
109
|
|
|
110
|
+
async def get_all_services_state(self) -> ServicesComposeState:
|
|
111
|
+
return await self.dc_shell.dc_state()
|
|
112
|
+
|
|
110
113
|
async def down_services(self, services: list[str] | None = None) -> None:
|
|
111
114
|
await self.dc_shell.dc_down(services=services)
|
|
@@ -7,6 +7,8 @@ from uber_compose.env_description.env_types import Env
|
|
|
7
7
|
from uber_compose.env_description.env_types import Environment
|
|
8
8
|
from uber_compose.env_description.env_types import Service
|
|
9
9
|
from uber_compose.env_description.env_types import ServiceMode
|
|
10
|
+
from uber_compose.helpers.env_id import get_stable_env_id as get_stable_env_id # noqa: F401
|
|
11
|
+
from uber_compose.helpers.env_id import get_template_hash as get_template_hash # noqa: F401
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def make_env_service_name(service, env_id):
|
|
@@ -52,8 +54,13 @@ def get_new_env_id() -> str:
|
|
|
52
54
|
|
|
53
55
|
def get_service_map(env: Environment, new_env_id: str):
|
|
54
56
|
return {
|
|
55
|
-
service_name:
|
|
56
|
-
|
|
57
|
+
service_name: (
|
|
58
|
+
service_name
|
|
59
|
+
if service.mode in (ServiceMode.OFF, ServiceMode.SINGLETON)
|
|
60
|
+
else make_env_service_name(service_name, new_env_id)
|
|
61
|
+
)
|
|
62
|
+
for service_name, service in env.get_services().items()
|
|
63
|
+
if service.mode != ServiceMode.OFF
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
|
|
@@ -23,10 +23,14 @@ def is_service_not_running_or_not_healthy(service_state: ServiceComposeState) ->
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
async def wait_all_services_up(attempts, delay_s, logger_func: Callable, get_compose_state,
|
|
26
|
-
is_service_in_bad_state=is_service_not_running_or_not_healthy
|
|
26
|
+
is_service_in_bad_state=is_service_not_running_or_not_healthy,
|
|
27
|
+
service_filter: Callable = None) -> JobResult:
|
|
27
28
|
counter_keeper = CountdownCounterKeeper(attempts)
|
|
28
29
|
state_keeper = StateKeeper()
|
|
29
30
|
|
|
31
|
+
if service_filter is None:
|
|
32
|
+
service_filter = lambda service_state: True
|
|
33
|
+
|
|
30
34
|
async def check_once() -> JobResult:
|
|
31
35
|
output_style = Style()
|
|
32
36
|
|
|
@@ -34,7 +38,7 @@ async def wait_all_services_up(attempts, delay_s, logger_func: Callable, get_com
|
|
|
34
38
|
logger_func(Text('Starting services check up', style=output_style.info))
|
|
35
39
|
state_keeper.update_state(ServicesState.DEFAULT_STATE)
|
|
36
40
|
|
|
37
|
-
services_state = await get_compose_state()
|
|
41
|
+
services_state = (await get_compose_state()).get_all_for(service_filter)
|
|
38
42
|
non_ready_services = [
|
|
39
43
|
service
|
|
40
44
|
for service in services_state
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from uber_compose import Environment
|
|
2
|
+
from uber_compose.core.docker_compose_shell.types import ServicesComposeState
|
|
3
|
+
from uber_compose.core.utils.compose_instance_cfg import get_service_map
|
|
4
|
+
from uber_compose.core.utils.state_waiting import is_service_not_running_or_not_healthy
|
|
5
|
+
from uber_compose.helpers.labels import Label
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def calc_broken_services(
|
|
9
|
+
services_state: ServicesComposeState,
|
|
10
|
+
config_template: Environment,
|
|
11
|
+
excluded_from_check: list[str] = None,
|
|
12
|
+
env_id: str = None,
|
|
13
|
+
) -> list[str]:
|
|
14
|
+
if excluded_from_check is None:
|
|
15
|
+
excluded_from_check = []
|
|
16
|
+
|
|
17
|
+
if env_id is None and services_state.get_any() is not None:
|
|
18
|
+
env_id = services_state.get_any().labels.get(Label.ENV_ID)
|
|
19
|
+
|
|
20
|
+
if env_id is not None:
|
|
21
|
+
expected_services_map = get_service_map(config_template, env_id)
|
|
22
|
+
else:
|
|
23
|
+
expected_services_map = {
|
|
24
|
+
service_name: service_name
|
|
25
|
+
for service_name in config_template.get_services_names()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
non_ready_services = [
|
|
29
|
+
service.name
|
|
30
|
+
for service in services_state
|
|
31
|
+
if is_service_not_running_or_not_healthy(service)
|
|
32
|
+
]
|
|
33
|
+
not_included_services = [
|
|
34
|
+
service
|
|
35
|
+
for service in expected_services_map.values()
|
|
36
|
+
if service not in services_state.get_services_names()
|
|
37
|
+
]
|
|
38
|
+
skipped_overridden_services = [
|
|
39
|
+
expected_services_map.get(service.service.name, service.service.name)
|
|
40
|
+
for service in config_template.get_overridden_services()
|
|
41
|
+
if expected_services_map.get(service.service.name, service.service.name)
|
|
42
|
+
not in services_state.get_services_names()
|
|
43
|
+
]
|
|
44
|
+
return list(
|
|
45
|
+
set(non_ready_services) | set(not_included_services)
|
|
46
|
+
- set(excluded_from_check) - set(skipped_overridden_services)
|
|
47
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
|
|
3
|
+
from uber_compose.core.sequence_run_types import DEFAULT_ENV_ID
|
|
4
|
+
|
|
5
|
+
_current_env_id: ContextVar[str] = ContextVar(
|
|
6
|
+
'uber_compose_current_env_id',
|
|
7
|
+
default=DEFAULT_ENV_ID,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def set_current_env_id(env_id: str) -> None:
|
|
12
|
+
_current_env_id.set(env_id)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_current_env_id() -> str:
|
|
16
|
+
return _current_env_id.get()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def reset_current_env_id() -> None:
|
|
20
|
+
_current_env_id.set(DEFAULT_ENV_ID)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
|
|
3
|
+
from uber_compose.helpers.bytes_pickle import base64_pickled
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_template_hash(config_template, length: int = 4) -> str:
|
|
7
|
+
payload = base64_pickled(config_template)
|
|
8
|
+
return hashlib.sha1(payload.encode()).hexdigest()[:length]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_stable_env_id(config_template, index: int) -> str:
|
|
12
|
+
return f'w{index}-{get_template_hash(config_template)}'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def stable_index(key: str, total: int) -> int:
|
|
16
|
+
total = max(total, 1)
|
|
17
|
+
digest = hashlib.sha1(str(key).encode()).hexdigest()
|
|
18
|
+
return int(digest, 16) % total
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ("get_stable_env_id", "get_template_hash", "stable_index",)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from enum import Enum
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UberComposeEvent(Enum):
|
|
9
|
+
AFTER_UP = 'after_up'
|
|
10
|
+
AFTER_UPDATED_ENV_UP = 'after_updated_env_up'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HooksRegistry:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._hooks: dict[UberComposeEvent, list[tuple[Callable[..., Any], tuple, dict]]] = defaultdict(list)
|
|
16
|
+
|
|
17
|
+
def on(self, event: UberComposeEvent, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
|
|
18
|
+
self._hooks[event].append((func, args, kwargs))
|
|
19
|
+
|
|
20
|
+
async def run(self, event: UberComposeEvent) -> None:
|
|
21
|
+
for func, args, kwargs in self._hooks[event]:
|
|
22
|
+
result = func(*args, **kwargs)
|
|
23
|
+
if inspect.isawaitable(result):
|
|
24
|
+
await result
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from uber_compose.parallel_plugin.grouper import assign_groups_to_workers
|
|
2
|
+
from uber_compose.parallel_plugin.grouper import build_execution_plan
|
|
3
|
+
from uber_compose.parallel_plugin.grouper import build_execution_plan_from_scheduler
|
|
4
|
+
from uber_compose.parallel_plugin.grouper import build_replica_execution_plan
|
|
5
|
+
from uber_compose.parallel_plugin.grouper import collect_group_counts
|
|
6
|
+
from uber_compose.parallel_plugin.grouping import env_group_key
|
|
7
|
+
from uber_compose.parallel_plugin.grouping import env_group_key_for
|
|
8
|
+
from uber_compose.parallel_plugin.grouping import scenario_env
|
|
9
|
+
from uber_compose.parallel_plugin.plugin import VedroUberComposeParallel
|
|
10
|
+
from uber_compose.parallel_plugin.plugin import VedroUberComposeParallelPlugin
|
|
11
|
+
from uber_compose.parallel_plugin.types import ExecutionPlan
|
|
12
|
+
from uber_compose.parallel_plugin.types import WorkerAssignment
|
|
13
|
+
from uber_compose.parallel_plugin.worker_api import get_env_parallelism
|
|
14
|
+
from uber_compose.parallel_plugin.worker_api import get_env_replicas_total
|
|
15
|
+
from uber_compose.parallel_plugin.worker_api import get_execution_plan
|
|
16
|
+
from uber_compose.parallel_plugin.worker_api import get_namespace_map
|
|
17
|
+
from uber_compose.parallel_plugin.worker_api import get_worker_groups
|
|
18
|
+
from uber_compose.parallel_plugin.worker_api import get_worker_id
|
|
19
|
+
from uber_compose.parallel_plugin.worker_api import get_worker_replica_index
|
|
20
|
+
from uber_compose.parallel_plugin.worker_api import get_workers_total
|
|
21
|
+
from uber_compose.parallel_plugin.worker_api import is_parallel_worker
|
|
22
|
+
from uber_compose.parallel_plugin.worker_api import is_parallel_worker_process
|
|
23
|
+
from uber_compose.parallel_plugin.worker_api import reset_env_parallelism
|
|
24
|
+
from uber_compose.parallel_plugin.worker_api import reset_execution_plan
|
|
25
|
+
from uber_compose.parallel_plugin.worker_api import set_env_parallelism
|
|
26
|
+
from uber_compose.parallel_plugin.worker_api import set_execution_plan
|
|
27
|
+
from uber_compose.parallel_plugin.worker_filter import ignore_foreign_scenarios
|
|
28
|
+
|
|
29
|
+
__all__ = (
|
|
30
|
+
'ExecutionPlan', 'WorkerAssignment',
|
|
31
|
+
'VedroUberComposeParallel', 'VedroUberComposeParallelPlugin',
|
|
32
|
+
'assign_groups_to_workers', 'build_execution_plan', 'build_execution_plan_from_scheduler',
|
|
33
|
+
'build_replica_execution_plan', 'collect_group_counts', 'env_group_key', 'env_group_key_for',
|
|
34
|
+
'get_env_parallelism', 'get_env_replicas_total', 'get_execution_plan', 'get_namespace_map',
|
|
35
|
+
'get_worker_groups', 'get_worker_id', 'get_worker_replica_index', 'get_workers_total',
|
|
36
|
+
'ignore_foreign_scenarios', 'is_parallel_worker', 'is_parallel_worker_process',
|
|
37
|
+
'reset_env_parallelism', 'reset_execution_plan', 'scenario_env',
|
|
38
|
+
'set_env_parallelism', 'set_execution_plan',
|
|
39
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from vedro.core import ScenarioScheduler
|
|
5
|
+
from vedro.core import VirtualScenario
|
|
6
|
+
|
|
7
|
+
from uber_compose.helpers.env_id import get_stable_env_id
|
|
8
|
+
from uber_compose.parallel_plugin.types import ExecutionPlan
|
|
9
|
+
from uber_compose.parallel_plugin.types import WorkerAssignment
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def collect_group_counts(
|
|
13
|
+
scenarios: ScenarioScheduler,
|
|
14
|
+
grouping_key: Callable[[VirtualScenario], str],
|
|
15
|
+
) -> dict[str, int]:
|
|
16
|
+
counts: dict[str, int] = defaultdict(int)
|
|
17
|
+
async for scenario in scenarios:
|
|
18
|
+
if scenario.is_skipped():
|
|
19
|
+
continue
|
|
20
|
+
counts[grouping_key(scenario)] += 1
|
|
21
|
+
return dict(counts)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def assign_groups_to_workers(group_counts: dict[str, int], workers_total: int) -> list[list[str]]:
|
|
25
|
+
workers_total = max(workers_total, 1)
|
|
26
|
+
workers: list[list[str]] = [[] for _ in range(workers_total)]
|
|
27
|
+
worker_loads = [0] * workers_total
|
|
28
|
+
|
|
29
|
+
groups = sorted(group_counts.items(), key=lambda item: (-item[1], item[0]))
|
|
30
|
+
for group_key, count in groups:
|
|
31
|
+
worker_id = min(range(workers_total), key=lambda index: (worker_loads[index], index))
|
|
32
|
+
workers[worker_id].append(group_key)
|
|
33
|
+
worker_loads[worker_id] += count
|
|
34
|
+
|
|
35
|
+
return workers
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_execution_plan(
|
|
39
|
+
group_counts: dict[str, int],
|
|
40
|
+
workers_total: int,
|
|
41
|
+
namespace_map: dict[str, str] | None = None,
|
|
42
|
+
) -> ExecutionPlan:
|
|
43
|
+
namespace_map = namespace_map or {}
|
|
44
|
+
workers = assign_groups_to_workers(group_counts, workers_total)
|
|
45
|
+
assignments = tuple(
|
|
46
|
+
WorkerAssignment(
|
|
47
|
+
worker_id=worker_id,
|
|
48
|
+
groups=tuple(sorted(groups)),
|
|
49
|
+
namespace_map={group: namespace_map[group] for group in groups if group in namespace_map},
|
|
50
|
+
)
|
|
51
|
+
for worker_id, groups in enumerate(workers)
|
|
52
|
+
)
|
|
53
|
+
return ExecutionPlan(workers_total=max(workers_total, 1), assignments=assignments)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def build_execution_plan_from_scheduler(
|
|
57
|
+
scenarios: ScenarioScheduler,
|
|
58
|
+
workers_total: int,
|
|
59
|
+
grouping_key: Callable[[VirtualScenario], str],
|
|
60
|
+
namespace_map: dict[str, str] | None = None,
|
|
61
|
+
) -> ExecutionPlan:
|
|
62
|
+
group_counts = await collect_group_counts(scenarios, grouping_key)
|
|
63
|
+
return build_execution_plan(group_counts, workers_total, namespace_map)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_replica_execution_plan(
|
|
67
|
+
group_counts: dict[str, int],
|
|
68
|
+
group_envs: dict[str, object],
|
|
69
|
+
namespace_counts: dict[str, int],
|
|
70
|
+
default_replicas: int = 1,
|
|
71
|
+
) -> ExecutionPlan:
|
|
72
|
+
assignments = []
|
|
73
|
+
worker_id = 0
|
|
74
|
+
for group_key in sorted(group_envs):
|
|
75
|
+
env = group_envs[group_key]
|
|
76
|
+
replicas = max(int(namespace_counts.get(group_key, default_replicas)), 1)
|
|
77
|
+
replicas = min(replicas, max(group_counts.get(group_key, 1), 1))
|
|
78
|
+
for replica_index in range(replicas):
|
|
79
|
+
assignments.append(WorkerAssignment(
|
|
80
|
+
worker_id=worker_id,
|
|
81
|
+
groups=(group_key,),
|
|
82
|
+
namespace_map={group_key: get_stable_env_id(env, replica_index)},
|
|
83
|
+
replica_index=replica_index,
|
|
84
|
+
replicas_total=replicas,
|
|
85
|
+
))
|
|
86
|
+
worker_id += 1
|
|
87
|
+
return ExecutionPlan(
|
|
88
|
+
workers_total=worker_id,
|
|
89
|
+
assignments=tuple(assignments),
|
|
90
|
+
env_templates=dict(group_envs),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
__all__ = (
|
|
95
|
+
'assign_groups_to_workers', 'build_execution_plan', 'build_execution_plan_from_scheduler',
|
|
96
|
+
'build_replica_execution_plan', 'collect_group_counts',
|
|
97
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from vedro.core import VirtualScenario
|
|
2
|
+
|
|
3
|
+
from uber_compose.env_description.env_types import DEFAULT_ENV_DESCRIPTION
|
|
4
|
+
from uber_compose.env_description.env_types import Environment
|
|
5
|
+
from uber_compose.helpers.bytes_pickle import base64_pickled
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def scenario_env(scenario: VirtualScenario) -> Environment | None:
|
|
9
|
+
original = getattr(scenario, '_orig_scenario', scenario)
|
|
10
|
+
env = None
|
|
11
|
+
if hasattr(original, 'tags'):
|
|
12
|
+
for tag in original.tags:
|
|
13
|
+
if isinstance(tag, Environment):
|
|
14
|
+
env = tag
|
|
15
|
+
if hasattr(original, 'env'):
|
|
16
|
+
env = original.env
|
|
17
|
+
return env
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def env_group_key_for(env: Environment | None) -> str:
|
|
21
|
+
if env is None or env.description == DEFAULT_ENV_DESCRIPTION:
|
|
22
|
+
env = None
|
|
23
|
+
return base64_pickled(env)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def env_group_key(scenario: VirtualScenario) -> str:
|
|
27
|
+
return env_group_key_for(scenario_env(scenario))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ("env_group_key", "env_group_key_for", "scenario_env",)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from vedro.core import ConfigType
|
|
4
|
+
from vedro.core import Dispatcher
|
|
5
|
+
from vedro.core import Plugin
|
|
6
|
+
from vedro.core import PluginConfig
|
|
7
|
+
from vedro.events import ArgParseEvent
|
|
8
|
+
from vedro.events import ArgParsedEvent
|
|
9
|
+
from vedro.events import ConfigLoadedEvent
|
|
10
|
+
from vedro.events import StartupEvent
|
|
11
|
+
|
|
12
|
+
from uber_compose.env_description.env_types import DEFAULT_ENV_DESCRIPTION
|
|
13
|
+
from uber_compose.parallel_plugin.grouper import build_execution_plan_from_scheduler
|
|
14
|
+
from uber_compose.parallel_plugin.grouper import build_replica_execution_plan
|
|
15
|
+
from uber_compose.parallel_plugin.grouper import collect_group_counts
|
|
16
|
+
from uber_compose.parallel_plugin.grouping import env_group_key
|
|
17
|
+
from uber_compose.parallel_plugin.grouping import env_group_key_for
|
|
18
|
+
from uber_compose.parallel_plugin.grouping import scenario_env
|
|
19
|
+
from uber_compose.parallel_plugin.runner import ParallelScenarioRunner
|
|
20
|
+
from uber_compose.parallel_plugin.worker_api import get_env_parallelism
|
|
21
|
+
from uber_compose.parallel_plugin.worker_api import get_env_replicas_total
|
|
22
|
+
from uber_compose.parallel_plugin.worker_api import get_worker_groups
|
|
23
|
+
from uber_compose.parallel_plugin.worker_api import get_worker_replica_index
|
|
24
|
+
from uber_compose.parallel_plugin.worker_api import is_parallel_worker_process
|
|
25
|
+
from uber_compose.parallel_plugin.worker_api import set_execution_plan
|
|
26
|
+
from uber_compose.parallel_plugin.worker_filter import ignore_foreign_scenarios
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VedroUberComposeParallelPlugin(Plugin):
|
|
30
|
+
def __init__(self, config: Type["VedroUberComposeParallel"]) -> None:
|
|
31
|
+
super().__init__(config)
|
|
32
|
+
self._enabled = config.enabled
|
|
33
|
+
self._grouping_key = config.grouping_key or env_group_key
|
|
34
|
+
self._parallel = max(int(config.parallel or 1), 1)
|
|
35
|
+
self._global_config: ConfigType | None = None
|
|
36
|
+
self._runner_registered = False
|
|
37
|
+
self._refresh_worker_state()
|
|
38
|
+
|
|
39
|
+
def _refresh_worker_state(self) -> None:
|
|
40
|
+
self._is_worker = is_parallel_worker_process()
|
|
41
|
+
self._worker_groups = get_worker_groups()
|
|
42
|
+
self._worker_replica_index = get_worker_replica_index()
|
|
43
|
+
self._env_replicas_total = get_env_replicas_total()
|
|
44
|
+
|
|
45
|
+
def subscribe(self, dispatcher: Dispatcher) -> None:
|
|
46
|
+
if not self._enabled:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
dispatcher.listen(ConfigLoadedEvent, self.on_config_loaded) \
|
|
50
|
+
.listen(ArgParseEvent, self.handle_arg_parse, priority=-100) \
|
|
51
|
+
.listen(ArgParsedEvent, self.handle_arg_parsed, priority=-100) \
|
|
52
|
+
.listen(StartupEvent, self.handle_prepare_scenarios, priority=-100)
|
|
53
|
+
|
|
54
|
+
def on_config_loaded(self, event: ConfigLoadedEvent) -> None:
|
|
55
|
+
self._global_config = event.config
|
|
56
|
+
|
|
57
|
+
def handle_arg_parse(self, event: ArgParseEvent) -> None:
|
|
58
|
+
group = event.arg_parser.add_argument_group("Uber Compose Parallel")
|
|
59
|
+
group.add_argument("--uc-parallel",
|
|
60
|
+
type=int,
|
|
61
|
+
default=None,
|
|
62
|
+
help="Number of parallel workers (1 keeps the sequential run)")
|
|
63
|
+
|
|
64
|
+
def handle_arg_parsed(self, event: ArgParsedEvent) -> None:
|
|
65
|
+
parallel = getattr(event.args, 'uc_parallel', None)
|
|
66
|
+
if parallel:
|
|
67
|
+
self._parallel = max(int(parallel), 1)
|
|
68
|
+
self._refresh_worker_state()
|
|
69
|
+
|
|
70
|
+
async def handle_prepare_scenarios(self, event: StartupEvent) -> None:
|
|
71
|
+
self._refresh_worker_state()
|
|
72
|
+
|
|
73
|
+
if self._is_worker:
|
|
74
|
+
await ignore_foreign_scenarios(
|
|
75
|
+
event.scheduler,
|
|
76
|
+
self._worker_groups,
|
|
77
|
+
self._grouping_key,
|
|
78
|
+
replica_index=self._worker_replica_index,
|
|
79
|
+
replicas_total=self._env_replicas_total,
|
|
80
|
+
)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
plan = await self._build_plan(event)
|
|
84
|
+
if plan is None or plan.workers_total <= 1:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
set_execution_plan(plan)
|
|
88
|
+
self._register_runner()
|
|
89
|
+
|
|
90
|
+
async def _build_plan(self, event: StartupEvent):
|
|
91
|
+
env_parallelism = get_env_parallelism()
|
|
92
|
+
if env_parallelism is not None:
|
|
93
|
+
return await self._build_replica_plan(event, env_parallelism)
|
|
94
|
+
if self._parallel <= 1:
|
|
95
|
+
return None
|
|
96
|
+
return await build_execution_plan_from_scheduler(
|
|
97
|
+
event.scheduler,
|
|
98
|
+
self._parallel,
|
|
99
|
+
self._grouping_key,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def _build_replica_plan(self, event: StartupEvent, env_parallelism):
|
|
103
|
+
counts_by_description, default_replicas = env_parallelism
|
|
104
|
+
group_counts = await collect_group_counts(event.scheduler, self._grouping_key)
|
|
105
|
+
group_envs = {}
|
|
106
|
+
namespace_counts = {}
|
|
107
|
+
|
|
108
|
+
async for scenario in event.scheduler:
|
|
109
|
+
if scenario.is_skipped():
|
|
110
|
+
continue
|
|
111
|
+
env = scenario_env(scenario)
|
|
112
|
+
group_key = env_group_key_for(env)
|
|
113
|
+
template = None if env is None or env.description == DEFAULT_ENV_DESCRIPTION else env
|
|
114
|
+
group_envs[group_key] = template
|
|
115
|
+
description = template.description if template is not None else DEFAULT_ENV_DESCRIPTION
|
|
116
|
+
namespace_counts[group_key] = counts_by_description.get(description, default_replicas)
|
|
117
|
+
|
|
118
|
+
return build_replica_execution_plan(
|
|
119
|
+
group_counts=group_counts,
|
|
120
|
+
group_envs=group_envs,
|
|
121
|
+
namespace_counts=namespace_counts,
|
|
122
|
+
default_replicas=default_replicas,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _register_runner(self) -> None:
|
|
126
|
+
if self._runner_registered or self._is_worker or self._global_config is None:
|
|
127
|
+
return
|
|
128
|
+
dispatcher = self._global_config.Registry.Dispatcher()
|
|
129
|
+
self._global_config.Registry.ScenarioRunner.register(
|
|
130
|
+
resolver=lambda: ParallelScenarioRunner(
|
|
131
|
+
dispatcher=dispatcher,
|
|
132
|
+
grouping_key=self._grouping_key,
|
|
133
|
+
),
|
|
134
|
+
registrant=self,
|
|
135
|
+
)
|
|
136
|
+
self._runner_registered = True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class VedroUberComposeParallel(PluginConfig):
|
|
140
|
+
plugin = VedroUberComposeParallelPlugin
|
|
141
|
+
|
|
142
|
+
enabled = False
|
|
143
|
+
|
|
144
|
+
parallel: int = 1
|
|
145
|
+
|
|
146
|
+
grouping_key = env_group_key
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ("VedroUberComposeParallel", "VedroUberComposeParallelPlugin",)
|