Skip to content

Commit

Permalink
feat(controller): Track N/M progress. See argoproj#2717 (argoproj#4194)
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Capras <alexcapras@gmail.com>
  • Loading branch information
alexec authored and alexcapras committed Nov 12, 2020
1 parent 032585f commit 4b7623c
Show file tree
Hide file tree
Showing 24 changed files with 325 additions and 19 deletions.
8 changes: 8 additions & 0 deletions api/openapi-spec/swagger.json
Expand Up @@ -3063,6 +3063,10 @@
"description": "PodIP captures the IP of the pod for daemoned steps",
"type": "string"
},
"progress": {
"description": "Progress to completion",
"type": "string"
},
"resourcesDuration": {
"description": "ResourcesDuration is indicative, but not accurate, resource duration. This is populated when the nodes completes.",
"type": "object",
Expand Down Expand Up @@ -4627,6 +4631,10 @@
"description": "Phase a simple, high-level summary of where the workflow is in its lifecycle.",
"type": "string"
},
"progress": {
"description": "Progress to completion",
"type": "string"
},
"resourcesDuration": {
"description": "ResourcesDuration is the total for the workflow",
"type": "object",
Expand Down
1 change: 1 addition & 0 deletions cmd/argo/commands/get.go
Expand Up @@ -145,6 +145,7 @@ func printWorkflowHelper(wf *wfv1.Workflow, getArgs getFlags) string {
if wf.Status.EstimatedDuration > 0 {
out += fmt.Sprintf(fmtStr, "EstimatedDuration:", humanize.Duration(wf.Status.EstimatedDuration.ToDuration()))
}
out += fmt.Sprintf(fmtStr, "Progress:", wf.Status.Progress)
}
if !wf.Status.ResourcesDuration.IsZero() {
out += fmt.Sprintf(fmtStr, "ResourcesDuration:", wf.Status.ResourcesDuration)
Expand Down
10 changes: 10 additions & 0 deletions cmd/argo/commands/get_test.go
Expand Up @@ -109,6 +109,16 @@ func TestStatusToNodeFieldSelector(t *testing.T) {
}

func Test_printWorkflowHelper(t *testing.T) {
t.Run("Progress", func(t *testing.T) {
var wf wfv1.Workflow
testutil.MustUnmarshallYAML(`
status:
phase: Running
progress: 1/2
`, &wf)
output := printWorkflowHelper(&wf, getFlags{})
assert.Regexp(t, `Progress: *1/2`, output)
})
t.Run("EstimatedDuration", func(t *testing.T) {
var wf wfv1.Workflow
testutil.MustUnmarshallYAML(`
Expand Down
2 changes: 2 additions & 0 deletions docs/fields.md
Expand Up @@ -672,6 +672,7 @@ WorkflowStatus contains overall status information about a workflow
|`outputs`|[`Outputs`](#outputs)|Outputs captures output values and artifact locations produced by the workflow via global outputs|
|`persistentVolumeClaims`|`Array<`[`Volume`](#volume)`>`|PersistentVolumeClaims tracks all PVCs that were created as part of the io.argoproj.workflow.v1alpha1. The contents of this list are drained at the end of the workflow.|
|`phase`|`string`|Phase a simple, high-level summary of where the workflow is in its lifecycle.|
|`progress`|`string`|Progress to completion|
|`resourcesDuration`|`Map< integer , int64 >`|ResourcesDuration is the total for the workflow|
|`startedAt`|[`Time`](#time)|Time at which this workflow started|
|`storedTemplates`|[`Template`](#template)|StoredTemplates is a mapping between a template ref and the node's status.|
Expand Down Expand Up @@ -1996,6 +1997,7 @@ NodeStatus contains status information about an individual node in the workflow
|`outputs`|[`Outputs`](#outputs)|Outputs captures output parameter values and artifact locations produced by this template invocation|
|`phase`|`string`|Phase a simple, high-level summary of where the node is in its lifecycle. Can be used as a state machine.|
|`podIP`|`string`|PodIP captures the IP of the pod for daemoned steps|
|`progress`|`string`|Progress to completion|
|`resourcesDuration`|`Map< integer , int64 >`|ResourcesDuration is indicative, but not accurate, resource duration. This is populated when the nodes completes.|
|`startedAt`|[`Time`](#time)|Time at which this node started|
|~`storedTemplateID`~|~`string`~|~StoredTemplateID is the ID of stored template.~ DEPRECATED: This value is not used anymore.|
Expand Down
26 changes: 26 additions & 0 deletions docs/progress.md
@@ -0,0 +1,26 @@
# Workflow Progress

![alpha](assets/alpha.svg)

> v2.12 and after
When you run a workflow, the controller will report on its progress.

We define progress as two numbers, `N/M` such that `0 <= N <= M and 0 <= M <= 1`.

* `N` is the number of completed tasks.
* `M` is the total number of tasks.

E.g. `0/0`, `0/1` or `50/100`.

Unlike [estimated duration](estimated-duration.md), progress is deterministic. I.e. it will be the same for each workflow, regardless of any problems.

Progress for each node is calculated as follows:

2. For a pod node either `1/1` if completed or `0/1` otherwise.
3. For non-leaf nodes, the sum of its children.

For a whole workflow's, progress is the sum of all its leaf nodes.

!!! Warning
`M` will increase during workflow run each time a node is added to the graph.
4 changes: 4 additions & 0 deletions manifests/base/crds/full/argoproj.io_workflows.yaml
Expand Up @@ -7502,6 +7502,8 @@ spec:
type: string
podIP:
type: string
progress:
type: string
resourcesDuration:
additionalProperties:
format: int64
Expand Down Expand Up @@ -8490,6 +8492,8 @@ spec:
type: array
phase:
type: string
progress:
type: string
resourcesDuration:
additionalProperties:
format: int64
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Expand Up @@ -42,6 +42,7 @@ nav:
- artifact-repository-ref.md
- resource-duration.md
- estimated-duration.md
- progress.md
- workflow-creator.md
- synchronization.md
- workflow-of-workflows.md
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/workflow/v1alpha1/generated.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions pkg/apis/workflow/v1alpha1/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions pkg/apis/workflow/v1alpha1/progress.go
@@ -0,0 +1,44 @@
package v1alpha1

import (
"fmt"
"strconv"
"strings"
)

// Progress in N/M format. N is number of task complete. M is number of tasks.
type Progress string

func NewProgress(n, m int64) (Progress, bool) {
return ParseProgress(fmt.Sprintf("%v/%v", n, m))
}

func ParseProgress(s string) (Progress, bool) {
v := Progress(s)
return v, v.IsValid()
}

func (in Progress) parts() []string {
return strings.SplitN(string(in), "/", 2)
}

func (in Progress) N() int64 {
return parseInt64(in.parts()[0])
}

func (in Progress) M() int64 {
return parseInt64(in.parts()[1])
}

func (in Progress) Add(x Progress) Progress {
return Progress(fmt.Sprintf("%v/%v", in.N()+x.N(), in.M()+x.M()))
}

func (in Progress) IsValid() bool {
return in != "" && in.N() >= 0 && in.N() <= in.M() && in.M() > 0
}

func parseInt64(s string) int64 {
v, _ := strconv.ParseInt(s, 10, 64)
return v
}
28 changes: 28 additions & 0 deletions pkg/apis/workflow/v1alpha1/progress_test.go
@@ -0,0 +1,28 @@
package v1alpha1

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestProgress(t *testing.T) {
t.Run("ParseProgress", func(t *testing.T) {
_, ok := ParseProgress("")
assert.False(t, ok)
progress, ok := ParseProgress("0/1")
assert.True(t, ok)
assert.Equal(t, Progress("0/1"), progress)
})
t.Run("IsValid", func(t *testing.T) {
assert.False(t, Progress("").IsValid())
assert.False(t, Progress("/0").IsValid())
assert.False(t, Progress("0/").IsValid())
assert.False(t, Progress("0/0").IsValid())
assert.False(t, Progress("1/0").IsValid())
assert.True(t, Progress("0/1").IsValid())
})
t.Run("Add", func(t *testing.T) {
assert.Equal(t, Progress("1/2"), Progress("0/0").Add("1/2"))
})
}
19 changes: 6 additions & 13 deletions pkg/apis/workflow/v1alpha1/workflow_types.go
Expand Up @@ -68,15 +68,6 @@ const (
NodeTypeSuspend NodeType = "Suspend"
)

func (t NodeType) IsLeaf() bool {
switch t {
case NodeTypePod, NodeTypeRetry, NodeTypeSkipped, NodeTypeSuspend:
return true
default:
return false
}
}

// PodGCStrategy is the strategy when to delete completed pods for GC.
type PodGCStrategy string

Expand Down Expand Up @@ -1127,6 +1118,9 @@ type WorkflowStatus struct {
// EstimatedDuration in seconds.
EstimatedDuration EstimatedDuration `json:"estimatedDuration,omitempty" protobuf:"varint,16,opt,name=estimatedDuration,casttype=EstimatedDuration"`

// Progress to completion
Progress Progress `json:"progress,omitempty" protobuf:"bytes,17,opt,name=progress,casttype=Progress"`

// A human readable message indicating details about why the workflow is in this condition.
Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"`

Expand Down Expand Up @@ -1391,6 +1385,9 @@ type NodeStatus struct {
// EstimatedDuration in seconds.
EstimatedDuration EstimatedDuration `json:"estimatedDuration,omitempty" protobuf:"varint,24,opt,name=estimatedDuration,casttype=EstimatedDuration"`

// Progress to completion
Progress Progress `json:"progress,omitempty" protobuf:"bytes,26,opt,name=progress,casttype=Progress"`

// ResourcesDuration is indicative, but not accurate, resource duration. This is populated when the nodes completes.
ResourcesDuration ResourcesDuration `json:"resourcesDuration,omitempty" protobuf:"bytes,21,opt,name=resourcesDuration"`

Expand Down Expand Up @@ -1566,10 +1563,6 @@ func (n NodeStatus) GetDuration() time.Duration {
return n.FinishedAt.Sub(n.StartedAt.Time)
}

func (in *NodeStatus) IsLeaf() bool {
return in.Type.IsLeaf()
}

// S3Bucket contains the access information required for interfacing with an S3 bucket
type S3Bucket struct {
// Endpoint is the hostname of the bucket endpoint
Expand Down
36 changes: 36 additions & 0 deletions test/e2e/progress_test.go
@@ -0,0 +1,36 @@
// +build e2e

package e2e

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
"github.com/argoproj/argo/test/e2e/fixtures"
)

type ProgressSuite struct {
fixtures.E2ESuite
}

func (s *ProgressSuite) TestDefaultProgress() {
s.Given().
Workflow("@testdata/basic-workflow.yaml").
When().
SubmitWorkflow().
WaitForWorkflow().
Then().
ExpectWorkflow(func(t *testing.T, metadata *metav1.ObjectMeta, status *wfv1.WorkflowStatus) {
assert.Equal(t, wfv1.NodeSucceeded, status.Phase)
assert.Equal(t, wfv1.Progress("1/1"), status.Progress)
assert.Equal(t, wfv1.Progress("1/1"), status.Nodes[metadata.Name].Progress)
})
}

func TestProgressSuite(t *testing.T) {
suite.Run(t, new(ProgressSuite))
}
2 changes: 2 additions & 0 deletions ui/src/app/shared/services/workflows-service.ts
Expand Up @@ -33,6 +33,7 @@ export class WorkflowsService {
'items.status.finishedAt',
'items.status.startedAt',
'items.status.estimatedDuration',
'items.status.progress',
'items.spec.suspend'
]
) {
Expand Down Expand Up @@ -83,6 +84,7 @@ export class WorkflowsService {
'result.object.status.phase',
'result.object.status.startedAt',
'result.object.status.estimatedDuration',
'result.object.status.progress',
'result.type',
'result.object.metadata.labels',
'result.object.spec.suspend'
Expand Down
Expand Up @@ -64,6 +64,7 @@ export const WorkflowNodeSummary = (props: Props) => {
title: 'DURATION',
value: <Ticker>{now => <DurationPanel duration={nodeDuration(props.node, now)} phase={props.node.phase} estimatedDuration={props.node.estimatedDuration} />}</Ticker>
},
{title: 'PROGRESS', value: props.node.progress || '-'},
{
title: 'MEMOIZATION',
value: (
Expand Down
3 changes: 2 additions & 1 deletion ui/src/app/workflows/components/workflow-summary-panel.tsx
Expand Up @@ -46,7 +46,8 @@ export const WorkflowSummaryPanel = (props: {workflow: Workflow}) => (
estimatedDuration={props.workflow.status.estimatedDuration}
/>
)
}
},
{title: 'Progress', value: props.workflow.status.progress || '-'}
];
const creator = props.workflow.metadata.labels[labels.creator];
if (creator) {
Expand Down
Expand Up @@ -314,6 +314,7 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
<div className='columns small-2'>STARTED</div>
<div className='columns small-2'>FINISHED</div>
<div className='columns small-1'>DURATION</div>
<div className='columns small-1'>PROGRESS</div>
<div className='columns small-1'>DETAILS</div>
</div>
</div>
Expand Down
Expand Up @@ -56,9 +56,10 @@ export class WorkflowsRow extends React.Component<WorkflowsRowProps, WorkflowRow
<div className='columns small-2'>
<Timestamp date={wf.status.finishedAt} />
</div>
<div className='columns small-2'>
<div className='columns small-1'>
<Ticker>{() => <DurationPanel phase={wf.status.phase} duration={wfDuration(wf.status)} estimatedDuration={wf.status.estimatedDuration} />}</Ticker>
</div>
<div className='columns small-1'>{wf.status.progress || '-'}</div>
<div className='columns small-1'>
<div className='workflows-list__labels-container'>
<div
Expand Down
9 changes: 9 additions & 0 deletions ui/src/models/workflows.ts
Expand Up @@ -459,6 +459,11 @@ export interface NodeStatus {
*/
estimatedDuration?: number;

/**
* Progress as numerator/denominator.
*/
progress?: string;

/**
* How much resource was requested.
*/
Expand Down Expand Up @@ -564,6 +569,10 @@ export interface WorkflowStatus {
*/
estimatedDuration?: number;

/**
* Progress as numerator/denominator.
*/
progress?: string;
/**
* A human readable message indicating details about why the workflow is in this condition.
*/
Expand Down

0 comments on commit 4b7623c

Please sign in to comment.