package scheduler import ( "errors" "fmt" "oc-scheduler/conf" "oc-scheduler/infrastructure/planner" "oc-scheduler/infrastructure/scheduling_resources" infUtils "oc-scheduler/infrastructure/utils" "strings" "time" "cloud.o-forge.io/core/oc-lib/models/booking" "cloud.o-forge.io/core/oc-lib/models/common/enum" "cloud.o-forge.io/core/oc-lib/models/common/pricing" "cloud.o-forge.io/core/oc-lib/models/utils" "cloud.o-forge.io/core/oc-lib/models/workflow" "cloud.o-forge.io/core/oc-lib/models/workflow_execution" "cloud.o-forge.io/core/oc-lib/tools" "github.com/google/uuid" "github.com/robfig/cron" ) // Schedule holds a resolved start/end pair for a single execution slot. type Schedule struct { Start time.Time End *time.Time } // WorkflowSchedule is the flying session object for a scheduling interaction. // It is never persisted; it lives only for the duration of a WebSocket check session. type WorkflowSchedule struct { UUID string `json:"id" validate:"required"` Workflow *workflow.Workflow `json:"workflow,omitempty"` WorkflowExecution []*workflow_execution.WorkflowExecution `json:"workflow_executions,omitempty"` Message string `json:"message,omitempty"` Warning string `json:"warning,omitempty"` Start time.Time `json:"start" validate:"required,ltfield=End"` End *time.Time `json:"end,omitempty"` DurationS float64 `json:"duration_s" default:"-1"` Cron string `json:"cron,omitempty"` BookingMode booking.BookingMode `json:"booking_mode,omitempty"` SelectedInstances workflow.ConfigItem `json:"selected_instances"` SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"` SelectedBuyings workflow.ConfigItem `json:"selected_buyings"` SelectedStrategies workflow.ConfigItem `json:"selected_strategies"` SelectedBillingStrategy pricing.BillingStrategy `json:"selected_billing_strategy"` // Confirm, when true, triggers Schedule() to confirm the drafts held by this session. Confirm bool `json:"confirm,omitempty"` } // CheckResult is the response payload for an availability check. type CheckResult struct { Available bool `json:"available"` Start time.Time `json:"start"` End *time.Time `json:"end,omitempty"` Warnings []string `json:"warnings,omitempty"` Preemptible bool `json:"preemptible,omitempty"` // SchedulingID is the session UUID the client must supply when confirming. SchedulingID string `json:"scheduling_id,omitempty"` } // --------------------------------------------------------------------------- // Check — availability // --------------------------------------------------------------------------- // Check verifies whether the requested slot is available across all resource peers. func (ws *WorkflowSchedule) Check(wfID string, asap bool, preemption bool, request *tools.APIRequest) (*CheckResult, error) { fmt.Println("CHECK", asap, "/", preemption) obj, code, err := workflow.NewAccessor(request).LoadOne(wfID) if code != 200 || err != nil { msg := "could not load workflow " + wfID if err != nil { msg += ": " + err.Error() } return nil, errors.New(msg) } wf := obj.(*workflow.Workflow) prepLead := conf.GetConfig().PrepLead() start := ws.Start if asap || start.IsZero() { start = time.Now().UTC().Add(prepLead) } else if start.Before(time.Now().UTC().Add(prepLead)) { // Explicit date is within the prep window — impossible to guarantee on time. return nil, fmt.Errorf( "start date %s is too soon: minimum lead time is %s (earliest: %s)", start.Format(time.RFC3339), prepLead, time.Now().UTC().Add(prepLead).Format(time.RFC3339), ) } end := ws.End if end == nil { if ws.DurationS > 0 { e := start.Add(time.Duration(ws.DurationS * float64(time.Second))) end = &e } else { _, longest, _, _, planErr := wf.Planify( start, nil, ws.SelectedInstances, ws.SelectedPartnerships, ws.SelectedBuyings, ws.SelectedStrategies, int(ws.BookingMode), nil, request, ) if planErr == nil && longest > 0 { e := start.Add(time.Duration(longest) * time.Second) end = &e } } } checkables := infUtils.CollectBookingResources(wf, ws.SelectedInstances) start, end, available, preemptible, warnings := planner.GetPlannerService().FindDate(wfID, checkables, start, end, preemption, asap) return &CheckResult{ Start: start, End: end, Available: available, Preemptible: preemptible, Warnings: warnings, }, nil } // --------------------------------------------------------------------------- // GetBuyAndBook — generate scheduling resources // --------------------------------------------------------------------------- // GetBuyAndBook runs Planify to generate the purchases and bookings for this session. func (ws *WorkflowSchedule) GetBuyAndBook(wfID string, request *tools.APIRequest) ( bool, *workflow.Workflow, []*workflow_execution.WorkflowExecution, []scheduling_resources.SchedulerObject, []scheduling_resources.SchedulerObject, error, ) { res, code, err := workflow.NewAccessor(request).LoadOne(wfID) if code != 200 { return false, nil, nil, nil, nil, errors.New("could not load the workflow: " + err.Error()) } wf := res.(*workflow.Workflow) isPreemptible, longest, priceds, wf, err := wf.Planify( ws.Start, ws.End, ws.SelectedInstances, ws.SelectedPartnerships, ws.SelectedBuyings, ws.SelectedStrategies, int(ws.BookingMode), nil, request, ) if err != nil { return false, wf, nil, nil, nil, err } ws.DurationS = longest ws.Message = "We estimate that the workflow will start at " + ws.Start.String() + " and last " + fmt.Sprintf("%v", ws.DurationS) + " seconds." if ws.End != nil && ws.Start.Add(time.Duration(longest)*time.Second).After(*ws.End) { ws.Warning = "The workflow may be too long to be executed in the given time frame, we will try to book it anyway\n" } execs, err := ws.GenerateExecutions(wf, isPreemptible) if err != nil { return false, wf, nil, nil, nil, err } var purchased, bookings []scheduling_resources.SchedulerObject for _, exec := range execs { for _, obj := range exec.Buy(ws.SelectedBillingStrategy, ws.UUID, wfID, priceds) { purchased = append(purchased, scheduling_resources.ToSchedulerObject(tools.PURCHASE_RESOURCE, obj)) } for _, obj := range exec.Book(ws.UUID, wfID, priceds) { bookings = append(bookings, scheduling_resources.ToSchedulerObject(tools.BOOKING, obj)) } } return true, wf, execs, purchased, bookings, nil } // --------------------------------------------------------------------------- // GenerateExecutions / GetDates // --------------------------------------------------------------------------- // GenerateExecutions expands the cron schedule into WorkflowExecution instances. func (ws *WorkflowSchedule) GenerateExecutions(wf *workflow.Workflow, isPreemptible bool) ([]*workflow_execution.WorkflowExecution, error) { dates, err := ws.GetDates() if err != nil { return nil, err } var executions []*workflow_execution.WorkflowExecution for _, date := range dates { obj := &workflow_execution.WorkflowExecution{ AbstractObject: utils.AbstractObject{ UUID: uuid.New().String(), Name: wf.Name + "_execution_" + date.Start.String(), }, Priority: 1, ExecutionsID: ws.UUID, ExecDate: date.Start, EndDate: date.End, State: enum.DRAFT, WorkflowID: wf.GetID(), } if ws.BookingMode != booking.PLANNED { obj.Priority = 0 } if ws.BookingMode == booking.PREEMPTED && isPreemptible { obj.Priority = 7 } executions = append(executions, obj) } return executions, nil } // GetDates parses the cron expression and returns execution date slots. func (ws *WorkflowSchedule) GetDates() ([]Schedule, error) { var schedule []Schedule if len(ws.Cron) > 0 { if ws.End == nil { return schedule, errors.New("a cron task should have an end date") } if ws.DurationS <= 0 { ws.DurationS = ws.End.Sub(ws.Start).Seconds() } cronStr := strings.Split(ws.Cron, " ") if len(cronStr) < 6 { return schedule, errors.New("Bad cron message: (" + ws.Cron + "). Should be at least ss mm hh dd MM dw") } subCron := strings.Join(cronStr[:6], " ") specParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) sched, err := specParser.Parse(subCron) if err != nil { return schedule, errors.New("Bad cron message: " + err.Error()) } for s := sched.Next(ws.Start); !s.IsZero() && s.Before(*ws.End); s = sched.Next(s) { e := s.Add(time.Duration(ws.DurationS) * time.Second) schedule = append(schedule, Schedule{Start: s, End: &e}) } } else { schedule = append(schedule, Schedule{Start: ws.Start, End: ws.End}) } return schedule, nil }