go.temporal.io/server@v1.23.0/common/archiver/filestore/history_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  // Filestore History Archiver will archive workflow histories to local disk.
    26  
    27  // Each Archive() request results in a file named in the format of
    28  // hash(namespaceID, workflowID, runID)_version.history being created in the specified
    29  // directory. Workflow histories stored in that file are encoded in JSON format.
    30  
    31  // The Get() method retrieves the archived histories from the directory specified in the
    32  // URI. It optionally takes in a NextPageToken which specifies the workflow close failover
    33  // version and the index of the first history batch that should be returned. Instead of
    34  // NextPageToken, caller can also provide a close failover version, in which case, Get() method
    35  // will return history batches starting from the beginning of that history version. If neither
    36  // of NextPageToken or close failover version is specified, the highest close failover version
    37  // will be picked.
    38  
    39  package filestore
    40  
    41  import (
    42  	"context"
    43  	"errors"
    44  	"os"
    45  	"path"
    46  	"strconv"
    47  
    48  	historypb "go.temporal.io/api/history/v1"
    49  	"go.temporal.io/api/serviceerror"
    50  
    51  	"go.temporal.io/server/common"
    52  	"go.temporal.io/server/common/archiver"
    53  	"go.temporal.io/server/common/codec"
    54  	"go.temporal.io/server/common/config"
    55  	"go.temporal.io/server/common/log"
    56  	"go.temporal.io/server/common/log/tag"
    57  )
    58  
    59  const (
    60  	// URIScheme is the scheme for the filestore implementation
    61  	URIScheme = "file"
    62  
    63  	errEncodeHistory = "failed to encode history batches"
    64  	errMakeDirectory = "failed to make directory"
    65  	errWriteFile     = "failed to write history to file"
    66  
    67  	targetHistoryBlobSize = 2 * 1024 * 1024 // 2MB
    68  )
    69  
    70  var (
    71  	errInvalidFileMode = errors.New("invalid file mode")
    72  	errInvalidDirMode  = errors.New("invalid directory mode")
    73  )
    74  
    75  type (
    76  	historyArchiver struct {
    77  		container *archiver.HistoryBootstrapContainer
    78  		fileMode  os.FileMode
    79  		dirMode   os.FileMode
    80  
    81  		// only set in test code
    82  		historyIterator archiver.HistoryIterator
    83  	}
    84  
    85  	getHistoryToken struct {
    86  		CloseFailoverVersion int64
    87  		NextBatchIdx         int
    88  	}
    89  )
    90  
    91  // NewHistoryArchiver creates a new archiver.HistoryArchiver based on filestore
    92  func NewHistoryArchiver(
    93  	container *archiver.HistoryBootstrapContainer,
    94  	config *config.FilestoreArchiver,
    95  ) (archiver.HistoryArchiver, error) {
    96  	return newHistoryArchiver(container, config, nil)
    97  }
    98  
    99  func newHistoryArchiver(
   100  	container *archiver.HistoryBootstrapContainer,
   101  	config *config.FilestoreArchiver,
   102  	historyIterator archiver.HistoryIterator,
   103  ) (*historyArchiver, error) {
   104  	fileMode, err := strconv.ParseUint(config.FileMode, 0, 32)
   105  	if err != nil {
   106  		return nil, errInvalidFileMode
   107  	}
   108  	dirMode, err := strconv.ParseUint(config.DirMode, 0, 32)
   109  	if err != nil {
   110  		return nil, errInvalidDirMode
   111  	}
   112  	return &historyArchiver{
   113  		container:       container,
   114  		fileMode:        os.FileMode(fileMode),
   115  		dirMode:         os.FileMode(dirMode),
   116  		historyIterator: historyIterator,
   117  	}, nil
   118  }
   119  
   120  func (h *historyArchiver) Archive(
   121  	ctx context.Context,
   122  	URI archiver.URI,
   123  	request *archiver.ArchiveHistoryRequest,
   124  	opts ...archiver.ArchiveOption,
   125  ) (err error) {
   126  	featureCatalog := archiver.GetFeatureCatalog(opts...)
   127  	defer func() {
   128  		if err != nil && !common.IsPersistenceTransientError(err) && featureCatalog.NonRetryableError != nil {
   129  			err = featureCatalog.NonRetryableError()
   130  		}
   131  	}()
   132  
   133  	logger := archiver.TagLoggerWithArchiveHistoryRequestAndURI(h.container.Logger, request, URI.String())
   134  
   135  	if err := h.ValidateURI(URI); err != nil {
   136  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(archiver.ErrReasonInvalidURI), tag.Error(err))
   137  		return err
   138  	}
   139  
   140  	if err := archiver.ValidateHistoryArchiveRequest(request); err != nil {
   141  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(archiver.ErrReasonInvalidArchiveRequest), tag.Error(err))
   142  		return err
   143  	}
   144  
   145  	historyIterator := h.historyIterator
   146  	if historyIterator == nil { // will only be set by testing code
   147  		historyIterator = archiver.NewHistoryIterator(request, h.container.ExecutionManager, targetHistoryBlobSize)
   148  	}
   149  
   150  	var historyBatches []*historypb.History
   151  	for historyIterator.HasNext() {
   152  		historyBlob, err := historyIterator.Next(ctx)
   153  		if err != nil {
   154  			if _, isNotFound := err.(*serviceerror.NotFound); isNotFound {
   155  				// workflow history no longer exists, may due to duplicated archival signal
   156  				// this may happen even in the middle of iterating history as two archival signals
   157  				// can be processed concurrently.
   158  				logger.Info(archiver.ArchiveSkippedInfoMsg)
   159  				return nil
   160  			}
   161  
   162  			logger = log.With(logger, tag.ArchivalArchiveFailReason(archiver.ErrReasonReadHistory), tag.Error(err))
   163  			if !common.IsPersistenceTransientError(err) {
   164  				logger.Error(archiver.ArchiveNonRetryableErrorMsg)
   165  			} else {
   166  				logger.Error(archiver.ArchiveTransientErrorMsg)
   167  			}
   168  			return err
   169  		}
   170  
   171  		if historyMutated(request, historyBlob.Body, historyBlob.Header.IsLast) {
   172  			logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(archiver.ErrReasonHistoryMutated))
   173  			return archiver.ErrHistoryMutated
   174  		}
   175  
   176  		historyBatches = append(historyBatches, historyBlob.Body...)
   177  	}
   178  
   179  	encoder := codec.NewJSONPBEncoder()
   180  	encodedHistoryBatches, err := encoder.EncodeHistories(historyBatches)
   181  	if err != nil {
   182  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(errEncodeHistory), tag.Error(err))
   183  		return err
   184  	}
   185  
   186  	dirPath := URI.Path()
   187  	if err = mkdirAll(dirPath, h.dirMode); err != nil {
   188  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(errMakeDirectory), tag.Error(err))
   189  		return err
   190  	}
   191  
   192  	filename := constructHistoryFilename(request.NamespaceID, request.WorkflowID, request.RunID, request.CloseFailoverVersion)
   193  	if err := writeFile(path.Join(dirPath, filename), encodedHistoryBatches, h.fileMode); err != nil {
   194  		logger.Error(archiver.ArchiveNonRetryableErrorMsg, tag.ArchivalArchiveFailReason(errWriteFile), tag.Error(err))
   195  		return err
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  func (h *historyArchiver) Get(
   202  	ctx context.Context,
   203  	URI archiver.URI,
   204  	request *archiver.GetHistoryRequest,
   205  ) (*archiver.GetHistoryResponse, error) {
   206  	if err := h.ValidateURI(URI); err != nil {
   207  		return nil, serviceerror.NewInvalidArgument(archiver.ErrInvalidURI.Error())
   208  	}
   209  
   210  	if err := archiver.ValidateGetRequest(request); err != nil {
   211  		return nil, serviceerror.NewInvalidArgument(archiver.ErrInvalidGetHistoryRequest.Error())
   212  	}
   213  
   214  	dirPath := URI.Path()
   215  	exists, err := directoryExists(dirPath)
   216  	if err != nil {
   217  		return nil, serviceerror.NewInternal(err.Error())
   218  	}
   219  	if !exists {
   220  		return nil, serviceerror.NewNotFound(archiver.ErrHistoryNotExist.Error())
   221  	}
   222  
   223  	var token *getHistoryToken
   224  	if request.NextPageToken != nil {
   225  		token, err = deserializeGetHistoryToken(request.NextPageToken)
   226  		if err != nil {
   227  			return nil, serviceerror.NewInvalidArgument(archiver.ErrNextPageTokenCorrupted.Error())
   228  		}
   229  	} else if request.CloseFailoverVersion != nil {
   230  		token = &getHistoryToken{
   231  			CloseFailoverVersion: *request.CloseFailoverVersion,
   232  			NextBatchIdx:         0,
   233  		}
   234  	} else {
   235  		highestVersion, err := getHighestVersion(dirPath, request)
   236  		if err != nil {
   237  			return nil, serviceerror.NewInternal(err.Error())
   238  		}
   239  		token = &getHistoryToken{
   240  			CloseFailoverVersion: *highestVersion,
   241  			NextBatchIdx:         0,
   242  		}
   243  	}
   244  
   245  	filename := constructHistoryFilename(request.NamespaceID, request.WorkflowID, request.RunID, token.CloseFailoverVersion)
   246  	filepath := path.Join(dirPath, filename)
   247  	exists, err = fileExists(filepath)
   248  	if err != nil {
   249  		return nil, serviceerror.NewInternal(err.Error())
   250  	}
   251  	if !exists {
   252  		return nil, serviceerror.NewNotFound(archiver.ErrHistoryNotExist.Error())
   253  	}
   254  
   255  	encodedHistoryBatches, err := readFile(filepath)
   256  	if err != nil {
   257  		return nil, serviceerror.NewInternal(err.Error())
   258  	}
   259  
   260  	encoder := codec.NewJSONPBEncoder()
   261  	historyBatches, err := encoder.DecodeHistories(encodedHistoryBatches)
   262  	if err != nil {
   263  		return nil, serviceerror.NewInternal(err.Error())
   264  	}
   265  	historyBatches = historyBatches[token.NextBatchIdx:]
   266  
   267  	response := &archiver.GetHistoryResponse{}
   268  	numOfEvents := 0
   269  	numOfBatches := 0
   270  	for _, batch := range historyBatches {
   271  		response.HistoryBatches = append(response.HistoryBatches, batch)
   272  		numOfBatches++
   273  		numOfEvents += len(batch.Events)
   274  		if numOfEvents >= request.PageSize {
   275  			break
   276  		}
   277  	}
   278  
   279  	if numOfBatches < len(historyBatches) {
   280  		token.NextBatchIdx += numOfBatches
   281  		nextToken, err := serializeToken(token)
   282  		if err != nil {
   283  			return nil, serviceerror.NewInternal(err.Error())
   284  		}
   285  		response.NextPageToken = nextToken
   286  	}
   287  
   288  	return response, nil
   289  }
   290  
   291  func (h *historyArchiver) ValidateURI(URI archiver.URI) error {
   292  	if URI.Scheme() != URIScheme {
   293  		return archiver.ErrURISchemeMismatch
   294  	}
   295  
   296  	return validateDirPath(URI.Path())
   297  }
   298  
   299  func getHighestVersion(dirPath string, request *archiver.GetHistoryRequest) (*int64, error) {
   300  	filenames, err := listFilesByPrefix(dirPath, constructHistoryFilenamePrefix(request.NamespaceID, request.WorkflowID, request.RunID))
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  
   305  	var highestVersion *int64
   306  	for _, filename := range filenames {
   307  		version, err := extractCloseFailoverVersion(filename)
   308  		if err != nil {
   309  			continue
   310  		}
   311  		if highestVersion == nil || version > *highestVersion {
   312  			highestVersion = &version
   313  		}
   314  	}
   315  	if highestVersion == nil {
   316  		return nil, archiver.ErrHistoryNotExist
   317  	}
   318  	return highestVersion, nil
   319  }