github.com/weaviate/weaviate@v1.24.6/adapters/clients/replication.go (about)

     1  //                           _       _
     2  // __      _____  __ ___   ___  __ _| |_ ___
     3  // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
     4  //  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
     5  //   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
     6  //
     7  //  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
     8  //
     9  //  CONTACT: hello@weaviate.io
    10  //
    11  
    12  package clients
    13  
    14  import (
    15  	"bytes"
    16  	"context"
    17  	"encoding/base64"
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"math/rand"
    22  	"net/http"
    23  	"net/url"
    24  	"time"
    25  
    26  	"github.com/go-openapi/strfmt"
    27  	"github.com/weaviate/weaviate/adapters/handlers/rest/clusterapi"
    28  	"github.com/weaviate/weaviate/entities/additional"
    29  	"github.com/weaviate/weaviate/entities/search"
    30  	"github.com/weaviate/weaviate/entities/storobj"
    31  	"github.com/weaviate/weaviate/usecases/objects"
    32  	"github.com/weaviate/weaviate/usecases/replica"
    33  )
    34  
    35  // ReplicationClient is to coordinate operations among replicas
    36  
    37  type replicationClient retryClient
    38  
    39  func NewReplicationClient(httpClient *http.Client) replica.Client {
    40  	return &replicationClient{
    41  		client:  httpClient,
    42  		retryer: newRetryer(),
    43  	}
    44  }
    45  
    46  // FetchObject fetches one object it exits
    47  func (c *replicationClient) FetchObject(ctx context.Context, host, index,
    48  	shard string, id strfmt.UUID, selectProps search.SelectProperties,
    49  	additional additional.Properties,
    50  ) (objects.Replica, error) {
    51  	resp := objects.Replica{}
    52  	req, err := newHttpReplicaRequest(ctx, http.MethodGet, host, index, shard, "", id.String(), nil)
    53  	if err != nil {
    54  		return resp, fmt.Errorf("create http request: %w", err)
    55  	}
    56  	err = c.doCustomUnmarshal(c.timeoutUnit*20, req, nil, resp.UnmarshalBinary)
    57  	return resp, err
    58  }
    59  
    60  func (c *replicationClient) DigestObjects(ctx context.Context,
    61  	host, index, shard string, ids []strfmt.UUID,
    62  ) (result []replica.RepairResponse, err error) {
    63  	var resp []replica.RepairResponse
    64  	body, err := json.Marshal(ids)
    65  	if err != nil {
    66  		return nil, fmt.Errorf("marshal digest objects input: %w", err)
    67  	}
    68  	req, err := newHttpReplicaRequest(
    69  		ctx, http.MethodGet, host, index, shard,
    70  		"", "_digest", bytes.NewReader(body))
    71  	if err != nil {
    72  		return resp, fmt.Errorf("create http request: %w", err)
    73  	}
    74  	err = c.do(c.timeoutUnit*20, req, body, &resp)
    75  	return resp, err
    76  }
    77  
    78  func (c *replicationClient) OverwriteObjects(ctx context.Context,
    79  	host, index, shard string, vobjects []*objects.VObject,
    80  ) ([]replica.RepairResponse, error) {
    81  	var resp []replica.RepairResponse
    82  	body, err := clusterapi.IndicesPayloads.VersionedObjectList.Marshal(vobjects)
    83  	if err != nil {
    84  		return nil, fmt.Errorf("encode request: %w", err)
    85  	}
    86  	req, err := newHttpReplicaRequest(
    87  		ctx, http.MethodPut, host, index, shard,
    88  		"", "_overwrite", bytes.NewReader(body))
    89  	if err != nil {
    90  		return resp, fmt.Errorf("create http request: %w", err)
    91  	}
    92  	err = c.do(c.timeoutUnit*90, req, body, &resp)
    93  	return resp, err
    94  }
    95  
    96  func (c *replicationClient) FetchObjects(ctx context.Context, host,
    97  	index, shard string, ids []strfmt.UUID,
    98  ) ([]objects.Replica, error) {
    99  	resp := make(objects.Replicas, len(ids))
   100  	idsBytes, err := json.Marshal(ids)
   101  	if err != nil {
   102  		return nil, fmt.Errorf("marshal ids: %w", err)
   103  	}
   104  
   105  	idsEncoded := base64.StdEncoding.EncodeToString(idsBytes)
   106  
   107  	req, err := newHttpReplicaRequest(ctx, http.MethodGet, host, index, shard, "", "", nil)
   108  	if err != nil {
   109  		return nil, fmt.Errorf("create http request: %w", err)
   110  	}
   111  
   112  	req.URL.RawQuery = url.Values{"ids": []string{idsEncoded}}.Encode()
   113  	err = c.doCustomUnmarshal(c.timeoutUnit*90, req, nil, resp.UnmarshalBinary)
   114  	return resp, err
   115  }
   116  
   117  func (c *replicationClient) PutObject(ctx context.Context, host, index,
   118  	shard, requestID string, obj *storobj.Object,
   119  ) (replica.SimpleResponse, error) {
   120  	var resp replica.SimpleResponse
   121  	body, err := clusterapi.IndicesPayloads.SingleObject.Marshal(obj)
   122  	if err != nil {
   123  		return resp, fmt.Errorf("encode request: %w", err)
   124  	}
   125  
   126  	req, err := newHttpReplicaRequest(ctx, http.MethodPost, host, index, shard, requestID, "", nil)
   127  	if err != nil {
   128  		return resp, fmt.Errorf("create http request: %w", err)
   129  	}
   130  
   131  	clusterapi.IndicesPayloads.SingleObject.SetContentTypeHeaderReq(req)
   132  	err = c.do(c.timeoutUnit*90, req, body, &resp)
   133  	return resp, err
   134  }
   135  
   136  func (c *replicationClient) DeleteObject(ctx context.Context, host, index,
   137  	shard, requestID string, uuid strfmt.UUID,
   138  ) (replica.SimpleResponse, error) {
   139  	var resp replica.SimpleResponse
   140  	req, err := newHttpReplicaRequest(ctx, http.MethodDelete, host, index, shard, requestID, uuid.String(), nil)
   141  	if err != nil {
   142  		return resp, fmt.Errorf("create http request: %w", err)
   143  	}
   144  
   145  	err = c.do(c.timeoutUnit*90, req, nil, &resp)
   146  	return resp, err
   147  }
   148  
   149  func (c *replicationClient) PutObjects(ctx context.Context, host, index,
   150  	shard, requestID string, objects []*storobj.Object,
   151  ) (replica.SimpleResponse, error) {
   152  	var resp replica.SimpleResponse
   153  	body, err := clusterapi.IndicesPayloads.ObjectList.Marshal(objects)
   154  	if err != nil {
   155  		return resp, fmt.Errorf("encode request: %w", err)
   156  	}
   157  	req, err := newHttpReplicaRequest(ctx, http.MethodPost, host, index, shard, requestID, "", nil)
   158  	if err != nil {
   159  		return resp, fmt.Errorf("create http request: %w", err)
   160  	}
   161  
   162  	clusterapi.IndicesPayloads.ObjectList.SetContentTypeHeaderReq(req)
   163  	err = c.do(c.timeoutUnit*90, req, body, &resp)
   164  	return resp, err
   165  }
   166  
   167  func (c *replicationClient) MergeObject(ctx context.Context, host, index, shard, requestID string,
   168  	doc *objects.MergeDocument,
   169  ) (replica.SimpleResponse, error) {
   170  	var resp replica.SimpleResponse
   171  	body, err := clusterapi.IndicesPayloads.MergeDoc.Marshal(*doc)
   172  	if err != nil {
   173  		return resp, fmt.Errorf("encode request: %w", err)
   174  	}
   175  
   176  	req, err := newHttpReplicaRequest(ctx, http.MethodPatch, host, index, shard,
   177  		requestID, doc.ID.String(), nil)
   178  	if err != nil {
   179  		return resp, fmt.Errorf("create http request: %w", err)
   180  	}
   181  
   182  	clusterapi.IndicesPayloads.MergeDoc.SetContentTypeHeaderReq(req)
   183  	err = c.do(c.timeoutUnit*90, req, body, &resp)
   184  	return resp, err
   185  }
   186  
   187  func (c *replicationClient) AddReferences(ctx context.Context, host, index,
   188  	shard, requestID string, refs []objects.BatchReference,
   189  ) (replica.SimpleResponse, error) {
   190  	var resp replica.SimpleResponse
   191  	body, err := clusterapi.IndicesPayloads.ReferenceList.Marshal(refs)
   192  	if err != nil {
   193  		return resp, fmt.Errorf("encode request: %w", err)
   194  	}
   195  	req, err := newHttpReplicaRequest(ctx, http.MethodPost, host, index, shard,
   196  		requestID, "references", nil)
   197  	if err != nil {
   198  		return resp, fmt.Errorf("create http request: %w", err)
   199  	}
   200  
   201  	clusterapi.IndicesPayloads.ReferenceList.SetContentTypeHeaderReq(req)
   202  	err = c.do(c.timeoutUnit*90, req, body, &resp)
   203  	return resp, err
   204  }
   205  
   206  func (c *replicationClient) DeleteObjects(ctx context.Context, host, index, shard, requestID string,
   207  	uuids []strfmt.UUID, dryRun bool,
   208  ) (resp replica.SimpleResponse, err error) {
   209  	body, err := clusterapi.IndicesPayloads.BatchDeleteParams.Marshal(uuids, dryRun)
   210  	if err != nil {
   211  		return resp, fmt.Errorf("encode request: %w", err)
   212  	}
   213  	req, err := newHttpReplicaRequest(ctx, http.MethodDelete, host, index, shard, requestID, "", nil)
   214  	if err != nil {
   215  		return resp, fmt.Errorf("create http request: %w", err)
   216  	}
   217  
   218  	clusterapi.IndicesPayloads.BatchDeleteParams.SetContentTypeHeaderReq(req)
   219  	err = c.do(c.timeoutUnit*90, req, body, &resp)
   220  	return resp, err
   221  }
   222  
   223  // Commit asks a host to commit and stores the response in the value pointed to by resp
   224  func (c *replicationClient) Commit(ctx context.Context, host, index, shard string, requestID string, resp interface{}) error {
   225  	req, err := newHttpReplicaCMD(host, "commit", index, shard, requestID, nil)
   226  	if err != nil {
   227  		return fmt.Errorf("create http request: %w", err)
   228  	}
   229  
   230  	return c.do(c.timeoutUnit*90, req, nil, resp)
   231  }
   232  
   233  func (c *replicationClient) Abort(ctx context.Context, host, index, shard, requestID string) (
   234  	resp replica.SimpleResponse, err error,
   235  ) {
   236  	req, err := newHttpReplicaCMD(host, "abort", index, shard, requestID, nil)
   237  	if err != nil {
   238  		return resp, fmt.Errorf("create http request: %w", err)
   239  	}
   240  
   241  	err = c.do(c.timeoutUnit*5, req, nil, &resp)
   242  	return resp, err
   243  }
   244  
   245  func newHttpReplicaRequest(ctx context.Context, method, host, index, shard, requestId, suffix string, body io.Reader) (*http.Request, error) {
   246  	path := fmt.Sprintf("/replicas/indices/%s/shards/%s/objects", index, shard)
   247  	if suffix != "" {
   248  		path = fmt.Sprintf("%s/%s", path, suffix)
   249  	}
   250  	u := url.URL{
   251  		Scheme: "http",
   252  		Host:   host,
   253  		Path:   path,
   254  	}
   255  
   256  	if requestId != "" {
   257  		u.RawQuery = url.Values{replica.RequestKey: []string{requestId}}.Encode()
   258  	}
   259  
   260  	return http.NewRequestWithContext(ctx, method, u.String(), body)
   261  }
   262  
   263  func newHttpReplicaCMD(host, cmd, index, shard, requestId string, body io.Reader) (*http.Request, error) {
   264  	path := fmt.Sprintf("/replicas/indices/%s/shards/%s:%s", index, shard, cmd)
   265  	q := url.Values{replica.RequestKey: []string{requestId}}.Encode()
   266  	url := url.URL{Scheme: "http", Host: host, Path: path, RawQuery: q}
   267  	return http.NewRequest(http.MethodPost, url.String(), body)
   268  }
   269  
   270  func (c *replicationClient) do(timeout time.Duration, req *http.Request, body []byte, resp interface{}) (err error) {
   271  	ctx, cancel := context.WithTimeout(req.Context(), timeout)
   272  	defer cancel()
   273  	try := func(ctx context.Context) (bool, error) {
   274  		if body != nil {
   275  			req.Body = io.NopCloser(bytes.NewReader(body))
   276  		}
   277  		res, err := c.client.Do(req)
   278  		if err != nil {
   279  			return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
   280  		}
   281  		defer res.Body.Close()
   282  
   283  		if code := res.StatusCode; code != http.StatusOK {
   284  			b, _ := io.ReadAll(res.Body)
   285  			return shouldRetry(code), fmt.Errorf("status code: %v, error: %s", code, b)
   286  		}
   287  		if err := json.NewDecoder(res.Body).Decode(resp); err != nil {
   288  			return false, fmt.Errorf("decode response: %w", err)
   289  		}
   290  		return false, nil
   291  	}
   292  	return c.retry(ctx, 9, try)
   293  }
   294  
   295  func (c *replicationClient) doCustomUnmarshal(timeout time.Duration,
   296  	req *http.Request, body []byte, decode func([]byte) error,
   297  ) (err error) {
   298  	return (*retryClient)(c).doWithCustomMarshaller(timeout, req, body, decode)
   299  }
   300  
   301  // backOff return a new random duration in the interval [d, 3d].
   302  // It implements truncated exponential back-off with introduced jitter.
   303  func backOff(d time.Duration) time.Duration {
   304  	return time.Duration(float64(d.Nanoseconds()*2) * (0.5 + rand.Float64()))
   305  }
   306  
   307  func shouldRetry(code int) bool {
   308  	return code == http.StatusInternalServerError ||
   309  		code == http.StatusTooManyRequests ||
   310  		code == http.StatusServiceUnavailable
   311  }