go.temporal.io/server@v1.23.0/common/archiver/gcloud/visibility_archiver.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 gcloud
    26  
    27  import (
    28  	"context"
    29  	"errors"
    30  	"fmt"
    31  	"path/filepath"
    32  	"strings"
    33  	"time"
    34  
    35  	"go.temporal.io/api/serviceerror"
    36  
    37  	archiverspb "go.temporal.io/server/api/archiver/v1"
    38  	"go.temporal.io/server/common/archiver"
    39  	"go.temporal.io/server/common/archiver/gcloud/connector"
    40  	"go.temporal.io/server/common/config"
    41  	"go.temporal.io/server/common/log/tag"
    42  	"go.temporal.io/server/common/metrics"
    43  	"go.temporal.io/server/common/searchattribute"
    44  )
    45  
    46  const (
    47  	errEncodeVisibilityRecord = "failed to encode visibility record"
    48  	indexKeyStartTimeout      = "startTimeout"
    49  	indexKeyCloseTimeout      = "closeTimeout"
    50  	timeoutInSeconds          = 5
    51  )
    52  
    53  var (
    54  	errRetryable = errors.New("retryable error")
    55  )
    56  
    57  type (
    58  	visibilityArchiver struct {
    59  		container     *archiver.VisibilityBootstrapContainer
    60  		gcloudStorage connector.Client
    61  		queryParser   QueryParser
    62  	}
    63  
    64  	queryVisibilityToken struct {
    65  		Offset int
    66  	}
    67  
    68  	queryVisibilityRequest struct {
    69  		namespaceID   string
    70  		pageSize      int
    71  		nextPageToken []byte
    72  		parsedQuery   *parsedQuery
    73  	}
    74  )
    75  
    76  func newVisibilityArchiver(container *archiver.VisibilityBootstrapContainer, storage connector.Client) *visibilityArchiver {
    77  	return &visibilityArchiver{
    78  		container:     container,
    79  		gcloudStorage: storage,
    80  		queryParser:   NewQueryParser(),
    81  	}
    82  }
    83  
    84  // NewVisibilityArchiver creates a new archiver.VisibilityArchiver based on filestore
    85  func NewVisibilityArchiver(container *archiver.VisibilityBootstrapContainer, config *config.GstorageArchiver) (archiver.VisibilityArchiver, error) {
    86  	storage, err := connector.NewClient(context.Background(), config)
    87  	return newVisibilityArchiver(container, storage), err
    88  }
    89  
    90  // Archive is used to archive one workflow visibility record.
    91  // Check the Archive() method of the HistoryArchiver interface in Step 2 for parameters' meaning and requirements.
    92  // The only difference is that the ArchiveOption parameter won't include an option for recording process.
    93  // Please make sure your implementation is lossless. If any in-memory batching mechanism is used, then those batched records will be lost during server restarts.
    94  // This method will be invoked when workflow closes. Note that because of conflict resolution, it is possible for a workflow to through the closing process multiple times, which means that this method can be invoked more than once after a workflow closes.
    95  func (v *visibilityArchiver) Archive(ctx context.Context, URI archiver.URI, request *archiverspb.VisibilityRecord, opts ...archiver.ArchiveOption) (err error) {
    96  	handler := v.container.MetricsHandler.WithTags(metrics.OperationTag(metrics.HistoryArchiverScope), metrics.NamespaceTag(request.Namespace))
    97  	featureCatalog := archiver.GetFeatureCatalog(opts...)
    98  	startTime := time.Now().UTC()
    99  	defer func() {
   100  		handler.Timer(metrics.ServiceLatency.Name()).Record(time.Since(startTime))
   101  		if err != nil {
   102  			if isRetryableError(err) {
   103  				handler.Counter(metrics.VisibilityArchiverArchiveTransientErrorCount.Name()).Record(1)
   104  			} else {
   105  				handler.Counter(metrics.VisibilityArchiverArchiveNonRetryableErrorCount.Name()).Record(1)
   106  				if featureCatalog.NonRetryableError != nil {
   107  					err = featureCatalog.NonRetryableError()
   108  				}
   109  			}
   110  		}
   111  	}()
   112  
   113  	logger := archiver.TagLoggerWithArchiveVisibilityRequestAndURI(v.container.Logger, request, URI.String())
   114  
   115  	if err := v.ValidateURI(URI); err != nil {
   116  		if isRetryableError(err) {
   117  			logger.Error(archiver.ArchiveTransientErrorMsg, tag.ArchivalArchiveFailReason(archiver.ErrReasonInvalidURI), tag.Error(err))
   118  			return err
   119  		}
   120  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(archiver.ErrReasonInvalidURI), tag.Error(err))
   121  		return err
   122  	}
   123  
   124  	if err := archiver.ValidateVisibilityArchivalRequest(request); err != nil {
   125  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(archiver.ErrReasonInvalidArchiveRequest), tag.Error(err))
   126  		return err
   127  	}
   128  
   129  	encodedVisibilityRecord, err := encode(request)
   130  	if err != nil {
   131  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(errEncodeVisibilityRecord), tag.Error(err))
   132  		return err
   133  	}
   134  
   135  	// The filename has the format: closeTimestamp_hash(runID).visibility
   136  	// This format allows the archiver to sort all records without reading the file contents
   137  	filename := constructVisibilityFilename(request.GetNamespaceId(), request.WorkflowTypeName, request.GetWorkflowId(), request.GetRunId(), indexKeyCloseTimeout, request.CloseTime.AsTime())
   138  	if err := v.gcloudStorage.Upload(ctx, URI, filename, encodedVisibilityRecord); err != nil {
   139  		logger.Error(archiver.ArchiveTransientErrorMsg, tag.ArchivalArchiveFailReason(errWriteFile), tag.Error(err))
   140  		return errRetryable
   141  	}
   142  
   143  	filename = constructVisibilityFilename(request.GetNamespaceId(), request.WorkflowTypeName, request.GetWorkflowId(), request.GetRunId(), indexKeyStartTimeout, request.StartTime.AsTime())
   144  	if err := v.gcloudStorage.Upload(ctx, URI, filename, encodedVisibilityRecord); err != nil {
   145  		logger.Error(archiver.ArchiveTransientErrorMsg, tag.ArchivalArchiveFailReason(errWriteFile), tag.Error(err))
   146  		return errRetryable
   147  	}
   148  
   149  	handler.Counter(metrics.VisibilityArchiveSuccessCount.Name()).Record(1)
   150  	return nil
   151  }
   152  
   153  // Query is used to retrieve archived visibility records.
   154  // Check the Get() method of the HistoryArchiver interface in Step 2 for parameters' meaning and requirements.
   155  // The request includes a string field called query, which describes what kind of visibility records should be returned. For example, it can be some SQL-like syntax query string.
   156  // Your implementation is responsible for parsing and validating the query, and also returning all visibility records that match the query.
   157  // Currently the maximum context timeout passed into the method is 3 minutes, so it's ok if this method takes a long time to run.
   158  func (v *visibilityArchiver) Query(
   159  	ctx context.Context,
   160  	URI archiver.URI,
   161  	request *archiver.QueryVisibilityRequest,
   162  	saTypeMap searchattribute.NameTypeMap,
   163  ) (*archiver.QueryVisibilityResponse, error) {
   164  
   165  	if err := v.ValidateURI(URI); err != nil {
   166  		return nil, &serviceerror.InvalidArgument{Message: archiver.ErrInvalidURI.Error()}
   167  	}
   168  
   169  	if err := archiver.ValidateQueryRequest(request); err != nil {
   170  		return nil, &serviceerror.InvalidArgument{Message: archiver.ErrInvalidQueryVisibilityRequest.Error()}
   171  	}
   172  
   173  	if strings.TrimSpace(request.Query) == "" {
   174  		return v.queryAll(ctx, URI, request, saTypeMap)
   175  	}
   176  
   177  	parsedQuery, err := v.queryParser.Parse(request.Query)
   178  	if err != nil {
   179  		return nil, &serviceerror.InvalidArgument{Message: err.Error()}
   180  	}
   181  
   182  	if parsedQuery.emptyResult {
   183  		return &archiver.QueryVisibilityResponse{}, nil
   184  	}
   185  
   186  	return v.query(
   187  		ctx,
   188  		URI,
   189  		&queryVisibilityRequest{
   190  			namespaceID:   request.NamespaceID,
   191  			pageSize:      request.PageSize,
   192  			nextPageToken: request.NextPageToken,
   193  			parsedQuery:   parsedQuery,
   194  		},
   195  		saTypeMap,
   196  	)
   197  }
   198  
   199  func (v *visibilityArchiver) query(
   200  	ctx context.Context,
   201  	uri archiver.URI,
   202  	request *queryVisibilityRequest,
   203  	saTypeMap searchattribute.NameTypeMap,
   204  ) (*archiver.QueryVisibilityResponse, error) {
   205  	prefix := constructVisibilityFilenamePrefix(request.namespaceID, indexKeyCloseTimeout)
   206  	if !request.parsedQuery.closeTime.IsZero() {
   207  		prefix = constructTimeBasedSearchKey(
   208  			request.namespaceID,
   209  			indexKeyCloseTimeout,
   210  			request.parsedQuery.closeTime,
   211  			*request.parsedQuery.searchPrecision,
   212  		)
   213  	}
   214  
   215  	if !request.parsedQuery.startTime.IsZero() {
   216  		prefix = constructTimeBasedSearchKey(
   217  			request.namespaceID,
   218  			indexKeyStartTimeout,
   219  			request.parsedQuery.startTime,
   220  			*request.parsedQuery.searchPrecision,
   221  		)
   222  	}
   223  
   224  	return v.queryPrefix(ctx, uri, request, saTypeMap, prefix)
   225  }
   226  
   227  func (v *visibilityArchiver) queryAll(
   228  	ctx context.Context,
   229  	URI archiver.URI,
   230  	request *archiver.QueryVisibilityRequest,
   231  	saTypeMap searchattribute.NameTypeMap,
   232  ) (*archiver.QueryVisibilityResponse, error) {
   233  
   234  	return v.queryPrefix(ctx, URI, &queryVisibilityRequest{
   235  		namespaceID:   request.NamespaceID,
   236  		pageSize:      request.PageSize,
   237  		nextPageToken: request.NextPageToken,
   238  		parsedQuery:   &parsedQuery{},
   239  	}, saTypeMap, request.NamespaceID)
   240  }
   241  
   242  func (v *visibilityArchiver) queryPrefix(ctx context.Context, uri archiver.URI, request *queryVisibilityRequest, saTypeMap searchattribute.NameTypeMap, prefix string) (*archiver.QueryVisibilityResponse, error) {
   243  	token, err := v.parseToken(request.nextPageToken)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	filters := make([]connector.Precondition, 0)
   249  	if request.parsedQuery.workflowID != nil {
   250  		filters = append(filters, newWorkflowIDPrecondition(hash(*request.parsedQuery.workflowID)))
   251  	}
   252  
   253  	if request.parsedQuery.runID != nil {
   254  		filters = append(filters, newWorkflowIDPrecondition(hash(*request.parsedQuery.runID)))
   255  	}
   256  
   257  	if request.parsedQuery.workflowType != nil {
   258  		filters = append(filters, newWorkflowIDPrecondition(hash(*request.parsedQuery.workflowType)))
   259  	}
   260  
   261  	filenames, completed, currentCursorPos, err := v.gcloudStorage.QueryWithFilters(ctx, uri, prefix, request.pageSize, token.Offset, filters)
   262  	if err != nil {
   263  		return nil, &serviceerror.InvalidArgument{Message: err.Error()}
   264  	}
   265  
   266  	response := &archiver.QueryVisibilityResponse{}
   267  	for _, file := range filenames {
   268  		encodedRecord, err := v.gcloudStorage.Get(ctx, uri, fmt.Sprintf("%s/%s", request.namespaceID, filepath.Base(file)))
   269  		if err != nil {
   270  			return nil, &serviceerror.InvalidArgument{Message: err.Error()}
   271  		}
   272  
   273  		record, err := decodeVisibilityRecord(encodedRecord)
   274  		if err != nil {
   275  			return nil, &serviceerror.InvalidArgument{Message: err.Error()}
   276  		}
   277  
   278  		executionInfo, err := convertToExecutionInfo(record, saTypeMap)
   279  		if err != nil {
   280  			return nil, serviceerror.NewInternal(err.Error())
   281  		}
   282  		response.Executions = append(response.Executions, executionInfo)
   283  	}
   284  
   285  	if !completed {
   286  		newToken := &queryVisibilityToken{
   287  			Offset: currentCursorPos,
   288  		}
   289  		encodedToken, err := serializeToken(newToken)
   290  		if err != nil {
   291  			return nil, &serviceerror.InvalidArgument{Message: err.Error()}
   292  		}
   293  		response.NextPageToken = encodedToken
   294  	}
   295  
   296  	return response, nil
   297  }
   298  
   299  func (v *visibilityArchiver) parseToken(nextPageToken []byte) (*queryVisibilityToken, error) {
   300  	token := new(queryVisibilityToken)
   301  	if nextPageToken != nil {
   302  		var err error
   303  		token, err = deserializeQueryVisibilityToken(nextPageToken)
   304  		if err != nil {
   305  			return nil, &serviceerror.InvalidArgument{Message: archiver.ErrNextPageTokenCorrupted.Error()}
   306  		}
   307  	}
   308  	return token, nil
   309  }
   310  
   311  // ValidateURI is used to define what a valid URI for an implementation is.
   312  func (v *visibilityArchiver) ValidateURI(URI archiver.URI) (err error) {
   313  	ctx, cancel := context.WithTimeout(context.Background(), timeoutInSeconds*time.Second)
   314  	defer cancel()
   315  
   316  	if err = v.validateURI(URI); err == nil {
   317  		_, err = v.gcloudStorage.Exist(ctx, URI, "")
   318  	}
   319  
   320  	return
   321  }
   322  
   323  func (v *visibilityArchiver) validateURI(URI archiver.URI) (err error) {
   324  	if URI.Scheme() != URIScheme {
   325  		return archiver.ErrURISchemeMismatch
   326  	}
   327  
   328  	if URI.Path() == "" || URI.Hostname() == "" {
   329  		return archiver.ErrInvalidURI
   330  	}
   331  
   332  	return
   333  }