-
Notifications
You must be signed in to change notification settings - Fork 80
refactor dependency engine to reuse and fit in reconcile loop #74
Conversation
There was a problem hiding this 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:
- 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.
- component with traits and scopes all created after dependency conditions meet.
- trait who need input from component
- component who need input from trait
There was a problem hiding this 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
@@ -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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
|
||
switch m.Operator { | ||
case v1alpha2.ConditionEqual: | ||
if m.Value != checkVal { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Signed-off-by: Hongchao Deng <[email protected]>
Signed-off-by: Hongchao Deng <[email protected]>
Signed-off-by: Hongchao Deng <[email protected]>
Signed-off-by: Hongchao Deng <[email protected]>
90e8e8a
to
5e7acd2
Compare
@wonderflow The AppConfig now will show the dependency status:
|
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]>
Signed-off-by: Hongchao Deng <[email protected]>
c4a0f21
to
98f51af
Compare
Signed-off-by: Hongchao Deng <[email protected]>
Added tests:
|
Signed-off-by: Hongchao Deng <[email protected]>
Since the initial submission the following comments has been addressed:
@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. |
There was a problem hiding this 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
examples/dependency/demo.yaml
Outdated
@@ -8,6 +8,8 @@ spec: | |||
kind: Foo | |||
metadata: | |||
name: source | |||
status: | |||
key: test |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
I think this PR also fixes #89, right? @hongchaodeng |
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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"` |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
return nil, errors.Wrapf(ErrDataOutputNotExist, "DataOutputName (%s)", in.ValueFrom.DataOutputName) | ||
} | ||
val, ready, err := r.checkSourceReady(ctx, s) | ||
if err != nil { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Signed-off-by: Hongchao Deng <[email protected]>
dba25eb
to
b62b8fc
Compare
Signed-off-by: Hongchao Deng <[email protected]>
LGTM, I have verified this and will follow up with a e2e test PR shortly. |
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]>
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.