Repository: mesos Updated Branches: refs/heads/master 7bb3c0432 -> 044b72e8e
Updated quota handler logic for hierarchical roles. The quota'd resources for a nested role are "included" within the quota'd resources for that role's parent. Hence, the quota of a node must always be greater than or equal to the sum of the quota'd resources of that role's children. When creating and removing quota, we must ensure that this invariant is not violated. When computing the cluster capacity heuristic, we must ensure that we do not "double-count" quota'd resources: e.g., if the cluster has a total capacity of 100 CPUs, role "x" has a quota guarantee of 80 CPUs, and role "x/y" has a quota guarantee of 40 CPUs, this does NOT violate the cluster capacity heuristic. Review: https://reviews.apache.org/r/57167 Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/d0f0f9d6 Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/d0f0f9d6 Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/d0f0f9d6 Branch: refs/heads/master Commit: d0f0f9d6b87ca8f800910ac51f21279d8619795a Parents: 7bb3c04 Author: Neil Conway <[email protected]> Authored: Tue Jan 31 17:24:11 2017 -0800 Committer: Neil Conway <[email protected]> Committed: Wed Apr 26 14:01:40 2017 -0400 ---------------------------------------------------------------------- src/master/quota_handler.cpp | 212 +++++++++++--- src/tests/hierarchical_allocator_tests.cpp | 87 ++++++ src/tests/master_quota_tests.cpp | 355 ++++++++++++++++++++++++ 3 files changed, 621 insertions(+), 33 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/d0f0f9d6/src/master/quota_handler.cpp ---------------------------------------------------------------------- diff --git a/src/master/quota_handler.cpp b/src/master/quota_handler.cpp index 7ff43a0..281fa1d 100644 --- a/src/master/quota_handler.cpp +++ b/src/master/quota_handler.cpp @@ -16,6 +16,7 @@ #include "master/master.hpp" +#include <memory> #include <list> #include <vector> @@ -63,12 +64,128 @@ using process::http::authentication::Principal; using std::list; using std::string; +using std::unique_ptr; using std::vector; namespace mesos { namespace internal { namespace master { +// Represents the tree of roles that have quota. The quota of a child +// node is "contained" in the quota of its parent node. This has two +// implications: +// +// (1) The quota of a parent must be greater than or equal to the +// sum of the quota of its children. +// +// (2) When computing the total resources guaranteed by quota, we +// don't want to double-count resource guarantees between a +// parent role and its children. +class QuotaTree +{ +public: + QuotaTree(const hashmap<string, Quota>& quotas) + : root(new Node("")) + { + foreachpair (const string& role, const Quota& quota, quotas) { + insert(role, quota); + } + } + + void insert(const string& role, const Quota& quota) + { + // Create the path from root->leaf in the tree. Any missing nodes + // are created implicitly. + vector<string> components = strings::tokenize(role, "/"); + CHECK(!components.empty()); + + Node* current = root.get(); + foreach (const string& component, components) { + if (!current->children.contains(component)) { + current->children[component] = unique_ptr<Node>(new Node(component)); + } + + current = current->children.at(component).get(); + } + + // Update `current` with the guaranteed quota resources for this + // role. A path in the tree should be associated with at most one + // quota guarantee, so the current guarantee should be empty. + CHECK(current->quota.info.guarantee().empty()); + current->quota = quota; + } + + // Check whether the tree satisfies the "parent >= sum(children)" + // constraint described above. + Option<Error> validate() const + { + // Don't check the root node because it does not have quota set. + foreachvalue (const unique_ptr<Node>& child, root->children) { + Option<Error> error = child->validate(); + if (error.isSome()) { + return error; + } + } + + return None(); + } + + // Returns the total resources requested by all quotas in the + // tree. Since a role's quota must be greater than or equal to the + // sum of the quota of its children, we can just sum the quota of + // the top-level roles. + Resources total() const + { + Resources result; + + // Don't include the root node because it does not have quota set. + foreachvalue (const unique_ptr<Node>& child, root->children) { + result += child->quota.info.guarantee(); + } + + return result; + } + +private: + struct Node + { + Node(const string& _name) : name(_name) {} + + Option<Error> validate() const + { + foreachvalue (const unique_ptr<Node>& child, children) { + Option<Error> error = child->validate(); + if (error.isSome()) { + return error; + } + } + + Resources childResources; + foreachvalue (const unique_ptr<Node>& child, children) { + childResources += child->quota.info.guarantee(); + } + + Resources selfResources = quota.info.guarantee(); + + if (!selfResources.contains(childResources)) { + return Error("Invalid quota configuration. Parent role '" + + name + "' with quota " + stringify(selfResources) + + " does not contain the sum of its children's" + + " resources (" + stringify(childResources) + ")"); + } + + return None(); + } + + const string name; + Quota quota; + hashmap<string, unique_ptr<Node>> children; + }; + + unique_ptr<Node> root; +}; + + Option<Error> Master::QuotaHandler::capacityHeuristic( const QuotaInfo& request) const { @@ -78,19 +195,21 @@ Option<Error> Master::QuotaHandler::capacityHeuristic( CHECK(master->isWhitelistedRole(request.role())); CHECK(!master->quotas.contains(request.role())); - // Calculate the total amount of resources requested by all quotas - // (including the request) in the cluster. - // NOTE: We have validated earlier that the quota for the role in the - // request does not exist, hence `master->quotas` is guaranteed not to - // contain the request role's quota yet. - // TODO(alexr): Relax this constraint once we allow updating quotas. - Resources totalQuota = request.guarantee(); - foreachvalue (const Quota& quota, master->quotas) { - totalQuota += quota.info.guarantee(); - } + hashmap<string, Quota> quotaMap = master->quotas; + + // Check that adding the requested quota to the existing quotas does + // not violate the capacity heuristic. + quotaMap[request.role()] = Quota{request}; + + QuotaTree quotaTree(quotaMap); + + CHECK_NONE(quotaTree.validate()); + + Resources totalQuota = quotaTree.total(); // Determine whether the total quota, including the new request, does // not exceed the sum of non-static cluster resources. + // // NOTE: We do not necessarily calculate the full sum of non-static // cluster resources. We apply the early termination logic as it can // reduce the cost of the function significantly. This early exit does @@ -354,11 +473,12 @@ Future<http::Response> Master::QuotaHandler::_set( QuotaInfo quotaInfo = create.get(); // Check that the `QuotaInfo` is a valid quota request. - Option<Error> validateError = quota::validation::quotaInfo(quotaInfo); - if (validateError.isSome()) { - return BadRequest( - "Failed to validate set quota request: " + - validateError.get().message); + { + Option<Error> error = quota::validation::quotaInfo(quotaInfo); + if (error.isSome()) { + return BadRequest( + "Failed to validate set quota request: " + error->message); + } } // Check that the role is on the role whitelist, if it exists. @@ -376,6 +496,22 @@ Future<http::Response> Master::QuotaHandler::_set( " for role '" + quotaInfo.role() + "' which already has quota"); } + hashmap<string, Quota> quotaMap = master->quotas; + + // Validate that adding this quota does not violate the hierarchical + // relationship between quotas. + quotaMap[quotaInfo.role()] = Quota{quotaInfo}; + + QuotaTree quotaTree(quotaMap); + + { + Option<Error> error = quotaTree.validate(); + if (error.isSome()) { + return BadRequest( + "Failed to validate set quota request: " + error->message); + } + } + // The force flag is used to overwrite the `capacityHeuristic` check. const bool forced = quotaRequest.force(); @@ -466,31 +602,26 @@ Future<http::Response> Master::QuotaHandler::remove( // Check that the request type is DELETE which is guaranteed by the master. CHECK_EQ("DELETE", request.method); - // Extract role from url. - vector<string> tokens = strings::tokenize(request.url.path, "/"); - - // Check that there are exactly 3 parts: {master,quota,'role'}. - if (tokens.size() != 3u) { - return BadRequest( - "Failed to parse request path '" + request.url.path + - "': 3 tokens ('master', 'quota', 'role') required, found " + - stringify(tokens.size()) + " token(s)"); - } - - // Check that "quota" is the second to last token. - if (tokens.end()[-2] != "quota") { - return BadRequest( - "Failed to parse request path '" + request.url.path + - "': Missing 'quota' endpoint"); + // Extract role from url. We expect the request path to have the + // format "/master/quota/role", where "role" is a role name. The + // role name itself may contain one or more slashes. Note that + // `strings::tokenize` returns the remainder of the string when the + // specified maximum number of tokens is reached. + vector<string> components = strings::tokenize(request.url.path, "/", 3u); + if (components.size() < 3u) { + return BadRequest("Failed to parse remove quota request for path '" + + request.url.path + "': expected 3 tokens, found " + + stringify(components.size()) + " tokens"); } - const string& role = tokens.back(); + CHECK_EQ(3u, components.size()); + string role = components.back(); // Check that the role is on the role whitelist, if it exists. if (!master->isWhitelistedRole(role)) { return BadRequest( "Failed to validate remove quota request for path '" + - request.url.path +"': Unknown role '" + role + "'"); + request.url.path + "': Unknown role '" + role + "'"); } // Check that we are removing an existing quota. @@ -500,6 +631,21 @@ Future<http::Response> Master::QuotaHandler::remove( "': Role '" + role + "' has no quota set"); } + hashmap<string, Quota> quotaMap = master->quotas; + + // Validate that removing the quota for `role` does not violate the + // hierarchical relationship between quotas. + quotaMap.erase(role); + + QuotaTree quotaTree(quotaMap); + + Option<Error> error = quotaTree.validate(); + if (error.isSome()) { + return BadRequest( + "Failed to remove quota for path '" + request.url.path + + "': " + error->message); + } + return _remove(role, principal); } http://git-wip-us.apache.org/repos/asf/mesos/blob/d0f0f9d6/src/tests/hierarchical_allocator_tests.cpp ---------------------------------------------------------------------- diff --git a/src/tests/hierarchical_allocator_tests.cpp b/src/tests/hierarchical_allocator_tests.cpp index 33e7b45..e2cd66d 100644 --- a/src/tests/hierarchical_allocator_tests.cpp +++ b/src/tests/hierarchical_allocator_tests.cpp @@ -4297,6 +4297,93 @@ TEST_F(HierarchicalAllocatorTest, DisproportionateQuotaVsAllocation) } +// This test checks that quota guarantees work correctly when a nested +// role is created as a child of an existing role that has quota. +TEST_F(HierarchicalAllocatorTest, NestedRoleQuota) +{ + // Pausing the clock is not necessary, but ensures that the test + // doesn't rely on the batch allocation in the allocator, which + // would slow down the test. + Clock::pause(); + + initialize(); + + const string PARENT_ROLE = "a/b"; + const string CHILD_ROLE1 = "a/b/c"; + const string CHILD_ROLE2 = "a/b/d"; + + // Create `framework1` and set quota for its role. + FrameworkInfo framework1 = createFrameworkInfo({PARENT_ROLE}); + allocator->addFramework(framework1.id(), framework1, {}, true); + + const Quota parentQuota = createQuota(PARENT_ROLE, "cpus:2;mem:1024"); + allocator->setQuota(PARENT_ROLE, parentQuota); + + SlaveInfo agent1 = createSlaveInfo("cpus:1;mem:512;disk:0"); + allocator->addSlave( + agent1.id(), + agent1, + AGENT_CAPABILITIES(), + None(), + agent1.resources(), + {}); + + // `framework1` will be offered all of `agent1`'s resources because + // it is the only framework in the only role with unsatisfied quota. + { + Allocation expected = Allocation( + framework1.id(), + {{PARENT_ROLE, {{agent1.id(), agent1.resources()}}}}); + + AWAIT_EXPECT_EQ(expected, allocations.get()); + } + + // `framework1` declines the resources on `agent1` for the duration + // of the test. + Filters longFilter; + longFilter.set_refuse_seconds(flags.allocation_interval.secs() * 10); + + allocator->recoverResources( + framework1.id(), + agent1.id(), + allocatedResources(agent1.resources(), PARENT_ROLE), + longFilter); + + // Register a framework in CHILD_ROLE1, which is a child role of + // PARENT_ROLE. In the current implementation, because CHILD_ROLE1 + // does not itself have quota, it will not be offered any of + // PARENT_ROLE's quota'd resources. This behavior may change in the + // future (MESOS-7150). + FrameworkInfo framework2 = createFrameworkInfo({CHILD_ROLE1}); + allocator->addFramework(framework2.id(), framework2, {}, true); + + // Trigger a batch allocation for good measure; we do not expect + // either framework to be offered resources. + Clock::advance(flags.allocation_interval); + Clock::settle(); + + Future<Allocation> allocation = allocations.get(); + EXPECT_TRUE(allocation.isPending()); + + // Register a framework in CHILD_ROLE2, which is a child role of + // PARENT_ROLE. Because CHILD_ROLE2 has quota, it will be offered + // resources. + FrameworkInfo framework3 = createFrameworkInfo({CHILD_ROLE2}); + allocator->addFramework(framework3.id(), framework3, {}, true); + + const Quota childQuota = createQuota(CHILD_ROLE2, "cpus:1;mem:512"); + allocator->setQuota(CHILD_ROLE2, childQuota); + + { + Allocation expected = Allocation( + framework3.id(), + {{CHILD_ROLE2, {{agent1.id(), agent1.resources()}}}}); + + AWAIT_EXPECT_EQ(expected, allocation); + } +} + + class HierarchicalAllocatorTestWithParam : public HierarchicalAllocatorTestBase, public WithParamInterface<bool> {}; http://git-wip-us.apache.org/repos/asf/mesos/blob/d0f0f9d6/src/tests/master_quota_tests.cpp ---------------------------------------------------------------------- diff --git a/src/tests/master_quota_tests.cpp b/src/tests/master_quota_tests.cpp index 1714ba1..7f94b92 100644 --- a/src/tests/master_quota_tests.cpp +++ b/src/tests/master_quota_tests.cpp @@ -14,6 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include <map> #include <string> #include <vector> @@ -50,6 +51,7 @@ using mesos::internal::slave::Slave; using mesos::master::detector::MasterDetector; +using mesos::quota::QuotaInfo; using mesos::quota::QuotaRequest; using mesos::quota::QuotaStatus; @@ -64,6 +66,7 @@ using process::http::OK; using process::http::Response; using process::http::Unauthorized; +using std::map; using std::string; using std::vector; @@ -1475,6 +1478,358 @@ TEST_F(MasterQuotaTest, AuthorizeGetUpdateQuotaRequestsWithoutPrincipal) } } + +// This test checks that quota can be successfully set, queried, and +// removed on a child role. +TEST_F(MasterQuotaTest, ChildRole) +{ + Try<Owned<cluster::Master>> master = StartMaster(); + ASSERT_SOME(master); + + // Use the force flag for setting quota that cannot be satisfied in + // this empty cluster without any agents. + const bool FORCE = true; + + const string PARENT_ROLE = "eng"; + const string CHILD_ROLE = "eng/dev"; + + // Set quota for the parent role. + Resources parentQuotaResources = Resources::parse("cpus:2;mem:1024").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(PARENT_ROLE, parentQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) << response->body; + } + + // Set quota for the child role. + Resources childQuotaResources = Resources::parse("cpus:1;mem:768").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(CHILD_ROLE, childQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) << response->body; + } + + // Query the configured quota. + { + Future<Response> response = process::http::get( + master.get()->pid, + "quota", + None(), + createBasicAuthHeaders(DEFAULT_CREDENTIAL)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) + << response->body; + + EXPECT_SOME_EQ( + "application/json", + response->headers.get("Content-Type")); + + const Try<JSON::Object> parse = JSON::parse<JSON::Object>(response->body); + + ASSERT_SOME(parse); + + // Convert JSON response to `QuotaStatus` protobuf. + const Try<QuotaStatus> status = ::protobuf::parse<QuotaStatus>(parse.get()); + ASSERT_FALSE(status.isError()); + ASSERT_EQ(2, status->infos().size()); + + // Don't assume that the quota for child and parent are returned + // in any particular order. + map<string, Resources> expected = {{PARENT_ROLE, parentQuotaResources}, + {CHILD_ROLE, childQuotaResources}}; + + map<string, Resources> actual = { + {status->infos(0).role(), status->infos(0).guarantee()}, + {status->infos(1).role(), status->infos(1).guarantee()} + }; + + EXPECT_EQ(expected, actual); + } + + // Remove quota for the child role. + { + Future<Response> response = process::http::requestDelete( + master.get()->pid, + "quota/" + CHILD_ROLE, + createBasicAuthHeaders(DEFAULT_CREDENTIAL)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) << response->body; + } +} + + +// This test checks that attempting to set quota on a child role is +// rejected if the child's parent does not have quota set. +TEST_F(MasterQuotaTest, ChildRoleWithNoParentQuota) +{ + Try<Owned<cluster::Master>> master = StartMaster(); + ASSERT_SOME(master); + + // Use the force flag for setting quota that cannot be satisfied in + // this empty cluster without any agents. + const bool FORCE = true; + + const string CHILD_ROLE = "eng/dev"; + + // Set quota for the child role. + Resources childQuotaResources = Resources::parse("cpus:1;mem:768").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(CHILD_ROLE, childQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(BadRequest().status, response) + << response->body; + } +} + + +// This test checks that a request to set quota for a child role is +// rejected if it exceeds the parent role's quota. +TEST_F(MasterQuotaTest, ChildRoleExceedsParentQuota) +{ + Try<Owned<cluster::Master>> master = StartMaster(); + ASSERT_SOME(master); + + // Use the force flag for setting quota that cannot be satisfied in + // this empty cluster without any agents. + const bool FORCE = true; + + const string PARENT_ROLE = "eng"; + const string CHILD_ROLE = "eng/dev"; + + // Set quota for the parent role. + Resources parentQuotaResources = Resources::parse("cpus:2;mem:768").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(PARENT_ROLE, parentQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) << response->body; + } + + // Attempt to set quota for the child role. Because the child role's + // quota exceeds the parent role's quota, this should not succeed. + Resources childQuotaResources = Resources::parse("cpus:1;mem:1024").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(CHILD_ROLE, childQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(BadRequest().status, response) + << response->body; + } +} + + +// This test checks that a request to set quota for a child role is +// rejected if it would result in the parent role's quota being +// smaller than the sum of the quota of its children. +TEST_F(MasterQuotaTest, ChildRoleSumExceedsParentQuota) +{ + Try<Owned<cluster::Master>> master = StartMaster(); + ASSERT_SOME(master); + + // Use the force flag for setting quota that cannot be satisfied in + // this empty cluster without any agents. + const bool FORCE = true; + + const string PARENT_ROLE = "eng"; + const string CHILD_ROLE1 = "eng/dev"; + const string CHILD_ROLE2 = "eng/prod"; + + // Set quota for the parent role. + Resources parentQuotaResources = Resources::parse("cpus:2;mem:768").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(PARENT_ROLE, parentQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) << response->body; + } + + // Set quota for the first child role. This should succeed. + Resources childQuotaResources = Resources::parse("cpus:1;mem:512").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(CHILD_ROLE1, childQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) + << response->body; + } + + // Attempt to set quota for the second child role. This should fail, + // because the sum of the quotas of the children of PARENT_ROLE + // would now exceed the quota of PARENT_ROLE. + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(CHILD_ROLE2, childQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(BadRequest().status, response) + << response->body; + } +} + + +// This test checks that a request to delete quota for a parent role +// is rejected since this would result in the child role's quota +// exceeding the parent role's quota. +TEST_F(MasterQuotaTest, ChildRoleDeleteParentQuota) +{ + Try<Owned<cluster::Master>> master = StartMaster(); + ASSERT_SOME(master); + + // Use the force flag for setting quota that cannot be satisfied in + // this empty cluster without any agents. + const bool FORCE = true; + + const string PARENT_ROLE = "eng"; + const string CHILD_ROLE = "eng/dev"; + + // Set quota for the parent role. + Resources parentQuotaResources = Resources::parse("cpus:2;mem:1024").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(PARENT_ROLE, parentQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) << response->body; + } + + // Set quota for the child role. + Resources childQuotaResources = Resources::parse("cpus:1;mem:512").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(CHILD_ROLE, childQuotaResources, FORCE)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) + << response->body; + } + + // Attempt to remove the quota for the parent role. This should not + // succeed. + { + Future<Response> response = process::http::requestDelete( + master.get()->pid, + "quota/" + PARENT_ROLE, + createBasicAuthHeaders(DEFAULT_CREDENTIAL)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(BadRequest().status, response) + << response->body; + } +} + + +// This test checks that the cluster capacity heuristic correctly +// interprets quota set on hierarchical roles. Specifically, quota on +// child roles should not be double-counted with the quota on the +// child's parent role. In other words, the total quota'd resources in +// the cluster is the sum of the quota on the top-level roles. +TEST_F(MasterQuotaTest, ClusterCapacityWithNestedRoles) +{ + TestAllocator<> allocator; + EXPECT_CALL(allocator, initialize(_, _, _, _)); + + Try<Owned<cluster::Master>> master = StartMaster(&allocator); + ASSERT_SOME(master); + + // Start an agent and wait until its resources are available. + Future<Resources> agentTotalResources; + EXPECT_CALL(allocator, addSlave(_, _, _, _, _, _)) + .WillOnce(DoAll(InvokeAddSlave(&allocator), + FutureArg<4>(&agentTotalResources))); + + Owned<MasterDetector> detector = master.get()->createDetector(); + Try<Owned<cluster::Slave>> slave = StartSlave(detector.get()); + ASSERT_SOME(slave); + + AWAIT_READY(agentTotalResources); + EXPECT_EQ(defaultAgentResources, agentTotalResources.get()); + + const string PARENT_ROLE1 = "eng"; + const string PARENT_ROLE2 = "sales"; + const string CHILD_ROLE = "eng/dev"; + + // Set quota for the first parent role. + Resources parent1QuotaResources = Resources::parse("cpus:1;mem:768").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(PARENT_ROLE1, parent1QuotaResources)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) << response->body; + } + + // Set quota for the child role. This should succeed, even though + // naively summing the parent and child quota would result in + // violating the cluster capacity heuristic. + Resources childQuotaResources = Resources::parse("cpus:1;mem:512").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(CHILD_ROLE, childQuotaResources)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) << response->body; + } + + // Set quota for the second parent role. This should succeed, even + // though naively summing the quota of the subtree rooted at + // PARENT_ROLE1 would violate the cluster capacity check. + Resources parent2QuotaResources = Resources::parse("cpus:1;mem:256").get(); + + { + Future<Response> response = process::http::post( + master.get()->pid, + "quota", + createBasicAuthHeaders(DEFAULT_CREDENTIAL), + createRequestBody(PARENT_ROLE2, parent2QuotaResources)); + + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response) << response->body; + } +} + } // namespace tests { } // namespace internal { } // namespace mesos {
