Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 90 additions & 8 deletions duffy/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import logging.config
import sys
import time
from datetime import timedelta
from pathlib import Path
from typing import List, Optional, Tuple, Union
Expand Down Expand Up @@ -137,7 +138,7 @@ def convert(self, value, param, ctx):
# CLI groups and commands


@click.group(name="duffy")
@click.group(name="duffy", context_settings={"help_option_names": ["-h", "--help"]})
@click.option(
"-l",
"--loglevel",
Expand Down Expand Up @@ -182,7 +183,7 @@ def cli(ctx: click.Context, loglevel: Optional[str], config_paths: Tuple[Path]):
# Check & dump configuration


@cli.group(name="config")
@cli.group(name="config", context_settings={"help_option_names": ["-h", "--help"]})
def config_subcmd():
"""Check and dump configuration."""

Expand Down Expand Up @@ -235,7 +236,7 @@ def setup_db(test_data):
# Handle database migrations


@cli.group()
@cli.group(context_settings={"help_option_names": ["-h", "--help"]})
def migration():
"""Handle database migrations."""
if not alembic_migration:
Expand Down Expand Up @@ -474,7 +475,7 @@ class FakeAPITenant:
is_admin = True


@cli.group("admin")
@cli.group("admin", context_settings={"help_option_names": ["-h", "--help"]})
def admin_group():
"""Administrate Duffy tenants."""
if not admin:
Expand Down Expand Up @@ -661,7 +662,7 @@ def admin_update_tenant(
)


@cli.group()
@cli.group(context_settings={"help_option_names": ["-h", "--help"]})
@click.option("--url", help="The base URL of the Duffy API.")
@click.option("--auth-name", help="The tenant name to authenticate with the Duffy API.")
@click.option("--auth-key", help="The tenant key to authenticate with the Duffy API.")
Expand Down Expand Up @@ -698,6 +699,9 @@ def client(
def client_list_sessions(obj):
"""Query active sessions for this tenant on the Duffy API."""
result = obj["client"].list_sessions()
if "error" in result:
click.echo(obj["formatter"].format(result), err=True)
sys.exit(1)
formatted_result = obj["formatter"].format(result)
# Only print newline if formatted_result isn't empty.
click.echo(formatted_result, nl=formatted_result)
Expand All @@ -709,6 +713,9 @@ def client_list_sessions(obj):
def client_show_session(obj, session_id: int):
"""Show one session identified by its id on the Duffy API."""
result = obj["client"].show_session(session_id)
if "error" in result:
click.echo(obj["formatter"].format(result), err=True)
sys.exit(1)
click.echo(obj["formatter"].format(result))


Expand All @@ -720,11 +727,76 @@ def client_show_session(obj, session_id: int):
required=True,
metavar="pool=<pool>,quantity=<quantity> [...]",
)
@click.option(
"--retry",
is_flag=True,
help="Enable retry on failure with fixed interval.",
)
@click.option(
"--timeout",
type=int,
default=5,
help="Total timeout in minutes for retries (default: 5).",
)
@click.option(
"--retry-interval",
type=int,
default=45,
help="Seconds to wait between retry attempts (default: 45).",
)
@click.pass_obj
def client_request_session(obj: dict, nodes_specs: List[str]):
def client_request_session(
obj: dict, nodes_specs: List[str], retry: bool, timeout: int, retry_interval: int
):
"""Request a session with nodes from the Duffy API."""
result = obj["client"].request_session(nodes_specs)
click.echo(obj["formatter"].format(result))
start_time = time.time()
timeout_seconds = timeout * 60 # Convert minutes to seconds
attempt = 0

while True:
attempt += 1
result = obj["client"].request_session(nodes_specs)

if "error" not in result:
# Success - output result and exit
if attempt > 1:
click.echo(f"Success after {attempt} attempts!", err=True)
click.echo(obj["formatter"].format(result))
return

# If retry is not enabled, exit immediately on error
if not retry:
click.echo(obj["formatter"].format(result), err=True)
sys.exit(1)

# Check if we've exceeded the timeout
elapsed_time = time.time() - start_time
if elapsed_time >= timeout_seconds:
click.echo(f"Timeout reached after {timeout} minutes. Last error:", err=True)
click.echo(obj["formatter"].format(result), err=True)
sys.exit(1)

# Calculate remaining time and next sleep duration
remaining_time = timeout_seconds - elapsed_time
sleep_duration = min(retry_interval, remaining_time)

if sleep_duration <= 0:
click.echo(f"Timeout reached after {timeout} minutes. Last error:", err=True)
click.echo(obj["formatter"].format(result), err=True)
sys.exit(1)

# Show progress every 5 attempts
if attempt % 5 == 0:
elapsed_minutes = elapsed_time / 60
click.echo(
f"Still trying... (attempt {attempt}, {elapsed_minutes:.1f}/{timeout} minutes elapsed)",
err=True,
)

click.echo(
f"Request failed (attempt {attempt}), retrying in {sleep_duration} seconds...", err=True
)
time.sleep(sleep_duration)


@client.command("retire-session")
Expand All @@ -733,6 +805,9 @@ def client_request_session(obj: dict, nodes_specs: List[str]):
def client_retire_session(obj: dict, session_id: int):
"""Retire an active Duffy session."""
result = obj["client"].retire_session(session_id)
if "error" in result:
click.echo(obj["formatter"].format(result), err=True)
sys.exit(1)
click.echo(obj["formatter"].format(result))


Expand All @@ -741,6 +816,10 @@ def client_retire_session(obj: dict, session_id: int):
def client_list_pools(obj: dict):
"""List configured Duffy node pools."""
result = obj["client"].list_pools()
if "error" in result:
formatted_result = obj["formatter"].format(result)
click.echo(formatted_result, err=True)
sys.exit(1)
formatted_result = obj["formatter"].format(result)
# Only print newline if formatted_result isn't empty.
click.echo(formatted_result, nl=formatted_result)
Expand All @@ -752,4 +831,7 @@ def client_list_pools(obj: dict):
def client_show_pool(obj: dict, name: str):
"""Show information about a Duffy node pool."""
result = obj["client"].show_pool(name)
if "error" in result:
click.echo(obj["formatter"].format(result), err=True)
sys.exit(1)
click.echo(obj["formatter"].format(result))
Loading