summaryrefslogtreecommitdiffstats
path: root/client/go/internal/vespa/target.go
diff options
context:
space:
mode:
Diffstat (limited to 'client/go/internal/vespa/target.go')
-rw-r--r--client/go/internal/vespa/target.go159
1 files changed, 98 insertions, 61 deletions
diff --git a/client/go/internal/vespa/target.go b/client/go/internal/vespa/target.go
index 6dd64dd1275..3c65369f986 100644
--- a/client/go/internal/vespa/target.go
+++ b/client/go/internal/vespa/target.go
@@ -4,9 +4,11 @@ package vespa
import (
"crypto/tls"
+ "errors"
"fmt"
"io"
"net/http"
+ "strings"
"sync"
"time"
@@ -27,18 +29,15 @@ const (
// A hosted Vespa target
TargetHosted = "hosted"
- // A Vespa service that handles deployments, either a config server or a controller
- DeployService = "deploy"
+ // LatestDeployment waits for a deployment to converge to latest generation
+ LatestDeployment int64 = -1
- // A Vespa service that handles queries.
- QueryService = "query"
-
- // A Vespa service that handles feeding of document. This may point to the same service as QueryService.
- DocumentService = "document"
-
- retryInterval = 2 * time.Second
+ // AnyDeployment waits for a deployment to converge on any generation
+ AnyDeployment int64 = -2
)
+var errWaitTimeout = errors.New("wait timed out")
+
// Authenticator authenticates the given HTTP request.
type Authenticator interface {
Authenticate(request *http.Request) error
@@ -50,9 +49,11 @@ type Service struct {
Name string
TLSOptions TLSOptions
- once sync.Once
- auth Authenticator
- httpClient util.HTTPClient
+ deployAPI bool
+ once sync.Once
+ auth Authenticator
+ httpClient util.HTTPClient
+ retryInterval time.Duration
}
// Target represents a Vespa platform, running named Vespa services.
@@ -66,8 +67,16 @@ type Target interface {
// Deployment returns the deployment managed by this target.
Deployment() Deployment
- // Service returns the service for given name. If timeout is non-zero, wait for the service to converge.
- Service(name string, timeout time.Duration, sessionOrRunID int64, cluster string) (*Service, error)
+ // DeployService returns the service providing the deploy API on this target.
+ DeployService() (*Service, error)
+
+ // ContainerServices returns all container services of the current deployment. If timeout is positive, wait for
+ // services to be discovered.
+ ContainerServices(timeout time.Duration) ([]*Service, error)
+
+ // AwaitDeployment waits for a deployment identified by id to succeed. It returns the id that succeeded, or an
+ // error. The exact meaning of id depends on the implementation.
+ AwaitDeployment(id int64, timeout time.Duration) (int64, error)
// PrintLog writes the logs of this deployment using given options to control output.
PrintLog(options LogOptions) error
@@ -114,29 +123,63 @@ func (s *Service) Do(request *http.Request, timeout time.Duration) (*http.Respon
func (s *Service) SetClient(client util.HTTPClient) { s.httpClient = client }
// Wait polls the health check of this service until it succeeds or timeout passes.
-func (s *Service) Wait(timeout time.Duration) (int, error) {
+func (s *Service) Wait(timeout time.Duration) error {
url := s.BaseURL
- switch s.Name {
- case DeployService:
+ if s.deployAPI {
url += "/status.html" // because /ApplicationStatus is not publicly reachable in Vespa Cloud
- case QueryService, DocumentService:
+ } else {
url += "/ApplicationStatus"
- default:
- return 0, fmt.Errorf("invalid service: %s", s.Name)
}
- return waitForOK(s, url, timeout)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return err
+ }
+ okFunc := func(status int, response []byte) (bool, error) { return isOK(status) }
+ status, err := wait(s, okFunc, func() *http.Request { return req }, timeout, s.retryInterval)
+ if err != nil {
+ waitDesc := ""
+ if timeout > 0 {
+ waitDesc = " (after waiting " + timeout.String() + ") "
+ }
+ statusDesc := ""
+ if status > 0 {
+ statusDesc = fmt.Sprintf(": status %d", status)
+ }
+ return fmt.Errorf("unhealthy %s%s%s at %s: %w", s.Description(), waitDesc, statusDesc, url, err)
+ }
+ return nil
}
func (s *Service) Description() string {
- switch s.Name {
- case QueryService:
- return "Container (query API)"
- case DocumentService:
- return "Container (document API)"
- case DeployService:
- return "Deploy API"
+ if s.deployAPI {
+ return "deploy API"
+ }
+ if s.Name == "" {
+ return "container"
+ }
+ return "container " + s.Name
+}
+
+// FindService returns the service of given name, found among services, if any.
+func FindService(name string, services []*Service) (*Service, error) {
+ if name == "" && len(services) == 1 {
+ return services[0], nil
+ }
+ names := make([]string, len(services))
+ for i, s := range services {
+ if name == s.Name {
+ return s, nil
+ }
+ names[i] = s.Name
}
- return fmt.Sprintf("No description of service %s", s.Name)
+ found := "no services found"
+ if len(names) > 0 {
+ found = "known services: " + strings.Join(names, ", ")
+ }
+ if name != "" {
+ return nil, fmt.Errorf("no such service: %q: %s", name, found)
+ }
+ return nil, fmt.Errorf("no service specified: %s", found)
}
func isOK(status int) (bool, error) {
@@ -145,57 +188,48 @@ func isOK(status int) (bool, error) {
case 2: // success
return true, nil
case 4: // client error
- return false, fmt.Errorf("request failed with status %d", status)
- default: // retry
+ return false, fmt.Errorf("got status %d", status)
+ default: // retry on everything else
return false, nil
}
}
-type responseFunc func(status int, response []byte) (bool, error)
+// responseFunc returns whether a HTTP request is considered successful, based on its status and response data.
+// Returning false indicates that the operation should be retried. An error is returned if the response is considered
+// terminal and that the request should not be retried.
+type responseFunc func(status int, response []byte) (ok bool, err error)
type requestFunc func() *http.Request
-// waitForOK queries url and returns its status code. If response status is not 2xx or 4xx, it is repeatedly queried
-// until timeout elapses.
-func waitForOK(service *Service, url string, timeout time.Duration) (int, error) {
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return 0, err
- }
- okFunc := func(status int, response []byte) (bool, error) {
- ok, err := isOK(status)
- if err != nil {
- return false, fmt.Errorf("failed to query %s at %s: %w", service.Description(), url, err)
- }
- return ok, err
- }
- return wait(service, okFunc, func() *http.Request { return req }, timeout)
-}
-
-func wait(service *Service, fn responseFunc, reqFn requestFunc, timeout time.Duration) (int, error) {
+// wait queries service until one of the following conditions are satisfied:
+//
+// 1. okFn returns true or a non-nil error
+// 2. timeout is exceeded
+//
+// It returns the last received HTTP status code and error, if any.
+func wait(service *Service, okFn responseFunc, reqFn requestFunc, timeout, retryInterval time.Duration) (int, error) {
var (
- httpErr error
- response *http.Response
- statusCode int
+ status int
+ response *http.Response
+ err error
)
deadline := time.Now().Add(timeout)
loopOnce := timeout == 0
for time.Now().Before(deadline) || loopOnce {
- req := reqFn()
- response, httpErr = service.Do(req, 10*time.Second)
- if httpErr == nil {
- statusCode = response.StatusCode
+ response, err = service.Do(reqFn(), 10*time.Second)
+ if err == nil {
+ status = response.StatusCode
body, err := io.ReadAll(response.Body)
if err != nil {
return 0, err
}
response.Body.Close()
- ok, err := fn(statusCode, body)
+ ok, err := okFn(status, body)
if err != nil {
- return statusCode, err
+ return status, err
}
if ok {
- return statusCode, nil
+ return status, nil
}
}
timeLeft := time.Until(deadline)
@@ -204,5 +238,8 @@ func wait(service *Service, fn responseFunc, reqFn requestFunc, timeout time.Dur
}
time.Sleep(retryInterval)
}
- return statusCode, httpErr
+ if err == nil {
+ return status, errWaitTimeout
+ }
+ return status, err
}