Use concurrent.futures for threading (#1483)

This commit is contained in:
Nikhil Rao 2023-07-31 17:45:40 -07:00 committed by GitHub
parent b9536bcf40
commit 91c0de4b5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 36 deletions

View File

@ -2,7 +2,6 @@
import os import os
import signal import signal
import threading
from pathlib import Path from pathlib import Path
import httpx import httpx
@ -167,19 +166,18 @@ def run(
# Post a telemetry event. # Post a telemetry event.
telemetry.send(f"run-{env.value}", config.telemetry_enabled) telemetry.send(f"run-{env.value}", config.telemetry_enabled)
# Run the frontend and backend.
if frontend:
setup_frontend(Path.cwd())
threading.Thread(target=frontend_cmd, args=(Path.cwd(), frontend_port)).start()
if backend:
threading.Thread(
target=backend_cmd,
args=(app.__name__, backend_host, backend_port),
).start()
# Display custom message when there is a keyboard interrupt. # Display custom message when there is a keyboard interrupt.
signal.signal(signal.SIGINT, processes.catch_keyboard_interrupt) signal.signal(signal.SIGINT, processes.catch_keyboard_interrupt)
# Run the frontend and backend together.
commands = []
if frontend:
setup_frontend(Path.cwd())
commands.append((frontend_cmd, Path.cwd(), frontend_port))
if backend:
commands.append((backend_cmd, app.__name__, backend_host, backend_port))
processes.run_concurrently(*commands)
@cli.command() @cli.command()
def deploy(dry_run: bool = typer.Option(False, help="Whether to run a dry run.")): def deploy(dry_run: bool = typer.Option(False, help="Whether to run a dry run.")):

View File

@ -121,7 +121,7 @@ def error(msg: str, **kwargs):
kwargs: Keyword arguments to pass to the print function. kwargs: Keyword arguments to pass to the print function.
""" """
if LOG_LEVEL <= LogLevel.ERROR: if LOG_LEVEL <= LogLevel.ERROR:
print(f"[red]Error: {msg}[/red]", **kwargs) print(f"[red]{msg}[/red]", **kwargs)
def ask( def ask(

View File

@ -9,7 +9,6 @@ import platform
import re import re
import sys import sys
import tempfile import tempfile
import threading
from fileinput import FileInput from fileinput import FileInput
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
@ -167,7 +166,7 @@ def get_default_app_name() -> str:
console.error( console.error(
f"The app directory cannot be named [bold]{constants.MODULE_NAME}[/bold]." f"The app directory cannot be named [bold]{constants.MODULE_NAME}[/bold]."
) )
raise typer.Exit() raise typer.Exit(1)
return app_name return app_name
@ -315,7 +314,7 @@ def install_node():
console.error( console.error(
f"Node.js version {constants.NODE_VERSION} or higher is required to run Reflex." f"Node.js version {constants.NODE_VERSION} or higher is required to run Reflex."
) )
raise typer.Exit() raise typer.Exit(1)
# Create the nvm directory and install. # Create the nvm directory and install.
path_ops.mkdir(constants.NVM_DIR) path_ops.mkdir(constants.NVM_DIR)
@ -332,7 +331,7 @@ def install_node():
], ],
env=env, env=env,
) )
processes.show_status("", process) processes.show_status("Installing node", process)
def install_bun(): def install_bun():
@ -401,14 +400,14 @@ def check_initialized(frontend: bool = True):
console.error( console.error(
f"The app is not initialized. Run [bold]{constants.MODULE_NAME} init[/bold] first." f"The app is not initialized. Run [bold]{constants.MODULE_NAME} init[/bold] first."
) )
raise typer.Exit() raise typer.Exit(1)
# Check that the template is up to date. # Check that the template is up to date.
if frontend and not is_latest_template(): if frontend and not is_latest_template():
console.error( console.error(
"The base app template has updated. Run [bold]reflex init[/bold] again." "The base app template has updated. Run [bold]reflex init[/bold] again."
) )
raise typer.Exit() raise typer.Exit(1)
# Print a warning for Windows users. # Print a warning for Windows users.
if IS_WINDOWS: if IS_WINDOWS:
@ -436,20 +435,11 @@ def initialize_frontend_dependencies():
path_ops.mkdir(constants.REFLEX_DIR) path_ops.mkdir(constants.REFLEX_DIR)
# Install the frontend dependencies. # Install the frontend dependencies.
threads = [ processes.run_concurrently(install_node, install_bun)
threading.Thread(target=initialize_bun),
threading.Thread(target=initialize_node),
]
for thread in threads:
thread.start()
# Set up the web directory. # Set up the web directory.
initialize_web_directory() initialize_web_directory()
# Wait for the threads to finish.
for thread in threads:
thread.join()
def check_admin_settings(): def check_admin_settings():
"""Check if admin settings are set and valid for logging in cli app.""" """Check if admin settings are set and valid for logging in cli app."""

View File

@ -2,15 +2,17 @@
from __future__ import annotations from __future__ import annotations
import collections
import contextlib import contextlib
import os import os
import signal import signal
import subprocess import subprocess
import sys from concurrent import futures
from typing import List, Optional from typing import Callable, List, Optional, Tuple, Union
from urllib.parse import urlparse from urllib.parse import urlparse
import psutil import psutil
import typer
from reflex import constants from reflex import constants
from reflex.config import get_config from reflex.config import get_config
@ -99,6 +101,10 @@ def change_or_terminate_port(port, _type) -> str:
Returns: Returns:
The new port or the current one. The new port or the current one.
Raises:
Exit: If the user wants to exit.
""" """
console.info( console.info(
f"Something is already running on port [bold underline]{port}[/bold underline]. This is the port the {_type} runs on." f"Something is already running on port [bold underline]{port}[/bold underline]. This is the port the {_type} runs on."
@ -120,7 +126,7 @@ def change_or_terminate_port(port, _type) -> str:
return new_port return new_port
else: else:
console.log("Exiting...") console.log("Exiting...")
sys.exit() raise typer.Exit()
def new_process(args, run: bool = False, show_logs: bool = False, **kwargs): def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
@ -153,6 +159,26 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
return fn(args, **kwargs) return fn(args, **kwargs)
def run_concurrently(*fns: Union[Callable, Tuple]):
"""Run functions concurrently in a thread pool.
Args:
*fns: The functions to run.
"""
# Convert the functions to tuples.
fns = [fn if isinstance(fn, tuple) else (fn,) for fn in fns] # type: ignore
# Run the functions concurrently.
with futures.ThreadPoolExecutor(max_workers=len(fns)) as executor:
# Submit the tasks.
tasks = [executor.submit(*fn) for fn in fns] # type: ignore
# Get the results in the order completed to check any exceptions.
for task in futures.as_completed(tasks):
task.result()
def stream_logs( def stream_logs(
message: str, message: str,
process: subprocess.Popen, process: subprocess.Popen,
@ -165,21 +191,27 @@ def stream_logs(
Yields: Yields:
The lines of the process output. The lines of the process output.
Raises:
Exit: If the process failed.
""" """
# Store the tail of the logs.
logs = collections.deque(maxlen=512)
with process: with process:
console.debug(message) console.debug(message)
if process.stdout is None: if process.stdout is None:
return return
for line in process.stdout: for line in process.stdout:
console.debug(line, end="") console.debug(line, end="")
logs.append(line)
yield line yield line
if process.returncode != 0: if process.returncode != 0:
console.error(f"Error during {message}") console.error(f"{message} failed with exit code {process.returncode}")
console.error( for line in logs:
"Run in with [bold]--loglevel debug[/bold] to see the full error." console.error(line, end="")
) console.error("Run with [bold]--loglevel debug [/bold] for the full log.")
os._exit(1) raise typer.Exit(1)
def show_logs( def show_logs(

View File

@ -1,5 +1,6 @@
"""Unit tests for the included testing tools.""" """Unit tests for the included testing tools."""
from reflex.testing import AppHarness from reflex.testing import AppHarness
from reflex.utils.prerequisites import IS_WINDOWS
def test_app_harness(tmp_path): def test_app_harness(tmp_path):
@ -8,6 +9,9 @@ def test_app_harness(tmp_path):
Args: Args:
tmp_path: pytest tmp_path fixture tmp_path: pytest tmp_path fixture
""" """
# Skip in Windows CI.
if IS_WINDOWS:
return
def BasicApp(): def BasicApp():
import reflex as rx import reflex as rx