forked from temporalio/temporal
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspec_processor.go
More file actions
214 lines (188 loc) · 7.05 KB
/
Copy pathspec_processor.go
File metadata and controls
214 lines (188 loc) · 7.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package scheduler
import (
"time"
enumspb "go.temporal.io/api/enums/v1"
schedulespb "go.temporal.io/server/api/schedule/v1"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
schedulescommon "go.temporal.io/server/common/schedules"
legacyscheduler "go.temporal.io/server/service/worker/scheduler"
"google.golang.org/protobuf/types/known/timestamppb"
)
//go:generate mockgen -package $GOPACKAGE -source $GOFILE -destination spec_processor_mock.go
type (
// SpecProcessor is used by the Generator and Backfiller to generate buffered
// actions according to the schedule spec.
SpecProcessor interface {
// ProcessTimeRange generates buffered actions according to the schedule spec for
// the given time range.
//
// The parameter manual is propagated to the returned BufferedStarts. When the limit
// is set to a non-nil pointer, it will be decremented for each buffered start, and
// the function will return early should limit reach 0.
//
// If backfillID is set, it will be used to generate request IDs.
ProcessTimeRange(
scheduler *Scheduler,
start, end time.Time,
overlapPolicy enumspb.ScheduleOverlapPolicy,
workflowID string,
backfillID string,
manual bool,
limit *int,
) (*ProcessedTimeRange, error)
// NextTime provides a peek at the next time in the spec following 'after'.
NextTime(scheduler *Scheduler, after time.Time) (legacyscheduler.GetNextTimeResult, error)
}
SpecProcessorImpl struct {
config *Config
metricsHandler metrics.Handler
logger log.Logger
specBuilder *legacyscheduler.SpecBuilder
}
ProcessedTimeRange struct {
NextWakeupTime time.Time
LastActionTime time.Time
BufferedStarts []*schedulespb.BufferedStart
// DroppedCount is the number of actions that would have been buffered but
// were dropped due to the limit being reached. Only populated when a limit
// is provided.
DroppedCount int64
}
)
func NewSpecProcessor(
config *Config,
metricsHandler metrics.Handler,
logger log.Logger,
specBuilder *legacyscheduler.SpecBuilder,
) *SpecProcessorImpl {
return &SpecProcessorImpl{
config: config,
metricsHandler: metricsHandler,
logger: logger,
specBuilder: specBuilder,
}
}
func (s *SpecProcessorImpl) ProcessTimeRange(
scheduler *Scheduler,
start, end time.Time,
overlapPolicy enumspb.ScheduleOverlapPolicy,
workflowID string,
backfillID string,
manual bool,
limit *int,
) (*ProcessedTimeRange, error) {
tweakables := s.config.Tweakables(scheduler.Namespace)
metricsHandler := newTaggedMetricsHandler(s.metricsHandler, scheduler)
overlapPolicy = scheduler.resolveOverlapPolicy(overlapPolicy)
s.logger.Debug("ProcessTimeRange",
tag.Time("start", start),
tag.Time("end", end),
tag.Any("overlap-policy", overlapPolicy),
tag.Bool("manual", manual))
// Peek at paused/remaining actions state and don't bother if we're not going to
// take an action now. (Don't count as missed catchup window either.)
// Skip over entire time range if paused or no actions can be taken.
//
// Manual (backfill/patch) runs are always buffered here.
if !scheduler.useScheduledAction(false) && !manual {
// Use end as last action time so that we don't reprocess time spent paused.
next, err := s.NextTime(scheduler, end)
if err != nil {
return nil, err
}
return &ProcessedTimeRange{
NextWakeupTime: next.Next,
LastActionTime: end,
BufferedStarts: nil,
}, nil
}
catchupWindow := catchupWindow(scheduler, tweakables)
// lastAction is used to set the high water mark for future ProcessTimeRange
// invocations. The code below will set a "last action" even when none is taken,
// simply to indicate that processing can permanently skip that period of time
// (e.g., it was prior to an update or past a catchup).
lastAction := end
var next legacyscheduler.GetNextTimeResult
var err error
var bufferedStarts []*schedulespb.BufferedStart
var droppedCount int64
recordedGenerateLatency := false
limitReached := false
for next, err = s.NextTime(scheduler, start); err == nil && (!next.Next.IsZero() && !next.Next.After(end)); next, err = s.NextTime(scheduler, next.Next) {
lastAction = next.Next
if scheduler.Info.UpdateTime.AsTime().After(next.Next) && !manual {
// If we've received an update that took effect after the LastProcessedTime high
// water mark, discard actions that were scheduled to kick off before the update.
// Skip this check for manual (backfill) actions since they explicitly request
// past times.
s.logger.Info("ProcessBuffer skipped an action due to update time",
tag.Time("updateTime", scheduler.Info.UpdateTime.AsTime()),
tag.Time("droppedActionTime", next.Next))
continue
}
// Record generate latency only for the first action in the batch to
// avoid inflating the metric when catching up over a large time range.
if !manual && !recordedGenerateLatency {
metricsHandler.Timer(metrics.ScheduleGenerateLatency.Name()).
Record(end.Sub(next.Next))
recordedGenerateLatency = true
}
if !manual && end.Sub(next.Next) > catchupWindow {
s.logger.Info("Schedule missed catchup window",
tag.Time("now", end),
tag.Time("time", next.Next))
// Action's nominal time was already past the catchup window when
// the generator processed the time range. It was never buffered.
metricsHandler.WithTags(
metrics.StringTag(metrics.ScheduleMissedReasonTag, metrics.ScheduleMissedReasonNotBuffered),
).Counter(metrics.ScheduleMissedCatchupWindow.Name()).Record(1)
scheduler.Info.MissedCatchupWindow++
continue
}
if limitReached {
droppedCount++
continue
}
bufferedStarts = append(bufferedStarts, &schedulespb.BufferedStart{
NominalTime: timestamppb.New(next.Nominal),
ActualTime: timestamppb.New(next.Next),
OverlapPolicy: overlapPolicy,
Manual: manual,
RequestId: generateRequestID(scheduler, backfillID, next.Nominal, next.Next),
WorkflowId: schedulescommon.GenerateWorkflowID(workflowID, next.Nominal),
})
if limit != nil {
if (*limit)--; *limit <= 0 {
// For manual (backfill) actions, break immediately so the caller
// can retry later. For automated actions, continue to count dropped.
if manual {
break
}
limitReached = true
}
}
}
return &ProcessedTimeRange{
NextWakeupTime: next.Next,
LastActionTime: lastAction,
BufferedStarts: bufferedStarts,
}, nil
}
func catchupWindow(s *Scheduler, tweakables Tweakables) time.Duration {
cw := s.Schedule.GetPolicies().GetCatchupWindow()
if cw == nil {
return tweakables.DefaultCatchupWindow
}
return max(cw.AsDuration(), tweakables.MinCatchupWindow)
}
// NextTime returns the next time result, or an error if the schedule cannot be compiled.
func (s *SpecProcessorImpl) NextTime(scheduler *Scheduler, after time.Time) (legacyscheduler.GetNextTimeResult, error) {
spec, err := scheduler.getCompiledSpec(s.specBuilder)
if err != nil {
s.logger.Error("Invalid schedule", tag.Error(err))
return legacyscheduler.GetNextTimeResult{}, err
}
return spec.GetNextTime(scheduler.jitterSeed(), after), nil
}