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  }