Skip to content

Commit

Permalink
feat(build): Add Build waiting condition
Browse files Browse the repository at this point in the history
* Add on Build.Status a "Scheduled" condition containing the reason for it's scheduling status

Ref apache#4542
  • Loading branch information
gansheer committed Feb 21, 2024
1 parent 6dbc035 commit 05ed327
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 94 deletions.
57 changes: 57 additions & 0 deletions e2e/builder/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

. "github.com/apache/camel-k/v2/e2e/support"
v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
corev1 "k8s.io/api/core/v1"
)

type kitOptions struct {
Expand Down Expand Up @@ -111,6 +112,15 @@ func TestKitMaxBuildLimit(t *testing.T) {
},
}, v1.BuildPhaseScheduling, v1.IntegrationKitPhaseNone)

// verify that last build is waiting
Eventually(BuildConditions(ns2, buildC), TestTimeoutMedium).ShouldNot(BeNil())
Eventually(
Build(ns2, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Status,
TestTimeoutShort).Should(Equal(corev1.ConditionFalse))
Eventually(
Build(ns2, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Reason,
TestTimeoutShort).Should(Equal(v1.BuildConditionWaitingReason))

var notExceedsMaxBuildLimit = func(runningBuilds int) bool {
return runningBuilds <= 2
}
Expand All @@ -131,10 +141,21 @@ func TestKitMaxBuildLimit(t *testing.T) {
// verify that all builds are successful
Eventually(BuildPhase(ns, buildA), TestTimeoutLong).Should(Equal(v1.BuildPhaseSucceeded))
Eventually(KitPhase(ns, buildA), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))

Eventually(BuildPhase(ns1, buildB), TestTimeoutLong).Should(Equal(v1.BuildPhaseSucceeded))
Eventually(KitPhase(ns1, buildB), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))

Eventually(BuildPhase(ns2, buildC), TestTimeoutLong).Should(Equal(v1.BuildPhaseSucceeded))
Eventually(KitPhase(ns2, buildC), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))

// verify that last build is scheduled
Eventually(BuildConditions(ns2, buildC), TestTimeoutMedium).ShouldNot(BeNil())
Eventually(
Build(ns2, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Status,
TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
Eventually(
Build(ns2, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Reason,
TestTimeoutShort).Should(Equal(v1.BuildConditionReadyReason))
})
})
})
Expand Down Expand Up @@ -187,6 +208,15 @@ func TestKitMaxBuildLimitFIFOStrategy(t *testing.T) {
},
}, v1.BuildPhaseScheduling, v1.IntegrationKitPhaseNone)

// verify that last build is waiting
Eventually(BuildConditions(ns, buildC), TestTimeoutMedium).ShouldNot(BeNil())
Eventually(
Build(ns, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Status,
TestTimeoutShort).Should(Equal(corev1.ConditionFalse))
Eventually(
Build(ns, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Reason,
TestTimeoutShort).Should(Equal(v1.BuildConditionWaitingReason))

var notExceedsMaxBuildLimit = func(runningBuilds int) bool {
return runningBuilds <= 2
}
Expand All @@ -211,6 +241,15 @@ func TestKitMaxBuildLimitFIFOStrategy(t *testing.T) {
Eventually(KitPhase(ns, buildB), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))
Eventually(BuildPhase(ns, buildC), TestTimeoutLong).Should(Equal(v1.BuildPhaseSucceeded))
Eventually(KitPhase(ns, buildC), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))

// verify that last build is scheduled
Eventually(BuildConditions(ns, buildC), TestTimeoutLong).ShouldNot(BeNil())
Eventually(
Build(ns, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Status,
TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
Eventually(
Build(ns, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Reason,
TestTimeoutShort).Should(Equal(v1.BuildConditionReadyReason))
})
}

Expand Down Expand Up @@ -261,6 +300,15 @@ func TestKitMaxBuildLimitDependencyMatchingStrategy(t *testing.T) {
},
}, v1.BuildPhaseScheduling, v1.IntegrationKitPhaseNone)

// verify that last build is waiting
Eventually(BuildConditions(ns, buildC), TestTimeoutMedium).ShouldNot(BeNil())
Eventually(
Build(ns, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Status,
TestTimeoutShort).Should(Equal(corev1.ConditionFalse))
Eventually(
Build(ns, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Reason,
TestTimeoutShort).Should(Equal(v1.BuildConditionWaitingReason))

var notExceedsMaxBuildLimit = func(runningBuilds int) bool {
return runningBuilds <= 2
}
Expand All @@ -285,6 +333,15 @@ func TestKitMaxBuildLimitDependencyMatchingStrategy(t *testing.T) {
Eventually(KitPhase(ns, buildB), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))
Eventually(BuildPhase(ns, buildC), TestTimeoutLong).Should(Equal(v1.BuildPhaseSucceeded))
Eventually(KitPhase(ns, buildC), TestTimeoutLong).Should(Equal(v1.IntegrationKitPhaseReady))

// verify that last build is scheduled
Eventually(BuildConditions(ns, buildC), TestTimeoutLong).ShouldNot(BeNil())
Eventually(
Build(ns, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Status,
TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
Eventually(
Build(ns, buildC)().Status.GetCondition(v1.BuildConditionScheduled).Reason,
TestTimeoutShort).Should(Equal(v1.BuildConditionReadyReason))
})
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/camel/v1/build_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,14 @@ const (
BuildPhaseInterrupted = "Interrupted"
// BuildPhaseError -- .
BuildPhaseError BuildPhase = "Error"

// BuildConditionScheduled --.
BuildConditionScheduled BuildConditionType = "Scheduled"

// BuildConditionReadyReason --.
BuildConditionReadyReason string = "Ready"
// BuildConditionWaitingReason --.
BuildConditionWaitingReason string = "Waiting"
)

// +genclient
Expand Down
51 changes: 42 additions & 9 deletions pkg/controller/build/build_monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"sync"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -31,14 +32,17 @@ import (
"github.com/apache/camel-k/v2/pkg/util/kubernetes"
)

const enqueuedMsg = "%s - the build (%s) gets enqueued"

var runningBuilds sync.Map

type Monitor struct {
maxRunningBuilds int32
buildOrderStrategy v1.BuildOrderStrategy
}

func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Build) (bool, error) {
func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Build) (bool, *v1.BuildCondition, error) {

var runningBuildsTotal int32
runningBuilds.Range(func(_, v interface{}) bool {
runningBuildsTotal++
Expand All @@ -54,24 +58,27 @@ func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Bui
}

if runningBuildsTotal >= bm.maxRunningBuilds {
reason := fmt.Sprintf(
"Maximum number of running builds (%d) exceeded",
runningBuildsTotal,
)
Log.WithValues("request-namespace", requestNamespace, "request-name", requestName, "max-running-builds-limit", runningBuildsTotal).
ForBuild(build).Infof("Maximum number of running builds (%d) exceeded - the build (%s) gets enqueued", runningBuildsTotal, build.Name)

ForBuild(build).Infof(enqueuedMsg, reason, build.Name)
// max number of running builds limit exceeded
return false, nil
return false, scheduledWaitingBuildcondition(build.Name, reason), nil
}

layout := build.Labels[v1.IntegrationKitLayoutLabel]

// Native builds can be run in parallel, as incremental images is not applicable.
if layout == v1.IntegrationKitLayoutNativeSources {
return true, nil
return true, scheduledReadyBuildcondition(build.Name), nil
}

// We assume incremental images is only applicable across images whose layout is identical
withCompatibleLayout, err := labels.NewRequirement(v1.IntegrationKitLayoutLabel, selection.Equals, []string{layout})
if err != nil {
return false, err
return false, nil, err
}

builds := &v1.BuildList{}
Expand All @@ -83,11 +90,12 @@ func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Bui
Selector: labels.NewSelector().Add(*withCompatibleLayout),
})
if err != nil {
return false, err
return false, nil, err
}

var reason string
allowed := true
condition := scheduledReadyBuildcondition(build.Name)
switch bm.buildOrderStrategy {
case v1.BuildOrderStrategyFIFO:
// Check on builds that have been created before the current build and grant precedence if any.
Expand Down Expand Up @@ -116,10 +124,11 @@ func (bm *Monitor) canSchedule(ctx context.Context, c ctrl.Reader, build *v1.Bui

if !allowed {
Log.WithValues("request-namespace", requestNamespace, "request-name", requestName, "order-strategy", bm.buildOrderStrategy).
ForBuild(build).Infof("%s - the build (%s) gets enqueued", reason, build.Name)
ForBuild(build).Infof(enqueuedMsg, reason, build.Name)
condition = scheduledWaitingBuildcondition(build.Name, reason)
}

return allowed, nil
return allowed, condition, nil
}

func monitorRunningBuild(build *v1.Build) {
Expand All @@ -129,3 +138,27 @@ func monitorRunningBuild(build *v1.Build) {
func monitorFinishedBuild(build *v1.Build) {
runningBuilds.Delete(types.NamespacedName{Namespace: build.Namespace, Name: build.Name}.String())
}

func scheduledReadyBuildcondition(buildName string) *v1.BuildCondition {
return scheduledBuildcondition(corev1.ConditionTrue, v1.BuildConditionReadyReason, fmt.Sprintf(
"the build (%s) is scheduled",
buildName,
))
}

func scheduledWaitingBuildcondition(buildName string, reason string) *v1.BuildCondition {
return scheduledBuildcondition(corev1.ConditionFalse, v1.BuildConditionWaitingReason, fmt.Sprintf(
enqueuedMsg,
reason,
buildName,
))
}

func scheduledBuildcondition(status corev1.ConditionStatus, reason string, msg string) *v1.BuildCondition {
return &v1.BuildCondition{
Type: v1.BuildConditionScheduled,
Status: status,
Reason: reason,
Message: msg,
}
}
Loading

0 comments on commit 05ed327

Please sign in to comment.