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 }