github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/pkg/requester/publicapi/client.go (about) 1 package publicapi 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/filecoin-project/bacalhau/pkg/bacerrors" 11 "github.com/filecoin-project/bacalhau/pkg/job" 12 "github.com/filecoin-project/bacalhau/pkg/model" 13 "github.com/filecoin-project/bacalhau/pkg/publicapi" 14 "github.com/filecoin-project/bacalhau/pkg/system" 15 "github.com/rs/zerolog/log" 16 ) 17 18 // APIRetryCount - for some queries (like read events and read state) 19 // we want to fail early (10 seconds should be ample time) 20 // but retry a number of times - this is to avoid network 21 // flakes failing the canary 22 const APIRetryCount = 5 23 const APIShortTimeoutSeconds = 10 24 25 // RequesterAPIClient is a utility for interacting with a node's API server. 26 type RequesterAPIClient struct { 27 publicapi.APIClient 28 } 29 30 // NewRequesterAPIClient returns a new client for a node's API server. 31 func NewRequesterAPIClient(baseURI string) *RequesterAPIClient { 32 return NewRequesterAPIClientFromClient(publicapi.NewAPIClient(baseURI)) 33 } 34 35 // NewRequesterAPIClientFromClient returns a new client for a node's API server. 36 func NewRequesterAPIClientFromClient(baseClient *publicapi.APIClient) *RequesterAPIClient { 37 return &RequesterAPIClient{ 38 APIClient: *baseClient, 39 } 40 } 41 42 // List returns the list of jobs in the node's transport. 43 func (apiClient *RequesterAPIClient) List( 44 ctx context.Context, 45 idFilter string, 46 includeTags []model.IncludedTag, 47 excludeTags []model.ExcludedTag, 48 maxJobs int, 49 returnAll bool, 50 sortBy string, 51 sortReverse bool, 52 ) ( 53 []*model.JobWithInfo, error) { 54 ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/requester/publicapi.RequesterAPIClient.List") 55 defer span.End() 56 57 req := listRequest{ 58 ClientID: system.GetClientID(), 59 MaxJobs: maxJobs, 60 JobID: idFilter, 61 IncludeTags: includeTags, 62 ExcludeTags: excludeTags, 63 ReturnAll: returnAll, 64 SortBy: sortBy, 65 SortReverse: sortReverse, 66 } 67 68 var res listResponse 69 if err := apiClient.Post(ctx, APIPrefix+"list", req, &res); err != nil { 70 e := err 71 return nil, e 72 } 73 74 return res.Jobs, nil 75 } 76 77 // Cancel will request that the job with the specified ID is stopped. The JobInfo will be returned if the cancel 78 // was submitted. If no match is found, Cancel returns false with a nil error. 79 func (apiClient *RequesterAPIClient) Cancel(ctx context.Context, jobID string, reason string) (*model.JobState, error) { 80 ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/requester/publicapi.RequesterAPIClient.Cancel") 81 defer span.End() 82 83 if jobID == "" { 84 return &model.JobState{}, fmt.Errorf("jobID must be non-empty in a Cancel call") 85 } 86 87 // Check the existence of a job with the provided ID, whether it is a short or long ID. 88 jobInfo, found, err := apiClient.Get(ctx, jobID) 89 if err != nil { 90 return &model.JobState{}, err 91 } 92 if !found { 93 return &model.JobState{}, bacerrors.NewJobNotFound(jobID) 94 } 95 96 // We potentially used the short jobID which `Get` supports and so let's switch 97 // to use the longer version. 98 jobID = jobInfo.State.JobID 99 100 // Create a payload before signing it with our local key (for verification on the 101 // server). 102 payload := model.JobCancelPayload{ 103 ClientID: system.GetClientID(), 104 JobID: jobID, 105 Reason: reason, 106 } 107 108 jsonData, err := model.JSONMarshalWithMax(payload) 109 if err != nil { 110 return &model.JobState{}, err 111 } 112 rawPayloadJSON := json.RawMessage(jsonData) 113 log.Ctx(ctx).Trace().RawJSON("json", rawPayloadJSON).Msgf("jsonRaw") 114 115 // sign the raw bytes representation of model.JobCreatePayload 116 signature, err := system.SignForClient(rawPayloadJSON) 117 if err != nil { 118 return &model.JobState{}, err 119 } 120 log.Ctx(ctx).Trace().Str("signature", signature).Msgf("signature") 121 122 req := cancelRequest{ 123 JobCancelPayload: &rawPayloadJSON, 124 ClientSignature: signature, 125 ClientPublicKey: system.GetClientPublicKey(), 126 } 127 128 var res cancelResponse 129 if err := apiClient.Post(ctx, APIPrefix+"cancel", req, &res); err != nil { 130 return &model.JobState{}, err 131 } 132 133 return res.State, nil 134 } 135 136 // Get returns job data for a particular job ID. If no match is found, Get returns false with a nil error. 137 func (apiClient *RequesterAPIClient) Get(ctx context.Context, jobID string) (*model.JobWithInfo, bool, error) { 138 ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/requester/publicapi.RequesterAPIClient.Get") 139 defer span.End() 140 141 if jobID == "" { 142 return &model.JobWithInfo{}, false, fmt.Errorf("jobID must be non-empty in a Get call") 143 } 144 145 jobsList, err := apiClient.List(ctx, jobID, model.IncludeAny, model.ExcludeNone, 1, false, "created_at", true) 146 if err != nil { 147 return &model.JobWithInfo{}, false, err 148 } 149 150 if len(jobsList) > 0 { 151 return jobsList[0], true, nil 152 } else { 153 return &model.JobWithInfo{}, false, bacerrors.NewJobNotFound(jobID) 154 } 155 } 156 157 func (apiClient *RequesterAPIClient) GetJobState(ctx context.Context, jobID string) (model.JobState, error) { 158 ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/requester/publicapi.RequesterAPIClient.GetJobState") 159 defer span.End() 160 161 if jobID == "" { 162 return model.JobState{}, fmt.Errorf("jobID must be non-empty in a GetJobStates call") 163 } 164 165 req := stateRequest{ 166 ClientID: system.GetClientID(), 167 JobID: jobID, 168 } 169 170 var res stateResponse 171 var outerErr error 172 173 for i := 0; i < APIRetryCount; i++ { 174 shortTimeoutCtx, cancelFn := context.WithTimeout(ctx, time.Second*APIShortTimeoutSeconds) 175 defer cancelFn() 176 err := apiClient.Post(shortTimeoutCtx, APIPrefix+"states", req, &res) 177 if err == nil { 178 return res.State, nil 179 } else { 180 log.Ctx(ctx).Debug().Err(err).Msg("apiclient read state error") 181 outerErr = err 182 } 183 } 184 return model.JobState{}, outerErr 185 } 186 187 func (apiClient *RequesterAPIClient) GetJobStateResolver() *job.StateResolver { 188 jobLoader := func(ctx context.Context, jobID string) (model.Job, error) { 189 j, _, err := apiClient.Get(ctx, jobID) 190 if err != nil { 191 return model.Job{}, fmt.Errorf("failed to load job %s: %w", jobID, err) 192 } 193 return j.Job, err 194 } 195 stateLoader := func(ctx context.Context, jobID string) (model.JobState, error) { 196 return apiClient.GetJobState(ctx, jobID) 197 } 198 return job.NewStateResolver(jobLoader, stateLoader) 199 } 200 201 func (apiClient *RequesterAPIClient) GetEvents(ctx context.Context, jobID string) (events []model.JobHistory, err error) { 202 ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/requester/publicapi.RequesterAPIClient.GetEvents") 203 defer span.End() 204 205 if jobID == "" { 206 return nil, fmt.Errorf("jobID must be non-empty in a GetEvents call") 207 } 208 209 req := eventsRequest{ 210 ClientID: system.GetClientID(), 211 JobID: jobID, 212 } 213 214 // Test if the context has been canceled before making the request. 215 var res eventsResponse 216 var outerErr error 217 218 for i := 0; i < APIRetryCount; i++ { 219 shortTimeoutCtx, cancelFn := context.WithTimeout(ctx, time.Second*APIShortTimeoutSeconds) 220 defer cancelFn() 221 err = apiClient.Post(shortTimeoutCtx, APIPrefix+"events", req, &res) 222 if err == nil { 223 return res.Events, nil 224 } else { 225 log.Ctx(ctx).Debug().Err(err).Msg("apiclient read events error") 226 outerErr = err 227 if strings.Contains(err.Error(), "context canceled") { 228 outerErr = bacerrors.NewContextCanceledError(ctx.Err().Error()) 229 } 230 } 231 } 232 return nil, outerErr 233 } 234 235 func (apiClient *RequesterAPIClient) GetResults(ctx context.Context, jobID string) (results []model.PublishedResult, err error) { 236 ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/requester/publicapi.RequesterAPIClient.GetResults") 237 defer span.End() 238 239 if jobID == "" { 240 return nil, fmt.Errorf("jobID must be non-empty in a GetResults call") 241 } 242 243 req := resultsRequest{ 244 ClientID: system.GetClientID(), 245 JobID: jobID, 246 } 247 248 var res resultsResponse 249 if err := apiClient.Post(ctx, APIPrefix+"results", req, &res); err != nil { 250 return nil, err 251 } 252 253 return res.Results, nil 254 } 255 256 // Submit submits a new job to the node's transport. 257 func (apiClient *RequesterAPIClient) Submit( 258 ctx context.Context, 259 j *model.Job, 260 ) (*model.Job, error) { 261 ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/requester/publicapi.RequesterAPIClient.Submit") 262 defer span.End() 263 264 data := model.JobCreatePayload{ 265 ClientID: system.GetClientID(), 266 APIVersion: j.APIVersion, 267 Spec: &j.Spec, 268 } 269 270 jsonData, err := model.JSONMarshalWithMax(data) 271 if err != nil { 272 return &model.Job{}, err 273 } 274 jsonRaw := json.RawMessage(jsonData) 275 log.Ctx(ctx).Trace().RawJSON("json", jsonRaw).Msgf("jsonRaw") 276 277 // sign the raw bytes representation of model.JobCreatePayload 278 signature, err := system.SignForClient(jsonRaw) 279 if err != nil { 280 return &model.Job{}, err 281 } 282 log.Ctx(ctx).Trace().Str("signature", signature).Msgf("signature") 283 284 var res submitResponse 285 req := submitRequest{ 286 JobCreatePayload: &jsonRaw, 287 ClientSignature: signature, 288 ClientPublicKey: system.GetClientPublicKey(), 289 } 290 291 err = apiClient.Post(ctx, APIPrefix+"submit", req, &res) 292 if err != nil { 293 return &model.Job{}, err 294 } 295 296 return res.Job, nil 297 } 298 299 func (apiClient *RequesterAPIClient) Debug(ctx context.Context) (map[string]model.DebugInfo, error) { 300 ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/requester/publicapi.RequesterAPIClient.Debug") 301 defer span.End() 302 303 req := struct{}{} 304 var res map[string]model.DebugInfo 305 if err := apiClient.Post(ctx, APIPrefix+"debug", req, &res); err != nil { 306 return res, err 307 } 308 309 return res, nil 310 }