Skip to content

Commit

Permalink
Filter for containerslots-label
Browse files Browse the repository at this point in the history
Signed-off-by: michael.freund <[email protected]>
  • Loading branch information
michael.freund authored and dongluochen committed May 3, 2016
1 parent e8cb44f commit 0ccc958
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 1 deletion.
13 changes: 13 additions & 0 deletions docs/scheduler/filter.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Each filter has a name that identifies it. The node filters are:

* `constraint`
* `health`
* `containerslots`

The container configuration filters are:

Expand All @@ -48,6 +49,8 @@ $ swarm manage --filter=health --filter=dependency

When creating a container or building an image, you use a `constraint` or
`health` filter to select a subset of nodes to consider for scheduling.
If there are nodes in the swarm that have a label with key as `containerslots`
and a number-value, Swarm will not launch more containers than the given number.

### Use a constraint filter

Expand Down Expand Up @@ -175,6 +178,16 @@ The node `health` filter prevents the scheduler form running containers
on unhealthy nodes. A node is considered unhealthy if the node is down or it
can't communicate with the cluster store.

### Use the containerslots filter

You may give your Docker nodes the containerslots label
```bash
$ docker daemon --label containerslots=3
```
Swarm will prevent running more than three containers at this node, if
all nodes are "full", an error is thrown. If the value is not castable
to an integer number or is not present, there will be no limit.

## Container filters

When creating a container, you can use three types of container filters:
Expand Down
1 change: 1 addition & 0 deletions scheduler/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func init() {
filters = []Filter{
&HealthFilter{},
&PortFilter{},
&SlotsFilter{},
&DependencyFilter{},
&AffinityFilter{},
&ConstraintFilter{},
Expand Down
51 changes: 51 additions & 0 deletions scheduler/filter/slots.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package filter

import (
"errors"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/scheduler/node"
"strconv"
)

var (
// ErrNoNodeWithFreeSlotsAvailable is exported
ErrNoNodeWithFreeSlotsAvailable = errors.New("No node with enough open slots available in the cluster")
)

//SlotsFilter only schedules containers with open slots.
type SlotsFilter struct {
}

// Name returns the name of the filter
func (f *SlotsFilter) Name() string {
return "containerslots"
}

// Filter is exported
func (f *SlotsFilter) Filter(_ *cluster.ContainerConfig, nodes []*node.Node, _ bool) ([]*node.Node, error) {
result := []*node.Node{}

for _, node := range nodes {

if slotsString, ok := node.Labels["containerslots"]; ok {
slots, err := strconv.Atoi(slotsString) //if err => cannot cast to int, so ignore the label
if err != nil || len(node.Containers) < slots {
result = append(result, node)
}
} else {
//no limit if label is missing
result = append(result, node)
}
}

if len(result) == 0 {
return nil, ErrNoNodeWithFreeSlotsAvailable
}

return result, nil
}

// GetFilters returns just the info that this node failed, because there where no free slots
func (f *SlotsFilter) GetFilters(config *cluster.ContainerConfig) ([]string, error) {
return []string{"free slots"}, nil
}
167 changes: 167 additions & 0 deletions scheduler/filter/slots_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package filter

import (
"testing"

"github.com/docker/engine-api/types"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/scheduler/node"
"github.com/stretchr/testify/assert"
)

var labelsWithSlots = make(map[string]string)
var labelsWithoutSlots = make(map[string]string)
var labelsWithStringSlot = make(map[string]string)

func testFixturesAllFreeNode() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
},
},
{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{},
},
}
}

func testFixturesPartlyFreeNode() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},
{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{},
},
}
}

func testFixturesAllNoLabelNode() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithoutSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},

{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithoutSlots,
Containers: []*cluster.Container{},
},
}
}

func testFixturesNoFreeNode() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},

{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},
}
}

func testFixturesNoFreeNodeButStringLabel() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},

{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithStringSlot,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},
}
}

func TestSlotsFilter(t *testing.T) {

labelsWithSlots["containerslots"] = "3"
labelsWithStringSlot["containerslots"] = "foo"

var (
f = SlotsFilter{}
nodesAllFree = testFixturesAllFreeNode()
nodesPartlyFree = testFixturesPartlyFreeNode()
nodesAllNoLabel = testFixturesAllNoLabelNode()
nodesNoFree = testFixturesNoFreeNode()
nodesNoFreeButStringLabel = testFixturesNoFreeNodeButStringLabel()
result []*node.Node
err error
)

result, err = f.Filter(&cluster.ContainerConfig{}, nodesAllFree, true)
assert.NoError(t, err)
assert.Equal(t, result, nodesAllFree)

result, err = f.Filter(&cluster.ContainerConfig{}, nodesPartlyFree, true)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodesPartlyFree[1])

result, err = f.Filter(&cluster.ContainerConfig{}, nodesAllNoLabel, true)
assert.NoError(t, err)
assert.Equal(t, result, nodesAllNoLabel)

result, err = f.Filter(&cluster.ContainerConfig{}, nodesNoFree, true)
assert.Equal(t, err, ErrNoNodeWithFreeSlotsAvailable)
assert.Nil(t, result)

result, err = f.Filter(&cluster.ContainerConfig{}, nodesNoFreeButStringLabel, true)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodesNoFreeButStringLabel[1])
}
2 changes: 1 addition & 1 deletion test/integration/build_with_filters.bats
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function teardown() {
run docker_swarm build --build-arg="constraint:node==node-9" $TESTDATA/build
[ "$status" -eq 1 ]
[[ "${lines[1]}" == *"Unable to find a node that satisfies the following conditions"* ]]
[[ "${lines[2]}" == *"[node==node-9]"* ]]
[[ "${lines[3]}" == *"[node==node-9]"* ]]

run docker_swarm images -q
[ "$status" -eq 0 ]
Expand Down
78 changes: 78 additions & 0 deletions test/integration/containerslots-filter.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bats

load helpers

function teardown() {
swarm_manage_cleanup
stop_docker
}

@test "containerslots filter" {
start_docker_with_busybox 2 --label containerslots=2
swarm_manage

# Use busybox to save image pulling time for integration test.
# Running the first 4 containers, it should be fine.
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]

# When trying to start the 5th one, it should be error finding a node with free slots.
run docker_swarm run -d -t busybox sh
[ "$status" -ne 0 ]
[[ "${lines[0]}" == *"Unable to find a node that satisfies the following conditions"* ]]
[[ "${lines[1]}" == *"free slots"* ]]

# And the number of running containers should be still 4.
run docker_swarm ps
[ "${#lines[@]}" -eq 5 ]
}

@test "containerslots without existing label" {
start_docker_with_busybox 2
swarm_manage

# Use busybox to save image pulling time for integration test.
# Running more than 5 containers, it should be fine.
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]

# And the number of running containers should be 5.
run docker_swarm ps
[ "${#lines[@]}" -eq 6 ]
}

@test "containerslots with invalid label" {
start_docker_with_busybox 2 --label containerslots="foo"
swarm_manage

# Use busybox to save image pulling time for integration test.
# Running more than 5 containers, it should be fine.
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]

# And the number of running containers should be 5.
run docker_swarm ps
[ "${#lines[@]}" -eq 6 ]
}

0 comments on commit 0ccc958

Please sign in to comment.