Skip to content

Commit

Permalink
ref(modes): Refactoring modes to support multiple concurrent modes (#173
Browse files Browse the repository at this point in the history
)

* fix(dependencies): Fixing dependency graph construction for simple modes

* ref(modes): Refactoring modes to support multiple concurrent modes

* Improving logic to handle nested modes

* Removing todo

* Removing left over code

* Removing redundant code

* Improving tests

* Removing optional from arg
  • Loading branch information
IanWoodard authored Dec 6, 2024
1 parent 3534d0a commit 34f6adc
Show file tree
Hide file tree
Showing 11 changed files with 94 additions and 161 deletions.
13 changes: 9 additions & 4 deletions devservices/commands/down.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,20 @@ def down(args: Namespace) -> None:
console.warning(f"{service.name} is not running")
exit(0)

mode = state.get_mode_for_service(service.name) or "default"
mode_dependencies = modes[mode]
active_modes = state.get_active_modes_for_service(service.name)
mode_dependencies = set()
for active_mode in active_modes:
active_mode_dependencies = modes.get(active_mode, [])
mode_dependencies.update(active_mode_dependencies)

with Status(
lambda: console.warning(f"Stopping {service.name}"),
lambda: console.success(f"{service.name} stopped"),
) as status:
try:
remote_dependencies = install_and_verify_dependencies(service, mode=mode)
remote_dependencies = install_and_verify_dependencies(
service, modes=active_modes
)
except DependencyError as de:
capture_exception(de)
status.failure(str(de))
Expand All @@ -89,7 +94,7 @@ def down(args: Namespace) -> None:
service, remote_dependencies
)
try:
_down(service, remote_dependencies, mode_dependencies, status)
_down(service, remote_dependencies, list(mode_dependencies), status)
except DockerComposeError as dce:
capture_exception(dce)
status.failure(f"Failed to stop {service.name}: {dce.stderr}")
Expand Down
4 changes: 2 additions & 2 deletions devservices/commands/list_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ def list_services(args: Namespace) -> None:

for service in services_to_show:
status = "running" if service.name in running_services else "stopped"
mode = state.get_mode_for_service(service.name)
active_modes = state.get_active_modes_for_service(service.name)
console.info(f"- {service.name}")
console.info(f" mode: {mode}")
console.info(f" modes: {active_modes}")
console.info(f" status: {status}")
console.info(f" location: {service.repo_path}")

Expand Down
56 changes: 2 additions & 54 deletions devservices/commands/up.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import concurrent.futures
import os
import subprocess
from argparse import _SubParsersAction
Expand All @@ -23,7 +22,6 @@
from devservices.utils.console import Console
from devservices.utils.console import Status
from devservices.utils.dependencies import construct_dependency_graph
from devservices.utils.dependencies import get_non_shared_remote_dependencies
from devservices.utils.dependencies import install_and_verify_dependencies
from devservices.utils.dependencies import InstalledRemoteDependency
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
Expand Down Expand Up @@ -69,64 +67,14 @@ def up(args: Namespace) -> None:
modes = service.config.modes
mode = args.mode

state = State()
started_services = state.get_started_services()
running_mode = state.get_mode_for_service(service.name) or "default"

# TODO: Remove this once we properly handle mode switching
if service.name in started_services and running_mode != mode:
console.warning(
f"Service '{service.name}' is already running in mode: '{running_mode}', restarting in mode: '{mode}'"
)
with Status() as status:
try:
remote_dependencies = install_and_verify_dependencies(
service, mode=running_mode
)
except DependencyError as de:
capture_exception(de)
status.failure(str(de))
exit(1)
except ModeDoesNotExistError as mde:
capture_exception(mde)
status.failure(str(mde))
exit(1)
service_config_file_path = os.path.join(
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
)
current_env = os.environ.copy()
running_mode_dependencies = modes[running_mode]
remote_dependencies_to_bring_down = get_non_shared_remote_dependencies(
service, remote_dependencies
)
down_docker_compose_commands = get_docker_compose_commands_to_run(
service=service,
remote_dependencies=list(remote_dependencies_to_bring_down),
current_env=current_env,
command="down",
options=[],
service_config_file_path=service_config_file_path,
mode_dependencies=running_mode_dependencies,
)

with concurrent.futures.ThreadPoolExecutor() as executor:
futures = [
executor.submit(run_cmd, cmd, current_env)
for cmd in down_docker_compose_commands
]
for future in concurrent.futures.as_completed(futures):
future.result()

state.remove_started_service(service.name)

with Status(
lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'"),
lambda: console.success(f"{service.name} started"),
) as status:
try:
status.info("Retrieving dependencies")
remote_dependencies = install_and_verify_dependencies(
service, force_update_dependencies=True, mode=mode
service, force_update_dependencies=True, modes=[mode]
)
except DependencyError as de:
capture_exception(de)
Expand All @@ -149,7 +97,7 @@ def up(args: Namespace) -> None:
exit(1)
# TODO: We should factor in healthchecks here before marking service as running
state = State()
state.add_started_service(service.name, mode)
state.update_started_service(service.name, mode)


def _bring_up_dependency(
Expand Down
17 changes: 13 additions & 4 deletions devservices/utils/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,20 @@ def _set_config(self, key: str, value: str) -> None:


def install_and_verify_dependencies(
service: Service, force_update_dependencies: bool = False, mode: str = "default"
service: Service,
force_update_dependencies: bool = False,
modes: list[str] | None = None,
) -> set[InstalledRemoteDependency]:
if mode not in service.config.modes:
raise ModeDoesNotExistError(service_name=service.name, mode=mode)
mode_dependencies = set(service.config.modes[mode])
"""
Install and verify dependencies for a service
"""
if modes is None:
modes = ["default"]
mode_dependencies = set()
for mode in modes:
if mode not in service.config.modes:
raise ModeDoesNotExistError(service_name=service.name, mode=mode)
mode_dependencies.update(service.config.modes[mode])
matching_dependencies = [
dependency
for dependency_key, dependency in list(service.config.dependencies.items())
Expand Down
31 changes: 20 additions & 11 deletions devservices/utils/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,26 @@ def initialize_database(self) -> None:
)
self.conn.commit()

def add_started_service(self, service_name: str, mode: str) -> None:
def update_started_service(self, service_name: str, mode: str) -> None:
cursor = self.conn.cursor()
started_services = self.get_started_services()
if service_name in started_services:
active_modes = self.get_active_modes_for_service(service_name)
if service_name in started_services and mode in active_modes:
return
cursor.execute(
"""
INSERT INTO started_services (service_name, mode) VALUES (?, ?)
""",
(service_name, mode),
)
if service_name in started_services:
cursor.execute(
"""
UPDATE started_services SET mode = ? WHERE service_name = ?
""",
(",".join(active_modes + [mode]), service_name),
)
else:
cursor.execute(
"""
INSERT INTO started_services (service_name, mode) VALUES (?, ?)
""",
(service_name, ",".join(active_modes + [mode])),
)
self.conn.commit()

def remove_started_service(self, service_name: str) -> None:
Expand All @@ -67,7 +76,7 @@ def get_started_services(self) -> list[str]:
)
return [row[0] for row in cursor.fetchall()]

def get_mode_for_service(self, service_name: str) -> str | None:
def get_active_modes_for_service(self, service_name: str) -> list[str]:
cursor = self.conn.cursor()
cursor.execute(
"""
Expand All @@ -77,8 +86,8 @@ def get_mode_for_service(self, service_name: str) -> str | None:
)
result = cursor.fetchone()
if result is None:
return None
return str(result[0])
return []
return str(result[0]).split(",")

def clear_state(self) -> None:
cursor = self.conn.cursor()
Expand Down
6 changes: 3 additions & 3 deletions tests/commands/test_down.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_down_simple(
"devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")
):
state = State()
state.add_started_service("example-service", "default")
state.update_started_service("example-service", "default")
down(args)

# Ensure the DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY is set and is relative
Expand Down Expand Up @@ -137,7 +137,7 @@ def test_down_error(

with mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
state = State()
state.add_started_service("example-service", "default")
state.update_started_service("example-service", "default")
with pytest.raises(SystemExit):
down(args)

Expand Down Expand Up @@ -202,7 +202,7 @@ def test_down_mode_simple(
"devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")
):
state = State()
state.add_started_service("example-service", "test")
state.update_started_service("example-service", "test")
down(args)

# Ensure the DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY is set and is relative
Expand Down
8 changes: 4 additions & 4 deletions tests/commands/test_list_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_list_running_services(
return_value=str(tmp_path / "code"),
), mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
state = State()
state.add_started_service("example-service", "default")
state.update_started_service("example-service", "default")
config = {
"x-sentry-service-config": {
"version": 0.1,
Expand Down Expand Up @@ -47,7 +47,7 @@ def test_list_running_services(

assert (
captured.out
== f"Running services:\n- example-service\n mode: default\n status: running\n location: {tmp_path / 'code' / 'example-service'}\n"
== f"Running services:\n- example-service\n modes: ['default']\n status: running\n location: {tmp_path / 'code' / 'example-service'}\n"
)


Expand All @@ -57,7 +57,7 @@ def test_list_all_services(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -
return_value=str(tmp_path / "code"),
), mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
state = State()
state.add_started_service("example-service", "default")
state.update_started_service("example-service", "default")
config = {
"x-sentry-service-config": {
"version": 0.1,
Expand Down Expand Up @@ -85,5 +85,5 @@ def test_list_all_services(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -

assert (
captured.out
== f"Services installed locally:\n- example-service\n mode: default\n status: running\n location: {tmp_path / 'code' / 'example-service'}\n"
== f"Services installed locally:\n- example-service\n modes: ['default']\n status: running\n location: {tmp_path / 'code' / 'example-service'}\n"
)
6 changes: 3 additions & 3 deletions tests/commands/test_purge.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_purge_with_cache_and_state_and_no_running_containers_confirmed(
cache_file.write_text("This is a test cache file.")

state = State()
state.add_started_service("test-service", "test-mode")
state.update_started_service("test-service", "test-mode")

assert cache_file.exists()
assert state.get_started_services() == ["test-service"]
Expand Down Expand Up @@ -96,7 +96,7 @@ def test_purge_with_cache_and_state_and_running_containers_with_networks_confirm
cache_file.write_text("This is a test cache file.")

state = State()
state.add_started_service("test-service", "test-mode")
state.update_started_service("test-service", "test-mode")

assert cache_file.exists()
assert state.get_started_services() == ["test-service"]
Expand Down Expand Up @@ -155,7 +155,7 @@ def test_purge_with_cache_and_state_and_running_containers_not_confirmed(
cache_file.write_text("This is a test cache file.")

state = State()
state.add_started_service("test-service", "test-mode")
state.update_started_service("test-service", "test-mode")

args = Namespace()
purge(args)
Expand Down
Loading

0 comments on commit 34f6adc

Please sign in to comment.