Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

increase quote timeout #42

Merged
merged 6 commits into from
Nov 28, 2024
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@
"typescript": "^5.6.2"
},
"version": "0.1.3"
}
}
146 changes: 101 additions & 45 deletions src/lib/buy/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,35 @@ function parsePricePerGpuHour(price?: string) {
return Number.parseFloat(priceWithoutDollar) * 100;
}

async function quoteAction(options: SfBuyOptions) {
const quote = await getQuoteFromParsedSfBuyOptions(options);
render(<QuoteDisplay quote={quote} />);
function QuoteComponent(
props: {
options: SfBuyOptions;
},
) {
const [quote, setQuote] = useState<Quote | null>(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
? (
<Box gap={1}>
<Spinner type="dots" />
<Box gap={1}>
<Text>Getting quote...</Text>
</Box>
</Box>
)
: <QuoteDisplay quote={quote} />;
}

/*
Expand All @@ -132,43 +158,71 @@ Flow is:
*/
async function buyOrderAction(options: SfBuyOptions) {
if (options.quote) {
return quoteAction(options);
}
render(<QuoteComponent options={options} />);
} 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(<QuoteAndBuy options={options} />);
}
}

// 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<BuyOrderProps | null>(null);

const duration = parseDuration(options.duration);
const startDate = parseStartAsDate(options.start);
const endsAt = roundEndDate(
dayjs(startDate).add(duration, "seconds").toDate(),
).toDate();

render(
<BuyOrder
price={pricePerGpuHour}
size={parseAccelerators(options.accelerators)}
startAt={startDate}
type={options.type}
endsAt={endsAt}
colocate={options.colocate}
/>,
);
// 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
? (
<Box gap={1}>
<Spinner type="dots" />
<Box gap={1}>
<Text>Getting quote...</Text>
</Box>
</Box>
)
: <BuyOrder {...orderProps} />;
}

function roundEndDate(endDate: Date) {
Expand Down Expand Up @@ -264,16 +318,16 @@ function BuyOrderPreview(
type Order =
| Awaited<ReturnType<typeof getOrder>>
| Awaited<ReturnType<typeof placeBuyOrder>>;

type BuyOrderProps = {
price: number;
size: number;
startAt: Date | "NOW";
endsAt: Date;
type: string;
colocate?: Array<string>;
};
function BuyOrder(
props: {
price: number;
size: number;
startAt: Date | "NOW";
endsAt: Date;
type: string;
colocate?: Array<string>;
},
props: BuyOrderProps,
) {
const [isLoading, setIsLoading] = useState(false);
const [value, setValue] = useState("");
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 8 additions & 7 deletions src/lib/clusters/kubeconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -105,7 +106,7 @@ export function createKubeconfig(props: {

export function mergeNamedItems<T extends { name: string }>(
items1: T[],
items2: T[]
items2: T[],
): T[] {
const map = new Map<string, T>();
for (const item of items1) {
Expand All @@ -119,7 +120,7 @@ export function mergeNamedItems<T extends { name: string }>(

export function mergeKubeconfigs(
oldConfig: Kubeconfig,
newConfig?: Kubeconfig
newConfig?: Kubeconfig,
): Kubeconfig {
if (!newConfig) {
return oldConfig;
Expand All @@ -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 },
};
Expand Down
6 changes: 5 additions & 1 deletion src/lib/contracts/ContractDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export function ContractDisplay(props: { contract: Contract }) {
return (
<Box key={interval} gap={1}>
<Box width={17} alignItems="flex-end">
<Text>{quantity * GPUS_PER_NODE} x {props.contract.instance_type} (gpus)</Text>
<Text>
{quantity * GPUS_PER_NODE} x {props.contract.instance_type}
{" "}
(gpus)
</Text>
</Box>
<Text dimColor>│</Text>
<Box gap={1}>
Expand Down
25 changes: 14 additions & 11 deletions src/lib/sell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function registerSell(program: Command) {
.option(
"-f, --flags <flags>",
"Specify additional flags as JSON",
JSON.parse
JSON.parse,
)
.action(async (options) => {
await placeSellOrder(options);
Expand All @@ -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 };
Expand Down Expand Up @@ -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}`,
);
}

Expand Down Expand Up @@ -133,7 +134,7 @@ async function placeSellOrder(options: {
priceCents,
totalDurationSecs,
nodes,
GPUS_PER_NODE
GPUS_PER_NODE,
);

const params: PlaceSellOrderParameters = {
Expand All @@ -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:
Expand Down