|
| 1 | +package chasm |
| 2 | + |
| 3 | +import ( |
| 4 | + "time" |
| 5 | + |
| 6 | + persistencespb "go.temporal.io/server/api/persistence/v1" |
| 7 | + "go.temporal.io/server/service/history/tasks" |
| 8 | + "google.golang.org/protobuf/types/known/timestamppb" |
| 9 | +) |
| 10 | + |
| 11 | +// timeSkippingTaskNodes builds a tree with three CategoryTimer tasks already materialized |
| 12 | +// (PhysicalTaskStatus == Created): a side-effect timer on the root, a side-effect timer on a child, |
| 13 | +// and a pure timer on the root. The scheduled times are in the (virtual) future and carry no |
| 14 | +// destination, so taskCategory classifies the side-effect tasks as CategoryTimer. |
| 15 | +func (s *nodeSuite) timeSkippingTaskNodes() map[string]*persistencespb.ChasmNode { |
| 16 | + now := s.timeSource.Now() |
| 17 | + return map[string]*persistencespb.ChasmNode{ |
| 18 | + "": { |
| 19 | + Metadata: &persistencespb.ChasmNodeMetadata{ |
| 20 | + InitialVersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1}, |
| 21 | + LastUpdateVersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1}, |
| 22 | + Attributes: &persistencespb.ChasmNodeMetadata_ComponentAttributes{ |
| 23 | + ComponentAttributes: &persistencespb.ChasmComponentAttributes{ |
| 24 | + TypeId: testComponentTypeID, |
| 25 | + PureTasks: []*persistencespb.ChasmComponentAttributes_Task{ |
| 26 | + { |
| 27 | + TypeId: testPureTaskTypeID, |
| 28 | + ScheduledTime: timestamppb.New(now.Add(time.Hour)), |
| 29 | + VersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1}, |
| 30 | + VersionedTransitionOffset: 1, |
| 31 | + PhysicalTaskStatus: physicalTaskStatusCreated, |
| 32 | + }, |
| 33 | + }, |
| 34 | + SideEffectTasks: []*persistencespb.ChasmComponentAttributes_Task{ |
| 35 | + { |
| 36 | + TypeId: testSideEffectTaskTypeID, |
| 37 | + ScheduledTime: timestamppb.New(now.Add(time.Hour)), |
| 38 | + VersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1}, |
| 39 | + VersionedTransitionOffset: 2, |
| 40 | + PhysicalTaskStatus: physicalTaskStatusCreated, |
| 41 | + }, |
| 42 | + }, |
| 43 | + }, |
| 44 | + }, |
| 45 | + }, |
| 46 | + }, |
| 47 | + "SubComponent1": { |
| 48 | + Metadata: &persistencespb.ChasmNodeMetadata{ |
| 49 | + InitialVersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1}, |
| 50 | + LastUpdateVersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1}, |
| 51 | + Attributes: &persistencespb.ChasmNodeMetadata_ComponentAttributes{ |
| 52 | + ComponentAttributes: &persistencespb.ChasmComponentAttributes{ |
| 53 | + TypeId: testSubComponent1TypeID, |
| 54 | + SideEffectTasks: []*persistencespb.ChasmComponentAttributes_Task{ |
| 55 | + { |
| 56 | + TypeId: testSideEffectTaskTypeID, |
| 57 | + ScheduledTime: timestamppb.New(now.Add(2 * time.Hour)), |
| 58 | + VersionedTransition: &persistencespb.VersionedTransition{TransitionCount: 1}, |
| 59 | + VersionedTransitionOffset: 3, |
| 60 | + PhysicalTaskStatus: physicalTaskStatusCreated, |
| 61 | + }, |
| 62 | + }, |
| 63 | + }, |
| 64 | + }, |
| 65 | + }, |
| 66 | + }, |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +// TestRegenerateTasksForTimeSkipping_RestampsEachTimerExactlyOnce verifies a time-skipping |
| 71 | +// regeneration re-emits exactly one physical task per logical CategoryTimer task — two side-effect |
| 72 | +// timers plus a single pure timer (the earliest across the tree) — and not duplicates. This is the |
| 73 | +// "generated only once" guarantee: even though every task starts already Created, the re-stamp emits |
| 74 | +// each one exactly once. |
| 75 | +func (s *nodeSuite) TestRegenerateTasksForTimeSkipping_RestampsEachTimerExactlyOnce() { |
| 76 | + root, err := s.newTestTree(s.timeSkippingTaskNodes()) |
| 77 | + s.NoError(err) |
| 78 | + |
| 79 | + err = root.regenerateTasksForTimeSkipping() |
| 80 | + s.NoError(err) |
| 81 | + |
| 82 | + // 2 side-effect CategoryTimer tasks (root + SubComponent1) + 1 pure timer (earliest) = 3, all |
| 83 | + // CategoryTimer. Not 6 (would indicate double-emission) and not 2 (would indicate the no-break |
| 84 | + // re-stamp skipped already-Created tasks). |
| 85 | + s.Equal(3, s.nodeBackend.NumTasksAdded()) |
| 86 | + s.Len(s.nodeBackend.TasksByCategory[tasks.CategoryTimer], 3) |
| 87 | +} |
| 88 | + |
| 89 | +// TestCloseTransaction_DoesNotRegenerateAlreadyMaterializedTimers verifies the complementary |
| 90 | +// property: a plain CloseTransaction (no time-skipping transition, no user-state change) does NOT |
| 91 | +// re-emit timer tasks that are already materialized. This is what keeps the active cluster from |
| 92 | +// generating new timer tasks on every transaction — only a skip (via regenerateTasksForTimeSkipping) |
| 93 | +// re-stamps them. |
| 94 | +func (s *nodeSuite) TestCloseTransaction_DoesNotRegenerateAlreadyMaterializedTimers() { |
| 95 | + root, err := s.newTestTree(s.timeSkippingTaskNodes()) |
| 96 | + s.NoError(err) |
| 97 | + |
| 98 | + // Time skipping is not enabled (the mock backend's GetTimeSkippingInfo returns nil) and nothing was |
| 99 | + // mutated, so the normal generation loop must skip the already-Created timer tasks. |
| 100 | + _, err = root.CloseTransaction() |
| 101 | + s.NoError(err) |
| 102 | + s.Equal(0, s.nodeBackend.NumTasksAdded()) |
| 103 | +} |
0 commit comments