Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow multiple schedules in a cron workflow #12616

Merged
merged 9 commits into from
Feb 19, 2024

Conversation

eduardodbr
Copy link
Member

@eduardodbr eduardodbr commented Feb 4, 2024

Fixes: #12493
Fixes: #9734

Motivation

This PR introduces a new CronWorkflow spec field Schedules, a list of cron schedules for a workflow to execute. It maintains backward compatibility with Spec.Schedule and adds a validation that ensures that both fields cannot be configured simultaneously.

Modifications

Controller

1 - This implementation tries to maintain the same behavior for the controller and operator. This biggest change was made to the cronFacadewhere a key can hold multiple cron entries instead of only one. This involved changing the signature for the method Load to return an array of cronWfOperationCtx since a key can hold multiple entries:

 func (f *cronFacade) Load(key string) ([]*cronWfOperationCtx, error). 

This method doesn't seem to be used anywhere and cronFacede is not exposed so I don't think there is any issue with this change. Let me know if you think otherwise.

2 - The controller is currently using the label cronworkflows.argoproj.io/last-used-schedule to store the current cron expression. This label is only used to check whether the cron expression has been updated since the last time the WF ran, so this PR changes it to be a comma-separated list of the schedules (if applicable). Whenever any of the schedules is updated the label will be changed.

Possible Issues

If two cron expressions overlap (for example the expressions */2 * * * * and */3 * * * * will overlap at minute 6,12,18,etc) only one WF will be executed because the second will be seen as a duplicate submission. I don't think there is an issue with this, but let me know if you think otherwise. This makes it a bit harder to create an E2E test that is deterministic. Because of that, I decided to not add an E2E test for this feature.

CLI

argo cron list was updated to show all schedules:

image

argo cron get was updated:

image

UI

NOTE: this is my first time ever doing react /typescript it is expected that the code lacks quality. Please suggest changes.

Cron Workflow list was updated to show all schedules:
image

Cron Workflow status viewer was also updated:

image

Cron Spec editor:

image

Verification

This change can be tested with the following cron workflow:

apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
  name: test-cron
spec:
  schedules:
  - "*/2 * * * *"
  - "*/3 * * * *"
  concurrencyPolicy: "Allow"
  startingDeadlineSeconds: 0
  workflowSpec:
    entrypoint: whalesay
    templates:
    - name: whalesay
      container:
        image: alpine:3.6
        command: [sh, -c]
        args: ["date; sleep 1"]

Signed-off-by: eduardodbr <eduardodbr@hotmail.com>
Signed-off-by: eduardodbr <eduardodbr@hotmail.com>
Signed-off-by: eduardodbr <eduardodbr@hotmail.com>
Signed-off-by: eduardodbr <eduardodbr@hotmail.com>
Signed-off-by: eduardodbr <eduardodbr@hotmail.com>
Signed-off-by: eduardodbr <eduardodbr@hotmail.com>
@eduardodbr
Copy link
Member Author

CI is failing because of #12596

Signed-off-by: eduardodbr <eduardodbr@hotmail.com>
@isubasinghe isubasinghe self-assigned this Feb 8, 2024
@tooptoop4
Copy link
Contributor

:shipit:

Copy link
Member

@terrytangyuan terrytangyuan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

Signed-off-by: eduardodbr <eduardodbr@hotmail.com>
Copy link
Member

@terrytangyuan terrytangyuan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM once conflicts are resolved

Signed-off-by: eduardodbr <eduardodbr@hotmail.com>
@terrytangyuan terrytangyuan merged commit 986b069 into argoproj:main Feb 19, 2024
27 checks passed
Copy link
Member

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: this is my first time ever doing react /typescript it is expected that the code lacks quality. Please suggest changes.

This seems to have not gotten a UI SME before merge. It's mostly good, but I left a few comments on improvements below. In particular, the possible unnecessary new line when schedules contains only a single schedule is something to check

Comment on lines +171 to +174
<>
<PrettySchedule schedule={schedule} />
<br />
</>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure that most of these instances can be simplified with a <p> paragraph element instead of a <> fragment + a <br /> line break

Suggested change
<>
<PrettySchedule schedule={schedule} />
<br />
</>
<p><PrettySchedule schedule={schedule} /></p>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also how does this look when schedules only contains a single schedule? It seems like there may be an unnecessary new line in that case since it is currently treated differently from schedule

Copy link
Member Author

@eduardodbr eduardodbr Feb 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this suggestion and it seems that replacing <> <br /> </> with:

  <p>
      <PrettySchedule schedule={schedule} />
  </p>

seems to be adding an unnecessary new line after the last schedule, as you can see here:

image

while the current behavior is shown as:

image

no new line

Copy link
Member

@agilgur5 agilgur5 Feb 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm we use <p> elements in the new markdown title & description feature. Might need to add similar CSS here (line-height, display, and vertical-align). That and it might not work properly if one column uses <p> while the other uses <br />.

Try that; if it doesn't work, I can take a look on my side

@@ -183,3 +199,18 @@ export function CronWorkflowList({match, location, history}: RouteComponentProps
</Page>
);
}

function getCronNextScheduledTime(spec: CronWorkflowSpec): Date {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this naming is quite confusing -- getNextScheduledTime already operates on cron schedules (and uses cron-parser directly and is imported from shared/cron.ts) so adding a function with Cron in it sounds identical.

It looks like you wrote "Cron" because it operates on the CronWorkflowSpec -- "Spec" would be better naming in that case.
Or "Multi" can differentiate it from the other one, since this operates on multiple schedules

Suggested change
function getCronNextScheduledTime(spec: CronWorkflowSpec): Date {
function getSpecNextScheduledTime(spec: CronWorkflowSpec): Date {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This func calculates the next scheduled time for the combination of cron expressions configured in the cron workflow spec, it handles both"schedule" and "schedules". Changing it to getSpecNextScheduledTime seems fine to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea the word "Cron" was ambiguous in this case, whereas "Spec" is very specific

@@ -21,6 +21,7 @@ export interface CronWorkflowSpec {
successfulJobsHistoryLimit?: number;
failedJobsHistoryLimit?: number;
timezone?: string;
schedules?: string[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes more sense on line 18 right below schedule

Comment on lines +159 to +162
<>
{schedule}
<br />
</>
Copy link
Member

@agilgur5 agilgur5 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<>
{schedule}
<br />
</>
<p>{schedule}</p>

same as above

Comment on lines +28 to +31
<>
<code>{schedule}</code> <PrettySchedule schedule={schedule} />
<br />
</>
Copy link
Member

@agilgur5 agilgur5 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<>
<code>{schedule}</code> <PrettySchedule schedule={schedule} />
<br />
</>
<p><code>{schedule}</code> <PrettySchedule schedule={schedule} /><p/>

same as above

Copy link
Member

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also clients (CLI and UI) should probably have updated labels: Schedule -> Schedules.

@@ -19,7 +19,18 @@ export function CronWorkflowStatusViewer({spec, status}: {spec: CronWorkflowSpec
title: 'Schedule',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be plural now, no?

Suggested change
title: 'Schedule',
title: 'Schedules',

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion here. It can be a schedule or multiple schedules, depending on the configuration. If you configure schedule instead of multiple schedules it will show a single cron expression:

Schedules:  */2 * * * *

which is also weird. Do you think it makes sense to show schedule or schedules based on how may cron expressions were configured?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I considered that being the case. My thought process included:

  • we might eventually deprecate and remove schedule if we have schedules, to simplify the code / remove tech debt etc.
  • Argo and k8s already have fields that are plural even if there is only 1 element in the list, for instance: labels, volumes, volumeMounts, sidecars, templates, tasks, steps, nodes, etc

"Schedule(s)" would be an in between option, although it is inconsistent with other 1 element lists, per above

@@ -12,8 +12,22 @@ export function CronWorkflowSpecEditor({onChange, spec}: {spec: CronWorkflowSpec
<div className='row white-box__details-row'>
<div className='columns small-3'>Schedule</div>
Copy link
Member

@agilgur5 agilgur5 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above on plurality

Suggested change
<div className='columns small-3'>Schedule</div>
<div className='columns small-3'>Schedules</div>

Copy link
Member

@agilgur5 agilgur5 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 138 should probably also be Schedules now.

I can't comment on the line since GH does not allow commenting on unchanged lines that are not close to a diff

Copy link
Member

@agilgur5 agilgur5 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar on line 69 below, SCHEDULE -> SCHEDULES

@@ -73,7 +73,7 @@ func getCronWorkflowGet(cwf *wfv1.CronWorkflow) string {
out += fmt.Sprintf(fmtStr, "Name:", cwf.ObjectMeta.Name)
out += fmt.Sprintf(fmtStr, "Namespace:", cwf.ObjectMeta.Namespace)
out += fmt.Sprintf(fmtStr, "Created:", humanize.Timestamp(cwf.ObjectMeta.CreationTimestamp.Time))
out += fmt.Sprintf(fmtStr, "Schedule:", cwf.Spec.Schedule)
out += fmt.Sprintf(fmtStr, "Schedule:", cwf.Spec.GetScheduleString())
Copy link
Member

@agilgur5 agilgur5 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
out += fmt.Sprintf(fmtStr, "Schedule:", cwf.Spec.GetScheduleString())
out += fmt.Sprintf(fmtStr, "Schedules:", cwf.Spec.GetScheduleString())

similar to above

Copy link
Member

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CronWorkflows docs also need updating around multiple schedules, with version specifiers (see #12581 for examples)

@@ -61,6 +63,8 @@ type CronWorkflowSpec struct {
WorkflowMetadata *metav1.ObjectMeta `json:"workflowMetadata,omitempty" protobuf:"bytes,9,opt,name=workflowMeta"`
// StopStrategy defines if the cron workflow will stop being triggered once a certain condition has been reached, involving a number of runs of the workflow
StopStrategy *StopStrategy `json:"stopStrategy,omitempty" protobuf:"bytes,10,opt,name=stopStrategy"`
// Schedules is a list of schedules to run the Workflow in Cron format
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Schedules is a list of schedules to run the Workflow in Cron format
// v3.6 and after: Schedules is a list of schedules to run the Workflow in Cron format

This should ideally have a version specifier

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants