go.uber.org/cadence@v1.2.9/internal/workflow_replayer_test.go (about) 1 // Copyright (c) 2017-2021 Uber Technologies Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package internal 22 23 import ( 24 "context" 25 "errors" 26 "fmt" 27 "testing" 28 "time" 29 30 "github.com/stretchr/testify/require" 31 "github.com/stretchr/testify/suite" 32 "go.uber.org/zap" 33 34 "go.uber.org/cadence/.gen/go/shared" 35 "go.uber.org/cadence/internal/common" 36 ) 37 38 type workflowReplayerSuite struct { 39 *require.Assertions 40 suite.Suite 41 42 replayer *WorkflowReplayer 43 logger *zap.Logger 44 } 45 46 var ( 47 testTaskList = "taskList" 48 ) 49 50 func TestWorkflowReplayerSuite(t *testing.T) { 51 s := new(workflowReplayerSuite) 52 suite.Run(t, s) 53 } 54 55 func (s *workflowReplayerSuite) SetupTest() { 56 s.Assertions = require.New(s.T()) 57 58 s.logger = getTestLogger(s.T()) 59 s.replayer = NewWorkflowReplayerWithOptions(ReplayOptions{ 60 ContextPropagators: []ContextPropagator{ 61 NewStringMapPropagator([]string{testHeader}), 62 }, 63 }) 64 s.replayer.RegisterWorkflow(testReplayWorkflow) 65 s.replayer.RegisterWorkflow(testReplayWorkflowLocalActivity) 66 s.replayer.RegisterWorkflow(testReplayWorkflowContextPropagator) 67 s.replayer.RegisterWorkflow(testReplayWorkflowFromFile) 68 s.replayer.RegisterWorkflow(testReplayWorkflowFromFileParent) 69 s.replayer.RegisterWorkflow(localActivitiesCallingOptionsWorkflow{s.T()}.Execute) 70 } 71 72 func (s *workflowReplayerSuite) TestReplayWorkflowHistory_Full() { 73 err := s.replayer.ReplayWorkflowHistory(s.logger, getTestReplayWorkflowFullHistory(s.T())) 74 s.NoError(err) 75 } 76 77 func (s *workflowReplayerSuite) TestReplayWorkflowHistory_Full_ResultMisMatch() { 78 fullHistory := getTestReplayWorkflowFullHistory(s.T()) 79 completedEvent := fullHistory.Events[len(fullHistory.Events)-1] 80 s.Equal(shared.EventTypeWorkflowExecutionCompleted, completedEvent.GetEventType()) 81 completedEvent.WorkflowExecutionCompletedEventAttributes.Result = []byte("some random result") 82 83 err := s.replayer.ReplayWorkflowHistory(s.logger, fullHistory) 84 s.Error(err) 85 } 86 87 func (s *workflowReplayerSuite) TestReplayWorkflowHistory_Full_ContinueAsNew() { 88 fullHistory := getTestReplayWorkflowFullHistory(s.T()) 89 completedEventIdx := len(fullHistory.Events) - 1 90 s.Equal(shared.EventTypeWorkflowExecutionCompleted, fullHistory.Events[completedEventIdx].GetEventType()) 91 fullHistory.Events[completedEventIdx] = createTestEventWorkflowExecutionContinuedAsNew(int64(completedEventIdx+1), nil) 92 93 err := s.replayer.ReplayWorkflowHistory(s.logger, fullHistory) 94 s.NoError(err) 95 } 96 97 func (s *workflowReplayerSuite) TestReplayWorkflowHistory_Partial_WithDecisionEvents() { 98 err := s.replayer.ReplayWorkflowHistory(s.logger, getTestReplayWorkflowPartialHistoryWithDecisionEvents(s.T())) 99 s.NoError(err) 100 } 101 102 func (s *workflowReplayerSuite) TestReplayWorkflowHistory_Partial_NoDecisionEvents() { 103 err := s.replayer.ReplayWorkflowHistory(s.logger, getTestReplayWorkflowPartialHistoryNoDecisionEvents(s.T())) 104 s.NoError(err) 105 } 106 107 func (s *workflowReplayerSuite) TestReplayWorkflowHistory_LocalActivity() { 108 err := s.replayer.ReplayWorkflowHistory(s.logger, getTestReplayWorkflowLocalActivityHistory(s.T())) 109 s.NoError(err) 110 } 111 112 func (s *workflowReplayerSuite) TestReplayWorkflowHistory_LocalActivity_Result_Mismatch() { 113 err := s.replayer.ReplayWorkflowHistory(s.logger, getTestReplayWorkflowLocalActivityResultMismatchHistory(s.T())) 114 s.Error(err) 115 } 116 117 func (s *workflowReplayerSuite) TestReplayWorkflowHistory_LocalActivity_Activity_Type_Mismatch() { 118 err := s.replayer.ReplayWorkflowHistory(s.logger, getTestReplayWorkflowLocalActivityTypeMismatchHistory(s.T())) 119 s.Error(err) 120 } 121 122 func (s *workflowReplayerSuite) TestReplayWorkflowHistory_ContextPropagator() { 123 err := s.replayer.ReplayWorkflowHistory(s.logger, getTestReplayWorkflowContextPropagatorHistory(s.T())) 124 s.NoError(err) 125 } 126 127 func (s *workflowReplayerSuite) TestReplayWorkflowHistoryFromFileLocalActivities() { 128 err := s.replayer.ReplayWorkflowHistoryFromJSONFile(s.logger, "testdata/localActivities.json") 129 s.NoError(err) 130 } 131 132 func (s *workflowReplayerSuite) TestReplayWorkflowHistoryFromFileParent() { 133 err := s.replayer.ReplayWorkflowHistoryFromJSONFile(s.logger, "testdata/parentWF.json") 134 s.NoError(err) 135 } 136 137 func (s *workflowReplayerSuite) TestReplayWorkflowHistoryFromFile() { 138 err := s.replayer.ReplayWorkflowHistoryFromJSONFile(s.logger, "testdata/sampleHistory.json") 139 s.NoError(err) 140 } 141 142 func testReplayWorkflow(ctx Context) error { 143 ao := ActivityOptions{ 144 ScheduleToStartTimeout: time.Second, 145 StartToCloseTimeout: time.Second, 146 } 147 ctx = WithActivityOptions(ctx, ao) 148 err := ExecuteActivity(ctx, "testActivity").Get(ctx, nil) 149 if err != nil { 150 GetLogger(ctx).Error("activity failed with error.", zap.Error(err)) 151 panic("Failed workflow") 152 } 153 return err 154 } 155 156 func testReplayWorkflowLocalActivity(ctx Context) error { 157 ao := LocalActivityOptions{ 158 ScheduleToCloseTimeout: time.Second, 159 } 160 ctx = WithLocalActivityOptions(ctx, ao) 161 err := ExecuteLocalActivity(ctx, testActivity).Get(ctx, nil) 162 if err != nil { 163 GetLogger(ctx).Error("activity failed with error.", zap.Error(err)) 164 panic("Failed workflow") 165 } 166 return err 167 } 168 169 func testReplayWorkflowContextPropagator(ctx Context) error { 170 value := ctx.Value(contextKey(testHeader)) 171 if val, ok := value.(string); ok && val != "" { 172 testReplayWorkflow(ctx) 173 return nil 174 } 175 176 return errors.New("context propagator is not setup correctly for workflow replayer") 177 } 178 179 func testReplayWorkflowFromFile(ctx Context) error { 180 ao := ActivityOptions{ 181 ScheduleToStartTimeout: time.Minute, 182 StartToCloseTimeout: time.Minute, 183 HeartbeatTimeout: 20 * time.Second, 184 WaitForCancellation: true, 185 } 186 ctx = WithActivityOptions(ctx, ao) 187 err := ExecuteActivity(ctx, "testActivityMultipleArgs", 2, "test", true).Get(ctx, nil) 188 if err != nil { 189 GetLogger(ctx).Error("activity failed with error.", zap.Error(err)) 190 panic("Failed workflow") 191 } 192 return err 193 } 194 195 func testReplayWorkflowFromFileParent(ctx Context) error { 196 execution := GetWorkflowInfo(ctx).WorkflowExecution 197 childID := fmt.Sprintf("child_workflow:%v", execution.RunID) 198 cwo := ChildWorkflowOptions{ 199 WorkflowID: childID, 200 ExecutionStartToCloseTimeout: time.Minute, 201 } 202 ctx = WithChildWorkflowOptions(ctx, cwo) 203 var result string 204 cwf := ExecuteChildWorkflow(ctx, testReplayWorkflowFromFile) 205 f1 := cwf.SignalChildWorkflow(ctx, "test-signal", "test-data") 206 err := f1.Get(ctx, nil) 207 if err != nil { 208 return err 209 } 210 return cwf.Get(ctx, &result) 211 } 212 213 func testActivity(ctx context.Context) error { 214 return nil 215 } 216 217 type localActivitiesCallingOptionsWorkflow struct { 218 t *testing.T 219 } 220 221 func (w localActivitiesCallingOptionsWorkflow) Execute(ctx Context, input []byte) (result []byte, err error) { 222 ao := LocalActivityOptions{ 223 ScheduleToCloseTimeout: time.Second, 224 } 225 ctx = WithLocalActivityOptions(ctx, ao) 226 227 // By functions. 228 err = ExecuteLocalActivity(ctx, testActivityByteArgs, input).Get(ctx, nil) 229 require.NoError(w.t, err, err) 230 231 err = ExecuteLocalActivity(ctx, testActivityMultipleArgs, 2, []string{"test"}, true).Get(ctx, nil) 232 require.NoError(w.t, err, err) 233 234 err = ExecuteLocalActivity(ctx, testActivityNoResult, 2, "test").Get(ctx, nil) 235 require.NoError(w.t, err, err) 236 237 err = ExecuteLocalActivity(ctx, testActivityNoContextArg, 2, "test").Get(ctx, nil) 238 require.NoError(w.t, err, err) 239 240 f := ExecuteLocalActivity(ctx, testActivityReturnByteArray) 241 var r []byte 242 err = f.Get(ctx, &r) 243 require.NoError(w.t, err, err) 244 require.Equal(w.t, []byte("testActivity"), r) 245 246 f = ExecuteLocalActivity(ctx, testActivityReturnInt) 247 var rInt int 248 err = f.Get(ctx, &rInt) 249 require.NoError(w.t, err, err) 250 require.Equal(w.t, 5, rInt) 251 252 f = ExecuteLocalActivity(ctx, testActivityReturnString) 253 var rString string 254 err = f.Get(ctx, &rString) 255 256 require.NoError(w.t, err, err) 257 require.Equal(w.t, "testActivity", rString) 258 259 f = ExecuteLocalActivity(ctx, testActivityReturnEmptyString) 260 var r2String string 261 err = f.Get(ctx, &r2String) 262 require.NoError(w.t, err, err) 263 require.Equal(w.t, "", r2String) 264 265 f = ExecuteLocalActivity(ctx, testActivityReturnEmptyStruct) 266 var r2Struct testActivityResult 267 err = f.Get(ctx, &r2Struct) 268 require.NoError(w.t, err, err) 269 require.Equal(w.t, testActivityResult{}, r2Struct) 270 271 f = ExecuteLocalActivity(ctx, testActivityReturnNilStructPtr) 272 var rStructPtr *testActivityResult 273 err = f.Get(ctx, &rStructPtr) 274 require.NoError(w.t, err, err) 275 require.True(w.t, rStructPtr == nil) 276 277 f = ExecuteLocalActivity(ctx, testActivityReturnStructPtr) 278 err = f.Get(ctx, &rStructPtr) 279 require.NoError(w.t, err, err) 280 require.Equal(w.t, *rStructPtr, testActivityResult{Index: 10}) 281 282 f = ExecuteLocalActivity(ctx, testActivityReturnNilStructPtrPtr) 283 var rStruct2Ptr **testActivityResult 284 err = f.Get(ctx, &rStruct2Ptr) 285 require.NoError(w.t, err, err) 286 require.True(w.t, rStruct2Ptr == nil) 287 288 f = ExecuteLocalActivity(ctx, testActivityReturnStructPtrPtr) 289 err = f.Get(ctx, &rStruct2Ptr) 290 require.NoError(w.t, err, err) 291 require.True(w.t, **rStruct2Ptr == testActivityResult{Index: 10}) 292 293 return []byte("Done"), nil 294 } 295 296 func getTestReplayWorkflowFullHistory(t *testing.T) *shared.History { 297 return &shared.History{ 298 Events: []*shared.HistoryEvent{ 299 createTestEventWorkflowExecutionStarted(1, &shared.WorkflowExecutionStartedEventAttributes{ 300 WorkflowType: &shared.WorkflowType{Name: common.StringPtr("go.uber.org/cadence/internal.testReplayWorkflow")}, 301 TaskList: &shared.TaskList{Name: common.StringPtr(testTaskList)}, 302 Input: testEncodeFunctionArgs(t, getDefaultDataConverter()), 303 }), 304 createTestEventDecisionTaskScheduled(2, &shared.DecisionTaskScheduledEventAttributes{}), 305 createTestEventDecisionTaskStarted(3), 306 createTestEventDecisionTaskCompleted(4, &shared.DecisionTaskCompletedEventAttributes{}), 307 createTestEventActivityTaskScheduled(5, &shared.ActivityTaskScheduledEventAttributes{ 308 ActivityId: common.StringPtr("0"), 309 ActivityType: &shared.ActivityType{Name: common.StringPtr("testActivity")}, 310 TaskList: &shared.TaskList{Name: &testTaskList}, 311 }), 312 createTestEventActivityTaskStarted(6, &shared.ActivityTaskStartedEventAttributes{ 313 ScheduledEventId: common.Int64Ptr(5), 314 }), 315 createTestEventActivityTaskCompleted(7, &shared.ActivityTaskCompletedEventAttributes{ 316 ScheduledEventId: common.Int64Ptr(5), 317 StartedEventId: common.Int64Ptr(6), 318 }), 319 createTestEventDecisionTaskScheduled(8, &shared.DecisionTaskScheduledEventAttributes{}), 320 createTestEventDecisionTaskStarted(9), 321 createTestEventDecisionTaskCompleted(10, &shared.DecisionTaskCompletedEventAttributes{ 322 ScheduledEventId: common.Int64Ptr(8), 323 StartedEventId: common.Int64Ptr(9), 324 }), 325 createTestEventWorkflowExecutionCompleted(11, &shared.WorkflowExecutionCompletedEventAttributes{ 326 DecisionTaskCompletedEventId: common.Int64Ptr(10), 327 }), 328 }, 329 } 330 } 331 332 func getTestReplayWorkflowPartialHistoryWithDecisionEvents(t *testing.T) *shared.History { 333 return &shared.History{ 334 Events: []*shared.HistoryEvent{ 335 createTestEventWorkflowExecutionStarted(1, &shared.WorkflowExecutionStartedEventAttributes{ 336 WorkflowType: &shared.WorkflowType{Name: common.StringPtr("go.uber.org/cadence/internal.testReplayWorkflow")}, 337 TaskList: &shared.TaskList{Name: common.StringPtr(testTaskList)}, 338 Input: testEncodeFunctionArgs(t, getDefaultDataConverter()), 339 }), 340 createTestEventDecisionTaskScheduled(2, &shared.DecisionTaskScheduledEventAttributes{}), 341 createTestEventDecisionTaskStarted(3), 342 createTestEventDecisionTaskCompleted(4, &shared.DecisionTaskCompletedEventAttributes{}), 343 createTestEventActivityTaskScheduled(5, &shared.ActivityTaskScheduledEventAttributes{ 344 ActivityId: common.StringPtr("0"), 345 ActivityType: &shared.ActivityType{Name: common.StringPtr("testActivity-fm")}, 346 TaskList: &shared.TaskList{Name: &testTaskList}, 347 }), 348 }, 349 } 350 } 351 352 func getTestReplayWorkflowPartialHistoryNoDecisionEvents(t *testing.T) *shared.History { 353 return &shared.History{ 354 Events: []*shared.HistoryEvent{ 355 createTestEventWorkflowExecutionStarted(1, &shared.WorkflowExecutionStartedEventAttributes{ 356 WorkflowType: &shared.WorkflowType{Name: common.StringPtr("go.uber.org/cadence/internal.testReplayWorkflow")}, 357 TaskList: &shared.TaskList{Name: common.StringPtr(testTaskList)}, 358 Input: testEncodeFunctionArgs(t, getDefaultDataConverter()), 359 }), 360 createTestEventDecisionTaskScheduled(2, &shared.DecisionTaskScheduledEventAttributes{}), 361 createTestEventDecisionTaskStarted(3), 362 createTestEventDecisionTaskFailed(4, &shared.DecisionTaskFailedEventAttributes{ScheduledEventId: common.Int64Ptr(2)}), 363 createTestEventDecisionTaskScheduled(5, &shared.DecisionTaskScheduledEventAttributes{}), 364 createTestEventDecisionTaskStarted(6), 365 }, 366 } 367 } 368 369 func getTestReplayWorkflowMismatchHistory(t *testing.T) *shared.History { 370 return &shared.History{ 371 Events: []*shared.HistoryEvent{ 372 createTestEventWorkflowExecutionStarted(1, &shared.WorkflowExecutionStartedEventAttributes{ 373 WorkflowType: &shared.WorkflowType{Name: common.StringPtr("go.uber.org/cadence/internal.testReplayWorkflow")}, 374 TaskList: &shared.TaskList{Name: common.StringPtr("taskList")}, 375 Input: testEncodeFunctionArgs(t, getDefaultDataConverter()), 376 }), 377 createTestEventDecisionTaskScheduled(2, &shared.DecisionTaskScheduledEventAttributes{}), 378 createTestEventDecisionTaskStarted(3), 379 createTestEventDecisionTaskCompleted(4, &shared.DecisionTaskCompletedEventAttributes{}), 380 createTestEventActivityTaskScheduled(5, &shared.ActivityTaskScheduledEventAttributes{ 381 ActivityId: common.StringPtr("0"), 382 ActivityType: &shared.ActivityType{Name: common.StringPtr("unknownActivityType")}, 383 TaskList: &shared.TaskList{Name: common.StringPtr("taskList")}, 384 }), 385 }, 386 } 387 } 388 389 func getTestReplayWorkflowLocalActivityHistory(t *testing.T) *shared.History { 390 return &shared.History{ 391 Events: []*shared.HistoryEvent{ 392 createTestEventWorkflowExecutionStarted(1, &shared.WorkflowExecutionStartedEventAttributes{ 393 WorkflowType: &shared.WorkflowType{Name: common.StringPtr("go.uber.org/cadence/internal.testReplayWorkflowLocalActivity")}, 394 TaskList: &shared.TaskList{Name: common.StringPtr(testTaskList)}, 395 Input: testEncodeFunctionArgs(t, getDefaultDataConverter()), 396 }), 397 createTestEventDecisionTaskScheduled(2, &shared.DecisionTaskScheduledEventAttributes{}), 398 createTestEventDecisionTaskStarted(3), 399 createTestEventDecisionTaskCompleted(4, &shared.DecisionTaskCompletedEventAttributes{}), 400 401 createTestEventLocalActivity(5, &shared.MarkerRecordedEventAttributes{ 402 MarkerName: common.StringPtr(localActivityMarkerName), 403 Details: createLocalActivityMarkerDataForTest("0", "go.uber.org/cadence/internal.testActivity"), 404 DecisionTaskCompletedEventId: common.Int64Ptr(4), 405 }), 406 407 createTestEventWorkflowExecutionCompleted(6, &shared.WorkflowExecutionCompletedEventAttributes{ 408 DecisionTaskCompletedEventId: common.Int64Ptr(4), 409 }), 410 }, 411 } 412 } 413 414 func getTestReplayWorkflowLocalActivityResultMismatchHistory(t *testing.T) *shared.History { 415 return &shared.History{ 416 Events: []*shared.HistoryEvent{ 417 createTestEventWorkflowExecutionStarted(1, &shared.WorkflowExecutionStartedEventAttributes{ 418 WorkflowType: &shared.WorkflowType{Name: common.StringPtr("go.uber.org/cadence/internal.testReplayWorkflowLocalActivity")}, 419 TaskList: &shared.TaskList{Name: common.StringPtr(testTaskList)}, 420 Input: testEncodeFunctionArgs(t, getDefaultDataConverter()), 421 }), 422 createTestEventDecisionTaskScheduled(2, &shared.DecisionTaskScheduledEventAttributes{}), 423 createTestEventDecisionTaskStarted(3), 424 createTestEventDecisionTaskCompleted(4, &shared.DecisionTaskCompletedEventAttributes{}), 425 426 createTestEventLocalActivity(5, &shared.MarkerRecordedEventAttributes{ 427 MarkerName: common.StringPtr(localActivityMarkerName), 428 Details: createLocalActivityMarkerDataForTest("0", ""), 429 DecisionTaskCompletedEventId: common.Int64Ptr(4), 430 }), 431 432 createTestEventWorkflowExecutionCompleted(6, &shared.WorkflowExecutionCompletedEventAttributes{ 433 Result: []byte("some-incorrect-result"), 434 DecisionTaskCompletedEventId: common.Int64Ptr(4), 435 }), 436 }, 437 } 438 } 439 440 func getTestReplayWorkflowLocalActivityTypeMismatchHistory(t *testing.T) *shared.History { 441 return &shared.History{ 442 Events: []*shared.HistoryEvent{ 443 createTestEventWorkflowExecutionStarted(1, &shared.WorkflowExecutionStartedEventAttributes{ 444 WorkflowType: &shared.WorkflowType{Name: common.StringPtr("go.uber.org/cadence/internal.testReplayWorkflowLocalActivity")}, 445 TaskList: &shared.TaskList{Name: common.StringPtr(testTaskList)}, 446 Input: testEncodeFunctionArgs(t, getDefaultDataConverter()), 447 }), 448 createTestEventDecisionTaskScheduled(2, &shared.DecisionTaskScheduledEventAttributes{}), 449 createTestEventDecisionTaskStarted(3), 450 createTestEventDecisionTaskCompleted(4, &shared.DecisionTaskCompletedEventAttributes{}), 451 452 createTestEventLocalActivity(5, &shared.MarkerRecordedEventAttributes{ 453 MarkerName: common.StringPtr(localActivityMarkerName), 454 Details: createLocalActivityMarkerDataForTest("0", "different-activity-type"), 455 DecisionTaskCompletedEventId: common.Int64Ptr(4), 456 }), 457 458 createTestEventWorkflowExecutionCompleted(6, &shared.WorkflowExecutionCompletedEventAttributes{ 459 DecisionTaskCompletedEventId: common.Int64Ptr(4), 460 }), 461 }, 462 } 463 } 464 465 func getTestReplayWorkflowContextPropagatorHistory(t *testing.T) *shared.History { 466 history := getTestReplayWorkflowFullHistory(t) 467 history.Events[0].WorkflowExecutionStartedEventAttributes.WorkflowType.Name = common.StringPtr("go.uber.org/cadence/internal.testReplayWorkflowContextPropagator") 468 history.Events[0].WorkflowExecutionStartedEventAttributes.Header = &shared.Header{ 469 Fields: map[string][]byte{testHeader: []byte("testValue")}, 470 } 471 return history 472 } 473 474 func createLocalActivityMarkerDataForTest(activityID, activityType string) []byte { 475 lamd := localActivityMarkerData{ 476 ActivityID: activityID, 477 ActivityType: activityType, 478 ReplayTime: time.Now(), 479 } 480 481 // encode marker data 482 markerData, err := encodeArg(nil, lamd) 483 if err != nil { 484 panic(fmt.Sprintf("error encoding local activity marker data: %v", err)) 485 } 486 return markerData 487 }