go.temporal.io/server@v1.23.0/common/archiver/s3store/util.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package s3store
    26  
    27  import (
    28  	"bytes"
    29  	"context"
    30  	"encoding/json"
    31  	"fmt"
    32  	"io"
    33  	"strings"
    34  	"time"
    35  
    36  	"github.com/aws/aws-sdk-go/aws"
    37  	"github.com/aws/aws-sdk-go/aws/awserr"
    38  	"github.com/aws/aws-sdk-go/service/s3"
    39  	"github.com/aws/aws-sdk-go/service/s3/s3iface"
    40  	commonpb "go.temporal.io/api/common/v1"
    41  	historypb "go.temporal.io/api/history/v1"
    42  	"go.temporal.io/api/serviceerror"
    43  	workflowpb "go.temporal.io/api/workflow/v1"
    44  	"go.uber.org/multierr"
    45  	"google.golang.org/protobuf/proto"
    46  
    47  	archiverspb "go.temporal.io/server/api/archiver/v1"
    48  	"go.temporal.io/server/common/archiver"
    49  	"go.temporal.io/server/common/codec"
    50  	"go.temporal.io/server/common/searchattribute"
    51  )
    52  
    53  // encoding & decoding util
    54  
    55  func Encode(message proto.Message) ([]byte, error) {
    56  	encoder := codec.NewJSONPBEncoder()
    57  	return encoder.Encode(message)
    58  }
    59  
    60  func decodeVisibilityRecord(data []byte) (*archiverspb.VisibilityRecord, error) {
    61  	record := &archiverspb.VisibilityRecord{}
    62  	encoder := codec.NewJSONPBEncoder()
    63  	err := encoder.Decode(data, record)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	return record, nil
    68  }
    69  
    70  func SerializeToken(token interface{}) ([]byte, error) {
    71  	if token == nil {
    72  		return nil, nil
    73  	}
    74  	return json.Marshal(token)
    75  }
    76  
    77  func deserializeGetHistoryToken(bytes []byte) (*getHistoryToken, error) {
    78  	token := &getHistoryToken{}
    79  	err := json.Unmarshal(bytes, token)
    80  	return token, err
    81  }
    82  
    83  func deserializeQueryVisibilityToken(bytes []byte) *string {
    84  	var ret = string(bytes)
    85  	return &ret
    86  }
    87  func serializeQueryVisibilityToken(token string) []byte {
    88  	return []byte(token)
    89  }
    90  
    91  // Only validates the scheme and buckets are passed
    92  func SoftValidateURI(URI archiver.URI) error {
    93  	if URI.Scheme() != URIScheme {
    94  		return archiver.ErrURISchemeMismatch
    95  	}
    96  	if len(URI.Hostname()) == 0 {
    97  		return errNoBucketSpecified
    98  	}
    99  	return nil
   100  }
   101  
   102  func BucketExists(ctx context.Context, s3cli s3iface.S3API, URI archiver.URI) error {
   103  	ctx, cancel := ensureContextTimeout(ctx)
   104  	defer cancel()
   105  	_, err := s3cli.HeadBucketWithContext(ctx, &s3.HeadBucketInput{
   106  		Bucket: aws.String(URI.Hostname()),
   107  	})
   108  	if err == nil {
   109  		return nil
   110  	}
   111  	if IsNotFoundError(err) {
   112  		return errBucketNotExists
   113  	}
   114  	return err
   115  }
   116  
   117  func KeyExists(ctx context.Context, s3cli s3iface.S3API, URI archiver.URI, key string) (bool, error) {
   118  	ctx, cancel := ensureContextTimeout(ctx)
   119  	defer cancel()
   120  	_, err := s3cli.HeadObjectWithContext(ctx, &s3.HeadObjectInput{
   121  		Bucket: aws.String(URI.Hostname()),
   122  		Key:    aws.String(key),
   123  	})
   124  	if err != nil {
   125  		if IsNotFoundError(err) {
   126  			return false, nil
   127  		}
   128  		return false, err
   129  	}
   130  	return true, nil
   131  }
   132  
   133  func IsNotFoundError(err error) bool {
   134  	aerr, ok := err.(awserr.Error)
   135  	return ok && (aerr.Code() == "NotFound")
   136  }
   137  
   138  // Key construction
   139  func constructHistoryKey(path, namespaceID, workflowID, runID string, version int64, batchIdx int) string {
   140  	prefix := constructHistoryKeyPrefixWithVersion(path, namespaceID, workflowID, runID, version)
   141  	return fmt.Sprintf("%s%d", prefix, batchIdx)
   142  }
   143  
   144  func constructHistoryKeyPrefixWithVersion(path, namespaceID, workflowID, runID string, version int64) string {
   145  	prefix := constructHistoryKeyPrefix(path, namespaceID, workflowID, runID)
   146  	return fmt.Sprintf("%s/%v/", prefix, version)
   147  }
   148  
   149  func constructHistoryKeyPrefix(path, namespaceID, workflowID, runID string) string {
   150  	return strings.TrimLeft(strings.Join([]string{path, namespaceID, "history", workflowID, runID}, "/"), "/")
   151  }
   152  
   153  func constructTimeBasedSearchKey(path, namespaceID, primaryIndexKey, primaryIndexValue, secondaryIndexKey string, t time.Time, precision string) string {
   154  	var timeFormat = ""
   155  	switch precision {
   156  	case PrecisionSecond:
   157  		timeFormat = ":05"
   158  		fallthrough
   159  	case PrecisionMinute:
   160  		timeFormat = ":04" + timeFormat
   161  		fallthrough
   162  	case PrecisionHour:
   163  		timeFormat = "15" + timeFormat
   164  		fallthrough
   165  	case PrecisionDay:
   166  		timeFormat = "2006-01-02T" + timeFormat
   167  	}
   168  
   169  	return fmt.Sprintf(
   170  		"%s/%s",
   171  		constructIndexedVisibilitySearchPrefix(path, namespaceID, primaryIndexKey, primaryIndexValue, secondaryIndexKey),
   172  		t.Format(timeFormat),
   173  	)
   174  }
   175  
   176  func constructTimestampIndex(path, namespaceID, primaryIndexKey, primaryIndexValue, secondaryIndexKey string, secondaryIndexValue time.Time, runID string) string {
   177  	return fmt.Sprintf(
   178  		"%s/%s/%s",
   179  		constructIndexedVisibilitySearchPrefix(path, namespaceID, primaryIndexKey, primaryIndexValue, secondaryIndexKey),
   180  		secondaryIndexValue.Format(time.RFC3339),
   181  		runID,
   182  	)
   183  }
   184  
   185  func constructIndexedVisibilitySearchPrefix(
   186  	path string,
   187  	namespaceID string,
   188  	primaryIndexKey string,
   189  	primaryIndexValue string,
   190  	secondaryIndexType string,
   191  ) string {
   192  	return strings.TrimLeft(
   193  		strings.Join(
   194  			[]string{path, namespaceID, "visibility", primaryIndexKey, primaryIndexValue, secondaryIndexType},
   195  			"/",
   196  		),
   197  		"/",
   198  	)
   199  }
   200  
   201  func constructVisibilitySearchPrefix(path, namespaceID string) string {
   202  	return strings.TrimLeft(strings.Join([]string{path, namespaceID, "visibility"}, "/"), "/")
   203  }
   204  
   205  func ensureContextTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
   206  	if _, ok := ctx.Deadline(); ok {
   207  		return ctx, func() {}
   208  	}
   209  	return context.WithTimeout(ctx, defaultBlobstoreTimeout)
   210  }
   211  func Upload(ctx context.Context, s3cli s3iface.S3API, URI archiver.URI, key string, data []byte) error {
   212  	ctx, cancel := ensureContextTimeout(ctx)
   213  	defer cancel()
   214  
   215  	_, err := s3cli.PutObjectWithContext(ctx, &s3.PutObjectInput{
   216  		Bucket: aws.String(URI.Hostname()),
   217  		Key:    aws.String(key),
   218  		Body:   bytes.NewReader(data),
   219  	})
   220  	if err != nil {
   221  		if aerr, ok := err.(awserr.Error); ok {
   222  			if aerr.Code() == s3.ErrCodeNoSuchBucket {
   223  				return serviceerror.NewInvalidArgument(errBucketNotExists.Error())
   224  			}
   225  		}
   226  		return err
   227  	}
   228  	return nil
   229  }
   230  
   231  func Download(ctx context.Context, s3cli s3iface.S3API, URI archiver.URI, key string) ([]byte, error) {
   232  	ctx, cancel := ensureContextTimeout(ctx)
   233  	defer cancel()
   234  	result, err := s3cli.GetObjectWithContext(ctx, &s3.GetObjectInput{
   235  		Bucket: aws.String(URI.Hostname()),
   236  		Key:    aws.String(key),
   237  	})
   238  
   239  	if err != nil {
   240  		if aerr, ok := err.(awserr.Error); ok {
   241  			if aerr.Code() == s3.ErrCodeNoSuchBucket {
   242  				return nil, serviceerror.NewInvalidArgument(errBucketNotExists.Error())
   243  			}
   244  
   245  			if aerr.Code() == s3.ErrCodeNoSuchKey {
   246  				return nil, serviceerror.NewNotFound(archiver.ErrHistoryNotExist.Error())
   247  			}
   248  		}
   249  		return nil, err
   250  	}
   251  
   252  	defer func() {
   253  		if ierr := result.Body.Close(); ierr != nil {
   254  			err = multierr.Append(err, ierr)
   255  		}
   256  	}()
   257  
   258  	body, err := io.ReadAll(result.Body)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	return body, nil
   263  }
   264  
   265  func historyMutated(request *archiver.ArchiveHistoryRequest, historyBatches []*historypb.History, isLast bool) bool {
   266  	lastBatch := historyBatches[len(historyBatches)-1].Events
   267  	lastEvent := lastBatch[len(lastBatch)-1]
   268  	lastFailoverVersion := lastEvent.GetVersion()
   269  	if lastFailoverVersion > request.CloseFailoverVersion {
   270  		return true
   271  	}
   272  
   273  	if !isLast {
   274  		return false
   275  	}
   276  	lastEventID := lastEvent.GetEventId()
   277  	return lastFailoverVersion != request.CloseFailoverVersion || lastEventID+1 != request.NextEventID
   278  }
   279  
   280  func convertToExecutionInfo(record *archiverspb.VisibilityRecord, saTypeMap searchattribute.NameTypeMap) (*workflowpb.WorkflowExecutionInfo, error) {
   281  	searchAttributes, err := searchattribute.Parse(record.SearchAttributes, &saTypeMap)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  
   286  	return &workflowpb.WorkflowExecutionInfo{
   287  		Execution: &commonpb.WorkflowExecution{
   288  			WorkflowId: record.GetWorkflowId(),
   289  			RunId:      record.GetRunId(),
   290  		},
   291  		Type: &commonpb.WorkflowType{
   292  			Name: record.WorkflowTypeName,
   293  		},
   294  		StartTime:        record.StartTime,
   295  		ExecutionTime:    record.ExecutionTime,
   296  		CloseTime:        record.CloseTime,
   297  		Status:           record.Status,
   298  		HistoryLength:    record.HistoryLength,
   299  		Memo:             record.Memo,
   300  		SearchAttributes: searchAttributes,
   301  	}, nil
   302  }