From 382a4bcafc3985b334461691a70ba650f525e7fc Mon Sep 17 00:00:00 2001 From: Sebastien La Duca Date: Wed, 27 Nov 2024 17:02:34 -0800 Subject: [PATCH] increase quote timeout (#42) * increase quote timeout * fmt * fix promise thingy * fix quote component * fix --quote hang --- package.json | 2 +- src/lib/buy/index.tsx | 146 ++++++++++++++++++-------- src/lib/clusters/kubeconfig.ts | 15 +-- src/lib/contracts/ContractDisplay.tsx | 6 +- src/lib/sell.ts | 25 +++-- 5 files changed, 129 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index c9a14f5..04eb320 100644 --- a/package.json +++ b/package.json @@ -40,4 +40,4 @@ "typescript": "^5.6.2" }, "version": "0.1.3" -} \ No newline at end of file +} diff --git a/src/lib/buy/index.tsx b/src/lib/buy/index.tsx index 578b092..47bd458 100644 --- a/src/lib/buy/index.tsx +++ b/src/lib/buy/index.tsx @@ -117,9 +117,35 @@ function parsePricePerGpuHour(price?: string) { return Number.parseFloat(priceWithoutDollar) * 100; } -async function quoteAction(options: SfBuyOptions) { - const quote = await getQuoteFromParsedSfBuyOptions(options); - render(); +function QuoteComponent( + props: { + options: SfBuyOptions; + }, +) { + const [quote, setQuote] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + (async () => { + const quote = await getQuoteFromParsedSfBuyOptions(props.options); + setIsLoading(false); + if (!quote) { + return; + } + setQuote(quote); + })(); + }, [props.options]); + + return isLoading + ? ( + + + + Getting quote... + + + ) + : ; } /* @@ -132,43 +158,71 @@ Flow is: */ async function buyOrderAction(options: SfBuyOptions) { if (options.quote) { - return quoteAction(options); - } + render(); + } else { + const nodes = parseAccelerators(options.accelerators); + if (!Number.isInteger(nodes)) { + return logAndQuit( + `You can only buy whole nodes, or 8 GPUs at a time. Got: ${options.accelerators}`, + ); + } - const nodes = parseAccelerators(options.accelerators); - if (!Number.isInteger(nodes)) { - return logAndQuit( - `You can only buy whole nodes, or 8 GPUs at a time. Got: ${options.accelerators}`, - ); + render(); } +} - // Grab the price per GPU hour, either - let pricePerGpuHour: number | null = parsePricePerGpuHour(options.price); - if (!pricePerGpuHour) { - const quote = await getQuoteFromParsedSfBuyOptions(options); - if (!quote) { - pricePerGpuHour = await getAggressivePricePerHour(options.type); - } else { - pricePerGpuHour = getPricePerGpuHourFromQuote(quote); - } - } +function QuoteAndBuy( + props: { + options: SfBuyOptions; + }, +) { + const [orderProps, setOrderProps] = useState(null); - const duration = parseDuration(options.duration); - const startDate = parseStartAsDate(options.start); - const endsAt = roundEndDate( - dayjs(startDate).add(duration, "seconds").toDate(), - ).toDate(); - - render( - , - ); + // submit a quote request, handle loading state + useEffect(() => { + (async () => { + const quote = await getQuoteFromParsedSfBuyOptions(props.options); + + // Grab the price per GPU hour, either + let pricePerGpuHour: number | null = parsePricePerGpuHour( + props.options.price, + ); + if (!pricePerGpuHour) { + const quote = await getQuoteFromParsedSfBuyOptions(props.options); + if (!quote) { + pricePerGpuHour = await getAggressivePricePerHour(props.options.type); + } else { + pricePerGpuHour = getPricePerGpuHourFromQuote(quote); + } + } + + const duration = parseDuration(props.options.duration); + const startDate = parseStartAsDate(props.options.start); + const endsAt = roundEndDate( + dayjs(startDate).add(duration, "seconds").toDate(), + ).toDate(); + + setOrderProps({ + type: props.options.type, + price: pricePerGpuHour, + size: parseAccelerators(props.options.accelerators), + startAt: startDate, + endsAt, + colocate: props.options.colocate, + }); + })(); + }, []); + + return orderProps === null + ? ( + + + + Getting quote... + + + ) + : ; } function roundEndDate(endDate: Date) { @@ -264,16 +318,16 @@ function BuyOrderPreview( type Order = | Awaited> | Awaited>; - +type BuyOrderProps = { + price: number; + size: number; + startAt: Date | "NOW"; + endsAt: Date; + type: string; + colocate?: Array; +}; function BuyOrder( - props: { - price: number; - size: number; - startAt: Date | "NOW"; - endsAt: Date; - type: string; - colocate?: Array; - }, + props: BuyOrderProps, ) { const [isLoading, setIsLoading] = useState(false); const [value, setValue] = useState(""); @@ -514,6 +568,8 @@ export async function getQuote(options: QuoteOptions) { : options.startsAt.toISOString(), }, }, + // timeout after 600 seconds + signal: AbortSignal.timeout(600 * 1000), }); if (!response.ok) { diff --git a/src/lib/clusters/kubeconfig.ts b/src/lib/clusters/kubeconfig.ts index 2e742bf..811f27b 100644 --- a/src/lib/clusters/kubeconfig.ts +++ b/src/lib/clusters/kubeconfig.ts @@ -94,7 +94,8 @@ export function createKubeconfig(props: { // Set current context based on provided cluster and user names if (currentContext) { - const contextName = `${currentContext.clusterName}@${currentContext.userName}`; + const contextName = + `${currentContext.clusterName}@${currentContext.userName}`; kubeconfig["current-context"] = contextName; } else if (kubeconfig.contexts.length > 0) { kubeconfig["current-context"] = kubeconfig.contexts[0].name; @@ -105,7 +106,7 @@ export function createKubeconfig(props: { export function mergeNamedItems( items1: T[], - items2: T[] + items2: T[], ): T[] { const map = new Map(); for (const item of items1) { @@ -119,7 +120,7 @@ export function mergeNamedItems( export function mergeKubeconfigs( oldConfig: Kubeconfig, - newConfig?: Kubeconfig + newConfig?: Kubeconfig, ): Kubeconfig { if (!newConfig) { return oldConfig; @@ -129,15 +130,15 @@ export function mergeKubeconfigs( apiVersion: newConfig.apiVersion || oldConfig.apiVersion, clusters: mergeNamedItems( oldConfig.clusters || [], - newConfig.clusters || [] + newConfig.clusters || [], ), contexts: mergeNamedItems( oldConfig.contexts || [], - newConfig.contexts || [] + newConfig.contexts || [], ), users: mergeNamedItems(oldConfig.users || [], newConfig.users || []), - "current-context": - newConfig["current-context"] || oldConfig["current-context"], + "current-context": newConfig["current-context"] || + oldConfig["current-context"], kind: newConfig.kind || oldConfig.kind, preferences: { ...oldConfig.preferences, ...newConfig.preferences }, }; diff --git a/src/lib/contracts/ContractDisplay.tsx b/src/lib/contracts/ContractDisplay.tsx index 476ff36..638744c 100644 --- a/src/lib/contracts/ContractDisplay.tsx +++ b/src/lib/contracts/ContractDisplay.tsx @@ -51,7 +51,11 @@ export function ContractDisplay(props: { contract: Contract }) { return ( - {quantity * GPUS_PER_NODE} x {props.contract.instance_type} (gpus) + + {quantity * GPUS_PER_NODE} x {props.contract.instance_type} + {" "} + (gpus) + diff --git a/src/lib/sell.ts b/src/lib/sell.ts index b66e102..dcafd18 100644 --- a/src/lib/sell.ts +++ b/src/lib/sell.ts @@ -32,7 +32,7 @@ export function registerSell(program: Command) { .option( "-f, --flags ", "Specify additional flags as JSON", - JSON.parse + JSON.parse, ) .action(async (options) => { await placeSellOrder(options); @@ -54,7 +54,7 @@ function contractStartAndEnd(contract: { }) { const startDate = dayjs(contract.shape.intervals[0]).toDate(); const endDate = dayjs( - contract.shape.intervals[contract.shape.intervals.length - 1] + contract.shape.intervals[contract.shape.intervals.length - 1], ).toDate(); return { startDate, endDate }; @@ -84,14 +84,15 @@ async function placeSellOrder(options: { if (contract?.status === "pending") { return logAndQuit( - `Contract ${options.contractId} is currently pending. Please try again in a few seconds.` + `Contract ${options.contractId} is currently pending. Please try again in a few seconds.`, ); } if (options.accelerators % GPUS_PER_NODE !== 0) { - const exampleCommand = `sf sell -n ${GPUS_PER_NODE} -c ${options.contractId}`; + const exampleCommand = + `sf sell -n ${GPUS_PER_NODE} -c ${options.contractId}`; return logAndQuit( - `At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}` + `At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}`, ); } @@ -133,7 +134,7 @@ async function placeSellOrder(options: { priceCents, totalDurationSecs, nodes, - GPUS_PER_NODE + GPUS_PER_NODE, ); const params: PlaceSellOrderParameters = { @@ -154,11 +155,13 @@ async function placeSellOrder(options: { switch (response.status) { case 400: return logAndQuit( - `Bad Request: ${error?.message}: ${JSON.stringify( - error?.details, - null, - 2 - )}` + `Bad Request: ${error?.message}: ${ + JSON.stringify( + error?.details, + null, + 2, + ) + }`, ); // return logAndQuit(`Bad Request: ${error?.message}`); case 401: