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 }