// setNullableFields sets fields that are nullable in API requests.
if s.Schedule.Policies == nil {
s.Schedule.Policies = &schedulepb.SchedulePolicies{}
}
s.Schedule.State = &schedulepb.ScheduleState{}
}
Frontier kind: Code frontier
unlabeled · c_9f03d7b21370
146 tests · 4987 LOC · 168 files · introduces 0 tests · 240 LOC · 9 files
The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.
Introduced files, introduced tests, and structurally relevant concept specialization
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.
Every exact file and test below is linked only from the concept that introduces it.
go.temporal.io/server/chasm/lib/scheduler/TestAllowedBufferedStartsDiscountsRetainedHistory/beyond_recentActionCount_reduces_capacity_1:1go.temporal.io/server/chasm/lib/scheduler/TestAllowedBufferedStartsDiscountsRetainedHistory/buffer_full_of_actionable_starts_clamps_to_zerogo.temporal.io/server/chasm/lib/scheduler/TestAllowedBufferedStartsDiscountsRetainedHistory/empty_buffer_keeps_full_capacitygo.temporal.io/server/chasm/lib/scheduler/TestCompletedHistoryDoesNotConsumeBackfillCapacitygo.temporal.io/server/common/circuitbreaker/TestTSCBWithDynamicSettingsgo.temporal.io/server/common/dynamicconfig/TestDeepCopy_OtherReferenceTypes_Nilgo.temporal.io/server/service/matching/configs/TestQuotasSuite/TestAPIPrioritiesOrderedgo.temporal.io/server/service/matching/configs/TestQuotasSuite/TestAPIToPriorityMappingEvery collected test enters the hierarchy at exactly one concept.
No tests are introduced at this concept. Its intent tests are introduced by other concepts.
Every collected source range enters the hierarchy at exactly one concept.
9 files ranked by introduced lines: 240 introduced LOC across 55 ranges. Expand a file to inspect source; the > gutter marks introduced lines.
// setNullableFields sets fields that are nullable in API requests.
if s.Schedule.Policies == nil {
s.Schedule.Policies = &schedulepb.SchedulePolicies{}
}
s.Schedule.State = &schedulepb.ScheduleState{}
}
// decremented when an action can be taken. When decrement is false, no state
// is mutated.
scheduleState := s.Schedule.GetState()
// If paused, don't do anything.
if scheduleState.Paused {
return false
}
// If unlimited actions, allow.
return true
}
// Otherwise check and decrement limit.
}
func (s *Scheduler) getCompiledSpec(specBuilder *scheduler.SpecBuilder) (*scheduler.CompiledSpec, error) {
scheduler.go
s.validateCachedState()
// Cache compiled spec.
if s.compiledSpec == nil {
cspec, err := specBuilder.NewCompiledSpec(s.Schedule.Spec)
if err != nil {
return nil, err
}
}
}
// WorkflowID returns the Workflow ID given as part of the request spec.
// During start generation, nominal time is suffixed to this ID.
return s.Schedule.GetAction().GetStartWorkflow().GetWorkflowId()
}
return fmt.Sprintf("%s-%s", s.NamespaceId, s.ScheduleId)
}
func (s *Scheduler) identity() string {
}
policy := s.Schedule.GetPolicies().GetOverlapPolicy()
if policy == enumspb.SCHEDULE_OVERLAP_POLICY_UNSPECIFIED {
policy = enumspb.SCHEDULE_OVERLAP_POLICY_SKIP
}
return policy
}
// ConflictToken doesn't match its cacheConflictToken field. Validation is only
// as effective as the Scheduler's backing persisted state is up-to-date.
if s.cacheConflictToken != s.ConflictToken {
// Bust stale cached fields.
s.compiledSpec = nil
return false
}
s.hasMoreBackfills()
}
idleTime time.Duration,
nextWakeup time.Time,
if idleTime == 0 ||
s.isHeldOpen() ||
(!nextWakeup.IsZero() && s.useScheduledAction(false)) {
return time.Time{}, false
}
return s.idleDeadline(ctx, idleTime), true
}
return len(s.Backfillers) > 0
}
type schedulerActionResult struct {
}
}
executionStatusSearchAttribute.Value(s.executionStatus()),
chasm.SearchAttributeTemporalSchedulePaused.Value(s.Schedule.GetState().GetPaused()),
}
if !s.Closed {
if gen := s.Generator.Get(ctx); len(gen.FutureActionTimes) > 0 {
out = append(out, scheduleNextActionTimeSearchAttribute.Value(gen.FutureActionTimes[0].AsTime()))
}
if s.IdleCloseTime != nil {
out = append(out, scheduleIdleCloseTimeSearchAttribute.Value(s.IdleCloseTime.AsTime()))
}
runningWorkflowCount := int64(len(invoker.runningWorkflowExecutions()))
bufferedStartsCount := int64(len(invoker.GetBufferedStarts()) - len(invoker.recentActions()))
// Emitted even when zero so that exact and range queries both work.
out = append(out,
scheduleRunningWorkflowCountSearchAttribute.Value(runningWorkflowCount),
scheduleBufferedStartsCountSearchAttribute.Value(bufferedStartsCount),
)
}
}
func (s *Scheduler) ListInfo(
ctx chasm.Context,
spec := common.CloneProto(s.Schedule.Spec)
// Clear fields that are too large/not useful for the list view.
spec.TimezoneData = nil
// Limit the number of specs and exclusions stored on the memo.
spec.ExcludeStructuredCalendar = util.SliceHead(spec.ExcludeStructuredCalendar, listInfoSpecFieldLimit)
spec.Interval = util.SliceHead(spec.Interval, listInfoSpecFieldLimit)
spec.StructuredCalendar = util.SliceHead(spec.StructuredCalendar, listInfoSpecFieldLimit)
generator := s.Generator.Get(ctx)
invoker := s.Invoker.Get(ctx)
return &schedulepb.ScheduleListInfo{
Spec: spec,
WorkflowType: s.Schedule.Action.GetStartWorkflow().GetWorkflowType(),
Notes: s.Schedule.State.Notes,
Paused: s.Schedule.State.Paused,
RecentActions: invoker.recentActions(),
FutureActionTimes: generator.FutureActionTimes,
}
}
// startWorkflowSearchAttributes returns the search attributes to be applied to
_ chasm.TaskAttributes,
_ *schedulerpb.GeneratorTask,
scheduler := generator.Scheduler.Get(ctx)
logger := newTaggedLogger(g.baseLogger, scheduler)
metricsHandler := newTaggedMetricsHandler(g.metricsHandler, scheduler)
invoker := scheduler.Invoker.Get(ctx)
now := ctx.Now(generator)
generator.getOrCreateEventLog(ctx).LogEvent(ctx, "generatorTask executed")
// If we have no last processed time, this is a new schedule.
if generator.LastProcessedTime == nil {
createdAt := timestamppb.New(now)
generator.LastProcessedTime = createdAt
// If the high water mark is earlier than when a schedule was updated, we must skip any actions that hadn't
// yet been processed.
if scheduler.Info.GetUpdateTime().AsTime().After(generator.LastProcessedTime.AsTime()) {
generator_tasks.go
generator.LastProcessedTime = scheduler.Info.GetUpdateTime()
}
// Process time range between last high water mark and system time.
t2 := now.UTC()
if t2.Before(t1) {
logger.Error("time went backwards",
tag.Stringer("time", t1),
}
var limit *int
if tweakables.MaxBufferSize > 0 {
remaining := tweakables.MaxBufferSize - len(invoker.GetBufferedStarts())
limit = &remaining
}
// Generate BufferedStarts and determine the next HWM. Actions are skipped when
// they can't be taken (paused, or limited and without any remaining actions),
// and dropped when the buffer is full.
scheduler,
t1, t2,
scheduler.overlapPolicy(),
scheduler.WorkflowID(),
"",
false,
limit,
)
if err != nil {
// An error here should be impossible, send to the DLQ.
return queueerrors.NewUnprocessableTaskError(
}
// Only system log on the first drop, as it's likely that a case that overruns
// will continue to overrun.
// to paused schedules vs. real work. Each fire while paused advances the
// HWM without buffering anything.
if scheduler.Schedule.State.Paused {
metricsHandler.Counter(metrics.ScheduleGeneratorPausedTicks.Name()).Record(1)
}
// Enqueue newly-generated buffered starts.
invoker.EnqueueBufferedStarts(ctx, result.BufferedStarts)
}
// Write the new high water mark and future action times.
generator.UpdateFutureActionTimes(ctx, g.specBuilder)
// Schedule the next timer task. Three outcomes are possible:
// - isIdle: the schedule is done; arm the idle task to close it.
// - NextWakeupTime is set: arm the next generator tick.
// - Neither: Hold open without a task. This requires both that
// isHeldOpen is true (paused or backfill pending) AND that no spec
// wakeup is available, e.g. a paused manual-only schedule. IdleTime=0
// also lands here. An external trigger (Patch.Unpause, Update, or a
// BackfillerTask's completion-time Generate call) revives us.
idleTimeTotal := tweakables.IdleTime
idleExpiration, isIdle := scheduler.getIdleExpiration(ctx, idleTimeTotal, result.NextWakeupTime)
if isIdle {
// Schedule is complete, no need for another buffer task. We keep the schedule's
// backing mutable state explicitly open for the idle period, during which the
// Not idle: the schedule has work again (or is being held open), so it's
// no longer pending an idle close.
if !result.NextWakeupTime.IsZero() {
// Keep the generator task perpetually scheduled. When paused, the next
// fire will simply advance the HWM without appending actions (handled in
// ProcessTimeRange).
generator.scheduleTask(ctx, result.NextWakeupTime)
} else {
// Hold open without a task: see the comment block above.
metricsHandler.Counter(metrics.SchedulerGeneratorLoopCompleted.Name()).Record(1)
}
}
attrs chasm.TaskInvocation,
_ *schedulerpb.GeneratorTask,
return validateTaskHighWaterMark(
generator.GetLastProcessedTime(),
attrs.ScheduledTime,
)
}
}
func newGeneratorWithState(ctx chasm.MutableContext, state *schedulerpb.GeneratorState) *Generator {
generator.go
generator := &Generator{
GeneratorState: state,
EventLog: chasm.NewComponentField(ctx, NewEventLog(ctx)),
}
return generator
}
// Generate immediately kicks off a new GeneratorTask. Used after updating the
// schedule specification.
g.scheduleTask(ctx, chasm.TaskScheduledTimeImmediate)
}
// scheduleTask schedules a GeneratorTask at the given time.
func (g *Generator) scheduleTask(ctx chasm.MutableContext, scheduledTime time.Time) {
generator.go
g.getOrCreateEventLog(ctx).LogEvent(ctx,
fmt.Sprintf("scheduled generatorTask for %s", scheduledTime.Format(time.RFC3339)))
ctx.AddTask(g, chasm.TaskAttributes{
ScheduledTime: scheduledTime,
}, &schedulerpb.GeneratorTask{})
}
return chasm.LifecycleStateRunning
}
// UpdateFutureActionTimes computes and stores the next scheduled action times.
ctx chasm.MutableContext,
specBuilder *scheduler.SpecBuilder,
futureTimes, err := g.computeFutureActionTimes(ctx, specBuilder)
if err != nil {
g.getOrCreateEventLog(ctx).LogEvent(ctx,
fmt.Sprintf("failed to update future action times: %v", err.Error()))
ctx chasm.Context,
specBuilder *scheduler.SpecBuilder,
sched := g.Scheduler.Get(ctx)
spec, err := sched.getCompiledSpec(specBuilder)
if err != nil {
return nil, err
}
if sched.Schedule.State.LimitedActions {
count = min(int(sched.Schedule.State.RemainingActions), recentActionCount)
}
// Start from max(now, updateTime) to ensure we skip times before the last update.
t := ctx.Now(g)
if updateTime := sched.Info.GetUpdateTime().AsTime(); updateTime.After(t) {
t = updateTime
}
res, err := spec.GetNextTime(sched.jitterSeed(), t)
if err != nil || res.Next.IsZero() {
// Over-excluded spec (limit) or end of schedule: return a partial list.
break
}
futureTimes = append(futureTimes, timestamppb.New(t))
}
}
}
func newInvokerWithState(ctx chasm.MutableContext, state *schedulerpb.InvokerState) *Invoker {
invoker.go
i := &Invoker{
InvokerState: state,
EventLog: chasm.NewComponentField(ctx, NewEventLog(ctx)),
}
return i
}
// EnqueueBufferedStarts adds new BufferedStarts to the invocation queue,
// runningWorkflowExecutions returns the list of workflow executions that
// have been started but not yet completed.
var running []*commonpb.WorkflowExecution
for _, start := range i.GetBufferedStarts() {
if start.GetRunId() != "" && start.GetCompleted() == nil {
running = append(running, &commonpb.WorkflowExecution{
// This includes both running workflows (with status RUNNING) and completed
// workflows (with their final status).
var results []*schedulepb.ScheduleActionResult
for _, start := range i.GetBufferedStarts() {
// Only include workflows that have been started (have a RunId).
if start.GetRunId() == "" {
func (*GeneratorTask) ProtoMessage() {}
mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_tasks_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
lastProcessedTime *timestamppb.Timestamp,
scheduledAt time.Time,
// Immediate tasks are always valid - they execute inline during the transaction.
if scheduledAt.IsZero() {
return true, nil
}
// If lastProcessedTime is not set, all scheduled tasks are valid.
if lastProcessedTime == nil || (lastProcessedTime.GetSeconds() == 0 && lastProcessedTime.GetNanos() == 0) {
util.go
return true, nil
}
// Scheduled tasks are valid if their time is after the high water mark.
}
// Value sets the integer value of the search attribute.
return SearchAttributeKeyValue{
Alias: s.alias,
Field: s.field,
Value: VisibilityValueInt64(value),
}
}
func (s SearchAttributeInt) typeMarker(_ int64) {}
}
eventLog, ok := g.EventLog.TryGet(ctx)
if ok {
return eventLog
}
eventLog = NewEventLog(ctx)
g.EventLog = chasm.NewComponentField(ctx, eventLog)
mi := &file_temporal_server_chasm_lib_scheduler_proto_v1_message_proto_msgTypes[5]
if x != nil {
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)