go.temporal.io/server@v1.23.0/common/archiver/gcloud/visibility_archiver_test.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 "testing" 31 "time" 32 33 "github.com/golang/mock/gomock" 34 "github.com/stretchr/testify/require" 35 "github.com/stretchr/testify/suite" 36 enumspb "go.temporal.io/api/enums/v1" 37 "go.temporal.io/api/serviceerror" 38 "go.temporal.io/api/workflow/v1" 39 40 "go.temporal.io/server/common/searchattribute" 41 "go.temporal.io/server/common/testing/protorequire" 42 43 archiverspb "go.temporal.io/server/api/archiver/v1" 44 "go.temporal.io/server/common/archiver" 45 "go.temporal.io/server/common/archiver/gcloud/connector" 46 "go.temporal.io/server/common/convert" 47 "go.temporal.io/server/common/log" 48 "go.temporal.io/server/common/metrics" 49 "go.temporal.io/server/common/primitives/timestamp" 50 ) 51 52 const ( 53 testWorkflowTypeName = "test-workflow-type" 54 exampleVisibilityRecord = `{"namespaceId":"test-namespace-id","namespace":"test-namespace","workflowId":"test-workflow-id","runId":"test-run-id","workflowTypeName":"test-workflow-type","startTime":"2020-02-05T09:56:14.804475Z","closeTime":"2020-02-05T09:56:15.946478Z","status":"Completed","historyLength":36,"memo":null,"searchAttributes":null,"historyArchivalUri":"gs://my-bucket-cad/temporal_archival/development"}` 55 exampleVisibilityRecord2 = `{"namespaceId":"test-namespace-id","namespace":"test-namespace", 56 "workflowId":"test-workflow-id2","runId":"test-run-id","workflowTypeName":"test-workflow-type", 57 "startTime":"2020-02-05T09:56:14.804475Z","closeTime":"2020-02-05T09:56:15.946478Z","status":"Completed","historyLength":36,"memo":null,"searchAttributes":null,"historyArchivalUri":"gs://my-bucket-cad/temporal_archival/development"}` 58 ) 59 60 func (s *visibilityArchiverSuite) SetupTest() { 61 s.Assertions = require.New(s.T()) 62 s.controller = gomock.NewController(s.T()) 63 s.container = &archiver.VisibilityBootstrapContainer{ 64 Logger: log.NewNoopLogger(), 65 MetricsHandler: metrics.NoopMetricsHandler, 66 } 67 s.expectedVisibilityRecords = []*archiverspb.VisibilityRecord{ 68 { 69 NamespaceId: testNamespaceID, 70 Namespace: testNamespace, 71 WorkflowId: testWorkflowID, 72 RunId: testRunID, 73 WorkflowTypeName: testWorkflowTypeName, 74 StartTime: timestamp.UnixOrZeroTimePtr(1580896574804475000), 75 CloseTime: timestamp.UnixOrZeroTimePtr(1580896575946478000), 76 Status: enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, 77 HistoryLength: 36, 78 }, 79 } 80 } 81 82 func (s *visibilityArchiverSuite) TearDownTest() { 83 s.controller.Finish() 84 } 85 86 func TestVisibilityArchiverSuiteSuite(t *testing.T) { 87 suite.Run(t, new(visibilityArchiverSuite)) 88 } 89 90 type visibilityArchiverSuite struct { 91 *require.Assertions 92 protorequire.ProtoAssertions 93 suite.Suite 94 controller *gomock.Controller 95 container *archiver.VisibilityBootstrapContainer 96 expectedVisibilityRecords []*archiverspb.VisibilityRecord 97 } 98 99 func (s *visibilityArchiverSuite) TestValidateVisibilityURI() { 100 testCases := []struct { 101 URI string 102 expectedErr error 103 }{ 104 { 105 URI: "wrongscheme:///a/b/c", 106 expectedErr: archiver.ErrURISchemeMismatch, 107 }, 108 { 109 URI: "gs:my-bucket-cad/temporal_archival/visibility", 110 expectedErr: archiver.ErrInvalidURI, 111 }, 112 { 113 URI: "gs://", 114 expectedErr: archiver.ErrInvalidURI, 115 }, 116 { 117 URI: "gs://my-bucket-cad", 118 expectedErr: archiver.ErrInvalidURI, 119 }, 120 { 121 URI: "gs:/my-bucket-cad/temporal_archival/visibility", 122 expectedErr: archiver.ErrInvalidURI, 123 }, 124 { 125 URI: "gs://my-bucket-cad/temporal_archival/visibility", 126 expectedErr: nil, 127 }, 128 } 129 130 storageWrapper := connector.NewMockClient(s.controller) 131 storageWrapper.EXPECT().Exist(gomock.Any(), gomock.Any(), "").Return(false, nil) 132 visibilityArchiver := new(visibilityArchiver) 133 visibilityArchiver.gcloudStorage = storageWrapper 134 for _, tc := range testCases { 135 URI, err := archiver.NewURI(tc.URI) 136 s.NoError(err) 137 s.Equal(tc.expectedErr, visibilityArchiver.ValidateURI(URI)) 138 } 139 } 140 141 func (s *visibilityArchiverSuite) TestArchive_Fail_InvalidVisibilityURI() { 142 ctx := context.Background() 143 URI, err := archiver.NewURI("wrongscheme://") 144 s.NoError(err) 145 storageWrapper := connector.NewMockClient(s.controller) 146 147 visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper) 148 s.NoError(err) 149 request := &archiverspb.VisibilityRecord{ 150 NamespaceId: testNamespaceID, 151 Namespace: testNamespace, 152 WorkflowId: testWorkflowID, 153 RunId: testRunID, 154 } 155 156 err = visibilityArchiver.Archive(ctx, URI, request) 157 s.Error(err) 158 } 159 160 func (s *visibilityArchiverSuite) TestQuery_Fail_InvalidVisibilityURI() { 161 ctx := context.Background() 162 URI, err := archiver.NewURI("wrongscheme://") 163 s.NoError(err) 164 storageWrapper := connector.NewMockClient(s.controller) 165 166 visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper) 167 s.NoError(err) 168 request := &archiver.QueryVisibilityRequest{ 169 NamespaceID: testNamespaceID, 170 PageSize: 10, 171 Query: "WorkflowType='type::example' AND CloseTime='2020-02-05T11:00:00Z' AND SearchPrecision='Day'", 172 } 173 174 _, err = visibilityArchiver.Query(ctx, URI, request, searchattribute.TestNameTypeMap) 175 s.Error(err) 176 } 177 178 func (s *visibilityArchiverSuite) TestVisibilityArchive() { 179 ctx := context.Background() 180 URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility") 181 s.NoError(err) 182 storageWrapper := connector.NewMockClient(s.controller) 183 storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil) 184 storageWrapper.EXPECT().Upload(gomock.Any(), URI, gomock.Any(), gomock.Any()).Return(nil).Times(2) 185 186 visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper) 187 s.NoError(err) 188 189 request := &archiverspb.VisibilityRecord{ 190 Namespace: testNamespace, 191 NamespaceId: testNamespaceID, 192 WorkflowId: testWorkflowID, 193 RunId: testRunID, 194 WorkflowTypeName: testWorkflowTypeName, 195 StartTime: timestamp.TimeNowPtrUtc(), 196 ExecutionTime: nil, // workflow without backoff 197 CloseTime: timestamp.TimeNowPtrUtc(), 198 Status: enumspb.WORKFLOW_EXECUTION_STATUS_FAILED, 199 HistoryLength: int64(101), 200 } 201 202 err = visibilityArchiver.Archive(ctx, URI, request) 203 s.NoError(err) 204 } 205 206 func (s *visibilityArchiverSuite) TestQuery_Fail_InvalidQuery() { 207 ctx := context.Background() 208 URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility") 209 s.NoError(err) 210 storageWrapper := connector.NewMockClient(s.controller) 211 storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil) 212 visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper) 213 s.NoError(err) 214 215 mockParser := NewMockQueryParser(s.controller) 216 mockParser.EXPECT().Parse(gomock.Any()).Return(nil, errors.New("invalid query")) 217 visibilityArchiver.queryParser = mockParser 218 response, err := visibilityArchiver.Query(ctx, URI, &archiver.QueryVisibilityRequest{ 219 NamespaceID: "some random namespaceID", 220 PageSize: 10, 221 Query: "some invalid query", 222 }, searchattribute.TestNameTypeMap) 223 s.Error(err) 224 s.Nil(response) 225 } 226 227 func (s *visibilityArchiverSuite) TestQuery_Fail_InvalidToken() { 228 URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility") 229 s.NoError(err) 230 storageWrapper := connector.NewMockClient(s.controller) 231 storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil) 232 visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper) 233 s.NoError(err) 234 235 mockParser := NewMockQueryParser(s.controller) 236 startTime, _ := time.Parse(time.RFC3339, "2019-10-04T11:00:00+00:00") 237 closeTime := startTime.Add(time.Hour) 238 precision := PrecisionDay 239 mockParser.EXPECT().Parse(gomock.Any()).Return(&parsedQuery{ 240 closeTime: closeTime, 241 startTime: startTime, 242 searchPrecision: &precision, 243 }, nil) 244 visibilityArchiver.queryParser = mockParser 245 request := &archiver.QueryVisibilityRequest{ 246 NamespaceID: testNamespaceID, 247 Query: "parsed by mockParser", 248 PageSize: 1, 249 NextPageToken: []byte{1, 2, 3}, 250 } 251 response, err := visibilityArchiver.Query(context.Background(), URI, request, searchattribute.TestNameTypeMap) 252 s.Error(err) 253 s.Nil(response) 254 } 255 256 func (s *visibilityArchiverSuite) TestQuery_Success_NoNextPageToken() { 257 ctx := context.Background() 258 URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility") 259 s.NoError(err) 260 storageWrapper := connector.NewMockClient(s.controller) 261 storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil) 262 storageWrapper.EXPECT().QueryWithFilters(gomock.Any(), URI, gomock.Any(), 10, 0, gomock.Any()).Return([]string{"closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility"}, true, 1, nil) 263 storageWrapper.EXPECT().Get(gomock.Any(), URI, "test-namespace-id/closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord), nil) 264 265 visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper) 266 s.NoError(err) 267 268 mockParser := NewMockQueryParser(s.controller) 269 dayPrecision := "Day" 270 closeTime, _ := time.Parse(time.RFC3339, "2019-10-04T11:00:00+00:00") 271 mockParser.EXPECT().Parse(gomock.Any()).Return(&parsedQuery{ 272 closeTime: closeTime, 273 searchPrecision: &dayPrecision, 274 workflowType: convert.StringPtr("MobileOnlyWorkflow::processMobileOnly"), 275 workflowID: convert.StringPtr(testWorkflowID), 276 runID: convert.StringPtr(testRunID), 277 }, nil) 278 visibilityArchiver.queryParser = mockParser 279 request := &archiver.QueryVisibilityRequest{ 280 NamespaceID: testNamespaceID, 281 PageSize: 10, 282 Query: "parsed by mockParser", 283 } 284 285 response, err := visibilityArchiver.Query(ctx, URI, request, searchattribute.TestNameTypeMap) 286 s.NoError(err) 287 s.NotNil(response) 288 s.Nil(response.NextPageToken) 289 s.Len(response.Executions, 1) 290 ei, err := convertToExecutionInfo(s.expectedVisibilityRecords[0], searchattribute.TestNameTypeMap) 291 s.NoError(err) 292 s.ProtoEqual(ei, response.Executions[0]) 293 } 294 295 func (s *visibilityArchiverSuite) TestQuery_Success_SmallPageSize() { 296 pageSize := 2 297 ctx := context.Background() 298 URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility") 299 s.NoError(err) 300 storageWrapper := connector.NewMockClient(s.controller) 301 storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil).Times(2) 302 storageWrapper.EXPECT().QueryWithFilters(gomock.Any(), URI, gomock.Any(), pageSize, 0, gomock.Any()).Return([]string{"closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility", "closeTimeout_2020-02-05T09:56:15Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility"}, false, 1, nil) 303 storageWrapper.EXPECT().QueryWithFilters(gomock.Any(), URI, gomock.Any(), pageSize, 1, gomock.Any()).Return([]string{"closeTimeout_2020-02-05T09:56:16Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility"}, true, 2, nil) 304 storageWrapper.EXPECT().Get(gomock.Any(), URI, "test-namespace-id/closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord), nil) 305 storageWrapper.EXPECT().Get(gomock.Any(), URI, "test-namespace-id/closeTimeout_2020-02-05T09:56:15Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord), nil) 306 storageWrapper.EXPECT().Get(gomock.Any(), URI, "test-namespace-id/closeTimeout_2020-02-05T09:56:16Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord), nil) 307 308 visibilityArchiver := newVisibilityArchiver(s.container, storageWrapper) 309 s.NoError(err) 310 311 mockParser := NewMockQueryParser(s.controller) 312 dayPrecision := "Day" 313 closeTime, _ := time.Parse(time.RFC3339, "2019-10-04T11:00:00+00:00") 314 mockParser.EXPECT().Parse(gomock.Any()).Return(&parsedQuery{ 315 closeTime: closeTime, 316 searchPrecision: &dayPrecision, 317 workflowType: convert.StringPtr("MobileOnlyWorkflow::processMobileOnly"), 318 workflowID: convert.StringPtr(testWorkflowID), 319 runID: convert.StringPtr(testRunID), 320 }, nil).AnyTimes() 321 visibilityArchiver.queryParser = mockParser 322 request := &archiver.QueryVisibilityRequest{ 323 NamespaceID: testNamespaceID, 324 PageSize: pageSize, 325 Query: "parsed by mockParser", 326 } 327 328 response, err := visibilityArchiver.Query(ctx, URI, request, searchattribute.TestNameTypeMap) 329 s.NoError(err) 330 s.NotNil(response) 331 s.NotNil(response.NextPageToken) 332 s.Len(response.Executions, 2) 333 ei, err := convertToExecutionInfo(s.expectedVisibilityRecords[0], searchattribute.TestNameTypeMap) 334 s.NoError(err) 335 s.ProtoEqual(ei, response.Executions[0]) 336 ei, err = convertToExecutionInfo(s.expectedVisibilityRecords[0], searchattribute.TestNameTypeMap) 337 s.NoError(err) 338 s.ProtoEqual(ei, response.Executions[1]) 339 340 request.NextPageToken = response.NextPageToken 341 response, err = visibilityArchiver.Query(ctx, URI, request, searchattribute.TestNameTypeMap) 342 s.NoError(err) 343 s.NotNil(response) 344 s.Nil(response.NextPageToken) 345 s.Len(response.Executions, 1) 346 ei, err = convertToExecutionInfo(s.expectedVisibilityRecords[0], searchattribute.TestNameTypeMap) 347 s.NoError(err) 348 s.ProtoEqual(ei, response.Executions[0]) 349 } 350 351 func (s *visibilityArchiverSuite) TestQuery_EmptyQuery_InvalidNamespace() { 352 URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility") 353 s.NoError(err) 354 storageWrapper := connector.NewMockClient(s.controller) 355 storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil) 356 arc := newVisibilityArchiver(s.container, storageWrapper) 357 req := &archiver.QueryVisibilityRequest{ 358 NamespaceID: "", 359 PageSize: 1, 360 NextPageToken: nil, 361 Query: "", 362 } 363 _, err = arc.Query(context.Background(), URI, req, searchattribute.TestNameTypeMap) 364 365 var svcErr *serviceerror.InvalidArgument 366 367 s.ErrorAs(err, &svcErr) 368 } 369 370 func (s *visibilityArchiverSuite) TestQuery_EmptyQuery_ZeroPageSize() { 371 URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility") 372 s.NoError(err) 373 storageWrapper := connector.NewMockClient(s.controller) 374 storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(false, nil) 375 arc := newVisibilityArchiver(s.container, storageWrapper) 376 377 req := &archiver.QueryVisibilityRequest{ 378 NamespaceID: testNamespaceID, 379 PageSize: 0, 380 NextPageToken: nil, 381 Query: "", 382 } 383 _, err = arc.Query(context.Background(), URI, req, searchattribute.TestNameTypeMap) 384 385 var svcErr *serviceerror.InvalidArgument 386 387 s.ErrorAs(err, &svcErr) 388 } 389 390 func (s *visibilityArchiverSuite) TestQuery_EmptyQuery_Pagination() { 391 URI, err := archiver.NewURI("gs://my-bucket-cad/temporal_archival/visibility") 392 s.NoError(err) 393 storageWrapper := connector.NewMockClient(s.controller) 394 storageWrapper.EXPECT().Exist(gomock.Any(), URI, gomock.Any()).Return(true, nil).Times(2) 395 storageWrapper.EXPECT().QueryWithFilters( 396 gomock.Any(), 397 URI, 398 gomock.Any(), 399 1, 400 0, 401 gomock.Any(), 402 ).Return( 403 []string{"closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility"}, 404 false, 405 1, 406 nil, 407 ) 408 storageWrapper.EXPECT().QueryWithFilters( 409 gomock.Any(), 410 URI, 411 gomock.Any(), 412 1, 413 1, 414 gomock.Any(), 415 ).Return( 416 []string{"closeTimeout_2020-02-05T09:56:14Z_test-workflow-id2_MobileOnlyWorkflow::processMobileOnly_test-run" + 417 "-id.visibility"}, 418 true, 419 2, 420 nil, 421 ) 422 storageWrapper.EXPECT().Get( 423 gomock.Any(), 424 URI, 425 "test-namespace-id/closeTimeout_2020-02-05T09:56:14Z_test-workflow-id_MobileOnlyWorkflow::processMobileOnly_test-run-id.visibility", 426 ).Return([]byte(exampleVisibilityRecord), nil) 427 storageWrapper.EXPECT().Get(gomock.Any(), URI, 428 "test-namespace-id/closeTimeout_2020-02-05T09:56:14Z_test-workflow-id2_MobileOnlyWorkflow"+ 429 "::processMobileOnly_test-run-id.visibility").Return([]byte(exampleVisibilityRecord2), nil) 430 431 arc := newVisibilityArchiver(s.container, storageWrapper) 432 433 response := &archiver.QueryVisibilityResponse{ 434 Executions: nil, 435 NextPageToken: nil, 436 } 437 438 limit := 10 439 executions := make(map[string]*workflow.WorkflowExecutionInfo, limit) 440 441 numPages := 2 442 for i := 0; i < numPages; i++ { 443 req := &archiver.QueryVisibilityRequest{ 444 NamespaceID: testNamespaceID, 445 PageSize: 1, 446 NextPageToken: response.NextPageToken, 447 Query: "", 448 } 449 response, err = arc.Query(context.Background(), URI, req, searchattribute.TestNameTypeMap) 450 s.NoError(err) 451 s.NotNil(response) 452 s.Len(response.Executions, 1) 453 454 s.Equal( 455 i == numPages-1, 456 response.NextPageToken == nil, 457 "should have no next page token on the last iteration", 458 ) 459 460 for _, execution := range response.Executions { 461 key := execution.Execution.GetWorkflowId() + 462 "/" + execution.Execution.GetRunId() + 463 "/" + execution.CloseTime.String() 464 executions[key] = execution 465 } 466 } 467 s.Len(executions, 2, "there should be exactly 2 unique executions") 468 }