Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate masterN.mesos record for current leader #169

Merged
merged 3 commits into from
Jun 12, 2015
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 62 additions & 23 deletions records/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package records
import (
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"io/ioutil"
"net"
Expand Down Expand Up @@ -286,15 +287,19 @@ func (rg *RecordGenerator) masterRecord(domain string, masters []string, leader
h := strings.Split(leader, "@")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make the leader be the first item of the masters slice.

func (rg *RecordGenerator) masterRecord(domain string, masters ...string) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current func implementation makes an assumption about the order of masters: it's the order in which you expect the ernumerated masterX records to be created. I think this is probably important: if a new leader is elected, you may not want it to become master0 simply because it's the leader. You probably want your DNS records to change as little as possible. And this func should have the least impact on enumeration order, or name/IP mappings - it's just creating the records. So let the caller do the work of ordering, sorting, whatever.. the masters list if a different outcome is desired.

Another consequence of the current overall mesos-dns app implementation is that the leader may not even be in the masters list at some point in time. masters is really fallback-masters (only consider these to be masters if I can't find a leader via ZK). At some point in time, they may not actually be masters any more. Consider a cluster of 3 nodes that suffers the loss of a member, and gains a a new member (VM crashed, was replaced by another VM). And the cycle repeats several times. You end up with a set of running masters (and leader) that's different than the set of statically configured fallback masters.

So the func tries to index the masters as they're listed and begrudgingly assigns the leading master an index out-of-band if it's not actually listed in the masters list. There are probably better ways to do it. But I'm really not trying to optimize that here. I'm just trying to fix #165 without introducing additional inconsistency into the system.

I should probably document some of this in the Go comments of the func.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added my prior ramblings to the masterRecord godocs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if len(h) < 2 {
logging.Error.Println(leader)
return // avoid a panic later
}
ip, port, err := getProto(h[1])
leaderAddress := h[1]
ip, port, err := getProto(leaderAddress)
if err != nil {
logging.Error.Println(err)
return
}
arec := "leader." + domain + "."
rg.insertRR(arec, ip, "A")
arec = "master." + domain + "."
rg.insertRR(arec, ip, "A")

// SRV records
tcp := "_leader._tcp." + domain + "."
udp := "_leader._udp." + domain + "."
Expand All @@ -303,22 +308,43 @@ func (rg *RecordGenerator) masterRecord(domain string, masters []string, leader
rg.insertRR(udp, host, "SRV")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to see this method have no side effects. It would make it easier to reason about and test. We'd isolate the data generation from the mutation effect.

records := masterRecords(tc.domain, tc.masters...) // referentially transparent: same inputs, same outputs, no side effects
if err := rg.insert(records...); err != nil { // side-effect, mutates rg internal state
   // handle error
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be tackled in a separate PR (good idea though!)


// if there is a list of masters, insert that as well
for i, master := range masters {

// skip leader
if leader == master {
continue
}
addedLeaderMasterN := false
idx := 0
for _, master := range masters {

ip, _, err := getProto(master)
if err != nil {
logging.Error.Println(err)
continue
}

// A records (master and masterN)
arec := "master." + domain + "."
if master != leaderAddress {
arec := "master." + domain + "."
added := rg.insertRR(arec, ip, "A")
if !added {
// duplicate master?!
continue
}
}

if master == leaderAddress && addedLeaderMasterN {
// duplicate leader in masters list?!
continue
}

arec := "master" + strconv.Itoa(idx) + "." + domain + "."
rg.insertRR(arec, ip, "A")
arec = "master" + strconv.Itoa(i) + "." + domain + "."
idx++

if master == leaderAddress {
addedLeaderMasterN = true
}
}
// flake: we ended up with a leader that's not in the list of all masters?
if !addedLeaderMasterN {
logging.Error.Printf("warning: leader %q is not in master list", leader)
arec = "master" + strconv.Itoa(idx) + "." + domain + "."
rg.insertRR(arec, ip, "A")
}
}
Expand Down Expand Up @@ -375,37 +401,47 @@ func (rg *RecordGenerator) setFromLocal(host string, ns string) {
}
}

// insertRR inserts host to name's map
// REFACTOR when storage is updated
func (rg *RecordGenerator) insertRR(name string, host string, rtype string) {
logging.VeryVerbose.Println("[" + rtype + "]\t" + name + ": " + host)

func (rg *RecordGenerator) exists(name string, host string, rtype string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

func (rg *RecordGenerator) exists(name, host, rtype string) bool

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

if rtype == "A" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd maintain an inverted index of host to names for this method.

_, ok := rg.records[rtype][host][name]
return ok

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should revisit the key datastructures holistically in order to support discoveryInfo, flexible naming, and http interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kozyraki agreed, in a separate PR

if val, ok := rg.As[name]; ok {
// check if A record already exists
// identical tasks on same slave
for _, b := range val {
if b == host {
return
return true
}
}
rg.As[name] = append(val, host)
} else {
rg.As[name] = []string{host}
}
} else {
if val, ok := rg.SRVs[name]; ok {
// check if SRV record already exists
for _, b := range val {
if b == host {
return
return true
}
}
rg.SRVs[name] = append(val, host)
} else {
rg.SRVs[name] = []string{host}
}
}
return false
}

// insertRR adds a record to the appropriate record map for the given name/host pair,
// but only if the pair is unique. returns true if added, false otherwise.
// TODO(???): REFACTOR when storage is updated
func (rg *RecordGenerator) insertRR(name string, host string, rtype string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Argument types can be coalesced.

func (rg *RecordGenerator) insertRR(name, host, rtype string) bool {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

logging.VeryVerbose.Println("[" + rtype + "]\t" + name + ": " + host)

if rg.exists(name, host, rtype) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wouldn't we be able to change a record? Perhaps this check could be better placed outside this function in call sites where it's actually needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm just maintaining existing functionality. i refactored so that I could use exists in a test. trying to keep the scope of this PR small.

that said, there's probably a lot of room for refactoring in the system. there's also lots of work planned for features and the bulk of this implementation is going to change at that point. probably not worth optimizing this now.

return false
}
if rtype == "A" {
val := rg.As[name]
rg.As[name] = append(val, host)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of making this a nested map?

if _, ok := rg.records[rtype]; !ok {
    rg.records[rtype] = map[string][]string{}
}
rg.records[rtype][name] = append(rg.records[rtype][name], host)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or even better:

if _, ok := rg.records[rtype]; !ok {
    rg.records[rtype] = map[string]map[string]struct{}{}
}

if _, ok := rg.records[rtype][name]; !ok {
    rg.records[rtype][name] = map[string]struct{}{}
    rg.records[rtype][host] = map[string]struct{}{}
}

rg.records[rtype][name][host] = struct{}{}
rg.records[rtype][host][name] = struct{}{}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good suggestions. if the system wasn't about to undergo major re-implementation to support new features, i'd agree to an approach like this.

} else {
val := rg.SRVs[name]
rg.SRVs[name] = append(val, host)
}
return true
}

// returns an array of ports from a range
Expand Down Expand Up @@ -448,6 +484,9 @@ func slaveIdTail(slaveID string) string {
// zk://username:password@host1:port1,host2:port2,.../path
// file:///path/to/file (where file contains one of the above)
func getProto(pair string) (string, string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it confusing we call this function getProto when we're using it with ip:port pairs too. We're essentially just splitting and returning the tokens as different return arguments so this is generalizable.

func split(s, sep string) (string, string, error) {
    ps := strings.SplitN(s, sep, 2)
    if len(ps) != 2 {
        return "", "", fmt.Errorf("%q not found in %q", sep, s)
    }
    return ps[0], ps[1], nil
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree. getProto is abused.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of SplitN we should probably attempt to net.SplitHostPort -- and if that fails we could to parse as a URL. Either way, I think that work should be done in a separate PR.

h := strings.Split(pair, ":")
h := strings.SplitN(pair, ":", 2)
if len(h) != 2 {
return "", "", fmt.Errorf("unable to parse proto from %q", pair)
}
return h[0], h[1], nil
}
124 changes: 124 additions & 0 deletions records/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"github.com/mesosphere/mesos-dns/logging"
"io/ioutil"
"reflect"
"testing"
)

Expand All @@ -13,6 +14,129 @@ func init() {
logging.SetupLogs()
}

func TestMasterRecord(t *testing.T) {
// masterRecord(domain string, masters []string, leader string)
type expectedRR struct {
name string
host string
rtype string
}
tt := []struct {
domain string
masters []string
leader string
exists []expectedRR
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this want or expect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

}{
{"foo.com", nil, "", nil},
{"foo.com", nil, "@", nil},
{"foo.com", nil, "1@", nil},
{"foo.com", nil, "@2", nil},
{"foo.com", nil, "3@4", nil},
{"foo.com", nil, "5@6:7",
[]expectedRR{
{"leader.foo.com.", "6", "A"},
{"master.foo.com.", "6", "A"},
{"master0.foo.com.", "6", "A"},
{"_leader._tcp.foo.com.", "leader.foo.com.:7", "SRV"},
{"_leader._udp.foo.com.", "leader.foo.com.:7", "SRV"},
}},
{"foo.com", []string{"6:7"}, "5@6:7",
[]expectedRR{
{"leader.foo.com.", "6", "A"},
{"master.foo.com.", "6", "A"},
{"master0.foo.com.", "6", "A"},
{"_leader._tcp.foo.com.", "leader.foo.com.:7", "SRV"},
{"_leader._udp.foo.com.", "leader.foo.com.:7", "SRV"},
}},
{"foo.com", []string{"8:9"}, "5@6:7",
[]expectedRR{
{"leader.foo.com.", "6", "A"},
{"master.foo.com.", "6", "A"},
{"master.foo.com.", "8", "A"},
{"master1.foo.com.", "6", "A"},
{"master0.foo.com.", "8", "A"},
{"_leader._tcp.foo.com.", "leader.foo.com.:7", "SRV"},
{"_leader._udp.foo.com.", "leader.foo.com.:7", "SRV"},
}},
{"foo.com", []string{"8:9", "8:9"}, "5@6:7",
[]expectedRR{
{"leader.foo.com.", "6", "A"},
{"master.foo.com.", "6", "A"},
{"master.foo.com.", "8", "A"},
{"master1.foo.com.", "6", "A"},
{"master0.foo.com.", "8", "A"},
{"_leader._tcp.foo.com.", "leader.foo.com.:7", "SRV"},
{"_leader._udp.foo.com.", "leader.foo.com.:7", "SRV"},
}},
{"foo.com", []string{"8:9", "6:7"}, "5@6:7",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add short descriptive comments explaining what each of these tests cases is verifying (where it makes sense)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

[]expectedRR{
{"leader.foo.com.", "6", "A"},
{"master.foo.com.", "6", "A"},
{"master.foo.com.", "8", "A"},
{"master1.foo.com.", "6", "A"},
{"master0.foo.com.", "8", "A"},
{"_leader._tcp.foo.com.", "leader.foo.com.:7", "SRV"},
{"_leader._udp.foo.com.", "leader.foo.com.:7", "SRV"},
}},
{"foo.com", []string{"8:9", "6:7", "6:7"}, "5@6:7",
[]expectedRR{
{"leader.foo.com.", "6", "A"},
{"master.foo.com.", "6", "A"},
{"master.foo.com.", "8", "A"},
{"master1.foo.com.", "6", "A"},
{"master0.foo.com.", "8", "A"},
{"_leader._tcp.foo.com.", "leader.foo.com.:7", "SRV"},
{"_leader._udp.foo.com.", "leader.foo.com.:7", "SRV"},
}},
{"foo.com", []string{"8:9", "6:7", "bob:0"}, "5@6:7",
[]expectedRR{
{"leader.foo.com.", "6", "A"},
{"master.foo.com.", "6", "A"},
{"master.foo.com.", "8", "A"},
{"master.foo.com.", "bob", "A"},
{"master0.foo.com.", "8", "A"},
{"master1.foo.com.", "6", "A"},
{"master2.foo.com.", "bob", "A"},
{"_leader._tcp.foo.com.", "leader.foo.com.:7", "SRV"},
{"_leader._udp.foo.com.", "leader.foo.com.:7", "SRV"},
}},
}
for i, tc := range tt {
rg := RecordGenerator{}
rg.As = make(rrs)
rg.SRVs = make(rrs)
t.Logf("test case %d", i+1)
rg.masterRecord(tc.domain, tc.masters, tc.leader)
if tc.exists == nil {
if len(rg.As) > 0 {
t.Fatalf("test case %d: unexpected As: %v", i+1, rg.As)
}
if len(rg.SRVs) > 0 {
t.Fatalf("test case %d: unexpected SRVs: %v", i+1, rg.SRVs)
}
}
expectedA := make(rrs)
expectedSRV := make(rrs)
for _, e := range tc.exists {
found := rg.exists(e.name, e.host, e.rtype)
if !found {
t.Fatalf("test case %d: missing expected record: name=%q host=%q rtype=%s, As=%v", i+1, e.name, e.host, e.rtype, rg.As)
}
if e.rtype == "A" {
expectedA[e.name] = append(expectedA[e.name], e.host)
} else {
expectedSRV[e.name] = append(expectedSRV[e.name], e.host)
}
}
if !reflect.DeepEqual(rg.As, expectedA) {
t.Fatalf("test case %d: expected As of %v instead of %v", i+1, expectedA, rg.As)
}
if !reflect.DeepEqual(rg.SRVs, expectedSRV) {
t.Fatalf("test case %d: expected SRVs of %v instead of %v", i+1, expectedSRV, rg.SRVs)
}
}
}

func TestSanitizedSlaveAddress(t *testing.T) {
x := sanitizedSlaveAddress("1.2.3.4")
if x != "1.2.3.4" {
Expand Down