in-memory ACID store for containers

This can be used by readers/queries so they don't need locks.

Signed-off-by: Fabio Kung <fabio.kung@gmail.com>
This commit is contained in:
Fabio Kung 2017-02-22 10:00:50 -08:00
parent cfc404a375
commit 054728b1f5
3 changed files with 300 additions and 0 deletions

152
container/snapshot.go Normal file
View file

@ -0,0 +1,152 @@
package container
import (
"fmt"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
)
// Snapshot is a read only view for Containers
type Snapshot struct {
ID string `json:"Id"`
Name string
Pid int
Managed bool
Image string
ImageID string
Command string
Ports []types.Port
ExposedPorts nat.PortSet
PublishPorts nat.PortSet
Labels map[string]string
State string
Status string
Health string
HostConfig struct {
NetworkMode string
Isolation string
}
NetworkSettings types.SummaryNetworkSettings
Mounts []types.MountPoint
Created time.Time
StartedAt time.Time
Running bool
Paused bool
ExitCode int
}
// Snapshot provides a read only view of a Container. Callers must hold a Lock on the container object.
func (container *Container) Snapshot() *Snapshot {
snapshot := &Snapshot{
ID: container.ID,
Name: container.Name,
Pid: container.Pid,
Managed: container.Managed,
ImageID: container.ImageID.String(),
Ports: []types.Port{},
ExposedPorts: make(nat.PortSet),
PublishPorts: make(nat.PortSet),
State: container.State.StateString(),
Status: container.State.String(),
Health: container.State.HealthString(),
Mounts: container.GetMountPoints(),
Created: container.Created,
StartedAt: container.StartedAt,
Running: container.Running,
Paused: container.Paused,
ExitCode: container.ExitCode(),
}
if container.HostConfig != nil {
snapshot.HostConfig.Isolation = string(container.HostConfig.Isolation)
snapshot.HostConfig.NetworkMode = string(container.HostConfig.NetworkMode)
for publish := range container.HostConfig.PortBindings {
snapshot.PublishPorts[publish] = struct{}{}
}
}
if container.Config != nil {
snapshot.Image = container.Config.Image
snapshot.Labels = container.Config.Labels
for exposed := range container.Config.ExposedPorts {
snapshot.ExposedPorts[exposed] = struct{}{}
}
}
if len(container.Args) > 0 {
args := []string{}
for _, arg := range container.Args {
if strings.Contains(arg, " ") {
args = append(args, fmt.Sprintf("'%s'", arg))
} else {
args = append(args, arg)
}
}
argsAsString := strings.Join(args, " ")
snapshot.Command = fmt.Sprintf("%s %s", container.Path, argsAsString)
} else {
snapshot.Command = container.Path
}
if container.NetworkSettings != nil {
networks := make(map[string]*network.EndpointSettings)
for name, netw := range container.NetworkSettings.Networks {
if netw == nil || netw.EndpointSettings == nil {
continue
}
networks[name] = &network.EndpointSettings{
EndpointID: netw.EndpointID,
Gateway: netw.Gateway,
IPAddress: netw.IPAddress,
IPPrefixLen: netw.IPPrefixLen,
IPv6Gateway: netw.IPv6Gateway,
GlobalIPv6Address: netw.GlobalIPv6Address,
GlobalIPv6PrefixLen: netw.GlobalIPv6PrefixLen,
MacAddress: netw.MacAddress,
NetworkID: netw.NetworkID,
}
if netw.IPAMConfig != nil {
networks[name].IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: netw.IPAMConfig.IPv4Address,
IPv6Address: netw.IPAMConfig.IPv6Address,
}
}
}
snapshot.NetworkSettings = types.SummaryNetworkSettings{Networks: networks}
for port, bindings := range container.NetworkSettings.Ports {
p, err := nat.ParsePort(port.Port())
if err != nil {
logrus.Warnf("invalid port map %+v", err)
continue
}
if len(bindings) == 0 {
snapshot.Ports = append(snapshot.Ports, types.Port{
PrivatePort: uint16(p),
Type: port.Proto(),
})
continue
}
for _, binding := range bindings {
h, err := nat.ParsePort(binding.HostPort)
if err != nil {
logrus.Warnf("invalid host port map %+v", err)
continue
}
snapshot.Ports = append(snapshot.Ports, types.Port{
PrivatePort: uint16(p),
PublicPort: uint16(h),
Type: port.Proto(),
IP: binding.HostIP,
})
}
}
}
return snapshot
}

90
container/view.go Normal file
View file

@ -0,0 +1,90 @@
package container
import "github.com/hashicorp/go-memdb"
const (
memdbTable = "containers"
memdbIDField = "ID"
memdbIDIndex = "id"
)
var schema = &memdb.DBSchema{
Tables: map[string]*memdb.TableSchema{
memdbTable: {
Name: memdbTable,
Indexes: map[string]*memdb.IndexSchema{
memdbIDIndex: {
Name: memdbIDIndex,
Unique: true,
Indexer: &memdb.StringFieldIndex{Field: memdbIDField},
},
},
},
},
}
// MemDB provides an in-memory transactional (ACID) container Store
type MemDB struct {
store *memdb.MemDB
}
// NewMemDB provides the default implementation, with the default schema
func NewMemDB() (*MemDB, error) {
store, err := memdb.NewMemDB(schema)
if err != nil {
return nil, err
}
return &MemDB{store: store}, nil
}
// Snapshot provides a consistent read-only View of the database
func (db *MemDB) Snapshot() *View {
return &View{db.store.Txn(false)}
}
// Save atomically updates the in-memory store
func (db *MemDB) Save(snapshot *Snapshot) error {
txn := db.store.Txn(true)
defer txn.Commit()
return txn.Insert(memdbTable, snapshot)
}
// Delete removes an item by ID
func (db *MemDB) Delete(id string) error {
txn := db.store.Txn(true)
defer txn.Commit()
return txn.Delete(memdbTable, &Snapshot{ID: id})
}
// View can be used by readers to avoid locking
type View struct {
txn *memdb.Txn
}
// All returns a all items in this snapshot
func (v *View) All() ([]Snapshot, error) {
var all []Snapshot
iter, err := v.txn.Get(memdbTable, memdbIDIndex)
if err != nil {
return nil, err
}
for {
item := iter.Next()
if item == nil {
break
}
snapshot := *(item.(*Snapshot)) // force a copy
all = append(all, snapshot)
}
return all, nil
}
//Get returns an item by id
func (v *View) Get(id string) (*Snapshot, error) {
s, err := v.txn.First(memdbTable, memdbIDIndex, id)
if err != nil {
return nil, err
}
snapshot := *(s.(*Snapshot)) // force a copy
return &snapshot, nil
}

58
container/view_test.go Normal file
View file

@ -0,0 +1,58 @@
package container
import "testing"
func TestViewSave(t *testing.T) {
db, err := NewMemDB()
if err != nil {
t.Fatal(err)
}
snapshot := NewBaseContainer("id", "root").Snapshot()
if err := db.Save(snapshot); err != nil {
t.Fatal(err)
}
}
func TestViewAll(t *testing.T) {
var (
db, _ = NewMemDB()
one = NewBaseContainer("id1", "root1").Snapshot()
two = NewBaseContainer("id2", "root2").Snapshot()
)
one.Pid = 10
two.Pid = 20
db.Save(one)
db.Save(two)
all, err := db.Snapshot().All()
if err != nil {
t.Fatal(err)
}
if l := len(all); l != 2 {
t.Fatalf("expected 2 items, got %d", l)
}
byID := make(map[string]Snapshot)
for i := range all {
byID[all[i].ID] = all[i]
}
if s, ok := byID["id1"]; !ok || s.Pid != 10 {
t.Fatalf("expected something different with for id1: %v", s)
}
if s, ok := byID["id2"]; !ok || s.Pid != 20 {
t.Fatalf("expected something different with for id1: %v", s)
}
}
func TestViewGet(t *testing.T) {
db, _ := NewMemDB()
one := NewBaseContainer("id", "root")
one.ImageID = "some-image-123"
db.Save(one.Snapshot())
s, err := db.Snapshot().Get("id")
if err != nil {
t.Fatal(err)
}
if s == nil || s.ImageID != "some-image-123" {
t.Fatalf("expected something different. Got: %v", s)
}
}