go.temporal.io/server@v1.23.0/common/archiver/filestore/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 filestore
    26  
    27  import (
    28  	"context"
    29  	"fmt"
    30  	"os"
    31  	"path"
    32  	"sort"
    33  	"strconv"
    34  	"strings"
    35  	"time"
    36  
    37  	commonpb "go.temporal.io/api/common/v1"
    38  	"go.temporal.io/api/serviceerror"
    39  	workflowpb "go.temporal.io/api/workflow/v1"
    40  
    41  	archiverspb "go.temporal.io/server/api/archiver/v1"
    42  	"go.temporal.io/server/common/archiver"
    43  	"go.temporal.io/server/common/config"
    44  	"go.temporal.io/server/common/log/tag"
    45  	"go.temporal.io/server/common/primitives/timestamp"
    46  	"go.temporal.io/server/common/searchattribute"
    47  )
    48  
    49  const (
    50  	errEncodeVisibilityRecord = "failed to encode visibility record"
    51  )
    52  
    53  type (
    54  	visibilityArchiver struct {
    55  		container   *archiver.VisibilityBootstrapContainer
    56  		fileMode    os.FileMode
    57  		dirMode     os.FileMode
    58  		queryParser QueryParser
    59  	}
    60  
    61  	queryVisibilityToken struct {
    62  		LastCloseTime time.Time
    63  		LastRunID     string
    64  	}
    65  
    66  	queryVisibilityRequest struct {
    67  		namespaceID   string
    68  		pageSize      int
    69  		nextPageToken []byte
    70  		parsedQuery   *parsedQuery
    71  	}
    72  )
    73  
    74  // NewVisibilityArchiver creates a new archiver.VisibilityArchiver based on filestore
    75  func NewVisibilityArchiver(
    76  	container *archiver.VisibilityBootstrapContainer,
    77  	config *config.FilestoreArchiver,
    78  ) (archiver.VisibilityArchiver, error) {
    79  	fileMode, err := strconv.ParseUint(config.FileMode, 0, 32)
    80  	if err != nil {
    81  		return nil, errInvalidFileMode
    82  	}
    83  	dirMode, err := strconv.ParseUint(config.DirMode, 0, 32)
    84  	if err != nil {
    85  		return nil, errInvalidDirMode
    86  	}
    87  	return &visibilityArchiver{
    88  		container:   container,
    89  		fileMode:    os.FileMode(fileMode),
    90  		dirMode:     os.FileMode(dirMode),
    91  		queryParser: NewQueryParser(),
    92  	}, nil
    93  }
    94  
    95  func (v *visibilityArchiver) Archive(
    96  	ctx context.Context,
    97  	URI archiver.URI,
    98  	request *archiverspb.VisibilityRecord,
    99  	opts ...archiver.ArchiveOption,
   100  ) (err error) {
   101  	featureCatalog := archiver.GetFeatureCatalog(opts...)
   102  	defer func() {
   103  		if err != nil && featureCatalog.NonRetryableError != nil {
   104  			err = featureCatalog.NonRetryableError()
   105  		}
   106  	}()
   107  
   108  	logger := archiver.TagLoggerWithArchiveVisibilityRequestAndURI(v.container.Logger, request, URI.String())
   109  
   110  	if err := v.ValidateURI(URI); err != nil {
   111  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(archiver.ErrReasonInvalidURI), tag.Error(err))
   112  		return err
   113  	}
   114  
   115  	if err := archiver.ValidateVisibilityArchivalRequest(request); err != nil {
   116  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(archiver.ErrReasonInvalidArchiveRequest), tag.Error(err))
   117  		return err
   118  	}
   119  
   120  	dirPath := path.Join(URI.Path(), request.GetNamespaceId())
   121  	if err = mkdirAll(dirPath, v.dirMode); err != nil {
   122  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(errMakeDirectory), tag.Error(err))
   123  		return err
   124  	}
   125  
   126  	encodedVisibilityRecord, err := encode(request)
   127  	if err != nil {
   128  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(errEncodeVisibilityRecord), tag.Error(err))
   129  		return err
   130  	}
   131  
   132  	// The filename has the format: closeTimestamp_hash(runID).visibility
   133  	// This format allows the archiver to sort all records without reading the file contents
   134  	filename := constructVisibilityFilename(request.CloseTime.AsTime(), request.GetRunId())
   135  	if err := writeFile(path.Join(dirPath, filename), encodedVisibilityRecord, v.fileMode); err != nil {
   136  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(errWriteFile), tag.Error(err))
   137  		return err
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  func (v *visibilityArchiver) Query(
   144  	ctx context.Context,
   145  	URI archiver.URI,
   146  	request *archiver.QueryVisibilityRequest,
   147  	saTypeMap searchattribute.NameTypeMap,
   148  ) (*archiver.QueryVisibilityResponse, error) {
   149  	if err := v.ValidateURI(URI); err != nil {
   150  		return nil, serviceerror.NewInvalidArgument(archiver.ErrInvalidURI.Error())
   151  	}
   152  
   153  	if err := archiver.ValidateQueryRequest(request); err != nil {
   154  		return nil, serviceerror.NewInvalidArgument(archiver.ErrInvalidQueryVisibilityRequest.Error())
   155  	}
   156  
   157  	parsedQuery, err := v.queryParser.Parse(request.Query)
   158  	if err != nil {
   159  		return nil, serviceerror.NewInvalidArgument(err.Error())
   160  	}
   161  
   162  	if parsedQuery.emptyResult {
   163  		return &archiver.QueryVisibilityResponse{}, nil
   164  	}
   165  
   166  	return v.query(
   167  		ctx,
   168  		URI,
   169  		&queryVisibilityRequest{
   170  			namespaceID:   request.NamespaceID,
   171  			pageSize:      request.PageSize,
   172  			nextPageToken: request.NextPageToken,
   173  			parsedQuery:   parsedQuery,
   174  		},
   175  		saTypeMap,
   176  	)
   177  }
   178  
   179  func (v *visibilityArchiver) query(
   180  	ctx context.Context,
   181  	URI archiver.URI,
   182  	request *queryVisibilityRequest,
   183  	saTypeMap searchattribute.NameTypeMap,
   184  ) (*archiver.QueryVisibilityResponse, error) {
   185  	var token *queryVisibilityToken
   186  	if request.nextPageToken != nil {
   187  		var err error
   188  		token, err = deserializeQueryVisibilityToken(request.nextPageToken)
   189  		if err != nil {
   190  			return nil, serviceerror.NewInvalidArgument(archiver.ErrNextPageTokenCorrupted.Error())
   191  		}
   192  	}
   193  
   194  	dirPath := path.Join(URI.Path(), request.namespaceID)
   195  	exists, err := directoryExists(dirPath)
   196  	if err != nil {
   197  		return nil, serviceerror.NewInternal(err.Error())
   198  	}
   199  	if !exists {
   200  		return &archiver.QueryVisibilityResponse{}, nil
   201  	}
   202  
   203  	files, err := listFiles(dirPath)
   204  	if err != nil {
   205  		return nil, serviceerror.NewInternal(err.Error())
   206  	}
   207  
   208  	files, err = sortAndFilterFiles(files, token)
   209  	if err != nil {
   210  		return nil, serviceerror.NewInternal(err.Error())
   211  	}
   212  	if len(files) == 0 {
   213  		return &archiver.QueryVisibilityResponse{}, nil
   214  	}
   215  
   216  	response := &archiver.QueryVisibilityResponse{}
   217  	for idx, file := range files {
   218  		encodedRecord, err := readFile(path.Join(dirPath, file))
   219  		if err != nil {
   220  			return nil, serviceerror.NewInternal(err.Error())
   221  		}
   222  
   223  		record, err := decodeVisibilityRecord(encodedRecord)
   224  		if err != nil {
   225  			return nil, serviceerror.NewInternal(err.Error())
   226  		}
   227  
   228  		if record.CloseTime.AsTime().Before(request.parsedQuery.earliestCloseTime) {
   229  			break
   230  		}
   231  
   232  		if matchQuery(record, request.parsedQuery) {
   233  			executionInfo, err := convertToExecutionInfo(record, saTypeMap)
   234  			if err != nil {
   235  				return nil, serviceerror.NewInternal(err.Error())
   236  			}
   237  
   238  			response.Executions = append(response.Executions, executionInfo)
   239  			if len(response.Executions) == request.pageSize {
   240  				if idx != len(files) {
   241  					newToken := &queryVisibilityToken{
   242  						LastCloseTime: timestamp.TimeValue(record.CloseTime),
   243  						LastRunID:     record.GetRunId(),
   244  					}
   245  					encodedToken, err := serializeToken(newToken)
   246  					if err != nil {
   247  						return nil, serviceerror.NewInternal(err.Error())
   248  					}
   249  					response.NextPageToken = encodedToken
   250  				}
   251  				break
   252  			}
   253  		}
   254  	}
   255  
   256  	return response, nil
   257  }
   258  
   259  func (v *visibilityArchiver) ValidateURI(URI archiver.URI) error {
   260  	if URI.Scheme() != URIScheme {
   261  		return archiver.ErrURISchemeMismatch
   262  	}
   263  
   264  	return validateDirPath((URI.Path()))
   265  }
   266  
   267  type parsedVisFilename struct {
   268  	name        string
   269  	closeTime   time.Time
   270  	hashedRunID string
   271  }
   272  
   273  // sortAndFilterFiles sort visibility record file names based on close timestamp (desc) and use hashed runID to break ties.
   274  // if a nextPageToken is give, it only returns filenames that have a smaller close timestamp
   275  func sortAndFilterFiles(filenames []string, token *queryVisibilityToken) ([]string, error) {
   276  	var parsedFilenames []*parsedVisFilename
   277  	for _, name := range filenames {
   278  		pieces := strings.FieldsFunc(name, func(r rune) bool {
   279  			return r == '_' || r == '.'
   280  		})
   281  		if len(pieces) != 3 {
   282  			return nil, fmt.Errorf("failed to parse visibility filename %s", name)
   283  		}
   284  
   285  		closeTime, err := strconv.ParseInt(pieces[0], 10, 64)
   286  		if err != nil {
   287  			return nil, fmt.Errorf("failed to parse visibility filename %s", name)
   288  		}
   289  		parsedFilenames = append(parsedFilenames, &parsedVisFilename{
   290  			name:        name,
   291  			closeTime:   timestamp.UnixOrZeroTime(closeTime),
   292  			hashedRunID: pieces[1],
   293  		})
   294  	}
   295  
   296  	sort.Slice(parsedFilenames, func(i, j int) bool {
   297  		if parsedFilenames[i].closeTime.Equal(parsedFilenames[j].closeTime) {
   298  			return parsedFilenames[i].hashedRunID > parsedFilenames[j].hashedRunID
   299  		}
   300  		return parsedFilenames[i].closeTime.After(parsedFilenames[j].closeTime)
   301  	})
   302  
   303  	startIdx := 0
   304  	if token != nil {
   305  		LastHashedRunID := hash(token.LastRunID)
   306  		startIdx = sort.Search(len(parsedFilenames), func(i int) bool {
   307  			if parsedFilenames[i].closeTime.Equal(token.LastCloseTime) {
   308  				return parsedFilenames[i].hashedRunID < LastHashedRunID
   309  			}
   310  			return parsedFilenames[i].closeTime.Before(token.LastCloseTime)
   311  		})
   312  	}
   313  
   314  	if startIdx == len(parsedFilenames) {
   315  		return []string{}, nil
   316  	}
   317  
   318  	var filteredFilenames []string
   319  	for _, parsedFilename := range parsedFilenames[startIdx:] {
   320  		filteredFilenames = append(filteredFilenames, parsedFilename.name)
   321  	}
   322  	return filteredFilenames, nil
   323  }
   324  
   325  func matchQuery(record *archiverspb.VisibilityRecord, query *parsedQuery) bool {
   326  	closeTime := record.CloseTime.AsTime()
   327  	if closeTime.Before(query.earliestCloseTime) || closeTime.After(query.latestCloseTime) {
   328  		return false
   329  	}
   330  	if query.workflowID != nil && record.GetWorkflowId() != *query.workflowID {
   331  		return false
   332  	}
   333  	if query.runID != nil && record.GetRunId() != *query.runID {
   334  		return false
   335  	}
   336  	if query.workflowTypeName != nil && record.WorkflowTypeName != *query.workflowTypeName {
   337  		return false
   338  	}
   339  	if query.status != nil && record.Status != *query.status {
   340  		return false
   341  	}
   342  	return true
   343  }
   344  
   345  func convertToExecutionInfo(record *archiverspb.VisibilityRecord, saTypeMap searchattribute.NameTypeMap) (*workflowpb.WorkflowExecutionInfo, error) {
   346  	searchAttributes, err := searchattribute.Parse(record.SearchAttributes, &saTypeMap)
   347  	if err != nil {
   348  		return nil, err
   349  	}
   350  
   351  	return &workflowpb.WorkflowExecutionInfo{
   352  		Execution: &commonpb.WorkflowExecution{
   353  			WorkflowId: record.GetWorkflowId(),
   354  			RunId:      record.GetRunId(),
   355  		},
   356  		Type: &commonpb.WorkflowType{
   357  			Name: record.WorkflowTypeName,
   358  		},
   359  		StartTime:        record.StartTime,
   360  		ExecutionTime:    record.ExecutionTime,
   361  		CloseTime:        record.CloseTime,
   362  		Status:           record.Status,
   363  		HistoryLength:    record.HistoryLength,
   364  		Memo:             record.Memo,
   365  		SearchAttributes: searchAttributes,
   366  	}, nil
   367  }