From 50116651dedcb48e7fb36ffbd6e48740933ba7ad Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Tue, 21 Jan 2025 12:38:25 +0200 Subject: [PATCH] feat: Add spot pod start and improved error handling - Added functionality to start spot pods using the RunPod API. - Improved error handling and clarity in the RunPod client. - Added more detailed comments to the code for better readability. - Refactored the HTTP client and utils to improve modularity. - Updated example to demonstrate spot pod creation and starting. Co-authored-by: mariobassem12 --- examples/develop/runpod/runpod_example.vsh | 14 ++++-- lib/clients/runpod/client.v | 10 ++++- lib/clients/runpod/runpod_http.v | 50 ++++++++++++++-------- lib/clients/runpod/utils.v | 28 +++++++++--- 4 files changed, 73 insertions(+), 29 deletions(-) diff --git a/examples/develop/runpod/runpod_example.vsh b/examples/develop/runpod/runpod_example.vsh index 9b5469ce..20a325dd 100755 --- a/examples/develop/runpod/runpod_example.vsh +++ b/examples/develop/runpod/runpod_example.vsh @@ -33,7 +33,7 @@ on_demand_pod_response := rp.create_on_demand_pod( println('Created pod with ID: ${on_demand_pod_response.id}') // create a spot pod -spot_pod_resp := rp.create_spot_pod( +spot_pod_response := rp.create_spot_pod( port: 1826 bid_per_gpu: 0.2 cloud_type: .secure @@ -55,8 +55,16 @@ spot_pod_resp := rp.create_spot_pod( }, ] )! -println('Created spot pod with ID: ${spot_pod_resp.id}') +println('Created spot pod with ID: ${spot_pod_response.id}') // start on-demand pod start_on_demand_pod := rp.start_on_demand_pod(pod_id: '${on_demand_pod_response.id}', gpu_count: 1)! -println('Started pod with ID: ${start_on_demand_pod.id}') +println('Started on demand pod with ID: ${start_on_demand_pod.id}') + +// start spot pod +start_spot_pod := rp.start_spot_pod( + pod_id: '${spot_pod_response.id}' + gpu_count: 1 + bid_per_gpu: 0.2 +)! +println('Started spot pod with ID: ${start_on_demand_pod.id}') diff --git a/lib/clients/runpod/client.v b/lib/clients/runpod/client.v index 7175cc8c..6fd10ebf 100644 --- a/lib/clients/runpod/client.v +++ b/lib/clients/runpod/client.v @@ -84,11 +84,17 @@ pub fn (mut rp RunPod) create_spot_pod(input PodRentInterruptableInput) !PodResu @[params] pub struct PodResume { pub mut: - pod_id string @[json: 'podId'] - gpu_count int @[json: 'gpuCount'] + pod_id string @[json: 'podId'] + gpu_count int @[json: 'gpuCount'] + bid_per_gpu f32 @[json: 'bidPerGpu'] } // Start On-Demand Pod pub fn (mut rp RunPod) start_on_demand_pod(input PodResume) !PodResult { return rp.start_on_demand_pod_request(input)! } + +// Start Spot Pod +pub fn (mut rp RunPod) start_spot_pod(input PodResume) !PodResult { + return rp.start_spot_pod_request(input)! +} diff --git a/lib/clients/runpod/runpod_http.v b/lib/clients/runpod/runpod_http.v index 8e5404cf..b5209585 100644 --- a/lib/clients/runpod/runpod_http.v +++ b/lib/clients/runpod/runpod_http.v @@ -1,21 +1,10 @@ module runpod -import freeflowuniverse.herolib.core.httpconnection -import json - -fn (mut rp RunPod) httpclient() !&httpconnection.HTTPConnection { - mut http_conn := httpconnection.new( - name: 'runpod_${rp.name}' - url: 'https://api.runpod.io' - cache: true - retry: 3 - )! - - // Add authorization header - http_conn.default_header.add(.authorization, 'Bearer ${rp.api_key}') - return http_conn -} - +// #### Internally method doing a network call to create a new on-demand pod. +// - Build the required query based pn the input sent by the user and send the request. +// - Decode the response received from the API into two objects `Data` and `Error`. +// - The data field should contains the pod details same as `PodResult` struct. +// - The error field should contain the error message. fn (mut rp RunPod) create_pod_find_and_deploy_on_demand_request(request PodFindAndDeployOnDemandRequest) !PodResult { gql := build_query( query_type: .mutation @@ -29,6 +18,11 @@ fn (mut rp RunPod) create_pod_find_and_deploy_on_demand_request(request PodFindA } } +// #### Internally method doing a network call to create a new spot pod. +// - Build the required query based pn the input sent by the user and send the request. +// - Decode the response received from the API into two objects `Data` and `Error`. +// - The data field should contains the pod details same as `PodResult` struct. +// - The error field should contain the error message. fn (mut rp RunPod) create_create_spot_pod_request(input PodRentInterruptableInput) !PodResult { gql := build_query( query_type: .mutation @@ -42,7 +36,11 @@ fn (mut rp RunPod) create_create_spot_pod_request(input PodRentInterruptableInpu } } -// Start On-Demand Pod +// #### Internally method doing a network call to start on demand pod. +// - Build the required query based pn the input sent by the user and send the request. +// - Decode the response received from the API into two objects `Data` and `Error`. +// - The data field should contains the pod details same as `PodResult` struct. +// - The error field should contain the error message. fn (mut rp RunPod) start_on_demand_pod_request(input PodResume) !PodResult { gql := build_query( query_type: .mutation @@ -55,3 +53,21 @@ fn (mut rp RunPod) start_on_demand_pod_request(input PodResume) !PodResult { return error('Could not find podRentInterruptable in response data: ${response_.data}') } } + +// #### Internally method doing a network call to start spot pod. +// - Build the required query based pn the input sent by the user and send the request. +// - Decode the response received from the API into two objects `Data` and `Error`. +// - The data field should contains the pod details same as `PodResult` struct. +// - The error field should contain the error message. +fn (mut rp RunPod) start_spot_pod_request(input PodResume) !PodResult { + gql := build_query( + query_type: .mutation + method_name: 'podBidResume' + request_model: input + response_model: PodResult{} + ) + response_ := rp.make_request[GqlResponse[PodResult]](.post, '/graphql', gql)! + return response_.data['podBidResume'] or { + return error('Could not find podRentInterruptable in response data: ${response_.data}') + } +} diff --git a/lib/clients/runpod/utils.v b/lib/clients/runpod/utils.v index 88d8b4ad..054f6407 100644 --- a/lib/clients/runpod/utils.v +++ b/lib/clients/runpod/utils.v @@ -3,9 +3,21 @@ module runpod import freeflowuniverse.herolib.core.httpconnection import json +// Represents the main structure for interacting with the RunPod API. +// Provides utilities to manage HTTP connections and perform GraphQL queries. +fn (mut rp RunPod) httpclient() !&httpconnection.HTTPConnection { + mut http_conn := httpconnection.new( + name: 'runpod_vclient_${rp.name}' + url: rp.base_url + cache: true + retry: 3 + )! + return http_conn +} + +// Retrieves the field name from the `FieldData` struct, either from its attributes or its name. fn get_field_name(field FieldData) string { mut field_name := '' - // Process attributes to fetch the JSON field name or fallback to field name if field.attrs.len > 0 { for attr in field.attrs { attrs := attr.trim_space().split(':') @@ -20,8 +32,8 @@ fn get_field_name(field FieldData) string { return field_name } +// Constructs JSON-like request fields from a struct. fn get_request_fields[T](struct_ T) string { - // Start the current level mut body_ := '{ ' mut fields := []string{} @@ -62,8 +74,8 @@ fn get_request_fields[T](struct_ T) string { return body_ } +// Constructs JSON-like response fields for a given struct. fn get_response_fields[R](struct_ R) string { - // Start the current level mut body_ := '{ ' $for field in R.fields { @@ -80,11 +92,13 @@ fn get_response_fields[R](struct_ R) string { return body_ } +// Enum representing the type of GraphQL operation. pub enum QueryType { query mutation } +// Struct for building GraphQL queries with request and response models. @[params] pub struct BuildQueryArgs[T, R] { pub: @@ -94,11 +108,8 @@ pub: response_model R @[required] } +// Builds a GraphQL query or mutation string from provided arguments. fn build_query[T, R](args BuildQueryArgs[T, R]) string { - // Convert input to JSON - // input_json := json.encode(request) - - // Build the GraphQL mutation string mut request_fields := get_request_fields(args.request_model) mut response_fields := get_response_fields(args.response_model) @@ -114,6 +125,7 @@ fn build_query[T, R](args BuildQueryArgs[T, R]) string { return json.encode(gql) } +// Converts the `QueryType` enum to its string representation. fn (q QueryType) to_string() string { return match q { .query { @@ -125,6 +137,7 @@ fn (q QueryType) to_string() string { } } +// Enum representing HTTP methods. enum HTTPMethod { get post @@ -132,6 +145,7 @@ enum HTTPMethod { delete } +// Sends an HTTP request to the RunPod API with the specified method, path, and data. fn (mut rp RunPod) make_request[T](method HTTPMethod, path string, data string) !T { mut request := httpconnection.Request{ prefix: path