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 <mariobassem12@gmail.com>
This commit is contained in:
Mahmoud Emad
2025-01-21 12:38:25 +02:00
parent 3fe350abe9
commit 50116651de
4 changed files with 73 additions and 29 deletions

View File

@@ -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}')

View File

@@ -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)!
}

View File

@@ -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}')
}
}

View File

@@ -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