From 7aa8235c402d384d0bd917064d28fd89a4813bf8 Mon Sep 17 00:00:00 2001 From: Geoffrey Yu Date: Sat, 30 Mar 2024 11:58:05 -0400 Subject: [PATCH 1/4] Proxy requests through BRAD to avoid CORS restrictions --- src/brad/ui/manager_impl.py | 53 +++++++++++-------- src/brad/ui/models.py | 10 ++++ ui/src/api.js | 17 +++++- ui/src/components/VdbeView.jsx | 2 +- ui/src/components/VirtualInfraView.jsx | 9 ++-- .../workload_utils/change_clients_api.py | 11 +--- 6 files changed, 64 insertions(+), 38 deletions(-) diff --git a/src/brad/ui/manager_impl.py b/src/brad/ui/manager_impl.py index 93eef308..bf4b8c4b 100644 --- a/src/brad/ui/manager_impl.py +++ b/src/brad/ui/manager_impl.py @@ -3,10 +3,10 @@ import logging import importlib.resources as pkg_resources import numpy as np -from fastapi import FastAPI +import requests +from fastapi import FastAPI, HTTPException from fastapi.staticfiles import StaticFiles from typing import Optional, List -from pydantic import BaseModel import brad.ui.static as brad_app from brad.blueprint import Blueprint @@ -26,6 +26,8 @@ VirtualInfrastructure, DisplayableTable, Status, + ClientState, + SetClientState, ) from brad.daemon.front_end_metrics import FrontEndMetric from brad.daemon.system_event_logger import SystemEventLogger, SystemEventRecord @@ -190,25 +192,34 @@ def get_system_state(filter_tables_for_demo: bool = False) -> SystemState: return system_state -class ClientState(BaseModel): - max_clients: int - curr_clients: int - - -class SetClientState(BaseModel): - curr_clients: int - - -@app.get("/clients") -def get_clients_dummy() -> ClientState: - # Used for debugging without starting the variable client runner. - return ClientState(max_clients=12, curr_clients=3) - - -@app.post("/clients") -def set_clients_dummy(clients: SetClientState) -> ClientState: - # Used for debugging without starting the variable client runner. - return ClientState(max_clients=12, curr_clients=clients.curr_clients) +@app.get("/api/1/clients") +def get_workload_clients(runner_port: Optional[int] = None) -> ClientState: + # This proxies the request to the runner, which runs as a different process + # and listens for requests on a different port. We require a proxy to avoid + # CORS restrictions. + if runner_port is None: + # Used for debugging without starting the variable client runner. + return ClientState(max_clients=12, curr_clients=3) + else: + r = requests.get(f"http://localhost:{runner_port}/clients") + if r.status_code != 200: + raise HTTPException(r.status_code, r.reason) + return ClientState(**r.json()) + + +@app.post("/api/1/clients") +def set_clients(clients: SetClientState) -> ClientState: + # This proxies the request to the runner, which runs as a different process + # and listens for requests on a different port. We require a proxy to avoid + # CORS restrictions. + if clients.runner_port is None: + # Used for debugging without starting the variable client runner. + return ClientState(max_clients=12, curr_clients=clients.curr_clients) + else: + r = requests.post(f"http://localhost:{clients.runner_port}/clients") + if r.status_code != 200: + raise HTTPException(r.status_code, r.reason) + return ClientState(**r.json()) def _analytics_table_mapper_temp(table_name: str, blueprint: Blueprint) -> List[str]: diff --git a/src/brad/ui/models.py b/src/brad/ui/models.py index aeb7b319..7389b663 100644 --- a/src/brad/ui/models.py +++ b/src/brad/ui/models.py @@ -107,3 +107,13 @@ class SystemState(BaseModel): virtual_infra: VirtualInfrastructure blueprint: DisplayableBlueprint next_blueprint: Optional[DisplayableBlueprint] + + +class ClientState(BaseModel): + max_clients: int + curr_clients: int + + +class SetClientState(BaseModel): + runner_port: Optional[int] = None + curr_clients: int diff --git a/ui/src/api.js b/ui/src/api.js index 7c03ac4f..7b64adcc 100644 --- a/ui/src/api.js +++ b/ui/src/api.js @@ -20,4 +20,19 @@ async function fetchSystemState(filterTablesForDemo) { return result.data; } -export { fetchMetrics, fetchSystemState }; +async function fetchWorkloadClients(port) { + const args = port != null ? {runner_port: port} : {}; + const result = await axios.get(`${API_PREFIX}/clients`, args); + return result.data; +} + +async function setWorkloadClients(port, numClients) { + const args = {curr_clients: numClients}; + if (port != null) { + args.runner_port = port; + } + const result = await axios.post(`${API_PREFIX}/clients`, args); + return result.data; +} + +export { fetchMetrics, fetchSystemState, fetchWorkloadClients, setWorkloadClients }; diff --git a/ui/src/components/VdbeView.jsx b/ui/src/components/VdbeView.jsx index 3958c780..63f12c02 100644 --- a/ui/src/components/VdbeView.jsx +++ b/ui/src/components/VdbeView.jsx @@ -49,7 +49,7 @@ function VdbeView({ max={workloadState.max_clients} value={workloadState.curr_clients} onChange={updateWorkloadNumClients} - debounceMs={2000} + debounceMs={800} /> )} diff --git a/ui/src/components/VirtualInfraView.jsx b/ui/src/components/VirtualInfraView.jsx index 214a4e7a..16ec8adf 100644 --- a/ui/src/components/VirtualInfraView.jsx +++ b/ui/src/components/VirtualInfraView.jsx @@ -3,6 +3,7 @@ import Panel from "./Panel"; import VdbeView from "./VdbeView"; import "./styles/VirtualInfraView.css"; import { useEffect, useState, useCallback } from "react"; +import { fetchWorkloadClients, setWorkloadClients } from "../api"; function baseEndpointFromObj({ host, port }) { return `http://${host}:${port}`; @@ -26,9 +27,7 @@ function VirtualInfraView({ return; } const baseEndpoint = baseEndpointFromObj(workloadRunners[vdbeIndex]); - const result = await axios.post(`${baseEndpoint}/clients`, { - curr_clients: numClients, - }); + const result = await setWorkloadClients(baseEndpoint.port, numClients); const newWorkloadState = result.data; // Skip the state update if there was no change. @@ -53,9 +52,9 @@ function VirtualInfraView({ const { workloadRunners } = endpoints; const promises = workloadRunners .map(baseEndpointFromObj) - .map((baseEndpoint) => axios.get(`${baseEndpoint}/clients`)); + .map((baseEndpoint) => fetchWorkloadClients(baseEndpoint.port)); const results = await Promise.all(promises); - setWorkloadStates(results.map(({ data }) => data)); + setWorkloadStates(results); }, [endpoints]); return ( diff --git a/workloads/IMDB_extended/workload_utils/change_clients_api.py b/workloads/IMDB_extended/workload_utils/change_clients_api.py index a7524850..c9c2dcb5 100644 --- a/workloads/IMDB_extended/workload_utils/change_clients_api.py +++ b/workloads/IMDB_extended/workload_utils/change_clients_api.py @@ -1,20 +1,11 @@ import uvicorn from fastapi import FastAPI -from pydantic import BaseModel from typing import Optional +from brad.ui.models import ClientState, SetClientState from .pause_controller import PauseController -class ClientState(BaseModel): - max_clients: int - curr_clients: int - - -class SetClientState(BaseModel): - curr_clients: int - - class Manager: def __init__(self, pc: PauseController) -> None: self.pc = pc From 8da2ceb0344b978c30fe02b68004e023745b1923 Mon Sep 17 00:00:00 2001 From: Geoffrey Yu Date: Sat, 30 Mar 2024 12:04:57 -0400 Subject: [PATCH 2/4] Fix lint --- setup.py | 2 ++ src/brad/ui/manager_impl.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8873f32d..16ee6e04 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,8 @@ "fastapi", "uvicorn[standard]", "pydantic", + "requests", + "types-requests", ] KEYWORDS = [] diff --git a/src/brad/ui/manager_impl.py b/src/brad/ui/manager_impl.py index bf4b8c4b..745a6e18 100644 --- a/src/brad/ui/manager_impl.py +++ b/src/brad/ui/manager_impl.py @@ -201,7 +201,7 @@ def get_workload_clients(runner_port: Optional[int] = None) -> ClientState: # Used for debugging without starting the variable client runner. return ClientState(max_clients=12, curr_clients=3) else: - r = requests.get(f"http://localhost:{runner_port}/clients") + r = requests.get(f"http://localhost:{runner_port}/clients", timeout=2) if r.status_code != 200: raise HTTPException(r.status_code, r.reason) return ClientState(**r.json()) @@ -216,7 +216,7 @@ def set_clients(clients: SetClientState) -> ClientState: # Used for debugging without starting the variable client runner. return ClientState(max_clients=12, curr_clients=clients.curr_clients) else: - r = requests.post(f"http://localhost:{clients.runner_port}/clients") + r = requests.post(f"http://localhost:{clients.runner_port}/clients", timeout=2) if r.status_code != 200: raise HTTPException(r.status_code, r.reason) return ClientState(**r.json()) From 17f5c5fd86eb9f034b9539446322a76638928f9f Mon Sep 17 00:00:00 2001 From: Geoffrey Yu Date: Sat, 30 Mar 2024 12:41:31 -0400 Subject: [PATCH 3/4] Fixes --- src/brad/ui/manager_impl.py | 22 ++++++++++++++-------- ui/src/App.jsx | 4 +--- ui/src/api.js | 2 +- ui/src/components/PerfView.jsx | 2 +- ui/src/components/VirtualInfraView.jsx | 13 +++---------- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/brad/ui/manager_impl.py b/src/brad/ui/manager_impl.py index 745a6e18..b73ccb21 100644 --- a/src/brad/ui/manager_impl.py +++ b/src/brad/ui/manager_impl.py @@ -201,10 +201,13 @@ def get_workload_clients(runner_port: Optional[int] = None) -> ClientState: # Used for debugging without starting the variable client runner. return ClientState(max_clients=12, curr_clients=3) else: - r = requests.get(f"http://localhost:{runner_port}/clients", timeout=2) - if r.status_code != 200: - raise HTTPException(r.status_code, r.reason) - return ClientState(**r.json()) + try: + r = requests.get(f"http://localhost:{runner_port}/clients", timeout=2) + if r.status_code != 200: + raise HTTPException(r.status_code, r.reason) + return ClientState(**r.json()) + except requests.ConnectionError: + raise HTTPException(400, f"Unable to connect to port {runner_port}") @app.post("/api/1/clients") @@ -216,10 +219,13 @@ def set_clients(clients: SetClientState) -> ClientState: # Used for debugging without starting the variable client runner. return ClientState(max_clients=12, curr_clients=clients.curr_clients) else: - r = requests.post(f"http://localhost:{clients.runner_port}/clients", timeout=2) - if r.status_code != 200: - raise HTTPException(r.status_code, r.reason) - return ClientState(**r.json()) + try: + r = requests.post(f"http://localhost:{clients.runner_port}/clients", timeout=2) + if r.status_code != 200: + raise HTTPException(r.status_code, r.reason) + return ClientState(**r.json()) + except requests.ConnectionError: + raise HTTPException(400, f"Unable to connect to port {clients.runner_port}") def _analytics_table_mapper_temp(table_name: str, blueprint: Blueprint) -> List[str]: diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 4e4c2060..1161fd19 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -27,12 +27,10 @@ function App() { { host: "localhost", port: 8585, - // port: 7583, }, { host: "localhost", port: 8586, - // port: 7583, }, ], }); @@ -72,7 +70,7 @@ function App() { let timeoutId = null; const refreshData = async () => { const newSystemState = await fetchSystemState( - /*filterTablesForDemo=*/ true, + /*filterTablesForDemo=*/ false, ); // TODO: Not the best way to check for equality. if (JSON.stringify(systemState) !== JSON.stringify(newSystemState)) { diff --git a/ui/src/api.js b/ui/src/api.js index 7b64adcc..6e8de406 100644 --- a/ui/src/api.js +++ b/ui/src/api.js @@ -21,7 +21,7 @@ async function fetchSystemState(filterTablesForDemo) { } async function fetchWorkloadClients(port) { - const args = port != null ? {runner_port: port} : {}; + const args = port != null ? {params: {runner_port: port}} : {}; const result = await axios.get(`${API_PREFIX}/clients`, args); return result.data; } diff --git a/ui/src/components/PerfView.jsx b/ui/src/components/PerfView.jsx index fe394b06..5f4f40b7 100644 --- a/ui/src/components/PerfView.jsx +++ b/ui/src/components/PerfView.jsx @@ -81,7 +81,7 @@ function PerfView({ virtualInfra }) { useEffect(() => { let timeoutId = null; const refreshData = async () => { - const rawMetrics = await fetchMetrics(60, /*useGenerated=*/ true); + const rawMetrics = await fetchMetrics(60, /*useGenerated=*/ false); const fetchedMetrics = parseMetrics(rawMetrics); const metricsManager = getMetricsManager(); const addedNewMetrics = metricsManager.mergeInMetrics(fetchedMetrics); diff --git a/ui/src/components/VirtualInfraView.jsx b/ui/src/components/VirtualInfraView.jsx index 16ec8adf..0a666a1e 100644 --- a/ui/src/components/VirtualInfraView.jsx +++ b/ui/src/components/VirtualInfraView.jsx @@ -1,14 +1,9 @@ -import axios from "axios"; import Panel from "./Panel"; import VdbeView from "./VdbeView"; import "./styles/VirtualInfraView.css"; import { useEffect, useState, useCallback } from "react"; import { fetchWorkloadClients, setWorkloadClients } from "../api"; -function baseEndpointFromObj({ host, port }) { - return `http://${host}:${port}`; -} - function VirtualInfraView({ virtualInfra, highlight, @@ -26,9 +21,8 @@ function VirtualInfraView({ ) { return; } - const baseEndpoint = baseEndpointFromObj(workloadRunners[vdbeIndex]); - const result = await setWorkloadClients(baseEndpoint.port, numClients); - const newWorkloadState = result.data; + const endpoint = workloadRunners[vdbeIndex]; + const newWorkloadState = await setWorkloadClients(endpoint.port, numClients); // Skip the state update if there was no change. const existingWorkloadState = workloadStates[vdbeIndex]; @@ -51,8 +45,7 @@ function VirtualInfraView({ useEffect(async () => { const { workloadRunners } = endpoints; const promises = workloadRunners - .map(baseEndpointFromObj) - .map((baseEndpoint) => fetchWorkloadClients(baseEndpoint.port)); + .map((endpoint) => fetchWorkloadClients(endpoint.port)); const results = await Promise.all(promises); setWorkloadStates(results); }, [endpoints]); From 4864e9045d408661db27ce5d29f8d99e4927edbf Mon Sep 17 00:00:00 2001 From: Geoffrey Yu Date: Sat, 30 Mar 2024 12:45:35 -0400 Subject: [PATCH 4/4] Fix lint, formatting --- src/brad/ui/manager_impl.py | 14 +++++++++----- ui/src/api.js | 11 ++++++++--- ui/src/components/VirtualInfraView.jsx | 10 +++++++--- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/brad/ui/manager_impl.py b/src/brad/ui/manager_impl.py index b73ccb21..d579f2d7 100644 --- a/src/brad/ui/manager_impl.py +++ b/src/brad/ui/manager_impl.py @@ -206,8 +206,8 @@ def get_workload_clients(runner_port: Optional[int] = None) -> ClientState: if r.status_code != 200: raise HTTPException(r.status_code, r.reason) return ClientState(**r.json()) - except requests.ConnectionError: - raise HTTPException(400, f"Unable to connect to port {runner_port}") + except requests.ConnectionError as ex: + raise HTTPException(400, f"Unable to connect to port {runner_port}") from ex @app.post("/api/1/clients") @@ -220,12 +220,16 @@ def set_clients(clients: SetClientState) -> ClientState: return ClientState(max_clients=12, curr_clients=clients.curr_clients) else: try: - r = requests.post(f"http://localhost:{clients.runner_port}/clients", timeout=2) + r = requests.post( + f"http://localhost:{clients.runner_port}/clients", timeout=2 + ) if r.status_code != 200: raise HTTPException(r.status_code, r.reason) return ClientState(**r.json()) - except requests.ConnectionError: - raise HTTPException(400, f"Unable to connect to port {clients.runner_port}") + except requests.ConnectionError as ex: + raise HTTPException( + 400, f"Unable to connect to port {clients.runner_port}" + ) from ex def _analytics_table_mapper_temp(table_name: str, blueprint: Blueprint) -> List[str]: diff --git a/ui/src/api.js b/ui/src/api.js index 6e8de406..b7e6b726 100644 --- a/ui/src/api.js +++ b/ui/src/api.js @@ -21,13 +21,13 @@ async function fetchSystemState(filterTablesForDemo) { } async function fetchWorkloadClients(port) { - const args = port != null ? {params: {runner_port: port}} : {}; + const args = port != null ? { params: { runner_port: port } } : {}; const result = await axios.get(`${API_PREFIX}/clients`, args); return result.data; } async function setWorkloadClients(port, numClients) { - const args = {curr_clients: numClients}; + const args = { curr_clients: numClients }; if (port != null) { args.runner_port = port; } @@ -35,4 +35,9 @@ async function setWorkloadClients(port, numClients) { return result.data; } -export { fetchMetrics, fetchSystemState, fetchWorkloadClients, setWorkloadClients }; +export { + fetchMetrics, + fetchSystemState, + fetchWorkloadClients, + setWorkloadClients, +}; diff --git a/ui/src/components/VirtualInfraView.jsx b/ui/src/components/VirtualInfraView.jsx index 0a666a1e..6947205f 100644 --- a/ui/src/components/VirtualInfraView.jsx +++ b/ui/src/components/VirtualInfraView.jsx @@ -22,7 +22,10 @@ function VirtualInfraView({ return; } const endpoint = workloadRunners[vdbeIndex]; - const newWorkloadState = await setWorkloadClients(endpoint.port, numClients); + const newWorkloadState = await setWorkloadClients( + endpoint.port, + numClients, + ); // Skip the state update if there was no change. const existingWorkloadState = workloadStates[vdbeIndex]; @@ -44,8 +47,9 @@ function VirtualInfraView({ useEffect(async () => { const { workloadRunners } = endpoints; - const promises = workloadRunners - .map((endpoint) => fetchWorkloadClients(endpoint.port)); + const promises = workloadRunners.map((endpoint) => + fetchWorkloadClients(endpoint.port), + ); const results = await Promise.all(promises); setWorkloadStates(results); }, [endpoints]);