Skip to content
This repository has been archived by the owner on Jun 8, 2022. It is now read-only.

refactor dependency engine to reuse and fit in reconcile loop #74

Merged
merged 11 commits into from
Jul 14, 2020

Conversation

hongchaodeng
Copy link
Member

Per discussion with @wonderflow , the previous DAGManager loop has unexpected corner cases like handling orphan-ed AppConfigs and dealing with Workload's traits and scopes.

A better implementation solution would be to base on existing reconcile loop and find the dependencies in each reconcile. If dependency is not satisfied, it would requeue and retry later.

Copy link
Member

@wonderflow wonderflow left a comment

Choose a reason for hiding this comment

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

I hope to see some test cases to at least cover below cases:

  1. component with traits and scopes won't be create if dependency conditions not meet requirement. AppConfig.status will indicate this is processing and what components are pending on.
  2. component with traits and scopes all created after dependency conditions meet.
  3. trait who need input from component
  4. component who need input from trait

pkg/controller/oam/applicationconfiguration/render.go Outdated Show resolved Hide resolved
pkg/controller/oam/applicationconfiguration/render.go Outdated Show resolved Hide resolved
pkg/controller/oam/applicationconfiguration/render.go Outdated Show resolved Hide resolved
pkg/dependency/dag.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@ryanzhang-oss ryanzhang-oss left a comment

Choose a reason for hiding this comment

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

IIRC, this code does not support the case of the following scenario
workloadA -> workloadA's trait

and we need more unit tests

pkg/dependency/dag.go Outdated Show resolved Hide resolved
pkg/dependency/dag.go Outdated Show resolved Hide resolved
pkg/controller/oam/applicationconfiguration/render.go Outdated Show resolved Hide resolved
pkg/controller/oam/applicationconfiguration/render.go Outdated Show resolved Hide resolved
@@ -392,8 +390,126 @@ func addDataOutputsToDAG(dag *dependency.DAG, outs []v1alpha2.DataOutput, obj *u
}
}

func addDataInputsToDAG(dag *dependency.DAG, ins []v1alpha2.DataInput, obj *unstructured.Unstructured, attaches []unstructured.Unstructured) {
func (r *components) handleDependency(ctx context.Context, w *Workload, acc v1alpha2.ApplicationConfigurationComponent, dag *dependency.DAG) (bool, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can scope have dependency too?

Copy link
Member Author

Choose a reason for hiding this comment

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

Currently there is no plan to support dependency w.r.t. scope. Only workloads and traits.
This is because I do not want to bloat the feature to cover scenarios that do not have real world use cases.

return true, nil
}

func (r *components) handleDataInput(ctx context.Context, ins []v1alpha2.DataInput, dag *dependency.DAG, obj *unstructured.Unstructured) (bool, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a UT for this function

pkg/controller/oam/applicationconfiguration/render.go Outdated Show resolved Hide resolved

switch m.Operator {
case v1alpha2.ConditionEqual:
if m.Value != checkVal {
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we assume it's a primitive type like string or int?

Copy link
Member Author

Choose a reason for hiding this comment

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

Currently only string type is supported. This should fix all the use cases as mentioned in the design doc.
We don't see any use case for int yet.

Copy link
Member Author

Choose a reason for hiding this comment

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

For complex objects, we are going to provide more high level functions like AutoBinding to fill the gap.

pkg/controller/oam/applicationconfiguration/render.go Outdated Show resolved Hide resolved
pkg/controller/oam/applicationconfiguration/render.go Outdated Show resolved Hide resolved
@hongchaodeng
Copy link
Member Author

@wonderflow The AppConfig now will show the dependency status:

  status:
    dependency:
      unsatisfied:
      - from:
          apiVersion: example.com/v1
          kind: Foo
          name: source
          fieldPath: status.key
        to:
          apiVersion: example.com/v1
          kind: Foo
          name: sink
          fieldPaths:
          - spec.key

@hongchaodeng
Copy link
Member Author

The reason why previous implementation just take the whole Workload and its Traits when dependency not met is just to show the prototype.

Now the current implementation has changed that to mark each Workload and Trait whether they are "ready" to apply. This is a more robust solution and easier to debug along with dependency status.

Signed-off-by: Hongchao Deng <[email protected]>
@hongchaodeng
Copy link
Member Author

Added tests:

--- PASS: TestMatchValue (0.00s)
    --- PASS: TestMatchValue/No_conditions_with_nonempty_value_should_match (0.00s)
    --- PASS: TestMatchValue/eq_condition_with_different_value_should_not_match (0.00s)
    --- PASS: TestMatchValue/notEmpty_condition_with_nonempty_value_should_match (0.00s)
    --- PASS: TestMatchValue/eq_condition_with_different_value_from_FieldPath_should_not_match (0.00s)
    --- PASS: TestMatchValue/No_conditions_with_empty_value_should_not_match (0.00s)
    --- PASS: TestMatchValue/eq_condition_with_same_value_should_match (0.00s)
    --- PASS: TestMatchValue/notEq_condition_with_different_value_should_match (0.00s)
    --- PASS: TestMatchValue/notEq_condition_with_same_value_should_not_match (0.00s)
    --- PASS: TestMatchValue/notEmpty_condition_with_empty_value_should_not_match (0.00s)
    --- PASS: TestMatchValue/eq_condition_with_same_value_from_FieldPath_should_match (0.00s)

--- PASS: TestDependency (0.01s)
    --- PASS: TestDependency/DataOutputName_doesn't_exist (0.00s)
    --- PASS: TestDependency/Workload_depends_on_another_Workload_that's_ready (0.00s)
    --- PASS: TestDependency/Workload_depends_on_a_Trait_that's_ready (0.00s)
    --- PASS: TestDependency/Trait_depends_on_a_Workload_that's_ready (0.00s)
    --- PASS: TestDependency/Trait_depends_on_another_unreadyTrait_that's_unready (0.00s)
    --- PASS: TestDependency/Trait_depends_on_another_unreadyTrait_that's_ready (0.00s)
    --- PASS: TestDependency/Workload_depends_on_another_Workload_that's_unready (0.00s)
    --- PASS: TestDependency/Workload_depends_on_a_Trait_that's_unready (0.00s)
    --- PASS: TestDependency/Trait_depends_on_a_Workload_that's_unready (0.00s)

Signed-off-by: Hongchao Deng <[email protected]>
@hongchaodeng
Copy link
Member Author

Since the initial submission the following comments has been addressed:

  • Add status to indicate the progress of dependency waiting
  • Handle unreadiness of Workload/Trait resources individually instead of bundling them all together.
  • Add UT to cover all major cases as shown above

@My-pleasure is trying to write e2e tests to cover dependency user story, which will be up for review early next week. I would like to merge this PR first instead of coupling his commits here.

Copy link
Member

@wonderflow wonderflow left a comment

Choose a reason for hiding this comment

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

Generally LGTM, some comments hope to be fixed. Thanks @hongchaodeng, really great work!

ping @ryanzhang-oss please also help review it

@@ -8,6 +8,8 @@ spec:
kind: Foo
metadata:
name: source
status:
key: test
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 not exist at initial time.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let me comment it out. Having this without typing every time is very convenient.


// Unready indicates whether this resource is unready to apply
// because of dependency not ready
Unready bool
Copy link
Member

Choose a reason for hiding this comment

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

What if the outside Unready is true while the Unready field here is false? Does it mean workload will apply before all trait is ready?

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 does not have any indication on ordering. It will comply with whatever ordering being applied but skip unready ones.

if err := a.client.Apply(ctx, wl.Workload, ao...); err != nil {
return errors.Wrapf(err, errFmtApplyWorkload, wl.Workload.GetName())
if !wl.Unready {
err := a.client.Apply(ctx, wl.Workload, ao...)
Copy link
Member

Choose a reason for hiding this comment

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

I think we can't apply workload before its trait still unready. They should work as a whole.

Copy link
Member Author

Choose a reason for hiding this comment

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

Why do we assume such logic in dependency? What's the reasoning?

@wonderflow
Copy link
Member

wonderflow commented Jul 13, 2020

I think this PR also fixes #89, right? @hongchaodeng

@hongchaodeng
Copy link
Member Author

I think this PR also fixes #89, right?

Yes. The inputs handling logic is AND-ed.

	uds := make([]v1alpha2.UnstaifiedDependency, 0)
	for _, in := range ins {
		s, ok := dag.Sources[in.ValueFrom.DataOutputName]
		val, ready, err := r.checkSourceReady(ctx, s)
		if !ready {
			uds = append(uds, makeUnsatisfiedDependency(obj, s, in))
			return uds, nil
		}
...
	}

Signed-off-by: Hongchao Deng <[email protected]>

// Unready indicates whether this resource is unready to apply
// because of dependency not ready
Unready bool
Copy link
Collaborator

Choose a reason for hiding this comment

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

a double negative expression like !Unready is usually not easy to read. Why not just called "ready" and flip the boolean value?

Copy link
Collaborator

Choose a reason for hiding this comment

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

the "ready" thing seems to be a good candidate to abstract out so it can applied to any other resources like scope

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason is that the default of bool is false.
The default of any Workload/Trait is actually ready (= !unready). The dataInputs field might not exist so it might not go into the dependency logic.

What about change the name to HasDep?

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 have changed this to HasDep. I think having a default false value is still very important in preventing mistaken init place.

// DependencyFromObject represents the object that dependency data comes from.
type DependencyFromObject struct {
runtimev1alpha1.TypedReference `json:",inline"`
FieldPath string `json:"fieldPath,omitempty"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we not allow dependencies from multiple objects?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is allowed.
The status will show each dependency for the same object in individual item:

status:
  dependencies:
  - from:
      name: source-a
    to:
      name: sink-a
  - from:
      name: source-b
    to:
      name: sink-a

But actually in current implementation, the runtime will save computing power and not print the second one as long as it finds any one dependency for a sink not satisfied.

pkg/controller/v1alpha2/applicationconfiguration/render.go Outdated Show resolved Hide resolved
return nil, errors.Wrapf(ErrDataOutputNotExist, "DataOutputName (%s)", in.ValueFrom.DataOutputName)
}
val, ready, err := r.checkSourceReady(ctx, s)
if err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

so this means that a "ready" source can be fliped to be "unready" due to network or some other errors. I think we at least should put a TODO here to fine-tune that user experience in the next PR.

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 is the reason we put the logic into reconcile and let controller runtime deal with error handling.
If such transient error happens, the AppConfig status will show it and from users' point of view they should be able to see it and wait for the network hiccup to pass away.

return nil
}

func (r *components) checkSourceReady(ctx context.Context, s *dagSource) (string, bool, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't seem to see a Unit test here, maybe we can ask the student to add it?

Copy link
Member Author

Choose a reason for hiding this comment

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

Let me answer all questions about the UT here.
All of the mentioned methods are covered in TestDependency. I have used go tool cover to check that.

pkg/controller/v1alpha2/applicationconfiguration/apply.go Outdated Show resolved Hide resolved
Signed-off-by: Hongchao Deng <[email protected]>
Signed-off-by: Hongchao Deng <[email protected]>
@My-pleasure
Copy link
Contributor

LGTM, I have verified this and will follow up with a e2e test PR shortly.

@hongchaodeng hongchaodeng merged commit 2968167 into crossplane:master Jul 14, 2020
@hongchaodeng hongchaodeng deleted the dep branch July 14, 2020 05:00
wonderflow added a commit to wonderflow/oam-kubernetes-runtime that referenced this pull request Jul 21, 2020
1. component versioning implemented by crossplane#35
2. dependency implemented by crossplane#74 and crossplane#53
3. rollout model don't need any change, it's best practice

Signed-off-by: 天元 <[email protected]>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants