From bc3f94681024d2d3b13b15a2f0cfded9be784057 Mon Sep 17 00:00:00 2001 From: Tom Wilkie Date: Sat, 23 Jan 2016 13:02:16 -0800 Subject: [PATCH] Make NodeMetadatas immutable. --- render/selectors.go | 5 +- report/edge_metadatas.go | 178 ++++++++++++++++++++++++++++++++++----- report/id_list.go | 3 + report/merge_test.go | 89 ++++++++++---------- report/node.go | 6 +- report/report_test.go | 2 +- report/topology.go | 4 +- 7 files changed, 214 insertions(+), 73 deletions(-) diff --git a/render/selectors.go b/render/selectors.go index 18032aac9..c7fe54047 100644 --- a/render/selectors.go +++ b/render/selectors.go @@ -27,14 +27,13 @@ func MakeRenderableNodes(t report.Topology) RenderableNodes { // Push EdgeMetadata to both ends of the edges for srcID, srcNode := range result { - for dstID, emd := range srcNode.Edges { + srcNode.Edges.ForEach(func(dstID string, emd report.EdgeMetadata) { srcNode.EdgeMetadata = srcNode.EdgeMetadata.Flatten(emd) dstNode := result[dstID] dstNode.EdgeMetadata = dstNode.EdgeMetadata.Flatten(emd.Reversed()) result[dstID] = dstNode - } - + }) result[srcID] = srcNode } return result diff --git a/report/edge_metadatas.go b/report/edge_metadatas.go index 41841b0a1..dd3950766 100644 --- a/report/edge_metadatas.go +++ b/report/edge_metadatas.go @@ -1,38 +1,176 @@ package report +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + "reflect" + "sort" + + "github.com/mndrix/ps" +) + // EdgeMetadatas collect metadata about each edge in a topology. Keys are the // remote node IDs, as in Adjacency. -type EdgeMetadatas map[string]EdgeMetadata - -// Copy returns a value copy of the EdgeMetadatas. -func (e EdgeMetadatas) Copy() EdgeMetadatas { - cp := make(EdgeMetadatas, len(e)) - for k, v := range e { - cp[k] = v.Copy() - } - return cp +type EdgeMetadatas struct { + psMap ps.Map } -// Merge merges the other object into this one, and returns the result object. -// The original is not modified. -func (e EdgeMetadatas) Merge(other EdgeMetadatas) EdgeMetadatas { - cp := e.Copy() - for k, v := range other { - cp[k] = cp[k].Merge(v) +// EmptyEdgeMetadatas is the set of empty EdgeMetadatas. +var EmptyEdgeMetadatas = EdgeMetadatas{ps.NewMap()} + +// MakeEdgeMetadatas returns EmptyEdgeMetadatas +func MakeEdgeMetadatas() EdgeMetadatas { + return EmptyEdgeMetadatas +} + +// Copy is a noop +func (c EdgeMetadatas) Copy() EdgeMetadatas { + return c +} + +// Add value to the counter 'key' +func (c EdgeMetadatas) Add(key string, value EdgeMetadata) EdgeMetadatas { + if existingValue, ok := c.psMap.Lookup(key); ok { + value.Merge(existingValue.(EdgeMetadata)) } - return cp + return EdgeMetadatas{ + c.psMap.Set(key, value), + } +} + +// Lookup the counter 'key' +func (c EdgeMetadatas) Lookup(key string) (EdgeMetadata, bool) { + existingValue, ok := c.psMap.Lookup(key) + if ok { + return existingValue.(EdgeMetadata), true + } + return EdgeMetadata{}, false +} + +// Merge produces a fresh Counters, container the keys from both inputs. When +// both inputs container the same key, the latter value is used. +func (c EdgeMetadatas) Merge(other EdgeMetadatas) EdgeMetadatas { + output := c.psMap + + other.psMap.ForEach(func(key string, otherVal interface{}) { + if val, ok := output.Lookup(key); ok { + output = output.Set(key, otherVal.(EdgeMetadata).Merge(val.(EdgeMetadata))) + } else { + output = output.Set(key, otherVal) + } + }) + + return EdgeMetadatas{output} } // Flatten flattens all the EdgeMetadatas in this set and returns the result. // The original is not modified. -func (e EdgeMetadatas) Flatten() EdgeMetadata { +func (c EdgeMetadatas) Flatten() EdgeMetadata { result := EdgeMetadata{} - for _, v := range e { - result = result.Flatten(v) - } + c.psMap.ForEach(func(_ string, v interface{}) { + result = result.Flatten(v.(EdgeMetadata)) + }) return result } +// ForEach executes f on each key value pair in the map +func (c EdgeMetadatas) ForEach(fn func(k string, v EdgeMetadata)) { + c.psMap.ForEach(func(key string, value interface{}) { + fn(key, value.(EdgeMetadata)) + }) +} + +func (c EdgeMetadatas) String() string { + keys := []string{} + for _, k := range c.psMap.Keys() { + keys = append(keys, k) + } + sort.Strings(keys) + + buf := bytes.NewBufferString("{") + for _, key := range keys { + val, _ := c.psMap.Lookup(key) + fmt.Fprintf(buf, "%s: %v, ", key, val) + } + fmt.Fprintf(buf, "}\n") + return buf.String() +} + +// DeepEqual tests equality with other Counters +func (c EdgeMetadatas) DeepEqual(i interface{}) bool { + d, ok := i.(EdgeMetadatas) + if !ok { + return false + } + + if c.psMap.Size() != d.psMap.Size() { + return false + } + + equal := true + c.psMap.ForEach(func(k string, val interface{}) { + if otherValue, ok := d.psMap.Lookup(k); !ok { + equal = false + } else { + equal = equal && reflect.DeepEqual(val, otherValue) + } + }) + return equal +} + +func (c EdgeMetadatas) toIntermediate() map[string]EdgeMetadata { + intermediate := map[string]EdgeMetadata{} + c.psMap.ForEach(func(key string, val interface{}) { + intermediate[key] = val.(EdgeMetadata) + }) + return intermediate +} + +func (c EdgeMetadatas) fromIntermediate(in map[string]EdgeMetadata) EdgeMetadatas { + out := ps.NewMap() + for k, v := range in { + out = out.Set(k, v) + } + return EdgeMetadatas{out} +} + +// MarshalJSON implements json.Marshaller +func (c EdgeMetadatas) MarshalJSON() ([]byte, error) { + if c.psMap != nil { + return json.Marshal(c.toIntermediate()) + } + return json.Marshal(nil) +} + +// UnmarshalJSON implements json.Unmarshaler +func (c *EdgeMetadatas) UnmarshalJSON(input []byte) error { + in := map[string]EdgeMetadata{} + if err := json.Unmarshal(input, &in); err != nil { + return err + } + *c = EdgeMetadatas{}.fromIntermediate(in) + return nil +} + +// GobEncode implements gob.Marshaller +func (c EdgeMetadatas) GobEncode() ([]byte, error) { + buf := bytes.Buffer{} + err := gob.NewEncoder(&buf).Encode(c.toIntermediate()) + return buf.Bytes(), err +} + +// GobDecode implements gob.Unmarshaller +func (c *EdgeMetadatas) GobDecode(input []byte) error { + in := map[string]EdgeMetadata{} + if err := gob.NewDecoder(bytes.NewBuffer(input)).Decode(&in); err != nil { + return err + } + *c = EdgeMetadatas{}.fromIntermediate(in) + return nil +} + // EdgeMetadata describes a superset of the metadata that probes can possibly // collect about a directed edge between two nodes in any topology. type EdgeMetadata struct { diff --git a/report/id_list.go b/report/id_list.go index 9c740e69b..50149a0c2 100644 --- a/report/id_list.go +++ b/report/id_list.go @@ -5,6 +5,9 @@ import "sort" // IDList is a list of string IDs, which are always sorted and unique. type IDList StringSet +// EmptyIDList is an Empty ID List. +var EmptyIDList = IDList(EmptyStringSet) + // MakeIDList makes a new IDList. func MakeIDList(ids ...string) IDList { return IDList(MakeStringSet(ids...)) diff --git a/report/merge_test.go b/report/merge_test.go index 8ee343029..6d82cc800 100644 --- a/report/merge_test.go +++ b/report/merge_test.go @@ -21,87 +21,88 @@ func TestMergeEdgeMetadatas(t *testing.T) { a, b, want report.EdgeMetadatas }{ "Empty a": { - a: report.EdgeMetadatas{}, - b: report.EdgeMetadatas{ - "hostA|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + a: report.EmptyEdgeMetadatas, + b: report.EmptyEdgeMetadatas. + Add("hostA|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(1), MaxConnCountTCP: newu64(2), - }, - }, - want: report.EdgeMetadatas{ - "hostA|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + }), + want: report.EmptyEdgeMetadatas. + Add("hostA|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(1), MaxConnCountTCP: newu64(2), - }, - }, + }), }, "Empty b": { - a: report.EdgeMetadatas{ - "hostA|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + a: report.EmptyEdgeMetadatas. + Add("hostA|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(12), EgressByteCount: newu64(999), - }, - }, - b: report.EdgeMetadatas{}, - want: report.EdgeMetadatas{ - "hostA|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + }), + b: report.EmptyEdgeMetadatas, + want: report.EmptyEdgeMetadatas. + Add("hostA|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(12), EgressByteCount: newu64(999), - }, - }, + }), }, "Host merge": { - a: report.EdgeMetadatas{ - "hostA|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + a: report.EmptyEdgeMetadatas. + Add("hostA|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(12), EgressByteCount: newu64(500), MaxConnCountTCP: newu64(4), - }, - }, - b: report.EdgeMetadatas{ - "hostQ|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + }), + b: report.EmptyEdgeMetadatas. + Add("hostQ|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(1), EgressByteCount: newu64(2), MaxConnCountTCP: newu64(6), - }, - }, - want: report.EdgeMetadatas{ - "hostA|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + }), + want: report.EmptyEdgeMetadatas. + Add("hostA|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(12), EgressByteCount: newu64(500), MaxConnCountTCP: newu64(4), - }, - "hostQ|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + }). + Add("hostQ|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(1), EgressByteCount: newu64(2), MaxConnCountTCP: newu64(6), - }, - }, + }), }, "Edge merge": { - a: report.EdgeMetadatas{ - "hostA|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + a: report.EmptyEdgeMetadatas. + Add("hostA|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(12), EgressByteCount: newu64(1000), MaxConnCountTCP: newu64(7), - }, - }, - b: report.EdgeMetadatas{ - "hostA|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + }), + b: report.EmptyEdgeMetadatas. + Add("hostA|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(1), IngressByteCount: newu64(123), EgressByteCount: newu64(2), MaxConnCountTCP: newu64(9), - }, - }, - want: report.EdgeMetadatas{ - "hostA|:192.168.1.1:12345|:192.168.1.2:80": report.EdgeMetadata{ + }), + want: report.EmptyEdgeMetadatas. + Add("hostA|:192.168.1.1:12345|:192.168.1.2:80", + report.EdgeMetadata{ EgressPacketCount: newu64(13), IngressByteCount: newu64(123), EgressByteCount: newu64(1002), MaxConnCountTCP: newu64(9), - }, - }, + }), }, } { if have := c.a.Merge(c.b); !reflect.DeepEqual(c.want, have) { diff --git a/report/node.go b/report/node.go index 1ff831019..043811b36 100644 --- a/report/node.go +++ b/report/node.go @@ -27,8 +27,8 @@ func MakeNode() Node { return Node{ Counters: EmptyCounters, Sets: EmptySets, - Adjacency: MakeIDList(), - Edges: EdgeMetadatas{}, + Adjacency: EmptyIDList, + Edges: EmptyEdgeMetadatas, Controls: MakeNodeControls(), Latest: EmptyLatestMap, Metrics: Metrics{}, @@ -134,7 +134,7 @@ func (n Node) WithAdjacent(a ...string) Node { func (n Node) WithEdge(dst string, md EdgeMetadata) Node { result := n.Copy() result.Adjacency = result.Adjacency.Add(dst) - result.Edges[dst] = md + result.Edges = result.Edges.Add(dst, md) return result } diff --git a/report/report_test.go b/report/report_test.go index eb87d35b6..84c9746b1 100644 --- a/report/report_test.go +++ b/report/report_test.go @@ -56,7 +56,7 @@ func TestNode(t *testing.T) { if node.Adjacency[0] != "foo" { t.Errorf("want foo, have %v", node.Adjacency) } - if *node.Edges["foo"].EgressPacketCount != 13 { + if v, ok := node.Edges.Lookup("foo"); ok && *v.EgressPacketCount != 13 { t.Errorf("want 13, have %v", node.Edges) } } diff --git a/report/topology.go b/report/topology.go index 68591f477..2cd890db4 100644 --- a/report/topology.go +++ b/report/topology.go @@ -97,11 +97,11 @@ func (t Topology) Validate() error { } // Check all the edge metadatas have entries in adjacencies - for dstNodeID := range nmd.Edges { + nmd.Edges.ForEach(func(dstNodeID string, _ EdgeMetadata) { if _, ok := t.Nodes[dstNodeID]; !ok { errs = append(errs, fmt.Sprintf("node %s missing for edge %q", dstNodeID, nodeID)) } - } + }) } if len(errs) > 0 {