Skip to content
This repository has been archived by the owner on Oct 23, 2024. It is now read-only.

Commit

Permalink
Initial stab at making deployment plans cheaper. Back port of https:/…
Browse files Browse the repository at this point in the history
  • Loading branch information
unterstein committed Feb 6, 2017
1 parent 1027968 commit 553b27f
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 26 deletions.
30 changes: 24 additions & 6 deletions src/main/scala/mesosphere/marathon/upgrade/DeploymentPlan.scala
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ object DeploymentPlan {
* similar logic.
*/
private[upgrade] def appsGroupedByLongestPath(
affectedAppIds: Set[PathId],
group: Group): SortedMap[Int, Set[AppDefinition]] = {

import org.jgrapht.DirectedGraph
Expand All @@ -192,9 +193,11 @@ object DeploymentPlan {

}

val unsortedEquivalenceClasses = group.transitiveApps.groupBy { app =>
longestPathFromVertex(group.dependencyGraph, app).length
}
val unsortedEquivalenceClasses = group.transitiveApps
.filter(app => affectedAppIds.contains(app.id))
.groupBy { app =>
longestPathFromVertex(group.dependencyGraph, app).length
}

SortedMap(unsortedEquivalenceClasses.toSeq: _*)
}
Expand All @@ -203,12 +206,12 @@ object DeploymentPlan {
* Returns a sequence of deployment steps, the order of which is derived
* from the topology of the target group's dependency graph.
*/
def dependencyOrderedSteps(original: Group, target: Group,
def dependencyOrderedSteps(original: Group, target: Group, affectedIds: Set[PathId],
toKill: Map[PathId, Iterable[Task]]): Seq[DeploymentStep] = {
val originalApps: Map[PathId, AppDefinition] =
original.transitiveApps.map(app => app.id -> app).toMap

val appsByLongestPath: SortedMap[Int, Set[AppDefinition]] = appsGroupedByLongestPath(target)
val appsByLongestPath: SortedMap[Int, Set[AppDefinition]] = appsGroupedByLongestPath(affectedIds, target)

appsByLongestPath.valuesIterator.map { (equivalenceClass: Set[AppDefinition]) =>
val actions: Set[DeploymentAction] = equivalenceClass.flatMap { (newApp: AppDefinition) =>
Expand Down Expand Up @@ -280,6 +283,21 @@ object DeploymentPlan {
}.to[Seq]
)

// applications that are either new or the specs are different should be considered for the dependency graph
val addedOrChanged: Set[PathId] = targetApps.flatMap {
case (appId, spec) =>
if (!originalApps.contains(appId) ||
(originalApps.contains(appId) && originalApps(appId) != spec)) {
// the above could be optimized/refined further by checking the version info. The tests are actually
// really bad about structuring this correctly though, so for now, we just make sure that
// the specs are different (or brand new)
Some(appId)
} else {
None
}
}(collection.breakOut)
val affectedApplications = addedOrChanged ++ (originalApps.keySet -- targetApps.keySet)

// 3. For each app in each dependency class,
//
// A. If this app is new, scale to the target number of instances.
Expand All @@ -292,7 +310,7 @@ object DeploymentPlan {
// the old app or the new app, whichever is less.
// ii. Restart the app, up to the new target number of instances.
//
steps ++= dependencyOrderedSteps(original, target, toKill)
steps ++= dependencyOrderedSteps(original, target, affectedApplications, toKill)

// Build the result.
val result = DeploymentPlan(
Expand Down
54 changes: 34 additions & 20 deletions src/test/scala/mesosphere/marathon/upgrade/DeploymentPlanTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
)))

When("the group's apps are grouped by the longest outbound path")
val partitionedApps = DeploymentPlan.appsGroupedByLongestPath(group)
val partitionedApps = DeploymentPlan.appsGroupedByLongestPath(Set(a.id, b.id, c.id, d.id), group)

Then("three equivalence classes should be computed")
partitionedApps should have size (3)
Expand Down Expand Up @@ -73,7 +73,7 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
)

When("the group's apps are grouped by the longest outbound path")
val partitionedApps = DeploymentPlan.appsGroupedByLongestPath(group)
val partitionedApps = DeploymentPlan.appsGroupedByLongestPath(group.transitiveApps.map(_.id), group)

Then("three equivalence classes should be computed")
partitionedApps should have size (4)
Expand Down Expand Up @@ -120,15 +120,16 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w

test("can compute affected app ids") {
val versionInfo = AppDefinition.VersionInfo.forNewConfig(Timestamp(10))
val versionInfo2 = AppDefinition.VersionInfo.forNewConfig(Timestamp(20))
val app: AppDefinition = AppDefinition("/app".toPath, Some("sleep 10"), versionInfo = versionInfo)
val app2: AppDefinition = AppDefinition("/app2".toPath, Some("cmd2"), versionInfo = versionInfo)
val app3: AppDefinition = AppDefinition("/app3".toPath, Some("cmd3"), versionInfo = versionInfo)
val unchanged: AppDefinition = AppDefinition("/unchanged".toPath, Some("unchanged"), versionInfo = versionInfo)

val apps = Map(app.id -> app, app2.id -> app2, app3.id -> app3, unchanged.id -> unchanged)

val updatedApp = app.copy(cmd = Some("sleep 30"))
val updatedApp2 = app2.copy(instances = 10)
val updatedApp = app.copy(cmd = Some("sleep 30"), versionInfo = versionInfo2)
val updatedApp2 = app2.copy(instances = 10, versionInfo = versionInfo2)
val updatedApp4 = AppDefinition("/app4".toPath, Some("cmd4"))
val update = Map(
updatedApp.id -> updatedApp,
Expand All @@ -154,6 +155,8 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
val strategy = UpgradeStrategy(0.75)

val versionInfo = AppDefinition.VersionInfo.forNewConfig(Timestamp(10))
val versionInfo2 = AppDefinition.VersionInfo.forNewConfig(Timestamp(20))

val mongo: (AppDefinition, AppDefinition) =
AppDefinition(mongoId, Some("mng1"), instances = 4, upgradeStrategy = strategy, versionInfo = versionInfo) ->
AppDefinition(mongoId, Some("mng2"), instances = 8, upgradeStrategy = strategy, versionInfo = versionInfo)
Expand All @@ -174,18 +177,21 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
)
))

val mongoUpdated = mongo._2.copy(versionInfo = versionInfo2)
val serviceUpdated = service._2.copy(versionInfo = versionInfo2)

val to = Group("/".toPath, groups = Set(Group("/test".toPath, groups = Set(
Group("/test/database".toPath, Map(mongo._2.id -> mongo._2)),
Group("/test/service".toPath, Map(service._2.id -> service._2))
Group("/test/database".toPath, Map(mongoUpdated.id -> mongoUpdated)),
Group("/test/service".toPath, Map(serviceUpdated.id -> serviceUpdated))
))))

When("the deployment plan is computed")
val plan = DeploymentPlan(from, to)

Then("the deployment steps are correct")
plan.steps should have size 2
plan.steps(0).actions.toSet should equal (Set(RestartApplication(mongo._2)))
plan.steps(1).actions.toSet should equal (Set(RestartApplication(service._2)))
plan.steps(0).actions.toSet should equal (Set(RestartApplication(mongoUpdated)))
plan.steps(1).actions.toSet should equal (Set(RestartApplication(serviceUpdated)))
}

test("when starting apps without dependencies, they are first started and then scaled parallely") {
Expand Down Expand Up @@ -222,6 +228,7 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
val strategy = UpgradeStrategy(0.75)

val versionInfo = AppDefinition.VersionInfo.forNewConfig(Timestamp(10))
val versionInfo2 = AppDefinition.VersionInfo.forNewConfig(Timestamp(20))

val mongo =
AppDefinition(mongoId, Some("mng1"), instances = 4, upgradeStrategy = strategy, versionInfo = versionInfo) ->
Expand All @@ -236,17 +243,20 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
Group("/test/service".toPath, Map(service._1.id -> service._1))
))))

val mongoUpdated = mongo._2.copy(versionInfo = versionInfo2)
val serviceUpdated = service._2.copy(versionInfo = versionInfo2)

val to: Group = Group("/".toPath, groups = Set(Group("/test".toPath, groups = Set(
Group("/test/database".toPath, Map(mongo._2.id -> mongo._2)),
Group("/test/service".toPath, Map(service._2.id -> service._2))
Group("/test/database".toPath, Map(mongoUpdated.id -> mongoUpdated)),
Group("/test/service".toPath, Map(serviceUpdated.id -> serviceUpdated))
))))

When("the deployment plan is computed")
val plan = DeploymentPlan(from, to)

Then("the deployment steps are correct")
plan.steps should have size 1
plan.steps(0).actions.toSet should equal (Set(RestartApplication(mongo._2), RestartApplication(service._2)))
plan.steps(0).actions.toSet should equal (Set(RestartApplication(mongoUpdated), RestartApplication(serviceUpdated)))
}

test("when updating a group with dependent and independent applications, the correct order is computed") {
Expand All @@ -257,6 +267,7 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
val strategy = UpgradeStrategy(0.75)

val versionInfo = AppDefinition.VersionInfo.forNewConfig(Timestamp(10))
val versionInfo2 = AppDefinition.VersionInfo.forNewConfig(Timestamp(20))

val mongo =
AppDefinition(mongoId, Some("mng1"), instances = 4, upgradeStrategy = strategy, versionInfo = versionInfo) ->
Expand All @@ -280,10 +291,13 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
Group("/test/independent".toPath, Map(independent._1.id -> independent._1))
))))

val mongoUpdated = mongo._2.copy(versionInfo = versionInfo2)
val serviceUpdated = service._2.copy(versionInfo = versionInfo2)
val independentUpdated = independent._2.copy(versionInfo = versionInfo2)
val to: Group = Group("/".toPath, groups = Set(Group("/test".toPath, groups = Set(
Group("/test/database".toPath, Map(mongo._2.id -> mongo._2)),
Group("/test/service".toPath, Map(service._2.id -> service._2, toStart.id -> toStart)),
Group("/test/independent".toPath, Map(independent._2.id -> independent._2))
Group("/test/database".toPath, Map(mongoUpdated.id -> mongoUpdated)),
Group("/test/service".toPath, Map(serviceUpdated.id -> serviceUpdated, toStart.id -> toStart)),
Group("/test/independent".toPath, Map(independentUpdated.id -> independentUpdated))
))))

When("the deployment plan is computed")
Expand All @@ -296,8 +310,8 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w

plan.steps(0).actions.toSet should equal (Set(StopApplication(toStop)))
plan.steps(1).actions.toSet should equal (Set(StartApplication(toStart, 0)))
plan.steps(2).actions.toSet should equal (Set(RestartApplication(mongo._2), RestartApplication(independent._2)))
plan.steps(3).actions.toSet should equal (Set(RestartApplication(service._2)))
plan.steps(2).actions.toSet should equal (Set(RestartApplication(mongoUpdated), RestartApplication(independentUpdated)))
plan.steps(3).actions.toSet should equal (Set(RestartApplication(serviceUpdated)))
plan.steps(4).actions.toSet should equal (Set(ScaleApplication(toStart, 2)))
}

Expand All @@ -322,7 +336,7 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
test("Should create non-empty deployment plan when only args have changed") {
val versionInfo: FullVersionInfo = AppDefinition.VersionInfo.forNewConfig(Timestamp(10))
val app = AppDefinition(id = "/test".toPath, cmd = Some("sleep 5"), versionInfo = versionInfo)
val appNew = app.copy(args = Some(Seq("foo")))
val appNew = app.copy(args = Some(Seq("foo")), versionInfo = AppDefinition.VersionInfo.forNewConfig(Timestamp(20)))

val from = Group("/".toPath, apps = Map(app.id -> app))
val to = from.copy(apps = Map(appNew.id -> appNew))
Expand Down Expand Up @@ -377,7 +391,7 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
)
)))

val newApp = oldApp.copy(instances = 5)
val newApp = oldApp.copy(instances = 5, versionInfo = AppDefinition.VersionInfo.forNewConfig(Timestamp(20)))
val targetGroup = Group("/".toPath, groups = Set(Group(
id = "/test".toPath,
apps = Map(newApp.id -> newApp),
Expand Down Expand Up @@ -417,8 +431,8 @@ class DeploymentPlanTest extends MarathonSpec with Matchers with GivenWhenThen w
val f = new Fixture()

When("We update the upgrade strategy to the default strategy")
val app2 = f.validResident.copy(upgradeStrategy = AppDefinition.DefaultUpgradeStrategy)
val group2 = f.group.copy(apps = Map(app2.id -> app2))
val app2 = f.validResident.copy(upgradeStrategy = AppDefinition.DefaultUpgradeStrategy, versionInfo = AppDefinition.VersionInfo.forNewConfig(Timestamp(10)))
val group2 = f.group.copy(groups = Set(f.group.group(PathId("/test")).get.copy(apps = Map(app2.id -> app2))))
val plan2 = DeploymentPlan(f.group, group2)

Then("The deployment is not valid")
Expand Down

0 comments on commit 553b27f

Please sign in to comment.