Hosting CLI: use http endpoint to return deploy milestones (#2085)

This commit is contained in:
Martin Xu 2023-11-03 12:13:46 -07:00 committed by GitHub
parent b313aaf3ef
commit 4c97b4c4c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 108 additions and 17 deletions

View File

@ -686,10 +686,14 @@ def deploy(
console.print("Waiting for server to report progress ...")
# Display the key events such as build, deploy, etc
server_report_deploy_success = asyncio.get_event_loop().run_until_complete(
hosting.display_deploy_milestones(key, from_iso_timestamp=deploy_requested_at)
server_report_deploy_success = hosting.poll_deploy_milestones(
key, from_iso_timestamp=deploy_requested_at
)
if not server_report_deploy_success:
if server_report_deploy_success is None:
console.warn("Hosting server timed out.")
console.warn("The deployment may still be in progress. Proceeding ...")
elif not server_report_deploy_success:
console.error("Hosting server reports failure.")
console.error(
f"Check the server logs using `reflex deployments build-logs {key}`"
@ -814,7 +818,7 @@ def get_deployment_status(
status = hosting.get_deployment_status(key)
# TODO: refactor all these tabulate calls
status.backend.updated_at = hosting.convert_to_local_time(
status.backend.updated_at = hosting.convert_to_local_time_str(
status.backend.updated_at or "N/A"
)
backend_status = status.backend.dict(exclude_none=True)
@ -823,7 +827,7 @@ def get_deployment_status(
console.print(tabulate([table], headers=headers))
# Add a new line in console
console.print("\n")
status.frontend.updated_at = hosting.convert_to_local_time(
status.frontend.updated_at = hosting.convert_to_local_time_str(
status.frontend.updated_at or "N/A"
)
frontend_status = status.frontend.dict(exclude_none=True)

View File

@ -9,7 +9,7 @@ import re
import time
import uuid
import webbrowser
from datetime import datetime
from datetime import datetime, timedelta
from http import HTTPStatus
from typing import List, Optional
@ -41,12 +41,14 @@ GET_DEPLOYMENT_STATUS_ENDPOINT = f"{config.cp_backend_url}/deployments"
GET_REGIONS_ENDPOINT = f"{config.cp_backend_url}/deployments/regions"
# Websocket endpoint to stream logs of a deployment
DEPLOYMENT_LOGS_ENDPOINT = f'{config.cp_backend_url.replace("http", "ws")}/deployments'
# The HTTP endpoint to fetch logs of a deployment
POST_DEPLOYMENT_LOGS_ENDPOINT = f"{config.cp_backend_url}/deployments/logs"
# Expected server response time to new deployment request. In seconds.
DEPLOYMENT_PICKUP_DELAY = 30
# End of deployment workflow message. Used to determine if it is the last message from server.
END_OF_DEPLOYMENT_MESSAGES = ["deploy success"]
# How many iterations to try and print the deployment event messages from server during deployment.
DEPLOYMENT_EVENT_MESSAGES_RETRIES = 90
DEPLOYMENT_EVENT_MESSAGES_RETRIES = 120
# Timeout limit for http requests
HTTP_REQUEST_TIMEOUT = 60 # seconds
@ -741,7 +743,23 @@ def get_deployment_status(key: str) -> DeploymentStatusResponse:
raise Exception("internal errors") from ex
def convert_to_local_time(iso_timestamp: str) -> str:
def convert_to_local_time_with_tz(iso_timestamp: str) -> datetime | None:
"""Helper function to convert the iso timestamp to local time.
Args:
iso_timestamp: The iso timestamp to convert.
Returns:
The converted timestamp with timezone.
"""
try:
return datetime.fromisoformat(iso_timestamp).astimezone()
except (TypeError, ValueError) as ex:
console.error(f"Unable to convert iso timestamp {iso_timestamp} due to {ex}.")
return None
def convert_to_local_time_str(iso_timestamp: str) -> str:
"""Convert the iso timestamp to local time.
Args:
@ -750,12 +768,9 @@ def convert_to_local_time(iso_timestamp: str) -> str:
Returns:
The converted timestamp string.
"""
try:
local_dt = datetime.fromisoformat(iso_timestamp).astimezone()
return local_dt.strftime("%Y-%m-%d %H:%M:%S.%f %Z")
except Exception as ex:
console.error(f"Unable to convert iso timestamp {iso_timestamp} due to {ex}.")
if (local_dt := convert_to_local_time_with_tz(iso_timestamp)) is None:
return iso_timestamp
return local_dt.strftime("%Y-%m-%d %H:%M:%S.%f %Z")
class LogType(str, enum.Enum):
@ -813,7 +828,7 @@ async def get_logs(
if v is None:
row_to_print[k] = str(v)
elif k == "timestamp":
row_to_print[k] = convert_to_local_time(v)
row_to_print[k] = convert_to_local_time_str(v)
else:
row_to_print[k] = v
print(" | ".join(row_to_print.values()))
@ -1045,6 +1060,78 @@ def log_out_on_browser():
)
def poll_deploy_milestones(key: str, from_iso_timestamp: datetime) -> bool | None:
"""Periodically poll the hosting server for deploy milestones.
Args:
key: The deployment key.
from_iso_timestamp: The timestamp of the deployment request time, this helps with the milestone query.
Raises:
ValueError: If a non-empty key is not provided.
Exception: If the user is not authenticated.
Returns:
False if server reports back failure, True otherwise. None if do not receive the end of deployment message.
"""
if not key:
raise ValueError("Non-empty key is required for querying deploy status.")
if not (token := requires_authenticated()):
raise Exception("not authenticated")
for _ in range(DEPLOYMENT_EVENT_MESSAGES_RETRIES):
try:
response = httpx.post(
POST_DEPLOYMENT_LOGS_ENDPOINT,
json={
"key": key,
"log_type": LogType.DEPLOY_LOG.value,
"from_iso_timestamp": from_iso_timestamp.astimezone().isoformat(),
},
headers=authorization_header(token),
)
response.raise_for_status()
# The return is expected to be a list of dicts
response_json = response.json()
for row in response_json:
console.print(
" | ".join(
[
convert_to_local_time_str(row["timestamp"]),
row["message"],
]
)
)
# update the from timestamp to the last timestamp of received message
if (
maybe_timestamp := convert_to_local_time_with_tz(row["timestamp"])
) is not None:
console.debug(
f"Updating from {from_iso_timestamp} to {maybe_timestamp}"
)
# Add a small delta so does not poll the same logs
from_iso_timestamp = maybe_timestamp + timedelta(microseconds=1e5)
else:
console.warn(f"Unable to parse timestamp {row['timestamp']}")
server_message = row["message"].lower()
if "fail" in server_message:
console.debug(
"Received failure message, stop event message streaming"
)
return False
if any(msg in server_message for msg in END_OF_DEPLOYMENT_MESSAGES):
console.debug(
"Received end of deployment message, stop event message streaming"
)
return True
time.sleep(1)
except httpx.HTTPError as he:
# This includes HTTP server and client error
console.debug(f"Unable to get more deployment events due to {he}.")
except Exception as ex:
console.warn(f"Unable to parse server response due to {ex}.")
async def display_deploy_milestones(key: str, from_iso_timestamp: datetime) -> bool:
"""Display the deploy milestone messages reported back from the hosting server.
@ -1078,7 +1165,7 @@ async def display_deploy_milestones(key: str, from_iso_timestamp: datetime) -> b
console.print(
" | ".join(
[
convert_to_local_time(row_json["timestamp"]),
convert_to_local_time_str(row_json["timestamp"]),
row_json["message"],
]
)

View File

@ -118,7 +118,7 @@ def test_deploy_non_interactive_success(
),
)
mocker.patch("reflex.utils.hosting.wait_for_server_to_pick_up_request")
mocker.patch("reflex.utils.hosting.display_deploy_milestones")
mocker.patch("reflex.utils.hosting.poll_deploy_milestones")
mocker.patch("reflex.utils.hosting.poll_backend", return_value=True)
mocker.patch("reflex.utils.hosting.poll_frontend", return_value=True)
# TODO: typer option default not working in test for app name
@ -351,7 +351,7 @@ def test_deploy_interactive(
),
)
mocker.patch("reflex.utils.hosting.wait_for_server_to_pick_up_request")
mocker.patch("reflex.utils.hosting.display_deploy_milestones")
mocker.patch("reflex.utils.hosting.poll_deploy_milestones")
mocker.patch("reflex.utils.hosting.poll_backend", return_value=True)
mocker.patch("reflex.utils.hosting.poll_frontend", return_value=True)