diff --git a/.gitignore b/.gitignore index fff60c139df..6b9bbeb787f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ *.swp tags +# Eclipse files +.project +.settings/ + # cbson & other C build dirs **/build @@ -21,3 +25,7 @@ third_party/mysql java/vtocc-client/target java/vtocc-jdbc-driver/target third_party/acolyte + +# intellij files +*.iml +.idea diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..26ba8f466cf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: go +go: + - 1.4 +env: + global: + - MYSQL_FLAVOR=MariaDB + matrix: + - MAKE_TARGET=java_vtgate_client_test + - MAKE_TARGET=unit_test_goveralls + - MAKE_TARGET=small_integration_test + - MAKE_TARGET=medium_integration_test + - MAKE_TARGET=large_integration_test + - MAKE_TARGET=queryservice_test + - MAKE_TARGET=unit_test +before_install: + - bash -v travis/dependencies.sh +install: + - bash -v bootstrap.sh +script: + - source dev.env + - travis_retry make $MAKE_TARGET diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 00000000000..41cbb1bedcb --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,14 @@ +{ + "ImportPath": "github.com/youtube/vitess", + "GoVersion": "go1.4.1", + "Packages": [ + "./go/vt/etcdtopo" + ], + "Deps": [ + { + "ImportPath": "github.com/coreos/go-etcd/etcd", + "Comment": "v0.2.0-rc1-127-g6fe04d5", + "Rev": "6fe04d580dfb71c9e34cbce2f4df9eefd1e1241e" + } + ] +} diff --git a/Godeps/Readme b/Godeps/Readme new file mode 100644 index 00000000000..4cdaa53d56d --- /dev/null +++ b/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/Godeps/_workspace/.gitignore b/Godeps/_workspace/.gitignore new file mode 100644 index 00000000000..f037d684ef2 --- /dev/null +++ b/Godeps/_workspace/.gitignore @@ -0,0 +1,2 @@ +/pkg +/bin diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child.go new file mode 100644 index 00000000000..7122be049e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child.go @@ -0,0 +1,23 @@ +package etcd + +// Add a new directory with a random etcd-generated key under the given path. +func (c *Client) AddChildDir(key string, ttl uint64) (*Response, error) { + raw, err := c.post(key, "", ttl) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +// Add a new file with a random etcd-generated key under the given path. +func (c *Client) AddChild(key string, value string, ttl uint64) (*Response, error) { + raw, err := c.post(key, value, ttl) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child_test.go new file mode 100644 index 00000000000..26223ff1c85 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/add_child_test.go @@ -0,0 +1,73 @@ +package etcd + +import "testing" + +func TestAddChild(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("fooDir", true) + c.Delete("nonexistentDir", true) + }() + + c.CreateDir("fooDir", 5) + + _, err := c.AddChild("fooDir", "v0", 5) + if err != nil { + t.Fatal(err) + } + + _, err = c.AddChild("fooDir", "v1", 5) + if err != nil { + t.Fatal(err) + } + + resp, err := c.Get("fooDir", true, false) + // The child with v0 should proceed the child with v1 because it's added + // earlier, so it should have a lower key. + if !(len(resp.Node.Nodes) == 2 && (resp.Node.Nodes[0].Value == "v0" && resp.Node.Nodes[1].Value == "v1")) { + t.Fatalf("AddChild 1 failed. There should be two chlidren whose values are v0 and v1, respectively."+ + " The response was: %#v", resp) + } + + // Creating a child under a nonexistent directory should succeed. + // The directory should be created. + resp, err = c.AddChild("nonexistentDir", "foo", 5) + if err != nil { + t.Fatal(err) + } +} + +func TestAddChildDir(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("fooDir", true) + c.Delete("nonexistentDir", true) + }() + + c.CreateDir("fooDir", 5) + + _, err := c.AddChildDir("fooDir", 5) + if err != nil { + t.Fatal(err) + } + + _, err = c.AddChildDir("fooDir", 5) + if err != nil { + t.Fatal(err) + } + + resp, err := c.Get("fooDir", true, false) + // The child with v0 should proceed the child with v1 because it's added + // earlier, so it should have a lower key. + if !(len(resp.Node.Nodes) == 2 && (len(resp.Node.Nodes[0].Nodes) == 0 && len(resp.Node.Nodes[1].Nodes) == 0)) { + t.Fatalf("AddChildDir 1 failed. There should be two chlidren whose values are v0 and v1, respectively."+ + " The response was: %#v", resp) + } + + // Creating a child under a nonexistent directory should succeed. + // The directory should be created. + resp, err = c.AddChildDir("nonexistentDir", 5) + if err != nil { + t.Fatal(err) + } +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client.go new file mode 100644 index 00000000000..f6ae5486173 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client.go @@ -0,0 +1,435 @@ +package etcd + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "path" + "time" +) + +// See SetConsistency for how to use these constants. +const ( + // Using strings rather than iota because the consistency level + // could be persisted to disk, so it'd be better to use + // human-readable values. + STRONG_CONSISTENCY = "STRONG" + WEAK_CONSISTENCY = "WEAK" +) + +const ( + defaultBufferSize = 10 +) + +type Config struct { + CertFile string `json:"certFile"` + KeyFile string `json:"keyFile"` + CaCertFile []string `json:"caCertFiles"` + DialTimeout time.Duration `json:"timeout"` + Consistency string `json:"consistency"` +} + +type Client struct { + config Config `json:"config"` + cluster *Cluster `json:"cluster"` + httpClient *http.Client + persistence io.Writer + cURLch chan string + // CheckRetry can be used to control the policy for failed requests + // and modify the cluster if needed. + // The client calls it before sending requests again, and + // stops retrying if CheckRetry returns some error. The cases that + // this function needs to handle include no response and unexpected + // http status code of response. + // If CheckRetry is nil, client will call the default one + // `DefaultCheckRetry`. + // Argument cluster is the etcd.Cluster object that these requests have been made on. + // Argument numReqs is the number of http.Requests that have been made so far. + // Argument lastResp is the http.Responses from the last request. + // Argument err is the reason of the failure. + CheckRetry func(cluster *Cluster, numReqs int, + lastResp http.Response, err error) error +} + +// NewClient create a basic client that is configured to be used +// with the given machine list. +func NewClient(machines []string) *Client { + config := Config{ + // default timeout is one second + DialTimeout: time.Second, + // default consistency level is STRONG + Consistency: STRONG_CONSISTENCY, + } + + client := &Client{ + cluster: NewCluster(machines), + config: config, + } + + client.initHTTPClient() + client.saveConfig() + + return client +} + +// NewTLSClient create a basic client with TLS configuration +func NewTLSClient(machines []string, cert, key, caCert string) (*Client, error) { + // overwrite the default machine to use https + if len(machines) == 0 { + machines = []string{"https://127.0.0.1:4001"} + } + + config := Config{ + // default timeout is one second + DialTimeout: time.Second, + // default consistency level is STRONG + Consistency: STRONG_CONSISTENCY, + CertFile: cert, + KeyFile: key, + CaCertFile: make([]string, 0), + } + + client := &Client{ + cluster: NewCluster(machines), + config: config, + } + + err := client.initHTTPSClient(cert, key) + if err != nil { + return nil, err + } + + err = client.AddRootCA(caCert) + + client.saveConfig() + + return client, nil +} + +// NewClientFromFile creates a client from a given file path. +// The given file is expected to use the JSON format. +func NewClientFromFile(fpath string) (*Client, error) { + fi, err := os.Open(fpath) + if err != nil { + return nil, err + } + + defer func() { + if err := fi.Close(); err != nil { + panic(err) + } + }() + + return NewClientFromReader(fi) +} + +// NewClientFromReader creates a Client configured from a given reader. +// The configuration is expected to use the JSON format. +func NewClientFromReader(reader io.Reader) (*Client, error) { + c := new(Client) + + b, err := ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + + err = json.Unmarshal(b, c) + if err != nil { + return nil, err + } + if c.config.CertFile == "" { + c.initHTTPClient() + } else { + err = c.initHTTPSClient(c.config.CertFile, c.config.KeyFile) + } + + if err != nil { + return nil, err + } + + for _, caCert := range c.config.CaCertFile { + if err := c.AddRootCA(caCert); err != nil { + return nil, err + } + } + + return c, nil +} + +// Override the Client's HTTP Transport object +func (c *Client) SetTransport(tr *http.Transport) { + c.httpClient.Transport = tr +} + +// initHTTPClient initializes a HTTP client for etcd client +func (c *Client) initHTTPClient() { + tr := &http.Transport{ + Dial: c.dial, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + c.httpClient = &http.Client{Transport: tr} +} + +// initHTTPClient initializes a HTTPS client for etcd client +func (c *Client) initHTTPSClient(cert, key string) error { + if cert == "" || key == "" { + return errors.New("Require both cert and key path") + } + + tlsCert, err := tls.LoadX509KeyPair(cert, key) + if err != nil { + return err + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + InsecureSkipVerify: true, + } + + tr := &http.Transport{ + TLSClientConfig: tlsConfig, + Dial: c.dial, + } + + c.httpClient = &http.Client{Transport: tr} + return nil +} + +// SetPersistence sets a writer to which the config will be +// written every time it's changed. +func (c *Client) SetPersistence(writer io.Writer) { + c.persistence = writer +} + +// SetConsistency changes the consistency level of the client. +// +// When consistency is set to STRONG_CONSISTENCY, all requests, +// including GET, are sent to the leader. This means that, assuming +// the absence of leader failures, GET requests are guaranteed to see +// the changes made by previous requests. +// +// When consistency is set to WEAK_CONSISTENCY, other requests +// are still sent to the leader, but GET requests are sent to a +// random server from the server pool. This reduces the read +// load on the leader, but it's not guaranteed that the GET requests +// will see changes made by previous requests (they might have not +// yet been committed on non-leader servers). +func (c *Client) SetConsistency(consistency string) error { + if !(consistency == STRONG_CONSISTENCY || consistency == WEAK_CONSISTENCY) { + return errors.New("The argument must be either STRONG_CONSISTENCY or WEAK_CONSISTENCY.") + } + c.config.Consistency = consistency + return nil +} + +// Sets the DialTimeout value +func (c *Client) SetDialTimeout(d time.Duration) { + c.config.DialTimeout = d +} + +// AddRootCA adds a root CA cert for the etcd client +func (c *Client) AddRootCA(caCert string) error { + if c.httpClient == nil { + return errors.New("Client has not been initialized yet!") + } + + certBytes, err := ioutil.ReadFile(caCert) + if err != nil { + return err + } + + tr, ok := c.httpClient.Transport.(*http.Transport) + + if !ok { + panic("AddRootCA(): Transport type assert should not fail") + } + + if tr.TLSClientConfig.RootCAs == nil { + caCertPool := x509.NewCertPool() + ok = caCertPool.AppendCertsFromPEM(certBytes) + if ok { + tr.TLSClientConfig.RootCAs = caCertPool + } + tr.TLSClientConfig.InsecureSkipVerify = false + } else { + ok = tr.TLSClientConfig.RootCAs.AppendCertsFromPEM(certBytes) + } + + if !ok { + err = errors.New("Unable to load caCert") + } + + c.config.CaCertFile = append(c.config.CaCertFile, caCert) + c.saveConfig() + + return err +} + +// SetCluster updates cluster information using the given machine list. +func (c *Client) SetCluster(machines []string) bool { + success := c.internalSyncCluster(machines) + return success +} + +func (c *Client) GetCluster() []string { + return c.cluster.Machines +} + +// SyncCluster updates the cluster information using the internal machine list. +func (c *Client) SyncCluster() bool { + return c.internalSyncCluster(c.cluster.Machines) +} + +// internalSyncCluster syncs cluster information using the given machine list. +func (c *Client) internalSyncCluster(machines []string) bool { + for _, machine := range machines { + httpPath := c.createHttpPath(machine, path.Join(version, "machines")) + resp, err := c.httpClient.Get(httpPath) + if err != nil { + // try another machine in the cluster + continue + } else { + b, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + // try another machine in the cluster + continue + } + + // update Machines List + c.cluster.updateFromStr(string(b)) + + // update leader + // the first one in the machine list is the leader + c.cluster.switchLeader(0) + + logger.Debug("sync.machines ", c.cluster.Machines) + c.saveConfig() + return true + } + } + return false +} + +// createHttpPath creates a complete HTTP URL. +// serverName should contain both the host name and a port number, if any. +func (c *Client) createHttpPath(serverName string, _path string) string { + u, err := url.Parse(serverName) + if err != nil { + panic(err) + } + + u.Path = path.Join(u.Path, _path) + + if u.Scheme == "" { + u.Scheme = "http" + } + return u.String() +} + +// dial attempts to open a TCP connection to the provided address, explicitly +// enabling keep-alives with a one-second interval. +func (c *Client) dial(network, addr string) (net.Conn, error) { + conn, err := net.DialTimeout(network, addr, c.config.DialTimeout) + if err != nil { + return nil, err + } + + tcpConn, ok := conn.(*net.TCPConn) + if !ok { + return nil, errors.New("Failed type-assertion of net.Conn as *net.TCPConn") + } + + // Keep TCP alive to check whether or not the remote machine is down + if err = tcpConn.SetKeepAlive(true); err != nil { + return nil, err + } + + if err = tcpConn.SetKeepAlivePeriod(time.Second); err != nil { + return nil, err + } + + return tcpConn, nil +} + +func (c *Client) OpenCURL() { + c.cURLch = make(chan string, defaultBufferSize) +} + +func (c *Client) CloseCURL() { + c.cURLch = nil +} + +func (c *Client) sendCURL(command string) { + go func() { + select { + case c.cURLch <- command: + default: + } + }() +} + +func (c *Client) RecvCURL() string { + return <-c.cURLch +} + +// saveConfig saves the current config using c.persistence. +func (c *Client) saveConfig() error { + if c.persistence != nil { + b, err := json.Marshal(c) + if err != nil { + return err + } + + _, err = c.persistence.Write(b) + if err != nil { + return err + } + } + + return nil +} + +// MarshalJSON implements the Marshaller interface +// as defined by the standard JSON package. +func (c *Client) MarshalJSON() ([]byte, error) { + b, err := json.Marshal(struct { + Config Config `json:"config"` + Cluster *Cluster `json:"cluster"` + }{ + Config: c.config, + Cluster: c.cluster, + }) + + if err != nil { + return nil, err + } + + return b, nil +} + +// UnmarshalJSON implements the Unmarshaller interface +// as defined by the standard JSON package. +func (c *Client) UnmarshalJSON(b []byte) error { + temp := struct { + Config Config `json:"config"` + Cluster *Cluster `json:"cluster"` + }{} + err := json.Unmarshal(b, &temp) + if err != nil { + return err + } + + c.cluster = temp.Cluster + c.config = temp.Config + return nil +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client_test.go new file mode 100644 index 00000000000..c245e479844 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/client_test.go @@ -0,0 +1,96 @@ +package etcd + +import ( + "encoding/json" + "fmt" + "net" + "net/url" + "os" + "testing" +) + +// To pass this test, we need to create a cluster of 3 machines +// The server should be listening on 127.0.0.1:4001, 4002, 4003 +func TestSync(t *testing.T) { + fmt.Println("Make sure there are three nodes at 0.0.0.0:4001-4003") + + // Explicit trailing slash to ensure this doesn't reproduce: + // https://github.com/coreos/go-etcd/issues/82 + c := NewClient([]string{"http://127.0.0.1:4001/"}) + + success := c.SyncCluster() + if !success { + t.Fatal("cannot sync machines") + } + + for _, m := range c.GetCluster() { + u, err := url.Parse(m) + if err != nil { + t.Fatal(err) + } + if u.Scheme != "http" { + t.Fatal("scheme must be http") + } + + host, _, err := net.SplitHostPort(u.Host) + if err != nil { + t.Fatal(err) + } + if host != "127.0.0.1" { + t.Fatal("Host must be 127.0.0.1") + } + } + + badMachines := []string{"abc", "edef"} + + success = c.SetCluster(badMachines) + + if success { + t.Fatal("should not sync on bad machines") + } + + goodMachines := []string{"127.0.0.1:4002"} + + success = c.SetCluster(goodMachines) + + if !success { + t.Fatal("cannot sync machines") + } else { + fmt.Println(c.cluster.Machines) + } + +} + +func TestPersistence(t *testing.T) { + c := NewClient(nil) + c.SyncCluster() + + fo, err := os.Create("config.json") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := fo.Close(); err != nil { + panic(err) + } + }() + + c.SetPersistence(fo) + err = c.saveConfig() + if err != nil { + t.Fatal(err) + } + + c2, err := NewClientFromFile("config.json") + if err != nil { + t.Fatal(err) + } + + // Verify that the two clients have the same config + b1, _ := json.Marshal(c) + b2, _ := json.Marshal(c2) + + if string(b1) != string(b2) { + t.Fatalf("The two configs should be equal!") + } +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/cluster.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/cluster.go new file mode 100644 index 00000000000..aaa20546e32 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/cluster.go @@ -0,0 +1,51 @@ +package etcd + +import ( + "net/url" + "strings" +) + +type Cluster struct { + Leader string `json:"leader"` + Machines []string `json:"machines"` +} + +func NewCluster(machines []string) *Cluster { + // if an empty slice was sent in then just assume HTTP 4001 on localhost + if len(machines) == 0 { + machines = []string{"http://127.0.0.1:4001"} + } + + // default leader and machines + return &Cluster{ + Leader: machines[0], + Machines: machines, + } +} + +// switchLeader switch the current leader to machines[num] +func (cl *Cluster) switchLeader(num int) { + logger.Debugf("switch.leader[from %v to %v]", + cl.Leader, cl.Machines[num]) + + cl.Leader = cl.Machines[num] +} + +func (cl *Cluster) updateFromStr(machines string) { + cl.Machines = strings.Split(machines, ", ") +} + +func (cl *Cluster) updateLeader(leader string) { + logger.Debugf("update.leader[%s,%s]", cl.Leader, leader) + cl.Leader = leader +} + +func (cl *Cluster) updateLeaderFromURL(u *url.URL) { + var leader string + if u.Scheme == "" { + leader = "http://" + u.Host + } else { + leader = u.Scheme + "://" + u.Host + } + cl.updateLeader(leader) +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete.go new file mode 100644 index 00000000000..11131bb7602 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete.go @@ -0,0 +1,34 @@ +package etcd + +import "fmt" + +func (c *Client) CompareAndDelete(key string, prevValue string, prevIndex uint64) (*Response, error) { + raw, err := c.RawCompareAndDelete(key, prevValue, prevIndex) + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +func (c *Client) RawCompareAndDelete(key string, prevValue string, prevIndex uint64) (*RawResponse, error) { + if prevValue == "" && prevIndex == 0 { + return nil, fmt.Errorf("You must give either prevValue or prevIndex.") + } + + options := Options{} + if prevValue != "" { + options["prevValue"] = prevValue + } + if prevIndex != 0 { + options["prevIndex"] = prevIndex + } + + raw, err := c.delete(key, options) + + if err != nil { + return nil, err + } + + return raw, err +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete_test.go new file mode 100644 index 00000000000..223e50f2916 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_delete_test.go @@ -0,0 +1,46 @@ +package etcd + +import ( + "testing" +) + +func TestCompareAndDelete(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("foo", true) + }() + + c.Set("foo", "bar", 5) + + // This should succeed an correct prevValue + resp, err := c.CompareAndDelete("foo", "bar", 0) + if err != nil { + t.Fatal(err) + } + if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) { + t.Fatalf("CompareAndDelete 1 prevNode failed: %#v", resp) + } + + resp, _ = c.Set("foo", "bar", 5) + // This should fail because it gives an incorrect prevValue + _, err = c.CompareAndDelete("foo", "xxx", 0) + if err == nil { + t.Fatalf("CompareAndDelete 2 should have failed. The response is: %#v", resp) + } + + // This should succeed because it gives an correct prevIndex + resp, err = c.CompareAndDelete("foo", "", resp.Node.ModifiedIndex) + if err != nil { + t.Fatal(err) + } + if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) { + t.Fatalf("CompareAndSwap 3 prevNode failed: %#v", resp) + } + + c.Set("foo", "bar", 5) + // This should fail because it gives an incorrect prevIndex + resp, err = c.CompareAndDelete("foo", "", 29817514) + if err == nil { + t.Fatalf("CompareAndDelete 4 should have failed. The response is: %#v", resp) + } +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap.go new file mode 100644 index 00000000000..bb4f90643ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap.go @@ -0,0 +1,36 @@ +package etcd + +import "fmt" + +func (c *Client) CompareAndSwap(key string, value string, ttl uint64, + prevValue string, prevIndex uint64) (*Response, error) { + raw, err := c.RawCompareAndSwap(key, value, ttl, prevValue, prevIndex) + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +func (c *Client) RawCompareAndSwap(key string, value string, ttl uint64, + prevValue string, prevIndex uint64) (*RawResponse, error) { + if prevValue == "" && prevIndex == 0 { + return nil, fmt.Errorf("You must give either prevValue or prevIndex.") + } + + options := Options{} + if prevValue != "" { + options["prevValue"] = prevValue + } + if prevIndex != 0 { + options["prevIndex"] = prevIndex + } + + raw, err := c.put(key, value, ttl, options) + + if err != nil { + return nil, err + } + + return raw, err +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap_test.go new file mode 100644 index 00000000000..14a1b00f5a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/compare_and_swap_test.go @@ -0,0 +1,57 @@ +package etcd + +import ( + "testing" +) + +func TestCompareAndSwap(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("foo", true) + }() + + c.Set("foo", "bar", 5) + + // This should succeed + resp, err := c.CompareAndSwap("foo", "bar2", 5, "bar", 0) + if err != nil { + t.Fatal(err) + } + if !(resp.Node.Value == "bar2" && resp.Node.Key == "/foo" && resp.Node.TTL == 5) { + t.Fatalf("CompareAndSwap 1 failed: %#v", resp) + } + + if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) { + t.Fatalf("CompareAndSwap 1 prevNode failed: %#v", resp) + } + + // This should fail because it gives an incorrect prevValue + resp, err = c.CompareAndSwap("foo", "bar3", 5, "xxx", 0) + if err == nil { + t.Fatalf("CompareAndSwap 2 should have failed. The response is: %#v", resp) + } + + resp, err = c.Set("foo", "bar", 5) + if err != nil { + t.Fatal(err) + } + + // This should succeed + resp, err = c.CompareAndSwap("foo", "bar2", 5, "", resp.Node.ModifiedIndex) + if err != nil { + t.Fatal(err) + } + if !(resp.Node.Value == "bar2" && resp.Node.Key == "/foo" && resp.Node.TTL == 5) { + t.Fatalf("CompareAndSwap 3 failed: %#v", resp) + } + + if !(resp.PrevNode.Value == "bar" && resp.PrevNode.Key == "/foo" && resp.PrevNode.TTL == 5) { + t.Fatalf("CompareAndSwap 3 prevNode failed: %#v", resp) + } + + // This should fail because it gives an incorrect prevIndex + resp, err = c.CompareAndSwap("foo", "bar3", 5, "", 29817514) + if err == nil { + t.Fatalf("CompareAndSwap 4 should have failed. The response is: %#v", resp) + } +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug.go new file mode 100644 index 00000000000..0f777886bae --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug.go @@ -0,0 +1,55 @@ +package etcd + +import ( + "fmt" + "io/ioutil" + "log" + "strings" +) + +var logger *etcdLogger + +func SetLogger(l *log.Logger) { + logger = &etcdLogger{l} +} + +func GetLogger() *log.Logger { + return logger.log +} + +type etcdLogger struct { + log *log.Logger +} + +func (p *etcdLogger) Debug(args ...interface{}) { + msg := "DEBUG: " + fmt.Sprint(args...) + p.log.Println(msg) +} + +func (p *etcdLogger) Debugf(f string, args ...interface{}) { + msg := "DEBUG: " + fmt.Sprintf(f, args...) + // Append newline if necessary + if !strings.HasSuffix(msg, "\n") { + msg = msg + "\n" + } + p.log.Print(msg) +} + +func (p *etcdLogger) Warning(args ...interface{}) { + msg := "WARNING: " + fmt.Sprint(args...) + p.log.Println(msg) +} + +func (p *etcdLogger) Warningf(f string, args ...interface{}) { + msg := "WARNING: " + fmt.Sprintf(f, args...) + // Append newline if necessary + if !strings.HasSuffix(msg, "\n") { + msg = msg + "\n" + } + p.log.Print(msg) +} + +func init() { + // Default logger uses the go default log. + SetLogger(log.New(ioutil.Discard, "go-etcd", log.LstdFlags)) +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug_test.go new file mode 100644 index 00000000000..97f6d1110bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/debug_test.go @@ -0,0 +1,28 @@ +package etcd + +import ( + "testing" +) + +type Foo struct{} +type Bar struct { + one string + two int +} + +// Tests that logs don't panic with arbitrary interfaces +func TestDebug(t *testing.T) { + f := &Foo{} + b := &Bar{"asfd", 3} + for _, test := range []interface{}{ + 1234, + "asdf", + f, + b, + } { + logger.Debug(test) + logger.Debugf("something, %s", test) + logger.Warning(test) + logger.Warningf("something, %s", test) + } +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete.go new file mode 100644 index 00000000000..b37accd7db3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete.go @@ -0,0 +1,40 @@ +package etcd + +// Delete deletes the given key. +// +// When recursive set to false, if the key points to a +// directory the method will fail. +// +// When recursive set to true, if the key points to a file, +// the file will be deleted; if the key points to a directory, +// then everything under the directory (including all child directories) +// will be deleted. +func (c *Client) Delete(key string, recursive bool) (*Response, error) { + raw, err := c.RawDelete(key, recursive, false) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +// DeleteDir deletes an empty directory or a key value pair +func (c *Client) DeleteDir(key string) (*Response, error) { + raw, err := c.RawDelete(key, false, true) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +func (c *Client) RawDelete(key string, recursive bool, dir bool) (*RawResponse, error) { + ops := Options{ + "recursive": recursive, + "dir": dir, + } + + return c.delete(key, ops) +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete_test.go new file mode 100644 index 00000000000..5904971556d --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/delete_test.go @@ -0,0 +1,81 @@ +package etcd + +import ( + "testing" +) + +func TestDelete(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("foo", true) + }() + + c.Set("foo", "bar", 5) + resp, err := c.Delete("foo", false) + if err != nil { + t.Fatal(err) + } + + if !(resp.Node.Value == "") { + t.Fatalf("Delete failed with %s", resp.Node.Value) + } + + if !(resp.PrevNode.Value == "bar") { + t.Fatalf("Delete PrevNode failed with %s", resp.Node.Value) + } + + resp, err = c.Delete("foo", false) + if err == nil { + t.Fatalf("Delete should have failed because the key foo did not exist. "+ + "The response was: %v", resp) + } +} + +func TestDeleteAll(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("foo", true) + c.Delete("fooDir", true) + }() + + c.SetDir("foo", 5) + // test delete an empty dir + resp, err := c.DeleteDir("foo") + if err != nil { + t.Fatal(err) + } + + if !(resp.Node.Value == "") { + t.Fatalf("DeleteAll 1 failed: %#v", resp) + } + + if !(resp.PrevNode.Dir == true && resp.PrevNode.Value == "") { + t.Fatalf("DeleteAll 1 PrevNode failed: %#v", resp) + } + + c.CreateDir("fooDir", 5) + c.Set("fooDir/foo", "bar", 5) + _, err = c.DeleteDir("fooDir") + if err == nil { + t.Fatal("should not able to delete a non-empty dir with deletedir") + } + + resp, err = c.Delete("fooDir", true) + if err != nil { + t.Fatal(err) + } + + if !(resp.Node.Value == "") { + t.Fatalf("DeleteAll 2 failed: %#v", resp) + } + + if !(resp.PrevNode.Dir == true && resp.PrevNode.Value == "") { + t.Fatalf("DeleteAll 2 PrevNode failed: %#v", resp) + } + + resp, err = c.Delete("foo", true) + if err == nil { + t.Fatalf("DeleteAll should have failed because the key foo did not exist. "+ + "The response was: %v", resp) + } +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/error.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/error.go new file mode 100644 index 00000000000..7e692872472 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/error.go @@ -0,0 +1,48 @@ +package etcd + +import ( + "encoding/json" + "fmt" +) + +const ( + ErrCodeEtcdNotReachable = 501 +) + +var ( + errorMap = map[int]string{ + ErrCodeEtcdNotReachable: "All the given peers are not reachable", + } +) + +type EtcdError struct { + ErrorCode int `json:"errorCode"` + Message string `json:"message"` + Cause string `json:"cause,omitempty"` + Index uint64 `json:"index"` +} + +func (e EtcdError) Error() string { + return fmt.Sprintf("%v: %v (%v) [%v]", e.ErrorCode, e.Message, e.Cause, e.Index) +} + +func newError(errorCode int, cause string, index uint64) *EtcdError { + return &EtcdError{ + ErrorCode: errorCode, + Message: errorMap[errorCode], + Cause: cause, + Index: index, + } +} + +func handleError(b []byte) error { + etcdErr := new(EtcdError) + + err := json.Unmarshal(b, etcdErr) + if err != nil { + logger.Warningf("cannot unmarshal etcd error: %v", err) + return err + } + + return etcdErr +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get.go new file mode 100644 index 00000000000..976bf07fd74 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get.go @@ -0,0 +1,27 @@ +package etcd + +// Get gets the file or directory associated with the given key. +// If the key points to a directory, files and directories under +// it will be returned in sorted or unsorted order, depending on +// the sort flag. +// If recursive is set to false, contents under child directories +// will not be returned. +// If recursive is set to true, all the contents will be returned. +func (c *Client) Get(key string, sort, recursive bool) (*Response, error) { + raw, err := c.RawGet(key, sort, recursive) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +func (c *Client) RawGet(key string, sort, recursive bool) (*RawResponse, error) { + ops := Options{ + "recursive": recursive, + "sorted": sort, + } + + return c.get(key, ops) +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get_test.go new file mode 100644 index 00000000000..279c4e26f8b --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/get_test.go @@ -0,0 +1,131 @@ +package etcd + +import ( + "reflect" + "testing" +) + +// cleanNode scrubs Expiration, ModifiedIndex and CreatedIndex of a node. +func cleanNode(n *Node) { + n.Expiration = nil + n.ModifiedIndex = 0 + n.CreatedIndex = 0 +} + +// cleanResult scrubs a result object two levels deep of Expiration, +// ModifiedIndex and CreatedIndex. +func cleanResult(result *Response) { + // TODO(philips): make this recursive. + cleanNode(result.Node) + for i, _ := range result.Node.Nodes { + cleanNode(result.Node.Nodes[i]) + for j, _ := range result.Node.Nodes[i].Nodes { + cleanNode(result.Node.Nodes[i].Nodes[j]) + } + } +} + +func TestGet(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("foo", true) + }() + + c.Set("foo", "bar", 5) + + result, err := c.Get("foo", false, false) + + if err != nil { + t.Fatal(err) + } + + if result.Node.Key != "/foo" || result.Node.Value != "bar" { + t.Fatalf("Get failed with %s %s %v", result.Node.Key, result.Node.Value, result.Node.TTL) + } + + result, err = c.Get("goo", false, false) + if err == nil { + t.Fatalf("should not be able to get non-exist key") + } +} + +func TestGetAll(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("fooDir", true) + }() + + c.CreateDir("fooDir", 5) + c.Set("fooDir/k0", "v0", 5) + c.Set("fooDir/k1", "v1", 5) + + // Return kv-pairs in sorted order + result, err := c.Get("fooDir", true, false) + + if err != nil { + t.Fatal(err) + } + + expected := Nodes{ + &Node{ + Key: "/fooDir/k0", + Value: "v0", + TTL: 5, + }, + &Node{ + Key: "/fooDir/k1", + Value: "v1", + TTL: 5, + }, + } + + cleanResult(result) + + if !reflect.DeepEqual(result.Node.Nodes, expected) { + t.Fatalf("(actual) %v != (expected) %v", result.Node.Nodes, expected) + } + + // Test the `recursive` option + c.CreateDir("fooDir/childDir", 5) + c.Set("fooDir/childDir/k2", "v2", 5) + + // Return kv-pairs in sorted order + result, err = c.Get("fooDir", true, true) + + cleanResult(result) + + if err != nil { + t.Fatal(err) + } + + expected = Nodes{ + &Node{ + Key: "/fooDir/childDir", + Dir: true, + Nodes: Nodes{ + &Node{ + Key: "/fooDir/childDir/k2", + Value: "v2", + TTL: 5, + }, + }, + TTL: 5, + }, + &Node{ + Key: "/fooDir/k0", + Value: "v0", + TTL: 5, + }, + &Node{ + Key: "/fooDir/k1", + Value: "v1", + TTL: 5, + }, + } + + cleanResult(result) + + if !reflect.DeepEqual(result.Node.Nodes, expected) { + t.Fatalf("(actual) %v != (expected) %v", result.Node.Nodes, expected) + } +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/options.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/options.go new file mode 100644 index 00000000000..701c9b35b97 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/options.go @@ -0,0 +1,72 @@ +package etcd + +import ( + "fmt" + "net/url" + "reflect" +) + +type Options map[string]interface{} + +// An internally-used data structure that represents a mapping +// between valid options and their kinds +type validOptions map[string]reflect.Kind + +// Valid options for GET, PUT, POST, DELETE +// Using CAPITALIZED_UNDERSCORE to emphasize that these +// values are meant to be used as constants. +var ( + VALID_GET_OPTIONS = validOptions{ + "recursive": reflect.Bool, + "consistent": reflect.Bool, + "sorted": reflect.Bool, + "wait": reflect.Bool, + "waitIndex": reflect.Uint64, + } + + VALID_PUT_OPTIONS = validOptions{ + "prevValue": reflect.String, + "prevIndex": reflect.Uint64, + "prevExist": reflect.Bool, + "dir": reflect.Bool, + } + + VALID_POST_OPTIONS = validOptions{} + + VALID_DELETE_OPTIONS = validOptions{ + "recursive": reflect.Bool, + "dir": reflect.Bool, + "prevValue": reflect.String, + "prevIndex": reflect.Uint64, + } +) + +// Convert options to a string of HTML parameters +func (ops Options) toParameters(validOps validOptions) (string, error) { + p := "?" + values := url.Values{} + + if ops == nil { + return "", nil + } + + for k, v := range ops { + // Check if the given option is valid (that it exists) + kind := validOps[k] + if kind == reflect.Invalid { + return "", fmt.Errorf("Invalid option: %v", k) + } + + // Check if the given option is of the valid type + t := reflect.TypeOf(v) + if kind != t.Kind() { + return "", fmt.Errorf("Option %s should be of %v kind, not of %v kind.", + k, kind, t.Kind()) + } + + values.Set(k, fmt.Sprintf("%v", v)) + } + + p += values.Encode() + return p, nil +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests.go new file mode 100644 index 00000000000..fa6d36a9f2e --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/requests.go @@ -0,0 +1,396 @@ +package etcd + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "math/rand" + "net/http" + "net/url" + "path" + "strings" + "sync" + "time" +) + +// Errors introduced by handling requests +var ( + ErrRequestCancelled = errors.New("sending request is cancelled") +) + +type RawRequest struct { + Method string + RelativePath string + Values url.Values + Cancel <-chan bool +} + +// NewRawRequest returns a new RawRequest +func NewRawRequest(method, relativePath string, values url.Values, cancel <-chan bool) *RawRequest { + return &RawRequest{ + Method: method, + RelativePath: relativePath, + Values: values, + Cancel: cancel, + } +} + +// getCancelable issues a cancelable GET request +func (c *Client) getCancelable(key string, options Options, + cancel <-chan bool) (*RawResponse, error) { + logger.Debugf("get %s [%s]", key, c.cluster.Leader) + p := keyToPath(key) + + // If consistency level is set to STRONG, append + // the `consistent` query string. + if c.config.Consistency == STRONG_CONSISTENCY { + options["consistent"] = true + } + + str, err := options.toParameters(VALID_GET_OPTIONS) + if err != nil { + return nil, err + } + p += str + + req := NewRawRequest("GET", p, nil, cancel) + resp, err := c.SendRequest(req) + + if err != nil { + return nil, err + } + + return resp, nil +} + +// get issues a GET request +func (c *Client) get(key string, options Options) (*RawResponse, error) { + return c.getCancelable(key, options, nil) +} + +// put issues a PUT request +func (c *Client) put(key string, value string, ttl uint64, + options Options) (*RawResponse, error) { + + logger.Debugf("put %s, %s, ttl: %d, [%s]", key, value, ttl, c.cluster.Leader) + p := keyToPath(key) + + str, err := options.toParameters(VALID_PUT_OPTIONS) + if err != nil { + return nil, err + } + p += str + + req := NewRawRequest("PUT", p, buildValues(value, ttl), nil) + resp, err := c.SendRequest(req) + + if err != nil { + return nil, err + } + + return resp, nil +} + +// post issues a POST request +func (c *Client) post(key string, value string, ttl uint64) (*RawResponse, error) { + logger.Debugf("post %s, %s, ttl: %d, [%s]", key, value, ttl, c.cluster.Leader) + p := keyToPath(key) + + req := NewRawRequest("POST", p, buildValues(value, ttl), nil) + resp, err := c.SendRequest(req) + + if err != nil { + return nil, err + } + + return resp, nil +} + +// delete issues a DELETE request +func (c *Client) delete(key string, options Options) (*RawResponse, error) { + logger.Debugf("delete %s [%s]", key, c.cluster.Leader) + p := keyToPath(key) + + str, err := options.toParameters(VALID_DELETE_OPTIONS) + if err != nil { + return nil, err + } + p += str + + req := NewRawRequest("DELETE", p, nil, nil) + resp, err := c.SendRequest(req) + + if err != nil { + return nil, err + } + + return resp, nil +} + +// SendRequest sends a HTTP request and returns a Response as defined by etcd +func (c *Client) SendRequest(rr *RawRequest) (*RawResponse, error) { + + var req *http.Request + var resp *http.Response + var httpPath string + var err error + var respBody []byte + + var numReqs = 1 + + checkRetry := c.CheckRetry + if checkRetry == nil { + checkRetry = DefaultCheckRetry + } + + cancelled := make(chan bool, 1) + reqLock := new(sync.Mutex) + + if rr.Cancel != nil { + cancelRoutine := make(chan bool) + defer close(cancelRoutine) + + go func() { + select { + case <-rr.Cancel: + cancelled <- true + logger.Debug("send.request is cancelled") + case <-cancelRoutine: + return + } + + // Repeat canceling request until this thread is stopped + // because we have no idea about whether it succeeds. + for { + reqLock.Lock() + c.httpClient.Transport.(*http.Transport).CancelRequest(req) + reqLock.Unlock() + + select { + case <-time.After(100 * time.Millisecond): + case <-cancelRoutine: + return + } + } + }() + } + + // If we connect to a follower and consistency is required, retry until + // we connect to a leader + sleep := 25 * time.Millisecond + maxSleep := time.Second + + for attempt := 0; ; attempt++ { + if attempt > 0 { + select { + case <-cancelled: + return nil, ErrRequestCancelled + case <-time.After(sleep): + sleep = sleep * 2 + if sleep > maxSleep { + sleep = maxSleep + } + } + } + + logger.Debug("Connecting to etcd: attempt ", attempt+1, " for ", rr.RelativePath) + + if rr.Method == "GET" && c.config.Consistency == WEAK_CONSISTENCY { + // If it's a GET and consistency level is set to WEAK, + // then use a random machine. + httpPath = c.getHttpPath(true, rr.RelativePath) + } else { + // Else use the leader. + httpPath = c.getHttpPath(false, rr.RelativePath) + } + + // Return a cURL command if curlChan is set + if c.cURLch != nil { + command := fmt.Sprintf("curl -X %s %s", rr.Method, httpPath) + for key, value := range rr.Values { + command += fmt.Sprintf(" -d %s=%s", key, value[0]) + } + c.sendCURL(command) + } + + logger.Debug("send.request.to ", httpPath, " | method ", rr.Method) + + req, err := func() (*http.Request, error) { + reqLock.Lock() + defer reqLock.Unlock() + + if rr.Values == nil { + if req, err = http.NewRequest(rr.Method, httpPath, nil); err != nil { + return nil, err + } + } else { + body := strings.NewReader(rr.Values.Encode()) + if req, err = http.NewRequest(rr.Method, httpPath, body); err != nil { + return nil, err + } + + req.Header.Set("Content-Type", + "application/x-www-form-urlencoded; param=value") + } + return req, nil + }() + + if err != nil { + return nil, err + } + + resp, err = c.httpClient.Do(req) + defer func() { + if resp != nil { + resp.Body.Close() + } + }() + + // If the request was cancelled, return ErrRequestCancelled directly + select { + case <-cancelled: + return nil, ErrRequestCancelled + default: + } + + numReqs++ + + // network error, change a machine! + if err != nil { + logger.Debug("network error: ", err.Error()) + lastResp := http.Response{} + if checkErr := checkRetry(c.cluster, numReqs, lastResp, err); checkErr != nil { + return nil, checkErr + } + + c.cluster.switchLeader(attempt % len(c.cluster.Machines)) + continue + } + + // if there is no error, it should receive response + logger.Debug("recv.response.from ", httpPath) + + if validHttpStatusCode[resp.StatusCode] { + // try to read byte code and break the loop + respBody, err = ioutil.ReadAll(resp.Body) + if err == nil { + logger.Debug("recv.success ", httpPath) + break + } + // ReadAll error may be caused due to cancel request + select { + case <-cancelled: + return nil, ErrRequestCancelled + default: + } + + if err == io.ErrUnexpectedEOF { + // underlying connection was closed prematurely, probably by timeout + // TODO: empty body or unexpectedEOF can cause http.Transport to get hosed; + // this allows the client to detect that and take evasive action. Need + // to revisit once code.google.com/p/go/issues/detail?id=8648 gets fixed. + respBody = []byte{} + break + } + } + + // if resp is TemporaryRedirect, set the new leader and retry + if resp.StatusCode == http.StatusTemporaryRedirect { + u, err := resp.Location() + + if err != nil { + logger.Warning(err) + } else { + // Update cluster leader based on redirect location + // because it should point to the leader address + c.cluster.updateLeaderFromURL(u) + logger.Debug("recv.response.relocate ", u.String()) + } + resp.Body.Close() + continue + } + + if checkErr := checkRetry(c.cluster, numReqs, *resp, + errors.New("Unexpected HTTP status code")); checkErr != nil { + return nil, checkErr + } + resp.Body.Close() + } + + r := &RawResponse{ + StatusCode: resp.StatusCode, + Body: respBody, + Header: resp.Header, + } + + return r, nil +} + +// DefaultCheckRetry defines the retrying behaviour for bad HTTP requests +// If we have retried 2 * machine number, stop retrying. +// If status code is InternalServerError, sleep for 200ms. +func DefaultCheckRetry(cluster *Cluster, numReqs int, lastResp http.Response, + err error) error { + + if numReqs >= 2*len(cluster.Machines) { + return newError(ErrCodeEtcdNotReachable, + "Tried to connect to each peer twice and failed", 0) + } + + code := lastResp.StatusCode + if code == http.StatusInternalServerError { + time.Sleep(time.Millisecond * 200) + + } + + logger.Warning("bad response status code", code) + return nil +} + +func (c *Client) getHttpPath(random bool, s ...string) string { + var machine string + if random { + machine = c.cluster.Machines[rand.Intn(len(c.cluster.Machines))] + } else { + machine = c.cluster.Leader + } + + fullPath := machine + "/" + version + for _, seg := range s { + fullPath = fullPath + "/" + seg + } + + return fullPath +} + +// buildValues builds a url.Values map according to the given value and ttl +func buildValues(value string, ttl uint64) url.Values { + v := url.Values{} + + if value != "" { + v.Set("value", value) + } + + if ttl > 0 { + v.Set("ttl", fmt.Sprintf("%v", ttl)) + } + + return v +} + +// convert key string to http path exclude version +// for example: key[foo] -> path[keys/foo] +// key[/] -> path[keys/] +func keyToPath(key string) string { + p := path.Join("keys", key) + + // corner case: if key is "/" or "//" ect + // path join will clear the tailing "/" + // we need to add it back + if p == "keys" { + p = "keys/" + } + + return p +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/response.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/response.go new file mode 100644 index 00000000000..1fe9b4e8711 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/response.go @@ -0,0 +1,89 @@ +package etcd + +import ( + "encoding/json" + "net/http" + "strconv" + "time" +) + +const ( + rawResponse = iota + normalResponse +) + +type responseType int + +type RawResponse struct { + StatusCode int + Body []byte + Header http.Header +} + +var ( + validHttpStatusCode = map[int]bool{ + http.StatusCreated: true, + http.StatusOK: true, + http.StatusBadRequest: true, + http.StatusNotFound: true, + http.StatusPreconditionFailed: true, + http.StatusForbidden: true, + } +) + +// Unmarshal parses RawResponse and stores the result in Response +func (rr *RawResponse) Unmarshal() (*Response, error) { + if rr.StatusCode != http.StatusOK && rr.StatusCode != http.StatusCreated { + return nil, handleError(rr.Body) + } + + resp := new(Response) + + err := json.Unmarshal(rr.Body, resp) + + if err != nil { + return nil, err + } + + // attach index and term to response + resp.EtcdIndex, _ = strconv.ParseUint(rr.Header.Get("X-Etcd-Index"), 10, 64) + resp.RaftIndex, _ = strconv.ParseUint(rr.Header.Get("X-Raft-Index"), 10, 64) + resp.RaftTerm, _ = strconv.ParseUint(rr.Header.Get("X-Raft-Term"), 10, 64) + + return resp, nil +} + +type Response struct { + Action string `json:"action"` + Node *Node `json:"node"` + PrevNode *Node `json:"prevNode,omitempty"` + EtcdIndex uint64 `json:"etcdIndex"` + RaftIndex uint64 `json:"raftIndex"` + RaftTerm uint64 `json:"raftTerm"` +} + +type Node struct { + Key string `json:"key, omitempty"` + Value string `json:"value,omitempty"` + Dir bool `json:"dir,omitempty"` + Expiration *time.Time `json:"expiration,omitempty"` + TTL int64 `json:"ttl,omitempty"` + Nodes Nodes `json:"nodes,omitempty"` + ModifiedIndex uint64 `json:"modifiedIndex,omitempty"` + CreatedIndex uint64 `json:"createdIndex,omitempty"` +} + +type Nodes []*Node + +// interfaces for sorting +func (ns Nodes) Len() int { + return len(ns) +} + +func (ns Nodes) Less(i, j int) bool { + return ns[i].Key < ns[j].Key +} + +func (ns Nodes) Swap(i, j int) { + ns[i], ns[j] = ns[j], ns[i] +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_curl_chan_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_curl_chan_test.go new file mode 100644 index 00000000000..756e317815a --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_curl_chan_test.go @@ -0,0 +1,42 @@ +package etcd + +import ( + "fmt" + "testing" +) + +func TestSetCurlChan(t *testing.T) { + c := NewClient(nil) + c.OpenCURL() + + defer func() { + c.Delete("foo", true) + }() + + _, err := c.Set("foo", "bar", 5) + if err != nil { + t.Fatal(err) + } + + expected := fmt.Sprintf("curl -X PUT %s/v2/keys/foo -d value=bar -d ttl=5", + c.cluster.Leader) + actual := c.RecvCURL() + if expected != actual { + t.Fatalf(`Command "%s" is not equal to expected value "%s"`, + actual, expected) + } + + c.SetConsistency(STRONG_CONSISTENCY) + _, err = c.Get("foo", false, false) + if err != nil { + t.Fatal(err) + } + + expected = fmt.Sprintf("curl -X GET %s/v2/keys/foo?consistent=true&recursive=false&sorted=false", + c.cluster.Leader) + actual = c.RecvCURL() + if expected != actual { + t.Fatalf(`Command "%s" is not equal to expected value "%s"`, + actual, expected) + } +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create.go new file mode 100644 index 00000000000..e2840cf3567 --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create.go @@ -0,0 +1,137 @@ +package etcd + +// Set sets the given key to the given value. +// It will create a new key value pair or replace the old one. +// It will not replace a existing directory. +func (c *Client) Set(key string, value string, ttl uint64) (*Response, error) { + raw, err := c.RawSet(key, value, ttl) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +// SetDir sets the given key to a directory. +// It will create a new directory or replace the old key value pair by a directory. +// It will not replace a existing directory. +func (c *Client) SetDir(key string, ttl uint64) (*Response, error) { + raw, err := c.RawSetDir(key, ttl) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +// CreateDir creates a directory. It succeeds only if +// the given key does not yet exist. +func (c *Client) CreateDir(key string, ttl uint64) (*Response, error) { + raw, err := c.RawCreateDir(key, ttl) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +// UpdateDir updates the given directory. It succeeds only if the +// given key already exists. +func (c *Client) UpdateDir(key string, ttl uint64) (*Response, error) { + raw, err := c.RawUpdateDir(key, ttl) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +// Create creates a file with the given value under the given key. It succeeds +// only if the given key does not yet exist. +func (c *Client) Create(key string, value string, ttl uint64) (*Response, error) { + raw, err := c.RawCreate(key, value, ttl) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +// CreateInOrder creates a file with a key that's guaranteed to be higher than other +// keys in the given directory. It is useful for creating queues. +func (c *Client) CreateInOrder(dir string, value string, ttl uint64) (*Response, error) { + raw, err := c.RawCreateInOrder(dir, value, ttl) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +// Update updates the given key to the given value. It succeeds only if the +// given key already exists. +func (c *Client) Update(key string, value string, ttl uint64) (*Response, error) { + raw, err := c.RawUpdate(key, value, ttl) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() +} + +func (c *Client) RawUpdateDir(key string, ttl uint64) (*RawResponse, error) { + ops := Options{ + "prevExist": true, + "dir": true, + } + + return c.put(key, "", ttl, ops) +} + +func (c *Client) RawCreateDir(key string, ttl uint64) (*RawResponse, error) { + ops := Options{ + "prevExist": false, + "dir": true, + } + + return c.put(key, "", ttl, ops) +} + +func (c *Client) RawSet(key string, value string, ttl uint64) (*RawResponse, error) { + return c.put(key, value, ttl, nil) +} + +func (c *Client) RawSetDir(key string, ttl uint64) (*RawResponse, error) { + ops := Options{ + "dir": true, + } + + return c.put(key, "", ttl, ops) +} + +func (c *Client) RawUpdate(key string, value string, ttl uint64) (*RawResponse, error) { + ops := Options{ + "prevExist": true, + } + + return c.put(key, value, ttl, ops) +} + +func (c *Client) RawCreate(key string, value string, ttl uint64) (*RawResponse, error) { + ops := Options{ + "prevExist": false, + } + + return c.put(key, value, ttl, ops) +} + +func (c *Client) RawCreateInOrder(dir string, value string, ttl uint64) (*RawResponse, error) { + return c.post(dir, value, ttl) +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create_test.go new file mode 100644 index 00000000000..ced0f06e7be --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/set_update_create_test.go @@ -0,0 +1,241 @@ +package etcd + +import ( + "testing" +) + +func TestSet(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("foo", true) + }() + + resp, err := c.Set("foo", "bar", 5) + if err != nil { + t.Fatal(err) + } + if resp.Node.Key != "/foo" || resp.Node.Value != "bar" || resp.Node.TTL != 5 { + t.Fatalf("Set 1 failed: %#v", resp) + } + if resp.PrevNode != nil { + t.Fatalf("Set 1 PrevNode failed: %#v", resp) + } + + resp, err = c.Set("foo", "bar2", 5) + if err != nil { + t.Fatal(err) + } + if !(resp.Node.Key == "/foo" && resp.Node.Value == "bar2" && resp.Node.TTL == 5) { + t.Fatalf("Set 2 failed: %#v", resp) + } + if resp.PrevNode.Key != "/foo" || resp.PrevNode.Value != "bar" || resp.Node.TTL != 5 { + t.Fatalf("Set 2 PrevNode failed: %#v", resp) + } +} + +func TestUpdate(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("foo", true) + c.Delete("nonexistent", true) + }() + + resp, err := c.Set("foo", "bar", 5) + + if err != nil { + t.Fatal(err) + } + + // This should succeed. + resp, err = c.Update("foo", "wakawaka", 5) + if err != nil { + t.Fatal(err) + } + + if !(resp.Action == "update" && resp.Node.Key == "/foo" && resp.Node.TTL == 5) { + t.Fatalf("Update 1 failed: %#v", resp) + } + if !(resp.PrevNode.Key == "/foo" && resp.PrevNode.Value == "bar" && resp.Node.TTL == 5) { + t.Fatalf("Update 1 prevValue failed: %#v", resp) + } + + // This should fail because the key does not exist. + resp, err = c.Update("nonexistent", "whatever", 5) + if err == nil { + t.Fatalf("The key %v did not exist, so the update should have failed."+ + "The response was: %#v", resp.Node.Key, resp) + } +} + +func TestCreate(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("newKey", true) + }() + + newKey := "/newKey" + newValue := "/newValue" + + // This should succeed + resp, err := c.Create(newKey, newValue, 5) + if err != nil { + t.Fatal(err) + } + + if !(resp.Action == "create" && resp.Node.Key == newKey && + resp.Node.Value == newValue && resp.Node.TTL == 5) { + t.Fatalf("Create 1 failed: %#v", resp) + } + if resp.PrevNode != nil { + t.Fatalf("Create 1 PrevNode failed: %#v", resp) + } + + // This should fail, because the key is already there + resp, err = c.Create(newKey, newValue, 5) + if err == nil { + t.Fatalf("The key %v did exist, so the creation should have failed."+ + "The response was: %#v", resp.Node.Key, resp) + } +} + +func TestCreateInOrder(t *testing.T) { + c := NewClient(nil) + dir := "/queue" + defer func() { + c.DeleteDir(dir) + }() + + var firstKey, secondKey string + + resp, err := c.CreateInOrder(dir, "1", 5) + if err != nil { + t.Fatal(err) + } + + if !(resp.Action == "create" && resp.Node.Value == "1" && resp.Node.TTL == 5) { + t.Fatalf("Create 1 failed: %#v", resp) + } + + firstKey = resp.Node.Key + + resp, err = c.CreateInOrder(dir, "2", 5) + if err != nil { + t.Fatal(err) + } + + if !(resp.Action == "create" && resp.Node.Value == "2" && resp.Node.TTL == 5) { + t.Fatalf("Create 2 failed: %#v", resp) + } + + secondKey = resp.Node.Key + + if firstKey >= secondKey { + t.Fatalf("Expected first key to be greater than second key, but %s is not greater than %s", + firstKey, secondKey) + } +} + +func TestSetDir(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("foo", true) + c.Delete("fooDir", true) + }() + + resp, err := c.CreateDir("fooDir", 5) + if err != nil { + t.Fatal(err) + } + if !(resp.Node.Key == "/fooDir" && resp.Node.Value == "" && resp.Node.TTL == 5) { + t.Fatalf("SetDir 1 failed: %#v", resp) + } + if resp.PrevNode != nil { + t.Fatalf("SetDir 1 PrevNode failed: %#v", resp) + } + + // This should fail because /fooDir already points to a directory + resp, err = c.CreateDir("/fooDir", 5) + if err == nil { + t.Fatalf("fooDir already points to a directory, so SetDir should have failed."+ + "The response was: %#v", resp) + } + + _, err = c.Set("foo", "bar", 5) + if err != nil { + t.Fatal(err) + } + + // This should succeed + // It should replace the key + resp, err = c.SetDir("foo", 5) + if err != nil { + t.Fatal(err) + } + if !(resp.Node.Key == "/foo" && resp.Node.Value == "" && resp.Node.TTL == 5) { + t.Fatalf("SetDir 2 failed: %#v", resp) + } + if !(resp.PrevNode.Key == "/foo" && resp.PrevNode.Value == "bar" && resp.PrevNode.TTL == 5) { + t.Fatalf("SetDir 2 failed: %#v", resp) + } +} + +func TestUpdateDir(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("fooDir", true) + }() + + resp, err := c.CreateDir("fooDir", 5) + if err != nil { + t.Fatal(err) + } + + // This should succeed. + resp, err = c.UpdateDir("fooDir", 5) + if err != nil { + t.Fatal(err) + } + + if !(resp.Action == "update" && resp.Node.Key == "/fooDir" && + resp.Node.Value == "" && resp.Node.TTL == 5) { + t.Fatalf("UpdateDir 1 failed: %#v", resp) + } + if !(resp.PrevNode.Key == "/fooDir" && resp.PrevNode.Dir == true && resp.PrevNode.TTL == 5) { + t.Fatalf("UpdateDir 1 PrevNode failed: %#v", resp) + } + + // This should fail because the key does not exist. + resp, err = c.UpdateDir("nonexistentDir", 5) + if err == nil { + t.Fatalf("The key %v did not exist, so the update should have failed."+ + "The response was: %#v", resp.Node.Key, resp) + } +} + +func TestCreateDir(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("fooDir", true) + }() + + // This should succeed + resp, err := c.CreateDir("fooDir", 5) + if err != nil { + t.Fatal(err) + } + + if !(resp.Action == "create" && resp.Node.Key == "/fooDir" && + resp.Node.Value == "" && resp.Node.TTL == 5) { + t.Fatalf("CreateDir 1 failed: %#v", resp) + } + if resp.PrevNode != nil { + t.Fatalf("CreateDir 1 PrevNode failed: %#v", resp) + } + + // This should fail, because the key is already there + resp, err = c.CreateDir("fooDir", 5) + if err == nil { + t.Fatalf("The key %v did exist, so the creation should have failed."+ + "The response was: %#v", resp.Node.Key, resp) + } +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/version.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/version.go new file mode 100644 index 00000000000..b3d05df70bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/version.go @@ -0,0 +1,3 @@ +package etcd + +const version = "v2" diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch.go new file mode 100644 index 00000000000..aa8d3df301c --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch.go @@ -0,0 +1,103 @@ +package etcd + +import ( + "errors" +) + +// Errors introduced by the Watch command. +var ( + ErrWatchStoppedByUser = errors.New("Watch stopped by the user via stop channel") +) + +// If recursive is set to true the watch returns the first change under the given +// prefix since the given index. +// +// If recursive is set to false the watch returns the first change to the given key +// since the given index. +// +// To watch for the latest change, set waitIndex = 0. +// +// If a receiver channel is given, it will be a long-term watch. Watch will block at the +//channel. After someone receives the channel, it will go on to watch that +// prefix. If a stop channel is given, the client can close long-term watch using +// the stop channel. +func (c *Client) Watch(prefix string, waitIndex uint64, recursive bool, + receiver chan *Response, stop chan bool) (*Response, error) { + logger.Debugf("watch %s [%s]", prefix, c.cluster.Leader) + if receiver == nil { + raw, err := c.watchOnce(prefix, waitIndex, recursive, stop) + + if err != nil { + return nil, err + } + + return raw.Unmarshal() + } + defer close(receiver) + + for { + raw, err := c.watchOnce(prefix, waitIndex, recursive, stop) + + if err != nil { + return nil, err + } + + resp, err := raw.Unmarshal() + + if err != nil { + return nil, err + } + + waitIndex = resp.Node.ModifiedIndex + 1 + receiver <- resp + } +} + +func (c *Client) RawWatch(prefix string, waitIndex uint64, recursive bool, + receiver chan *RawResponse, stop chan bool) (*RawResponse, error) { + + logger.Debugf("rawWatch %s [%s]", prefix, c.cluster.Leader) + if receiver == nil { + return c.watchOnce(prefix, waitIndex, recursive, stop) + } + + for { + raw, err := c.watchOnce(prefix, waitIndex, recursive, stop) + + if err != nil { + return nil, err + } + + resp, err := raw.Unmarshal() + + if err != nil { + return nil, err + } + + waitIndex = resp.Node.ModifiedIndex + 1 + receiver <- raw + } +} + +// helper func +// return when there is change under the given prefix +func (c *Client) watchOnce(key string, waitIndex uint64, recursive bool, stop chan bool) (*RawResponse, error) { + + options := Options{ + "wait": true, + } + if waitIndex > 0 { + options["waitIndex"] = waitIndex + } + if recursive { + options["recursive"] = true + } + + resp, err := c.getCancelable(key, options, stop) + + if err == ErrRequestCancelled { + return nil, ErrWatchStoppedByUser + } + + return resp, err +} diff --git a/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch_test.go b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch_test.go new file mode 100644 index 00000000000..43e1dfeb81f --- /dev/null +++ b/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd/watch_test.go @@ -0,0 +1,119 @@ +package etcd + +import ( + "fmt" + "runtime" + "testing" + "time" +) + +func TestWatch(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("watch_foo", true) + }() + + go setHelper("watch_foo", "bar", c) + + resp, err := c.Watch("watch_foo", 0, false, nil, nil) + if err != nil { + t.Fatal(err) + } + if !(resp.Node.Key == "/watch_foo" && resp.Node.Value == "bar") { + t.Fatalf("Watch 1 failed: %#v", resp) + } + + go setHelper("watch_foo", "bar", c) + + resp, err = c.Watch("watch_foo", resp.Node.ModifiedIndex+1, false, nil, nil) + if err != nil { + t.Fatal(err) + } + if !(resp.Node.Key == "/watch_foo" && resp.Node.Value == "bar") { + t.Fatalf("Watch 2 failed: %#v", resp) + } + + routineNum := runtime.NumGoroutine() + + ch := make(chan *Response, 10) + stop := make(chan bool, 1) + + go setLoop("watch_foo", "bar", c) + + go receiver(ch, stop) + + _, err = c.Watch("watch_foo", 0, false, ch, stop) + if err != ErrWatchStoppedByUser { + t.Fatalf("Watch returned a non-user stop error") + } + + if newRoutineNum := runtime.NumGoroutine(); newRoutineNum != routineNum { + t.Fatalf("Routine numbers differ after watch stop: %v, %v", routineNum, newRoutineNum) + } +} + +func TestWatchAll(t *testing.T) { + c := NewClient(nil) + defer func() { + c.Delete("watch_foo", true) + }() + + go setHelper("watch_foo/foo", "bar", c) + + resp, err := c.Watch("watch_foo", 0, true, nil, nil) + if err != nil { + t.Fatal(err) + } + if !(resp.Node.Key == "/watch_foo/foo" && resp.Node.Value == "bar") { + t.Fatalf("WatchAll 1 failed: %#v", resp) + } + + go setHelper("watch_foo/foo", "bar", c) + + resp, err = c.Watch("watch_foo", resp.Node.ModifiedIndex+1, true, nil, nil) + if err != nil { + t.Fatal(err) + } + if !(resp.Node.Key == "/watch_foo/foo" && resp.Node.Value == "bar") { + t.Fatalf("WatchAll 2 failed: %#v", resp) + } + + ch := make(chan *Response, 10) + stop := make(chan bool, 1) + + routineNum := runtime.NumGoroutine() + + go setLoop("watch_foo/foo", "bar", c) + + go receiver(ch, stop) + + _, err = c.Watch("watch_foo", 0, true, ch, stop) + if err != ErrWatchStoppedByUser { + t.Fatalf("Watch returned a non-user stop error") + } + + if newRoutineNum := runtime.NumGoroutine(); newRoutineNum != routineNum { + t.Fatalf("Routine numbers differ after watch stop: %v, %v", routineNum, newRoutineNum) + } +} + +func setHelper(key, value string, c *Client) { + time.Sleep(time.Second) + c.Set(key, value, 100) +} + +func setLoop(key, value string, c *Client) { + time.Sleep(time.Second) + for i := 0; i < 10; i++ { + newValue := fmt.Sprintf("%s_%v", value, i) + c.Set(key, newValue, 100) + time.Sleep(time.Second / 10) + } +} + +func receiver(c chan *Response, stop chan bool) { + for i := 0; i < 10; i++ { + <-c + } + stop <- true +} diff --git a/Makefile b/Makefile index 9d3f792d4ca..770191ce76f 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,16 @@ MAKEFLAGS = -s all: build test +# Values to be burned into the binary at build-time. +LDFLAGS = "\ + -X github.com/youtube/vitess/go/vt/servenv.buildHost '$$(hostname)'\ + -X github.com/youtube/vitess/go/vt/servenv.buildUser '$$(whoami)'\ + -X github.com/youtube/vitess/go/vt/servenv.buildGitRev '$$(git rev-parse HEAD)'\ + -X github.com/youtube/vitess/go/vt/servenv.buildTime '$$(LC_ALL=C date)'\ +" + build: - go install ./go/... + godep go install -ldflags ${LDFLAGS} ./go/... # Set VT_TEST_FLAGS to pass flags to python tests. # For example, verbose output: export VT_TEST_FLAGS=-v @@ -21,16 +29,27 @@ clean: rm -rf java/vtocc-client/target java/vtocc-jdbc-driver/target third_party/acolyte unit_test: - go test ./go/... + godep go test ./go/... # Run the code coverage tools, compute aggregate. # If you want to improve in a directory, run: # go test -coverprofile=coverage.out && go tool cover -html=coverage.out unit_test_cover: - go test -cover ./go/... | misc/parse_cover.py + godep go test -cover ./go/... | misc/parse_cover.py unit_test_race: - go test -race ./go/... + godep go test -race ./go/... + +# Run coverage and upload to coveralls.io. +# Requires the secret COVERALLS_TOKEN env variable to be set. +unit_test_goveralls: + go list -f '{{if len .TestGoFiles}}godep go test -coverprofile={{.Dir}}/.coverprofile {{.ImportPath}}{{end}}' ./go/... | xargs -i sh -c {} + gover ./go/ + # Travis doesn't set the token for forked pull requests, so skip + # upload if COVERALLS_TOKEN is unset. + if ! [ -z "$$COVERALLS_TOKEN" ]; then \ + goveralls -coverprofile=gover.coverprofile -repotoken $$COVERALLS_TOKEN; \ + fi queryservice_test: echo $$(date): Running test/queryservice_test.py... @@ -55,35 +74,60 @@ site_integration_test_files = \ zkocc_test.py # These tests should be run by developers after making code changes. -integration_test_files = \ - binlog.py \ - clone.py \ - initial_sharding_bytes.py \ +# Integration tests are grouped into 3 suites. +# - small: under 30 secs +# - medium: 30 secs - 1 min +# - large: over 1 min +small_integration_test_files = \ initial_sharding.py \ - keyrange_test.py \ + initial_sharding_bytes.py \ + vertical_split.py \ + vertical_split_vtgate.py \ + schema.py \ keyspace_test.py \ + keyrange_test.py \ mysqlctl.py \ - reparent.py \ - resharding_bytes.py \ - resharding.py \ - rowcache_invalidator.py \ - secure.py \ - schema.py \ sharded.py \ + secure.py \ + binlog.py \ + clone.py \ + update_stream.py + +medium_integration_test_files = \ tabletmanager.py \ - update_stream.py \ - vertical_split.py \ - vertical_split_vtgate.py \ + reparent.py \ vtdb_test.py \ vtgate_utils_test.py \ + rowcache_invalidator.py + +large_integration_test_files = \ vtgatev2_test.py \ zkocc_test.py +# The following tests are considered too flaky to be included +# in the continous integration test suites +ci_skip_integration_test_files = \ + resharding_bytes.py \ + resharding.py + +# Run the following tests after making worker changes +worker_integration_test_files = \ + binlog.py \ + resharding.py \ + resharding_bytes.py \ + vertical_split.py \ + vertical_split_vtgate.py \ + initial_sharding.py \ + initial_sharding_bytes.py + .ONESHELL: SHELL = /bin/bash -integration_test: + +# function to execute a list of integration test files +# exits on first failure +define run_integration_tests cd test ; \ - for t in $(integration_test_files) ; do \ + for t in $1 ; do \ echo $$(date): Running test/$$t... ; \ output=$$(time ./$$t $$VT_TEST_FLAGS 2>&1) ; \ if [[ $$? != 0 ]]; then \ @@ -92,62 +136,38 @@ integration_test: fi ; \ echo ; \ done +endef + +small_integration_test: + $(call run_integration_tests, $(small_integration_test_files)) + +medium_integration_test: + $(call run_integration_tests, $(medium_integration_test_files)) + +large_integration_test: + $(call run_integration_tests, $(large_integration_test_files)) + +ci_skip_integration_test: + $(call run_integration_tests, $(ci_skip_integration_test_files)) + +worker_test: + godep go test ./go/vt/worker/ + $(call run_integration_tests, $(worker_integration_test_files)) + +integration_test: small_integration_test medium_integration_test large_integration_test ci_skip_integration_test site_integration_test: - cd test ; \ - for t in $(site_integration_test_files) ; do \ - echo $$(date): Running test/$$t... ; \ - output=$$(time ./$$t $$VT_TEST_FLAGS 2>&1) ; \ - if [[ $$? != 0 ]]; then \ - echo "$$output" >&2 ; \ - exit 1 ; \ - fi ; \ - echo ; \ - done + $(call run_integration_tests, $(site_integration_test_files)) # this rule only works if bootstrap.sh was successfully ran in ./java java_test: cd java && mvn verify -bson: - bsongen -file ./go/mysql/proto/structs.go -type QueryResult -o ./go/mysql/proto/query_result_bson.go - bsongen -file ./go/mysql/proto/structs.go -type Field -o ./go/mysql/proto/field_bson.go - bsongen -file ./go/mysql/proto/structs.go -type Charset -o ./go/mysql/proto/charset_bson.go - bsongen -file ./go/vt/key/key.go -type KeyRange -o ./go/vt/key/key_range_bson.go - bsongen -file ./go/vt/key/key.go -type KeyspaceId -o ./go/vt/key/keyspace_id_bson.go - bsongen -file ./go/vt/key/key.go -type KeyspaceIdType -o ./go/vt/key/keyspace_id_type_bson.go - bsongen -file ./go/vt/tabletserver/proto/structs.go -type Query -o ./go/vt/tabletserver/proto/query_bson.go - bsongen -file ./go/vt/tabletserver/proto/structs.go -type Session -o ./go/vt/tabletserver/proto/session_bson.go - bsongen -file ./go/vt/tabletserver/proto/structs.go -type BoundQuery -o ./go/vt/tabletserver/proto/bound_query_bson.go - bsongen -file ./go/vt/tabletserver/proto/structs.go -type QueryList -o ./go/vt/tabletserver/proto/query_list_bson.go - bsongen -file ./go/vt/tabletserver/proto/structs.go -type QueryResultList -o ./go/vt/tabletserver/proto/query_result_list_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type Query -o ./go/vt/vtgate/proto/query_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type QueryShard -o ./go/vt/vtgate/proto/query_shard_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type BatchQueryShard -o ./go/vt/vtgate/proto/batch_query_shard_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type KeyspaceIdQuery -o ./go/vt/vtgate/proto/keyspace_id_query_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type KeyRangeQuery -o ./go/vt/vtgate/proto/key_range_query_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type EntityId -o ./go/vt/vtgate/proto/entity_id_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type EntityIdsQuery -o ./go/vt/vtgate/proto/entity_ids_query_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type KeyspaceIdBatchQuery -o ./go/vt/vtgate/proto/keyspace_id_batch_query_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type Session -o ./go/vt/vtgate/proto/session_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type ShardSession -o ./go/vt/vtgate/proto/shard_session_bson.go - bsongen -file ./go/vt/vtgate/proto/vtgate_proto.go -type QueryResult -o ./go/vt/vtgate/proto/query_result_bson.go - bsongen -file ./go/vt/topo/srvshard.go -type SrvShard -o ./go/vt/topo/srvshard_bson.go - bsongen -file ./go/vt/topo/srvshard.go -type SrvKeyspace -o ./go/vt/topo/srvkeyspace_bson.go - bsongen -file ./go/vt/topo/srvshard.go -type KeyspacePartition -o ./go/vt/topo/keyspace_partition_bson.go - bsongen -file ./go/vt/topo/tablet.go -type TabletType -o ./go/vt/topo/tablet_type_bson.go - bsongen -file ./go/vt/topo/toporeader.go -type GetSrvKeyspaceNamesArgs -o ./go/vt/topo/get_srv_keyspace_names_args_bson.go - bsongen -file ./go/vt/topo/toporeader.go -type GetSrvKeyspaceArgs -o ./go/vt/topo/get_srv_keyspace_args_bson.go - bsongen -file ./go/vt/topo/toporeader.go -type SrvKeyspaceNames -o ./go/vt/topo/srv_keyspace_names_bson.go - bsongen -file ./go/vt/topo/toporeader.go -type GetEndPointsArgs -o ./go/vt/topo/get_end_points_args_bson.go - bsongen -file ./go/vt/binlog/proto/binlog_player.go -type BlpPosition -o ./go/vt/binlog/proto/blp_position_bson.go - bsongen -file ./go/vt/binlog/proto/binlog_player.go -type BlpPositionList -o ./go/vt/binlog/proto/blp_position_list_bson.go - bsongen -file ./go/vt/binlog/proto/binlog_transaction.go -type BinlogTransaction -o ./go/vt/binlog/proto/binlog_transaction_bson.go - bsongen -file ./go/vt/binlog/proto/binlog_transaction.go -type Statement -o ./go/vt/binlog/proto/statement_bson.go - bsongen -file ./go/vt/binlog/proto/stream_event.go -type StreamEvent -o ./go/vt/binlog/proto/stream_event_bson.go - bsongen -file ./go/zk/zkocc_structs.go -type ZkPath -o ./go/zk/zkpath_bson.go - bsongen -file ./go/zk/zkocc_structs.go -type ZkPathV -o ./go/zk/zkpathv_bson.go - bsongen -file ./go/zk/zkocc_structs.go -type ZkStat -o ./go/zk/zkstat_bson.go - bsongen -file ./go/zk/zkocc_structs.go -type ZkNode -o ./go/zk/zknode_bson.go - bsongen -file ./go/zk/zkocc_structs.go -type ZkNodeV -o ./go/zk/zknodev_bson.go +java_vtgate_client_test: + mvn -f java/vtgate-client/pom.xml clean verify + +v3_test: + cd test && ./vtgatev3_test.py +bson: + go generate ./go/... diff --git a/README.md b/README.md index 504d00358f9..f5631a93f4b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Vitess +# Vitess

[![Build Status](https://travis-ci.org/youtube/vitess.svg?branch=master)](https://travis-ci.org/youtube/vitess/builds) [![Coverage Status](https://coveralls.io/repos/youtube/vitess/badge.png)](https://coveralls.io/r/youtube/vitess)

Vitess is a set of servers and tools meant to facilitate scaling of MySQL databases for the web. It's been developed since 2011, and is currently used as diff --git a/bootstrap.sh b/bootstrap.sh index c43e0ad915c..2b6b6bbc46d 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -9,6 +9,17 @@ if [ ! -f bootstrap.sh ]; then exit 1 fi +if [ "$USER" == "root" ]; then + echo "Vitess cannot run as root. Please bootstrap with a non-root user." + exit 1 +fi + +go version 2>&1 >/dev/null +if [ $? != 0 ]; then + echo "Go is not installed or is not on \$PATH" + exit 1 +fi + . ./dev.env mkdir -p $VTROOT/dist @@ -38,16 +49,42 @@ ln -nfs $VTTOP/third_party/go/launchpad.net $VTROOT/src go install launchpad.net/gozk/zookeeper go get code.google.com/p/goprotobuf/proto -go get code.google.com/p/go.net/context -go get code.google.com/p/go.tools/cmd/goimports +go get golang.org/x/net/context +go get golang.org/x/tools/cmd/goimports go get github.com/golang/glog +go get github.com/golang/lint/golint +go get github.com/tools/godep + +# goversion_min returns true if major.minor go version is at least some value. +function goversion_min() { + [[ "$(go version)" =~ go([0-9]+)\.([0-9]+) ]] + gotmajor=${BASH_REMATCH[1]} + gotminor=${BASH_REMATCH[2]} + [[ "$1" =~ ([0-9]+)\.([0-9]+) ]] + wantmajor=${BASH_REMATCH[1]} + wantminor=${BASH_REMATCH[2]} + [ "$gotmajor" -lt "$wantmajor" ] && return 1 + [ "$gotmajor" -gt "$wantmajor" ] && return 0 + [ "$gotminor" -lt "$wantminor" ] && return 1 + return 0 +} + +# Packages for uploading code coverage to coveralls.io. +# The cover tool needs to be installed into the Go toolchain, so it will fail +# if Go is installed somewhere that requires root access. However, this tool +# is optional, so we should hide any errors to avoid confusion. +if goversion_min 1.4; then + go get golang.org/x/tools/cmd/cover &> /dev/null +else + go get code.google.com/p/go.tools/cmd/cover &> /dev/null +fi +go get github.com/modocache/gover +go get github.com/mattn/goveralls ln -snf $VTTOP/config $VTROOT/config ln -snf $VTTOP/data $VTROOT/data ln -snf $VTTOP/py $VTROOT/py-vtdb ln -snf $VTTOP/go/zk/zkctl/zksrv.sh $VTROOT/bin/zksrv.sh -ln -snf $VTTOP/test/vthook-copy_snapshot_from_storage.sh $VTROOT/vthook/copy_snapshot_from_storage -ln -snf $VTTOP/test/vthook-copy_snapshot_to_storage.sh $VTROOT/vthook/copy_snapshot_to_storage ln -snf $VTTOP/test/vthook-test.sh $VTROOT/vthook/test.sh # install mysql @@ -127,4 +164,6 @@ fi # create pre-commit hooks echo "creating git pre-commit hooks" ln -sf $VTTOP/misc/git/pre-commit $VTTOP/.git/hooks/pre-commit -echo "source dev.env in your shell to complete the setup." + +echo +echo "bootstrap finished - run 'source dev.env' in your shell before building." diff --git a/config/mycnf/default-fast.cnf b/config/mycnf/default-fast.cnf index 833c259595d..53f38151431 100644 --- a/config/mycnf/default-fast.cnf +++ b/config/mycnf/default-fast.cnf @@ -61,5 +61,5 @@ tmpdir = {{.TmpDir}} tmp_table_size = 32M transaction-isolation = REPEATABLE-READ # READ-COMMITTED would be better, but mysql 5.1 disables this with statement based replication -# READ-UNCOMMTTED might be better +# READ-UNCOMMITTED might be better lower_case_table_names = 1 diff --git a/config/mycnf/default.cnf b/config/mycnf/default.cnf index ddd40998f9b..b332c2ec47a 100644 --- a/config/mycnf/default.cnf +++ b/config/mycnf/default.cnf @@ -11,14 +11,14 @@ default-storage-engine = innodb expire_logs_days = 3 innodb_additional_mem_pool_size = 32M innodb_autoextend_increment = 64 -innodb_buffer_pool_size = 64M +innodb_buffer_pool_size = 32M innodb_data_file_path = ibdata1:10M:autoextend innodb_data_home_dir = {{.InnodbDataHomeDir}} innodb_file_per_table innodb_flush_log_at_trx_commit = 2 innodb_flush_method = O_DIRECT innodb_lock_wait_timeout = 20 -innodb_log_buffer_size = 64M +innodb_log_buffer_size = 8M innodb_log_file_size = 64M innodb_log_files_in_group = 2 innodb_log_group_home_dir = {{.InnodbLogGroupHomeDir}} @@ -61,5 +61,5 @@ tmpdir = {{.TmpDir}} tmp_table_size = 32M transaction-isolation = REPEATABLE-READ # READ-COMMITTED would be better, but mysql 5.1 disables this with statement based replication -# READ-UNCOMMTTED might be better +# READ-UNCOMMITTED might be better lower_case_table_names = 1 diff --git a/data/bootstrap/upgrade.sh b/data/bootstrap/upgrade.sh index 241b52a5e9d..7f3bafa3799 100755 --- a/data/bootstrap/upgrade.sh +++ b/data/bootstrap/upgrade.sh @@ -10,18 +10,23 @@ fi mysql_port=33306 tablet_uid=99999 +logdir=$VTDATAROOT/tmp tablet_dir=$VTDATAROOT/vt_0000099999 +mysqlctl_args="-log_dir $logdir -db-config-dba-uname vt_dba -db-config-dba-charset utf8 -tablet_uid $tablet_uid -mysql_port $mysql_port" + set -e +mkdir -p $logdir + echo Starting mysqld -mysqlctl -tablet_uid=$tablet_uid -mysql_port=$mysql_port init -skip_schema +mysqlctl $mysqlctl_args init -skip_schema echo Running mysql_upgrade mysql_upgrade --socket=$tablet_dir/mysql.sock --user=vt_dba echo Stopping mysqld -mysqlctl -tablet_uid=$tablet_uid -mysql_port=$mysql_port shutdown +mysqlctl $mysqlctl_args shutdown newfile=mysql-db-dir_$(cat $tablet_dir/data/mysql_upgrade_info).tbz @@ -30,4 +35,4 @@ echo Creating new bootstrap file: $newfile mv $tablet_dir/data.tbz ./$newfile echo Removing tablet directory -rm -r $tablet_dir \ No newline at end of file +rm -r $tablet_dir diff --git a/data/test/sqlparser_test/parse_pass.sql b/data/test/sqlparser_test/parse_pass.sql index 826c07c9ab4..56b8e37058a 100644 --- a/data/test/sqlparser_test/parse_pass.sql +++ b/data/test/sqlparser_test/parse_pass.sql @@ -61,6 +61,7 @@ select /* and */ 1 from t where a = b and a = c select /* or */ 1 from t where a = b or a = c select /* not */ 1 from t where not a = b select /* exists */ 1 from t where exists (select 1 from t) +select /* keyrange */ 1 from t where keyrange(1, 2) select /* (boolean) */ 1 from t where not (a = b) select /* in value list */ 1 from t where a in (b, c) select /* in select */ 1 from t where a in (select 1 from t) diff --git a/data/test/vtgate/dml_cases.txt b/data/test/vtgate/dml_cases.txt index 3e2aeb1b148..0c13946bbdc 100644 --- a/data/test/vtgate/dml_cases.txt +++ b/data/test/vtgate/dml_cases.txt @@ -3,10 +3,12 @@ { "ID":"NoPlan", "Reason":"table nouser not found", - "Table":null, + "Table": "", "Original":"update nouser set val = 1", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -15,10 +17,12 @@ { "ID":"NoPlan", "Reason":"table nouser not found", - "Table":null, + "Table": "", "Original":"delete from nouser", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -30,7 +34,9 @@ "Table":"main1", "Original": "update main1 set val = 1", "Rewritten":"update main1 set val = 1", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -42,7 +48,9 @@ "Table":"main1", "Original": "delete from main1", "Rewritten":"delete from main1", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -50,11 +58,13 @@ "update user set val = 1" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "update has multi-shard where clause", "Table": "user", "Original": "update user set val = 1", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -62,63 +72,83 @@ "delete from user" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "delete has multi-shard where clause", "Table": "user", "Original": "delete from user", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } # update by primary keyspace id "update user set val = 1 where id = 1" { - "ID": "UpdateSingleShardKey", + "ID": "UpdateEqual", "Reason": "", "Table": "user", "Original": "update user set val = 1 where id = 1", "Rewritten": "update user set val = 1 where id = 1", - "Index": { - "Type": 0, - "Column": "id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values": 1 } # delete from by primary keyspace id "delete from user where id = 1" { - "ID": "DeleteSingleShardKey", + "ID": "DeleteEqual", "Reason": "", "Table": "user", "Original": "delete from user where id = 1", "Rewritten": "delete from user where id = 1", - "Index": { - "Type": 0, - "Column": "id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "select id, name from user where id = 1 for update", + "Vindex": "user_index", + "Col": "id", "Values": 1 } +# update KEYRANGE +"update user set val = 1 where keyrange(1, 2)" +{ + "ID": "NoPlan", + "Reason": "update has multi-shard where clause", + "Table": "user", + "Original": "update user set val = 1 where keyrange(1, 2)", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + +# delete KEYRANGE +"delete from user where keyrange(1, 2)" +{ + "ID": "NoPlan", + "Reason": "delete has multi-shard where clause", + "Table": "user", + "Original": "delete from user where keyrange(1, 2)", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + # update with primary id through IN clause "update user set val = 1 where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "update has multi-shard where clause", "Table": "user", "Original": "update user set val = 1 where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -126,11 +156,41 @@ "delete from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "delete has multi-shard where clause", "Table": "user", "Original": "delete from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + +# update with non-unique key +"update user set val = 1 where name = 'foo'" +{ + "ID": "NoPlan", + "Reason": "update has multi-shard where clause", + "Table": "user", + "Original": "update user set val = 1 where name = 'foo'", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + +# delete from with primary id through IN clause +"delete from user where name = 'foo'" +{ + "ID": "NoPlan", + "Reason": "delete has multi-shard where clause", + "Table": "user", + "Original": "delete from user where name = 'foo'", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -138,11 +198,13 @@ "update user set val = 1 where user_id = 1" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "update has multi-shard where clause", "Table": "user", "Original": "update user set val = 1 where user_id = 1", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -150,51 +212,55 @@ "delete from user where user_id = 1" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "delete has multi-shard where clause", "Table": "user", "Original": "delete from user where user_id = 1", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } # update by lookup "update music set val = 1 where id = 1" { - "ID": "UpdateSingleLookup", + "ID": "UpdateEqual", "Reason": "", "Table": "music", "Original": "update music set val = 1 where id = 1", "Rewritten": "update music set val = 1 where id = 1", - "Index": { - "Type": 1, - "Column": "id", - "Name": "music_user_map", - "From": "music_id", - "To": "user_id", - "Owner": "music", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "music_user_map", + "Col": "id", "Values": 1 } # delete from by lookup "delete from music where id = 1" { - "ID": "DeleteSingleLookup", + "ID": "DeleteEqual", "Reason": "", "Table": "music", "Original": "delete from music where id = 1", "Rewritten": "delete from music where id = 1", - "Index": { - "Type": 1, - "Column": "id", - "Name": "music_user_map", - "From": "music_id", - "To": "user_id", - "Owner": "music", - "IsAutoInc": true - }, + "Subquery": "select id from music where id = 1 for update", + "Vindex": "music_user_map", + "Col": "id", + "Values": 1 +} + +# delete from, no owned vindexes +"delete from music_extra where user_id = 1" +{ + "ID": "DeleteEqual", + "Reason": "", + "Table": "music_extra", + "Original": "delete from music_extra where user_id = 1", + "Rewritten": "delete from music_extra where user_id = 1", + "Subquery": "", + "Vindex": "user_index", + "Col": "user_id", "Values": 1 } @@ -202,11 +268,13 @@ "update music set val = 1 where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "update has multi-shard where clause", "Table": "music", "Original": "update music set val = 1 where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -214,11 +282,13 @@ "delete from music where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "delete has multi-shard where clause", "Table": "music", "Original": "delete from music where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -230,6 +300,8 @@ "Table": "music", "Original": "update music set id = 1 where id = 1", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } diff --git a/data/test/vtgate/insert_cases.txt b/data/test/vtgate/insert_cases.txt index 931b160f136..ecba1d45823 100644 --- a/data/test/vtgate/insert_cases.txt +++ b/data/test/vtgate/insert_cases.txt @@ -6,7 +6,9 @@ "Table":"main1", "Original":"insert into main1 values(1, 2)", "Rewritten":"insert into main1 values (1, 2)", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -18,7 +20,9 @@ "Table": "user", "Original":"insert into user values(1, 2, 3)", "Rewritten":"", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -30,7 +34,9 @@ "Table":"user", "Original":"insert into user(id) select 1 from dual", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -42,7 +48,9 @@ "Table":"user", "Original":"insert into user(id) values (1), (2)", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -54,7 +62,9 @@ "Table":"user", "Original":"insert into user(id) values (select 1 from dual)", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -66,78 +76,92 @@ "Table":"user", "Original":"insert into user(id) values (1, 2)", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } -# insert ShardKey owned autoinc +# insert with one vindex "insert into user(id) values (1)" { "ID":"InsertSharded", "Reason":"", "Table":"user", "Original":"insert into user(id) values (1)", - "Rewritten":"insert into user(id) values (:_id)", - "Index":null, - "Values":[1] + "Rewritten":"insert into user(id, name) values (:_id, :_name)", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values":[1, null] } -# insert ShardKey owned autoinc +# insert with non vindex "insert into user(nonid) values (2)" { "ID":"InsertSharded", "Reason":"", "Table":"user", "Original":"insert into user(nonid) values (2)", - "Rewritten":"insert into user(nonid, id) values (2, :_id)", - "Index":null, - "Values":[null] + "Rewritten":"insert into user(nonid, id, name) values (2, :_id, :_name)", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values":[null, null] } -# insert Lookup owned no-autoinc -"insert into music(user_id, id) values(1, 2)" +# insert with all vindexes supplied +"insert into user(nonid, name, id) values (2, 'foo', 1)" { "ID":"InsertSharded", "Reason":"", - "Table":"music", - "Original":"insert into music(user_id, id) values(1, 2)", - "Rewritten":"insert into music(user_id, id) values (1, :_id)", - "Index":null, - "Values":[1, 2] + "Table":"user", + "Original":"insert into user(nonid, name, id) values (2, 'foo', 1)", + "Rewritten":"insert into user(nonid, name, id) values (2, :_name, :_id)", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values":[1,"Zm9v"] } -# insert unowned -"insert into music_extra(music_id, user_id) values(1, 2)" +# insert invalid index value +"insert into music_extra(music_id, user_id) values(1, 1.1)" { - "ID":"InsertSharded", - "Reason":"", + "ID":"NoPlan", + "Reason":"could not convert val: 1.1, pos: 1: strconv.ParseUint: parsing \"1.1\": invalid syntax", "Table":"music_extra", - "Original":"insert into music_extra(music_id, user_id) values(1, 2)", - "Rewritten":"insert into music_extra(music_id, user_id) values (1, 2)", - "Index":null, - "Values":[2, 1] + "Original":"insert into music_extra(music_id, user_id) values(1, 1.1)", + "Rewritten":"", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values":null } -# insert missing index value -"insert into music_extra(music_id) values(1)" +# insert invalid index value +"insert into music_extra(music_id, user_id) values(1, id)" { "ID":"NoPlan", - "Reason":"must supply value for indexed column: user_id", + "Reason":"could not convert val: id, pos: 1: id is not a value", "Table":"music_extra", - "Original":"insert into music_extra(music_id) values(1)", + "Original":"insert into music_extra(music_id, user_id) values(1, id)", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } -# insert invalid index value -"insert into music_extra(music_id, user_id) values(1, 1.1)" +# insert invalid table +"insert into noexist(music_id, user_id) values(1, 1.1)" { "ID":"NoPlan", - "Reason":"could not convert val: 1.1, pos: 1", - "Table":"music_extra", - "Original":"insert into music_extra(music_id, user_id) values(1, 1.1)", + "Reason":"table noexist not found", + "Table":"", + "Original":"insert into noexist(music_id, user_id) values(1, 1.1)", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } diff --git a/data/test/vtgate/router_test.json b/data/test/vtgate/router_test.json deleted file mode 100644 index 1dfb8e77e35..00000000000 --- a/data/test/vtgate/router_test.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "Keyspaces": { - "TestRouter": { - "ShardingScheme": 1, - "Indexes": { - "user_index": { - "Type": 0, - "Owner": "user", - "IsAutoInc": true - }, - "music_user_map": { - "Type": 1, - "From": "music_id", - "To": "user_id", - "Owner": "music", - "IsAutoInc": true - } - }, - "Tables": { - "user": { - "IndexColumns": [ - { - "Column": "id", - "IndexName": "user_index" - } - ] - }, - "user_extra": { - "IndexColumns": [ - { - "Column": "user_id", - "IndexName": "user_index" - } - ] - }, - "music": { - "IndexColumns": [ - { - "Column": "user_id", - "IndexName": "user_index" - }, - { - "Column": "id", - "IndexName": "music_user_map" - } - ] - }, - "music_extra": { - "IndexColumns": [ - { - "Column": "user_id", - "IndexName": "user_index" - }, - { - "Column": "music_id", - "IndexName": "music_user_map" - } - ] - } - } - } - } -} diff --git a/data/test/vtgate/schema_test.json b/data/test/vtgate/schema_test.json index 78d5c57fee8..8db6b09e48f 100644 --- a/data/test/vtgate/schema_test.json +++ b/data/test/vtgate/schema_test.json @@ -1,68 +1,77 @@ { "Keyspaces": { "user": { - "ShardingScheme": 1, - "Indexes": { + "Sharded": true, + "Vindexes": { "user_index": { - "Type": 0, - "Owner": "user", - "IsAutoInc": true + "Type": "hash", + "Owner": "user" }, "music_user_map": { - "Type": 1, - "From": "music_id", - "To": "user_id", - "Owner": "music", - "IsAutoInc": true + "Type": "lookup", + "Owner": "music" + }, + "name_user_map": { + "Type": "multi", + "Owner": "user" } }, - "Tables": { + "Classes": { "user": { - "IndexColumns": [ + "ColVindexes": [ + { + "Col": "id", + "Name": "user_index" + }, { - "Column": "id", - "IndexName": "user_index" + "Col": "name", + "Name": "name_user_map" } ] }, "user_extra": { - "IndexColumns": [ + "ColVindexes": [ { - "Column": "user_id", - "IndexName": "user_index" + "Col": "user_id", + "Name": "user_index" } ] }, "music": { - "IndexColumns": [ + "ColVindexes": [ { - "Column": "user_id", - "IndexName": "user_index" + "Col": "user_id", + "Name": "user_index" }, { - "Column": "id", - "IndexName": "music_user_map" + "Col": "id", + "Name": "music_user_map" } ] }, "music_extra": { - "IndexColumns": [ + "ColVindexes": [ { - "Column": "user_id", - "IndexName": "user_index" + "Col": "user_id", + "Name": "user_index" }, { - "Column": "music_id", - "IndexName": "music_user_map" + "Col": "music_id", + "Name": "music_user_map" } ] } + }, + "Tables": { + "user": "user", + "user_extra": "user_extra", + "music": "music", + "music_extra": "music_extra" } }, "main": { - "ShardingScheme": 0, "Tables": { - "main1": {} + "main1": "" } } } diff --git a/data/test/vtgate/select_cases.txt b/data/test/vtgate/select_cases.txt index 2f7c6603796..46767560304 100644 --- a/data/test/vtgate/select_cases.txt +++ b/data/test/vtgate/select_cases.txt @@ -3,10 +3,12 @@ { "ID":"NoPlan", "Reason":"syntax error at position 4 near the", - "Table":null, + "Table": "", "Original":"the quick brown fox", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -14,11 +16,13 @@ "select * from user union select * from user" { "ID":"NoPlan", - "Reason":"too complex", - "Table":null, + "Reason":"cannot build a plan for this construct", + "Table": "", "Original":"select * from user union select * from user", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -26,11 +30,13 @@ "set a=1" { "ID":"NoPlan", - "Reason":"too complex", - "Table":null, + "Reason":"cannot build a plan for this construct", + "Table": "", "Original":"set a=1", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -38,11 +44,13 @@ "create table a()" { "ID":"NoPlan", - "Reason":"too complex", - "Table":null, + "Reason":"cannot build a plan for this construct", + "Table": "", "Original":"create table a()", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -50,11 +58,13 @@ "explain select * from user" { "ID":"NoPlan", - "Reason":"too complex", - "Table":null, + "Reason":"cannot build a plan for this construct", + "Table": "", "Original":"explain select * from user", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -63,10 +73,12 @@ { "ID":"NoPlan", "Reason":"complex table expression", - "Table":null, + "Table": "", "Original":"select * from (select 2 from dual)", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -75,10 +87,12 @@ { "ID":"NoPlan", "Reason":"table nouser not found", - "Table":null, + "Table": "", "Original":"select * from nouser where id = 1", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -87,10 +101,12 @@ { "ID":"NoPlan", "Reason":"complex table expression", - "Table":null, + "Table": "", "Original":"select * from music, user where id = 1", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -99,10 +115,12 @@ { "ID":"NoPlan", "Reason":"complex table expression", - "Table":null, + "Table": "", "Original":"select * from (user) where id = 1", "Rewritten":"", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -111,10 +129,12 @@ { "ID":"SelectUnsharded", "Reason":"", - "Table":"main1", + "Table": "main1", "Original":"select * from main1", - "Rewritten":"select * from main1", - "Index":null, + "Rewritten":"", + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -122,11 +142,13 @@ "select * from user" { "ID": "SelectScatter", - "Reason": "no where clause", + "Reason": "", "Table": "user", "Original":"select * from user", "Rewritten": "select * from user", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -138,7 +160,9 @@ "Table": "user", "Original":"select * from user where id in (select * from music)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -150,7 +174,9 @@ "Table": "user", "Original":"select * from user where not (id in (select * from music))", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -162,7 +188,9 @@ "Table": "user", "Original":"select * from user where id between (select 1 from dual) and 2", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -174,7 +202,9 @@ "Table": "user", "Original":"select * from user where (select 1 from dual) is null", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -186,7 +216,9 @@ "Table": "user", "Original":"select * from user where exists (select 1 from dual)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -198,7 +230,9 @@ "Table": "user", "Original":"select * from user where 1+1 = (select 1 from dual)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -210,7 +244,9 @@ "Table": "user", "Original":"select * from user where id = -(select 1 from dual)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -222,7 +258,9 @@ "Table": "user", "Original":"select * from user where id = func(1, (select 1 from dual))", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -230,11 +268,13 @@ "select * from user where id = func(1)" { "ID":"SelectScatter", - "Reason":"no index match", - "Table":"user", + "Reason":"", + "Table": "user", "Original":"select * from user where id = func(1)", "Rewritten":"select * from user where id = func(1)", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -246,7 +286,9 @@ "Table": "user", "Original":"select * from user where id = case (select 1 from dual) when a=b then c end", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -258,7 +300,9 @@ "Table": "user", "Original":"select * from user where id = case aa when a = b then c else (select 1 from dual) end", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -270,7 +314,9 @@ "Table": "user", "Original":"select * from user where id = case aa when (select 1 from dual) = b then c else d end", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -282,7 +328,9 @@ "Table": "user", "Original":"select * from user where id = case aa when a = b then (select 1 from dual) else d end", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -290,11 +338,13 @@ "select * from user where id = case aa when a = b then c end" { "ID":"SelectScatter", - "Reason":"no index match", - "Table":"user", + "Reason":"", + "Table": "user", "Original":"select * from user where id = case aa when a = b then c end", "Rewritten": "select * from user where id = case aa when a = b then c end", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -302,83 +352,97 @@ "select * from user where 1 = id" { "ID":"SelectScatter", - "Reason":"no index match", - "Table":"user", + "Reason":"", + "Table": "user", "Original":"select * from user where 1 = id", "Rewritten":"select * from user where 1 = id", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } # select by primary keyspace id "select * from user where id = 1" { - "ID": "SelectSingleShardKey", + "ID": "SelectEqual", "Reason": "", "Table": "user", "Original":"select * from user where id = 1", "Rewritten": "select * from user where id = 1", - "Index": { - "Type": 0, - "Column": "id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values": 1 } +# select by primary keyspace id unsigned value +"select * from user where id = 9223372036854775808" +{ + "ID": "SelectEqual", + "Reason": "", + "Table": "user", + "Original":"select * from user where id = 9223372036854775808", + "Rewritten": "select * from user where id = 9223372036854775808", + "Subquery": "", + "Vindex": "user_index", + "Col": "id", + "Values": 9223372036854775808 +} + +# select by non-unique index +"select * from user where name = 'foo'" +{ + "ID": "SelectEqual", + "Reason": "", + "Table": "user", + "Original":"select * from user where name = 'foo'", + "Rewritten": "select * from user where name = 'foo'", + "Subquery": "", + "Vindex": "name_user_map", + "Col": "name", + "Values": "Zm9v" +} + # select by primary keyspace id, invalid value "select * from user where id = 1.1" { "ID":"SelectScatter", - "Reason":"no index match", - "Table":"user", + "Reason":"", + "Table": "user", "Original":"select * from user where id = 1.1", "Rewritten":"select * from user where id = 1.1", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } # select with primary keyspace id through bind var "select * from user where id = :id" { - "ID": "SelectSingleShardKey", + "ID": "SelectEqual", "Reason": "", "Table": "user", "Original":"select * from user where id = :id", "Rewritten": "select * from user where id = :id", - "Index": { - "Type": 0, - "Column": "id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values": ":id" } # select with primary id through IN clause "select * from user where id in (1, 2)" { - "ID": "SelectMultiShardKey", + "ID": "SelectIN", "Reason": "", "Table": "user", "Original":"select * from user where id in (1, 2)", "Rewritten": "select * from user where id in ::_vals", - "Index": { - "Type": 0, - "Column": "id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values": [ 1, 2 @@ -389,11 +453,13 @@ "select * from user where id in (1+1, 2)" { "ID":"SelectScatter", - "Reason":"no index match", - "Table":"user", + "Reason":"", + "Table": "user", "Original":"select * from user where id in (1+1, 2)", "Rewritten":"select * from user where id in (1+1, 2)", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } @@ -401,103 +467,83 @@ "select * from user where id in (1.1, 2)" { "ID":"SelectScatter", - "Reason":"no index match", - "Table":"user", + "Reason":"", + "Table": "user", "Original":"select * from user where id in (1.1, 2)", "Rewritten":"select * from user where id in (1.1, 2)", - "Index":null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values":null } -# select with no index match +# select with no vindex match "select * from user where user_id = 1" { "ID": "SelectScatter", - "Reason": "no index match", + "Reason": "", "Table": "user", "Original":"select * from user where user_id = 1", "Rewritten": "select * from user where user_id = 1", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } # select with primary id with different column name "select * from user_extra where user_id = 1" { - "ID": "SelectSingleShardKey", + "ID": "SelectEqual", "Reason": "", "Table": "user_extra", "Original":"select * from user_extra where user_id = 1", "Rewritten": "select * from user_extra where user_id = 1", - "Index": { - "Type": 0, - "Column": "user_id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "user_id", "Values": 1 } -# select with primary id when there's more than one index +# select with primary id when there's more than one vindex "select * from music where user_id = 1" { - "ID": "SelectSingleShardKey", + "ID": "SelectEqual", "Reason": "", "Table": "music", "Original":"select * from music where user_id = 1", "Rewritten": "select * from music where user_id = 1", - "Index": { - "Type": 0, - "Column": "user_id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "user_id", "Values": 1 } # select by lookup "select * from music where id = 1" { - "ID": "SelectSingleLookup", + "ID": "SelectEqual", "Reason": "", "Table": "music", "Original":"select * from music where id = 1", "Rewritten": "select * from music where id = 1", - "Index": { - "Type": 1, - "Column": "id", - "Name": "music_user_map", - "From": "music_id", - "To": "user_id", - "Owner": "music", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "music_user_map", + "Col": "id", "Values": 1 } # select by lookup with IN clause "select * from music where id in (1, 2)" { - "ID": "SelectMultiLookup", + "ID": "SelectIN", "Reason": "", "Table": "music", "Original":"select * from music where id in (1, 2)", "Rewritten": "select * from music where id in ::_vals", - "Index": { - "Type": 1, - "Column": "id", - "Name": "music_user_map", - "From": "music_id", - "To": "user_id", - "Owner": "music", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "music_user_map", + "Col": "id", "Values": [ 1, 2 @@ -507,20 +553,14 @@ # select by lookup with IN clause and bind vars "select * from music where id in (:a, 2)" { - "ID": "SelectMultiLookup", + "ID": "SelectIN", "Reason": "", "Table": "music", "Original":"select * from music where id in (:a, 2)", "Rewritten": "select * from music where id in ::_vals", - "Index": { - "Type": 1, - "Column": "id", - "Name": "music_user_map", - "From": "music_id", - "To": "user_id", - "Owner": "music", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "music_user_map", + "Col": "id", "Values": [ ":a", 2 @@ -530,40 +570,28 @@ # select by lookup with list bind var "select * from music where id in ::list" { - "ID": "SelectMultiLookup", + "ID": "SelectIN", "Reason": "", "Table": "music", "Original":"select * from music where id in ::list", "Rewritten": "select * from music where id in ::_vals", - "Index": { - "Type": 1, - "Column": "id", - "Name": "music_user_map", - "From": "music_id", - "To": "user_id", - "Owner": "music", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "music_user_map", + "Col": "id", "Values": "::list" } # select by lookup if there's no primary key "select * from music_extra where music_id = 1" { - "ID": "SelectSingleLookup", + "ID": "SelectEqual", "Reason": "", "Table": "music_extra", "Original":"select * from music_extra where music_id = 1", "Rewritten": "select * from music_extra where music_id = 1", - "Index": { - "Type": 1, - "Column": "music_id", - "Name": "music_user_map", - "From": "music_id", - "To": "user_id", - "Owner": "music", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "music_user_map", + "Col": "music_id", "Values": 1 } @@ -571,31 +599,27 @@ "select * from user where id = 1 and var = 2 or var = 3" { "ID": "SelectScatter", - "Reason": "no index match", + "Reason": "", "Table": "user", "Original":"select * from user where id = 1 and var = 2 or var = 3", "Rewritten": "select * from user where id = 1 and var = 2 or var = 3", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } # select with acceptable parenthesized OR clause at end "select * from user where id = 1 and (var = 2 or var = 3)" { - "ID": "SelectSingleShardKey", + "ID": "SelectEqual", "Reason": "", "Table": "user", "Original":"select * from user where id = 1 and (var = 2 or var = 3)", "Rewritten": "select * from user where id = 1 and (var = 2 or var = 3)", - "Index": { - "Type": 0, - "Column": "id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values": 1 } @@ -603,43 +627,279 @@ "select * from user where var = 2 or var = 3 and id = 1" { "ID": "SelectScatter", - "Reason": "no index match", + "Reason": "", "Table": "user", "Original":"select * from user where var = 2 or var = 3 and id = 1", "Rewritten": "select * from user where var = 2 or var = 3 and id = 1", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } # select with acceptable parenthesized OR clause at beginning "select * from user where (var = 2 or var = 3) and id = 1" { - "ID": "SelectSingleShardKey", + "ID": "SelectEqual", "Reason": "", "Table": "user", "Original":"select * from user where (var = 2 or var = 3) and id = 1", "Rewritten": "select * from user where (var = 2 or var = 3) and id = 1", - "Index": { - "Type": 0, - "Column": "id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values": 1 } +# select with KEYRANGE +"select * from user where keyrange(1, 2)" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where keyrange(1, 2)", + "Rewritten": "select * from user", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select with KEYRANGE parenthesized +"select * from user where (keyrange(1, 2))" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where (keyrange(1, 2))", + "Rewritten": "select * from user", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select KEYRANGE in AND lhs +"select * from user where keyrange(1, 2) and a = 1" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where keyrange(1, 2) and a = 1", + "Rewritten": "select * from user where a = 1", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select KEYRANGE in AND rhs +"select * from user where a = 1 and keyrange(1, 2)" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where a = 1 and keyrange(1, 2)", + "Rewritten": "select * from user where a = 1", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select KEYRANGE in AND lhs parenthesized +"select * from user where (keyrange(1, 2)) and a = 1" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where (keyrange(1, 2)) and a = 1", + "Rewritten": "select * from user where a = 1", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select KEYRANGE in AND rhs parenthesized +"select * from user where a = 1 and (keyrange(1, 2))" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where a = 1 and (keyrange(1, 2))", + "Rewritten": "select * from user where a = 1", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select KEYRANGE in AND lhs double parenthesized +"select * from user where ((keyrange(1, 2))) and a = 1" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where ((keyrange(1, 2))) and a = 1", + "Rewritten": "select * from user where a = 1", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select KEYRANGE in AND rhs double parenthesized +"select * from user where a = 1 and ((keyrange(1, 2)))" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where a = 1 and ((keyrange(1, 2)))", + "Rewritten": "select * from user where a = 1", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select KEYRANGE in double AND double parenthesized lhs +"select * from user where (b = 2 and (keyrange(1, 2))) and a = 1" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where (b = 2 and (keyrange(1, 2))) and a = 1", + "Rewritten": "select * from user where (b = 2) and a = 1", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select KEYRANGE in double AND double parenthesized rhs +"select * from user where a = 1 and (b = 2 and (keyrange(1, 2)))" +{ + "ID": "SelectKeyrange", + "Reason": "", + "Table": "user", + "Original":"select * from user where a = 1 and (b = 2 and (keyrange(1, 2)))", + "Rewritten": "select * from user where a = 1 and (b = 2)", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": [1, 2] +} + +# select with KEYRANGE syntax error +"select * from user where keyrange(1+1, 2)" +{ + "ID": "NoPlan", + "Reason": "syntax error at position 37", + "Table": "", + "Original":"select * from user where keyrange(1+1, 2)", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + +# select KEYRANGE innvalid lhs +"select * from user where keyrange(1.1, 2)" +{ + "ID": "NoPlan", + "Reason": "invalid keyrange: strconv.ParseUint: parsing \"1.1\": invalid syntax", + "Table": "user", + "Original":"select * from user where keyrange(1.1, 2)", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + +# select KEYRANGE innvalid lhs of AND +"select * from user where keyrange(1.1, 2) and a = 1" +{ + "ID": "NoPlan", + "Reason": "invalid keyrange: strconv.ParseUint: parsing \"1.1\": invalid syntax", + "Table": "user", + "Original":"select * from user where keyrange(1.1, 2) and a = 1", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + +# select KEYRANGE innvalid in parenthesized +"select * from user where (keyrange(1.1, 2))" +{ + "ID": "NoPlan", + "Reason": "invalid keyrange: strconv.ParseUint: parsing \"1.1\": invalid syntax", + "Table": "user", + "Original":"select * from user where (keyrange(1.1, 2))", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + +# select KEYRANGE innvalid rhs of AND +"select * from user where a = 1 and keyrange(1.1, 2)" +{ + "ID": "NoPlan", + "Reason": "invalid keyrange: strconv.ParseUint: parsing \"1.1\": invalid syntax", + "Table": "user", + "Original":"select * from user where a = 1 and keyrange(1.1, 2)", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + +# select KEYRANGE ininvalid rhs +"select * from user where keyrange(1, 2.2)" +{ + "ID": "NoPlan", + "Reason": "invalid keyrange: strconv.ParseUint: parsing \"2.2\": invalid syntax", + "Table": "user", + "Original":"select * from user where keyrange(1, 2.2)", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + # aggregates in select, simple "select count(*) from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select count(*) from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", + "Values": null +} + +# aggregates in select, non-unique vindex +"select count(*) from user where name = 'foo'" +{ + "ID": "NoPlan", + "Reason": "multi-shard query has post-processing constructs", + "Table": "user", + "Original":"select count(*) from user where name = 'foo'", + "Rewritten": "", + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -647,11 +907,13 @@ "select a = 1 and count(*) = 1 from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select a = 1 and count(*) = 1 from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -659,11 +921,13 @@ "select a = 1 or count(*) = 1 from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select a = 1 or count(*) = 1 from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -671,11 +935,13 @@ "select (not count(*) = 1) from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select (not count(*) = 1) from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -683,11 +949,13 @@ "select count(*) between 1 and 2 from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select count(*) between 1 and 2 from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -695,51 +963,41 @@ "select count(*) is null from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select count(*) is null from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } -# aggregates in select, EXISTS (cannot aggregate) +# subquery in select list, EXISTS (cannot aggregate) "select exists (select 1 from dual) from user where id in (1, 2)" { - "ID":"SelectMultiShardKey", + "ID":"SelectIN", "Reason":"", - "Table":"user", + "Table": "user", "Original":"select exists (select 1 from dual) from user where id in (1, 2)", "Rewritten":"select exists (select 1 from dual) from user where id in ::_vals", - "Index":{ - "Type":0, - "Column":"id", - "Name":"user_index", - "From":"", - "To":"", - "Owner":"user", - "IsAutoInc":true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values":[1,2] } -# aggregates in select, subquery +# subquery in select in select list "select (select 1 from dual) from user where id in (1, 2)" { - "ID":"SelectMultiShardKey", + "ID":"SelectIN", "Reason":"", - "Table":"user", + "Table": "user", "Original":"select (select 1 from dual) from user where id in (1, 2)", "Rewritten": "select (select 1 from dual) from user where id in ::_vals", - "Index":{ - "Type":0, - "Column":"id", - "Name":"user_index", - "From":"", - "To":"", - "Owner":"user", - "IsAutoInc":true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values":[1,2] } @@ -747,11 +1005,13 @@ "select count(*)+1 from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select count(*)+1 from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -759,11 +1019,13 @@ "select -count(*) from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select -count(*) from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -771,31 +1033,27 @@ "select fun(1, count(*)) from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select fun(1, count(*)) from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } # aggregates in select, non-aggregate function "select fun(*) from user where id in (1, 2)" { - "ID": "SelectMultiShardKey", + "ID": "SelectIN", "Reason": "", "Table": "user", "Original":"select fun(*) from user where id in (1, 2)", "Rewritten": "select fun(*) from user where id in ::_vals", - "Index": { - "Type": 0, - "Column": "id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values": [ 1, 2 @@ -806,11 +1064,13 @@ "select case count(*) when a = b then d end from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select case count(*) when a = b then d end from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -818,11 +1078,13 @@ "select case a when a = b then d else count(*) end from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select case a when a = b then d else count(*) end from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -830,11 +1092,13 @@ "select case a when count(*) = b then d else e end from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select case a when count(*) = b then d else e end from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } @@ -842,31 +1106,27 @@ "select case a when a = b then count(*) else e end from user where id in (1, 2)" { "ID": "NoPlan", - "Reason": "too complex", + "Reason": "multi-shard query has post-processing constructs", "Table": "user", "Original":"select case a when a = b then count(*) else e end from user where id in (1, 2)", "Rewritten": "", - "Index": null, + "Subquery": "", + "Vindex": "", + "Col": "", "Values": null } # aggregates in select, no aggregates "select case a when a = b then d else e end from user where id in (1, 2)" { - "ID": "SelectMultiShardKey", + "ID": "SelectIN", "Reason": "", "Table": "user", "Original":"select case a when a = b then d else e end from user where id in (1, 2)", "Rewritten": "select case a when a = b then d else e end from user where id in ::_vals", - "Index": { - "Type": 0, - "Column": "id", - "Name": "user_index", - "From": "", - "To": "", - "Owner": "user", - "IsAutoInc": true - }, + "Subquery": "", + "Vindex": "user_index", + "Col": "id", "Values": [ 1, 2 diff --git a/doc/GettingStarted.md b/doc/GettingStarted.md index b96d74f32af..135b3c8054f 100644 --- a/doc/GettingStarted.md +++ b/doc/GettingStarted.md @@ -3,18 +3,20 @@ If you run into issues or have questions, you can use our mailing list: vitess@g ## Dependencies -* We currently develop on Ubuntu 12.04 and 14.04. -* You'll need some kind of Java Runtime (for ZooKeeper). +* We currently develop on Ubuntu 14.04 (Trusty) and Debian 7.0 (Wheezy). +* You'll need some kind of Java Runtime if you use ZooKeeper. We use OpenJDK (*sudo apt-get install openjdk-7-jre*). -* [Go](http://golang.org) 1.2+: Needed for building Vitess. -* [MariaDB](https://mariadb.org/): We currently develop with version 10.0.13. +* [Go](http://golang.org) 1.3+: Needed for building Vitess. +* [MariaDB](https://mariadb.org/): We currently develop with version 10.0.14. Other 10.0.x versions may also work. -* [ZooKeeper](http://zookeeper.apache.org/): By default, Vitess - uses Zookeeper as the lock service. It is possible to plug in - something else as long as the new service supports the - necessary API functions. +* [ZooKeeper](http://zookeeper.apache.org/) + or [etcd](https://github.com/coreos/etcd) 0.4.6: + By default, Vitess uses ZooKeeper as the lock service. + We also have a plugin for etcd. See the Building section below. + It is possible to plug in something else as long as the new service supports + the necessary API functions. * [Memcached](http://memcached.org): Used for the rowcache. -* [Python](http://python.org): For the client and testing. +* [Python](http://python.org) 2.7: For the client and testing. ## Building @@ -24,6 +26,13 @@ If you run into issues or have questions, you can use our mailing list: vitess@g You can use any installation method (src/bin/rpm/deb), but be sure to include the client development headers (**libmariadbclient-dev**). +ZooKeeper 3.3.5 is included by default. If you plan to use it, you don't need +to install anything else. + +If you want to use etcd instead, install +[etcd 0.4.6](https://github.com/coreos/etcd/releases/tag/v0.4.6) +and make sure the "etcd" command is on your path. + Then download and build Vitess. Note that the value of MYSQL_FLAVOR is case-sensitive. If the mysql_config command from libmariadbclient-dev is not on the PATH, you'll need to *export VT_MYSQL_ROOT=/path/to/mariadb* before running bootstrap.sh, @@ -42,6 +51,12 @@ make build ## Testing +If you want to use etcd, set the following environment variable: + +``` sh +export VT_TEST_FLAGS='--topo-server-flavor=etcd' +``` + The full set of tests included in the default _make_ and _make test_ targets is intended for use by Vitess developers to verify code changes. These tests simulate a small cluster by launching many servers on the local @@ -92,10 +107,7 @@ This could indicate that no Java Runtime is installed. Some of the larger tests use up to 4GB of temporary space on disk. ## Setting up a cluster -TODO: Expand on all sections -### Setup zookeeper -### Start a MySql instance -### Start vttablet -### Start vtgate -### Write a client -### Test + +Once you have a successful `make build`, you can proceed to start up a +[local cluster](https://github.com/youtube/vitess/tree/master/examples/local) +for testing. diff --git a/doc/LifeOfAQuery.md b/doc/LifeOfAQuery.md new file mode 100644 index 00000000000..33a0480ab99 --- /dev/null +++ b/doc/LifeOfAQuery.md @@ -0,0 +1,129 @@ +Life of A Query +===================== + +* [From Client to VtGate](#from-client-to-vtgate) +* [From VtGate to VtTablet](#from-vtgate-to-vttablet) +* [From VtTablet to MySQL](#from-vttablet-to-mysql) +* [Put it all together](#put-it-all-together) +* [TopoServer](#toposerver) +* [Streaming Query](#streaming-query) +* [Scatter Query](#scatter-query) +* [Misc](#misc) + * [Rpc Server Code Path (VtGate)](#rpc-server-code-path-vtgate) + * [VtGate to VtTablet Code Path](#vtgate-to-vttablet-code-path) + * [VtTablet to MySQL Code Path](#vttablet-to-mysql-code-path) + +A query means a request for information from database and it involves four componenets in the case of Vitess, including client application, VtGate, VtTablet and MySQL instance. This doc explains interaction happens between and within components. + +![](./life_of_a_query.png) + +At a very high level, as the graph shows, first client sends a query to VtGate. VtGate then resolves the query and routes it to the right VtTablets. For each VtTablet that receives the query, it does necessary validations and passes the query to underlying MySQL instance. After gathering results from MySQL, VtTablet sends response back to VtGate. Once VtGate receives response from all VtTablets, it sends the combined result to client. In the presence of VtTablet errors, VtGate will retry the query if errors are recoverable and it only fails the query if either errors are unrecoverable or maximum retry times has been reached. + +## From Client to VtGate + +A client application first sends a bson rpc with an embedded sql query to VtGate. VtGate's rpc server unmarshals this rpc request, call appropriate VtGate method and return its result back to client. As following graph shows, VtGate has a rpc server that listens to localhost:port/\_bson\_rpc\_ for http requests and localhost:port/\_bson\_rpc\_/auth for https requests. + +![](./life_of_a_query_client_to_vtgate.png) + +VtGate keeps an in-memory table that stores all available rpc methods for each service, e.g. VtGate uses "VTGate" as its service name and most its methods defined in [go/vt/vtgate/vtgate.go](../go/vt/vtgate/vtgate.go) are used to serve rpc request [go/rpcplus/server.go](../go/rpcplus/server.go). + +## From VtGate to VtTablet + +![](./life_of_a_query_vtgate_to_vttablet.png) + +After receiving a rpc call from client and one of its Execute* method being invoked, VtGate needs to figure out which shards should receive the query and send query to each of them. In addition, VtGate talks to topo server to get necessary information to create a VtTablet connection for each shard. At this point, VtGate is able to send query to the right VtTablets in parallel. VtGate also does retry if timeout happens or some VtTablets return recoverable errors. + +Internally, VtGate has a ScatterConn instance and uses it to execute queries across multiple ShardConn connections. A ScatterConn performs the query on selected shards in parallel. It first obtains a ShardConn connection for every shard and sends query use ShardConn's execute method. If the requested session is in a transaction, it will open a new transactions on the connection, and updates the Session with the transaction id. If the session already contains a transaction id for the shard, it reuses it. If there are any unrecoverable errors during a transaction, it rolls back the transaction for all shards. + +A ShardConn object represents a load balanced connection to a group of VtTablets that belong the same shard. ShardConn can be concurrently used across goroutines. + +## From VtTablet to MySQL + +![](./life_of_a_query_vttablet_to_mysql.png) + +Once received a rpc call from VtGate, VtTablet do a few checks before passing query to MySQL. It first validates the current VtTablet state including sessions id, then generates a query plan and applies predefined query rules and do ACL check. It also checks whether the query hits row cache and returns result immediately if so. In addition, VtTablet consolidates duplicate queries from executing simultaneously and shares results between them. At this point, VtTablet has no way but pass the query down to MySQL layer and waits for the result. + +## Put it all together + +![](./life_of_a_query_all.png) + +## TopoServer + +A topo server stores information to help VtGate navigate query to the right VtTablets. It contains keyspace to shards mappings, keyspace id to shard mapping and ports that a VtTablet listens to (EndPoint). VtGates caches those information in the memory and periodically do updates if there are changes happened in the topo server. + +## Streaming Query + +In general speaking, a streaming query means query results will be returned as a stream. In Vitess's case, both VtGate and VtTablet will send result back as soon as it is available. VtTablet by default will collect a fixed number of rows returned from MySQL, send them back to VtGate and repeats the above step until all rows have been returned. + +## Scatter Query + +A scatter query, as its name indicates, will hit multiple shards. In Vitess, a scatter query is recognized once VtGate determines a query need to hit multiple VtTablets. VtGate then sends query to these VtTablets, assembles the result after receiving all response and returns the combined result to the client. + +## Misc + +### Rpc Server Code Path (VtGate) + +Init a rpc server + +``` +go/cmd/vtgate/vtgate.go: main() -> + go/vt/servenv/servenv.go: RunDefault() -> // use the port specified in command line "--port" + go/vt/servenv/run.go: Run(port int) -> + go/vt/servenv/rpc.go: ServeRPC() -> // set up rpc server + go/rpcwrap/bsonrpc/codec.go: ServeRPC() -> // set up bson rpc server + go/rpcwrap/rpcwrap.go: ServeRPC("bson", NewServerCodec) -> // common code to register rpc server +``` + +ServeRPC("bson", NewServerCodec) register a rpcHandler instance whose ServeHTTP(http.ResponseWriter, *http.Request) will be called for every http request + +Rpc server handle http request + +``` +go/rpcwrap/rpcwrap.go rpcHandler.ServeHTTP -> +go/rpcwrap/rpcwrap.go rpcHandler.server.ServeCodecWithContext -> +go/rpcplus/server.go Server.ServeCodecWithContext(context.Context, ServerCodec) (note: rpcHandler uses a global DefaultServer instance defined in the sever.go) -> +go/rpcplus/server.go Server.readRequest(ServeCodec) will use a given codec to extract (service, methodType, request, request arguments, reply value, keep reading), go/rpcplus/server.go +Finally we do "service.call(..)" with parameters provided in the request. In the current setup, service.call will always call some method in VtGate (go/vt/vtgate/vtgate.go). +``` + +### VtGate to VtTablet Code Path + +Here is the code path for a query with keyspace id. + +``` +go/vt/vtgate/vtgate.go VTGate.ExecuteKeyspaceIds(context.Context, *proto.KeyspaceIdQuery, *proto.QueryResult) -> + go/vt/vtgate/resolver.go resolver.ExecuteKeyspaceIds(context.Context, *proto.KeyspaceIdQuery) -> + go/vt/vtgate/resolver.go resolver.Execute -> + go/vt/vtgate/scatter_conn.go ScatterConn.Execute -> + go/vt/vtgate/scatter_conn.go ScatterConn.multiGo -> + go/vt/vtgate/scatter_conn.go ScatterConn.getConnection -> + go/vt/vtgate/shard_conn.go ShardConn.Execute -> + go/vt/vtgate/shard_conn.go ShardConn.withRetry -> + go/vt/vtgate/shard_conn.go ShardConn.getConn -> + go/vt/tabletserver/tabletconn/tablet_conn.go tabletconn.GetDialer -> + go/vt/tabletserver/tabletconn/tablet_conn.go tabletconn.TabletConn.Execute -> + go/vt/tabletserver/gorpctabletconn/conn.go TabletBson.Execute -> + go/vt/tabletserver/gorpctabletconn/conn.go TabletBson.rpcClient.Call -> + go/rpcplus/client.go rpcplus.Client.Call -> + go/rpcplus/client.go rpcplus.Client.Go -> + go/rpcplus/client.go rpcplus.Client.send +``` + +### VtTablet to MySQL Code Path + +Here is the code path for a select query. + +``` +go/vt/tabletserver/sqlquery.go SqlQuery.Execute -> +go/vt/tabletserver/query_executor.go QueryExecutor.Execute -> +go/vt/tabletserver/query_executor.go QueryExecutor.execSelect -> +go/vt/tabletserver/request_context.go RequestContext.getConn -> // QueryExecutor composes a RequestContext +go/vt/tabletserver/request_context.go RequestContext.fullFetch -> +go/vt/tabletserver/request_context.go RequestContext.execSQL -> +go/vt/tabletserver/request_context.go RequestContext.execSQLNoPanic -> +go/vt/tabletserver/request_context.go RequestContext.execSQLOnce -> +go/vt/dbconnpool/connection_pool.go PoolConnection.ExecuteFetch (current implementation is in DBConnection) -> +go/vt/dbconnpool/connection.go PooledConnection.DBConnection.ExecuteFetch -> +go/mysql/mysql.go mysql.Connection.ExecuteFetch -> +go/mysql/mysql.go mysql.Connection.fetchAll +``` diff --git a/doc/Reparenting.md b/doc/Reparenting.md index 525322ec8ab..b2b27dd09aa 100644 --- a/doc/Reparenting.md +++ b/doc/Reparenting.md @@ -15,29 +15,21 @@ They are triggered by using the 'vtctl ReparentShard' command. See the help for ## External Reparents In this part, we assume another tool has been reparenting our servers. We then trigger the -'vtctl ShardExternallyReparented' command. +'vtctl TabletExternallyReparented' command. The flow for that command is as follows: - the shard is locked in the global topology server. - we read the Shard object from the global topology server. -- we read all the tablets in the replication graph for the shard. We also check the new master is in the map. Note we allow partial reads here, so if a data center is down, as long as the data center containing the new master is up, we keep going. -- we call the 'SlaveWasPromoted' remote action on the new master. This remote action makes sure the new master is not a MySQL slave of another server (the 'show slave status' command should not return anything, meaning 'reset slave' should ave been called). -- for every host in the replication graph, we call the 'SlaveWasRestarted' action. It takes as paremeter the address of the new master. On each slave, it executes a 'show slave status'. If the master matches the new master, we update the topology server record for that tablet with the new master, and the replication graph for that tablet as well. If it doesn't match, we keep the old record in the replication graph (pointing at whatever master was there before). We optionally Scrap tablets that bad (disabled by default). -- if a smaller percentage than a configurable value of the slaves works (80% be default), we stop here. +- we read all the tablets in the replication graph for the shard. Note we allow partial reads here, so if a data center is down, as long as the data center containing the new master is up, we keep going. +- the new master performs a 'SlaveWasPromoted' action. This remote action makes sure the new master is not a MySQL slave of another server (the 'show slave status' command should not return anything, meaning 'reset slave' should ave been called). +- for every host in the replication graph, we call the 'SlaveWasRestarted' action. It takes as paremeter the address of the new master. On each slave, we update the topology server record for that tablet with the new master, and the replication graph for that tablet as well. +- for the old master, if it doesn't successfully return from 'SlaveWasRestarted', we change its type to 'spare' (so a dead old master doesn't interfere). - we then update the Shard object with the new master. - we rebuild the serving graph for that shard. This will update the 'master' record for sure, and also keep all the tablets that have successfully reparented. -Optional Flags: -- -accept-success-percents=80: will declare success if more than that many slaves can be reparented -- -continue_on_unexpected_master=false: if a slave has the wrong master, we'll just log the error and keep going -- -scrap-stragglers=false: will scrap bad hosts - Failure cases: - The global topology server has to be available for locking and modification during this operation. If not, the operation will just fail. -- If a single topology server is down in one data center 9and it's nto the master data center), the tablets in that data center will be ignored by the reparent. Provided it doesn't trigger the 80% threshold, this is not a big deal. When the topology server comes back up, just re-run 'vtctl InitTablet' on the tablets, and that will fix their master record. -- If scrap-straggler is false (the default), a tablet that has the wrong master will be kept in the replication graph with its original master. When we rebuild the serving graph, that tablet won't be added, as it doesn't have the right master. -- if more than 20% of the tablets fails, we don't update the Shard object, and don't rebuild. We assume something is seriously wrong, and it might be our process, not the servers. Figuring out the cause and re-running 'vtctl ShardExternallyReparented' should work. -- if for some reasons none of the slaves report the right master (replication is going through a proxy for instance, and the master address is not what the clients are showing in 'show slave status'), the result is pretty bad. All slaves are kept in the replication graph, but with their old (incorrect) master. Next time a Shard rebuild happens, all the servers will disappear. At that point, fixing the issue and then re-parenting will work. +- If a single topology server is down in one data center (and it's nto the master data center), the tablets in that data center will be ignored by the reparent. When the topology server comes back up, just re-run 'vtctl InitTablet' on the tablets, and that will fix their master record. ## Reparenting And Serving Graph diff --git a/doc/ReplicationGraph.md b/doc/ReplicationGraph.md index 8dc81767a92..17baa238ad9 100644 --- a/doc/ReplicationGraph.md +++ b/doc/ReplicationGraph.md @@ -1,7 +1,7 @@ # Replication Graph The replication graph contains the mysql replication information for a shard. Currently, we only support one layer -of replication (a single master wiht multiple slaves), but the design doesn't preclude us from supporting +of replication (a single master with multiple slaves), but the design doesn't preclude us from supporting hierarchical replication later on. ## Master @@ -14,7 +14,7 @@ When creating a master (using 'vtctl InitTablet ... master'), we make sure Maste The slaves are added to the ShardReplication object present on each local topology server. So for slaves, the replication graph is colocated in the same cell as the tablets themselves. This makes disaster recovery much easier: -when loosing a data center, the replication graph for other data centers is not lost. +when losing a data center, the replication graph for other data centers is not lost. When creating a slave (using 'vtctl InitTablet ... replica' for instance), we get the master record (if not specified) from the MasterAlias of the Shard. We then add an entry in the ReplicationLinks list of the ShardReplication object for the tablet’s cell (we create ShardReplication if it doesn’t exist yet). diff --git a/doc/SchemaManagement.md b/doc/SchemaManagement.md index 1bcf76c1c71..bcfe1ed2874 100644 --- a/doc/SchemaManagement.md +++ b/doc/SchemaManagement.md @@ -7,25 +7,36 @@ The schema is the list of tables and how to create them. It is managed by vtctl. The following vtctl commands exist to look at the schema, and validate it's the same on all databases. ``` -GetSchema +GetSchema +``` +where \ is in the format of "\-\" + +Example: +``` +$ vtctl -wait-time=30s GetSchema cell01-01234567 ``` displays the full schema for a tablet ``` -ValidateSchemaShard +ValidateSchemaShard +``` +where \ is the format of "\/\" + +Example: +``` +$ vtctl -wait-time=30s ValidateSchemaShard keyspace01/10-20 ``` validate the master schema matches all the slaves. ``` -ValidateSchemaKeyspace +ValidateSchemaKeyspace ``` validate the master schema from shard 0 matches all the other tablets in the keyspace. - Example: ``` -$ vtctl -wait-time=30s ValidateSchemaKeyspace /zk/global/vt/keyspaces/user +$ vtctl -wait-time=30s ValidateSchemaKeyspace user ``` ## Changing the Schema @@ -92,34 +103,34 @@ We will return the following information: This translates into the following vtctl commands: ``` -PreflightSchema {-sql= || -sql_file=} +PreflightSchema {-sql= || -sql_file=} ``` apply the schema change to a temporary database to gather before and after schema and validate the change. The sql can be inlined or read from a file. This will create a temporary database, copy the existing keyspace schema into it, apply the schema change, and re-read the resulting schema. ``` $ echo "create table test_table(id int);" > change.sql -$ vtctl PreflightSchema -sql_file=change.sql /zk/nyc/vt/tablets/0002009001 +$ vtctl PreflightSchema -sql_file=change.sql nyc-0002009001 ``` ``` -ApplySchema {-sql= || -sql_file=} [-skip_preflight] [-stop_replication] +ApplySchema {-sql= || -sql_file=} [-skip_preflight] [-stop_replication] ``` apply the schema change to the specific tablet (allowing replication by default). The sql can be inlined or read from a file. a PreflightSchema operation will first be used to make sure the schema is OK (unless skip_preflight is specified). ``` -ApplySchemaShard {-sql= || -sql_file=} [-simple] [-new_parent=] +ApplySchemaShard {-sql= || -sql_file=} [-simple] [-new_parent=] ``` -apply the schema change to the specific shard. If simple is specified, we just apply on the live master. Otherwise we do the shell game and will optionally re-parent. +apply the schema change to the specific shard. If simple is specified, we just apply on the live master. Otherwise we do the shell game and will optionally re-parent. if new_parent is set, we will also reparent (otherwise the master won't be touched at all). Using the force flag will cause a bunch of checks to be ignored, use with care. ``` -$ vtctl ApplySchemaShard --sql-file=change.sql -simple /zk/global/vt/keyspaces/vtx/shards/0 -$ vtctl ApplySchemaShard --sql-file=change.sql -new_parent=/zk/nyc/vt/tablets/0002009002 /zk/global/vt/keyspaces/vtx/shards/0 +$ vtctl ApplySchemaShard --sql-file=change.sql -simple vtx/0 +$ vtctl ApplySchemaShard --sql-file=change.sql -new_parent=nyc-0002009002 vtx/0 ``` ``` -ApplySchemaKeyspace {-sql= || -sql_file=} [-simple] +ApplySchemaKeyspace {-sql= || -sql_file=} [-simple] ``` -apply the schema change to the specified shard. If simple is specified, we just apply on the live master. Otherwise we will need to do the shell game. So we will apply the schema change to every single slave. +apply the schema change to the specified shard. If simple is specified, we just apply on the live master. Otherwise we will need to do the shell game. So we will apply the schema change to every single slave. diff --git a/doc/Tools.md b/doc/Tools.md index 601d4605dd3..e2c24093c19 100644 --- a/doc/Tools.md +++ b/doc/Tools.md @@ -56,7 +56,7 @@ This is useful for trouble-shooting, or to get a good high level picture of all the servers and their current state. ### vtworker -vtworker is meant to host long-running processes. It supports a plugin infrastructure, and offers libraries to easily pick tablets to use. We have developped: +vtworker is meant to host long-running processes. It supports a plugin infrastructure, and offers libraries to easily pick tablets to use. We have developed: - resharding differ jobs: meant to check data integrity during shard splits and joins. - vertical split differ jobs: meant to check data integrity during vertical splits and joins. diff --git a/doc/ZookeeperData.md b/doc/ZookeeperData.md index f0442b3d8df..9de63929412 100644 --- a/doc/ZookeeperData.md +++ b/doc/ZookeeperData.md @@ -27,6 +27,8 @@ type Keyspace struct { ``` ``` +# NOTE: You need to source zookeeper client config file, like so: +# export ZK_CLIENT_CONFIG=/path/to/zk/client.conf $ zk ls /zk/global/vt/keyspaces/ruser action actionlog @@ -71,7 +73,7 @@ type Shard struct { } // SourceShard represents a data source for filtered replication -// accross shards. When this is used in a destination shard, the master +// across shards. When this is used in a destination shard, the master // of that shard will run filtered replication. type SourceShard struct { // Uid is the unique ID for this SourceShard object. @@ -220,7 +222,7 @@ The python client lists that directory at startup to find all the keyspaces. ### SrvKeyspace -The keyspace data is stored under /zk//vt/ns/ +The keyspace data is stored under `/zk//vt/ns/` ```go // see go/vt/topo/srvshard.go for latest version @@ -295,7 +297,7 @@ $ vtctl RebuildKeyspaceGraph When building a new Cell, this command should be run for every keyspace. Rebuilding a keyspace graph will: -- find all the shard names in the keyspace from looking at the children of /zk/global/vt/keyspaces//shards +- find all the shard names in the keyspace from looking at the children of `/zk/global/vt/keyspaces//shards` - rebuild the graph for every shard in the keyspace (see below) - find the list of cells to update by collecting the cells for each tablet of the first shard - compute, sanity check and update the keyspace graph object in every cell @@ -304,7 +306,7 @@ The python client reads the nodes to find the shard map (KeyRanges, TabletTypes, ### SrvShard -The shard data is stored under /zk//vt/ns// +The shard data is stored under `/zk//vt/ns//` ``` $ zk cat /zk/nyc/vt/ns/rlookup/0 @@ -318,7 +320,7 @@ $ zk cat /zk/nyc/vt/ns/rlookup/0 ### EndPoints -We also have per serving type data under /zk//vt/ns/// +We also have per serving type data under `/zk//vt/ns///` ``` $ zk cat /zk/nyc/vt/ns/rlookup/0/master @@ -329,9 +331,9 @@ $ zk cat /zk/nyc/vt/ns/rlookup/0/master "host": "nyc-db274.nyc.youtube.com", "port": 0, "named_port_map": { - "_mysql": 3306, - "_vtocc": 8101, - "_vts": 8102 + "mysql": 3306, + "vt": 8101, + "vts": 8102 } } ] @@ -344,8 +346,8 @@ Note this will rebuild the serving graph for all cells, not just one cell. Rebuilding a shard serving graph will: - compute the data to write by looking at all the tablets from the replicaton graph -- write all the /zk//vt/ns/// nodes everywhere -- delete any pre-existing /zk//vt/ns/// that is not in use any more -- compute and write all the /zk//vt/ns// nodes everywhere +- write all the `/zk//vt/ns///` nodes everywhere +- delete any pre-existing `/zk//vt/ns///` that is not in use any more +- compute and write all the `/zk//vt/ns//` nodes everywhere The clients read the per-type data nodes to find servers to talk to. When resolving ruser.10-20.master, it will try to read /zk/local/vt/ns/ruser/10-20/master. diff --git a/doc/life_of_a_query.png b/doc/life_of_a_query.png new file mode 100644 index 00000000000..8bd84478110 Binary files /dev/null and b/doc/life_of_a_query.png differ diff --git a/doc/life_of_a_query.xml b/doc/life_of_a_query.xml new file mode 100644 index 00000000000..502c728e210 --- /dev/null +++ b/doc/life_of_a_query.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/life_of_a_query_all.png b/doc/life_of_a_query_all.png new file mode 100644 index 00000000000..1f5c99a09a4 Binary files /dev/null and b/doc/life_of_a_query_all.png differ diff --git a/doc/life_of_a_query_all.xml b/doc/life_of_a_query_all.xml new file mode 100644 index 00000000000..d9de791073d --- /dev/null +++ b/doc/life_of_a_query_all.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/life_of_a_query_client_to_vtgate.png b/doc/life_of_a_query_client_to_vtgate.png new file mode 100644 index 00000000000..17ea88e700f Binary files /dev/null and b/doc/life_of_a_query_client_to_vtgate.png differ diff --git a/doc/life_of_a_query_client_to_vtgate.xml b/doc/life_of_a_query_client_to_vtgate.xml new file mode 100644 index 00000000000..ea3e8967257 --- /dev/null +++ b/doc/life_of_a_query_client_to_vtgate.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/life_of_a_query_vtgate_to_vttablet.png b/doc/life_of_a_query_vtgate_to_vttablet.png new file mode 100644 index 00000000000..16ea722e4a4 Binary files /dev/null and b/doc/life_of_a_query_vtgate_to_vttablet.png differ diff --git a/doc/life_of_a_query_vtgate_to_vttablet.xml b/doc/life_of_a_query_vtgate_to_vttablet.xml new file mode 100644 index 00000000000..8baf7a09bc9 --- /dev/null +++ b/doc/life_of_a_query_vtgate_to_vttablet.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/life_of_a_query_vttablet_to_mysql.png b/doc/life_of_a_query_vttablet_to_mysql.png new file mode 100644 index 00000000000..bcac1ee97bf Binary files /dev/null and b/doc/life_of_a_query_vttablet_to_mysql.png differ diff --git a/doc/life_of_a_query_vttablet_to_mysql.xml b/doc/life_of_a_query_vttablet_to_mysql.xml new file mode 100644 index 00000000000..b0e99e0cce5 --- /dev/null +++ b/doc/life_of_a_query_vttablet_to_mysql.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docker/etcd/Dockerfile b/docker/etcd/Dockerfile new file mode 100644 index 00000000000..0c1b1db8e80 --- /dev/null +++ b/docker/etcd/Dockerfile @@ -0,0 +1,14 @@ +# This is a Dockerfile for etcd that is built on the same base image +# as the Vitess Dockerfile, so the base image can be shared. +# +# This image also contains bash, which is needed for startup scripts, +# such as those found in the Vitess Kubernetes example. The official +# etcd Docker image on quay.io doesn't have any shell in it. +FROM golang:1.3-wheezy + +RUN mkdir -p src/github.com/coreos && \ + cd src/github.com/coreos && \ + curl -sL https://github.com/coreos/etcd/archive/v0.4.6.tar.gz | tar -xz && \ + mv etcd-0.4.6 etcd && \ + go install github.com/coreos/etcd +CMD ["etcd"] diff --git a/examples/demo/cgi-bin/data.py b/examples/demo/cgi-bin/data.py new file mode 100755 index 00000000000..b8fbc4dabe6 --- /dev/null +++ b/examples/demo/cgi-bin/data.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2015, Google Inc. All rights reserved. +# Use of this source code is governed by a BSD-style license that can +# be found in the LICENSE file. +""" +This module allows you to bring up and tear down keyspaces. +""" + +import cgi +import json +import subprocess +import sys +import threading +import time + +from vtdb import vtgatev3 + +def exec_query(cursor, title, query, response): + try: + if not query or query == "undefined": + return + if query.startswith("select"): + cursor.execute(query, {}) + else: + cursor.begin() + cursor.execute(query, {}) + cursor.commit() + response[title] = { + "title": title, + "description": cursor.description, + "rowcount": cursor.rowcount, + "lastrowid": cursor.lastrowid, + "results": cursor.results, + } + except Exception as e: + response[title] = { + "title": title, + "error": str(e), + } + +def capture_log(prefix, port, queries): + p = subprocess.Popen(["curl", "-s", "-N", "http://localhost:%d/debug/querylog" % port], stdout=subprocess.PIPE) + def collect(): + for line in iter(p.stdout.readline, ''): + query = line.split("\t")[10].strip('"') + if not query: + continue + queries.append([prefix, query]) + t = threading.Thread(target=collect) + t.daemon = True + t.start() + return p + +def main(): + print "Content-Type: application/json\n" + try: + conn = vtgatev3.connect("localhost:15009", 10.0) + cursor = conn.cursor("master") + + args = cgi.FieldStorage() + query = args.getvalue("query") + response = {} + + try: + queries = [] + user0 = capture_log("user0", 15003, queries) + user1 = capture_log("user1", 15005, queries) + lookup = capture_log("lookup", 15007, queries) + time.sleep(0.25) + exec_query(cursor, "result", query, response) + finally: + user0.terminate() + user1.terminate() + lookup.terminate() + time.sleep(0.25) + response["queries"] = queries + + exec_query(cursor, "user0", "select * from user where keyrange('','\x80')", response) + exec_query(cursor, "user1", "select * from user where keyrange('\x80', '')", response) + exec_query(cursor, "user_extra0", "select * from user_extra where keyrange('','\x80')", response) + exec_query(cursor, "user_extra1", "select * from user_extra where keyrange('\x80', '')", response) + + exec_query(cursor, "music0", "select * from music where keyrange('','\x80')", response) + exec_query(cursor, "music1", "select * from music where keyrange('\x80', '')", response) + exec_query(cursor, "music_extra0", "select * from music_extra where keyrange('','\x80')", response) + exec_query(cursor, "music_extra1", "select * from music_extra where keyrange('\x80', '')", response) + + exec_query(cursor, "user_idx", "select * from user_idx", response) + exec_query(cursor, "name_user_idx", "select * from name_user_idx", response) + exec_query(cursor, "music_user_idx", "select * from music_user_idx", response) + + + print json.dumps(response) + except Exception as e: + print json.dumps({"error": str(e)}) + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demo.js b/examples/demo/demo.js new file mode 100644 index 00000000000..6d001e4e9cf --- /dev/null +++ b/examples/demo/demo.js @@ -0,0 +1,67 @@ +/** + * Copyright 2015, Google Inc. All rights reserved. Use of this source code is + * governed by a BSD-style license that can be found in the LICENSE file. + */ +'use strict'; + +function DemoController($scope, $http) { + + function init() { + $scope.samples = [ + "insert into user(name) values('test1') /* run this a few times */", + "insert into user(user_id, name) values(6, 'test2') /* app-suplied user_id */", + "insert into user(user_id, name) values(6, null) /* error: name must be supplied */", + "select * from user where user_id=6 /* unique select */", + "select * from user where name='test1' /* non-unique select */", + "select * from user where user_id in (1, 6) /* unique multi-select */", + "select * from user where name in ('test1', 'test2') /* non-unique multi-select */", + "select * from user /* scatter */", + "select count(*) from user where user_id=1 /* aggregation on unique vindex */", + "select count(*) from user where name='foo' /* error: aggregation on non-unique vindex */", + "select * from user where user_id=1 limit 1 /* limit on unique vindex */", + "update user set user_id=1 where user_id=2 /* error: cannot change vindex columns */", + "delete from user where user_id=1 /* other 'test1' in name_user_idx unaffected */", + "delete from user where name='test1' /* error: cannot delete by non-unique vindex */", + "", + "insert into user_extra(user_id, extra) values(1, 'extra1')", + "insert into user_extra(extra) values('extra1') /* error: must supply value for user_id */", + "select * from user_extra where extra='extra1' /* scatter */", + "update user_extra set extra='extra2' where user_id=1 /* allowed */", + "delete from user_extra where user_id=1 /* vindexes are unchanged */", + "", + "insert into music(user_id) values(1) /* auto-inc on music_id */", + "insert into music(user_id, music_id) values(1, 6) /* explicit music_id value */", + "select * from music where user_id=1", + "delete from music where music_id=6 /* one row deleted */", + "delete from music where user_id=1 /* multiple rows deleted */", + "", + "insert into music_extra(music_id) values(1) /* keyspace_id back-computed */", + "insert into music_extra(music_id, keyspace_id) values(1, 1) /* invalid keyspace id */", + ]; + $scope.submitQuery() + } + + $scope.submitQuery = function() { + try { + $http({ + method: 'POST', + url: '/cgi-bin/data.py', + data: "query=" + $scope.query, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }).success(function(data, status, headers, config) { + $scope.result = angular.fromJson(data); + }); + } catch (err) { + $scope.result.error = err.message; + } + }; + + $scope.setQuery = function($query) { + $scope.query = $query; + angular.element("#query_input").focus(); + }; + + init(); +} diff --git a/examples/demo/index.html b/examples/demo/index.html new file mode 100644 index 00000000000..cb4e7c0e06e --- /dev/null +++ b/examples/demo/index.html @@ -0,0 +1,102 @@ + + + + + + + +Vitess V3 demo + + +
+
+

Vitess V3 demo

+
+
+
+
+ + +
+ +
+
+ + + + + +
{{queryInfo[0]}}{{queryInfo[1]}}
+
+ {{result.error}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + diff --git a/examples/demo/result.html b/examples/demo/result.html new file mode 100644 index 00000000000..a9d2471a132 --- /dev/null +++ b/examples/demo/result.html @@ -0,0 +1,22 @@ +
+
+
{{curResult.title}}
+ last_id: {{res.lastrowid}} +
+
+ + + + + + + +
{{field[0]}}
{{cell}}
+
+
+
+ rows_affected: {{curResult.rowcount}} last_id: {{curResult.lastrowid}}
+
+ {{curResult.title}}: {{curResult.error}}
\ No newline at end of file diff --git a/examples/kubernetes/README.md b/examples/kubernetes/README.md index fc41794da6b..7d6c9d3da70 100644 --- a/examples/kubernetes/README.md +++ b/examples/kubernetes/README.md @@ -1,232 +1,271 @@ # Vitess on Kubernetes This directory contains an example configuration for running Vitess on -[Kubernetes](https://github.com/GoogleCloudPlatform/kubernetes/). Refer to the -appropriate [Getting Started Guide](https://github.com/GoogleCloudPlatform/kubernetes/#contents) -to get Kubernetes up and running if you haven't already. +[Kubernetes](https://github.com/GoogleCloudPlatform/kubernetes/). -## Requirements +These instructions are written for running in +[Google Container Engine](https://cloud.google.com/container-engine/), +but they can be adapted to run on other +[platforms that Kubernetes supports](https://github.com/GoogleCloudPlatform/kubernetes/tree/master/docs/getting-started-guides). -This example currently requires Kubernetes 0.4.x. -Later versions have introduced -[incompatible changes](https://groups.google.com/forum/#!topic/kubernetes-announce/idiwm36dN-g) -that break ZooKeeper support. The Kubernetes team plans to support -[ZooKeeper's use case](https://github.com/GoogleCloudPlatform/kubernetes/issues/1802) -again in the future. Until then, please *git checkout* the -[v0.4.3](https://github.com/GoogleCloudPlatform/kubernetes/tree/v0.4.3) -tag (or any newer v0.4.x) in your Kubernetes repository. +## Prerequisites -The easiest way to run the local commands like vtctl is just to install -[Docker](https://www.docker.com/) -on your workstation. You can also adapt the commands below to use a local -[Vitess build](https://github.com/youtube/vitess/blob/master/doc/GettingStarted.md) -by removing the docker preamble if you prefer. +If you're running Kubernetes manually, instead of through Container Engine, +make sure to use at least +[v0.9.2](https://github.com/GoogleCloudPlatform/kubernetes/releases). +Container Engine will use the latest available release by default. -## Starting ZooKeeper +You'll need [Go 1.3+](http://golang.org/doc/install) in order to build the +`vtctlclient` tool used to issue commands to Vitess: -Once you have a running Kubernetes deployment, make sure -*kubernetes/cluster/kubecfg.sh* is in your path, and then run: +### Build and install vtctlclient ``` -vitess$ examples/kubernetes/zk-up.sh +$ go get github.com/youtube/vitess/go/cmd/vtctlclient ``` -This will create a quorum of ZooKeeper servers. You can check the status of the -pods with *kubecfg.sh list pods*, or by using the -[Kubernetes web interface](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/ux.md). -Note that it may take a while for each minion to download the Docker images the -first time it needs them, during which time the pod status will be *Waiting*. +### Set the path to kubectl + +If you're running in Container Engine, set the `KUBECTL` environment variable +to point to the `gcloud` command: -Clients can connect to port 2181 of any -[minion](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/DESIGN.md#cluster-architecture) -(assuming the firewall is set to allow it), and the Kubernetes proxy will -load-balance the connection to any of the servers. +``` +$ export KUBECTL='gcloud preview container kubectl' +``` -A simple way to test out your ZooKeeper deployment is by logging into one of -your minions and running the *zk* client utility inside Docker. For example, if -you are running [Kubernetes on Google Compute Engine](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/getting-started-guides/gce.md): +If you're running Kubernetes manually, set the `KUBECTL` environment variable +to point to the location of `kubectl.sh`. For example: ``` -# log in to a minion -$ gcloud compute ssh kubernetes-minion-1 +$ export KUBECTL=$HOME/kubernetes/cluster/kubectl.sh +``` + +### Create a Container Engine cluster -# show zk command usage -kubernetes-minion-1:~$ sudo docker run -ti --rm vitess/base zk +Follow the steps to +[enable the Container Engine API](https://cloud.google.com/container-engine/docs/before-you-begin). -# create a test node in ZooKeeper -kubernetes-minion-1:~$ sudo docker run -ti --rm vitess/base zk -zk.addrs $HOSTNAME:2181 touch -p /zk/test_cell/vt +Set the [zone](https://cloud.google.com/compute/docs/zones#available) you want to use: -# check that the node is there -kubernetes-minion-1:~$ sudo docker run -ti --rm vitess/base zk -zk.addrs $HOSTNAME:2181 ls /zk/test_cell +``` +$ gcloud config set compute/zone us-central1-b ``` -To tear down the ZooKeeper deployment (again, with *kubecfg.sh* in your path): +Then create a cluster: ``` -vitess$ examples/kubernetes/zk-down.sh +$ gcloud preview container clusters create example --machine-type n1-standard-1 --num-nodes 3 ``` -## Starting vtctld +## Start an etcd cluster for Vitess -The vtctld server provides a web interface to inspect the state of the system, -and also accepts RPC commands to modify the system. +Once you have a running Kubernetes deployment, make sure to set `KUBECTL` +as described above, and then run: ``` -vitess/examples/kubernetes$ kubecfg.sh -c vtctld-service.yaml create services -vitess/examples/kubernetes$ kubecfg.sh -c vtctld-pod.yaml create pods +vitess/examples/kubernetes$ ./etcd-up.sh ``` -To access vtctld from your workstation, open up port 15000 to any minion in your -firewall. Then get the external address of that minion and visit *http://<minion-addr>:15000/*. - -## Issuing commands with vtctlclient +This will create two clusters: one for the 'global' cell, and one for the +'test' cell. +You can check the status of the pods with `$KUBECTL get pods`. +Note that it may take a while for each minion to download the Docker images the +first time it needs them, during which time the pod status will be `Pending`. -If you've opened port 15000 on your minion's firewall, you can run *vtctlclient* -locally to issue commands: +In general, each `-up.sh` script in this example has a corresponding `-down.sh` +in case you want to stop certain pieces without bringing down the whole cluster. +For example, to tear down the etcd deployment: ``` -# check the connection to vtctld, and list available commands -$ sudo docker run -ti --rm vitess/base vtctlclient -server :15000 +vitess/examples/kubernetes$ ./etcd-down.sh +``` + +## Start vtctld + +The vtctld server provides a web interface to inspect the state of the system, +and also accepts RPC commands from `vtctlclient` to modify the system. -# create a global keyspace record -$ sudo docker run -ti --rm vitess/base vtctlclient -server :15000 CreateKeyspace my_keyspace +``` +vitess/examples/kubernetes$ ./vtctld-up.sh ``` -If you don't want to open the port on the firewall, you can SSH into one of your -minions and perform the above commands against the minion's local Kubernetes proxy. -For example: +To let you access vtctld from outside Kubernetes, the vtctld service is created +with the createExternalLoadBalancer option. On supported platforms, Kubernetes +will then automatically create an external IP that load balances onto the pods +comprising the service. Note that you also need to open port 15000 in your +firewall. ``` -# log in to a minion -$ gcloud compute ssh kubernetes-minion-1 +# open port 15000 +$ gcloud compute firewall-rules create vtctld --allow tcp:15000 -# run a command -kubernetes-minion-1:~$ sudo docker run -ti --rm vitess/base vtctlclient -server $HOSTNAME:15000 CreateKeyspace your_keyspace +# get the address of the load balancer for vtctld +$ gcloud compute forwarding-rules list +NAME REGION IP_ADDRESS IP_PROTOCOL TARGET +vtctld us-central1 12.34.56.78 TCP us-central1/targetPools/vtctld ``` -## Creating a keyspace and shard +In the example above, you would then access vtctld at +http://12.34.56.78:15000/ once the pod has entered the `Running` state. + +## Control vtctld with vtctlclient -This creates the initial paths in the topology server. +If you've opened port 15000 on your firewall, you can run `vtctlclient` +locally to issue commands. Depending on your actual vtctld IP, +the `vtctlclient` command will look different. So from here on, we'll assume +you've made an alias called `kvtctl` with your particular parameters, such as: ``` -$ alias vtctl="sudo docker run -ti --rm vitess/base vtctlclient -server :15000" -$ vtctl CreateKeyspace test_keyspace -$ vtctl CreateShard test_keyspace/0 +$ alias kvtctl='vtctlclient -server 12.34.56.78:15000' + +# check the connection to vtctld, and list available commands +$ kvtctl ``` -## Launching vttablets +## Start vttablets We launch vttablet in a [pod](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/pods.md) -along with mysqld. The following script will instantiate *vttablet-pod-template.yaml* -for a master and two replicas. +along with mysqld. The following script will instantiate `vttablet-pod-template.yaml` +for three replicas. ``` vitess/examples/kubernetes$ ./vttablet-up.sh ``` -Wait for the pods to enter Running state (*kubecfg.sh list pods*). +Wait for the pods to enter Running state (`$KUBECTL get pods`). Again, this may take a while if a pod was scheduled on a minion that needs to download the Vitess Docker image. Eventually you should see the tablets show up -in the *DB topology* summary page of vtctld (*http://<minion-addr>:15000/dbtopo*). - -### Troubleshooting +in the *DB topology* summary page of vtctld (`http://12.34.56.78:15000/dbtopo`). -You can log into the minion corresponding to one of the pods to check the logs. -For example, on GCE that would look like this: +By bringing up tablets into a previously empty keyspace, we effectively just +created a new shard. To initialize the keyspace for the new shard, we need to +perform a keyspace rebuild: ``` -# which minion is the vttabetl-101 pod on? -$ kubecfg.sh list pods | grep vttablet-101 -vttablet-101 vitess/root,vitess/root kubernetes-minion-2 Running +$ kvtctl RebuildKeyspaceGraph test_keyspace +``` -# ssh to the minion -$ gcloud compute ssh kubernetes-minion-2 +Note that most vtctlclient commands produce no output on success. -# find the Docker containers for the tablet -kubernetes-minion-2:~$ sudo docker ps | grep vttablet-101 -1de8493ecc9a vitess/root:latest [...] k8s_mysql... -d8c5ed2c4d53 vitess/root:latest [...] k8s_vttablet... -f89f0554a8aa vitess/root:latest [...] k8s_net... +### Status pages for vttablets -# exec a shell inside the mysql or vttablet container -kubernetes-minion-2:~$ sudo docker exec -ti 1de8493ecc9a bash +Each vttablet serves a set of HTML status pages on its primary port. +The vtctld interface provides links on each tablet entry marked *[status]*, +but these links are to internal per-pod IPs that can only be accessed from +within Kubernetes. As a workaround, you can proxy over an SSH connection to +a Kubernetes minion, or launch a proxy as a Kubernetes service. -# look at log files for Vitess or MySQL -root@vttablet-101:vitess# cd /vt/vtdataroot/tmp -root@vttablet-101:tmp# ls -mysqlctld.INFO -vttablet.INFO -vttablet.log -root@vttablet-101:tmp# cd /vt/vtdataroot/vt_0000000101 -root@vttablet-101:vt_0000000101# cat error.log -``` +In the future, we plan to accomplish the proxying via the Kubernetes API +server, without the need for additional setup. -### Viewing vttablet status +## Elect a master vttablet -Each vttablet serves a set of HTML status pages on its primary port. -The vtctld interface provides links on each tablet entry, but these currently -don't work when running within Kubernetes. Because there is no DNS server in -Kubernetes yet, we can't use the hostname of the pod to find the tablet, since -that hostname is not resolvable outside the pod itself. Also, we use internal -IP addresses to communicate within the cluster because in a typical cloud -environment, network fees are charged differently when instances communicate -on external IPs. +The vttablets have all been started as replicas, but there is no master yet. +When we pick a master vttablet, Vitess will also take care of connecting the +other replicas' mysqld instances to start replicating from the master mysqld. -As a result, getting access to a tablet's status page from your workstation -outside the cluster is a bit tricky. Currently, this example assigns a unique -port to every tablet and then publishes that port to the Docker host machine. -For example, the tablet with UID 101 is assigned port 15101. You then have to -look up the external IP of the minion that is running vttablet-101 -(via *kubecfg.sh list pods*), and visit -*http://<minion-addr>:15101/debug/status*. You'll of course need access -to these ports from your workstation to be allowed by any firewalls. +Since this is the first time we're starting up the shard, there is no existing +replication happening, so we use the -force flag on ReparentShard to skip the +usual validation of each tablet's replication state. -## Starting MySQL replication +``` +$ kvtctl ReparentShard -force test_keyspace/0 test-0000000100 +``` -The vttablets have been assigned master and replica roles by the startup -script, but MySQL itself has not been told to start replicating. -To do that, we do a forced reparent to the existing master. +Once this is done, you should see one master and two replicas in vtctld's web +interface. You can also check this on the command line with vtctlclient: ``` -$ vtctl RebuildShardGraph test_keyspace/0 -$ vtctl ReparentShard -force test_keyspace/0 test_cell-0000000100 -$ vtctl RebuildKeyspaceGraph test_keyspace +$ kvtctl ListAllTablets test +test-0000000100 test_keyspace 0 master 10.244.4.6:15002 10.244.4.6:3306 [] +test-0000000101 test_keyspace 0 replica 10.244.1.8:15002 10.244.1.8:3306 [] +test-0000000102 test_keyspace 0 replica 10.244.1.9:15002 10.244.1.9:3306 [] ``` -## Creating a table +## Create a table -The vtctl tool can manage schema across all tablets in a keyspace. -To create the table defined in *create_test_table.sql*: +The `vtctlclient` tool can manage schema across all tablets in a keyspace. +To create the table defined in `create_test_table.sql`: ``` -vitess/examples/kubernetes$ vtctl ApplySchemaKeyspace -simple -sql "$(cat create_test_table.sql)" test_keyspace +# run this from the example dir so it finds the create_test_table.sql file +vitess/examples/kubernetes$ kvtctl ApplySchemaKeyspace -simple -sql "$(cat create_test_table.sql)" test_keyspace ``` -## Launching the vtgate pool +## Start a vtgate pool Clients send queries to Vitess through vtgate, which routes them to the correct vttablet(s) behind the scenes. In Kubernetes, we define a vtgate -service (currently using Services v1 on $SERVICE_HOST:15001) that load -balances connections to a pool of vtgate pods curated by a +service that distributes connections to a pool of vtgate pods curated by a [replication controller](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/replication-controller.md). ``` vitess/examples/kubernetes$ ./vtgate-up.sh ``` -## Creating a client app +## Start the sample GuestBook app server + +The GuestBook app in this example is ported from the +[Kubernetes GuestBook example](https://github.com/GoogleCloudPlatform/kubernetes/tree/master/examples/guestbook-go). +The server-side code has been rewritten in Python to use Vitess as the storage +engine. The client-side code (HTML/JavaScript) is essentially unchanged. + +``` +vitess/examples/kubernetes$ ./guestbook-up.sh + +# open port 3000 in the firewall +$ gcloud compute firewall-rules create guestbook --allow tcp:3000 + +# find the external IP of the load balancer for the guestbook service +$ gcloud compute forwarding-rules list +NAME REGION IP_ADDRESS IP_PROTOCOL TARGET +guestbook us-central1 1.2.3.4 TCP us-central1/targetPools/guestbook +vtctld us-central1 12.34.56.78 TCP us-central1/targetPools/vtctld +``` + +Once the pods are running, the GuestBook should be accessible from port 3000 on +the external IP, for example: http://1.2.3.4:3000/ + +Try opening multiple browser windows of the app, and adding an entry on one +side. The JavaScript on each page polls the app server once a second, so the +other windows should update automatically. Since the app serves read-only +requests by querying Vitess in 'replica' mode, this confirms that replication +is working. + +See the +[GuestBook source](https://github.com/youtube/vitess/tree/master/examples/kubernetes/guestbook) +for more details on how the app server interacts with Vitess. + +## Tear down and clean up -The file *client.py* contains a simple example app that connects to vtgate -and executes some queries. Assuming you have opened firewall access from -your workstation to port 15001, you can run it locally and point it at any -minion: +Tear down the Container Engine cluster: ``` -$ sudo docker run -ti --rm vitess/base bash -c '$VTTOP/examples/kubernetes/client.py --server=:15001' -Inserting into master... -Reading from master... -(1L, 'V is for speed') -Reading from replica... -(1L, 'V is for speed') +$ gcloud preview container clusters delete example ``` + +Clean up other entities created for this example: + +``` +$ gcloud compute forwarding-rules delete vtctld +$ gcloud compute firewall-rules delete vtctld +$ gcloud compute target-pools delete vtctld +``` + +## Troubleshooting + +If a pod enters the `Running` state, but the server doesn't respond as expected, +try checking the pod output with the `kubectl log` command: + +``` +# show logs for container 'vttablet' within pod 'vttablet-100' +$ $KUBECTL log vttablet-100 vttablet + +# show logs for container 'mysql' within pod 'vttablet-100' +$ $KUBECTL log vttablet-100 mysql +``` + +You can post the logs somewhere and send a link to the +[Vitess mailing list](https://groups.google.com/forum/#!forum/vitess) +to get more help. diff --git a/examples/kubernetes/create_test_table.sql b/examples/kubernetes/create_test_table.sql index 6bd1c604e3c..e6660fea1c6 100644 --- a/examples/kubernetes/create_test_table.sql +++ b/examples/kubernetes/create_test_table.sql @@ -1,6 +1,6 @@ CREATE TABLE test_table ( id BIGINT AUTO_INCREMENT, - msg VARCHAR(64), + msg VARCHAR(250), PRIMARY KEY (id) ) ENGINE=InnoDB diff --git a/examples/kubernetes/env.sh b/examples/kubernetes/env.sh new file mode 100644 index 00000000000..3fc074d042e --- /dev/null +++ b/examples/kubernetes/env.sh @@ -0,0 +1,6 @@ +# This is an include file used by the other scripts in this directory. + +if [ -z "$KUBECTL" ]; then + echo 'Please set KUBECTL env var to point to kubectl or kubectl.sh' + exit 1 +fi diff --git a/examples/kubernetes/etcd-controller-template.yaml b/examples/kubernetes/etcd-controller-template.yaml new file mode 100644 index 00000000000..420632a0822 --- /dev/null +++ b/examples/kubernetes/etcd-controller-template.yaml @@ -0,0 +1,44 @@ +apiVersion: v1beta1 +kind: ReplicationController +id: etcd-{{cell}} +desiredState: + replicas: 3 + replicaSelector: + name: etcd + cell: {{cell}} + podTemplate: + desiredState: + manifest: + version: v1beta1 + id: etcd-{{cell}} + containers: + - name: etcd + image: vitess/etcd:v0.4.6 + command: + - bash + - "-c" + - >- + ipaddr=$(hostname -i) + + global_etcd=$ETCD_GLOBAL_SERVICE_HOST:$ETCD_GLOBAL_SERVICE_PORT + + cell="{{cell}}" && + local_etcd_host_var="ETCD_${cell^^}_SERVICE_HOST" && + local_etcd_port_var="ETCD_${cell^^}_SERVICE_PORT" && + local_etcd=${!local_etcd_host_var}:${!local_etcd_port_var} + + if [ "{{cell}}" != "global" ]; then + until curl -L http://$global_etcd/v2/keys/vt/cells/{{cell}} + -XPUT -d value=http://$local_etcd; do + echo "[$(date)] waiting for global etcd to register cell '{{cell}}'"; + sleep 1; + done; + fi + + etcd -name $HOSTNAME -peer-addr $ipaddr:7001 -addr $ipaddr:4001 -discovery {{discovery}} + labels: + name: etcd + cell: {{cell}} +labels: + name: etcd + cell: {{cell}} diff --git a/examples/kubernetes/etcd-down.sh b/examples/kubernetes/etcd-down.sh new file mode 100755 index 00000000000..3ccee6b6bdb --- /dev/null +++ b/examples/kubernetes/etcd-down.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# This is an example script that tears down the etcd servers started by +# etcd-up.sh. + +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh + +# Delete replication controllers +for cell in 'global' 'test'; do + echo "Stopping etcd replicationController for $cell cell..." + $KUBECTL stop replicationController etcd-$cell + + echo "Deleting etcd service for $cell cell..." + $KUBECTL delete service etcd-$cell +done + diff --git a/examples/kubernetes/etcd-service-template.yaml b/examples/kubernetes/etcd-service-template.yaml new file mode 100644 index 00000000000..91fb4fb4ddb --- /dev/null +++ b/examples/kubernetes/etcd-service-template.yaml @@ -0,0 +1,11 @@ +apiVersion: v1beta1 +kind: Service +id: etcd-{{cell}} +port: 4001 +containerPort: 4001 +selector: + name: etcd + cell: {{cell}} +labels: + name: etcd + cell: {{cell}} diff --git a/examples/kubernetes/etcd-up.sh b/examples/kubernetes/etcd-up.sh new file mode 100755 index 00000000000..1d1a986a9c0 --- /dev/null +++ b/examples/kubernetes/etcd-up.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# This is an example script that creates etcd clusters. +# Vitess requires a global cluster, as well as one for each cell. +# +# For automatic discovery, an etcd cluster can be bootstrapped from an +# existing cluster. In this example, we use an externally-run discovery +# service, but you can use your own. See the etcd docs for more: +# https://github.com/coreos/etcd/blob/v0.4.6/Documentation/cluster-discovery.md + +set -e + +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh + +for cell in 'global' 'test'; do + # Generate a discovery token. + echo "Generating discovery token for $cell cell..." + discovery=$(curl -sL https://discovery.etcd.io/new) + + # Create the client service, which will load-balance across all replicas. + echo "Creating etcd service for $cell cell..." + cat etcd-service-template.yaml | \ + sed -e "s/{{cell}}/$cell/g" | \ + $KUBECTL create -f - + + # Create the replication controller. + echo "Creating etcd replicationController for $cell cell..." + cat etcd-controller-template.yaml | \ + sed -e "s/{{cell}}/$cell/g" -e "s,{{discovery}},$discovery,g" | \ + $KUBECTL create -f - +done + diff --git a/examples/kubernetes/guestbook-controller.yaml b/examples/kubernetes/guestbook-controller.yaml new file mode 100644 index 00000000000..ef6479defd8 --- /dev/null +++ b/examples/kubernetes/guestbook-controller.yaml @@ -0,0 +1,21 @@ +apiVersion: v1beta1 +kind: ReplicationController +id: guestbook +desiredState: + replicas: 3 + replicaSelector: {name: guestbook} + podTemplate: + desiredState: + manifest: + version: v1beta1 + id: guestbook + containers: + - name: guestbook + image: vitess/guestbook + ports: + - name: http-server + containerPort: 8080 + labels: + name: guestbook +labels: + name: guestbook diff --git a/examples/kubernetes/guestbook-down.sh b/examples/kubernetes/guestbook-down.sh new file mode 100755 index 00000000000..b9dd80ab582 --- /dev/null +++ b/examples/kubernetes/guestbook-down.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# This is an example script that stops guestbook. + +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh + +echo "Stopping guestbook replicationController..." +$KUBECTL stop replicationController guestbook + +echo "Deleting guestbook service..." +$KUBECTL delete service guestbook diff --git a/examples/kubernetes/guestbook-service.yaml b/examples/kubernetes/guestbook-service.yaml new file mode 100644 index 00000000000..d9732ce002d --- /dev/null +++ b/examples/kubernetes/guestbook-service.yaml @@ -0,0 +1,8 @@ +apiVersion: v1beta1 +kind: Service +id: guestbook +port: 3000 +containerPort: http-server +selector: + name: guestbook +createExternalLoadBalancer: true diff --git a/examples/kubernetes/guestbook-up.sh b/examples/kubernetes/guestbook-up.sh new file mode 100755 index 00000000000..5ebfffe8ea3 --- /dev/null +++ b/examples/kubernetes/guestbook-up.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# This is an example script that starts a guestbook replicationController. + +set -e + +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh + +echo "Creating guestbook service..." +$KUBECTL create -f guestbook-service.yaml + +echo "Creating guestbook replicationController..." +$KUBECTL create -f guestbook-controller.yaml diff --git a/examples/kubernetes/guestbook/Dockerfile b/examples/kubernetes/guestbook/Dockerfile new file mode 100644 index 00000000000..c9fe0675337 --- /dev/null +++ b/examples/kubernetes/guestbook/Dockerfile @@ -0,0 +1,5 @@ +# This Dockerfile should be built from within the accompanying build.sh script. +FROM google/python-runtime + +ADD tmp /app/site-packages +ENV PYTHONPATH /app/site-packages diff --git a/examples/kubernetes/guestbook/README.md b/examples/kubernetes/guestbook/README.md new file mode 100644 index 00000000000..efd04fb2afd --- /dev/null +++ b/examples/kubernetes/guestbook/README.md @@ -0,0 +1,11 @@ +# vitess/guestbook + +This is a Docker image for a sample guestbook app that uses Vitess. + +It is essentially a port of the +[kubernetes/guestbook-go](https://github.com/GoogleCloudPlatform/kubernetes/tree/master/examples/guestbook-go) +example, but using Python instead of Go for the app server, +and Vitess instead of Redis for the storage engine. + +Note that the Dockerfile should be built with the accompanying build.sh script. +See the comments in the script for more information. diff --git a/examples/kubernetes/guestbook/build.sh b/examples/kubernetes/guestbook/build.sh new file mode 100755 index 00000000000..f61fda907b5 --- /dev/null +++ b/examples/kubernetes/guestbook/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# This is a script to build the vitess/guestbook Docker image. +# It must be run from within a bootstrapped vitess tree, after dev.env. + +set -e + +mkdir tmp + +# Collect all the local Python libs we need. +cp -R $VTTOP/py/* tmp/ +for pypath in $(find $VTROOT/dist -name site-packages); do + cp -R $pypath/* tmp/ +done + +# Build the Docker image. +docker build -t vitess/guestbook . + +# Clean up. +rm -rf tmp diff --git a/examples/kubernetes/guestbook/main.py b/examples/kubernetes/guestbook/main.py new file mode 100644 index 00000000000..2fe251511f3 --- /dev/null +++ b/examples/kubernetes/guestbook/main.py @@ -0,0 +1,65 @@ +import os +import json + +from flask import Flask +app = Flask(__name__) + +from vtdb import keyrange +from vtdb import keyrange_constants +from vtdb import vtgatev2 +from vtdb import vtgate_cursor +from zk import zkocc + +# Constants and params +UNSHARDED = [keyrange.KeyRange(keyrange_constants.NON_PARTIAL_KEYRANGE)] + +# conn is the connection to vtgate. +conn = None + +@app.route("/") +def index(): + return app.send_static_file('index.html') + +@app.route("/lrange/guestbook") +def list_guestbook(): + # Read the list from a replica. + cursor = conn.cursor('test_keyspace', 'replica', keyranges=UNSHARDED) + + cursor.execute('SELECT * FROM test_table ORDER BY id', {}) + entries = [row[1] for row in cursor.fetchall()] + cursor.close() + + return json.dumps(entries) + +@app.route("/rpush/guestbook/") +def add_entry(value): + # Insert a row on the master. + cursor = conn.cursor('test_keyspace', 'master', keyranges=UNSHARDED, writable=True) + + cursor.begin() + cursor.execute('INSERT INTO test_table (msg) VALUES (%(msg)s)', + {'msg': value}) + cursor.commit() + + # Read the list back from master (critical read) because it's + # important that the user sees his own addition immediately. + cursor.execute('SELECT * FROM test_table ORDER BY id', {}) + entries = [row[1] for row in cursor.fetchall()] + cursor.close() + + return json.dumps(entries) + +@app.route("/env") +def env(): + return json.dumps(dict(os.environ)) + +if __name__ == "__main__": + timeout = 10 # connect timeout in seconds + + # Get vtgate service address from Kubernetes environment. + addr = '%s:%s' % (os.environ['VTGATE_SERVICE_HOST'], os.environ['VTGATE_SERVICE_PORT']) + + # Connect to vtgate. + conn = vtgatev2.connect({'vt': [addr]}, timeout) + + app.run(host='0.0.0.0', port=8080, debug=True) diff --git a/examples/kubernetes/guestbook/requirements.txt b/examples/kubernetes/guestbook/requirements.txt new file mode 100644 index 00000000000..880a7bc4f2e --- /dev/null +++ b/examples/kubernetes/guestbook/requirements.txt @@ -0,0 +1 @@ +Flask==0.10 diff --git a/examples/kubernetes/guestbook/static/index.html b/examples/kubernetes/guestbook/static/index.html new file mode 100644 index 00000000000..946a2b3d986 --- /dev/null +++ b/examples/kubernetes/guestbook/static/index.html @@ -0,0 +1,33 @@ + + + + + + + + Guestbook + + + + +
+

Waiting for database connection...

+
+ +
+
+ + Submit +
+
+ +
+

+

/env +

+ + + + diff --git a/examples/kubernetes/guestbook/static/script.js b/examples/kubernetes/guestbook/static/script.js new file mode 100644 index 00000000000..a0a545b0564 --- /dev/null +++ b/examples/kubernetes/guestbook/static/script.js @@ -0,0 +1,46 @@ +$(document).ready(function() { + var headerTitleElement = $("#header h1"); + var entriesElement = $("#guestbook-entries"); + var formElement = $("#guestbook-form"); + var submitElement = $("#guestbook-submit"); + var entryContentElement = $("#guestbook-entry-content"); + var hostAddressElement = $("#guestbook-host-address"); + + var appendGuestbookEntries = function(data) { + entriesElement.empty(); + $.each(data, function(key, val) { + entriesElement.append("

" + val + "

"); + }); + } + + var handleSubmission = function(e) { + e.preventDefault(); + var entryValue = entryContentElement.val() + if (entryValue.length > 0) { + entriesElement.append("

...

"); + $.getJSON("rpush/guestbook/" + entryValue, appendGuestbookEntries); + } + return false; + } + + // colors = purple, blue, red, green, yellow + var colors = ["#549", "#18d", "#d31", "#2a4", "#db1"]; + var randomColor = colors[Math.floor(5 * Math.random())]; + (function setElementsColor(color) { + headerTitleElement.css("color", color); + entryContentElement.css("box-shadow", "inset 0 0 0 2px " + color); + submitElement.css("background-color", color); + })(randomColor); + + submitElement.click(handleSubmission); + formElement.submit(handleSubmission); + hostAddressElement.append(document.URL); + + // Poll every second. + (function fetchGuestbook() { + $.getJSON("lrange/guestbook").done(appendGuestbookEntries).always( + function() { + setTimeout(fetchGuestbook, 1000); + }); + })(); +}); diff --git a/examples/kubernetes/guestbook/static/style.css b/examples/kubernetes/guestbook/static/style.css new file mode 100644 index 00000000000..fd1c393fb08 --- /dev/null +++ b/examples/kubernetes/guestbook/static/style.css @@ -0,0 +1,61 @@ +body, input { + color: #123; + font-family: "Gill Sans", sans-serif; +} + +div { + overflow: hidden; + padding: 1em 0; + position: relative; + text-align: center; +} + +h1, h2, p, input, a { + font-weight: 300; + margin: 0; +} + +h1 { + color: #BDB76B; + font-size: 3.5em; +} + +h2 { + color: #999; +} + +form { + margin: 0 auto; + max-width: 50em; + text-align: center; +} + +input { + border: 0; + border-radius: 1000px; + box-shadow: inset 0 0 0 2px #BDB76B; + display: inline; + font-size: 1.5em; + margin-bottom: 1em; + outline: none; + padding: .5em 5%; + width: 55%; +} + +form a { + background: #BDB76B; + border: 0; + border-radius: 1000px; + color: #FFF; + font-size: 1.25em; + font-weight: 400; + padding: .75em 2em; + text-decoration: none; + text-transform: uppercase; + white-space: normal; +} + +p { + font-size: 1.5em; + line-height: 1.5; +} diff --git a/examples/kubernetes/vtctld-down.sh b/examples/kubernetes/vtctld-down.sh index bb915d7f51e..44b398a6a4b 100755 --- a/examples/kubernetes/vtctld-down.sh +++ b/examples/kubernetes/vtctld-down.sh @@ -1,10 +1,12 @@ #!/bin/bash # This is an example script that stops vtctld. -# It assumes that kubernetes/cluster/kubecfg.sh is in the path. + +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh echo "Deleting vtctld pod..." -kubecfg.sh delete pods/vtctld +$KUBECTL delete pod vtctld echo "Deleting vtctld service..." -kubecfg.sh delete services/vtctld +$KUBECTL delete service vtctld diff --git a/examples/kubernetes/vtctld-pod.yaml b/examples/kubernetes/vtctld-pod.yaml index 7767138016c..b75fbd09efc 100644 --- a/examples/kubernetes/vtctld-pod.yaml +++ b/examples/kubernetes/vtctld-pod.yaml @@ -17,13 +17,9 @@ desiredState: - sh - "-c" - >- - echo "{\"test_cell\":\"$SERVICE_HOST:2181\",\"global\":\"$SERVICE_HOST:2181\"}" > /vt/zk-client-conf.json && mkdir -p $VTDATAROOT/tmp && chown -R vitess /vt && - su -p -c "/vt/bin/vtctld -debug -templates $VTTOP/go/cmd/vtctld/templates -log_dir $VTDATAROOT/tmp -port 15000" vitess - env: - - name: ZK_CLIENT_CONFIG - value: /vt/zk-client-conf.json + su -p -c "/vt/bin/vtctld -debug -templates $VTTOP/go/cmd/vtctld/templates -log_dir $VTDATAROOT/tmp -alsologtostderr -port 15000 -topo_implementation etcd -etcd_global_addrs http://$ETCD_GLOBAL_SERVICE_HOST:$ETCD_GLOBAL_SERVICE_PORT" vitess volumes: - name: syslog source: {hostDir: {path: /dev/log}} diff --git a/examples/kubernetes/vtctld-service.yaml b/examples/kubernetes/vtctld-service.yaml index b4f1fe1d2b5..0da91dd6350 100644 --- a/examples/kubernetes/vtctld-service.yaml +++ b/examples/kubernetes/vtctld-service.yaml @@ -3,6 +3,7 @@ kind: Service id: vtctld port: 15000 containerPort: 15000 +createExternalLoadBalancer: true selector: name: vtctld labels: diff --git a/examples/kubernetes/vtctld-up.sh b/examples/kubernetes/vtctld-up.sh index 950241d0426..d8360af69c1 100755 --- a/examples/kubernetes/vtctld-up.sh +++ b/examples/kubernetes/vtctld-up.sh @@ -1,12 +1,14 @@ #!/bin/bash # This is an example script that starts vtctld. -# It assumes that kubernetes/cluster/kubecfg.sh is in the path. set -e +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh + echo "Creating vtctld service..." -kubecfg.sh -c vtctld-service.yaml create services +$KUBECTL create -f vtctld-service.yaml echo "Creating vtctld pod..." -kubecfg.sh -c vtctld-pod.yaml create pods +$KUBECTL create -f vtctld-pod.yaml diff --git a/examples/kubernetes/vtgate-controller.yaml b/examples/kubernetes/vtgate-controller.yaml index f60e317c169..87e0d5d8d39 100644 --- a/examples/kubernetes/vtgate-controller.yaml +++ b/examples/kubernetes/vtgate-controller.yaml @@ -1,6 +1,6 @@ apiVersion: v1beta1 kind: ReplicationController -id: vtgateController +id: vtgate desiredState: replicas: 3 replicaSelector: {name: vtgate} @@ -8,7 +8,7 @@ desiredState: desiredState: manifest: version: v1beta1 - id: vtgateController + id: vtgate containers: - name: vtgate image: vitess/root @@ -21,13 +21,15 @@ desiredState: - sh - "-c" - >- - echo "{\"test_cell\":\"$SERVICE_HOST:2181\",\"global\":\"$SERVICE_HOST:2181\"}" > /vt/zk-client-conf.json && mkdir -p $VTDATAROOT/tmp && chown -R vitess /vt && - su -p -c "/vt/bin/vtgate -log_dir $VTDATAROOT/tmp -port 15001 -cell test_cell" vitess - env: - - name: ZK_CLIENT_CONFIG - value: /vt/zk-client-conf.json + su -p -c "/vt/bin/vtgate + -topo_implementation etcd + -etcd_global_addrs http://$ETCD_GLOBAL_SERVICE_HOST:$ETCD_GLOBAL_SERVICE_PORT + -log_dir $VTDATAROOT/tmp + -alsologtostderr + -port 15001 + -cell test" vitess volumes: - name: syslog source: {hostDir: {path: /dev/log}} diff --git a/examples/kubernetes/vtgate-down.sh b/examples/kubernetes/vtgate-down.sh index 9b168a03838..df7a794c769 100755 --- a/examples/kubernetes/vtgate-down.sh +++ b/examples/kubernetes/vtgate-down.sh @@ -1,13 +1,12 @@ #!/bin/bash # This is an example script that stops vtgate. -# It assumes that kubernetes/cluster/kubecfg.sh is in the path. -echo "Deleting pods created by vtgate replicationController..." -kubecfg.sh stop vtgateController +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh -echo "Deleting vtgate replicationController..." -kubecfg.sh delete replicationControllers/vtgateController +echo "Stopping vtgate replicationController..." +$KUBECTL stop replicationController vtgate echo "Deleting vtgate service..." -kubecfg.sh delete services/vtgate +$KUBECTL delete service vtgate diff --git a/examples/kubernetes/vtgate-up.sh b/examples/kubernetes/vtgate-up.sh index bba9ab0a3ba..f751a5a60e3 100755 --- a/examples/kubernetes/vtgate-up.sh +++ b/examples/kubernetes/vtgate-up.sh @@ -1,12 +1,14 @@ #!/bin/bash # This is an example script that starts a vtgate replicationController. -# It assumes that kubernetes/cluster/kubecfg.sh is in the path. set -e +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh + echo "Creating vtgate service..." -kubecfg.sh -c vtgate-service.yaml create services +$KUBECTL create -f vtgate-service.yaml echo "Creating vtgate replicationController..." -kubecfg.sh -c vtgate-controller.yaml create replicationControllers +$KUBECTL create -f vtgate-controller.yaml diff --git a/examples/kubernetes/vttablet-down.sh b/examples/kubernetes/vttablet-down.sh index db9c4fc330f..84bdd4582e7 100755 --- a/examples/kubernetes/vttablet-down.sh +++ b/examples/kubernetes/vttablet-down.sh @@ -1,11 +1,14 @@ #!/bin/bash # This is an example script that tears down the vttablet pods started by -# vttablet-up.sh. It assumes that kubernetes/cluster/kubecfg.sh is in the path. +# vttablet-up.sh. -# Create the pods for shard-0 -cell=test_cell -keyspace=test_keyspace +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh + +# Delete the pods for shard-0 +cell='test' +keyspace='test_keyspace' shard=0 uid_base=100 @@ -14,5 +17,5 @@ for uid_index in 0 1 2; do printf -v alias '%s-%010d' $cell $uid echo "Deleting pod for tablet $alias..." - kubecfg.sh delete pods/vttablet-$uid + $KUBECTL delete pod vttablet-$uid done diff --git a/examples/kubernetes/vttablet-pod-template.yaml b/examples/kubernetes/vttablet-pod-template.yaml index 70efb2af4d6..a6681009aba 100644 --- a/examples/kubernetes/vttablet-pod-template.yaml +++ b/examples/kubernetes/vttablet-pod-template.yaml @@ -19,63 +19,47 @@ desiredState: - >- set -e - echo "{\"test_cell\":\"$SERVICE_HOST:2181\",\"global\":\"$SERVICE_HOST:2181\"}" > /vt/zk-client-conf.json - - log_file=$VTDATAROOT/tmp/vttablet.log - mysql_socket="$VTDATAROOT/{{tablet_subdir}}/mysql.sock" - hostname=$(hostname -i) - mkdir -p $VTDATAROOT/tmp chown -R vitess /vt - su -p -s /bin/bash -c "/vt/bin/vtctlclient - -log_dir $VTDATAROOT/tmp - -server $SERVICE_HOST:15000 - InitTablet -force -parent - -port {{port}} - -hostname $hostname - -keyspace {{keyspace}} - -shard {{shard}} - {{alias}} {{type}} - &>> $log_file" vitess - while [ ! -e $mysql_socket ]; do - echo "[$(date)] waiting for $mysql_socket" >> $log_file ; + echo "[$(date)] waiting for $mysql_socket" ; sleep 1 ; done su -p -s /bin/bash -c "mysql -u vt_dba -S $mysql_socket - -e 'CREATE DATABASE IF NOT EXISTS vt_{{keyspace}}' - &>> $log_file" vitess + -e 'CREATE DATABASE IF NOT EXISTS vt_{{keyspace}}'" vitess su -p -s /bin/bash -c "/vt/bin/vttablet + -topo_implementation etcd + -etcd_global_addrs http://$ETCD_GLOBAL_SERVICE_HOST:$ETCD_GLOBAL_SERVICE_PORT -log_dir $VTDATAROOT/tmp + -alsologtostderr -port {{port}} -tablet-path {{alias}} - -tablet_hostname $hostname + -tablet_hostname $(hostname -i) + -init_keyspace {{keyspace}} + -init_shard {{shard}} -target_tablet_type replica -mysqlctl_socket $VTDATAROOT/mysqlctl.sock + -db-config-app-uname vt_app -db-config-app-dbname vt_{{keyspace}} + -db-config-app-charset utf8 + -db-config-dba-uname vt_dba -db-config-dba-dbname vt_{{keyspace}} + -db-config-dba-charset utf8 + -db-config-repl-uname vt_repl -db-config-repl-dbname vt_{{keyspace}} + -db-config-repl-charset utf8 + -db-config-filtered-uname vt_filtered -db-config-filtered-dbname vt_{{keyspace}} + -db-config-filtered-charset utf8 -enable-rowcache -rowcache-bin /usr/bin/memcached - -rowcache-socket $VTDATAROOT/{{tablet_subdir}}/memcache.sock - &>> $log_file" vitess - env: - - name: ZK_CLIENT_CONFIG - value: /vt/zk-client-conf.json - ports: - - # We only publish this port so we can access the tablet's status - # page from outside the Kubernetes cluster. You can unpublish it if - # you don't care about that. - name: vttablet - containerPort: {{port}} - hostPort: {{port}} + -rowcache-socket $VTDATAROOT/{{tablet_subdir}}/memcache.sock" vitess - name: mysql image: vitess/root volumeMounts: @@ -87,13 +71,28 @@ desiredState: - sh - "-c" - >- - echo "{\"test_cell\":\"$SERVICE_HOST:2181\",\"global\":\"$SERVICE_HOST:2181\"}" > /vt/zk-client-conf.json && mkdir -p $VTDATAROOT/tmp && - chown -R vitess /vt && - su -p -c "/vt/bin/mysqlctld -log_dir $VTDATAROOT/tmp -tablet_uid {{uid}} -socket_file $VTDATAROOT/mysqlctl.sock -bootstrap_archive mysql-db-dir_10.0.13-MariaDB.tbz" vitess + chown -R vitess /vt + + su -p -c "/vt/bin/mysqlctld + -log_dir $VTDATAROOT/tmp + -alsologtostderr + -tablet_uid {{uid}} + -socket_file $VTDATAROOT/mysqlctl.sock + -db-config-app-uname vt_app + -db-config-app-dbname vt_{{keyspace}} + -db-config-app-charset utf8 + -db-config-dba-uname vt_dba + -db-config-dba-dbname vt_{{keyspace}} + -db-config-dba-charset utf8 + -db-config-repl-uname vt_repl + -db-config-repl-dbname vt_{{keyspace}} + -db-config-repl-charset utf8 + -db-config-filtered-uname vt_filtered + -db-config-filtered-dbname vt_{{keyspace}} + -db-config-filtered-charset utf8 + -bootstrap_archive mysql-db-dir_10.0.13-MariaDB.tbz" vitess env: - - name: ZK_CLIENT_CONFIG - value: /vt/zk-client-conf.json - name: EXTRA_MY_CNF value: /vt/config/mycnf/master_mariadb.cnf volumes: @@ -103,6 +102,6 @@ desiredState: source: {emptyDir: {}} labels: name: vttablet - keyspace: {{keyspace}} - shard: {{shard}} - tabletAlias: {{alias}} + keyspace: "{{keyspace}}" + shard: "{{shard}}" + tablet: "{{alias}}" diff --git a/examples/kubernetes/vttablet-up.sh b/examples/kubernetes/vttablet-up.sh index f62cf8edb91..f77377e2a5a 100755 --- a/examples/kubernetes/vttablet-up.sh +++ b/examples/kubernetes/vttablet-up.sh @@ -1,16 +1,18 @@ #!/bin/bash # This is an example script that creates a single shard vttablet deployment. -# It assumes that kubernetes/cluster/kubecfg.sh is in the path. set -e +script_root=`dirname "${BASH_SOURCE}"` +source $script_root/env.sh + # Create the pods for shard-0 -cell=test_cell -keyspace=test_keyspace +cell='test' +keyspace='test_keyspace' shard=0 uid_base=100 -port_base=15000 +port=15002 echo "Creating $keyspace.shard-$shard pods in cell $cell..." for uid_index in 0 1 2; do @@ -18,30 +20,16 @@ for uid_index in 0 1 2; do printf -v alias '%s-%010d' $cell $uid printf -v tablet_subdir 'vt_%010d' $uid - # It's not strictly necessary to assign a unique port to every tablet since - # Kubernetes gives every pod its own IP address. However, Kubernetes currently - # doesn't provide routing from the internet into a particular pod, so we have - # to publish a port to the host if we want to access each tablet's status page - # from a workstation. As a result, we need tablets to have unique ports or - # else Kubernetes will be unable to schedule more than one tablet per host. - port=$[$port_base + $uid] - - if [ "$uid_index" == "0" ]; then - type=master - else - type=replica - fi - echo "Creating pod for tablet $alias..." # Expand template variables sed_script="" - for var in alias cell uid keyspace shard type port tablet_subdir; do + for var in alias cell uid keyspace shard port tablet_subdir; do sed_script+="s/{{$var}}/${!var}/g;" done - # Instantiate template and send to kubecfg. + # Instantiate template and send to kubectl. cat vttablet-pod-template.yaml | \ sed -e "$sed_script" | \ - kubecfg.sh -c - create pods + $KUBECTL create -f - done diff --git a/examples/kubernetes/zk-client-service.yaml b/examples/kubernetes/zk-client-service.yaml deleted file mode 100644 index c4e274a7c4f..00000000000 --- a/examples/kubernetes/zk-client-service.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1beta1 -kind: Service -id: zk-client -port: 2181 -containerPort: 2181 -selector: - name: zk -labels: - name: zk diff --git a/examples/kubernetes/zk-down.sh b/examples/kubernetes/zk-down.sh deleted file mode 100755 index 1c2d8b06b74..00000000000 --- a/examples/kubernetes/zk-down.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# This is an example script that tears down the ZooKeeper servers started by -# zk-up.sh. It assumes that kubernetes/cluster/kubecfg.sh is in the path. - -# Delete pods. -for zkid in 1 2 3; do - echo "Deleting zk$zkid pod..." - kubecfg.sh delete pods/zk$zkid -done - -# Delete client service. -echo "Deleting zk-client service..." -kubecfg.sh delete services/zk-client - -# Delete leader and election services. -for zkid in 1 2 3; do - echo "Deleting zk$zkid-leader service..." - kubecfg.sh delete services/zk$zkid-leader - - echo "Deleting zk$zkid-election service..." - kubecfg.sh delete services/zk$zkid-election -done diff --git a/examples/kubernetes/zk-pod-template.yaml b/examples/kubernetes/zk-pod-template.yaml deleted file mode 100644 index ea23e8fedb8..00000000000 --- a/examples/kubernetes/zk-pod-template.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: v1beta1 -kind: Pod -id: zk{{zkid}} -desiredState: - manifest: - version: v1beta1 - id: zk{{zkid}} - containers: - - name: zk{{zkid}} - image: vitess/root - volumeMounts: - - name: syslog - mountPath: /dev/log - - name: vtdataroot - mountPath: /vt/vtdataroot - command: - - sh - - "-c" - - >- - mkdir -p $VTDATAROOT/tmp && - chown -R vitess /vt && - su -p -c "/vt/bin/zkctld -zk.myid {{zkid}} -zk.cfg {{zkcfg}} -log_dir $VTDATAROOT/tmp" vitess - volumes: - - name: syslog - source: {hostDir: {path: /dev/log}} - - name: vtdataroot - source: {emptyDir: {}} -labels: - name: zk - zkid: {{zkid}} diff --git a/examples/kubernetes/zk-service-template.yaml b/examples/kubernetes/zk-service-template.yaml deleted file mode 100644 index 392a8e03e4a..00000000000 --- a/examples/kubernetes/zk-service-template.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1beta1 -kind: Service -id: zk{{zkid}}-{{svc}} -port: {{port}}{{zkid}} -containerPort: {{port}}{{zkid}} -selector: - name: zk - zkid: {{zkid}} -labels: - name: zk - zkid: {{zkid}} diff --git a/examples/kubernetes/zk-up.sh b/examples/kubernetes/zk-up.sh deleted file mode 100755 index 4c6b13e74ea..00000000000 --- a/examples/kubernetes/zk-up.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -# This is an example script that creates a quorum of ZooKeeper servers. -# It assumes that kubernetes/cluster/kubecfg.sh is in the path. - -set -e - -# List of all servers in the quorum. -zkcfg=(\ - '1@$SERVICE_HOST:28881:38881:2181' \ - '2@$SERVICE_HOST:28882:38882:2181' \ - '3@$SERVICE_HOST:28883:38883:2181' \ - ) -printf -v zkcfg ",%s" "${zkcfg[@]}" -zkcfg=${zkcfg:1} - -# Create the client service, which will load-balance across all replicas. -echo "Creating zk services..." -kubecfg.sh -c zk-client-service.yaml create services - -# Create a service for the leader and election ports of each replica. -# This is necessary because ZooKeeper servers need to know how to specifically -# contact replica N (not just "any replica") in order to create a quorum. -# We also have to append the zkid of each server to the port number, because -# every service in Kubernetes needs a unique port number (for now). - -ports=( 2888 3888 ) -svcs=( leader election ) - -for zkid in 1 2 3; do - for i in 0 1; do - port=${ports[$i]} - svc=${svcs[$i]} - - cat zk-service-template.yaml | \ - sed -e "s/{{zkid}}/$zkid/g" -e "s/{{port}}/$port/g" -e "s/{{svc}}/$svc/g" | \ - kubecfg.sh -c - create services - done -done - -# Create the pods. -echo "Creating zk pods..." -for zkid in 1 2 3; do - cat zk-pod-template.yaml | \ - sed -e "s/{{zkid}}/$zkid/g" -e "s/{{zkcfg}}/$zkcfg/g" | \ - kubecfg.sh -c - create pods -done diff --git a/examples/local/README.md b/examples/local/README.md new file mode 100644 index 00000000000..9e58cb1611e --- /dev/null +++ b/examples/local/README.md @@ -0,0 +1,192 @@ +# Local Vitess Cluster + +This directory contains example scripts to bring up a Vitess cluster on your +local machine, which may be useful for experimentation. These scripts can +also serve as a starting point for configuring Vitess into your preferred +deployment strategy or toolset. + +## Requirements + +You should have completed a successful `make build` after following the +[Getting Started](https://github.com/youtube/vitess/blob/master/doc/GettingStarted.md) +guide. + +## Configuration + +Before starting, set `VTROOT` to the base of the Vitess tree that you built. +For example, if you ran `make build` while in +`$HOME/vt/src/github.com/youtube/vitess`, then you should set: +`export VTROOT=$HOME/vt` + +Also set `VTDATAROOT` to the directory where you want data files and logs to +be stored. For example: `export VTDATAROOT=$HOME/vtdataroot` + +Alternatively, if you are testing on the same machine you just used for +building, you can source `dev.env` as described the Getting Started guide. + +## Starting ZooKeeper + +The way servers in a Vitess cluster find each other is by looking for dynamic +configuration stored in a distributed lock service. For this example, we will +use ZooKeeper. The following script creates a small ZooKeeper cluster. + +``` +vitess/examples/local$ ./zk-up.sh +Starting zk servers... +Waiting for zk servers to be ready... +``` + +Once we have a ZooKeeper cluster, we only need to tell each Vitess process how +to connect to ZooKeeper. Then they can find all the other Vitess processes +by coordinating via ZooKeeper. The way we tell them how to find ZooKeeper is by +setting the `ZK_CLIENT_CONFIG` environment variable to the path to the file +`zk-client-conf.json`, which contains ZK server addresses for each cell. + +## Starting vtctld + +The vtctld server provides a web interface to view all the coordination +information stored in ZooKeeper. + +``` +vitess/examples/local$ ./vtctld-up.sh +Starting vtctld... +Access vtctld at http://localhost:15000 +``` + +There won't be anything there yet, but the menu should come up, +verifying that vtctld is running. + +## Issuing commands with vtctlclient + +The vtctld server also accepts commands from the vtctlclient tool, +which is used to administer the cluster. + +``` +# list available commands +vitess/examples/local$ $VTROOT/bin/vtctlclient -server localhost:15000 +``` + +## Starting vttablets + +For this example, we will bring up 3 tablets. + +``` +vitess/examples/local$ ./vttablet-up.sh +Starting MySQL for tablet test-0000000100... +Starting vttablet for test-0000000100... +Access tablet test-0000000100 at http://localhost:15100/debug/status +Starting MySQL for tablet test-0000000101... +Starting vttablet for test-0000000101... +Access tablet test-0000000101 at http://localhost:15101/debug/status +Starting MySQL for tablet test-0000000102... +Starting vttablet for test-0000000102... +Access tablet test-0000000102 at http://localhost:15102/debug/status +``` + +Once they are up, go back to the vtctld web page and click on the +*DbTopology Tool*. You should see all three tablets listed. If you click the +address of one of the tablets, you'll see the coordination data stored in +ZooKeeper. At the top of that page, there is also `[status]` link, which takes +you to the debug page generated by the tablet itself. + +By bringing up tablets into a previously empty keyspace, we effectively just +created a new shard. To initialize the keyspace for the new shard, we need to +perform a keyspace rebuild: + +``` +$ $VTROOT/bin/vtctlclient -server localhost:15000 RebuildKeyspaceGraph test_keyspace +``` + +Note that most vtctlclient commands produce no output on success. + +## Electing a master vttablet + +The vttablets have all been started as replicas, but there is no master yet. +When we pick a master vttablet, Vitess will also take care of connecting the +other replicas' mysqld instances to start replicating from the master mysqld. + +Since this is the first time we're starting up the shard, there is no existing +replication happening, so we use the -force flag on ReparentShard to skip the +usual validation of each tablet's replication state. + +``` +$ $VTROOT/bin/vtctlclient -server localhost:15000 ReparentShard -force test_keyspace/0 test-0000000100 +``` + +Once this is done, you should see one master and two replicas in vtctld's web +interface. You can also check this on the command line with vtctlclient: + +``` +$ $VTROOT/bin/vtctlclient -server localhost:15000 ListAllTablets test +test-0000000100 test_keyspace 0 master localhost:15100 localhost:33100 [] +test-0000000101 test_keyspace 0 replica localhost:15101 localhost:33101 [] +test-0000000102 test_keyspace 0 replica localhost:15102 localhost:33102 [] +``` + +## Creating a table + +The vtctl tool can manage schema across all tablets in a keyspace. +To create the table defined in `create_test_table.sql`: + +``` +vitess/examples/local$ $VTROOT/bin/vtctlclient -server localhost:15000 ApplySchemaKeyspace -simple -sql "$(cat create_test_table.sql)" test_keyspace +``` + +# Starting vtgate + +Clients send queries to Vitess through vtgate, which routes them to the +correct vttablet behind the scenes. In a real deployment, you would likely +run multiple vtgate instances to share the load. For this local example, +we only need one. + +``` +vitess/examples/local$ ./vtgate-up.sh +``` + +## Creating a client app + +The file `client.py` contains a simple example app that connects to vtgate +and executes some queries. To run it, you need to add the Vitess Python +packages to your `PYTHONPATH`. Or you can use the wrapper script `client.sh` +that temporarily sets up the environment and then runs `client.py`. + +``` +vitess/examples/local$ ./client.sh --server=localhost:15001 +Inserting into master... +Reading from master... +(1L, 'V is for speed') +Reading from replica... +(1L, 'V is for speed') +``` + +## Tearing down the cluster + +Assuming your `VTDATAROOT` directory is something that you use just for this, +you can kill all the processes we've started like this: + +``` +# look for processes we started +$ pgrep -u $USER -f -l $VTDATAROOT + +# if that list looks right, then kill them +$ pkill -u $USER -f $VTDATAROOT +``` + +To start over, you should also clear out the contents of `VTDATAROOT`: + +``` +$ cd $VTDATAROOT +/home/user/vt/vtdataroot$ rm -rf * +``` + +## Troubleshooting + +If anything goes wrong, check the logs in your `$VTDATAROOT/tmp` directory +for error messages. There are also some tablet-specific logs, as well as +MySQL logs in the various `$VTDATAROOT/vt_*` directories. + +If you need help diagnosing a problem, send a message to our +[mailing list](https://groups.google.com/forum/#!forum/vitess). +In addition to any errors you see at the command-line, it would also help to +upload an archive of your `$VTDATAROOT` directory to a file sharing service +and provide a link to it. diff --git a/examples/kubernetes/client.py b/examples/local/client.py old mode 100755 new mode 100644 similarity index 73% rename from examples/kubernetes/client.py rename to examples/local/client.py index 913a010d39e..20db25bab73 --- a/examples/kubernetes/client.py +++ b/examples/local/client.py @@ -6,12 +6,10 @@ from vtdb import keyrange_constants from vtdb import vtgatev2 from vtdb import vtgate_cursor -from vtdb import topology from zk import zkocc # Constants and params UNSHARDED = [keyrange.KeyRange(keyrange_constants.NON_PARTIAL_KEYRANGE)] -cursorclass = vtgate_cursor.VTGateCursor # Parse args parser = argparse.ArgumentParser() @@ -19,20 +17,14 @@ parser.add_argument('--timeout', dest='timeout', type=float, default='10.0') args = parser.parse_args() -vtgate_addrs = {"_vt": [args.server]} +vtgate_addrs = {"vt": [args.server]} # Connect conn = vtgatev2.connect(vtgate_addrs, args.timeout) -# Read topology -# This is a temporary work-around until the VTGate V2 client is topology-free. -topoconn = zkocc.ZkOccConnection(args.server, 'test_cell', args.timeout) -topology.read_topology(topoconn) -topoconn.close() - # Insert something. print('Inserting into master...') -cursor = conn.cursor(cursorclass, conn, 'test_keyspace', 'master', +cursor = conn.cursor('test_keyspace', 'master', keyranges=UNSHARDED, writable=True) cursor.begin() cursor.execute( @@ -52,7 +44,7 @@ # Read from a replica. # Note that this may be behind master due to replication lag. print('Reading from replica...') -cursor = conn.cursor(cursorclass, conn, 'test_keyspace', 'replica', +cursor = conn.cursor('test_keyspace', 'replica', keyranges=UNSHARDED) cursor.execute('SELECT * FROM test_table', {}) for row in cursor.fetchall(): diff --git a/examples/local/client.sh b/examples/local/client.sh new file mode 100755 index 00000000000..09dc573a374 --- /dev/null +++ b/examples/local/client.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# This is a wrapper script that sets up the environment for client.py. + +set -e + +hostname=`hostname -f` + +# We expect to find zk-client-conf.json in the same folder as this script. +script_root=`dirname "${BASH_SOURCE}"` + +# Set up environment. +for pkg in `find $VTROOT/dist -name site-packages`; do + export PYTHONPATH=$pkg:$PYTHONPATH +done + +export PYTHONPATH=$VTROOT/py-vtdb:$PYTHONPATH + +exec env python $script_root/client.py $* diff --git a/examples/local/create_test_table.sql b/examples/local/create_test_table.sql new file mode 100644 index 00000000000..e6660fea1c6 --- /dev/null +++ b/examples/local/create_test_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE test_table ( + id BIGINT AUTO_INCREMENT, + msg VARCHAR(250), + PRIMARY KEY (id) +) ENGINE=InnoDB + diff --git a/examples/local/vtctld-up.sh b/examples/local/vtctld-up.sh new file mode 100755 index 00000000000..c112435fa99 --- /dev/null +++ b/examples/local/vtctld-up.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# This is an example script that starts vtctld. + +set -e + +port=15000 + +hostname=`hostname -f` + +# We expect to find zk-client-conf.json in the same folder as this script. +script_root=`dirname "${BASH_SOURCE}"` + +# Set up environment. +export VTTOP=$VTROOT/src/github.com/youtube/vitess +export LD_LIBRARY_PATH=$VTROOT/dist/vt-zookeeper-3.3.5/lib:$LD_LIBRARY_PATH +export ZK_CLIENT_CONFIG=$script_root/zk-client-conf.json +mkdir -p $VTDATAROOT/tmp + +echo "Starting vtctld..." +$VTROOT/bin/vtctld -debug -templates $VTTOP/go/cmd/vtctld/templates \ + -log_dir $VTDATAROOT/tmp -port $port > $VTDATAROOT/tmp/vtctld.out 2>&1 & +disown -a + +echo "Access vtctld at http://$hostname:$port" diff --git a/examples/local/vtgate-up.sh b/examples/local/vtgate-up.sh new file mode 100755 index 00000000000..faaaa9d25d6 --- /dev/null +++ b/examples/local/vtgate-up.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# This is an example script that starts a single vtgate. + +set -e + +cell='test' +port=15001 + +hostname=`hostname -f` + +# We expect to find zk-client-conf.json in the same folder as this script. +script_root=`dirname "${BASH_SOURCE}"` + +# Set up environment. +export LD_LIBRARY_PATH=$VTROOT/dist/vt-zookeeper-3.3.5/lib:$LD_LIBRARY_PATH +export ZK_CLIENT_CONFIG=$script_root/zk-client-conf.json +mkdir -p $VTDATAROOT/tmp + +# Start vtgate. +$VTROOT/bin/vtgate -log_dir $VTDATAROOT/tmp -port $port -cell $cell \ + > $VTDATAROOT/tmp/vtgate.out 2>&1 & +echo "Access vtgate at http://$hostname:$port/debug/status" + +disown -a diff --git a/examples/local/vttablet-up.sh b/examples/local/vttablet-up.sh new file mode 100755 index 00000000000..919e51847c1 --- /dev/null +++ b/examples/local/vttablet-up.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# This is an example script that creates a single shard vttablet deployment. + +set -e + +cell='test' +keyspace='test_keyspace' +shard=0 +uid_base=100 +port_base=15100 +mysql_port_base=33100 + +hostname=`hostname -f` + +# We expect to find zk-client-conf.json in the same folder as this script. +script_root=`dirname "${BASH_SOURCE}"` + +dbconfig_flags="\ + -db-config-app-uname vt_app \ + -db-config-app-dbname vt_$keyspace \ + -db-config-app-charset utf8 \ + -db-config-dba-uname vt_dba \ + -db-config-dba-charset utf8 \ + -db-config-repl-uname vt_repl \ + -db-config-repl-dbname vt_$keyspace \ + -db-config-repl-charset utf8 \ + -db-config-filtered-uname vt_filtered \ + -db-config-filtered-dbname vt_$keyspace \ + -db-config-filtered-charset utf8" + +# Set up environment. +export LD_LIBRARY_PATH=$VTROOT/dist/vt-zookeeper-3.3.5/lib:$LD_LIBRARY_PATH +export ZK_CLIENT_CONFIG=$script_root/zk-client-conf.json +export EXTRA_MY_CNF=$VTROOT/config/mycnf/master_mariadb.cnf +mkdir -p $VTDATAROOT/tmp + +# Try to find mysqld_safe on PATH. +if [ -z "$VT_MYSQL_ROOT" ]; then + mysql_path=`which mysqld_safe` + if [ -z "$mysql_path" ]; then + echo "Can't guess location of mysqld_safe. Please set VT_MYSQL_ROOT so it can be found at \$VT_MYSQL_ROOT/bin/mysqld_safe." + exit 1 + fi + export VT_MYSQL_ROOT=$(dirname `dirname $mysql_path`) +fi + +# Look for memcached. +memcached_path=`which memcached` +if [ -z "$memcached_path" ]; then + echo "Can't find memcached. Please make sure it is available in PATH." + exit 1 +fi + +# Start 3 vttablets. +for uid_index in 0 1 2; do + uid=$[$uid_base + $uid_index] + port=$[$port_base + $uid_index] + mysql_port=$[$mysql_port_base + $uid_index] + printf -v alias '%s-%010d' $cell $uid + printf -v tablet_dir 'vt_%010d' $uid + + echo "Starting MySQL for tablet $alias..." + $VTROOT/bin/mysqlctl -log_dir $VTDATAROOT/tmp -tablet_uid $uid $dbconfig_flags \ + -mysql_port $mysql_port \ + init -bootstrap_archive mysql-db-dir_10.0.13-MariaDB.tbz + + $VT_MYSQL_ROOT/bin/mysql -u vt_dba -S $VTDATAROOT/$tablet_dir/mysql.sock \ + -e "CREATE DATABASE IF NOT EXISTS vt_$keyspace" + + echo "Starting vttablet for $alias..." + $VTROOT/bin/vttablet -log_dir $VTDATAROOT/tmp -port $port $dbconfig_flags \ + -tablet-path $alias \ + -init_keyspace $keyspace \ + -init_shard $shard \ + -target_tablet_type replica \ + -enable-rowcache \ + -rowcache-bin $memcached_path \ + -rowcache-socket $VTDATAROOT/$tablet_dir/memcache.sock \ + > $VTDATAROOT/$tablet_dir/vttablet.out 2>&1 & + + echo "Access tablet $alias at http://$hostname:$port/debug/status" +done + +disown -a diff --git a/examples/local/zk-client-conf.json b/examples/local/zk-client-conf.json new file mode 100644 index 00000000000..02aa41d2b26 --- /dev/null +++ b/examples/local/zk-client-conf.json @@ -0,0 +1,4 @@ +{ + "test": "localhost:21811,localhost:21812,localhost:21813", + "global": "localhost:21811,localhost:21812,localhost:21813" +} diff --git a/examples/local/zk-up.sh b/examples/local/zk-up.sh new file mode 100755 index 00000000000..be88f56fabc --- /dev/null +++ b/examples/local/zk-up.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# This is an example script that creates a quorum of ZooKeeper servers. + +set -e + +hostname=`hostname -f` + +# Each ZooKeeper server needs a list of all servers in the quorum. +# Since we're running them all locally, we need to give them unique ports. +# In a real deployment, these should be on different machines, and their +# respective hostnames should be given. +zkcfg=(\ + "1@$hostname:28881:38881:21811" \ + "2@$hostname:28882:38882:21812" \ + "3@$hostname:28883:38883:21813" \ + ) +printf -v zkcfg ",%s" "${zkcfg[@]}" +zkcfg=${zkcfg:1} + +# Set up environment. +export LD_LIBRARY_PATH=$VTROOT/dist/vt-zookeeper-3.3.5/lib:$LD_LIBRARY_PATH +mkdir -p $VTDATAROOT/tmp + +# Start ZooKeeper servers. +# The "zkctl init" command won't return until the server is able to contact its +# peers, so we need to start them all in the background and then wait for them. +echo "Starting zk servers..." +for zkid in 1 2 3; do + $VTROOT/bin/zkctl -zk.myid $zkid -zk.cfg $zkcfg -log_dir $VTDATAROOT/tmp init \ + > $VTDATAROOT/tmp/zkctl_$zkid.out 2>&1 & +done + +# Wait for all the zkctl commands to return. +echo "Waiting for zk servers to be ready..." +wait diff --git a/go/cgzip/cgzip_test.go b/go/cgzip/cgzip_test.go index 26b500c4784..99d0290215d 100644 --- a/go/cgzip/cgzip_test.go +++ b/go/cgzip/cgzip_test.go @@ -25,18 +25,26 @@ func newPrettyTimer(name string) *prettyTimer { } func (pt *prettyTimer) stopAndPrintCompress(t *testing.T, size, processed int) { - durationMs := int(int64(time.Now().Sub(pt.before)) / 1000) + duration := time.Since(pt.before) t.Log(pt.name + ":") t.Log(" size :", size) - t.Log(" time :", durationMs, "ms") - t.Log(" speed:", processed*1000/durationMs, "KB/s") + t.Log(" time :", duration.String()) + if duration != 0 { + t.Logf(" speed: %.0f KB/s", float64(processed)/duration.Seconds()/1024.0) + } else { + t.Log(" processed:", processed, "B") + } } func (pt *prettyTimer) stopAndPrintUncompress(t *testing.T, processed int) { - durationMs := int(int64(time.Now().Sub(pt.before)) / 1000) + duration := time.Since(pt.before) t.Log(" " + pt.name + ":") - t.Log(" time :", durationMs, "ms") - t.Log(" speed:", processed*1000/durationMs, "KB/s") + t.Log(" time :", duration.String()) + if duration != 0 { + t.Logf(" speed: %.0f KB/s", float64(processed)/duration.Seconds()/1024.0) + } else { + t.Log(" processed:", processed, "B") + } } func compareCompressedBuffer(t *testing.T, source []byte, compressed *bytes.Buffer) { diff --git a/go/cmd/bsongen/bsongen_test.go b/go/cmd/bsongen/bsongen_test.go index d3ccd0d0b88..0efab6289ca 100644 --- a/go/cmd/bsongen/bsongen_test.go +++ b/go/cmd/bsongen/bsongen_test.go @@ -6,7 +6,7 @@ package main import ( "bytes" - "fmt" + "errors" "io/ioutil" "os" "os/exec" @@ -17,23 +17,40 @@ import ( ) func TestValidFiles(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode.") + } + inputs := testfiles.Glob("bson_test/input*.go") for _, input := range inputs { b, err := ioutil.ReadFile(input) if err != nil { - t.Fatal(err) + t.Fatalf("ioutil.ReadFile error: %v", err) } - out, err := generateCode(string(b), "MyType") + want, err := ioutil.ReadFile(strings.Replace(input, "input", "output", 1)) if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - return + t.Fatalf("ioutil.ReadFile error: %v", err) } - want, err := ioutil.ReadFile(strings.Replace(input, "input", "output", 1)) + + out, err := generateCode(string(b), "MyType") if err != nil { - t.Fatal(err) + t.Fatalf("generateCode error: %v", err) } + // goimports is flaky. So, let's not test that part. - d, err := diff(skip_imports(want), skip_imports(out)) + want, err = skipImports(want) + if err != nil { + t.Fatalf("skipImports error: %v", err) + } + out, err = skipImports(out) + if err != nil { + t.Fatalf("skipImports error: %v", err) + } + + d, err := diff(want, out) + if err != nil { + t.Fatalf("diff error: %v", err) + } if len(d) != 0 { t.Errorf("Unexpected output for %s:\n%s", input, string(d)) if testing.Verbose() { @@ -71,18 +88,16 @@ func diff(b1, b2 []byte) (data []byte, err error) { return } -func skip_imports(b []byte) []byte { - buf := bytes.NewBuffer(b) - for { - line, err := buf.ReadBytes('\n') - if err != nil { - return b[:0] - } - if len(line) == 0 || line[0] != ')' { - continue - } - return b[len(b)-buf.Len():] +func skipImports(b []byte) ([]byte, error) { + begin := bytes.Index(b, []byte("\nimport (\n")) + if begin < 0 { + return nil, errors.New("couldn't find beginning of import block") + } + end := bytes.Index(b, []byte("\n)\n")) + if end < 0 { + return nil, errors.New("couldn't find end of imports block") } + return append(b[:begin], b[end+3:]...), nil } var invalidInputs = []struct{ title, input, err string }{ diff --git a/go/cmd/mysqlctl/mysqlctl.go b/go/cmd/mysqlctl/mysqlctl.go index ddc8e10ff00..dc6a933b264 100644 --- a/go/cmd/mysqlctl/mysqlctl.go +++ b/go/cmd/mysqlctl/mysqlctl.go @@ -8,13 +8,12 @@ package main import ( "flag" "fmt" - "net/url" "os" - "strings" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" + "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/vt/dbconfigs" - "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/mysqlctl" ) @@ -22,204 +21,121 @@ import ( var ( port = flag.Int("port", 6612, "vtocc port") mysqlPort = flag.Int("mysql_port", 3306, "mysql port") - tabletUid = flag.Uint("tablet_uid", 41983, "tablet uid") + tabletUID = flag.Uint("tablet_uid", 41983, "tablet uid") mysqlSocket = flag.String("mysql_socket", "", "path to the mysql socket") tabletAddr string ) -func initCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { +func initCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) error { waitTime := subFlags.Duration("wait_time", mysqlctl.MysqlWaitTime, "how long to wait for startup") bootstrapArchive := subFlags.String("bootstrap_archive", "mysql-db-dir.tbz", "name of bootstrap archive within vitess/data/bootstrap directory") skipSchema := subFlags.Bool("skip_schema", false, "don't apply initial schema") subFlags.Parse(args) if err := mysqld.Init(*waitTime, *bootstrapArchive, *skipSchema); err != nil { - log.Fatalf("failed init mysql: %v", err) + return fmt.Errorf("failed init mysql: %v", err) } + return nil } -func multisnapshotCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { - concurrency := subFlags.Int("concurrency", 8, "how many compression jobs to run simultaneously") - spec := subFlags.String("spec", "-", "shard specification") - tablesString := subFlags.String("tables", "", "dump only this comma separated list of regexp for tables") - excludeTablesString := subFlags.String("exclude_tables", "", "do not dump this comma separated list of regexp for tables") - skipSlaveRestart := subFlags.Bool("skip_slave_restart", false, "after the snapshot is done, do not restart slave replication") - maximumFilesize := subFlags.Uint64("maximum_file_size", 128*1024*1024, "the maximum size for an uncompressed data file") - keyType := subFlags.String("key_type", "uint64", "type of the key column") - subFlags.Parse(args) - if subFlags.NArg() != 2 { - log.Fatalf("action multisnapshot requires ") - } - - shards, err := key.ParseShardingSpec(*spec) - if err != nil { - log.Fatalf("multisnapshot failed: %v", err) - } - var tables []string - if *tablesString != "" { - tables = strings.Split(*tablesString, ",") - } - var excludedTables []string - if *excludeTablesString != "" { - excludedTables = strings.Split(*excludeTablesString, ",") - } - - kit := key.KeyspaceIdType(*keyType) - if !key.IsKeyspaceIdTypeInList(kit, key.AllKeyspaceIdTypes) { - log.Fatalf("invalid key_type") - } - - filenames, err := mysqld.CreateMultiSnapshot(logutil.NewConsoleLogger(), shards, subFlags.Arg(0), subFlags.Arg(1), kit, tabletAddr, false, *concurrency, tables, excludedTables, *skipSlaveRestart, *maximumFilesize, nil) - if err != nil { - log.Fatalf("multisnapshot failed: %v", err) - } else { - log.Infof("manifest locations: %v", filenames) - } -} - -func multiRestoreCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { - starts := subFlags.String("starts", "", "starts of the key range") - ends := subFlags.String("ends", "", "ends of the key range") - fetchRetryCount := subFlags.Int("fetch_retry_count", 3, "how many times to retry a failed transfer") - concurrency := subFlags.Int("concurrency", 8, "how many concurrent db inserts to run simultaneously") - fetchConcurrency := subFlags.Int("fetch_concurrency", 4, "how many files to fetch simultaneously") - insertTableConcurrency := subFlags.Int("insert_table_concurrency", 4, "how many myisam tables to load into a single destination table simultaneously") - strategyStr := subFlags.String("strategy", "", "which strategy to use for restore, use -strategy=-help for values") - - subFlags.Parse(args) - logger := logutil.NewConsoleLogger() - strategy, err := mysqlctl.NewSplitStrategy(logger, *strategyStr) - if err != nil { - log.Fatalf("invalid strategy: %v", err) - } - if subFlags.NArg() < 2 { - log.Fatalf("multirestore requires [/]... %v", args) - } - - startArray := strings.Split(*starts, ",") - endArray := strings.Split(*ends, ",") - if len(startArray) != len(endArray) || len(startArray) != subFlags.NArg()-1 { - log.Fatalf("Need as many starts and ends as source URLs") - } - - keyRanges := make([]key.KeyRange, len(startArray)) - for i, s := range startArray { - var err error - keyRanges[i], err = key.ParseKeyRangeParts(s, endArray[i]) - if err != nil { - log.Fatalf("Invalid start or end: %v", err) - } - } - - dbName, dbis := subFlags.Arg(0), subFlags.Args()[1:] - sources := make([]*url.URL, len(dbis)) - for i, dbi := range dbis { - if !strings.HasPrefix(dbi, "vttp://") && !strings.HasPrefix(dbi, "http://") { - dbi = "vttp://" + dbi - } - dbUrl, err := url.Parse(dbi) - if err != nil { - log.Fatalf("incorrect source url: %v", err) - } - sources[i] = dbUrl - } - if err := mysqld.MultiRestore(logger, dbName, keyRanges, sources, nil, *concurrency, *fetchConcurrency, *insertTableConcurrency, *fetchRetryCount, strategy); err != nil { - log.Fatalf("multirestore failed: %v", err) - } -} - -func restoreCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { +func restoreCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) error { dontWaitForSlaveStart := subFlags.Bool("dont_wait_for_slave_start", false, "won't wait for replication to start (useful when restoring from master server)") fetchConcurrency := subFlags.Int("fetch_concurrency", 3, "how many files to fetch simultaneously") fetchRetryCount := subFlags.Int("fetch_retry_count", 3, "how many times to retry a failed transfer") subFlags.Parse(args) if subFlags.NArg() != 1 { - log.Fatalf("Command restore requires ") + return fmt.Errorf("Command restore requires ") } rs, err := mysqlctl.ReadSnapshotManifest(subFlags.Arg(0)) - if err == nil { - err = mysqld.RestoreFromSnapshot(logutil.NewConsoleLogger(), rs, *fetchConcurrency, *fetchRetryCount, *dontWaitForSlaveStart, nil) + if err != nil { + return fmt.Errorf("restore failed: ReadSnapshotManifest: %v", err) } + err = mysqld.RestoreFromSnapshot(logutil.NewConsoleLogger(), rs, *fetchConcurrency, *fetchRetryCount, *dontWaitForSlaveStart, nil) if err != nil { - log.Fatalf("restore failed: %v", err) + return fmt.Errorf("restore failed: RestoreFromSnapshot: %v", err) } + return nil } -func shutdownCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { +func shutdownCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) error { waitTime := subFlags.Duration("wait_time", mysqlctl.MysqlWaitTime, "how long to wait for shutdown") subFlags.Parse(args) - if mysqlErr := mysqld.Shutdown(true, *waitTime); mysqlErr != nil { - log.Fatalf("failed shutdown mysql: %v", mysqlErr) + if err := mysqld.Shutdown(true, *waitTime); err != nil { + return fmt.Errorf("failed shutdown mysql: %v", err) } + return nil } -func snapshotCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { +func snapshotCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) error { concurrency := subFlags.Int("concurrency", 4, "how many compression jobs to run simultaneously") subFlags.Parse(args) if subFlags.NArg() != 1 { - log.Fatalf("Command snapshot requires ") + return fmt.Errorf("Command snapshot requires ") } filename, _, _, err := mysqld.CreateSnapshot(logutil.NewConsoleLogger(), subFlags.Arg(0), tabletAddr, false, *concurrency, false, nil) if err != nil { - log.Fatalf("snapshot failed: %v", err) - } else { - log.Infof("manifest location: %v", filename) + return fmt.Errorf("snapshot failed: %v", err) } + log.Infof("manifest location: %v", filename) + return nil } -func snapshotSourceStartCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { +func snapshotSourceStartCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) error { concurrency := subFlags.Int("concurrency", 4, "how many checksum jobs to run simultaneously") subFlags.Parse(args) if subFlags.NArg() != 1 { - log.Fatalf("Command snapshotsourcestart requires ") + return fmt.Errorf("Command snapshotsourcestart requires ") } filename, slaveStartRequired, readOnly, err := mysqld.CreateSnapshot(logutil.NewConsoleLogger(), subFlags.Arg(0), tabletAddr, false, *concurrency, true, nil) if err != nil { - log.Fatalf("snapshot failed: %v", err) - } else { - log.Infof("manifest location: %v", filename) - log.Infof("slave start required: %v", slaveStartRequired) - log.Infof("read only: %v", readOnly) + return fmt.Errorf("snapshot failed: %v", err) } + log.Infof("manifest location: %v", filename) + log.Infof("slave start required: %v", slaveStartRequired) + log.Infof("read only: %v", readOnly) + return nil } -func snapshotSourceEndCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { +func snapshotSourceEndCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) error { slaveStartRequired := subFlags.Bool("slave_start", false, "will restart replication") readWrite := subFlags.Bool("read_write", false, "will make the server read-write") subFlags.Parse(args) err := mysqld.SnapshotSourceEnd(*slaveStartRequired, !(*readWrite), true, map[string]string{}) if err != nil { - log.Fatalf("snapshotsourceend failed: %v", err) + return fmt.Errorf("snapshotsourceend failed: %v", err) } + return nil } -func startCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { +func startCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) error { waitTime := subFlags.Duration("wait_time", mysqlctl.MysqlWaitTime, "how long to wait for startup") subFlags.Parse(args) if err := mysqld.Start(*waitTime); err != nil { - log.Fatalf("failed start mysql: %v", err) + return fmt.Errorf("failed start mysql: %v", err) } + return nil } -func teardownCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) { +func teardownCmd(mysqld *mysqlctl.Mysqld, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will remove the root directory even if mysqld shutdown fails") subFlags.Parse(args) if err := mysqld.Teardown(*force); err != nil { - log.Fatalf("failed teardown mysql (forced? %v): %v", *force, err) + return fmt.Errorf("failed teardown mysql (forced? %v): %v", *force, err) } + return nil } type command struct { name string - method func(*mysqlctl.Mysqld, *flag.FlagSet, []string) + method func(*mysqlctl.Mysqld, *flag.FlagSet, []string) error params string help string } @@ -246,14 +162,10 @@ var commands = []command{ command{"restore", restoreCmd, "[-fetch_concurrency=3] [-fetch_retry_count=3] [-dont_wait_for_slave_start] ", "Restores a full snapshot"}, - command{"multirestore", multiRestoreCmd, - "[-force] [-concurrency=3] [-fetch_concurrency=4] [-insert_table_concurrency=4] [-fetch_retry_count=3] [-starts=start1,start2,...] [-ends=end1,end2,...] [-strategy=] [/]...", - "Restores a snapshot form multiple hosts"}, - command{"multisnapshot", multisnapshotCmd, "[-concurrency=8] [-spec='-'] [-tables=''] [-exclude_tables=''] [-skip_slave_restart] [-maximum_file_size=134217728] ", - "Makes a complete snapshot using 'select * into' commands."}, } func main() { + defer exit.Recover() defer logutil.Flush() flag.Usage = func() { @@ -272,19 +184,23 @@ func main() { } fmt.Fprintf(os.Stderr, "\n") } - dbconfigs.RegisterFlags() + + flags := dbconfigs.AppConfig | dbconfigs.DbaConfig | + dbconfigs.FilteredConfig | dbconfigs.ReplConfig + dbconfigs.RegisterFlags(flags) flag.Parse() - tabletAddr = fmt.Sprintf("%v:%v", "localhost", *port) - mycnf := mysqlctl.NewMycnf(uint32(*tabletUid), *mysqlPort) + tabletAddr = netutil.JoinHostPort("localhost", *port) + mycnf := mysqlctl.NewMycnf(uint32(*tabletUID), *mysqlPort) if *mysqlSocket != "" { mycnf.SocketFile = *mysqlSocket } - dbcfgs, err := dbconfigs.Init(mycnf.SocketFile) + dbcfgs, err := dbconfigs.Init(mycnf.SocketFile, flags) if err != nil { - log.Fatalf("%v", err) + log.Errorf("%v", err) + exit.Return(1) } mysqld := mysqlctl.NewMysqld("Dba", mycnf, &dbcfgs.Dba, &dbcfgs.Repl) defer mysqld.Close() @@ -299,9 +215,13 @@ func main() { subFlags.PrintDefaults() } - cmd.method(mysqld, subFlags, flag.Args()[1:]) + if err := cmd.method(mysqld, subFlags, flag.Args()[1:]); err != nil { + log.Error(err) + exit.Return(1) + } return } } - log.Fatalf("invalid action: %v", action) + log.Errorf("invalid action: %v", action) + exit.Return(1) } diff --git a/go/cmd/mysqlctld/mysqlctld.go b/go/cmd/mysqlctld/mysqlctld.go index 2cddbd024d5..ebaa7c7096b 100644 --- a/go/cmd/mysqlctld/mysqlctld.go +++ b/go/cmd/mysqlctld/mysqlctld.go @@ -45,7 +45,9 @@ func main() { defer exit.Recover() defer logutil.Flush() - dbconfigs.RegisterFlags() + flags := dbconfigs.AppConfig | dbconfigs.DbaConfig | + dbconfigs.FilteredConfig | dbconfigs.ReplConfig + dbconfigs.RegisterFlags(flags) flag.Parse() mycnf := mysqlctl.NewMycnf(uint32(*tabletUid), *mysqlPort) @@ -53,7 +55,7 @@ func main() { mycnf.SocketFile = *mysqlSocket } - dbcfgs, err := dbconfigs.Init(mycnf.SocketFile) + dbcfgs, err := dbconfigs.Init(mycnf.SocketFile, flags) if err != nil { log.Errorf("%v", err) exit.Return(255) diff --git a/go/cmd/query_analyzer/query_analyzer.go b/go/cmd/query_analyzer/query_analyzer.go index 2953e4e6fcc..2523d74872e 100644 --- a/go/cmd/query_analyzer/query_analyzer.go +++ b/go/cmd/query_analyzer/query_analyzer.go @@ -14,6 +14,7 @@ import ( "sort" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/vt/sqlparser" ) @@ -31,26 +32,30 @@ var ( queries = make(map[string]int) ) -type Stat struct { +type stat struct { Query string Count int } -type Stats []Stat +type stats []stat -func (a Stats) Len() int { return len(a) } -func (a Stats) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a Stats) Less(i, j int) bool { return a[i].Count > a[j].Count } +func (a stats) Len() int { return len(a) } +func (a stats) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a stats) Less(i, j int) bool { return a[i].Count > a[j].Count } func main() { + defer exit.Recover() flag.Parse() for _, filename := range flag.Args() { fmt.Printf("processing: %s\n", filename) - processFile(filename) + if err := processFile(filename); err != nil { + log.Errorf("processFile error: %v", err) + exit.Return(1) + } } - var stats = make(Stats, 0, 128) + var stats = make(stats, 0, 128) for k, v := range queries { - stats = append(stats, Stat{Query: k, Count: v}) + stats = append(stats, stat{Query: k, Count: v}) } sort.Sort(stats) for _, s := range stats { @@ -58,10 +63,10 @@ func main() { } } -func processFile(filename string) { +func processFile(filename string) error { f, err := os.Open(filename) if err != nil { - log.Fatal(err) + return err } r := bufio.NewReader(f) for { @@ -70,10 +75,11 @@ func processFile(filename string) { if err == io.EOF { break } - log.Fatal(err) + return err } analyze(line) } + return nil } func analyze(line []byte) { @@ -89,12 +95,12 @@ func analyze(line []byte) { return } bindIndex = 0 - buf := sqlparser.NewTrackedBuffer(FormatWithBind) + buf := sqlparser.NewTrackedBuffer(formatWithBind) buf.Myprintf("%v", ast) addQuery(buf.ParsedQuery().Query) } -func FormatWithBind(buf *sqlparser.TrackedBuffer, node sqlparser.SQLNode) { +func formatWithBind(buf *sqlparser.TrackedBuffer, node sqlparser.SQLNode) { switch node := node.(type) { case sqlparser.StrVal, sqlparser.NumVal: buf.WriteArg(fmt.Sprintf(":v%d", bindIndex)) diff --git a/go/cmd/topo2topo/plugin_etcdtopo.go b/go/cmd/topo2topo/plugin_etcdtopo.go new file mode 100644 index 00000000000..1bd833657b2 --- /dev/null +++ b/go/cmd/topo2topo/plugin_etcdtopo.go @@ -0,0 +1,11 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// This plugin imports etcdtopo to register the etcd implementation of TopoServer. + +import ( + _ "github.com/youtube/vitess/go/vt/etcdtopo" +) diff --git a/go/cmd/topo2topo/topo2topo.go b/go/cmd/topo2topo/topo2topo.go index e33c4b4fa02..d379ba1bc81 100644 --- a/go/cmd/topo2topo/topo2topo.go +++ b/go/cmd/topo2topo/topo2topo.go @@ -37,7 +37,8 @@ func main() { } if *fromTopo == "" || *toTopo == "" { - log.Fatalf("Need both from and to topo") + log.Errorf("Need both from and to topo") + exit.Return(1) } fromTS := topo.GetServerByName(*fromTopo) diff --git a/go/cmd/vtclient2/plugin_etcdtopo.go b/go/cmd/vtclient2/plugin_etcdtopo.go new file mode 100644 index 00000000000..1bd833657b2 --- /dev/null +++ b/go/cmd/vtclient2/plugin_etcdtopo.go @@ -0,0 +1,11 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// This plugin imports etcdtopo to register the etcd implementation of TopoServer. + +import ( + _ "github.com/youtube/vitess/go/vt/etcdtopo" +) diff --git a/go/cmd/vtclient2/plugin_zktopo.go b/go/cmd/vtclient2/plugin_zktopo.go new file mode 100644 index 00000000000..77409609bc4 --- /dev/null +++ b/go/cmd/vtclient2/plugin_zktopo.go @@ -0,0 +1,11 @@ +// Copyright 2013, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// Imports and register the Zookeeper TopologyServer + +import ( + _ "github.com/youtube/vitess/go/vt/zktopo" +) diff --git a/go/cmd/vtclient2/vtclient2.go b/go/cmd/vtclient2/vtclient2.go index 564d099702d..66f2654bc3d 100644 --- a/go/cmd/vtclient2/vtclient2.go +++ b/go/cmd/vtclient2/vtclient2.go @@ -14,6 +14,7 @@ import ( log "github.com/golang/glog" "github.com/youtube/vitess/go/db" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/vt/client2" _ "github.com/youtube/vitess/go/vt/client2/tablet" "github.com/youtube/vitess/go/vt/logutil" @@ -93,6 +94,7 @@ func isDml(sql string) bool { } func main() { + defer exit.Recover() defer logutil.Flush() flag.Parse() @@ -100,13 +102,14 @@ func main() { if len(args) == 0 { flag.Usage() - os.Exit(1) + exit.Return(1) } client2.RegisterShardedDrivers() conn, err := db.Open(*driver, *server) if err != nil { - log.Fatalf("client error: %v", err) + log.Errorf("client error: %v", err) + exit.Return(1) } log.Infof("Sending the query...") @@ -116,17 +119,20 @@ func main() { if isDml(args[0]) { t, err := conn.Begin() if err != nil { - log.Fatalf("begin failed: %v", err) + log.Errorf("begin failed: %v", err) + exit.Return(1) } r, err := conn.Exec(args[0], bindvars) if err != nil { - log.Fatalf("exec failed: %v", err) + log.Errorf("exec failed: %v", err) + exit.Return(1) } err = t.Commit() if err != nil { - log.Fatalf("commit failed: %v", err) + log.Errorf("commit failed: %v", err) + exit.Return(1) } n, err := r.RowsAffected() @@ -136,13 +142,15 @@ func main() { // launch the query r, err := conn.Exec(args[0], bindvars) if err != nil { - log.Fatalf("client error: %v", err) + log.Errorf("client error: %v", err) + exit.Return(1) } // get the headers cols := r.Columns() if err != nil { - log.Fatalf("client error: %v", err) + log.Errorf("client error: %v", err) + exit.Return(1) } // print the header @@ -177,7 +185,8 @@ func main() { rowIndex++ } if err := r.Err(); err != nil { - log.Fatalf("Error %v\n", err) + log.Errorf("Error %v\n", err) + exit.Return(1) } log.Infof("Total time: %v / Row count: %v", time.Now().Sub(now), rowIndex) } diff --git a/go/cmd/vtctl/vtctl.go b/go/cmd/vtctl/vtctl.go index 375592e6ce6..eee44456d5c 100644 --- a/go/cmd/vtctl/vtctl.go +++ b/go/cmd/vtctl/vtctl.go @@ -22,11 +22,12 @@ import ( "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtctl" "github.com/youtube/vitess/go/vt/wrangler" + "golang.org/x/net/context" ) var ( waitTime = flag.Duration("wait-time", 24*time.Hour, "time to wait on an action") - lockWaitTimeout = flag.Duration("lock-wait-timeout", 0, "time to wait for a lock before starting an action") + lockWaitTimeout = flag.Duration("lock-wait-timeout", time.Minute, "time to wait for a lock before starting an action") ) func init() { @@ -42,15 +43,13 @@ func init() { } // signal handling, centralized here -func installSignalHandlers() { +func installSignalHandlers(cancel func()) { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) go func() { <-sigChan - // we got a signal, notify our modules: - // - wrangler will interrupt anything waiting on a shard or - // keyspace lock - wrangler.SignalInterrupt() + // we got a signal, cancel the current ctx + cancel() }() } @@ -65,7 +64,6 @@ func main() { exit.Return(1) } action := args[0] - installSignalHandlers() startMsg := fmt.Sprintf("USER=%v SUDO_USER=%v %v", os.Getenv("USER"), os.Getenv("SUDO_USER"), strings.Join(os.Args, " ")) @@ -78,9 +76,12 @@ func main() { topoServer := topo.GetServer() defer topo.CloseServers() - wr := wrangler.New(logutil.NewConsoleLogger(), topoServer, *waitTime, *lockWaitTimeout) + ctx, cancel := context.WithTimeout(context.Background(), *waitTime) + wr := wrangler.New(logutil.NewConsoleLogger(), topoServer, *lockWaitTimeout) + installSignalHandlers(cancel) - err := vtctl.RunCommand(wr, args) + err := vtctl.RunCommand(ctx, wr, args) + cancel() switch err { case vtctl.ErrUnknownCommand: flag.Usage() diff --git a/go/cmd/vtctlclient/main.go b/go/cmd/vtctlclient/main.go index d237f415d5d..65250a7c106 100644 --- a/go/cmd/vtctlclient/main.go +++ b/go/cmd/vtctlclient/main.go @@ -10,6 +10,7 @@ import ( "time" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/vtctl/vtctlclient" ) @@ -24,19 +25,23 @@ var ( ) func main() { + defer exit.Recover() + flag.Parse() // create the client client, err := vtctlclient.New(*server, *dialTimeout) if err != nil { - log.Fatalf("Cannot dial to server %v: %v", *server, err) + log.Errorf("Cannot dial to server %v: %v", *server, err) + exit.Return(1) } defer client.Close() // run the command c, errFunc := client.ExecuteVtctlCommand(flag.Args(), *actionTimeout, *lockWaitTimeout) if err = errFunc(); err != nil { - log.Fatalf("Cannot execute remote command: %v", err) + log.Errorf("Cannot execute remote command: %v", err) + exit.Return(1) } // stream the result @@ -55,6 +60,7 @@ func main() { // then display the overall error if err = errFunc(); err != nil { - log.Fatalf("Remote error: %v", err) + log.Errorf("Remote error: %v", err) + exit.Return(1) } } diff --git a/go/cmd/vtctld/actions.go b/go/cmd/vtctld/actions.go index f2889583fe9..62715c8ab84 100644 --- a/go/cmd/vtctld/actions.go +++ b/go/cmd/vtctld/actions.go @@ -12,6 +12,7 @@ import ( "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/wrangler" + "golang.org/x/net/context" ) var ( @@ -19,6 +20,7 @@ var ( lockTimeout = flag.Duration("lock_timeout", actionnode.DefaultLockTimeout, "lock time for wrangler/topo operations") ) +// ActionResult contains the result of an action. If Error, the aciton failed. type ActionResult struct { Name string Parameters string @@ -35,11 +37,11 @@ func (ar *ActionResult) error(text string) { // some action on a Topology object. It should return a message for // the user or an empty string in case there's nothing interesting to // be communicated. -type actionKeyspaceMethod func(wr *wrangler.Wrangler, keyspace string, r *http.Request) (output string, err error) +type actionKeyspaceMethod func(ctx context.Context, wr *wrangler.Wrangler, keyspace string, r *http.Request) (output string, err error) -type actionShardMethod func(wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (output string, err error) +type actionShardMethod func(ctx context.Context, wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (output string, err error) -type actionTabletMethod func(wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (output string, err error) +type actionTabletMethod func(ctx context.Context, wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (output string, err error) type actionTabletRecord struct { role string @@ -55,6 +57,8 @@ type ActionRepository struct { ts topo.Server } +// NewActionRepository creates and returns a new ActionRepository, +// with no actions. func NewActionRepository(ts topo.Server) *ActionRepository { return &ActionRepository{ keyspaceActions: make(map[string]actionKeyspaceMethod), @@ -64,14 +68,17 @@ func NewActionRepository(ts topo.Server) *ActionRepository { } } +// RegisterKeyspaceAction registers a new action on a keyspace. func (ar *ActionRepository) RegisterKeyspaceAction(name string, method actionKeyspaceMethod) { ar.keyspaceActions[name] = method } +// RegisterShardAction registers a new action on a shard. func (ar *ActionRepository) RegisterShardAction(name string, method actionShardMethod) { ar.shardActions[name] = method } +// RegisterTabletAction registers a new action on a tablet. func (ar *ActionRepository) RegisterTabletAction(name, role string, method actionTabletMethod) { ar.tabletActions[name] = actionTabletRecord{ role: role, @@ -79,6 +86,7 @@ func (ar *ActionRepository) RegisterTabletAction(name, role string, method actio } } +// ApplyKeyspaceAction applies the provided action to the keyspace. func (ar *ActionRepository) ApplyKeyspaceAction(actionName, keyspace string, r *http.Request) *ActionResult { result := &ActionResult{Name: actionName, Parameters: keyspace} @@ -88,8 +96,11 @@ func (ar *ActionRepository) ApplyKeyspaceAction(actionName, keyspace string, r * return result } - wr := wrangler.New(logutil.NewConsoleLogger(), ar.ts, *actionTimeout, *lockTimeout) - output, err := action(wr, keyspace, r) + // FIXME(alainjobart) copy web context info + ctx, cancel := context.WithTimeout(context.TODO(), *actionTimeout) + wr := wrangler.New(logutil.NewConsoleLogger(), ar.ts, *lockTimeout) + output, err := action(ctx, wr, keyspace, r) + cancel() if err != nil { result.error(err.Error()) return result @@ -98,6 +109,7 @@ func (ar *ActionRepository) ApplyKeyspaceAction(actionName, keyspace string, r * return result } +// ApplyShardAction applies the provided action to the shard. func (ar *ActionRepository) ApplyShardAction(actionName, keyspace, shard string, r *http.Request) *ActionResult { // if the shard name contains a '-', we assume it's the // name for a ranged based shard, so we lower case it. @@ -111,8 +123,12 @@ func (ar *ActionRepository) ApplyShardAction(actionName, keyspace, shard string, result.error("Unknown shard action") return result } - wr := wrangler.New(logutil.NewConsoleLogger(), ar.ts, *actionTimeout, *lockTimeout) - output, err := action(wr, keyspace, shard, r) + + // FIXME(alainjobart) copy web context info + ctx, cancel := context.WithTimeout(context.TODO(), *actionTimeout) + wr := wrangler.New(logutil.NewConsoleLogger(), ar.ts, *lockTimeout) + output, err := action(ctx, wr, keyspace, shard, r) + cancel() if err != nil { result.error(err.Error()) return result @@ -121,6 +137,7 @@ func (ar *ActionRepository) ApplyShardAction(actionName, keyspace, shard string, return result } +// ApplyTabletAction applies the provided action to the tablet. func (ar *ActionRepository) ApplyTabletAction(actionName string, tabletAlias topo.TabletAlias, r *http.Request) *ActionResult { result := &ActionResult{Name: actionName, Parameters: tabletAlias.String()} @@ -139,8 +156,11 @@ func (ar *ActionRepository) ApplyTabletAction(actionName string, tabletAlias top } // run the action - wr := wrangler.New(logutil.NewConsoleLogger(), ar.ts, *actionTimeout, *lockTimeout) - output, err := action.method(wr, tabletAlias, r) + // FIXME(alainjobart) copy web context info + ctx, cancel := context.WithTimeout(context.TODO(), *actionTimeout) + wr := wrangler.New(logutil.NewConsoleLogger(), ar.ts, *lockTimeout) + output, err := action.method(ctx, wr, tabletAlias, r) + cancel() if err != nil { result.error(err.Error()) return result @@ -149,8 +169,8 @@ func (ar *ActionRepository) ApplyTabletAction(actionName string, tabletAlias top return result } -// Populate{Keyspace,Shard,Tablet}Actions populates result with -// actions that can be performed on its node. +// PopulateKeyspaceActions populates result with actions that can be +// performed on the keyspace. func (ar ActionRepository) PopulateKeyspaceActions(actions map[string]template.URL, keyspace string) { for name := range ar.keyspaceActions { values := url.Values{} @@ -160,6 +180,8 @@ func (ar ActionRepository) PopulateKeyspaceActions(actions map[string]template.U } } +// PopulateShardActions populates result with actions that can be +// performed on the shard. func (ar ActionRepository) PopulateShardActions(actions map[string]template.URL, keyspace, shard string) { for name := range ar.shardActions { values := url.Values{} @@ -170,6 +192,8 @@ func (ar ActionRepository) PopulateShardActions(actions map[string]template.URL, } } +// PopulateTabletActions populates result with actions that can be +// performed on the tablet. func (ar ActionRepository) PopulateTabletActions(actions map[string]template.URL, tabletAlias string, r *http.Request) { for name, value := range ar.tabletActions { // check we are authorized for the role we need diff --git a/go/cmd/vtctld/explorer.go b/go/cmd/vtctld/explorer.go index d791577bee2..ae28f8e2c15 100644 --- a/go/cmd/vtctld/explorer.go +++ b/go/cmd/vtctld/explorer.go @@ -1,22 +1,24 @@ package main import ( + "errors" + "fmt" "net/http" "path" "strings" + "sync" log "github.com/golang/glog" + "github.com/youtube/vitess/go/cmd/vtctld/proto" "github.com/youtube/vitess/go/vt/topo" ) -var explorers = make(map[string]Explorer) - // Explorer allows exploring a topology server. type Explorer interface { // HandlePath returns a result (suitable to be passed to a // template) appropriate for url, using actionRepo to populate // the actions in result. - HandlePath(actionRepo *ActionRepository, url string, r *http.Request) interface{} + HandlePath(actionRepo proto.ActionRepository, url string, r *http.Request) interface{} // GetKeyspacePath returns an explorer path that will contain // information about the named keyspace. @@ -50,11 +52,29 @@ type Explorer interface { GetReplicationSlaves(cell, keyspace, shard string) string } -// HandleExplorer serves explorer under url, using a template named -// templateName. -func HandleExplorer(name, url, templateName string, explorer Explorer) { - explorers[name] = explorer +var ( + // explorerMutex protects against concurrent registration attempts (e.g. OnRun). + // Other than registration, the explorer should never change. + explorerMutex sync.Mutex + explorerName string + explorer Explorer +) + +// HandleExplorer registers the Explorer under url, using the given template. +// It should be called by a plugin either from init() or from servenv.OnRun(). +// Only one Explorer can be registered in a given instance of vtctld. +func HandleExplorer(name, url, templateName string, exp Explorer) { + explorerMutex.Lock() + defer explorerMutex.Unlock() + + if explorer != nil { + panic(fmt.Sprintf("Only one Explorer can be registered in vtctld. Trying to register %q, but %q was already registered.", name, explorerName)) + } + + explorer = exp + explorerName = name indexContent.ToplevelLinks[name+" explorer"] = url + http.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { httpError(w, "cannot parse form: %s", err) @@ -74,3 +94,56 @@ func HandleExplorer(name, url, templateName string, explorer Explorer) { templateLoader.ServeTemplate(templateName, result, w, r) }) } + +// handleExplorerRedirect returns the redirect target URL. +func handleExplorerRedirect(r *http.Request) (string, error) { + keyspace := r.FormValue("keyspace") + shard := r.FormValue("shard") + cell := r.FormValue("cell") + + switch r.FormValue("type") { + case "keyspace": + if keyspace == "" { + return "", errors.New("keyspace is required for this redirect") + } + return explorer.GetKeyspacePath(keyspace), nil + case "shard": + if keyspace == "" || shard == "" { + return "", errors.New("keyspace and shard are required for this redirect") + } + return explorer.GetShardPath(keyspace, shard), nil + case "srv_keyspace": + if keyspace == "" || cell == "" { + return "", errors.New("keyspace and cell are required for this redirect") + } + return explorer.GetSrvKeyspacePath(cell, keyspace), nil + case "srv_shard": + if keyspace == "" || shard == "" || cell == "" { + return "", errors.New("keyspace, shard, and cell are required for this redirect") + } + return explorer.GetSrvShardPath(cell, keyspace, shard), nil + case "srv_type": + tabletType := r.FormValue("tablet_type") + if keyspace == "" || shard == "" || cell == "" || tabletType == "" { + return "", errors.New("keyspace, shard, cell, and tablet_type are required for this redirect") + } + return explorer.GetSrvTypePath(cell, keyspace, shard, topo.TabletType(tabletType)), nil + case "tablet": + alias := r.FormValue("alias") + if alias == "" { + return "", errors.New("alias is required for this redirect") + } + tabletAlias, err := topo.ParseTabletAliasString(alias) + if err != nil { + return "", fmt.Errorf("bad tablet alias %q: %v", alias, err) + } + return explorer.GetTabletPath(tabletAlias), nil + case "replication": + if keyspace == "" || shard == "" || cell == "" { + return "", errors.New("keyspace, shard, and cell are required for this redirect") + } + return explorer.GetReplicationSlaves(cell, keyspace, shard), nil + default: + return "", errors.New("bad redirect type") + } +} diff --git a/go/cmd/vtctld/plugin_etcdtopo.go b/go/cmd/vtctld/plugin_etcdtopo.go new file mode 100644 index 00000000000..0dfba08ed83 --- /dev/null +++ b/go/cmd/vtctld/plugin_etcdtopo.go @@ -0,0 +1,22 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// This plugin imports etcdtopo to register the etcd implementation of TopoServer. + +import ( + "github.com/youtube/vitess/go/vt/etcdtopo" + "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/topo" +) + +func init() { + // Wait until flags are parsed, so we can check which topo server is in use. + servenv.OnRun(func() { + if etcdServer, ok := topo.GetServer().(*etcdtopo.Server); ok { + HandleExplorer("etcd", "/etcd/", "etcd.html", etcdtopo.NewExplorer(etcdServer)) + } + }) +} diff --git a/go/cmd/vtctld/plugin_zktopo.go b/go/cmd/vtctld/plugin_zktopo.go index 1ecdf1d3761..16c383b80ea 100644 --- a/go/cmd/vtctld/plugin_zktopo.go +++ b/go/cmd/vtctld/plugin_zktopo.go @@ -15,60 +15,70 @@ import ( "sort" "strings" - log "github.com/golang/glog" + "github.com/youtube/vitess/go/cmd/vtctld/proto" + "github.com/youtube/vitess/go/netutil" + "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/zktopo" "github.com/youtube/vitess/go/zk" ) func init() { - // handles /zk paths - ts := topo.GetServerByName("zookeeper") - if ts == nil { - log.Error("zookeeper explorer disabled: no zktopo.Server") - return - } - - HandleExplorer("zk", "/zk/", "zk.html", NewZkExplorer(ts.(*zktopo.Server).GetZConn())) + // Wait until flags are parsed, so we can check which topo server is in use. + servenv.OnRun(func() { + if zkServer, ok := topo.GetServer().(*zktopo.Server); ok { + HandleExplorer("zk", "/zk/", "zk.html", NewZkExplorer(zkServer.GetZConn())) + } + }) } +// ZkExplorer implements Explorer type ZkExplorer struct { zconn zk.Conn } +// NewZkExplorer returns an Explorer implementation for Zookeeper func NewZkExplorer(zconn zk.Conn) *ZkExplorer { return &ZkExplorer{zconn} } +// GetKeyspacePath is part of the Explorer interface func (ex ZkExplorer) GetKeyspacePath(keyspace string) string { return path.Join("/zk/global/vt/keyspaces", keyspace) } +// GetShardPath is part of the Explorer interface func (ex ZkExplorer) GetShardPath(keyspace, shard string) string { return path.Join("/zk/global/vt/keyspaces", keyspace, "shards", shard) } +// GetSrvKeyspacePath is part of the Explorer interface func (ex ZkExplorer) GetSrvKeyspacePath(cell, keyspace string) string { return path.Join("/zk", cell, "vt/ns", keyspace) } +// GetSrvShardPath is part of the Explorer interface func (ex ZkExplorer) GetSrvShardPath(cell, keyspace, shard string) string { return path.Join("/zk", cell, "/vt/ns", keyspace, shard) } +// GetSrvTypePath is part of the Explorer interface func (ex ZkExplorer) GetSrvTypePath(cell, keyspace, shard string, tabletType topo.TabletType) string { return path.Join("/zk", cell, "/vt/ns", keyspace, shard, string(tabletType)) } +// GetTabletPath is part of the Explorer interface func (ex ZkExplorer) GetTabletPath(alias topo.TabletAlias) string { - return path.Join("/zk", alias.Cell, "vt/tablets", alias.TabletUidStr()) + return path.Join("/zk", alias.Cell, "vt/tablets", alias.TabletUIDStr()) } +// GetReplicationSlaves is part of the Explorer interface func (ex ZkExplorer) GetReplicationSlaves(cell, keyspace, shard string) string { return path.Join("/zk", cell, "vt/replication", keyspace, shard) } -func (ex ZkExplorer) HandlePath(actionRepo *ActionRepository, zkPath string, r *http.Request) interface{} { +// HandlePath is part of the Explorer interface +func (ex ZkExplorer) HandlePath(actionRepo proto.ActionRepository, zkPath string, r *http.Request) interface{} { result := NewZkResult(zkPath) if zkPath == "/zk" { @@ -123,14 +133,11 @@ func (ex ZkExplorer) addTabletLinks(data string, result *ZkResult) { } if port, ok := t.Portmap["vt"]; ok { - result.Links["status"] = template.URL(fmt.Sprintf("http://%v:%v/debug/status", t.Hostname, port)) - } - - if !t.Parent.IsZero() { - result.Links["parent"] = template.URL(fmt.Sprintf("/zk/%v/vt/tablets/%v", t.Parent.Cell, t.Parent.TabletUidStr())) + result.Links["status"] = template.URL(fmt.Sprintf("http://%v/debug/status", netutil.JoinHostPort(t.Hostname, port))) } } +// ZkResult is the node for a zk path type ZkResult struct { Path string Data string @@ -140,6 +147,7 @@ type ZkResult struct { Error string } +// NewZkResult creates a new ZkResult for the path with no links nor actions. func NewZkResult(zkPath string) *ZkResult { return &ZkResult{ Links: make(map[string]template.URL), diff --git a/go/cmd/vtctld/proto/actions.go b/go/cmd/vtctld/proto/actions.go new file mode 100644 index 00000000000..09a59748885 --- /dev/null +++ b/go/cmd/vtctld/proto/actions.go @@ -0,0 +1,19 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package proto contains interfaces that other packages may need to interact +// with vtctld, such as to implement plugins. +package proto + +import ( + "html/template" + "net/http" +) + +// ActionRepository is an interface for Explorer plugins to populate actions. +type ActionRepository interface { + PopulateKeyspaceActions(actions map[string]template.URL, keyspace string) + PopulateShardActions(actions map[string]template.URL, keyspace, shard string) + PopulateTabletActions(actions map[string]template.URL, tabletAlias string, r *http.Request) +} diff --git a/go/cmd/vtctld/tablet_data.go b/go/cmd/vtctld/tablet_data.go new file mode 100644 index 00000000000..ed789dbf419 --- /dev/null +++ b/go/cmd/vtctld/tablet_data.go @@ -0,0 +1,120 @@ +package main + +import ( + "encoding/json" + "reflect" + "sync" + + "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" + "github.com/youtube/vitess/go/vt/tabletmanager/tmclient" + "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" +) + +// This file maintains a tablet health cache. It establishes streaming +// connections with tablets, and updates its internal state with the +// result. The first time something is requested, the returned data is +// empty. We assume the frontend will ask again a few seconds later +// and get the up-to-date data then. + +// TabletHealth is the structure we export via json. +// Private fields are not exported. +type TabletHealth struct { + // mu protects the entire data structure + mu sync.Mutex + + Version int + HealthStreamReply actionnode.HealthStreamReply + result []byte + lastError error +} + +func newTabletHealth(thc *tabletHealthCache, tabletAlias topo.TabletAlias) (*TabletHealth, error) { + th := &TabletHealth{ + Version: 1, + } + th.HealthStreamReply.Tablet = &topo.Tablet{ + Alias: tabletAlias, + } + var err error + th.result, err = json.MarshalIndent(th, "", " ") + if err != nil { + return nil, err + } + go th.update(thc, tabletAlias) + return th, nil +} + +func (th *TabletHealth) update(thc *tabletHealthCache, tabletAlias topo.TabletAlias) { + defer thc.delete(tabletAlias) + + ti, err := thc.ts.GetTablet(tabletAlias) + if err != nil { + return + } + + ctx := context.Background() + c, errFunc, err := thc.tmc.HealthStream(ctx, ti) + if err != nil { + return + } + + for hsr := range c { + th.mu.Lock() + if !reflect.DeepEqual(hsr, &th.HealthStreamReply) { + th.HealthStreamReply = *hsr + th.Version++ + th.result, th.lastError = json.MarshalIndent(th, "", " ") + } + th.mu.Unlock() + } + + // we call errFunc as some implementations may use this to + // free resources. + errFunc() +} + +func (th *TabletHealth) get() ([]byte, error) { + th.mu.Lock() + defer th.mu.Unlock() + return th.result, th.lastError +} + +type tabletHealthCache struct { + ts topo.Server + tmc tmclient.TabletManagerClient + + mu sync.Mutex + tabletMap map[topo.TabletAlias]*TabletHealth +} + +func newTabletHealthCache(ts topo.Server, tmc tmclient.TabletManagerClient) *tabletHealthCache { + return &tabletHealthCache{ + ts: ts, + tmc: tmc, + tabletMap: make(map[topo.TabletAlias]*TabletHealth), + } +} + +func (thc *tabletHealthCache) get(tabletAlias topo.TabletAlias) ([]byte, error) { + thc.mu.Lock() + th, ok := thc.tabletMap[tabletAlias] + if !ok { + var err error + th, err = newTabletHealth(thc, tabletAlias) + if err != nil { + thc.mu.Unlock() + return nil, err + } + thc.tabletMap[tabletAlias] = th + } + thc.mu.Unlock() + + return th.get() +} + +func (thc *tabletHealthCache) delete(tabletAlias topo.TabletAlias) { + thc.mu.Lock() + delete(thc.tabletMap, tabletAlias) + thc.mu.Unlock() +} diff --git a/go/cmd/vtctld/tablet_data_test.go b/go/cmd/vtctld/tablet_data_test.go new file mode 100644 index 00000000000..e0048426294 --- /dev/null +++ b/go/cmd/vtctld/tablet_data_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "encoding/json" + "testing" + "time" + + "github.com/youtube/vitess/go/vt/logutil" + "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" + "github.com/youtube/vitess/go/vt/tabletmanager/tmclient" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/wrangler" + "github.com/youtube/vitess/go/vt/wrangler/testlib" + "github.com/youtube/vitess/go/vt/zktopo" +) + +func TestTabletData(t *testing.T) { + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) + wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Second) + + tablet1 := testlib.NewFakeTablet(t, wr, "cell1", 0, topo.TYPE_MASTER, testlib.TabletKeyspaceShard(t, "ks", "-80")) + tablet1.StartActionLoop(t, wr) + defer tablet1.StopActionLoop(t) + + thc := newTabletHealthCache(ts, tmclient.NewTabletManagerClient()) + + // get the first result, it's not containing any data but the alias + result, err := thc.get(tablet1.Tablet.Alias) + if err != nil { + t.Fatalf("thc.get failed: %v", err) + } + var unpacked TabletHealth + if err := json.Unmarshal(result, &unpacked); err != nil { + t.Fatalf("bad json: %v", err) + } + if unpacked.HealthStreamReply.Tablet.Alias != tablet1.Tablet.Alias { + t.Fatalf("wrong alias: %v", &unpacked) + } + if unpacked.Version != 1 { + t.Errorf("wrong version, got %v was expecting 1", unpacked.Version) + } + + // wait for the streaming RPC to be established + timeout := 5 * time.Second + for { + if tablet1.Agent.HealthStreamMapSize() > 0 { + break + } + timeout -= 10 * time.Millisecond + if timeout < 0 { + t.Fatalf("timeout waiting for streaming RPC to be established") + } + time.Sleep(10 * time.Millisecond) + } + + // feed some data from the tablet, with just a data marker + hsr := &actionnode.HealthStreamReply{ + BinlogPlayerMapSize: 42, + } + tablet1.Agent.BroadcastHealthStreamReply(hsr) + + // and wait for the cache to pick it up + timeout = 5 * time.Second + for { + result, err = thc.get(tablet1.Tablet.Alias) + if err != nil { + t.Fatalf("thc.get failed: %v", err) + } + if err := json.Unmarshal(result, &unpacked); err != nil { + t.Fatalf("bad json: %v", err) + } + if unpacked.HealthStreamReply.BinlogPlayerMapSize == 42 { + if unpacked.Version != 2 { + t.Errorf("wrong version, got %v was expecting 2", unpacked.Version) + } + break + } + timeout -= 10 * time.Millisecond + if timeout < 0 { + t.Fatalf("timeout waiting for streaming RPC to be established") + } + time.Sleep(10 * time.Millisecond) + } +} diff --git a/go/cmd/vtctld/template.go b/go/cmd/vtctld/template.go new file mode 100644 index 00000000000..a71e137ce0c --- /dev/null +++ b/go/cmd/vtctld/template.go @@ -0,0 +1,231 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "io" + "net/http" + "path" + "reflect" + "strings" + + "github.com/youtube/vitess/go/vt/topo" +) + +// FHtmlize writes data to w as debug HTML (using definition lists). +func FHtmlize(w io.Writer, data interface{}) { + v := reflect.Indirect(reflect.ValueOf(data)) + typ := v.Type() + switch typ.Kind() { + case reflect.Struct: + fmt.Fprintf(w, "
", typ.Name()) + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if field.PkgPath != "" { + continue + } + fmt.Fprintf(w, "
%v
", field.Name) + fmt.Fprint(w, "
") + FHtmlize(w, v.Field(i).Interface()) + fmt.Fprint(w, "
") + } + fmt.Fprintf(w, "
") + case reflect.Slice: + fmt.Fprint(w, "
    ") + for i := 0; i < v.Len(); i++ { + fmt.Fprint(w, "
  • ") + FHtmlize(w, v.Index(i).Interface()) + fmt.Fprint(w, "
  • ") + } + fmt.Fprint(w, "
") + case reflect.Map: + fmt.Fprintf(w, "
") + for _, k := range v.MapKeys() { + fmt.Fprint(w, "
") + FHtmlize(w, k.Interface()) + fmt.Fprint(w, "
") + fmt.Fprint(w, "
") + FHtmlize(w, v.MapIndex(k).Interface()) + fmt.Fprint(w, "
") + } + fmt.Fprintf(w, "
") + default: + printed := fmt.Sprintf("%v", v.Interface()) + if printed == "" { + printed = " " + } + fmt.Fprint(w, printed) + } +} + +// Htmlize returns a debug HTML representation of data. +func Htmlize(data interface{}) string { + b := new(bytes.Buffer) + FHtmlize(b, data) + return b.String() +} + +func link(text, href string) string { + return fmt.Sprintf("%v", href, text) +} + +func breadCrumbs(fullPath string) template.HTML { + parts := strings.Split(fullPath, "/") + paths := make([]string, len(parts)) + for i, part := range parts { + if i == 0 { + paths[i] = "/" + continue + } + paths[i] = path.Join(paths[i-1], part) + } + b := new(bytes.Buffer) + for i, part := range parts[1 : len(parts)-1] { + fmt.Fprint(b, "/"+link(part, paths[i+1])) + } + fmt.Fprintf(b, "/"+parts[len(parts)-1]) + return template.HTML(b.String()) +} + +var funcMap = template.FuncMap{ + "htmlize": func(o interface{}) template.HTML { + return template.HTML(Htmlize(o)) + }, + "hasprefix": strings.HasPrefix, + "intequal": func(left, right int) bool { + return left == right + }, + "breadcrumbs": breadCrumbs, + "keyspace": func(keyspace string) template.HTML { + if explorer == nil { + return template.HTML(keyspace) + } + return template.HTML(link(keyspace, explorer.GetKeyspacePath(keyspace))) + }, + "srv_keyspace": func(cell, keyspace string) template.HTML { + if explorer == nil { + return template.HTML(keyspace) + } + return template.HTML(link(keyspace, explorer.GetSrvKeyspacePath(cell, keyspace))) + }, + "shard": func(keyspace, shard string) template.HTML { + if explorer == nil { + return template.HTML(shard) + } + return template.HTML(link(shard, explorer.GetShardPath(keyspace, shard))) + }, + "srv_shard": func(cell, keyspace, shard string) template.HTML { + if explorer == nil { + return template.HTML(shard) + } + return template.HTML(link(shard, explorer.GetSrvShardPath(cell, keyspace, shard))) + }, + "tablet": func(alias topo.TabletAlias, shortname string) template.HTML { + if explorer == nil { + return template.HTML(shortname) + } + return template.HTML(link(shortname, explorer.GetTabletPath(alias))) + }, +} + +var dummyTemplate = template.Must(template.New("dummy").Funcs(funcMap).Parse(` + + + + + + + {{ htmlize . }} + + +`)) + +// TemplateLoader is a helper class to load html templates +type TemplateLoader struct { + Directory string + usesDummy bool + template *template.Template +} + +func (loader *TemplateLoader) compile() (*template.Template, error) { + return template.New("main").Funcs(funcMap).ParseGlob(path.Join(loader.Directory, "[a-z]*")) +} + +func (loader *TemplateLoader) makeErrorTemplate(errorMessage string) *template.Template { + return template.Must(template.New("error").Parse(fmt.Sprintf("Error in template: %s", errorMessage))) +} + +// NewTemplateLoader returns a template loader with templates from +// directory. If directory is "", fallbackTemplate will be used +// (regardless of the wanted template name). If debug is true, +// templates will be recompiled each time. +func NewTemplateLoader(directory string, fallbackTemplate *template.Template, debug bool) *TemplateLoader { + loader := &TemplateLoader{Directory: directory} + if directory == "" { + loader.usesDummy = true + loader.template = fallbackTemplate + return loader + } + if !debug { + tmpl, err := loader.compile() + if err != nil { + panic(err) + } + loader.template = tmpl + } + return loader +} + +// Lookup will find a template by name and return it +func (loader *TemplateLoader) Lookup(name string) (*template.Template, error) { + if loader.usesDummy { + return loader.template, nil + } + var err error + source := loader.template + if loader.template == nil { + source, err = loader.compile() + if err != nil { + return nil, err + } + } + tmpl := source.Lookup(name) + if tmpl == nil { + err := fmt.Errorf("template %v not available", name) + return nil, err + } + return tmpl, nil +} + +// ServeTemplate executes the named template passing data into it. If +// the format GET parameter is equal to "json", serves data as JSON +// instead. +func (loader *TemplateLoader) ServeTemplate(templateName string, data interface{}, w http.ResponseWriter, r *http.Request) { + switch r.URL.Query().Get("format") { + case "json": + j, err := json.MarshalIndent(data, "", " ") + if err != nil { + httpError(w, "JSON error%s", err) + return + } + w.Write(j) + default: + tmpl, err := loader.Lookup(templateName) + if err != nil { + httpError(w, "error in template loader: %v", err) + return + } + if err := tmpl.Execute(w, data); err != nil { + httpError(w, "error executing template: %v", err) + } + } +} diff --git a/go/cmd/vtctld/template_test.go b/go/cmd/vtctld/template_test.go new file mode 100644 index 00000000000..6750d1f661b --- /dev/null +++ b/go/cmd/vtctld/template_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "html/template" + "testing" +) + +func TestHtmlizeStruct(t *testing.T) { + type simpleStruct struct { + Start, End int + } + + type funkyStruct struct { + SomeString string + SomeInt int + Embedded simpleStruct + EmptyString string + } + input := funkyStruct{SomeString: "a string", SomeInt: 42, Embedded: simpleStruct{1, 42}} + want := `
SomeString
a string
SomeInt
42
Embedded
Start
1
End
42
EmptyString
 
` + if got := Htmlize(input); got != want { + t.Errorf("Htmlize: got %q, want %q", got, want) + } +} + +func TestHtmlizeMap(t *testing.T) { + // We can't test multiple entries, since Htmlize supports maps whose keys can't be sorted. + input := map[string]string{"dog": "apple"} + want := `
dog
apple
` + if got := Htmlize(input); got != want { + t.Errorf("Htmlize: got %q, want %q", got, want) + } +} + +func TestHtmlizeSlice(t *testing.T) { + input := []string{"aaa", "bbb", "ccc"} + want := `
  • aaa
  • bbb
  • ccc
` + if got := Htmlize(input); got != want { + t.Errorf("Htmlize: got %q, want %q", got, want) + } +} + +func TestBreadCrumbs(t *testing.T) { + input := "/grandparent/parent/node" + want := template.HTML(`/grandparent/parent/node`) + if got := breadCrumbs(input); got != want { + t.Errorf("breadCrumbs(%q) = %q, want %q", input, got, want) + } +} diff --git a/go/cmd/vtctld/templates/etcd.html b/go/cmd/vtctld/templates/etcd.html new file mode 100644 index 00000000000..b440b6a374d --- /dev/null +++ b/go/cmd/vtctld/templates/etcd.html @@ -0,0 +1,45 @@ + + +Etcd Explorer + + + + +{{$path := .Path}} + +{{if .Error}} +

Error

+{{.Error}} +{{else}} +

{{breadcrumbs .Path}}

+{{range $key, $link := .Links}} + [{{$key}}] +{{end}} +

Children

+
    +
  • ..
  • + {{range .Children}} +
  • {{.}}
  • + {{end}} + +
+ +{{with .Data}} +

Data

+
{{.}}
+{{end}} + +{{with .Actions}} +

Actions

+
    + {{range $name, $href := .}} +
  • {{$name}}
  • + {{end}} +
+{{end}} + +{{end}} + + diff --git a/go/cmd/vtctld/templates/vschema.html b/go/cmd/vtctld/templates/vschema.html new file mode 100644 index 00000000000..5939cee073d --- /dev/null +++ b/go/cmd/vtctld/templates/vschema.html @@ -0,0 +1,25 @@ + + +VSchema view + + + + + +{{if .Error}} +

Error

+
{{.Error}}
+{{else}} +

Success

+{{end}} +
{{.Output}}
+ +
+ Upload VSchema

+ +
+ + + diff --git a/go/cmd/vtctld/topo_data.go b/go/cmd/vtctld/topo_data.go new file mode 100644 index 00000000000..be5220ddb88 --- /dev/null +++ b/go/cmd/vtctld/topo_data.go @@ -0,0 +1,373 @@ +package main + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "sync" + "time" + + "github.com/youtube/vitess/go/vt/topo" +) + +// This file includes the support for serving topo data to an ajax-based +// front-end. There are three ways we collect data: +// - reading topology records that don't change too often, caching them +// with a somewhat big TTL, and have a 'flush' command in case it's needed. +// (list of cells, tablets in a cell/shard, ...) +// - subscribing to topology change channels (serving graph) +// - establishing streaming connections to vttablets to get up-to-date +// health reports. + +// VersionedObject is the interface implemented by objects in the versioned +// object cache VersionedObjectCache object. +type VersionedObject interface { + GetVersion() int + SetVersion(int) + Reset() +} + +// BaseVersionedObject is the base implementation for VersionedObject +// that handles the version stuff. It handles GetVersion and SetVersion. +// Inherited types still need to implement Reset(). +type BaseVersionedObject struct { + Version int +} + +// GetVersion is part of the VersionedObject interface +func (bvo *BaseVersionedObject) GetVersion() int { + return bvo.Version +} + +// SetVersion is part of the VersionedObject interface +func (bvo *BaseVersionedObject) SetVersion(version int) { + bvo.Version = version +} + +// GetVersionedObjectFunc is the function the cache will call to get +// the object itself. +type GetVersionedObjectFunc func() (VersionedObject, error) + +// VersionedObjectCache is the main cache object. Just needs a method to get +// the content. +type VersionedObjectCache struct { + getObject GetVersionedObjectFunc + + mu sync.Mutex + timestamp time.Time + versionedObject VersionedObject + result []byte +} + +// NewVersionedObjectCache returns a new VersionedObjectCache object. +func NewVersionedObjectCache(getObject GetVersionedObjectFunc) *VersionedObjectCache { + return &VersionedObjectCache{ + getObject: getObject, + } +} + +// Get returns the versioned value from the cache. +func (voc *VersionedObjectCache) Get() ([]byte, error) { + voc.mu.Lock() + defer voc.mu.Unlock() + + now := time.Now() + if now.Sub(voc.timestamp) < 5*time.Minute { + return voc.result, nil + } + + newObject, err := voc.getObject() + if err != nil { + return nil, err + } + if voc.versionedObject != nil { + // we already have an object, check if it's equal + newObject.SetVersion(voc.versionedObject.GetVersion()) + if reflect.DeepEqual(newObject, voc.versionedObject) { + voc.timestamp = now + return voc.result, nil + } + + // it's not equal increment the version + newObject.SetVersion(voc.versionedObject.GetVersion() + 1) + } else { + newObject.SetVersion(1) + } + + // we have a new object, update the version, and save it + voc.result, err = json.MarshalIndent(newObject, "", " ") + if err != nil { + return nil, err + } + voc.versionedObject = newObject + voc.timestamp = now + return voc.result, nil +} + +// Flush will flush the cache, and force a version increment, so +// clients will reload. +func (voc *VersionedObjectCache) Flush() { + voc.mu.Lock() + defer voc.mu.Unlock() + + // we reset timestamp and content, so the Version will increase again + // and force a client refresh, even if the data is the same. + voc.timestamp = time.Time{} + if voc.versionedObject != nil { + voc.versionedObject.Reset() + } +} + +// VersionedObjectCacheFactory knows how to construct a VersionedObjectCache +// from the key +type VersionedObjectCacheFactory func(key string) *VersionedObjectCache + +// VersionedObjectCacheMap is a map of VersionedObjectCache protected +// by a mutex. +type VersionedObjectCacheMap struct { + factory VersionedObjectCacheFactory + + mu sync.Mutex + cacheMap map[string]*VersionedObjectCache +} + +// NewVersionedObjectCacheMap returns a new VersionedObjectCacheFactory +// that uses the factory method to create individual VersionedObjectCache. +func NewVersionedObjectCacheMap(factory VersionedObjectCacheFactory) *VersionedObjectCacheMap { + return &VersionedObjectCacheMap{ + factory: factory, + cacheMap: make(map[string]*VersionedObjectCache), + } +} + +// Get finds the right VersionedObjectCache and returns its value +func (vocm *VersionedObjectCacheMap) Get(key string) ([]byte, error) { + vocm.mu.Lock() + voc, ok := vocm.cacheMap[key] + if !ok { + voc = vocm.factory(key) + vocm.cacheMap[key] = voc + } + vocm.mu.Unlock() + + return voc.Get() +} + +// Flush will flush the entire cache +func (vocm *VersionedObjectCacheMap) Flush() { + vocm.mu.Lock() + defer vocm.mu.Unlock() + for _, voc := range vocm.cacheMap { + voc.Flush() + } +} + +// KnownCells contains the cached result of topo.Server.GetKnownCells +type KnownCells struct { + BaseVersionedObject + + // Cells is the list of Known Cells for this topology + Cells []string +} + +// Reset is part of the VersionedObject interface +func (kc *KnownCells) Reset() { + kc.Cells = nil +} + +func newKnownCellsCache(ts topo.Server) *VersionedObjectCache { + return NewVersionedObjectCache(func() (VersionedObject, error) { + cells, err := ts.GetKnownCells() + if err != nil { + return nil, err + } + return &KnownCells{ + Cells: cells, + }, nil + }) +} + +// Keyspaces contains the cached result of topo.Server.GetKeyspaces +type Keyspaces struct { + BaseVersionedObject + + // Keyspaces is the list of Keyspaces for this topology + Keyspaces []string +} + +// Reset is part of the VersionedObject interface +func (k *Keyspaces) Reset() { + k.Keyspaces = nil +} + +func newKeyspacesCache(ts topo.Server) *VersionedObjectCache { + return NewVersionedObjectCache(func() (VersionedObject, error) { + keyspaces, err := ts.GetKeyspaces() + if err != nil { + return nil, err + } + return &Keyspaces{ + Keyspaces: keyspaces, + }, nil + }) +} + +// Keyspace contains the cached results of topo.Server.GetKeyspace +type Keyspace struct { + BaseVersionedObject + + // KeyspaceName is the name of this keyspace + KeyspaceName string + + // Keyspace is the topo value of this keyspace + Keyspace *topo.Keyspace +} + +// Reset is part of the VersionedObject interface +func (k *Keyspace) Reset() { + k.KeyspaceName = "" + k.Keyspace = nil +} + +func newKeyspaceCache(ts topo.Server) *VersionedObjectCacheMap { + return NewVersionedObjectCacheMap(func(key string) *VersionedObjectCache { + return NewVersionedObjectCache(func() (VersionedObject, error) { + k, err := ts.GetKeyspace(key) + if err != nil { + return nil, err + } + return &Keyspace{ + KeyspaceName: k.KeyspaceName(), + Keyspace: k.Keyspace, + }, nil + }) + }) +} + +// ShardNames contains the cached results of topo.Server.GetShardNames +type ShardNames struct { + BaseVersionedObject + + // KeyspaceName is the name of the keyspace this result applies to + KeyspaceName string + + // ShardNames is the list of shard names for this keyspace + ShardNames []string +} + +// Reset is part of the VersionedObject interface +func (s *ShardNames) Reset() { + s.KeyspaceName = "" + s.ShardNames = nil +} + +func newShardNamesCache(ts topo.Server) *VersionedObjectCacheMap { + return NewVersionedObjectCacheMap(func(key string) *VersionedObjectCache { + return NewVersionedObjectCache(func() (VersionedObject, error) { + sn, err := ts.GetShardNames(key) + if err != nil { + return nil, err + } + return &ShardNames{ + KeyspaceName: key, + ShardNames: sn, + }, nil + }) + }) +} + +// Shard contains the cached results of topo.Server.GetShard +// the map key is keyspace/shard +type Shard struct { + BaseVersionedObject + + // KeyspaceName is the keyspace for this shard + KeyspaceName string + + // ShardName is the name for this shard + ShardName string + + // Shard is the topo value of this shard + Shard *topo.Shard +} + +// Reset is part of the VersionedObject interface +func (s *Shard) Reset() { + s.KeyspaceName = "" + s.ShardName = "" + s.Shard = nil +} + +func newShardCache(ts topo.Server) *VersionedObjectCacheMap { + return NewVersionedObjectCacheMap(func(key string) *VersionedObjectCache { + return NewVersionedObjectCache(func() (VersionedObject, error) { + + keyspace, shard, err := topo.ParseKeyspaceShardString(key) + if err != nil { + return nil, err + } + s, err := ts.GetShard(keyspace, shard) + if err != nil { + return nil, err + } + return &Shard{ + KeyspaceName: s.Keyspace(), + ShardName: s.ShardName(), + Shard: s.Shard, + }, nil + }) + }) +} + +// CellShardTablets contains the cached results of the list of tablets +// in a cell / keyspace / shard. +// The map key is cell/keyspace/shard. +type CellShardTablets struct { + BaseVersionedObject + + // Cell is the name of the cell + Cell string + + // KeyspaceName is the keyspace for this shard + KeyspaceName string + + // ShardName is the name for this shard + ShardName string + + // TabletAliases is the array os tablet aliases + TabletAliases []topo.TabletAlias +} + +// Reset is part of the VersionedObject interface +func (cst *CellShardTablets) Reset() { + cst.Cell = "" + cst.KeyspaceName = "" + cst.ShardName = "" + cst.TabletAliases = nil +} + +func newCellShardTabletsCache(ts topo.Server) *VersionedObjectCacheMap { + return NewVersionedObjectCacheMap(func(key string) *VersionedObjectCache { + return NewVersionedObjectCache(func() (VersionedObject, error) { + parts := strings.Split(key, "/") + if len(parts) != 3 { + return nil, fmt.Errorf("Invalid shard tablets path: %v", key) + } + sr, err := ts.GetShardReplication(parts[0], parts[1], parts[2]) + if err != nil { + return nil, err + } + result := &CellShardTablets{ + Cell: parts[0], + KeyspaceName: parts[1], + ShardName: parts[2], + TabletAliases: make([]topo.TabletAlias, len(sr.ReplicationLinks)), + } + for i, rl := range sr.ReplicationLinks { + result.TabletAliases[i] = rl.TabletAlias + } + return result, nil + }) + }) +} diff --git a/go/cmd/vtctld/topo_data_test.go b/go/cmd/vtctld/topo_data_test.go new file mode 100644 index 00000000000..8e34c8d4a6b --- /dev/null +++ b/go/cmd/vtctld/topo_data_test.go @@ -0,0 +1,328 @@ +package main + +import ( + "encoding/json" + "reflect" + "testing" + "time" + + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/zktopo" +) + +func testVersionedObjectCache(t *testing.T, voc *VersionedObjectCache, vo VersionedObject, expectedVO VersionedObject) { + result, err := voc.Get() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := json.Unmarshal(result, vo); err != nil { + t.Fatalf("bad json: %v", err) + } + if vo.GetVersion() != 1 { + t.Fatalf("Got wrong initial version: %v", vo.GetVersion()) + } + expectedVO.SetVersion(1) + if !reflect.DeepEqual(vo, expectedVO) { + t.Fatalf("Got bad result: %#v expected: %#v", vo, expectedVO) + } + + result2, err := voc.Get() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(result, result2) { + t.Fatalf("Bad content from cache: %v != %v", string(result), string(result2)) + } + + // force a re-get with same content, version shouldn't change + voc.timestamp = time.Time{} + result2, err = voc.Get() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(result, result2) { + t.Fatalf("Bad content from cache: %v != %v", string(result), string(result2)) + } + + // force a reget with different content, version should change + voc.timestamp = time.Time{} + voc.versionedObject.Reset() // poking inside the object here + result, err = voc.Get() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := json.Unmarshal(result, vo); err != nil { + t.Fatalf("bad json: %v", err) + } + if vo.GetVersion() != 2 { + t.Fatalf("Got wrong second version: %v", vo.GetVersion()) + } + expectedVO.SetVersion(2) + if !reflect.DeepEqual(vo, expectedVO) { + t.Fatalf("Got bad result: %#v expected: %#v", vo, expectedVO) + } + + // force a flush and see the version increase again + voc.Flush() + result, err = voc.Get() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := json.Unmarshal(result, vo); err != nil { + t.Fatalf("bad json: %v", err) + } + if vo.GetVersion() != 3 { + t.Fatalf("Got wrong third version: %v", vo.GetVersion()) + } + expectedVO.SetVersion(3) + if !reflect.DeepEqual(vo, expectedVO) { + t.Fatalf("Got bad result: %#v expected: %#v", vo, expectedVO) + } +} + +func testVersionedObjectCacheMap(t *testing.T, vocm *VersionedObjectCacheMap, key string, vo VersionedObject, expectedVO VersionedObject) { + result, err := vocm.Get(key) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := json.Unmarshal(result, vo); err != nil { + t.Fatalf("bad json: %v", err) + } + if vo.GetVersion() != 1 { + t.Fatalf("Got wrong initial version: %v", vo.GetVersion()) + } + expectedVO.SetVersion(1) + if !reflect.DeepEqual(vo, expectedVO) { + t.Fatalf("Got bad result: %#v expected: %#v", vo, expectedVO) + } + + result2, err := vocm.Get(key) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(result, result2) { + t.Fatalf("Bad content from cache: %v != %v", string(result), string(result2)) + } + + // force a re-get with same content, version shouldn't change + vocm.cacheMap[key].timestamp = time.Time{} + result2, err = vocm.Get(key) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(result, result2) { + t.Fatalf("Bad content from cache: %v != %v", string(result), string(result2)) + } + + // force a reget with different content, version should change + vocm.cacheMap[key].timestamp = time.Time{} + vocm.cacheMap[key].versionedObject.Reset() // poking inside the object here + result, err = vocm.Get(key) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := json.Unmarshal(result, vo); err != nil { + t.Fatalf("bad json: %v", err) + } + if vo.GetVersion() != 2 { + t.Fatalf("Got wrong second version: %v", vo.GetVersion()) + } + expectedVO.SetVersion(2) + if !reflect.DeepEqual(vo, expectedVO) { + t.Fatalf("Got bad result: %#v expected: %#v", vo, expectedVO) + } + + // force a flush and see the version increase again + vocm.Flush() + result, err = vocm.Get(key) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := json.Unmarshal(result, vo); err != nil { + t.Fatalf("bad json: %v", err) + } + if vo.GetVersion() != 3 { + t.Fatalf("Got wrong third version: %v", vo.GetVersion()) + } + expectedVO.SetVersion(3) + if !reflect.DeepEqual(vo, expectedVO) { + t.Fatalf("Got bad result: %#v expected: %#v", vo, expectedVO) + } +} + +func TestKnownCellsCache(t *testing.T) { + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) + kcc := newKnownCellsCache(ts) + var kc KnownCells + expectedKc := KnownCells{ + Cells: []string{"cell1", "cell2"}, + } + + testVersionedObjectCache(t, kcc, &kc, &expectedKc) +} + +func TestKeyspacesCache(t *testing.T) { + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) + if err := ts.CreateKeyspace("ks1", &topo.Keyspace{}); err != nil { + t.Fatalf("CreateKeyspace failed: %v", err) + } + if err := ts.CreateKeyspace("ks2", &topo.Keyspace{}); err != nil { + t.Fatalf("CreateKeyspace failed: %v", err) + } + kc := newKeyspacesCache(ts) + var k Keyspaces + expectedK := Keyspaces{ + Keyspaces: []string{"ks1", "ks2"}, + } + + testVersionedObjectCache(t, kc, &k, &expectedK) +} + +func TestKeyspaceCache(t *testing.T) { + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) + if err := ts.CreateKeyspace("ks1", &topo.Keyspace{ + ShardingColumnName: "sharding_key", + }); err != nil { + t.Fatalf("CreateKeyspace failed: %v", err) + } + if err := ts.CreateKeyspace("ks2", &topo.Keyspace{ + SplitShardCount: 10, + }); err != nil { + t.Fatalf("CreateKeyspace failed: %v", err) + } + kc := newKeyspaceCache(ts) + var k Keyspace + + expectedK := Keyspace{ + KeyspaceName: "ks1", + Keyspace: &topo.Keyspace{ + ShardingColumnName: "sharding_key", + }, + } + testVersionedObjectCacheMap(t, kc, "ks1", &k, &expectedK) + + expectedK = Keyspace{ + KeyspaceName: "ks2", + Keyspace: &topo.Keyspace{ + SplitShardCount: 10, + }, + } + testVersionedObjectCacheMap(t, kc, "ks2", &k, &expectedK) +} + +func TestShardNamesCache(t *testing.T) { + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) + if err := ts.CreateKeyspace("ks1", &topo.Keyspace{ + ShardingColumnName: "sharding_key", + }); err != nil { + t.Fatalf("CreateKeyspace failed: %v", err) + } + if err := ts.CreateShard("ks1", "s1", &topo.Shard{ + Cells: []string{"cell1", "cell2"}, + }); err != nil { + t.Fatalf("CreateShard failed: %v", err) + } + if err := ts.CreateShard("ks1", "s2", &topo.Shard{ + MasterAlias: topo.TabletAlias{ + Cell: "cell1", + Uid: 12, + }, + }); err != nil { + t.Fatalf("CreateShard failed: %v", err) + } + snc := newShardNamesCache(ts) + var sn ShardNames + + expectedSN := ShardNames{ + KeyspaceName: "ks1", + ShardNames: []string{"s1", "s2"}, + } + testVersionedObjectCacheMap(t, snc, "ks1", &sn, &expectedSN) +} + +func TestShardCache(t *testing.T) { + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) + if err := ts.CreateKeyspace("ks1", &topo.Keyspace{ + ShardingColumnName: "sharding_key", + }); err != nil { + t.Fatalf("CreateKeyspace failed: %v", err) + } + if err := ts.CreateShard("ks1", "s1", &topo.Shard{ + Cells: []string{"cell1", "cell2"}, + }); err != nil { + t.Fatalf("CreateShard failed: %v", err) + } + if err := ts.CreateShard("ks1", "s2", &topo.Shard{ + MasterAlias: topo.TabletAlias{ + Cell: "cell1", + Uid: 12, + }, + }); err != nil { + t.Fatalf("CreateShard failed: %v", err) + } + sc := newShardCache(ts) + var s Shard + + expectedS := Shard{ + KeyspaceName: "ks1", + ShardName: "s1", + Shard: &topo.Shard{ + Cells: []string{"cell1", "cell2"}, + }, + } + testVersionedObjectCacheMap(t, sc, "ks1/s1", &s, &expectedS) + + expectedS = Shard{ + KeyspaceName: "ks1", + ShardName: "s2", + Shard: &topo.Shard{ + MasterAlias: topo.TabletAlias{ + Cell: "cell1", + Uid: 12, + }, + }, + } + testVersionedObjectCacheMap(t, sc, "ks1/s2", &s, &expectedS) +} + +func TestCellShardTabletsCache(t *testing.T) { + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) + if err := ts.UpdateShardReplicationFields("cell1", "ks1", "s1", func(sr *topo.ShardReplication) error { + sr.ReplicationLinks = []topo.ReplicationLink{ + topo.ReplicationLink{ + TabletAlias: topo.TabletAlias{ + Cell: "cell1", + Uid: 12, + }, + }, + topo.ReplicationLink{ + TabletAlias: topo.TabletAlias{ + Cell: "cell1", + Uid: 13, + }, + }, + } + return nil + }); err != nil { + t.Fatalf("UpdateShardReplicationFields failed: %v", err) + } + cstc := newCellShardTabletsCache(ts) + var cst CellShardTablets + + expectedCST := CellShardTablets{ + Cell: "cell1", + KeyspaceName: "ks1", + ShardName: "s1", + TabletAliases: []topo.TabletAlias{ + topo.TabletAlias{ + Cell: "cell1", + Uid: 12, + }, + topo.TabletAlias{ + Cell: "cell1", + Uid: 13, + }, + }, + } + testVersionedObjectCacheMap(t, cstc, "cell1/ks1/s1", &cst, &expectedCST) +} diff --git a/go/cmd/vtctld/vtctld.go b/go/cmd/vtctld/vtctld.go index fa82f8301e0..6465eb7f992 100644 --- a/go/cmd/vtctld/vtctld.go +++ b/go/cmd/vtctld/vtctld.go @@ -1,32 +1,26 @@ package main import ( - "bytes" - "encoding/json" "flag" "fmt" - "html/template" - "io" "net/http" - "path" - "reflect" "strings" - "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/acl" - "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/tabletmanager/tmclient" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" "github.com/youtube/vitess/go/vt/wrangler" ) var ( - templateDir = flag.String("templates", "", "directory containing templates") - debug = flag.Bool("debug", false, "recompile templates for every request") + templateDir = flag.String("templates", "", "directory containing templates") + debug = flag.Bool("debug", false, "recompile templates for every request") + schemaEditorDir = flag.String("schema-editor-dir", "", "directory containing schema_editor/") ) func init() { @@ -34,298 +28,18 @@ func init() { servenv.InitServiceMapForBsonRpcService("vtctl") } -// FHtmlize writes data to w as debug HTML (using definition lists). -func FHtmlize(w io.Writer, data interface{}) { - v := reflect.Indirect(reflect.ValueOf(data)) - typ := v.Type() - switch typ.Kind() { - case reflect.Struct: - fmt.Fprintf(w, "
", typ.Name()) - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - if field.PkgPath != "" { - continue - } - fmt.Fprintf(w, "
%v
", field.Name) - fmt.Fprint(w, "
") - FHtmlize(w, v.Field(i).Interface()) - fmt.Fprint(w, "
") - } - fmt.Fprintf(w, "
") - case reflect.Slice: - fmt.Fprint(w, "
    ") - for i := 0; i < v.Len(); i++ { - FHtmlize(w, v.Index(i).Interface()) - } - fmt.Fprint(w, "
") - case reflect.Map: - fmt.Fprintf(w, "
") - for _, k := range v.MapKeys() { - fmt.Fprint(w, "
") - FHtmlize(w, k.Interface()) - fmt.Fprint(w, "
") - fmt.Fprint(w, "
") - FHtmlize(w, v.MapIndex(k).Interface()) - fmt.Fprint(w, "
") - - } - fmt.Fprintf(w, "
") - case reflect.String: - quoted := fmt.Sprintf("%q", v.Interface()) - printed := quoted[1 : len(quoted)-1] - if printed == "" { - printed = " " - } - fmt.Fprintf(w, "%s", printed) - default: - printed := fmt.Sprintf("%v", v.Interface()) - if printed == "" { - printed = " " - } - fmt.Fprint(w, printed) - } -} - -// Htmlize returns a debug HTML representation of data. -func Htmlize(data interface{}) string { - b := new(bytes.Buffer) - FHtmlize(b, data) - return b.String() -} - -func link(text, href string) string { - return fmt.Sprintf("%v", href, text) -} - -// Plugins need to overwrite: -// keyspace(keyspace) -// shard(keyspace, shard) -// if they want to create links on these guys -var funcMap = template.FuncMap{ - "htmlize": func(o interface{}) template.HTML { - return template.HTML(Htmlize(o)) - }, - "hasprefix": strings.HasPrefix, - "intequal": func(left, right int) bool { - return left == right - }, - "breadcrumbs": func(fullPath string) template.HTML { - parts := strings.Split(fullPath, "/") - paths := make([]string, len(parts)) - for i, part := range parts { - if i == 0 { - paths[i] = "/" - continue - } - paths[i] = path.Join(paths[i-1], part) - } - b := new(bytes.Buffer) - for i, part := range parts[1 : len(parts)-1] { - fmt.Fprintf(b, "/%v", paths[i+1], part) - } - fmt.Fprintf(b, "/"+parts[len(parts)-1]) - return template.HTML(b.String()) - }, - "keyspace": func(keyspace string) template.HTML { - switch len(explorers) { - case 0: - return template.HTML(keyspace) - case 1: - for _, explorer := range explorers { - return template.HTML(link(keyspace, explorer.GetKeyspacePath(keyspace))) - } - default: - b := new(bytes.Buffer) - fmt.Fprintf(b, "%v", keyspace) - for name, explorer := range explorers { - fmt.Fprintf(b, " [%v]", link(name, explorer.GetKeyspacePath(keyspace))) - } - return template.HTML(b.String()) - } - panic("unreachable") - }, - "srv_keyspace": func(cell, keyspace string) template.HTML { - switch len(explorers) { - case 0: - return template.HTML(keyspace) - case 1: - for _, explorer := range explorers { - return template.HTML(link(keyspace, explorer.GetSrvKeyspacePath(cell, keyspace))) - } - default: - b := new(bytes.Buffer) - fmt.Fprintf(b, "%v", keyspace) - for name, explorer := range explorers { - fmt.Fprintf(b, " [%v]", link(name, explorer.GetSrvKeyspacePath(cell, keyspace))) - } - return template.HTML(b.String()) - } - panic("unreachable") - }, - "shard": func(keyspace, shard string) template.HTML { - switch len(explorers) { - case 0: - return template.HTML(shard) - case 1: - for _, explorer := range explorers { - return template.HTML(link(shard, explorer.GetShardPath(keyspace, shard))) - } - default: - b := new(bytes.Buffer) - fmt.Fprintf(b, "%v", shard) - for name, explorer := range explorers { - fmt.Fprintf(b, ` [%v]`, link(name, explorer.GetShardPath(keyspace, shard))) - } - return template.HTML(b.String()) - } - panic("unreachable") - }, - "srv_shard": func(cell, keyspace, shard string) template.HTML { - switch len(explorers) { - case 0: - return template.HTML(shard) - case 1: - for _, explorer := range explorers { - return template.HTML(link(shard, explorer.GetSrvShardPath(cell, keyspace, shard))) - } - default: - b := new(bytes.Buffer) - fmt.Fprintf(b, "%v", shard) - for name, explorer := range explorers { - fmt.Fprintf(b, ` [%v]`, link(name, explorer.GetSrvShardPath(cell, keyspace, shard))) - } - return template.HTML(b.String()) - } - panic("unreachable") - }, - "tablet": func(alias topo.TabletAlias, shortname string) template.HTML { - switch len(explorers) { - case 0: - return template.HTML(shortname) - case 1: - for _, explorer := range explorers { - return template.HTML(link(shortname, explorer.GetTabletPath(alias))) - } - default: - b := new(bytes.Buffer) - fmt.Fprintf(b, "%v", shortname) - for name, explorer := range explorers { - fmt.Fprintf(b, ` [%v]`, link(name, explorer.GetTabletPath(alias))) - } - return template.HTML(b.String()) - } - panic("unreachable") - }, -} - -var dummyTemplate = template.Must(template.New("dummy").Funcs(funcMap).Parse(` - - - - - - - {{ htmlize . }} - - -`)) - -type TemplateLoader struct { - Directory string - usesDummy bool - template *template.Template -} - -func (loader *TemplateLoader) compile() (*template.Template, error) { - return template.New("main").Funcs(funcMap).ParseGlob(path.Join(loader.Directory, "[a-z]*")) -} - -func (loader *TemplateLoader) makeErrorTemplate(errorMessage string) *template.Template { - return template.Must(template.New("error").Parse(fmt.Sprintf("Error in template: %s", errorMessage))) -} - -// NewTemplateLoader returns a template loader with templates from -// directory. If directory is "", fallbackTemplate will be used -// (regardless of the wanted template name). If debug is true, -// templates will be recompiled each time. -func NewTemplateLoader(directory string, fallbackTemplate *template.Template, debug bool) *TemplateLoader { - loader := &TemplateLoader{Directory: directory} - if directory == "" { - loader.usesDummy = true - loader.template = fallbackTemplate - return loader - } - if !debug { - tmpl, err := loader.compile() - if err != nil { - panic(err) - } - loader.template = tmpl - } - return loader -} - -func (loader *TemplateLoader) Lookup(name string) (*template.Template, error) { - if loader.usesDummy { - return loader.template, nil - } - var err error - source := loader.template - if loader.template == nil { - source, err = loader.compile() - if err != nil { - return nil, err - } - } - tmpl := source.Lookup(name) - if tmpl == nil { - err := fmt.Errorf("template %v not available", name) - return nil, err - } - return tmpl, nil -} - -// ServeTemplate executes the named template passing data into it. If -// the format GET parameter is equal to "json", serves data as JSON -// instead. -func (tl *TemplateLoader) ServeTemplate(templateName string, data interface{}, w http.ResponseWriter, r *http.Request) { - switch r.URL.Query().Get("format") { - case "json": - j, err := json.MarshalIndent(data, "", " ") - if err != nil { - httpError(w, "JSON error%s", err) - return - } - w.Write(j) - default: - tmpl, err := tl.Lookup(templateName) - if err != nil { - httpError(w, "error in template loader: %v", err) - return - } - if err := tmpl.Execute(w, data); err != nil { - httpError(w, "error executing template: %v", err) - } - } -} - func httpError(w http.ResponseWriter, format string, err error) { log.Errorf(format, err) http.Error(w, fmt.Sprintf(format, err), http.StatusInternalServerError) } +// DbTopologyResult encapsulates a topotools.Topology and the possible error type DbTopologyResult struct { Topology *topotools.Topology Error string } +// IndexContent has the list of toplevel links type IndexContent struct { // maps a name to a linked URL ToplevelLinks map[string]string @@ -338,6 +52,8 @@ var indexContent = IndexContent{ ToplevelLinks: map[string]string{ "DbTopology Tool": "/dbtopo", "Serving Graph": "/serving_graph", + "Schema editor": "/schema_editor", + "Schema view": "/vschema", }, } var ts topo.Server @@ -351,64 +67,62 @@ func main() { ts = topo.GetServer() defer topo.CloseServers() - wr := wrangler.New(logutil.NewConsoleLogger(), ts, 30*time.Second, 30*time.Second) - actionRepo = NewActionRepository(ts) // keyspace actions actionRepo.RegisterKeyspaceAction("ValidateKeyspace", - func(wr *wrangler.Wrangler, keyspace string, r *http.Request) (string, error) { - return "", wr.ValidateKeyspace(keyspace, false) + func(ctx context.Context, wr *wrangler.Wrangler, keyspace string, r *http.Request) (string, error) { + return "", wr.ValidateKeyspace(ctx, keyspace, false) }) actionRepo.RegisterKeyspaceAction("ValidateSchemaKeyspace", - func(wr *wrangler.Wrangler, keyspace string, r *http.Request) (string, error) { - return "", wr.ValidateSchemaKeyspace(keyspace, nil, false) + func(ctx context.Context, wr *wrangler.Wrangler, keyspace string, r *http.Request) (string, error) { + return "", wr.ValidateSchemaKeyspace(ctx, keyspace, nil, false) }) actionRepo.RegisterKeyspaceAction("ValidateVersionKeyspace", - func(wr *wrangler.Wrangler, keyspace string, r *http.Request) (string, error) { - return "", wr.ValidateVersionKeyspace(keyspace) + func(ctx context.Context, wr *wrangler.Wrangler, keyspace string, r *http.Request) (string, error) { + return "", wr.ValidateVersionKeyspace(ctx, keyspace) }) actionRepo.RegisterKeyspaceAction("ValidatePermissionsKeyspace", - func(wr *wrangler.Wrangler, keyspace string, r *http.Request) (string, error) { - return "", wr.ValidatePermissionsKeyspace(keyspace) + func(ctx context.Context, wr *wrangler.Wrangler, keyspace string, r *http.Request) (string, error) { + return "", wr.ValidatePermissionsKeyspace(ctx, keyspace) }) // shard actions actionRepo.RegisterShardAction("ValidateShard", - func(wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (string, error) { - return "", wr.ValidateShard(keyspace, shard, false) + func(ctx context.Context, wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (string, error) { + return "", wr.ValidateShard(ctx, keyspace, shard, false) }) actionRepo.RegisterShardAction("ValidateSchemaShard", - func(wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (string, error) { - return "", wr.ValidateSchemaShard(keyspace, shard, nil, false) + func(ctx context.Context, wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (string, error) { + return "", wr.ValidateSchemaShard(ctx, keyspace, shard, nil, false) }) actionRepo.RegisterShardAction("ValidateVersionShard", - func(wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (string, error) { - return "", wr.ValidateVersionShard(keyspace, shard) + func(ctx context.Context, wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (string, error) { + return "", wr.ValidateVersionShard(ctx, keyspace, shard) }) actionRepo.RegisterShardAction("ValidatePermissionsShard", - func(wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (string, error) { - return "", wr.ValidatePermissionsShard(keyspace, shard) + func(ctx context.Context, wr *wrangler.Wrangler, keyspace, shard string, r *http.Request) (string, error) { + return "", wr.ValidatePermissionsShard(ctx, keyspace, shard) }) // tablet actions actionRepo.RegisterTabletAction("Ping", "", - func(wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) { + func(ctx context.Context, wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) { ti, err := wr.TopoServer().GetTablet(tabletAlias) if err != nil { return "", err } - return "", wr.TabletManagerClient().Ping(context.TODO(), ti, 10*time.Second) + return "", wr.TabletManagerClient().Ping(ctx, ti) }) actionRepo.RegisterTabletAction("ScrapTablet", acl.ADMIN, - func(wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) { + func(ctx context.Context, wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) { // refuse to scrap tablets that are not spare ti, err := wr.TopoServer().GetTablet(tabletAlias) if err != nil { @@ -417,11 +131,11 @@ func main() { if ti.Type != topo.TYPE_SPARE { return "", fmt.Errorf("Can only scrap spare tablets") } - return "", wr.Scrap(tabletAlias, false, false) + return "", wr.Scrap(ctx, tabletAlias, false, false) }) actionRepo.RegisterTabletAction("ScrapTabletForce", acl.ADMIN, - func(wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) { + func(ctx context.Context, wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) { // refuse to scrap tablets that are not spare ti, err := wr.TopoServer().GetTablet(tabletAlias) if err != nil { @@ -430,11 +144,11 @@ func main() { if ti.Type != topo.TYPE_SPARE { return "", fmt.Errorf("Can only scrap spare tablets") } - return "", wr.Scrap(tabletAlias, true, false) + return "", wr.Scrap(ctx, tabletAlias, true, false) }) actionRepo.RegisterTabletAction("DeleteTablet", acl.ADMIN, - func(wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) { + func(ctx context.Context, wr *wrangler.Wrangler, tabletAlias topo.TabletAlias, r *http.Request) (string, error) { return "", wr.DeleteTablet(tabletAlias) }) @@ -526,7 +240,7 @@ func main() { return } result := DbTopologyResult{} - topology, err := topotools.DbTopology(wr.TopoServer()) + topology, err := topotools.DbTopology(context.TODO(), ts) if err != nil { result.Error = err.Error() } else { @@ -545,150 +259,220 @@ func main() { if err != nil { httpError(w, "cannot get known cells: %v", err) return - } else { - templateLoader.ServeTemplate("serving_graph_cells.html", cells, w, r) } + templateLoader.ServeTemplate("serving_graph_cells.html", cells, w, r) return } - servingGraph := topotools.DbServingGraph(wr.TopoServer(), cell) + servingGraph := topotools.DbServingGraph(ts, cell) templateLoader.ServeTemplate("serving_graph.html", servingGraph, w, r) }) + // vschema editor + http.HandleFunc("/schema_editor/", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, *schemaEditorDir+r.URL.Path) + }) + + // vschema viewer + http.HandleFunc("/vschema", func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + httpError(w, "cannot parse form: %s", err) + return + } + schemafier, ok := ts.(topo.Schemafier) + if !ok { + httpError(w, "%s", fmt.Errorf("%T doesn's support schemafier API", ts)) + } + var data struct { + Error error + Input, Output string + } + switch r.Method { + case "POST": + data.Input = r.FormValue("vschema") + data.Error = schemafier.SaveVSchema(data.Input) + } + vschema, err := schemafier.GetVSchema() + if err != nil { + if data.Error == nil { + data.Error = fmt.Errorf("Error fetching schema: %s", err) + } + } + data.Output = vschema + templateLoader.ServeTemplate("vschema.html", data, w, r) + }) + // redirects for explorers http.HandleFunc("/explorers/redirect", func(w http.ResponseWriter, r *http.Request) { + if explorer == nil { + http.Error(w, "no explorer configured", http.StatusInternalServerError) + return + } if err := r.ParseForm(); err != nil { httpError(w, "cannot parse form: %s", err) return } - var explorer Explorer - switch len(explorers) { - case 0: - http.Error(w, "no explorer configured", http.StatusInternalServerError) + target, err := handleExplorerRedirect(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return - case 1: - for _, ex := range explorers { - explorer = ex - } - default: - explorerName := r.FormValue("explorer") - var ok bool - explorer, ok = explorers[explorerName] - if !ok { - http.Error(w, "bad explorer name", http.StatusBadRequest) - return - } } - var target string - switch r.FormValue("type") { - case "keyspace": - keyspace := r.FormValue("keyspace") - if keyspace == "" { - http.Error(w, "keyspace is obligatory for this redirect", http.StatusBadRequest) - return - } - target = explorer.GetKeyspacePath(keyspace) + http.Redirect(w, r, target, http.StatusFound) + }) - case "shard": - keyspace, shard := r.FormValue("keyspace"), r.FormValue("shard") - if keyspace == "" || shard == "" { - http.Error(w, "keyspace and shard are obligatory for this redirect", http.StatusBadRequest) - return - } - target = explorer.GetShardPath(keyspace, shard) + // serve some data + knownCellsCache := newKnownCellsCache(ts) + http.HandleFunc("/json/KnownCells", func(w http.ResponseWriter, r *http.Request) { + result, err := knownCellsCache.Get() + if err != nil { + httpError(w, "error getting known cells: %v", err) + return + } + w.Write(result) + }) - case "srv_keyspace": - cell := r.FormValue("cell") - if cell == "" { - http.Error(w, "cell is obligatory for this redirect", http.StatusBadRequest) - return - } - keyspace := r.FormValue("keyspace") - if keyspace == "" { - http.Error(w, "keyspace is obligatory for this redirect", http.StatusBadRequest) - return - } - target = explorer.GetSrvKeyspacePath(cell, keyspace) + keyspacesCache := newKeyspacesCache(ts) + http.HandleFunc("/json/Keyspaces", func(w http.ResponseWriter, r *http.Request) { + result, err := keyspacesCache.Get() + if err != nil { + httpError(w, "error getting keyspaces: %v", err) + return + } + w.Write(result) + }) - case "srv_shard": - cell := r.FormValue("cell") - if cell == "" { - http.Error(w, "cell is obligatory for this redirect", http.StatusBadRequest) - return - } - keyspace := r.FormValue("keyspace") - if keyspace == "" { - http.Error(w, "keyspace is obligatory for this redirect", http.StatusBadRequest) - return - } - shard := r.FormValue("shard") - if shard == "" { - http.Error(w, "shard is obligatory for this redirect", http.StatusBadRequest) - return - } - target = explorer.GetSrvShardPath(cell, keyspace, shard) + keyspaceCache := newKeyspaceCache(ts) + http.HandleFunc("/json/Keyspace", func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + httpError(w, "cannot parse form: %s", err) + return + } + keyspace := r.FormValue("keyspace") + if keyspace == "" { + http.Error(w, "no keyspace provided", http.StatusBadRequest) + return + } + result, err := keyspaceCache.Get(keyspace) + if err != nil { + httpError(w, "error getting keyspace: %v", err) + return + } + w.Write(result) + }) - case "srv_type": - cell := r.FormValue("cell") - if cell == "" { - http.Error(w, "cell is obligatory for this redirect", http.StatusBadRequest) - return - } - keyspace := r.FormValue("keyspace") - if keyspace == "" { - http.Error(w, "keyspace is obligatory for this redirect", http.StatusBadRequest) - return - } - shard := r.FormValue("shard") - if shard == "" { - http.Error(w, "shard is obligatory for this redirect", http.StatusBadRequest) - return - } - tabletType := r.FormValue("tablet_type") - if tabletType == "" { - http.Error(w, "tablet_type is obligatory for this redirect", http.StatusBadRequest) - return - } - target = explorer.GetSrvTypePath(cell, keyspace, shard, topo.TabletType(tabletType)) + shardNamesCache := newShardNamesCache(ts) + http.HandleFunc("/json/ShardNames", func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + httpError(w, "cannot parse form: %s", err) + return + } + keyspace := r.FormValue("keyspace") + if keyspace == "" { + http.Error(w, "no keyspace provided", http.StatusBadRequest) + return + } + result, err := shardNamesCache.Get(keyspace) + if err != nil { + httpError(w, "error getting shardNames: %v", err) + return + } + w.Write(result) + }) - case "tablet": - aliasName := r.FormValue("alias") - if aliasName == "" { - http.Error(w, "keyspace is obligatory for this redirect", http.StatusBadRequest) - return - } - alias, err := topo.ParseTabletAliasString(aliasName) - if err != nil { - http.Error(w, "bad tablet alias", http.StatusBadRequest) - return - } - target = explorer.GetTabletPath(alias) + shardCache := newShardCache(ts) + http.HandleFunc("/json/Shard", func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + httpError(w, "cannot parse form: %s", err) + return + } + keyspace := r.FormValue("keyspace") + if keyspace == "" { + http.Error(w, "no keyspace provided", http.StatusBadRequest) + return + } + shard := r.FormValue("shard") + if shard == "" { + http.Error(w, "no shard provided", http.StatusBadRequest) + return + } + result, err := shardCache.Get(keyspace + "/" + shard) + if err != nil { + httpError(w, "error getting shard: %v", err) + return + } + w.Write(result) + }) - case "replication": - cell := r.FormValue("cell") - if cell == "" { - http.Error(w, "cell is obligatory for this redirect", http.StatusBadRequest) - return - } - keyspace := r.FormValue("keyspace") - if keyspace == "" { - http.Error(w, "keyspace is obligatory for this redirect", http.StatusBadRequest) - return - } - shard := r.FormValue("shard") - if shard == "" { - http.Error(w, "shard is obligatory for this redirect", http.StatusBadRequest) - return - } - target = explorer.GetReplicationSlaves(cell, keyspace, shard) + cellShardTabletsCache := newCellShardTabletsCache(ts) + http.HandleFunc("/json/CellShardTablets", func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + httpError(w, "cannot parse form: %s", err) + return + } + cell := r.FormValue("cell") + if cell == "" { + http.Error(w, "no cell provided", http.StatusBadRequest) + return + } + keyspace := r.FormValue("keyspace") + if keyspace == "" { + http.Error(w, "no keyspace provided", http.StatusBadRequest) + return + } + shard := r.FormValue("shard") + if shard == "" { + http.Error(w, "no shard provided", http.StatusBadRequest) + return + } + result, err := cellShardTabletsCache.Get(cell + "/" + keyspace + "/" + shard) + if err != nil { + httpError(w, "error getting shard: %v", err) + return + } + w.Write(result) + }) + + // flush all data and will force a full client reload + http.HandleFunc("/json/flush", func(w http.ResponseWriter, r *http.Request) { + knownCellsCache.Flush() + keyspacesCache.Flush() + keyspaceCache.Flush() + shardNamesCache.Flush() + shardCache.Flush() + cellShardTabletsCache.Flush() + }) - default: - http.Error(w, "bad redirect type", http.StatusBadRequest) + // handle tablet cache + tabletHealthCache := newTabletHealthCache(ts, tmclient.NewTabletManagerClient()) + http.HandleFunc("/json/TabletHealth", func(w http.ResponseWriter, r *http.Request) { + cell := r.FormValue("cell") + if cell == "" { + http.Error(w, "no cell provided", http.StatusBadRequest) return } - http.Redirect(w, r, target, http.StatusFound) + uid := r.FormValue("uid") + if uid == "" { + http.Error(w, "no uid provided", http.StatusBadRequest) + return + } + tabletAlias := topo.TabletAlias{ + Cell: cell, + } + var err error + tabletAlias.Uid, err = topo.ParseUID(uid) + if err != nil { + http.Error(w, "cannot parse uid", http.StatusBadRequest) + return + } + result, err := tabletHealthCache.get(tabletAlias) + if err != nil { + httpError(w, "error getting tablet health: %v", err) + return + } + w.Write(result) }) + servenv.RunDefault() } diff --git a/go/cmd/vtctld/vtctld_test.go b/go/cmd/vtctld/vtctld_test.go deleted file mode 100644 index a31130f95d8..00000000000 --- a/go/cmd/vtctld/vtctld_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "testing" -) - -type simpleStruct struct { - Start, End int -} - -type funkyStruct struct { - SomeString string - SomeInt int - Embedded simpleStruct - EmptyString string -} - -func TestHtmlize(t *testing.T) { - o := funkyStruct{SomeString: "a string", SomeInt: 42, Embedded: simpleStruct{1, 42}} - - expected := `
SomeString
a string
SomeInt
42
Embedded
Start
1
End
42
EmptyString
 
` - if html := Htmlize(o); html != expected { - t.Errorf("Wrong html: got %q, expected %q", html, expected) - } -} diff --git a/go/cmd/vtgate/plugin_etcdtopo.go b/go/cmd/vtgate/plugin_etcdtopo.go new file mode 100644 index 00000000000..1bd833657b2 --- /dev/null +++ b/go/cmd/vtgate/plugin_etcdtopo.go @@ -0,0 +1,11 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// This plugin imports etcdtopo to register the etcd implementation of TopoServer. + +import ( + _ "github.com/youtube/vitess/go/vt/etcdtopo" +) diff --git a/go/cmd/vtgate/plugin_zkocctopo.go b/go/cmd/vtgate/plugin_zkocctopo.go deleted file mode 100644 index b0a3c6c2676..00000000000 --- a/go/cmd/vtgate/plugin_zkocctopo.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -// Imports and register the Zookeeper TopologyServer with Zkocc Connection - -import ( - "github.com/youtube/vitess/go/vt/topo" - "github.com/youtube/vitess/go/vt/zktopo" - "github.com/youtube/vitess/go/zk" -) - -func init() { - zkoccconn := zk.NewMetaConn(true) - topo.RegisterServer("zkocc", zktopo.NewServer(zkoccconn)) -} diff --git a/go/cmd/vtgate/status.go b/go/cmd/vtgate/status.go index 630e9c5f12f..851d4cf04da 100644 --- a/go/cmd/vtgate/status.go +++ b/go/cmd/vtgate/status.go @@ -186,6 +186,9 @@ google.setOnLoadCallback(function() { ` ) +// For use by plugins which wish to avoid racing when registering status page parts. +var onStatusRegistered func() + func init() { servenv.OnRun(func() { servenv.AddStatusPart("Topology Cache", topoTemplate, func() interface{} { @@ -194,5 +197,8 @@ func init() { servenv.AddStatusPart("Stats", statsTemplate, func() interface{} { return nil }) + if onStatusRegistered != nil { + onStatusRegistered() + } }) } diff --git a/go/cmd/vtgate/toporeader.go b/go/cmd/vtgate/toporeader.go index 7b4fd5fab32..f61a87fe971 100644 --- a/go/cmd/vtgate/toporeader.go +++ b/go/cmd/vtgate/toporeader.go @@ -1,13 +1,14 @@ package main import ( - "code.google.com/p/go.net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/stats" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtgate" + "golang.org/x/net/context" ) +// TopoReader implements topo.TopoReader. type TopoReader struct { // the server to get data from ts vtgate.SrvTopoServer @@ -17,6 +18,7 @@ type TopoReader struct { errorCount *stats.Counters } +// NewTopoReader creates a new TopoReader. func NewTopoReader(ts vtgate.SrvTopoServer) *TopoReader { return &TopoReader{ ts: ts, @@ -25,6 +27,7 @@ func NewTopoReader(ts vtgate.SrvTopoServer) *TopoReader { } } +// GetSrvKeyspaceNames returns the names of all keyspaces for the cell. func (tr *TopoReader) GetSrvKeyspaceNames(ctx context.Context, req *topo.GetSrvKeyspaceNamesArgs, reply *topo.SrvKeyspaceNames) error { tr.queryCount.Add(req.Cell, 1) var err error @@ -37,6 +40,8 @@ func (tr *TopoReader) GetSrvKeyspaceNames(ctx context.Context, req *topo.GetSrvK return nil } +// GetSrvKeyspace returns information about a keyspace +// in a particular cell. func (tr *TopoReader) GetSrvKeyspace(ctx context.Context, req *topo.GetSrvKeyspaceArgs, reply *topo.SrvKeyspace) (err error) { tr.queryCount.Add(req.Cell, 1) keyspace, err := tr.ts.GetSrvKeyspace(ctx, req.Cell, req.Keyspace) @@ -49,6 +54,22 @@ func (tr *TopoReader) GetSrvKeyspace(ctx context.Context, req *topo.GetSrvKeyspa return nil } +// GetSrvShard returns information about a shard for a keyspace +// in a particular cell. +func (tr *TopoReader) GetSrvShard(ctx context.Context, req *topo.GetSrvShardArgs, reply *topo.SrvShard) (err error) { + tr.queryCount.Add(req.Cell, 1) + shard, err := tr.ts.GetSrvShard(ctx, req.Cell, req.Keyspace, req.Shard) + if err != nil { + log.Warningf("GetSrvShard(%v,%v,%v) failed: %v", req.Cell, req.Keyspace, req.Shard, err) + tr.errorCount.Add(req.Cell, 1) + return err + } + *reply = *shard + return nil +} + +// GetEndPoints returns addresses for a tablet type in a shard +// in a keyspace. func (tr *TopoReader) GetEndPoints(ctx context.Context, req *topo.GetEndPointsArgs, reply *topo.EndPoints) (err error) { tr.queryCount.Add(req.Cell, 1) addrs, err := tr.ts.GetEndPoints(ctx, req.Cell, req.Keyspace, req.Shard, req.TabletType) diff --git a/go/cmd/vtgate/vtgate.go b/go/cmd/vtgate/vtgate.go index 842292bf2c1..6a119e4b31a 100644 --- a/go/cmd/vtgate/vtgate.go +++ b/go/cmd/vtgate/vtgate.go @@ -8,13 +8,17 @@ import ( "flag" "time" + log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtgate" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" ) var ( cell = flag.String("cell", "test_nj", "cell to use") + schemaFile = flag.String("vschema_file", "", "JSON schema file") retryDelay = flag.Duration("retry-delay", 200*time.Millisecond, "retry delay") retryCount = flag.Int("retry-count", 10, "retry count") timeout = flag.Duration("timeout", 5*time.Second, "connection and call timeout") @@ -33,12 +37,43 @@ func init() { } func main() { + defer exit.Recover() + flag.Parse() servenv.Init() ts := topo.GetServer() defer topo.CloseServers() + var schema *planbuilder.Schema + log.Info(*cell, *schemaFile) + if *schemaFile != "" { + var err error + if schema, err = planbuilder.LoadFile(*schemaFile); err != nil { + log.Error(err) + exit.Return(1) + } + log.Infof("v3 is enabled: loaded schema from file: %v", *schemaFile) + } else { + schemafier, ok := ts.(topo.Schemafier) + if !ok { + log.Infof("Skipping v3 initialization: topo does not suppurt schemafier interface") + goto startServer + } + schemaJSON, err := schemafier.GetVSchema() + if err != nil { + log.Warningf("Skipping v3 initialization: GetVSchema failed: %v", err) + goto startServer + } + schema, err = planbuilder.NewSchema([]byte(schemaJSON)) + if err != nil { + log.Warningf("Skipping v3 initialization: GetVSchema failed: %v", err) + goto startServer + } + log.Infof("v3 is enabled: loaded schema from topo") + } + +startServer: resilientSrvTopoServer = vtgate.NewResilientSrvTopoServer(ts, "ResilientSrvTopoServer") // For the initial phase vtgate is exposing @@ -47,6 +82,6 @@ func main() { topoReader = NewTopoReader(resilientSrvTopoServer) servenv.Register("toporeader", topoReader) - vtgate.Init(resilientSrvTopoServer, *cell, *retryDelay, *retryCount, *timeout, *maxInFlight) + vtgate.Init(resilientSrvTopoServer, schema, *cell, *retryDelay, *retryCount, *timeout, *maxInFlight) servenv.RunDefault() } diff --git a/go/cmd/vtjanitor/vtjanitor.go b/go/cmd/vtjanitor/vtjanitor.go index 7bd86b07e62..3e08ab82520 100644 --- a/go/cmd/vtjanitor/vtjanitor.go +++ b/go/cmd/vtjanitor/vtjanitor.go @@ -7,6 +7,7 @@ import ( "time" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/flagutil" "github.com/youtube/vitess/go/vt/janitor" "github.com/youtube/vitess/go/vt/logutil" @@ -19,7 +20,6 @@ import ( var ( sleepTime = flag.Duration("sleep_time", 3*time.Minute, "how long to sleep between janitor runs") lockTimeout = flag.Duration("lock_timeout", actionnode.DefaultLockTimeout, "lock time for wrangler/topo operations") - actionTimeout = flag.Duration("action_timeout", wrangler.DefaultActionTimeout, "time to wait for an action before resorting to force") keyspace = flag.String("keyspace", "", "keyspace to manage") shard = flag.String("shard", "", "shard to manage") dryRunModules flagutil.StringListValue @@ -45,18 +45,22 @@ func init() { } func main() { + defer exit.Recover() + flag.Parse() servenv.Init() ts := topo.GetServer() - scheduler, err := janitor.New(*keyspace, *shard, ts, wrangler.New(logutil.NewConsoleLogger(), ts, *actionTimeout, *lockTimeout), *sleepTime) + scheduler, err := janitor.New(*keyspace, *shard, ts, wrangler.New(logutil.NewConsoleLogger(), ts, *lockTimeout), *sleepTime) if err != nil { - log.Fatalf("janitor.New: %v", err) + log.Errorf("janitor.New: %v", err) + exit.Return(1) } if len(activeModules)+len(dryRunModules) < 1 { - log.Fatal("no modules to run specified") + log.Error("no modules to run specified") + exit.Return(1) } scheduler.Enable(activeModules) diff --git a/go/cmd/vtocc/plugin_filecustomrule.go b/go/cmd/vtocc/plugin_filecustomrule.go new file mode 100644 index 00000000000..84c68d85560 --- /dev/null +++ b/go/cmd/vtocc/plugin_filecustomrule.go @@ -0,0 +1,11 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// Imports and register the file custom rule source + +import ( + _ "github.com/youtube/vitess/go/vt/tabletserver/customrule/filecustomrule" +) diff --git a/go/cmd/vtocc/status.go b/go/cmd/vtocc/status.go index 3bdb3d19773..6c196417d03 100644 --- a/go/cmd/vtocc/status.go +++ b/go/cmd/vtocc/status.go @@ -5,8 +5,14 @@ import ( "github.com/youtube/vitess/go/vt/tabletserver" ) +// For use by plugins which wish to avoid racing when registering status page parts. +var onStatusRegistered func() + func init() { servenv.OnRun(func() { tabletserver.AddStatusPart() + if onStatusRegistered != nil { + onStatusRegistered() + } }) } diff --git a/go/cmd/vtocc/vtocc.go b/go/cmd/vtocc/vtocc.go index ec752efc046..c93ee2cf182 100644 --- a/go/cmd/vtocc/vtocc.go +++ b/go/cmd/vtocc/vtocc.go @@ -7,9 +7,12 @@ package main import ( "encoding/json" "flag" + "fmt" "io/ioutil" + "time" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/vt/dbconfigs" "github.com/youtube/vitess/go/vt/mysqlctl" "github.com/youtube/vitess/go/vt/servenv" @@ -33,17 +36,23 @@ func init() { } func main() { - dbconfigs.RegisterFlags() + defer exit.Recover() + + flags := dbconfigs.AppConfig | dbconfigs.DbaConfig | + dbconfigs.FilteredConfig | dbconfigs.ReplConfig + dbconfigs.RegisterFlags(flags) flag.Parse() if len(flag.Args()) > 0 { flag.Usage() - log.Fatalf("vtocc doesn't take any positional arguments") + log.Errorf("vtocc doesn't take any positional arguments") + exit.Return(1) } servenv.Init() - dbConfigs, err := dbconfigs.Init("") + dbConfigs, err := dbconfigs.Init("", flags) if err != nil { - log.Fatalf("Cannot initialize App dbconfig: %v", err) + log.Errorf("Cannot initialize App dbconfig: %v", err) + exit.Return(1) } if *enableRowcache { dbConfigs.App.EnableRowcache = true @@ -54,7 +63,10 @@ func main() { mycnf := &mysqlctl.Mycnf{BinLogPath: *binlogPath} mysqld := mysqlctl.NewMysqld("Dba", mycnf, &dbConfigs.Dba, &dbConfigs.Repl) - unmarshalFile(*overridesFile, &schemaOverrides) + if err := unmarshalFile(*overridesFile, &schemaOverrides); err != nil { + log.Error(err) + exit.Return(1) + } data, _ := json.MarshalIndent(schemaOverrides, "", " ") log.Infof("schemaOverrides: %s\n", data) @@ -63,10 +75,15 @@ func main() { } tabletserver.InitQueryService() - err = tabletserver.AllowQueries(&dbConfigs.App, schemaOverrides, tabletserver.LoadCustomRules(), mysqld, true) - if err != nil { - return - } + // Query service can go into NOT_SERVING state if mysql goes down. + // So, continuously retry starting the service. So, it tries to come + // back up if it went down. + go func() { + for { + _ = tabletserver.AllowQueries(dbConfigs, schemaOverrides, mysqld) + time.Sleep(30 * time.Second) + } + }() log.Infof("starting vtocc %v", *servenv.Port) servenv.OnTerm(func() { @@ -76,14 +93,15 @@ func main() { servenv.RunDefault() } -func unmarshalFile(name string, val interface{}) { +func unmarshalFile(name string, val interface{}) error { if name != "" { data, err := ioutil.ReadFile(name) if err != nil { - log.Fatalf("could not read %v: %v", val, err) + return fmt.Errorf("unmarshalFile: could not read %v: %v", val, err) } if err = json.Unmarshal(data, val); err != nil { - log.Fatalf("could not read %s: %v", val, err) + return fmt.Errorf("unmarshalFile: could not read %s: %v", val, err) } } + return nil } diff --git a/go/cmd/vtprimecache/main.go b/go/cmd/vtprimecache/main.go index 3e15e8709f0..37634ce50ed 100644 --- a/go/cmd/vtprimecache/main.go +++ b/go/cmd/vtprimecache/main.go @@ -10,6 +10,7 @@ import ( "time" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/vt/dbconfigs" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/primecache" @@ -23,14 +24,17 @@ var ( ) func main() { + defer exit.Recover() defer logutil.Flush() - dbconfigs.RegisterFlags() + flags := dbconfigs.AppConfig | dbconfigs.DbaConfig + dbconfigs.RegisterFlags(flags) flag.Parse() - dbcfgs, err := dbconfigs.Init(*mysqlSocketFile) + dbcfgs, err := dbconfigs.Init(*mysqlSocketFile, flags) if err != nil { - log.Fatalf("Failed to init dbconfigs: %v", err) + log.Errorf("Failed to init dbconfigs: %v", err) + exit.Return(1) } pc := primecache.NewPrimeCache(dbcfgs, *relayLogsPath) diff --git a/go/cmd/vttablet/health.go b/go/cmd/vttablet/health.go index 5bf97b9393e..4da06680a8d 100644 --- a/go/cmd/vttablet/health.go +++ b/go/cmd/vttablet/health.go @@ -2,20 +2,48 @@ package main import ( "flag" + "fmt" + "html/template" + "time" "github.com/youtube/vitess/go/vt/health" "github.com/youtube/vitess/go/vt/mysqlctl" "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/tabletserver" + "github.com/youtube/vitess/go/vt/topo" ) var ( - allowedReplicationLag = flag.Int("allowed_replication_lag", 0, "how many seconds of replication lag will make this tablet unhealthy (ignored if the value is 0)") + enableReplicationLagCheck = flag.Bool("enable_replication_lag_check", false, "will register the mysql health check module that directly calls mysql") ) +// queryServiceRunning implements health.Reporter +type queryServiceRunning struct{} + +// Report is part of the health.Reporter interface +func (qsr *queryServiceRunning) Report(tabletType topo.TabletType, shouldQueryServiceBeRunning bool) (time.Duration, error) { + isQueryServiceRunning := tabletserver.SqlQueryRpcService.GetState() == "SERVING" + if shouldQueryServiceBeRunning != isQueryServiceRunning { + return 0, fmt.Errorf("QueryService running=%v, expected=%v", isQueryServiceRunning, shouldQueryServiceBeRunning) + } + if isQueryServiceRunning { + if err := tabletserver.IsHealthy(); err != nil { + return 0, fmt.Errorf("QueryService is running, but not healthy: %v", err) + } + } + return 0, nil +} + +// HTMLName is part of the health.Reporter interface +func (qsr *queryServiceRunning) HTMLName() template.HTML { + return template.HTML("QueryServiceRunning") +} + func init() { servenv.OnRun(func() { - if *allowedReplicationLag > 0 { - health.Register("replication_reporter", mysqlctl.MySQLReplicationLag(agent.Mysqld, *allowedReplicationLag)) + if *enableReplicationLagCheck { + health.Register("replication_reporter", mysqlctl.MySQLReplicationLag(agent.Mysqld)) } + health.Register("query_service_reporter", &queryServiceRunning{}) }) } diff --git a/go/cmd/vttablet/healthz.go b/go/cmd/vttablet/healthz.go new file mode 100644 index 00000000000..99986f68a44 --- /dev/null +++ b/go/cmd/vttablet/healthz.go @@ -0,0 +1,30 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package main + +import ( + "fmt" + "net/http" + + "github.com/youtube/vitess/go/vt/servenv" +) + +// This file registers a /healthz URL that reports the health of the agent. + +var okMessage = []byte("ok\n") + +func init() { + servenv.OnRun(func() { + http.Handle("/healthz", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if _, err := agent.Healthy(); err != nil { + http.Error(rw, fmt.Sprintf("500 internal server error: agent not healthy: %v", err), http.StatusInternalServerError) + return + } + + rw.Header().Set("Content-Length", fmt.Sprintf("%v", len(okMessage))) + rw.WriteHeader(http.StatusOK) + rw.Write(okMessage) + })) + }) +} diff --git a/go/cmd/vttablet/plugin_etcdtopo.go b/go/cmd/vttablet/plugin_etcdtopo.go new file mode 100644 index 00000000000..1bd833657b2 --- /dev/null +++ b/go/cmd/vttablet/plugin_etcdtopo.go @@ -0,0 +1,11 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// This plugin imports etcdtopo to register the etcd implementation of TopoServer. + +import ( + _ "github.com/youtube/vitess/go/vt/etcdtopo" +) diff --git a/go/cmd/vttablet/plugin_filecustomrule.go b/go/cmd/vttablet/plugin_filecustomrule.go new file mode 100644 index 00000000000..84c68d85560 --- /dev/null +++ b/go/cmd/vttablet/plugin_filecustomrule.go @@ -0,0 +1,11 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// Imports and register the file custom rule source + +import ( + _ "github.com/youtube/vitess/go/vt/tabletserver/customrule/filecustomrule" +) diff --git a/go/cmd/vttablet/plugin_zkcustomrule.go b/go/cmd/vttablet/plugin_zkcustomrule.go new file mode 100644 index 00000000000..f36a2092342 --- /dev/null +++ b/go/cmd/vttablet/plugin_zkcustomrule.go @@ -0,0 +1,11 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// Imports and register the zookeeper custom rule source + +import ( + _ "github.com/youtube/vitess/go/vt/tabletserver/customrule/zkcustomrule" +) diff --git a/go/cmd/vttablet/status.go b/go/cmd/vttablet/status.go index 2040fdc71a1..f3ddb74786d 100644 --- a/go/cmd/vttablet/status.go +++ b/go/cmd/vttablet/status.go @@ -63,7 +63,8 @@ var ( Current Transaction Log
- Health Check
+ Health Check
+ Query Service Health Check
Memcache
Current Stream Queries
@@ -73,7 +74,7 @@ var ( // healthTemplate is just about the tablet health healthTemplate = ` -
Current status: {{.Current}}
+
Current status: {{.CurrentHTML}}

Polling health information from {{github_com_youtube_vitess_health_html_name}}.

History

@@ -84,14 +85,7 @@ var ( {{range .Records}} - {{end}}
{{.Time.Format "Jan 2, 2006 at 15:04:05 (MST)"}} - {{ if eq "unhealthy" .Class}} - unhealthy: {{.Error}} - {{else if eq "unhappy" .Class}} - unhappy (reasons: {{range $key, $value := .Result}}{{$key}}: {{$value}} {{end}}) - {{else}} - healthy - {{end}} + {{.HTML}}
@@ -154,13 +148,20 @@ type healthStatus struct { Records []interface{} } -func (hs *healthStatus) Current() string { +func (hs *healthStatus) CurrentClass() string { if len(hs.Records) > 0 { return hs.Records[0].(*tabletmanager.HealthRecord).Class() } return "unknown" } +func (hs *healthStatus) CurrentHTML() template.HTML { + if len(hs.Records) > 0 { + return hs.Records[0].(*tabletmanager.HealthRecord).HTML() + } + return template.HTML("unknown") +} + func healthHTMLName() template.HTML { return health.HTMLName() } diff --git a/go/cmd/vttablet/vttablet.go b/go/cmd/vttablet/vttablet.go index 48c85b56a14..4fc591f76c6 100644 --- a/go/cmd/vttablet/vttablet.go +++ b/go/cmd/vttablet/vttablet.go @@ -7,9 +7,9 @@ package main import ( "flag" - "strings" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/vt/binlog" "github.com/youtube/vitess/go/vt/dbconfigs" "github.com/youtube/vitess/go/vt/mysqlctl" @@ -19,10 +19,11 @@ import ( "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/tabletserver" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) var ( - tabletPath = flag.String("tablet-path", "", "tablet alias or path to zk node representing the tablet") + tabletPath = flag.String("tablet-path", "", "tablet alias") enableRowcache = flag.Bool("enable-rowcache", false, "enable rowcacche") overridesFile = flag.String("schema-override", "", "schema overrides file") tableAclConfig = flag.String("table-acl-config", "", "path to table access checker config file") @@ -39,46 +40,40 @@ func init() { servenv.InitServiceMapForBsonRpcService("updatestream") } -// tabletParamToTabletAlias takes either an old style ZK tablet path or a -// new style tablet alias as a string, and returns a TabletAlias. -func tabletParamToTabletAlias(param string) topo.TabletAlias { - if param[0] == '/' { - // old zookeeper path, convert to new-style string tablet alias - zkPathParts := strings.Split(param, "/") - if len(zkPathParts) != 6 || zkPathParts[0] != "" || zkPathParts[1] != "zk" || zkPathParts[3] != "vt" || zkPathParts[4] != "tablets" { - log.Fatalf("Invalid tablet path: %v", param) - } - param = zkPathParts[2] + "-" + zkPathParts[5] - } - result, err := topo.ParseTabletAliasString(param) - if err != nil { - log.Fatalf("Invalid tablet alias %v: %v", param, err) - } - return result -} - func main() { - dbconfigs.RegisterFlags() + defer exit.Recover() + + flags := dbconfigs.AppConfig | dbconfigs.DbaConfig | + dbconfigs.FilteredConfig | dbconfigs.ReplConfig + dbconfigs.RegisterFlags(flags) mysqlctl.RegisterFlags() flag.Parse() if len(flag.Args()) > 0 { flag.Usage() - log.Fatalf("vttablet doesn't take any positional arguments") + log.Errorf("vttablet doesn't take any positional arguments") + exit.Return(1) } servenv.Init() if *tabletPath == "" { - log.Fatalf("tabletPath required") + log.Errorf("tabletPath required") + exit.Return(1) + } + tabletAlias, err := topo.ParseTabletAliasString(*tabletPath) + + if err != nil { + log.Error(err) + exit.Return(1) } - tabletAlias := tabletParamToTabletAlias(*tabletPath) mycnf, err := mysqlctl.NewMycnfFromFlags(tabletAlias.Uid) if err != nil { - log.Fatalf("mycnf read failed: %v", err) + log.Errorf("mycnf read failed: %v", err) + exit.Return(1) } - dbcfgs, err := dbconfigs.Init(mycnf.SocketFile) + dbcfgs, err := dbconfigs.Init(mycnf.SocketFile, flags) if err != nil { log.Warning(err) } @@ -91,9 +86,10 @@ func main() { binlog.RegisterUpdateStreamService(mycnf) // Depends on both query and updateStream. - agent, err = tabletmanager.NewActionAgent(tabletAlias, dbcfgs, mycnf, *servenv.Port, *servenv.SecurePort, *overridesFile, *lockTimeout) + agent, err = tabletmanager.NewActionAgent(context.Background(), tabletAlias, dbcfgs, mycnf, *servenv.Port, *servenv.SecurePort, *overridesFile, *lockTimeout) if err != nil { - log.Fatal(err) + log.Error(err) + exit.Return(1) } tabletmanager.HttpHandleSnapshots(mycnf, tabletAlias.Uid) diff --git a/go/cmd/vtworker/command.go b/go/cmd/vtworker/command.go index ed07c99b59a..34790efc297 100644 --- a/go/cmd/vtworker/command.go +++ b/go/cmd/vtworker/command.go @@ -22,7 +22,7 @@ var ( type command struct { Name string - method func(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) worker.Worker + method func(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) (worker.Worker, error) interactive func(wr *wrangler.Wrangler, w http.ResponseWriter, r *http.Request) params string Help string // if help is empty, won't list the command @@ -73,23 +73,7 @@ func addCommand(groupName string, c command) { panic(fmt.Errorf("Trying to add to missing group %v", groupName)) } -func shardParamToKeyspaceShard(param string) (string, string) { - if param[0] == '/' { - // old zookeeper path, convert to new-style - zkPathParts := strings.Split(param, "/") - if len(zkPathParts) != 8 || zkPathParts[0] != "" || zkPathParts[1] != "zk" || zkPathParts[2] != "global" || zkPathParts[3] != "vt" || zkPathParts[4] != "keyspaces" || zkPathParts[6] != "shards" { - log.Fatalf("Invalid shard path: %v", param) - } - return zkPathParts[5], zkPathParts[7] - } - zkPathParts := strings.Split(param, "/") - if len(zkPathParts) != 2 { - log.Fatalf("Invalid shard path: %v", param) - } - return zkPathParts[0], zkPathParts[1] -} - -func commandWorker(wr *wrangler.Wrangler, args []string) worker.Worker { +func commandWorker(wr *wrangler.Wrangler, args []string) (worker.Worker, error) { action := args[0] actionLowerCase := strings.ToLower(action) @@ -107,15 +91,17 @@ func commandWorker(wr *wrangler.Wrangler, args []string) worker.Worker { } } flag.Usage() - log.Fatalf("Unknown command %#v\n\n", action) - return nil + return nil, fmt.Errorf("unknown command: %v", action) } -func runCommand(args []string) { - wrk := commandWorker(wr, args) +func runCommand(args []string) error { + wrk, err := commandWorker(wr, args) + if err != nil { + return err + } done, err := setAndStartWorker(wrk) if err != nil { - log.Fatalf("Cannot set worker: %v", err) + return fmt.Errorf("cannot set worker: %v", err) } // a go routine displays the status every second @@ -135,4 +121,6 @@ func runCommand(args []string) { } } }() + + return nil } diff --git a/go/cmd/vtworker/interactive.go b/go/cmd/vtworker/interactive.go index dba6ede9b83..6527b275231 100644 --- a/go/cmd/vtworker/interactive.go +++ b/go/cmd/vtworker/interactive.go @@ -45,10 +45,11 @@ func httpError(w http.ResponseWriter, format string, err error) { http.Error(w, fmt.Sprintf(format, err), http.StatusInternalServerError) } -func loadTemplate(name, contents string) *template.Template { +func mustParseTemplate(name, contents string) *template.Template { t, err := template.New(name).Parse(contents) if err != nil { - log.Fatalf("Cannot parse %v template: %v", name, err) + // An invalid template here is a programming error. + panic(fmt.Sprintf("cannot parse %v template: %v", name, err)) } return t } @@ -60,8 +61,8 @@ func executeTemplate(w http.ResponseWriter, t *template.Template, data interface } func initInteractiveMode() { - indexTemplate := loadTemplate("index", indexHTML) - subIndexTemplate := loadTemplate("subIndex", subIndexHTML) + indexTemplate := mustParseTemplate("index", indexHTML) + subIndexTemplate := mustParseTemplate("subIndex", subIndexHTML) // toplevel menu http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { diff --git a/go/cmd/vtworker/plugin_etcdtopo.go b/go/cmd/vtworker/plugin_etcdtopo.go new file mode 100644 index 00000000000..1bd833657b2 --- /dev/null +++ b/go/cmd/vtworker/plugin_etcdtopo.go @@ -0,0 +1,11 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// This plugin imports etcdtopo to register the etcd implementation of TopoServer. + +import ( + _ "github.com/youtube/vitess/go/vt/etcdtopo" +) diff --git a/go/cmd/vtworker/plugin_zktopo.go b/go/cmd/vtworker/plugin_zktopo.go new file mode 100644 index 00000000000..77409609bc4 --- /dev/null +++ b/go/cmd/vtworker/plugin_zktopo.go @@ -0,0 +1,11 @@ +// Copyright 2013, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// Imports and register the Zookeeper TopologyServer + +import ( + _ "github.com/youtube/vitess/go/vt/zktopo" +) diff --git a/go/cmd/vtworker/split_clone.go b/go/cmd/vtworker/split_clone.go index 782bb4a464c..53348a2913d 100644 --- a/go/cmd/vtworker/split_clone.go +++ b/go/cmd/vtworker/split_clone.go @@ -12,9 +12,9 @@ import ( "strings" "sync" - log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" "github.com/youtube/vitess/go/vt/worker" "github.com/youtube/vitess/go/vt/wrangler" @@ -56,6 +56,8 @@ const splitCloneHTML2 = `

+ +

@@ -70,43 +72,39 @@ const splitCloneHTML2 = `
  • populateBlpCheckpoint: creates (if necessary) and populates the blp_checkpoint table in the destination. Required for filtered replication to start.
  • dontStartBinlogPlayer: (requires populateBlpCheckpoint) will setup, but not start binlog replication on the destination. The flag has to be manually cleared from the _vt.blp_checkpoint table.
  • -
  • skipAutoIncrement(TTT): we won't add the AUTO_INCREMENT back to that table.
  • -
  • skipSetSourceShards: we won't set SourceShards on the destination shards, disabling filtered replication. Usefull for worker tests.
  • -
-

The following flags are also supported, but their use is very strongly discouraged:

-
    -
  • delayPrimaryKey: we won't add the primary key until after the table is populated.
  • -
  • delaySecondaryIndexes: we won't add the secondary indexes until after the table is populated.
  • -
  • useMyIsam: create the table as MyISAM, then convert it to InnoDB after population.
  • -
  • writeBinLogs: write all operations to the binlogs.
  • +
  • skipSetSourceShards: we won't set SourceShards on the destination shards, disabling filtered replication. Useful for worker tests.
` -var splitCloneTemplate = loadTemplate("splitClone", splitCloneHTML) -var splitCloneTemplate2 = loadTemplate("splitClone2", splitCloneHTML2) +var splitCloneTemplate = mustParseTemplate("splitClone", splitCloneHTML) +var splitCloneTemplate2 = mustParseTemplate("splitClone2", splitCloneHTML2) -func commandSplitClone(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) worker.Worker { +func commandSplitClone(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) (worker.Worker, error) { excludeTables := subFlags.String("exclude_tables", "", "comma separated list of tables to exclude") strategy := subFlags.String("strategy", "", "which strategy to use for restore, use 'mysqlctl multirestore -strategy=-help' for more info") sourceReaderCount := subFlags.Int("source_reader_count", defaultSourceReaderCount, "number of concurrent streaming queries to use on the source") + destinationPackCount := subFlags.Int("destination_pack_count", defaultDestinationPackCount, "number of packets to pack in one destination insert") minTableSizeForSplit := subFlags.Int("min_table_size_for_split", defaultMinTableSizeForSplit, "tables bigger than this size on disk in bytes will be split into source_reader_count chunks if possible") destinationWriterCount := subFlags.Int("destination_writer_count", defaultDestinationWriterCount, "number of concurrent RPCs to execute on the destination") subFlags.Parse(args) if subFlags.NArg() != 1 { - log.Fatalf("command SplitClone requires ") + return nil, fmt.Errorf("command SplitClone requires ") } - keyspace, shard := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) + if err != nil { + return nil, err + } var excludeTableArray []string if *excludeTables != "" { excludeTableArray = strings.Split(*excludeTables, ",") } - worker, err := worker.NewSplitCloneWorker(wr, *cell, keyspace, shard, excludeTableArray, *strategy, *sourceReaderCount, uint64(*minTableSizeForSplit), *destinationWriterCount) + worker, err := worker.NewSplitCloneWorker(wr, *cell, keyspace, shard, excludeTableArray, *strategy, *sourceReaderCount, *destinationPackCount, uint64(*minTableSizeForSplit), *destinationWriterCount) if err != nil { - log.Fatalf("cannot create split clone worker: %v", err) + return nil, fmt.Errorf("cannot create split clone worker: %v", err) } - return worker + return worker, nil } func keyspacesWithOverlappingShards(wr *wrangler.Wrangler) ([]map[string]string, error) { @@ -179,6 +177,7 @@ func interactiveSplitClone(wr *wrangler.Wrangler, w http.ResponseWriter, r *http result["Keyspace"] = keyspace result["Shard"] = shard result["DefaultSourceReaderCount"] = fmt.Sprintf("%v", defaultSourceReaderCount) + result["DefaultDestinationPackCount"] = fmt.Sprintf("%v", defaultDestinationPackCount) result["DefaultMinTableSizeForSplit"] = fmt.Sprintf("%v", defaultMinTableSizeForSplit) result["DefaultDestinationWriterCount"] = fmt.Sprintf("%v", defaultDestinationWriterCount) executeTemplate(w, splitCloneTemplate2, result) @@ -186,6 +185,7 @@ func interactiveSplitClone(wr *wrangler.Wrangler, w http.ResponseWriter, r *http } // get other parameters + destinationPackCountStr := r.FormValue("destinationPackCount") excludeTables := r.FormValue("excludeTables") excludeTableArray := strings.Split(excludeTables, ",") strategy := r.FormValue("strategy") @@ -194,6 +194,11 @@ func interactiveSplitClone(wr *wrangler.Wrangler, w http.ResponseWriter, r *http httpError(w, "cannot parse sourceReaderCount: %s", err) return } + destinationPackCount, err := strconv.ParseInt(destinationPackCountStr, 0, 64) + if err != nil { + httpError(w, "cannot parse destinationPackCount: %s", err) + return + } minTableSizeForSplitStr := r.FormValue("minTableSizeForSplit") minTableSizeForSplit, err := strconv.ParseInt(minTableSizeForSplitStr, 0, 64) if err != nil { @@ -208,7 +213,7 @@ func interactiveSplitClone(wr *wrangler.Wrangler, w http.ResponseWriter, r *http } // start the clone job - wrk, err := worker.NewSplitCloneWorker(wr, *cell, keyspace, shard, excludeTableArray, strategy, int(sourceReaderCount), uint64(minTableSizeForSplit), int(destinationWriterCount)) + wrk, err := worker.NewSplitCloneWorker(wr, *cell, keyspace, shard, excludeTableArray, strategy, int(sourceReaderCount), int(destinationPackCount), uint64(minTableSizeForSplit), int(destinationWriterCount)) if err != nil { httpError(w, "cannot create worker: %v", err) return @@ -224,6 +229,6 @@ func interactiveSplitClone(wr *wrangler.Wrangler, w http.ResponseWriter, r *http func init() { addCommand("Clones", command{"SplitClone", commandSplitClone, interactiveSplitClone, - "[--exclude_tables=''] [--strategy=''] ", + "[--exclude_tables=''] [--strategy=''] ", "Replicates the data and creates configuration for a horizontal split."}) } diff --git a/go/cmd/vtworker/split_diff.go b/go/cmd/vtworker/split_diff.go index 890df6a377a..605dbe86823 100644 --- a/go/cmd/vtworker/split_diff.go +++ b/go/cmd/vtworker/split_diff.go @@ -10,9 +10,9 @@ import ( "net/http" "sync" - log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/worker" "github.com/youtube/vitess/go/vt/wrangler" ) @@ -35,15 +35,18 @@ const splitDiffHTML = ` ` -var splitDiffTemplate = loadTemplate("splitDiff", splitDiffHTML) +var splitDiffTemplate = mustParseTemplate("splitDiff", splitDiffHTML) -func commandSplitDiff(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) worker.Worker { +func commandSplitDiff(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) (worker.Worker, error) { subFlags.Parse(args) if subFlags.NArg() != 1 { - log.Fatalf("command SplitDiff requires ") + return nil, fmt.Errorf("command SplitDiff requires ") } - keyspace, shard := shardParamToKeyspaceShard(subFlags.Arg(0)) - return worker.NewSplitDiffWorker(wr, *cell, keyspace, shard) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) + if err != nil { + return nil, err + } + return worker.NewSplitDiffWorker(wr, *cell, keyspace, shard), nil } // shardsWithSources returns all the shards that have SourceShards set @@ -135,6 +138,6 @@ func interactiveSplitDiff(wr *wrangler.Wrangler, w http.ResponseWriter, r *http. func init() { addCommand("Diffs", command{"SplitDiff", commandSplitDiff, interactiveSplitDiff, - "", + "", "Diffs a rdonly destination shard against its SourceShards"}) } diff --git a/go/cmd/vtworker/status.go b/go/cmd/vtworker/status.go index d56c4e740d1..6a43f85e3f3 100644 --- a/go/cmd/vtworker/status.go +++ b/go/cmd/vtworker/status.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + "github.com/youtube/vitess/go/acl" "github.com/youtube/vitess/go/vt/servenv" ) @@ -45,6 +46,8 @@ const workerStatusHTML = ` {{if .Done}}

Reset Job

+ {{else}} +

Cancel Job

{{end}} {{else}}

This worker is idle.

@@ -56,8 +59,12 @@ const workerStatusHTML = ` func initStatusHandling() { // code to serve /status - workerTemplate := loadTemplate("worker", workerStatusHTML) + workerTemplate := mustParseTemplate("worker", workerStatusHTML) http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { + if err := acl.CheckAccessHTTP(r, acl.ADMIN); err != nil { + acl.SendError(w, err) + return + } currentWorkerMutex.Lock() wrk := currentWorker logger := currentMemoryLogger @@ -81,13 +88,17 @@ func initStatusHandling() { executeTemplate(w, workerTemplate, data) }) - // add the section in statusz that does auto-refresh of status div + // add the section in status that does auto-refresh of status div servenv.AddStatusPart("Worker Status", workerStatusPartHTML, func() interface{} { return nil }) // reset handler http.HandleFunc("/reset", func(w http.ResponseWriter, r *http.Request) { + if err := acl.CheckAccessHTTP(r, acl.ADMIN); err != nil { + acl.SendError(w, err) + return + } currentWorkerMutex.Lock() wrk := currentWorker done := currentDone @@ -112,4 +123,26 @@ func initStatusHandling() { httpError(w, "worker still executing", nil) } }) + + // cancel handler + http.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) { + if err := acl.CheckAccessHTTP(r, acl.ADMIN); err != nil { + acl.SendError(w, err) + return + } + currentWorkerMutex.Lock() + wrk := currentWorker + currentWorkerMutex.Unlock() + + // no worker, we go to the menu + if wrk == nil { + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + // otherwise, cancel the running worker and go back to the status page + wrk.Cancel() + http.Redirect(w, r, servenv.StatusURLPath(), http.StatusTemporaryRedirect) + + }) } diff --git a/go/cmd/vtworker/vertical_split_clone.go b/go/cmd/vtworker/vertical_split_clone.go index 4095065133a..a6f9e8cd0ca 100644 --- a/go/cmd/vtworker/vertical_split_clone.go +++ b/go/cmd/vtworker/vertical_split_clone.go @@ -12,15 +12,16 @@ import ( "strings" "sync" - log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/worker" "github.com/youtube/vitess/go/vt/wrangler" ) const ( defaultSourceReaderCount = 10 + defaultDestinationPackCount = 10 defaultMinTableSizeForSplit = 1024 * 1024 defaultDestinationWriterCount = 20 ) @@ -61,6 +62,8 @@ const verticalSplitCloneHTML2 = `

+ +

@@ -74,43 +77,39 @@ const verticalSplitCloneHTML2 = `
  • populateBlpCheckpoint: creates (if necessary) and populates the blp_checkpoint table in the destination. Required for filtered replication to start.
  • dontStartBinlogPlayer: (requires populateBlpCheckpoint) will setup, but not start binlog replication on the destination. The flag has to be manually cleared from the _vt.blp_checkpoint table.
  • -
  • skipAutoIncrement(TTT): we won't add the AUTO_INCREMENT back to that table.
  • -
  • skipSetSourceShards: we won't set SourceShards on the destination shards, disabling filtered replication. Usefull for worker tests.
  • -
-

The following flags are also supported, but their use is very strongly discouraged:

-
    -
  • delayPrimaryKey: we won't add the primary key until after the table is populated.
  • -
  • delaySecondaryIndexes: we won't add the secondary indexes until after the table is populated.
  • -
  • useMyIsam: create the table as MyISAM, then convert it to InnoDB after population.
  • -
  • writeBinLogs: write all operations to the binlogs.
  • +
  • skipSetSourceShards: we won't set SourceShards on the destination shards, disabling filtered replication. Useful for worker tests.
` -var verticalSplitCloneTemplate = loadTemplate("verticalSplitClone", verticalSplitCloneHTML) -var verticalSplitCloneTemplate2 = loadTemplate("verticalSplitClone2", verticalSplitCloneHTML2) +var verticalSplitCloneTemplate = mustParseTemplate("verticalSplitClone", verticalSplitCloneHTML) +var verticalSplitCloneTemplate2 = mustParseTemplate("verticalSplitClone2", verticalSplitCloneHTML2) -func commandVerticalSplitClone(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) worker.Worker { +func commandVerticalSplitClone(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) (worker.Worker, error) { tables := subFlags.String("tables", "", "comma separated list of tables to replicate (used for vertical split)") strategy := subFlags.String("strategy", "", "which strategy to use for restore, use 'mysqlctl multirestore -strategy=-help' for more info") sourceReaderCount := subFlags.Int("source_reader_count", defaultSourceReaderCount, "number of concurrent streaming queries to use on the source") + destinationPackCount := subFlags.Int("destination_pack_count", defaultDestinationPackCount, "number of packets to pack in one destination insert") minTableSizeForSplit := subFlags.Int("min_table_size_for_split", defaultMinTableSizeForSplit, "tables bigger than this size on disk in bytes will be split into source_reader_count chunks if possible") destinationWriterCount := subFlags.Int("destination_writer_count", defaultDestinationWriterCount, "number of concurrent RPCs to execute on the destination") subFlags.Parse(args) if subFlags.NArg() != 1 { - log.Fatalf("command VerticalSplitClone requires ") + return nil, fmt.Errorf("command VerticalSplitClone requires ") } - keyspace, shard := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) + if err != nil { + return nil, err + } var tableArray []string if *tables != "" { tableArray = strings.Split(*tables, ",") } - worker, err := worker.NewVerticalSplitCloneWorker(wr, *cell, keyspace, shard, tableArray, *strategy, *sourceReaderCount, uint64(*minTableSizeForSplit), *destinationWriterCount) + worker, err := worker.NewVerticalSplitCloneWorker(wr, *cell, keyspace, shard, tableArray, *strategy, *sourceReaderCount, *destinationPackCount, uint64(*minTableSizeForSplit), *destinationWriterCount) if err != nil { - log.Fatalf("cannot create worker: %v", err) + return nil, fmt.Errorf("cannot create worker: %v", err) } - return worker + return worker, nil } // keyspacesWithServedFrom returns all the keyspaces that have ServedFrom set @@ -179,6 +178,7 @@ func interactiveVerticalSplitClone(wr *wrangler.Wrangler, w http.ResponseWriter, result := make(map[string]interface{}) result["Keyspace"] = keyspace result["DefaultSourceReaderCount"] = fmt.Sprintf("%v", defaultSourceReaderCount) + result["DefaultDestinationPackCount"] = fmt.Sprintf("%v", defaultDestinationPackCount) result["DefaultMinTableSizeForSplit"] = fmt.Sprintf("%v", defaultMinTableSizeForSplit) result["DefaultDestinationWriterCount"] = fmt.Sprintf("%v", defaultDestinationWriterCount) executeTemplate(w, verticalSplitCloneTemplate2, result) @@ -194,6 +194,12 @@ func interactiveVerticalSplitClone(wr *wrangler.Wrangler, w http.ResponseWriter, httpError(w, "cannot parse sourceReaderCount: %s", err) return } + destinationPackCountStr := r.FormValue("destinationPackCount") + destinationPackCount, err := strconv.ParseInt(destinationPackCountStr, 0, 64) + if err != nil { + httpError(w, "cannot parse destinationPackCount: %s", err) + return + } minTableSizeForSplitStr := r.FormValue("minTableSizeForSplit") minTableSizeForSplit, err := strconv.ParseInt(minTableSizeForSplitStr, 0, 64) if err != nil { @@ -208,7 +214,7 @@ func interactiveVerticalSplitClone(wr *wrangler.Wrangler, w http.ResponseWriter, } // start the clone job - wrk, err := worker.NewVerticalSplitCloneWorker(wr, *cell, keyspace, "0", tableArray, strategy, int(sourceReaderCount), uint64(minTableSizeForSplit), int(destinationWriterCount)) + wrk, err := worker.NewVerticalSplitCloneWorker(wr, *cell, keyspace, "0", tableArray, strategy, int(sourceReaderCount), int(destinationPackCount), uint64(minTableSizeForSplit), int(destinationWriterCount)) if err != nil { httpError(w, "cannot create worker: %v", err) } @@ -223,6 +229,6 @@ func interactiveVerticalSplitClone(wr *wrangler.Wrangler, w http.ResponseWriter, func init() { addCommand("Clones", command{"VerticalSplitClone", commandVerticalSplitClone, interactiveVerticalSplitClone, - "[--tables=''] [--strategy=''] ", + "[--tables=''] [--strategy=''] ", "Replicates the data and creates configuration for a vertical split."}) } diff --git a/go/cmd/vtworker/vertical_split_diff.go b/go/cmd/vtworker/vertical_split_diff.go index 94acd6ad587..0994ca805bf 100644 --- a/go/cmd/vtworker/vertical_split_diff.go +++ b/go/cmd/vtworker/vertical_split_diff.go @@ -10,9 +10,9 @@ import ( "net/http" "sync" - log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/worker" "github.com/youtube/vitess/go/vt/wrangler" ) @@ -35,15 +35,18 @@ const verticalSplitDiffHTML = ` ` -var verticalSplitDiffTemplate = loadTemplate("verticalSplitDiff", verticalSplitDiffHTML) +var verticalSplitDiffTemplate = mustParseTemplate("verticalSplitDiff", verticalSplitDiffHTML) -func commandVerticalSplitDiff(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) worker.Worker { +func commandVerticalSplitDiff(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) (worker.Worker, error) { subFlags.Parse(args) if subFlags.NArg() != 1 { - log.Fatalf("command VerticalSplitDiff requires ") + return nil, fmt.Errorf("command VerticalSplitDiff requires ") } - keyspace, shard := shardParamToKeyspaceShard(subFlags.Arg(0)) - return worker.NewVerticalSplitDiffWorker(wr, *cell, keyspace, shard) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) + if err != nil { + return nil, err + } + return worker.NewVerticalSplitDiffWorker(wr, *cell, keyspace, shard), nil } // shardsWithTablesSources returns all the shards that have SourceShards set @@ -135,6 +138,6 @@ func interactiveVerticalSplitDiff(wr *wrangler.Wrangler, w http.ResponseWriter, func init() { addCommand("Diffs", command{"VerticalSplitDiff", commandVerticalSplitDiff, interactiveVerticalSplitDiff, - "", + "", "Diffs a rdonly destination keyspace against its SourceShard for a vertical split"}) } diff --git a/go/cmd/vtworker/vtworker.go b/go/cmd/vtworker/vtworker.go index 6f90d3092c4..e2ccc5e3afc 100644 --- a/go/cmd/vtworker/vtworker.go +++ b/go/cmd/vtworker/vtworker.go @@ -22,6 +22,7 @@ import ( "time" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/topo" @@ -37,21 +38,6 @@ func init() { servenv.RegisterDefaultFlags() } -// signal handling, centralized here -func installSignalHandlers() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) - go func() { - <-sigChan - // we got a signal, notify our modules: - // - wr will interrupt anything waiting on a shard or - // keyspace lock - // - worker will cancel any running job - wrangler.SignalInterrupt() - worker.SignalInterrupt() - }() -} - var ( // global wrangler object we'll use wr *wrangler.Wrangler @@ -63,6 +49,19 @@ var ( currentDone chan struct{} ) +// signal handling, centralized here +func installSignalHandlers() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) + go func() { + <-sigChan + // we got a signal, notify our modules + currentWorkerMutex.Lock() + defer currentWorkerMutex.Unlock() + currentWorker.Cancel() + }() +} + // setAndStartWorker will set the current worker. // We always log to both memory logger (for display on the web) and // console logger (for records / display of command line worker). @@ -89,26 +88,30 @@ func setAndStartWorker(wrk worker.Worker) (chan struct{}, error) { } func main() { + defer exit.Recover() + flag.Parse() args := flag.Args() - installSignalHandlers() - servenv.Init() defer servenv.Close() ts := topo.GetServer() defer topo.CloseServers() - // the logger will be replaced when we start a job - wr = wrangler.New(logutil.NewConsoleLogger(), ts, 30*time.Second, 30*time.Second) + // The logger will be replaced when we start a job. + wr = wrangler.New(logutil.NewConsoleLogger(), ts, 30*time.Second) if len(args) == 0 { - // interactive mode, initialize the web UI to chose a command + // In interactive mode, initialize the web UI to choose a command. initInteractiveMode() } else { - // single command mode, just runs it - runCommand(args) + // In single command mode, just run it. + if err := runCommand(args); err != nil { + log.Error(err) + exit.Return(1) + } } + installSignalHandlers() initStatusHandling() servenv.RunDefault() diff --git a/go/cmd/zk/zkcmd.go b/go/cmd/zk/zkcmd.go index 173973fbfc7..127e51e64a1 100644 --- a/go/cmd/zk/zkcmd.go +++ b/go/cmd/zk/zkcmd.go @@ -16,6 +16,7 @@ import ( "time" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/terminal" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/zk" @@ -88,7 +89,7 @@ const ( timeFmtMicro = "2006-01-02 15:04:05.000000" ) -type cmdFunc func(subFlags *flag.FlagSet, args []string) +type cmdFunc func(subFlags *flag.FlagSet, args []string) error var cmdMap map[string]cmdFunc var zconn zk.Conn @@ -111,15 +112,15 @@ func init() { "zip": cmdZip, } - zconn = zk.NewMetaConn(false) + zconn = zk.NewMetaConn() } var ( - zkAddrs = flag.String("zk.addrs", "", "list of zookeeper servers (server1:port1,server2:port2,...) which overrides the conf file") - zkoccAddr = flag.String("zk.zkocc-addr", "", "if specified, talk to a zkocc process. Only cat and ls are permited") + zkAddrs = flag.String("zk.addrs", "", "list of zookeeper servers (server1:port1,server2:port2,...) which overrides the conf file") ) func main() { + defer exit.Recover() defer logutil.Flush() flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %v:\n", os.Args[0]) @@ -130,25 +131,15 @@ func main() { args := flag.Args() if len(args) == 0 { flag.Usage() - os.Exit(1) + exit.Return(1) } if *zkAddrs != "" { - if *zkoccAddr != "" { - log.Fatalf("zk.addrs and zk.zkocc-addr are mutually exclusive") - } var err error zconn, _, err = zk.DialZkTimeout(*zkAddrs, 5*time.Second, 10*time.Second) if err != nil { - log.Fatalf("zk connect failed: %v", err.Error()) - } - } - - if *zkoccAddr != "" { - var err error - zconn, err = zk.DialZkocc(*zkoccAddr, 5*time.Second) - if err != nil { - log.Fatalf("zkocc connect failed: %v", err.Error()) + log.Errorf("zk connect failed: %v", err.Error()) + exit.Return(1) } } @@ -156,7 +147,10 @@ func main() { args = args[1:] if cmd, ok := cmdMap[cmdName]; ok { subFlags := flag.NewFlagSet(cmdName, flag.ExitOnError) - cmd(subFlags, args) + if err := cmd(subFlags, args); err != nil { + log.Error(err) + exit.Return(1) + } } } @@ -171,7 +165,7 @@ func isZkFile(path string) bool { return strings.HasPrefix(path, "/zk") } -func cmdWait(subFlags *flag.FlagSet, args []string) { +func cmdWait(subFlags *flag.FlagSet, args []string) error { var ( exitIfExists = subFlags.Bool("e", false, "exit if the path already exists") ) @@ -179,7 +173,7 @@ func cmdWait(subFlags *flag.FlagSet, args []string) { subFlags.Parse(args) if subFlags.NArg() != 1 { - log.Fatalf("wait: can only wait for one path") + return fmt.Errorf("wait: can only wait for one path") } zkPath := subFlags.Arg(0) isDir := zkPath[len(zkPath)-1] == '/' @@ -196,19 +190,19 @@ func cmdWait(subFlags *flag.FlagSet, args []string) { if zookeeper.IsError(err, zookeeper.ZNONODE) { _, wait, err = zconn.ExistsW(zkPath) } else { - log.Fatalf("wait: error %v: %v", zkPath, err) + return fmt.Errorf("wait: error %v: %v", zkPath, err) } } else { if *exitIfExists { - fmt.Printf("already exists: %v\n", zkPath) - return + return fmt.Errorf("already exists: %v\n", zkPath) } } event := <-wait fmt.Printf("event: %v\n", event) + return nil } -func cmdQlock(subFlags *flag.FlagSet, args []string) { +func cmdQlock(subFlags *flag.FlagSet, args []string) error { var ( lockWaitTimeout = subFlags.Duration("lock-wait-timeout", 0, "wait for a lock for the specified duration") ) @@ -222,13 +216,14 @@ func cmdQlock(subFlags *flag.FlagSet, args []string) { close(interrupted) }() if err := zk.ObtainQueueLock(zconn, zkPath, *lockWaitTimeout, interrupted); err != nil { - log.Fatalf("qlock: error %v: %v", zkPath, err) + return fmt.Errorf("qlock: error %v: %v", zkPath, err) } fmt.Printf("qlock: locked %v\n", zkPath) + return nil } // Create an ephemeral node an just wait. -func cmdElock(subFlags *flag.FlagSet, args []string) { +func cmdElock(subFlags *flag.FlagSet, args []string) error { subFlags.Parse(args) zkPath := fixZkPath(subFlags.Arg(0)) // Speed up case where we die nicely, otherwise you have to wait for @@ -239,23 +234,23 @@ func cmdElock(subFlags *flag.FlagSet, args []string) { for { _, err := zconn.Create(zkPath, "", zookeeper.EPHEMERAL, zookeeper.WorldACL(zookeeper.PERM_ALL)) if err != nil { - log.Fatalf("elock: error %v: %v", zkPath, err) + return fmt.Errorf("elock: error %v: %v", zkPath, err) } watchLoop: for { _, _, watch, err := zconn.GetW(zkPath) if err != nil { - log.Fatalf("elock: error %v: %v", zkPath, err) + return fmt.Errorf("elock: error %v: %v", zkPath, err) } select { case <-sigRecv: zconn.Delete(zkPath, -1) - return + return nil case event := <-watch: log.Infof("elock: event %v: %v", zkPath, event) if !event.Ok() { - //log.Fatalf("elock: error %v: %v", zkPath, event) + //return fmt.Errorf("elock: error %v: %v", zkPath, event) break watchLoop } } @@ -264,7 +259,7 @@ func cmdElock(subFlags *flag.FlagSet, args []string) { } // Watch for changes to the node. -func cmdWatch(subFlags *flag.FlagSet, args []string) { +func cmdWatch(subFlags *flag.FlagSet, args []string) error { subFlags.Parse(args) // Speed up case where we die nicely, otherwise you have to wait for // the server to notice the client's demise. @@ -276,7 +271,7 @@ func cmdWatch(subFlags *flag.FlagSet, args []string) { zkPath := fixZkPath(arg) _, _, watch, err := zconn.GetW(zkPath) if err != nil { - log.Fatalf("watch error: %v", err) + return fmt.Errorf("watch error: %v", err) } go func() { eventChan <- <-watch @@ -286,13 +281,13 @@ func cmdWatch(subFlags *flag.FlagSet, args []string) { for { select { case <-sigRecv: - return + return nil case event := <-eventChan: log.Infof("watch: event %v: %v", event.Path, event) if event.Type == zookeeper.EVENT_CHANGED { data, stat, watch, err := zconn.GetW(event.Path) if err != nil { - log.Fatalf("ERROR: failed to watch %v", err) + return fmt.Errorf("ERROR: failed to watch %v", err) } log.Infof("watch: %v %v\n", event.Path, stat) println(data) @@ -300,14 +295,14 @@ func cmdWatch(subFlags *flag.FlagSet, args []string) { eventChan <- <-watch }() } else if event.State == zookeeper.STATE_CLOSED { - return + return nil } else if event.Type == zookeeper.EVENT_DELETED { log.Infof("watch: %v deleted\n", event.Path) } else { // Most likely a session event - try t _, _, watch, err := zconn.GetW(event.Path) if err != nil { - log.Fatalf("ERROR: failed to watch %v", err) + return fmt.Errorf("ERROR: failed to watch %v", err) } go func() { eventChan <- <-watch @@ -317,7 +312,7 @@ func cmdWatch(subFlags *flag.FlagSet, args []string) { } } -func cmdLs(subFlags *flag.FlagSet, args []string) { +func cmdLs(subFlags *flag.FlagSet, args []string) error { var ( longListing = subFlags.Bool("l", false, "long listing") directoryListing = subFlags.Bool("d", false, "list directory instead of contents") @@ -326,17 +321,17 @@ func cmdLs(subFlags *flag.FlagSet, args []string) { ) subFlags.Parse(args) if subFlags.NArg() == 0 { - log.Fatal("ls: no path specified") + return fmt.Errorf("ls: no path specified") } // FIXME(szopa): shadowing? resolved, err := zk.ResolveWildcards(zconn, subFlags.Args()) if err != nil { - log.Fatalf("ls: invalid wildcards: %v", err) + return fmt.Errorf("ls: invalid wildcards: %v", err) } if len(resolved) == 0 { // the wildcards didn't result in anything, we're // done. - return + return nil } hasError := false @@ -413,8 +408,9 @@ func cmdLs(subFlags *flag.FlagSet, args []string) { } } if hasError { - os.Exit(1) + return fmt.Errorf("ls: some paths had errors") } + return nil } func fmtPath(stat zk.Stat, zkPath string, showFullPath bool, longListing bool) { @@ -440,16 +436,15 @@ func fmtPath(stat zk.Stat, zkPath string, showFullPath bool, longListing bool) { perms = "-rw-rw-rw-" } // always print the Local version of the time. zookeeper's - // go / C library would return a local time, whereas - // gorpc to zkocc returns a UTC time. By always printing the - // Local version we make them the same. + // go / C library would return a local time anyway, but + // might as well be sure. fmt.Printf("%v %v %v % 8v % 20v %v\n", perms, "zk", "zk", stat.DataLength(), stat.MTime().Local().Format(timeFmt), name) } else { fmt.Printf("%v\n", name) } } -func cmdTouch(subFlags *flag.FlagSet, args []string) { +func cmdTouch(subFlags *flag.FlagSet, args []string) error { var ( createParents = subFlags.Bool("p", false, "create parents") touchOnly = subFlags.Bool("c", false, "touch only - don't create") @@ -457,12 +452,12 @@ func cmdTouch(subFlags *flag.FlagSet, args []string) { subFlags.Parse(args) if subFlags.NArg() != 1 { - log.Fatal("touch: need to specify exactly one path") + return fmt.Errorf("touch: need to specify exactly one path") } zkPath := fixZkPath(subFlags.Arg(0)) if !isZkFile(zkPath) { - log.Fatalf("touch: not a /zk file %v", zkPath) + return fmt.Errorf("touch: not a /zk file %v", zkPath) } var ( @@ -477,14 +472,14 @@ func cmdTouch(subFlags *flag.FlagSet, args []string) { case zookeeper.IsError(err, zookeeper.ZNONODE): create = true default: - log.Fatalf("touch: cannot access %v: %v", zkPath, err) + return fmt.Errorf("touch: cannot access %v: %v", zkPath, err) } switch { case !create: _, err = zconn.Set(zkPath, data, version) case *touchOnly: - log.Fatalf("touch: no such path %v", zkPath) + return fmt.Errorf("touch: no such path %v", zkPath) case *createParents: _, err = zk.CreateRecursive(zconn, zkPath, data, 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) default: @@ -492,11 +487,12 @@ func cmdTouch(subFlags *flag.FlagSet, args []string) { } if err != nil { - log.Fatalf("touch: cannot modify %v: %v", zkPath, err) + return fmt.Errorf("touch: cannot modify %v: %v", zkPath, err) } + return nil } -func cmdRm(subFlags *flag.FlagSet, args []string) { +func cmdRm(subFlags *flag.FlagSet, args []string) error { var ( force = subFlags.Bool("f", false, "no warning on nonexistent node") recursiveDelete = subFlags.Bool("r", false, "recursive delete") @@ -507,25 +503,25 @@ func cmdRm(subFlags *flag.FlagSet, args []string) { *recursiveDelete = *recursiveDelete || *forceAndRecursive if subFlags.NArg() == 0 { - log.Fatal("rm: no path specified") + return fmt.Errorf("rm: no path specified") } if *recursiveDelete { for _, arg := range subFlags.Args() { zkPath := fixZkPath(arg) if strings.Count(zkPath, "/") < 4 { - log.Fatalf("rm: overly general path: %v", zkPath) + return fmt.Errorf("rm: overly general path: %v", zkPath) } } } resolved, err := zk.ResolveWildcards(zconn, subFlags.Args()) if err != nil { - log.Fatalf("rm: invalid wildcards: %v", err) + return fmt.Errorf("rm: invalid wildcards: %v", err) } if len(resolved) == 0 { // the wildcards didn't result in anything, we're done - return + return nil } hasError := false @@ -545,26 +541,27 @@ func cmdRm(subFlags *flag.FlagSet, args []string) { if hasError { // to be consistent with the command line 'rm -f', return // 0 if using 'zk rm -f' and the file doesn't exist. - os.Exit(1) + return fmt.Errorf("rm: some paths had errors") } + return nil } -func cmdCat(subFlags *flag.FlagSet, args []string) { +func cmdCat(subFlags *flag.FlagSet, args []string) error { var ( longListing = subFlags.Bool("l", false, "long listing") force = subFlags.Bool("f", false, "no warning on nonexistent node") ) subFlags.Parse(args) if subFlags.NArg() == 0 { - log.Fatal("cat: no path specified") + return fmt.Errorf("cat: no path specified") } resolved, err := zk.ResolveWildcards(zconn, subFlags.Args()) if err != nil { - log.Fatalf("cat: invalid wildcards: %v", err) + return fmt.Errorf("cat: invalid wildcards: %v", err) } if len(resolved) == 0 { // the wildcards didn't result in anything, we're done - return + return nil } hasError := false @@ -587,17 +584,18 @@ func cmdCat(subFlags *flag.FlagSet, args []string) { } } if hasError { - os.Exit(1) + return fmt.Errorf("cat: some paths had errors") } + return nil } -func cmdEdit(subFlags *flag.FlagSet, args []string) { +func cmdEdit(subFlags *flag.FlagSet, args []string) error { var ( force = subFlags.Bool("f", false, "no warning on nonexistent node") ) subFlags.Parse(args) if subFlags.NArg() == 0 { - log.Fatal("edit: no path specified") + return fmt.Errorf("edit: no path specified") } arg := subFlags.Arg(0) zkPath := fixZkPath(arg) @@ -606,7 +604,7 @@ func cmdEdit(subFlags *flag.FlagSet, args []string) { if !*force || !zookeeper.IsError(err, zookeeper.ZNONODE) { log.Warningf("edit: cannot access %v: %v", zkPath, err) } - os.Exit(1) + return fmt.Errorf("edit: cannot access %v: %v", zkPath, err) } name := path.Base(zkPath) @@ -617,8 +615,7 @@ func cmdEdit(subFlags *flag.FlagSet, args []string) { f.Close() } if err != nil { - log.Warningf("edit: cannot write file %v", err) - os.Exit(1) + return fmt.Errorf("edit: cannot write file %v", err) } cmd := exec.Command(os.Getenv("EDITOR"), tmpPath) @@ -628,13 +625,13 @@ func cmdEdit(subFlags *flag.FlagSet, args []string) { err = cmd.Run() if err != nil { os.Remove(tmpPath) - log.Fatalf("edit: cannot start $EDITOR: %v", err) + return fmt.Errorf("edit: cannot start $EDITOR: %v", err) } fileData, err := ioutil.ReadFile(tmpPath) if err != nil { os.Remove(tmpPath) - log.Fatalf("edit: cannot read file %v", err) + return fmt.Errorf("edit: cannot read file %v", err) } if string(fileData) != data { @@ -642,29 +639,30 @@ func cmdEdit(subFlags *flag.FlagSet, args []string) { _, err = zconn.Set(zkPath, string(fileData), stat.Version()) if err != nil { os.Remove(tmpPath) - log.Fatalf("edit: cannot write zk file %v", err) + return fmt.Errorf("edit: cannot write zk file %v", err) } } os.Remove(tmpPath) + return nil } -func cmdStat(subFlags *flag.FlagSet, args []string) { +func cmdStat(subFlags *flag.FlagSet, args []string) error { var ( force = subFlags.Bool("f", false, "no warning on nonexistent node") ) subFlags.Parse(args) if subFlags.NArg() == 0 { - log.Fatal("stat: no path specified") + return fmt.Errorf("stat: no path specified") } resolved, err := zk.ResolveWildcards(zconn, subFlags.Args()) if err != nil { - log.Fatalf("stat: invalid wildcards: %v", err) + return fmt.Errorf("stat: invalid wildcards: %v", err) } if len(resolved) == 0 { // the wildcards didn't result in anything, we're done - return + return nil } hasError := false @@ -694,8 +692,9 @@ func cmdStat(subFlags *flag.FlagSet, args []string) { } } if hasError { - os.Exit(1) + return fmt.Errorf("stat: some paths had errors") } + return nil } var charPermMap map[string]uint32 @@ -728,21 +727,21 @@ func fmtAcl(acl zookeeper.ACL) string { return s } -func cmdChmod(subFlags *flag.FlagSet, args []string) { +func cmdChmod(subFlags *flag.FlagSet, args []string) error { subFlags.Parse(args) if subFlags.NArg() < 2 { - log.Fatal("chmod: no permission specified") + return fmt.Errorf("chmod: no permission specified") } mode := subFlags.Arg(0) if mode[0] != 'n' { - log.Fatal("chmod: invalid mode") + return fmt.Errorf("chmod: invalid mode") } addPerms := false if mode[1] == '+' { addPerms = true } else if mode[1] != '-' { - log.Fatal("chmod: invalid mode") + return fmt.Errorf("chmod: invalid mode") } var permMask uint32 @@ -752,11 +751,11 @@ func cmdChmod(subFlags *flag.FlagSet, args []string) { resolved, err := zk.ResolveWildcards(zconn, subFlags.Args()[1:]) if err != nil { - log.Fatalf("chmod: invalid wildcards: %v", err) + return fmt.Errorf("chmod: invalid wildcards: %v", err) } if len(resolved) == 0 { // the wildcards didn't result in anything, we're done - return + return nil } hasError := false @@ -781,19 +780,20 @@ func cmdChmod(subFlags *flag.FlagSet, args []string) { } } if hasError { - os.Exit(1) + return fmt.Errorf("chmod: some paths had errors") } + return nil } -func cmdCp(subFlags *flag.FlagSet, args []string) { +func cmdCp(subFlags *flag.FlagSet, args []string) error { subFlags.Parse(args) switch { case subFlags.NArg() < 2: - log.Fatalf("cp: need to specify source and destination paths") + return fmt.Errorf("cp: need to specify source and destination paths") case subFlags.NArg() == 2: - fileCp(args[0], args[1]) + return fileCp(args[0], args[1]) default: - multiFileCp(args) + return multiFileCp(args) } } @@ -801,17 +801,16 @@ func getPathData(filePath string) (string, error) { if isZkFile(filePath) { data, _, err := zconn.Get(filePath) return data, err - } else { - var err error - file, err := os.Open(filePath) + } + var err error + file, err := os.Open(filePath) + if err == nil { + data, err := ioutil.ReadAll(file) if err == nil { - data, err := ioutil.ReadAll(file) - if err == nil { - return string(data), err - } + return string(data), err } - return "", err } + return "", err } func setPathData(filePath, data string) error { @@ -821,23 +820,22 @@ func setPathData(filePath, data string) error { _, err = zk.CreateRecursive(zconn, filePath, data, 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) } return err - } else { - return ioutil.WriteFile(filePath, []byte(data), 0666) } + return ioutil.WriteFile(filePath, []byte(data), 0666) } -func fileCp(srcPath, dstPath string) { +func fileCp(srcPath, dstPath string) error { dstIsDir := dstPath[len(dstPath)-1] == '/' srcPath = fixZkPath(srcPath) dstPath = fixZkPath(dstPath) if !isZkFile(srcPath) && !isZkFile(dstPath) { - log.Fatal("cp: neither src nor dst is a /zk file: exitting") + return fmt.Errorf("cp: neither src nor dst is a /zk file: exitting") } data, err := getPathData(srcPath) if err != nil { - log.Fatalf("cp: cannot read %v: %v", srcPath, err) + return fmt.Errorf("cp: cannot read %v: %v", srcPath, err) } // If we are copying to a local directory - say '.', make the filename @@ -846,7 +844,7 @@ func fileCp(srcPath, dstPath string) { fileInfo, err := os.Stat(dstPath) if err != nil { if err.(*os.PathError).Err != syscall.ENOENT { - log.Fatalf("cp: cannot stat %v: %v", dstPath, err) + return fmt.Errorf("cp: cannot stat %v: %v", dstPath, err) } } else if fileInfo.IsDir() { dstPath = path.Join(dstPath, path.Base(srcPath)) @@ -857,11 +855,12 @@ func fileCp(srcPath, dstPath string) { dstPath = path.Join(dstPath, path.Base(srcPath)) } if err := setPathData(dstPath, data); err != nil { - log.Fatalf("cp: cannot write %v: %v", dstPath, err) + return fmt.Errorf("cp: cannot write %v: %v", dstPath, err) } + return nil } -func multiFileCp(args []string) { +func multiFileCp(args []string) error { dstPath := args[len(args)-1] if dstPath[len(dstPath)-1] != '/' { // In multifile context, dstPath must be a directory. @@ -869,8 +868,11 @@ func multiFileCp(args []string) { } for _, srcPath := range args[:len(args)-1] { - fileCp(srcPath, dstPath) + if err := fileCp(srcPath, dstPath); err != nil { + return err + } } + return nil } type zkItem struct { @@ -882,21 +884,21 @@ type zkItem struct { // Store a zk tree in a zip archive. This won't be immediately useful to // zip tools since even "directories" can contain data. -func cmdZip(subFlags *flag.FlagSet, args []string) { +func cmdZip(subFlags *flag.FlagSet, args []string) error { subFlags.Parse(args) if subFlags.NArg() < 2 { - log.Fatalf("zip: need to specify source and destination paths") + return fmt.Errorf("zip: need to specify source and destination paths") } dstPath := subFlags.Arg(subFlags.NArg() - 1) paths := subFlags.Args()[:len(args)-1] if !strings.HasSuffix(dstPath, ".zip") { - log.Fatalf("zip: need to specify destination .zip path: %v", dstPath) + return fmt.Errorf("zip: need to specify destination .zip path: %v", dstPath) } zipFile, err := os.Create(dstPath) if err != nil { - log.Fatalf("zip: error %v", err) + return fmt.Errorf("zip: error %v", err) } wg := sync.WaitGroup{} @@ -905,7 +907,7 @@ func cmdZip(subFlags *flag.FlagSet, args []string) { zkPath := fixZkPath(arg) children, err := zk.ChildrenRecursive(zconn, zkPath) if err != nil { - log.Fatalf("zip: error %v", err) + return fmt.Errorf("zip: error %v", err) } for _, child := range children { toAdd := path.Join(zkPath, child) @@ -926,7 +928,7 @@ func cmdZip(subFlags *flag.FlagSet, args []string) { for item := range items { path, data, stat, err := item.path, item.data, item.stat, item.err if err != nil { - log.Fatalf("zip: get failed: %v", err) + return fmt.Errorf("zip: get failed: %v", err) } // Skip ephemerals - not sure why you would archive them. if stat.EphemeralOwner() > 0 { @@ -936,46 +938,47 @@ func cmdZip(subFlags *flag.FlagSet, args []string) { fi.SetModTime(stat.MTime()) f, err := zipWriter.CreateHeader(fi) if err != nil { - log.Fatalf("zip: create failed: %v", err) + return fmt.Errorf("zip: create failed: %v", err) } _, err = f.Write([]byte(data)) if err != nil { - log.Fatalf("zip: create failed: %v", err) + return fmt.Errorf("zip: create failed: %v", err) } } err = zipWriter.Close() if err != nil { - log.Fatalf("zip: close failed: %v", err) + return fmt.Errorf("zip: close failed: %v", err) } zipFile.Close() + return nil } -func cmdUnzip(subFlags *flag.FlagSet, args []string) { +func cmdUnzip(subFlags *flag.FlagSet, args []string) error { subFlags.Parse(args) if subFlags.NArg() != 2 { - log.Fatalf("zip: need to specify source and destination paths") + return fmt.Errorf("zip: need to specify source and destination paths") } srcPath, dstPath := subFlags.Arg(0), subFlags.Arg(1) if !strings.HasSuffix(srcPath, ".zip") { - log.Fatalf("zip: need to specify src .zip path: %v", srcPath) + return fmt.Errorf("zip: need to specify src .zip path: %v", srcPath) } zipReader, err := zip.OpenReader(srcPath) if err != nil { - log.Fatalf("zip: error %v", err) + return fmt.Errorf("zip: error %v", err) } defer zipReader.Close() for _, zf := range zipReader.File { rc, err := zf.Open() if err != nil { - log.Fatalf("unzip: error %v", err) + return fmt.Errorf("unzip: error %v", err) } data, err := ioutil.ReadAll(rc) if err != nil { - log.Fatalf("unzip: failed reading archive: %v", err) + return fmt.Errorf("unzip: failed reading archive: %v", err) } zkPath := zf.Name if dstPath != "/" { @@ -983,12 +986,13 @@ func cmdUnzip(subFlags *flag.FlagSet, args []string) { } _, err = zk.CreateRecursive(zconn, zkPath, string(data), 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) if err != nil && !zookeeper.IsError(err, zookeeper.ZNODEEXISTS) { - log.Fatalf("unzip: zk create failed: %v", err) + return fmt.Errorf("unzip: zk create failed: %v", err) } _, err = zconn.Set(zkPath, string(data), -1) if err != nil { - log.Fatalf("unzip: zk set failed: %v", err) + return fmt.Errorf("unzip: zk set failed: %v", err) } rc.Close() } + return nil } diff --git a/go/cmd/zkclient2/zkclient2.go b/go/cmd/zkclient2/zkclient2.go index c6ec8147bc1..f11a42ab91d 100644 --- a/go/cmd/zkclient2/zkclient2.go +++ b/go/cmd/zkclient2/zkclient2.go @@ -12,24 +12,23 @@ import ( "sort" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/rpcplus" "github.com/youtube/vitess/go/rpcwrap/bsonrpc" "github.com/youtube/vitess/go/sync2" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/topo" - "github.com/youtube/vitess/go/zk" ) var ( usage = ` -Queries the zkocc zookeeper cache, for test purposes. In get mode, if more -than one value is asked for, will use getv. +Queries the topo server, for test purposes. ` - mode = flag.String("mode", "get", "which operation to run on the node (get, children, qps, qps2)") - server = flag.String("server", "localhost:3801", "zkocc server to dial") + mode = flag.String("mode", "get", "which operation to run on the node (getSrvKeyspaceNames, getSrvKeyspace, getEndPoints, qps)") + server = flag.String("server", "localhost:3801", "topo server to dial") timeout = flag.Duration("timeout", 5*time.Second, "connection timeout") cpuProfile = flag.String("cpu_profile", "", "write cpu profile to file") ) @@ -45,60 +44,11 @@ func init() { func connect() *rpcplus.Client { rpcClient, err := bsonrpc.DialHTTP("tcp", *server, *timeout, nil) if err != nil { - log.Fatalf("Can't connect to zkocc: %v", err) + log.Fatalf("Can't connect to topo server: %v", err) } return rpcClient } -func get(rpcClient *rpcplus.Client, path string, verbose bool) { - // it's a get - zkPath := &zk.ZkPath{Path: path} - zkNode := &zk.ZkNode{} - if err := rpcClient.Call(context.TODO(), "ZkReader.Get", zkPath, zkNode); err != nil { - log.Fatalf("ZkReader.Get error: %v", err) - } - if verbose { - println(fmt.Sprintf("%v = %v (NumChildren=%v, Version=%v, Cached=%v, Stale=%v)", zkNode.Path, zkNode.Data, zkNode.Stat.NumChildren(), zkNode.Stat.Version(), zkNode.Cached, zkNode.Stale)) - } - -} - -func getv(rpcClient *rpcplus.Client, paths []string, verbose bool) { - zkPathV := &zk.ZkPathV{Paths: make([]string, len(paths))} - for i, v := range paths { - zkPathV.Paths[i] = v - } - zkNodeV := &zk.ZkNodeV{} - if err := rpcClient.Call(context.TODO(), "ZkReader.GetV", zkPathV, zkNodeV); err != nil { - log.Fatalf("ZkReader.GetV error: %v", err) - } - if verbose { - for i, zkNode := range zkNodeV.Nodes { - println(fmt.Sprintf("[%v] %v = %v (NumChildren=%v, Version=%v, Cached=%v, Stale=%v)", i, zkNode.Path, zkNode.Data, zkNode.Stat.NumChildren(), zkNode.Stat.Version(), zkNode.Cached, zkNode.Stale)) - } - } -} - -func children(rpcClient *rpcplus.Client, paths []string, verbose bool) { - for _, v := range paths { - zkPath := &zk.ZkPath{Path: v} - zkNode := &zk.ZkNode{} - if err := rpcClient.Call(context.TODO(), "ZkReader.Children", zkPath, zkNode); err != nil { - log.Fatalf("ZkReader.Children error: %v", err) - } - if verbose { - println(fmt.Sprintf("Path = %v", zkNode.Path)) - for i, child := range zkNode.Children { - println(fmt.Sprintf("Child[%v] = %v", i, child)) - } - println(fmt.Sprintf("NumChildren = %v", zkNode.Stat.NumChildren())) - println(fmt.Sprintf("CVersion = %v", zkNode.Stat.CVersion())) - println(fmt.Sprintf("Cached = %v", zkNode.Cached)) - println(fmt.Sprintf("Stale = %v", zkNode.Stale)) - } - } -} - func getSrvKeyspaceNames(rpcClient *rpcplus.Client, cell string, verbose bool) { req := &topo.GetSrvKeyspaceNamesArgs{ Cell: cell, @@ -135,9 +85,6 @@ func getSrvKeyspace(rpcClient *rpcplus.Client, cell, keyspace string, verbose bo println(fmt.Sprintf(" Shards[%v]=%v", i, s.KeyRange.String())) } } - for i, s := range reply.Shards { - println(fmt.Sprintf("Shards[%v]=%v", i, s.KeyRange.String())) - } for i, t := range reply.TabletTypes { println(fmt.Sprintf("TabletTypes[%v] = %v", i, t)) } @@ -162,38 +109,9 @@ func getEndPoints(rpcClient *rpcplus.Client, cell, keyspace, shard, tabletType s } } -// qps is a function used by tests to run a zkocc load check. -// It will get zk paths as fast as possible and display the QPS. -func qps(paths []string) { - var count sync2.AtomicInt32 - for _, path := range paths { - for i := 0; i < 10; i++ { - go func() { - rpcClient := connect() - for true { - get(rpcClient, path, false) - count.Add(1) - } - }() - } - } - - ticker := time.NewTicker(time.Second) - i := 0 - for _ = range ticker.C { - c := count.Get() - count.Set(0) - println(fmt.Sprintf("QPS = %v", c)) - i++ - if i == 10 { - break - } - } -} - -// qps2 is a function used by tests to run a vtgate load check. +// qps is a function used by tests to run a vtgate load check. // It will get the same srvKeyspaces as fast as possible and display the QPS. -func qps2(cell string, keyspaces []string) { +func qps(cell string, keyspaces []string) { var count sync2.AtomicInt32 for _, keyspace := range keyspaces { for i := 0; i < 10; i++ { @@ -221,42 +139,33 @@ func qps2(cell string, keyspaces []string) { } func main() { + defer exit.Recover() defer logutil.Flush() flag.Parse() args := flag.Args() if len(args) == 0 { flag.Usage() - os.Exit(1) + exit.Return(1) } if *cpuProfile != "" { f, err := os.Create(*cpuProfile) if err != nil { - log.Fatal(err) + log.Error(err) + exit.Return(1) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } - if *mode == "get" { - rpcClient := connect() - if len(args) == 1 { - get(rpcClient, args[0], true) - } else { - getv(rpcClient, args, true) - } - - } else if *mode == "children" { - rpcClient := connect() - children(rpcClient, args, true) - - } else if *mode == "getSrvKeyspaceNames" { + if *mode == "getSrvKeyspaceNames" { rpcClient := connect() if len(args) == 1 { getSrvKeyspaceNames(rpcClient, args[0], true) } else { - log.Fatalf("getSrvKeyspaceNames only takes one argument") + log.Errorf("getSrvKeyspaceNames only takes one argument") + exit.Return(1) } } else if *mode == "getSrvKeyspace" { @@ -264,7 +173,8 @@ func main() { if len(args) == 2 { getSrvKeyspace(rpcClient, args[0], args[1], true) } else { - log.Fatalf("getSrvKeyspace only takes two arguments") + log.Errorf("getSrvKeyspace only takes two arguments") + exit.Return(1) } } else if *mode == "getEndPoints" { @@ -272,17 +182,16 @@ func main() { if len(args) == 4 { getEndPoints(rpcClient, args[0], args[1], args[2], args[3], true) } else { - log.Fatalf("getEndPoints only takes four arguments") + log.Errorf("getEndPoints only takes four arguments") + exit.Return(1) } } else if *mode == "qps" { - qps(args) - - } else if *mode == "qps2" { - qps2(args[0], args[1:]) + qps(args[0], args[1:]) } else { flag.Usage() - log.Fatalf("Invalid mode: %v", *mode) + log.Errorf("Invalid mode: %v", *mode) + exit.Return(1) } } diff --git a/go/cmd/zkctl/zkctl.go b/go/cmd/zkctl/zkctl.go index c9a5165c2aa..f66db4b7bf9 100644 --- a/go/cmd/zkctl/zkctl.go +++ b/go/cmd/zkctl/zkctl.go @@ -13,6 +13,7 @@ import ( "strings" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/zk/zkctl" ) @@ -53,6 +54,7 @@ func confirm(prompt string) bool { } func main() { + defer exit.Recover() defer logutil.Flush() flag.Parse() @@ -60,7 +62,7 @@ func main() { if len(args) == 0 { flag.Usage() - os.Exit(1) + exit.Return(1) } zkConfig := zkctl.MakeZkConfigFromString(*zkCfg, uint32(*myId)) @@ -78,9 +80,11 @@ func main() { case "teardown": err = zkd.Teardown() default: - log.Fatalf("invalid action: %v", action) + log.Errorf("invalid action: %v", action) + exit.Return(1) } if err != nil { - log.Fatalf("failed %v: %v", action, err) + log.Errorf("failed %v: %v", action, err) + exit.Return(1) } } diff --git a/go/cmd/zkns2pdns/pdns.go b/go/cmd/zkns2pdns/pdns.go index fb4215a8ac9..2c1bb9eae55 100644 --- a/go/cmd/zkns2pdns/pdns.go +++ b/go/cmd/zkns2pdns/pdns.go @@ -22,6 +22,7 @@ import ( "strings" log "github.com/golang/glog" + "github.com/youtube/vitess/go/exit" "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/stats" "github.com/youtube/vitess/go/vt/logutil" @@ -334,6 +335,7 @@ func (pd *pdns) Serve(r io.Reader, w io.Writer) { } func main() { + defer exit.Recover() defer logutil.Flush() zknsDomain := flag.String("zkns-domain", "", "The naming hierarchy portion to serve") @@ -345,12 +347,13 @@ func main() { go func() { err := http.ListenAndServe(*bindAddr, nil) if err != nil { - log.Fatalf("ListenAndServe: %s", err) + log.Errorf("ListenAndServe: %s", err) + exit.Return(1) } }() } - zconn := zk.NewMetaConn(false) + zconn := zk.NewMetaConn() fqdn := netutil.FullyQualifiedHostnameOrPanic() zr1 := newZknsResolver(zconn, fqdn, *zknsDomain, *zknsRoot) pd := &pdns{zr1} diff --git a/go/cmd/zkocc/toporeader.go b/go/cmd/zkocc/toporeader.go deleted file mode 100644 index 660353ebd39..00000000000 --- a/go/cmd/zkocc/toporeader.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "path" - - "code.google.com/p/go.net/context" - "github.com/youtube/vitess/go/vt/topo" - "github.com/youtube/vitess/go/zk" -) - -type TopoReader struct { - zkr zk.ZkReader -} - -// FIXME(ryszard): These methods are kinda copy-and-pasted from -// zktopo.Server. In the long-term, the TopoReader should just take a -// topo.Server, which would be backed by a caching ZooKeeper -// connection. - -func zkPathForVt(cell string) string { - return fmt.Sprintf("/zk/%v/vt/ns", cell) -} - -func zkPathForVtKeyspace(cell, keyspace string) string { - return path.Join(zkPathForVt(cell), keyspace) -} - -func zkPathForVtType(cell, keyspace, shard string, tabletType topo.TabletType) string { - return path.Join(zkPathForVt(cell), keyspace, shard, string(tabletType)) -} - -func (tr *TopoReader) GetSrvKeyspaceNames(ctx context.Context, req *topo.GetSrvKeyspaceNamesArgs, reply *topo.SrvKeyspaceNames) error { - vtPath := zkPathForVt(req.Cell) - zkrReply := &zk.ZkNode{} - if err := tr.zkr.Children(&zk.ZkPath{Path: vtPath}, zkrReply); err != nil { - return err - } - reply.Entries = zkrReply.Children - return nil -} - -func (tr *TopoReader) GetSrvKeyspace(ctx context.Context, req *topo.GetSrvKeyspaceArgs, reply *topo.SrvKeyspace) (err error) { - keyspacePath := zkPathForVtKeyspace(req.Cell, req.Keyspace) - zkrReply := &zk.ZkNode{} - if err := tr.zkr.Get(&zk.ZkPath{Path: keyspacePath}, zkrReply); err != nil { - return err - } - - keyspace := topo.NewSrvKeyspace(int64(zkrReply.Stat.Version())) - if len(zkrReply.Data) > 0 { - if err := json.Unmarshal([]byte(zkrReply.Data), keyspace); err != nil { - return fmt.Errorf("SrvKeyspace unmarshal failed: %v %v", zkrReply.Data, err) - } - } - *reply = *keyspace - return -} - -func (tr *TopoReader) GetEndPoints(ctx context.Context, req *topo.GetEndPointsArgs, reply *topo.EndPoints) (err error) { - tabletTypePath := zkPathForVtType(req.Cell, req.Keyspace, req.Shard, req.TabletType) - zkrReply := &zk.ZkNode{} - if err := tr.zkr.Get(&zk.ZkPath{Path: tabletTypePath}, zkrReply); err != nil { - return err - } - if len(zkrReply.Data) > 0 { - if err := json.Unmarshal([]byte(zkrReply.Data), reply); err != nil { - return fmt.Errorf("EndPoints unmarshal failed: %v %v", zkrReply.Data, err) - } - } - return nil -} diff --git a/go/cmd/zkocc/zkocc.go b/go/cmd/zkocc/zkocc.go deleted file mode 100644 index 4cd69ad9460..00000000000 --- a/go/cmd/zkocc/zkocc.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/youtube/vitess/go/vt/servenv" - "github.com/youtube/vitess/go/zk" - "github.com/youtube/vitess/go/zk/zkocc" -) - -var usage = `Cache open zookeeper connections and allow cheap read requests -through a lightweight RPC interface. The optional parameters are cell -names to try to connect to at startup, versus waiting for the first -request to connect. -` - -var ( - resolveLocal = flag.Bool("resolve-local", false, "if specified, will try to resolve /zk/local/ paths. If not set, they will fail.") -) - -func init() { - servenv.RegisterDefaultFlags() - servenv.ServiceMap["bsonrpc-vt-toporeader"] = true - servenv.ServiceMap["bsonrpc-auth-vt-toporeader"] = true - - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) - flag.PrintDefaults() - fmt.Fprintf(os.Stderr, usage) - } -} - -// zkocc: a proxy for zk -func main() { - flag.Parse() - servenv.Init() - - zkr := zkocc.NewZkReader(*resolveLocal, flag.Args()) - zk.RegisterZkReader(zkr) - - servenv.Register("toporeader", &TopoReader{zkr: zkr}) - servenv.RunDefault() -} diff --git a/go/event/event.go b/go/event/event.go index 14a9ac0773e..c22d39c1c00 100644 --- a/go/event/event.go +++ b/go/event/event.go @@ -77,7 +77,7 @@ var ( ) // BadListenerError is raised via panic() when AddListener is called with an -// invalid listener funcion. +// invalid listener function. type BadListenerError string func (why BadListenerError) Error() string { diff --git a/go/mysql/mysql.go b/go/mysql/mysql.go index 6543201d7cf..e32ee734b68 100644 --- a/go/mysql/mysql.go +++ b/go/mysql/mysql.go @@ -36,24 +36,36 @@ func init() { } const ( - DUP_ENTRY = C.ER_DUP_ENTRY - LOCK_WAIT_TIMEOUT = C.ER_LOCK_WAIT_TIMEOUT - LOCK_DEADLOCK = C.ER_LOCK_DEADLOCK - OPTION_PREVENTS_STATEMENT = C.ER_OPTION_PREVENTS_STATEMENT + // ErrDupEntry is C.ER_DUP_ENTRY + ErrDupEntry = C.ER_DUP_ENTRY - REDACTED_PASSWORD = "****" + // ErrLockWaitTimeout is C.ER_LOCK_WAIT_TIMEOUT + ErrLockWaitTimeout = C.ER_LOCK_WAIT_TIMEOUT + + // ErrLockDeadlock is C.ER_LOCK_DEADLOCK + ErrLockDeadlock = C.ER_LOCK_DEADLOCK + + // ErrOptionPreventsStatement is C.ER_OPTION_PREVENTS_STATEMENT + ErrOptionPreventsStatement = C.ER_OPTION_PREVENTS_STATEMENT + + // RedactedPassword is the password value used in redacted configs + RedactedPassword = "****" ) +// SqlError is the error structure returned from calling a mysql +// library function type SqlError struct { Num int Message string Query string } +// NewSqlError returns a new SqlError func NewSqlError(number int, format string, args ...interface{}) *SqlError { return &SqlError{Num: number, Message: fmt.Sprintf(format, args...)} } +// Error implements the error interface func (se *SqlError) Error() string { if se.Query == "" { return fmt.Sprintf("%v (errno %v)", se.Message, se.Num) @@ -61,6 +73,7 @@ func (se *SqlError) Error() string { return fmt.Sprintf("%v (errno %v) during query: %s", se.Message, se.Num, se.Query) } +// Number returns the internal mysql error code func (se *SqlError) Number() int { return se.Num } @@ -72,6 +85,7 @@ func handleError(err *error) { } } +// ConnectionParams contains all the parameters to use to connect to mysql type ConnectionParams struct { Host string `json:"host"` Port int `json:"port"` @@ -90,26 +104,32 @@ type ConnectionParams struct { SslKey string `json:"ssl_key"` } +// EnableMultiStatements will set the right flag on the parameters func (c *ConnectionParams) EnableMultiStatements() { c.Flags |= C.CLIENT_MULTI_STATEMENTS } +// EnableSSL will set the right flag on the parameters func (c *ConnectionParams) EnableSSL() { c.Flags |= C.CLIENT_SSL } +// SslEnabled returns if SSL is enabled func (c *ConnectionParams) SslEnabled() bool { return (c.Flags & C.CLIENT_SSL) != 0 } +// Redact will alter the ConnectionParams so they can be displayed func (c *ConnectionParams) Redact() { - c.Pass = REDACTED_PASSWORD + c.Pass = RedactedPassword } +// Connection encapsulates a C mysql library connection type Connection struct { c C.VT_CONN } +// Connect uses the connection parameters to connect and returns the connection func Connect(params ConnectionParams) (conn *Connection, err error) { defer handleError(&err) @@ -122,28 +142,31 @@ func Connect(params ConnectionParams) (conn *Connection, err error) { defer cfree(pass) dbname := C.CString(params.DbName) defer cfree(dbname) - unix_socket := C.CString(params.UnixSocket) - defer cfree(unix_socket) + unixSocket := C.CString(params.UnixSocket) + defer cfree(unixSocket) charset := C.CString(params.Charset) defer cfree(charset) flags := C.ulong(params.Flags) conn = &Connection{} - if C.vt_connect(&conn.c, host, uname, pass, dbname, port, unix_socket, charset, flags) != 0 { + if C.vt_connect(&conn.c, host, uname, pass, dbname, port, unixSocket, charset, flags) != 0 { defer conn.Close() return nil, conn.lastError("") } return conn, nil } +// Close closes the mysql connection func (conn *Connection) Close() { C.vt_close(&conn.c) } +// IsClosed returns if the connection was ever closed func (conn *Connection) IsClosed() bool { return conn.c.mysql == nil } +// ExecuteFetch executes the query on the connection func (conn *Connection) ExecuteFetch(query string, maxrows int, wantfields bool) (qr *proto.QueryResult, err error) { if conn.IsClosed() { return nil, NewSqlError(2006, "Connection is closed") @@ -192,7 +215,8 @@ func (conn *Connection) ExecuteFetchMap(query string) (map[string]string, error) return rowMap, nil } -// when using ExecuteStreamFetch, use FetchNext on the Connection until it returns nil or error +// ExecuteStreamFetch starts a streaming query to mysql. Use FetchNext +// on the Connection until it returns nil or error func (conn *Connection) ExecuteStreamFetch(query string) (err error) { if conn.IsClosed() { return NewSqlError(2006, "Connection is closed") @@ -203,6 +227,7 @@ func (conn *Connection) ExecuteStreamFetch(query string) (err error) { return nil } +// Fields returns the current fields description for the query func (conn *Connection) Fields() (fields []proto.Field) { nfields := int(conn.c.num_fields) if nfields == 0 { @@ -238,6 +263,7 @@ func (conn *Connection) fetchAll() (rows [][]sqltypes.Value, err error) { return rows, nil } +// FetchNext returns the next row for a query func (conn *Connection) FetchNext() (row []sqltypes.Value, err error) { vtrow := C.vt_fetch_next(&conn.c) if vtrow.has_error != 0 { @@ -269,12 +295,13 @@ func (conn *Connection) FetchNext() (row []sqltypes.Value, err error) { return row, nil } +// CloseResult finishes the result set func (conn *Connection) CloseResult() { C.vt_close_result(&conn.c) } -// Id returns the MySQL thread_id of the connection. -func (conn *Connection) Id() int64 { +// ID returns the MySQL thread_id of the connection. +func (conn *Connection) ID() int64 { if conn.c.mysql == nil { return 0 } @@ -316,13 +343,13 @@ func (conn *Connection) SendCommand(command uint32, data []byte) error { return nil } -// ForceClose closes a MySQL connection forcibly at the socket level, instead of -// gracefully through mysql_close(). This is necessary when a thread is blocked -// in a call to ReadPacket(), and another thread wants to cancel the read. We -// can't use mysql_close() because it isn't safe to use while another thread is -// blocked in an I/O call on that MySQL connection. -func (conn *Connection) ForceClose() { - C.vt_force_close(&conn.c) +// Shutdown invokes the low-level shutdown call on the socket associated with +// a MySQL connection to stop ongoing communication. This is necessary when a +// thread is blocked in a MySQL I/O call, such as ReadPacket(), and another +// thread wants to cancel the operation. We can't use mysql_close() because it +// isn't thread-safe. +func (conn *Connection) Shutdown() { + C.vt_shutdown(&conn.c) } // GetCharset returns the current numerical values of the per-session character @@ -373,6 +400,7 @@ func (conn *Connection) SetCharset(cs proto.Charset) error { return err } +// BuildValue returns a sqltypes.Value from the passed in fields func BuildValue(bytes []byte, fieldType uint32) sqltypes.Value { if bytes == nil { return sqltypes.NULL diff --git a/go/mysql/proto/structs.go b/go/mysql/proto/structs.go index 7c0fdd3a1ea..1615dcc68de 100644 --- a/go/mysql/proto/structs.go +++ b/go/mysql/proto/structs.go @@ -41,12 +41,14 @@ const ( VT_GEOMETRY = 255 ) -// Field described a column returned by mysql +// Field describes a column returned by MySQL. type Field struct { Name string Type int64 } +//go:generate bsongen -file $GOFILE -type Field -o field_bson.go + // QueryResult is the structure returned by the mysql library. // When transmitted over the wire, the Rows all come back as strings // and lose their original sqltypes. use Fields.Type to convert @@ -58,6 +60,8 @@ type QueryResult struct { Rows [][]sqltypes.Value } +//go:generate bsongen -file $GOFILE -type QueryResult -o query_result_bson.go + // Charset contains the per-statement character set settings that accompany // binlog QUERY_EVENT entries. type Charset struct { @@ -66,10 +70,11 @@ type Charset struct { Server int // @@session.collation_server } +//go:generate bsongen -file $GOFILE -type Charset -o charset_bson.go + // Convert takes a type and a value, and returns the type: // - nil for NULL value -// - int64 for integer number types that fit in 64 bits -// (signed or unsigned are all converted to signed) +// - int64 if possible, otherwise, uint64 // - float64 for floating point values that fit in a float // - []byte for everything else func Convert(mysqlType int64, val sqltypes.Value) (interface{}, error) { @@ -79,7 +84,16 @@ func Convert(mysqlType int64, val sqltypes.Value) (interface{}, error) { switch mysqlType { case VT_TINY, VT_SHORT, VT_LONG, VT_LONGLONG, VT_INT24: - return strconv.ParseInt(val.String(), 0, 64) + val := val.String() + signed, err := strconv.ParseInt(val, 0, 64) + if err == nil { + return signed, nil + } + unsigned, err := strconv.ParseUint(val, 0, 64) + if err == nil { + return unsigned, nil + } + return nil, err case VT_FLOAT, VT_DOUBLE: return strconv.ParseFloat(val.String(), 64) } diff --git a/go/mysql/proto/structs_test.go b/go/mysql/proto/structs_test.go new file mode 100644 index 00000000000..568e6b67f96 --- /dev/null +++ b/go/mysql/proto/structs_test.go @@ -0,0 +1,92 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proto + +import ( + "testing" + + "github.com/youtube/vitess/go/sqltypes" +) + +func TestConvert(t *testing.T) { + cases := []struct { + Desc string + Typ int64 + Val sqltypes.Value + Want interface{} + }{{ + Desc: "null", + Typ: VT_LONG, + Val: sqltypes.Value{}, + Want: nil, + }, { + Desc: "decimal", + Typ: VT_DECIMAL, + Val: sqltypes.MakeString([]byte("aa")), + Want: "aa", + }, { + Desc: "tiny", + Typ: VT_TINY, + Val: sqltypes.MakeString([]byte("1")), + Want: int64(1), + }, { + Desc: "short", + Typ: VT_SHORT, + Val: sqltypes.MakeString([]byte("1")), + Want: int64(1), + }, { + Desc: "long", + Typ: VT_LONG, + Val: sqltypes.MakeString([]byte("1")), + Want: int64(1), + }, { + Desc: "longlong", + Typ: VT_LONGLONG, + Val: sqltypes.MakeString([]byte("1")), + Want: int64(1), + }, { + Desc: "int24", + Typ: VT_INT24, + Val: sqltypes.MakeString([]byte("1")), + Want: int64(1), + }, { + Desc: "float", + Typ: VT_FLOAT, + Val: sqltypes.MakeString([]byte("1")), + Want: float64(1), + }, { + Desc: "double", + Typ: VT_DOUBLE, + Val: sqltypes.MakeString([]byte("1")), + Want: float64(1), + }, { + Desc: "large int", + Typ: VT_LONGLONG, + Val: sqltypes.MakeString([]byte("9223372036854775808")), + Want: uint64(9223372036854775808), + }, { + Desc: "float for int", + Typ: VT_LONGLONG, + Val: sqltypes.MakeString([]byte("1.1")), + Want: `strconv.ParseUint: parsing "1.1": invalid syntax`, + }, { + Desc: "string for float", + Typ: VT_FLOAT, + Val: sqltypes.MakeString([]byte("aa")), + Want: `strconv.ParseFloat: parsing "aa": invalid syntax`, + }} + + for _, c := range cases { + r, err := Convert(c.Typ, c.Val) + if err != nil { + r = err.Error() + } else if _, ok := r.([]byte); ok { + r = string(r.([]byte)) + } + if r != c.Want { + t.Errorf("%s: %+v, want %+v", c.Desc, r, c.Want) + } + } +} diff --git a/go/mysql/vtmysql.c b/go/mysql/vtmysql.c index 1da64b55cfc..b238520d4c2 100644 --- a/go/mysql/vtmysql.c +++ b/go/mysql/vtmysql.c @@ -47,7 +47,7 @@ int vt_connect( } void vt_close(VT_CONN *conn) { - if(conn->mysql) { + if (conn->mysql) { mysql_thread_init(); mysql_close(conn->mysql); conn->mysql = 0; @@ -161,10 +161,10 @@ unsigned long vt_cli_safe_read(VT_CONN *conn) { return len == packet_error ? 0 : len; } -void vt_force_close(VT_CONN *conn) { +void vt_shutdown(VT_CONN *conn) { mysql_thread_init(); - // Close the underlying socket of a MYSQL connection object. + // Shut down the underlying socket of a MYSQL connection object. if (conn->mysql->net.vio) - vio_close(conn->mysql->net.vio); + vio_socket_shutdown(conn->mysql->net.vio, 2 /* SHUT_RDWR */); } diff --git a/go/mysql/vtmysql.h b/go/mysql/vtmysql.h index c7bee9fe681..f28ffde50e8 100644 --- a/go/mysql/vtmysql.h +++ b/go/mysql/vtmysql.h @@ -63,6 +63,6 @@ my_bool vt_simple_command( // or 0 if there was an error. unsigned long vt_cli_safe_read(VT_CONN *conn); -// vt_force_close: Kill a MySQL connection at the socket level, to unblock +// vt_shutdown: Kill a MySQL connection at the socket level, to unblock // a thread that is waiting on a read call. -void vt_force_close(VT_CONN *conn); +void vt_shutdown(VT_CONN *conn); diff --git a/go/mysql/vtmysql_internals.h b/go/mysql/vtmysql_internals.h index 5ab5dee2000..bf5e23f1526 100644 --- a/go/mysql/vtmysql_internals.h +++ b/go/mysql/vtmysql_internals.h @@ -13,8 +13,9 @@ #define NULL ((void*)0) #endif -// vio_close is not declared anywhere in the libmysqlclient headers. -int vio_close(Vio*); +// These low-level vio functions are not declared +// anywhere in the libmysqlclient headers. +int vio_socket_shutdown(Vio *vio, int how); // cli_safe_read is declared in sql_common.h. unsigned long cli_safe_read(MYSQL *mysql); diff --git a/go/netutil/netutil.go b/go/netutil/netutil.go index 2d057952e52..46bf5705fad 100644 --- a/go/netutil/netutil.go +++ b/go/netutil/netutil.go @@ -23,13 +23,13 @@ func init() { // byPriorityWeight sorts records by ascending priority and weight. type byPriorityWeight []*net.SRV -func (s byPriorityWeight) Len() int { return len(s) } +func (addrs byPriorityWeight) Len() int { return len(addrs) } -func (s byPriorityWeight) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (addrs byPriorityWeight) Swap(i, j int) { addrs[i], addrs[j] = addrs[j], addrs[i] } -func (s byPriorityWeight) Less(i, j int) bool { - return s[i].Priority < s[j].Priority || - (s[i].Priority == s[j].Priority && s[i].Weight < s[j].Weight) +func (addrs byPriorityWeight) Less(i, j int) bool { + return addrs[i].Priority < addrs[j].Priority || + (addrs[i].Priority == addrs[j].Priority && addrs[i].Weight < addrs[j].Weight) } // shuffleByWeight shuffles SRV records by weight using the algorithm @@ -76,20 +76,33 @@ func SortRfc2782(srvs []*net.SRV) { byPriorityWeight(srvs).sortRfc2782() } -// SplitHostPort is an extension to net.SplitHostPort that also parses the -// integer port +// SplitHostPort is an alternative to net.SplitHostPort that also parses the +// integer port. In addition, it is more tolerant of improperly escaped IPv6 +// addresses, such as "::1:456", which should actually be "[::1]:456". func SplitHostPort(addr string) (string, int, error) { host, port, err := net.SplitHostPort(addr) if err != nil { - return "", 0, err + // If the above proper parsing fails, fall back on a naive split. + i := strings.LastIndex(addr, ":") + if i < 0 { + return "", 0, fmt.Errorf("SplitHostPort: missing port in %q", addr) + } + host = addr[:i] + port = addr[i+1:] } - p, err := strconv.ParseInt(port, 10, 16) + p, err := strconv.ParseUint(port, 10, 16) if err != nil { - return "", 0, err + return "", 0, fmt.Errorf("SplitHostPort: can't parse port %q: %v", port, err) } return host, int(p), nil } +// JoinHostPort is an extension to net.JoinHostPort that also formats the +// integer port. +func JoinHostPort(host string, port int) string { + return net.JoinHostPort(host, strconv.FormatInt(int64(port), 10)) +} + // FullyQualifiedHostname returns the full hostname with domain func FullyQualifiedHostname() (string, error) { hostname, err := os.Hostname() @@ -115,9 +128,9 @@ func FullyQualifiedHostnameOrPanic() string { } // ResolveAddr can resolve an address where the host has been left -// blank, like ":3306" +// blank, like ":3306". func ResolveAddr(addr string) (string, error) { - host, port, err := SplitHostPort(addr) + host, port, err := net.SplitHostPort(addr) if err != nil { return "", err } @@ -127,7 +140,7 @@ func ResolveAddr(addr string) (string, error) { return "", err } } - return fmt.Sprintf("%v:%v", host, port), nil + return net.JoinHostPort(host, port), nil } // ResolveIpAddr resolves the address:port part into an IP address:port pair @@ -143,7 +156,7 @@ func ResolveIpAddr(addr string) (string, error) { return net.JoinHostPort(ipAddrs[0], port), nil } -// ResolveIpAddr resolves the address:port part into an IP address:port pair +// ResolveIPv4Addr resolves the address:port part into an IP address:port pair func ResolveIPv4Addr(addr string) (string, error) { host, port, err := net.SplitHostPort(addr) if err != nil { diff --git a/go/netutil/netutil_test.go b/go/netutil/netutil_test.go index af86f63d72c..cdd5445eefe 100644 --- a/go/netutil/netutil_test.go +++ b/go/netutil/netutil_test.go @@ -64,3 +64,55 @@ func testWeighting(t *testing.T, margin float64) { func TestWeighting(t *testing.T) { testWeighting(t, 0.05) } + +func TestSplitHostPort(t *testing.T) { + type addr struct { + host string + port int + } + table := map[string]addr{ + "host-name:132": addr{host: "host-name", port: 132}, + "hostname:65535": addr{host: "hostname", port: 65535}, + "[::1]:321": addr{host: "::1", port: 321}, + "::1:432": addr{host: "::1", port: 432}, + } + for input, want := range table { + gotHost, gotPort, err := SplitHostPort(input) + if err != nil { + t.Errorf("SplitHostPort error: %v", err) + } + if gotHost != want.host || gotPort != want.port { + t.Errorf("SplitHostPort(%#v) = (%v, %v), want (%v, %v)", input, gotHost, gotPort, want.host, want.port) + } + } +} + +func TestSplitHostPortFail(t *testing.T) { + // These cases should all fail to parse. + inputs := []string{ + "host-name", + "host-name:123abc", + } + for _, input := range inputs { + _, _, err := SplitHostPort(input) + if err == nil { + t.Errorf("expected error from SplitHostPort(%q), but got none", input) + } + } +} + +func TestJoinHostPort(t *testing.T) { + type addr struct { + host string + port int + } + table := map[string]addr{ + "host-name:132": addr{host: "host-name", port: 132}, + "[::1]:321": addr{host: "::1", port: 321}, + } + for want, input := range table { + if got := JoinHostPort(input.host, input.port); got != want { + t.Errorf("SplitHostPort(%v, %v) = %#v, want %#v", input.host, input.port, got, want) + } + } +} diff --git a/go/pools/resource_pool.go b/go/pools/resource_pool.go index 1883b3e2596..f29b1bd1311 100644 --- a/go/pools/resource_pool.go +++ b/go/pools/resource_pool.go @@ -47,8 +47,11 @@ type resourceWrapper struct { } // NewResourcePool creates a new ResourcePool pool. -// capacity is the initial capacity of the pool. -// maxCap is the maximum capacity. +// capacity is the number of active resources in the pool: +// there can be up to 'capacity' of these at a given time. +// maxCap specifies the extent to which the pool can be resized +// in the future through the SetCapacity function. +// You cannot resize the pool beyond maxCap. // If a resource is unused beyond idleTimeout, it's discarded. // An idleTimeout of 0 means that there is no timeout. func NewResourcePool(factory Factory, capacity, maxCap int, idleTimeout time.Duration) *ResourcePool { diff --git a/go/rpcplus/client.go b/go/rpcplus/client.go index f23d48cc874..3e4520a76bb 100644 --- a/go/rpcplus/client.go +++ b/go/rpcplus/client.go @@ -15,8 +15,8 @@ import ( "reflect" "sync" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/trace" + "golang.org/x/net/context" ) // ServerError represents an error that has been returned from @@ -27,6 +27,7 @@ func (e ServerError) Error() string { return string(e) } +// ErrShutdown holds the specific error for closing/closed connections var ErrShutdown = errors.New("connection is shut down") // Call represents an active RPC. @@ -303,6 +304,7 @@ func Dial(network, address string) (*Client, error) { return NewClient(conn), nil } +// Close closes the client connection func (client *Client) Close() error { client.mutex.Lock() if client.shutdown || client.closing { @@ -343,7 +345,7 @@ func (client *Client) Go(ctx context.Context, serviceMethod string, args interfa return call } -// Go invokes the streaming function asynchronously. It returns the Call structure representing +// StreamGo invokes the streaming function asynchronously. It returns the Call structure representing // the invocation. func (client *Client) StreamGo(serviceMethod string, args interface{}, replyStream interface{}) *Call { // first check the replyStream object is a stream of pointers to a data structure diff --git a/go/rpcplus/jsonrpc/all_test.go b/go/rpcplus/jsonrpc/all_test.go index 7775b24e48f..1543c6cf94a 100644 --- a/go/rpcplus/jsonrpc/all_test.go +++ b/go/rpcplus/jsonrpc/all_test.go @@ -12,7 +12,7 @@ import ( "net" "testing" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/rpcplus" ) @@ -67,7 +67,7 @@ func init() { func TestServer(t *testing.T) { type addResp struct { - Id interface{} `json:"id"` + ID interface{} `json:"id"` Result Reply `json:"result"` Error interface{} `json:"error"` } @@ -88,8 +88,8 @@ func TestServer(t *testing.T) { if resp.Error != nil { t.Fatalf("resp.Error: %s", resp.Error) } - if resp.Id.(string) != string(i) { - t.Fatalf("resp: bad id %q want %q", resp.Id.(string), string(i)) + if resp.ID.(string) != string(i) { + t.Fatalf("resp: bad id %q want %q", resp.ID.(string), string(i)) } if resp.Result.C != 2*i+1 { t.Fatalf("resp: bad result: %d+%d=%d", i, i+1, resp.Result.C) @@ -203,7 +203,7 @@ func TestStreamingCall(t *testing.T) { if row.C != count { t.Fatal("unexpected value:", row.C) } - count += 1 + count++ // log.Println("Values: ", row) } diff --git a/go/rpcplus/jsonrpc/client.go b/go/rpcplus/jsonrpc/client.go index 777a7d95b59..8306f9e32cf 100644 --- a/go/rpcplus/jsonrpc/client.go +++ b/go/rpcplus/jsonrpc/client.go @@ -7,10 +7,13 @@ package jsonrpc import ( + "bytes" "encoding/json" + "errors" "fmt" "io" "net" + "net/http" "sync" rpc "github.com/youtube/vitess/go/rpcplus" @@ -46,7 +49,7 @@ func NewClientCodec(conn io.ReadWriteCloser) rpc.ClientCodec { type clientRequest struct { Method string `json:"method"` Params [1]interface{} `json:"params"` - Id uint64 `json:"id"` + ID uint64 `json:"id"` } func (c *clientCodec) WriteRequest(r *rpc.Request, param interface{}) error { @@ -55,18 +58,18 @@ func (c *clientCodec) WriteRequest(r *rpc.Request, param interface{}) error { c.mutex.Unlock() c.req.Method = r.ServiceMethod c.req.Params[0] = param - c.req.Id = r.Seq + c.req.ID = r.Seq return c.enc.Encode(&c.req) } type clientResponse struct { - Id uint64 `json:"id"` + ID uint64 `json:"id"` Result *json.RawMessage `json:"result"` Error interface{} `json:"error"` } func (r *clientResponse) reset() { - r.Id = 0 + r.ID = 0 r.Result = nil r.Error = nil } @@ -78,12 +81,12 @@ func (c *clientCodec) ReadResponseHeader(r *rpc.Response) error { } c.mutex.Lock() - r.ServiceMethod = c.pending[c.resp.Id] - delete(c.pending, c.resp.Id) + r.ServiceMethod = c.pending[c.resp.ID] + delete(c.pending, c.resp.ID) c.mutex.Unlock() r.Error = "" - r.Seq = c.resp.Id + r.Seq = c.resp.ID if c.resp.Error != nil { x, ok := c.resp.Error.(string) if !ok { @@ -122,3 +125,66 @@ func Dial(network, address string) (*rpc.Client, error) { } return NewClient(conn), err } + +// HTTPClient holds the required parameters and functions for communicating with +// the HTTP RPC server +type HTTPClient struct { + Addr string + seq uint64 + m sync.Mutex +} + +// NewHTTPClient creates a helper json rpc client for regular http based +// endpoints +func NewHTTPClient(addr string) *HTTPClient { + return &HTTPClient{ + Addr: addr, + seq: 0, + m: sync.Mutex{}, + } +} + +// Call calls the http rpc endpoint with given parameters, uses POST request and +// can be called by multiple go routines +func (h *HTTPClient) Call(serviceMethod string, args interface{}, reply interface{}) error { + var params [1]interface{} + params[0] = args + + h.m.Lock() + seq := h.seq + h.seq++ + h.m.Unlock() + + cr := &clientRequest{ + Method: serviceMethod, + Params: params, + ID: seq, + } + + byteData, err := json.Marshal(cr) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", h.Addr, bytes.NewReader(byteData)) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + v := &clientResponse{} + err = json.NewDecoder(res.Body).Decode(v) + if err != nil { + return err + } + + if v.Error != nil { + return errors.New(v.Error.(string)) + } + + return json.Unmarshal(*v.Result, reply) +} diff --git a/go/rpcplus/jsonrpc/server.go b/go/rpcplus/jsonrpc/server.go index a9564328f25..d2971f5f21f 100644 --- a/go/rpcplus/jsonrpc/server.go +++ b/go/rpcplus/jsonrpc/server.go @@ -10,8 +10,8 @@ import ( "io" "sync" - "code.google.com/p/go.net/context" rpc "github.com/youtube/vitess/go/rpcplus" + "golang.org/x/net/context" ) type serverCodec struct { @@ -47,7 +47,7 @@ func NewServerCodec(conn io.ReadWriteCloser) rpc.ServerCodec { type serverRequest struct { Method string `json:"method"` Params *json.RawMessage `json:"params"` - Id *json.RawMessage `json:"id"` + ID *json.RawMessage `json:"id"` } func (r *serverRequest) reset() { @@ -55,13 +55,13 @@ func (r *serverRequest) reset() { if r.Params != nil { *r.Params = (*r.Params)[0:0] } - if r.Id != nil { - *r.Id = (*r.Id)[0:0] + if r.ID != nil { + *r.ID = (*r.ID)[0:0] } } type serverResponse struct { - Id *json.RawMessage `json:"id"` + ID *json.RawMessage `json:"id"` Result interface{} `json:"result"` Error interface{} `json:"error"` } @@ -78,8 +78,8 @@ func (c *serverCodec) ReadRequestHeader(r *rpc.Request) error { // internal uint64 and save JSON on the side. c.mutex.Lock() c.seq++ - c.pending[c.seq] = c.req.Id - c.req.Id = nil + c.pending[c.seq] = c.req.ID + c.req.ID = nil r.Seq = c.seq c.mutex.Unlock() @@ -118,7 +118,7 @@ func (c *serverCodec) WriteResponse(r *rpc.Response, x interface{}, last bool) e // Invalid request so no id. Use JSON null. b = &null } - resp.Id = b + resp.ID = b resp.Result = x if r.Error == "" { resp.Error = nil @@ -128,6 +128,7 @@ func (c *serverCodec) WriteResponse(r *rpc.Response, x interface{}, last bool) e return c.enc.Encode(resp) } +// Close closes the server connection func (c *serverCodec) Close() error { return c.c.Close() } diff --git a/go/rpcplus/server.go b/go/rpcplus/server.go index 576163c4318..c6d6dd7cf70 100644 --- a/go/rpcplus/server.go +++ b/go/rpcplus/server.go @@ -138,12 +138,14 @@ import ( "unicode" "unicode/utf8" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" ) const ( - // Defaults used by HandleHTTP - DefaultRPCPath = "/_goRPC_" + // DefaultRPCPath used as handler location for HandleHTTP + DefaultRPCPath = "/_goRPC_" + + // DefaultDebugPath used as debug handler location for HandleHTTP DefaultDebugPath = "/debug/rpc" ) @@ -566,11 +568,11 @@ func (server *Server) ServeCodecWithContext(ctx context.Context, codec ServerCod codec.Close() } -func (mtype *methodType) prepareContext(ctx context.Context) reflect.Value { +func (m *methodType) prepareContext(ctx context.Context) reflect.Value { if contextv := reflect.ValueOf(ctx); contextv.IsValid() { return contextv } - return reflect.Zero(mtype.ContextType) + return reflect.Zero(m.ContextType) } // ServeRequest is like ServeCodec but synchronously serves a single request. diff --git a/go/rpcplus/server_test.go b/go/rpcplus/server_test.go index 6ca7306bccf..9f87b4f9d23 100644 --- a/go/rpcplus/server_test.go +++ b/go/rpcplus/server_test.go @@ -18,7 +18,7 @@ import ( "testing" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" ) var ( @@ -29,7 +29,7 @@ var ( ) const ( - newHttpPath = "/foo" + newHTTPPath = "/foo" ) type Args struct { @@ -97,7 +97,7 @@ func startServer() { go Accept(l) HandleHTTP() - httpOnce.Do(startHttpServer) + httpOnce.Do(startHTTPServer) } func startNewServer() { @@ -109,11 +109,11 @@ func startNewServer() { log.Println("NewServer test RPC server listening on", newServerAddr) go Accept(l) - newServer.HandleHTTP(newHttpPath, "/bar") - httpOnce.Do(startHttpServer) + newServer.HandleHTTP(newHTTPPath, "/bar") + httpOnce.Do(startHTTPServer) } -func startHttpServer() { +func startHTTPServer() { server := httptest.NewServer(nil) httpServerAddr = server.Listener.Addr().String() log.Println("Test HTTP RPC server listening on", httpServerAddr) @@ -254,7 +254,7 @@ func TestHTTP(t *testing.T) { once.Do(startServer) testHTTPRPC(ctx, t, "") newOnce.Do(startNewServer) - testHTTPRPC(ctx, t, newHttpPath) + testHTTPRPC(ctx, t, newHTTPPath) } func testHTTPRPC(ctx context.Context, t *testing.T, path string) { diff --git a/go/rpcplus/streaming_test.go b/go/rpcplus/streaming_test.go index 7fff542d3fa..143685a501e 100644 --- a/go/rpcplus/streaming_test.go +++ b/go/rpcplus/streaming_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" ) const ( @@ -28,13 +28,15 @@ type StreamingReply struct { Index int } +var errTriggeredInTheMiddle = errors.New("triggered error in middle") + type StreamingArith int func (t *StreamingArith) Thrive(args StreamingArgs, sendReply func(reply interface{}) error) error { for i := 0; i < args.Count; i++ { if i == args.ErrorAt { - return errors.New("Triggered error in middle") + return errTriggeredInTheMiddle } if i == args.BadTypeAt { // send args instead of response @@ -92,7 +94,7 @@ func callOnceAndCheck(t *testing.T, client *Client) { if row.Index != count { t.Fatal("unexpected value:", row.Index) } - count += 1 + count++ // log.Println("Values: ", row.C, row.Index) } @@ -168,12 +170,12 @@ func TestInterruptedCallByServer(t *testing.T) { if row.Index != count { t.Fatal("unexpected value:", row.Index) } - count += 1 + count++ } if count != 30 { t.Fatal("received error before the right time:", count) } - if c.Error.Error() != "Triggered error in middle" { + if c.Error.Error() != errTriggeredInTheMiddle.Error() { t.Fatal("received wrong error message:", c.Error) } @@ -188,7 +190,7 @@ func TestInterruptedCallByServer(t *testing.T) { if ok { t.Fatal("expected closed channel") } - if c.Error.Error() != "Triggered error in middle" { + if c.Error.Error() != errTriggeredInTheMiddle.Error() { t.Fatal("received wrong error message:", c.Error) } @@ -211,7 +213,7 @@ func TestBadTypeByServer(t *testing.T) { if row.Index != count { t.Fatal("unexpected value:", row.Index) } - count += 1 + count++ } if count != 30 { t.Fatal("received error before the right time:", count) diff --git a/go/rpcwrap/auth/authentication.go b/go/rpcwrap/auth/authentication.go index 5fdf0c29f80..c6f5a8f8cf3 100644 --- a/go/rpcwrap/auth/authentication.go +++ b/go/rpcwrap/auth/authentication.go @@ -1,3 +1,4 @@ +// Package auth provides authentication codecs package auth import ( @@ -7,10 +8,10 @@ import ( "io/ioutil" "strings" - "code.google.com/p/go.net/context" log "github.com/golang/glog" rpc "github.com/youtube/vitess/go/rpcplus" "github.com/youtube/vitess/go/rpcwrap/proto" + "golang.org/x/net/context" ) // UnusedArgument is a type used to indicate an argument that is @@ -46,7 +47,10 @@ func (c *cramMD5Credentials) Load(filename string) error { const CRAMMD5MaxRequests = 2 var ( - AuthenticationServer = rpc.NewServer() + // AuthenticationServer holds a default server for authentication + AuthenticationServer = rpc.NewServer() + + // DefaultAuthenticatorCRAMMD5 holds a default authenticator DefaultAuthenticatorCRAMMD5 = NewAuthenticatorCRAMMD5() ) @@ -54,6 +58,7 @@ var ( // authenticate. var AuthenticationFailed = errors.New("authentication error: authentication failed") +// NewAuthenticatorCRAMMD5 creates a new CRAM-MD5 authenticator func NewAuthenticatorCRAMMD5() *AuthenticatorCRAMMD5 { return &AuthenticatorCRAMMD5{make(cramMD5Credentials)} } @@ -113,12 +118,15 @@ func (a *AuthenticatorCRAMMD5) Authenticate(ctx context.Context, req *Authentica return AuthenticationFailed } +// GetNewChallengeReply holds reply data for Challenge type GetNewChallengeReply struct { Challenge string } +// AuthenticateReply holds reply data for Authenticate type AuthenticateReply struct{} +// AuthenticateRequest holds request data for AuthenticateRequest type AuthenticateRequest struct { Proof string state authenticationState diff --git a/go/rpcwrap/auth/crammd5.go b/go/rpcwrap/auth/crammd5.go index bc3a4cf132b..cdec2a73234 100644 --- a/go/rpcwrap/auth/crammd5.go +++ b/go/rpcwrap/auth/crammd5.go @@ -11,6 +11,7 @@ import ( "time" ) +// CRAMMD5GetChallenge creates a new RFC822 compatible ID func CRAMMD5GetChallenge() (string, error) { var randDigits uint32 err := binary.Read(rand.Reader, binary.LittleEndian, &randDigits) @@ -27,6 +28,7 @@ func CRAMMD5GetChallenge() (string, error) { return fmt.Sprintf("<%d.%d@%s>", randDigits, timestamp, hostname), nil } +// CRAMMD5GetExpected creates a "possible" ID with the given credentials func CRAMMD5GetExpected(username, secret, challenge string) string { var ret []byte hash := hmac.New(md5.New, []byte(secret)) diff --git a/go/rpcwrap/bsonrpc/codecs.go b/go/rpcwrap/bsonrpc/codecs.go index de093f09700..0831f0fa21e 100644 --- a/go/rpcwrap/bsonrpc/codecs.go +++ b/go/rpcwrap/bsonrpc/codecs.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package bsonrpc provides codecs for bsonrpc communication package bsonrpc import ( @@ -20,16 +21,21 @@ const ( codecName = "bson" ) +// ClientCodec holds required parameters for providing a client codec for +// bsonrpc type ClientCodec struct { rwc io.ReadWriteCloser } +// NewClientCodec creates a new client codec for bsonrpc communication func NewClientCodec(conn io.ReadWriteCloser) rpc.ClientCodec { return &ClientCodec{conn} } +// DefaultBufferSize holds the default value for buffer size const DefaultBufferSize = 4096 +// WriteRequest sends the request to the server func (cc *ClientCodec) WriteRequest(r *rpc.Request, body interface{}) error { buf := bytes2.NewChunkedWriter(DefaultBufferSize) if err := bson.MarshalToBuffer(buf, &RequestBson{r}); err != nil { @@ -42,35 +48,44 @@ func (cc *ClientCodec) WriteRequest(r *rpc.Request, body interface{}) error { return err } +// ReadResponseHeader reads the header of server response func (cc *ClientCodec) ReadResponseHeader(r *rpc.Response) error { return bson.UnmarshalFromStream(cc.rwc, &ResponseBson{r}) } +// ReadResponseBody reads the body of server response func (cc *ClientCodec) ReadResponseBody(body interface{}) error { return bson.UnmarshalFromStream(cc.rwc, body) } +// Close closes the codec func (cc *ClientCodec) Close() error { return cc.rwc.Close() } +// ServerCodec holds required parameters for providing a server codec for +// bsonrpc type ServerCodec struct { rwc io.ReadWriteCloser cw *bytes2.ChunkedWriter } +// NewServerCodec creates a new server codec for bsonrpc communication func NewServerCodec(conn io.ReadWriteCloser) rpc.ServerCodec { return &ServerCodec{conn, bytes2.NewChunkedWriter(DefaultBufferSize)} } +// ReadRequestHeader reads the header of the request func (sc *ServerCodec) ReadRequestHeader(r *rpc.Request) error { return bson.UnmarshalFromStream(sc.rwc, &RequestBson{r}) } +// ReadRequestBody reads the body of the request func (sc *ServerCodec) ReadRequestBody(body interface{}) error { return bson.UnmarshalFromStream(sc.rwc, body) } +// WriteResponse send the response of the request to the client func (sc *ServerCodec) WriteResponse(r *rpc.Response, body interface{}, last bool) error { if err := bson.MarshalToBuffer(sc.cw, &ResponseBson{r}); err != nil { return err @@ -83,26 +98,33 @@ func (sc *ServerCodec) WriteResponse(r *rpc.Response, body interface{}, last boo return err } +// Close closes the codec func (sc *ServerCodec) Close() error { return sc.rwc.Close() } +// DialHTTP dials a HTTP endpoint with bsonrpc codec func DialHTTP(network, address string, connectTimeout time.Duration, config *tls.Config) (*rpc.Client, error) { return rpcwrap.DialHTTP(network, address, codecName, NewClientCodec, connectTimeout, config) } +// DialAuthHTTP dials a HTTP endpoint with bsonrpc codec as authentication enabled func DialAuthHTTP(network, address, user, password string, connectTimeout time.Duration, config *tls.Config) (*rpc.Client, error) { return rpcwrap.DialAuthHTTP(network, address, user, password, codecName, NewClientCodec, connectTimeout, config) } +// ServeRPC serves bsonrpc codec with the default rpc server func ServeRPC() { rpcwrap.ServeRPC(codecName, NewServerCodec) } +// ServeAuthRPC serves bsonrpc codec with the default authentication enabled rpc +// server func ServeAuthRPC() { rpcwrap.ServeAuthRPC(codecName, NewServerCodec) } +// ServeCustomRPC serves bsonrpc codec with a custom rpc server func ServeCustomRPC(handler *http.ServeMux, server *rpc.Server, useAuth bool) { rpcwrap.ServeCustomRPC(handler, server, useAuth, codecName, NewServerCodec) } diff --git a/go/rpcwrap/bsonrpc/custom_codecs.go b/go/rpcwrap/bsonrpc/custom_codecs.go index fc50ca18c9d..2988afd0ba7 100644 --- a/go/rpcwrap/bsonrpc/custom_codecs.go +++ b/go/rpcwrap/bsonrpc/custom_codecs.go @@ -12,10 +12,12 @@ import ( rpc "github.com/youtube/vitess/go/rpcplus" ) +// RequestBson provides bson rpc request parameters type RequestBson struct { *rpc.Request } +// MarshalBson marshals request to the given writer with optional prefix func (req *RequestBson) MarshalBson(buf *bytes2.ChunkedWriter, key string) { bson.EncodeOptionalPrefix(buf, bson.Object, key) lenWriter := bson.NewLenWriter(buf) @@ -26,6 +28,8 @@ func (req *RequestBson) MarshalBson(buf *bytes2.ChunkedWriter, key string) { lenWriter.Close() } +// UnmarshalBson unmarshals request to the given byte buffer as verifying the +// kind func (req *RequestBson) UnmarshalBson(buf *bytes.Buffer, kind byte) { bson.VerifyObject(kind) bson.Next(buf, 4) @@ -45,10 +49,12 @@ func (req *RequestBson) UnmarshalBson(buf *bytes.Buffer, kind byte) { } } +// ResponseBson provides bson rpc request parameters type ResponseBson struct { *rpc.Response } +// MarshalBson marshals response to the given writer with optional prefix func (resp *ResponseBson) MarshalBson(buf *bytes2.ChunkedWriter, key string) { bson.EncodeOptionalPrefix(buf, bson.Object, key) lenWriter := bson.NewLenWriter(buf) @@ -60,6 +66,8 @@ func (resp *ResponseBson) MarshalBson(buf *bytes2.ChunkedWriter, key string) { lenWriter.Close() } +// UnmarshalBson unmarshals response to the given byte buffer as verifying the +// kind func (resp *ResponseBson) UnmarshalBson(buf *bytes.Buffer, kind byte) { bson.VerifyObject(kind) bson.Next(buf, 4) diff --git a/go/rpcwrap/jsonrpc/wrapper.go b/go/rpcwrap/jsonrpc/wrapper.go index ba1977a0097..1bbc91de719 100644 --- a/go/rpcwrap/jsonrpc/wrapper.go +++ b/go/rpcwrap/jsonrpc/wrapper.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package jsonrpc provides wrappers for json rpc communication package jsonrpc import ( @@ -13,14 +14,18 @@ import ( "github.com/youtube/vitess/go/rpcwrap" ) +// DialHTTP dials a json rpc HTTP endpoint with optional TLS config func DialHTTP(network, address string, connectTimeout time.Duration, config *tls.Config) (*rpc.Client, error) { return rpcwrap.DialHTTP(network, address, "json", oldjson.NewClientCodec, connectTimeout, config) } +// ServeRPC serves a json rpc endpoint using default server func ServeRPC() { rpcwrap.ServeRPC("json", oldjson.NewServerCodec) } +// ServeAuthRPC serves a json rpc endpoint using authentication enabled default +// server func ServeAuthRPC() { rpcwrap.ServeAuthRPC("json", oldjson.NewServerCodec) } diff --git a/go/rpcwrap/proto/proto.go b/go/rpcwrap/proto/proto.go index cb1378ae655..b3211232d8b 100644 --- a/go/rpcwrap/proto/proto.go +++ b/go/rpcwrap/proto/proto.go @@ -1,9 +1,10 @@ +// Package proto provides protocol functions package proto import ( "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" ) type contextKey int @@ -56,6 +57,7 @@ func SetUsername(ctx context.Context, username string) (ok bool) { return true } +// NewContext creates a default context satisfying context.Context func NewContext(remoteAddr string) context.Context { return &rpcContext{remoteAddr: remoteAddr} } @@ -70,14 +72,17 @@ func (ctx *rpcContext) Deadline() (deadline time.Time, ok bool) { return time.Time{}, false } +// Done channel for cancelation. func (ctx *rpcContext) Done() <-chan struct{} { return nil } +// Err is stub for interface function func (ctx *rpcContext) Err() error { return nil } +// Value returns only predefined variables if already set func (ctx *rpcContext) Value(key interface{}) interface{} { k, ok := key.(contextKey) if !ok { diff --git a/go/rpcwrap/rcpwrap_httprpc_test.go b/go/rpcwrap/rcpwrap_httprpc_test.go new file mode 100644 index 00000000000..d48cb0939a6 --- /dev/null +++ b/go/rpcwrap/rcpwrap_httprpc_test.go @@ -0,0 +1,135 @@ +package rpcwrap + +import ( + "errors" + "log" + "net" + "net/http" + + "github.com/youtube/vitess/go/rpcplus" + "github.com/youtube/vitess/go/rpcplus/jsonrpc" + "golang.org/x/net/context" + + "testing" +) + +type Request struct { + A, B int +} + +type Arith int + +func (t *Arith) Success(ctx context.Context, args *Request, reply *int) error { + *reply = args.A * args.B + return nil +} + +func (t *Arith) Fail(ctx context.Context, args *Request, reply *int) error { + return errors.New("fail") +} + +func (t *Arith) Context(ctx context.Context, args *Request, reply *int) error { + if data := ctx.Value("context"); data == nil { + return errors.New("context is not set") + } + + return nil +} + +func startListeningWithContext(ctx context.Context) net.Listener { + server := rpcplus.NewServer() + server.Register(new(Arith)) + + mux := http.NewServeMux() + + contextCreator := func(req *http.Request) context.Context { + return ctx + } + + ServeHTTPRPC( + mux, // httpmuxer + server, // rpcserver + "json", // codec name + jsonrpc.NewServerCodec, // jsoncodec + contextCreator, // contextCreator + ) + + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + log.Fatal(err) + } + + go http.Serve(l, mux) + return l +} + +func startListening() net.Listener { + return startListeningWithContext(context.Background()) +} + +func createAddr(l net.Listener) string { + return "http://" + l.Addr().String() + GetRpcPath("json", false) +} + +func TestSuccess(t *testing.T) { + l := startListening() + defer l.Close() + + params := &Request{ + A: 7, + B: 8, + } + + var r int + + err := jsonrpc.NewHTTPClient(createAddr(l)).Call("Arith.Success", params, &r) + if err != nil { + t.Fatal(err.Error()) + } + if r != 56 { + t.Fatalf("Expected: 56, but got: %d", r) + } +} + +func TestFail(t *testing.T) { + l := startListening() + defer l.Close() + + params := &Request{ + A: 7, + B: 8, + } + + var r int + + err := jsonrpc.NewHTTPClient(createAddr(l)).Call("Arith.Fail", params, &r) + if err == nil { + t.Fatal("Expected a non-nil err") + } + + if err.Error() != "fail" { + t.Fatalf("Expected \"fail\" as err message, but got %s", err.Error()) + } + + if r != 0 { + t.Fatalf("Expected: 0, but got: %d", r) + } +} + +func TestContext(t *testing.T) { + ctx := context.WithValue(context.Background(), "context", "value") + l := startListeningWithContext(ctx) + defer l.Close() + + params := &Request{ + A: 7, + B: 8, + } + + var r int + + err := jsonrpc.NewHTTPClient(createAddr(l)).Call("Arith.Context", params, &r) + if err != nil { + t.Fatal(err.Error()) + } +} diff --git a/go/rpcwrap/rpcwrap.go b/go/rpcwrap/rpcwrap.go index 0f4fc94cef0..735e244ca9c 100644 --- a/go/rpcwrap/rpcwrap.go +++ b/go/rpcwrap/rpcwrap.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package rpcwrap provides wrappers for rpcplus package package rpcwrap import ( @@ -13,7 +14,7 @@ import ( "net/http" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" log "github.com/golang/glog" rpc "github.com/youtube/vitess/go/rpcplus" @@ -31,20 +32,24 @@ var ( connAccepted = stats.NewInt("connection-accepted") ) +// ClientCodecFactory holds pattern for other client codec factories type ClientCodecFactory func(conn io.ReadWriteCloser) rpc.ClientCodec +// BufferedConnection holds connection data for codecs type BufferedConnection struct { isClosed bool *bufio.Reader io.WriteCloser } +// NewBufferedConnection creates a new Buffered Connection func NewBufferedConnection(conn io.ReadWriteCloser) *BufferedConnection { connCount.Add(1) connAccepted.Add(1) return &BufferedConnection{false, bufio.NewReader(conn), conn} } +// Close closes the buffered connection // FIXME(sougou/szopa): Find a better way to track connection count. func (bc *BufferedConnection) Close() error { if !bc.isClosed { @@ -119,6 +124,7 @@ func dialHTTP(network, address, codecName string, cFactory ClientCodecFactory, a return nil, &net.OpError{Op: "dial-http", Net: network + " " + address, Addr: nil, Err: err} } +// ServerCodecFactory holds pattern for other server codec factories type ServerCodecFactory func(conn io.ReadWriteCloser) rpc.ServerCodec // ServeRPC handles rpc requests using the hijack scheme of rpc @@ -185,3 +191,75 @@ func GetRpcPath(codecName string, auth bool) string { } return path } + +// ServeHTTPRPC serves the given http rpc requests with the provided ServeMux, +// does not support built-in authentication +func ServeHTTPRPC( + handler *http.ServeMux, + server *rpc.Server, + codecName string, + cFactory ServerCodecFactory, + contextCreator func(*http.Request) context.Context) { + + handler.Handle( + GetRpcPath(codecName, false), + &httpRPCHandler{ + cFactory: cFactory, + server: server, + contextCreator: contextCreator, + }, + ) +} + +// httpRPCHandler handles rpc queries for a all types of HTTP requests, does not +// maintain a persistent connection. +type httpRPCHandler struct { + cFactory ServerCodecFactory + server *rpc.Server + // contextCreator creates an application specific context, while creating + // the context it should not read the request body nor write anything to + // headers + contextCreator func(*http.Request) context.Context +} + +// ServeHTTP implements http.Handler's ServeHTTP +func (h *httpRPCHandler) ServeHTTP(c http.ResponseWriter, req *http.Request) { + codec := h.cFactory(&httpReadWriteCloser{rw: c, req: req}) + + var ctx context.Context + + if h.contextCreator != nil { + ctx = h.contextCreator(req) + } else { + ctx = proto.NewContext(req.RemoteAddr) + } + + h.server.ServeRequestWithContext( + ctx, + codec, + ) + + codec.Close() +} + +// httpReadWriteCloser wraps http.ResponseWriter and http.Request, with the help +// of those, implements ReadWriteCloser interface +type httpReadWriteCloser struct { + rw http.ResponseWriter + req *http.Request +} + +// Read implements Reader interface +func (i *httpReadWriteCloser) Read(p []byte) (n int, err error) { + return i.req.Body.Read(p) +} + +// Write implements Writer interface +func (i *httpReadWriteCloser) Write(p []byte) (n int, err error) { + return i.rw.Write(p) +} + +// Close implements Closer interface +func (i *httpReadWriteCloser) Close() error { + return i.req.Body.Close() +} diff --git a/go/streamlog/streamlog.go b/go/streamlog/streamlog.go index 32d82ae85e3..2ef90081e15 100644 --- a/go/streamlog/streamlog.go +++ b/go/streamlog/streamlog.go @@ -18,7 +18,6 @@ import ( var ( sendCount = stats.NewCounters("StreamlogSend") - internalDropCount = stats.NewCounters("StreamlogInternallyDroppedMessages") deliveredCount = stats.NewMultiCounters("StreamlogDelivered", []string{"Log", "Subscriber"}) deliveryDropCount = stats.NewMultiCounters("StreamlogDeliveryDroppedMessages", []string{"Log", "Subscriber"}) ) @@ -51,12 +50,8 @@ func New(name string, size int) *StreamLogger { // Send sends message to all the writers subscribed to logger. Calling // Send does not block. func (logger *StreamLogger) Send(message interface{}) { + logger.dataQueue <- message sendCount.Add(logger.name, 1) - select { - case logger.dataQueue <- message: - default: - internalDropCount.Add(logger.name, 1) - } } // Subscribe returns a channel which can be used to listen diff --git a/go/sync2/semaphore.go b/go/sync2/semaphore.go index 57aecb75dec..6629042e5fe 100644 --- a/go/sync2/semaphore.go +++ b/go/sync2/semaphore.go @@ -49,6 +49,17 @@ func (sem *Semaphore) Acquire() bool { } } +// TryAcquire acquires a semaphore if it's immediately available. +// It returns false otherwise. +func (sem *Semaphore) TryAcquire() bool { + select { + case <-sem.slots: + return true + default: + return false + } +} + // Release releases the acquired semaphore. You must // not release more than the number of semaphores you've // acquired. diff --git a/go/sync2/semaphore_test.go b/go/sync2/semaphore_test.go index 207c8f98ff2..753137b4d75 100644 --- a/go/sync2/semaphore_test.go +++ b/go/sync2/semaphore_test.go @@ -20,7 +20,7 @@ func TestSemaNoTimeout(t *testing.T) { }() s.Acquire() if !released { - t.Errorf("want true, got false") + t.Errorf("release: false, want true") } } @@ -31,11 +31,25 @@ func TestSemaTimeout(t *testing.T) { time.Sleep(10 * time.Millisecond) s.Release() }() - if ok := s.Acquire(); ok { - t.Errorf("want false, got true") + if s.Acquire() { + t.Errorf("Acquire: true, want false") } time.Sleep(10 * time.Millisecond) - if ok := s.Acquire(); !ok { - t.Errorf("want true, got false") + if !s.Acquire() { + t.Errorf("Acquire: false, want true") + } +} + +func TestSemaTryAcquire(t *testing.T) { + s := NewSemaphore(1, 0) + if !s.TryAcquire() { + t.Errorf("TryAcquire: false, want true") + } + if s.TryAcquire() { + t.Errorf("TryAcquire: true, want false") + } + s.Release() + if !s.TryAcquire() { + t.Errorf("TryAcquire: false, want true") } } diff --git a/go/timer/timer.go b/go/timer/timer.go index 92250b91316..761b1526ef9 100644 --- a/go/timer/timer.go +++ b/go/timer/timer.go @@ -3,26 +3,26 @@ // license that can be found in the LICENSE file. /* - Package timer provides timer functionality that can be controlled - by the user. You start the timer by providing it a callback function, - which it will call at the specified interval. +Package timer provides timer functionality that can be controlled +by the user. You start the timer by providing it a callback function, +which it will call at the specified interval. - var t = timer.NewTimer(1e9) - t.Start(KeepHouse) + var t = timer.NewTimer(1e9) + t.Start(KeepHouse) - func KeepHouse() { - // do house keeping work - } + func KeepHouse() { + // do house keeping work + } - You can stop the timer by calling t.Stop, which is guaranteed to - wait if KeepHouse is being executed. +You can stop the timer by calling t.Stop, which is guaranteed to +wait if KeepHouse is being executed. - You can create an untimely trigger by calling t.Trigger. You can also - schedule an untimely trigger by calling t.TriggerAfter. +You can create an untimely trigger by calling t.Trigger. You can also +schedule an untimely trigger by calling t.TriggerAfter. - The timer interval can be changed on the fly by calling t.SetInterval. - A zero value interval will cause the timer to wait indefinitely, and it - will react only to an explicit Trigger or Stop. +The timer interval can be changed on the fly by calling t.SetInterval. +A zero value interval will cause the timer to wait indefinitely, and it +will react only to an explicit Trigger or Stop. */ package timer diff --git a/go/timer/timer_test.go b/go/timer/timer_test.go index 7cdc72ac171..d05eebb1f00 100644 --- a/go/timer/timer_test.go +++ b/go/timer/timer_test.go @@ -11,10 +11,10 @@ import ( ) const ( - one = time.Duration(1e9) - half = time.Duration(500e6) - quarter = time.Duration(250e6) - tenth = time.Duration(100e6) + one = time.Duration(1e8) + half = time.Duration(500e5) + quarter = time.Duration(250e5) + tenth = time.Duration(100e5) ) var numcalls int32 diff --git a/go/trace/trace.go b/go/trace/trace.go index 39edfdcc16f..cf6e065de47 100644 --- a/go/trace/trace.go +++ b/go/trace/trace.go @@ -3,11 +3,12 @@ // license that can be found in the LICENSE file. // Package trace contains a helper interface that allows various tracing -// tools to be plugged in to components using this interface. +// tools to be plugged in to components using this interface. If no plugin is +// registered, the default one makes all trace calls into no-ops. package trace import ( - "code.google.com/p/go.net/context" + "golang.org/x/net/context" ) // Span represents a unit of work within a trace. After creating a Span with @@ -57,13 +58,32 @@ func NewSpanFromContext(ctx context.Context) Span { return NewSpan(nil) } -// spanFactory should be changed by a plugin during init() to a factory that -// creates an actual Span implementation for that plugin's tracing framework. -var spanFactory interface { +// CopySpan creates a new context from parentCtx, with only the trace span +// copied over from spanCtx, if it has any. If not, parentCtx is returned. +func CopySpan(parentCtx, spanCtx context.Context) context.Context { + if span, ok := FromContext(spanCtx); ok { + return NewContext(parentCtx, span) + } + return parentCtx +} + +// SpanFactory is an interface for creating spans or extracting them from Contexts. +type SpanFactory interface { New(parent Span) Span FromContext(ctx context.Context) (Span, bool) NewContext(parent context.Context, span Span) context.Context -} = fakeSpanFactory{} +} + +var spanFactory SpanFactory = fakeSpanFactory{} + +// RegisterSpanFactory should be called by a plugin during init() to install a +// factory that creates Spans for that plugin's tracing framework. Each call to +// RegisterSpanFactory will overwrite any previous setting. If no factory is +// registered, the default fake factory will produce Spans whose methods are all +// no-ops. +func RegisterSpanFactory(sf SpanFactory) { + spanFactory = sf +} type fakeSpanFactory struct{} diff --git a/go/trace/trace_test.go b/go/trace/trace_test.go new file mode 100644 index 00000000000..5553c81c89a --- /dev/null +++ b/go/trace/trace_test.go @@ -0,0 +1,26 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package trace + +import ( + "testing" + + "golang.org/x/net/context" +) + +func TestFakeSpan(t *testing.T) { + ctx := context.Background() + RegisterSpanFactory(fakeSpanFactory{}) + + // It should be safe to call all the usual methods as if a plugin were installed. + span := NewSpanFromContext(ctx) + span.StartLocal("label") + span.StartClient("label") + span.StartServer("label") + span.Annotate("key", "value") + span.Finish() + NewContext(ctx, span) + CopySpan(ctx, ctx) +} diff --git a/go/umgmt/client.go b/go/umgmt/client.go deleted file mode 100644 index f2c91a8deed..00000000000 --- a/go/umgmt/client.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package umgmt - -import ( - "io" - "net" - "net/rpc" - - log "github.com/golang/glog" -) - -type Client struct { - *rpc.Client - conn io.ReadWriteCloser -} - -func Dial(address string) (*Client, error) { - conn, err := net.Dial("unix", address) - if err != nil { - return nil, err - } - client := new(Client) - client.conn = conn - client.Client = rpc.NewClient(conn) - return client, nil -} - -func (client *Client) Close() error { - if client.conn != nil { - return client.conn.Close() - } - return nil -} - -func (client *Client) Ping() (string, error) { - request := new(Request) - reply := new(Reply) - err := client.Call("UmgmtService.Ping", request, reply) - if err != nil { - log.Errorf("rpc err: %v", err) - return "ERROR", err - } - return reply.Message, nil -} - -func (client *Client) CloseListeners() error { - request := new(Request) - reply := new(Reply) - err := client.Call("UmgmtService.CloseListeners", request, reply) - if err != nil { - log.Errorf("CloseListeners err: %v", err) - } - return err -} - -func (client *Client) GracefulShutdown() error { - request := new(Request) - reply := new(Reply) - err := client.Call("UmgmtService.GracefulShutdown", request, reply) - if err != nil && err != io.ErrUnexpectedEOF { - log.Errorf("GracefulShutdown err: %v", err) - } - return err -} diff --git a/go/umgmt/fdpass.go b/go/umgmt/fdpass.go deleted file mode 100644 index ffca5efe369..00000000000 --- a/go/umgmt/fdpass.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package umgmt - -import ( - "fmt" - "net" - "os" - "syscall" -) - -func SendFd(conn *net.UnixConn, file *os.File) error { - rights := syscall.UnixRights(int(file.Fd())) - dummy := []byte("x") - n, oobn, err := conn.WriteMsgUnix(dummy, rights, nil) - if err != nil { - return fmt.Errorf("sendfd: err %v", err) - } - if n != len(dummy) { - return fmt.Errorf("sendfd: short write %v", conn) - } - if oobn != len(rights) { - return fmt.Errorf("sendfd: short oob write %v", conn) - } - return nil -} - -func RecvFd(conn *net.UnixConn) (*os.File, error) { - buf := make([]byte, 32) - oob := make([]byte, 32) - _, oobn, _, _, err := conn.ReadMsgUnix(buf, oob) - if err != nil { - return nil, fmt.Errorf("recvfd: err %v", err) - } - scms, err := syscall.ParseSocketControlMessage(oob[:oobn]) - if err != nil { - return nil, fmt.Errorf("recvfd: ParseSocketControlMessage failed %v", err) - } - if len(scms) != 1 { - return nil, fmt.Errorf("recvfd: SocketControlMessage count not 1: %v", len(scms)) - } - scm := scms[0] - fds, err := syscall.ParseUnixRights(&scm) - if err != nil { - return nil, fmt.Errorf("recvfd: ParseUnixRights failed %v", err) - } - if len(fds) != 1 { - return nil, fmt.Errorf("recvfd: fd count not 1: %v", len(fds)) - } - return os.NewFile(uintptr(fds[0]), "passed-fd"), nil -} diff --git a/go/umgmt/http.go b/go/umgmt/http.go deleted file mode 100644 index 90c69275b62..00000000000 --- a/go/umgmt/http.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package umgmt - -import ( - "crypto/tls" - "crypto/x509" - "net" - "net/http" - "os" - "strings" - "sync" - - log "github.com/golang/glog" - "github.com/youtube/vitess/go/stats" -) - -var connectionCount = stats.NewInt("ConnectionCount") -var connectionAccepted = stats.NewInt("ConnectionAccepted") - -type connCountConn struct { - net.Conn - listener *connCountListener - closed bool -} - -func (c *connCountConn) Close() (err error) { - // in case there is a race closing a client - if c.closed { - return nil - } - err = c.Conn.Close() - c.closed = true - connectionCount.Add(-1) - c.listener.Lock() - delete(c.listener.connMap, c) - c.listener.Unlock() - c.listener = nil - return -} - -// wrap up listener and server-side connection so we can count them -type connCountListener struct { - sync.Mutex - net.Listener - connMap map[*connCountConn]bool -} - -func newHttpListener(l net.Listener) *connCountListener { - return &connCountListener{Listener: l, - connMap: make(map[*connCountConn]bool, 8192)} -} - -func (l *connCountListener) CloseClients() { - l.Lock() - conns := make([]*connCountConn, 0, len(l.connMap)) - for conn := range l.connMap { - conns = append(conns, conn) - } - l.Unlock() - - for _, conn := range conns { - conn.Close() - } -} - -func (l *connCountListener) Accept() (c net.Conn, err error) { - c, err = l.Listener.Accept() - connectionAccepted.Add(1) - if err == nil { - connectionCount.Add(1) - } - if c != nil { - ccc := &connCountConn{c, l, false} - l.Lock() - l.connMap[ccc] = true - l.Unlock() - c = ccc - } - return -} - -func asyncListener(listener net.Listener) { - httpListener := newHttpListener(listener) - AddListener(httpListener) - httpErr := http.Serve(httpListener, nil) - httpListener.Close() - if httpErr != nil { - // This is net.errClosing, which is conveniently non-public. - // Squelch this expected case. - if !strings.Contains(httpErr.Error(), "use of closed network connection") { - log.Errorf("StartHttpServer error: %v", httpErr) - } - } -} - -// StartHttpServer binds and starts an http server. -// usually it is called like: -// umgmt.AddStartupCallback(func () { umgmt.StartHttpServer(addr) }) -func StartHttpServer(addr string) { - httpListener, httpErr := net.Listen("tcp", addr) - if httpErr != nil { - log.Fatalf("StartHttpServer failed: %v", httpErr) - } - go asyncListener(httpListener) -} - -// StartHttpsServer binds and starts an https server. -func StartHttpsServer(addr string, certFile, keyFile, caFile string) { - config := tls.Config{} - - // load the server cert / key - cert, err := tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - log.Fatalf("StartHttpsServer.LoadX509KeyPair failed: %v", err) - } - config.Certificates = []tls.Certificate{cert} - - // load the ca if necessary - // FIXME(alainjobart) this doesn't quite work yet, have - // to investigate - if caFile != "" { - config.ClientCAs = x509.NewCertPool() - - ca, err := os.Open(caFile) - if err != nil { - log.Fatalf("StartHttpsServer failed to open caFile %v: %v", caFile, err) - } - defer ca.Close() - - fi, err := ca.Stat() - if err != nil { - log.Fatalf("StartHttpsServer failed to stat caFile %v: %v", caFile, err) - } - - pemCerts := make([]byte, fi.Size()) - if _, err = ca.Read(pemCerts); err != nil { - log.Fatalf("StartHttpsServer failed to read caFile %v: %v", caFile, err) - } - - if !config.ClientCAs.AppendCertsFromPEM(pemCerts) { - log.Fatalf("StartHttpsServer failed to parse caFile %v", caFile) - } - - config.ClientAuth = tls.RequireAndVerifyClientCert - } - - httpsListener, err := tls.Listen("tcp", addr, &config) - if err != nil { - log.Fatalf("StartHttpsServer failed: %v", err) - } - go asyncListener(httpsListener) -} diff --git a/go/umgmt/server.go b/go/umgmt/server.go deleted file mode 100644 index 9c6e915721e..00000000000 --- a/go/umgmt/server.go +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package umgmt (micromanagment) provides a tiny server running on a unix -// domain socket. -// -// It is meant as an alternative to signals for handling graceful -// server management. The decision to use unix domain sockets was -// motivated by future intend to implement file descriptor passing. -// -// The underlying unix socket acts as a guard for starting up a -// server. Once that socket has be acquired it is assumed that -// previously bound sockets will be released and startup can -// continue. You must delegate execution of your server initialization -// to this module via AddStartupCallback(). -package umgmt - -import ( - "fmt" - "net" - "net/rpc" - "os" - "sync" - "syscall" - "time" - - log "github.com/golang/glog" -) - -type Request struct{} - -type Reply struct { - Message string -} - -type UmgmtListener interface { - Close() error - Addr() net.Addr -} - -type UmgmtCallback func() - -type UmgmtService struct { - mutex sync.Mutex - listeners []UmgmtListener - startupCallbacks []UmgmtCallback - shutdownCallbacks []UmgmtCallback - closeCallbacks []UmgmtCallback - done chan bool - - _lameDuckPeriod time.Duration - _rebindDelay time.Duration -} - -func newService() *UmgmtService { - return &UmgmtService{ - listeners: make([]UmgmtListener, 0, 8), - startupCallbacks: make([]UmgmtCallback, 0, 8), - shutdownCallbacks: make([]UmgmtCallback, 0, 8), - closeCallbacks: make([]UmgmtCallback, 0, 8), - done: make(chan bool, 1)} -} - -func (service *UmgmtService) lameDuckPeriod() time.Duration { - service.mutex.Lock() - defer service.mutex.Unlock() - return service._lameDuckPeriod -} - -func (service *UmgmtService) rebindDelay() time.Duration { - service.mutex.Lock() - defer service.mutex.Unlock() - return service._rebindDelay -} - -func (service *UmgmtService) addListener(l UmgmtListener) { - service.mutex.Lock() - defer service.mutex.Unlock() - service.listeners = append(service.listeners, l) -} - -func (service *UmgmtService) addStartupCallback(f UmgmtCallback) { - service.mutex.Lock() - defer service.mutex.Unlock() - service.startupCallbacks = append(service.startupCallbacks, f) -} - -func (service *UmgmtService) addCloseCallback(f UmgmtCallback) { - service.mutex.Lock() - defer service.mutex.Unlock() - service.closeCallbacks = append(service.closeCallbacks, f) -} - -func (service *UmgmtService) addShutdownCallback(f UmgmtCallback) { - service.mutex.Lock() - defer service.mutex.Unlock() - service.shutdownCallbacks = append(service.shutdownCallbacks, f) -} - -func (service *UmgmtService) Ping(request *Request, reply *Reply) error { - log.Infof("ping") - reply.Message = "pong" - return nil -} - -func (service *UmgmtService) CloseListeners(request *Request, reply *Reply) (err error) { - // NOTE(msolomon) block this method because we assume that when it returns to the client - // that there is a very high chance that the listeners have actually closed. - return service.closeListeners() -} - -func (service *UmgmtService) closeListeners() (err error) { - service.mutex.Lock() - defer service.mutex.Unlock() - for _, l := range service.listeners { - addr := l.Addr() - closeErr := l.Close() - if closeErr != nil { - err := fmt.Errorf("failed to close listener on %v err:%v", addr, closeErr) - // just return that at least one error happened, the log will reveal the rest - log.Errorf("%s", err) - } - log.Infof("closed listener %v", addr) - } - for _, f := range service.closeCallbacks { - go f() - } - // Prevent duplicate execution. - service.listeners = service.listeners[:0] - return -} - -func (service *UmgmtService) GracefulShutdown(request *Request, reply *Reply) (err error) { - // NOTE(msolomon) you can't reliably return from this kind of message, nor can a - // sane process expect an answer. Do this in a background goroutine and return quickly - go service.gracefulShutdown() - return -} - -func (service *UmgmtService) gracefulShutdown() { - service.mutex.Lock() - defer func() { service.done <- true }() - defer service.mutex.Unlock() - for _, f := range service.shutdownCallbacks { - f() - } - // Prevent duplicate execution. - service.shutdownCallbacks = service.shutdownCallbacks[:0] -} - -type UmgmtServer struct { - sync.Mutex - quit bool - listener net.Listener - connMap map[net.Conn]bool -} - -// NOTE(msolomon) This function handles requests serially. Multiple clients -// to umgmt doesn't make sense. -func (server *UmgmtServer) Serve() error { - defer server.listener.Close() - var tempDelay time.Duration // how long to sleep on accept failure - log.Infof("started umgmt server: %v", server.listener.Addr()) - for { - conn, err := server.listener.Accept() - if err != nil { - if ne, ok := err.(net.Error); ok && ne.Temporary() { - if tempDelay == 0 { - tempDelay = 5 * time.Millisecond - } else { - tempDelay *= 2 - } - if max := 1 * time.Second; tempDelay > max { - tempDelay = max - } - log.Warningf("umgmt: Accept error: %v; retrying in %v", err, tempDelay) - time.Sleep(tempDelay) - continue - } - - server.Lock() - if server.quit { - // If we are quitting, an EINVAL is expected. - err = nil - } - server.Unlock() - return err - } - - server.Lock() - server.connMap[conn] = true - server.Unlock() - - rpc.ServeConn(conn) - - server.Lock() - delete(server.connMap, conn) - server.Unlock() - } -} - -func (server *UmgmtServer) Addr() net.Addr { - return server.listener.Addr() -} - -func (server *UmgmtServer) Close() error { - server.Lock() - defer server.Unlock() - - server.quit = true - if server.listener != nil { - return server.listener.Close() - } - return nil -} - -func (server *UmgmtServer) handleGracefulShutdown() error { - server.Lock() - conns := make([]net.Conn, 0, len(server.connMap)) - for conn := range server.connMap { - conns = append(conns, conn) - } - server.Unlock() - // Closing the connection locks the connMap with an http connection. - // Operating on a copy of the list is fine for now, but this indicates the locking - // should be simplified if possible. - for conn := range server.connMap { - conn.Close() - } - return nil -} - -var defaultService = newService() - -func ListenAndServe(addr string) error { - rpc.Register(defaultService) - server := &UmgmtServer{connMap: make(map[net.Conn]bool)} - defer func() { - if err := server.Close(); err != nil { - log.Infof("umgmt server closed: %v", err) - } - }() - - var umgmtClient *Client - - for i := 2; i > 0; i-- { - l, e := net.Listen("unix", addr) - if e != nil { - if umgmtClient != nil { - umgmtClient.Close() - } - - if checkError(e, syscall.EADDRINUSE) { - var clientErr error - umgmtClient, clientErr = Dial(addr) - if clientErr == nil { - closeErr := umgmtClient.CloseListeners() - if closeErr != nil { - log.Errorf("umgmt CloseListeners err:%v", closeErr) - } - // wait for rpc to finish - rebindDelay := defaultService.rebindDelay() - if rebindDelay > 0.0 { - log.Infof("umgmt delaying rebind %v", rebindDelay) - time.Sleep(rebindDelay) - } - continue - } else if checkError(clientErr, syscall.ECONNREFUSED) { - log.Warningf("umgmt forced socket removal: %v", addr) - if rmErr := os.Remove(addr); rmErr != nil { - log.Errorf("umgmt failed removing socket: %v", rmErr) - } - } else { - return e - } - } else { - return e - } - } else { - server.listener = l - break - } - } - if server.listener == nil { - return fmt.Errorf("unable to rebind umgmt socket") - } - // register the umgmt server itself for dropping - this seems like - // the common case. i can't see when you *wouldn't* want to drop yourself - defaultService.addListener(server) - defaultService.addShutdownCallback(func() { - server.handleGracefulShutdown() - }) - - // fire off the startup callbacks. if these bind ports, they should - // call AddListener. - for _, f := range defaultService.startupCallbacks { - f() - } - - if umgmtClient != nil { - go func() { - time.Sleep(defaultService.lameDuckPeriod()) - umgmtClient.GracefulShutdown() - umgmtClient.Close() - }() - } - err := server.Serve() - // If we exitted gracefully, wait for the service to finish callbacks. - if err == nil { - <-defaultService.done - } - return err -} - -func AddListener(listener UmgmtListener) { - defaultService.addListener(listener) -} - -func AddShutdownCallback(f UmgmtCallback) { - defaultService.addShutdownCallback(f) -} - -func AddStartupCallback(f UmgmtCallback) { - defaultService.addStartupCallback(f) -} - -func AddCloseCallback(f UmgmtCallback) { - defaultService.addCloseCallback(f) -} - -func SetLameDuckPeriod(f float32) { - defaultService.mutex.Lock() - defaultService._lameDuckPeriod = time.Duration(f * 1.0e9) - defaultService.mutex.Unlock() -} - -func SetRebindDelay(f float32) { - defaultService.mutex.Lock() - defaultService._rebindDelay = time.Duration(f * 1.0e9) - defaultService.mutex.Unlock() -} - -func SigTermHandler(signal os.Signal) { - log.Infof("SigTermHandler") - defaultService.closeListeners() - time.Sleep(defaultService.lameDuckPeriod()) - defaultService.gracefulShutdown() -} - -// this is a temporary hack around a few different ways of wrapping -// error codes coming out of the system libraries -func checkError(err, testErr error) bool { - //log.Errorf("checkError %T(%v) == %T(%v)", err, err, testErr, testErr) - if err == testErr { - return true - } - - if opErr, ok := err.(*net.OpError); ok { - return opErr.Err == testErr - } - - return false -} diff --git a/go/umgmt/umgmt_test.go b/go/umgmt/umgmt_test.go deleted file mode 100644 index 461ef86f8ae..00000000000 --- a/go/umgmt/umgmt_test.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package umgmt - -import ( - "testing" - - log "github.com/golang/glog" -) - -var ready = make(chan bool) - -func serve(t *testing.T) { - AddStartupCallback(func() { ready <- true }) - AddShutdownCallback(func() { log.Infof("test server GracefulShutdown callback") }) - err := ListenAndServe("/tmp/test-sock") - if err != nil { - t.Fatalf("listen err: %v", err) - } - log.Infof("test server finished") -} - -func TestUmgmt(t *testing.T) { - go serve(t) - <-ready - - client, err := Dial("/tmp/test-sock") - if err != nil { - t.Fatalf("can't connect %v", err) - } - request := new(Request) - - reply := new(Reply) - callErr := client.Call("UmgmtService.Ping", request, reply) - if callErr != nil { - t.Fatalf("callErr: %v", callErr) - } - log.Infof("Ping reply: %v", reply.Message) - - reply = new(Reply) - callErr = client.Call("UmgmtService.CloseListeners", reply, reply) - if callErr != nil { - t.Fatalf("callErr: %v", callErr) - } - log.Infof("CloseListeners reply: %v", reply.Message) - - reply = new(Reply) - callErr = client.Call("UmgmtService.GracefulShutdown", reply, reply) - if callErr != nil { - t.Fatalf("callErr: %v", callErr) - } - log.Infof("GracefulShutdown reply: %v", reply.Message) -} diff --git a/go/umgmt/umgmtping/umgmtping.go b/go/umgmt/umgmtping/umgmtping.go deleted file mode 100644 index 244ac9cbd27..00000000000 --- a/go/umgmt/umgmtping/umgmtping.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "flag" - - "github.com/youtube/vitess/go/umgmt" -) - -var sockPath = flag.String("sock-path", "", "") - -func main() { - flag.Parse() - println("sock path: ", *sockPath) - uc, err := umgmt.Dial(*sockPath) - if err != nil { - panic(err) - } - msg, err := uc.Ping() - if err != nil { - panic(err) - } - println("msg: ", msg) -} diff --git a/go/vt/binlog/proto/binlog_player.go b/go/vt/binlog/proto/binlog_player.go index 9d40e9a2899..530582d687b 100644 --- a/go/vt/binlog/proto/binlog_player.go +++ b/go/vt/binlog/proto/binlog_player.go @@ -18,11 +18,15 @@ type BlpPosition struct { Position myproto.ReplicationPosition } +//go:generate bsongen -file $GOFILE -type BlpPosition -o blp_position_bson.go + // BlpPositionList is a list of BlpPosition, not sorted. type BlpPositionList struct { Entries []BlpPosition } +//go:generate bsongen -file $GOFILE -type BlpPositionList -o blp_position_list_bson.go + // FindBlpPositionById returns the BlpPosition with the given id, or error func (bpl *BlpPositionList) FindBlpPositionById(id uint32) (*BlpPosition, error) { for _, pos := range bpl.Entries { diff --git a/go/vt/binlog/proto/binlog_transaction.go b/go/vt/binlog/proto/binlog_transaction.go index 7bb36fc5d29..18e194c25f5 100644 --- a/go/vt/binlog/proto/binlog_transaction.go +++ b/go/vt/binlog/proto/binlog_transaction.go @@ -41,6 +41,8 @@ type BinlogTransaction struct { GTIDField myproto.GTIDField } +//go:generate bsongen -file $GOFILE -type BinlogTransaction -o binlog_transaction_bson.go + // Statement represents one statement as read from the binlog. type Statement struct { Category int @@ -48,6 +50,8 @@ type Statement struct { Sql []byte } +//go:generate bsongen -file $GOFILE -type Statement -o statement_bson.go + // String pretty-prints a statement. func (s Statement) String() string { if cat, ok := BL_CATEGORY_NAMES[s.Category]; ok { diff --git a/go/vt/binlog/proto/stream_event.go b/go/vt/binlog/proto/stream_event.go index ad8f0c2947d..a768a25c47d 100644 --- a/go/vt/binlog/proto/stream_event.go +++ b/go/vt/binlog/proto/stream_event.go @@ -27,3 +27,5 @@ type StreamEvent struct { // POS GTIDField myproto.GTIDField } + +//go:generate bsongen -file $GOFILE -type StreamEvent -o stream_event_bson.go diff --git a/go/vt/callinfo/callinfo.go b/go/vt/callinfo/callinfo.go index f195dd471c2..5ed94c32360 100644 --- a/go/vt/callinfo/callinfo.go +++ b/go/vt/callinfo/callinfo.go @@ -4,7 +4,7 @@ package callinfo import ( "html/template" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" ) type CallInfo interface { diff --git a/go/vt/callinfo/plugin_rpcwrap.go b/go/vt/callinfo/plugin_rpcwrap.go index 03a9939be88..e1adc706f30 100644 --- a/go/vt/callinfo/plugin_rpcwrap.go +++ b/go/vt/callinfo/plugin_rpcwrap.go @@ -4,8 +4,8 @@ import ( "fmt" "html/template" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/rpcwrap/proto" + "golang.org/x/net/context" ) type rpcWrapInfo struct { diff --git a/go/vt/vtgate/deprecated_router.go b/go/vt/client2/deprecated_router.go similarity index 99% rename from go/vt/vtgate/deprecated_router.go rename to go/vt/client2/deprecated_router.go index 399f6a673d9..accafeb057b 100644 --- a/go/vt/vtgate/deprecated_router.go +++ b/go/vt/client2/deprecated_router.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package vtgate +package client2 import ( "fmt" diff --git a/go/vt/vtgate/deprecated_router_test.go b/go/vt/client2/deprecated_router_test.go similarity index 99% rename from go/vt/vtgate/deprecated_router_test.go rename to go/vt/client2/deprecated_router_test.go index a15ecf99222..5d9bc57b346 100644 --- a/go/vt/vtgate/deprecated_router_test.go +++ b/go/vt/client2/deprecated_router_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package vtgate +package client2 import ( "bufio" diff --git a/go/vt/client2/sharded.go b/go/vt/client2/sharded.go index c423a246d23..acb18a268b3 100644 --- a/go/vt/client2/sharded.go +++ b/go/vt/client2/sharded.go @@ -1,6 +1,7 @@ // Copyright 2012, Google Inc. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. + package client2 import ( @@ -16,7 +17,6 @@ import ( "github.com/youtube/vitess/go/vt/client2/tablet" "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/topo" - "github.com/youtube/vitess/go/vt/vtgate" "github.com/youtube/vitess/go/vt/zktopo" "github.com/youtube/vitess/go/zk" ) @@ -25,7 +25,7 @@ import ( // database. // // The ShardedConn can handles several separate aspects: -// * loading/reloading tablet addresses on demand from zk/zkocc +// * loading/reloading tablet addresses on demand from zk // * maintaining at most one connection to each tablet as required // * transaction tracking across shards // * preflight checking all transactions before attempting to commit @@ -124,10 +124,10 @@ func (sc *ShardedConn) readKeyspace() error { return fmt.Errorf("vt: GetSrvKeyspace failed %v", err) } - sc.conns = make([]*tablet.VtConn, len(sc.srvKeyspace.Shards)) - sc.shardMaxKeys = make([]key.KeyspaceId, len(sc.srvKeyspace.Shards)) + sc.conns = make([]*tablet.VtConn, len(sc.srvKeyspace.Partitions[sc.tabletType].Shards)) + sc.shardMaxKeys = make([]key.KeyspaceId, len(sc.srvKeyspace.Partitions[sc.tabletType].Shards)) - for i, srvShard := range sc.srvKeyspace.Shards { + for i, srvShard := range sc.srvKeyspace.Partitions[sc.tabletType].Shards { sc.shardMaxKeys[i] = srvShard.KeyRange.End } @@ -233,7 +233,7 @@ func (sc *ShardedConn) Exec(query string, bindVars map[string]interface{}) (db.R if sc.srvKeyspace == nil { return nil, ErrNotConnected } - shards, err := vtgate.GetShardList(query, bindVars, sc.shardMaxKeys) + shards, err := GetShardList(query, bindVars, sc.shardMaxKeys) if err != nil { return nil, err } @@ -526,7 +526,7 @@ func (sc *ShardedConn) ExecuteBatch(queryList []ClientQuery, keyVal interface{}) */ func (sc *ShardedConn) dial(shardIdx int) (conn *tablet.VtConn, err error) { - srvShard := &(sc.srvKeyspace.Shards[shardIdx]) + srvShard := &(sc.srvKeyspace.Partitions[sc.tabletType].Shards[shardIdx]) shard := fmt.Sprintf("%v-%v", srvShard.KeyRange.Start.Hex(), srvShard.KeyRange.End.Hex()) // Hack to handle non-range based shards. if !srvShard.KeyRange.IsPartial() { @@ -560,7 +560,7 @@ type sDriver struct { // for direct zk connection: vtzk://host:port/cell/keyspace/tabletType // we always use a MetaConn, host and port are ignored. -// the driver name dictates if we use zk or zkocc, and streaming or not +// the driver name dictates if we streaming or not func (driver *sDriver) Open(name string) (sc db.Conn, err error) { if !strings.HasPrefix(name, "vtzk://") { // add a default protocol talking to zk @@ -587,14 +587,8 @@ func RegisterShardedDrivers() { db.Register("vtdb-streaming", &sDriver{ts, true}) // forced zk topo server - zconn := zk.NewMetaConn(false) + zconn := zk.NewMetaConn() zkts := zktopo.NewServer(zconn) db.Register("vtdb-zk", &sDriver{zkts, false}) db.Register("vtdb-zk-streaming", &sDriver{zkts, true}) - - // forced zkocc topo server - zkoccconn := zk.NewMetaConn(true) - zktsro := zktopo.NewServer(zkoccconn) - db.Register("vtdb-zkocc", &sDriver{zktsro, false}) - db.Register("vtdb-zkocc-streaming", &sDriver{zktsro, true}) } diff --git a/go/vt/client2/tablet/tclient.go b/go/vt/client2/tablet/tclient.go index 827367bf6df..228e0b01a15 100644 --- a/go/vt/client2/tablet/tclient.go +++ b/go/vt/client2/tablet/tclient.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// API compliant to the requirements of database/sql +// Package tablet is an API compliant to the requirements of database/sql // Open expects name to be "hostname:port/keyspace/shard" // For query arguments, we assume place-holders in the query string // in the form of :v0, :v1, etc. @@ -15,7 +15,6 @@ import ( "strings" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/db" mproto "github.com/youtube/vitess/go/mysql/proto" @@ -23,6 +22,7 @@ import ( "github.com/youtube/vitess/go/sqltypes" "github.com/youtube/vitess/go/vt/tabletserver/tabletconn" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) var ( @@ -113,7 +113,7 @@ func (conn *Conn) dial() (err error) { endPoint := topo.EndPoint{ Host: host, NamedPortMap: map[string]int{ - "_vtocc": port, + "vt": port, }, } diff --git a/go/vt/client2/tablet/vclient.go b/go/vt/client2/tablet/vclient.go index 261845a507c..50031e007e9 100644 --- a/go/vt/client2/tablet/vclient.go +++ b/go/vt/client2/tablet/vclient.go @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This implements some additional error handling logic to make the client -// more robust in the face of transient problems with easy solutions. +// Package tablet implements some additional error handling logic to +// make the client more robust in the face of transient problems with +// easy solutions. package tablet import ( diff --git a/go/vt/context/context.go b/go/vt/context/context.go deleted file mode 100644 index aa2cbb6dd6e..00000000000 --- a/go/vt/context/context.go +++ /dev/null @@ -1,12 +0,0 @@ -package context - -import "time" - -// DummyContext is a dummy implementation of Context. -type DummyContext struct { -} - -func (dc *DummyContext) Deadline() (deadline time.Time, ok bool) { return time.Time{}, false } -func (dc *DummyContext) Done() <-chan struct{} { return nil } -func (dc *DummyContext) Err() error { return nil } -func (dc *DummyContext) Value(key interface{}) interface{} { return nil } diff --git a/go/vt/dbconfigs/credentials.go b/go/vt/dbconfigs/credentials.go index 79d04b495e0..d17a26558cd 100644 --- a/go/vt/dbconfigs/credentials.go +++ b/go/vt/dbconfigs/credentials.go @@ -6,7 +6,7 @@ package dbconfigs // This file contains logic for a plugable credentials system. // The default implementation is file based. -// The flags are global, but only programs that need to acess the database +// The flags are global, but only programs that need to access the database // link with this library, so we should be safe. import ( @@ -25,7 +25,8 @@ var ( // 'file' implementation flags dbCredentialsFile = flag.String("db-credentials-file", "", "db credentials file") - // error returned by credential server when the user doesn't exist + // ErrUnknownUser is returned by credential server when the + // user doesn't exist ErrUnknownUser = errors.New("unknown user") ) @@ -37,10 +38,6 @@ type CredentialsServer interface { // Note this call needs to be thread safe, as we may call this from // multiple go routines. GetUserAndPassword(user string) (string, string, error) - - // GetSubprocessFlags returns the flags to send to a subprocess - // to initialize the exact same CredentialsServer - GetSubprocessFlags() []string } // AllCredentialsServers contains all the known CredentialsServer @@ -58,17 +55,6 @@ func GetCredentialsServer() CredentialsServer { return cs } -// getCredentialsServerSubprocessFlags returns the flags to use for -// sub-processes -func getCredentialsServerSubprocessFlags() []string { - result := []string{ - "-db-credentials-server", *dbCredentialsServer, - } - cs := GetCredentialsServer() - result = append(result, cs.GetSubprocessFlags()...) - return result -} - // FileCredentialsServer is a simple implementation of CredentialsServer using // a json file. Protected by mu. type FileCredentialsServer struct { @@ -76,6 +62,7 @@ type FileCredentialsServer struct { dbCredentials map[string][]string } +// GetUserAndPassword is part of the CredentialsServer interface func (fcs *FileCredentialsServer) GetUserAndPassword(user string) (string, string, error) { fcs.mu.Lock() defer fcs.mu.Unlock() @@ -93,17 +80,11 @@ func (fcs *FileCredentialsServer) GetUserAndPassword(user string) (string, strin } } - if passwd, ok := fcs.dbCredentials[user]; !ok { + passwd, ok := fcs.dbCredentials[user] + if !ok { return "", "", ErrUnknownUser - } else { - return user, passwd[0], nil - } -} - -func (fcs *FileCredentialsServer) GetSubprocessFlags() []string { - return []string{ - "-db-credentials-file", *dbCredentialsFile, } + return user, passwd[0], nil } func init() { diff --git a/go/vt/dbconfigs/dbconfigs.go b/go/vt/dbconfigs/dbconfigs.go index d8c444cbf46..31af2aa29fd 100644 --- a/go/vt/dbconfigs/dbconfigs.go +++ b/go/vt/dbconfigs/dbconfigs.go @@ -9,38 +9,31 @@ package dbconfigs import ( "encoding/json" "flag" - "strconv" + "fmt" log "github.com/golang/glog" "github.com/youtube/vitess/go/mysql" ) // Offer a default config. -var DefaultDBConfigs = DBConfigs{ - App: DBConfig{ - ConnectionParams: mysql.ConnectionParams{ - Uname: "vt_app", - Charset: "utf8", - }, - }, - Dba: mysql.ConnectionParams{ - Uname: "vt_dba", - Charset: "utf8", - }, - Filtered: mysql.ConnectionParams{ - Uname: "vt_filtered", - Charset: "utf8", - }, - Repl: mysql.ConnectionParams{ - Uname: "vt_repl", - Charset: "utf8", - }, -} +var DefaultDBConfigs = DBConfigs{} // We keep a global singleton for the db configs, and that's the one // the flags will change var dbConfigs DBConfigs +// DBConfigFlag describes which flags we need +type DBConfigFlag int + +// config flags +const ( + EmptyConfig DBConfigFlag = 0 + AppConfig DBConfigFlag = 1 << iota + DbaConfig + FilteredConfig + ReplConfig +) + // The flags will change the global singleton func registerConnFlags(connParams *mysql.ConnectionParams, name string, defaultParams mysql.ConnectionParams) { flag.StringVar(&connParams.Host, "db-config-"+name+"-host", defaultParams.Host, "db "+name+" connection host") @@ -55,50 +48,61 @@ func registerConnFlags(connParams *mysql.ConnectionParams, name string, defaultP flag.StringVar(&connParams.SslCaPath, "db-config-"+name+"-ssl-ca-path", defaultParams.SslCaPath, "db "+name+" connection ssl ca path") flag.StringVar(&connParams.SslCert, "db-config-"+name+"-ssl-cert", defaultParams.SslCert, "db "+name+" connection ssl certificate") flag.StringVar(&connParams.SslKey, "db-config-"+name+"-ssl-key", defaultParams.SslKey, "db "+name+" connection ssl key") - } -// vttablet will register client, dba and repl. -func RegisterFlags() { - registerConnFlags(&dbConfigs.Dba, "dba", DefaultDBConfigs.Dba) - registerConnFlags(&dbConfigs.Filtered, "filtered", DefaultDBConfigs.Filtered) - registerConnFlags(&dbConfigs.Repl, "repl", DefaultDBConfigs.Repl) - registerConnFlags(&dbConfigs.App.ConnectionParams, "app", DefaultDBConfigs.App.ConnectionParams) +// RegisterFlags registers the flags for the given DBConfigFlag. +// For instance, vttablet will register client, dba and repl. +// Returns all registered flags. +func RegisterFlags(flags DBConfigFlag) DBConfigFlag { + if flags == EmptyConfig { + panic("No DB config is provided.") + } + registeredFlags := EmptyConfig + if AppConfig&flags != 0 { + registerConnFlags(&dbConfigs.App.ConnectionParams, "app", DefaultDBConfigs.App.ConnectionParams) + registeredFlags |= AppConfig + } + if DbaConfig&flags != 0 { + registerConnFlags(&dbConfigs.Dba, "dba", DefaultDBConfigs.Dba) + registeredFlags |= DbaConfig + } + if FilteredConfig&flags != 0 { + registerConnFlags(&dbConfigs.Filtered, "filtered", DefaultDBConfigs.Filtered) + registeredFlags |= FilteredConfig + } + if ReplConfig&flags != 0 { + registerConnFlags(&dbConfigs.Repl, "repl", DefaultDBConfigs.Repl) + registeredFlags |= ReplConfig + } flag.StringVar(&dbConfigs.App.Keyspace, "db-config-app-keyspace", DefaultDBConfigs.App.Keyspace, "db app connection keyspace") flag.StringVar(&dbConfigs.App.Shard, "db-config-app-shard", DefaultDBConfigs.App.Shard, "db app connection shard") + return registeredFlags } -// InitConnectionParams may overwrite the socket file, +// initConnectionParams may overwrite the socket file, // and refresh the password to check that works. -func InitConnectionParams(cp *mysql.ConnectionParams, socketFile string) error { +func initConnectionParams(cp *mysql.ConnectionParams, socketFile string) error { if socketFile != "" { cp.UnixSocket = socketFile } - params := *cp - return refreshPassword(¶ms) + _, err := MysqlParams(cp) + return err } -// refreshPassword uses the CredentialServer to refresh the password -// to use. -func refreshPassword(params *mysql.ConnectionParams) error { - user, passwd, err := GetCredentialsServer().GetUserAndPassword(params.Uname) +// MysqlParams returns a copy of our ConnectionParams that we can use +// to connect, after going through the CredentialsServer. +func MysqlParams(cp *mysql.ConnectionParams) (mysql.ConnectionParams, error) { + result := *cp + user, passwd, err := GetCredentialsServer().GetUserAndPassword(cp.Uname) switch err { case nil: - params.Uname = user - params.Pass = passwd + result.Uname = user + result.Pass = passwd case ErrUnknownUser: - default: - return err + // we just use what we have, and will fail later anyway + err = nil } - return nil -} - -// returns a copy of our ConnectionParams that we can use to connect, -// after going through the CredentialsServer. -func MysqlParams(cp *mysql.ConnectionParams) (mysql.ConnectionParams, error) { - params := *cp - err := refreshPassword(¶ms) - return params, err + return result, err } // DBConfig encapsulates a ConnectionParams object and adds a keyspace and a @@ -132,7 +136,7 @@ type DBConfigs struct { } func (dbcfgs *DBConfigs) String() string { - if dbcfgs.App.ConnectionParams.Pass != mysql.REDACTED_PASSWORD { + if dbcfgs.App.ConnectionParams.Pass != mysql.RedactedPassword { panic("Cannot log a non-redacted DBConfig") } data, err := json.MarshalIndent(dbcfgs, "", " ") @@ -142,7 +146,7 @@ func (dbcfgs *DBConfigs) String() string { return string(data) } -// This will remove the password, so the object can be logged +// Redact will remove the password, so the object can be logged func (dbcfgs *DBConfigs) Redact() { dbcfgs.App.ConnectionParams.Redact() dbcfgs.Dba.Redact() @@ -150,78 +154,40 @@ func (dbcfgs *DBConfigs) Redact() { dbcfgs.Repl.Redact() } -// Initialize app, dba, filterec and repl configs -func Init(socketFile string) (*DBConfigs, error) { - if err := InitConnectionParams(&dbConfigs.App.ConnectionParams, socketFile); err != nil { - return nil, err +// Init will initialize app, dba, filterec and repl configs +func Init(socketFile string, flags DBConfigFlag) (*DBConfigs, error) { + if flags == EmptyConfig { + panic("No DB config is provided.") } - if err := InitConnectionParams(&dbConfigs.Dba, socketFile); err != nil { - return nil, err + if AppConfig&flags != 0 { + if err := initConnectionParams(&dbConfigs.App.ConnectionParams, socketFile); err != nil { + return nil, fmt.Errorf("app dbconfig cannot be initialized: %v", err) + } } - if err := InitConnectionParams(&dbConfigs.Filtered, socketFile); err != nil { - return nil, err + if DbaConfig&flags != 0 { + if err := initConnectionParams(&dbConfigs.Dba, socketFile); err != nil { + return nil, fmt.Errorf("dba dbconfig cannot be initialized: %v", err) + } } - if err := InitConnectionParams(&dbConfigs.Repl, socketFile); err != nil { - return nil, err + if FilteredConfig&flags != 0 { + if err := initConnectionParams(&dbConfigs.Filtered, socketFile); err != nil { + return nil, fmt.Errorf("filtered dbconfig cannot be initialized: %v", err) + } + } + if ReplConfig&flags != 0 { + if err := initConnectionParams(&dbConfigs.Repl, socketFile); err != nil { + return nil, fmt.Errorf("repl dbconfig cannot be initialized: %v", err) + } } - // the Dba connection is not linked to a specific database // (allows us to create them) - dbConfigs.Dba.DbName = "" + if dbConfigs.Dba.DbName != "" { + log.Warningf("dba dbname is set to '%v', ignoring the value", dbConfigs.Dba.DbName) + dbConfigs.Dba.DbName = "" + } toLog := dbConfigs toLog.Redact() log.Infof("DBConfigs: %v\n", toLog.String()) return &dbConfigs, nil } - -func GetSubprocessFlags() []string { - cmd := []string{} - f := func(connParams *mysql.ConnectionParams, name string) { - if connParams.Host != "" { - cmd = append(cmd, "-db-config-"+name+"-host", connParams.Host) - } - if connParams.Port > 0 { - cmd = append(cmd, "-db-config-"+name+"-port", strconv.Itoa(connParams.Port)) - } - if connParams.Uname != "" { - cmd = append(cmd, "-db-config-"+name+"-uname", connParams.Uname) - } - if connParams.DbName != "" { - cmd = append(cmd, "-db-config-"+name+"-dbname", connParams.DbName) - } - if connParams.UnixSocket != "" { - cmd = append(cmd, "-db-config-"+name+"-unixsocket", connParams.UnixSocket) - } - if connParams.Charset != "" { - cmd = append(cmd, "-db-config-"+name+"-charset", connParams.Charset) - } - if connParams.Flags > 0 { - cmd = append(cmd, "-db-config-"+name+"-flags", strconv.FormatUint(connParams.Flags, 10)) - } - if connParams.SslCa != "" { - cmd = append(cmd, "-db-config-"+name+"-ssl-ca", connParams.SslCa) - } - if connParams.SslCaPath != "" { - cmd = append(cmd, "-db-config-"+name+"-ssl-ca-path", connParams.SslCaPath) - } - if connParams.SslCert != "" { - cmd = append(cmd, "-db-config-"+name+"-ssl-cert", connParams.SslCert) - } - if connParams.SslKey != "" { - cmd = append(cmd, "-db-config-"+name+"-ssl-key", connParams.SslKey) - } - } - f(&dbConfigs.App.ConnectionParams, "app") - if dbConfigs.App.Keyspace != "" { - cmd = append(cmd, "-db-config-app-keyspace", dbConfigs.App.Keyspace) - } - if dbConfigs.App.Shard != "" { - cmd = append(cmd, "-db-config-app-shard", dbConfigs.App.Shard) - } - f(&dbConfigs.Dba, "dba") - f(&dbConfigs.Filtered, "filtered") - f(&dbConfigs.Repl, "repl") - cmd = append(cmd, getCredentialsServerSubprocessFlags()...) - return cmd -} diff --git a/go/vt/dbconfigs/dbconfigs_test.go b/go/vt/dbconfigs/dbconfigs_test.go new file mode 100644 index 00000000000..1d8e57b237d --- /dev/null +++ b/go/vt/dbconfigs/dbconfigs_test.go @@ -0,0 +1,43 @@ +// Copyright 2012, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package dbconfigs + +import "testing" + +func TestRegisterFlagsWithoutFlags(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("RegisterFlags should panic with empty db flags") + } + }() + dbConfigs = DBConfigs{} + RegisterFlags(EmptyConfig) +} + +func TestRegisterFlagsWithSomeFlags(t *testing.T) { + dbConfigs = DBConfigs{} + registeredFlags := RegisterFlags(DbaConfig | ReplConfig) + if registeredFlags&AppConfig != 0 { + t.Error("App connection params should not be registered.") + } + if registeredFlags&DbaConfig == 0 { + t.Error("Dba connection params should be registered.") + } + if registeredFlags&FilteredConfig != 0 { + t.Error("Filtered connection params should not be registered.") + } + if registeredFlags&ReplConfig == 0 { + t.Error("Repl connection params should be registered.") + } +} + +func TestInitWithEmptyFlags(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Init should panic with empty db flags") + } + }() + Init("", EmptyConfig) +} diff --git a/go/vt/dbconnpool/connection_pool.go b/go/vt/dbconnpool/connection_pool.go index 9667d13c152..a5be35e870c 100644 --- a/go/vt/dbconnpool/connection_pool.go +++ b/go/vt/dbconnpool/connection_pool.go @@ -20,17 +20,20 @@ import ( ) var ( - CONN_POOL_CLOSED_ERR = errors.New("connection pool is closed") + // ErrConnPoolClosed is returned / panicked whent he + // connection pool is closed. + ErrConnPoolClosed = errors.New("connection pool is closed") ) // PoolConnection is the interface implemented by users of this specialized pool. type PoolConnection interface { ExecuteFetch(query string, maxrows int, wantfields bool) (*proto.QueryResult, error) ExecuteStreamFetch(query string, callback func(*proto.QueryResult) error, streamBufferSize int) error - Id() int64 + ID() int64 Close() IsClosed() bool Recycle() + Reconnect() error } // CreateConnectionFunc is the factory method to create new connections @@ -98,7 +101,7 @@ func (cp *ConnectionPool) Close() { func (cp *ConnectionPool) Get(timeout time.Duration) (PoolConnection, error) { p := cp.pool() if p == nil { - return nil, CONN_POOL_CLOSED_ERR + return nil, ErrConnPoolClosed } r, err := p.Get(timeout) if err != nil { @@ -112,7 +115,7 @@ func (cp *ConnectionPool) Get(timeout time.Duration) (PoolConnection, error) { func (cp *ConnectionPool) TryGet() (PoolConnection, error) { p := cp.pool() if p == nil { - return nil, CONN_POOL_CLOSED_ERR + return nil, ErrConnPoolClosed } r, err := p.TryGet() if err != nil || r == nil { @@ -125,7 +128,7 @@ func (cp *ConnectionPool) TryGet() (PoolConnection, error) { func (cp *ConnectionPool) Put(conn PoolConnection) { p := cp.pool() if p == nil { - panic(CONN_POOL_CLOSED_ERR) + panic(ErrConnPoolClosed) } p.Put(conn) } diff --git a/go/vt/dbconnpool/pooled_connection.go b/go/vt/dbconnpool/pooled_connection.go index 9e0d9b9ead9..9f7143f7d77 100644 --- a/go/vt/dbconnpool/pooled_connection.go +++ b/go/vt/dbconnpool/pooled_connection.go @@ -12,7 +12,9 @@ import ( // PooledDBConnection re-exposes DBConnection as a PoolConnection type PooledDBConnection struct { *DBConnection - pool *ConnectionPool + info *mysql.ConnectionParams + mysqlStats *stats.Timings + pool *ConnectionPool } // Recycle implements PoolConnection's Recycle @@ -24,6 +26,16 @@ func (pc *PooledDBConnection) Recycle() { } } +func (pc *PooledDBConnection) Reconnect() error { + pc.DBConnection.Close() + newConn, err := NewDBConnection(pc.info, pc.mysqlStats) + if err != nil { + return err + } + pc.DBConnection = newConn + return nil +} + // DBConnectionCreator is the wrapper function to use to create a pool // of DBConnection objects. // @@ -35,11 +47,17 @@ func (pc *PooledDBConnection) Recycle() { // conn, err := pool.Get() // ... func DBConnectionCreator(info *mysql.ConnectionParams, mysqlStats *stats.Timings) CreateConnectionFunc { + newInfo := *info return func(pool *ConnectionPool) (PoolConnection, error) { c, err := NewDBConnection(info, mysqlStats) if err != nil { return nil, err } - return &PooledDBConnection{c, pool}, nil + return &PooledDBConnection{ + DBConnection: c, + info: &newInfo, + mysqlStats: mysqlStats, + pool: pool, + }, nil } } diff --git a/go/vt/etcdtopo/cell.go b/go/vt/etcdtopo/cell.go new file mode 100644 index 00000000000..787ea43a037 --- /dev/null +++ b/go/vt/etcdtopo/cell.go @@ -0,0 +1,108 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "fmt" + "path" + "strings" + + log "github.com/golang/glog" +) + +// cellClient wraps a Client for keeping track of cell-local clusters. +type cellClient struct { + Client + + // version is the etcd ModifiedIndex of the cell record we read from the + // global cluster for this client. + version int64 +} + +func (s *Server) getCellList() ([]string, error) { + resp, err := s.getGlobal().Get(cellsDirPath, true /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + if resp.Node == nil { + return nil, ErrBadResponse + } + var cells []string + for _, node := range resp.Node.Nodes { + cells = append(cells, path.Base(node.Key)) + } + return cells, nil +} + +// cell returns a client for the given cell-local etcd cluster. +// It caches clients for previously requested cells. +func (s *Server) getCell(cell string) (*cellClient, error) { + // Return a cached client if present. + s._cellsMutex.Lock() + client, ok := s._cells[cell] + s._cellsMutex.Unlock() + if ok { + return client, nil + } + + // Fetch cell cluster addresses from the global cluster. + // These can proceed concurrently (we've released the lock). + addrs, version, err := s.getCellAddrs(cell) + if err != nil { + return nil, err + } + + // Update the cache. + s._cellsMutex.Lock() + defer s._cellsMutex.Unlock() + + // Check if another goroutine beat us to creating a client for this cell. + if client, ok = s._cells[cell]; ok { + // Update the client only if we've fetched newer data. + if version > client.version { + client.SetCluster(addrs) + client.version = version + } + return client, nil + } + + // Create the client. + client = &cellClient{Client: s.newClient(addrs), version: version} + s._cells[cell] = client + return client, nil +} + +// getCellAddrs returns the list of etcd servers to try for the given cell-local +// cluster. These lists are stored in the global etcd cluster. +// The etcd ModifiedIndex (version) of the node is also returned. +func (s *Server) getCellAddrs(cell string) ([]string, int64, error) { + nodePath := cellFilePath(cell) + resp, err := s.getGlobal().Get(nodePath, false /* sort */, false /* recursive */) + if err != nil { + return nil, -1, convertError(err) + } + if resp.Node == nil { + return nil, -1, ErrBadResponse + } + if resp.Node.Value == "" { + return nil, -1, fmt.Errorf("cell node %v is empty, expected list of addresses", nodePath) + } + + return strings.Split(resp.Node.Value, ","), int64(resp.Node.ModifiedIndex), nil +} + +func (s *Server) getGlobal() Client { + s._globalOnce.Do(func() { + if len(globalAddrs) == 0 { + // This means either a TopoServer method was called before flag parsing, + // or the flag was not specified. Either way, it is a fatal condition. + log.Fatal("etcdtopo: list of addresses for global cluster is empty") + } + log.Infof("etcdtopo: global address list = %v", globalAddrs) + s._global = s.newClient([]string(globalAddrs)) + }) + + return s._global +} diff --git a/go/vt/etcdtopo/client.go b/go/vt/etcdtopo/client.go new file mode 100644 index 00000000000..e8f8ca291d7 --- /dev/null +++ b/go/vt/etcdtopo/client.go @@ -0,0 +1,27 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "github.com/coreos/go-etcd/etcd" +) + +func newEtcdClient(machines []string) Client { + return etcd.NewClient(machines) +} + +// Client contains the parts of etcd.Client that are needed. +type Client interface { + CompareAndDelete(key string, prevValue string, prevIndex uint64) (*etcd.Response, error) + CompareAndSwap(key string, value string, ttl uint64, + prevValue string, prevIndex uint64) (*etcd.Response, error) + Create(key string, value string, ttl uint64) (*etcd.Response, error) + Delete(key string, recursive bool) (*etcd.Response, error) + Get(key string, sort, recursive bool) (*etcd.Response, error) + Set(key string, value string, ttl uint64) (*etcd.Response, error) + SetCluster(machines []string) bool + Watch(prefix string, waitIndex uint64, recursive bool, + receiver chan *etcd.Response, stop chan bool) (*etcd.Response, error) +} diff --git a/go/vt/etcdtopo/client_test.go b/go/vt/etcdtopo/client_test.go new file mode 100644 index 00000000000..91620447051 --- /dev/null +++ b/go/vt/etcdtopo/client_test.go @@ -0,0 +1,303 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "path" + "sort" + "strings" + "sync" + + "github.com/coreos/go-etcd/etcd" +) + +type fakeNode struct { + node *etcd.Node + + mu sync.Mutex + watchIndex int + watches map[int]chan *etcd.Response +} + +func newFakeNode(node *etcd.Node) *fakeNode { + return &fakeNode{ + node: node, + watches: make(map[int]chan *etcd.Response), + } +} + +func (fn *fakeNode) notify(action string) { + fn.mu.Lock() + defer fn.mu.Unlock() + for _, w := range fn.watches { + var node *etcd.Node + if fn.node != nil { + node = &etcd.Node{} + *node = *fn.node + } + w <- &etcd.Response{ + Action: action, + Node: node, + } + } +} + +type fakeClient struct { + cell string + nodes map[string]*fakeNode + index uint64 + + sync.Mutex +} + +func newTestClient(machines []string) Client { + // In tests, the first machine address is just the cell name. + return &fakeClient{ + cell: machines[0], + nodes: map[string]*fakeNode{ + "/": newFakeNode(&etcd.Node{Key: "/", Dir: true}), + }, + } +} + +func (c *fakeClient) createParentDirs(key string) { + dir := path.Dir(key) + for dir != "" { + fn, ok := c.nodes[dir] + if ok && fn.node != nil { + return + } + if !ok { + fn = newFakeNode(nil) + c.nodes[dir] = fn + } + fn.node = &etcd.Node{Key: dir, Dir: true, CreatedIndex: c.index, ModifiedIndex: c.index} + dir = path.Dir(dir) + } +} + +func (c *fakeClient) CompareAndDelete(key string, prevValue string, prevIndex uint64) (*etcd.Response, error) { + c.Lock() + + if prevValue != "" { + panic("not implemented") + } + + n, ok := c.nodes[key] + if !ok || n.node == nil { + c.Unlock() + return nil, &etcd.EtcdError{ErrorCode: EcodeKeyNotFound} + } + if n.node.ModifiedIndex != prevIndex { + c.Unlock() + return nil, &etcd.EtcdError{ErrorCode: EcodeTestFailed} + } + + c.index++ + n.node = nil + c.Unlock() + n.notify("compareAndDelete") + return &etcd.Response{}, nil +} + +func (c *fakeClient) CompareAndSwap(key string, value string, ttl uint64, + prevValue string, prevIndex uint64) (*etcd.Response, error) { + c.Lock() + + n, ok := c.nodes[key] + if !ok || n.node == nil { + c.Unlock() + return nil, &etcd.EtcdError{ErrorCode: EcodeKeyNotFound} + } + if prevValue != "" && n.node.Value != prevValue { + c.Unlock() + return nil, &etcd.EtcdError{ErrorCode: EcodeTestFailed} + } + if prevIndex != 0 && n.node.ModifiedIndex != prevIndex { + c.Unlock() + return nil, &etcd.EtcdError{ErrorCode: EcodeTestFailed} + } + + c.index++ + n.node.ModifiedIndex = c.index + n.node.Value = value + c.nodes[key] = n + node := *n.node + c.Unlock() + n.notify("compareAndSwap") + return &etcd.Response{Node: &node}, nil +} + +func (c *fakeClient) Create(key string, value string, ttl uint64) (*etcd.Response, error) { + c.Lock() + + n, ok := c.nodes[key] + if ok && n.node != nil { + c.Unlock() + return nil, &etcd.EtcdError{ErrorCode: EcodeNodeExist} + } + + c.index++ + c.createParentDirs(key) + if !ok { + n = newFakeNode(nil) + c.nodes[key] = n + } + n.node = &etcd.Node{ + Key: key, + Value: value, + CreatedIndex: c.index, + ModifiedIndex: c.index, + } + node := *n.node + c.Unlock() + n.notify("create") + return &etcd.Response{Node: &node}, nil +} + +func (c *fakeClient) Delete(key string, recursive bool) (*etcd.Response, error) { + c.Lock() + + n, ok := c.nodes[key] + if !ok || n.node == nil { + c.Unlock() + return nil, &etcd.EtcdError{ErrorCode: EcodeKeyNotFound} + } + + n.node = nil + notifyList := []*fakeNode{n} + + if recursive { + for k, n := range c.nodes { + if strings.HasPrefix(k, key+"/") { + n.node = nil + notifyList = append(notifyList, n) + } + } + } + c.Unlock() + for _, n = range notifyList { + n.notify("delete") + } + return &etcd.Response{}, nil +} + +func (c *fakeClient) Get(key string, sortFiles, recursive bool) (*etcd.Response, error) { + c.Lock() + defer c.Unlock() + + if recursive { + panic("not implemented") + } + + n, ok := c.nodes[key] + if !ok || n.node == nil { + return nil, &etcd.EtcdError{ErrorCode: EcodeKeyNotFound} + } + node := *n.node + resp := &etcd.Response{Node: &node} + if !n.node.Dir { + return resp, nil + } + + // List the directory. + targetDir := key + "/" + for k, n := range c.nodes { + if n.node == nil { + continue + } + dir, file := path.Split(k) + if dir == targetDir && !strings.HasPrefix(file, "_") { + node := *n.node + resp.Node.Nodes = append(resp.Node.Nodes, &node) + } + } + if sortFiles { + sort.Sort(resp.Node.Nodes) + } + return resp, nil +} + +func (c *fakeClient) Set(key string, value string, ttl uint64) (*etcd.Response, error) { + c.Lock() + + c.index++ + + c.createParentDirs(key) + n, ok := c.nodes[key] + if !ok { + n = newFakeNode(nil) + c.nodes[key] = n + } + if n.node != nil { + n.node.Value = value + n.node.ModifiedIndex = c.index + } else { + n.node = &etcd.Node{Key: key, Value: value, CreatedIndex: c.index, ModifiedIndex: c.index} + } + node := *n.node + c.Unlock() + + n.notify("set") + return &etcd.Response{Node: &node}, nil +} + +func (c *fakeClient) SetCluster(machines []string) bool { + c.Lock() + defer c.Unlock() + + c.cell = machines[0] + return true +} + +func (c *fakeClient) Watch(prefix string, waitIndex uint64, recursive bool, + receiver chan *etcd.Response, stop chan bool) (*etcd.Response, error) { + + if recursive { + panic("not implemented") + } + + // We need a buffered forwarderfor 2 reasons: + // - in the select loop below, we only write to receiver if + // stop has not been closed. Otherwise we introduce race + // conditions. + // - we are waiting on forwarder and taking the node mutex. + // fakeNode.notify write to forwarder, and also takes the node + // mutex. Both can deadlock each-other. By buffering the + // channel, we make sure 10 notify() call can finish and not + // deadlock. We do a few of them in the serial locking code + // in tests. + forwarder := make(chan *etcd.Response, 10) + + // add the watch under the lock + c.Lock() + c.createParentDirs(prefix) + n, ok := c.nodes[prefix] + if !ok { + n = newFakeNode(nil) + c.nodes[prefix] = n + } + c.Unlock() + + n.mu.Lock() + index := n.watchIndex + n.watchIndex++ + n.watches[index] = forwarder + n.mu.Unlock() + + // and wait until we stop, each action will write to forwarder, send + // these along. + for { + select { + case <-stop: + n.mu.Lock() + delete(n.watches, index) + n.mu.Unlock() + return &etcd.Response{}, nil + case r := <-forwarder: + receiver <- r + } + } +} diff --git a/go/vt/etcdtopo/config.go b/go/vt/etcdtopo/config.go new file mode 100644 index 00000000000..607741f6913 --- /dev/null +++ b/go/vt/etcdtopo/config.go @@ -0,0 +1,106 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "flag" + "path" + + "github.com/youtube/vitess/go/flagutil" +) + +const ( + // Paths within the etcd keyspace. + rootPath = "/vt" + cellsDirPath = rootPath + "/cells" + keyspacesDirPath = rootPath + "/keyspaces" + tabletsDirPath = rootPath + "/tablets" + replicationDirPath = rootPath + "/replication" + servingDirPath = rootPath + "/ns" + vschemaPath = rootPath + "/vschema" + + // Magic file names. Directories in etcd cannot have data. Files whose names + // begin with '_' are hidden from directory listings. + dataFilename = "_Data" + keyspaceFilename = dataFilename + shardFilename = dataFilename + tabletFilename = dataFilename + shardReplicationFilename = dataFilename + srvKeyspaceFilename = dataFilename + srvShardFilename = dataFilename + endPointsFilename = dataFilename +) + +var ( + globalAddrs flagutil.StringListValue +) + +func init() { + flag.Var(&globalAddrs, "etcd_global_addrs", "comma-separated list of addresses (http://host:port) for global etcd cluster") +} + +func cellFilePath(cell string) string { + return path.Join(cellsDirPath, cell) +} + +func keyspaceDirPath(keyspace string) string { + return path.Join(keyspacesDirPath, keyspace) +} + +func keyspaceFilePath(keyspace string) string { + return path.Join(keyspaceDirPath(keyspace), keyspaceFilename) +} + +func shardsDirPath(keyspace string) string { + return keyspaceDirPath(keyspace) +} + +func shardDirPath(keyspace, shard string) string { + return path.Join(shardsDirPath(keyspace), shard) +} + +func shardFilePath(keyspace, shard string) string { + return path.Join(shardDirPath(keyspace, shard), shardFilename) +} + +func tabletDirPath(tablet string) string { + return path.Join(tabletsDirPath, tablet) +} + +func tabletFilePath(tablet string) string { + return path.Join(tabletDirPath(tablet), tabletFilename) +} + +func shardReplicationDirPath(keyspace, shard string) string { + return path.Join(replicationDirPath, keyspace, shard) +} + +func shardReplicationFilePath(keyspace, shard string) string { + return path.Join(shardReplicationDirPath(keyspace, shard), shardReplicationFilename) +} + +func srvKeyspaceDirPath(keyspace string) string { + return path.Join(servingDirPath, keyspace) +} + +func srvKeyspaceFilePath(keyspace string) string { + return path.Join(srvKeyspaceDirPath(keyspace), srvKeyspaceFilename) +} + +func srvShardDirPath(keyspace, shard string) string { + return path.Join(srvKeyspaceDirPath(keyspace), shard) +} + +func srvShardFilePath(keyspace, shard string) string { + return path.Join(srvShardDirPath(keyspace, shard), srvShardFilename) +} + +func endPointsDirPath(keyspace, shard, tabletType string) string { + return path.Join(srvShardDirPath(keyspace, shard), tabletType) +} + +func endPointsFilePath(keyspace, shard, tabletType string) string { + return path.Join(endPointsDirPath(keyspace, shard, tabletType), endPointsFilename) +} diff --git a/go/vt/etcdtopo/error.go b/go/vt/etcdtopo/error.go new file mode 100644 index 00000000000..0b0ba7af414 --- /dev/null +++ b/go/vt/etcdtopo/error.go @@ -0,0 +1,76 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "errors" + + "golang.org/x/net/context" + + "github.com/coreos/go-etcd/etcd" + "github.com/youtube/vitess/go/vt/topo" +) + +// Errors specific to this package. +var ( + // ErrBadResponse is returned from this package if the response from the etcd + // server does not contain the data that the API promises. The etcd client + // unmarshals JSON from the server into a Response struct that uses pointers, + // so we need to check for nil pointers, or else a misbehaving etcd could + // cause us to panic. + ErrBadResponse = errors.New("etcd request returned success, but response is missing required data") +) + +// Error codes returned by etcd: +// https://github.com/coreos/etcd/blob/v0.4.6/Documentation/errorcode.md +const ( + EcodeKeyNotFound = 100 + EcodeTestFailed = 101 + EcodeNotFile = 102 + EcodeNoMorePeer = 103 + EcodeNotDir = 104 + EcodeNodeExist = 105 + EcodeKeyIsPreserved = 106 + EcodeRootROnly = 107 + EcodeDirNotEmpty = 108 + + EcodeValueRequired = 200 + EcodePrevValueRequired = 201 + EcodeTTLNaN = 202 + EcodeIndexNaN = 203 + + EcodeRaftInternal = 300 + EcodeLeaderElect = 301 + + EcodeWatcherCleared = 400 + EcodeEventIndexCleared = 401 +) + +// convertError converts etcd-specific errors to corresponding topo errors, if +// they exist, and passes others through. It also converts context errors to +// topo package equivalents. +func convertError(err error) error { + switch typeErr := err.(type) { + case *etcd.EtcdError: + switch typeErr.ErrorCode { + case EcodeTestFailed: + return topo.ErrBadVersion + case EcodeKeyNotFound: + return topo.ErrNoNode + case EcodeNodeExist: + return topo.ErrNodeExists + case EcodeDirNotEmpty: + return topo.ErrNotEmpty + } + default: + switch err { + case context.Canceled: + return topo.ErrInterrupted + case context.DeadlineExceeded: + return topo.ErrTimeout + } + } + return err +} diff --git a/go/vt/etcdtopo/explorer.go b/go/vt/etcdtopo/explorer.go new file mode 100644 index 00000000000..5a7c73bc7b1 --- /dev/null +++ b/go/vt/etcdtopo/explorer.go @@ -0,0 +1,206 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "encoding/json" + "fmt" + "html/template" + "net/http" + "path" + "strings" + + "github.com/coreos/go-etcd/etcd" + ctlproto "github.com/youtube/vitess/go/cmd/vtctld/proto" + "github.com/youtube/vitess/go/netutil" + "github.com/youtube/vitess/go/vt/topo" +) + +const ( + explorerRoot = "/etcd" + globalCell = "global" +) + +// Explorer is an implementation of vtctld's Explorer interface for etcd. +type Explorer struct { + ts *Server +} + +// NewExplorer implements vtctld Explorer. +func NewExplorer(ts *Server) *Explorer { + return &Explorer{ts: ts} +} + +// GetKeyspacePath implements vtctld Explorer. +func (ex Explorer) GetKeyspacePath(keyspace string) string { + return path.Join(explorerRoot, globalCell, keyspaceDirPath(keyspace)) +} + +// GetShardPath implements vtctld Explorer. +func (ex Explorer) GetShardPath(keyspace, shard string) string { + return path.Join(explorerRoot, globalCell, shardDirPath(keyspace, shard)) +} + +// GetSrvKeyspacePath implements vtctld Explorer. +func (ex Explorer) GetSrvKeyspacePath(cell, keyspace string) string { + return path.Join(explorerRoot, cell, srvKeyspaceDirPath(keyspace)) +} + +// GetSrvShardPath implements vtctld Explorer. +func (ex Explorer) GetSrvShardPath(cell, keyspace, shard string) string { + return path.Join(explorerRoot, cell, srvShardDirPath(keyspace, shard)) +} + +// GetSrvTypePath implements vtctld Explorer. +func (ex Explorer) GetSrvTypePath(cell, keyspace, shard string, tabletType topo.TabletType) string { + return path.Join(explorerRoot, cell, endPointsDirPath(keyspace, shard, string(tabletType))) +} + +// GetTabletPath implements vtctld Explorer. +func (ex Explorer) GetTabletPath(alias topo.TabletAlias) string { + return path.Join(explorerRoot, alias.Cell, tabletDirPath(alias.String())) +} + +// GetReplicationSlaves implements vtctld Explorer. +func (ex Explorer) GetReplicationSlaves(cell, keyspace, shard string) string { + return path.Join(explorerRoot, cell, shardReplicationDirPath(keyspace, shard)) +} + +// HandlePath implements vtctld Explorer. +func (ex Explorer) HandlePath(actionRepo ctlproto.ActionRepository, rPath string, r *http.Request) interface{} { + result := newExplorerResult(rPath) + + // Cut off explorerRoot prefix. + if !strings.HasPrefix(rPath, explorerRoot) { + result.Error = "invalid etcd explorer path: " + rPath + return result + } + rPath = rPath[len(explorerRoot):] + + // Root is a list of cells. + if rPath == "" { + cells, err := ex.ts.getCellList() + if err != nil { + result.Error = err.Error() + return result + } + result.Children = append([]string{globalCell}, cells...) + return result + } + + // Get a client for the requested cell. + var client Client + cell, rPath, err := splitCellPath(rPath) + if err != nil { + result.Error = err.Error() + return result + } + if cell == globalCell { + client = ex.ts.getGlobal() + } else { + client, err = ex.ts.getCell(cell) + if err != nil { + result.Error = "Can't get cell: " + err.Error() + return result + } + } + + // Get the requested node data. + resp, err := client.Get(rPath, true /* sort */, false /* recursive */) + if err != nil { + result.Error = err.Error() + return result + } + if resp.Node == nil { + result.Error = ErrBadResponse.Error() + return result + } + result.Data = getNodeData(client, resp.Node) + + // Populate children. + for _, node := range resp.Node.Nodes { + result.Children = append(result.Children, path.Base(node.Key)) + } + + // Populate actions. + if m, _ := path.Match(keyspaceDirPath("*"), rPath); m { + actionRepo.PopulateKeyspaceActions(result.Actions, path.Base(rPath)) + } else if m, _ := path.Match(shardDirPath("*", "*"), rPath); m { + if keyspace, shard, err := splitShardDirPath(rPath); err == nil { + actionRepo.PopulateShardActions(result.Actions, keyspace, shard) + } + } else if m, _ := path.Match(tabletDirPath("*"), rPath); m { + actionRepo.PopulateTabletActions(result.Actions, path.Base(rPath), r) + addTabletLinks(result, result.Data) + } + return result +} + +type explorerResult struct { + Path string + Data string + Links map[string]template.URL + Children []string + Actions map[string]template.URL + Error string +} + +func newExplorerResult(p string) *explorerResult { + return &explorerResult{ + Links: make(map[string]template.URL), + Actions: make(map[string]template.URL), + Path: p, + } +} + +func getNodeData(client Client, node *etcd.Node) string { + if !node.Dir { + return node.Value + } + // Directories don't have data, but some directories have a special data file. + resp, err := client.Get(path.Join(node.Key, dataFilename), false /* sort */, false /* recursive */) + if err != nil || resp.Node == nil { + return "" + } + return resp.Node.Value +} + +// splitCellPath returns the cell name, and the rest of the path. +// For example: "/cell/rest/of/path" -> "cell", "/rest/of/path" +func splitCellPath(p string) (cell, rest string, err error) { + parts := strings.SplitN(p, "/", 3) + if len(parts) < 2 || parts[0] != "" { + return "", "", fmt.Errorf("invalid etcd explorer path: %v", p) + } + if len(parts) < 3 { + return parts[1], "/", nil + } + return parts[1], "/" + parts[2], nil +} + +// splitShardDirPath takes a path that matches the path.Match() pattern +// shardDirPath("*", "*") and returns the keyspace and shard. +// +// We assume the path is of the form "/vt/keyspaces/*/*". +// If that ever changes, the unit test for this function will detect it. +func splitShardDirPath(p string) (keyspace, shard string, err error) { + parts := strings.Split(p, "/") + if len(parts) != 5 { + return "", "", fmt.Errorf("invalid shard dir path: %v", p) + } + return parts[3], parts[4], nil +} + +func addTabletLinks(result *explorerResult, data string) { + t := &topo.Tablet{} + err := json.Unmarshal([]byte(data), t) + if err != nil { + return + } + + if port, ok := t.Portmap["vt"]; ok { + result.Links["status"] = template.URL(fmt.Sprintf("http://%v/debug/status", netutil.JoinHostPort(t.Hostname, port))) + } +} diff --git a/go/vt/etcdtopo/explorer_test.go b/go/vt/etcdtopo/explorer_test.go new file mode 100644 index 00000000000..2f415d8ef01 --- /dev/null +++ b/go/vt/etcdtopo/explorer_test.go @@ -0,0 +1,198 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "html/template" + "net/http" + "path" + "reflect" + "strings" + "testing" + + "github.com/youtube/vitess/go/jscfg" + "github.com/youtube/vitess/go/vt/topo" +) + +func TestSplitCellPath(t *testing.T) { + table := map[string][]string{ + "/cell-a": []string{"cell-a", "/"}, + "/cell-b/x": []string{"cell-b", "/x"}, + "/cell1/other/stuff": []string{"cell1", "/other/stuff"}, + } + for input, want := range table { + cell, rest, err := splitCellPath(input) + if err != nil { + t.Errorf("splitCellPath error: %v", err) + } + if cell != want[0] || rest != want[1] { + t.Errorf("splitCellPath(%q) = (%q, %q), want (%q, %q)", + input, cell, rest, want[0], want[1]) + } + } +} + +func TestSplitShardDirPath(t *testing.T) { + // Make sure keyspace/shard names are preserved through a "round-trip". + input := shardDirPath("my-keyspace", "my-shard") + keyspace, shard, err := splitShardDirPath(input) + if err != nil { + t.Errorf("splitShardDirPath error: %v", err) + } + if keyspace != "my-keyspace" || shard != "my-shard" { + t.Errorf("splitShardDirPath(%q) = (%q, %q), want (%q, %q)", + input, keyspace, shard, "my-keyspace", "my-shard") + } +} + +func TestHandlePathInvalid(t *testing.T) { + // Don't panic! + ex := NewExplorer(nil) + result := ex.HandlePath(nil, "xxx", nil) + exResult := result.(*explorerResult) + if want := "invalid"; !strings.Contains(exResult.Error, want) { + t.Errorf("HandlePath returned wrong error: got %q, want %q", exResult.Error, want) + } +} + +func TestHandlePathRoot(t *testing.T) { + input := explorerRoot + cells := []string{"cell1", "cell2", "cell3"} + want := []string{"global", "cell1", "cell2", "cell3"} + + ts := newTestServer(t, cells) + ex := NewExplorer(ts) + result := ex.HandlePath(nil, input, nil) + exResult := result.(*explorerResult) + if got := exResult.Children; !reflect.DeepEqual(got, want) { + t.Errorf("HandlePath(%q) = %v, want %v", input, got, want) + } +} + +func TestHandlePathKeyspace(t *testing.T) { + input := path.Join(explorerRoot, "global", keyspaceDirPath("test_keyspace")) + cells := []string{"cell1", "cell2", "cell3"} + keyspace := &topo.Keyspace{} + shard := &topo.Shard{} + want := jscfg.ToJson(keyspace) + + ts := newTestServer(t, cells) + if err := ts.CreateKeyspace("test_keyspace", keyspace); err != nil { + t.Fatalf("CreateKeyspace error: %v", err) + } + if err := ts.CreateShard("test_keyspace", "10-20", shard); err != nil { + t.Fatalf("CreateShard error: %v", err) + } + if err := ts.CreateShard("test_keyspace", "20-30", shard); err != nil { + t.Fatalf("CreateShard error: %v", err) + } + + m := &mockActionRepo{} + ex := NewExplorer(ts) + result := ex.HandlePath(m, input, nil) + exResult := result.(*explorerResult) + if got := exResult.Data; got != want { + t.Errorf("HandlePath(%q) = %q, want %q", input, got, want) + } + if got, want := exResult.Children, []string{"10-20", "20-30"}; !reflect.DeepEqual(got, want) { + t.Errorf("Children = %v, want %v", got, want) + } + if m.keyspaceActions == nil { + t.Errorf("ActionRepository.PopulateKeyspaceActions not called") + } + if m.keyspace != "test_keyspace" { + t.Errorf("ActionRepository called with keyspace %q, want %q", m.keyspace, "test_keyspace") + } +} + +func TestHandlePathShard(t *testing.T) { + input := path.Join(explorerRoot, "global", shardDirPath("test_keyspace", "-80")) + cells := []string{"cell1", "cell2", "cell3"} + keyspace := &topo.Keyspace{} + shard := &topo.Shard{} + want := jscfg.ToJson(shard) + + ts := newTestServer(t, cells) + if err := ts.CreateKeyspace("test_keyspace", keyspace); err != nil { + t.Fatalf("CreateKeyspace error: %v", err) + } + if err := ts.CreateShard("test_keyspace", "-80", shard); err != nil { + t.Fatalf("CreateShard error: %v", err) + } + + m := &mockActionRepo{} + ex := NewExplorer(ts) + result := ex.HandlePath(m, input, nil) + exResult := result.(*explorerResult) + if got := exResult.Data; got != want { + t.Errorf("HandlePath(%q) = %q, want %q", input, got, want) + } + if m.shardActions == nil { + t.Errorf("ActionRepository.PopulateShardActions not called") + } + if m.keyspace != "test_keyspace" { + t.Errorf("ActionRepository called with keyspace %q, want %q", m.keyspace, "test_keyspace") + } + if m.shard != "-80" { + t.Errorf("ActionRepository called with shard %q, want %q", m.shard, "-80") + } +} + +func TestHandlePathTablet(t *testing.T) { + input := path.Join(explorerRoot, "cell1", tabletDirPath("cell1-0000000123")) + cells := []string{"cell1", "cell2", "cell3"} + tablet := &topo.Tablet{ + Alias: topo.TabletAlias{Cell: "cell1", Uid: 123}, + Hostname: "example.com", + Portmap: map[string]int{"vt": 4321}, + } + want := jscfg.ToJson(tablet) + + ts := newTestServer(t, cells) + if err := ts.CreateTablet(tablet); err != nil { + t.Fatalf("CreateTablet error: %v", err) + } + + m := &mockActionRepo{} + ex := NewExplorer(ts) + result := ex.HandlePath(m, input, nil) + exResult := result.(*explorerResult) + if got := exResult.Data; got != want { + t.Errorf("HandlePath(%q) = %q, want %q", input, got, want) + } + wantLinks := map[string]template.URL{ + "status": template.URL("http://example.com:4321/debug/status"), + } + for k, want := range wantLinks { + if got := exResult.Links[k]; got != want { + t.Errorf("Links[%q] = %v, want %v", k, got, want) + } + } + if m.tabletActions == nil { + t.Errorf("ActionRepository.PopulateTabletActions not called") + } + if m.tablet != "cell1-0000000123" { + t.Errorf("ActionRepository called with tablet %q, want %q", m.tablet, "cell1-0000000123") + } +} + +type mockActionRepo struct { + keyspace, shard, tablet string + keyspaceActions, shardActions, tabletActions map[string]template.URL +} + +func (m *mockActionRepo) PopulateKeyspaceActions(actions map[string]template.URL, keyspace string) { + m.keyspace = keyspace + m.keyspaceActions = actions +} +func (m *mockActionRepo) PopulateShardActions(actions map[string]template.URL, keyspace, shard string) { + m.keyspace = keyspace + m.shard = shard + m.shardActions = actions +} +func (m *mockActionRepo) PopulateTabletActions(actions map[string]template.URL, tablet string, r *http.Request) { + m.tablet = tablet + m.tabletActions = actions +} diff --git a/go/vt/etcdtopo/keyspace.go b/go/vt/etcdtopo/keyspace.go new file mode 100644 index 00000000000..10954a8db4b --- /dev/null +++ b/go/vt/etcdtopo/keyspace.go @@ -0,0 +1,126 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/youtube/vitess/go/event" + "github.com/youtube/vitess/go/jscfg" + "github.com/youtube/vitess/go/vt/concurrency" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/topo/events" +) + +// CreateKeyspace implements topo.Server. +func (s *Server) CreateKeyspace(keyspace string, value *topo.Keyspace) error { + data := jscfg.ToJson(value) + global := s.getGlobal() + + resp, err := global.Create(keyspaceFilePath(keyspace), data, 0 /* ttl */) + if err != nil { + return convertError(err) + } + if err := initLockFile(global, keyspaceDirPath(keyspace)); err != nil { + return err + } + + // We don't return ErrBadResponse in this case because the Create() suceeeded + // and we don't really need the version to satisfy our contract - we're only + // logging it. + version := int64(-1) + if resp.Node != nil { + version = int64(resp.Node.ModifiedIndex) + } + event.Dispatch(&events.KeyspaceChange{ + KeyspaceInfo: *topo.NewKeyspaceInfo(keyspace, value, version), + Status: "created", + }) + return nil +} + +// UpdateKeyspace implements topo.Server. +func (s *Server) UpdateKeyspace(ki *topo.KeyspaceInfo, existingVersion int64) (int64, error) { + data := jscfg.ToJson(ki.Keyspace) + + resp, err := s.getGlobal().CompareAndSwap(keyspaceFilePath(ki.KeyspaceName()), + data, 0 /* ttl */, "" /* prevValue */, uint64(existingVersion)) + if err != nil { + return -1, convertError(err) + } + if resp.Node == nil { + return -1, ErrBadResponse + } + + event.Dispatch(&events.KeyspaceChange{ + KeyspaceInfo: *ki, + Status: "updated", + }) + return int64(resp.Node.ModifiedIndex), nil +} + +// GetKeyspace implements topo.Server. +func (s *Server) GetKeyspace(keyspace string) (*topo.KeyspaceInfo, error) { + resp, err := s.getGlobal().Get(keyspaceFilePath(keyspace), false /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + if resp.Node == nil { + return nil, ErrBadResponse + } + + value := &topo.Keyspace{} + if err := json.Unmarshal([]byte(resp.Node.Value), value); err != nil { + return nil, fmt.Errorf("bad keyspace data (%v): %q", err, resp.Node.Value) + } + + return topo.NewKeyspaceInfo(keyspace, value, int64(resp.Node.ModifiedIndex)), nil +} + +// GetKeyspaces implements topo.Server. +func (s *Server) GetKeyspaces() ([]string, error) { + resp, err := s.getGlobal().Get(keyspacesDirPath, true /* sort */, false /* recursive */) + if err != nil { + err = convertError(err) + if err == topo.ErrNoNode { + return nil, nil + } + return nil, err + } + return getNodeNames(resp) +} + +// DeleteKeyspaceShards implements topo.Server. +func (s *Server) DeleteKeyspaceShards(keyspace string) error { + shards, err := s.GetShardNames(keyspace) + if err != nil { + return err + } + + wg := sync.WaitGroup{} + rec := concurrency.AllErrorRecorder{} + global := s.getGlobal() + for _, shard := range shards { + wg.Add(1) + go func(shard string) { + defer wg.Done() + _, err := global.Delete(shardDirPath(keyspace, shard), true /* recursive */) + rec.RecordError(convertError(err)) + }(shard) + } + wg.Wait() + + if err = rec.Error(); err != nil { + return err + } + + event.Dispatch(&events.KeyspaceChange{ + KeyspaceInfo: *topo.NewKeyspaceInfo(keyspace, nil, -1), + Status: "deleted all shards", + }) + return nil +} diff --git a/go/vt/etcdtopo/lock.go b/go/vt/etcdtopo/lock.go new file mode 100644 index 00000000000..b3752b0538c --- /dev/null +++ b/go/vt/etcdtopo/lock.go @@ -0,0 +1,226 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "fmt" + "path" + "strconv" + + "github.com/coreos/go-etcd/etcd" + log "github.com/golang/glog" + "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" +) + +const ( + lockFilename = "_Lock" + + // We can't use "" as the magic value for an un-held lock, since the etcd + // client library doesn't support CAS with an empty prevValue. It should + // also be something that the lock description is not allowed to be. + openLockContents = "" +) + +func initLockFile(client Client, dirPath string) error { + _, err := client.Set(path.Join(dirPath, lockFilename), openLockContents, 0 /* ttl */) + return convertError(err) +} + +// lock implements a simple distributed mutex lock on a directory in etcd. +// There used to be a lock module in etcd, and there will be again someday, but +// currently (as of v0.4.x) it has been removed due to lack of maintenance. +// +// See: https://github.com/coreos/etcd/blob/v0.4.6/Documentation/modules.md +// +// TODO(enisoc): Use etcd lock module if/when it exists. +// +// If mustExist is true, then before locking a directory, the file "_Lock" must +// already exist. This allows rejection of lock attempts on directories that +// don't exist yet, since otherwise etcd would automatically create parent +// directories. That means any directory that might be locked with mustExist +// should have a _Lock file created with initLockFile() as soon as the directory +// is created. +func lock(ctx context.Context, client Client, dirPath, contents string, mustExist bool) (string, error) { + lockPath := path.Join(dirPath, lockFilename) + var err, lockHeldErr error + if mustExist { + lockHeldErr = topo.ErrBadVersion + } else { + lockHeldErr = topo.ErrNodeExists + } + + // Don't let contents conflict with openLockContents. + contents = "held by: " + contents + + for { + // Check ctx.Done before the each attempt, so the entire function is a no-op + // if it's called with a Done context. + select { + case <-ctx.Done(): + return "", convertError(ctx.Err()) + default: + } + + var resp *etcd.Response + if mustExist { + // CAS will fail if the lock file isn't the magic "empty" value. + resp, err = client.CompareAndSwap(lockPath, contents, 0, /* ttl */ + openLockContents /* prevValue */, 0 /* prevIndex */) + } else { + // Create will fail if the lock file already exists. + resp, err = client.Create(lockPath, contents, 0 /* ttl */) + } + if err == nil { + if resp.Node == nil { + return "", ErrBadResponse + } + + // We got the lock. The index of the lock file can be used to + // verify during unlock() that we only delete our own lock. + // Add the index at the end of the lockPath to form the actionPath. + lockID := strconv.FormatUint(resp.Node.ModifiedIndex, 10) + return path.Join(lockPath, lockID), nil + } + + // If it fails for any reason other than lockHeldErr + // (meaning the lock is already held), then just give up. + if topoErr := convertError(err); topoErr != lockHeldErr { + return "", topoErr + } + etcdErr, ok := err.(*etcd.EtcdError) + if !ok { + return "", fmt.Errorf("error from etcd client has wrong type: got %#v, want %T", err, etcdErr) + } + + // The lock is already being held. + // Wait for the lock file to be deleted, then try again. + if err = waitForLock(ctx, client, lockPath, etcdErr.Index+1, mustExist); err != nil { + return "", err + } + } +} + +// unlock releases a lock acquired by lock() on the given directory. +// The string returned by lock() should be passed as the actionPath. +// +// mustExist specifies whether the lock was acquired with mustExist. +func unlock(client Client, dirPath, actionPath string, mustExist bool) error { + lockID := path.Base(actionPath) + lockPath := path.Join(dirPath, lockFilename) + + // Sanity check. + if checkPath := path.Join(lockPath, lockID); checkPath != actionPath { + return fmt.Errorf("unlock: actionPath doesn't match directory being unlocked: %q != %q", actionPath, checkPath) + } + + // Delete the node only if it belongs to us (the index matches). + prevIndex, err := strconv.ParseUint(lockID, 10, 64) + if err != nil { + return fmt.Errorf("unlock: can't parse lock ID (%v) in actionPath (%v): %v", lockID, actionPath, err) + } + if mustExist { + _, err = client.CompareAndSwap(lockPath, openLockContents, /* value */ + 0 /* ttl */, "" /* prevValue */, prevIndex) + } else { + _, err = client.CompareAndDelete(lockPath, "" /* prevValue */, prevIndex) + } + if err != nil { + return convertError(err) + } + + return nil +} + +// waitForLock will start a watch on the lockPath and return nil iff the watch +// returns an event saying the file was deleted. The waitIndex should be one +// plus the index at which you last found that the lock was held, to ensure that +// no delete actions are missed. +// +// mustExist specifies whether lock() was called with mustExist. +func waitForLock(ctx context.Context, client Client, lockPath string, waitIndex uint64, mustExist bool) error { + watch := make(chan *etcd.Response) + stop := make(chan bool) + defer close(stop) + + // Watch() will loop indefinitely, sending all updates starting at waitIndex + // to the watch chan until stop is closed, or an error occurs. + watchErr := make(chan error, 1) + go func() { + _, err := client.Watch(lockPath, waitIndex, false /* recursive */, watch, stop) + watchErr <- err + }() + + for { + select { + case <-ctx.Done(): + return convertError(ctx.Err()) + case err := <-watchErr: + return convertError(err) + case resp := <-watch: + if mustExist { + if resp.Node != nil && resp.Node.Value == openLockContents { + return nil + } + } else { + if resp.Action == "compareAndDelete" { + return nil + } + } + } + } +} + +// LockSrvShardForAction implements topo.Server. +func (s *Server) LockSrvShardForAction(ctx context.Context, cellName, keyspace, shard, contents string) (string, error) { + cell, err := s.getCell(cellName) + if err != nil { + return "", err + } + + return lock(ctx, cell.Client, srvShardDirPath(keyspace, shard), contents, + false /* mustExist */) +} + +// UnlockSrvShardForAction implements topo.Server. +func (s *Server) UnlockSrvShardForAction(cellName, keyspace, shard, actionPath, results string) error { + log.Infof("results of %v: %v", actionPath, results) + + cell, err := s.getCell(cellName) + if err != nil { + return err + } + + return unlock(cell.Client, srvShardDirPath(keyspace, shard), actionPath, + false /* mustExist */) +} + +// LockKeyspaceForAction implements topo.Server. +func (s *Server) LockKeyspaceForAction(ctx context.Context, keyspace, contents string) (string, error) { + return lock(ctx, s.getGlobal(), keyspaceDirPath(keyspace), contents, + true /* mustExist */) +} + +// UnlockKeyspaceForAction implements topo.Server. +func (s *Server) UnlockKeyspaceForAction(keyspace, actionPath, results string) error { + log.Infof("results of %v: %v", actionPath, results) + + return unlock(s.getGlobal(), keyspaceDirPath(keyspace), actionPath, + true /* mustExist */) +} + +// LockShardForAction implements topo.Server. +func (s *Server) LockShardForAction(ctx context.Context, keyspace, shard, contents string) (string, error) { + return lock(ctx, s.getGlobal(), shardDirPath(keyspace, shard), contents, + true /* mustExist */) +} + +// UnlockShardForAction implements topo.Server. +func (s *Server) UnlockShardForAction(keyspace, shard, actionPath, results string) error { + log.Infof("results of %v: %v", actionPath, results) + + return unlock(s.getGlobal(), shardDirPath(keyspace, shard), actionPath, + true /* mustExist */) +} diff --git a/go/vt/etcdtopo/replication_graph.go b/go/vt/etcdtopo/replication_graph.go new file mode 100644 index 00000000000..99cb1ba8b58 --- /dev/null +++ b/go/vt/etcdtopo/replication_graph.go @@ -0,0 +1,121 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "encoding/json" + "fmt" + + "github.com/youtube/vitess/go/jscfg" + "github.com/youtube/vitess/go/vt/topo" +) + +// UpdateShardReplicationFields implements topo.Server. +func (s *Server) UpdateShardReplicationFields(cell, keyspace, shard string, updateFunc func(*topo.ShardReplication) error) error { + var sri *topo.ShardReplicationInfo + var version int64 + var err error + + for { + if sri, version, err = s.getShardReplication(cell, keyspace, shard); err != nil { + if err == topo.ErrNoNode { + // Pass an empty struct to the update func, as specified in topo.Server. + sri = topo.NewShardReplicationInfo(&topo.ShardReplication{}, cell, keyspace, shard) + version = -1 + } else { + return err + } + } + if err = updateFunc(sri.ShardReplication); err != nil { + return err + } + if version == -1 { + if _, err = s.createShardReplication(sri); err != topo.ErrNodeExists { + return err + } + } else { + if _, err = s.updateShardReplication(sri, version); err != topo.ErrBadVersion { + return err + } + } + } +} + +func (s *Server) updateShardReplication(sri *topo.ShardReplicationInfo, existingVersion int64) (int64, error) { + cell, err := s.getCell(sri.Cell()) + if err != nil { + return -1, err + } + + data := jscfg.ToJson(sri.ShardReplication) + resp, err := cell.CompareAndSwap(shardReplicationFilePath(sri.Keyspace(), sri.Shard()), + data, 0 /* ttl */, "" /* prevValue */, uint64(existingVersion)) + if err != nil { + return -1, convertError(err) + } + if resp.Node == nil { + return -1, ErrBadResponse + } + + return int64(resp.Node.ModifiedIndex), nil +} + +func (s *Server) createShardReplication(sri *topo.ShardReplicationInfo) (int64, error) { + cell, err := s.getCell(sri.Cell()) + if err != nil { + return -1, err + } + + data := jscfg.ToJson(sri.ShardReplication) + resp, err := cell.Create(shardReplicationFilePath(sri.Keyspace(), sri.Shard()), + data, 0 /* ttl */) + if err != nil { + return -1, convertError(err) + } + if resp.Node == nil { + return -1, ErrBadResponse + } + + return int64(resp.Node.ModifiedIndex), nil +} + +// GetShardReplication implements topo.Server. +func (s *Server) GetShardReplication(cell, keyspace, shard string) (*topo.ShardReplicationInfo, error) { + sri, _, err := s.getShardReplication(cell, keyspace, shard) + return sri, err +} + +func (s *Server) getShardReplication(cellName, keyspace, shard string) (*topo.ShardReplicationInfo, int64, error) { + cell, err := s.getCell(cellName) + if err != nil { + return nil, -1, err + } + + resp, err := cell.Get(shardReplicationFilePath(keyspace, shard), false /* sort */, false /* recursive */) + if err != nil { + return nil, -1, convertError(err) + } + if resp.Node == nil { + return nil, -1, ErrBadResponse + } + + value := &topo.ShardReplication{} + if err := json.Unmarshal([]byte(resp.Node.Value), value); err != nil { + return nil, -1, fmt.Errorf("bad shard replication data (%v): %q", err, resp.Node.Value) + } + + return topo.NewShardReplicationInfo(value, cellName, keyspace, shard), int64(resp.Node.ModifiedIndex), nil +} + +// DeleteShardReplication implements topo.Server. +func (s *Server) DeleteShardReplication(cellName, keyspace, shard string) error { + cell, err := s.getCell(cellName) + if err != nil { + return err + } + + _, err = cell.Delete(shardReplicationDirPath(keyspace, shard), true /* recursive */) + return convertError(err) +} diff --git a/go/vt/etcdtopo/server.go b/go/vt/etcdtopo/server.go new file mode 100644 index 00000000000..e57bbbda4dd --- /dev/null +++ b/go/vt/etcdtopo/server.go @@ -0,0 +1,73 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package etcdtopo implements topo.Server with etcd as the backend. + +We expect the following behavior from the etcd client library: + + - Get and Delete return EcodeKeyNotFound if the node doesn't exist. + - Create returns EcodeNodeExist if the node already exists. + - Intermediate directories are always created automatically if necessary. + - CompareAndSwap returns EcodeKeyNotFound if the node doesn't exist already. + It returns EcodeTestFailed if the provided version index doesn't match. + +We follow these conventions within this package: + + - Call convertError(err) on any errors returned from the etcd client library. + Functions defined in this package can be assumed to have already converted + errors as necessary. +*/ +package etcdtopo + +import ( + "sync" + + "github.com/youtube/vitess/go/vt/topo" +) + +// Server is the implementation of topo.Server for etcd. +type Server struct { + // _global is a client configured to talk to a list of etcd instances + // representing the global etcd cluster. It should be accessed with the + // Server.getGlobal() method, which will initialize _global on first + // invocation with the list of global addresses from the command-line flag. + _global Client + _globalOnce sync.Once + + // _cells contains clients configured to talk to a list of etcd instances + // representing local etcd clusters. These should be accessed with the + // Server.getCell() method, which will read the list of addresses for that + // cell from the global cluster and create clients as needed. + _cells map[string]*cellClient + _cellsMutex sync.Mutex + + // newClient is the function this server uses to create a new Client. + newClient func(machines []string) Client +} + +// Close implements topo.Server. +func (s *Server) Close() { +} + +// GetKnownCells implements topo.Server. +func (s *Server) GetKnownCells() ([]string, error) { + resp, err := s.getGlobal().Get(cellsDirPath, true /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + return getNodeNames(resp) +} + +// NewServer returns a new etcdtopo.Server. +func NewServer() *Server { + return &Server{ + _cells: make(map[string]*cellClient), + newClient: newEtcdClient, + } +} + +func init() { + topo.RegisterServer("etcd", NewServer()) +} diff --git a/go/vt/etcdtopo/server_test.go b/go/vt/etcdtopo/server_test.go new file mode 100644 index 00000000000..e4dcf35ad8d --- /dev/null +++ b/go/vt/etcdtopo/server_test.go @@ -0,0 +1,103 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "testing" + + "github.com/youtube/vitess/go/flagutil" + "github.com/youtube/vitess/go/vt/topo/test" + "golang.org/x/net/context" +) + +func newTestServer(t *testing.T, cells []string) *Server { + s := &Server{ + _cells: make(map[string]*cellClient), + newClient: newTestClient, + } + + // In tests, use cell name as the address. + globalAddrs = flagutil.StringListValue([]string{"global"}) + c := s.getGlobal() + + // Add local cell "addresses" to the global cell. + for _, cell := range cells { + c.Set("/vt/cells/"+cell, cell, 0) + } + + return s +} + +func TestKeyspace(t *testing.T) { + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckKeyspace(t, ts) +} + +func TestShard(t *testing.T) { + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckShard(context.Background(), t, ts) +} + +func TestTablet(t *testing.T) { + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckTablet(context.Background(), t, ts) +} + +func TestShardReplication(t *testing.T) { + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckShardReplication(t, ts) +} + +func TestServingGraph(t *testing.T) { + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckServingGraph(context.Background(), t, ts) +} + +func TestWatchEndPoints(t *testing.T) { + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckWatchEndPoints(context.Background(), t, ts) +} + +func TestKeyspaceLock(t *testing.T) { + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckKeyspaceLock(t, ts) +} + +func TestShardLock(t *testing.T) { + if testing.Short() { + t.Skip("skipping wait-based test in short mode.") + } + + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckShardLock(t, ts) +} + +func TestSrvShardLock(t *testing.T) { + if testing.Short() { + t.Skip("skipping wait-based test in short mode.") + } + + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckSrvShardLock(t, ts) +} + +func TestVSchema(t *testing.T) { + if testing.Short() { + t.Skip("skipping wait-based test in short mode.") + } + + ts := newTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckVSchema(t, ts) +} diff --git a/go/vt/etcdtopo/serving_graph.go b/go/vt/etcdtopo/serving_graph.go new file mode 100644 index 00000000000..88a4f49cfe1 --- /dev/null +++ b/go/vt/etcdtopo/serving_graph.go @@ -0,0 +1,313 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "encoding/json" + "fmt" + "path" + "time" + + "github.com/coreos/go-etcd/etcd" + log "github.com/golang/glog" + "github.com/youtube/vitess/go/jscfg" + "github.com/youtube/vitess/go/vt/topo" +) + +// WatchSleepDuration is how many seconds interval to poll for in case +// we get an error from the Watch method. It is exported so individual +// test and main programs can change it. +var WatchSleepDuration = 30 * time.Second + +// GetSrvTabletTypesPerShard implements topo.Server. +func (s *Server) GetSrvTabletTypesPerShard(cellName, keyspace, shard string) ([]topo.TabletType, error) { + cell, err := s.getCell(cellName) + if err != nil { + return nil, err + } + + resp, err := cell.Get(srvShardDirPath(keyspace, shard), false /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + if resp.Node == nil { + return nil, ErrBadResponse + } + + tabletTypes := make([]topo.TabletType, 0, len(resp.Node.Nodes)) + for _, n := range resp.Node.Nodes { + tabletTypes = append(tabletTypes, topo.TabletType(path.Base(n.Key))) + } + return tabletTypes, nil +} + +// UpdateEndPoints implements topo.Server. +func (s *Server) UpdateEndPoints(cellName, keyspace, shard string, tabletType topo.TabletType, addrs *topo.EndPoints) error { + cell, err := s.getCell(cellName) + if err != nil { + return err + } + + data := jscfg.ToJson(addrs) + + _, err = cell.Set(endPointsFilePath(keyspace, shard, string(tabletType)), data, 0 /* ttl */) + return convertError(err) +} + +// updateEndPoints updates the EndPoints file only if the version matches. +func (s *Server) updateEndPoints(cellName, keyspace, shard string, tabletType topo.TabletType, addrs *topo.EndPoints, version int64) error { + cell, err := s.getCell(cellName) + if err != nil { + return err + } + + data := jscfg.ToJson(addrs) + + _, err = cell.CompareAndSwap(endPointsFilePath(keyspace, shard, string(tabletType)), data, 0, /* ttl */ + "" /* prevValue */, uint64(version)) + return convertError(err) +} + +// GetEndPoints implements topo.Server. +func (s *Server) GetEndPoints(cell, keyspace, shard string, tabletType topo.TabletType) (*topo.EndPoints, error) { + value, _, err := s.getEndPoints(cell, keyspace, shard, tabletType) + return value, err +} + +func (s *Server) getEndPoints(cellName, keyspace, shard string, tabletType topo.TabletType) (*topo.EndPoints, int64, error) { + cell, err := s.getCell(cellName) + if err != nil { + return nil, -1, err + } + + resp, err := cell.Get(endPointsFilePath(keyspace, shard, string(tabletType)), false /* sort */, false /* recursive */) + if err != nil { + return nil, -1, convertError(err) + } + if resp.Node == nil { + return nil, -1, ErrBadResponse + } + + value := &topo.EndPoints{} + if resp.Node.Value != "" { + if err := json.Unmarshal([]byte(resp.Node.Value), value); err != nil { + return nil, -1, fmt.Errorf("bad end points data (%v): %q", err, resp.Node.Value) + } + } + return value, int64(resp.Node.ModifiedIndex), nil +} + +// DeleteEndPoints implements topo.Server. +func (s *Server) DeleteEndPoints(cellName, keyspace, shard string, tabletType topo.TabletType) error { + cell, err := s.getCell(cellName) + if err != nil { + return err + } + + _, err = cell.Delete(endPointsDirPath(keyspace, shard, string(tabletType)), true /* recursive */) + return convertError(err) +} + +// UpdateSrvShard implements topo.Server. +func (s *Server) UpdateSrvShard(cellName, keyspace, shard string, srvShard *topo.SrvShard) error { + cell, err := s.getCell(cellName) + if err != nil { + return err + } + + data := jscfg.ToJson(srvShard) + + _, err = cell.Set(srvShardFilePath(keyspace, shard), data, 0 /* ttl */) + return convertError(err) +} + +// GetSrvShard implements topo.Server. +func (s *Server) GetSrvShard(cellName, keyspace, shard string) (*topo.SrvShard, error) { + cell, err := s.getCell(cellName) + if err != nil { + return nil, err + } + + resp, err := cell.Get(srvShardFilePath(keyspace, shard), false /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + if resp.Node == nil { + return nil, ErrBadResponse + } + + value := topo.NewSrvShard(int64(resp.Node.ModifiedIndex)) + if err := json.Unmarshal([]byte(resp.Node.Value), value); err != nil { + return nil, fmt.Errorf("bad serving shard data (%v): %q", err, resp.Node.Value) + } + return value, nil +} + +// DeleteSrvShard implements topo.Server. +func (s *Server) DeleteSrvShard(cellName, keyspace, shard string) error { + cell, err := s.getCell(cellName) + if err != nil { + return err + } + + _, err = cell.Delete(srvShardDirPath(keyspace, shard), true /* recursive */) + return convertError(err) +} + +// UpdateSrvKeyspace implements topo.Server. +func (s *Server) UpdateSrvKeyspace(cellName, keyspace string, srvKeyspace *topo.SrvKeyspace) error { + cell, err := s.getCell(cellName) + if err != nil { + return err + } + + data := jscfg.ToJson(srvKeyspace) + + _, err = cell.Set(srvKeyspaceFilePath(keyspace), data, 0 /* ttl */) + return convertError(err) +} + +// GetSrvKeyspace implements topo.Server. +func (s *Server) GetSrvKeyspace(cellName, keyspace string) (*topo.SrvKeyspace, error) { + cell, err := s.getCell(cellName) + if err != nil { + return nil, err + } + + resp, err := cell.Get(srvKeyspaceFilePath(keyspace), false /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + if resp.Node == nil { + return nil, ErrBadResponse + } + + value := topo.NewSrvKeyspace(int64(resp.Node.ModifiedIndex)) + if err := json.Unmarshal([]byte(resp.Node.Value), value); err != nil { + return nil, fmt.Errorf("bad serving keyspace data (%v): %q", err, resp.Node.Value) + } + return value, nil +} + +// GetSrvKeyspaceNames implements topo.Server. +func (s *Server) GetSrvKeyspaceNames(cellName string) ([]string, error) { + cell, err := s.getCell(cellName) + if err != nil { + return nil, err + } + + resp, err := cell.Get(servingDirPath, true /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + return getNodeNames(resp) +} + +// UpdateTabletEndpoint implements topo.Server. +func (s *Server) UpdateTabletEndpoint(cell, keyspace, shard string, tabletType topo.TabletType, addr *topo.EndPoint) error { + for { + addrs, version, err := s.getEndPoints(cell, keyspace, shard, tabletType) + if err == topo.ErrNoNode { + // It's ok if the EndPoints file doesn't exist yet. See topo.Server. + return nil + } + if err != nil { + return err + } + + // Update or add the record for the specified tablet. + found := false + for i, ep := range addrs.Entries { + if ep.Uid == addr.Uid { + found = true + addrs.Entries[i] = *addr + break + } + } + if !found { + addrs.Entries = append(addrs.Entries, *addr) + } + + // Update the record + err = s.updateEndPoints(cell, keyspace, shard, tabletType, addrs, version) + if err != topo.ErrBadVersion { + return err + } + } +} + +// WatchEndPoints is part of the topo.Server interface +func (s *Server) WatchEndPoints(cellName, keyspace, shard string, tabletType topo.TabletType) (<-chan *topo.EndPoints, chan<- struct{}, error) { + cell, err := s.getCell(cellName) + if err != nil { + return nil, nil, fmt.Errorf("WatchEndPoints cannot get cell: %v", err) + } + filePath := endPointsFilePath(keyspace, shard, string(tabletType)) + + notifications := make(chan *topo.EndPoints, 10) + stopWatching := make(chan struct{}) + + // The watch go routine will stop if the 'stop' channel is closed. + // Otherwise it will try to watch everything in a loop, and send events + // to the 'watch' channel. + watch := make(chan *etcd.Response) + stop := make(chan bool) + go func() { + // get the current version of the file + ep, modifiedVersion, err := s.getEndPoints(cellName, keyspace, shard, tabletType) + if err != nil { + // node doesn't exist + modifiedVersion = 0 + ep = nil + } + + // re-check for stop here to be safe, in case the + // getEndPoints took a long time + select { + case <-stop: + return + case notifications <- ep: + } + + for { + if _, err := cell.Client.Watch(filePath, uint64(modifiedVersion), false /* recursive */, watch, stop); err != nil { + log.Errorf("Watch on %v failed, waiting for %v to retry: %v", filePath, WatchSleepDuration, err) + timer := time.After(WatchSleepDuration) + select { + case <-stop: + return + case <-timer: + } + } + } + }() + + // This go routine is the main event handling routine: + // - it will stop if stopWatching is closed. + // - if it receives a notification from the watch, it will forward it + // to the notifications channel. + go func() { + for { + select { + case resp := <-watch: + var ep *topo.EndPoints + if resp.Node != nil && resp.Node.Value != "" { + ep = &topo.EndPoints{} + if err := json.Unmarshal([]byte(resp.Node.Value), ep); err != nil { + log.Errorf("failed to Unmarshal EndPoints for %v: %v", filePath, err) + continue + } + } + notifications <- ep + case <-stopWatching: + close(stop) + close(notifications) + return + } + } + }() + + return notifications, stopWatching, nil +} diff --git a/go/vt/etcdtopo/shard.go b/go/vt/etcdtopo/shard.go new file mode 100644 index 00000000000..32b20df7664 --- /dev/null +++ b/go/vt/etcdtopo/shard.go @@ -0,0 +1,109 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "encoding/json" + "fmt" + + "github.com/youtube/vitess/go/event" + "github.com/youtube/vitess/go/jscfg" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/topo/events" +) + +// CreateShard implements topo.Server. +func (s *Server) CreateShard(keyspace, shard string, value *topo.Shard) error { + data := jscfg.ToJson(value) + global := s.getGlobal() + + resp, err := global.Create(shardFilePath(keyspace, shard), data, 0 /* ttl */) + if err != nil { + return convertError(err) + } + if err := initLockFile(global, shardDirPath(keyspace, shard)); err != nil { + return err + } + + // We don't return ErrBadResponse in this case because the Create() suceeeded + // and we don't really need the version to satisfy our contract - we're only + // logging it. + version := int64(-1) + if resp.Node != nil { + version = int64(resp.Node.ModifiedIndex) + } + event.Dispatch(&events.ShardChange{ + ShardInfo: *topo.NewShardInfo(keyspace, shard, value, version), + Status: "created", + }) + return nil +} + +// UpdateShard implements topo.Server. +func (s *Server) UpdateShard(si *topo.ShardInfo, existingVersion int64) (int64, error) { + data := jscfg.ToJson(si.Shard) + + resp, err := s.getGlobal().CompareAndSwap(shardFilePath(si.Keyspace(), si.ShardName()), + data, 0 /* ttl */, "" /* prevValue */, uint64(existingVersion)) + if err != nil { + return -1, convertError(err) + } + if resp.Node == nil { + return -1, ErrBadResponse + } + + event.Dispatch(&events.ShardChange{ + ShardInfo: *si, + Status: "updated", + }) + return int64(resp.Node.ModifiedIndex), nil +} + +// ValidateShard implements topo.Server. +func (s *Server) ValidateShard(keyspace, shard string) error { + _, err := s.GetShard(keyspace, shard) + return err +} + +// GetShard implements topo.Server. +func (s *Server) GetShard(keyspace, shard string) (*topo.ShardInfo, error) { + resp, err := s.getGlobal().Get(shardFilePath(keyspace, shard), false /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + if resp.Node == nil { + return nil, ErrBadResponse + } + + value := &topo.Shard{} + if err := json.Unmarshal([]byte(resp.Node.Value), value); err != nil { + return nil, fmt.Errorf("bad shard data (%v): %q", err, resp.Node.Value) + } + + return topo.NewShardInfo(keyspace, shard, value, int64(resp.Node.ModifiedIndex)), nil +} + +// GetShardNames implements topo.Server. +func (s *Server) GetShardNames(keyspace string) ([]string, error) { + resp, err := s.getGlobal().Get(shardsDirPath(keyspace), true /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + return getNodeNames(resp) +} + +// DeleteShard implements topo.Server. +func (s *Server) DeleteShard(keyspace, shard string) error { + _, err := s.getGlobal().Delete(shardDirPath(keyspace, shard), true /* recursive */) + if err != nil { + return convertError(err) + } + + event.Dispatch(&events.ShardChange{ + ShardInfo: *topo.NewShardInfo(keyspace, shard, nil, -1), + Status: "deleted", + }) + return nil +} diff --git a/go/vt/etcdtopo/tablet.go b/go/vt/etcdtopo/tablet.go new file mode 100644 index 00000000000..a32be398acd --- /dev/null +++ b/go/vt/etcdtopo/tablet.go @@ -0,0 +1,173 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "encoding/json" + "fmt" + + "github.com/youtube/vitess/go/event" + "github.com/youtube/vitess/go/jscfg" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/topo/events" +) + +// CreateTablet implements topo.Server. +func (s *Server) CreateTablet(tablet *topo.Tablet) error { + cell, err := s.getCell(tablet.Alias.Cell) + if err != nil { + return err + } + + data := jscfg.ToJson(tablet) + _, err = cell.Create(tabletFilePath(tablet.Alias.String()), data, 0 /* ttl */) + if err != nil { + return convertError(err) + } + + event.Dispatch(&events.TabletChange{ + Tablet: *tablet, + Status: "created", + }) + return nil +} + +// UpdateTablet implements topo.Server. +func (s *Server) UpdateTablet(ti *topo.TabletInfo, existingVersion int64) (int64, error) { + cell, err := s.getCell(ti.Alias.Cell) + if err != nil { + return -1, err + } + + data := jscfg.ToJson(ti.Tablet) + resp, err := cell.CompareAndSwap(tabletFilePath(ti.Alias.String()), + data, 0 /* ttl */, "" /* prevValue */, uint64(existingVersion)) + if err != nil { + return -1, convertError(err) + } + if resp.Node == nil { + return -1, ErrBadResponse + } + + event.Dispatch(&events.TabletChange{ + Tablet: *ti.Tablet, + Status: "updated", + }) + return int64(resp.Node.ModifiedIndex), nil +} + +// UpdateTabletFields implements topo.Server. +func (s *Server) UpdateTabletFields(tabletAlias topo.TabletAlias, updateFunc func(*topo.Tablet) error) error { + var ti *topo.TabletInfo + var err error + + for { + if ti, err = s.GetTablet(tabletAlias); err != nil { + return err + } + if err = updateFunc(ti.Tablet); err != nil { + return err + } + if _, err = s.UpdateTablet(ti, ti.Version()); err != topo.ErrBadVersion { + break + } + } + if err != nil { + return err + } + + event.Dispatch(&events.TabletChange{ + Tablet: *ti.Tablet, + Status: "updated", + }) + return nil +} + +// DeleteTablet implements topo.Server. +func (s *Server) DeleteTablet(tabletAlias topo.TabletAlias) error { + cell, err := s.getCell(tabletAlias.Cell) + if err != nil { + return err + } + + // Get the keyspace and shard names for the TabletChange event. + ti, tiErr := s.GetTablet(tabletAlias) + + _, err = cell.Delete(tabletDirPath(tabletAlias.String()), true /* recursive */) + if err != nil { + return convertError(err) + } + + // Only try to log if we have the required info. + if tiErr == nil { + // Only copy the identity info for the tablet. The rest has been deleted. + event.Dispatch(&events.TabletChange{ + Tablet: topo.Tablet{ + Alias: ti.Tablet.Alias, + Keyspace: ti.Tablet.Keyspace, + Shard: ti.Tablet.Shard, + }, + Status: "deleted", + }) + } + return nil +} + +// ValidateTablet implements topo.Server. +func (s *Server) ValidateTablet(tabletAlias topo.TabletAlias) error { + _, err := s.GetTablet(tabletAlias) + return err +} + +// GetTablet implements topo.Server. +func (s *Server) GetTablet(tabletAlias topo.TabletAlias) (*topo.TabletInfo, error) { + cell, err := s.getCell(tabletAlias.Cell) + if err != nil { + return nil, err + } + + resp, err := cell.Get(tabletFilePath(tabletAlias.String()), false /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + if resp.Node == nil { + return nil, ErrBadResponse + } + + value := &topo.Tablet{} + if err := json.Unmarshal([]byte(resp.Node.Value), value); err != nil { + return nil, fmt.Errorf("bad tablet data (%v): %q", err, resp.Node.Value) + } + + return topo.NewTabletInfo(value, int64(resp.Node.ModifiedIndex)), nil +} + +// GetTabletsByCell implements topo.Server. +func (s *Server) GetTabletsByCell(cellName string) ([]topo.TabletAlias, error) { + cell, err := s.getCell(cellName) + if err != nil { + return nil, err + } + + resp, err := cell.Get(tabletsDirPath, false /* sort */, false /* recursive */) + if err != nil { + return nil, convertError(err) + } + + nodes, err := getNodeNames(resp) + if err != nil { + return nil, err + } + + tablets := make([]topo.TabletAlias, 0, len(nodes)) + for _, node := range nodes { + tabletAlias, err := topo.ParseTabletAliasString(node) + if err != nil { + return nil, err + } + tablets = append(tablets, tabletAlias) + } + return tablets, nil +} diff --git a/go/vt/etcdtopo/util.go b/go/vt/etcdtopo/util.go new file mode 100644 index 00000000000..f1967ecc397 --- /dev/null +++ b/go/vt/etcdtopo/util.go @@ -0,0 +1,26 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "path" + + "github.com/coreos/go-etcd/etcd" +) + +// getNodeNames returns a list of sub-node names listed in the given Response. +// Key names are given as fully qualified paths in the Response, so we return +// the base name. +func getNodeNames(resp *etcd.Response) ([]string, error) { + if resp.Node == nil { + return nil, ErrBadResponse + } + + names := make([]string, 0, len(resp.Node.Nodes)) + for _, n := range resp.Node.Nodes { + names = append(names, path.Base(n.Key)) + } + return names, nil +} diff --git a/go/vt/etcdtopo/util_test.go b/go/vt/etcdtopo/util_test.go new file mode 100644 index 00000000000..56c7cf08804 --- /dev/null +++ b/go/vt/etcdtopo/util_test.go @@ -0,0 +1,39 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etcdtopo + +import ( + "reflect" + "testing" + + "github.com/coreos/go-etcd/etcd" +) + +func TestGetNodeNamesNil(t *testing.T) { + input := &etcd.Response{} + if _, got := getNodeNames(input); got != ErrBadResponse { + t.Errorf("wrong error: got %#v, want %#v", got, ErrBadResponse) + } +} + +func TestGetNodeNames(t *testing.T) { + input := &etcd.Response{ + Node: &etcd.Node{ + Nodes: etcd.Nodes{ + &etcd.Node{Key: "/dir/dir/node1"}, + &etcd.Node{Key: "/dir/dir/node2"}, + &etcd.Node{Key: "/dir/dir/node3"}, + }, + }, + } + want := []string{"node1", "node2", "node3"} + got, err := getNodeNames(input) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("getNodeNames() = %#v, want %#v", got, want) + } +} diff --git a/go/vt/etcdtopo/vschema.go b/go/vt/etcdtopo/vschema.go new file mode 100644 index 00000000000..7a37234f4f1 --- /dev/null +++ b/go/vt/etcdtopo/vschema.go @@ -0,0 +1,44 @@ +package etcdtopo + +import ( + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" + // vindexes needs to be imported so that they register + // themselves against vtgate/planbuilder. This will allow + // us to sanity check the schema being uploaded. + _ "github.com/youtube/vitess/go/vt/vtgate/vindexes" +) + +/* +This file contains the vschema management code for etcdtopo.Server +*/ + +// SaveVSchema saves the JSON vschema into the topo. +func (s *Server) SaveVSchema(vschema string) error { + _, err := planbuilder.NewSchema([]byte(vschema)) + if err != nil { + return err + } + + _, err = s.getGlobal().Set(vschemaPath, vschema, 0 /* ttl */) + if err != nil { + return convertError(err) + } + return nil +} + +// GetVSchema fetches the JSON vschema from the topo. +func (s *Server) GetVSchema() (string, error) { + resp, err := s.getGlobal().Get(vschemaPath, false /* sort */, false /* recursive */) + if err != nil { + err = convertError(err) + if err == topo.ErrNoNode { + return "{}", nil + } + return "", err + } + if resp.Node == nil { + return "", ErrBadResponse + } + return resp.Node.Value, nil +} diff --git a/go/vt/health/health.go b/go/vt/health/health.go index 6567eaed4af..c44c52014ca 100644 --- a/go/vt/health/health.go +++ b/go/vt/health/health.go @@ -3,8 +3,10 @@ package health import ( "fmt" "html/template" + "sort" "strings" "sync" + "time" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/topo" @@ -14,27 +16,19 @@ var ( defaultAggregator *Aggregator ) -const ( - // ReplicationLag should be the key for any reporters - // reporting MySQL repliaction lag. - ReplicationLag = "replication_lag" - - // ReplicationLagHigh should be the value for any reporters - // indicating that the replication lag is too high. - ReplicationLagHigh = "high" -) - func init() { defaultAggregator = NewAggregator() } // Reporter reports the health status of a tablet. type Reporter interface { - // Report returns a map of health states for the tablet - // assuming that its tablet type is typ. If Report returns an - // error it implies that the tablet is in a bad shape and not - // able to handle queries. - Report(typ topo.TabletType) (status map[string]string, err error) + // Report returns the replication delay gathered by this + // module (or 0 if it thinks it's not behind), assuming that + // its tablet type is TabletType, and that its query service + // should be running or not. If Report returns an error it + // implies that the tablet is in a bad shape and not able to + // handle queries. + Report(tabletType topo.TabletType, shouldQueryServiceBeRunning bool) (replicationDelay time.Duration, err error) // HTMLName returns a displayable name for the module. // Can be used to be displayed in the status page. @@ -42,11 +36,11 @@ type Reporter interface { } // FunctionReporter is a function that may act as a Reporter. -type FunctionReporter func(typ topo.TabletType) (map[string]string, error) +type FunctionReporter func(topo.TabletType, bool) (time.Duration, error) // Report implements Reporter.Report -func (fc FunctionReporter) Report(typ topo.TabletType) (status map[string]string, err error) { - return fc(typ) +func (fc FunctionReporter) Report(tabletType topo.TabletType, shouldQueryServiceBeRunning bool) (time.Duration, error) { + return fc(tabletType, shouldQueryServiceBeRunning) } // HTMLName implements Reporter.HTMLName @@ -68,46 +62,44 @@ func NewAggregator() *Aggregator { } } -// Run runs aggregates health statuses from all the reporters. If any +// Run aggregates health statuses from all the reporters. If any // errors occur during the reporting, they will be logged, but only // the first error will be returned. -// It may return an empty map if no health condition is detected. Note -// it will not return nil, but an empty map. -func (ag *Aggregator) Run(typ topo.TabletType) (map[string]string, error) { +// The returned replication delay will be the highest of all the replication +// delays returned by the Reporter implementations (although typically +// only one implementation will actually return a meaningful one). +func (ag *Aggregator) Run(tabletType topo.TabletType, shouldQueryServiceBeRunning bool) (time.Duration, error) { var ( wg sync.WaitGroup rec concurrency.AllErrorRecorder ) - results := make(chan map[string]string, len(ag.reporters)) + results := make(chan time.Duration, len(ag.reporters)) ag.mu.Lock() for name, rep := range ag.reporters { wg.Add(1) go func(name string, rep Reporter) { defer wg.Done() - status, err := rep.Report(typ) + replicationDelay, err := rep.Report(tabletType, shouldQueryServiceBeRunning) if err != nil { rec.RecordError(fmt.Errorf("%v: %v", name, err)) return } - results <- status + results <- replicationDelay }(name, rep) } ag.mu.Unlock() wg.Wait() close(results) if err := rec.Error(); err != nil { - return nil, err + return 0, err } // merge and return the results - result := make(map[string]string) - for part := range results { - for k, v := range part { - if _, ok := result[k]; ok { - return nil, fmt.Errorf("duplicate key: %v", k) - } - result[k] = v + var result time.Duration + for replicationDelay := range results { + if replicationDelay > result { + result = replicationDelay } } return result, nil @@ -133,13 +125,14 @@ func (ag *Aggregator) HTMLName() template.HTML { for _, rep := range ag.reporters { result = append(result, string(rep.HTMLName())) } + sort.Strings(result) return template.HTML(strings.Join(result, "  +  ")) } // Run collects all the health statuses from the default health // aggregator. -func Run(typ topo.TabletType) (map[string]string, error) { - return defaultAggregator.Run(typ) +func Run(tabletType topo.TabletType, shouldQueryServiceBeRunning bool) (time.Duration, error) { + return defaultAggregator.Run(tabletType, shouldQueryServiceBeRunning) } // Register registers rep under name with the default health diff --git a/go/vt/health/health_test.go b/go/vt/health/health_test.go index 23d132dd7e1..4f3ac32dd3b 100644 --- a/go/vt/health/health_test.go +++ b/go/vt/health/health_test.go @@ -2,8 +2,8 @@ package health import ( "errors" - "reflect" "testing" + "time" "github.com/youtube/vitess/go/vt/topo" ) @@ -12,36 +12,32 @@ func TestReporters(t *testing.T) { ag := NewAggregator() - ag.Register("a", FunctionReporter(func(typ topo.TabletType) (map[string]string, error) { - return map[string]string{"a": "value", "b": "value"}, nil + ag.Register("a", FunctionReporter(func(topo.TabletType, bool) (time.Duration, error) { + return 10 * time.Second, nil })) - ag.Register("b", FunctionReporter(func(typ topo.TabletType) (map[string]string, error) { - return map[string]string{"c": "value"}, nil + ag.Register("b", FunctionReporter(func(topo.TabletType, bool) (time.Duration, error) { + return 5 * time.Second, nil })) - status, err := ag.Run(topo.TYPE_REPLICA) + delay, err := ag.Run(topo.TYPE_REPLICA, true) if err != nil { t.Error(err) } - if want := map[string]string(map[string]string{"a": "value", "b": "value", "c": "value"}); !reflect.DeepEqual(status, want) { - t.Errorf("status=%#v, want %#v", status, want) + if delay != 10*time.Second { + t.Errorf("delay=%v, want 10s", delay) } - ag.Register("c", FunctionReporter(func(typ topo.TabletType) (map[string]string, error) { - return nil, errors.New("e error") + ag.Register("c", FunctionReporter(func(topo.TabletType, bool) (time.Duration, error) { + return 0, errors.New("e error") })) - if _, err := ag.Run(topo.TYPE_REPLICA); err == nil { + if _, err := ag.Run(topo.TYPE_REPLICA, false); err == nil { t.Errorf("ag.Run: expected error") } - // Handle duplicate keys. - ag.Register("d", FunctionReporter(func(typ topo.TabletType) (map[string]string, error) { - return map[string]string{"a": "value"}, nil - })) - - if _, err := ag.Run(topo.TYPE_REPLICA); err == nil { - t.Errorf("ag.Run: expected error for duplicate keys") + name := ag.HTMLName() + if string(name) != "FunctionReporter  +  FunctionReporter  +  FunctionReporter" { + t.Errorf("ag.HTMLName() returned: %v", name) } } diff --git a/go/vt/key/key.go b/go/vt/key/key.go index bc9ce5bd257..c101d1a4cc0 100644 --- a/go/vt/key/key.go +++ b/go/vt/key/key.go @@ -26,6 +26,8 @@ var MaxKey = KeyspaceId("") // KeyspaceId is the type we base sharding on. type KeyspaceId string +//go:generate bsongen -file $GOFILE -type KeyspaceId -o keyspace_id_bson.go + // Hex prints a KeyspaceId in lower case hex. func (kid KeyspaceId) Hex() HexKeyspaceId { return HexKeyspaceId(hex.EncodeToString([]byte(kid))) @@ -84,6 +86,8 @@ func (hkid HexKeyspaceId) Unhex() (KeyspaceId, error) { // Usually we don't care, but some parts of the code will need that info. type KeyspaceIdType string +//go:generate bsongen -file $GOFILE -type KeyspaceIdType -o keyspace_id_type_bson.go + const ( // unset - no type for this KeyspaceId KIT_UNSET = KeyspaceIdType("") @@ -119,12 +123,14 @@ func IsKeyspaceIdTypeInList(typ KeyspaceIdType, types []KeyspaceIdType) bool { // // KeyRange is an interval of KeyspaceId values. It contains Start, -// but excludes End. In other words, it is: [Start, End[ +// but excludes End. In other words, it is: [Start, End) type KeyRange struct { Start KeyspaceId End KeyspaceId } +//go:generate bsongen -file $GOFILE -type KeyRange -o key_range_bson.go + func (kr KeyRange) MapKey() string { return string(kr.Start) + "-" + string(kr.End) } diff --git a/go/vt/logutil/logutil.go b/go/vt/logutil/logutil.go index a1e66461fad..df87eae20f8 100644 --- a/go/vt/logutil/logutil.go +++ b/go/vt/logutil/logutil.go @@ -4,7 +4,6 @@ package logutil import ( - "flag" stdlog "log" log "github.com/golang/glog" @@ -22,13 +21,3 @@ func init() { stdlog.SetFlags(0) stdlog.SetOutput(new(logShim)) } - -// GetSubprocessFlags returns the list of flags to use to have subprocesses -// log in the same directory as the current process. -func GetSubprocessFlags() []string { - logDir := flag.Lookup("log_dir") - if logDir == nil { - panic("the logging module doesn't specify a log_dir flag") - } - return []string{"-log_dir", logDir.Value.String()} -} diff --git a/go/vt/mysqlctl/clone.go b/go/vt/mysqlctl/clone.go index c1c979e876b..7bbd5e8dbde 100644 --- a/go/vt/mysqlctl/clone.go +++ b/go/vt/mysqlctl/clone.go @@ -29,6 +29,7 @@ const ( const ( SnapshotManifestFile = "snapshot_manifest.json" + SnapshotURLPath = "/snapshot" ) // Validate that this instance is a reasonable source of data. @@ -264,7 +265,7 @@ func (mysqld *Mysqld) CreateSnapshot(logger logutil.Logger, dbName, sourceAddr s // Stop sources of writes so we can get a consistent replication position. // If the source is a slave use the master replication position - // unless we are allowing hierachical replicas. + // unless we are allowing hierarchical replicas. masterAddr := "" var replicationPosition proto.ReplicationPosition if sourceIsMaster { diff --git a/go/vt/mysqlctl/csvsplitter/csv_reader.go b/go/vt/mysqlctl/csvsplitter/csv_reader.go deleted file mode 100644 index 3460887459c..00000000000 --- a/go/vt/mysqlctl/csvsplitter/csv_reader.go +++ /dev/null @@ -1,56 +0,0 @@ -package csvsplitter - -import ( - "bufio" - "bytes" - "io" -) - -type CSVReader struct { - reader *bufio.Reader - delim byte - buf *bytes.Buffer -} - -func NewCSVReader(r io.Reader, delim byte) *CSVReader { - return &CSVReader{ - reader: bufio.NewReader(r), - delim: delim, - buf: bytes.NewBuffer(make([]byte, 0, 1024)), - } -} - -// ReadRecord returns a keyspaceId and a line from which it was -// extracted, with the keyspaceId stripped. -func (r CSVReader) ReadRecord() (line []byte, err error) { - defer r.buf.Reset() - - escaped := false - inQuote := false - for { - b, err := r.reader.ReadByte() - if err != nil { - // Assumption: the csv file ends with a - // newline. Otherwise io.EOF should be treated - // separately. - return nil, err - } - - r.buf.WriteByte(b) - - if escaped { - escaped = false - continue - } - switch b { - case '\\': - escaped = true - case '"': - inQuote = !inQuote - case '\n': - if !inQuote { - return r.buf.Bytes(), nil - } - } - } -} diff --git a/go/vt/mysqlctl/csvsplitter/csv_reader_test.go b/go/vt/mysqlctl/csvsplitter/csv_reader_test.go deleted file mode 100644 index 547bed63c89..00000000000 --- a/go/vt/mysqlctl/csvsplitter/csv_reader_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package csvsplitter - -import ( - "io" - "os" - "testing" - - "github.com/youtube/vitess/go/testfiles" -) - -func readLines(t *testing.T, name string) []string { - file, err := os.Open(testfiles.Locate(name)) - if err != nil { - t.Fatalf("Cannot open %v: %v", name, err) - } - r := NewCSVReader(file, ',') - - lines := make([]string, 0) - - for { - line, err := r.ReadRecord() - if err == io.EOF { - break - } - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - lines = append(lines, string(line)) - } - - return lines -} - -func checkWantedLines(t *testing.T, got, expected []string) { - if len(got) != len(expected) { - t.Fatalf("Wrong number of records: expected %v, got %v", len(expected), len(got)) - } - - for i, wanted := range expected { - if got[i] != wanted { - t.Errorf("Wrong line: expected %q got %q", wanted, got[i]) - } - } - -} - -func TestCSVReader1(t *testing.T) { - // csvsplitter_mean.csv was generated using "select keyspaced_id, - // tablename.* into outfile". - lines := readLines(t, "csvsplitter_mean.csv") - - wantedTable := []string{ - "1,\"x\x9c\xf3H\xcd\xc9\xc9W(\xcf/\xcaI\x01\\0\x18\xab\x04=\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",1,\"ala\\\nhas a cat\\\n\",1\n", - "2,\"x\x9c\xf3\xc8\xcfIT\xc8-\xcdK\xc9\a\\0\x13\xfe\x03\xc8\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",2,\"ala\\\ntiene un gato\\\\\\\n\r\\\n\",2\n", - "3,\"x\x9cs\xceL\xccW\xc8\xcd\xcfK\xc9\a\\0\x13\x88\x03\xba\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",3,\"ala\\\nha un gatto\\\\n\\\n\",3\n", - "4,\"x\x9cs\xca\xcf\xcb\xca/-R\xc8\xcd\xcfKI\x05\\0#:\x05\x13\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",3,\",,,ala \\\"\\\n,a un chat\",4\n", - } - - checkWantedLines(t, lines, wantedTable) -} - -func TestCSVReader2(t *testing.T) { - // mean.csvcsvsplitter_mean.csv was generated from - // csvsplitter_mean.csv and changing the ids into hex - lines := readLines(t, "csvsplitter_mean_hex.csv") - - wantedTable := []string{ - "\"78fe\",\"x\x9c\xf3H\xcd\xc9\xc9W(\xcf/\xcaI\x01\\0\x18\xab\x04=\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",1,\"ala\\\nhas a cat\\\n\",1\n", - "\"34ef\",\"x\x9c\xf3\xc8\xcfIT\xc8-\xcdK\xc9\a\\0\x13\xfe\x03\xc8\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",2,\"ala\\\ntiene un gato\\\\\\\n\r\\\n\",2\n", - "\"A4F6\",\"x\x9cs\xceL\xccW\xc8\xcd\xcfK\xc9\a\\0\x13\x88\x03\xba\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",3,\"ala\\\nha un gatto\\\\n\\\n\",3\n", - "\"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\",\"x\x9cs\xca\xcf\xcb\xca/-R\xc8\xcd\xcfKI\x05\\0#:\x05\x13\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",3,\",,,ala \\\"\\\n,a un chat\",4\n", - } - - checkWantedLines(t, lines, wantedTable) -} diff --git a/go/vt/mysqlctl/csvsplitter/keyspace_csv_reader.go b/go/vt/mysqlctl/csvsplitter/keyspace_csv_reader.go deleted file mode 100644 index 7a16ef26275..00000000000 --- a/go/vt/mysqlctl/csvsplitter/keyspace_csv_reader.go +++ /dev/null @@ -1,84 +0,0 @@ -package csvsplitter - -import ( - "bufio" - "bytes" - "io" - "strconv" - - "github.com/youtube/vitess/go/vt/key" -) - -type KeyspaceCSVReader struct { - reader *bufio.Reader - delim byte - numberColumn bool - buf *bytes.Buffer -} - -func NewKeyspaceCSVReader(r io.Reader, delim byte, numberColumn bool) *KeyspaceCSVReader { - return &KeyspaceCSVReader{ - reader: bufio.NewReader(r), - delim: delim, - numberColumn: numberColumn, - buf: bytes.NewBuffer(make([]byte, 0, 1024)), - } -} - -// ReadRecord returns a keyspaceId and a line from which it was -// extracted, with the keyspaceId stripped. -func (r KeyspaceCSVReader) ReadRecord() (keyspaceId key.KeyspaceId, line []byte, err error) { - k, err := r.reader.ReadString(r.delim) - if err != nil { - return key.MinKey, nil, err - } - if r.numberColumn { - // the line starts with: - // NNNN, - // so remove the comma - kid, err := strconv.ParseUint(k[:len(k)-1], 10, 64) - if err != nil { - return key.MinKey, nil, err - } - keyspaceId = key.Uint64Key(kid).KeyspaceId() - } else { - // the line starts with: - // "HHHH", - // so remove the quotes and comma - keyspaceId, err = key.HexKeyspaceId(k[1 : len(k)-2]).Unhex() - if err != nil { - return key.MinKey, nil, err - } - } - - defer r.buf.Reset() - - escaped := false - inQuote := false - for { - b, err := r.reader.ReadByte() - if err != nil { - // Assumption: the csv file ends with a - // newline. Otherwise io.EOF should be treated - // separately. - return key.MinKey, nil, err - } - - r.buf.WriteByte(b) - - if escaped { - escaped = false - continue - } - switch b { - case '\\': - escaped = true - case '"': - inQuote = !inQuote - case '\n': - if !inQuote { - return keyspaceId, r.buf.Bytes(), nil - } - } - } -} diff --git a/go/vt/mysqlctl/csvsplitter/keyspace_csv_reader_test.go b/go/vt/mysqlctl/csvsplitter/keyspace_csv_reader_test.go deleted file mode 100644 index 3ec6e4b67ef..00000000000 --- a/go/vt/mysqlctl/csvsplitter/keyspace_csv_reader_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package csvsplitter - -import ( - "io" - "os" - "testing" - - "github.com/youtube/vitess/go/testfiles" - "github.com/youtube/vitess/go/vt/key" -) - -type pair struct { - kid key.KeyspaceId - line string -} - -func readData(t *testing.T, name string, numberColumn bool) []pair { - file, err := os.Open(testfiles.Locate(name)) - if err != nil { - t.Fatalf("Cannot open %v: %v", name, err) - } - r := NewKeyspaceCSVReader(file, ',', numberColumn) - - keyspaceIds := make([]pair, 0) - - for { - kid, line, err := r.ReadRecord() - if err == io.EOF { - break - } - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - keyspaceIds = append(keyspaceIds, pair{kid, string(line)}) - } - - return keyspaceIds -} - -func checkWanted(t *testing.T, got, expected []pair) { - if len(got) != len(expected) { - t.Fatalf("Wrong number of records: expected %v, got %v", len(expected), len(got)) - } - - for i, wanted := range expected { - if got[i].kid != key.KeyspaceId(wanted.kid) { - t.Errorf("Wrong keyspace_id: expected %#v, got %#v", wanted.kid, got[i].kid) - } - if got[i].line != wanted.line { - t.Errorf("Wrong line: expected %q got %q", wanted.line, got[i].line) - } - } - -} - -func TestCSVSplitterNumber(t *testing.T) { - // csvsplitter_mean.csv was generated using "select keyspaced_id, - // tablename.* into outfile". - keyspaceIds := readData(t, "csvsplitter_mean.csv", true) - - wantedTable := []pair{ - {key.Uint64Key(1).KeyspaceId(), "\"x\x9c\xf3H\xcd\xc9\xc9W(\xcf/\xcaI\x01\\0\x18\xab\x04=\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",1,\"ala\\\nhas a cat\\\n\",1\n"}, - {key.Uint64Key(2).KeyspaceId(), "\"x\x9c\xf3\xc8\xcfIT\xc8-\xcdK\xc9\a\\0\x13\xfe\x03\xc8\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",2,\"ala\\\ntiene un gato\\\\\\\n\r\\\n\",2\n"}, - {key.Uint64Key(3).KeyspaceId(), "\"x\x9cs\xceL\xccW\xc8\xcd\xcfK\xc9\a\\0\x13\x88\x03\xba\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",3,\"ala\\\nha un gatto\\\\n\\\n\",3\n"}, - {key.Uint64Key(4).KeyspaceId(), "\"x\x9cs\xca\xcf\xcb\xca/-R\xc8\xcd\xcfKI\x05\\0#:\x05\x13\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",3,\",,,ala \\\"\\\n,a un chat\",4\n"}, - } - - checkWanted(t, keyspaceIds, wantedTable) -} - -func hexOrDie(t *testing.T, hex string) key.KeyspaceId { - kid, err := key.HexKeyspaceId(hex).Unhex() - if err != nil { - t.Fatalf("Unhex failed: %v", err) - } - return kid -} - -func TestCSVSplitterHex(t *testing.T) { - // mean.csvcsvsplitter_mean.csv was generated from - // csvsplitter_mean.csv and changing the ids into hex - keyspaceIds := readData(t, "csvsplitter_mean_hex.csv", false) - - wantedTable := []pair{ - {hexOrDie(t, "78fe"), "\"x\x9c\xf3H\xcd\xc9\xc9W(\xcf/\xcaI\x01\\0\x18\xab\x04=\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",1,\"ala\\\nhas a cat\\\n\",1\n"}, - {hexOrDie(t, "34ef"), "\"x\x9c\xf3\xc8\xcfIT\xc8-\xcdK\xc9\a\\0\x13\xfe\x03\xc8\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",2,\"ala\\\ntiene un gato\\\\\\\n\r\\\n\",2\n"}, - {hexOrDie(t, "a4f6"), "\"x\x9cs\xceL\xccW\xc8\xcd\xcfK\xc9\a\\0\x13\x88\x03\xba\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",3,\"ala\\\nha un gatto\\\\n\\\n\",3\n"}, - {hexOrDie(t, "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"), "\"x\x9cs\xca\xcf\xcb\xca/-R\xc8\xcd\xcfKI\x05\\0#:\x05\x13\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\",3,\",,,ala \\\"\\\n,a un chat\",4\n"}, - } - - checkWanted(t, keyspaceIds, wantedTable) -} diff --git a/go/vt/mysqlctl/fileutil.go b/go/vt/mysqlctl/fileutil.go index d97131e017c..3276171d89e 100644 --- a/go/vt/mysqlctl/fileutil.go +++ b/go/vt/mysqlctl/fileutil.go @@ -11,7 +11,6 @@ import ( "fmt" "hash" // "hash/crc64" - "encoding/json" "io" "io/ioutil" "net/http" @@ -24,7 +23,6 @@ import ( log "github.com/golang/glog" "github.com/youtube/vitess/go/cgzip" - "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/mysqlctl/proto" ) @@ -306,56 +304,6 @@ func newSnapshotManifest(addr, mysqlAddr, masterAddr, dbName string, files []Sna return rs, nil } -func fetchSnapshotManifestWithRetry(addr, dbName string, keyRange key.KeyRange, retryCount int) (ssm *SplitSnapshotManifest, err error) { - for i := 0; i < retryCount; i++ { - if ssm, err = fetchSnapshotManifest(addr, dbName, keyRange); err == nil { - return - } - } - return -} - -// fetchSnapshotManifest fetches the manifest for keyRange from -// vttablet serving at addr. -func fetchSnapshotManifest(addr, dbName string, keyRange key.KeyRange) (*SplitSnapshotManifest, error) { - shardName := fmt.Sprintf("%v-%v,%v", dbName, keyRange.Start.Hex(), keyRange.End.Hex()) - path := path.Join(SnapshotURLPath, "data", shardName, partialSnapshotManifestFile) - url := addr + path - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if sc := resp.StatusCode; sc != 200 { - return nil, fmt.Errorf("GET %v returned with a non-200 status code (%v): %q", url, sc, data) - } - - ssm := new(SplitSnapshotManifest) - if err = json.Unmarshal(data, ssm); err != nil { - return nil, fmt.Errorf("fetchSnapshotManifest failed: %v %v", url, err) - } - return ssm, nil -} - -func readSnapshotManifest(location string) (*SplitSnapshotManifest, error) { - filename := path.Join(location, partialSnapshotManifestFile) - data, err := ioutil.ReadFile(filename) - if err != nil { - return nil, fmt.Errorf("io.ReadFile failed: %v", err) - } - ssm := new(SplitSnapshotManifest) - if err = json.Unmarshal(data, ssm); err != nil { - return nil, fmt.Errorf("json.Unmarshal failed: %v %v", filename, err) - } - return ssm, nil -} - // fetchFile fetches data from the web server. It then sends it to a // tee, which on one side has an hash checksum reader, and on the other // a gunzip reader writing to a file. It will compare the hash @@ -493,23 +441,6 @@ func fetchFileWithRetry(srcUrl, srcHash, dstFilename string, fetchRetryCount int return err } -// uncompressLocalFile reads a compressed file, and then sends it to a -// tee, which on one side has an hash checksum reader, and on the other -// a gunzip reader writing to a file. It will compare the hash -// checksum after the copy is done. -func uncompressLocalFile(srcPath, srcHash, dstFilename string) error { - log.Infof("uncompressLocalFile: starting to uncompress %v from %v", dstFilename, srcPath) - - // open the source file - reader, err := os.Open(srcPath) - if err != nil { - return fmt.Errorf("cannot open file %v: %v", srcPath, err) - } - defer reader.Close() - - return uncompressAndCheck(reader, srcHash, dstFilename, true) -} - // FIXME(msolomon) Should we add deadlines? What really matters more // than a deadline is probably a sense of progress, more like a // "progress timeout" - how long will we wait if there is no change in diff --git a/go/vt/mysqlctl/gorpcmysqlctlclient/client.go b/go/vt/mysqlctl/gorpcmysqlctlclient/client.go index 663ef6acd92..f7eb82091c3 100644 --- a/go/vt/mysqlctl/gorpcmysqlctlclient/client.go +++ b/go/vt/mysqlctl/gorpcmysqlctlclient/client.go @@ -10,7 +10,7 @@ import ( "fmt" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" rpc "github.com/youtube/vitess/go/rpcplus" "github.com/youtube/vitess/go/rpcwrap/bsonrpc" diff --git a/go/vt/mysqlctl/gorpcmysqlctlserver/server.go b/go/vt/mysqlctl/gorpcmysqlctlserver/server.go index 4c218b3dc2e..5de7fb2b71a 100644 --- a/go/vt/mysqlctl/gorpcmysqlctlserver/server.go +++ b/go/vt/mysqlctl/gorpcmysqlctlserver/server.go @@ -11,9 +11,9 @@ package gorpcmysqlctlserver import ( "time" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/vt/mysqlctl" "github.com/youtube/vitess/go/vt/servenv" + "golang.org/x/net/context" ) // MysqlctlServer is our RPC server. diff --git a/go/vt/mysqlctl/health.go b/go/vt/mysqlctl/health.go index c1bbba142e5..a9f240f77a8 100644 --- a/go/vt/mysqlctl/health.go +++ b/go/vt/mysqlctl/health.go @@ -3,39 +3,40 @@ package mysqlctl import ( "fmt" "html/template" + "time" "github.com/youtube/vitess/go/vt/health" "github.com/youtube/vitess/go/vt/topo" ) -// mySQLReplicationLag implements health.Reporter +// mysqlReplicationLag implements health.Reporter type mysqlReplicationLag struct { - mysqld *Mysqld - allowedLagInSeconds int + mysqld *Mysqld } -func (mrl *mysqlReplicationLag) Report(typ topo.TabletType) (status map[string]string, err error) { - if !topo.IsSlaveType(typ) { - return nil, nil +// Report is part of the health.Reporter interface +func (mrl *mysqlReplicationLag) Report(tabletType topo.TabletType, shouldQueryServiceBeRunning bool) (time.Duration, error) { + if !topo.IsSlaveType(tabletType) { + return 0, nil } slaveStatus, err := mrl.mysqld.SlaveStatus() if err != nil { - return nil, err + return 0, err } - if !slaveStatus.SlaveRunning() || int(slaveStatus.SecondsBehindMaster) > mrl.allowedLagInSeconds { - return map[string]string{health.ReplicationLag: health.ReplicationLagHigh}, nil + if !slaveStatus.SlaveRunning() { + return 0, fmt.Errorf("Replication is not running") } - - return nil, nil + return time.Duration(slaveStatus.SecondsBehindMaster) * time.Second, nil } +// HTMLName is part of the health.Reporter interface func (mrl *mysqlReplicationLag) HTMLName() template.HTML { - return template.HTML(fmt.Sprintf("MySQLReplicationLag(allowedLag=%v)", mrl.allowedLagInSeconds)) + return template.HTML("MySQLReplicationLag") } -// MySQLReplication lag returns a reporter that reports the MySQL -// replication lag. It uses the key "replication_lag". -func MySQLReplicationLag(mysqld *Mysqld, allowedLagInSeconds int) health.Reporter { - return &mysqlReplicationLag{mysqld, allowedLagInSeconds} +// MySQLReplicationLag lag returns a reporter that reports the MySQL +// replication lag. +func MySQLReplicationLag(mysqld *Mysqld) health.Reporter { + return &mysqlReplicationLag{mysqld} } diff --git a/go/vt/mysqlctl/mycnf_flag.go b/go/vt/mysqlctl/mycnf_flag.go index 369065fcb2b..c554a074031 100644 --- a/go/vt/mysqlctl/mycnf_flag.go +++ b/go/vt/mysqlctl/mycnf_flag.go @@ -6,18 +6,17 @@ package mysqlctl import ( "flag" - "fmt" log "github.com/golang/glog" ) // This file handles using command line flags to create a Mycnf object. // Since whoever links with this module doesn't necessarely need the flags, -// RegisterFlags needs to be called explicitely to set the flags up. +// RegisterFlags needs to be called explicitly to set the flags up. var ( // the individual command line parameters - flagServerId *int + flagServerID *int flagMysqlPort *int flagDataDir *string flagInnodbDataHomeDir *string @@ -42,7 +41,7 @@ var ( // specifying the values of a mycnf config file. See NewMycnfFromFlags // to get the supported modes. func RegisterFlags() { - flagServerId = flag.Int("mycnf_server_id", 0, "mysql server id of the server (if specified, mycnf-file will be ignored)") + flagServerID = flag.Int("mycnf_server_id", 0, "mysql server id of the server (if specified, mycnf-file will be ignored)") flagMysqlPort = flag.Int("mycnf_mysql_port", 0, "port mysql is listening on") flagDataDir = flag.String("mycnf_data_dir", "", "data directory for mysql") flagInnodbDataHomeDir = flag.String("mycnf_innodb_data_home_dir", "", "Innodb data home directory") @@ -77,10 +76,10 @@ func RegisterFlags() { // RegisterCommandLineFlags should have been called before calling // this, otherwise we'll panic. func NewMycnfFromFlags(uid uint32) (mycnf *Mycnf, err error) { - if *flagServerId != 0 { + if *flagServerID != 0 { log.Info("mycnf_server_id is specified, using command line parameters for mysql config") return &Mycnf{ - ServerId: uint32(*flagServerId), + ServerId: uint32(*flagServerID), MysqlPort: *flagMysqlPort, DataDir: *flagDataDir, InnodbDataHomeDir: *flagInnodbDataHomeDir, @@ -100,51 +99,18 @@ func NewMycnfFromFlags(uid uint32) (mycnf *Mycnf, err error) { // This is probably not going to be used by anybody, // but fill in a default value. (Note it's used by // mysqld.Start, in which case it is correct). - path: mycnfFile(uint32(*flagServerId)), + path: mycnfFile(uint32(*flagServerID)), }, nil - } else { - if *flagMycnfFile == "" { - if uid == 0 { - log.Fatalf("No mycnf_server_id, no mycnf-file, and no backup server id to use") - } - *flagMycnfFile = mycnfFile(uid) - log.Infof("No mycnf_server_id, no mycnf-file specified, using default config for server id %v: %v", uid, *flagMycnfFile) - } else { - log.Infof("No mycnf_server_id specified, using mycnf-file file %v", *flagMycnfFile) - } - return ReadMycnf(*flagMycnfFile) } -} -// GetSubprocessFlags returns the flags to pass to a subprocess to -// have the exact same mycnf config as us. -// -// RegisterCommandLineFlags and NewMycnfFromFlags should have been -// called before this. -func GetSubprocessFlags() []string { - if *flagServerId != 0 { - // all from command line - return []string{ - "-mycnf_server_id", fmt.Sprintf("%v", *flagServerId), - "-mycnf_mysql_port", fmt.Sprintf("%v", *flagMysqlPort), - "-mycnf_data_dir", *flagDataDir, - "-mycnf_innodb_data_home_dir", *flagInnodbDataHomeDir, - "-mycnf_innodb_log_group_home_dir", *flagInnodbLogGroupHomeDir, - "-mycnf_socket_file", *flagSocketFile, - "-mycnf_error_log_path", *flagErrorLogPath, - "-mycnf_slow_log_path", *flagSlowLogPath, - "-mycnf_relay_log_path", *flagRelayLogPath, - "-mycnf_relay_log_index_path", *flagRelayLogIndexPath, - "-mycnf_relay_log_info_path", *flagRelayLogInfoPath, - "-mycnf_bin_log_path", *flagBinLogPath, - "-mycnf_master_info_file", *flagMasterInfoFile, - "-mycnf_pid_file", *flagPidFile, - "-mycnf_tmp_dir", *flagTmpDir, - "-mycnf_slave_load_tmp_dir", *flagSlaveLoadTmpDir, + if *flagMycnfFile == "" { + if uid == 0 { + log.Fatalf("No mycnf_server_id, no mycnf-file, and no backup server id to use") } + *flagMycnfFile = mycnfFile(uid) + log.Infof("No mycnf_server_id, no mycnf-file specified, using default config for server id %v: %v", uid, *flagMycnfFile) + } else { + log.Infof("No mycnf_server_id specified, using mycnf-file file %v", *flagMycnfFile) } - - // Just pass through the mycnf-file param, it has been altered - // if we didn't get it but guessed it from uid. - return []string{"-mycnf-file", *flagMycnfFile} + return ReadMycnf(*flagMycnfFile) } diff --git a/go/vt/mysqlctl/mysqld.go b/go/vt/mysqlctl/mysqld.go index a304f301144..152c6f3ed86 100644 --- a/go/vt/mysqlctl/mysqld.go +++ b/go/vt/mysqlctl/mysqld.go @@ -486,7 +486,7 @@ func deleteTopDir(dir string) (removalErr error) { // Addr returns the fully qualified host name + port for this instance. func (mysqld *Mysqld) Addr() string { hostname := netutil.FullyQualifiedHostnameOrPanic() - return fmt.Sprintf("%v:%v", hostname, mysqld.config.MysqlPort) + return netutil.JoinHostPort(hostname, mysqld.config.MysqlPort) } // IpAddr returns the IP address for this instance @@ -498,7 +498,7 @@ func (mysqld *Mysqld) IpAddr() string { return addr } -// executes some SQL commands using a mysql command line interface process +// ExecuteMysqlCommand executes some SQL commands using a mysql command line interface process func (mysqld *Mysqld) ExecuteMysqlCommand(sql string) error { dir, err := vtenv.VtMysqlRoot() if err != nil { diff --git a/go/vt/mysqlctl/proto/replication.go b/go/vt/mysqlctl/proto/replication.go index b72f4ed7d72..725005cd778 100644 --- a/go/vt/mysqlctl/proto/replication.go +++ b/go/vt/mysqlctl/proto/replication.go @@ -8,11 +8,11 @@ import ( "bytes" "encoding/json" "fmt" - "strconv" "strings" "github.com/youtube/vitess/go/bson" "github.com/youtube/vitess/go/bytes2" + "github.com/youtube/vitess/go/netutil" ) // ReplicationPosition represents the information necessary to describe which @@ -207,20 +207,23 @@ type ReplicationStatus struct { MasterConnectRetry int } +// SlaveRunning returns true iff both the Slave IO and Slave SQL threads are +// running. func (rs *ReplicationStatus) SlaveRunning() bool { return rs.SlaveIORunning && rs.SlaveSQLRunning } +// MasterAddr returns the host:port address of the master. func (rs *ReplicationStatus) MasterAddr() string { - return fmt.Sprintf("%v:%v", rs.MasterHost, rs.MasterPort) + return netutil.JoinHostPort(rs.MasterHost, rs.MasterPort) } +// NewReplicationStatus creates a ReplicationStatus pointing to masterAddr. func NewReplicationStatus(masterAddr string) (*ReplicationStatus, error) { - addrPieces := strings.Split(masterAddr, ":") - port, err := strconv.Atoi(addrPieces[1]) + host, port, err := netutil.SplitHostPort(masterAddr) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid masterAddr: %q, %v", masterAddr, err) } return &ReplicationStatus{MasterConnectRetry: 10, - MasterHost: addrPieces[0], MasterPort: port}, nil + MasterHost: host, MasterPort: port}, nil } diff --git a/go/vt/mysqlctl/proto/replication_test.go b/go/vt/mysqlctl/proto/replication_test.go index c82f0fee9b8..d3c952f3282 100644 --- a/go/vt/mysqlctl/proto/replication_test.go +++ b/go/vt/mysqlctl/proto/replication_test.go @@ -528,27 +528,41 @@ func TestReplicationStatusSlaveSQLNotRunning(t *testing.T) { } func TestReplicationStatusMasterAddr(t *testing.T) { - input := &ReplicationStatus{ - MasterHost: "master-host", - MasterPort: 1234, - } - want := "master-host:1234" - if got := input.MasterAddr(); got != want { - t.Errorf("%#v.MasterAddr() = %v, want %v", input, got, want) + table := map[string]*ReplicationStatus{ + "master-host:1234": &ReplicationStatus{ + MasterHost: "master-host", + MasterPort: 1234, + }, + "[::1]:4321": &ReplicationStatus{ + MasterHost: "::1", + MasterPort: 4321, + }, + } + for want, input := range table { + if got := input.MasterAddr(); got != want { + t.Errorf("%#v.MasterAddr() = %v, want %v", input, got, want) + } } } func TestNewReplicationStatus(t *testing.T) { - input := "master-host:1234" - want := &ReplicationStatus{ - MasterHost: "master-host", - MasterPort: 1234, - } - got, err := NewReplicationStatus(input) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if got.MasterHost != want.MasterHost || got.MasterPort != want.MasterPort { - t.Errorf("NewReplicationStatus(%#v) = %#v, want %#v", input, got, want) + table := map[string]*ReplicationStatus{ + "master-host:1234": &ReplicationStatus{ + MasterHost: "master-host", + MasterPort: 1234, + }, + "[::1]:4321": &ReplicationStatus{ + MasterHost: "::1", + MasterPort: 4321, + }, + } + for input, want := range table { + got, err := NewReplicationStatus(input) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if got.MasterHost != want.MasterHost || got.MasterPort != want.MasterPort { + t.Errorf("NewReplicationStatus(%#v) = %#v, want %#v", input, got, want) + } } } diff --git a/go/vt/mysqlctl/proto/schema.go b/go/vt/mysqlctl/proto/schema.go index 0cacb71357a..9002a0edcc4 100644 --- a/go/vt/mysqlctl/proto/schema.go +++ b/go/vt/mysqlctl/proto/schema.go @@ -9,6 +9,7 @@ import ( "encoding/hex" "fmt" "sort" + "strings" "github.com/youtube/vitess/go/jscfg" "github.com/youtube/vitess/go/vt/concurrency" @@ -88,6 +89,32 @@ func (sd *SchemaDefinition) GetTable(table string) (td *TableDefinition, ok bool return nil, false } +// ToSQLStrings converts a SchemaDefinition to an array of SQL strings. The array contains all +// the SQL statements needed for creating the database, tables, and views - in that order. +// All SQL statements will have {{.DatabaseName}} in place of the actual db name. +func (sd *SchemaDefinition) ToSQLStrings() []string { + sqlStrings := make([]string, 0, len(sd.TableDefinitions)+1) + createViewSql := make([]string, 0, len(sd.TableDefinitions)) + + sqlStrings = append(sqlStrings, sd.DatabaseSchema) + + for _, td := range sd.TableDefinitions { + if td.Type == TABLE_VIEW { + createViewSql = append(createViewSql, td.Schema) + } else { + lines := strings.Split(td.Schema, "\n") + for i, line := range lines { + if strings.HasPrefix(line, "CREATE TABLE `") { + lines[i] = strings.Replace(line, "CREATE TABLE `", "CREATE TABLE `{{.DatabaseName}}`.`", 1) + } + } + sqlStrings = append(sqlStrings, strings.Join(lines, "\n")) + } + } + + return append(sqlStrings, createViewSql...) +} + // generates a report on what's different between two SchemaDefinition // for now, we skip the VIEW entirely. func DiffSchema(leftName string, left *SchemaDefinition, rightName string, right *SchemaDefinition, er concurrency.ErrorRecorder) { diff --git a/go/vt/mysqlctl/proto/schema_test.go b/go/vt/mysqlctl/proto/schema_test.go index 83ddffe7f6c..507e9881d9d 100644 --- a/go/vt/mysqlctl/proto/schema_test.go +++ b/go/vt/mysqlctl/proto/schema_test.go @@ -5,9 +5,124 @@ package proto import ( + "reflect" "testing" ) +var basicTable1 = &TableDefinition{ + Name: "table1", + Schema: "table schema 1", + Type: TABLE_BASE_TABLE, +} +var basicTable2 = &TableDefinition{ + Name: "table2", + Schema: "table schema 2", + Type: TABLE_BASE_TABLE, +} + +var table3 = &TableDefinition{ + Name: "table2", + Schema: "CREATE TABLE `table3` (\n" + + "id bigint not null,\n" + + ") Engine=InnoDB", + Type: TABLE_BASE_TABLE, +} + +var view1 = &TableDefinition{ + Name: "view1", + Schema: "view schema 1", + Type: TABLE_VIEW, +} + +var view2 = &TableDefinition{ + Name: "view2", + Schema: "view schema 2", + Type: TABLE_VIEW, +} + +func TestToSQLStrings(t *testing.T) { + var testcases = []struct { + input *SchemaDefinition + want []string + }{ + { + // basic SchemaDefinition with create db statement, basic table and basic view + input: &SchemaDefinition{ + DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", + TableDefinitions: []*TableDefinition{ + basicTable1, + view1, + }, + }, + want: []string{"CREATE DATABASE {{.DatabaseName}}", basicTable1.Schema, view1.Schema}, + }, + { + // SchemaDefinition doesn't need any tables or views + input: &SchemaDefinition{ + DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", + }, + want: []string{"CREATE DATABASE {{.DatabaseName}}"}, + }, + { + // and can even have an empty DatabaseSchema + input: &SchemaDefinition{}, + want: []string{""}, + }, + { + // with tables but no views + input: &SchemaDefinition{ + DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", + TableDefinitions: []*TableDefinition{ + basicTable1, + basicTable2, + }, + }, + want: []string{"CREATE DATABASE {{.DatabaseName}}", basicTable1.Schema, basicTable2.Schema}, + }, + { + // multiple tables and views should be ordered with all tables before views + input: &SchemaDefinition{ + DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", + TableDefinitions: []*TableDefinition{ + view1, + view2, + basicTable1, + basicTable2, + }, + }, + want: []string{ + "CREATE DATABASE {{.DatabaseName}}", + basicTable1.Schema, basicTable2.Schema, + view1.Schema, view2.Schema, + }, + }, + { + // valid table schema gets correctly rewritten to include DatabaseName + input: &SchemaDefinition{ + DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", + TableDefinitions: []*TableDefinition{ + basicTable1, + table3, + }, + }, + want: []string{ + "CREATE DATABASE {{.DatabaseName}}", + basicTable1.Schema, + "CREATE TABLE `{{.DatabaseName}}`.`table3` (\n" + + "id bigint not null,\n" + + ") Engine=InnoDB", + }, + }, + } + + for _, tc := range testcases { + got := tc.input.ToSQLStrings() + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("ToSQLStrings() on SchemaDefinition %v returned %v; want %v", tc.input, got, tc.want) + } + } +} + func testDiff(t *testing.T, left, right *SchemaDefinition, leftName, rightName string, expected []string) { actual := DiffSchemaToArray(leftName, left, rightName, right) diff --git a/go/vt/mysqlctl/replication.go b/go/vt/mysqlctl/replication.go index 7022ccd845c..ab6c9a37d02 100644 --- a/go/vt/mysqlctl/replication.go +++ b/go/vt/mysqlctl/replication.go @@ -12,7 +12,6 @@ import ( "bytes" "errors" "fmt" - "net" "os" "path" "strconv" @@ -22,6 +21,7 @@ import ( log "github.com/golang/glog" "github.com/youtube/vitess/go/mysql" + "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/vt/binlog/binlogplayer" blproto "github.com/youtube/vitess/go/vt/binlog/proto" "github.com/youtube/vitess/go/vt/dbconfigs" @@ -381,7 +381,7 @@ func (mysqld *Mysqld) FindSlaves() ([]string, error) { addrs := make([]string, 0, 32) for _, row := range qr.Rows { if row[colCommand].String() == binlogDumpCommand { - host, _, err := net.SplitHostPort(row[colClientAddr].String()) + host, _, err := netutil.SplitHostPort(row[colClientAddr].String()) if err != nil { return nil, fmt.Errorf("FindSlaves: malformed addr %v", err) } diff --git a/go/vt/mysqlctl/slave_connection.go b/go/vt/mysqlctl/slave_connection.go index e26bb5bc00e..4e8b6124926 100644 --- a/go/vt/mysqlctl/slave_connection.go +++ b/go/vt/mysqlctl/slave_connection.go @@ -127,8 +127,8 @@ func (sc *SlaveConnection) StartBinlogDump(startPos proto.ReplicationPosition) ( // The ID for the slave connection is recycled back into the pool. func (sc *SlaveConnection) Close() { if sc.Connection != nil { - log.Infof("force-closing slave socket to unblock reads") - sc.Connection.ForceClose() + log.Infof("shutting down slave socket to unblock reads") + sc.Connection.Shutdown() log.Infof("waiting for slave dump thread to end") sc.svm.Stop() @@ -142,7 +142,7 @@ func (sc *SlaveConnection) Close() { // makeBinlogDumpCommand builds a buffer containing the data for a MySQL // COM_BINLOG_DUMP command. -func makeBinlogDumpCommand(pos uint32, flags uint16, server_id uint32, filename string) []byte { +func makeBinlogDumpCommand(pos uint32, flags uint16, serverID uint32, filename string) []byte { var buf bytes.Buffer buf.Grow(4 + 2 + 4 + len(filename)) @@ -151,7 +151,7 @@ func makeBinlogDumpCommand(pos uint32, flags uint16, server_id uint32, filename // binlog_flags (2 bytes) binary.Write(&buf, binary.LittleEndian, flags) // server_id of slave (4 bytes) - binary.Write(&buf, binary.LittleEndian, server_id) + binary.Write(&buf, binary.LittleEndian, serverID) // binlog_filename (string with no terminator and no length) buf.WriteString(filename) diff --git a/go/vt/mysqlctl/split.go b/go/vt/mysqlctl/split.go deleted file mode 100644 index 49fe108b559..00000000000 --- a/go/vt/mysqlctl/split.go +++ /dev/null @@ -1,1060 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package mysqlctl - -// FIXME(msolomon) this actions were copy/pasted from replication.go because -// they were conceptually quite similar. They should be reconciled at some -// point. - -/* -Given a single shard, split into 2 subshards, each addressing some subset of the total key ranges. - - -T is the tablet server controlling M -R is the entity_id key range that T handles -M is the master mysql db - S is the stemcell mysql slave, which takes no normal traffic (does this have a tablet server?) - -M', M" are new master db's, each of which will have some subset of the key range of M -S', S" are new stemcell db's, each of which will have some number of slaves -T', T" are the corresponding tablet servers for M'/M" - - Assume masters take a significant amount of read traffic (unlike EMD). - -Resharding may be implemented as a subprocess from the tablet server that communicates back over a netchan. This will make it easier to patch without taking down the tablet server. - Acquire machine resources (M'/M", S'/S", ...) - 2*M + 2*S + min((N+X), 2*min # of replicas) + (2 * Lag) -N is replica count local to M -X is replicas outside of M's datacenter - Laggards are optional (but probably good) -The global minimum for replicas per shard is ~3 for durability and the ability to clone while you are online serving queries. -Install/init tablet server processes T'/T" -Install/init mysql on M'/M" - SET GLOBAL read_only = 1; -does this allow replication to proceed? -what about commands issued by SUPER? -Arrange replication layout amongst new instances -If there are latency/geographic considerations, this is where they manifest themselves. In general, the stemcells will be the source of the replication streams. Each geographic area should have a stemcell which acts as the parent for all other slaves in that area. The local stemcell should slave from the master's stemcell. It should look like a shrub more than a tree. -Alternatively, this layout can be used for an initial copy of the table dumps. After the initial data load, the replication streams can be set up. This might be faster, but is likely to be more complex to manage. -Apply baseline schema -turn off indexes to increase throughput? can't do this on InnoDB -Stop replication on stemcell S -Record replication position on S for M' and M" -Given two key ranges, R' and R" set the replication key range on M' and M" -this requires modifications to mysql replication which I have made in the past to be redone -This should be fixable to row-based replication as well. - For each table on S, export subranges to M' and M": - SELECT * FROM table WHERE R'.start <= id AND id < R'.end - SELECT * FROM table WHERE R".start <= id AND id < R".end -Feed dump query streams in M' and M" respectively -use some sort of SELECT INTO..., LOAD FROM... to optimize? -use some sort of COMMIT buffering to optimize? -disable AUTOCOMMIT - SET UNIQUE_CHECKS=0; do some stuff; SET UNIQUE_CHECKS=1; -use the tablet server to compress or do data-only instead of sending full SQL commands -will single replication threads handle the inserts fast enough downstream of S' and S"? -Once the bulk export is complete, restart replication on S. - Once the bulk import is complete, rebuild tables? (might not be necessary since data is sequential) -Reparent M' and M" to S -set the key range that replication will accept -Start splitting replication on M' and M" - Wait for M'/M" to catch up to S (implying caught up to M) - Wait for S'x and S"x hosts (mysql instances slaved from the new stemcells) to catch up to M'/M". - S'Lag and S"Lag (24 hour lag hosts) will not be 24 hrs behind for 23+ hrs -Writes can now be shunted from M to M'/M" -writes are likely to be warm from replication -reads will be cold since there is no traffic going to the T'/T" - the row cache is empty -row cache could be warmed, but the invalidation is tricky if you are allowing writes -8GB of cache will take 120 seconds to transfer, even if you can nearly max out the 1Gb port to an adjacent machine -if shards are small, this might not be a big deal -Start failing writes on T, report that T split to smart clients. - SET GLOBAL read_only = 1 on M to prevent ghost writes. - Set T to refuse new connections (read or write) -Disconnect replication on M'/M" from S. - SET GLOBAL read_only = 0 on M'/M" to allow new writes. -Update table wrangler and reassign R'/R" to T'/T". -T disconnects reading clients and shutsdown mysql. -How aggressively can we do this? The faster the better. -Garbage collect the hosts. -leave the 24 lag for 1 day -*/ - -import ( - "bufio" - "fmt" - "io" - "net/url" - "os" - "path" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/youtube/vitess/go/bufio2" - "github.com/youtube/vitess/go/cgzip" - "github.com/youtube/vitess/go/sync2" - "github.com/youtube/vitess/go/vt/binlog/binlogplayer" - "github.com/youtube/vitess/go/vt/concurrency" - "github.com/youtube/vitess/go/vt/dbconnpool" - "github.com/youtube/vitess/go/vt/hook" - "github.com/youtube/vitess/go/vt/key" - "github.com/youtube/vitess/go/vt/logutil" - "github.com/youtube/vitess/go/vt/mysqlctl/csvsplitter" - "github.com/youtube/vitess/go/vt/mysqlctl/proto" -) - -const ( - partialSnapshotManifestFile = "partial_snapshot_manifest.json" - SnapshotURLPath = "/snapshot" -) - -// replaceError replaces original with recent if recent is not nil, -// logging original if it wasn't nil. This should be used in deferred -// cleanup functions if they change the returned error. -func replaceError(logger logutil.Logger, original, recent error) error { - if recent == nil { - return original - } - if original != nil { - logger.Errorf("One of multiple error: %v", original) - } - return recent -} - -type SplitSnapshotManifest struct { - // Source describes the files and our tablet - Source *SnapshotManifest - - // KeyRange describes the data present in this snapshot - // When splitting 40-80 into 40-60 and 60-80, this would - // have 40-60 for instance. - KeyRange key.KeyRange - - // The schema for this server - SchemaDefinition *proto.SchemaDefinition -} - -// NewSplitSnapshotManifest creates a new SplitSnapshotManifest. -// myAddr and myMysqlAddr are the local server addresses. -// masterAddr is the address of the server to use as master. -// pos is the replication position to use on that master. -// myMasterPos is the local server master position -func NewSplitSnapshotManifest(myAddr, myMysqlAddr, masterAddr, dbName string, files []SnapshotFile, pos, myMasterPos proto.ReplicationPosition, keyRange key.KeyRange, sd *proto.SchemaDefinition) (*SplitSnapshotManifest, error) { - sm, err := newSnapshotManifest(myAddr, myMysqlAddr, masterAddr, dbName, files, pos, myMasterPos) - if err != nil { - return nil, err - } - return &SplitSnapshotManifest{ - Source: sm, - KeyRange: keyRange, - SchemaDefinition: sd, - }, nil -} - -// SanityCheckManifests checks if the ssms can be restored together. -func SanityCheckManifests(ssms []*SplitSnapshotManifest) error { - first := ssms[0] - for _, ssm := range ssms[1:] { - if ssm.SchemaDefinition.Version != first.SchemaDefinition.Version { - return fmt.Errorf("multirestore sanity check: schema versions don't match: %v, %v", ssm, first) - } - } - return nil -} - -// getReplicationPositionForClones returns what position the clones -// need to replicate from. Can be ours if we are a master, or our master's. -func (mysqld *Mysqld) getReplicationPositionForClones(allowHierarchicalReplication bool) (replicationPosition proto.ReplicationPosition, masterAddr string, err error) { - // If the source is a slave use the master replication position, - // unless we are allowing hierachical replicas. - var status *proto.ReplicationStatus - status, err = mysqld.SlaveStatus() - if err == ErrNotSlave { - // we are really a master, so we need that position - replicationPosition, err = mysqld.MasterPosition() - if err != nil { - return - } - masterAddr = mysqld.IpAddr() - return - } - if err != nil { - return - } - replicationPosition = status.Position - - // we are a slave, check our replication strategy - if allowHierarchicalReplication { - masterAddr = mysqld.IpAddr() - } else { - masterAddr, err = mysqld.GetMasterAddr() - } - return -} - -func (mysqld *Mysqld) prepareToSnapshot(logger logutil.Logger, allowHierarchicalReplication bool, hookExtraEnv map[string]string) (slaveStartRequired, readOnly bool, replicationPosition, myMasterPosition proto.ReplicationPosition, masterAddr string, connToRelease dbconnpool.PoolConnection, err error) { - // save initial state so we can restore on Start() - if slaveStatus, slaveErr := mysqld.SlaveStatus(); slaveErr == nil { - slaveStartRequired = slaveStatus.SlaveRunning() - } - - // For masters, set read-only so we don't write anything during snapshot - readOnly = true - if readOnly, err = mysqld.IsReadOnly(); err != nil { - return - } - - logger.Infof("Set Read Only") - if !readOnly { - mysqld.SetReadOnly(true) - } - logger.Infof("Stop Slave") - if err = mysqld.StopSlave(hookExtraEnv); err != nil { - return - } - - // Get the replication position and master addr - replicationPosition, masterAddr, err = mysqld.getReplicationPositionForClones(allowHierarchicalReplication) - if err != nil { - return - } - - // get our master position, some targets may use it - myMasterPosition, err = mysqld.MasterPosition() - if err != nil && err != ErrNotMaster { - // this is a real error - return - } - - logger.Infof("Flush tables") - if connToRelease, err = mysqld.dbaPool.Get(0); err != nil { - return - } - logger.Infof("exec FLUSH TABLES WITH READ LOCK") - if _, err = connToRelease.ExecuteFetch("FLUSH TABLES WITH READ LOCK", 10000, false); err != nil { - connToRelease.Recycle() - return - } - - return -} - -func (mysqld *Mysqld) restoreAfterSnapshot(logger logutil.Logger, slaveStartRequired, readOnly bool, hookExtraEnv map[string]string, connToRelease dbconnpool.PoolConnection) (err error) { - // Try to fix mysqld regardless of snapshot success.. - logger.Infof("exec UNLOCK TABLES") - _, err = connToRelease.ExecuteFetch("UNLOCK TABLES", 10000, false) - connToRelease.Recycle() - if err != nil { - return fmt.Errorf("failed to UNLOCK TABLES: %v", err) - } - - // restore original mysqld state that we saved above - if slaveStartRequired { - if err = mysqld.StartSlave(hookExtraEnv); err != nil { - return - } - // this should be quick, but we might as well just wait - if err = mysqld.WaitForSlaveStart(5); err != nil { - return - } - } - if err = mysqld.SetReadOnly(readOnly); err != nil { - return - } - return nil -} - -type namedHasherWriter struct { - // creation parameters - filenamePattern string - snapshotDir string - tableName string - maximumFilesize uint64 - - // our current pipeline - inputBuffer *bufio2.AsyncWriter - gzip *cgzip.Writer - hasher *hasher - fileBuffer *bufio.Writer - file *os.File - - // where we are - currentSize uint64 - currentIndex uint - snapshotFiles []SnapshotFile -} - -func newCompressedNamedHasherWriter(filenamePattern, snapshotDir, tableName string, maximumFilesize uint64) (*namedHasherWriter, error) { - w := &namedHasherWriter{filenamePattern: filenamePattern, snapshotDir: snapshotDir, tableName: tableName, maximumFilesize: maximumFilesize, snapshotFiles: make([]SnapshotFile, 0, 5)} - if err := w.Open(); err != nil { - return nil, err - } - return w, nil -} - -func (nhw *namedHasherWriter) Open() (err error) { - // The pipeline looks like this: - // - // +---> buffer +---> file - // | 32K - // buffer +---> gzip +---> tee + - // 32K | - // +---> hasher - // - // The buffer in front of gzip is needed so that the data is - // compressed only when there's a reasonable amount of it. - - filename := fmt.Sprintf(nhw.filenamePattern, nhw.currentIndex) - nhw.file, err = os.Create(filename) - if err != nil { - return - } - nhw.fileBuffer = bufio.NewWriterSize(nhw.file, 32*1024) - nhw.hasher = newHasher() - tee := io.MultiWriter(nhw.fileBuffer, nhw.hasher) - // create the gzip compression filter - nhw.gzip, err = cgzip.NewWriterLevel(tee, cgzip.Z_BEST_SPEED) - if err != nil { - return - } - nhw.inputBuffer = bufio2.NewAsyncWriterSize(nhw.gzip, 32*1024, 3) - return -} - -func (nhw *namedHasherWriter) Close() (err error) { - // I have to dismantle the pipeline, starting from the - // top. Some of the elements are flushers, others are closers, - // which is why this code is so ugly. - if err = nhw.inputBuffer.Flush(); err != nil { - return - } - if err = nhw.gzip.Close(); err != nil { - return - } - if err = nhw.fileBuffer.Flush(); err != nil { - return - } - filename := nhw.file.Name() - if err = nhw.file.Close(); err != nil { - return - } - - // then add the snapshot file we created to our list - fi, err := os.Stat(filename) - if err != nil { - return err - } - relativePath, err := filepath.Rel(nhw.snapshotDir, filename) - if err != nil { - return err - } - nhw.snapshotFiles = append(nhw.snapshotFiles, SnapshotFile{relativePath, fi.Size(), nhw.hasher.HashString(), nhw.tableName}) - - nhw.inputBuffer = nil - nhw.hasher = nil - nhw.gzip = nil - nhw.file = nil - nhw.fileBuffer = nil - nhw.currentSize = 0 - return nil -} - -func (nhw *namedHasherWriter) Rotate() error { - if err := nhw.Close(); err != nil { - return err - } - nhw.currentIndex++ - if err := nhw.Open(); err != nil { - return err - } - return nil -} - -func (nhw *namedHasherWriter) Write(p []byte) (n int, err error) { - size := uint64(len(p)) - if size+nhw.currentSize > nhw.maximumFilesize && nhw.currentSize > 0 { - // if we write this, we'll go over the file limit - // (make sure we've written something at least to move - // forward) - if err := nhw.Rotate(); err != nil { - return 0, err - } - } - nhw.currentSize += size - - return nhw.inputBuffer.Write(p) -} - -// SnapshotFiles returns the snapshot files appropriate for the data -// written by the namedHasherWriter. Calling SnapshotFiles will close -// any outstanding file. -func (nhw *namedHasherWriter) SnapshotFiles() ([]SnapshotFile, error) { - if nhw.inputBuffer != nil { - if err := nhw.Close(); err != nil { - return nil, err - } - } - return nhw.snapshotFiles, nil -} - -// dumpTableSplit will dump a table, and then split it according to keyspace_id -// into multiple files. -func (mysqld *Mysqld) dumpTableSplit(logger logutil.Logger, td *proto.TableDefinition, dbName, keyName string, keyType key.KeyspaceIdType, mainCloneSourcePath string, cloneSourcePaths map[key.KeyRange]string, maximumFilesize uint64) (map[key.KeyRange][]SnapshotFile, error) { - filename := path.Join(mainCloneSourcePath, td.Name+".csv") - selectIntoOutfile := `SELECT {{.KeyspaceIdColumnName}}, {{.Columns}} INTO OUTFILE "{{.TableOutputPath}}" CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\' LINES TERMINATED BY '\n' FROM {{.TableName}}` - queryParams := map[string]string{ - "TableName": dbName + "." + td.Name, - "Columns": strings.Join(td.Columns, ", "), - "KeyspaceIdColumnName": keyName, - "TableOutputPath": filename, - } - numberColumn := true - if keyType == key.KIT_BYTES { - numberColumn = false - queryParams["KeyspaceIdColumnName"] = "HEX(" + keyName + ")" - } - sio, err := fillStringTemplate(selectIntoOutfile, queryParams) - if err != nil { - return nil, fmt.Errorf("fillStringTemplate for %v: %v", td.Name, err) - } - if err := mysqld.ExecuteSuperQuery(sio); err != nil { - return nil, fmt.Errorf("ExecuteSuperQuery failed for %v with query %v: %v", td.Name, sio, err) - } - - file, err := os.Open(filename) - if err != nil { - return nil, fmt.Errorf("Cannot open file %v for table %v: %v", filename, td.Name, err) - } - - defer func() { - file.Close() - if e := os.Remove(filename); e != nil { - logger.Errorf("Cannot remove %v: %v", filename, e) - } - }() - - hasherWriters := make(map[key.KeyRange]*namedHasherWriter) - - for kr, cloneSourcePath := range cloneSourcePaths { - filenamePattern := path.Join(cloneSourcePath, td.Name+".%v.csv.gz") - w, err := newCompressedNamedHasherWriter(filenamePattern, mysqld.SnapshotDir, td.Name, maximumFilesize) - if err != nil { - return nil, fmt.Errorf("newCompressedNamedHasherWriter failed for %v: %v", td.Name, err) - } - hasherWriters[kr] = w - } - - splitter := csvsplitter.NewKeyspaceCSVReader(file, ',', numberColumn) - for { - keyspaceId, line, err := splitter.ReadRecord() - if err == io.EOF { - break - } - if err != nil { - return nil, fmt.Errorf("ReadRecord failed for table %v: %v", td.Name, err) - } - for kr, w := range hasherWriters { - if kr.Contains(keyspaceId) { - _, err = w.Write(line) - if err != nil { - return nil, fmt.Errorf("Write failed for %v: %v", td.Name, err) - } - break - } - } - } - - snapshotFiles := make(map[key.KeyRange][]SnapshotFile) - for i, hw := range hasherWriters { - if snapshotFiles[i], err = hw.SnapshotFiles(); err != nil { - return nil, fmt.Errorf("SnapshotFiles failed for %v: %v", td.Name, err) - } - } - - return snapshotFiles, nil -} - -// dumpTableFull will dump the contents of a full table, and then -// chunk it up in multiple compressed files. -func (mysqld *Mysqld) dumpTableFull(logger logutil.Logger, td *proto.TableDefinition, dbName, mainCloneSourcePath string, cloneSourcePath string, maximumFilesize uint64) ([]SnapshotFile, error) { - filename := path.Join(mainCloneSourcePath, td.Name+".csv") - selectIntoOutfile := `SELECT {{.Columns}} INTO OUTFILE "{{.TableOutputPath}}" CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\' LINES TERMINATED BY '\n' FROM {{.TableName}}` - queryParams := map[string]string{ - "TableName": dbName + "." + td.Name, - "Columns": strings.Join(td.Columns, ", "), - "TableOutputPath": filename, - } - sio, err := fillStringTemplate(selectIntoOutfile, queryParams) - if err != nil { - return nil, err - } - if err := mysqld.ExecuteSuperQuery(sio); err != nil { - return nil, err - } - - file, err := os.Open(filename) - if err != nil { - return nil, err - } - - defer func() { - file.Close() - if e := os.Remove(filename); e != nil { - logger.Errorf("Cannot remove %v: %v", filename, e) - } - }() - - filenamePattern := path.Join(cloneSourcePath, td.Name+".%v.csv.gz") - hasherWriter, err := newCompressedNamedHasherWriter(filenamePattern, mysqld.SnapshotDir, td.Name, maximumFilesize) - if err != nil { - return nil, err - } - - splitter := csvsplitter.NewCSVReader(file, ',') - for { - line, err := splitter.ReadRecord() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - _, err = hasherWriter.Write(line) - if err != nil { - return nil, err - } - } - - return hasherWriter.SnapshotFiles() -} - -// CreateMultiSnapshot create snapshots of the data. -// - for a resharding snapshot, keyRanges+keyName+keyType are set, -// and tables is empty. This action will create multiple snapshots, -// one per keyRange. -// - for a vertical split, tables is set, keyRanges = [KeyRange{}] and -// keyName+keyType are empty. It will create a single snapshot of -// the contents of the tables. -// Note combinations of table subset and keyranges are not supported. -func (mysqld *Mysqld) CreateMultiSnapshot(logger logutil.Logger, keyRanges []key.KeyRange, dbName, keyName string, keyType key.KeyspaceIdType, sourceAddr string, allowHierarchicalReplication bool, snapshotConcurrency int, tables, excludeTables []string, skipSlaveRestart bool, maximumFilesize uint64, hookExtraEnv map[string]string) (snapshotManifestFilenames []string, err error) { - if dbName == "" { - err = fmt.Errorf("no database name provided") - return - } - if len(tables) > 0 { - if len(keyRanges) != 1 || keyRanges[0].IsPartial() { - return nil, fmt.Errorf("With tables specified, can only have one full KeyRange") - } - } - - // same logic applies here - logger.Infof("validateCloneSource") - if err = mysqld.validateCloneSource(false, hookExtraEnv); err != nil { - return - } - - // clean out and start fresh - cloneSourcePaths := make(map[key.KeyRange]string) - for _, keyRange := range keyRanges { - cloneSourcePaths[keyRange] = path.Join(mysqld.SnapshotDir, dataDir, dbName+"-"+string(keyRange.Start.Hex())+","+string(keyRange.End.Hex())) - } - for _, _path := range cloneSourcePaths { - if err = os.RemoveAll(_path); err != nil { - return - } - if err = os.MkdirAll(_path, 0775); err != nil { - return - } - } - - mainCloneSourcePath := path.Join(mysqld.SnapshotDir, dataDir, dbName+"-all") - if err = os.RemoveAll(mainCloneSourcePath); err != nil { - return - } - if err = os.MkdirAll(mainCloneSourcePath, 0775); err != nil { - return - } - - // get the schema for each table - sd, fetchErr := mysqld.GetSchema(dbName, tables, excludeTables, true) - if fetchErr != nil { - return []string{}, fetchErr - } - if len(sd.TableDefinitions) == 0 { - return []string{}, fmt.Errorf("empty table list for %v", dbName) - } - sd.SortByReverseDataLength() - - // prepareToSnapshot will get the tablet in the rigth state, - // and return the current mysql status. - slaveStartRequired, readOnly, replicationPosition, myMasterPosition, masterAddr, conn, err := mysqld.prepareToSnapshot(logger, allowHierarchicalReplication, hookExtraEnv) - if err != nil { - return - } - if skipSlaveRestart { - if slaveStartRequired { - logger.Infof("Overriding slaveStartRequired to false") - } - slaveStartRequired = false - } - defer func() { - err = replaceError(logger, err, mysqld.restoreAfterSnapshot(logger, slaveStartRequired, readOnly, hookExtraEnv, conn)) - }() - - // dump the files in parallel with a pre-defined concurrency - datafiles := make([]map[key.KeyRange][]SnapshotFile, len(sd.TableDefinitions)) - dumpTableWorker := func(i int) (err error) { - table := sd.TableDefinitions[i] - if table.Type != proto.TABLE_BASE_TABLE { - // we just skip views here - return nil - } - if len(tables) > 0 { - sfs, err := mysqld.dumpTableFull(logger, table, dbName, mainCloneSourcePath, cloneSourcePaths[key.KeyRange{}], maximumFilesize) - if err != nil { - return err - } - datafiles[i] = map[key.KeyRange][]SnapshotFile{ - key.KeyRange{}: sfs, - } - } else { - datafiles[i], err = mysqld.dumpTableSplit(logger, table, dbName, keyName, keyType, mainCloneSourcePath, cloneSourcePaths, maximumFilesize) - } - return - } - if err = ConcurrentMap(snapshotConcurrency, len(sd.TableDefinitions), dumpTableWorker); err != nil { - return - } - - if e := os.Remove(mainCloneSourcePath); e != nil { - logger.Errorf("Cannot remove %v: %v", mainCloneSourcePath, e) - } - - // Check the replication position after snapshot is done - // hasn't changed, to be sure we haven't inserted any data - newReplicationPosition, _, err := mysqld.getReplicationPositionForClones(allowHierarchicalReplication) - if err != nil { - return - } - if !newReplicationPosition.Equal(replicationPosition) { - return nil, fmt.Errorf("replicationPosition position changed during snapshot, from %v to %v", replicationPosition, newReplicationPosition) - } - - // Write all the manifest files - ssmFiles := make([]string, len(keyRanges)) - for i, kr := range keyRanges { - krDatafiles := make([]SnapshotFile, 0, len(datafiles)) - for _, m := range datafiles { - krDatafiles = append(krDatafiles, m[kr]...) - } - ssm, err := NewSplitSnapshotManifest(sourceAddr, mysqld.IpAddr(), - masterAddr, dbName, krDatafiles, replicationPosition, - myMasterPosition, kr, sd) - if err != nil { - return nil, err - } - ssmFiles[i] = path.Join(cloneSourcePaths[kr], partialSnapshotManifestFile) - if err = writeJson(ssmFiles[i], ssm); err != nil { - return nil, err - } - } - - // Call the (optional) hook to send the files somewhere else - wg := sync.WaitGroup{} - rec := concurrency.AllErrorRecorder{} - for _, kr := range keyRanges { - wg.Add(1) - go func(kr key.KeyRange) { - defer wg.Done() - h := hook.NewSimpleHook("copy_snapshot_to_storage") - h.ExtraEnv = make(map[string]string) - for k, v := range hookExtraEnv { - h.ExtraEnv[k] = v - } - h.ExtraEnv["KEYRANGE"] = fmt.Sprintf("%v-%v", kr.Start.Hex(), kr.End.Hex()) - h.ExtraEnv["SNAPSHOT_PATH"] = cloneSourcePaths[kr] - rec.RecordError(h.ExecuteOptional()) - }(kr) - } - wg.Wait() - if rec.HasErrors() { - return nil, err - } - - // Return all the URLs for the MANIFESTs - snapshotURLPaths := make([]string, len(keyRanges)) - for i := 0; i < len(keyRanges); i++ { - relative, err := filepath.Rel(mysqld.SnapshotDir, ssmFiles[i]) - if err != nil { - return nil, err - } - snapshotURLPaths[i] = path.Join(SnapshotURLPath, relative) - } - return snapshotURLPaths, nil -} - -type localSnapshotFile struct { - manifest *SplitSnapshotManifest - file *SnapshotFile - basePath string -} - -func (lsf localSnapshotFile) filename() string { - return lsf.file.getLocalFilename(path.Join(lsf.basePath, lsf.manifest.Source.Addr)) -} - -func (lsf localSnapshotFile) url() string { - return "http://" + lsf.manifest.Source.Addr + path.Join(SnapshotURLPath, lsf.file.Path) -} - -func (lsf localSnapshotFile) tableName() string { - return lsf.file.TableName -} - -// MakeSplitCreateTableSql returns a table creation statement -// that is modified to be faster, and the associated optional -// 'alter table' to modify the table at the end. -func MakeSplitCreateTableSql(logger logutil.Logger, schema, databaseName, tableName string, strategy *SplitStrategy) (string, string, error) { - alters := make([]string, 0, 5) - lines := strings.Split(schema, "\n") - - for i, line := range lines { - if strings.HasPrefix(line, "CREATE TABLE `") { - lines[i] = strings.Replace(line, "CREATE TABLE `", "CREATE TABLE `"+databaseName+"`.`", 1) - continue - } - - if strings.Contains(line, " AUTO_INCREMENT") { - skipAutoIncrement := strategy.SkipAutoIncrementOnTable(tableName) - if skipAutoIncrement { - logger.Infof("Will drop AUTO_INCREMENT from table %v", tableName) - } - - // add an alter if we need to add the auto increment after the fact - if strategy.DelayAutoIncrement && !skipAutoIncrement { - alters = append(alters, "MODIFY "+line[:len(line)-1]) - } - - // remove the auto increment if we need to - if skipAutoIncrement || strategy.DelayAutoIncrement { - lines[i] = strings.Replace(line, " AUTO_INCREMENT", "", 1) - } - - continue - } - - isPrimaryKey := strings.Contains(line, " PRIMARY KEY") - isSecondaryIndex := !isPrimaryKey && strings.Contains(line, " KEY") - if (isPrimaryKey && strategy.DelayPrimaryKey) || (isSecondaryIndex && strategy.DelaySecondaryIndexes) { - - // remove the comma at the end of the previous line, - lines[i-1] = lines[i-1][:len(lines[i-1])-1] - - // keep our comma if any (so the next index - // might remove it) - // also add the key definition to the alters - if strings.HasSuffix(line, ",") { - lines[i] = "," - alters = append(alters, "ADD "+line[:len(line)-1]) - } else { - lines[i] = "" - alters = append(alters, "ADD "+line) - } - } - - if strategy.UseMyIsam && strings.Contains(line, " ENGINE=InnoDB") { - lines[i] = strings.Replace(line, " ENGINE=InnoDB", " ENGINE=MyISAM", 1) - alters = append(alters, "ENGINE=InnoDB") - } - } - - alter := "" - if len(alters) > 0 { - alter = "ALTER TABLE `" + databaseName + "`.`" + tableName + "` " + strings.Join(alters, ", ") - } - return strings.Join(lines, "\n"), alter, nil -} - -// buildQueryList builds the list of queries to use to run the provided -// query on the provided database -func buildQueryList(destinationDbName, query string, writeBinLogs bool) []string { - queries := make([]string, 0, 4) - if !writeBinLogs { - queries = append(queries, "SET sql_log_bin = OFF") - queries = append(queries, "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED") - } - queries = append(queries, "USE `"+destinationDbName+"`") - queries = append(queries, query) - return queries -} - -// MultiRestore is the main entry point for multi restore. -// -// We will either: -// - read from the network if sourceAddrs != nil -// - read from a disk snapshot if fromStoragePaths != nil -func (mysqld *Mysqld) MultiRestore(logger logutil.Logger, destinationDbName string, keyRanges []key.KeyRange, sourceAddrs []*url.URL, fromStoragePaths []string, snapshotConcurrency, fetchConcurrency, insertTableConcurrency, fetchRetryCount int, strategy *SplitStrategy) (err error) { - var manifests []*SplitSnapshotManifest - if sourceAddrs != nil { - // get the manifests from the network - manifests = make([]*SplitSnapshotManifest, len(sourceAddrs)) - rc := concurrency.NewResourceConstraint(fetchConcurrency) - for i, sourceAddr := range sourceAddrs { - rc.Add(1) - go func(sourceAddr *url.URL, i int) { - rc.Acquire() - defer rc.ReleaseAndDone() - if rc.HasErrors() { - return - } - - var sourceDbName string - if len(sourceAddr.Path) < 2 { // "" or "/" - sourceDbName = destinationDbName - } else { - sourceDbName = sourceAddr.Path[1:] - } - ssm, e := fetchSnapshotManifestWithRetry("http://"+sourceAddr.Host, sourceDbName, keyRanges[i], fetchRetryCount) - manifests[i] = ssm - rc.RecordError(e) - }(sourceAddr, i) - } - if err = rc.Wait(); err != nil { - return - } - } else { - // get the manifests from the local snapshots - manifests = make([]*SplitSnapshotManifest, len(fromStoragePaths)) - for i, fromStoragePath := range fromStoragePaths { - var err error - manifests[i], err = readSnapshotManifest(fromStoragePath) - if err != nil { - return err - } - } - } - - if e := SanityCheckManifests(manifests); e != nil { - return e - } - - tempStoragePath := path.Join(mysqld.SnapshotDir, "multirestore", destinationDbName) - - // Start fresh - if err = os.RemoveAll(tempStoragePath); err != nil { - return - } - - if err = os.MkdirAll(tempStoragePath, 0775); err != nil { - return err - } - - defer func() { - if e := os.RemoveAll(tempStoragePath); e != nil { - logger.Errorf("error removing %v: %v", tempStoragePath, e) - } - - }() - - // Handle our concurrency: - // - fetchConcurrency tasks for network / decompress from disk - // - insertTableConcurrency for table inserts from a file - // into an innodb table - // - snapshotConcurrency tasks for table inserts / modify tables - sems := make(map[string]*sync2.Semaphore, len(manifests[0].SchemaDefinition.TableDefinitions)+2) - sems["net"] = sync2.NewSemaphore(fetchConcurrency, 0) - sems["db"] = sync2.NewSemaphore(snapshotConcurrency, 0) - - // Store the alter table statements for after restore, - // and how many jobs we're running on each table - // TODO(alainjobart) the jobCount map is a bit weird. replace it - // with a map of WaitGroups, initialized to the number of files - // per table. Have extra go routines for the tables with auto_increment - // to wait on the waitgroup, and apply the modify_table. - postSql := make(map[string]string, len(manifests[0].SchemaDefinition.TableDefinitions)) - jobCount := make(map[string]*sync2.AtomicInt32) - - // Create the database (it's a good check to know if we're running - // multirestore a second time too!) - manifest := manifests[0] // I am assuming they all match - createDatabase, e := fillStringTemplate(manifest.SchemaDefinition.DatabaseSchema, map[string]string{"DatabaseName": destinationDbName}) - if e != nil { - return e - } - if createDatabase == "" { - return fmt.Errorf("Empty create database statement") - } - - createDbCmds := make([]string, 0, len(manifest.SchemaDefinition.TableDefinitions)+2) - if !strategy.WriteBinLogs { - createDbCmds = append(createDbCmds, "SET sql_log_bin = OFF") - } - createDbCmds = append(createDbCmds, createDatabase) - createDbCmds = append(createDbCmds, "USE `"+destinationDbName+"`") - createViewCmds := make([]string, 0, 16) - for _, td := range manifest.SchemaDefinition.TableDefinitions { - if td.Type == proto.TABLE_BASE_TABLE { - createDbCmd, alterTable, err := MakeSplitCreateTableSql(logger, td.Schema, destinationDbName, td.Name, strategy) - if err != nil { - return err - } - if alterTable != "" { - postSql[td.Name] = alterTable - } - jobCount[td.Name] = new(sync2.AtomicInt32) - createDbCmds = append(createDbCmds, createDbCmd) - sems["table-"+td.Name] = sync2.NewSemaphore(insertTableConcurrency, 0) - } else { - // views are just created with the right db name - // and no data will ever go in them. We create them - // after all tables are created, as they will - // probably depend on real tables. - createViewCmd, err := fillStringTemplate(td.Schema, map[string]string{"DatabaseName": destinationDbName}) - if err != nil { - return err - } - createViewCmds = append(createViewCmds, createViewCmd) - } - } - createDbCmds = append(createDbCmds, createViewCmds...) - if err = mysqld.ExecuteSuperQueryList(createDbCmds); err != nil { - return - } - - // compute how many jobs we will have - for _, manifest := range manifests { - for _, file := range manifest.Source.Files { - jobCount[file.TableName].Add(1) - } - } - - loadDataInfile := `LOAD DATA INFILE '{{.TableInputPath}}' INTO TABLE {{.TableName}} CHARACTER SET binary FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\' LINES TERMINATED BY '\n' ({{.Columns}})` - - // fetch all the csv files, and apply them one at a time. Note - // this might start many go routines, and they'll all be - // waiting on the resource semaphores. - mrc := concurrency.NewMultiResourceConstraint(sems) - for manifestIndex, manifest := range manifests { - if err = os.Mkdir(path.Join(tempStoragePath, manifest.Source.Addr), 0775); err != nil { - return err - } - - for i := range manifest.Source.Files { - lsf := localSnapshotFile{manifest: manifest, file: &manifest.Source.Files[i], basePath: tempStoragePath} - mrc.Add(1) - go func(manifestIndex, i int) { - defer mrc.Done() - - // compute a few things now, so if we can't we - // don't take resources: - // - get the schema - td, ok := manifest.SchemaDefinition.GetTable(lsf.tableName()) - if !ok { - mrc.RecordError(fmt.Errorf("No table named %v in schema", lsf.tableName())) - return - } - - // - get the load data statement - queryParams := map[string]string{ - "TableInputPath": lsf.filename(), - "TableName": lsf.tableName(), - "Columns": strings.Join(td.Columns, ", "), - } - loadStatement, e := fillStringTemplate(loadDataInfile, queryParams) - if e != nil { - mrc.RecordError(e) - return - } - - // get the file, using the 'net' resource - mrc.Acquire("net") - if mrc.HasErrors() { - mrc.Release("net") - return - } - if sourceAddrs == nil { - e = uncompressLocalFile(path.Join(fromStoragePaths[manifestIndex], path.Base(lsf.file.Path)), lsf.file.Hash, lsf.filename()) - } else { - e = fetchFileWithRetry(lsf.url(), lsf.file.Hash, lsf.filename(), fetchRetryCount) - } - mrc.Release("net") - if e != nil { - mrc.RecordError(e) - return - } - defer os.Remove(lsf.filename()) - - // acquire the table lock (we do this first - // so we maximize access to db. Otherwise - // if 8 threads had gotten the db lock but - // were writing to the same table, only one - // load would go at once) - tableLockName := "table-" + lsf.tableName() - mrc.Acquire(tableLockName) - defer func() { - mrc.Release(tableLockName) - }() - if mrc.HasErrors() { - return - } - - // acquire the db lock - mrc.Acquire("db") - defer func() { - mrc.Release("db") - }() - if mrc.HasErrors() { - return - } - - // load the data in - queries := buildQueryList(destinationDbName, loadStatement, strategy.WriteBinLogs) - e = mysqld.ExecuteSuperQueryList(queries) - if e != nil { - mrc.RecordError(e) - return - } - - // if we're running the last insert, - // potentially re-add the auto-increments - remainingInserts := jobCount[lsf.tableName()].Add(-1) - if remainingInserts == 0 && postSql[lsf.tableName()] != "" { - queries = buildQueryList(destinationDbName, postSql[lsf.tableName()], strategy.WriteBinLogs) - e = mysqld.ExecuteSuperQueryList(queries) - if e != nil { - mrc.RecordError(e) - return - } - } - }(manifestIndex, i) - } - } - - if err = mrc.Wait(); err != nil { - return err - } - - // populate blp_checkpoint table if we want to - if strategy.PopulateBlpCheckpoint { - queries := make([]string, 0, 4) - if !strategy.WriteBinLogs { - queries = append(queries, "SET sql_log_bin = OFF") - queries = append(queries, "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED") - } - queries = append(queries, binlogplayer.CreateBlpCheckpoint()...) - flags := "" - if strategy.DontStartBinlogPlayer { - flags = binlogplayer.BLP_FLAG_DONT_START - } - for manifestIndex, manifest := range manifests { - queries = append(queries, binlogplayer.PopulateBlpCheckpoint(uint32(manifestIndex), manifest.Source.MasterPosition, time.Now().Unix(), flags)) - } - if err = mysqld.ExecuteSuperQueryList(queries); err != nil { - return err - } - } - return nil -} diff --git a/go/vt/mysqlctl/split_strategy.go b/go/vt/mysqlctl/split_strategy.go index 1c667456a91..880e281988c 100644 --- a/go/vt/mysqlctl/split_strategy.go +++ b/go/vt/mysqlctl/split_strategy.go @@ -14,26 +14,6 @@ import ( // SplitStrategy is the configuration for a split clone. type SplitStrategy struct { - // DelayPrimaryKey will create the table without primary keys, - // and then apply them later as an alter - DelayPrimaryKey bool - - // DelaySecondaryIndexes will delay creating the secondary indexes - DelaySecondaryIndexes bool - - // SkipAutoIncrement will remove the auto increment field - // form the given tables - SkipAutoIncrement []string - - // UseMyIsam will use MyIsam tables to restore, then convert to InnoDB - UseMyIsam bool - - // DelayAutoIncrement will delay the addition of the auto-increment column - DelayAutoIncrement bool - - // WriteBinLogs will enable writing to the bin logs - WriteBinLogs bool - // PopulateBlpCheckpoint will drive the population of the blp_checkpoint table PopulateBlpCheckpoint bool @@ -42,9 +22,6 @@ type SplitStrategy struct { // SkipSetSourceShards will not set the source shards at the end of restore SkipSetSourceShards bool - - // WriteMastersOnly will write only to the master of the destination shard, with binlog enabled so that replicas can catch up - WriteMastersOnly bool } func NewSplitStrategy(logger logutil.Logger, argsStr string) (*SplitStrategy, error) { @@ -58,69 +35,25 @@ func NewSplitStrategy(logger logutil.Logger, argsStr string) (*SplitStrategy, er logger.Printf("Strategy flag has the following options:\n") flagSet.PrintDefaults() } - delayPrimaryKey := flagSet.Bool("delay_primary_key", false, "delays the application of the primary key until after the data population") - delaySecondaryIndexes := flagSet.Bool("delay_secondary_indexes", false, "delays the application of secondary indexes until after the data population") - skipAutoIncrementStr := flagSet.String("skip_auto_increment", "", "comma spearated list of tables on which not to re-introduce auto-increment") - useMyIsam := flagSet.Bool("use_my_isam", false, "uses MyISAM table types for restores, then switches to InnoDB") - delayAutoIncrement := flagSet.Bool("delay_auto_increment", false, "don't add auto_increment at table creation, but re-introduces them later") - writeBinLogs := flagSet.Bool("write_bin_logs", false, "write write to the binlogs on the destination") populateBlpCheckpoint := flagSet.Bool("populate_blp_checkpoint", false, "populates the blp checkpoint table") dontStartBinlogPlayer := flagSet.Bool("dont_start_binlog_player", false, "do not start the binlog player after restore is complete") skipSetSourceShards := flagSet.Bool("skip_set_source_shards", false, "do not set the SourceShar field on destination shards") - writeMastersOnly := flagSet.Bool("write_masters_only", false, "rite only to the master of the destination shard, with binlog enabled so that replicas can catch up") if err := flagSet.Parse(args); err != nil { return nil, fmt.Errorf("cannot parse strategy: %v", err) } if flagSet.NArg() > 0 { return nil, fmt.Errorf("strategy doesn't have positional arguments") } - var skipAutoIncrement []string - if *skipAutoIncrementStr != "" { - skipAutoIncrement = strings.Split(*skipAutoIncrementStr, ",") - } + return &SplitStrategy{ - DelayPrimaryKey: *delayPrimaryKey, - DelaySecondaryIndexes: *delaySecondaryIndexes, - SkipAutoIncrement: skipAutoIncrement, - UseMyIsam: *useMyIsam, - DelayAutoIncrement: *delayAutoIncrement, - WriteBinLogs: *writeBinLogs, PopulateBlpCheckpoint: *populateBlpCheckpoint, DontStartBinlogPlayer: *dontStartBinlogPlayer, SkipSetSourceShards: *skipSetSourceShards, - WriteMastersOnly: *writeMastersOnly, }, nil } -func (strategy *SplitStrategy) SkipAutoIncrementOnTable(table string) bool { - for _, t := range strategy.SkipAutoIncrement { - if t == table { - return true - } - } - return false -} - func (strategy *SplitStrategy) String() string { var result []string - if strategy.DelayPrimaryKey { - result = append(result, "-delay_primary_key") - } - if strategy.DelaySecondaryIndexes { - result = append(result, "-delay_secondary_indexes") - } - if len(strategy.SkipAutoIncrement) > 0 { - result = append(result, "-skip_auto_increment="+strings.Join(strategy.SkipAutoIncrement, ",")) - } - if strategy.UseMyIsam { - result = append(result, "-use_my_isam") - } - if strategy.DelayAutoIncrement { - result = append(result, "-delay_auto_increment") - } - if strategy.WriteBinLogs { - result = append(result, "-write_bin_logs") - } if strategy.PopulateBlpCheckpoint { result = append(result, "-populate_blp_checkpoint") } @@ -130,8 +63,5 @@ func (strategy *SplitStrategy) String() string { if strategy.SkipSetSourceShards { result = append(result, "-skip_set_source_shards") } - if strategy.WriteMastersOnly { - result = append(result, "-write_masters_only") - } return strings.Join(result, " ") } diff --git a/go/vt/mysqlctl/split_test.go b/go/vt/mysqlctl/split_test.go deleted file mode 100644 index 99d87508fb9..00000000000 --- a/go/vt/mysqlctl/split_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2014, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package mysqlctl - -import ( - "testing" - - "github.com/youtube/vitess/go/vt/logutil" -) - -func testMakeSplitCreateTableSql(t *testing.T, testCase, schema, strategyStr, expectedCreate, expectedAlter, expectedError string) { - logger := logutil.NewMemoryLogger() - strategy, err := NewSplitStrategy(logger, strategyStr) - if err != nil { - t.Fatalf("%v: got strategy parsing error: %v", testCase, err) - } - create, alter, err := MakeSplitCreateTableSql(logger, schema, "DBNAME", "TABLENAME", strategy) - if expectedError != "" { - if err == nil || err.Error() != expectedError { - t.Fatalf("%v: got '%v' but was expecting error '%v'", testCase, err, expectedError) - } - } - if err != nil { - t.Fatalf("%v: expected no error but got: %v", testCase, err) - } - if create != expectedCreate { - t.Errorf("%v: create mismatch: got:\n%vexpected:\n%v", testCase, create, expectedCreate) - } - if alter != expectedAlter { - t.Errorf("%v: alter mismatch: got:\n%vexpected:\n%v", testCase, alter, expectedAlter) - } -} - -func TestMakeSplitCreateTableSql(t *testing.T) { - testMakeSplitCreateTableSql(t, "simple table no index", - "CREATE TABLE `TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL,\n"+ - " `msg` varchar(64) DEFAULT NULL\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "", - "CREATE TABLE `DBNAME`.`TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL,\n"+ - " `msg` varchar(64) DEFAULT NULL\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "", "") - - testMakeSplitCreateTableSql(t, "simple table primary key", - "CREATE TABLE `TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " PRIMARY KEY (`id`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "", - "CREATE TABLE `DBNAME`.`TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " PRIMARY KEY (`id`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "", "") - - testMakeSplitCreateTableSql(t, "simple table delay primary key", - "CREATE TABLE `TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " PRIMARY KEY (`id`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "-delay_primary_key", - "CREATE TABLE `DBNAME`.`TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL,\n"+ - " `msg` varchar(64) DEFAULT NULL\n"+ - "\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "ALTER TABLE `DBNAME`.`TABLENAME` ADD PRIMARY KEY (`id`)", "") - - testMakeSplitCreateTableSql(t, "simple table primary key auto increment", - "CREATE TABLE `TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL AUTO_INCREMENT,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " PRIMARY KEY (`id`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "", - "CREATE TABLE `DBNAME`.`TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL AUTO_INCREMENT,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " PRIMARY KEY (`id`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "", "") - - testMakeSplitCreateTableSql(t, "simple table primary key skip auto increment", - "CREATE TABLE `TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL AUTO_INCREMENT,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " PRIMARY KEY (`id`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "-skip_auto_increment=TABLENAME", - "CREATE TABLE `DBNAME`.`TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " PRIMARY KEY (`id`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "", "") - - testMakeSplitCreateTableSql(t, "simple table primary key delay auto increment", - "CREATE TABLE `TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL AUTO_INCREMENT,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " PRIMARY KEY (`id`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "-delay_auto_increment", - "CREATE TABLE `DBNAME`.`TABLENAME` (\n"+ - " `id` bigint(2) NOT NULL,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " PRIMARY KEY (`id`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8", - "ALTER TABLE `DBNAME`.`TABLENAME` MODIFY `id` bigint(2) NOT NULL AUTO_INCREMENT", "") -} diff --git a/go/vt/primecache/primecache.go b/go/vt/primecache/primecache.go index 5843923704f..1376ae5562b 100644 --- a/go/vt/primecache/primecache.go +++ b/go/vt/primecache/primecache.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// primecache primes the MySQL buffer cache with the rows that are +// Package primecache primes the MySQL buffer cache with the rows that are // going to be modified by the replication stream. It only activates // if we're falling behind on replication. package primecache diff --git a/go/vt/rpc/unused_test.go b/go/vt/rpc/unused_test.go index c7f618acb93..ff7bbe6e580 100644 --- a/go/vt/rpc/unused_test.go +++ b/go/vt/rpc/unused_test.go @@ -105,7 +105,7 @@ func TestUnmarshalEmptyStructIntoStruct(t *testing.T) { } // It should always be possible to add fields to something that's already a - // struct. The struct name is irrelevent since it's never encoded. + // struct. The struct name is irrelevant since it's never encoded. var out struct{ A, B string } if err := bson.Unmarshal(buf, &out); err != nil { t.Errorf("bson.Unmarshal: %v", err) diff --git a/go/vt/servenv/buildinfo.go b/go/vt/servenv/buildinfo.go new file mode 100644 index 00000000000..89183edcaa8 --- /dev/null +++ b/go/vt/servenv/buildinfo.go @@ -0,0 +1,26 @@ +package servenv + +import ( + "fmt" + "time" + + "github.com/youtube/vitess/go/stats" +) + +var ( + buildHost = "" + buildUser = "" + buildTime = "" + buildGitRev = "" +) + +func init() { + t, err := time.Parse(time.UnixDate, buildTime) + if buildTime != "" && err != nil { + panic(fmt.Sprintf("Couldn't parse build timestamp %q: %v", buildTime, err)) + } + stats.NewString("BuildHost").Set(buildHost) + stats.NewString("BuildUser").Set(buildUser) + stats.NewInt("BuildTimestamp").Set(t.Unix()) + stats.NewString("BuildGitRev").Set(buildGitRev) +} diff --git a/go/vt/servenv/servenv.go b/go/vt/servenv/servenv.go index 1a07956da59..db87bc04cbe 100644 --- a/go/vt/servenv/servenv.go +++ b/go/vt/servenv/servenv.go @@ -18,11 +18,7 @@ package servenv import ( - "crypto/md5" - "encoding/hex" "flag" - "fmt" - "io" "net/url" "os" "runtime" @@ -93,33 +89,9 @@ func Init() { fdl := stats.NewInt("MaxFds") fdl.Set(int64(fdLimit.Cur)) - if err := exportBinaryVersion(); err != nil { - log.Fatalf("servenv.Init: exportBinaryVersion: %v", err) - } - onInitHooks.Fire() } -func exportBinaryVersion() error { - hasher := md5.New() - exeFile, err := os.Open("/proc/self/exe") - if err != nil { - return err - } - if _, err = io.Copy(hasher, exeFile); err != nil { - return err - } - md5sum := hex.EncodeToString(hasher.Sum(nil)) - fileInfo, err := exeFile.Stat() - if err != nil { - return err - } - mtime := fileInfo.ModTime().Format(time.RFC3339) - version := mtime + " " + md5sum - stats.NewString("BinaryVersion").Set(version) - return nil -} - func populateListeningURL() { host, err := netutil.FullyQualifiedHostname() if err != nil { @@ -130,7 +102,7 @@ func populateListeningURL() { } ListeningURL = url.URL{ Scheme: "http", - Host: fmt.Sprintf("%v:%v", host, *Port), + Host: netutil.JoinHostPort(host, *Port), Path: "/", } } diff --git a/go/vt/servenv/status.go b/go/vt/servenv/status.go index 693071722fc..e337a836059 100644 --- a/go/vt/servenv/status.go +++ b/go/vt/servenv/status.go @@ -131,7 +131,7 @@ func AddStatusPart(banner, frag string, f func() interface{}) { if err != nil { secs[len(secs)-1] = section{ Banner: banner, - Fragment: "bad statusz template: {{.}}", + Fragment: "bad status template: {{.}}", F: func() interface{} { return err }, } } diff --git a/go/vt/sqlparser/ast.go b/go/vt/sqlparser/ast.go index 2ffcd78469d..1e9fe31dd32 100644 --- a/go/vt/sqlparser/ast.go +++ b/go/vt/sqlparser/ast.go @@ -434,7 +434,7 @@ func NewWhere(typ string, expr BoolExpr) *Where { } func (node *Where) Format(buf *TrackedBuffer) { - if node == nil { + if node == nil || node.Expr == nil { return } buf.Myprintf(" %s %v", node.Type, node.Expr) @@ -454,6 +454,7 @@ func (*ComparisonExpr) IExpr() {} func (*RangeCond) IExpr() {} func (*NullCheck) IExpr() {} func (*ExistsExpr) IExpr() {} +func (*KeyrangeExpr) IExpr() {} func (StrVal) IExpr() {} func (NumVal) IExpr() {} func (ValArg) IExpr() {} @@ -481,6 +482,7 @@ func (*ComparisonExpr) IBoolExpr() {} func (*RangeCond) IBoolExpr() {} func (*NullCheck) IBoolExpr() {} func (*ExistsExpr) IBoolExpr() {} +func (*KeyrangeExpr) IBoolExpr() {} // AndExpr represents an AND expression. type AndExpr struct { @@ -585,6 +587,15 @@ func (node *ExistsExpr) Format(buf *TrackedBuffer) { buf.Myprintf("exists %v", node.Subquery) } +// KeyrangeExpr represents a KEYRANGE expression. +type KeyrangeExpr struct { + Start, End ValExpr +} + +func (node *KeyrangeExpr) Format(buf *TrackedBuffer) { + buf.Myprintf("keyrange(%v, %v)", node.Start, node.End) +} + // ValExpr represents a value expression. type ValExpr interface { IValExpr() diff --git a/go/vt/sqlparser/ast_test.go b/go/vt/sqlparser/ast_test.go index 7eac5c509cc..a0e776fd019 100644 --- a/go/vt/sqlparser/ast_test.go +++ b/go/vt/sqlparser/ast_test.go @@ -6,6 +6,21 @@ package sqlparser import "testing" +func TestWhere(t *testing.T) { + var w *Where + buf := NewTrackedBuffer(nil) + w.Format(buf) + if buf.String() != "" { + t.Errorf("w.Format(nil): %q, want \"\"", buf.String) + } + w = NewWhere(AST_WHERE, nil) + buf = NewTrackedBuffer(nil) + w.Format(buf) + if buf.String() != "" { + t.Errorf("w.Format(&Where{nil}: %q, want \"\"", buf.String) + } +} + func TestLimits(t *testing.T) { var l *Limit o, r, err := l.Limits() diff --git a/go/vt/sqlparser/sql.go b/go/vt/sqlparser/sql.go index 3f5c802cf03..8fde7d78a66 100644 --- a/go/vt/sqlparser/sql.go +++ b/go/vt/sqlparser/sql.go @@ -94,56 +94,57 @@ const KEY = 57373 const DEFAULT = 57374 const SET = 57375 const LOCK = 57376 -const ID = 57377 -const STRING = 57378 -const NUMBER = 57379 -const VALUE_ARG = 57380 -const LIST_ARG = 57381 -const COMMENT = 57382 -const LE = 57383 -const GE = 57384 -const NE = 57385 -const NULL_SAFE_EQUAL = 57386 -const UNION = 57387 -const MINUS = 57388 -const EXCEPT = 57389 -const INTERSECT = 57390 -const JOIN = 57391 -const STRAIGHT_JOIN = 57392 -const LEFT = 57393 -const RIGHT = 57394 -const INNER = 57395 -const OUTER = 57396 -const CROSS = 57397 -const NATURAL = 57398 -const USE = 57399 -const FORCE = 57400 -const ON = 57401 -const OR = 57402 -const AND = 57403 -const NOT = 57404 -const UNARY = 57405 -const CASE = 57406 -const WHEN = 57407 -const THEN = 57408 -const ELSE = 57409 -const END = 57410 -const CREATE = 57411 -const ALTER = 57412 -const DROP = 57413 -const RENAME = 57414 -const ANALYZE = 57415 -const TABLE = 57416 -const INDEX = 57417 -const VIEW = 57418 -const TO = 57419 -const IGNORE = 57420 -const IF = 57421 -const UNIQUE = 57422 -const USING = 57423 -const SHOW = 57424 -const DESCRIBE = 57425 -const EXPLAIN = 57426 +const KEYRANGE = 57377 +const ID = 57378 +const STRING = 57379 +const NUMBER = 57380 +const VALUE_ARG = 57381 +const LIST_ARG = 57382 +const COMMENT = 57383 +const LE = 57384 +const GE = 57385 +const NE = 57386 +const NULL_SAFE_EQUAL = 57387 +const UNION = 57388 +const MINUS = 57389 +const EXCEPT = 57390 +const INTERSECT = 57391 +const JOIN = 57392 +const STRAIGHT_JOIN = 57393 +const LEFT = 57394 +const RIGHT = 57395 +const INNER = 57396 +const OUTER = 57397 +const CROSS = 57398 +const NATURAL = 57399 +const USE = 57400 +const FORCE = 57401 +const ON = 57402 +const OR = 57403 +const AND = 57404 +const NOT = 57405 +const UNARY = 57406 +const CASE = 57407 +const WHEN = 57408 +const THEN = 57409 +const ELSE = 57410 +const END = 57411 +const CREATE = 57412 +const ALTER = 57413 +const DROP = 57414 +const RENAME = 57415 +const ANALYZE = 57416 +const TABLE = 57417 +const INDEX = 57418 +const VIEW = 57419 +const TO = 57420 +const IGNORE = 57421 +const IF = 57422 +const UNIQUE = 57423 +const USING = 57424 +const SHOW = 57425 +const DESCRIBE = 57426 +const EXPLAIN = 57427 var yyToknames = []string{ "LEX_ERROR", @@ -177,6 +178,7 @@ var yyToknames = []string{ "DEFAULT", "SET", "LOCK", + "KEYRANGE", "ID", "STRING", "NUMBER", @@ -256,129 +258,135 @@ var yyExca = []int{ -2, 0, } -const yyNprod = 202 +const yyNprod = 203 const yyPrivate = 57344 var yyTokenNames []string var yyStates []string -const yyLast = 601 +const yyLast = 652 var yyAct = []int{ - 94, 290, 158, 358, 91, 85, 326, 245, 62, 161, - 92, 282, 236, 80, 196, 207, 367, 160, 3, 367, - 63, 176, 184, 135, 134, 81, 50, 256, 257, 258, - 259, 260, 367, 261, 262, 129, 228, 288, 65, 129, - 76, 70, 64, 68, 73, 53, 252, 129, 77, 233, - 123, 97, 51, 52, 337, 102, 101, 226, 86, 107, - 228, 43, 369, 44, 336, 368, 84, 98, 99, 100, - 119, 317, 311, 46, 47, 48, 89, 335, 366, 127, - 105, 316, 313, 287, 131, 277, 28, 29, 30, 31, - 69, 72, 162, 275, 157, 159, 163, 120, 49, 88, - 122, 45, 237, 103, 104, 82, 229, 38, 118, 40, - 108, 170, 65, 41, 267, 65, 64, 180, 179, 64, - 174, 133, 135, 134, 116, 106, 307, 309, 237, 112, - 280, 231, 86, 202, 180, 178, 227, 319, 134, 206, - 204, 205, 214, 215, 181, 218, 219, 220, 221, 222, - 223, 224, 225, 167, 194, 201, 308, 314, 332, 142, - 143, 144, 145, 146, 147, 148, 149, 230, 86, 86, - 71, 216, 190, 65, 65, 135, 134, 64, 243, 232, - 234, 241, 342, 247, 200, 249, 147, 148, 149, 240, - 283, 188, 248, 209, 191, 126, 244, 142, 143, 144, - 145, 146, 147, 148, 149, 14, 15, 16, 17, 203, - 334, 230, 250, 266, 217, 270, 271, 253, 268, 142, - 143, 144, 145, 146, 147, 148, 149, 269, 333, 301, - 299, 274, 305, 18, 302, 300, 86, 145, 146, 147, - 148, 149, 304, 281, 187, 189, 186, 276, 279, 303, - 285, 114, 289, 286, 200, 228, 59, 345, 346, 256, - 257, 258, 259, 260, 128, 261, 262, 209, 114, 297, - 298, 28, 29, 30, 31, 315, 343, 321, 115, 283, - 199, 353, 75, 318, 19, 20, 22, 21, 23, 65, - 198, 323, 352, 322, 324, 327, 177, 24, 25, 26, - 142, 143, 144, 145, 146, 147, 148, 149, 351, 129, - 200, 200, 164, 210, 177, 110, 168, 338, 113, 208, - 328, 272, 339, 142, 143, 144, 145, 146, 147, 148, - 149, 78, 341, 166, 230, 14, 348, 347, 350, 172, - 254, 349, 165, 109, 265, 355, 327, 71, 132, 357, - 356, 173, 359, 359, 359, 65, 360, 361, 114, 64, - 264, 97, 364, 362, 71, 199, 101, 66, 372, 107, - 312, 310, 373, 294, 374, 198, 84, 98, 99, 100, - 365, 320, 293, 193, 192, 175, 89, 124, 121, 117, - 105, 60, 79, 74, 111, 340, 14, 58, 14, 211, - 273, 212, 213, 371, 182, 125, 32, 56, 331, 88, - 292, 97, 54, 103, 104, 82, 101, 291, 246, 107, - 108, 239, 34, 35, 36, 37, 66, 98, 99, 100, - 330, 296, 177, 61, 370, 106, 89, 14, 354, 33, - 105, 183, 39, 251, 185, 42, 67, 242, 171, 363, - 97, 14, 344, 325, 329, 101, 295, 278, 107, 88, - 169, 235, 96, 103, 104, 66, 98, 99, 100, 93, - 108, 101, 95, 284, 107, 89, 90, 238, 136, 105, - 87, 66, 98, 99, 100, 106, 306, 197, 255, 195, - 83, 164, 263, 130, 55, 105, 27, 57, 88, 13, - 12, 11, 103, 104, 10, 9, 8, 7, 6, 108, - 101, 5, 4, 107, 2, 1, 0, 0, 103, 104, - 66, 98, 99, 100, 106, 108, 0, 0, 0, 0, - 164, 0, 0, 0, 105, 0, 0, 0, 0, 0, - 106, 0, 0, 0, 0, 137, 141, 139, 140, 0, - 0, 0, 0, 0, 0, 0, 0, 103, 104, 0, - 0, 0, 0, 0, 108, 153, 154, 155, 156, 0, - 150, 151, 152, 0, 0, 0, 0, 0, 0, 106, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 138, 142, 143, 144, 145, 146, 147, 148, - 149, + 94, 294, 159, 364, 91, 85, 331, 248, 62, 162, + 92, 198, 286, 239, 80, 209, 63, 178, 90, 161, + 3, 373, 373, 81, 344, 373, 50, 103, 259, 260, + 261, 262, 263, 186, 264, 265, 130, 76, 65, 230, + 292, 70, 64, 130, 73, 130, 68, 53, 77, 136, + 135, 255, 51, 52, 28, 29, 30, 31, 86, 230, + 311, 313, 124, 38, 72, 40, 342, 375, 374, 41, + 120, 372, 43, 315, 44, 240, 46, 47, 48, 128, + 341, 49, 320, 228, 132, 317, 291, 192, 340, 280, + 312, 278, 163, 69, 158, 160, 164, 121, 45, 322, + 123, 240, 270, 284, 229, 231, 190, 136, 135, 134, + 193, 117, 172, 65, 113, 218, 65, 64, 182, 181, + 64, 176, 324, 119, 71, 168, 146, 147, 148, 149, + 150, 135, 180, 86, 204, 182, 148, 149, 150, 337, + 208, 206, 207, 216, 217, 183, 220, 221, 222, 223, + 224, 225, 226, 227, 203, 196, 115, 202, 287, 219, + 189, 191, 188, 205, 136, 135, 211, 287, 232, 86, + 86, 251, 127, 59, 339, 65, 65, 338, 309, 64, + 246, 234, 236, 244, 348, 250, 305, 252, 308, 237, + 303, 306, 307, 243, 247, 304, 115, 179, 179, 143, + 144, 145, 146, 147, 148, 149, 150, 230, 14, 15, + 16, 17, 269, 232, 253, 256, 129, 273, 274, 349, + 271, 28, 29, 30, 31, 14, 326, 281, 202, 272, + 201, 212, 111, 277, 75, 114, 18, 210, 86, 116, + 200, 211, 257, 115, 359, 174, 285, 358, 357, 112, + 279, 165, 283, 289, 170, 293, 201, 290, 175, 169, + 351, 352, 130, 110, 167, 166, 200, 268, 71, 301, + 302, 66, 316, 259, 260, 261, 262, 263, 319, 264, + 265, 314, 298, 78, 267, 202, 202, 323, 19, 20, + 22, 21, 23, 65, 346, 328, 297, 327, 329, 332, + 321, 24, 25, 26, 143, 144, 145, 146, 147, 148, + 149, 150, 143, 144, 145, 146, 147, 148, 149, 150, + 195, 343, 194, 133, 333, 177, 318, 345, 143, 144, + 145, 146, 147, 148, 149, 150, 102, 347, 125, 232, + 71, 354, 353, 356, 122, 118, 355, 276, 99, 100, + 101, 361, 332, 60, 79, 363, 362, 74, 365, 365, + 365, 65, 366, 367, 325, 64, 235, 14, 97, 368, + 370, 58, 377, 102, 378, 184, 108, 126, 379, 213, + 380, 214, 215, 98, 84, 99, 100, 101, 371, 56, + 242, 32, 54, 295, 89, 336, 296, 275, 106, 143, + 144, 145, 146, 147, 148, 149, 150, 34, 35, 36, + 37, 249, 335, 300, 179, 61, 33, 88, 376, 360, + 14, 104, 105, 82, 185, 39, 254, 187, 109, 97, + 42, 67, 245, 173, 102, 369, 350, 108, 330, 334, + 299, 282, 171, 107, 98, 84, 99, 100, 101, 233, + 238, 96, 93, 95, 288, 89, 241, 137, 87, 106, + 310, 199, 258, 197, 83, 14, 266, 131, 55, 27, + 57, 13, 12, 11, 10, 9, 8, 7, 88, 6, + 97, 5, 104, 105, 82, 102, 4, 2, 108, 109, + 1, 0, 0, 0, 0, 98, 66, 99, 100, 101, + 97, 0, 0, 0, 107, 102, 89, 0, 108, 0, + 106, 0, 0, 0, 0, 98, 66, 99, 100, 101, + 0, 0, 0, 0, 0, 14, 89, 0, 0, 88, + 106, 0, 0, 104, 105, 0, 0, 0, 0, 0, + 109, 0, 0, 0, 0, 102, 0, 0, 108, 88, + 0, 0, 0, 104, 105, 107, 66, 99, 100, 101, + 109, 0, 0, 0, 0, 102, 165, 0, 108, 0, + 106, 0, 0, 0, 0, 107, 66, 99, 100, 101, + 0, 0, 0, 0, 0, 0, 165, 0, 0, 0, + 106, 0, 0, 104, 105, 138, 142, 140, 141, 0, + 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 104, 105, 107, 154, 155, 156, 157, + 109, 151, 152, 153, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 107, 0, 0, 0, 0, + 0, 0, 0, 139, 143, 144, 145, 146, 147, 148, + 149, 150, } var yyPact = []int{ - 200, -1000, -1000, 221, -1000, -1000, -1000, -1000, -1000, -1000, - -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 18, - -30, 12, -16, 9, -1000, -1000, -1000, 432, 395, -1000, - -1000, -1000, 389, -1000, 368, 356, 424, 332, -51, 0, - 312, -1000, 2, 312, -1000, 358, -54, 312, -54, 357, - -1000, -1000, -1000, -1000, -1000, 341, -1000, 303, 356, 361, - 52, 356, 197, -1000, 232, -1000, 47, 354, 40, 312, - -1000, -1000, 353, -1000, -42, 352, 385, 130, 312, -1000, - 255, -1000, -1000, 329, 44, 109, 524, -1000, 430, 391, - -1000, -1000, -1000, 485, 297, 288, -1000, 271, -1000, -1000, - -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 485, -1000, - 306, 332, 350, 422, 332, 485, 312, -1000, 384, -74, - -1000, 159, -1000, 349, -1000, -1000, 348, -1000, 245, 341, - -1000, -1000, 312, 135, 430, 430, 485, 274, 378, 485, - 485, 146, 485, 485, 485, 485, 485, 485, 485, 485, - -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 524, -43, - 36, 6, 524, -1000, 446, 31, 341, -1000, 432, 22, - 150, 393, 332, 332, 304, -1000, 405, 430, -1000, 150, - -1000, -1000, -1000, 127, 312, -1000, -46, -1000, -1000, -1000, - -1000, -1000, -1000, -1000, -1000, 286, 204, 325, 330, 37, - -1000, -1000, -1000, -1000, -1000, 71, 150, -1000, 446, -1000, - -1000, 274, 485, 485, 150, 254, -1000, 375, 165, 165, - 165, 112, 112, -1000, -1000, -1000, -1000, -1000, 485, -1000, - 150, -1000, -7, 341, -15, 48, -1000, 430, 125, 267, - 221, 214, -17, -1000, 405, 402, 396, 109, 347, -1000, - -1000, 338, -1000, 420, 245, 245, -1000, -1000, 175, 174, - 194, 187, 177, 63, -1000, 336, -28, 335, -18, -1000, - 150, 90, 485, -1000, 150, -1000, -19, -1000, -12, -1000, - 485, 56, -1000, 351, 223, -1000, -1000, -1000, 332, 402, - -1000, 485, 485, -1000, -1000, 418, 394, 204, 93, -1000, - 173, -1000, 155, -1000, -1000, -1000, -1000, -13, -26, -36, - -1000, -1000, -1000, -1000, 485, 150, -1000, -1000, 150, 485, - 364, 267, -1000, -1000, 128, 222, -1000, 231, -1000, 405, - 430, 485, 430, -1000, -1000, 263, 247, 236, 150, 150, - 431, -1000, 485, 485, -1000, -1000, -1000, 402, 109, 201, - 109, 312, 312, 312, 332, 150, -1000, 346, -22, -1000, - -35, -38, 197, -1000, 427, 382, -1000, 312, -1000, -1000, - -1000, 312, -1000, 312, -1000, + 203, -1000, -1000, 170, -1000, -1000, -1000, -1000, -1000, -1000, + -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -27, + -20, 8, -14, -9, -1000, -1000, -1000, 415, 375, -1000, + -1000, -1000, 371, -1000, 342, 317, 406, 235, -49, 2, + 232, -1000, -26, 232, -1000, 321, -58, 232, -58, 318, + -1000, -1000, -1000, -1000, -1000, 409, -1000, 222, 317, 216, + 36, 317, 141, -1000, 192, -1000, 33, 309, 54, 232, + -1000, -1000, 308, -1000, -31, 302, 357, 106, 232, -1000, + 207, -1000, -1000, 304, 31, 97, 574, -1000, 480, 460, + -1000, -1000, -1000, 540, 219, 218, -1000, 213, 208, -1000, + -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 540, + -1000, 212, 235, 289, 404, 235, 540, 232, -1000, 355, + -64, -1000, 74, -1000, 286, -1000, -1000, 284, -1000, 194, + 409, -1000, -1000, 232, 88, 480, 480, 540, 191, 358, + 540, 540, 90, 540, 540, 540, 540, 540, 540, 540, + 540, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 574, + -18, 3, 4, 574, -1000, 520, 348, 409, -1000, 415, + 311, -6, 242, 362, 235, 235, 188, -1000, 398, 480, + -1000, 242, -1000, -1000, -1000, 105, 232, -1000, -42, -1000, + -1000, -1000, -1000, -1000, -1000, -1000, -1000, 187, 217, 248, + 220, 24, -1000, -1000, -1000, -1000, -1000, 63, 242, -1000, + 520, -1000, -1000, 191, 540, 540, 242, 329, -1000, 322, + 53, 53, 53, 61, 61, -1000, -1000, -1000, -1000, -1000, + 540, -1000, 242, -1000, -10, 409, -12, 172, 20, -1000, + 480, 92, 205, 170, 101, -15, -1000, 398, 378, 382, + 97, 260, -1000, -1000, 246, -1000, 402, 194, 194, -1000, + -1000, 134, 130, 136, 132, 122, -4, -1000, 245, -28, + 236, -16, -1000, 242, 258, 540, -1000, 242, -1000, -19, + -1000, 311, 15, -1000, 540, 40, -1000, 334, 171, -1000, + -1000, -1000, 235, 378, -1000, 540, 540, -1000, -1000, 400, + 381, 217, 73, -1000, 121, -1000, 118, -1000, -1000, -1000, + -1000, -3, -11, -25, -1000, -1000, -1000, -1000, 540, 242, + -1000, -77, -1000, 242, 540, 263, 205, -1000, -1000, 129, + 164, -1000, 234, -1000, 398, 480, 540, 480, -1000, -1000, + 202, 201, 198, 242, -1000, 242, 412, -1000, 540, 540, + -1000, -1000, -1000, 378, 97, 152, 97, 232, 232, 232, + 235, 242, -1000, 354, -30, -1000, -33, -34, 141, -1000, + 411, 351, -1000, 232, -1000, -1000, -1000, 232, -1000, 232, + -1000, } var yyPgo = []int{ - 0, 515, 514, 17, 512, 511, 508, 507, 506, 505, - 504, 501, 500, 499, 406, 497, 496, 494, 13, 25, - 493, 492, 490, 489, 14, 488, 487, 256, 486, 3, - 21, 5, 480, 478, 477, 476, 2, 15, 9, 473, - 10, 472, 55, 469, 4, 462, 461, 12, 460, 457, - 456, 454, 7, 453, 6, 452, 1, 449, 448, 447, - 11, 8, 20, 282, 446, 445, 444, 443, 442, 441, - 0, 26, 439, + 0, 490, 487, 19, 486, 481, 479, 477, 476, 475, + 474, 473, 472, 471, 391, 470, 469, 468, 14, 23, + 467, 466, 464, 463, 11, 462, 461, 173, 460, 3, + 17, 5, 458, 457, 456, 18, 2, 15, 9, 454, + 10, 453, 27, 452, 4, 451, 450, 13, 442, 441, + 440, 439, 7, 438, 6, 436, 1, 435, 433, 432, + 12, 8, 16, 234, 431, 430, 427, 426, 425, 424, + 0, 26, 416, } var yyR1 = []int{ @@ -392,17 +400,17 @@ var yyR1 = []int{ 25, 25, 25, 26, 26, 26, 27, 27, 28, 28, 28, 28, 29, 29, 30, 30, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, - 32, 33, 33, 33, 33, 33, 33, 33, 37, 37, - 37, 42, 38, 38, 36, 36, 36, 36, 36, 36, + 32, 32, 33, 33, 33, 33, 33, 33, 33, 37, + 37, 37, 42, 38, 38, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, - 36, 41, 41, 43, 43, 43, 45, 48, 48, 46, - 46, 47, 49, 49, 44, 44, 35, 35, 35, 35, - 50, 50, 51, 51, 52, 52, 53, 53, 54, 55, - 55, 55, 56, 56, 56, 57, 57, 57, 58, 58, - 59, 59, 60, 60, 34, 34, 39, 39, 40, 40, - 61, 61, 62, 63, 63, 64, 64, 65, 65, 66, - 66, 66, 66, 66, 67, 67, 68, 68, 69, 69, - 70, 71, + 36, 36, 41, 41, 43, 43, 43, 45, 48, 48, + 46, 46, 47, 49, 49, 44, 44, 35, 35, 35, + 35, 50, 50, 51, 51, 52, 52, 53, 53, 54, + 55, 55, 55, 56, 56, 56, 57, 57, 57, 58, + 58, 59, 59, 60, 60, 34, 34, 39, 39, 40, + 40, 61, 61, 62, 63, 63, 64, 64, 65, 65, + 66, 66, 66, 66, 66, 67, 67, 68, 68, 69, + 69, 70, 71, } var yyR2 = []int{ @@ -416,115 +424,117 @@ var yyR2 = []int{ 2, 2, 2, 1, 3, 1, 1, 3, 0, 5, 5, 5, 1, 3, 0, 2, 1, 3, 3, 2, 3, 3, 3, 4, 3, 4, 5, 6, 3, 4, - 2, 1, 1, 1, 1, 1, 1, 1, 3, 1, - 1, 3, 1, 3, 1, 1, 1, 3, 3, 3, - 3, 3, 3, 3, 3, 2, 3, 4, 5, 4, - 1, 1, 1, 1, 1, 1, 5, 0, 1, 1, - 2, 4, 0, 2, 1, 3, 1, 1, 1, 1, - 0, 3, 0, 2, 0, 3, 1, 3, 2, 0, - 1, 1, 0, 2, 4, 0, 2, 4, 0, 3, - 1, 3, 0, 5, 2, 1, 1, 3, 3, 1, - 1, 3, 3, 0, 2, 0, 3, 0, 1, 1, - 1, 1, 1, 1, 0, 1, 0, 1, 0, 2, - 1, 0, + 2, 6, 1, 1, 1, 1, 1, 1, 1, 3, + 1, 1, 3, 1, 3, 1, 1, 1, 3, 3, + 3, 3, 3, 3, 3, 3, 2, 3, 4, 5, + 4, 1, 1, 1, 1, 1, 1, 5, 0, 1, + 1, 2, 4, 0, 2, 1, 3, 1, 1, 1, + 1, 0, 3, 0, 2, 0, 3, 1, 3, 2, + 0, 1, 1, 0, 2, 4, 0, 2, 4, 0, + 3, 1, 3, 0, 5, 2, 1, 1, 3, 3, + 1, 1, 3, 3, 0, 2, 0, 3, 0, 1, + 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, + 2, 1, 0, } var yyChk = []int{ -1000, -1, -2, -3, -4, -5, -6, -7, -8, -9, - -10, -11, -12, -13, 5, 6, 7, 8, 33, 84, - 85, 87, 86, 88, 97, 98, 99, -16, 50, 51, - 52, 53, -14, -72, -14, -14, -14, -14, 89, -68, - 91, 95, -65, 91, 93, 89, 89, 90, 91, 89, + -10, -11, -12, -13, 5, 6, 7, 8, 33, 85, + 86, 88, 87, 89, 98, 99, 100, -16, 51, 52, + 53, 54, -14, -72, -14, -14, -14, -14, 90, -68, + 92, 96, -65, 92, 94, 90, 90, 91, 92, 90, -71, -71, -71, -3, 17, -17, 18, -15, 29, -27, - 35, 9, -61, -62, -44, -70, 35, -64, 94, 90, - -70, 35, 89, -70, 35, -63, 94, -70, -63, 35, - -18, -19, 74, -22, 35, -31, -36, -32, 68, 45, - -35, -44, -40, -43, -70, -41, -45, 20, 36, 37, - 38, 25, -42, 72, 73, 49, 94, 28, 79, 40, - -27, 33, 77, -27, 54, 46, 77, 35, 68, -70, - -71, 35, -71, 92, 35, 20, 65, -70, 9, 54, - -20, -70, 19, 77, 67, 66, -33, 21, 68, 23, - 24, 22, 69, 70, 71, 72, 73, 74, 75, 76, - 46, 47, 48, 41, 42, 43, 44, -31, -36, -31, - -3, -38, -36, -36, 45, 45, 45, -42, 45, -48, - -36, -58, 33, 45, -61, 35, -30, 10, -62, -36, - -70, -71, 20, -69, 96, -66, 87, 85, 32, 86, - 13, 35, 35, 35, -71, -23, -24, -26, 45, 35, - -42, -19, -70, 74, -31, -31, -36, -37, 45, -42, - 39, 21, 23, 24, -36, -36, 25, 68, -36, -36, - -36, -36, -36, -36, -36, -36, 100, 100, 54, 100, - -36, 100, -18, 18, -18, -46, -47, 80, -34, 28, - -3, -61, -59, -44, -30, -52, 13, -31, 65, -70, - -71, -67, 92, -30, 54, -25, 55, 56, 57, 58, - 59, 61, 62, -21, 35, 19, -24, 77, -38, -37, - -36, -36, 67, 25, -36, 100, -18, 100, -49, -47, - 82, -31, -60, 65, -39, -40, -60, 100, 54, -52, - -56, 15, 14, 35, 35, -50, 11, -24, -24, 55, - 60, 55, 60, 55, 55, 55, -28, 63, 93, 64, - 35, 100, 35, 100, 67, -36, 100, 83, -36, 81, - 30, 54, -44, -56, -36, -53, -54, -36, -71, -51, - 12, 14, 65, 55, 55, 90, 90, 90, -36, -36, - 31, -40, 54, 54, -55, 26, 27, -52, -31, -38, - -31, 45, 45, 45, 7, -36, -54, -56, -29, -70, - -29, -29, -61, -57, 16, 34, 100, 54, 100, 100, - 7, 21, -70, -70, -70, + 36, 9, -61, -62, -44, -70, 36, -64, 95, 91, + -70, 36, 90, -70, 36, -63, 95, -70, -63, 36, + -18, -19, 75, -22, 36, -31, -36, -32, 69, 46, + -35, -44, -40, -43, -70, -41, -45, 20, 35, 37, + 38, 39, 25, -42, 73, 74, 50, 95, 28, 80, + 41, -27, 33, 78, -27, 55, 47, 78, 36, 69, + -70, -71, 36, -71, 93, 36, 20, 66, -70, 9, + 55, -20, -70, 19, 78, 68, 67, -33, 21, 69, + 23, 24, 22, 70, 71, 72, 73, 74, 75, 76, + 77, 47, 48, 49, 42, 43, 44, 45, -31, -36, + -31, -3, -38, -36, -36, 46, 46, 46, -42, 46, + 46, -48, -36, -58, 33, 46, -61, 36, -30, 10, + -62, -36, -70, -71, 20, -69, 97, -66, 88, 86, + 32, 87, 13, 36, 36, 36, -71, -23, -24, -26, + 46, 36, -42, -19, -70, 75, -31, -31, -36, -37, + 46, -42, 40, 21, 23, 24, -36, -36, 25, 69, + -36, -36, -36, -36, -36, -36, -36, -36, 101, 101, + 55, 101, -36, 101, -18, 18, -18, -35, -46, -47, + 81, -34, 28, -3, -61, -59, -44, -30, -52, 13, + -31, 66, -70, -71, -67, 93, -30, 55, -25, 56, + 57, 58, 59, 60, 62, 63, -21, 36, 19, -24, + 78, -38, -37, -36, -36, 68, 25, -36, 101, -18, + 101, 55, -49, -47, 83, -31, -60, 66, -39, -40, + -60, 101, 55, -52, -56, 15, 14, 36, 36, -50, + 11, -24, -24, 56, 61, 56, 61, 56, 56, 56, + -28, 64, 94, 65, 36, 101, 36, 101, 68, -36, + 101, -35, 84, -36, 82, 30, 55, -44, -56, -36, + -53, -54, -36, -71, -51, 12, 14, 66, 56, 56, + 91, 91, 91, -36, 101, -36, 31, -40, 55, 55, + -55, 26, 27, -52, -31, -38, -31, 46, 46, 46, + 7, -36, -54, -56, -29, -70, -29, -29, -61, -57, + 16, 34, 101, 55, 101, 101, 7, 21, -70, -70, + -70, } var yyDef = []int{ 0, -2, 1, 2, 3, 4, 5, 6, 7, 8, - 9, 10, 11, 12, 34, 34, 34, 34, 34, 196, - 187, 0, 0, 0, 201, 201, 201, 0, 38, 40, - 41, 42, 43, 36, 0, 0, 0, 0, 185, 0, - 0, 197, 0, 0, 188, 0, 183, 0, 183, 0, + 9, 10, 11, 12, 34, 34, 34, 34, 34, 197, + 188, 0, 0, 0, 202, 202, 202, 0, 38, 40, + 41, 42, 43, 36, 0, 0, 0, 0, 186, 0, + 0, 198, 0, 0, 189, 0, 184, 0, 184, 0, 31, 32, 33, 14, 39, 0, 44, 35, 0, 0, - 76, 0, 19, 180, 0, 144, 200, 0, 0, 0, - 201, 200, 0, 201, 0, 0, 0, 0, 0, 30, - 0, 45, 47, 52, 200, 50, 51, 86, 0, 0, - 114, 115, 116, 0, 144, 0, 130, 0, 146, 147, - 148, 149, 179, 133, 134, 135, 131, 132, 137, 37, - 168, 0, 0, 84, 0, 0, 0, 201, 0, 198, - 22, 0, 25, 0, 27, 184, 0, 201, 0, 0, - 48, 53, 0, 0, 0, 0, 0, 0, 0, 0, + 76, 0, 19, 181, 0, 145, 201, 0, 0, 0, + 202, 201, 0, 202, 0, 0, 0, 0, 0, 30, + 0, 45, 47, 52, 201, 50, 51, 86, 0, 0, + 115, 116, 117, 0, 145, 0, 131, 0, 0, 147, + 148, 149, 150, 180, 134, 135, 136, 132, 133, 138, + 37, 169, 0, 0, 84, 0, 0, 0, 202, 0, + 199, 22, 0, 25, 0, 27, 185, 0, 202, 0, + 0, 48, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 101, 102, 103, 104, 105, 106, 107, 89, 0, 0, - 0, 0, 112, 125, 0, 0, 0, 100, 0, 0, - 138, 0, 0, 0, 84, 77, 154, 0, 181, 182, - 145, 20, 186, 0, 0, 201, 194, 189, 190, 191, - 192, 193, 26, 28, 29, 84, 55, 61, 0, 73, - 75, 46, 54, 49, 87, 88, 91, 92, 0, 109, - 110, 0, 0, 0, 94, 0, 98, 0, 117, 118, - 119, 120, 121, 122, 123, 124, 90, 111, 0, 178, - 112, 126, 0, 0, 0, 142, 139, 0, 172, 0, - 175, 172, 0, 170, 154, 162, 0, 85, 0, 199, - 23, 0, 195, 150, 0, 0, 64, 65, 0, 0, - 0, 0, 0, 78, 62, 0, 0, 0, 0, 93, - 95, 0, 0, 99, 113, 127, 0, 129, 0, 140, - 0, 0, 15, 0, 174, 176, 16, 169, 0, 162, - 18, 0, 0, 201, 24, 152, 0, 56, 59, 66, - 0, 68, 0, 70, 71, 72, 57, 0, 0, 0, - 63, 58, 74, 108, 0, 96, 128, 136, 143, 0, - 0, 0, 171, 17, 163, 155, 156, 159, 21, 154, - 0, 0, 0, 67, 69, 0, 0, 0, 97, 141, - 0, 177, 0, 0, 158, 160, 161, 162, 153, 151, - 60, 0, 0, 0, 0, 164, 157, 165, 0, 82, - 0, 0, 173, 13, 0, 0, 79, 0, 80, 81, - 166, 0, 83, 0, 167, + 0, 102, 103, 104, 105, 106, 107, 108, 89, 0, + 0, 0, 0, 113, 126, 0, 0, 0, 100, 0, + 0, 0, 139, 0, 0, 0, 84, 77, 155, 0, + 182, 183, 146, 20, 187, 0, 0, 202, 195, 190, + 191, 192, 193, 194, 26, 28, 29, 84, 55, 61, + 0, 73, 75, 46, 54, 49, 87, 88, 91, 92, + 0, 110, 111, 0, 0, 0, 94, 0, 98, 0, + 118, 119, 120, 121, 122, 123, 124, 125, 90, 112, + 0, 179, 113, 127, 0, 0, 0, 0, 143, 140, + 0, 173, 0, 176, 173, 0, 171, 155, 163, 0, + 85, 0, 200, 23, 0, 196, 151, 0, 0, 64, + 65, 0, 0, 0, 0, 0, 78, 62, 0, 0, + 0, 0, 93, 95, 0, 0, 99, 114, 128, 0, + 130, 0, 0, 141, 0, 0, 15, 0, 175, 177, + 16, 170, 0, 163, 18, 0, 0, 202, 24, 153, + 0, 56, 59, 66, 0, 68, 0, 70, 71, 72, + 57, 0, 0, 0, 63, 58, 74, 109, 0, 96, + 129, 0, 137, 144, 0, 0, 0, 172, 17, 164, + 156, 157, 160, 21, 155, 0, 0, 0, 67, 69, + 0, 0, 0, 97, 101, 142, 0, 178, 0, 0, + 159, 161, 162, 163, 154, 152, 60, 0, 0, 0, + 0, 165, 158, 166, 0, 82, 0, 0, 174, 13, + 0, 0, 79, 0, 80, 81, 167, 0, 83, 0, + 168, } var yyTok1 = []int{ 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 76, 69, 3, - 45, 100, 74, 72, 54, 73, 77, 75, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 77, 70, 3, + 46, 101, 75, 73, 55, 74, 78, 76, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 47, 46, 48, 3, 3, 3, 3, 3, 3, 3, + 48, 47, 49, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 71, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 72, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 70, 3, 49, + 3, 3, 3, 3, 71, 3, 50, } var yyTok2 = []int{ @@ -532,11 +542,11 @@ var yyTok2 = []int{ 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 42, 43, 44, 50, 51, 52, 53, 55, 56, 57, + 42, 43, 44, 45, 51, 52, 53, 54, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, - 68, 78, 79, 80, 81, 82, 83, 84, 85, 86, + 68, 69, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, - 97, 98, 99, + 97, 98, 99, 100, } var yyTok3 = []int{ 0, @@ -1245,127 +1255,132 @@ yydefault: yyVAL.boolExpr = &ExistsExpr{Subquery: yyS[yypt-0].subquery} } case 101: - //line sql.y:585 + //line sql.y:583 { - yyVAL.str = AST_EQ + yyVAL.boolExpr = &KeyrangeExpr{Start: yyS[yypt-3].valExpr, End: yyS[yypt-1].valExpr} } case 102: //line sql.y:589 { - yyVAL.str = AST_LT + yyVAL.str = AST_EQ } case 103: //line sql.y:593 { - yyVAL.str = AST_GT + yyVAL.str = AST_LT } case 104: //line sql.y:597 { - yyVAL.str = AST_LE + yyVAL.str = AST_GT } case 105: //line sql.y:601 { - yyVAL.str = AST_GE + yyVAL.str = AST_LE } case 106: //line sql.y:605 { - yyVAL.str = AST_NE + yyVAL.str = AST_GE } case 107: //line sql.y:609 { - yyVAL.str = AST_NSE + yyVAL.str = AST_NE } case 108: - //line sql.y:615 + //line sql.y:613 { - yyVAL.colTuple = ValTuple(yyS[yypt-1].valExprs) + yyVAL.str = AST_NSE } case 109: //line sql.y:619 { - yyVAL.colTuple = yyS[yypt-0].subquery + yyVAL.colTuple = ValTuple(yyS[yypt-1].valExprs) } case 110: //line sql.y:623 { - yyVAL.colTuple = ListArg(yyS[yypt-0].bytes) + yyVAL.colTuple = yyS[yypt-0].subquery } case 111: - //line sql.y:629 + //line sql.y:627 { - yyVAL.subquery = &Subquery{yyS[yypt-1].selStmt} + yyVAL.colTuple = ListArg(yyS[yypt-0].bytes) } case 112: - //line sql.y:635 + //line sql.y:633 { - yyVAL.valExprs = ValExprs{yyS[yypt-0].valExpr} + yyVAL.subquery = &Subquery{yyS[yypt-1].selStmt} } case 113: //line sql.y:639 { - yyVAL.valExprs = append(yyS[yypt-2].valExprs, yyS[yypt-0].valExpr) + yyVAL.valExprs = ValExprs{yyS[yypt-0].valExpr} } case 114: - //line sql.y:645 + //line sql.y:643 { - yyVAL.valExpr = yyS[yypt-0].valExpr + yyVAL.valExprs = append(yyS[yypt-2].valExprs, yyS[yypt-0].valExpr) } case 115: //line sql.y:649 { - yyVAL.valExpr = yyS[yypt-0].colName + yyVAL.valExpr = yyS[yypt-0].valExpr } case 116: //line sql.y:653 { - yyVAL.valExpr = yyS[yypt-0].rowTuple + yyVAL.valExpr = yyS[yypt-0].colName } case 117: //line sql.y:657 { - yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_BITAND, Right: yyS[yypt-0].valExpr} + yyVAL.valExpr = yyS[yypt-0].rowTuple } case 118: //line sql.y:661 { - yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_BITOR, Right: yyS[yypt-0].valExpr} + yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_BITAND, Right: yyS[yypt-0].valExpr} } case 119: //line sql.y:665 { - yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_BITXOR, Right: yyS[yypt-0].valExpr} + yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_BITOR, Right: yyS[yypt-0].valExpr} } case 120: //line sql.y:669 { - yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_PLUS, Right: yyS[yypt-0].valExpr} + yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_BITXOR, Right: yyS[yypt-0].valExpr} } case 121: //line sql.y:673 { - yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_MINUS, Right: yyS[yypt-0].valExpr} + yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_PLUS, Right: yyS[yypt-0].valExpr} } case 122: //line sql.y:677 { - yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_MULT, Right: yyS[yypt-0].valExpr} + yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_MINUS, Right: yyS[yypt-0].valExpr} } case 123: //line sql.y:681 { - yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_DIV, Right: yyS[yypt-0].valExpr} + yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_MULT, Right: yyS[yypt-0].valExpr} } case 124: //line sql.y:685 { - yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_MOD, Right: yyS[yypt-0].valExpr} + yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_DIV, Right: yyS[yypt-0].valExpr} } case 125: //line sql.y:689 + { + yyVAL.valExpr = &BinaryExpr{Left: yyS[yypt-2].valExpr, Operator: AST_MOD, Right: yyS[yypt-0].valExpr} + } + case 126: + //line sql.y:693 { if num, ok := yyS[yypt-0].valExpr.(NumVal); ok { switch yyS[yypt-1].byt { @@ -1380,175 +1395,170 @@ yydefault: yyVAL.valExpr = &UnaryExpr{Operator: yyS[yypt-1].byt, Expr: yyS[yypt-0].valExpr} } } - case 126: - //line sql.y:704 - { - yyVAL.valExpr = &FuncExpr{Name: yyS[yypt-2].bytes} - } case 127: //line sql.y:708 { - yyVAL.valExpr = &FuncExpr{Name: yyS[yypt-3].bytes, Exprs: yyS[yypt-1].selectExprs} + yyVAL.valExpr = &FuncExpr{Name: yyS[yypt-2].bytes} } case 128: //line sql.y:712 { - yyVAL.valExpr = &FuncExpr{Name: yyS[yypt-4].bytes, Distinct: true, Exprs: yyS[yypt-1].selectExprs} + yyVAL.valExpr = &FuncExpr{Name: yyS[yypt-3].bytes, Exprs: yyS[yypt-1].selectExprs} } case 129: //line sql.y:716 { - yyVAL.valExpr = &FuncExpr{Name: yyS[yypt-3].bytes, Exprs: yyS[yypt-1].selectExprs} + yyVAL.valExpr = &FuncExpr{Name: yyS[yypt-4].bytes, Distinct: true, Exprs: yyS[yypt-1].selectExprs} } case 130: //line sql.y:720 { - yyVAL.valExpr = yyS[yypt-0].caseExpr + yyVAL.valExpr = &FuncExpr{Name: yyS[yypt-3].bytes, Exprs: yyS[yypt-1].selectExprs} } case 131: - //line sql.y:726 + //line sql.y:724 { - yyVAL.bytes = IF_BYTES + yyVAL.valExpr = yyS[yypt-0].caseExpr } case 132: //line sql.y:730 { - yyVAL.bytes = VALUES_BYTES + yyVAL.bytes = IF_BYTES } case 133: - //line sql.y:736 + //line sql.y:734 { - yyVAL.byt = AST_UPLUS + yyVAL.bytes = VALUES_BYTES } case 134: //line sql.y:740 { - yyVAL.byt = AST_UMINUS + yyVAL.byt = AST_UPLUS } case 135: //line sql.y:744 { - yyVAL.byt = AST_TILDA + yyVAL.byt = AST_UMINUS } case 136: - //line sql.y:750 + //line sql.y:748 { - yyVAL.caseExpr = &CaseExpr{Expr: yyS[yypt-3].valExpr, Whens: yyS[yypt-2].whens, Else: yyS[yypt-1].valExpr} + yyVAL.byt = AST_TILDA } case 137: - //line sql.y:755 + //line sql.y:754 { - yyVAL.valExpr = nil + yyVAL.caseExpr = &CaseExpr{Expr: yyS[yypt-3].valExpr, Whens: yyS[yypt-2].whens, Else: yyS[yypt-1].valExpr} } case 138: //line sql.y:759 { - yyVAL.valExpr = yyS[yypt-0].valExpr + yyVAL.valExpr = nil } case 139: - //line sql.y:765 + //line sql.y:763 { - yyVAL.whens = []*When{yyS[yypt-0].when} + yyVAL.valExpr = yyS[yypt-0].valExpr } case 140: //line sql.y:769 { - yyVAL.whens = append(yyS[yypt-1].whens, yyS[yypt-0].when) + yyVAL.whens = []*When{yyS[yypt-0].when} } case 141: - //line sql.y:775 + //line sql.y:773 { - yyVAL.when = &When{Cond: yyS[yypt-2].boolExpr, Val: yyS[yypt-0].valExpr} + yyVAL.whens = append(yyS[yypt-1].whens, yyS[yypt-0].when) } case 142: - //line sql.y:780 + //line sql.y:779 { - yyVAL.valExpr = nil + yyVAL.when = &When{Cond: yyS[yypt-2].boolExpr, Val: yyS[yypt-0].valExpr} } case 143: //line sql.y:784 { - yyVAL.valExpr = yyS[yypt-0].valExpr + yyVAL.valExpr = nil } case 144: - //line sql.y:790 + //line sql.y:788 { - yyVAL.colName = &ColName{Name: yyS[yypt-0].bytes} + yyVAL.valExpr = yyS[yypt-0].valExpr } case 145: //line sql.y:794 { - yyVAL.colName = &ColName{Qualifier: yyS[yypt-2].bytes, Name: yyS[yypt-0].bytes} + yyVAL.colName = &ColName{Name: yyS[yypt-0].bytes} } case 146: - //line sql.y:800 + //line sql.y:798 { - yyVAL.valExpr = StrVal(yyS[yypt-0].bytes) + yyVAL.colName = &ColName{Qualifier: yyS[yypt-2].bytes, Name: yyS[yypt-0].bytes} } case 147: //line sql.y:804 { - yyVAL.valExpr = NumVal(yyS[yypt-0].bytes) + yyVAL.valExpr = StrVal(yyS[yypt-0].bytes) } case 148: //line sql.y:808 { - yyVAL.valExpr = ValArg(yyS[yypt-0].bytes) + yyVAL.valExpr = NumVal(yyS[yypt-0].bytes) } case 149: //line sql.y:812 { - yyVAL.valExpr = &NullVal{} + yyVAL.valExpr = ValArg(yyS[yypt-0].bytes) } case 150: - //line sql.y:817 + //line sql.y:816 { - yyVAL.valExprs = nil + yyVAL.valExpr = &NullVal{} } case 151: //line sql.y:821 { - yyVAL.valExprs = yyS[yypt-0].valExprs + yyVAL.valExprs = nil } case 152: - //line sql.y:826 + //line sql.y:825 { - yyVAL.boolExpr = nil + yyVAL.valExprs = yyS[yypt-0].valExprs } case 153: //line sql.y:830 { - yyVAL.boolExpr = yyS[yypt-0].boolExpr + yyVAL.boolExpr = nil } case 154: - //line sql.y:835 + //line sql.y:834 { - yyVAL.orderBy = nil + yyVAL.boolExpr = yyS[yypt-0].boolExpr } case 155: //line sql.y:839 { - yyVAL.orderBy = yyS[yypt-0].orderBy + yyVAL.orderBy = nil } case 156: - //line sql.y:845 + //line sql.y:843 { - yyVAL.orderBy = OrderBy{yyS[yypt-0].order} + yyVAL.orderBy = yyS[yypt-0].orderBy } case 157: //line sql.y:849 { - yyVAL.orderBy = append(yyS[yypt-2].orderBy, yyS[yypt-0].order) + yyVAL.orderBy = OrderBy{yyS[yypt-0].order} } case 158: - //line sql.y:855 + //line sql.y:853 { - yyVAL.order = &Order{Expr: yyS[yypt-1].valExpr, Direction: yyS[yypt-0].str} + yyVAL.orderBy = append(yyS[yypt-2].orderBy, yyS[yypt-0].order) } case 159: - //line sql.y:860 + //line sql.y:859 { - yyVAL.str = AST_ASC + yyVAL.order = &Order{Expr: yyS[yypt-1].valExpr, Direction: yyS[yypt-0].str} } case 160: //line sql.y:864 @@ -1558,35 +1568,40 @@ yydefault: case 161: //line sql.y:868 { - yyVAL.str = AST_DESC + yyVAL.str = AST_ASC } case 162: - //line sql.y:873 + //line sql.y:872 { - yyVAL.limit = nil + yyVAL.str = AST_DESC } case 163: //line sql.y:877 { - yyVAL.limit = &Limit{Rowcount: yyS[yypt-0].valExpr} + yyVAL.limit = nil } case 164: //line sql.y:881 { - yyVAL.limit = &Limit{Offset: yyS[yypt-2].valExpr, Rowcount: yyS[yypt-0].valExpr} + yyVAL.limit = &Limit{Rowcount: yyS[yypt-0].valExpr} } case 165: - //line sql.y:886 + //line sql.y:885 { - yyVAL.str = "" + yyVAL.limit = &Limit{Offset: yyS[yypt-2].valExpr, Rowcount: yyS[yypt-0].valExpr} } case 166: //line sql.y:890 { - yyVAL.str = AST_FOR_UPDATE + yyVAL.str = "" } case 167: //line sql.y:894 + { + yyVAL.str = AST_FOR_UPDATE + } + case 168: + //line sql.y:898 { if !bytes.Equal(yyS[yypt-1].bytes, SHARE) { yylex.Error("expecting share") @@ -1598,108 +1613,103 @@ yydefault: } yyVAL.str = AST_SHARE_MODE } - case 168: - //line sql.y:907 - { - yyVAL.columns = nil - } case 169: //line sql.y:911 { - yyVAL.columns = yyS[yypt-1].columns + yyVAL.columns = nil } case 170: - //line sql.y:917 + //line sql.y:915 { - yyVAL.columns = Columns{&NonStarExpr{Expr: yyS[yypt-0].colName}} + yyVAL.columns = yyS[yypt-1].columns } case 171: //line sql.y:921 { - yyVAL.columns = append(yyVAL.columns, &NonStarExpr{Expr: yyS[yypt-0].colName}) + yyVAL.columns = Columns{&NonStarExpr{Expr: yyS[yypt-0].colName}} } case 172: - //line sql.y:926 + //line sql.y:925 { - yyVAL.updateExprs = nil + yyVAL.columns = append(yyVAL.columns, &NonStarExpr{Expr: yyS[yypt-0].colName}) } case 173: //line sql.y:930 { - yyVAL.updateExprs = yyS[yypt-0].updateExprs + yyVAL.updateExprs = nil } case 174: - //line sql.y:936 + //line sql.y:934 { - yyVAL.insRows = yyS[yypt-0].values + yyVAL.updateExprs = yyS[yypt-0].updateExprs } case 175: //line sql.y:940 { - yyVAL.insRows = yyS[yypt-0].selStmt + yyVAL.insRows = yyS[yypt-0].values } case 176: - //line sql.y:946 + //line sql.y:944 { - yyVAL.values = Values{yyS[yypt-0].rowTuple} + yyVAL.insRows = yyS[yypt-0].selStmt } case 177: //line sql.y:950 { - yyVAL.values = append(yyS[yypt-2].values, yyS[yypt-0].rowTuple) + yyVAL.values = Values{yyS[yypt-0].rowTuple} } case 178: - //line sql.y:956 + //line sql.y:954 { - yyVAL.rowTuple = ValTuple(yyS[yypt-1].valExprs) + yyVAL.values = append(yyS[yypt-2].values, yyS[yypt-0].rowTuple) } case 179: //line sql.y:960 { - yyVAL.rowTuple = yyS[yypt-0].subquery + yyVAL.rowTuple = ValTuple(yyS[yypt-1].valExprs) } case 180: - //line sql.y:966 + //line sql.y:964 { - yyVAL.updateExprs = UpdateExprs{yyS[yypt-0].updateExpr} + yyVAL.rowTuple = yyS[yypt-0].subquery } case 181: //line sql.y:970 { - yyVAL.updateExprs = append(yyS[yypt-2].updateExprs, yyS[yypt-0].updateExpr) + yyVAL.updateExprs = UpdateExprs{yyS[yypt-0].updateExpr} } case 182: - //line sql.y:976 + //line sql.y:974 { - yyVAL.updateExpr = &UpdateExpr{Name: yyS[yypt-2].colName, Expr: yyS[yypt-0].valExpr} + yyVAL.updateExprs = append(yyS[yypt-2].updateExprs, yyS[yypt-0].updateExpr) } case 183: - //line sql.y:981 + //line sql.y:980 { - yyVAL.empty = struct{}{} + yyVAL.updateExpr = &UpdateExpr{Name: yyS[yypt-2].colName, Expr: yyS[yypt-0].valExpr} } case 184: - //line sql.y:983 + //line sql.y:985 { yyVAL.empty = struct{}{} } case 185: - //line sql.y:986 + //line sql.y:987 { yyVAL.empty = struct{}{} } case 186: - //line sql.y:988 + //line sql.y:990 { yyVAL.empty = struct{}{} } case 187: - //line sql.y:991 + //line sql.y:992 { yyVAL.empty = struct{}{} } case 188: - //line sql.y:993 + //line sql.y:995 { yyVAL.empty = struct{}{} } @@ -1709,62 +1719,67 @@ yydefault: yyVAL.empty = struct{}{} } case 190: - //line sql.y:999 + //line sql.y:1001 { yyVAL.empty = struct{}{} } case 191: - //line sql.y:1001 + //line sql.y:1003 { yyVAL.empty = struct{}{} } case 192: - //line sql.y:1003 + //line sql.y:1005 { yyVAL.empty = struct{}{} } case 193: - //line sql.y:1005 + //line sql.y:1007 { yyVAL.empty = struct{}{} } case 194: - //line sql.y:1008 + //line sql.y:1009 { yyVAL.empty = struct{}{} } case 195: - //line sql.y:1010 + //line sql.y:1012 { yyVAL.empty = struct{}{} } case 196: - //line sql.y:1013 + //line sql.y:1014 { yyVAL.empty = struct{}{} } case 197: - //line sql.y:1015 + //line sql.y:1017 { yyVAL.empty = struct{}{} } case 198: - //line sql.y:1018 + //line sql.y:1019 { yyVAL.empty = struct{}{} } case 199: - //line sql.y:1020 + //line sql.y:1022 { yyVAL.empty = struct{}{} } case 200: //line sql.y:1024 { - yyVAL.bytes = bytes.ToLower(yyS[yypt-0].bytes) + yyVAL.empty = struct{}{} } case 201: - //line sql.y:1029 + //line sql.y:1028 + { + yyVAL.bytes = bytes.ToLower(yyS[yypt-0].bytes) + } + case 202: + //line sql.y:1033 { ForceEOF(yylex) } diff --git a/go/vt/sqlparser/sql.y b/go/vt/sqlparser/sql.y index 8c609ca6ec6..47863ba940a 100644 --- a/go/vt/sqlparser/sql.y +++ b/go/vt/sqlparser/sql.y @@ -66,7 +66,7 @@ var ( %token LEX_ERROR %token SELECT INSERT UPDATE DELETE FROM WHERE GROUP HAVING ORDER BY LIMIT FOR -%token ALL DISTINCT AS EXISTS IN IS LIKE BETWEEN NULL ASC DESC VALUES INTO DUPLICATE KEY DEFAULT SET LOCK +%token ALL DISTINCT AS EXISTS IN IS LIKE BETWEEN NULL ASC DESC VALUES INTO DUPLICATE KEY DEFAULT SET LOCK KEYRANGE %token ID STRING NUMBER VALUE_ARG LIST_ARG COMMENT %token LE GE NE NULL_SAFE_EQUAL %token '(' '=' '<' '>' '~' @@ -579,6 +579,10 @@ condition: { $$ = &ExistsExpr{Subquery: $2} } +| KEYRANGE '(' value ',' value ')' + { + $$ = &KeyrangeExpr{Start: $3, End: $5} + } compare: '=' diff --git a/go/vt/sqlparser/token.go b/go/vt/sqlparser/token.go index dc4340340de..c9f8dd4998e 100644 --- a/go/vt/sqlparser/token.go +++ b/go/vt/sqlparser/token.go @@ -74,6 +74,7 @@ var keywords = map[string]int{ "is": IS, "join": JOIN, "key": KEY, + "keyrange": KEYRANGE, "left": LEFT, "like": LIKE, "limit": LIMIT, diff --git a/go/vt/status/status.go b/go/vt/status/status.go index d3d08531dc9..f60b94f9915 100644 --- a/go/vt/status/status.go +++ b/go/vt/status/status.go @@ -18,8 +18,8 @@ import ( ) var ( - vtctldAddr = flag.String("vtctld_addr", "", "address of a vtctld instance") - vtctldTopoExplorer = flag.String("vtctld_topo_explorer", "zk", "topo explorer to be used in status links") + vtctldAddr = flag.String("vtctld_addr", "", "address of a vtctld instance") + _ = flag.String("vtctld_topo_explorer", "", "this flag is no longer used") ) // MakeVtctldRedirect returns an absolute vtctld url that will @@ -56,7 +56,6 @@ func VtctldKeyspace(keyspace string) template.HTML { return MakeVtctldRedirect(keyspace, map[string]string{ "type": "keyspace", - "explorer": *vtctldTopoExplorer, "keyspace": keyspace, }) } @@ -66,7 +65,6 @@ func VtctldKeyspace(keyspace string) template.HTML { func VtctldShard(keyspace, shard string) template.HTML { return MakeVtctldRedirect(shard, map[string]string{ "type": "shard", - "explorer": *vtctldTopoExplorer, "keyspace": keyspace, "shard": shard, }) @@ -83,7 +81,6 @@ func VtctldSrvCell(cell string) template.HTML { func VtctldSrvKeyspace(cell, keyspace string) template.HTML { return MakeVtctldRedirect(keyspace, map[string]string{ "type": "srv_keyspace", - "explorer": *vtctldTopoExplorer, "cell": cell, "keyspace": keyspace, }) @@ -94,7 +91,6 @@ func VtctldSrvKeyspace(cell, keyspace string) template.HTML { func VtctldSrvShard(cell, keyspace, shard string) template.HTML { return MakeVtctldRedirect(shard, map[string]string{ "type": "srv_shard", - "explorer": *vtctldTopoExplorer, "cell": cell, "keyspace": keyspace, "shard": shard, @@ -104,18 +100,16 @@ func VtctldSrvShard(cell, keyspace, shard string) template.HTML { // VtctldSrvType returns the tablet type, possibly linked to the // EndPoints page in vtctld. func VtctldSrvType(cell, keyspace, shard string, tabletType topo.TabletType) template.HTML { - if topo.IsInServingGraph(tabletType) { - return MakeVtctldRedirect(string(tabletType), map[string]string{ - "type": "srv_type", - "explorer": *vtctldTopoExplorer, - "cell": cell, - "keyspace": keyspace, - "shard": shard, - "tablet_type": string(tabletType), - }) - } else { + if !topo.IsInServingGraph(tabletType) { return template.HTML(tabletType) } + return MakeVtctldRedirect(string(tabletType), map[string]string{ + "type": "srv_type", + "cell": cell, + "keyspace": keyspace, + "shard": shard, + "tablet_type": string(tabletType), + }) } // VtctldReplication returns 'cell/keyspace/shard', possibly linked to the @@ -124,7 +118,6 @@ func VtctldReplication(cell, keyspace, shard string) template.HTML { return MakeVtctldRedirect(fmt.Sprintf("%v/%v/%v", cell, keyspace, shard), map[string]string{ "type": "replication", - "explorer": *vtctldTopoExplorer, "keyspace": keyspace, "shard": shard, "cell": cell, @@ -135,9 +128,8 @@ func VtctldReplication(cell, keyspace, shard string) template.HTML { // Tablet page in vtctld. func VtctldTablet(aliasName string) template.HTML { return MakeVtctldRedirect(aliasName, map[string]string{ - "type": "tablet", - "explorer": *vtctldTopoExplorer, - "alias": aliasName, + "type": "tablet", + "alias": aliasName, }) } diff --git a/go/vt/tableacl/tableacl.go b/go/vt/tableacl/tableacl.go index 7e7bdbf9a6a..4f18aaa3adc 100644 --- a/go/vt/tableacl/tableacl.go +++ b/go/vt/tableacl/tableacl.go @@ -79,9 +79,9 @@ func load(config []byte) (map[*regexp.Regexp]map[Role]ACL, error) { // Authorized returns the list of entities who have at least the // minimum specified Role on a table func Authorized(table string, minRole Role) ACL { + // If table ACL is disabled, return nil if tableAcl == nil { - // No ACLs, allow all access - return all() + return nil } for re, accessMap := range tableAcl { if !re.MatchString(table) { diff --git a/go/vt/tableacl/tableacl_test.go b/go/vt/tableacl/tableacl_test.go index 3b02b503bce..83bd46da742 100644 --- a/go/vt/tableacl/tableacl_test.go +++ b/go/vt/tableacl/tableacl_test.go @@ -56,7 +56,7 @@ func TestAllowUnmatchedTable(t *testing.T) { checkAccess(configData, "UNMATCHED_TABLE", ADMIN, t, true) } -func TestAllUserReadAcess(t *testing.T) { +func TestAllUserReadAccess(t *testing.T) { configData := []byte(`{"table[0-9]+":{"Reader":"` + ALL + `", "WRITER":"u3"}}`) checkAccess(configData, "table1", READER, t, true) } @@ -66,6 +66,14 @@ func TestAllUserWriteAccess(t *testing.T) { checkAccess(configData, "table1", WRITER, t, true) } +func TestDisabled(t *testing.T) { + tableAcl = nil + got := Authorized("table1", READER) + if got != nil { + t.Errorf("table acl disabled, got: %v, want: nil", got) + } +} + func checkLoad(configData []byte, valid bool, t *testing.T) { var err error tableAcl, err = load(configData) diff --git a/go/vt/tabletmanager/actionnode/actionnode.go b/go/vt/tabletmanager/actionnode/actionnode.go index d7a20feaff4..2baa770c110 100644 --- a/go/vt/tabletmanager/actionnode/actionnode.go +++ b/go/vt/tabletmanager/actionnode/actionnode.go @@ -4,7 +4,7 @@ // Actions modify the state of a tablet, shard or keyspace. // -// They are currenty managed through a series of queues stored in +// They are currently managed through a series of queues stored in // topology server, or RPCs. Switching to RPCs only now. package actionnode @@ -123,6 +123,9 @@ const ( // the topo server. TABLET_ACTION_RUN_HEALTH_CHECK = "RunHealthCheck" + // HealthStream will stream the health status + TABLET_ACTION_HEALTH_STREAM = "HealthStream" + // ReloadSchema tells the tablet to reload its schema. TABLET_ACTION_RELOAD_SCHEMA = "ReloadSchema" @@ -153,12 +156,6 @@ const ( // Restore will restore a backup TABLET_ACTION_RESTORE = "Restore" - // MultiSnapshot takes a split snapshot - TABLET_ACTION_MULTI_SNAPSHOT = "MultiSnapshot" - - // MultiRestore restores a split snapshot - TABLET_ACTION_MULTI_RESTORE = "MultiRestore" - // // Shard actions - involve all tablets in a shard. // These are just descriptive and used for locking / logging. @@ -174,8 +171,6 @@ const ( SHARD_ACTION_APPLY_SCHEMA = "ApplySchemaShard" // Changes the ServedTypes inside a shard SHARD_ACTION_SET_SERVED_TYPES = "SetShardServedTypes" - // Multi-restore on all tablets of a shard in parallel - SHARD_ACTION_MULTI_RESTORE = "ShardMultiRestore" // Migrate served types from one shard to another SHARD_ACTION_MIGRATE_SERVED_TYPES = "MigrateServedTypes" // Update the Shard object (Cells, ...) diff --git a/go/vt/tabletmanager/actionnode/structs.go b/go/vt/tabletmanager/actionnode/structs.go index 62a9adf6c0b..201e6b8bdf5 100644 --- a/go/vt/tabletmanager/actionnode/structs.go +++ b/go/vt/tabletmanager/actionnode/structs.go @@ -6,8 +6,8 @@ package actionnode import ( "fmt" + "time" - "github.com/youtube/vitess/go/vt/key" myproto "github.com/youtube/vitess/go/vt/mysqlctl/proto" "github.com/youtube/vitess/go/vt/topo" ) @@ -25,6 +25,29 @@ Note it's OK to rename the structures as the type name is not saved in json. // tablet action node structures +// HealthStreamReply is the structure we stream from HealthStream +type HealthStreamReply struct { + // Tablet is the current tablet record, as cached by tabletmanager + Tablet *topo.Tablet + + // BinlogPlayerMapSize is the size of the binlog player map. + // If non zero, the ReplicationDelay is the binlog players' maximum + // replication delay. + BinlogPlayerMapSize int64 + + // HealthError is the last error we got from health check, + // or empty is the server is healthy. + HealthError string + + // ReplicationDelay is either from MySQL replication, or from + // filtered replication + ReplicationDelay time.Duration + + // TODO(alainjobart) add some QPS reporting data here +} + +// RestartSlaveData is returned by the master, and used to promote or +// restart slaves type RestartSlaveData struct { ReplicationStatus *myproto.ReplicationStatus WaitPosition myproto.ReplicationPosition @@ -37,16 +60,19 @@ func (rsd *RestartSlaveData) String() string { return fmt.Sprintf("RestartSlaveData{ReplicationStatus:%#v WaitPosition:%#v TimePromoted:%v Parent:%v Force:%v}", rsd.ReplicationStatus, rsd.WaitPosition, rsd.TimePromoted, rsd.Parent, rsd.Force) } +// SlaveWasRestartedArgs is the paylod for SlaveWasRestarted type SlaveWasRestartedArgs struct { Parent topo.TabletAlias } +// SnapshotArgs is the paylod for Snapshot type SnapshotArgs struct { Concurrency int ServerMode bool ForceMasterSnapshot bool } +// SnapshotReply is the response for Snapshot type SnapshotReply struct { ParentAlias topo.TabletAlias ManifestPath string @@ -56,39 +82,19 @@ type SnapshotReply struct { ReadOnly bool } -type MultiSnapshotReply struct { - ParentAlias topo.TabletAlias - ManifestPaths []string -} - +// SnapshotSourceEndArgs is the payload for SnapshotSourceEnd type SnapshotSourceEndArgs struct { SlaveStartRequired bool ReadOnly bool OriginalType topo.TabletType } -type MultiSnapshotArgs struct { - KeyRanges []key.KeyRange - Tables []string - ExcludeTables []string - Concurrency int - SkipSlaveRestart bool - MaximumFilesize uint64 -} - -type MultiRestoreArgs struct { - SrcTabletAliases []topo.TabletAlias - Concurrency int - FetchConcurrency int - InsertTableConcurrency int - FetchRetryCount int - Strategy string -} - +// ReserveForRestoreArgs is the payload for ReserveForRestore type ReserveForRestoreArgs struct { SrcTabletAlias topo.TabletAlias } +// RestoreArgs is the payload for Restore type RestoreArgs struct { SrcTabletAlias topo.TabletAlias SrcFilePath string @@ -101,34 +107,40 @@ type RestoreArgs struct { // shard action node structures +// ApplySchemaShardArgs is the payload for ApplySchemaShard type ApplySchemaShardArgs struct { MasterTabletAlias topo.TabletAlias Change string Simple bool } +// SetShardServedTypesArgs is the payload for SetShardServedTypes type SetShardServedTypesArgs struct { Cells []string ServedType topo.TabletType } +// MigrateServedTypesArgs is the payload for MigrateServedTypes type MigrateServedTypesArgs struct { ServedType topo.TabletType } // keyspace action node structures +// ApplySchemaKeyspaceArgs is the payload for ApplySchemaKeyspace type ApplySchemaKeyspaceArgs struct { Change string Simple bool } +// MigrateServedFromArgs is the payload for MigrateServedFrom type MigrateServedFromArgs struct { ServedType topo.TabletType } // methods to build the shard action nodes +// ReparentShard returns an ActionNode func ReparentShard(tabletAlias topo.TabletAlias) *ActionNode { return (&ActionNode{ Action: SHARD_ACTION_REPARENT, @@ -136,6 +148,7 @@ func ReparentShard(tabletAlias topo.TabletAlias) *ActionNode { }).SetGuid() } +// ShardExternallyReparented returns an ActionNode func ShardExternallyReparented(tabletAlias topo.TabletAlias) *ActionNode { return (&ActionNode{ Action: SHARD_ACTION_EXTERNALLY_REPARENTED, @@ -143,18 +156,21 @@ func ShardExternallyReparented(tabletAlias topo.TabletAlias) *ActionNode { }).SetGuid() } +// RebuildShard returns an ActionNode func RebuildShard() *ActionNode { return (&ActionNode{ Action: SHARD_ACTION_REBUILD, }).SetGuid() } +// CheckShard returns an ActionNode func CheckShard() *ActionNode { return (&ActionNode{ Action: SHARD_ACTION_CHECK, }).SetGuid() } +// ApplySchemaShard returns an ActionNode func ApplySchemaShard(masterTabletAlias topo.TabletAlias, change string, simple bool) *ActionNode { return (&ActionNode{ Action: SHARD_ACTION_APPLY_SCHEMA, @@ -166,6 +182,7 @@ func ApplySchemaShard(masterTabletAlias topo.TabletAlias, change string, simple }).SetGuid() } +// SetShardServedTypes returns an ActionNode func SetShardServedTypes(cells []string, servedType topo.TabletType) *ActionNode { return (&ActionNode{ Action: SHARD_ACTION_SET_SERVED_TYPES, @@ -176,13 +193,7 @@ func SetShardServedTypes(cells []string, servedType topo.TabletType) *ActionNode }).SetGuid() } -func ShardMultiRestore(args *MultiRestoreArgs) *ActionNode { - return (&ActionNode{ - Action: SHARD_ACTION_MULTI_RESTORE, - Args: args, - }).SetGuid() -} - +// MigrateServedTypes returns an ActionNode func MigrateServedTypes(servedType topo.TabletType) *ActionNode { return (&ActionNode{ Action: SHARD_ACTION_MIGRATE_SERVED_TYPES, @@ -192,6 +203,7 @@ func MigrateServedTypes(servedType topo.TabletType) *ActionNode { }).SetGuid() } +// UpdateShard returns an ActionNode func UpdateShard() *ActionNode { return (&ActionNode{ Action: SHARD_ACTION_UPDATE_SHARD, @@ -200,24 +212,28 @@ func UpdateShard() *ActionNode { // methods to build the keyspace action nodes +// RebuildKeyspace returns an ActionNode func RebuildKeyspace() *ActionNode { return (&ActionNode{ Action: KEYSPACE_ACTION_REBUILD, }).SetGuid() } +// SetKeyspaceShardingInfo returns an ActionNode func SetKeyspaceShardingInfo() *ActionNode { return (&ActionNode{ Action: KEYSPACE_ACTION_SET_SHARDING_INFO, }).SetGuid() } +// SetKeyspaceServedFrom returns an ActionNode func SetKeyspaceServedFrom() *ActionNode { return (&ActionNode{ Action: KEYSPACE_ACTION_SET_SERVED_FROM, }).SetGuid() } +// ApplySchemaKeyspace returns an ActionNode func ApplySchemaKeyspace(change string, simple bool) *ActionNode { return (&ActionNode{ Action: KEYSPACE_ACTION_APPLY_SCHEMA, @@ -228,6 +244,7 @@ func ApplySchemaKeyspace(change string, simple bool) *ActionNode { }).SetGuid() } +// MigrateServedFrom returns an ActionNode func MigrateServedFrom(servedType topo.TabletType) *ActionNode { return (&ActionNode{ Action: KEYSPACE_ACTION_MIGRATE_SERVED_FROM, @@ -239,6 +256,7 @@ func MigrateServedFrom(servedType topo.TabletType) *ActionNode { //methods to build the serving shard action nodes +// RebuildSrvShard returns an ActionNode func RebuildSrvShard() *ActionNode { return (&ActionNode{ Action: SRV_SHARD_ACTION_REBUILD, diff --git a/go/vt/tabletmanager/actionnode/utils.go b/go/vt/tabletmanager/actionnode/utils.go index a45bdcad353..23764a1f4ce 100644 --- a/go/vt/tabletmanager/actionnode/utils.go +++ b/go/vt/tabletmanager/actionnode/utils.go @@ -10,10 +10,10 @@ package actionnode import ( "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/trace" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) var ( @@ -24,7 +24,7 @@ var ( // LockKeyspace will lock the keyspace in the topology server. // UnlockKeyspace should be called if this returns no error. -func (n *ActionNode) LockKeyspace(ctx context.Context, ts topo.Server, keyspace string, lockTimeout time.Duration, interrupted chan struct{}) (lockPath string, err error) { +func (n *ActionNode) LockKeyspace(ctx context.Context, ts topo.Server, keyspace string) (lockPath string, err error) { log.Infof("Locking keyspace %v for action %v", keyspace, n.Action) span := trace.NewSpanFromContext(ctx) @@ -33,11 +33,17 @@ func (n *ActionNode) LockKeyspace(ctx context.Context, ts topo.Server, keyspace span.Annotate("keyspace", keyspace) defer span.Finish() - return ts.LockKeyspaceForAction(keyspace, n.ToJson(), lockTimeout, interrupted) + return ts.LockKeyspaceForAction(ctx, keyspace, n.ToJson()) } // UnlockKeyspace unlocks a previously locked keyspace. -func (n *ActionNode) UnlockKeyspace(ts topo.Server, keyspace string, lockPath string, actionError error) error { +func (n *ActionNode) UnlockKeyspace(ctx context.Context, ts topo.Server, keyspace string, lockPath string, actionError error) error { + span := trace.NewSpanFromContext(ctx) + span.StartClient("TopoServer.UnlockKeyspaceForAction") + span.Annotate("action", n.Action) + span.Annotate("keyspace", keyspace) + defer span.Finish() + // first update the actionNode if actionError != nil { log.Infof("Unlocking keyspace %v for action %v with error %v", keyspace, n.Action, actionError) @@ -61,7 +67,7 @@ func (n *ActionNode) UnlockKeyspace(ts topo.Server, keyspace string, lockPath st // LockShard will lock the shard in the topology server. // UnlockShard should be called if this returns no error. -func (n *ActionNode) LockShard(ctx context.Context, ts topo.Server, keyspace, shard string, lockTimeout time.Duration, interrupted chan struct{}) (lockPath string, err error) { +func (n *ActionNode) LockShard(ctx context.Context, ts topo.Server, keyspace, shard string) (lockPath string, err error) { log.Infof("Locking shard %v/%v for action %v", keyspace, shard, n.Action) span := trace.NewSpanFromContext(ctx) @@ -71,11 +77,18 @@ func (n *ActionNode) LockShard(ctx context.Context, ts topo.Server, keyspace, sh span.Annotate("shard", shard) defer span.Finish() - return ts.LockShardForAction(keyspace, shard, n.ToJson(), lockTimeout, interrupted) + return ts.LockShardForAction(ctx, keyspace, shard, n.ToJson()) } // UnlockShard unlocks a previously locked shard. -func (n *ActionNode) UnlockShard(ts topo.Server, keyspace, shard string, lockPath string, actionError error) error { +func (n *ActionNode) UnlockShard(ctx context.Context, ts topo.Server, keyspace, shard string, lockPath string, actionError error) error { + span := trace.NewSpanFromContext(ctx) + span.StartClient("TopoServer.UnlockShardForAction") + span.Annotate("action", n.Action) + span.Annotate("keyspace", keyspace) + span.Annotate("shard", shard) + defer span.Finish() + // first update the actionNode if actionError != nil { log.Infof("Unlocking shard %v/%v for action %v with error %v", keyspace, shard, n.Action, actionError) @@ -99,7 +112,7 @@ func (n *ActionNode) UnlockShard(ts topo.Server, keyspace, shard string, lockPat // LockSrvShard will lock the serving shard in the topology server. // UnlockSrvShard should be called if this returns no error. -func (n *ActionNode) LockSrvShard(ctx context.Context, ts topo.Server, cell, keyspace, shard string, lockTimeout time.Duration, interrupted chan struct{}) (lockPath string, err error) { +func (n *ActionNode) LockSrvShard(ctx context.Context, ts topo.Server, cell, keyspace, shard string) (lockPath string, err error) { log.Infof("Locking serving shard %v/%v/%v for action %v", cell, keyspace, shard, n.Action) span := trace.NewSpanFromContext(ctx) @@ -110,11 +123,19 @@ func (n *ActionNode) LockSrvShard(ctx context.Context, ts topo.Server, cell, key span.Annotate("cell", cell) defer span.Finish() - return ts.LockSrvShardForAction(cell, keyspace, shard, n.ToJson(), lockTimeout, interrupted) + return ts.LockSrvShardForAction(ctx, cell, keyspace, shard, n.ToJson()) } // UnlockSrvShard unlocks a previously locked serving shard. -func (n *ActionNode) UnlockSrvShard(ts topo.Server, cell, keyspace, shard string, lockPath string, actionError error) error { +func (n *ActionNode) UnlockSrvShard(ctx context.Context, ts topo.Server, cell, keyspace, shard string, lockPath string, actionError error) error { + span := trace.NewSpanFromContext(ctx) + span.StartClient("TopoServer.UnlockSrvShardForAction") + span.Annotate("action", n.Action) + span.Annotate("keyspace", keyspace) + span.Annotate("shard", shard) + span.Annotate("cell", cell) + defer span.Finish() + // first update the actionNode if actionError != nil { log.Infof("Unlocking serving shard %v/%v/%v for action %v with error %v", cell, keyspace, shard, n.Action, actionError) diff --git a/go/vt/tabletmanager/after_action.go b/go/vt/tabletmanager/after_action.go index 7d4ef5fa56d..888fb3d045d 100644 --- a/go/vt/tabletmanager/after_action.go +++ b/go/vt/tabletmanager/after_action.go @@ -11,8 +11,11 @@ import ( "reflect" "strings" + "golang.org/x/net/context" + log "github.com/golang/glog" "github.com/youtube/vitess/go/stats" + "github.com/youtube/vitess/go/trace" "github.com/youtube/vitess/go/vt/binlog" "github.com/youtube/vitess/go/vt/tabletserver" "github.com/youtube/vitess/go/vt/tabletserver/planbuilder" @@ -31,6 +34,12 @@ var ( historyLength = 16 ) +// Query rules from keyrange +const keyrangeQueryRules string = "KeyrangeQueryRules" + +// Query rules from blacklist +const blacklistQueryRules string = "BlacklistQueryRules" + func (agent *ActionAgent) allowQueries(tablet *topo.Tablet, blacklistedTables []string) error { if agent.DBConfigs == nil { // test instance, do nothing @@ -54,22 +63,23 @@ func (agent *ActionAgent) allowQueries(tablet *topo.Tablet, blacklistedTables [] agent.DBConfigs.App.EnableInvalidator = false } - qrs, err := agent.createQueryRules(tablet, blacklistedTables) + err := agent.loadKeyspaceAndBlacklistRules(tablet, blacklistedTables) if err != nil { return err } - return tabletserver.AllowQueries(&agent.DBConfigs.App, agent.SchemaOverrides, qrs, agent.Mysqld, false) + return tabletserver.AllowQueries(agent.DBConfigs, agent.SchemaOverrides, agent.Mysqld) } -// createQueryRules computes the query rules that match the tablet record -func (agent *ActionAgent) createQueryRules(tablet *topo.Tablet, blacklistedTables []string) (qrs *tabletserver.QueryRules, err error) { - qrs = tabletserver.LoadCustomRules() - +// loadKeyspaceAndBlacklistRules does what the name suggests: +// 1. load and build keyrange query rules +// 2. load and build blacklist query rules +func (agent *ActionAgent) loadKeyspaceAndBlacklistRules(tablet *topo.Tablet, blacklistedTables []string) (err error) { // Keyrange rules + keyrangeRules := tabletserver.NewQueryRules() if tablet.KeyRange.IsPartial() { log.Infof("Restricting to keyrange: %v", tablet.KeyRange) - dml_plans := []struct { + dmlPlans := []struct { planID planbuilder.PlanType onAbsent bool }{ @@ -79,7 +89,7 @@ func (agent *ActionAgent) createQueryRules(tablet *topo.Tablet, blacklistedTable {planbuilder.PLAN_DML_PK, false}, {planbuilder.PLAN_DML_SUBQUERY, false}, } - for _, plan := range dml_plans { + for _, plan := range dmlPlans { qr := tabletserver.NewQueryRule( fmt.Sprintf("enforce keyspace_id range for %v", plan.planID), fmt.Sprintf("keyspace_id_not_in_range_%v", plan.planID), @@ -88,27 +98,38 @@ func (agent *ActionAgent) createQueryRules(tablet *topo.Tablet, blacklistedTable qr.AddPlanCond(plan.planID) err := qr.AddBindVarCond("keyspace_id", plan.onAbsent, true, tabletserver.QR_NOTIN, tablet.KeyRange) if err != nil { - return nil, fmt.Errorf("Unable to add keyspace rule: %v", err) + return fmt.Errorf("Unable to add keyspace rule: %v", err) } - qrs.Add(qr) + keyrangeRules.Add(qr) } } // Blacklisted tables + blacklistRules := tabletserver.NewQueryRules() if len(blacklistedTables) > 0 { // tables, first resolve wildcards tables, err := agent.Mysqld.ResolveTables(tablet.DbName(), blacklistedTables) if err != nil { - return nil, err + return err } log.Infof("Blacklisting tables %v", strings.Join(tables, ", ")) qr := tabletserver.NewQueryRule("enforce blacklisted tables", "blacklisted_table", tabletserver.QR_FAIL_RETRY) for _, t := range tables { qr.AddTableCond(t) } - qrs.Add(qr) + blacklistRules.Add(qr) + } + // Push all three sets of QueryRules to SqlQueryRpcService + loadRuleErr := tabletserver.SetQueryRules(keyrangeQueryRules, keyrangeRules) + if loadRuleErr != nil { + log.Warningf("Fail to load query rule set %s: %s", keyrangeQueryRules, loadRuleErr) } - return qrs, nil + + loadRuleErr = tabletserver.SetQueryRules(blacklistQueryRules, blacklistRules) + if loadRuleErr != nil { + log.Warningf("Fail to load query rule set %s: %s", blacklistQueryRules, loadRuleErr) + } + return nil } func (agent *ActionAgent) disallowQueries() { @@ -121,7 +142,11 @@ func (agent *ActionAgent) disallowQueries() { // changeCallback is run after every action that might // have changed something in the tablet record. -func (agent *ActionAgent) changeCallback(oldTablet, newTablet *topo.Tablet) error { +func (agent *ActionAgent) changeCallback(ctx context.Context, oldTablet, newTablet *topo.Tablet) error { + span := trace.NewSpanFromContext(ctx) + span.StartLocal("ActionAgent.changeCallback") + defer span.Finish() + allowQuery := newTablet.IsRunningQueryService() // Read the shard to get SourceShards / TabletControlMap if @@ -131,7 +156,7 @@ func (agent *ActionAgent) changeCallback(oldTablet, newTablet *topo.Tablet) erro var blacklistedTables []string var err error if allowQuery { - shardInfo, err = agent.TopoServer.GetShard(newTablet.Keyspace, newTablet.Shard) + shardInfo, err = topo.GetShard(ctx, agent.TopoServer, newTablet.Keyspace, newTablet.Shard) if err != nil { log.Errorf("Cannot read shard for this tablet %v, might have inaccurate SourceShards and TabletControls: %v", newTablet.Alias, err) } else { @@ -214,3 +239,9 @@ func (agent *ActionAgent) changeCallback(oldTablet, newTablet *topo.Tablet) erro } return nil } + +func init() { + // Register query rule sources under control of agent + tabletserver.QueryRuleSources.RegisterQueryRuleSource(keyrangeQueryRules) + tabletserver.QueryRuleSources.RegisterQueryRuleSource(blacklistQueryRules) +} diff --git a/go/vt/tabletmanager/agent.go b/go/vt/tabletmanager/agent.go index 26f7cbf6df8..01c1e67d07e 100644 --- a/go/vt/tabletmanager/agent.go +++ b/go/vt/tabletmanager/agent.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. /* -Package agent exports the ActionAgent object. It keeps the local tablet +Package tabletmanager exports the ActionAgent object. It keeps the local tablet state, starts / stops all associated services (query service, update stream, binlog players, ...), and handles tabletmanager RPCs to update the state. @@ -26,20 +26,20 @@ import ( "flag" "fmt" "net" - "os" "sync" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/history" "github.com/youtube/vitess/go/jscfg" "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/stats" + "github.com/youtube/vitess/go/trace" "github.com/youtube/vitess/go/vt/dbconfigs" - "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/mysqlctl" + "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/tabletserver" "github.com/youtube/vitess/go/vt/topo" ) @@ -61,9 +61,12 @@ type ActionAgent struct { SchemaOverrides []tabletserver.SchemaOverride BinlogPlayerMap *BinlogPlayerMap LockTimeout time.Duration - - // Internal variables - done chan struct{} // closed when we are done. + // batchCtx is given to the agent by its creator, and should be used for + // any background tasks spawned by the agent. + batchCtx context.Context + // finalizeReparentCtx represents the background finalize step of a + // TabletExternallyReparented call. + finalizeReparentCtx context.Context // This is the History of the health checks, public so status // pages can display it @@ -80,6 +83,18 @@ type ActionAgent struct { _tablet *topo.TabletInfo _tabletControl *topo.TabletControl _waitingForMysql bool + + // if the agent is healthy, this is nil. Otherwise it contains + // the reason we're not healthy. + _healthy error + + // replication delay the last time we got it + _replicationDelay time.Duration + + // healthStreamMutex protects all the following fields + healthStreamMutex sync.Mutex + healthStreamIndex int + healthStreamMap map[int]chan<- *actionnode.HealthStreamReply } func loadSchemaOverrides(overridesFile string) []tabletserver.SchemaOverride { @@ -97,8 +112,12 @@ func loadSchemaOverrides(overridesFile string) []tabletserver.SchemaOverride { } // NewActionAgent creates a new ActionAgent and registers all the -// associated services +// associated services. +// +// batchCtx is the context that the agent will use for any background tasks +// it spawns. func NewActionAgent( + batchCtx context.Context, tabletAlias topo.TabletAlias, dbcfgs *dbconfigs.DBConfigs, mycnf *mysqlctl.Mycnf, @@ -112,6 +131,7 @@ func NewActionAgent( mysqld := mysqlctl.NewMysqld("Dba", mycnf, &dbcfgs.Dba, &dbcfgs.Repl) agent = &ActionAgent{ + batchCtx: batchCtx, TopoServer: topoServer, TabletAlias: tabletAlias, Mysqld: mysqld, @@ -119,9 +139,15 @@ func NewActionAgent( DBConfigs: dbcfgs, SchemaOverrides: schemaOverrides, LockTimeout: lockTimeout, - done: make(chan struct{}), History: history.New(historyLength), lastHealthMapCount: stats.NewInt("LastHealthMapCount"), + _healthy: fmt.Errorf("healthcheck not run yet"), + healthStreamMap: make(map[int]chan<- *actionnode.HealthStreamReply), + } + + // try to initialize the tablet if we have to + if err := agent.InitTablet(port, securePort); err != nil { + return nil, err } // Start the binlog player services, not playing at start. @@ -154,8 +180,9 @@ func NewActionAgent( // NewTestActionAgent creates an agent for test purposes. Only a // subset of features are supported now, but we'll add more over time. -func NewTestActionAgent(ts topo.Server, tabletAlias topo.TabletAlias, port int, mysqlDaemon mysqlctl.MysqlDaemon) (agent *ActionAgent) { +func NewTestActionAgent(batchCtx context.Context, ts topo.Server, tabletAlias topo.TabletAlias, port int, mysqlDaemon mysqlctl.MysqlDaemon) (agent *ActionAgent) { agent = &ActionAgent{ + batchCtx: batchCtx, TopoServer: ts, TabletAlias: tabletAlias, Mysqld: nil, @@ -163,9 +190,10 @@ func NewTestActionAgent(ts topo.Server, tabletAlias topo.TabletAlias, port int, DBConfigs: nil, SchemaOverrides: nil, BinlogPlayerMap: nil, - done: make(chan struct{}), History: history.New(historyLength), lastHealthMapCount: new(stats.Int), + _healthy: fmt.Errorf("healthcheck not run yet"), + healthStreamMap: make(map[int]chan<- *actionnode.HealthStreamReply), } if err := agent.Start(0, port, 0); err != nil { panic(fmt.Errorf("agent.Start(%v) failed: %v", tabletAlias, err)) @@ -173,25 +201,32 @@ func NewTestActionAgent(ts topo.Server, tabletAlias topo.TabletAlias, port int, return agent } -func (agent *ActionAgent) updateState(oldTablet *topo.Tablet, context string) error { +func (agent *ActionAgent) updateState(ctx context.Context, oldTablet *topo.Tablet, reason string) error { agent.mutex.Lock() newTablet := agent._tablet.Tablet agent.mutex.Unlock() - log.Infof("Running tablet callback after action %v", context) - return agent.changeCallback(oldTablet, newTablet) + log.Infof("Running tablet callback because: %v", reason) + return agent.changeCallback(ctx, oldTablet, newTablet) } -func (agent *ActionAgent) readTablet() error { - tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) +func (agent *ActionAgent) readTablet(ctx context.Context) (*topo.TabletInfo, error) { + tablet, err := topo.GetTablet(ctx, agent.TopoServer, agent.TabletAlias) if err != nil { - return err + return nil, err } agent.mutex.Lock() agent._tablet = tablet agent.mutex.Unlock() - return nil + return tablet, nil +} + +func (agent *ActionAgent) setTablet(tablet *topo.TabletInfo) { + agent.mutex.Lock() + agent._tablet = tablet + agent.mutex.Unlock() } +// Tablet reads the stored TabletInfo from the agent, protected by mutex. func (agent *ActionAgent) Tablet() *topo.TabletInfo { agent.mutex.Lock() tablet := agent._tablet @@ -199,6 +234,15 @@ func (agent *ActionAgent) Tablet() *topo.TabletInfo { return tablet } +// Healthy reads the result of the latest healthcheck, protected by mutex. +func (agent *ActionAgent) Healthy() (time.Duration, error) { + agent.mutex.Lock() + defer agent.mutex.Unlock() + return agent._replicationDelay, agent._healthy +} + +// BlacklistedTables reads the list of blacklisted tables from the TabletControl +// record (if any) stored in the agent, protected by mutex. func (agent *ActionAgent) BlacklistedTables() []string { var blacklistedTables []string agent.mutex.Lock() @@ -209,6 +253,8 @@ func (agent *ActionAgent) BlacklistedTables() []string { return blacklistedTables } +// DisableQueryService reads the DisableQueryService field from the TabletControl +// record (if any) stored in the agent, protected by mutex. func (agent *ActionAgent) DisableQueryService() bool { disable := false agent.mutex.Lock() @@ -227,26 +273,32 @@ func (agent *ActionAgent) setTabletControl(tc *topo.TabletControl) { // refreshTablet needs to be run after an action may have changed the current // state of the tablet. -func (agent *ActionAgent) refreshTablet(context string) error { +func (agent *ActionAgent) refreshTablet(ctx context.Context, reason string) error { log.Infof("Executing post-action state refresh") + span := trace.NewSpanFromContext(ctx) + span.StartLocal("ActionAgent.refreshTablet") + span.Annotate("reason", reason) + defer span.Finish() + ctx = trace.NewContext(ctx, span) + // Save the old tablet so callbacks can have a better idea of // the precise nature of the transition. oldTablet := agent.Tablet().Tablet // Actions should have side effects on the tablet, so reload the data. - if err := agent.readTablet(); err != nil { - log.Warningf("Failed rereading tablet after %v - services may be inconsistent: %v", context, err) - return fmt.Errorf("Failed rereading tablet after %v: %v", context, err) + if _, err := agent.readTablet(ctx); err != nil { + log.Warningf("Failed rereading tablet after %v - services may be inconsistent: %v", reason, err) + return fmt.Errorf("Failed rereading tablet after %v: %v", reason, err) } - if updatedTablet := agent.checkTabletMysqlPort(agent.Tablet()); updatedTablet != nil { + if updatedTablet := agent.checkTabletMysqlPort(ctx, agent.Tablet()); updatedTablet != nil { agent.mutex.Lock() agent._tablet = updatedTablet agent.mutex.Unlock() } - if err := agent.updateState(oldTablet, context); err != nil { + if err := agent.updateState(ctx, oldTablet, reason); err != nil { return err } log.Infof("Done with post-action state refresh") @@ -280,10 +332,11 @@ func (agent *ActionAgent) verifyServingAddrs() error { return agent.TopoServer.UpdateTabletEndpoint(agent.Tablet().Tablet.Alias.Cell, agent.Tablet().Keyspace, agent.Tablet().Shard, agent.Tablet().Type, addr) } -// bindAddr: the address for the query service advertised by this agent +// Start validates and updates the topology records for the tablet, and performs +// the initial state change callback to start tablet services. func (agent *ActionAgent) Start(mysqlPort, vtPort, vtsPort int) error { var err error - if err = agent.readTablet(); err != nil { + if _, err = agent.readTablet(context.TODO()); err != nil { return err } @@ -326,13 +379,7 @@ func (agent *ActionAgent) Start(mysqlPort, vtPort, vtsPort int) error { } // Reread to get the changes we just made - if err := agent.readTablet(); err != nil { - return err - } - - data := fmt.Sprintf("host:%v\npid:%v\n", hostname, os.Getpid()) - - if err := agent.TopoServer.CreateTabletPidNode(agent.TabletAlias, data, agent.done); err != nil { + if _, err := agent.readTablet(context.TODO()); err != nil { return err } @@ -345,7 +392,7 @@ func (agent *ActionAgent) Start(mysqlPort, vtPort, vtsPort int) error { } oldTablet := &topo.Tablet{} - if err = agent.updateState(oldTablet, "Start"); err != nil { + if err = agent.updateState(context.TODO(), oldTablet, "Start"); err != nil { log.Warningf("Initial updateState failed, will need a state change before running properly: %v", err) } return nil @@ -353,7 +400,6 @@ func (agent *ActionAgent) Start(mysqlPort, vtPort, vtsPort int) error { // Stop shutdowns this agent. func (agent *ActionAgent) Stop() { - close(agent.done) if agent.BinlogPlayerMap != nil { agent.BinlogPlayerMap.StopAllPlayersAndReset() } @@ -369,7 +415,7 @@ func (agent *ActionAgent) hookExtraEnv() map[string]string { // checkTabletMysqlPort will check the mysql port for the tablet is good, // and if not will try to update it. -func (agent *ActionAgent) checkTabletMysqlPort(tablet *topo.TabletInfo) *topo.TabletInfo { +func (agent *ActionAgent) checkTabletMysqlPort(ctx context.Context, tablet *topo.TabletInfo) *topo.TabletInfo { mport, err := agent.MysqlDaemon.GetMysqlPort() if err != nil { log.Warningf("Cannot get current mysql port, not checking it: %v", err) @@ -382,7 +428,7 @@ func (agent *ActionAgent) checkTabletMysqlPort(tablet *topo.TabletInfo) *topo.Ta log.Warningf("MySQL port has changed from %v to %v, updating it in tablet record", tablet.Portmap["mysql"], mport) tablet.Portmap["mysql"] = mport - if err := topo.UpdateTablet(context.TODO(), agent.TopoServer, tablet); err != nil { + if err := topo.UpdateTablet(ctx, agent.TopoServer, tablet); err != nil { log.Warningf("Failed to update tablet record, may use old mysql port") return nil } @@ -390,11 +436,24 @@ func (agent *ActionAgent) checkTabletMysqlPort(tablet *topo.TabletInfo) *topo.Ta return tablet } -var getSubprocessFlagsFuncs []func() []string +// BroadcastHealthStreamReply will send the HealthStreamReply to all +// listening clients. +func (agent *ActionAgent) BroadcastHealthStreamReply(hsr *actionnode.HealthStreamReply) { + agent.healthStreamMutex.Lock() + defer agent.healthStreamMutex.Unlock() + for _, c := range agent.healthStreamMap { + // do not block on any write + select { + case c <- hsr: + default: + } + } +} -func init() { - getSubprocessFlagsFuncs = append(getSubprocessFlagsFuncs, logutil.GetSubprocessFlags) - getSubprocessFlagsFuncs = append(getSubprocessFlagsFuncs, topo.GetSubprocessFlags) - getSubprocessFlagsFuncs = append(getSubprocessFlagsFuncs, dbconfigs.GetSubprocessFlags) - getSubprocessFlagsFuncs = append(getSubprocessFlagsFuncs, mysqlctl.GetSubprocessFlags) +// HealthStreamMapSize returns the size of the healthStreamMap +// (used for tests). +func (agent *ActionAgent) HealthStreamMapSize() int { + agent.healthStreamMutex.Lock() + defer agent.healthStreamMutex.Unlock() + return len(agent.healthStreamMap) } diff --git a/go/vt/tabletmanager/agent_rpc_actions.go b/go/vt/tabletmanager/agent_rpc_actions.go index 04f73d193e6..bd417b42796 100644 --- a/go/vt/tabletmanager/agent_rpc_actions.go +++ b/go/vt/tabletmanager/agent_rpc_actions.go @@ -9,37 +9,31 @@ import ( "fmt" "io/ioutil" "net/http" - "net/url" "path" "strings" - "sync" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" - "github.com/youtube/vitess/go/event" "github.com/youtube/vitess/go/mysql/proto" blproto "github.com/youtube/vitess/go/vt/binlog/proto" - "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/hook" "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/mysqlctl" myproto "github.com/youtube/vitess/go/vt/mysqlctl/proto" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" - "github.com/youtube/vitess/go/vt/tabletmanager/tmclient" "github.com/youtube/vitess/go/vt/tabletserver" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" - "github.com/youtube/vitess/go/vt/topotools/events" + "golang.org/x/net/context" ) // This file contains the actions that exist as RPC only on the ActionAgent. // The various rpc server implementations just call these. -// RpcAgent defines the interface implemented by the Agent for RPCs. +// RPCAgent defines the interface implemented by the Agent for RPCs. // It is useful for RPC implementations to test their full stack. -type RpcAgent interface { +type RPCAgent interface { // RPC calls // Various read-only methods @@ -66,6 +60,9 @@ type RpcAgent interface { RunHealthCheck(ctx context.Context, targetTabletType topo.TabletType) + RegisterHealthStream(chan<- *actionnode.HealthStreamReply) (int, error) + UnregisterHealthStream(int) error + ReloadSchema(ctx context.Context) PreflightSchema(ctx context.Context, change string) (*myproto.SchemaChangeResult, error) @@ -90,7 +87,7 @@ type RpcAgent interface { StartSlave(ctx context.Context) error - TabletExternallyReparented(ctx context.Context, externalID string, actionTimeout time.Duration) error + TabletExternallyReparented(ctx context.Context, externalID string) error GetSlaves(ctx context.Context) ([]string, error) @@ -126,43 +123,39 @@ type RpcAgent interface { Restore(ctx context.Context, args *actionnode.RestoreArgs, logger logutil.Logger) error - MultiSnapshot(ctx context.Context, args *actionnode.MultiSnapshotArgs, logger logutil.Logger) (*actionnode.MultiSnapshotReply, error) - - MultiRestore(ctx context.Context, args *actionnode.MultiRestoreArgs, logger logutil.Logger) error - // RPC helpers - RpcWrap(ctx context.Context, name string, args, reply interface{}, f func() error) error - RpcWrapLock(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error - RpcWrapLockAction(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error + RPCWrap(ctx context.Context, name string, args, reply interface{}, f func() error) error + RPCWrapLock(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error + RPCWrapLockAction(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error } // TODO(alainjobart): all the calls mention something like: -// Should be called under RpcWrap. +// Should be called under RPCWrap. // Eventually, when all calls are going through RPCs, we'll refactor // this so there is only one wrapper, and the extra stuff done by the -// RpcWrapXXX methods will be done internally. Until then, it's safer +// RPCWrapXXX methods will be done internally. Until then, it's safer // to have the comment. // Ping makes sure RPCs work, and refreshes the tablet record. -// Should be called under RpcWrap. +// Should be called under RPCWrap. func (agent *ActionAgent) Ping(ctx context.Context, args string) string { return args } // GetSchema returns the schema. -// Should be called under RpcWrap. +// Should be called under RPCWrap. func (agent *ActionAgent) GetSchema(ctx context.Context, tables, excludeTables []string, includeViews bool) (*myproto.SchemaDefinition, error) { return agent.MysqlDaemon.GetSchema(agent.Tablet().DbName(), tables, excludeTables, includeViews) } // GetPermissions returns the db permissions. -// Should be called under RpcWrap. +// Should be called under RPCWrap. func (agent *ActionAgent) GetPermissions(ctx context.Context) (*myproto.Permissions, error) { return agent.Mysqld.GetPermissions() } // SetReadOnly makes the mysql instance read-only or read-write -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) SetReadOnly(ctx context.Context, rdonly bool) error { err := agent.Mysqld.SetReadOnly(rdonly) if err != nil { @@ -182,43 +175,63 @@ func (agent *ActionAgent) SetReadOnly(ctx context.Context, rdonly bool) error { } // ChangeType changes the tablet type -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) ChangeType(ctx context.Context, tabletType topo.TabletType) error { - return topotools.ChangeType(agent.TopoServer, agent.TabletAlias, tabletType, nil, true /*runHooks*/) + return topotools.ChangeType(ctx, agent.TopoServer, agent.TabletAlias, tabletType, nil, true /*runHooks*/) } // Scrap scraps the live running tablet -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) Scrap(ctx context.Context) error { - return topotools.Scrap(agent.TopoServer, agent.TabletAlias, false) + return topotools.Scrap(ctx, agent.TopoServer, agent.TabletAlias, false) } // Sleep sleeps for the duration -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) Sleep(ctx context.Context, duration time.Duration) { time.Sleep(duration) } // ExecuteHook executes the provided hook locally, and returns the result. -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) ExecuteHook(ctx context.Context, hk *hook.Hook) *hook.HookResult { topotools.ConfigureTabletHook(hk, agent.TabletAlias) return hk.Execute() } // RefreshState reload the tablet record from the topo server. -// Should be called under RpcWrapLockAction, so it actually works. +// Should be called under RPCWrapLockAction, so it actually works. func (agent *ActionAgent) RefreshState(ctx context.Context) { } // RunHealthCheck will manually run the health check on the tablet -// Should be called under RpcWrap. +// Should be called under RPCWrap. func (agent *ActionAgent) RunHealthCheck(ctx context.Context, targetTabletType topo.TabletType) { agent.runHealthCheck(targetTabletType) } +// RegisterHealthStream adds a health stream channel to our list +func (agent *ActionAgent) RegisterHealthStream(c chan<- *actionnode.HealthStreamReply) (int, error) { + agent.healthStreamMutex.Lock() + defer agent.healthStreamMutex.Unlock() + + id := agent.healthStreamIndex + agent.healthStreamIndex++ + agent.healthStreamMap[id] = c + return id, nil +} + +// UnregisterHealthStream removes a health stream channel from our list +func (agent *ActionAgent) UnregisterHealthStream(id int) error { + agent.healthStreamMutex.Lock() + defer agent.healthStreamMutex.Unlock() + + delete(agent.healthStreamMap, id) + return nil +} + // ReloadSchema will reload the schema -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) ReloadSchema(ctx context.Context) { if agent.DBConfigs == nil { // we skip this for test instances that can't connect to the DB anyway @@ -232,7 +245,7 @@ func (agent *ActionAgent) ReloadSchema(ctx context.Context) { } // PreflightSchema will try out the schema change -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) PreflightSchema(ctx context.Context, change string) (*myproto.SchemaChangeResult, error) { // get the db name from the tablet tablet := agent.Tablet() @@ -242,7 +255,7 @@ func (agent *ActionAgent) PreflightSchema(ctx context.Context, change string) (* } // ApplySchema will apply a schema change -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) ApplySchema(ctx context.Context, change *myproto.SchemaChange) (*myproto.SchemaChangeResult, error) { // get the db name from the tablet tablet := agent.Tablet() @@ -259,7 +272,7 @@ func (agent *ActionAgent) ApplySchema(ctx context.Context, change *myproto.Schem } // ExecuteFetch will execute the given query, possibly disabling binlogs. -// Should be called under RpcWrap. +// Should be called under RPCWrap. func (agent *ActionAgent) ExecuteFetch(ctx context.Context, query string, maxrows int, wantFields, disableBinlogs bool) (*proto.QueryResult, error) { // get a connection conn, err := agent.MysqlDaemon.GetDbaConnection() @@ -293,14 +306,14 @@ func (agent *ActionAgent) ExecuteFetch(ctx context.Context, query string, maxrow } // SlaveStatus returns the replication status -// Should be called under RpcWrap. +// Should be called under RPCWrap. func (agent *ActionAgent) SlaveStatus(ctx context.Context) (*myproto.ReplicationStatus, error) { return agent.MysqlDaemon.SlaveStatus() } // WaitSlavePosition waits until we reach the provided position, // and returns the current position -// Should be called under RpcWrapLock. +// Should be called under RPCWrapLock. func (agent *ActionAgent) WaitSlavePosition(ctx context.Context, position myproto.ReplicationPosition, waitTimeout time.Duration) (*myproto.ReplicationStatus, error) { if err := agent.Mysqld.WaitMasterPos(position, waitTimeout); err != nil { return nil, err @@ -310,14 +323,14 @@ func (agent *ActionAgent) WaitSlavePosition(ctx context.Context, position myprot } // MasterPosition returns the master position -// Should be called under RpcWrap. +// Should be called under RPCWrap. func (agent *ActionAgent) MasterPosition(ctx context.Context) (myproto.ReplicationPosition, error) { return agent.Mysqld.MasterPosition() } // ReparentPosition returns the RestartSlaveData for the provided // ReplicationPosition. -// Should be called under RpcWrap. +// Should be called under RPCWrap. func (agent *ActionAgent) ReparentPosition(ctx context.Context, rp *myproto.ReplicationPosition) (*actionnode.RestartSlaveData, error) { replicationStatus, waitPosition, timePromoted, err := agent.Mysqld.ReparentPosition(*rp) if err != nil { @@ -332,7 +345,7 @@ func (agent *ActionAgent) ReparentPosition(ctx context.Context, rp *myproto.Repl } // StopSlave will stop the replication -// Should be called under RpcWrapLock. +// Should be called under RPCWrapLock. func (agent *ActionAgent) StopSlave(ctx context.Context) error { return agent.MysqlDaemon.StopSlave(agent.hookExtraEnv()) } @@ -350,194 +363,26 @@ func (agent *ActionAgent) StopSlaveMinimum(ctx context.Context, position myproto } // StartSlave will start the replication -// Should be called under RpcWrapLock. +// Should be called under RPCWrapLock. func (agent *ActionAgent) StartSlave(ctx context.Context) error { return agent.MysqlDaemon.StartSlave(agent.hookExtraEnv()) } -// TabletExternallyReparented updates all topo records so the current -// tablet is the new master for this shard. -// Should be called under RpcWrapLock. -func (agent *ActionAgent) TabletExternallyReparented(ctx context.Context, externalID string, actionTimeout time.Duration) error { - tablet := agent.Tablet() - - // fast quick check on the shard to see if we're not the master already - shardInfo, err := agent.TopoServer.GetShard(tablet.Keyspace, tablet.Shard) - if err != nil { - log.Warningf("TabletExternallyReparented: Cannot read the shard %v/%v: %v", tablet.Keyspace, tablet.Shard, err) - return err - } - if shardInfo.MasterAlias == agent.TabletAlias { - // we are already the master, nothing more to do. - return nil - } - - // grab the shard lock - actionNode := actionnode.ShardExternallyReparented(agent.TabletAlias) - interrupted := make(chan struct{}) - lockPath, err := actionNode.LockShard(ctx, agent.TopoServer, tablet.Keyspace, tablet.Shard, agent.LockTimeout, interrupted) - if err != nil { - log.Warningf("TabletExternallyReparented: Cannot lock shard %v/%v: %v", tablet.Keyspace, tablet.Shard, err) - return err - } - - // do the work - runAfterAction, err := agent.tabletExternallyReparentedLocked(ctx, externalID, actionTimeout, interrupted) - if err != nil { - log.Warningf("TabletExternallyReparented: internal error: %v", err) - } - - // release the lock in any case, and run refreshTablet if necessary - err = actionNode.UnlockShard(agent.TopoServer, tablet.Keyspace, tablet.Shard, lockPath, err) - if runAfterAction { - if refreshErr := agent.refreshTablet("RPC(TabletExternallyReparented)"); refreshErr != nil { - if err == nil { - // no error yet, now we have one - err = refreshErr - } else { - //have an error already, keep the original one - log.Warningf("refreshTablet failed with error: %v", refreshErr) - } - } - } - return err -} - -// tabletExternallyReparentedLocked is called with the shard lock. -// It returns if agent.refreshTablet should be called, and the error. -// Note both are set independently (can have both true and an error). -func (agent *ActionAgent) tabletExternallyReparentedLocked(ctx context.Context, externalID string, actionTimeout time.Duration, interrupted chan struct{}) (bool, error) { - // re-read the tablet record to be sure we have the latest version - tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) - if err != nil { - return false, err - } - - // read the shard, make sure again the master is not already good. - shardInfo, err := agent.TopoServer.GetShard(tablet.Keyspace, tablet.Shard) - if err != nil { - return false, err - } - if shardInfo.MasterAlias == tablet.Alias { - log.Infof("TabletExternallyReparented: tablet became the master before we get the lock?") - return false, nil - } - log.Infof("TabletExternallyReparented called and we're not the master, doing the work") - - // Read the tablets, make sure the master elect is known to the shard - // (it's this tablet, so it better be!). - // Note we will keep going with a partial tablet map, which usually - // happens when a cell is not reachable. After these checks, the - // guarantees we'll have are: - // - global cell is reachable (we just locked and read the shard) - // - the local cell that contains the new master is reachable - // (as we're going to check the new master is in the list) - // That should be enough. - tabletMap, err := topo.GetTabletMapForShard(agent.TopoServer, tablet.Keyspace, tablet.Shard) - switch err { - case nil: - // keep going - case topo.ErrPartialResult: - log.Warningf("Got topo.ErrPartialResult from GetTabletMapForShard, may need to re-init some tablets") - default: - return false, err - } - masterElectTablet, ok := tabletMap[tablet.Alias] - if !ok { - return false, fmt.Errorf("this master-elect tablet %v not found in replication graph %v/%v %v", tablet.Alias, tablet.Keyspace, tablet.Shard, topotools.MapKeys(tabletMap)) - } - - // Create reusable Reparent event with available info - ev := &events.Reparent{ - ShardInfo: *shardInfo, - NewMaster: *tablet.Tablet, - ExternalID: externalID, - } - - if oldMasterTablet, ok := tabletMap[shardInfo.MasterAlias]; ok { - ev.OldMaster = *oldMasterTablet.Tablet - } - - defer func() { - if err != nil { - event.DispatchUpdate(ev, "failed: "+err.Error()) - } - }() - - // sort the tablets, and handle them - slaveTabletMap, masterTabletMap := topotools.SortedTabletMap(tabletMap) - event.DispatchUpdate(ev, "starting external from tablet") - - // We fix the new master in the replication graph. - // Note after this call, we may have changed the tablet record, - // so we will always return true, so the tablet record is re-read - // by the agent. - event.DispatchUpdate(ev, "mark ourself as new master") - err = agent.updateReplicationGraphForPromotedSlave(ctx, tablet) - if err != nil { - // This suggests we can't talk to topo server. This is bad. - return true, fmt.Errorf("updateReplicationGraphForPromotedSlave failed: %v", err) - } - - // Once this tablet is promoted, remove it from our maps - delete(slaveTabletMap, tablet.Alias) - delete(masterTabletMap, tablet.Alias) - - // Then fix all the slaves, including the old master. This - // last step is very likely to time out for some tablets (one - // random guy is dead, the old master is dead, ...). We - // execute them all in parallel until we get to - // wr.ActionTimeout(). After this, no other action with a - // timeout is executed, so even if we got to the timeout, - // we're still good. - event.DispatchUpdate(ev, "restarting slaves") - logger := logutil.NewConsoleLogger() - tmc := tmclient.NewTabletManagerClient() - topotools.RestartSlavesExternal(agent.TopoServer, logger, slaveTabletMap, masterTabletMap, masterElectTablet.Alias, func(ti *topo.TabletInfo, swrd *actionnode.SlaveWasRestartedArgs) error { - return tmc.SlaveWasRestarted(ctx, ti, swrd, actionTimeout) - }) - - // Compute the list of Cells we need to rebuild: old master and - // all other cells if reparenting to another cell. - cells := []string{shardInfo.MasterAlias.Cell} - if shardInfo.MasterAlias.Cell != tablet.Alias.Cell { - cells = nil - } - - // now update the master record in the shard object - event.DispatchUpdate(ev, "updating shard record") - log.Infof("Updating Shard's MasterAlias record") - shardInfo.MasterAlias = tablet.Alias - if err = topo.UpdateShard(ctx, agent.TopoServer, shardInfo); err != nil { - return true, err - } - - // and rebuild the shard serving graph - event.DispatchUpdate(ev, "rebuilding shard serving graph") - log.Infof("Rebuilding shard serving graph data") - if _, err = topotools.RebuildShard(ctx, logger, agent.TopoServer, tablet.Keyspace, tablet.Shard, cells, agent.LockTimeout, interrupted); err != nil { - return true, err - } - - event.DispatchUpdate(ev, "finished") - return true, nil -} - // GetSlaves returns the address of all the slaves -// Should be called under RpcWrap. +// Should be called under RPCWrap. func (agent *ActionAgent) GetSlaves(ctx context.Context) ([]string, error) { return agent.Mysqld.FindSlaves() } // WaitBlpPosition waits until a specific filtered replication position is // reached. -// Should be called under RpcWrapLock. +// Should be called under RPCWrapLock. func (agent *ActionAgent) WaitBlpPosition(ctx context.Context, blpPosition *blproto.BlpPosition, waitTime time.Duration) error { return agent.Mysqld.WaitBlpPosition(blpPosition, waitTime) } // StopBlp stops the binlog players, and return their positions. -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) StopBlp(ctx context.Context) (*blproto.BlpPositionList, error) { if agent.BinlogPlayerMap == nil { return nil, fmt.Errorf("No BinlogPlayerMap configured") @@ -547,7 +392,7 @@ func (agent *ActionAgent) StopBlp(ctx context.Context) (*blproto.BlpPositionList } // StartBlp starts the binlog players -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) StartBlp(ctx context.Context) error { if agent.BinlogPlayerMap == nil { return fmt.Errorf("No BinlogPlayerMap configured") @@ -574,7 +419,7 @@ func (agent *ActionAgent) RunBlpUntil(ctx context.Context, bpl *blproto.BlpPosit // // DemoteMaster demotes the current master, and marks it read-only in the topo. -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) DemoteMaster(ctx context.Context) error { _, err := agent.Mysqld.DemoteMaster() if err != nil { @@ -593,7 +438,7 @@ func (agent *ActionAgent) DemoteMaster(ctx context.Context) error { // PromoteSlave transforms the current tablet from a slave to a master. // It returns the data needed for other tablets to become a slave. -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) PromoteSlave(ctx context.Context) (*actionnode.RestartSlaveData, error) { tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) if err != nil { @@ -603,7 +448,7 @@ func (agent *ActionAgent) PromoteSlave(ctx context.Context) (*actionnode.Restart // Perform the action. rsd := &actionnode.RestartSlaveData{ Parent: tablet.Alias, - Force: (tablet.Parent.Uid == topo.NO_TABLET), + Force: (tablet.Type == topo.TYPE_MASTER), } rsd.ReplicationStatus, rsd.WaitPosition, rsd.TimePromoted, err = agent.Mysqld.PromoteSlave(false, agent.hookExtraEnv()) if err != nil { @@ -615,7 +460,7 @@ func (agent *ActionAgent) PromoteSlave(ctx context.Context) (*actionnode.Restart } // SlaveWasPromoted promotes a slave to master, no questions asked. -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) SlaveWasPromoted(ctx context.Context) error { tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) if err != nil { @@ -626,85 +471,37 @@ func (agent *ActionAgent) SlaveWasPromoted(ctx context.Context) error { } // RestartSlave tells the tablet it has a new master -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) RestartSlave(ctx context.Context, rsd *actionnode.RestartSlaveData) error { tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) if err != nil { return err } - - // If this check fails, we seem reparented. The only part that - // could have failed is the insert in the replication - // graph. Do NOT try to reparent again. That will either wedge - // replication or corrupt data. - if tablet.Parent != rsd.Parent { - log.V(6).Infof("restart with new parent") - // Remove tablet from the replication graph. - if err = topo.DeleteTabletReplicationData(agent.TopoServer, tablet.Tablet); err != nil && err != topo.ErrNoNode { - return err - } - - // Move a lag slave into the orphan lag type so we can safely ignore - // this reparenting until replication catches up. - if tablet.Type == topo.TYPE_LAG { - tablet.Type = topo.TYPE_LAG_ORPHAN - } else { - err = agent.Mysqld.RestartSlave(rsd.ReplicationStatus, rsd.WaitPosition, rsd.TimePromoted) - if err != nil { - return err - } - } - // Once this action completes, update authoritive tablet node first. - tablet.Parent = rsd.Parent - err = topo.UpdateTablet(ctx, agent.TopoServer, tablet) - if err != nil { - return err - } - } else if rsd.Force { - err = agent.Mysqld.RestartSlave(rsd.ReplicationStatus, rsd.WaitPosition, rsd.TimePromoted) - if err != nil { - return err - } - // Complete the special orphan accounting. - if tablet.Type == topo.TYPE_LAG_ORPHAN { - tablet.Type = topo.TYPE_LAG - err = topo.UpdateTablet(ctx, agent.TopoServer, tablet) - if err != nil { - return err - } - } - } else { - // There is nothing to safely reparent, so check replication. If - // either replication thread is not running, report an error. - status, err := agent.Mysqld.SlaveStatus() - if err != nil { - return fmt.Errorf("cannot verify replication for slave: %v", err) - } - if !status.SlaveRunning() { - return fmt.Errorf("replication not running for slave") - } + if tablet.Type == topo.TYPE_LAG && !rsd.Force { + // if tablet is behind on replication, keep it lagged, but orphan it + tablet.Type = topo.TYPE_LAG_ORPHAN + return topo.UpdateTablet(ctx, agent.TopoServer, tablet) } - - // Insert the new tablet location in the replication graph now that - // we've updated the tablet. - err = topo.UpdateTabletReplicationData(ctx, agent.TopoServer, tablet.Tablet) - if err != nil && err != topo.ErrNodeExists { + if err = agent.Mysqld.RestartSlave(rsd.ReplicationStatus, rsd.WaitPosition, rsd.TimePromoted); err != nil { return err } - + // Complete the special orphan accounting. + if tablet.Type == topo.TYPE_LAG_ORPHAN { + tablet.Type = topo.TYPE_LAG + return topo.UpdateTablet(ctx, agent.TopoServer, tablet) + } return nil } // SlaveWasRestarted updates the parent record for a tablet. -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) SlaveWasRestarted(ctx context.Context, swrd *actionnode.SlaveWasRestartedArgs) error { tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) if err != nil { return err } - // Once this action completes, update authoritive tablet node first. - tablet.Parent = swrd.Parent + // Once this action completes, update authoritative tablet node first. if tablet.Type == topo.TYPE_MASTER { tablet.Type = topo.TYPE_SPARE tablet.State = topo.STATE_READ_ONLY @@ -726,7 +523,7 @@ func (agent *ActionAgent) SlaveWasRestarted(ctx context.Context, swrd *actionnod // BreakSlaves will tinker with the replication stream in a way that // will stop all the slaves. -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) BreakSlaves(ctx context.Context) error { return agent.Mysqld.BreakSlaves() } @@ -737,8 +534,6 @@ func (agent *ActionAgent) updateReplicationGraphForPromotedSlave(ctx context.Con // Update tablet regardless - trend towards consistency. tablet.State = topo.STATE_READ_WRITE tablet.Type = topo.TYPE_MASTER - tablet.Parent.Cell = "" - tablet.Parent.Uid = topo.NO_TABLET tablet.Health = nil err := topo.UpdateTablet(ctx, agent.TopoServer, tablet) if err != nil { @@ -764,7 +559,7 @@ func (agent *ActionAgent) updateReplicationGraphForPromotedSlave(ctx context.Con // // Snapshot takes a db snapshot -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) Snapshot(ctx context.Context, args *actionnode.SnapshotArgs, logger logutil.Logger) (*actionnode.SnapshotReply, error) { // update our type to TYPE_BACKUP tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) @@ -785,14 +580,14 @@ func (agent *ActionAgent) Snapshot(ctx context.Context, args *actionnode.Snapsho tablet.Tablet.Type = topo.TYPE_BACKUP err = topo.UpdateTablet(ctx, agent.TopoServer, tablet) } else { - err = topotools.ChangeType(agent.TopoServer, tablet.Alias, topo.TYPE_BACKUP, make(map[string]string), true /*runHooks*/) + err = topotools.ChangeType(ctx, agent.TopoServer, tablet.Alias, topo.TYPE_BACKUP, make(map[string]string), true /*runHooks*/) } if err != nil { return nil, err } // let's update our internal state (stop query service and other things) - if err := agent.refreshTablet("snapshotStart"); err != nil { + if err := agent.refreshTablet(ctx, "snapshotStart"); err != nil { return nil, fmt.Errorf("failed to update state before snaphost: %v", err) } @@ -814,12 +609,12 @@ func (agent *ActionAgent) Snapshot(ctx context.Context, args *actionnode.Snapsho log.Infof("change type back after snapshot: %v", newType) } } - if tablet.Parent.Uid == topo.NO_TABLET && args.ForceMasterSnapshot && newType != topo.TYPE_SNAPSHOT_SOURCE { + if originalType == topo.TYPE_MASTER && args.ForceMasterSnapshot && newType != topo.TYPE_SNAPSHOT_SOURCE { log.Infof("force change type backup -> master: %v", tablet.Alias) tablet.Tablet.Type = topo.TYPE_MASTER err = topo.UpdateTablet(ctx, agent.TopoServer, tablet) } else { - err = topotools.ChangeType(agent.TopoServer, tablet.Alias, newType, nil, true /*runHooks*/) + err = topotools.ChangeType(ctx, agent.TopoServer, tablet.Alias, newType, nil, true /*runHooks*/) } if err != nil { // failure in changing the topology type is probably worse, @@ -838,19 +633,23 @@ func (agent *ActionAgent) Snapshot(ctx context.Context, args *actionnode.Snapsho SlaveStartRequired: slaveStartRequired, ReadOnly: readOnly, } - if tablet.Parent.Uid == topo.NO_TABLET { + if tablet.Type == topo.TYPE_MASTER { // If this is a master, this will be the new parent. - // FIXME(msolomon) this doesn't work in hierarchical replication. sr.ParentAlias = tablet.Alias } else { - sr.ParentAlias = tablet.Parent + // Otherwise get the master from the shard record + si, err := agent.TopoServer.GetShard(tablet.Keyspace, tablet.Shard) + if err != nil { + return nil, err + } + sr.ParentAlias = si.MasterAlias } return sr, nil } // SnapshotSourceEnd restores the state of the server after a // Snapshot(server_mode =true) -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) SnapshotSourceEnd(ctx context.Context, args *actionnode.SnapshotSourceEndArgs) error { tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) if err != nil { @@ -871,7 +670,7 @@ func (agent *ActionAgent) SnapshotSourceEnd(ctx context.Context, args *actionnod tablet.Tablet.Type = topo.TYPE_MASTER err = topo.UpdateTablet(ctx, agent.TopoServer, tablet) } else { - err = topotools.ChangeType(agent.TopoServer, tablet.Alias, args.OriginalType, make(map[string]string), true /*runHooks*/) + err = topotools.ChangeType(ctx, agent.TopoServer, tablet.Alias, args.OriginalType, make(map[string]string), true /*runHooks*/) } return err @@ -883,7 +682,7 @@ func (agent *ActionAgent) SnapshotSourceEnd(ctx context.Context, args *actionnod // a successful ReserveForRestore but a failed Snapshot) // - to SCRAP if something in the process on the target host fails // - to SPARE if the clone works -func (agent *ActionAgent) changeTypeToRestore(ctx context.Context, tablet, sourceTablet *topo.TabletInfo, parentAlias topo.TabletAlias, keyRange key.KeyRange) error { +func (agent *ActionAgent) changeTypeToRestore(ctx context.Context, tablet, sourceTablet *topo.TabletInfo, keyRange key.KeyRange) error { // run the optional preflight_assigned hook hk := hook.NewSimpleHook("preflight_assigned") topotools.ConfigureTabletHook(hk, agent.TabletAlias) @@ -892,7 +691,6 @@ func (agent *ActionAgent) changeTypeToRestore(ctx context.Context, tablet, sourc } // change the type - tablet.Parent = parentAlias tablet.Keyspace = sourceTablet.Keyspace tablet.Shard = sourceTablet.Shard tablet.Type = topo.TYPE_RESTORE @@ -908,7 +706,7 @@ func (agent *ActionAgent) changeTypeToRestore(ctx context.Context, tablet, sourc // ReserveForRestore reserves the current tablet for an upcoming // restore operation. -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) ReserveForRestore(ctx context.Context, args *actionnode.ReserveForRestoreArgs) error { // first check mysql, no need to go further if we can't restore if err := agent.Mysqld.ValidateCloneTarget(agent.hookExtraEnv()); err != nil { @@ -930,20 +728,10 @@ func (agent *ActionAgent) ReserveForRestore(ctx context.Context, args *actionnod return err } - // find the parent tablet alias we will be using - var parentAlias topo.TabletAlias - if sourceTablet.Parent.Uid == topo.NO_TABLET { - // If this is a master, this will be the new parent. - // FIXME(msolomon) this doesn't work in hierarchical replication. - parentAlias = sourceTablet.Alias - } else { - parentAlias = sourceTablet.Parent - } - - return agent.changeTypeToRestore(ctx, tablet, sourceTablet, parentAlias, sourceTablet.KeyRange) + return agent.changeTypeToRestore(ctx, tablet, sourceTablet, sourceTablet.KeyRange) } -func fetchAndParseJsonFile(addr, filename string, result interface{}) error { +func fetchAndParseJSONFile(addr, filename string, result interface{}) error { // read the manifest murl := "http://" + addr + filename resp, err := http.Get(murl) @@ -963,13 +751,15 @@ func fetchAndParseJsonFile(addr, filename string, result interface{}) error { return json.Unmarshal(data, result) } -// Operate on restore tablet. +// Restore stops the tablet's mysqld, replaces its data folder with a snapshot, +// and then restarts it. +// // Check that the SnapshotManifest is valid and the master has not changed. // Shutdown mysqld. // Load the snapshot from source tablet. // Restart mysqld and replication. // Put tablet into the replication graph as a spare. -// Should be called under RpcWrapLockAction. +// Should be called under RPCWrapLockAction. func (agent *ActionAgent) Restore(ctx context.Context, args *actionnode.RestoreArgs, logger logutil.Logger) error { // read our current tablet, verify its state tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) @@ -1005,12 +795,12 @@ func (agent *ActionAgent) Restore(ctx context.Context, args *actionnode.RestoreA // read & unpack the manifest sm := new(mysqlctl.SnapshotManifest) - if err := fetchAndParseJsonFile(sourceTablet.Addr(), args.SrcFilePath, sm); err != nil { + if err := fetchAndParseJSONFile(sourceTablet.Addr(), args.SrcFilePath, sm); err != nil { return err } if !args.WasReserved { - if err := agent.changeTypeToRestore(ctx, tablet, sourceTablet, parentTablet.Alias, sourceTablet.KeyRange); err != nil { + if err := agent.changeTypeToRestore(ctx, tablet, sourceTablet, sourceTablet.KeyRange); err != nil { return err } } @@ -1021,7 +811,7 @@ func (agent *ActionAgent) Restore(ctx context.Context, args *actionnode.RestoreA // do the work if err := agent.Mysqld.RestoreFromSnapshot(l, sm, args.FetchConcurrency, args.FetchRetryCount, args.DontWaitForSlaveStart, agent.hookExtraEnv()); err != nil { log.Errorf("RestoreFromSnapshot failed (%v), scrapping", err) - if err := topotools.Scrap(agent.TopoServer, agent.TabletAlias, false); err != nil { + if err := topotools.Scrap(ctx, agent.TopoServer, agent.TabletAlias, false); err != nil { log.Errorf("Failed to Scrap after failed RestoreFromSnapshot: %v", err) } @@ -1032,148 +822,5 @@ func (agent *ActionAgent) Restore(ctx context.Context, args *actionnode.RestoreA agent.ReloadSchema(ctx) // change to TYPE_SPARE, we're done! - return topotools.ChangeType(agent.TopoServer, agent.TabletAlias, topo.TYPE_SPARE, nil, true) -} - -// MultiSnapshot takes a multi-part snapshot -// Should be called under RpcWrapLockAction. -func (agent *ActionAgent) MultiSnapshot(ctx context.Context, args *actionnode.MultiSnapshotArgs, logger logutil.Logger) (*actionnode.MultiSnapshotReply, error) { - tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) - if err != nil { - return nil, err - } - ki, err := agent.TopoServer.GetKeyspace(tablet.Keyspace) - if err != nil { - return nil, err - } - - if tablet.Type != topo.TYPE_BACKUP { - return nil, fmt.Errorf("expected backup type, not %v", tablet.Type) - } - - // create the loggers: tee to console and source - l := logutil.NewTeeLogger(logutil.NewConsoleLogger(), logger) - - filenames, err := agent.Mysqld.CreateMultiSnapshot(l, args.KeyRanges, tablet.DbName(), ki.ShardingColumnName, ki.ShardingColumnType, tablet.Addr(), false, args.Concurrency, args.Tables, args.ExcludeTables, args.SkipSlaveRestart, args.MaximumFilesize, agent.hookExtraEnv()) - if err != nil { - return nil, err - } - - sr := &actionnode.MultiSnapshotReply{ManifestPaths: filenames} - if tablet.Parent.Uid == topo.NO_TABLET { - // If this is a master, this will be the new parent. - // FIXME(msolomon) this doens't work in hierarchical replication. - sr.ParentAlias = tablet.Alias - } else { - sr.ParentAlias = tablet.Parent - } - return sr, nil -} - -// MultiRestore performs the multi-part restore. -// Should be called under RpcWrapLockAction. -func (agent *ActionAgent) MultiRestore(ctx context.Context, args *actionnode.MultiRestoreArgs, logger logutil.Logger) error { - // read our current tablet, verify its state - // we only support restoring to the master or active replicas - tablet, err := agent.TopoServer.GetTablet(agent.TabletAlias) - if err != nil { - return err - } - if tablet.Type != topo.TYPE_MASTER && !topo.IsSlaveType(tablet.Type) { - return fmt.Errorf("expected master, or slave type, not %v", tablet.Type) - } - // get source tablets addresses - sourceAddrs := make([]*url.URL, len(args.SrcTabletAliases)) - keyRanges := make([]key.KeyRange, len(args.SrcTabletAliases)) - fromStoragePaths := make([]string, len(args.SrcTabletAliases)) - for i, alias := range args.SrcTabletAliases { - t, e := agent.TopoServer.GetTablet(alias) - if e != nil { - return e - } - sourceAddrs[i] = &url.URL{ - Host: t.Addr(), - Path: "/" + t.DbName(), - } - keyRanges[i], e = key.KeyRangesOverlap(tablet.KeyRange, t.KeyRange) - if e != nil { - return e - } - fromStoragePaths[i] = path.Join(agent.Mysqld.SnapshotDir, "from-storage", fmt.Sprintf("from-%v-%v", keyRanges[i].Start.Hex(), keyRanges[i].End.Hex())) - } - - // change type to restore, no change to replication graph - originalType := tablet.Type - tablet.Type = topo.TYPE_RESTORE - err = topo.UpdateTablet(ctx, agent.TopoServer, tablet) - if err != nil { - return err - } - - // first try to get the data from a remote storage - wg := sync.WaitGroup{} - rec := concurrency.AllErrorRecorder{} - for i, alias := range args.SrcTabletAliases { - wg.Add(1) - go func(i int, alias topo.TabletAlias) { - defer wg.Done() - h := hook.NewSimpleHook("copy_snapshot_from_storage") - h.ExtraEnv = make(map[string]string) - for k, v := range agent.hookExtraEnv() { - h.ExtraEnv[k] = v - } - h.ExtraEnv["KEYRANGE"] = fmt.Sprintf("%v-%v", keyRanges[i].Start.Hex(), keyRanges[i].End.Hex()) - h.ExtraEnv["SNAPSHOT_PATH"] = fromStoragePaths[i] - h.ExtraEnv["SOURCE_TABLET_ALIAS"] = alias.String() - hr := h.Execute() - if hr.ExitStatus != hook.HOOK_SUCCESS { - rec.RecordError(fmt.Errorf("%v hook failed(%v): %v", h.Name, hr.ExitStatus, hr.Stderr)) - } - }(i, alias) - } - wg.Wait() - // stop replication for slaves, so it doesn't interfere - if topo.IsSlaveType(originalType) { - if err := agent.Mysqld.StopSlave(map[string]string{"TABLET_ALIAS": tablet.Alias.String()}); err != nil { - return err - } - } - - // create the loggers: tee to console and source - l := logutil.NewTeeLogger(logutil.NewConsoleLogger(), logger) - - // parse the strategy - strategy, err := mysqlctl.NewSplitStrategy(l, args.Strategy) - if err != nil { - return fmt.Errorf("error parsing strategy: %v", err) - } - - // run the action, scrap if it fails - if rec.HasErrors() { - log.Infof("Got errors trying to get snapshots from storage, trying to get them from original tablets: %v", rec.Error()) - err = agent.Mysqld.MultiRestore(l, tablet.DbName(), keyRanges, sourceAddrs, nil, args.Concurrency, args.FetchConcurrency, args.InsertTableConcurrency, args.FetchRetryCount, strategy) - } else { - log.Infof("Got snapshots from storage, reading them from disk directly") - err = agent.Mysqld.MultiRestore(l, tablet.DbName(), keyRanges, nil, fromStoragePaths, args.Concurrency, args.FetchConcurrency, args.InsertTableConcurrency, args.FetchRetryCount, strategy) - } - if err != nil { - if e := topotools.Scrap(agent.TopoServer, agent.TabletAlias, false); e != nil { - log.Errorf("Failed to Scrap after failed RestoreFromMultiSnapshot: %v", e) - } - return err - } - - // reload the schema - agent.ReloadSchema(ctx) - - // restart replication - if topo.IsSlaveType(originalType) { - if err := agent.Mysqld.StartSlave(map[string]string{"TABLET_ALIAS": tablet.Alias.String()}); err != nil { - return err - } - } - - // restore type back - tablet.Type = originalType - return topo.UpdateTablet(ctx, agent.TopoServer, tablet) + return topotools.ChangeType(ctx, agent.TopoServer, agent.TabletAlias, topo.TYPE_SPARE, nil, true) } diff --git a/go/vt/tabletmanager/agentrpctest/test_agent_rpc.go b/go/vt/tabletmanager/agentrpctest/test_agent_rpc.go index aebb224177e..79688bddf75 100644 --- a/go/vt/tabletmanager/agentrpctest/test_agent_rpc.go +++ b/go/vt/tabletmanager/agentrpctest/test_agent_rpc.go @@ -11,34 +11,33 @@ import ( "testing" "time" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/sqltypes" blproto "github.com/youtube/vitess/go/vt/binlog/proto" "github.com/youtube/vitess/go/vt/hook" - "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/logutil" myproto "github.com/youtube/vitess/go/vt/mysqlctl/proto" "github.com/youtube/vitess/go/vt/tabletmanager" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/tabletmanager/tmclient" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) -// fakeRpcAgent implements tabletmanager.RpcAgent and fills in all +// fakeRPCAgent implements tabletmanager.RPCAgent and fills in all // possible values in all APIs -type fakeRpcAgent struct { +type fakeRPCAgent struct { t *testing.T } -// NewFakeRpcAgent returns a fake tabletmanager.RpcAgent that's just a mirror. -func NewFakeRpcAgent(t *testing.T) tabletmanager.RpcAgent { - return &fakeRpcAgent{t} +// NewFakeRPCAgent returns a fake tabletmanager.RPCAgent that's just a mirror. +func NewFakeRPCAgent(t *testing.T) tabletmanager.RPCAgent { + return &fakeRPCAgent{t} } // The way this test is organized is a repetition of: // - static test data for a call -// - implementation of the tabletmanager.RpcAgent method for fakeRpcAgent +// - implementation of the tabletmanager.RPCAgent method for fakeRPCAgent // - static test method for the call (client side) // for each possible method of the interface. // This makes the implementations all in the same spot. @@ -92,12 +91,12 @@ func compareLoggedStuff(t *testing.T, name string, logChannel <-chan *logutil.Lo // Various read-only methods // -func (fra *fakeRpcAgent) Ping(ctx context.Context, args string) string { +func (fra *fakeRPCAgent) Ping(ctx context.Context, args string) string { return args } -func agentRpcTestPing(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.Ping(ctx, ti, time.Minute) +func agentRPCTestPing(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.Ping(ctx, ti) if err != nil { t.Errorf("Ping failed: %v", err) } @@ -130,15 +129,15 @@ var testGetSchemaReply = &myproto.SchemaDefinition{ Version: "xxx", } -func (fra *fakeRpcAgent) GetSchema(ctx context.Context, tables, excludeTables []string, includeViews bool) (*myproto.SchemaDefinition, error) { +func (fra *fakeRPCAgent) GetSchema(ctx context.Context, tables, excludeTables []string, includeViews bool) (*myproto.SchemaDefinition, error) { compare(fra.t, "GetSchema tables", tables, testGetSchemaTables) compare(fra.t, "GetSchema excludeTables", excludeTables, testGetSchemaExcludeTables) compareBool(fra.t, "GetSchema includeViews", includeViews) return testGetSchemaReply, nil } -func agentRpcTestGetSchema(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - result, err := client.GetSchema(ctx, ti, testGetSchemaTables, testGetSchemaExcludeTables, true, time.Minute) +func agentRPCTestGetSchema(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + result, err := client.GetSchema(ctx, ti, testGetSchemaTables, testGetSchemaExcludeTables, true) compareError(t, "GetSchema", err, result, testGetSchemaReply) } @@ -177,12 +176,12 @@ var testGetPermissionsReply = &myproto.Permissions{ }, } -func (fra *fakeRpcAgent) GetPermissions(ctx context.Context) (*myproto.Permissions, error) { +func (fra *fakeRPCAgent) GetPermissions(ctx context.Context) (*myproto.Permissions, error) { return testGetPermissionsReply, nil } -func agentRpcTestGetPermissions(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - result, err := client.GetPermissions(ctx, ti, time.Minute) +func agentRPCTestGetPermissions(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + result, err := client.GetPermissions(ctx, ti) compareError(t, "GetPermissions", err, result, testGetPermissionsReply) } @@ -192,21 +191,21 @@ func agentRpcTestGetPermissions(ctx context.Context, t *testing.T, client tmclie var testSetReadOnlyExpectedValue bool -func (fra *fakeRpcAgent) SetReadOnly(ctx context.Context, rdonly bool) error { +func (fra *fakeRPCAgent) SetReadOnly(ctx context.Context, rdonly bool) error { if rdonly != testSetReadOnlyExpectedValue { fra.t.Errorf("Wrong SetReadOnly value: got %v expected %v", rdonly, testSetReadOnlyExpectedValue) } return nil } -func agentRpcTestSetReadOnly(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { +func agentRPCTestSetReadOnly(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { testSetReadOnlyExpectedValue = true - err := client.SetReadOnly(ctx, ti, time.Minute) + err := client.SetReadOnly(ctx, ti) if err != nil { t.Errorf("SetReadOnly failed: %v", err) } testSetReadOnlyExpectedValue = false - err = client.SetReadWrite(ctx, ti, time.Minute) + err = client.SetReadWrite(ctx, ti) if err != nil { t.Errorf("SetReadWrite failed: %v", err) } @@ -214,39 +213,39 @@ func agentRpcTestSetReadOnly(ctx context.Context, t *testing.T, client tmclient. var testChangeTypeValue = topo.TYPE_REPLICA -func (fra *fakeRpcAgent) ChangeType(ctx context.Context, tabletType topo.TabletType) error { +func (fra *fakeRPCAgent) ChangeType(ctx context.Context, tabletType topo.TabletType) error { compare(fra.t, "ChangeType tabletType", tabletType, testChangeTypeValue) return nil } -func agentRpcTestChangeType(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.ChangeType(ctx, ti, testChangeTypeValue, time.Minute) +func agentRPCTestChangeType(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.ChangeType(ctx, ti, testChangeTypeValue) if err != nil { t.Errorf("ChangeType failed: %v", err) } } -var testScrapError = fmt.Errorf("Scrap Failed!") +var errTestScrap = fmt.Errorf("Scrap Failed!") -func (fra *fakeRpcAgent) Scrap(ctx context.Context) error { - return testScrapError +func (fra *fakeRPCAgent) Scrap(ctx context.Context) error { + return errTestScrap } -func agentRpcTestScrap(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.Scrap(ctx, ti, time.Minute) - if strings.Index(err.Error(), testScrapError.Error()) == -1 { - t.Errorf("Unexpected Scrap result: got %v expected %v", err, testScrapError) +func agentRPCTestScrap(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.Scrap(ctx, ti) + if strings.Index(err.Error(), errTestScrap.Error()) == -1 { + t.Errorf("Unexpected Scrap result: got %v expected %v", err, errTestScrap) } } var testSleepDuration = time.Minute -func (fra *fakeRpcAgent) Sleep(ctx context.Context, duration time.Duration) { +func (fra *fakeRPCAgent) Sleep(ctx context.Context, duration time.Duration) { compare(fra.t, "Sleep duration", duration, testSleepDuration) } -func agentRpcTestSleep(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.Sleep(ctx, ti, testSleepDuration, time.Minute) +func agentRPCTestSleep(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.Sleep(ctx, ti, testSleepDuration) if err != nil { t.Errorf("Sleep failed: %v", err) } @@ -266,27 +265,27 @@ var testExecuteHookHookResult = &hook.HookResult{ Stderr: "err", } -func (fra *fakeRpcAgent) ExecuteHook(ctx context.Context, hk *hook.Hook) *hook.HookResult { +func (fra *fakeRPCAgent) ExecuteHook(ctx context.Context, hk *hook.Hook) *hook.HookResult { compare(fra.t, "ExecuteHook hook", hk, testExecuteHookHook) return testExecuteHookHookResult } -func agentRpcTestExecuteHook(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - hr, err := client.ExecuteHook(ctx, ti, testExecuteHookHook, time.Minute) +func agentRPCTestExecuteHook(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + hr, err := client.ExecuteHook(ctx, ti, testExecuteHookHook) compareError(t, "ExecuteHook", err, hr, testExecuteHookHookResult) } var testRefreshStateCalled = false -func (fra *fakeRpcAgent) RefreshState(ctx context.Context) { +func (fra *fakeRPCAgent) RefreshState(ctx context.Context) { if testRefreshStateCalled { fra.t.Errorf("RefreshState called multiple times?") } testRefreshStateCalled = true } -func agentRpcTestRefreshState(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.RefreshState(ctx, ti, time.Minute) +func agentRPCTestRefreshState(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.RefreshState(ctx, ti) if err != nil { t.Errorf("RefreshState failed: %v", err) } @@ -297,28 +296,91 @@ func agentRpcTestRefreshState(ctx context.Context, t *testing.T, client tmclient var testRunHealthCheckValue = topo.TYPE_RDONLY -func (fra *fakeRpcAgent) RunHealthCheck(ctx context.Context, targetTabletType topo.TabletType) { +func (fra *fakeRPCAgent) RunHealthCheck(ctx context.Context, targetTabletType topo.TabletType) { compare(fra.t, "RunHealthCheck tabletType", targetTabletType, testRunHealthCheckValue) } -func agentRpcTestRunHealthCheck(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.RunHealthCheck(ctx, ti, testRunHealthCheckValue, time.Minute) +func agentRPCTestRunHealthCheck(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.RunHealthCheck(ctx, ti, testRunHealthCheckValue) if err != nil { t.Errorf("RunHealthCheck failed: %v", err) } } +// this test is a bit of a hack: we write something on the channel +// upon registration, and we also return an error, so the streaming query +// ends right there. Otherwise we have no real way to trigger a real +// communication error, that ends the streaming. +var testHealthStreamHealthStreamReply = &actionnode.HealthStreamReply{ + Tablet: &topo.Tablet{ + Alias: topo.TabletAlias{ + Cell: "cell1", + Uid: 123, + }, + Hostname: "host", + IPAddr: "1.2.3.4", + Portmap: map[string]int{ + "vt": 2345, + }, + Tags: map[string]string{ + "tag1": "value1", + }, + Health: map[string]string{ + "health1": "value1", + }, + Keyspace: "keyspace", + Shard: "shard", + Type: topo.TYPE_MASTER, + State: topo.STATE_READ_WRITE, + DbNameOverride: "overruled!", + }, + BinlogPlayerMapSize: 3, + HealthError: "bad rep bad", + ReplicationDelay: 50 * time.Second, +} +var testRegisterHealthStreamError = "to trigger a server error" + +func (fra *fakeRPCAgent) RegisterHealthStream(c chan<- *actionnode.HealthStreamReply) (int, error) { + c <- testHealthStreamHealthStreamReply + return 0, fmt.Errorf(testRegisterHealthStreamError) +} + +func (fra *fakeRPCAgent) UnregisterHealthStream(int) error { + return nil +} + +func agentRPCTestHealthStream(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + c, errFunc, err := client.HealthStream(ctx, ti) + if err != nil { + t.Fatalf("HealthStream failed: %v", err) + } + // channel should have one response, then closed + hsr, ok := <-c + if !ok { + t.Fatalf("HealthStream got no response") + } + _, ok = <-c + if ok { + t.Fatalf("HealthStream wasn't closed") + } + err = errFunc() + if !strings.Contains(err.Error(), testRegisterHealthStreamError) { + t.Fatalf("HealthStream failed with the wrong error: %v", err) + } + compareError(t, "HealthStream", nil, *hsr, *testHealthStreamHealthStreamReply) +} + var testReloadSchemaCalled = false -func (fra *fakeRpcAgent) ReloadSchema(ctx context.Context) { +func (fra *fakeRPCAgent) ReloadSchema(ctx context.Context) { if testReloadSchemaCalled { fra.t.Errorf("ReloadSchema called multiple times?") } testReloadSchemaCalled = true } -func agentRpcTestReloadSchema(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.ReloadSchema(ctx, ti, time.Minute) +func agentRPCTestReloadSchema(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.ReloadSchema(ctx, ti) if err != nil { t.Errorf("ReloadSchema failed: %v", err) } @@ -333,13 +395,13 @@ var testSchemaChangeResult = &myproto.SchemaChangeResult{ AfterSchema: testGetSchemaReply, } -func (fra *fakeRpcAgent) PreflightSchema(ctx context.Context, change string) (*myproto.SchemaChangeResult, error) { +func (fra *fakeRPCAgent) PreflightSchema(ctx context.Context, change string) (*myproto.SchemaChangeResult, error) { compare(fra.t, "PreflightSchema result", change, testPreflightSchema) return testSchemaChangeResult, nil } -func agentRpcTestPreflightSchema(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - scr, err := client.PreflightSchema(ctx, ti, testPreflightSchema, time.Minute) +func agentRPCTestPreflightSchema(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + scr, err := client.PreflightSchema(ctx, ti, testPreflightSchema) compareError(t, "PreflightSchema", err, scr, testSchemaChangeResult) } @@ -351,13 +413,13 @@ var testSchemaChange = &myproto.SchemaChange{ AfterSchema: testGetSchemaReply, } -func (fra *fakeRpcAgent) ApplySchema(ctx context.Context, change *myproto.SchemaChange) (*myproto.SchemaChangeResult, error) { +func (fra *fakeRPCAgent) ApplySchema(ctx context.Context, change *myproto.SchemaChange) (*myproto.SchemaChangeResult, error) { compare(fra.t, "ApplySchema change", change, testSchemaChange) return testSchemaChangeResult, nil } -func agentRpcTestApplySchema(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - scr, err := client.ApplySchema(ctx, ti, testSchemaChange, time.Minute) +func agentRPCTestApplySchema(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + scr, err := client.ApplySchema(ctx, ti, testSchemaChange) compareError(t, "ApplySchema", err, scr, testSchemaChangeResult) } @@ -383,7 +445,7 @@ var testExecuteFetchResult = &mproto.QueryResult{ }, } -func (fra *fakeRpcAgent) ExecuteFetch(ctx context.Context, query string, maxrows int, wantFields, disableBinlogs bool) (*mproto.QueryResult, error) { +func (fra *fakeRPCAgent) ExecuteFetch(ctx context.Context, query string, maxrows int, wantFields, disableBinlogs bool) (*mproto.QueryResult, error) { compare(fra.t, "ExecuteFetch query", query, testExecuteFetchQuery) compare(fra.t, "ExecuteFetch maxrows", maxrows, testExecuteFetchMaxRows) compareBool(fra.t, "ExecuteFetch wantFields", wantFields) @@ -391,8 +453,8 @@ func (fra *fakeRpcAgent) ExecuteFetch(ctx context.Context, query string, maxrows return testExecuteFetchResult, nil } -func agentRpcTestExecuteFetch(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - qr, err := client.ExecuteFetch(ctx, ti, testExecuteFetchQuery, testExecuteFetchMaxRows, true, true, time.Minute) +func agentRPCTestExecuteFetch(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + qr, err := client.ExecuteFetch(ctx, ti, testExecuteFetchQuery, testExecuteFetchMaxRows, true, true) compareError(t, "ExecuteFetch", err, qr, testExecuteFetchResult) } @@ -415,12 +477,12 @@ var testReplicationStatus = &myproto.ReplicationStatus{ MasterConnectRetry: 12, } -func (fra *fakeRpcAgent) SlaveStatus(ctx context.Context) (*myproto.ReplicationStatus, error) { +func (fra *fakeRPCAgent) SlaveStatus(ctx context.Context) (*myproto.ReplicationStatus, error) { return testReplicationStatus, nil } -func agentRpcTestSlaveStatus(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - rs, err := client.SlaveStatus(ctx, ti, time.Minute) +func agentRPCTestSlaveStatus(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + rs, err := client.SlaveStatus(ctx, ti) compareError(t, "SlaveStatus", err, rs, testReplicationStatus) } @@ -433,23 +495,23 @@ var testReplicationPosition = myproto.ReplicationPosition{ } var testWaitSlavePositionWaitTimeout = time.Hour -func (fra *fakeRpcAgent) WaitSlavePosition(ctx context.Context, position myproto.ReplicationPosition, waitTimeout time.Duration) (*myproto.ReplicationStatus, error) { +func (fra *fakeRPCAgent) WaitSlavePosition(ctx context.Context, position myproto.ReplicationPosition, waitTimeout time.Duration) (*myproto.ReplicationStatus, error) { compare(fra.t, "WaitSlavePosition position", position, testReplicationPosition) compare(fra.t, "WaitSlavePosition waitTimeout", waitTimeout, testWaitSlavePositionWaitTimeout) return testReplicationStatus, nil } -func agentRpcTestWaitSlavePosition(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { +func agentRPCTestWaitSlavePosition(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { rs, err := client.WaitSlavePosition(ctx, ti, testReplicationPosition, testWaitSlavePositionWaitTimeout) compareError(t, "WaitSlavePosition", err, rs, testReplicationStatus) } -func (fra *fakeRpcAgent) MasterPosition(ctx context.Context) (myproto.ReplicationPosition, error) { +func (fra *fakeRPCAgent) MasterPosition(ctx context.Context) (myproto.ReplicationPosition, error) { return testReplicationPosition, nil } -func agentRpcTestMasterPosition(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - rs, err := client.MasterPosition(ctx, ti, time.Minute) +func agentRPCTestMasterPosition(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + rs, err := client.MasterPosition(ctx, ti) compareError(t, "MasterPosition", err, rs, testReplicationPosition) } @@ -464,73 +526,73 @@ var testRestartSlaveData = &actionnode.RestartSlaveData{ Force: true, } -func (fra *fakeRpcAgent) ReparentPosition(ctx context.Context, rp *myproto.ReplicationPosition) (*actionnode.RestartSlaveData, error) { +func (fra *fakeRPCAgent) ReparentPosition(ctx context.Context, rp *myproto.ReplicationPosition) (*actionnode.RestartSlaveData, error) { compare(fra.t, "ReparentPosition position", rp.GTIDSet, testReplicationPosition.GTIDSet) return testRestartSlaveData, nil } -func agentRpcTestReparentPosition(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - rsd, err := client.ReparentPosition(ctx, ti, &testReplicationPosition, time.Minute) +func agentRPCTestReparentPosition(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + rsd, err := client.ReparentPosition(ctx, ti, &testReplicationPosition) compareError(t, "ReparentPosition", err, rsd, testRestartSlaveData) } var testStopSlaveCalled = false -func (fra *fakeRpcAgent) StopSlave(ctx context.Context) error { +func (fra *fakeRPCAgent) StopSlave(ctx context.Context) error { testStopSlaveCalled = true return nil } -func agentRpcTestStopSlave(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.StopSlave(ctx, ti, time.Minute) +func agentRPCTestStopSlave(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.StopSlave(ctx, ti) compareError(t, "StopSlave", err, true, testStopSlaveCalled) } var testStopSlaveMinimumWaitTime = time.Hour -func (fra *fakeRpcAgent) StopSlaveMinimum(ctx context.Context, position myproto.ReplicationPosition, waitTime time.Duration) (*myproto.ReplicationStatus, error) { +func (fra *fakeRPCAgent) StopSlaveMinimum(ctx context.Context, position myproto.ReplicationPosition, waitTime time.Duration) (*myproto.ReplicationStatus, error) { compare(fra.t, "StopSlaveMinimum position", position.GTIDSet, testReplicationPosition.GTIDSet) compare(fra.t, "StopSlaveMinimum waitTime", waitTime, testStopSlaveMinimumWaitTime) return testReplicationStatus, nil } -func agentRpcTestStopSlaveMinimum(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { +func agentRPCTestStopSlaveMinimum(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { rs, err := client.StopSlaveMinimum(ctx, ti, testReplicationPosition, testStopSlaveMinimumWaitTime) compareError(t, "StopSlave", err, rs, testReplicationStatus) } var testStartSlaveCalled = false -func (fra *fakeRpcAgent) StartSlave(ctx context.Context) error { +func (fra *fakeRPCAgent) StartSlave(ctx context.Context) error { testStartSlaveCalled = true return nil } -func agentRpcTestStartSlave(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.StartSlave(ctx, ti, time.Minute) +func agentRPCTestStartSlave(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.StartSlave(ctx, ti) compareError(t, "StartSlave", err, true, testStartSlaveCalled) } var testTabletExternallyReparentedCalled = false -func (fra *fakeRpcAgent) TabletExternallyReparented(ctx context.Context, externalID string, actionTimeout time.Duration) error { +func (fra *fakeRPCAgent) TabletExternallyReparented(ctx context.Context, externalID string) error { testTabletExternallyReparentedCalled = true return nil } -func agentRpcTestTabletExternallyReparented(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.TabletExternallyReparented(ctx, ti, "", time.Minute) +func agentRPCTestTabletExternallyReparented(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.TabletExternallyReparented(ctx, ti, "") compareError(t, "TabletExternallyReparented", err, true, testTabletExternallyReparentedCalled) } var testGetSlavesResult = []string{"slave1", "slave2"} -func (fra *fakeRpcAgent) GetSlaves(ctx context.Context) ([]string, error) { +func (fra *fakeRPCAgent) GetSlaves(ctx context.Context) ([]string, error) { return testGetSlavesResult, nil } -func agentRpcTestGetSlaves(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - s, err := client.GetSlaves(ctx, ti, time.Minute) +func agentRPCTestGetSlaves(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + s, err := client.GetSlaves(ctx, ti) compareError(t, "GetSlaves", err, s, testGetSlavesResult) } @@ -541,14 +603,14 @@ var testBlpPosition = &blproto.BlpPosition{ var testWaitBlpPositionWaitTime = time.Hour var testWaitBlpPositionCalled = false -func (fra *fakeRpcAgent) WaitBlpPosition(ctx context.Context, blpPosition *blproto.BlpPosition, waitTime time.Duration) error { +func (fra *fakeRPCAgent) WaitBlpPosition(ctx context.Context, blpPosition *blproto.BlpPosition, waitTime time.Duration) error { compare(fra.t, "WaitBlpPosition blpPosition", blpPosition, testBlpPosition) compare(fra.t, "WaitBlpPosition waitTime", waitTime, testWaitBlpPositionWaitTime) testWaitBlpPositionCalled = true return nil } -func agentRpcTestWaitBlpPosition(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { +func agentRPCTestWaitBlpPosition(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { err := client.WaitBlpPosition(ctx, ti, *testBlpPosition, testWaitBlpPositionWaitTime) compareError(t, "WaitBlpPosition", err, true, testWaitBlpPositionCalled) } @@ -559,36 +621,36 @@ var testBlpPositionList = &blproto.BlpPositionList{ }, } -func (fra *fakeRpcAgent) StopBlp(ctx context.Context) (*blproto.BlpPositionList, error) { +func (fra *fakeRPCAgent) StopBlp(ctx context.Context) (*blproto.BlpPositionList, error) { return testBlpPositionList, nil } -func agentRpcTestStopBlp(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - bpl, err := client.StopBlp(ctx, ti, time.Minute) +func agentRPCTestStopBlp(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + bpl, err := client.StopBlp(ctx, ti) compareError(t, "StopBlp", err, bpl, testBlpPositionList) } var testStartBlpCalled = false -func (fra *fakeRpcAgent) StartBlp(ctx context.Context) error { +func (fra *fakeRPCAgent) StartBlp(ctx context.Context) error { testStartBlpCalled = true return nil } -func agentRpcTestStartBlp(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.StartBlp(ctx, ti, time.Minute) +func agentRPCTestStartBlp(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.StartBlp(ctx, ti) compareError(t, "StartBlp", err, true, testStartBlpCalled) } var testRunBlpUntilWaitTime = 3 * time.Minute -func (fra *fakeRpcAgent) RunBlpUntil(ctx context.Context, bpl *blproto.BlpPositionList, waitTime time.Duration) (*myproto.ReplicationPosition, error) { +func (fra *fakeRPCAgent) RunBlpUntil(ctx context.Context, bpl *blproto.BlpPositionList, waitTime time.Duration) (*myproto.ReplicationPosition, error) { compare(fra.t, "RunBlpUntil bpl", bpl, testBlpPositionList) compare(fra.t, "RunBlpUntil waitTime", waitTime, testRunBlpUntilWaitTime) return &testReplicationPosition, nil } -func agentRpcTestRunBlpUntil(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { +func agentRPCTestRunBlpUntil(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { rp, err := client.RunBlpUntil(ctx, ti, testBlpPositionList, testRunBlpUntilWaitTime) compareError(t, "RunBlpUntil", err, rp, testReplicationPosition) } @@ -599,47 +661,47 @@ func agentRpcTestRunBlpUntil(ctx context.Context, t *testing.T, client tmclient. var testDemoteMasterCalled = false -func (fra *fakeRpcAgent) DemoteMaster(ctx context.Context) error { +func (fra *fakeRPCAgent) DemoteMaster(ctx context.Context) error { testDemoteMasterCalled = true return nil } -func agentRpcTestDemoteMaster(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.DemoteMaster(ctx, ti, time.Minute) +func agentRPCTestDemoteMaster(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.DemoteMaster(ctx, ti) compareError(t, "DemoteMaster", err, true, testDemoteMasterCalled) } -func (fra *fakeRpcAgent) PromoteSlave(ctx context.Context) (*actionnode.RestartSlaveData, error) { +func (fra *fakeRPCAgent) PromoteSlave(ctx context.Context) (*actionnode.RestartSlaveData, error) { return testRestartSlaveData, nil } -func agentRpcTestPromoteSlave(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - rsd, err := client.PromoteSlave(ctx, ti, time.Minute) +func agentRPCTestPromoteSlave(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + rsd, err := client.PromoteSlave(ctx, ti) compareError(t, "PromoteSlave", err, rsd, testRestartSlaveData) } var testSlaveWasPromotedCalled = false -func (fra *fakeRpcAgent) SlaveWasPromoted(ctx context.Context) error { +func (fra *fakeRPCAgent) SlaveWasPromoted(ctx context.Context) error { testSlaveWasPromotedCalled = true return nil } -func agentRpcTestSlaveWasPromoted(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.SlaveWasPromoted(ctx, ti, time.Minute) +func agentRPCTestSlaveWasPromoted(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.SlaveWasPromoted(ctx, ti) compareError(t, "SlaveWasPromoted", err, true, testSlaveWasPromotedCalled) } var testRestartSlaveCalled = false -func (fra *fakeRpcAgent) RestartSlave(ctx context.Context, rsd *actionnode.RestartSlaveData) error { +func (fra *fakeRPCAgent) RestartSlave(ctx context.Context, rsd *actionnode.RestartSlaveData) error { compare(fra.t, "RestartSlave rsd", rsd, testRestartSlaveData) testRestartSlaveCalled = true return nil } -func agentRpcTestRestartSlave(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.RestartSlave(ctx, ti, testRestartSlaveData, time.Minute) +func agentRPCTestRestartSlave(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.RestartSlave(ctx, ti, testRestartSlaveData) compareError(t, "RestartSlave", err, true, testRestartSlaveCalled) } @@ -651,26 +713,26 @@ var testSlaveWasRestartedArgs = &actionnode.SlaveWasRestartedArgs{ } var testSlaveWasRestartedCalled = false -func (fra *fakeRpcAgent) SlaveWasRestarted(ctx context.Context, swrd *actionnode.SlaveWasRestartedArgs) error { +func (fra *fakeRPCAgent) SlaveWasRestarted(ctx context.Context, swrd *actionnode.SlaveWasRestartedArgs) error { compare(fra.t, "SlaveWasRestarted swrd", swrd, testSlaveWasRestartedArgs) testSlaveWasRestartedCalled = true return nil } -func agentRpcTestSlaveWasRestarted(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.SlaveWasRestarted(ctx, ti, testSlaveWasRestartedArgs, time.Minute) +func agentRPCTestSlaveWasRestarted(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.SlaveWasRestarted(ctx, ti, testSlaveWasRestartedArgs) compareError(t, "RestartSlave", err, true, testRestartSlaveCalled) } var testBreakSlavesCalled = false -func (fra *fakeRpcAgent) BreakSlaves(ctx context.Context) error { +func (fra *fakeRPCAgent) BreakSlaves(ctx context.Context) error { testBreakSlavesCalled = true return nil } -func agentRpcTestBreakSlaves(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.BreakSlaves(ctx, ti, time.Minute) +func agentRPCTestBreakSlaves(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.BreakSlaves(ctx, ti) compareError(t, "BreakSlaves", err, true, testBreakSlavesCalled) } @@ -693,14 +755,14 @@ var testSnapshotReply = &actionnode.SnapshotReply{ ReadOnly: true, } -func (fra *fakeRpcAgent) Snapshot(ctx context.Context, args *actionnode.SnapshotArgs, logger logutil.Logger) (*actionnode.SnapshotReply, error) { +func (fra *fakeRPCAgent) Snapshot(ctx context.Context, args *actionnode.SnapshotArgs, logger logutil.Logger) (*actionnode.SnapshotReply, error) { compare(fra.t, "Snapshot args", args, testSnapshotArgs) logStuff(logger, 0) return testSnapshotReply, nil } -func agentRpcTestSnapshot(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - logChannel, errFunc, err := client.Snapshot(ctx, ti, testSnapshotArgs, time.Minute) +func agentRPCTestSnapshot(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + logChannel, errFunc, err := client.Snapshot(ctx, ti, testSnapshotArgs) if err != nil { t.Fatalf("Snapshot failed: %v", err) } @@ -716,14 +778,14 @@ var testSnapshotSourceEndArgs = &actionnode.SnapshotSourceEndArgs{ } var testSnapshotSourceEndCalled = false -func (fra *fakeRpcAgent) SnapshotSourceEnd(ctx context.Context, args *actionnode.SnapshotSourceEndArgs) error { +func (fra *fakeRPCAgent) SnapshotSourceEnd(ctx context.Context, args *actionnode.SnapshotSourceEndArgs) error { compare(fra.t, "SnapshotSourceEnd args", args, testSnapshotSourceEndArgs) testSnapshotSourceEndCalled = true return nil } -func agentRpcTestSnapshotSourceEnd(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.SnapshotSourceEnd(ctx, ti, testSnapshotSourceEndArgs, time.Minute) +func agentRPCTestSnapshotSourceEnd(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.SnapshotSourceEnd(ctx, ti, testSnapshotSourceEndArgs) compareError(t, "SnapshotSourceEnd", err, true, testSnapshotSourceEndCalled) } @@ -735,14 +797,14 @@ var testReserveForRestoreArgs = &actionnode.ReserveForRestoreArgs{ } var testReserveForRestoreCalled = false -func (fra *fakeRpcAgent) ReserveForRestore(ctx context.Context, args *actionnode.ReserveForRestoreArgs) error { +func (fra *fakeRPCAgent) ReserveForRestore(ctx context.Context, args *actionnode.ReserveForRestoreArgs) error { compare(fra.t, "ReserveForRestore args", args, testReserveForRestoreArgs) testReserveForRestoreCalled = true return nil } -func agentRpcTestReserveForRestore(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - err := client.ReserveForRestore(ctx, ti, testReserveForRestoreArgs, time.Minute) +func agentRPCTestReserveForRestore(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + err := client.ReserveForRestore(ctx, ti, testReserveForRestoreArgs) compareError(t, "ReserveForRestore", err, true, testReserveForRestoreCalled) } @@ -763,15 +825,15 @@ var testRestoreArgs = &actionnode.RestoreArgs{ } var testRestoreCalled = false -func (fra *fakeRpcAgent) Restore(ctx context.Context, args *actionnode.RestoreArgs, logger logutil.Logger) error { +func (fra *fakeRPCAgent) Restore(ctx context.Context, args *actionnode.RestoreArgs, logger logutil.Logger) error { compare(fra.t, "Restore args", args, testRestoreArgs) logStuff(logger, 10) testRestoreCalled = true return nil } -func agentRpcTestRestore(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - logChannel, errFunc, err := client.Restore(ctx, ti, testRestoreArgs, time.Minute) +func agentRPCTestRestore(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + logChannel, errFunc, err := client.Restore(ctx, ti, testRestoreArgs) if err != nil { t.Fatalf("Restore failed: %v", err) } @@ -780,151 +842,79 @@ func agentRpcTestRestore(ctx context.Context, t *testing.T, client tmclient.Tabl compareError(t, "Restore", err, true, testRestoreCalled) } -var testMultiSnapshotArgs = &actionnode.MultiSnapshotArgs{ - KeyRanges: []key.KeyRange{ - key.KeyRange{ - Start: "", - End: "", - }, - }, - Tables: []string{"table1", "table2"}, - ExcludeTables: []string{"etable1", "etable2"}, - Concurrency: 34, - SkipSlaveRestart: true, - MaximumFilesize: 0x2000, -} -var testMultiSnapshotReply = &actionnode.MultiSnapshotReply{ - ParentAlias: topo.TabletAlias{ - Cell: "test", - Uid: 4567, - }, - ManifestPaths: []string{"path1", "path2"}, -} - -func (fra *fakeRpcAgent) MultiSnapshot(ctx context.Context, args *actionnode.MultiSnapshotArgs, logger logutil.Logger) (*actionnode.MultiSnapshotReply, error) { - compare(fra.t, "MultiSnapshot args", args, testMultiSnapshotArgs) - logStuff(logger, 100) - return testMultiSnapshotReply, nil -} - -func agentRpcTestMultiSnapshot(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - logChannel, errFunc, err := client.MultiSnapshot(ctx, ti, testMultiSnapshotArgs, time.Minute) - if err != nil { - t.Fatalf("MultiSnapshot failed: %v", err) - } - compareLoggedStuff(t, "MultiSnapshot", logChannel, 100) - sr, err := errFunc() - compareError(t, "MultiSnapshot", err, sr, testMultiSnapshotReply) -} - -var testMultiRestoreArgs = &actionnode.MultiRestoreArgs{ - SrcTabletAliases: []topo.TabletAlias{ - topo.TabletAlias{ - Cell: "jail1", - Uid: 8902, - }, - topo.TabletAlias{ - Cell: "jail2", - Uid: 8901, - }, - }, - Concurrency: 124, - FetchConcurrency: 162, - InsertTableConcurrency: 6178, - FetchRetryCount: 887, - Strategy: "cool one", -} -var testMultiRestoreCalled = false - -func (fra *fakeRpcAgent) MultiRestore(ctx context.Context, args *actionnode.MultiRestoreArgs, logger logutil.Logger) error { - compare(fra.t, "MultiRestore args", args, testMultiRestoreArgs) - logStuff(logger, 1000) - testMultiRestoreCalled = true - return nil -} - -func agentRpcTestMultiRestore(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { - logChannel, errFunc, err := client.MultiRestore(ctx, ti, testMultiRestoreArgs, time.Minute) - if err != nil { - t.Fatalf("MultiRestore failed: %v", err) - } - compareLoggedStuff(t, "MultiRestore", logChannel, 1000) - err = errFunc() - compareError(t, "MultiRestore", err, true, testMultiRestoreCalled) -} - // // RPC helpers // -// RpcWrap is part of the RpcAgent interface -func (fra *fakeRpcAgent) RpcWrap(ctx context.Context, name string, args, reply interface{}, f func() error) error { +// RPCWrap is part of the RPCAgent interface +func (fra *fakeRPCAgent) RPCWrap(ctx context.Context, name string, args, reply interface{}, f func() error) error { return f() } -// RpcWrapLock is part of the RpcAgent interface -func (fra *fakeRpcAgent) RpcWrapLock(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error { +// RPCWrapLock is part of the RPCAgent interface +func (fra *fakeRPCAgent) RPCWrapLock(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error { return f() } -// RpcWrapLockAction is part of the RpcAgent interface -func (fra *fakeRpcAgent) RpcWrapLockAction(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error { +// RPCWrapLockAction is part of the RPCAgent interface +func (fra *fakeRPCAgent) RPCWrapLockAction(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error { return f() } // methods to test individual API calls -// AgentRpcTestSuite will run the test suite using the provided client and +// Run will run the test suite using the provided client and // the provided tablet. Tablet's vt address needs to be configured so -// the client will connect to a server backed by our RpcAgent (returned -// by NewFakeRpcAgent) -func AgentRpcTestSuite(ctx context.Context, t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { +// the client will connect to a server backed by our RPCAgent (returned +// by NewFakeRPCAgent) +func Run(t *testing.T, client tmclient.TabletManagerClient, ti *topo.TabletInfo) { + ctx := context.Background() + // Various read-only methods - agentRpcTestPing(ctx, t, client, ti) - agentRpcTestGetSchema(ctx, t, client, ti) - agentRpcTestGetPermissions(ctx, t, client, ti) + agentRPCTestPing(ctx, t, client, ti) + agentRPCTestGetSchema(ctx, t, client, ti) + agentRPCTestGetPermissions(ctx, t, client, ti) // Various read-write methods - agentRpcTestSetReadOnly(ctx, t, client, ti) - agentRpcTestChangeType(ctx, t, client, ti) - agentRpcTestScrap(ctx, t, client, ti) - agentRpcTestSleep(ctx, t, client, ti) - agentRpcTestExecuteHook(ctx, t, client, ti) - agentRpcTestRefreshState(ctx, t, client, ti) - agentRpcTestRunHealthCheck(ctx, t, client, ti) - agentRpcTestReloadSchema(ctx, t, client, ti) - agentRpcTestPreflightSchema(ctx, t, client, ti) - agentRpcTestApplySchema(ctx, t, client, ti) - agentRpcTestExecuteFetch(ctx, t, client, ti) + agentRPCTestSetReadOnly(ctx, t, client, ti) + agentRPCTestChangeType(ctx, t, client, ti) + agentRPCTestScrap(ctx, t, client, ti) + agentRPCTestSleep(ctx, t, client, ti) + agentRPCTestExecuteHook(ctx, t, client, ti) + agentRPCTestRefreshState(ctx, t, client, ti) + agentRPCTestRunHealthCheck(ctx, t, client, ti) + agentRPCTestHealthStream(ctx, t, client, ti) + agentRPCTestReloadSchema(ctx, t, client, ti) + agentRPCTestPreflightSchema(ctx, t, client, ti) + agentRPCTestApplySchema(ctx, t, client, ti) + agentRPCTestExecuteFetch(ctx, t, client, ti) // Replication related methods - agentRpcTestSlaveStatus(ctx, t, client, ti) - agentRpcTestWaitSlavePosition(ctx, t, client, ti) - agentRpcTestMasterPosition(ctx, t, client, ti) - agentRpcTestReparentPosition(ctx, t, client, ti) - agentRpcTestStopSlave(ctx, t, client, ti) - agentRpcTestStopSlaveMinimum(ctx, t, client, ti) - agentRpcTestStartSlave(ctx, t, client, ti) - agentRpcTestTabletExternallyReparented(ctx, t, client, ti) - agentRpcTestGetSlaves(ctx, t, client, ti) - agentRpcTestWaitBlpPosition(ctx, t, client, ti) - agentRpcTestStopBlp(ctx, t, client, ti) - agentRpcTestStartBlp(ctx, t, client, ti) - agentRpcTestRunBlpUntil(ctx, t, client, ti) + agentRPCTestSlaveStatus(ctx, t, client, ti) + agentRPCTestWaitSlavePosition(ctx, t, client, ti) + agentRPCTestMasterPosition(ctx, t, client, ti) + agentRPCTestReparentPosition(ctx, t, client, ti) + agentRPCTestStopSlave(ctx, t, client, ti) + agentRPCTestStopSlaveMinimum(ctx, t, client, ti) + agentRPCTestStartSlave(ctx, t, client, ti) + agentRPCTestTabletExternallyReparented(ctx, t, client, ti) + agentRPCTestGetSlaves(ctx, t, client, ti) + agentRPCTestWaitBlpPosition(ctx, t, client, ti) + agentRPCTestStopBlp(ctx, t, client, ti) + agentRPCTestStartBlp(ctx, t, client, ti) + agentRPCTestRunBlpUntil(ctx, t, client, ti) // Reparenting related functions - agentRpcTestDemoteMaster(ctx, t, client, ti) - agentRpcTestPromoteSlave(ctx, t, client, ti) - agentRpcTestSlaveWasPromoted(ctx, t, client, ti) - agentRpcTestRestartSlave(ctx, t, client, ti) - agentRpcTestSlaveWasRestarted(ctx, t, client, ti) - agentRpcTestBreakSlaves(ctx, t, client, ti) + agentRPCTestDemoteMaster(ctx, t, client, ti) + agentRPCTestPromoteSlave(ctx, t, client, ti) + agentRPCTestSlaveWasPromoted(ctx, t, client, ti) + agentRPCTestRestartSlave(ctx, t, client, ti) + agentRPCTestSlaveWasRestarted(ctx, t, client, ti) + agentRPCTestBreakSlaves(ctx, t, client, ti) // Backup / restore related methods - agentRpcTestSnapshot(ctx, t, client, ti) - agentRpcTestSnapshotSourceEnd(ctx, t, client, ti) - agentRpcTestReserveForRestore(ctx, t, client, ti) - agentRpcTestRestore(ctx, t, client, ti) - agentRpcTestMultiSnapshot(ctx, t, client, ti) - agentRpcTestMultiRestore(ctx, t, client, ti) + agentRPCTestSnapshot(ctx, t, client, ti) + agentRPCTestSnapshotSourceEnd(ctx, t, client, ti) + agentRPCTestReserveForRestore(ctx, t, client, ti) + agentRPCTestRestore(ctx, t, client, ti) } diff --git a/go/vt/tabletmanager/binlog.go b/go/vt/tabletmanager/binlog.go index 929b1a6f4f1..25fd8e1c68b 100644 --- a/go/vt/tabletmanager/binlog.go +++ b/go/vt/tabletmanager/binlog.go @@ -17,6 +17,7 @@ import ( log "github.com/golang/glog" "github.com/youtube/vitess/go/mysql" + "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/stats" "github.com/youtube/vitess/go/vt/binlog/binlogplayer" blproto "github.com/youtube/vitess/go/vt/binlog/proto" @@ -243,7 +244,8 @@ func (bpc *BinlogPlayerController) Iteration() (err error) { return fmt.Errorf("empty source tablet list for %v %v %v", bpc.cell, bpc.sourceShard.String(), topo.TYPE_REPLICA) } newServerIndex := rand.Intn(len(addrs.Entries)) - addr := fmt.Sprintf("%v:%v", addrs.Entries[newServerIndex].Host, addrs.Entries[newServerIndex].NamedPortMap["_vtocc"]) + port, _ := addrs.Entries[newServerIndex].NamedPortMap["vt"] + addr := netutil.JoinHostPort(addrs.Entries[newServerIndex].Host, port) // save our current server bpc.playerMutex.Lock() @@ -265,17 +267,16 @@ func (bpc *BinlogPlayerController) Iteration() (err error) { // tables, just get them player := binlogplayer.NewBinlogPlayerTables(vtClient, addr, tables, startPosition, bpc.stopPosition, bpc.binlogPlayerStats) return player.ApplyBinlogEvents(bpc.interrupted) - } else { - // the data we have to replicate is the intersection of the - // source keyrange and our keyrange - overlap, err := key.KeyRangesOverlap(bpc.sourceShard.KeyRange, bpc.keyRange) - if err != nil { - return fmt.Errorf("Source shard %v doesn't overlap destination shard %v", bpc.sourceShard.KeyRange, bpc.keyRange) - } - - player := binlogplayer.NewBinlogPlayerKeyRange(vtClient, addr, bpc.keyspaceIdType, overlap, startPosition, bpc.stopPosition, bpc.binlogPlayerStats) - return player.ApplyBinlogEvents(bpc.interrupted) } + // the data we have to replicate is the intersection of the + // source keyrange and our keyrange + overlap, err := key.KeyRangesOverlap(bpc.sourceShard.KeyRange, bpc.keyRange) + if err != nil { + return fmt.Errorf("Source shard %v doesn't overlap destination shard %v", bpc.sourceShard.KeyRange, bpc.keyRange) + } + + player := binlogplayer.NewBinlogPlayerKeyRange(vtClient, addr, bpc.keyspaceIdType, overlap, startPosition, bpc.stopPosition, bpc.binlogPlayerStats) + return player.ApplyBinlogEvents(bpc.interrupted) } // BlpPosition returns the current position for a controller, as read from diff --git a/go/vt/tabletmanager/gorpcproto/structs.go b/go/vt/tabletmanager/gorpcproto/structs.go index 651f6f0e21d..373400cd120 100644 --- a/go/vt/tabletmanager/gorpcproto/structs.go +++ b/go/vt/tabletmanager/gorpcproto/structs.go @@ -74,11 +74,6 @@ type SnapshotStreamingReply struct { Result *actionnode.SnapshotReply } -type MultiSnapshotStreamingReply struct { - Log *logutil.LoggerEvent - Result *actionnode.MultiSnapshotReply -} - type TabletExternallyReparentedArgs struct { ExternalID string } diff --git a/go/vt/tabletmanager/gorpctmclient/gorpc_client.go b/go/vt/tabletmanager/gorpctmclient/gorpc_client.go index beb07d217c1..fc77de385e0 100644 --- a/go/vt/tabletmanager/gorpctmclient/gorpc_client.go +++ b/go/vt/tabletmanager/gorpctmclient/gorpc_client.go @@ -8,7 +8,6 @@ import ( "fmt" "time" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/rpcwrap/bsonrpc" blproto "github.com/youtube/vitess/go/vt/binlog/proto" @@ -20,39 +19,50 @@ import ( "github.com/youtube/vitess/go/vt/tabletmanager/gorpcproto" "github.com/youtube/vitess/go/vt/tabletmanager/tmclient" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) +type timeoutError error + func init() { tmclient.RegisterTabletManagerClientFactory("bson", func() tmclient.TabletManagerClient { - return &GoRpcTabletManagerClient{} + return &GoRPCTabletManagerClient{} }) } -// GoRpcTabletManagerClient implements tmclient.TabletManagerClient -type GoRpcTabletManagerClient struct{} +// GoRPCTabletManagerClient implements tmclient.TabletManagerClient +type GoRPCTabletManagerClient struct{} -func (client *GoRpcTabletManagerClient) rpcCallTablet(ctx context.Context, tablet *topo.TabletInfo, name string, args, reply interface{}, waitTime time.Duration) error { - // create the RPC client, using waitTime as the connect - // timeout, and starting the overall timeout as well - tmr := time.NewTimer(waitTime) - defer tmr.Stop() - rpcClient, err := bsonrpc.DialHTTP("tcp", tablet.Addr(), waitTime, nil) +// rpcCallTablet wil execute the RPC on the remote server. +func (client *GoRPCTabletManagerClient) rpcCallTablet(ctx context.Context, tablet *topo.TabletInfo, name string, args, reply interface{}) error { + // create the RPC client, using ctx.Deadline if set, or no timeout. + var connectTimeout time.Duration + deadline, ok := ctx.Deadline() + if ok { + connectTimeout = deadline.Sub(time.Now()) + if connectTimeout < 0 { + return timeoutError(fmt.Errorf("timeout connecting to TabletManager.%v on %v", name, tablet.Alias)) + } + } + rpcClient, err := bsonrpc.DialHTTP("tcp", tablet.Addr(), connectTimeout, nil) if err != nil { return fmt.Errorf("RPC error for %v: %v", tablet.Alias, err.Error()) } defer rpcClient.Close() - // do the call in the remaining time + // use the context Done() channel. Will handle context timeout. call := rpcClient.Go(ctx, "TabletManager."+name, args, reply, nil) select { - case <-tmr.C: - return fmt.Errorf("Timeout waiting for TabletManager.%v to %v", name, tablet.Alias) + case <-ctx.Done(): + if ctx.Err() == context.DeadlineExceeded { + return timeoutError(fmt.Errorf("timeout waiting for TabletManager.%v to %v", name, tablet.Alias)) + } + return fmt.Errorf("interrupted waiting for TabletManager.%v to %v", name, tablet.Alias) case <-call.Done: if call.Error != nil { - return fmt.Errorf("Remote error for %v: %v", tablet.Alias, call.Error.Error()) - } else { - return nil + return fmt.Errorf("remote error for %v: %v", tablet.Alias, call.Error.Error()) } + return nil } } @@ -60,41 +70,46 @@ func (client *GoRpcTabletManagerClient) rpcCallTablet(ctx context.Context, table // Various read-only methods // -func (client *GoRpcTabletManagerClient) Ping(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { +// Ping is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) Ping(ctx context.Context, tablet *topo.TabletInfo) error { var result string - err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_PING, "payload", &result, waitTime) + err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_PING, "payload", &result) if err != nil { return err } if result != "payload" { - return fmt.Errorf("Bad ping result: %v", result) + return fmt.Errorf("bad ping result: %v", result) } return nil } -func (client *GoRpcTabletManagerClient) Sleep(ctx context.Context, tablet *topo.TabletInfo, duration, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SLEEP, &duration, &rpc.Unused{}, waitTime) +// Sleep is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) Sleep(ctx context.Context, tablet *topo.TabletInfo, duration time.Duration) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SLEEP, &duration, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) ExecuteHook(ctx context.Context, tablet *topo.TabletInfo, hk *hook.Hook, waitTime time.Duration) (*hook.HookResult, error) { +// ExecuteHook is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) ExecuteHook(ctx context.Context, tablet *topo.TabletInfo, hk *hook.Hook) (*hook.HookResult, error) { var hr hook.HookResult - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_EXECUTE_HOOK, hk, &hr, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_EXECUTE_HOOK, hk, &hr); err != nil { return nil, err } return &hr, nil } -func (client *GoRpcTabletManagerClient) GetSchema(ctx context.Context, tablet *topo.TabletInfo, tables, excludeTables []string, includeViews bool, waitTime time.Duration) (*myproto.SchemaDefinition, error) { +// GetSchema is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) GetSchema(ctx context.Context, tablet *topo.TabletInfo, tables, excludeTables []string, includeViews bool) (*myproto.SchemaDefinition, error) { var sd myproto.SchemaDefinition - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_GET_SCHEMA, &gorpcproto.GetSchemaArgs{Tables: tables, ExcludeTables: excludeTables, IncludeViews: includeViews}, &sd, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_GET_SCHEMA, &gorpcproto.GetSchemaArgs{Tables: tables, ExcludeTables: excludeTables, IncludeViews: includeViews}, &sd); err != nil { return nil, err } return &sd, nil } -func (client *GoRpcTabletManagerClient) GetPermissions(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (*myproto.Permissions, error) { +// GetPermissions is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) GetPermissions(ctx context.Context, tablet *topo.TabletInfo) (*myproto.Permissions, error) { var p myproto.Permissions - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_GET_PERMISSIONS, &rpc.Unused{}, &p, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_GET_PERMISSIONS, &rpc.Unused{}, &p); err != nil { return nil, err } return &p, nil @@ -104,53 +119,110 @@ func (client *GoRpcTabletManagerClient) GetPermissions(ctx context.Context, tabl // Various read-write methods // -func (client *GoRpcTabletManagerClient) SetReadOnly(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SET_RDONLY, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// SetReadOnly is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) SetReadOnly(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SET_RDONLY, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) SetReadWrite(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SET_RDWR, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// SetReadWrite is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) SetReadWrite(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SET_RDWR, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) ChangeType(ctx context.Context, tablet *topo.TabletInfo, dbType topo.TabletType, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_CHANGE_TYPE, &dbType, &rpc.Unused{}, waitTime) +// ChangeType is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) ChangeType(ctx context.Context, tablet *topo.TabletInfo, dbType topo.TabletType) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_CHANGE_TYPE, &dbType, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) Scrap(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SCRAP, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// Scrap is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) Scrap(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SCRAP, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) RefreshState(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_REFRESH_STATE, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// RefreshState is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) RefreshState(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_REFRESH_STATE, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) RunHealthCheck(ctx context.Context, tablet *topo.TabletInfo, targetTabletType topo.TabletType, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_RUN_HEALTH_CHECK, &targetTabletType, &rpc.Unused{}, waitTime) +// RunHealthCheck is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) RunHealthCheck(ctx context.Context, tablet *topo.TabletInfo, targetTabletType topo.TabletType) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_RUN_HEALTH_CHECK, &targetTabletType, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) ReloadSchema(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_RELOAD_SCHEMA, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// HealthStream is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) HealthStream(ctx context.Context, tablet *topo.TabletInfo) (<-chan *actionnode.HealthStreamReply, tmclient.ErrFunc, error) { + var connectTimeout time.Duration + deadline, ok := ctx.Deadline() + if ok { + connectTimeout = deadline.Sub(time.Now()) + if connectTimeout < 0 { + return nil, nil, timeoutError(fmt.Errorf("timeout connecting to TabletManager.HealthStream on %v", tablet.Alias)) + } + } + rpcClient, err := bsonrpc.DialHTTP("tcp", tablet.Addr(), connectTimeout, nil) + if err != nil { + return nil, nil, err + } + + logstream := make(chan *actionnode.HealthStreamReply, 10) + rpcstream := make(chan *actionnode.HealthStreamReply, 10) + c := rpcClient.StreamGo("TabletManager.HealthStream", "", rpcstream) + interrupted := false + go func() { + for { + select { + case <-ctx.Done(): + // context is done + interrupted = true + close(logstream) + rpcClient.Close() + return + case hsr, ok := <-rpcstream: + if !ok { + close(logstream) + rpcClient.Close() + return + } + logstream <- hsr + } + } + }() + return logstream, func() error { + // this is only called after streaming is done + if interrupted { + return fmt.Errorf("TabletManager.HealthStreamReply interrupted by context") + } + return c.Error + }, nil +} + +// ReloadSchema is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) ReloadSchema(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_RELOAD_SCHEMA, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) PreflightSchema(ctx context.Context, tablet *topo.TabletInfo, change string, waitTime time.Duration) (*myproto.SchemaChangeResult, error) { +// PreflightSchema is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) PreflightSchema(ctx context.Context, tablet *topo.TabletInfo, change string) (*myproto.SchemaChangeResult, error) { var scr myproto.SchemaChangeResult - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_PREFLIGHT_SCHEMA, change, &scr, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_PREFLIGHT_SCHEMA, change, &scr); err != nil { return nil, err } return &scr, nil } -func (client *GoRpcTabletManagerClient) ApplySchema(ctx context.Context, tablet *topo.TabletInfo, change *myproto.SchemaChange, waitTime time.Duration) (*myproto.SchemaChangeResult, error) { +// ApplySchema is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) ApplySchema(ctx context.Context, tablet *topo.TabletInfo, change *myproto.SchemaChange) (*myproto.SchemaChangeResult, error) { var scr myproto.SchemaChangeResult - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_APPLY_SCHEMA, change, &scr, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_APPLY_SCHEMA, change, &scr); err != nil { return nil, err } return &scr, nil } -func (client *GoRpcTabletManagerClient) ExecuteFetch(ctx context.Context, tablet *topo.TabletInfo, query string, maxRows int, wantFields, disableBinlogs bool, waitTime time.Duration) (*mproto.QueryResult, error) { +// ExecuteFetch is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) ExecuteFetch(ctx context.Context, tablet *topo.TabletInfo, query string, maxRows int, wantFields, disableBinlogs bool) (*mproto.QueryResult, error) { var qr mproto.QueryResult - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_EXECUTE_FETCH, &gorpcproto.ExecuteFetchArgs{Query: query, MaxRows: maxRows, WantFields: wantFields, DisableBinlogs: disableBinlogs}, &qr, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_EXECUTE_FETCH, &gorpcproto.ExecuteFetchArgs{Query: query, MaxRows: maxRows, WantFields: wantFields, DisableBinlogs: disableBinlogs}, &qr); err != nil { return nil, err } return &qr, nil @@ -160,97 +232,110 @@ func (client *GoRpcTabletManagerClient) ExecuteFetch(ctx context.Context, tablet // Replication related methods // -func (client *GoRpcTabletManagerClient) SlaveStatus(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (*myproto.ReplicationStatus, error) { +// SlaveStatus is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) SlaveStatus(ctx context.Context, tablet *topo.TabletInfo) (*myproto.ReplicationStatus, error) { var status myproto.ReplicationStatus - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SLAVE_STATUS, &rpc.Unused{}, &status, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SLAVE_STATUS, &rpc.Unused{}, &status); err != nil { return nil, err } return &status, nil } -func (client *GoRpcTabletManagerClient) WaitSlavePosition(ctx context.Context, tablet *topo.TabletInfo, waitPos myproto.ReplicationPosition, waitTime time.Duration) (*myproto.ReplicationStatus, error) { +// WaitSlavePosition is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) WaitSlavePosition(ctx context.Context, tablet *topo.TabletInfo, waitPos myproto.ReplicationPosition, waitTime time.Duration) (*myproto.ReplicationStatus, error) { var status myproto.ReplicationStatus if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_WAIT_SLAVE_POSITION, &gorpcproto.WaitSlavePositionArgs{ Position: waitPos, WaitTimeout: waitTime, - }, &status, waitTime); err != nil { + }, &status); err != nil { return nil, err } return &status, nil } -func (client *GoRpcTabletManagerClient) MasterPosition(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (myproto.ReplicationPosition, error) { +// MasterPosition is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) MasterPosition(ctx context.Context, tablet *topo.TabletInfo) (myproto.ReplicationPosition, error) { var rp myproto.ReplicationPosition - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_MASTER_POSITION, &rpc.Unused{}, &rp, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_MASTER_POSITION, &rpc.Unused{}, &rp); err != nil { return rp, err } return rp, nil } -func (client *GoRpcTabletManagerClient) ReparentPosition(ctx context.Context, tablet *topo.TabletInfo, rp *myproto.ReplicationPosition, waitTime time.Duration) (*actionnode.RestartSlaveData, error) { +// ReparentPosition is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) ReparentPosition(ctx context.Context, tablet *topo.TabletInfo, rp *myproto.ReplicationPosition) (*actionnode.RestartSlaveData, error) { var rsd actionnode.RestartSlaveData - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_REPARENT_POSITION, rp, &rsd, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_REPARENT_POSITION, rp, &rsd); err != nil { return nil, err } return &rsd, nil } -func (client *GoRpcTabletManagerClient) StopSlave(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_STOP_SLAVE, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// StopSlave is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) StopSlave(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_STOP_SLAVE, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) StopSlaveMinimum(ctx context.Context, tablet *topo.TabletInfo, minPos myproto.ReplicationPosition, waitTime time.Duration) (*myproto.ReplicationStatus, error) { +// StopSlaveMinimum is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) StopSlaveMinimum(ctx context.Context, tablet *topo.TabletInfo, minPos myproto.ReplicationPosition, waitTime time.Duration) (*myproto.ReplicationStatus, error) { var status myproto.ReplicationStatus if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_STOP_SLAVE_MINIMUM, &gorpcproto.StopSlaveMinimumArgs{ Position: minPos, WaitTime: waitTime, - }, &status, waitTime); err != nil { + }, &status); err != nil { return nil, err } return &status, nil } -func (client *GoRpcTabletManagerClient) StartSlave(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_START_SLAVE, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// StartSlave is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) StartSlave(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_START_SLAVE, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) TabletExternallyReparented(ctx context.Context, tablet *topo.TabletInfo, externalID string, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_EXTERNALLY_REPARENTED, &gorpcproto.TabletExternallyReparentedArgs{ExternalID: externalID}, &rpc.Unused{}, waitTime) +// TabletExternallyReparented is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) TabletExternallyReparented(ctx context.Context, tablet *topo.TabletInfo, externalID string) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_EXTERNALLY_REPARENTED, &gorpcproto.TabletExternallyReparentedArgs{ExternalID: externalID}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) GetSlaves(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) ([]string, error) { +// GetSlaves is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) GetSlaves(ctx context.Context, tablet *topo.TabletInfo) ([]string, error) { var sl gorpcproto.GetSlavesReply - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_GET_SLAVES, &rpc.Unused{}, &sl, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_GET_SLAVES, &rpc.Unused{}, &sl); err != nil { return nil, err } return sl.Addrs, nil } -func (client *GoRpcTabletManagerClient) WaitBlpPosition(ctx context.Context, tablet *topo.TabletInfo, blpPosition blproto.BlpPosition, waitTime time.Duration) error { +// WaitBlpPosition is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) WaitBlpPosition(ctx context.Context, tablet *topo.TabletInfo, blpPosition blproto.BlpPosition, waitTime time.Duration) error { return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_WAIT_BLP_POSITION, &gorpcproto.WaitBlpPositionArgs{ BlpPosition: blpPosition, WaitTimeout: waitTime, - }, &rpc.Unused{}, waitTime) + }, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) StopBlp(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (*blproto.BlpPositionList, error) { +// StopBlp is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) StopBlp(ctx context.Context, tablet *topo.TabletInfo) (*blproto.BlpPositionList, error) { var bpl blproto.BlpPositionList - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_STOP_BLP, &rpc.Unused{}, &bpl, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_STOP_BLP, &rpc.Unused{}, &bpl); err != nil { return nil, err } return &bpl, nil } -func (client *GoRpcTabletManagerClient) StartBlp(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_START_BLP, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// StartBlp is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) StartBlp(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_START_BLP, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) RunBlpUntil(ctx context.Context, tablet *topo.TabletInfo, positions *blproto.BlpPositionList, waitTime time.Duration) (myproto.ReplicationPosition, error) { +// RunBlpUntil is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) RunBlpUntil(ctx context.Context, tablet *topo.TabletInfo, positions *blproto.BlpPositionList, waitTime time.Duration) (myproto.ReplicationPosition, error) { var pos myproto.ReplicationPosition if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_RUN_BLP_UNTIL, &gorpcproto.RunBlpUntilArgs{ BlpPositionList: positions, WaitTimeout: waitTime, - }, &pos, waitTime); err != nil { + }, &pos); err != nil { return myproto.ReplicationPosition{}, err } return pos, nil @@ -260,40 +345,55 @@ func (client *GoRpcTabletManagerClient) RunBlpUntil(ctx context.Context, tablet // Reparenting related functions // -func (client *GoRpcTabletManagerClient) DemoteMaster(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_DEMOTE_MASTER, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// DemoteMaster is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) DemoteMaster(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_DEMOTE_MASTER, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) PromoteSlave(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (*actionnode.RestartSlaveData, error) { +// PromoteSlave is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) PromoteSlave(ctx context.Context, tablet *topo.TabletInfo) (*actionnode.RestartSlaveData, error) { var rsd actionnode.RestartSlaveData - if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_PROMOTE_SLAVE, &rpc.Unused{}, &rsd, waitTime); err != nil { + if err := client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_PROMOTE_SLAVE, &rpc.Unused{}, &rsd); err != nil { return nil, err } return &rsd, nil } -func (client *GoRpcTabletManagerClient) SlaveWasPromoted(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SLAVE_WAS_PROMOTED, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// SlaveWasPromoted is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) SlaveWasPromoted(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SLAVE_WAS_PROMOTED, &rpc.Unused{}, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) RestartSlave(ctx context.Context, tablet *topo.TabletInfo, rsd *actionnode.RestartSlaveData, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_RESTART_SLAVE, rsd, &rpc.Unused{}, waitTime) +// RestartSlave is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) RestartSlave(ctx context.Context, tablet *topo.TabletInfo, rsd *actionnode.RestartSlaveData) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_RESTART_SLAVE, rsd, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) SlaveWasRestarted(ctx context.Context, tablet *topo.TabletInfo, args *actionnode.SlaveWasRestartedArgs, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SLAVE_WAS_RESTARTED, args, &rpc.Unused{}, waitTime) +// SlaveWasRestarted is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) SlaveWasRestarted(ctx context.Context, tablet *topo.TabletInfo, args *actionnode.SlaveWasRestartedArgs) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SLAVE_WAS_RESTARTED, args, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) BreakSlaves(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_BREAK_SLAVES, &rpc.Unused{}, &rpc.Unused{}, waitTime) +// BreakSlaves is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) BreakSlaves(ctx context.Context, tablet *topo.TabletInfo) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_BREAK_SLAVES, &rpc.Unused{}, &rpc.Unused{}) } // // Backup related methods // -func (client *GoRpcTabletManagerClient) Snapshot(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.SnapshotArgs, waitTime time.Duration) (<-chan *logutil.LoggerEvent, tmclient.SnapshotReplyFunc, error) { - rpcClient, err := bsonrpc.DialHTTP("tcp", tablet.Addr(), waitTime, nil) +// Snapshot is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) Snapshot(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.SnapshotArgs) (<-chan *logutil.LoggerEvent, tmclient.SnapshotReplyFunc, error) { + var connectTimeout time.Duration + deadline, ok := ctx.Deadline() + if ok { + connectTimeout = deadline.Sub(time.Now()) + if connectTimeout < 0 { + return nil, nil, timeoutError(fmt.Errorf("timeout connecting to TabletManager.Snapshot on %v", tablet.Alias)) + } + } + rpcClient, err := bsonrpc.DialHTTP("tcp", tablet.Addr(), connectTimeout, nil) if err != nil { return nil, nil, err } @@ -303,83 +403,107 @@ func (client *GoRpcTabletManagerClient) Snapshot(ctx context.Context, tablet *to result := &actionnode.SnapshotReply{} c := rpcClient.StreamGo("TabletManager.Snapshot", sa, rpcstream) + interrupted := false go func() { - for ssr := range rpcstream { - if ssr.Log != nil { - logstream <- ssr.Log - } - if ssr.Result != nil { - *result = *ssr.Result + for { + select { + case <-ctx.Done(): + // context is done + interrupted = true + close(logstream) + rpcClient.Close() + return + case ssr, ok := <-rpcstream: + if !ok { + close(logstream) + rpcClient.Close() + return + } + if ssr.Log != nil { + logstream <- ssr.Log + } + if ssr.Result != nil { + *result = *ssr.Result + } } } - close(logstream) - rpcClient.Close() }() return logstream, func() (*actionnode.SnapshotReply, error) { + // this is only called after streaming is done + if interrupted { + return nil, fmt.Errorf("TabletManager.Snapshot interrupted by context") + } return result, c.Error }, nil } -func (client *GoRpcTabletManagerClient) SnapshotSourceEnd(ctx context.Context, tablet *topo.TabletInfo, args *actionnode.SnapshotSourceEndArgs, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SNAPSHOT_SOURCE_END, args, &rpc.Unused{}, waitTime) +// SnapshotSourceEnd is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) SnapshotSourceEnd(ctx context.Context, tablet *topo.TabletInfo, args *actionnode.SnapshotSourceEndArgs) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_SNAPSHOT_SOURCE_END, args, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) ReserveForRestore(ctx context.Context, tablet *topo.TabletInfo, args *actionnode.ReserveForRestoreArgs, waitTime time.Duration) error { - return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_RESERVE_FOR_RESTORE, args, &rpc.Unused{}, waitTime) +// ReserveForRestore is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) ReserveForRestore(ctx context.Context, tablet *topo.TabletInfo, args *actionnode.ReserveForRestoreArgs) error { + return client.rpcCallTablet(ctx, tablet, actionnode.TABLET_ACTION_RESERVE_FOR_RESTORE, args, &rpc.Unused{}) } -func (client *GoRpcTabletManagerClient) Restore(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.RestoreArgs, waitTime time.Duration) (<-chan *logutil.LoggerEvent, tmclient.ErrFunc, error) { - rpcClient, err := bsonrpc.DialHTTP("tcp", tablet.Addr(), waitTime, nil) - if err != nil { - return nil, nil, err +// Restore is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) Restore(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.RestoreArgs) (<-chan *logutil.LoggerEvent, tmclient.ErrFunc, error) { + var connectTimeout time.Duration + deadline, ok := ctx.Deadline() + if ok { + connectTimeout = deadline.Sub(time.Now()) + if connectTimeout < 0 { + return nil, nil, timeoutError(fmt.Errorf("timeout connecting to TabletManager.Restore on %v", tablet.Alias)) + } } - - logstream := make(chan *logutil.LoggerEvent, 10) - c := rpcClient.StreamGo("TabletManager.Restore", sa, logstream) - return logstream, func() error { - rpcClient.Close() - return c.Error - }, nil -} - -func (client *GoRpcTabletManagerClient) MultiSnapshot(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.MultiSnapshotArgs, waitTime time.Duration) (<-chan *logutil.LoggerEvent, tmclient.MultiSnapshotReplyFunc, error) { - rpcClient, err := bsonrpc.DialHTTP("tcp", tablet.Addr(), waitTime, nil) + rpcClient, err := bsonrpc.DialHTTP("tcp", tablet.Addr(), connectTimeout, nil) if err != nil { return nil, nil, err } logstream := make(chan *logutil.LoggerEvent, 10) - rpcstream := make(chan *gorpcproto.MultiSnapshotStreamingReply, 10) - result := &actionnode.MultiSnapshotReply{} - - c := rpcClient.StreamGo("TabletManager.MultiSnapshot", sa, rpcstream) + rpcstream := make(chan *logutil.LoggerEvent, 10) + c := rpcClient.StreamGo("TabletManager.Restore", sa, rpcstream) + interrupted := false go func() { - for ssr := range rpcstream { - if ssr.Log != nil { - logstream <- ssr.Log - } - if ssr.Result != nil { - *result = *ssr.Result + for { + select { + case <-ctx.Done(): + // context is done + interrupted = true + close(logstream) + rpcClient.Close() + return + case ssr, ok := <-rpcstream: + if !ok { + close(logstream) + rpcClient.Close() + return + } + logstream <- ssr } } - close(logstream) - rpcClient.Close() }() - return logstream, func() (*actionnode.MultiSnapshotReply, error) { - return result, c.Error + return logstream, func() error { + // this is only called after streaming is done + if interrupted { + return fmt.Errorf("TabletManager.Restore interrupted by context") + } + return c.Error }, nil } -func (client *GoRpcTabletManagerClient) MultiRestore(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.MultiRestoreArgs, waitTime time.Duration) (<-chan *logutil.LoggerEvent, tmclient.ErrFunc, error) { - rpcClient, err := bsonrpc.DialHTTP("tcp", tablet.Addr(), waitTime, nil) - if err != nil { - return nil, nil, err - } +// +// RPC related methods +// - logstream := make(chan *logutil.LoggerEvent, 10) - c := rpcClient.StreamGo("TabletManager.MultiRestore", sa, logstream) - return logstream, func() error { - rpcClient.Close() - return c.Error - }, nil +// IsTimeoutError is part of the tmclient.TabletManagerClient interface +func (client *GoRPCTabletManagerClient) IsTimeoutError(err error) bool { + switch err.(type) { + case timeoutError: + return true + default: + return false + } } diff --git a/go/vt/tabletmanager/gorpctmserver/gorpc_server.go b/go/vt/tabletmanager/gorpctmserver/gorpc_server.go index 1441a1175ff..9fee9d52727 100644 --- a/go/vt/tabletmanager/gorpctmserver/gorpc_server.go +++ b/go/vt/tabletmanager/gorpctmserver/gorpc_server.go @@ -8,7 +8,6 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/rpcplus" blproto "github.com/youtube/vitess/go/vt/binlog/proto" @@ -21,41 +20,46 @@ import ( "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/tabletmanager/gorpcproto" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // TabletManager is the Go RPC implementation of the RPC service type TabletManager struct { // implementation of the agent to call - agent tabletmanager.RpcAgent + agent tabletmanager.RPCAgent } // // Various read-only methods // +// Ping wraps RPCAgent. func (tm *TabletManager) Ping(ctx context.Context, args, reply *string) error { - return tm.agent.RpcWrap(ctx, actionnode.TABLET_ACTION_PING, args, reply, func() error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_PING, args, reply, func() error { *reply = tm.agent.Ping(ctx, *args) return nil }) } +// Sleep wraps RPCAgent. func (tm *TabletManager) Sleep(ctx context.Context, args *time.Duration, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_SLEEP, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_SLEEP, args, reply, true, func() error { tm.agent.Sleep(ctx, *args) return nil }) } +// ExecuteHook wraps RPCAgent. func (tm *TabletManager) ExecuteHook(ctx context.Context, args *hook.Hook, reply *hook.HookResult) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_EXECUTE_HOOK, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_EXECUTE_HOOK, args, reply, true, func() error { *reply = *tm.agent.ExecuteHook(ctx, args) return nil }) } +// GetSchema wraps RPCAgent. func (tm *TabletManager) GetSchema(ctx context.Context, args *gorpcproto.GetSchemaArgs, reply *myproto.SchemaDefinition) error { - return tm.agent.RpcWrap(ctx, actionnode.TABLET_ACTION_GET_SCHEMA, args, reply, func() error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_GET_SCHEMA, args, reply, func() error { sd, err := tm.agent.GetSchema(ctx, args.Tables, args.ExcludeTables, args.IncludeViews) if err == nil { *reply = *sd @@ -64,8 +68,9 @@ func (tm *TabletManager) GetSchema(ctx context.Context, args *gorpcproto.GetSche }) } +// GetPermissions wraps RPCAgent. func (tm *TabletManager) GetPermissions(ctx context.Context, args *rpc.Unused, reply *myproto.Permissions) error { - return tm.agent.RpcWrap(ctx, actionnode.TABLET_ACTION_GET_PERMISSIONS, args, reply, func() error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_GET_PERMISSIONS, args, reply, func() error { p, err := tm.agent.GetPermissions(ctx) if err == nil { *reply = *p @@ -78,53 +83,88 @@ func (tm *TabletManager) GetPermissions(ctx context.Context, args *rpc.Unused, r // Various read-write methods // +// SetReadOnly wraps RPCAgent. func (tm *TabletManager) SetReadOnly(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_SET_RDONLY, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_SET_RDONLY, args, reply, true, func() error { return tm.agent.SetReadOnly(ctx, true) }) } +// SetReadWrite wraps RPCAgent. func (tm *TabletManager) SetReadWrite(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_SET_RDWR, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_SET_RDWR, args, reply, true, func() error { return tm.agent.SetReadOnly(ctx, false) }) } +// ChangeType wraps RPCAgent. func (tm *TabletManager) ChangeType(ctx context.Context, args *topo.TabletType, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_CHANGE_TYPE, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_CHANGE_TYPE, args, reply, true, func() error { return tm.agent.ChangeType(ctx, *args) }) } +// Scrap wraps RPCAgent. func (tm *TabletManager) Scrap(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_SCRAP, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_SCRAP, args, reply, true, func() error { return tm.agent.Scrap(ctx) }) } +// RefreshState wraps RPCAgent. func (tm *TabletManager) RefreshState(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_REFRESH_STATE, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_REFRESH_STATE, args, reply, true, func() error { tm.agent.RefreshState(ctx) return nil }) } +// RunHealthCheck wraps RPCAgent. func (tm *TabletManager) RunHealthCheck(ctx context.Context, args *topo.TabletType, reply *rpc.Unused) error { - return tm.agent.RpcWrap(ctx, actionnode.TABLET_ACTION_RUN_HEALTH_CHECK, args, reply, func() error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_RUN_HEALTH_CHECK, args, reply, func() error { tm.agent.RunHealthCheck(ctx, *args) return nil }) } +// HealthStream wraps RPCAgent. +func (tm *TabletManager) HealthStream(ctx context.Context, args *rpc.Unused, sendReply func(interface{}) error) error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_HEALTH_STREAM, args, nil, func() error { + c := make(chan *actionnode.HealthStreamReply, 10) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + for hsr := range c { + // we send until the client disconnects + if err := sendReply(hsr); err != nil { + return + } + } + }() + + id, err := tm.agent.RegisterHealthStream(c) + if err != nil { + close(c) + wg.Wait() + return err + } + wg.Wait() + return tm.agent.UnregisterHealthStream(id) + }) +} + +// ReloadSchema wraps RPCAgent. func (tm *TabletManager) ReloadSchema(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_RELOAD_SCHEMA, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_RELOAD_SCHEMA, args, reply, true, func() error { tm.agent.ReloadSchema(ctx) return nil }) } +// PreflightSchema wraps RPCAgent. func (tm *TabletManager) PreflightSchema(ctx context.Context, args *string, reply *myproto.SchemaChangeResult) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_PREFLIGHT_SCHEMA, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_PREFLIGHT_SCHEMA, args, reply, true, func() error { scr, err := tm.agent.PreflightSchema(ctx, *args) if err == nil { *reply = *scr @@ -133,8 +173,9 @@ func (tm *TabletManager) PreflightSchema(ctx context.Context, args *string, repl }) } +// ApplySchema wraps RPCAgent. func (tm *TabletManager) ApplySchema(ctx context.Context, args *myproto.SchemaChange, reply *myproto.SchemaChangeResult) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_APPLY_SCHEMA, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_APPLY_SCHEMA, args, reply, true, func() error { scr, err := tm.agent.ApplySchema(ctx, args) if err == nil { *reply = *scr @@ -143,8 +184,9 @@ func (tm *TabletManager) ApplySchema(ctx context.Context, args *myproto.SchemaCh }) } +// ExecuteFetch wraps RPCAgent. func (tm *TabletManager) ExecuteFetch(ctx context.Context, args *gorpcproto.ExecuteFetchArgs, reply *mproto.QueryResult) error { - return tm.agent.RpcWrap(ctx, actionnode.TABLET_ACTION_EXECUTE_FETCH, args, reply, func() error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_EXECUTE_FETCH, args, reply, func() error { qr, err := tm.agent.ExecuteFetch(ctx, args.Query, args.MaxRows, args.WantFields, args.DisableBinlogs) if err == nil { *reply = *qr @@ -157,8 +199,9 @@ func (tm *TabletManager) ExecuteFetch(ctx context.Context, args *gorpcproto.Exec // Replication related methods // +// SlaveStatus wraps RPCAgent. func (tm *TabletManager) SlaveStatus(ctx context.Context, args *rpc.Unused, reply *myproto.ReplicationStatus) error { - return tm.agent.RpcWrap(ctx, actionnode.TABLET_ACTION_SLAVE_STATUS, args, reply, func() error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_SLAVE_STATUS, args, reply, func() error { status, err := tm.agent.SlaveStatus(ctx) if err == nil { *reply = *status @@ -167,8 +210,9 @@ func (tm *TabletManager) SlaveStatus(ctx context.Context, args *rpc.Unused, repl }) } +// WaitSlavePosition wraps RPCAgent. func (tm *TabletManager) WaitSlavePosition(ctx context.Context, args *gorpcproto.WaitSlavePositionArgs, reply *myproto.ReplicationStatus) error { - return tm.agent.RpcWrapLock(ctx, actionnode.TABLET_ACTION_WAIT_SLAVE_POSITION, args, reply, true, func() error { + return tm.agent.RPCWrapLock(ctx, actionnode.TABLET_ACTION_WAIT_SLAVE_POSITION, args, reply, true, func() error { status, err := tm.agent.WaitSlavePosition(ctx, args.Position, args.WaitTimeout) if err == nil { *reply = *status @@ -177,8 +221,9 @@ func (tm *TabletManager) WaitSlavePosition(ctx context.Context, args *gorpcproto }) } +// MasterPosition wraps RPCAgent. func (tm *TabletManager) MasterPosition(ctx context.Context, args *rpc.Unused, reply *myproto.ReplicationPosition) error { - return tm.agent.RpcWrap(ctx, actionnode.TABLET_ACTION_MASTER_POSITION, args, reply, func() error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_MASTER_POSITION, args, reply, func() error { position, err := tm.agent.MasterPosition(ctx) if err == nil { *reply = position @@ -187,8 +232,9 @@ func (tm *TabletManager) MasterPosition(ctx context.Context, args *rpc.Unused, r }) } +// ReparentPosition wraps RPCAgent. func (tm *TabletManager) ReparentPosition(ctx context.Context, args *myproto.ReplicationPosition, reply *actionnode.RestartSlaveData) error { - return tm.agent.RpcWrap(ctx, actionnode.TABLET_ACTION_REPARENT_POSITION, args, reply, func() error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_REPARENT_POSITION, args, reply, func() error { rsd, err := tm.agent.ReparentPosition(ctx, args) if err == nil { *reply = *rsd @@ -197,14 +243,16 @@ func (tm *TabletManager) ReparentPosition(ctx context.Context, args *myproto.Rep }) } +// StopSlave wraps RPCAgent. func (tm *TabletManager) StopSlave(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLock(ctx, actionnode.TABLET_ACTION_STOP_SLAVE, args, reply, true, func() error { + return tm.agent.RPCWrapLock(ctx, actionnode.TABLET_ACTION_STOP_SLAVE, args, reply, true, func() error { return tm.agent.StopSlave(ctx) }) } +// StopSlaveMinimum wraps RPCAgent. func (tm *TabletManager) StopSlaveMinimum(ctx context.Context, args *gorpcproto.StopSlaveMinimumArgs, reply *myproto.ReplicationStatus) error { - return tm.agent.RpcWrapLock(ctx, actionnode.TABLET_ACTION_STOP_SLAVE_MINIMUM, args, reply, true, func() error { + return tm.agent.RPCWrapLock(ctx, actionnode.TABLET_ACTION_STOP_SLAVE_MINIMUM, args, reply, true, func() error { status, err := tm.agent.StopSlaveMinimum(ctx, args.Position, args.WaitTime) if err == nil { *reply = *status @@ -213,37 +261,44 @@ func (tm *TabletManager) StopSlaveMinimum(ctx context.Context, args *gorpcproto. }) } +// StartSlave wraps RPCAgent. func (tm *TabletManager) StartSlave(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLock(ctx, actionnode.TABLET_ACTION_START_SLAVE, args, reply, true, func() error { + return tm.agent.RPCWrapLock(ctx, actionnode.TABLET_ACTION_START_SLAVE, args, reply, true, func() error { return tm.agent.StartSlave(ctx) }) } +// TabletExternallyReparented wraps RPCAgent. func (tm *TabletManager) TabletExternallyReparented(ctx context.Context, args *gorpcproto.TabletExternallyReparentedArgs, reply *rpc.Unused) error { // TODO(alainjobart) we should forward the RPC deadline from // the original gorpc call. Until we support that, use a // reasonable hard-coded value. - return tm.agent.RpcWrapLock(ctx, actionnode.TABLET_ACTION_EXTERNALLY_REPARENTED, args, reply, false, func() error { - return tm.agent.TabletExternallyReparented(ctx, args.ExternalID, 30*time.Second) + ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) + defer cancel() + return tm.agent.RPCWrapLock(ctx, actionnode.TABLET_ACTION_EXTERNALLY_REPARENTED, args, reply, false, func() error { + return tm.agent.TabletExternallyReparented(ctx, args.ExternalID) }) } +// GetSlaves wraps RPCAgent. func (tm *TabletManager) GetSlaves(ctx context.Context, args *rpc.Unused, reply *gorpcproto.GetSlavesReply) error { - return tm.agent.RpcWrap(ctx, actionnode.TABLET_ACTION_GET_SLAVES, args, reply, func() error { + return tm.agent.RPCWrap(ctx, actionnode.TABLET_ACTION_GET_SLAVES, args, reply, func() error { var err error reply.Addrs, err = tm.agent.GetSlaves(ctx) return err }) } +// WaitBlpPosition wraps RPCAgent. func (tm *TabletManager) WaitBlpPosition(ctx context.Context, args *gorpcproto.WaitBlpPositionArgs, reply *rpc.Unused) error { - return tm.agent.RpcWrapLock(ctx, actionnode.TABLET_ACTION_WAIT_BLP_POSITION, args, reply, true, func() error { + return tm.agent.RPCWrapLock(ctx, actionnode.TABLET_ACTION_WAIT_BLP_POSITION, args, reply, true, func() error { return tm.agent.WaitBlpPosition(ctx, &args.BlpPosition, args.WaitTimeout) }) } +// StopBlp wraps RPCAgent. func (tm *TabletManager) StopBlp(ctx context.Context, args *rpc.Unused, reply *blproto.BlpPositionList) error { - return tm.agent.RpcWrapLock(ctx, actionnode.TABLET_ACTION_STOP_BLP, args, reply, true, func() error { + return tm.agent.RPCWrapLock(ctx, actionnode.TABLET_ACTION_STOP_BLP, args, reply, true, func() error { positions, err := tm.agent.StopBlp(ctx) if err == nil { *reply = *positions @@ -252,14 +307,16 @@ func (tm *TabletManager) StopBlp(ctx context.Context, args *rpc.Unused, reply *b }) } +// StartBlp wraps RPCAgent. func (tm *TabletManager) StartBlp(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLock(ctx, actionnode.TABLET_ACTION_START_BLP, args, reply, true, func() error { + return tm.agent.RPCWrapLock(ctx, actionnode.TABLET_ACTION_START_BLP, args, reply, true, func() error { return tm.agent.StartBlp(ctx) }) } +// RunBlpUntil wraps RPCAgent. func (tm *TabletManager) RunBlpUntil(ctx context.Context, args *gorpcproto.RunBlpUntilArgs, reply *myproto.ReplicationPosition) error { - return tm.agent.RpcWrapLock(ctx, actionnode.TABLET_ACTION_RUN_BLP_UNTIL, args, reply, true, func() error { + return tm.agent.RPCWrapLock(ctx, actionnode.TABLET_ACTION_RUN_BLP_UNTIL, args, reply, true, func() error { position, err := tm.agent.RunBlpUntil(ctx, args.BlpPositionList, args.WaitTimeout) if err == nil { *reply = *position @@ -272,14 +329,16 @@ func (tm *TabletManager) RunBlpUntil(ctx context.Context, args *gorpcproto.RunBl // Reparenting related functions // +// DemoteMaster wraps RPCAgent. func (tm *TabletManager) DemoteMaster(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_DEMOTE_MASTER, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_DEMOTE_MASTER, args, reply, true, func() error { return tm.agent.DemoteMaster(ctx) }) } +// PromoteSlave wraps RPCAgent. func (tm *TabletManager) PromoteSlave(ctx context.Context, args *rpc.Unused, reply *actionnode.RestartSlaveData) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_PROMOTE_SLAVE, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_PROMOTE_SLAVE, args, reply, true, func() error { rsd, err := tm.agent.PromoteSlave(ctx) if err == nil { *reply = *rsd @@ -288,34 +347,39 @@ func (tm *TabletManager) PromoteSlave(ctx context.Context, args *rpc.Unused, rep }) } +// SlaveWasPromoted wraps RPCAgent. func (tm *TabletManager) SlaveWasPromoted(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_SLAVE_WAS_PROMOTED, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_SLAVE_WAS_PROMOTED, args, reply, true, func() error { return tm.agent.SlaveWasPromoted(ctx) }) } +// RestartSlave wraps RPCAgent. func (tm *TabletManager) RestartSlave(ctx context.Context, args *actionnode.RestartSlaveData, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_RESTART_SLAVE, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_RESTART_SLAVE, args, reply, true, func() error { return tm.agent.RestartSlave(ctx, args) }) } +// SlaveWasRestarted wraps RPCAgent. func (tm *TabletManager) SlaveWasRestarted(ctx context.Context, args *actionnode.SlaveWasRestartedArgs, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_SLAVE_WAS_RESTARTED, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_SLAVE_WAS_RESTARTED, args, reply, true, func() error { return tm.agent.SlaveWasRestarted(ctx, args) }) } +// BreakSlaves wraps RPCAgent. func (tm *TabletManager) BreakSlaves(ctx context.Context, args *rpc.Unused, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_BREAK_SLAVES, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_BREAK_SLAVES, args, reply, true, func() error { return tm.agent.BreakSlaves(ctx) }) } // backup related methods +// Snapshot wraps RPCAgent. func (tm *TabletManager) Snapshot(ctx context.Context, args *actionnode.SnapshotArgs, sendReply func(interface{}) error) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_SNAPSHOT, args, nil, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_SNAPSHOT, args, nil, true, func() error { // create a logger, send the result back to the caller logger := logutil.NewChannelLogger(10) wg := sync.WaitGroup{} @@ -348,20 +412,23 @@ func (tm *TabletManager) Snapshot(ctx context.Context, args *actionnode.Snapshot }) } +// SnapshotSourceEnd wraps RPCAgent. func (tm *TabletManager) SnapshotSourceEnd(ctx context.Context, args *actionnode.SnapshotSourceEndArgs, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_SNAPSHOT_SOURCE_END, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_SNAPSHOT_SOURCE_END, args, reply, true, func() error { return tm.agent.SnapshotSourceEnd(ctx, args) }) } +// ReserveForRestore wraps RPCAgent. func (tm *TabletManager) ReserveForRestore(ctx context.Context, args *actionnode.ReserveForRestoreArgs, reply *rpc.Unused) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_RESERVE_FOR_RESTORE, args, reply, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_RESERVE_FOR_RESTORE, args, reply, true, func() error { return tm.agent.ReserveForRestore(ctx, args) }) } +// Restore wraps RPCAgent. func (tm *TabletManager) Restore(ctx context.Context, args *actionnode.RestoreArgs, sendReply func(interface{}) error) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_RESTORE, args, nil, true, func() error { + return tm.agent.RPCWrapLockAction(ctx, actionnode.TABLET_ACTION_RESTORE, args, nil, true, func() error { // create a logger, send the result back to the caller logger := logutil.NewChannelLogger(10) wg := sync.WaitGroup{} @@ -384,64 +451,6 @@ func (tm *TabletManager) Restore(ctx context.Context, args *actionnode.RestoreAr }) } -func (tm *TabletManager) MultiSnapshot(ctx context.Context, args *actionnode.MultiSnapshotArgs, sendReply func(interface{}) error) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_MULTI_SNAPSHOT, args, nil, true, func() error { - // create a logger, send the result back to the caller - logger := logutil.NewChannelLogger(10) - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - for e := range logger { - ssr := &gorpcproto.MultiSnapshotStreamingReply{ - Log: &e, - } - // Note we don't interrupt the loop here, as - // we still need to flush and finish the - // command, even if the channel to the client - // has been broken. We'll just keep trying to send. - sendReply(ssr) - } - wg.Done() - }() - - sr, err := tm.agent.MultiSnapshot(ctx, args, logger) - close(logger) - wg.Wait() - if err != nil { - return err - } - ssr := &gorpcproto.MultiSnapshotStreamingReply{ - Result: sr, - } - sendReply(ssr) - return nil - }) -} - -func (tm *TabletManager) MultiRestore(ctx context.Context, args *actionnode.MultiRestoreArgs, sendReply func(interface{}) error) error { - return tm.agent.RpcWrapLockAction(ctx, actionnode.TABLET_ACTION_MULTI_RESTORE, args, nil, true, func() error { - // create a logger, send the result back to the caller - logger := logutil.NewChannelLogger(10) - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - for e := range logger { - // Note we don't interrupt the loop here, as - // we still need to flush and finish the - // command, even if the channel to the client - // has been broken. We'll just keep trying to send. - sendReply(&e) - } - wg.Done() - }() - - err := tm.agent.MultiRestore(ctx, args, logger) - close(logger) - wg.Wait() - return err - }) -} - // registration glue func init() { diff --git a/go/vt/tabletmanager/gorpctmserver/gorpc_server_test.go b/go/vt/tabletmanager/gorpctmserver/gorpc_server_test.go index 3350503b8a2..579aa8f3adc 100644 --- a/go/vt/tabletmanager/gorpctmserver/gorpc_server_test.go +++ b/go/vt/tabletmanager/gorpctmserver/gorpc_server_test.go @@ -9,7 +9,6 @@ import ( "net/http" "testing" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/rpcplus" "github.com/youtube/vitess/go/rpcwrap/bsonrpc" "github.com/youtube/vitess/go/vt/tabletmanager/agentrpctest" @@ -19,7 +18,7 @@ import ( // the test here creates a fake server implementation, a fake client // implementation, and runs the test suite against the setup. -func TestGoRpcTMServer(t *testing.T) { +func TestGoRPCTMServer(t *testing.T) { // Listen on a random port listener, err := net.Listen("tcp", ":0") if err != nil { @@ -29,7 +28,7 @@ func TestGoRpcTMServer(t *testing.T) { // Create a Go Rpc server and listen on the port server := rpcplus.NewServer() - server.Register(&TabletManager{agentrpctest.NewFakeRpcAgent(t)}) + server.Register(&TabletManager{agentrpctest.NewFakeRPCAgent(t)}) // create the HTTP server, serve the server from it handler := http.NewServeMux() @@ -40,7 +39,7 @@ func TestGoRpcTMServer(t *testing.T) { go httpServer.Serve(listener) // Create a Go Rpc client to talk to the fake tablet - client := &gorpctmclient.GoRpcTabletManagerClient{} + client := &gorpctmclient.GoRPCTabletManagerClient{} ti := topo.NewTabletInfo(&topo.Tablet{ Alias: topo.TabletAlias{ Cell: "test", @@ -53,5 +52,5 @@ func TestGoRpcTMServer(t *testing.T) { }, 0) // and run the test suite - agentrpctest.AgentRpcTestSuite(context.Background(), t, client, ti) + agentrpctest.Run(t, client, ti) } diff --git a/go/vt/tabletmanager/healthcheck.go b/go/vt/tabletmanager/healthcheck.go index 68baf334bf3..4e2ba749804 100644 --- a/go/vt/tabletmanager/healthcheck.go +++ b/go/vt/tabletmanager/healthcheck.go @@ -11,53 +11,81 @@ package tabletmanager import ( "flag" "fmt" + "html/template" "reflect" "time" - "code.google.com/p/go.net/context" - log "github.com/golang/glog" "github.com/youtube/vitess/go/timer" "github.com/youtube/vitess/go/vt/health" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" ) +const ( + defaultDegradedThreshold = time.Duration(30 * time.Second) + defaultUnhealthyThreshold = time.Duration(2 * time.Hour) +) + var ( healthCheckInterval = flag.Duration("health_check_interval", 20*time.Second, "Interval between health checks") targetTabletType = flag.String("target_tablet_type", "", "The tablet type we are thriving to be when healthy. When not healthy, we'll go to spare.") + degradedThreshold = flag.Duration("degraded_threshold", defaultDegradedThreshold, "replication lag after which a replica is considered degraded") + unhealthyThreshold = flag.Duration("unhealthy_threshold", defaultUnhealthyThreshold, "replication lag after which a replica is considered unhealthy") ) // HealthRecord records one run of the health checker type HealthRecord struct { - Error error - Result map[string]string - Time time.Time + Error error + ReplicationDelay time.Duration + Time time.Time } -// This returns a readable one word version of the health +// Class returns a human-readable one word version of the health state. func (r *HealthRecord) Class() string { switch { case r.Error != nil: return "unhealthy" - case len(r.Result) > 0: + case r.ReplicationDelay > *degradedThreshold: return "unhappy" default: return "healthy" } } +// HTML returns a html version to be displayed on UIs +func (r *HealthRecord) HTML() template.HTML { + switch { + case r.Error != nil: + return template.HTML(fmt.Sprintf("unhealthy: %v", r.Error)) + case r.ReplicationDelay > *degradedThreshold: + return template.HTML(fmt.Sprintf("unhappy: %v behind on replication", r.ReplicationDelay)) + default: + if r.ReplicationDelay > 0 { + return template.HTML(fmt.Sprintf("healthy: only %v behind on replication", r.ReplicationDelay)) + } + return template.HTML("healthy") + } +} + // IsDuplicate implements history.Deduplicable func (r *HealthRecord) IsDuplicate(other interface{}) bool { rother, ok := other.(*HealthRecord) if !ok { return false } - return reflect.DeepEqual(r.Error, rother.Error) && reflect.DeepEqual(r.Result, rother.Result) + if !reflect.DeepEqual(r.Error, rother.Error) { + return false + } + unhealthy := r.ReplicationDelay > *degradedThreshold + unhealthyOther := rother.ReplicationDelay > *degradedThreshold + return (unhealthy && unhealthyOther) || (!unhealthy && !unhealthyOther) } +// IsRunningHealthCheck indicates if the agent is configured to run healthchecks. func (agent *ActionAgent) IsRunningHealthCheck() bool { return *targetTabletType != "" } @@ -83,6 +111,7 @@ func (agent *ActionAgent) initHeathCheck() { t.Start(func() { agent.runHealthCheck(topo.TabletType(*targetTabletType)) }) + t.Trigger() } // runHealthCheck takes the action mutex, runs the health check, @@ -103,32 +132,52 @@ func (agent *ActionAgent) runHealthCheck(targetTabletType topo.TabletType) { tabletControl := agent._tabletControl agent.mutex.Unlock() + // figure out if we should be running the query service + shouldQueryServiceBeRunning := false + var blacklistedTables []string + if topo.IsRunningQueryService(targetTabletType) && agent.BinlogPlayerMap.size() == 0 { + shouldQueryServiceBeRunning = true + if tabletControl != nil { + blacklistedTables = tabletControl.BlacklistedTables + if tabletControl.DisableQueryService { + shouldQueryServiceBeRunning = false + } + } + } + // run the health check typeForHealthCheck := targetTabletType if tablet.Type == topo.TYPE_MASTER { typeForHealthCheck = topo.TYPE_MASTER } - health, err := health.Run(typeForHealthCheck) + replicationDelay, err := health.Run(typeForHealthCheck, shouldQueryServiceBeRunning) + health := make(map[string]string) + if err == nil { + if replicationDelay > *unhealthyThreshold { + err = fmt.Errorf("reported replication lag: %v higher than unhealthy threshold: %v", replicationDelay.Seconds(), unhealthyThreshold.Seconds()) + } else if replicationDelay > *degradedThreshold { + health[topo.ReplicationLag] = topo.ReplicationLagHigh + } + } // Figure out if we should be running QueryService. If we should, - // and we aren't, and we're otherwise healthy, try to start it. - if err == nil && topo.IsRunningQueryService(targetTabletType) && agent.BinlogPlayerMap.size() == 0 { - var blacklistedTables []string - disableQueryService := false - if tabletControl != nil { - blacklistedTables = tabletControl.BlacklistedTables - disableQueryService = tabletControl.DisableQueryService - } - if !disableQueryService { + // and we aren't, try to start it (even if we're not healthy, + // the reason we might not be healthy is the query service not running!) + if shouldQueryServiceBeRunning { + if err == nil { + // we remember this new possible error err = agent.allowQueries(tablet.Tablet, blacklistedTables) + } else { + // we ignore the error + agent.allowQueries(tablet.Tablet, blacklistedTables) } } // save the health record record := &HealthRecord{ - Error: err, - Result: health, - Time: time.Now(), + Error: err, + ReplicationDelay: replicationDelay, + Time: time.Now(), } agent.History.Add(record) @@ -163,6 +212,25 @@ func (agent *ActionAgent) runHealthCheck(targetTabletType topo.TabletType) { } } + // remember our health status + agent.mutex.Lock() + agent._healthy = err + agent._replicationDelay = replicationDelay + agent.mutex.Unlock() + + // send it to our observers, after we've updated the tablet state + // (Tablet is a pointer, and below we will alter the Tablet + // record to be correct. + hsr := &actionnode.HealthStreamReply{ + Tablet: tablet.Tablet, + BinlogPlayerMapSize: agent.BinlogPlayerMap.size(), + ReplicationDelay: replicationDelay, + } + if err != nil { + hsr.HealthError = err.Error() + } + defer agent.BroadcastHealthStreamReply(hsr) + // Update our topo.Server state, start with no change newTabletType := tablet.Type if err != nil { @@ -203,10 +271,12 @@ func (agent *ActionAgent) runHealthCheck(targetTabletType topo.TabletType) { // Change the Type, update the health. Note we pass in a map // that's not nil, meaning if it's empty, we will clear it. - if err := topotools.ChangeType(agent.TopoServer, tablet.Alias, newTabletType, health, true /*runHooks*/); err != nil { + if err := topotools.ChangeType(agent.batchCtx, agent.TopoServer, tablet.Alias, newTabletType, health, true /*runHooks*/); err != nil { log.Infof("Error updating tablet record: %v", err) return } + tablet.Health = health + tablet.Type = newTabletType // Rebuild the serving graph in our cell, only if we're dealing with // a serving type @@ -215,7 +285,7 @@ func (agent *ActionAgent) runHealthCheck(targetTabletType topo.TabletType) { } // run the post action callbacks, not much we can do with returned error - if err := agent.refreshTablet("healthcheck"); err != nil { + if err := agent.refreshTablet(agent.batchCtx, "healthcheck"); err != nil { log.Warningf("refreshTablet failed: %v", err) } } @@ -238,7 +308,7 @@ func (agent *ActionAgent) terminateHealthChecks(targetTabletType topo.TabletType // Change the Type to spare, update the health. Note we pass in a map // that's not nil, meaning we will clear it. - if err := topotools.ChangeType(agent.TopoServer, tablet.Alias, topo.TYPE_SPARE, make(map[string]string), true /*runHooks*/); err != nil { + if err := topotools.ChangeType(agent.batchCtx, agent.TopoServer, tablet.Alias, topo.TYPE_SPARE, make(map[string]string), true /*runHooks*/); err != nil { log.Infof("Error updating tablet record: %v", err) return } @@ -253,7 +323,7 @@ func (agent *ActionAgent) terminateHealthChecks(targetTabletType topo.TabletType // ourself as OnTermSync (synchronous). The rest can be done asynchronously. go func() { // Run the post action callbacks (let them shutdown the query service) - if err := agent.refreshTablet("terminatehealthcheck"); err != nil { + if err := agent.refreshTablet(agent.batchCtx, "terminatehealthcheck"); err != nil { log.Warningf("refreshTablet failed: %v", err) } }() @@ -262,11 +332,8 @@ func (agent *ActionAgent) terminateHealthChecks(targetTabletType topo.TabletType // rebuildShardIfNeeded will rebuild the serving graph if we need to func (agent *ActionAgent) rebuildShardIfNeeded(tablet *topo.TabletInfo, targetTabletType topo.TabletType) error { if topo.IsInServingGraph(targetTabletType) { - // TODO: interrupted may need to be a global one closed when we exit - interrupted := make(chan struct{}) - // no need to take the shard lock in this case - if _, err := topotools.RebuildShard(context.TODO(), logutil.NewConsoleLogger(), agent.TopoServer, tablet.Keyspace, tablet.Shard, []string{tablet.Alias.Cell}, agent.LockTimeout, interrupted); err != nil { + if _, err := topotools.RebuildShard(agent.batchCtx, logutil.NewConsoleLogger(), agent.TopoServer, tablet.Keyspace, tablet.Shard, []string{tablet.Alias.Cell}, agent.LockTimeout); err != nil { return fmt.Errorf("topotools.RebuildShard returned an error: %v", err) } } diff --git a/go/vt/tabletmanager/healthcheck_test.go b/go/vt/tabletmanager/healthcheck_test.go index 1a884300d44..4621da6dca0 100644 --- a/go/vt/tabletmanager/healthcheck_test.go +++ b/go/vt/tabletmanager/healthcheck_test.go @@ -24,18 +24,18 @@ func TestHealthRecordDeduplication(t *testing.T) { duplicate: true, }, { - left: &HealthRecord{Time: now, Result: map[string]string{"a": "1"}}, - right: &HealthRecord{Time: later, Result: map[string]string{"a": "1"}}, + left: &HealthRecord{Time: now, ReplicationDelay: defaultDegradedThreshold / 2}, + right: &HealthRecord{Time: later, ReplicationDelay: defaultDegradedThreshold / 3}, duplicate: true, }, { - left: &HealthRecord{Time: now, Result: map[string]string{"a": "1"}}, - right: &HealthRecord{Time: later, Result: map[string]string{"a": "2"}}, + left: &HealthRecord{Time: now, ReplicationDelay: defaultDegradedThreshold / 2}, + right: &HealthRecord{Time: later, ReplicationDelay: defaultDegradedThreshold * 2}, duplicate: false, }, { - left: &HealthRecord{Time: now, Error: errors.New("foo"), Result: map[string]string{"a": "1"}}, - right: &HealthRecord{Time: later, Result: map[string]string{"a": "1"}}, + left: &HealthRecord{Time: now, Error: errors.New("foo"), ReplicationDelay: defaultDegradedThreshold * 2}, + right: &HealthRecord{Time: later, ReplicationDelay: defaultDegradedThreshold * 2}, duplicate: false, }, } @@ -61,11 +61,11 @@ func TestHealthRecordClass(t *testing.T) { state: "unhealthy", }, { - r: &HealthRecord{Result: map[string]string{"1": "1"}}, + r: &HealthRecord{ReplicationDelay: defaultDegradedThreshold * 2}, state: "unhappy", }, { - r: &HealthRecord{Result: map[string]string{}}, + r: &HealthRecord{ReplicationDelay: defaultDegradedThreshold / 2}, state: "healthy", }, } diff --git a/go/vt/tabletmanager/init_tablet.go b/go/vt/tabletmanager/init_tablet.go new file mode 100644 index 00000000000..10aaf2c169a --- /dev/null +++ b/go/vt/tabletmanager/init_tablet.go @@ -0,0 +1,228 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tabletmanager + +// This file handles the initialization of the tablet at startup time. +// It is only enabled if init_tablet_type or init_keyspace is set. + +import ( + "flag" + "fmt" + "time" + + log "github.com/golang/glog" + "github.com/youtube/vitess/go/flagutil" + "github.com/youtube/vitess/go/netutil" + "github.com/youtube/vitess/go/vt/logutil" + "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/topotools" + "golang.org/x/net/context" +) + +var ( + initDbNameOverride = flag.String("init_db_name_override", "", "(init parameter) override the name of the db used by vttablet") + initKeyspace = flag.String("init_keyspace", "", "(init parameter) keyspace to use for this tablet") + initShard = flag.String("init_shard", "", "(init parameter) shard to use for this tablet") + initTags flagutil.StringMapValue + initTabletType = flag.String("init_tablet_type", "", "(init parameter) the tablet type to use for this tablet. Incompatible with target_tablet_type.") + initTimeout = flag.Duration("init_timeout", 1*time.Minute, "(init parameter) timeout to use for the init phase.") +) + +func init() { + flag.Var(&initTags, "init_tags", "(init parameter) comma separated list of key:value pairs used to tag the tablet") +} + +// InitTablet initializes the tablet record if necessary. +func (agent *ActionAgent) InitTablet(port, securePort int) error { + // only enabled if one of init_tablet_type (when healthcheck + // is disabled) or init_keyspace (when healthcheck is enabled) + // is passed in, then check other parameters + if *initTabletType == "" && *initKeyspace == "" { + return nil + } + + // figure out our default target type + var tabletType topo.TabletType + if *initTabletType != "" { + if *targetTabletType != "" { + log.Fatalf("cannot specify both target_tablet_type and init_tablet_type parameters (as they might conflict)") + } + + // use the type specified on the command line + tabletType = topo.TabletType(*initTabletType) + if !topo.IsTypeInList(tabletType, topo.AllTabletTypes) { + log.Fatalf("InitTablet encountered unknown init_tablet_type '%v'", *initTabletType) + } + if tabletType == topo.TYPE_MASTER || tabletType == topo.TYPE_SCRAP { + // We disallow TYPE_MASTER, so we don't have to change + // shard.MasterAlias, and deal with the corner cases. + // We also disallow TYPE_SCRAP, obviously. + log.Fatalf("init_tablet_type cannot be %v", tabletType) + } + + } else if *targetTabletType != "" { + // use spare, the healthcheck will turn us into what + // we need to be eventually + tabletType = topo.TYPE_SPARE + + } else { + log.Fatalf("if init tablet is enabled, one of init_tablet_type or target_tablet_type needs to be specified") + } + + // create a context for this whole operation + ctx, cancel := context.WithTimeout(agent.batchCtx, *initTimeout) + defer cancel() + + // if we're assigned to a shard, make sure it exists, see if + // we are its master, and update its cells list if necessary + if tabletType != topo.TYPE_IDLE { + if *initKeyspace == "" || *initShard == "" { + log.Fatalf("if init tablet is enabled and the target type is not idle, init_keyspace and init_shard also need to be specified") + } + shard, _, err := topo.ValidateShardName(*initShard) + if err != nil { + log.Fatalf("cannot validate shard name: %v", err) + } + + log.Infof("Reading shard record %v/%v", *initKeyspace, shard) + + // read the shard, create it if necessary + si, err := agent.TopoServer.GetShard(*initKeyspace, shard) + if err == topo.ErrNoNode { + // create the keyspace, maybe it already exists + if err := agent.TopoServer.CreateKeyspace(*initKeyspace, &topo.Keyspace{}); err != nil && err != topo.ErrNodeExists { + return fmt.Errorf("CreateKeyspace(%v) failed: %v", *initKeyspace, err) + } + + // create the shard + if err := topo.CreateShard(agent.TopoServer, *initKeyspace, shard); err != nil { + return fmt.Errorf("CreateShard(%v/%v) failed: %v", *initKeyspace, shard, err) + } + + // and re-read the shard object + si, err = agent.TopoServer.GetShard(*initKeyspace, shard) + } + if err != nil { + return fmt.Errorf("InitTablet cannot read shard: %v", err) + } + if si.MasterAlias == agent.TabletAlias { + // we are the current master for this shard (probably + // means the master tablet process was just restarted), + // so InitTablet as master. + tabletType = topo.TYPE_MASTER + } + + // See if we need to add the tablet's cell to the shard's cell + // list. If we do, it has to be under the shard lock. + if !si.HasCell(agent.TabletAlias.Cell) { + actionNode := actionnode.UpdateShard() + lockPath, err := actionNode.LockShard(ctx, agent.TopoServer, *initKeyspace, shard) + if err != nil { + return fmt.Errorf("LockShard(%v/%v) failed: %v", *initKeyspace, shard, err) + } + + // re-read the shard with the lock + si, err = agent.TopoServer.GetShard(*initKeyspace, shard) + if err != nil { + return actionNode.UnlockShard(ctx, agent.TopoServer, *initKeyspace, shard, lockPath, err) + } + + // see if we really need to update it now + if !si.HasCell(agent.TabletAlias.Cell) { + si.Cells = append(si.Cells, agent.TabletAlias.Cell) + + // write it back + if err := topo.UpdateShard(ctx, agent.TopoServer, si); err != nil { + return actionNode.UnlockShard(ctx, agent.TopoServer, *initKeyspace, shard, lockPath, err) + } + } + + // and unlock + if err := actionNode.UnlockShard(ctx, agent.TopoServer, *initKeyspace, shard, lockPath, nil); err != nil { + return err + } + } + } + log.Infof("Initializing the tablet for type %v", tabletType) + + // figure out the hostname + hostname := *tabletHostname + if hostname == "" { + var err error + hostname, err = netutil.FullyQualifiedHostname() + if err != nil { + return err + } + } + + // create and populate tablet record + tablet := &topo.Tablet{ + Alias: agent.TabletAlias, + Hostname: hostname, + Portmap: make(map[string]int), + Keyspace: *initKeyspace, + Shard: *initShard, + Type: tabletType, + DbNameOverride: *initDbNameOverride, + Tags: initTags, + } + if port != 0 { + tablet.Portmap["vt"] = port + } + if securePort != 0 { + tablet.Portmap["vts"] = securePort + } + if err := tablet.Complete(); err != nil { + return fmt.Errorf("InitTablet tablet.Complete failed: %v", err) + } + + // now try to create the record + err := topo.CreateTablet(agent.TopoServer, tablet) + switch err { + case nil: + // it worked, we're good, can update the replication graph + if tablet.IsInReplicationGraph() { + if err := topo.UpdateTabletReplicationData(ctx, agent.TopoServer, tablet); err != nil { + return fmt.Errorf("UpdateTabletReplicationData failed: %v", err) + } + } + + case topo.ErrNodeExists: + // The node already exists, will just try to update + // it. So we read it first. + oldTablet, err := agent.TopoServer.GetTablet(tablet.Alias) + if err != nil { + fmt.Errorf("InitTablet failed to read existing tablet record: %v", err) + } + + // Sanity check the keyspace and shard + if oldTablet.Keyspace != tablet.Keyspace || oldTablet.Shard != tablet.Shard { + return fmt.Errorf("InitTablet failed because existing tablet keyspace and shard %v/%v differ from the provided ones %v/%v", oldTablet.Keyspace, oldTablet.Shard, tablet.Keyspace, tablet.Shard) + } + + // And overwrite the rest + *(oldTablet.Tablet) = *tablet + if err := topo.UpdateTablet(ctx, agent.TopoServer, oldTablet); err != nil { + return fmt.Errorf("UpdateTablet failed: %v", err) + } + + // Note we don't need to UpdateTabletReplicationData + // as the tablet already existed with the right data + // in the replication graph + default: + return fmt.Errorf("CreateTablet failed: %v", err) + } + + // and now rebuild the serving graph. Note we do that in any case, + // to clean any inaccurate record from any part of the serving graph. + if tabletType != topo.TYPE_IDLE { + if _, err := topotools.RebuildShard(ctx, logutil.NewConsoleLogger(), agent.TopoServer, tablet.Keyspace, tablet.Shard, []string{tablet.Alias.Cell}, agent.LockTimeout); err != nil { + return fmt.Errorf("RebuildShard failed: %v", err) + } + } + + return nil +} diff --git a/go/vt/tabletmanager/init_tablet_test.go b/go/vt/tabletmanager/init_tablet_test.go new file mode 100644 index 00000000000..e08525ffe9b --- /dev/null +++ b/go/vt/tabletmanager/init_tablet_test.go @@ -0,0 +1,178 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tabletmanager + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/youtube/vitess/go/history" + "github.com/youtube/vitess/go/stats" + "github.com/youtube/vitess/go/vt/mysqlctl" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/zktopo" + "golang.org/x/net/context" +) + +// TestInitTablet will test the InitTablet code creates / updates the +// tablet node correctly. Note we modify global parameters (the flags) +// so this has to be in one test. +func TestInitTablet(t *testing.T) { + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) + tabletAlias := topo.TabletAlias{ + Cell: "cell1", + Uid: 1, + } + + // start with idle, and a tablet record that doesn't exist + port := 1234 + securePort := 2345 + mysqlDaemon := &mysqlctl.FakeMysqlDaemon{} + agent := &ActionAgent{ + TopoServer: ts, + TabletAlias: tabletAlias, + Mysqld: nil, + MysqlDaemon: mysqlDaemon, + DBConfigs: nil, + SchemaOverrides: nil, + BinlogPlayerMap: nil, + LockTimeout: 10 * time.Second, + batchCtx: context.Background(), + History: history.New(historyLength), + lastHealthMapCount: new(stats.Int), + _healthy: fmt.Errorf("healthcheck not run yet"), + } + *initTabletType = "idle" + *tabletHostname = "localhost" + if err := agent.InitTablet(port, securePort); err != nil { + t.Fatalf("NewTestActionAgent(idle) failed: %v", err) + } + ti, err := ts.GetTablet(tabletAlias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if ti.Type != topo.TYPE_IDLE { + t.Errorf("wrong type for tablet: %v", ti.Type) + } + if ti.Hostname != "localhost" { + t.Errorf("wrong hostname for tablet: %v", ti.Hostname) + } + if ti.Portmap["vt"] != port { + t.Errorf("wrong port for tablet: %v", ti.Portmap["vt"]) + } + if ti.Portmap["vts"] != securePort { + t.Errorf("wrong secure port for tablet: %v", ti.Portmap["vts"]) + } + + // try again now that the node exists + port = 3456 + securePort = 4567 + if err := agent.InitTablet(port, securePort); err != nil { + t.Fatalf("NewTestActionAgent(idle again) failed: %v", err) + } + ti, err = ts.GetTablet(tabletAlias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if ti.Portmap["vt"] != port { + t.Errorf("wrong port for tablet: %v", ti.Portmap["vt"]) + } + if ti.Portmap["vts"] != securePort { + t.Errorf("wrong secure port for tablet: %v", ti.Portmap["vts"]) + } + + // try with a keyspace and shard on the previously idle tablet, + // should fail + *initTabletType = "replica" + *initKeyspace = "test_keyspace" + *initShard = "-80" + if err := agent.InitTablet(port, securePort); err == nil || !strings.Contains(err.Error(), "InitTablet failed because existing tablet keyspace and shard / differ from the provided ones test_keyspace/-80") { + t.Fatalf("InitTablet(type over idle) didn't fail correctly: %v", err) + } + + // now let's use a different real tablet in a shard, that will create + // the keyspace and shard. + tabletAlias = topo.TabletAlias{ + Cell: "cell1", + Uid: 2, + } + agent.TabletAlias = tabletAlias + if err := agent.InitTablet(port, securePort); err != nil { + t.Fatalf("InitTablet(type) failed: %v", err) + } + si, err := ts.GetShard("test_keyspace", "-80") + if err != nil { + t.Fatalf("GetShard failed: %v", err) + } + if len(si.Cells) != 1 || si.Cells[0] != "cell1" { + t.Errorf("shard.Cells not updated properly: %v", si) + } + ti, err = ts.GetTablet(tabletAlias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if ti.Type != topo.TYPE_REPLICA { + t.Errorf("wrong tablet type: %v", ti.Type) + } + + // try to init again, this time with health check on + *initTabletType = "" + *targetTabletType = "replica" + if err := agent.InitTablet(port, securePort); err != nil { + t.Fatalf("InitTablet(type, healthcheck) failed: %v", err) + } + ti, err = ts.GetTablet(tabletAlias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if ti.Type != topo.TYPE_SPARE { + t.Errorf("wrong tablet type: %v", ti.Type) + } + + // update shard's master to our alias, then try to init again + si, err = ts.GetShard("test_keyspace", "-80") + if err != nil { + t.Fatalf("GetShard failed: %v", err) + } + si.MasterAlias = tabletAlias + if err := topo.UpdateShard(context.Background(), ts, si); err != nil { + t.Fatalf("UpdateShard failed: %v", err) + } + if err := agent.InitTablet(port, securePort); err != nil { + t.Fatalf("InitTablet(type, healthcheck) failed: %v", err) + } + ti, err = ts.GetTablet(tabletAlias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if ti.Type != topo.TYPE_MASTER { + t.Errorf("wrong tablet type: %v", ti.Type) + } + + // init again with the tablet_type set, no healthcheck + // (also check db name override and tags here) + *initTabletType = "replica" + *targetTabletType = "" + *initDbNameOverride = "DBNAME" + initTags.Set("aaa:bbb") + if err := agent.InitTablet(port, securePort); err != nil { + t.Fatalf("InitTablet(type, healthcheck) failed: %v", err) + } + ti, err = ts.GetTablet(tabletAlias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if ti.Type != topo.TYPE_MASTER { + t.Errorf("wrong tablet type: %v", ti.Type) + } + if ti.DbNameOverride != "DBNAME" { + t.Errorf("wrong tablet DbNameOverride: %v", ti.DbNameOverride) + } + if len(ti.Tags) != 1 || ti.Tags["aaa"] != "bbb" { + t.Errorf("wrong tablet tags: %v", ti.Tags) + } +} diff --git a/go/vt/tabletmanager/reparent.go b/go/vt/tabletmanager/reparent.go new file mode 100644 index 00000000000..aee1ca7d686 --- /dev/null +++ b/go/vt/tabletmanager/reparent.go @@ -0,0 +1,387 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tabletmanager + +import ( + "flag" + "fmt" + "sync" + "time" + + log "github.com/golang/glog" + "github.com/youtube/vitess/go/event" + "github.com/youtube/vitess/go/trace" + "github.com/youtube/vitess/go/vt/concurrency" + "github.com/youtube/vitess/go/vt/logutil" + "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" + "github.com/youtube/vitess/go/vt/tabletmanager/tmclient" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/topotools" + "github.com/youtube/vitess/go/vt/topotools/events" + "golang.org/x/net/context" +) + +var ( + fastReparent = flag.Bool("fast_external_reparent", false, "Skip updating of fields in topology that aren't needed if all MySQL reparents are done by an external tool, instead of by Vitess directly.") + + finalizeReparentTimeout = flag.Duration("finalize_external_reparent_timeout", 10*time.Second, "Timeout for the finalize stage of a fast external reparent reconciliation.") +) + +// SetReparentFlags changes flag values. It should only be used in tests. +func SetReparentFlags(fast bool, timeout time.Duration) { + *fastReparent = fast + *finalizeReparentTimeout = timeout +} + +// fastTabletExternallyReparented completely replaces TabletExternallyReparented +// if the -fast_external_reparent flag is specified. +func (agent *ActionAgent) fastTabletExternallyReparented(ctx context.Context, externalID string) (err error) { + // If there is a finalize step running, wait for it to finish or time out + // before checking the global shard record again. + if agent.finalizeReparentCtx != nil { + select { + case <-agent.finalizeReparentCtx.Done(): + agent.finalizeReparentCtx = nil + case <-ctx.Done(): + return ctx.Err() + } + } + + tablet := agent.Tablet() + + // Check the global shard record. + si, err := topo.GetShard(ctx, agent.TopoServer, tablet.Keyspace, tablet.Shard) + if err != nil { + log.Warningf("fastTabletExternallyReparented: failed to read global shard record for %v/%v: %v", tablet.Keyspace, tablet.Shard, err) + return err + } + if si.MasterAlias == tablet.Alias { + // We may get called on the current master even when nothing has changed. + // If the global shard record is already updated, it means we successfully + // finished a previous reparent to this tablet. + return nil + } + + // Create a reusable Reparent event with available info. + ev := &events.Reparent{ + ShardInfo: *si, + NewMaster: *tablet.Tablet, + OldMaster: topo.Tablet{Alias: si.MasterAlias, Type: topo.TYPE_MASTER}, + ExternalID: externalID, + } + defer func() { + if err != nil { + event.DispatchUpdate(ev, "failed: "+err.Error()) + } + }() + event.DispatchUpdate(ev, "starting external from tablet (fast)") + + // Execute state change to master by force-updating only the local copy of the + // tablet record. The actual record in topo will be updated later. + log.Infof("fastTabletExternallyReparented: executing change callback for state change to MASTER") + oldTablet := *tablet.Tablet + newTablet := oldTablet + newTablet.Type = topo.TYPE_MASTER + newTablet.State = topo.STATE_READ_WRITE + newTablet.Health = nil + agent.setTablet(topo.NewTabletInfo(&newTablet, -1)) + if err := agent.updateState(ctx, &oldTablet, "fastTabletExternallyReparented"); err != nil { + return fmt.Errorf("fastTabletExternallyReparented: failed to change tablet state to MASTER: %v", err) + } + + // Directly write the new master endpoint in the serving graph. + // We will do a true rebuild in the background soon, but in the meantime, + // this will be enough for clients to re-resolve the new master. + event.DispatchUpdate(ev, "writing new master endpoint") + log.Infof("fastTabletExternallyReparented: writing new master endpoint to serving graph") + ep, err := tablet.EndPoint() + if err != nil { + return fmt.Errorf("fastTabletExternallyReparented: failed to generate EndPoint for tablet %v: %v", tablet.Alias, err) + } + err = topo.UpdateEndPoints(ctx, agent.TopoServer, tablet.Alias.Cell, + si.Keyspace(), si.ShardName(), topo.TYPE_MASTER, + &topo.EndPoints{Entries: []topo.EndPoint{*ep}}) + if err != nil { + return fmt.Errorf("fastTabletExternallyReparented: failed to update master endpoint: %v", err) + } + + // Start the finalize stage with a background context, but connect the trace. + bgCtx, cancel := context.WithTimeout(agent.batchCtx, *finalizeReparentTimeout) + bgCtx = trace.CopySpan(bgCtx, ctx) + agent.finalizeReparentCtx = bgCtx + go func() { + err := agent.finalizeTabletExternallyReparented(bgCtx, si, ev) + cancel() + + if err != nil { + log.Warningf("finalizeTabletExternallyReparented error: %v", err) + event.DispatchUpdate(ev, "failed: "+err.Error()) + } + }() + + return nil +} + +// finalizeTabletExternallyReparented performs slow, synchronized reconciliation +// tasks that ensure topology is self-consistent, and then marks the reparent as +// finished by updating the global shard record. +func (agent *ActionAgent) finalizeTabletExternallyReparented(ctx context.Context, si *topo.ShardInfo, ev *events.Reparent) (err error) { + var wg sync.WaitGroup + var errs concurrency.AllErrorRecorder + oldMasterAlias := si.MasterAlias + + // Update the tablet records for the old and new master concurrently. + // We don't need a lock to update them because they are the source of truth, + // not derived values (like the serving graph). + event.DispatchUpdate(ev, "updating old and new master tablet records") + log.Infof("finalizeTabletExternallyReparented: updating tablet records") + wg.Add(1) + go func() { + // Update our own record to master. + err := topo.UpdateTabletFields(ctx, agent.TopoServer, agent.TabletAlias, + func(tablet *topo.Tablet) error { + tablet.Type = topo.TYPE_MASTER + tablet.State = topo.STATE_READ_WRITE + tablet.Health = nil + return nil + }) + errs.RecordError(err) + wg.Done() + }() + + if !oldMasterAlias.IsZero() { + wg.Add(1) + go func() { + // Force the old master to spare. + var oldMasterTablet *topo.Tablet + err := topo.UpdateTabletFields(ctx, agent.TopoServer, oldMasterAlias, + func(tablet *topo.Tablet) error { + tablet.Type = topo.TYPE_SPARE + oldMasterTablet = tablet + return nil + }) + errs.RecordError(err) + wg.Done() + if err != nil { + return + } + + // Tell the old master to refresh its state. We don't need to wait for it. + if oldMasterTablet != nil { + tmc := tmclient.NewTabletManagerClient() + tmc.RefreshState(ctx, topo.NewTabletInfo(oldMasterTablet, -1)) + } + }() + } + + tablet := agent.Tablet() + + // Wait for the tablet records to be updated. At that point, any rebuild will + // see the new master, so we're ready to mark the reparent as done in the + // global shard record. + wg.Wait() + if errs.HasErrors() { + return errs.Error() + } + + // Update the master field in the global shard record. We don't use a lock + // here anymore. The lock was only to ensure that the global shard record + // didn't get modified between the time when we read it and the time when we + // write it back. Now we use an update loop pattern to do that instead. + event.DispatchUpdate(ev, "updating global shard record") + log.Infof("finalizeTabletExternallyReparented: updating global shard record") + topo.UpdateShardFields(ctx, agent.TopoServer, tablet.Keyspace, tablet.Shard, func(shard *topo.Shard) error { + shard.MasterAlias = tablet.Alias + return nil + }) + + // Rebuild the shard serving graph in the necessary cells. + // If it's a cross-cell reparent, rebuild all cells (by passing nil). + // If it's a same-cell reparent, we only need to rebuild the master cell. + event.DispatchUpdate(ev, "rebuilding shard serving graph") + var cells []string + if oldMasterAlias.Cell == tablet.Alias.Cell { + cells = []string{tablet.Alias.Cell} + } + logger := logutil.NewConsoleLogger() + log.Infof("finalizeTabletExternallyReparented: rebuilding shard") + if _, err = topotools.RebuildShard(ctx, logger, agent.TopoServer, tablet.Keyspace, tablet.Shard, cells, agent.LockTimeout); err != nil { + return err + } + + event.DispatchUpdate(ev, "finished") + return nil +} + +// TabletExternallyReparented updates all topo records so the current +// tablet is the new master for this shard. +// Should be called under RPCWrapLock. +func (agent *ActionAgent) TabletExternallyReparented(ctx context.Context, externalID string) error { + if *fastReparent { + return agent.fastTabletExternallyReparented(ctx, externalID) + } + + tablet := agent.Tablet() + + // fast quick check on the shard to see if we're not the master already + shardInfo, err := topo.GetShard(ctx, agent.TopoServer, tablet.Keyspace, tablet.Shard) + if err != nil { + log.Warningf("TabletExternallyReparented: Cannot read the shard %v/%v: %v", tablet.Keyspace, tablet.Shard, err) + return err + } + if shardInfo.MasterAlias == agent.TabletAlias { + // we are already the master, nothing more to do. + return nil + } + + // grab the shard lock + actionNode := actionnode.ShardExternallyReparented(agent.TabletAlias) + lockPath, err := actionNode.LockShard(ctx, agent.TopoServer, tablet.Keyspace, tablet.Shard) + if err != nil { + log.Warningf("TabletExternallyReparented: Cannot lock shard %v/%v: %v", tablet.Keyspace, tablet.Shard, err) + return err + } + + // do the work + runAfterAction, err := agent.tabletExternallyReparentedLocked(ctx, externalID) + if err != nil { + log.Warningf("TabletExternallyReparented: internal error: %v", err) + } + + // release the lock in any case, and run refreshTablet if necessary + err = actionNode.UnlockShard(ctx, agent.TopoServer, tablet.Keyspace, tablet.Shard, lockPath, err) + if runAfterAction { + if refreshErr := agent.refreshTablet(ctx, "RPC(TabletExternallyReparented)"); refreshErr != nil { + if err == nil { + // no error yet, now we have one + err = refreshErr + } else { + //have an error already, keep the original one + log.Warningf("refreshTablet failed with error: %v", refreshErr) + } + } + } + return err +} + +// tabletExternallyReparentedLocked is called with the shard lock. +// It returns if agent.refreshTablet should be called, and the error. +// Note both are set independently (can have both true and an error). +func (agent *ActionAgent) tabletExternallyReparentedLocked(ctx context.Context, externalID string) (bool, error) { + // re-read the tablet record to be sure we have the latest version + tablet, err := topo.GetTablet(ctx, agent.TopoServer, agent.TabletAlias) + if err != nil { + return false, err + } + + // read the shard, make sure again the master is not already good. + shardInfo, err := topo.GetShard(ctx, agent.TopoServer, tablet.Keyspace, tablet.Shard) + if err != nil { + return false, err + } + if shardInfo.MasterAlias == tablet.Alias { + log.Infof("TabletExternallyReparented: tablet became the master before we get the lock?") + return false, nil + } + log.Infof("TabletExternallyReparented called and we're not the master, doing the work") + + // Read the tablets, make sure the master elect is known to the shard + // (it's this tablet, so it better be!). + // Note we will keep going with a partial tablet map, which usually + // happens when a cell is not reachable. After these checks, the + // guarantees we'll have are: + // - global cell is reachable (we just locked and read the shard) + // - the local cell that contains the new master is reachable + // (as we're going to check the new master is in the list) + // That should be enough. + tabletMap, err := topo.GetTabletMapForShard(ctx, agent.TopoServer, tablet.Keyspace, tablet.Shard) + switch err { + case nil: + // keep going + case topo.ErrPartialResult: + log.Warningf("Got topo.ErrPartialResult from GetTabletMapForShard, may need to re-init some tablets") + default: + return false, err + } + masterElectTablet, ok := tabletMap[tablet.Alias] + if !ok { + return false, fmt.Errorf("this master-elect tablet %v not found in replication graph %v/%v %v", tablet.Alias, tablet.Keyspace, tablet.Shard, topotools.MapKeys(tabletMap)) + } + + // Create reusable Reparent event with available info + ev := &events.Reparent{ + ShardInfo: *shardInfo, + NewMaster: *tablet.Tablet, + ExternalID: externalID, + } + + if oldMasterTablet, ok := tabletMap[shardInfo.MasterAlias]; ok { + ev.OldMaster = *oldMasterTablet.Tablet + } + + defer func() { + if err != nil { + event.DispatchUpdate(ev, "failed: "+err.Error()) + } + }() + + // sort the tablets, and handle them + slaveTabletMap, masterTabletMap := topotools.SortedTabletMap(tabletMap) + event.DispatchUpdate(ev, "starting external from tablet") + + // We fix the new master in the replication graph. + // Note after this call, we may have changed the tablet record, + // so we will always return true, so the tablet record is re-read + // by the agent. + event.DispatchUpdate(ev, "mark ourself as new master") + err = agent.updateReplicationGraphForPromotedSlave(ctx, tablet) + if err != nil { + // This suggests we can't talk to topo server. This is bad. + return true, fmt.Errorf("updateReplicationGraphForPromotedSlave failed: %v", err) + } + + // Once this tablet is promoted, remove it from our maps + delete(slaveTabletMap, tablet.Alias) + delete(masterTabletMap, tablet.Alias) + + // Then fix all the slaves, including the old master. This + // last step is very likely to time out for some tablets (one + // random guy is dead, the old master is dead, ...). We + // execute them all in parallel until we get to + // wr.ActionTimeout(). After this, no other action with a + // timeout is executed, so even if we got to the timeout, + // we're still good. + event.DispatchUpdate(ev, "restarting slaves") + logger := logutil.NewConsoleLogger() + tmc := tmclient.NewTabletManagerClient() + topotools.RestartSlavesExternal(agent.TopoServer, logger, slaveTabletMap, masterTabletMap, masterElectTablet.Alias, func(ti *topo.TabletInfo, swrd *actionnode.SlaveWasRestartedArgs) error { + return tmc.SlaveWasRestarted(ctx, ti, swrd) + }) + + // Compute the list of Cells we need to rebuild: old master and + // all other cells if reparenting to another cell. + cells := []string{shardInfo.MasterAlias.Cell} + if shardInfo.MasterAlias.Cell != tablet.Alias.Cell { + cells = nil + } + + // now update the master record in the shard object + event.DispatchUpdate(ev, "updating shard record") + log.Infof("Updating Shard's MasterAlias record") + shardInfo.MasterAlias = tablet.Alias + if err = topo.UpdateShard(ctx, agent.TopoServer, shardInfo); err != nil { + return true, err + } + + // and rebuild the shard serving graph + event.DispatchUpdate(ev, "rebuilding shard serving graph") + log.Infof("Rebuilding shard serving graph data") + if _, err = topotools.RebuildShard(ctx, logger, agent.TopoServer, tablet.Keyspace, tablet.Shard, cells, agent.LockTimeout); err != nil { + return true, err + } + + event.DispatchUpdate(ev, "finished") + return true, nil +} diff --git a/go/vt/tabletmanager/rpc_server.go b/go/vt/tabletmanager/rpc_server.go index faa49c27fb2..eb88eddccf8 100644 --- a/go/vt/tabletmanager/rpc_server.go +++ b/go/vt/tabletmanager/rpc_server.go @@ -8,9 +8,9 @@ import ( "fmt" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/callinfo" + "golang.org/x/net/context" ) // This file contains the RPC method helpers for the tablet manager. @@ -54,43 +54,39 @@ func (agent *ActionAgent) rpcWrapper(ctx context.Context, name string, args, rep log.Infof("TabletManager.%v(%v)(from %v): %#v", name, args, from, reply) } if runAfterAction { - err = agent.refreshTablet("RPC(" + name + ")") + err = agent.refreshTablet(ctx, "RPC("+name+")") } return } -// There are multiple kinds of actions: -// 1 - read-only actions that can be executed in parallel. -// verbose is forced to false there. -// 2 - read-write actions that change something, and need to take the -// action lock. -// 3 - read-write actions that need to take the action lock, and also -// need to reload the tablet state. - -func (agent *ActionAgent) RpcWrap(ctx context.Context, name string, args, reply interface{}, f func() error) error { +// RPCWrap is for read-only actions that can be executed concurrently. +// verbose is forced to false. +func (agent *ActionAgent) RPCWrap(ctx context.Context, name string, args, reply interface{}, f func() error) error { return agent.rpcWrapper(ctx, name, args, reply, false /*verbose*/, f, false /*lock*/, false /*runAfterAction*/) } -func (agent *ActionAgent) RpcWrapLock(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error { +// RPCWrapLock is for actions that should not run concurrently with each other. +func (agent *ActionAgent) RPCWrapLock(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error { return agent.rpcWrapper(ctx, name, args, reply, verbose, f, true /*lock*/, false /*runAfterAction*/) } -func (agent *ActionAgent) RpcWrapLockAction(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error { +// RPCWrapLockAction is the same as RPCWrapLock, plus it will call refreshTablet +// after the action returns. +func (agent *ActionAgent) RPCWrapLockAction(ctx context.Context, name string, args, reply interface{}, verbose bool, f func() error) error { return agent.rpcWrapper(ctx, name, args, reply, verbose, f, true /*lock*/, true /*runAfterAction*/) } // -// Glue to delay registration of RPC servers until we have all the objects -// - +// RegisterQueryService is used to delay registration of RPC servers until we have all the objects. type RegisterQueryService func(*ActionAgent) +// RegisterQueryServices is a list of functions to call when the delayed registration is triggered. var RegisterQueryServices []RegisterQueryService -// registerQueryService will register all the instances +// registerQueryService will register all the instances. func (agent *ActionAgent) registerQueryService() { for _, f := range RegisterQueryServices { f(agent) diff --git a/go/vt/tabletmanager/tmclient/rpc_client_api.go b/go/vt/tabletmanager/tmclient/rpc_client_api.go index 118db176c6b..81b54508119 100644 --- a/go/vt/tabletmanager/tmclient/rpc_client_api.go +++ b/go/vt/tabletmanager/tmclient/rpc_client_api.go @@ -8,7 +8,6 @@ import ( "flag" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" mproto "github.com/youtube/vitess/go/mysql/proto" blproto "github.com/youtube/vitess/go/vt/binlog/proto" @@ -17,6 +16,7 @@ import ( myproto "github.com/youtube/vitess/go/vt/mysqlctl/proto" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) var tabletManagerProtocol = flag.String("tablet_manager_protocol", "bson", "the protocol to use to talk to vttablet") @@ -27,9 +27,6 @@ type ErrFunc func() error // SnapshotReplyFunc is used by Snapshot to return result and error type SnapshotReplyFunc func() (*actionnode.SnapshotReply, error) -// MultiSnapshotReplyFunc is used by MultiSnapshot to return result and error -type MultiSnapshotReplyFunc func() (*actionnode.MultiSnapshotReply, error) - // TabletManagerClient defines the interface used to talk to a remote tablet type TabletManagerClient interface { // @@ -37,81 +34,85 @@ type TabletManagerClient interface { // // Ping will try to ping the remote tablet - Ping(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + Ping(ctx context.Context, tablet *topo.TabletInfo) error // GetSchema asks the remote tablet for its database schema - GetSchema(ctx context.Context, tablet *topo.TabletInfo, tables, excludeTables []string, includeViews bool, waitTime time.Duration) (*myproto.SchemaDefinition, error) + GetSchema(ctx context.Context, tablet *topo.TabletInfo, tables, excludeTables []string, includeViews bool) (*myproto.SchemaDefinition, error) // GetPermissions asks the remote tablet for its permissions list - GetPermissions(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (*myproto.Permissions, error) + GetPermissions(ctx context.Context, tablet *topo.TabletInfo) (*myproto.Permissions, error) // // Various read-write methods // // SetReadOnly makes the mysql instance read-only - SetReadOnly(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + SetReadOnly(ctx context.Context, tablet *topo.TabletInfo) error // SetReadWrite makes the mysql instance read-write - SetReadWrite(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + SetReadWrite(ctx context.Context, tablet *topo.TabletInfo) error // ChangeType asks the remote tablet to change its type - ChangeType(ctx context.Context, tablet *topo.TabletInfo, dbType topo.TabletType, waitTime time.Duration) error + ChangeType(ctx context.Context, tablet *topo.TabletInfo, dbType topo.TabletType) error // Scrap scraps the live running tablet - Scrap(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + Scrap(ctx context.Context, tablet *topo.TabletInfo) error // Sleep will sleep for a duration (used for tests) - Sleep(ctx context.Context, tablet *topo.TabletInfo, duration, waitTime time.Duration) error + Sleep(ctx context.Context, tablet *topo.TabletInfo, duration time.Duration) error // ExecuteHook executes the provided hook remotely - ExecuteHook(ctx context.Context, tablet *topo.TabletInfo, hk *hook.Hook, waitTime time.Duration) (*hook.HookResult, error) + ExecuteHook(ctx context.Context, tablet *topo.TabletInfo, hk *hook.Hook) (*hook.HookResult, error) // RefreshState asks the remote tablet to reload its tablet record - RefreshState(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + RefreshState(ctx context.Context, tablet *topo.TabletInfo) error // RunHealthCheck asks the remote tablet to run a health check cycle - RunHealthCheck(ctx context.Context, tablet *topo.TabletInfo, targetTabletType topo.TabletType, waitTime time.Duration) error + RunHealthCheck(ctx context.Context, tablet *topo.TabletInfo, targetTabletType topo.TabletType) error + + // HealthStream asks the tablet to stream its health status on + // a regular basis + HealthStream(ctx context.Context, tablet *topo.TabletInfo) (<-chan *actionnode.HealthStreamReply, ErrFunc, error) // ReloadSchema asks the remote tablet to reload its schema - ReloadSchema(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + ReloadSchema(ctx context.Context, tablet *topo.TabletInfo) error // PreflightSchema will test a schema change - PreflightSchema(ctx context.Context, tablet *topo.TabletInfo, change string, waitTime time.Duration) (*myproto.SchemaChangeResult, error) + PreflightSchema(ctx context.Context, tablet *topo.TabletInfo, change string) (*myproto.SchemaChangeResult, error) // ApplySchema will apply a schema change - ApplySchema(ctx context.Context, tablet *topo.TabletInfo, change *myproto.SchemaChange, waitTime time.Duration) (*myproto.SchemaChangeResult, error) + ApplySchema(ctx context.Context, tablet *topo.TabletInfo, change *myproto.SchemaChange) (*myproto.SchemaChangeResult, error) // ExecuteFetch executes a query remotely using the DBA pool - ExecuteFetch(ctx context.Context, tablet *topo.TabletInfo, query string, maxRows int, wantFields, disableBinlogs bool, waitTime time.Duration) (*mproto.QueryResult, error) + ExecuteFetch(ctx context.Context, tablet *topo.TabletInfo, query string, maxRows int, wantFields, disableBinlogs bool) (*mproto.QueryResult, error) // // Replication related methods // // SlaveStatus returns the tablet's mysql slave status. - SlaveStatus(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (*myproto.ReplicationStatus, error) + SlaveStatus(ctx context.Context, tablet *topo.TabletInfo) (*myproto.ReplicationStatus, error) // WaitSlavePosition asks the tablet to wait until it reaches that // position in mysql replication WaitSlavePosition(ctx context.Context, tablet *topo.TabletInfo, waitPos myproto.ReplicationPosition, waitTime time.Duration) (*myproto.ReplicationStatus, error) // MasterPosition returns the tablet's master position - MasterPosition(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (myproto.ReplicationPosition, error) + MasterPosition(ctx context.Context, tablet *topo.TabletInfo) (myproto.ReplicationPosition, error) // ReparentPosition returns the data for a slave to use to reparent // to the target tablet at the given position. - ReparentPosition(ctx context.Context, tablet *topo.TabletInfo, rp *myproto.ReplicationPosition, waitTime time.Duration) (*actionnode.RestartSlaveData, error) + ReparentPosition(ctx context.Context, tablet *topo.TabletInfo, rp *myproto.ReplicationPosition) (*actionnode.RestartSlaveData, error) // StopSlave stops the mysql replication - StopSlave(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + StopSlave(ctx context.Context, tablet *topo.TabletInfo) error // StopSlaveMinimum stops the mysql replication after it reaches // the provided minimum point StopSlaveMinimum(ctx context.Context, tablet *topo.TabletInfo, stopPos myproto.ReplicationPosition, waitTime time.Duration) (*myproto.ReplicationStatus, error) // StartSlave starts the mysql replication - StartSlave(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + StartSlave(ctx context.Context, tablet *topo.TabletInfo) error // TabletExternallyReparented tells a tablet it is now the master, after an // external tool has already promoted the underlying mysqld to master and @@ -119,10 +120,10 @@ type TabletManagerClient interface { // // externalID is an optional string provided by the external tool that // vttablet will emit in logs to facilitate cross-referencing. - TabletExternallyReparented(ctx context.Context, tablet *topo.TabletInfo, externalID string, waitTime time.Duration) error + TabletExternallyReparented(ctx context.Context, tablet *topo.TabletInfo, externalID string) error // GetSlaves returns the addresses of the slaves - GetSlaves(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) ([]string, error) + GetSlaves(ctx context.Context, tablet *topo.TabletInfo) ([]string, error) // WaitBlpPosition asks the tablet to wait until it reaches that // position in replication @@ -130,10 +131,10 @@ type TabletManagerClient interface { // StopBlp asks the tablet to stop all its binlog players, // and returns the current position for all of them - StopBlp(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (*blproto.BlpPositionList, error) + StopBlp(ctx context.Context, tablet *topo.TabletInfo) (*blproto.BlpPositionList, error) // StartBlp asks the tablet to restart its binlog players - StartBlp(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + StartBlp(ctx context.Context, tablet *topo.TabletInfo) error // RunBlpUntil asks the tablet to restart its binlog players until // it reaches the given positions, if not there yet. @@ -144,51 +145,56 @@ type TabletManagerClient interface { // // DemoteMaster tells the soon-to-be-former master it's gonna change - DemoteMaster(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + DemoteMaster(ctx context.Context, tablet *topo.TabletInfo) error // PromoteSlave transforms the tablet from a slave to a master. - PromoteSlave(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) (*actionnode.RestartSlaveData, error) + PromoteSlave(ctx context.Context, tablet *topo.TabletInfo) (*actionnode.RestartSlaveData, error) // SlaveWasPromoted tells the remote tablet it is now the master - SlaveWasPromoted(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + SlaveWasPromoted(ctx context.Context, tablet *topo.TabletInfo) error // RestartSlave tells the remote tablet it has a new master - RestartSlave(ctx context.Context, tablet *topo.TabletInfo, rsd *actionnode.RestartSlaveData, waitTime time.Duration) error + RestartSlave(ctx context.Context, tablet *topo.TabletInfo, rsd *actionnode.RestartSlaveData) error // SlaveWasRestarted tells the remote tablet its master has changed - SlaveWasRestarted(ctx context.Context, tablet *topo.TabletInfo, args *actionnode.SlaveWasRestartedArgs, waitTime time.Duration) error + SlaveWasRestarted(ctx context.Context, tablet *topo.TabletInfo, args *actionnode.SlaveWasRestartedArgs) error // BreakSlaves will tinker with the replication stream in a // way that will stop all the slaves. - BreakSlaves(ctx context.Context, tablet *topo.TabletInfo, waitTime time.Duration) error + BreakSlaves(ctx context.Context, tablet *topo.TabletInfo) error // // Backup / restore related methods // // Snapshot takes a database snapshot - Snapshot(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.SnapshotArgs, waitTime time.Duration) (<-chan *logutil.LoggerEvent, SnapshotReplyFunc, error) + Snapshot(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.SnapshotArgs) (<-chan *logutil.LoggerEvent, SnapshotReplyFunc, error) // SnapshotSourceEnd restarts the mysql server - SnapshotSourceEnd(ctx context.Context, tablet *topo.TabletInfo, ssea *actionnode.SnapshotSourceEndArgs, waitTime time.Duration) error + SnapshotSourceEnd(ctx context.Context, tablet *topo.TabletInfo, ssea *actionnode.SnapshotSourceEndArgs) error // ReserveForRestore will prepare a server for restore - ReserveForRestore(ctx context.Context, tablet *topo.TabletInfo, rfra *actionnode.ReserveForRestoreArgs, waitTime time.Duration) error + ReserveForRestore(ctx context.Context, tablet *topo.TabletInfo, rfra *actionnode.ReserveForRestoreArgs) error // Restore restores a database snapshot - Restore(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.RestoreArgs, waitTime time.Duration) (<-chan *logutil.LoggerEvent, ErrFunc, error) + Restore(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.RestoreArgs) (<-chan *logutil.LoggerEvent, ErrFunc, error) - // MultiSnapshot takes a database snapshot - MultiSnapshot(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.MultiSnapshotArgs, waitTime time.Duration) (<-chan *logutil.LoggerEvent, MultiSnapshotReplyFunc, error) + // + // RPC related methods + // - // MultiRestore restores a database snapshot - MultiRestore(ctx context.Context, tablet *topo.TabletInfo, sa *actionnode.MultiRestoreArgs, waitTime time.Duration) (<-chan *logutil.LoggerEvent, ErrFunc, error) + // IsTimeoutError checks if an error was caused by an RPC layer timeout vs an application-specific one + IsTimeoutError(err error) bool } +// TabletManagerClientFactory is the factory method to create +// TabletManagerClient objects. type TabletManagerClientFactory func() TabletManagerClient var tabletManagerClientFactories = make(map[string]TabletManagerClientFactory) +// RegisterTabletManagerClientFactory allows modules to register +// TabletManagerClient implementations. Should be called on init(). func RegisterTabletManagerClientFactory(name string, factory TabletManagerClientFactory) { if _, ok := tabletManagerClientFactories[name]; ok { log.Fatalf("RegisterTabletManagerClient %s already exists", name) @@ -196,6 +202,8 @@ func RegisterTabletManagerClientFactory(name string, factory TabletManagerClient tabletManagerClientFactories[name] = factory } +// NewTabletManagerClient creates a new TabletManagerClient. Should be +// called after flags are parsed. func NewTabletManagerClient() TabletManagerClient { f, ok := tabletManagerClientFactories[*tabletManagerProtocol] if !ok { diff --git a/go/vt/tabletserver/cache_pool.go b/go/vt/tabletserver/cache_pool.go index fbbae9f3a6d..f3d030054d5 100644 --- a/go/vt/tabletserver/cache_pool.go +++ b/go/vt/tabletserver/cache_pool.go @@ -65,7 +65,8 @@ func NewCachePool(name string, rowCacheConfig RowCacheConfig, queryTimeout time. cp.port = rowCacheConfig.Socket } if rowCacheConfig.TcpPort > 0 { - cp.port = strconv.Itoa(rowCacheConfig.TcpPort) + //address: ":11211" + cp.port = ":" + strconv.Itoa(rowCacheConfig.TcpPort) } if rowCacheConfig.Connections > 0 { if rowCacheConfig.Connections <= 50 { @@ -87,10 +88,10 @@ func (cp *CachePool) Open() { cp.mu.Lock() defer cp.mu.Unlock() if cp.pool != nil { - panic(NewTabletError(FATAL, "rowcache is already open")) + panic(NewTabletError(ErrFatal, "rowcache is already open")) } if cp.rowCacheConfig.Binary == "" { - panic(NewTabletError(FATAL, "rowcache binary not specified")) + panic(NewTabletError(ErrFatal, "rowcache binary not specified")) } cp.startMemcache() log.Infof("rowcache is enabled") @@ -110,7 +111,7 @@ func (cp *CachePool) startMemcache() { commandLine := cp.rowCacheConfig.GetSubprocessFlags() cp.cmd = exec.Command(commandLine[0], commandLine[1:]...) if err := cp.cmd.Start(); err != nil { - panic(NewTabletError(FATAL, "can't start memcache: %v", err)) + panic(NewTabletError(ErrFatal, "can't start memcache: %v", err)) } attempts := 0 for { @@ -128,7 +129,7 @@ func (cp *CachePool) startMemcache() { continue } if _, err = c.Set("health", 0, 0, []byte("ok")); err != nil { - panic(NewTabletError(FATAL, "can't communicate with memcache: %v", err)) + panic(NewTabletError(ErrFatal, "can't communicate with memcache: %v", err)) } c.Close() break @@ -181,11 +182,11 @@ func (cp *CachePool) getPool() *pools.ResourcePool { func (cp *CachePool) Get(timeout time.Duration) *memcache.Connection { pool := cp.getPool() if pool == nil { - panic(NewTabletError(FATAL, "cache pool is not open")) + panic(NewTabletError(ErrFatal, "cache pool is not open")) } r, err := pool.Get(timeout) if err != nil { - panic(NewTabletErrorSql(FATAL, err)) + panic(NewTabletErrorSql(ErrFatal, err)) } return r.(*memcache.Connection) } diff --git a/go/vt/tabletserver/codex.go b/go/vt/tabletserver/codex.go index 57ad55c5ce2..6ef6a1aa72e 100644 --- a/go/vt/tabletserver/codex.go +++ b/go/vt/tabletserver/codex.go @@ -90,14 +90,14 @@ func resolvePKValues(tableInfo *TableInfo, pkValues []interface{}, bindVars map[ func resolveListArg(col *schema.TableColumn, key string, bindVars map[string]interface{}) ([]sqltypes.Value, error) { val, _, err := sqlparser.FetchBindVar(key, bindVars) if err != nil { - return nil, NewTabletError(FAIL, "%v", err) + return nil, NewTabletError(ErrFail, "%v", err) } list := val.([]interface{}) resolved := make([]sqltypes.Value, len(list)) for i, v := range list { sqlval, err := sqltypes.BuildValue(v) if err != nil { - return nil, NewTabletError(FAIL, "%v", err) + return nil, NewTabletError(ErrFail, "%v", err) } if err = validateValue(col, sqlval); err != nil { return nil, err @@ -134,11 +134,11 @@ func resolveValue(col *schema.TableColumn, value interface{}, bindVars map[strin case string: val, _, err := sqlparser.FetchBindVar(v, bindVars) if err != nil { - return result, NewTabletError(FAIL, "%v", err) + return result, NewTabletError(ErrFail, "%v", err) } sqlval, err := sqltypes.BuildValue(val) if err != nil { - return result, NewTabletError(FAIL, "%v", err) + return result, NewTabletError(ErrFail, "%v", err) } result = sqlval case sqltypes.Value: @@ -155,7 +155,7 @@ func resolveValue(col *schema.TableColumn, value interface{}, bindVars map[strin func validateRow(tableInfo *TableInfo, columnNumbers []int, row []sqltypes.Value) error { if len(row) != len(columnNumbers) { - return NewTabletError(FAIL, "data inconsistency %d vs %d", len(row), len(columnNumbers)) + return NewTabletError(ErrFail, "data inconsistency %d vs %d", len(row), len(columnNumbers)) } for j, value := range row { if err := validateValue(&tableInfo.Columns[columnNumbers[j]], value); err != nil { @@ -173,11 +173,11 @@ func validateValue(col *schema.TableColumn, value sqltypes.Value) error { switch col.Category { case schema.CAT_NUMBER: if !value.IsNumeric() { - return NewTabletError(FAIL, "type mismatch, expecting numeric type for %v", value) + return NewTabletError(ErrFail, "type mismatch, expecting numeric type for %v", value) } case schema.CAT_VARBINARY: if !value.IsString() { - return NewTabletError(FAIL, "type mismatch, expecting string type for %v", value) + return NewTabletError(ErrFail, "type mismatch, expecting string type for %v", value) } } return nil @@ -190,7 +190,7 @@ func getLimit(limit interface{}, bv map[string]interface{}) int64 { case string: lookup, ok := bv[lim[1:]] if !ok { - panic(NewTabletError(FAIL, "missing bind var %s", lim)) + panic(NewTabletError(ErrFail, "missing bind var %s", lim)) } var newlim int64 switch l := lookup.(type) { @@ -201,10 +201,10 @@ func getLimit(limit interface{}, bv map[string]interface{}) int64 { case int: newlim = int64(l) default: - panic(NewTabletError(FAIL, "want number type for %s, got %T", lim, lookup)) + panic(NewTabletError(ErrFail, "want number type for %s, got %T", lim, lookup)) } if newlim < 0 { - panic(NewTabletError(FAIL, "negative limit %d", newlim)) + panic(NewTabletError(ErrFail, "negative limit %d", newlim)) } return newlim case int64: diff --git a/go/vt/tabletserver/connection_killer.go b/go/vt/tabletserver/connection_killer.go index a107b29bd79..a55f83bd540 100644 --- a/go/vt/tabletserver/connection_killer.go +++ b/go/vt/tabletserver/connection_killer.go @@ -59,7 +59,7 @@ type QueryDeadliner chan bool func (ck *ConnectionKiller) SetDeadline(connID int64, deadline Deadline) QueryDeadliner { timeout, err := deadline.Timeout() if err != nil { - panic(NewTabletError(FAIL, "SetDeadline: %v", err)) + panic(NewTabletError(ErrFail, "SetDeadline: %v", err)) } if timeout == 0 { return nil diff --git a/go/vt/tabletserver/consolidator.go b/go/vt/tabletserver/consolidator.go index d697f310883..7d301be8fd7 100644 --- a/go/vt/tabletserver/consolidator.go +++ b/go/vt/tabletserver/consolidator.go @@ -17,7 +17,7 @@ import ( ) var ( - waitError = NewTabletError(FAIL, "Error waiting for consolidation") + waitError = NewTabletError(ErrFail, "Error waiting for consolidation") ) // Consolidator consolidates duplicate queries from executing simulaneously diff --git a/go/vt/tabletserver/customrule/filecustomrule/filecustomrule.go b/go/vt/tabletserver/customrule/filecustomrule/filecustomrule.go new file mode 100644 index 00000000000..1ae0f884eaa --- /dev/null +++ b/go/vt/tabletserver/customrule/filecustomrule/filecustomrule.go @@ -0,0 +1,86 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package filecustomrule implements static custom rule from a config file +package filecustomrule + +import ( + "flag" + "io/ioutil" + "time" + + log "github.com/golang/glog" + "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/tabletserver" +) + +var ( + // Actual FileCustomRule object in charge of rule updates + fileCustomRule = NewFileCustomRule() + // Commandline flag to specify rule path + fileRulePath = flag.String("filecustomrules", "", "file based custom rule path") +) + +// FileCustomRule is an implementation of CustomRuleManager, it reads custom query +// rules from local file for once and push it to vttablet +type FileCustomRule struct { + path string // Path to the file containing custom query rules + currentRuleSet *tabletserver.QueryRules // Query rules built from local file + currentRuleSetTimestamp int64 // Unix timestamp when currentRuleSet is built from local file +} + +// FileCustomRuleSource is the name of the file based custom rule source +const FileCustomRuleSource string = "FILE_CUSTOM_RULE" + +// NewFileCustomRule returns pointer to new FileCustomRule structure +func NewFileCustomRule() (fcr *FileCustomRule) { + fcr = new(FileCustomRule) + fcr.path = "" + fcr.currentRuleSet = tabletserver.NewQueryRules() + return fcr +} + +// Open try to build query rules from local file and push the rules to vttablet +func (fcr *FileCustomRule) Open(rulePath string) error { + fcr.path = rulePath + if fcr.path == "" { + // Don't go further if path is empty + return nil + } + data, err := ioutil.ReadFile(fcr.path) + if err != nil { + log.Warningf("Error reading file %v: %v", fcr.path, err) + // Don't update any internal cache, just return error + return err + } + qrs := tabletserver.NewQueryRules() + err = qrs.UnmarshalJSON(data) + if err != nil { + log.Warningf("Error unmarshaling query rules %v", err) + return err + } + fcr.currentRuleSetTimestamp = time.Now().Unix() + fcr.currentRuleSet = qrs.Copy() + // Push query rules to vttablet + tabletserver.SetQueryRules(FileCustomRuleSource, qrs.Copy()) + log.Infof("Custom rule loaded from file: %s", fcr.path) + return nil +} + +// GetRules returns query rules built from local file +func (fcr *FileCustomRule) GetRules() (qrs *tabletserver.QueryRules, version int64, err error) { + return fcr.currentRuleSet.Copy(), fcr.currentRuleSetTimestamp, nil +} + +// ActivateFileCustomRules activates this static file based custom rule mechanism +func ActivateFileCustomRules() { + if *fileRulePath != "" { + tabletserver.QueryRuleSources.RegisterQueryRuleSource(FileCustomRuleSource) + fileCustomRule.Open(*fileRulePath) + } +} + +func init() { + servenv.OnRun(ActivateFileCustomRules) +} diff --git a/go/vt/tabletserver/customrule/filecustomrule/filecustomrule_test.go b/go/vt/tabletserver/customrule/filecustomrule/filecustomrule_test.go new file mode 100644 index 00000000000..e9814c4bd70 --- /dev/null +++ b/go/vt/tabletserver/customrule/filecustomrule/filecustomrule_test.go @@ -0,0 +1,52 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filecustomrule + +import ( + "io/ioutil" + "os" + "path" + "testing" + + "github.com/youtube/vitess/go/vt/tabletserver" +) + +var customRule1 = `[ + { + "Name": "r1", + "Description": "disallow bindvar 'asdfg'", + "BindVarConds":[{ + "Name": "asdfg", + "OnAbsent": false, + "Operator": "NOOP" + }] + } + ]` + +func TestFileCustomRule(t *testing.T) { + var qrs *tabletserver.QueryRules + rulepath := path.Join(os.TempDir(), ".customrule.json") + // Set r1 and try to get it back + err := ioutil.WriteFile(rulepath, []byte(customRule1), os.FileMode(0644)) + if err != nil { + t.Fatalf("Cannot write r1 to rule file %s, err=%v", rulepath, err) + } + + fcr := NewFileCustomRule() + // Let FileCustomRule to build rule from the local file + err = fcr.Open(rulepath) + if err != nil { + t.Fatalf("Cannot open file custom rule service, err=%v", err) + } + // Fetch query rules we built to verify correctness + qrs, _, err = fcr.GetRules() + if err != nil { + t.Fatalf("GetRules returns error: %v", err) + } + qr := qrs.Find("r1") + if qr == nil { + t.Fatalf("Expect custom rule r1 to be found, but got nothing, qrs=%v", qrs) + } +} diff --git a/go/vt/tabletserver/customrule/zkcustomrule/zkcustomrule.go b/go/vt/tabletserver/customrule/zkcustomrule/zkcustomrule.go new file mode 100644 index 00000000000..83ec2a9ef09 --- /dev/null +++ b/go/vt/tabletserver/customrule/zkcustomrule/zkcustomrule.go @@ -0,0 +1,160 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package zkcustomrule + +import ( + "flag" + "reflect" + "sync" + "time" + + log "github.com/golang/glog" + "github.com/youtube/vitess/go/vt/servenv" + "github.com/youtube/vitess/go/vt/tabletserver" + "github.com/youtube/vitess/go/zk" + "launchpad.net/gozk/zookeeper" +) + +var ( + // Actual ZkCustomRule object in charge of rule updates + zkCustomRule = NewZkCustomRule(zk.NewMetaConn()) + // Commandline flag to specify rule path in zookeeper + zkRulePath = flag.String("zkcustomrules", "", "zookeeper based custom rule path") +) + +// Invalid rule version, used to mark invalid query rules +const InvalidQueryRulesVersion int64 = -1 + +// Zookeeper based custom rule source name +const ZkCustomRuleSource string = "ZK_CUSTOM_RULE" + +// ZkCustomRule is Zookeeper backed implementation of CustomRuleManager +type ZkCustomRule struct { + mu sync.Mutex + path string + zconn zk.Conn + watch <-chan zookeeper.Event // Zookeeper watch for listenning data change notifications + currentRuleSet *tabletserver.QueryRules + currentRuleSetVersion int64 // implemented with Zookeeper transaction id + finish chan int +} + +// NewZkCustomRule Creates new ZkCustomRule structure +func NewZkCustomRule(zkconn zk.Conn) *ZkCustomRule { + return &ZkCustomRule{ + zconn: zkconn, + currentRuleSet: tabletserver.NewQueryRules(), + currentRuleSetVersion: InvalidQueryRulesVersion, + finish: make(chan int, 1)} +} + +// Open Registers Zookeeper watch, gets inital QueryRules and starts polling routine +func (zkcr *ZkCustomRule) Open(rulePath string) (err error) { + zkcr.path = rulePath + err = zkcr.refreshWatch() + if err != nil { + return err + } + err = zkcr.refreshData(false) + if err != nil { + return err + } + go zkcr.poll() + return nil +} + +// refreshWatch gets a new watch channel for ZkCustomRule, it is called when +// the old watch channel is closed on errors +func (zkcr *ZkCustomRule) refreshWatch() error { + _, _, watch, err := zkcr.zconn.GetW(zkcr.path) + if err != nil { + log.Warningf("Fail to get a valid watch from ZK service: %v", err) + return err + } + zkcr.watch = watch + return nil +} + +// refreshData gets query rules from Zookeeper and refresh internal QueryRules cache +// this function will also call SqlQuery.SetQueryRules to propagate rule changes to query service +func (zkcr *ZkCustomRule) refreshData(nodeRemoval bool) error { + data, stat, err := zkcr.zconn.Get(zkcr.path) + zkcr.mu.Lock() + defer zkcr.mu.Unlock() + if err == nil { + qrs := tabletserver.NewQueryRules() + if !nodeRemoval { + err = qrs.UnmarshalJSON([]byte(data)) + if err != nil { + log.Warningf("Error unmarshaling query rules %v, original data '%s'", err, data) + return nil + } + } + zkcr.currentRuleSetVersion = stat.Mzxid() + if !reflect.DeepEqual(zkcr.currentRuleSet, qrs) { + zkcr.currentRuleSet = qrs.Copy() + tabletserver.SetQueryRules(ZkCustomRuleSource, qrs.Copy()) + log.Infof("Custom rule version %v fetched from Zookeeper and applied to vttablet", zkcr.currentRuleSetVersion) + } + return nil + } + log.Warningf("Error encountered when trying to get data and watch from Zk: %v", err) + return err +} + +const sleepDuringZkFailure time.Duration = 30 + +// poll polls the Zookeeper watch channel for data changes and refresh watch channel if watch channel is closed +// by Zookeeper Go library on error conditions such as connection reset +func (zkcr *ZkCustomRule) poll() { + for { + select { + case <-zkcr.finish: + return + case event := <-zkcr.watch: + switch event.Type { + case zookeeper.EVENT_CREATED, zookeeper.EVENT_CHANGED, zookeeper.EVENT_DELETED: + err := zkcr.refreshData(event.Type == zookeeper.EVENT_DELETED) // refresh rules + if err != nil { + // Sleep to avoid busy waiting during connection re-establishment + <-time.After(time.Second * sleepDuringZkFailure) + } + case zookeeper.EVENT_CLOSED: + err := zkcr.refreshWatch() // need to to get a new watch + if err != nil { + // Sleep to avoid busy waiting during connection re-establishment + <-time.After(time.Second * sleepDuringZkFailure) + } + zkcr.refreshData(false) + } + } + } +} + +// Close signals an termination to polling go routine and closes Zookeeper connection object +func (zkcr *ZkCustomRule) Close() { + zkcr.zconn.Close() + zkcr.finish <- 1 +} + +// GetRules retrives cached rules +func (zkcr *ZkCustomRule) GetRules() (qrs *tabletserver.QueryRules, version int64, err error) { + zkcr.mu.Lock() + defer zkcr.mu.Unlock() + return zkcr.currentRuleSet.Copy(), zkcr.currentRuleSetVersion, nil +} + +// ActivateZkCustomRules activates zookeeper dynamic custom rule mechanism +func ActivateZkCustomRules() { + if *zkRulePath != "" { + tabletserver.QueryRuleSources.RegisterQueryRuleSource(ZkCustomRuleSource) + zkCustomRule.Open(*zkRulePath) + } +} + +func init() { + servenv.OnRun(ActivateZkCustomRules) + servenv.OnTerm(zkCustomRule.Close) +} diff --git a/go/vt/tabletserver/customrule/zkcustomrule/zkcustomrule_test.go b/go/vt/tabletserver/customrule/zkcustomrule/zkcustomrule_test.go new file mode 100644 index 00000000000..08073be8c64 --- /dev/null +++ b/go/vt/tabletserver/customrule/zkcustomrule/zkcustomrule_test.go @@ -0,0 +1,109 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package zkcustomrule + +import ( + "reflect" + "testing" + "time" + + "github.com/youtube/vitess/go/vt/tabletserver" + "github.com/youtube/vitess/go/zk" + "github.com/youtube/vitess/go/zk/fakezk" + "launchpad.net/gozk/zookeeper" +) + +var customRule1 string = `[ + { + "Name": "r1", + "Description": "disallow bindvar 'asdfg'", + "BindVarConds":[{ + "Name": "asdfg", + "OnAbsent": false, + "Operator": "NOOP" + }] + } + ]` + +var customRule2 string = `[ + { + "Name": "r2", + "Description": "disallow insert on table test", + "TableNames" : ["test"], + "Query" : "(insert)|(INSERT)" + } + ]` +var conn zk.Conn + +func setUpFakeZk(t *testing.T) { + conn = fakezk.NewConn() + conn.Create("/zk", "", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) + conn.Create("/zk/fake", "", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) + conn.Create("/zk/fake/customrules", "", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) + conn.Create("/zk/fake/customrules/testrules", "customrule1", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) + conn.Set("/zk/fake/customrules/testrules", customRule1, -1) +} + +func TestZkCustomRule(t *testing.T) { + setUpFakeZk(t) + zkcr := NewZkCustomRule(conn) + err := zkcr.Open("/zk/fake/customrules/testrules") + if err != nil { + t.Fatalf("Cannot open zookeeper custom rule service, err=%v", err) + } + + var qrs *tabletserver.QueryRules + // Test if we can successfully fetch the original rule (test GetRules) + qrs, _, err = zkcr.GetRules() + if err != nil { + t.Fatalf("GetRules of ZkCustomRule should always return nil error, but we receive %v", err) + } + qr := qrs.Find("r1") + if qr == nil { + t.Fatalf("Expect custom rule r1 to be found, but got nothing, qrs=%v", qrs) + } + + // Test updating rules + conn.Set("/zk/fake/customrules/testrules", customRule2, -1) + <-time.After(time.Second) //Wait for the polling thread to respond + qrs, _, err = zkcr.GetRules() + if err != nil { + t.Fatalf("GetRules of ZkCustomRule should always return nil error, but we receive %v", err) + } + qr = qrs.Find("r2") + if qr == nil { + t.Fatalf("Expect custom rule r2 to be found, but got nothing, qrs=%v", qrs) + } + qr = qrs.Find("r1") + if qr != nil { + t.Fatalf("Custom rule r1 should not be found after r2 is set") + } + + // Test rule path removal + conn.Delete("/zk/fake/customrules/testrules", -1) + <-time.After(time.Second) + qrs, _, err = zkcr.GetRules() + if err != nil { + t.Fatalf("GetRules of ZkCustomRule should always return nil error, but we receive %v", err) + } + if reflect.DeepEqual(qrs, tabletserver.NewQueryRules()) { + t.Fatalf("Expect empty rule at this point") + } + + // Test rule path revival + conn.Create("/zk/fake/customrules/testrules", "customrule2", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) + conn.Set("/zk/fake/customrules/testrules", customRule2, -1) + <-time.After(time.Second) //Wait for the polling thread to respond + qrs, _, err = zkcr.GetRules() + if err != nil { + t.Fatalf("GetRules of ZkCustomRule should always return nil error, but we receive %v", err) + } + qr = qrs.Find("r2") + if qr == nil { + t.Fatalf("Expect custom rule r2 to be found, but got nothing, qrs=%v", qrs) + } + + zkcr.Close() +} diff --git a/go/vt/tabletserver/gorpcqueryservice/sqlquery.go b/go/vt/tabletserver/gorpcqueryservice/sqlquery.go index 8d332b2758d..5c5821cd325 100644 --- a/go/vt/tabletserver/gorpcqueryservice/sqlquery.go +++ b/go/vt/tabletserver/gorpcqueryservice/sqlquery.go @@ -5,11 +5,11 @@ package gorpcqueryservice import ( - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/tabletserver" "github.com/youtube/vitess/go/vt/tabletserver/proto" + "golang.org/x/net/context" ) type SqlQuery struct { diff --git a/go/vt/tabletserver/gorpctabletconn/conn.go b/go/vt/tabletserver/gorpctabletconn/conn.go index 673573565b9..8b044b2c84e 100644 --- a/go/vt/tabletserver/gorpctabletconn/conn.go +++ b/go/vt/tabletserver/gorpctabletconn/conn.go @@ -12,14 +12,15 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/rpcplus" "github.com/youtube/vitess/go/rpcwrap/bsonrpc" "github.com/youtube/vitess/go/vt/rpc" tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" "github.com/youtube/vitess/go/vt/tabletserver/tabletconn" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) var ( @@ -45,11 +46,11 @@ func DialTablet(ctx context.Context, endPoint topo.EndPoint, keyspace, shard str var addr string var config *tls.Config if *tabletBsonEncrypted { - addr = fmt.Sprintf("%v:%v", endPoint.Host, endPoint.NamedPortMap["_vts"]) + addr = netutil.JoinHostPort(endPoint.Host, endPoint.NamedPortMap["vts"]) config = &tls.Config{} config.InsecureSkipVerify = true } else { - addr = fmt.Sprintf("%v:%v", endPoint.Host, endPoint.NamedPortMap["_vtocc"]) + addr = netutil.JoinHostPort(endPoint.Host, endPoint.NamedPortMap["vt"]) } conn := &TabletBson{endPoint: endPoint} diff --git a/go/vt/tabletserver/memcache_stats.go b/go/vt/tabletserver/memcache_stats.go index e683a9dcf76..41afbde6cf5 100644 --- a/go/vt/tabletserver/memcache_stats.go +++ b/go/vt/tabletserver/memcache_stats.go @@ -338,7 +338,9 @@ func (s *MemcacheStats) readStats(k string, proc func(key, value string)) { continue } items := strings.Split(line, " ") - if len(items) != 3 { + //if using apt-get, memcached info would be:STAT version 1.4.14 (Ubuntu) + //so less then 3 would be compatible with original memcached + if len(items) < 3 { log.Errorf("Unexpected stats: %v", line) internalErrors.Add("MemcacheStats", 1) continue diff --git a/go/vt/tabletserver/proto/structs.go b/go/vt/tabletserver/proto/structs.go index 5c8af3ba9a2..75102b1aa9e 100644 --- a/go/vt/tabletserver/proto/structs.go +++ b/go/vt/tabletserver/proto/structs.go @@ -27,6 +27,8 @@ type Query struct { TransactionId int64 } +//go:generate bsongen -file $GOFILE -type Query -o query_bson.go + // String prints a readable version of Query, and also truncates // data if it's too long func (query *Query) String() string { @@ -59,21 +61,29 @@ type BoundQuery struct { BindVariables map[string]interface{} } +//go:generate bsongen -file $GOFILE -type BoundQuery -o bound_query_bson.go + type QueryList struct { Queries []BoundQuery SessionId int64 TransactionId int64 } +//go:generate bsongen -file $GOFILE -type QueryList -o query_list_bson.go + type QueryResultList struct { List []mproto.QueryResult } +//go:generate bsongen -file $GOFILE -type QueryResultList -o query_result_list_bson.go + type Session struct { SessionId int64 TransactionId int64 } +//go:generate bsongen -file $GOFILE -type Session -o session_bson.go + type TransactionInfo struct { TransactionId int64 } diff --git a/go/vt/tabletserver/qr_test.go b/go/vt/tabletserver/qr_test.go index 26ebf5a4737..05bd142b85a 100644 --- a/go/vt/tabletserver/qr_test.go +++ b/go/vt/tabletserver/qr_test.go @@ -164,7 +164,7 @@ func TestFilterByPlan(t *testing.T) { t.Errorf("want 1, got %#v, %#v", qrs1.rules[0], qrs1.rules[1]) } if qrs1.rules[0].Name != "r4" { - t.Errorf("want r5, got %s", qrs1.rules[0].Name) + t.Errorf("want r4, got %s", qrs1.rules[0].Name) } qr5 := NewQueryRule("rule 5", "r5", QR_FAIL) diff --git a/go/vt/tabletserver/qri_test.go b/go/vt/tabletserver/qri_test.go new file mode 100644 index 00000000000..bfa39890871 --- /dev/null +++ b/go/vt/tabletserver/qri_test.go @@ -0,0 +1,201 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tabletserver + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/youtube/vitess/go/vt/key" + "github.com/youtube/vitess/go/vt/tabletserver/planbuilder" +) + +var ( + keyrangeRules *QueryRules + blacklistRules *QueryRules + otherRules *QueryRules +) + +// mimic query rules from keyrange +const keyrangeQueryRules string = "KEYRANGE_QUERY_RULES" + +// mimic query rules from blacklist +const blacklistQueryRules string = "BLACKLIST_QUERY_RULES" + +// mimic query rules from custom source +const customQueryRules string = "CUSTOM_QUERY_RULES" + +func setupQueryRules() { + var qr *QueryRule + // mock keyrange rules + keyrangeRules = NewQueryRules() + dml_plans := []struct { + planID planbuilder.PlanType + onAbsent bool + }{ + {planbuilder.PLAN_INSERT_PK, true}, + {planbuilder.PLAN_INSERT_SUBQUERY, true}, + {planbuilder.PLAN_PASS_DML, false}, + {planbuilder.PLAN_DML_PK, false}, + {planbuilder.PLAN_DML_SUBQUERY, false}, + } + for _, plan := range dml_plans { + qr = NewQueryRule( + fmt.Sprintf("enforce keyspace_id range for %v", plan.planID), + fmt.Sprintf("keyspace_id_not_in_range_%v", plan.planID), + QR_FAIL, + ) + qr.AddPlanCond(plan.planID) + qr.AddBindVarCond("keyspace_id", plan.onAbsent, true, QR_NOTIN, key.KeyRange{Start: "aa", End: "zz"}) + keyrangeRules.Add(qr) + } + + // mock blacklisted tables + blacklistRules = NewQueryRules() + blacklistedTables := []string{"bannedtable1", "bannedtable2", "bannedtable3"} + qr = NewQueryRule("enforce blacklisted tables", "blacklisted_table", QR_FAIL_RETRY) + for _, t := range blacklistedTables { + qr.AddTableCond(t) + } + blacklistRules.Add(qr) + + // mock custom rules + otherRules = NewQueryRules() + qr = NewQueryRule("sample custom rule", "customrule_ban_bindvar", QR_FAIL) + qr.AddTableCond("t_customer") + qr.AddBindVarCond("bindvar1", true, false, QR_NOOP, nil) + otherRules.Add(qr) +} + +func TestQueryRuleInfoGetSetQueryRules(t *testing.T) { + setupQueryRules() + qri := NewQueryRuleInfo() + + qri.RegisterQueryRuleSource(keyrangeQueryRules) + qri.RegisterQueryRuleSource(blacklistQueryRules) + qri.RegisterQueryRuleSource(customQueryRules) + + // Test if we can get a QueryRules without a predefined rule set name + qrs, err := qri.GetRules("Foo") + if err == nil { + t.Errorf("GetRules shouldn't succeed with 'Foo' as the rule set name") + } + if qrs == nil { + t.Errorf("GetRules should always return empty QueryRules and never nil") + } + if !reflect.DeepEqual(qrs, NewQueryRules()) { + t.Errorf("QueryRuleInfo contains only empty QueryRules at the beginning") + } + + // Test if we can set a QueryRules without a predefined rule set name + err = qri.SetRules("Foo", NewQueryRules()) + if err == nil { + t.Errorf("SetRules shouldn't succeed with 'Foo' as the rule set name") + } + + // Test if we can successfully set QueryRules previously mocked into QueryRuleInfo + err = qri.SetRules(keyrangeQueryRules, keyrangeRules) + if err != nil { + t.Errorf("Failed to set keyrange QueryRules : %s", err) + } + err = qri.SetRules(blacklistQueryRules, blacklistRules) + if err != nil { + t.Errorf("Failed to set blacklist QueryRules: %s", err) + } + err = qri.SetRules(customQueryRules, otherRules) + if err != nil { + t.Errorf("Failed to set custom QueryRules: %s", err) + } + + // Test if we can successfully retrive rules that've been set + qrs, err = qri.GetRules(keyrangeQueryRules) + if err != nil { + t.Errorf("GetRules failed to retrieve keyrangeQueryRules that has been set: %s", err) + } + if !reflect.DeepEqual(qrs, keyrangeRules) { + t.Errorf("keyrangeQueryRules retrived is %v, but the expected value should be %v", qrs, keyrangeRules) + } + + qrs, err = qri.GetRules(blacklistQueryRules) + if err != nil { + t.Errorf("GetRules failed to retrieve blacklistQueryRules that has been set: %s", err) + } + if !reflect.DeepEqual(qrs, blacklistRules) { + t.Errorf("blacklistQueryRules retrived is %v, but the expected value should be %v", qrs, blacklistRules) + } + + qrs, err = qri.GetRules(customQueryRules) + if err != nil { + t.Errorf("GetRules failed to retrieve customQueryRules that has been set: %s", err) + } + if !reflect.DeepEqual(qrs, otherRules) { + t.Errorf("customQueryRules retrived is %v, but the expected value should be %v", qrs, customQueryRules) + } +} + +func TestQueryRuleInfoFilterByPlan(t *testing.T) { + var qrs *QueryRules + setupQueryRules() + qri := NewQueryRuleInfo() + + qri.RegisterQueryRuleSource(keyrangeQueryRules) + qri.RegisterQueryRuleSource(blacklistQueryRules) + qri.RegisterQueryRuleSource(customQueryRules) + + qri.SetRules(keyrangeQueryRules, keyrangeRules) + qri.SetRules(blacklistQueryRules, blacklistRules) + qri.SetRules(customQueryRules, otherRules) + + // Test filter by keyrange rule + qrs = qri.filterByPlan("insert into t_test values(123, 456, 'abc')", planbuilder.PLAN_INSERT_PK, "t_test") + if l := len(qrs.rules); l != 1 { + t.Errorf("Insert PK query matches %d rules, but we expect %d", l, 1) + } + if !strings.HasPrefix(qrs.rules[0].Name, "keyspace_id_not_in_range") { + t.Errorf("Insert PK query matches rule '%s', but we expect rule with prefix '%s'", qrs.rules[0].Name, "keyspace_id_not_in_range") + } + + // Test filter by blacklist rule + qrs = qri.filterByPlan("select * from bannedtable2", planbuilder.PLAN_PASS_SELECT, "bannedtable2") + if l := len(qrs.rules); l != 1 { + t.Errorf("Select from bannedtable matches %d rules, but we expect %d", l, 1) + } + if !strings.HasPrefix(qrs.rules[0].Name, "blacklisted_table") { + t.Errorf("Select from bannedtable query matches rule '%s', but we expect rule with prefix '%s'", qrs.rules[0].Name, "blacklisted_table") + } + + // Test filter by custom rule + qrs = qri.filterByPlan("select cid from t_customer limit 10", planbuilder.PLAN_PASS_SELECT, "t_customer") + if l := len(qrs.rules); l != 1 { + t.Errorf("Select from t_customer matches %d rules, but we expect %d", l, 1) + } + if !strings.HasPrefix(qrs.rules[0].Name, "customrule_ban_bindvar") { + t.Errorf("Select from t_customer matches rule '%s', but we expect rule with prefix '%s'", qrs.rules[0].Name, "customrule_ban_bindvar") + } + + // Test match two rules: both keyrange rule and custom rule will be matched + otherRules = NewQueryRules() + qr := NewQueryRule("sample custom rule", "customrule_ban_bindvar", QR_FAIL) + qr.AddBindVarCond("bindvar1", true, false, QR_NOOP, nil) + otherRules.Add(qr) + qri.SetRules(customQueryRules, otherRules) + qrs = qri.filterByPlan("insert into t_test values (:bindvar1, 123, 'test')", planbuilder.PLAN_INSERT_PK, "t_test") + if l := len(qrs.rules); l != 2 { + t.Errorf("Insert into t_test matches %d rules: %v, but we expect %d rules to be matched", l, qrs.rules, 2) + } + if strings.HasPrefix(qrs.rules[0].Name, "keyspace_id_not_in_range") && + strings.HasPrefix(qrs.rules[1].Name, "customrule_ban_bindvar") { + return + } + if strings.HasPrefix(qrs.rules[1].Name, "keyspace_id_not_in_range") && + strings.HasPrefix(qrs.rules[0].Name, "customrule_ban_bindvar") { + return + } + + t.Errorf("Insert into t_test matches rule[0] '%s' and rule[1] '%s', but we expect rule[0] with prefix '%s' and rule[1] with prefix '%s'", + qrs.rules[0].Name, qrs.rules[1].Name, "keyspace_id_not_in_range", "customrule_ban_bindvar") +} diff --git a/go/vt/tabletserver/query_engine.go b/go/vt/tabletserver/query_engine.go index 8d088933b61..9458f9b85e5 100644 --- a/go/vt/tabletserver/query_engine.go +++ b/go/vt/tabletserver/query_engine.go @@ -39,7 +39,7 @@ const spotCheckMultiplier = 1e6 // TODO(sougou): Switch to error return scheme. type QueryEngine struct { schemaInfo *SchemaInfo - dbconfig *dbconfigs.DBConfig + dbconfigs *dbconfigs.DBConfigs // Pools cachePool *CachePool @@ -85,11 +85,11 @@ var ( internalErrors *stats.Counters resultStats *stats.Histogram spotCheckCount *stats.Int - QPSRates *stats.Rates + qpsRates *stats.Rates resultBuckets = []int64{0, 1, 5, 10, 50, 100, 500, 1000, 5000, 10000} - connPoolClosedErr = NewTabletError(FATAL, "connection pool is closed") + connPoolClosedErr = NewTabletError(ErrFatal, "connection pool is closed") ) // CacheInvalidator provides the abstraction needed for an instant invalidation @@ -104,10 +104,10 @@ func getOrPanic(pool *dbconnpool.ConnectionPool) dbconnpool.PoolConnection { if err == nil { return conn } - if err == dbconnpool.CONN_POOL_CLOSED_ERR { + if err == dbconnpool.ErrConnPoolClosed { panic(connPoolClosedErr) } - panic(NewTabletErrorSql(FATAL, err)) + panic(NewTabletErrorSql(ErrFatal, err)) } // NewQueryEngine creates a new QueryEngine. @@ -174,7 +174,7 @@ func NewQueryEngine(config Config) *QueryEngine { stats.Publish("StreamBufferSize", stats.IntFunc(qe.streamBufferSize.Get)) stats.Publish("QueryTimeout", stats.DurationFunc(qe.queryTimeout.Get)) queryStats = stats.NewTimings("Queries") - QPSRates = stats.NewRates("QPS", queryStats, 15, 60*time.Second) + qpsRates = stats.NewRates("QPS", queryStats, 15, 60*time.Second) waitStats = stats.NewTimings("Waits") killStats = stats.NewCounters("Kills") infoErrors = stats.NewCounters("InfoErrors") @@ -190,43 +190,51 @@ func NewQueryEngine(config Config) *QueryEngine { } // Open must be called before sending requests to QueryEngine. -func (qe *QueryEngine) Open(dbconfig *dbconfigs.DBConfig, schemaOverrides []SchemaOverride, qrs *QueryRules, mysqld *mysqlctl.Mysqld) { - qe.dbconfig = dbconfig - connFactory := dbconnpool.DBConnectionCreator(&dbconfig.ConnectionParams, mysqlStats) +func (qe *QueryEngine) Open(dbconfigs *dbconfigs.DBConfigs, schemaOverrides []SchemaOverride, mysqld *mysqlctl.Mysqld) { + qe.dbconfigs = dbconfigs + connFactory := dbconnpool.DBConnectionCreator(&dbconfigs.App.ConnectionParams, mysqlStats) + // Create dba params based on App connection params + // and Dba credentials. + dba := dbconfigs.App.ConnectionParams + if dbconfigs.Dba.Uname != "" { + dba.Uname = dbconfigs.Dba.Uname + dba.Pass = dbconfigs.Dba.Pass + } + dbaConnFactory := dbconnpool.DBConnectionCreator(&dba, mysqlStats) strictMode := false if qe.strictMode.Get() != 0 { strictMode = true } - if !strictMode && dbconfig.EnableRowcache { - panic(NewTabletError(FATAL, "Rowcache cannot be enabled when queryserver-config-strict-mode is false")) + if !strictMode && dbconfigs.App.EnableRowcache { + panic(NewTabletError(ErrFatal, "Rowcache cannot be enabled when queryserver-config-strict-mode is false")) } - if dbconfig.EnableRowcache { + if dbconfigs.App.EnableRowcache { qe.cachePool.Open() log.Infof("rowcache is enabled") } else { // Invalidator should not be enabled if rowcache is not enabled. - dbconfig.EnableInvalidator = false + dbconfigs.App.EnableInvalidator = false log.Infof("rowcache is not enabled") } start := time.Now() // schemaInfo depends on cachePool. Every table that has a rowcache // points to the cachePool. - qe.schemaInfo.Open(connFactory, schemaOverrides, qe.cachePool, qrs, strictMode) + qe.schemaInfo.Open(dbaConnFactory, schemaOverrides, qe.cachePool, strictMode) log.Infof("Time taken to load the schema: %v", time.Now().Sub(start)) // Start the invalidator only after schema is loaded. // This will allow qe to find the table info // for the invalidation events that will start coming // immediately. - if dbconfig.EnableInvalidator { - qe.invalidator.Open(dbconfig.DbName, mysqld) + if dbconfigs.App.EnableInvalidator { + qe.invalidator.Open(dbconfigs.App.DbName, mysqld) } qe.connPool.Open(connFactory) qe.streamConnPool.Open(connFactory) qe.txPool.Open(connFactory) - qe.connKiller.Open(connFactory) + qe.connKiller.Open(dbaConnFactory) } // Launch launches the specified function inside a goroutine. @@ -248,6 +256,20 @@ func (qe *QueryEngine) Launch(f func()) { }() } +// CheckMySQL returns true if we can connect to MySQL. +func (qe *QueryEngine) CheckMySQL() bool { + conn, err := dbconnpool.NewDBConnection(&qe.dbconfigs.App.ConnectionParams, mysqlStats) + if err != nil { + if IsConnErr(err) { + return false + } + log.Warningf("checking MySQL, unexpected error: %v", err) + return true + } + conn.Close() + return true +} + // WaitForTxEmpty must be called before calling Close. // Before calling WaitForTxEmpty, you must ensure that there // will be no more calls to Begin. @@ -268,5 +290,5 @@ func (qe *QueryEngine) Close() { qe.invalidator.Close() qe.schemaInfo.Close() qe.cachePool.Close() - qe.dbconfig = nil + qe.dbconfigs = nil } diff --git a/go/vt/tabletserver/query_executor.go b/go/vt/tabletserver/query_executor.go index 733d13330e1..66bc290d087 100644 --- a/go/vt/tabletserver/query_executor.go +++ b/go/vt/tabletserver/query_executor.go @@ -8,7 +8,6 @@ import ( "fmt" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/sqltypes" @@ -17,6 +16,7 @@ import ( "github.com/youtube/vitess/go/vt/schema" "github.com/youtube/vitess/go/vt/sqlparser" "github.com/youtube/vitess/go/vt/tabletserver/planbuilder" + "golang.org/x/net/context" ) // QueryExecutor is used for executing a query request. @@ -40,9 +40,12 @@ func (qre *QueryExecutor) Execute() (reply *mproto.QueryResult) { queryStats.Add(planName, duration) if reply == nil { qre.plan.AddStats(1, duration, 0, 1) - } else { - qre.plan.AddStats(1, duration, int64(len(reply.Rows)), 0) + return } + qre.plan.AddStats(1, duration, int64(reply.RowsAffected), 0) + qre.logStats.RowsAffected = int(reply.RowsAffected) + qre.logStats.Rows = reply.Rows + resultStats.Add(int64(len(reply.Rows))) }(time.Now()) qre.checkPermissions() @@ -63,7 +66,7 @@ func (qre *QueryExecutor) Execute() (reply *mproto.QueryResult) { switch qre.plan.PlanId { case planbuilder.PLAN_PASS_DML: if qre.qe.strictMode.Get() != 0 { - panic(NewTabletError(FAIL, "DML too complex")) + panic(NewTabletError(ErrFail, "DML too complex")) } reply = qre.directFetch(conn, qre.plan.FullQuery, qre.bindVars, nil) case planbuilder.PLAN_INSERT_PK: @@ -83,7 +86,7 @@ func (qre *QueryExecutor) Execute() (reply *mproto.QueryResult) { switch qre.plan.PlanId { case planbuilder.PLAN_PASS_SELECT: if qre.plan.Reason == planbuilder.REASON_LOCK { - panic(NewTabletError(FAIL, "Disallowed outside transaction")) + panic(NewTabletError(ErrFail, "Disallowed outside transaction")) } reply = qre.execSelect() case planbuilder.PLAN_PK_IN: @@ -97,15 +100,9 @@ func (qre *QueryExecutor) Execute() (reply *mproto.QueryResult) { defer conn.Recycle() reply = qre.execSQL(conn, qre.query, true) default: - panic(NewTabletError(NOT_IN_TX, "DMLs not allowed outside of transactions")) + panic(NewTabletError(ErrNotInTx, "DMLs not allowed outside of transactions")) } } - if qre.plan.PlanId.IsSelect() { - qre.logStats.RowsAffected = int(reply.RowsAffected) - resultStats.Add(int64(reply.RowsAffected)) - qre.logStats.Rows = reply.Rows - } - return reply } @@ -120,7 +117,7 @@ func (qre *QueryExecutor) Stream(sendReply func(*mproto.QueryResult) error) { conn := qre.getConn(qre.qe.streamConnPool) defer conn.Recycle() - qd := NewQueryDetail(qre.query, qre.logStats.context, conn.Id()) + qd := NewQueryDetail(qre.query, qre.logStats.context, conn.ID()) qre.qe.streamQList.Add(qd) defer qre.qe.streamQList.Remove(qd) @@ -138,16 +135,17 @@ func (qre *QueryExecutor) checkPermissions() { action, desc := qre.plan.Rules.getAction(ci.RemoteAddr(), ci.Username(), qre.bindVars) switch action { case QR_FAIL: - panic(NewTabletError(FAIL, "Query disallowed due to rule: %s", desc)) + panic(NewTabletError(ErrFail, "Query disallowed due to rule: %s", desc)) case QR_FAIL_RETRY: - panic(NewTabletError(RETRY, "Query disallowed due to rule: %s", desc)) + panic(NewTabletError(ErrRetry, "Query disallowed due to rule: %s", desc)) } - // ACLs - if !qre.plan.Authorized.IsMember(ci.Username()) { + // Perform table ACL check if it is enabled + if qre.plan.Authorized != nil && !qre.plan.Authorized.IsMember(ci.Username()) { errStr := fmt.Sprintf("table acl error: %q cannot run %v on table %q", ci.Username(), qre.plan.PlanId, qre.plan.TableName) + // Raise error if in strictTableAcl mode, else just log an error if qre.qe.strictTableAcl { - panic(NewTabletError(FAIL, "%s", errStr)) + panic(NewTabletError(ErrFail, "%s", errStr)) } qre.qe.accessCheckerLogger.Errorf("%s", errStr) } @@ -156,7 +154,7 @@ func (qre *QueryExecutor) checkPermissions() { func (qre *QueryExecutor) execDDL() *mproto.QueryResult { ddlPlan := planbuilder.DDLParse(qre.query) if ddlPlan.Action == "" { - panic(NewTabletError(FAIL, "DDL is not understood")) + panic(NewTabletError(ErrFail, "DDL is not understood")) } txid := qre.qe.txPool.Begin() @@ -332,7 +330,7 @@ func (qre *QueryExecutor) execInsertSubquery(conn dbconnpool.PoolConnection) (re return &mproto.QueryResult{RowsAffected: 0} } if len(qre.plan.ColumnNumbers) != len(innerRows[0]) { - panic(NewTabletError(FAIL, "Subquery length does not match column list")) + panic(NewTabletError(ErrFail, "Subquery length does not match column list")) } pkRows := make([][]sqltypes.Value, len(innerRows)) for i, innerRow := range innerRows { @@ -427,19 +425,19 @@ func (qre *QueryExecutor) execSet() (result *mproto.QueryResult) { case "vt_max_result_size": val := getInt64(qre.plan.SetValue) if val < 1 { - panic(NewTabletError(FAIL, "vt_max_result_size out of range %v", val)) + panic(NewTabletError(ErrFail, "vt_max_result_size out of range %v", val)) } qre.qe.maxResultSize.Set(val) case "vt_max_dml_rows": val := getInt64(qre.plan.SetValue) if val < 1 { - panic(NewTabletError(FAIL, "vt_max_dml_rows out of range %v", val)) + panic(NewTabletError(ErrFail, "vt_max_dml_rows out of range %v", val)) } qre.qe.maxDMLRows.Set(val) case "vt_stream_buffer_size": val := getInt64(qre.plan.SetValue) if val < 1024 { - panic(NewTabletError(FAIL, "vt_stream_buffer_size out of range %v", val)) + panic(NewTabletError(ErrFail, "vt_stream_buffer_size out of range %v", val)) } qre.qe.streamBufferSize.Set(val) case "vt_query_timeout": @@ -469,7 +467,7 @@ func getInt64(v interface{}) int64 { if ival, ok := v.(int64); ok { return ival } - panic(NewTabletError(FAIL, "expecting int")) + panic(NewTabletError(ErrFail, "expecting int")) } func getFloat64(v interface{}) float64 { @@ -479,7 +477,7 @@ func getFloat64(v interface{}) float64 { if fval, ok := v.(float64); ok { return fval } - panic(NewTabletError(FAIL, "expecting number")) + panic(NewTabletError(ErrFail, "expecting number")) } func getDuration(v interface{}) time.Duration { diff --git a/go/vt/tabletserver/query_list.go b/go/vt/tabletserver/query_list.go index 8fcdbaa665c..eb5e11a60e7 100644 --- a/go/vt/tabletserver/query_list.go +++ b/go/vt/tabletserver/query_list.go @@ -7,8 +7,8 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/vt/callinfo" + "golang.org/x/net/context" ) // QueryDetail is a simple wrapper for Query, Context and PoolConnection diff --git a/go/vt/tabletserver/query_list_test.go b/go/vt/tabletserver/query_list_test.go index 03e1a5e8078..f2d682cb63a 100644 --- a/go/vt/tabletserver/query_list_test.go +++ b/go/vt/tabletserver/query_list_test.go @@ -3,13 +3,13 @@ package tabletserver import ( "testing" - "github.com/youtube/vitess/go/vt/context" + "golang.org/x/net/context" ) func TestQueryList(t *testing.T) { ql := NewQueryList(nil) connID := int64(1) - qd := NewQueryDetail("", &context.DummyContext{}, connID) + qd := NewQueryDetail("", context.Background(), connID) ql.Add(qd) if qd1, ok := ql.queryDetails[connID]; !ok || qd1.connID != connID { @@ -17,7 +17,7 @@ func TestQueryList(t *testing.T) { } conn2ID := int64(2) - qd2 := NewQueryDetail("", &context.DummyContext{}, conn2ID) + qd2 := NewQueryDetail("", context.Background(), conn2ID) ql.Add(qd2) rows := ql.GetQueryzRows() diff --git a/go/vt/tabletserver/query_rules.go b/go/vt/tabletserver/query_rules.go index adfb0280cca..aad4ba04557 100644 --- a/go/vt/tabletserver/query_rules.go +++ b/go/vt/tabletserver/query_rules.go @@ -39,6 +39,13 @@ func (qrs *QueryRules) Copy() (newqrs *QueryRules) { return newqrs } +// Append merges the rules from another QueryRules into the receiver +func (qrs *QueryRules) Append(otherqrs *QueryRules) { + for _, qr := range otherqrs.rules { + qrs.rules = append(qrs.rules, qr) + } +} + // Add adds a QueryRule to QueryRules. It does not check // for duplicates. func (qrs *QueryRules) Add(qr *QueryRule) { @@ -75,10 +82,10 @@ func (qrs *QueryRules) UnmarshalJSON(data []byte) (err error) { var rulesInfo []map[string]interface{} err = json.Unmarshal(data, &rulesInfo) if err != nil { - return NewTabletError(FAIL, "%v", err) + return NewTabletError(ErrFail, "%v", err) } for _, ruleInfo := range rulesInfo { - qr, err := buildQueryRule(ruleInfo) + qr, err := BuildQueryRule(ruleInfo) if err != nil { return err } @@ -254,7 +261,7 @@ func (qr *QueryRule) AddBindVarCond(name string, onAbsent, onMismatch bool, op O // Change the value to compiled regexp re, err := regexp.Compile(makeExact(v)) if err != nil { - return NewTabletError(FAIL, "processing %s: %v", v, err) + return NewTabletError(ErrFail, "processing %s: %v", v, err) } converted = bvcre{re} } else { @@ -266,13 +273,13 @@ func (qr *QueryRule) AddBindVarCond(name string, onAbsent, onMismatch bool, op O } converted = bvcKeyRange(v) default: - return NewTabletError(FAIL, "type %T not allowed as condition operand (%v)", value, value) + return NewTabletError(ErrFail, "type %T not allowed as condition operand (%v)", value, value) } qr.bindVarConds = append(qr.bindVarConds, BindVarCond{name, onAbsent, onMismatch, op, converted}) return nil Error: - return NewTabletError(FAIL, "invalid operator %s for type %T (%v)", op, value, value) + return NewTabletError(ErrFail, "invalid operator %s for type %T (%v)", op, value, value) } // filterByPlan returns a new QueryRule if the query and planid match. @@ -682,7 +689,14 @@ func getstring(val interface{}) (sv string, status int) { //----------------------------------------------- // Support functions for JSON -func buildQueryRule(ruleInfo map[string]interface{}) (qr *QueryRule, err error) { +func MapStrOperator(strop string) (op Operator, err error) { + if op, ok := opmap[strop]; ok { + return op, nil + } + return QR_NOOP, NewTabletError(ErrFail, "invalid Operator %s", strop) +} + +func BuildQueryRule(ruleInfo map[string]interface{}) (qr *QueryRule, err error) { qr = NewQueryRule("", "", QR_FAIL) for k, v := range ruleInfo { var sv string @@ -692,15 +706,15 @@ func buildQueryRule(ruleInfo map[string]interface{}) (qr *QueryRule, err error) case "Name", "Description", "RequestIP", "User", "Query", "Action": sv, ok = v.(string) if !ok { - return nil, NewTabletError(FAIL, "want string for %s", k) + return nil, NewTabletError(ErrFail, "want string for %s", k) } case "Plans", "BindVarConds", "TableNames": lv, ok = v.([]interface{}) if !ok { - return nil, NewTabletError(FAIL, "want list for %s", k) + return nil, NewTabletError(ErrFail, "want list for %s", k) } default: - return nil, NewTabletError(FAIL, "unrecognized tag %s", k) + return nil, NewTabletError(ErrFail, "unrecognized tag %s", k) } switch k { case "Name": @@ -710,27 +724,27 @@ func buildQueryRule(ruleInfo map[string]interface{}) (qr *QueryRule, err error) case "RequestIP": err = qr.SetIPCond(sv) if err != nil { - return nil, NewTabletError(FAIL, "could not set IP condition: %v", sv) + return nil, NewTabletError(ErrFail, "could not set IP condition: %v", sv) } case "User": err = qr.SetUserCond(sv) if err != nil { - return nil, NewTabletError(FAIL, "could not set User condition: %v", sv) + return nil, NewTabletError(ErrFail, "could not set User condition: %v", sv) } case "Query": err = qr.SetQueryCond(sv) if err != nil { - return nil, NewTabletError(FAIL, "could not set Query condition: %v", sv) + return nil, NewTabletError(ErrFail, "could not set Query condition: %v", sv) } case "Plans": for _, p := range lv { pv, ok := p.(string) if !ok { - return nil, NewTabletError(FAIL, "want string for Plans") + return nil, NewTabletError(ErrFail, "want string for Plans") } pt, ok := planbuilder.PlanByName(pv) if !ok { - return nil, NewTabletError(FAIL, "invalid plan name: %s", pv) + return nil, NewTabletError(ErrFail, "invalid plan name: %s", pv) } qr.AddPlanCond(pt) } @@ -738,7 +752,7 @@ func buildQueryRule(ruleInfo map[string]interface{}) (qr *QueryRule, err error) for _, t := range lv { tableName, ok := t.(string) if !ok { - return nil, NewTabletError(FAIL, "want string for TableNames") + return nil, NewTabletError(ErrFail, "want string for TableNames") } qr.AddTableCond(tableName) } @@ -761,7 +775,7 @@ func buildQueryRule(ruleInfo map[string]interface{}) (qr *QueryRule, err error) case "FAIL_RETRY": qr.act = QR_FAIL_RETRY default: - return nil, NewTabletError(FAIL, "invalid Action %s", sv) + return nil, NewTabletError(ErrFail, "invalid Action %s", sv) } } } @@ -771,46 +785,45 @@ func buildQueryRule(ruleInfo map[string]interface{}) (qr *QueryRule, err error) func buildBindVarCondition(bvc interface{}) (name string, onAbsent, onMismatch bool, op Operator, value interface{}, err error) { bvcinfo, ok := bvc.(map[string]interface{}) if !ok { - err = NewTabletError(FAIL, "want json object for bind var conditions") + err = NewTabletError(ErrFail, "want json object for bind var conditions") return } var v interface{} v, ok = bvcinfo["Name"] if !ok { - err = NewTabletError(FAIL, "Name missing in BindVarConds") + err = NewTabletError(ErrFail, "Name missing in BindVarConds") return } name, ok = v.(string) if !ok { - err = NewTabletError(FAIL, "want string for Name in BindVarConds") + err = NewTabletError(ErrFail, "want string for Name in BindVarConds") return } v, ok = bvcinfo["OnAbsent"] if !ok { - err = NewTabletError(FAIL, "OnAbsent missing in BindVarConds") + err = NewTabletError(ErrFail, "OnAbsent missing in BindVarConds") return } onAbsent, ok = v.(bool) if !ok { - err = NewTabletError(FAIL, "want bool for OnAbsent") + err = NewTabletError(ErrFail, "want bool for OnAbsent") return } v, ok = bvcinfo["Operator"] if !ok { - err = NewTabletError(FAIL, "Operator missing in BindVarConds") + err = NewTabletError(ErrFail, "Operator missing in BindVarConds") return } strop, ok := v.(string) if !ok { - err = NewTabletError(FAIL, "want string for Operator") + err = NewTabletError(ErrFail, "want string for Operator") return } - op, ok = opmap[strop] - if !ok { - err = NewTabletError(FAIL, "invalid Operator %s", strop) + op, err = MapStrOperator(strop) + if err != nil { return } if op == QR_NOOP { @@ -818,25 +831,25 @@ func buildBindVarCondition(bvc interface{}) (name string, onAbsent, onMismatch b } v, ok = bvcinfo["Value"] if !ok { - err = NewTabletError(FAIL, "Value missing in BindVarConds") + err = NewTabletError(ErrFail, "Value missing in BindVarConds") return } if op >= QR_EQ && op <= QR_LE { strvalue, ok := v.(string) if !ok { - err = NewTabletError(FAIL, "want string: %v", v) + err = NewTabletError(ErrFail, "want string: %v", v) return } if strop[0] == 'U' { value, err = strconv.ParseUint(strvalue, 0, 64) if err != nil { - err = NewTabletError(FAIL, "want uint64: %s", strvalue) + err = NewTabletError(ErrFail, "want uint64: %s", strvalue) return } } else if strop[0] == 'I' { value, err = strconv.ParseInt(strvalue, 0, 64) if err != nil { - err = NewTabletError(FAIL, "want int64: %s", strvalue) + err = NewTabletError(ErrFail, "want int64: %s", strvalue) return } } else if strop[0] == 'S' { @@ -847,37 +860,37 @@ func buildBindVarCondition(bvc interface{}) (name string, onAbsent, onMismatch b } else if op == QR_MATCH || op == QR_NOMATCH { strvalue, ok := v.(string) if !ok { - err = NewTabletError(FAIL, "want string: %v", v) + err = NewTabletError(ErrFail, "want string: %v", v) return } value = strvalue } else if op == QR_IN || op == QR_NOTIN { kr, ok := v.(map[string]interface{}) if !ok { - err = NewTabletError(FAIL, "want keyrange for Value") + err = NewTabletError(ErrFail, "want keyrange for Value") return } var keyrange key.KeyRange strstart, ok := kr["Start"] if !ok { - err = NewTabletError(FAIL, "Start missing in KeyRange") + err = NewTabletError(ErrFail, "Start missing in KeyRange") return } start, ok := strstart.(string) if !ok { - err = NewTabletError(FAIL, "want string for Start") + err = NewTabletError(ErrFail, "want string for Start") return } keyrange.Start = key.KeyspaceId(start) strend, ok := kr["End"] if !ok { - err = NewTabletError(FAIL, "End missing in KeyRange") + err = NewTabletError(ErrFail, "End missing in KeyRange") return } end, ok := strend.(string) if !ok { - err = NewTabletError(FAIL, "want string for End") + err = NewTabletError(ErrFail, "want string for End") return } keyrange.End = key.KeyspaceId(end) @@ -886,12 +899,12 @@ func buildBindVarCondition(bvc interface{}) (name string, onAbsent, onMismatch b v, ok = bvcinfo["OnMismatch"] if !ok { - err = NewTabletError(FAIL, "OnMismatch missing in BindVarConds") + err = NewTabletError(ErrFail, "OnMismatch missing in BindVarConds") return } onMismatch, ok = v.(bool) if !ok { - err = NewTabletError(FAIL, "want bool for OnAbsent") + err = NewTabletError(ErrFail, "want bool for OnAbsent") return } return diff --git a/go/vt/tabletserver/queryctl.go b/go/vt/tabletserver/queryctl.go index ba95c26e43f..328a8a3465f 100644 --- a/go/vt/tabletserver/queryctl.go +++ b/go/vt/tabletserver/queryctl.go @@ -7,25 +7,27 @@ package tabletserver import ( "flag" "fmt" - "io/ioutil" "net/http" "net/url" "strconv" + "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/acl" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/streamlog" + "github.com/youtube/vitess/go/sync2" "github.com/youtube/vitess/go/vt/dbconfigs" "github.com/youtube/vitess/go/vt/mysqlctl" "github.com/youtube/vitess/go/vt/tabletserver/proto" + "golang.org/x/net/context" ) var ( queryLogHandler = flag.String("query-log-stream-handler", "/debug/querylog", "URL handler for streaming queries log") txLogHandler = flag.String("transaction-log-stream-handler", "/debug/txlog", "URL handler for streaming transactions log") - customRules = flag.String("customrules", "", "custom query rules file") + + checkMySLQThrottler = sync2.NewSemaphore(1, 0) ) func init() { @@ -159,10 +161,9 @@ func RegisterQueryService() { http.HandleFunc("/debug/health", healthCheck) } -// AllowQueries can take an indefinite amount of time to return because -// it keeps retrying until it obtains a valid connection to the database. -func AllowQueries(dbconfig *dbconfigs.DBConfig, schemaOverrides []SchemaOverride, qrs *QueryRules, mysqld *mysqlctl.Mysqld, waitForMysql bool) error { - return SqlQueryRpcService.allowQueries(dbconfig, schemaOverrides, qrs, mysqld, waitForMysql) +// AllowQueries starts the query service. +func AllowQueries(dbconfigs *dbconfigs.DBConfigs, schemaOverrides []SchemaOverride, mysqld *mysqlctl.Mysqld) error { + return SqlQueryRpcService.allowQueries(dbconfigs, schemaOverrides, mysqld) } // DisallowQueries can take a long time to return (not indefinite) because @@ -179,16 +180,42 @@ func ReloadSchema() { SqlQueryRpcService.qe.schemaInfo.triggerReload() } +// CheckMySQL verifies that MySQL is still reachable by connecting to it. +// If it's not reachable, it shuts down the query service. +// This function rate-limits the check to no more than once per second. +func CheckMySQL() { + if !checkMySLQThrottler.TryAcquire() { + return + } + defer func() { + time.Sleep(1 * time.Second) + checkMySLQThrottler.Release() + }() + defer logError() + if SqlQueryRpcService.checkMySQL() { + return + } + log.Infof("Check MySQL failed. Shutting down query service") + DisallowQueries() +} + func GetSessionId() int64 { return SqlQueryRpcService.sessionId } -func SetQueryRules(qrs *QueryRules) { - SqlQueryRpcService.qe.schemaInfo.SetRules(qrs) +// GetQueryRules is the tabletserver level API to get current query rules +func GetQueryRules(ruleSource string) (*QueryRules, error) { + return QueryRuleSources.GetRules(ruleSource) } -func GetQueryRules() (qrs *QueryRules) { - return SqlQueryRpcService.qe.schemaInfo.GetRules() +// SetQueryRules is the tabletserver level API to write current query rules +func SetQueryRules(ruleSource string, qrs *QueryRules) error { + err := QueryRuleSources.SetRules(ruleSource, qrs) + if err != nil { + return err + } + SqlQueryRpcService.qe.schemaInfo.ClearQueryPlanCache() + return nil } // IsHealthy returns nil if the query service is healthy (able to @@ -236,23 +263,3 @@ func InitQueryService() { TxLogger.ServeLogs(*txLogHandler, buildFmter(TxLogger)) RegisterQueryService() } - -// LoadCustomRules returns custom rules as specified by the command -// line flags. -func LoadCustomRules() (qrs *QueryRules) { - if *customRules == "" { - return NewQueryRules() - } - - data, err := ioutil.ReadFile(*customRules) - if err != nil { - log.Fatalf("Error reading file %v: %v", *customRules, err) - } - - qrs = NewQueryRules() - err = qrs.UnmarshalJSON(data) - if err != nil { - log.Fatalf("Error unmarshaling query rules %v", err) - } - return qrs -} diff --git a/go/vt/tabletserver/querylogz.go b/go/vt/tabletserver/querylogz.go index cf2da49f3d2..ae8ab03e440 100644 --- a/go/vt/tabletserver/querylogz.go +++ b/go/vt/tabletserver/querylogz.go @@ -30,7 +30,8 @@ var ( SQL Queries Sources - Response Size (Rows) + RowsAffected + Response Size Cache Hits Cache Misses Cache Absent @@ -57,6 +58,7 @@ var ( {{.OriginalSql | unquote | cssWrappable}} {{.NumberOfQueries}} {{.FmtQuerySources}} + {{.RowsAffected}} {{.SizeOfResponse}} {{.CacheHits}} {{.CacheMisses}} diff --git a/go/vt/tabletserver/queryrule_info.go b/go/vt/tabletserver/queryrule_info.go new file mode 100644 index 00000000000..e1c163c037f --- /dev/null +++ b/go/vt/tabletserver/queryrule_info.go @@ -0,0 +1,79 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tabletserver + +import ( + "errors" + "sync" + + log "github.com/golang/glog" + "github.com/youtube/vitess/go/vt/tabletserver/planbuilder" +) + +// Global variable to keep track of every registered query rule source +var QueryRuleSources = NewQueryRuleInfo() + +// QueryRuleInfo is the maintainer of QueryRules from multiple sources +type QueryRuleInfo struct { + // mutex to protect following queryRulesMap + mu sync.Mutex + // queryRulesMap maps the names of different query rule sources to the actual QueryRules structure + queryRulesMap map[string]*QueryRules +} + +// NewQueryRuleInfo returns an empty QueryRuleInfo object for use +func NewQueryRuleInfo() *QueryRuleInfo { + qri := &QueryRuleInfo{ + queryRulesMap: map[string]*QueryRules{}, + } + return qri +} + +// RegisterQueryRuleSource registers a query rule source name with QueryRuleInfo +func (qri *QueryRuleInfo) RegisterQueryRuleSource(ruleSource string) { + qri.mu.Lock() + defer qri.mu.Unlock() + if _, existed := qri.queryRulesMap[ruleSource]; existed { + log.Fatal("Query rule source " + ruleSource + " has been registered") + } + qri.queryRulesMap[ruleSource] = NewQueryRules() +} + +// SetRules takes an external QueryRules structure and overwrite one of the +// internal QueryRules as designated by ruleSource parameter +func (qri *QueryRuleInfo) SetRules(ruleSource string, newRules *QueryRules) error { + if newRules == nil { + newRules = NewQueryRules() + } + qri.mu.Lock() + defer qri.mu.Unlock() + if _, ok := qri.queryRulesMap[ruleSource]; ok { + qri.queryRulesMap[ruleSource] = newRules.Copy() + return nil + } + return errors.New("Rule source identifier " + ruleSource + " is not valid") +} + +// GetRules returns the corresponding QueryRules as designated by ruleSource parameter +func (qri *QueryRuleInfo) GetRules(ruleSource string) (*QueryRules, error) { + qri.mu.Lock() + defer qri.mu.Unlock() + if ruleset, ok := qri.queryRulesMap[ruleSource]; ok { + return ruleset.Copy(), nil + } + return NewQueryRules(), errors.New("Rule source identifier " + ruleSource + " is not valid") +} + +// filterByPlan creates a new QueryRules by prefiltering on all query rules that are contained in internal +// QueryRules structures, in other words, query rules from all predefined sources will be applied +func (qri *QueryRuleInfo) filterByPlan(query string, planid planbuilder.PlanType, tableName string) (newqrs *QueryRules) { + qri.mu.Lock() + defer qri.mu.Unlock() + newqrs = NewQueryRules() + for _, rules := range qri.queryRulesMap { + newqrs.Append(rules.filterByPlan(query, planid, tableName)) + } + return newqrs +} diff --git a/go/vt/tabletserver/request_context.go b/go/vt/tabletserver/request_context.go index 4ae5454b9de..3d9ebcf8196 100644 --- a/go/vt/tabletserver/request_context.go +++ b/go/vt/tabletserver/request_context.go @@ -7,13 +7,14 @@ package tabletserver import ( "time" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/hack" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/vt/dbconnpool" "github.com/youtube/vitess/go/vt/sqlparser" + "golang.org/x/net/context" ) +// RequestContext encapsulates a context and associated variables for a request type RequestContext struct { ctx context.Context logStats *SQLQueryStats @@ -25,17 +26,18 @@ func (rqc *RequestContext) getConn(pool *dbconnpool.ConnectionPool) dbconnpool.P start := time.Now() timeout, err := rqc.deadline.Timeout() if err != nil { - panic(NewTabletError(FAIL, "getConn: %v", err)) + panic(NewTabletError(ErrFail, "getConn: %v", err)) } conn, err := pool.Get(timeout) switch err { case nil: rqc.logStats.WaitingForConnection += time.Now().Sub(start) return conn - case dbconnpool.CONN_POOL_CLOSED_ERR: + case dbconnpool.ErrConnPoolClosed: panic(connPoolClosedErr) } - panic(NewTabletErrorSql(FATAL, err)) + go CheckMySQL() + panic(NewTabletErrorSql(ErrFatal, err)) } func (rqc *RequestContext) qFetch(logStats *SQLQueryStats, parsedQuery *sqlparser.ParsedQuery, bindVars map[string]interface{}) (result *mproto.QueryResult) { @@ -46,12 +48,13 @@ func (rqc *RequestContext) qFetch(logStats *SQLQueryStats, parsedQuery *sqlparse waitingForConnectionStart := time.Now() timeout, err := rqc.deadline.Timeout() if err != nil { - q.Err = NewTabletError(FAIL, "qFetch: %v", err) + q.Err = NewTabletError(ErrFail, "qFetch: %v", err) } conn, err := rqc.qe.connPool.Get(timeout) logStats.WaitingForConnection += time.Now().Sub(waitingForConnectionStart) if err != nil { - q.Err = NewTabletErrorSql(FATAL, err) + go CheckMySQL() + q.Err = NewTabletErrorSql(ErrFatal, err) } else { defer conn.Recycle() q.Result, q.Err = rqc.execSQLNoPanic(conn, sql, false) @@ -86,7 +89,7 @@ func (rqc *RequestContext) generateFinalSql(parsedQuery *sqlparser.ParsedQuery, bindVars["#maxLimit"] = rqc.qe.maxResultSize.Get() + 1 sql, err := parsedQuery.GenerateQuery(bindVars) if err != nil { - panic(NewTabletError(FAIL, "%s", err)) + panic(NewTabletError(ErrFail, "%s", err)) } if buildStreamComment != nil { sql = append(sql, buildStreamComment...) @@ -105,7 +108,28 @@ func (rqc *RequestContext) execSQL(conn dbconnpool.PoolConnection, sql string, w } func (rqc *RequestContext) execSQLNoPanic(conn dbconnpool.PoolConnection, sql string, wantfields bool) (*mproto.QueryResult, error) { - if qd := rqc.qe.connKiller.SetDeadline(conn.Id(), rqc.deadline); qd != nil { + for attempt := 1; attempt <= 2; attempt++ { + r, err := rqc.execSQLOnce(conn, sql, wantfields) + switch { + case err == nil: + return r, nil + case !IsConnErr(err): + return nil, NewTabletErrorSql(ErrFail, err) + case attempt == 2: + return nil, NewTabletErrorSql(ErrFatal, err) + } + err2 := conn.Reconnect() + if err2 != nil { + go CheckMySQL() + return nil, NewTabletErrorSql(ErrFatal, err) + } + } + panic("unreachable") +} + +// execSQLOnce returns a normal error that needs to be wrapped into a TabletError by the caller. +func (rqc *RequestContext) execSQLOnce(conn dbconnpool.PoolConnection, sql string, wantfields bool) (*mproto.QueryResult, error) { + if qd := rqc.qe.connKiller.SetDeadline(conn.ID(), rqc.deadline); qd != nil { defer qd.Done() } @@ -113,7 +137,7 @@ func (rqc *RequestContext) execSQLNoPanic(conn dbconnpool.PoolConnection, sql st result, err := conn.ExecuteFetch(sql, int(rqc.qe.maxResultSize.Get()), wantfields) rqc.logStats.AddRewrittenSql(sql, start) if err != nil { - return nil, NewTabletErrorSql(FAIL, err) + return nil, err } return result, nil } @@ -123,6 +147,6 @@ func (rqc *RequestContext) execStreamSQL(conn dbconnpool.PoolConnection, sql str err := conn.ExecuteStreamFetch(sql, callback, int(rqc.qe.streamBufferSize.Get())) rqc.logStats.AddRewrittenSql(sql, start) if err != nil { - panic(NewTabletErrorSql(FAIL, err)) + panic(NewTabletErrorSql(ErrFail, err)) } } diff --git a/go/vt/tabletserver/rowcache.go b/go/vt/tabletserver/rowcache.go index 6ec59ff3a43..da61cbe5eae 100644 --- a/go/vt/tabletserver/rowcache.go +++ b/go/vt/tabletserver/rowcache.go @@ -62,7 +62,7 @@ func (rc *RowCache) Get(keys []string) (results map[string]RCResult) { if err != nil { conn.Close() conn = nil - panic(NewTabletError(FATAL, "%s", err)) + panic(NewTabletError(ErrFatal, "%s", err)) } results = make(map[string]RCResult, len(mkeys)) for _, mcresult := range mcresults { @@ -75,7 +75,7 @@ func (rc *RowCache) Get(keys []string) (results map[string]RCResult) { } row := rc.decodeRow(mcresult.Value) if row == nil { - panic(NewTabletError(FAIL, "Corrupt data for %s", mcresult.Key)) + panic(NewTabletError(ErrFatal, "Corrupt data for %s", mcresult.Key)) } results[mcresult.Key[prefixlen:]] = RCResult{Row: row, Cas: mcresult.Cas} } @@ -106,7 +106,7 @@ func (rc *RowCache) Set(key string, row []sqltypes.Value, cas uint64) { if err != nil { conn.Close() conn = nil - panic(NewTabletError(FATAL, "%s", err)) + panic(NewTabletError(ErrFatal, "%s", err)) } } @@ -122,7 +122,7 @@ func (rc *RowCache) Delete(key string) { if err != nil { conn.Close() conn = nil - panic(NewTabletError(FATAL, "%s", err)) + panic(NewTabletError(ErrFatal, "%s", err)) } } diff --git a/go/vt/tabletserver/rowcache_invalidator.go b/go/vt/tabletserver/rowcache_invalidator.go index f8094827a69..5fb34e3a41f 100644 --- a/go/vt/tabletserver/rowcache_invalidator.go +++ b/go/vt/tabletserver/rowcache_invalidator.go @@ -79,10 +79,10 @@ func NewRowcacheInvalidator(qe *QueryEngine) *RowcacheInvalidator { func (rci *RowcacheInvalidator) Open(dbname string, mysqld *mysqlctl.Mysqld) { rp, err := mysqld.MasterPosition() if err != nil { - panic(NewTabletError(FATAL, "Rowcache invalidator aborting: cannot determine replication position: %v", err)) + panic(NewTabletError(ErrFatal, "Rowcache invalidator aborting: cannot determine replication position: %v", err)) } if mysqld.Cnf().BinLogPath == "" { - panic(NewTabletError(FATAL, "Rowcache invalidator aborting: binlog path not specified")) + panic(NewTabletError(ErrFatal, "Rowcache invalidator aborting: binlog path not specified")) } rci.dbname = dbname rci.mysqld = mysqld @@ -119,6 +119,9 @@ func (rci *RowcacheInvalidator) run(ctx *sync2.ServiceContext) error { if err == nil { break } + if IsConnErr(err) { + go CheckMySQL() + } log.Errorf("binlog.ServeUpdateStream returned err '%v', retrying in 1 second.", err.Error()) internalErrors.Add("Invalidation", 1) time.Sleep(1 * time.Second) @@ -165,7 +168,7 @@ func (rci *RowcacheInvalidator) handleDMLEvent(event *blproto.StreamEvent) { invalidations := int64(0) tableInfo := rci.qe.schemaInfo.GetTable(event.TableName) if tableInfo == nil { - panic(NewTabletError(FAIL, "Table %s not found", event.TableName)) + panic(NewTabletError(ErrFail, "Table %s not found", event.TableName)) } if tableInfo.CacheType == schema.CACHE_NONE { return @@ -196,7 +199,7 @@ func (rci *RowcacheInvalidator) handleDMLEvent(event *blproto.StreamEvent) { func (rci *RowcacheInvalidator) handleDDLEvent(ddl string) { ddlPlan := planbuilder.DDLParse(ddl) if ddlPlan.Action == "" { - panic(NewTabletError(FAIL, "DDL is not understood")) + panic(NewTabletError(ErrFail, "DDL is not understood")) } if ddlPlan.TableName != "" && ddlPlan.TableName != ddlPlan.NewName { // It's a drop or rename. @@ -230,7 +233,7 @@ func (rci *RowcacheInvalidator) handleUnrecognizedEvent(sql string) { } // Ignore cross-db statements. - if table.Qualifier != nil && string(table.Qualifier) != rci.qe.dbconfig.DbName { + if table.Qualifier != nil && string(table.Qualifier) != rci.qe.dbconfigs.App.DbName { return } diff --git a/go/vt/tabletserver/schema_info.go b/go/vt/tabletserver/schema_info.go index 5526e22d625..761a827067c 100644 --- a/go/vt/tabletserver/schema_info.go +++ b/go/vt/tabletserver/schema_info.go @@ -81,7 +81,6 @@ type SchemaInfo struct { tables map[string]*TableInfo overrides []SchemaOverride queries *cache.LRUCache - rules *QueryRules connPool *dbconnpool.ConnectionPool cachePool *CachePool lastChange time.Time @@ -91,7 +90,6 @@ type SchemaInfo struct { func NewSchemaInfo(queryCacheSize int, reloadTime time.Duration, idleTimeout time.Duration) *SchemaInfo { si := &SchemaInfo{ queries: cache.NewLRUCache(int64(queryCacheSize)), - rules: NewQueryRules(), connPool: dbconnpool.NewConnectionPool("", 2, idleTimeout), ticks: timer.NewTimer(reloadTime), } @@ -115,7 +113,7 @@ func NewSchemaInfo(queryCacheSize int, reloadTime time.Duration, idleTimeout tim return si } -func (si *SchemaInfo) Open(connFactory dbconnpool.CreateConnectionFunc, schemaOverrides []SchemaOverride, cachePool *CachePool, qrs *QueryRules, strictMode bool) { +func (si *SchemaInfo) Open(connFactory dbconnpool.CreateConnectionFunc, schemaOverrides []SchemaOverride, cachePool *CachePool, strictMode bool) { si.connPool.Open(connFactory) // Get time first because it needs a connection from the pool. curTime := si.mysqlTime() @@ -124,13 +122,13 @@ func (si *SchemaInfo) Open(connFactory dbconnpool.CreateConnectionFunc, schemaOv defer conn.Recycle() if strictMode && !conn.(*dbconnpool.PooledDBConnection).VerifyStrict() { - panic(NewTabletError(FATAL, "Could not verify strict mode")) + panic(NewTabletError(ErrFatal, "Could not verify strict mode")) } si.cachePool = cachePool tables, err := conn.ExecuteFetch(base_show_tables, maxTableCount, false) if err != nil { - panic(NewTabletError(FATAL, "Could not get table list: %v", err)) + panic(NewTabletError(ErrFatal, "Could not get table list: %v", err)) } si.tables = make(map[string]*TableInfo, len(tables.Rows)) @@ -148,7 +146,7 @@ func (si *SchemaInfo) Open(connFactory dbconnpool.CreateConnectionFunc, schemaOv si.cachePool, ) if err != nil { - panic(NewTabletError(FATAL, "Could not get load table %s: %v", tableName, err)) + panic(NewTabletError(ErrFatal, "Could not get load table %s: %v", tableName, err)) } si.tables[tableName] = tableInfo } @@ -159,7 +157,6 @@ func (si *SchemaInfo) Open(connFactory dbconnpool.CreateConnectionFunc, schemaOv si.lastChange = curTime // Clear is not really needed. Doing it for good measure. si.queries.Clear() - si.rules = qrs.Copy() si.ticks.Start(func() { si.Reload() }) } @@ -211,7 +208,6 @@ func (si *SchemaInfo) Close() { si.tables = nil si.overrides = nil si.queries.Clear() - si.rules = NewQueryRules() } func (si *SchemaInfo) Reload() { @@ -244,14 +240,14 @@ func (si *SchemaInfo) mysqlTime() time.Time { defer conn.Recycle() tm, err := conn.ExecuteFetch("select unix_timestamp()", 1, false) if err != nil { - panic(NewTabletError(FAIL, "Could not get MySQL time: %v", err)) + panic(NewTabletError(ErrFail, "Could not get MySQL time: %v", err)) } if len(tm.Rows) != 1 || len(tm.Rows[0]) != 1 || tm.Rows[0][0].IsNull() { - panic(NewTabletError(FAIL, "Unexpected result for MySQL time: %+v", tm.Rows)) + panic(NewTabletError(ErrFail, "Unexpected result for MySQL time: %+v", tm.Rows)) } t, err := strconv.ParseInt(tm.Rows[0][0].String(), 10, 64) if err != nil { - panic(NewTabletError(FAIL, "Could not parse time %+v: %v", tm, err)) + panic(NewTabletError(ErrFail, "Could not parse time %+v: %v", tm, err)) } return time.Unix(t, 0) } @@ -262,6 +258,13 @@ func (si *SchemaInfo) triggerReload() { si.ticks.Trigger() } +// ClearQueryPlanCache should be called if query plan cache is potentially obsolete +func (si *SchemaInfo) ClearQueryPlanCache() { + si.mu.Lock() + defer si.mu.Unlock() + si.queries.Clear() +} + func (si *SchemaInfo) CreateOrUpdateTable(tableName string) { si.mu.Lock() defer si.mu.Unlock() @@ -270,7 +273,7 @@ func (si *SchemaInfo) CreateOrUpdateTable(tableName string) { defer conn.Recycle() tables, err := conn.ExecuteFetch(fmt.Sprintf("%s and table_name = '%s'", base_show_tables, tableName), 1, false) if err != nil { - panic(NewTabletError(FAIL, "Error fetching table %s: %v", tableName, err)) + panic(NewTabletError(ErrFail, "Error fetching table %s: %v", tableName, err)) } if len(tables.Rows) != 1 { // This can happen if DDLs race with each other. @@ -344,10 +347,10 @@ func (si *SchemaInfo) GetPlan(logStats *SQLQueryStats, sql string) *ExecPlan { } splan, err := planbuilder.GetExecPlan(sql, GetTable) if err != nil { - panic(NewTabletError(FAIL, "%s", err)) + panic(NewTabletError(ErrFail, "%s", err)) } plan := &ExecPlan{ExecPlan: splan, TableInfo: tableInfo} - plan.Rules = si.rules.filterByPlan(sql, plan.PlanId, plan.TableName) + plan.Rules = QueryRuleSources.filterByPlan(sql, plan.PlanId, plan.TableName) plan.Authorized = tableacl.Authorized(plan.TableName, plan.PlanId.MinRole()) if plan.PlanId.IsSelect() { if plan.FieldQuery == nil { @@ -360,7 +363,7 @@ func (si *SchemaInfo) GetPlan(logStats *SQLQueryStats, sql string) *ExecPlan { r, err := conn.ExecuteFetch(sql, 1, true) logStats.AddRewrittenSql(sql, start) if err != nil { - panic(NewTabletError(FAIL, "Error fetching fields: %v", err)) + panic(NewTabletError(ErrFail, "Error fetching fields: %v", err)) } plan.Fields = r.Fields } @@ -386,27 +389,14 @@ func (si *SchemaInfo) GetStreamPlan(sql string) *ExecPlan { } splan, err := planbuilder.GetStreamExecPlan(sql, GetTable) if err != nil { - panic(NewTabletError(FAIL, "%s", err)) + panic(NewTabletError(ErrFail, "%s", err)) } plan := &ExecPlan{ExecPlan: splan, TableInfo: tableInfo} - plan.Rules = si.rules.filterByPlan(sql, plan.PlanId, plan.TableName) + plan.Rules = QueryRuleSources.filterByPlan(sql, plan.PlanId, plan.TableName) plan.Authorized = tableacl.Authorized(plan.TableName, plan.PlanId.MinRole()) return plan } -func (si *SchemaInfo) SetRules(qrs *QueryRules) { - si.mu.Lock() - defer si.mu.Unlock() - si.rules = qrs.Copy() - si.queries.Clear() -} - -func (si *SchemaInfo) GetRules() (qrs *QueryRules) { - si.mu.Lock() - defer si.mu.Unlock() - return si.rules.Copy() -} - func (si *SchemaInfo) GetTable(tableName string) *TableInfo { si.mu.Lock() defer si.mu.Unlock() @@ -432,7 +422,7 @@ func (si *SchemaInfo) getQuery(sql string) *ExecPlan { func (si *SchemaInfo) SetQueryCacheSize(size int) { if size <= 0 { - panic(NewTabletError(FAIL, "cache size %v out of range", size)) + panic(NewTabletError(ErrFail, "cache size %v out of range", size)) } si.queries.SetCapacity(int64(size)) } diff --git a/go/vt/tabletserver/sqlquery.go b/go/vt/tabletserver/sqlquery.go index b9683379217..8fee23b1872 100644 --- a/go/vt/tabletserver/sqlquery.go +++ b/go/vt/tabletserver/sqlquery.go @@ -11,7 +11,6 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/mysql" mproto "github.com/youtube/vitess/go/mysql/proto" @@ -22,6 +21,7 @@ import ( "github.com/youtube/vitess/go/vt/dbconnpool" "github.com/youtube/vitess/go/vt/mysqlctl" "github.com/youtube/vitess/go/vt/tabletserver/proto" + "golang.org/x/net/context" ) const ( @@ -76,7 +76,6 @@ type SqlQuery struct { qe *QueryEngine sessionId int64 dbconfig *dbconfigs.DBConfig - mysqld *mysqlctl.Mysqld } // NewSqlQuery creates an instance of SqlQuery. Only one instance @@ -102,39 +101,34 @@ func (sq *SqlQuery) setState(state int64) { } // allowQueries starts the query service. -// If the state is anything other than NOT_SERVING, it fails. +// If the state is other than SERVING or NOT_SERVING, it fails. // If allowQuery succeeds, the resulting state is SERVING. // Otherwise, it reverts back to NOT_SERVING. // While allowQuery is running, the state is set to INITIALIZING. // If waitForMysql is set to true, allowQueries will not return // until it's able to connect to mysql. // No other operations are allowed when allowQueries is running. -func (sq *SqlQuery) allowQueries(dbconfig *dbconfigs.DBConfig, schemaOverrides []SchemaOverride, qrs *QueryRules, mysqld *mysqlctl.Mysqld, waitForMysql bool) (err error) { +func (sq *SqlQuery) allowQueries(dbconfigs *dbconfigs.DBConfigs, schemaOverrides []SchemaOverride, mysqld *mysqlctl.Mysqld) (err error) { + // Fast path + if sq.state.Get() == SERVING { + return nil + } sq.mu.Lock() defer sq.mu.Unlock() if sq.state.Get() != NOT_SERVING { - terr := NewTabletError(FATAL, "cannot start query service, current state: %s", sq.GetState()) + terr := NewTabletError(ErrFatal, "cannot start query service, current state: %s", sq.GetState()) return terr } // state is NOT_SERVING sq.setState(INITIALIZING) - if waitForMysql { - waitTime := time.Second - for { - c, err := dbconnpool.NewDBConnection(&dbconfig.ConnectionParams, mysqlStats) - if err == nil { - c.Close() - break - } - log.Warningf("mysql.Connect() error, retrying in %v: %v", waitTime, err) - time.Sleep(waitTime) - // Cap at 32 seconds - if waitTime < 30*time.Second { - waitTime = waitTime * 2 - } - } + c, err := dbconnpool.NewDBConnection(&dbconfigs.App.ConnectionParams, mysqlStats) + if err != nil { + log.Infof("allowQueries failed: %v", err) + sq.setState(NOT_SERVING) + return err } + c.Close() defer func() { if x := recover(); x != nil { @@ -147,9 +141,8 @@ func (sq *SqlQuery) allowQueries(dbconfig *dbconfigs.DBConfig, schemaOverrides [ sq.setState(SERVING) }() - sq.qe.Open(dbconfig, schemaOverrides, qrs, mysqld) - sq.dbconfig = dbconfig - sq.mysqld = mysqld + sq.qe.Open(dbconfigs, schemaOverrides, mysqld) + sq.dbconfig = &dbconfigs.App sq.sessionId = Rand() log.Infof("Session id: %d", sq.sessionId) return nil @@ -191,25 +184,41 @@ func (sq *SqlQuery) disallowQueries() { sq.setState(NOT_SERVING) sq.mu.Unlock() }() - log.Infof("Stopping query service: %d", sq.sessionId) + log.Infof("Stopping query service. Session id: %d", sq.sessionId) sq.qe.Close() sq.sessionId = 0 sq.dbconfig = &dbconfigs.DBConfig{} } +// checkMySQL returns true if we can connect to MySQL. +// The function returns false only if the query service is running +// and we're unable to make a connection. +func (sq *SqlQuery) checkMySQL() bool { + if err := sq.startRequest(sq.sessionId, false); err != nil { + return true + } + defer sq.endRequest() + defer func() { + if x := recover(); x != nil { + log.Errorf("Checking MySQL, unexpected error: %v", x) + } + }() + return sq.qe.CheckMySQL() +} + // GetSessionId returns a sessionInfo response if the state is SERVING. func (sq *SqlQuery) GetSessionId(sessionParams *proto.SessionParams, sessionInfo *proto.SessionInfo) error { // We perform a lockless read of state because we don't care if it changes // after we check its value. if sq.state.Get() != SERVING { - return NewTabletError(RETRY, "Query server is in %s state", sq.GetState()) + return NewTabletError(ErrRetry, "Query server is in %s state", sq.GetState()) } // state was SERVING if sessionParams.Keyspace != sq.dbconfig.Keyspace { - return NewTabletError(FATAL, "Keyspace mismatch, expecting %v, received %v", sq.dbconfig.Keyspace, sessionParams.Keyspace) + return NewTabletError(ErrFatal, "Keyspace mismatch, expecting %v, received %v", sq.dbconfig.Keyspace, sessionParams.Keyspace) } if strings.ToLower(sessionParams.Shard) != strings.ToLower(sq.dbconfig.Shard) { - return NewTabletError(FATAL, "Shard mismatch, expecting %v, received %v", sq.dbconfig.Shard, sessionParams.Shard) + return NewTabletError(ErrFatal, "Shard mismatch, expecting %v, received %v", sq.dbconfig.Shard, sessionParams.Shard) } sessionInfo.SessionId = sq.sessionId return nil @@ -223,11 +232,11 @@ func (sq *SqlQuery) Begin(context context.Context, session *proto.Session, txInf defer sq.mu.RUnlock() defer handleError(&err, logStats) if sq.state.Get() != SERVING { - return NewTabletError(RETRY, "cannot begin transaction in state %s", sq.GetState()) + return NewTabletError(ErrRetry, "cannot begin transaction in state %s", sq.GetState()) } // state is SERVING if session.SessionId == 0 || session.SessionId != sq.sessionId { - return NewTabletError(RETRY, "Invalid session Id %v", session.SessionId) + return NewTabletError(ErrRetry, "Invalid session Id %v", session.SessionId) } defer queryStats.Record("BEGIN", time.Now()) txInfo.TransactionId = sq.qe.txPool.Begin() @@ -272,17 +281,17 @@ func handleExecError(query *proto.Query, err *error, logStats *SQLQueryStats) { terr, ok := x.(*TabletError) if !ok { log.Errorf("Uncaught panic for %v:\n%v\n%s", query, x, tb.Stack(4)) - *err = NewTabletError(FAIL, "%v: uncaught panic for %v", x, query) + *err = NewTabletError(ErrFail, "%v: uncaught panic for %v", x, query) internalErrors.Add("Panic", 1) return } *err = terr terr.RecordStats() // suppress these errors in logs - if terr.ErrorType == RETRY || terr.ErrorType == TX_POOL_FULL || terr.SqlError == mysql.DUP_ENTRY { + if terr.ErrorType == ErrRetry || terr.ErrorType == ErrTxPoolFull || terr.SqlError == mysql.ErrDupEntry { return } - if terr.ErrorType == FATAL { + if terr.ErrorType == ErrFatal { log.Errorf("%v: %v", terr, query) } else { log.Warningf("%v: %v", terr, query) @@ -331,7 +340,7 @@ func (sq *SqlQuery) Execute(context context.Context, query *proto.Query, reply * func (sq *SqlQuery) StreamExecute(context context.Context, query *proto.Query, sendReply func(*mproto.QueryResult) error) (err error) { // check cases we don't handle yet if query.TransactionId != 0 { - return NewTabletError(FAIL, "Transactions not supported with streaming") + return NewTabletError(ErrFail, "Transactions not supported with streaming") } logStats := newSqlQueryStats("StreamExecute", context) @@ -367,7 +376,7 @@ func (sq *SqlQuery) StreamExecute(context context.Context, query *proto.Query, s // its own transaction, in which case it's expected to commit it also. func (sq *SqlQuery) ExecuteBatch(context context.Context, queryList *proto.QueryList, reply *proto.QueryResultList) (err error) { if len(queryList.Queries) == 0 { - return NewTabletError(FAIL, "Empty query list") + return NewTabletError(ErrFail, "Empty query list") } allowShutdown := (queryList.TransactionId != 0) @@ -388,7 +397,7 @@ func (sq *SqlQuery) ExecuteBatch(context context.Context, queryList *proto.Query switch trimmed { case "begin": if session.TransactionId != 0 { - panic(NewTabletError(FAIL, "Nested transactions disallowed")) + panic(NewTabletError(ErrFail, "Nested transactions disallowed")) } var txInfo proto.TransactionInfo if err = sq.Begin(context, &session, &txInfo); err != nil { @@ -399,7 +408,7 @@ func (sq *SqlQuery) ExecuteBatch(context context.Context, queryList *proto.Query reply.List = append(reply.List, mproto.QueryResult{}) case "commit": if !beginCalled { - panic(NewTabletError(FAIL, "Cannot commit without begin")) + panic(NewTabletError(ErrFail, "Cannot commit without begin")) } if err = sq.Commit(context, &session); err != nil { return err @@ -426,7 +435,7 @@ func (sq *SqlQuery) ExecuteBatch(context context.Context, queryList *proto.Query } if beginCalled { sq.Rollback(context, &session) - panic(NewTabletError(FAIL, "begin called with no commit")) + panic(NewTabletError(ErrFail, "begin called with no commit")) } return nil } @@ -441,7 +450,7 @@ func (sq *SqlQuery) SplitQuery(context context.Context, req *proto.SplitQueryReq splitter := NewQuerySplitter(&(req.Query), req.SplitCount, sq.qe.schemaInfo) err = splitter.validateQuery() if err != nil { - return NewTabletError(FAIL, "query validation error: %s", err) + return NewTabletError(ErrFail, "query validation error: %s", err) } // Partial initialization or QueryExecutor is enough to call execSQL requestContext := RequestContext{ @@ -451,6 +460,7 @@ func (sq *SqlQuery) SplitQuery(context context.Context, req *proto.SplitQueryReq deadline: NewDeadline(sq.qe.queryTimeout.Get()), } conn := requestContext.getConn(sq.qe.connPool) + defer conn.Recycle() // TODO: For fetching pkMinMax, include where clauses on the // primary key, if any, in the original query which might give a narrower // range of PKs to work with. @@ -475,11 +485,11 @@ func (sq *SqlQuery) startRequest(sessionId int64, allowShutdown bool) (err error if allowShutdown && st == SHUTTING_TX { goto verifySession } - return NewTabletError(RETRY, "operation not allowed in state %s", sq.GetState()) + return NewTabletError(ErrRetry, "operation not allowed in state %s", sq.GetState()) verifySession: if sessionId == 0 || sessionId != sq.sessionId { - return NewTabletError(RETRY, "Invalid session Id %v", sessionId) + return NewTabletError(ErrRetry, "Invalid session Id %v", sessionId) } sq.requests.Add(1) return nil diff --git a/go/vt/tabletserver/status.go b/go/vt/tabletserver/status.go index b9c98ef9065..babfa0589b5 100644 --- a/go/vt/tabletserver/status.go +++ b/go/vt/tabletserver/status.go @@ -82,7 +82,7 @@ func AddStatusPart() { status := queryserviceStatus{ State: SqlQueryRpcService.GetState(), } - rates := QPSRates.Get() + rates := qpsRates.Get() if qps, ok := rates["All"]; ok && len(qps) > 0 { status.CurrentQPS = qps[0] diff --git a/go/vt/tabletserver/streamlogger.go b/go/vt/tabletserver/streamlogger.go index 3df447884d7..b97b5562f36 100644 --- a/go/vt/tabletserver/streamlogger.go +++ b/go/vt/tabletserver/streamlogger.go @@ -12,11 +12,11 @@ import ( "strings" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/sqltypes" "github.com/youtube/vitess/go/streamlog" "github.com/youtube/vitess/go/vt/callinfo" + "golang.org/x/net/context" ) var SqlQueryLogger = streamlog.New("SqlQuery", 50) @@ -173,7 +173,7 @@ func (log *SQLQueryStats) ErrorStr() string { func (log *SQLQueryStats) Format(params url.Values) string { _, fullBindParams := params["full"] return fmt.Sprintf( - "%v\t%v\t%v\t%v\t%v\t%.6f\t%v\t%q\t%v\t%v\t%q\t%v\t%.6f\t%.6f\t%v\t%v\t%v\t%v\t%v\t%q\t\n", + "%v\t%v\t%v\t%v\t%v\t%.6f\t%v\t%q\t%v\t%v\t%q\t%v\t%.6f\t%.6f\t%v\t%v\t%v\t%v\t%v\t%v\t%q\t\n", log.Method, log.RemoteAddr(), log.Username(), @@ -188,6 +188,7 @@ func (log *SQLQueryStats) Format(params url.Values) string { log.FmtQuerySources(), log.MysqlResponseTime.Seconds(), log.WaitingForConnection.Seconds(), + log.RowsAffected, log.SizeOfResponse(), log.CacheHits, log.CacheMisses, diff --git a/go/vt/tabletserver/tablet_error.go b/go/vt/tabletserver/tablet_error.go index 31a2fe6230f..03e8d2c03f9 100644 --- a/go/vt/tabletserver/tablet_error.go +++ b/go/vt/tabletserver/tablet_error.go @@ -6,6 +6,8 @@ package tabletserver import ( "fmt" + "regexp" + "strconv" "strings" "time" @@ -16,15 +18,25 @@ import ( ) const ( - FAIL = iota - RETRY - FATAL - TX_POOL_FULL - NOT_IN_TX + // ErrFail is returned when a query fails + ErrFail = iota + + // ErrRetry is returned when a query can be retried + ErrRetry + + // ErrFatal is returned when a query cannot be retried + ErrFatal + + // ErrTxPoolFull is returned when we can't get a connection + ErrTxPoolFull + + // ErrNotInTx is returned when we're not in a transaction but should be + ErrNotInTx ) var logTxPoolFull = logutil.NewThrottledLogger("TxPoolFull", 1*time.Minute) +// TabletError is the erro type we use in this library type TabletError struct { ErrorType int Message string @@ -36,6 +48,7 @@ type hasNumber interface { Number() int } +// NewTabletError returns a TabletError of the given type func NewTabletError(errorType int, format string, args ...interface{}) *TabletError { return &TabletError{ ErrorType: errorType, @@ -43,6 +56,7 @@ func NewTabletError(errorType int, format string, args ...interface{}) *TabletEr } } +// NewTabletErrorSql returns a TabletError based on the error func NewTabletErrorSql(errorType int, err error) *TabletError { var errnum int errstr := err.Error() @@ -50,8 +64,8 @@ func NewTabletErrorSql(errorType int, err error) *TabletError { errnum = sqlErr.Number() // Override error type if MySQL is in read-only mode. It's probably because // there was a remaster and there are old clients still connected. - if errnum == mysql.OPTION_PREVENTS_STATEMENT && strings.Contains(errstr, "read-only") { - errorType = RETRY + if errnum == mysql.ErrOptionPreventsStatement && strings.Contains(errstr, "read-only") { + errorType = ErrRetry } } return &TabletError{ @@ -61,36 +75,68 @@ func NewTabletErrorSql(errorType int, err error) *TabletError { } } +var errExtract = regexp.MustCompile(`.*\(errno ([0-9]*)\).*`) + +// IsConnErr returns true if the error is a connection error. If +// the error is of type TabletError or hasNumber, it checks the error +// code. Otherwise, it parses the string looking for (errno xxxx) +// and uses the extracted value to determine if it's a conn error. +func IsConnErr(err error) bool { + var sqlError int + switch err := err.(type) { + case *TabletError: + sqlError = err.SqlError + case hasNumber: + sqlError = err.Number() + default: + match := errExtract.FindStringSubmatch(err.Error()) + if match != nil { + return false + } + var convErr error + sqlError, convErr = strconv.Atoi(match[1]) + if convErr != nil { + return false + } + } + // 2013 means that someone sniped the query. + if sqlError == 2013 { + return false + } + return sqlError >= 2000 && sqlError <= 2018 +} + func (te *TabletError) Error() string { format := "error: %s" switch te.ErrorType { - case RETRY: + case ErrRetry: format = "retry: %s" - case FATAL: + case ErrFatal: format = "fatal: %s" - case TX_POOL_FULL: + case ErrTxPoolFull: format = "tx_pool_full: %s" - case NOT_IN_TX: + case ErrNotInTx: format = "not_in_tx: %s" } return fmt.Sprintf(format, te.Message) } +// RecordStats will record the error in the proper stat bucket func (te *TabletError) RecordStats() { switch te.ErrorType { - case RETRY: + case ErrRetry: infoErrors.Add("Retry", 1) - case FATAL: + case ErrFatal: infoErrors.Add("Fatal", 1) - case TX_POOL_FULL: + case ErrTxPoolFull: errorStats.Add("TxPoolFull", 1) - case NOT_IN_TX: + case ErrNotInTx: errorStats.Add("NotInTx", 1) default: switch te.SqlError { - case mysql.DUP_ENTRY: + case mysql.ErrDupEntry: infoErrors.Add("DupKey", 1) - case mysql.LOCK_WAIT_TIMEOUT, mysql.LOCK_DEADLOCK: + case mysql.ErrLockWaitTimeout, mysql.ErrLockDeadlock: errorStats.Add("Deadlock", 1) default: errorStats.Add("Fail", 1) @@ -103,16 +149,16 @@ func handleError(err *error, logStats *SQLQueryStats) { terr, ok := x.(*TabletError) if !ok { log.Errorf("Uncaught panic:\n%v\n%s", x, tb.Stack(4)) - *err = NewTabletError(FAIL, "%v: uncaught panic", x) + *err = NewTabletError(ErrFail, "%v: uncaught panic", x) internalErrors.Add("Panic", 1) return } *err = terr terr.RecordStats() - if terr.ErrorType == RETRY { // Retry errors are too spammy + if terr.ErrorType == ErrRetry { // Retry errors are too spammy return } - if terr.ErrorType == TX_POOL_FULL { + if terr.ErrorType == ErrTxPoolFull { logTxPoolFull.Errorf("%v", terr) } else { log.Errorf("%v", terr) @@ -132,7 +178,7 @@ func logError() { internalErrors.Add("Panic", 1) return } - if terr.ErrorType == TX_POOL_FULL { + if terr.ErrorType == ErrTxPoolFull { logTxPoolFull.Errorf("%v", terr) } else { log.Errorf("%v", terr) diff --git a/go/vt/tabletserver/tabletconn/tablet_conn.go b/go/vt/tabletserver/tabletconn/tablet_conn.go index 22ec170d28b..d3cee65f0b3 100644 --- a/go/vt/tabletserver/tabletconn/tablet_conn.go +++ b/go/vt/tabletserver/tabletconn/tablet_conn.go @@ -8,11 +8,11 @@ import ( "flag" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" mproto "github.com/youtube/vitess/go/mysql/proto" tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) const ( diff --git a/go/vt/tabletserver/tx_pool.go b/go/vt/tabletserver/tx_pool.go index e5593da1a4e..79a0868d50b 100644 --- a/go/vt/tabletserver/tx_pool.go +++ b/go/vt/tabletserver/tx_pool.go @@ -5,6 +5,7 @@ package tabletserver import ( + "errors" "fmt" "net/url" "strings" @@ -103,7 +104,7 @@ func (axp *TxPool) TransactionKiller() { defer logError() for _, v := range axp.activePool.GetOutdated(time.Duration(axp.Timeout()), "for rollback") { conn := v.(*TxConnection) - log.Warningf("killing transaction: %s", conn.Format(nil)) + log.Warningf("killing transaction (exceeded timeout: %v): %s", axp.Timeout(), conn.Format(nil)) killStats.Add("Transactions", 1) conn.Close() conn.discard(TX_KILL) @@ -114,17 +115,17 @@ func (axp *TxPool) Begin() int64 { conn, err := axp.pool.Get(axp.poolTimeout.Get()) if err != nil { switch err { - case dbconnpool.CONN_POOL_CLOSED_ERR: + case dbconnpool.ErrConnPoolClosed: panic(connPoolClosedErr) case pools.TIMEOUT_ERR: axp.LogActive() - panic(NewTabletError(TX_POOL_FULL, "Transaction pool connection limit exceeded")) + panic(NewTabletError(ErrTxPoolFull, "Transaction pool connection limit exceeded")) } - panic(NewTabletErrorSql(FATAL, err)) + panic(NewTabletErrorSql(ErrFatal, err)) } if _, err := conn.ExecuteFetch(BEGIN, 1, false); err != nil { conn.Recycle() - panic(NewTabletErrorSql(FAIL, err)) + panic(NewTabletErrorSql(ErrFail, err)) } transactionId := axp.lastId.Add(1) axp.activePool.Register(transactionId, newTxConnection(conn, transactionId, axp)) @@ -141,7 +142,7 @@ func (axp *TxPool) SafeCommit(transactionId int64) (invalidList map[string]Dirty axp.txStats.Add("Completed", time.Now().Sub(conn.StartTime)) if _, fetchErr := conn.ExecuteFetch(COMMIT, 1, false); fetchErr != nil { conn.Close() - err = NewTabletErrorSql(FAIL, fetchErr) + err = NewTabletErrorSql(ErrFail, fetchErr) } return } @@ -152,7 +153,7 @@ func (axp *TxPool) Rollback(transactionId int64) { axp.txStats.Add("Aborted", time.Now().Sub(conn.StartTime)) if _, err := conn.ExecuteFetch(ROLLBACK, 1, false); err != nil { conn.Close() - panic(NewTabletErrorSql(FAIL, err)) + panic(NewTabletErrorSql(ErrFail, err)) } } @@ -160,7 +161,7 @@ func (axp *TxPool) Rollback(transactionId int64) { func (axp *TxPool) Get(transactionId int64) (conn *TxConnection) { v, err := axp.activePool.Get(transactionId, "for query") if err != nil { - panic(NewTabletError(NOT_IN_TX, "Transaction %d: %v", transactionId, err)) + panic(NewTabletError(ErrNotInTx, "Transaction %d: %v", transactionId, err)) } return v.(*TxConnection) } @@ -234,6 +235,10 @@ func (txc *TxConnection) Recycle() { } } +func (txc *TxConnection) Reconnect() error { + return errors.New("Reconnect not allowed for TxConnection") +} + func (txc *TxConnection) RecordQuery(query string) { txc.Queries = append(txc.Queries, query) } diff --git a/go/vt/topo/get_srv_shard_args_bson.go b/go/vt/topo/get_srv_shard_args_bson.go new file mode 100644 index 00000000000..9b276ed3c90 --- /dev/null +++ b/go/vt/topo/get_srv_shard_args_bson.go @@ -0,0 +1,53 @@ +// Copyright 2012, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package topo + +// DO NOT EDIT. +// FILE GENERATED BY BSONGEN. + +import ( + "bytes" + + "github.com/youtube/vitess/go/bson" + "github.com/youtube/vitess/go/bytes2" +) + +// MarshalBson bson-encodes GetSrvShardArgs. +func (getSrvShardArgs *GetSrvShardArgs) MarshalBson(buf *bytes2.ChunkedWriter, key string) { + bson.EncodeOptionalPrefix(buf, bson.Object, key) + lenWriter := bson.NewLenWriter(buf) + + bson.EncodeString(buf, "Cell", getSrvShardArgs.Cell) + bson.EncodeString(buf, "Keyspace", getSrvShardArgs.Keyspace) + bson.EncodeString(buf, "Shard", getSrvShardArgs.Shard) + + lenWriter.Close() +} + +// UnmarshalBson bson-decodes into GetSrvShardArgs. +func (getSrvShardArgs *GetSrvShardArgs) UnmarshalBson(buf *bytes.Buffer, kind byte) { + switch kind { + case bson.EOO, bson.Object: + // valid + case bson.Null: + return + default: + panic(bson.NewBsonError("unexpected kind %v for GetSrvShardArgs", kind)) + } + bson.Next(buf, 4) + + for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { + switch bson.ReadCString(buf) { + case "Cell": + getSrvShardArgs.Cell = bson.DecodeString(buf, kind) + case "Keyspace": + getSrvShardArgs.Keyspace = bson.DecodeString(buf, kind) + case "Shard": + getSrvShardArgs.Shard = bson.DecodeString(buf, kind) + default: + bson.Skip(buf, kind) + } + } +} diff --git a/go/vt/topo/helpers/copy.go b/go/vt/topo/helpers/copy.go index 749bc8e1d31..aa921ca22eb 100644 --- a/go/vt/topo/helpers/copy.go +++ b/go/vt/topo/helpers/copy.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// helpers package contains a few utility classes to handle topo.Server +// Package helpers contains a few utility classes to handle topo.Server // objects, and transitions from one topo implementation to another. package helpers diff --git a/go/vt/topo/helpers/copy_test.go b/go/vt/topo/helpers/copy_test.go index 4f4e2f683c0..8eab0b9323c 100644 --- a/go/vt/topo/helpers/copy_test.go +++ b/go/vt/topo/helpers/copy_test.go @@ -44,7 +44,6 @@ func createSetup(t *testing.T) (topo.Server, topo.Server) { Uid: 123, }, Hostname: "masterhost", - Parent: topo.TabletAlias{}, IPAddr: "1.2.3.4", Portmap: map[string]int{ "vt": 8101, @@ -73,10 +72,6 @@ func createSetup(t *testing.T) (topo.Server, topo.Server) { }, Hostname: "slavehost", - Parent: topo.TabletAlias{ - Cell: "test_cell", - Uid: 123, - }, Keyspace: "test_keyspace", Shard: "0", Type: topo.TYPE_REPLICA, diff --git a/go/vt/topo/helpers/tee.go b/go/vt/topo/helpers/tee.go index a743f8419f0..939f1e79fa1 100644 --- a/go/vt/topo/helpers/tee.go +++ b/go/vt/topo/helpers/tee.go @@ -7,10 +7,10 @@ package helpers import ( "fmt" "sync" - "time" log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // Tee is an implementation of topo.Server that uses a primary @@ -53,6 +53,7 @@ type versionMapping struct { readFromSecondVersion int64 } +// NewTee returns a new topo.Server object func NewTee(primary, secondary topo.Server, reverseLockOrder bool) *Tee { lockFirst := primary lockSecond := secondary @@ -80,6 +81,7 @@ func NewTee(primary, secondary topo.Server, reverseLockOrder bool) *Tee { // topo.Server management interface. // +// Close is part of the topo.Server interface func (tee *Tee) Close() { tee.primary.Close() tee.secondary.Close() @@ -89,6 +91,7 @@ func (tee *Tee) Close() { // Cell management, global // +// GetKnownCells is part of the topo.Server interface func (tee *Tee) GetKnownCells() ([]string, error) { return tee.readFrom.GetKnownCells() } @@ -97,6 +100,7 @@ func (tee *Tee) GetKnownCells() ([]string, error) { // Keyspace management, global. // +// CreateKeyspace is part of the topo.Server interface func (tee *Tee) CreateKeyspace(keyspace string, value *topo.Keyspace) error { if err := tee.primary.CreateKeyspace(keyspace, value); err != nil { return err @@ -109,6 +113,7 @@ func (tee *Tee) CreateKeyspace(keyspace string, value *topo.Keyspace) error { return nil } +// UpdateKeyspace is part of the topo.Server interface func (tee *Tee) UpdateKeyspace(ki *topo.KeyspaceInfo, existingVersion int64) (newVersion int64, err error) { if newVersion, err = tee.primary.UpdateKeyspace(ki, existingVersion); err != nil { // failed on primary, not updating secondary @@ -160,6 +165,7 @@ func (tee *Tee) UpdateKeyspace(ki *topo.KeyspaceInfo, existingVersion int64) (ne return } +// GetKeyspace is part of the topo.Server interface func (tee *Tee) GetKeyspace(keyspace string) (*topo.KeyspaceInfo, error) { ki, err := tee.readFrom.GetKeyspace(keyspace) if err != nil { @@ -181,10 +187,12 @@ func (tee *Tee) GetKeyspace(keyspace string) (*topo.KeyspaceInfo, error) { return ki, nil } +// GetKeyspaces is part of the topo.Server interface func (tee *Tee) GetKeyspaces() ([]string, error) { return tee.readFrom.GetKeyspaces() } +// DeleteKeyspaceShards is part of the topo.Server interface func (tee *Tee) DeleteKeyspaceShards(keyspace string) error { if err := tee.primary.DeleteKeyspaceShards(keyspace); err != nil { return err @@ -201,6 +209,7 @@ func (tee *Tee) DeleteKeyspaceShards(keyspace string) error { // Shard management, global. // +// CreateShard is part of the topo.Server interface func (tee *Tee) CreateShard(keyspace, shard string, value *topo.Shard) error { err := tee.primary.CreateShard(keyspace, shard, value) if err != nil && err != topo.ErrNodeExists { @@ -215,6 +224,7 @@ func (tee *Tee) CreateShard(keyspace, shard string, value *topo.Shard) error { return err } +// UpdateShard is part of the topo.Server interface func (tee *Tee) UpdateShard(si *topo.ShardInfo, existingVersion int64) (newVersion int64, err error) { if newVersion, err = tee.primary.UpdateShard(si, existingVersion); err != nil { // failed on primary, not updating secondary @@ -266,6 +276,7 @@ func (tee *Tee) UpdateShard(si *topo.ShardInfo, existingVersion int64) (newVersi return } +// ValidateShard is part of the topo.Server interface func (tee *Tee) ValidateShard(keyspace, shard string) error { err := tee.primary.ValidateShard(keyspace, shard) if err != nil { @@ -279,6 +290,7 @@ func (tee *Tee) ValidateShard(keyspace, shard string) error { return nil } +// GetShard is part of the topo.Server interface func (tee *Tee) GetShard(keyspace, shard string) (*topo.ShardInfo, error) { si, err := tee.readFrom.GetShard(keyspace, shard) if err != nil { @@ -300,10 +312,12 @@ func (tee *Tee) GetShard(keyspace, shard string) (*topo.ShardInfo, error) { return si, nil } +// GetShardNames is part of the topo.Server interface func (tee *Tee) GetShardNames(keyspace string) ([]string, error) { return tee.readFrom.GetShardNames(keyspace) } +// DeleteShard is part of the topo.Server interface func (tee *Tee) DeleteShard(keyspace, shard string) error { err := tee.primary.DeleteShard(keyspace, shard) if err != nil && err != topo.ErrNoNode { @@ -321,6 +335,7 @@ func (tee *Tee) DeleteShard(keyspace, shard string) error { // Tablet management, per cell. // +// CreateTablet is part of the topo.Server interface func (tee *Tee) CreateTablet(tablet *topo.Tablet) error { err := tee.primary.CreateTablet(tablet) if err != nil && err != topo.ErrNodeExists { @@ -334,6 +349,7 @@ func (tee *Tee) CreateTablet(tablet *topo.Tablet) error { return err } +// UpdateTablet is part of the topo.Server interface func (tee *Tee) UpdateTablet(tablet *topo.TabletInfo, existingVersion int64) (newVersion int64, err error) { if newVersion, err = tee.primary.UpdateTablet(tablet, existingVersion); err != nil { // failed on primary, not updating secondary @@ -385,6 +401,7 @@ func (tee *Tee) UpdateTablet(tablet *topo.TabletInfo, existingVersion int64) (ne return } +// UpdateTabletFields is part of the topo.Server interface func (tee *Tee) UpdateTabletFields(tabletAlias topo.TabletAlias, update func(*topo.Tablet) error) error { if err := tee.primary.UpdateTabletFields(tabletAlias, update); err != nil { // failed on primary, not updating secondary @@ -398,6 +415,7 @@ func (tee *Tee) UpdateTabletFields(tabletAlias topo.TabletAlias, update func(*to return nil } +// DeleteTablet is part of the topo.Server interface func (tee *Tee) DeleteTablet(alias topo.TabletAlias) error { if err := tee.primary.DeleteTablet(alias); err != nil { return err @@ -410,18 +428,7 @@ func (tee *Tee) DeleteTablet(alias topo.TabletAlias) error { return nil } -func (tee *Tee) ValidateTablet(alias topo.TabletAlias) error { - if err := tee.primary.ValidateTablet(alias); err != nil { - return err - } - - if err := tee.secondary.ValidateTablet(alias); err != nil { - // not critical enough to fail - log.Warningf("secondary.ValidateTablet(%v) failed: %v", alias, err) - } - return nil -} - +// GetTablet is part of the topo.Server interface func (tee *Tee) GetTablet(alias topo.TabletAlias) (*topo.TabletInfo, error) { ti, err := tee.readFrom.GetTablet(alias) if err != nil { @@ -443,6 +450,7 @@ func (tee *Tee) GetTablet(alias topo.TabletAlias) (*topo.TabletInfo, error) { return ti, nil } +// GetTabletsByCell is part of the topo.Server interface func (tee *Tee) GetTabletsByCell(cell string) ([]topo.TabletAlias, error) { return tee.readFrom.GetTabletsByCell(cell) } @@ -451,6 +459,7 @@ func (tee *Tee) GetTabletsByCell(cell string) ([]topo.TabletAlias, error) { // Shard replication graph management, local. // +// UpdateShardReplicationFields is part of the topo.Server interface func (tee *Tee) UpdateShardReplicationFields(cell, keyspace, shard string, update func(*topo.ShardReplication) error) error { if err := tee.primary.UpdateShardReplicationFields(cell, keyspace, shard, update); err != nil { // failed on primary, not updating secondary @@ -464,10 +473,12 @@ func (tee *Tee) UpdateShardReplicationFields(cell, keyspace, shard string, updat return nil } +// GetShardReplication is part of the topo.Server interface func (tee *Tee) GetShardReplication(cell, keyspace, shard string) (*topo.ShardReplicationInfo, error) { return tee.readFrom.GetShardReplication(cell, keyspace, shard) } +// DeleteShardReplication is part of the topo.Server interface func (tee *Tee) DeleteShardReplication(cell, keyspace, shard string) error { if err := tee.primary.DeleteShardReplication(cell, keyspace, shard); err != nil { return err @@ -484,15 +495,16 @@ func (tee *Tee) DeleteShardReplication(cell, keyspace, shard string) error { // Serving Graph management, per cell. // -func (tee *Tee) LockSrvShardForAction(cell, keyspace, shard, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +// LockSrvShardForAction is part of the topo.Server interface +func (tee *Tee) LockSrvShardForAction(ctx context.Context, cell, keyspace, shard, contents string) (string, error) { // lock lockFirst - pLockPath, err := tee.lockFirst.LockSrvShardForAction(cell, keyspace, shard, contents, timeout, interrupted) + pLockPath, err := tee.lockFirst.LockSrvShardForAction(ctx, cell, keyspace, shard, contents) if err != nil { return "", err } // lock lockSecond - sLockPath, err := tee.lockSecond.LockSrvShardForAction(cell, keyspace, shard, contents, timeout, interrupted) + sLockPath, err := tee.lockSecond.LockSrvShardForAction(ctx, cell, keyspace, shard, contents) if err != nil { if err := tee.lockFirst.UnlockSrvShardForAction(cell, keyspace, shard, pLockPath, "{}"); err != nil { log.Warningf("Failed to unlock lockFirst shard after failed lockSecond lock for %v/%v/%v", cell, keyspace, shard) @@ -507,6 +519,7 @@ func (tee *Tee) LockSrvShardForAction(cell, keyspace, shard, contents string, ti return pLockPath, nil } +// UnlockSrvShardForAction is part of the topo.Server interface func (tee *Tee) UnlockSrvShardForAction(cell, keyspace, shard, lockPath, results string) error { // get from map tee.mu.Lock() // not using defer for unlock, to minimize lock time @@ -531,10 +544,12 @@ func (tee *Tee) UnlockSrvShardForAction(cell, keyspace, shard, lockPath, results return perr } +// GetSrvTabletTypesPerShard is part of the topo.Server interface func (tee *Tee) GetSrvTabletTypesPerShard(cell, keyspace, shard string) ([]topo.TabletType, error) { return tee.readFrom.GetSrvTabletTypesPerShard(cell, keyspace, shard) } +// UpdateEndPoints is part of the topo.Server interface func (tee *Tee) UpdateEndPoints(cell, keyspace, shard string, tabletType topo.TabletType, addrs *topo.EndPoints) error { if err := tee.primary.UpdateEndPoints(cell, keyspace, shard, tabletType, addrs); err != nil { return err @@ -547,10 +562,12 @@ func (tee *Tee) UpdateEndPoints(cell, keyspace, shard string, tabletType topo.Ta return nil } +// GetEndPoints is part of the topo.Server interface func (tee *Tee) GetEndPoints(cell, keyspace, shard string, tabletType topo.TabletType) (*topo.EndPoints, error) { return tee.readFrom.GetEndPoints(cell, keyspace, shard, tabletType) } +// DeleteEndPoints is part of the topo.Server interface func (tee *Tee) DeleteEndPoints(cell, keyspace, shard string, tabletType topo.TabletType) error { err := tee.primary.DeleteEndPoints(cell, keyspace, shard, tabletType) if err != nil && err != topo.ErrNoNode { @@ -564,6 +581,7 @@ func (tee *Tee) DeleteEndPoints(cell, keyspace, shard string, tabletType topo.Ta return err } +// UpdateSrvShard is part of the topo.Server interface func (tee *Tee) UpdateSrvShard(cell, keyspace, shard string, srvShard *topo.SrvShard) error { if err := tee.primary.UpdateSrvShard(cell, keyspace, shard, srvShard); err != nil { return err @@ -576,10 +594,12 @@ func (tee *Tee) UpdateSrvShard(cell, keyspace, shard string, srvShard *topo.SrvS return nil } +// GetSrvShard is part of the topo.Server interface func (tee *Tee) GetSrvShard(cell, keyspace, shard string) (*topo.SrvShard, error) { return tee.readFrom.GetSrvShard(cell, keyspace, shard) } +// DeleteSrvShard is part of the topo.Server interface func (tee *Tee) DeleteSrvShard(cell, keyspace, shard string) error { err := tee.primary.DeleteSrvShard(cell, keyspace, shard) if err != nil && err != topo.ErrNoNode { @@ -593,6 +613,7 @@ func (tee *Tee) DeleteSrvShard(cell, keyspace, shard string) error { return err } +// UpdateSrvKeyspace is part of the topo.Server interface func (tee *Tee) UpdateSrvKeyspace(cell, keyspace string, srvKeyspace *topo.SrvKeyspace) error { if err := tee.primary.UpdateSrvKeyspace(cell, keyspace, srvKeyspace); err != nil { return err @@ -605,14 +626,17 @@ func (tee *Tee) UpdateSrvKeyspace(cell, keyspace string, srvKeyspace *topo.SrvKe return nil } +// GetSrvKeyspace is part of the topo.Server interface func (tee *Tee) GetSrvKeyspace(cell, keyspace string) (*topo.SrvKeyspace, error) { return tee.readFrom.GetSrvKeyspace(cell, keyspace) } +// GetSrvKeyspaceNames is part of the topo.Server interface func (tee *Tee) GetSrvKeyspaceNames(cell string) ([]string, error) { return tee.readFrom.GetSrvKeyspaceNames(cell) } +// UpdateTabletEndpoint is part of the topo.Server interface func (tee *Tee) UpdateTabletEndpoint(cell, keyspace, shard string, tabletType topo.TabletType, addr *topo.EndPoint) error { if err := tee.primary.UpdateTabletEndpoint(cell, keyspace, shard, tabletType, addr); err != nil { return err @@ -625,19 +649,26 @@ func (tee *Tee) UpdateTabletEndpoint(cell, keyspace, shard string, tabletType to return nil } +// WatchEndPoints is part of the topo.Server interface. +// We only watch for changes on the primary. +func (tee *Tee) WatchEndPoints(cell, keyspace, shard string, tabletType topo.TabletType) (<-chan *topo.EndPoints, chan<- struct{}, error) { + return tee.primary.WatchEndPoints(cell, keyspace, shard, tabletType) +} + // // Keyspace and Shard locks for actions, global. // -func (tee *Tee) LockKeyspaceForAction(keyspace, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +// LockKeyspaceForAction is part of the topo.Server interface +func (tee *Tee) LockKeyspaceForAction(ctx context.Context, keyspace, contents string) (string, error) { // lock lockFirst - pLockPath, err := tee.lockFirst.LockKeyspaceForAction(keyspace, contents, timeout, interrupted) + pLockPath, err := tee.lockFirst.LockKeyspaceForAction(ctx, keyspace, contents) if err != nil { return "", err } // lock lockSecond - sLockPath, err := tee.lockSecond.LockKeyspaceForAction(keyspace, contents, timeout, interrupted) + sLockPath, err := tee.lockSecond.LockKeyspaceForAction(ctx, keyspace, contents) if err != nil { if err := tee.lockFirst.UnlockKeyspaceForAction(keyspace, pLockPath, "{}"); err != nil { log.Warningf("Failed to unlock lockFirst keyspace after failed lockSecond lock for %v", keyspace) @@ -652,6 +683,7 @@ func (tee *Tee) LockKeyspaceForAction(keyspace, contents string, timeout time.Du return pLockPath, nil } +// UnlockKeyspaceForAction is part of the topo.Server interface func (tee *Tee) UnlockKeyspaceForAction(keyspace, lockPath, results string) error { // get from map tee.mu.Lock() // not using defer for unlock, to minimize lock time @@ -676,15 +708,16 @@ func (tee *Tee) UnlockKeyspaceForAction(keyspace, lockPath, results string) erro return perr } -func (tee *Tee) LockShardForAction(keyspace, shard, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +// LockShardForAction is part of the topo.Server interface +func (tee *Tee) LockShardForAction(ctx context.Context, keyspace, shard, contents string) (string, error) { // lock lockFirst - pLockPath, err := tee.lockFirst.LockShardForAction(keyspace, shard, contents, timeout, interrupted) + pLockPath, err := tee.lockFirst.LockShardForAction(ctx, keyspace, shard, contents) if err != nil { return "", err } // lock lockSecond - sLockPath, err := tee.lockSecond.LockShardForAction(keyspace, shard, contents, timeout, interrupted) + sLockPath, err := tee.lockSecond.LockShardForAction(ctx, keyspace, shard, contents) if err != nil { if err := tee.lockFirst.UnlockShardForAction(keyspace, shard, pLockPath, "{}"); err != nil { log.Warningf("Failed to unlock lockFirst shard after failed lockSecond lock for %v/%v", keyspace, shard) @@ -699,6 +732,7 @@ func (tee *Tee) LockShardForAction(keyspace, shard, contents string, timeout tim return pLockPath, nil } +// UnlockShardForAction is part of the topo.Server interface func (tee *Tee) UnlockShardForAction(keyspace, shard, lockPath, results string) error { // get from map tee.mu.Lock() // not using defer for unlock, to minimize lock time @@ -722,36 +756,3 @@ func (tee *Tee) UnlockShardForAction(keyspace, shard, lockPath, results string) } return perr } - -// -// Supporting the local agent process, local cell. -// - -func (tee *Tee) CreateTabletPidNode(tabletAlias topo.TabletAlias, contents string, done chan struct{}) error { - // if the primary fails, no need to go on - if err := tee.primary.CreateTabletPidNode(tabletAlias, contents, done); err != nil { - return err - } - - if err := tee.secondary.CreateTabletPidNode(tabletAlias, contents, done); err != nil { - log.Warningf("secondary.CreateTabletPidNode(%v) failed: %v", tabletAlias, err) - } - return nil -} - -func (tee *Tee) ValidateTabletPidNode(tabletAlias topo.TabletAlias) error { - // if the primary fails, no need to go on - if err := tee.primary.ValidateTabletPidNode(tabletAlias); err != nil { - return err - } - - if err := tee.secondary.ValidateTabletPidNode(tabletAlias); err != nil { - log.Warningf("secondary.ValidateTabletPidNode(%v) failed: %v", tabletAlias, err) - } - return nil -} - -func (tee *Tee) GetSubprocessFlags() []string { - p := tee.primary.GetSubprocessFlags() - return append(p, tee.secondary.GetSubprocessFlags()...) -} diff --git a/go/vt/topo/helpers/tee_topo_test.go b/go/vt/topo/helpers/tee_topo_test.go index 173fc7f4ff6..d21a99f2bb7 100644 --- a/go/vt/topo/helpers/tee_topo_test.go +++ b/go/vt/topo/helpers/tee_topo_test.go @@ -7,8 +7,9 @@ package helpers import ( "fmt" "testing" + "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topo/test" @@ -54,7 +55,7 @@ func TestKeyspace(t *testing.T) { func TestShard(t *testing.T) { ts := newFakeTeeServer(t) - test.CheckShard(t, ts) + test.CheckShard(context.Background(), t, ts) } func TestTablet(t *testing.T) { @@ -64,7 +65,13 @@ func TestTablet(t *testing.T) { func TestServingGraph(t *testing.T) { ts := newFakeTeeServer(t) - test.CheckServingGraph(t, ts) + test.CheckServingGraph(context.Background(), t, ts) +} + +func TestWatchEndPoints(t *testing.T) { + zktopo.WatchSleepDuration = 2 * time.Millisecond + ts := newFakeTeeServer(t) + test.CheckWatchEndPoints(context.Background(), t, ts) } func TestShardReplication(t *testing.T) { @@ -94,8 +101,3 @@ func TestSrvShardLock(t *testing.T) { ts := newFakeTeeServer(t) test.CheckSrvShardLock(t, ts) } - -func TestPid(t *testing.T) { - ts := newFakeTeeServer(t) - test.CheckPid(t, ts) -} diff --git a/go/vt/topo/keyspace.go b/go/vt/topo/keyspace.go index 43fe6b7f198..b029f2a1e57 100644 --- a/go/vt/topo/keyspace.go +++ b/go/vt/topo/keyspace.go @@ -19,7 +19,7 @@ import ( // KeyspaceServedFrom is a per-cell record to redirect traffic to another // keyspace. Used for vertical splits. type KeyspaceServedFrom struct { - // who is targetted + // who is targeted Cells []string // nil means all cells // where to redirect @@ -47,7 +47,7 @@ type Keyspace struct { // cover 1/Nth of the entire space or more. // It is usually the number of shards in the system. If a keyspace // is being resharded from M to P shards, it should be max(M, P). - // That way we can guarantee a query that is targetted to 1/N of the + // That way we can guarantee a query that is targeted to 1/N of the // keyspace will land on just one shard. SplitShardCount int32 } diff --git a/go/vt/topo/naming.go b/go/vt/topo/naming.go index 03f590ade4e..5f2322aef66 100644 --- a/go/vt/topo/naming.go +++ b/go/vt/topo/naming.go @@ -29,7 +29,7 @@ import ( const ( // DefaultPortName is the port named used by SrvEntries // if "" is given as the named port. - DefaultPortName = "_vtocc" + DefaultPortName = "vt" ) // EndPoint describes a tablet (maybe composed of multiple processes) diff --git a/go/vt/topo/replication.go b/go/vt/topo/replication.go index 85af309ca0a..49859bf403d 100644 --- a/go/vt/topo/replication.go +++ b/go/vt/topo/replication.go @@ -5,26 +5,24 @@ package topo import ( - "code.google.com/p/go.net/context" log "github.com/golang/glog" + "golang.org/x/net/context" "github.com/youtube/vitess/go/trace" "github.com/youtube/vitess/go/vt/logutil" ) -// ReplicationLink describes a MySQL replication relationship. -// For now, we only insert ReplicationLink for slave tablets. -// We want to add records for master tablets as well, with a Parent.IsZero(). +// ReplicationLink describes a tablet that is linked in a shard. +// We will add a record for all tablets in a shard. type ReplicationLink struct { TabletAlias TabletAlias - Parent TabletAlias } -// ShardReplication describes the MySQL replication relationships +// ShardReplication describes all the tablets for a shard // whithin a cell. type ShardReplication struct { // Note there can be only one ReplicationLink in this array - // for a given Slave (each Slave can only have one parent) + // for a given Tablet ReplicationLinks []ReplicationLink } @@ -74,7 +72,7 @@ func (sri *ShardReplicationInfo) Shard() string { // UpdateShardReplicationRecord is a low level function to add / update an // entry to the ShardReplication object. -func UpdateShardReplicationRecord(ctx context.Context, ts Server, keyspace, shard string, tabletAlias, parent TabletAlias) error { +func UpdateShardReplicationRecord(ctx context.Context, ts Server, keyspace, shard string, tabletAlias TabletAlias) error { span := trace.NewSpanFromContext(ctx) span.StartClient("TopoServer.UpdateShardReplicationFields") span.Annotate("keyspace", keyspace) @@ -93,13 +91,11 @@ func UpdateShardReplicationRecord(ctx context.Context, ts Server, keyspace, shar continue } found = true - // update the master - link.Parent = parent } links = append(links, link) } if !found { - links = append(links, ReplicationLink{TabletAlias: tabletAlias, Parent: parent}) + links = append(links, ReplicationLink{TabletAlias: tabletAlias}) } sr.ReplicationLinks = links return nil diff --git a/go/vt/topo/server.go b/go/vt/topo/server.go index 2091cb80281..df3a882de3d 100644 --- a/go/vt/topo/server.go +++ b/go/vt/topo/server.go @@ -8,9 +8,9 @@ import ( "errors" "flag" "fmt" - "time" log "github.com/golang/glog" + "golang.org/x/net/context" ) var ( @@ -155,9 +155,6 @@ type Server interface { // Can return ErrNoNode if the tablet doesn't exist. DeleteTablet(alias TabletAlias) error - // ValidateTablet performs routine checks on the tablet. - ValidateTablet(alias TabletAlias) error - // GetTablet returns the tablet data (includes the current version). // Can return ErrNoNode if the tablet doesn't exist. GetTablet(alias TabletAlias) (*TabletInfo, error) @@ -191,11 +188,11 @@ type Server interface { // LockSrvShardForAction locks the serving shard in order to // perform the action described by contents. It will wait for - // the lock for at most duration. The wait can be interrupted - // if the interrupted channel is closed. It returns the lock - // path. + // the lock until at most ctx.Done(). The wait can be interrupted + // by cancelling the context. It returns the lock path. + // // Can return ErrTimeout or ErrInterrupted. - LockSrvShardForAction(cell, keyspace, shard, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) + LockSrvShardForAction(ctx context.Context, cell, keyspace, shard, contents string) (string, error) // UnlockSrvShardForAction unlocks a serving shard. UnlockSrvShardForAction(cell, keyspace, shard, lockPath, results string) error @@ -219,6 +216,21 @@ type Server interface { // Can return ErrNoNode. DeleteEndPoints(cell, keyspace, shard string, tabletType TabletType) error + // WatchEndPoints returns a channel that receives notifications + // every time EndPoints for the given type changes. + // It should receive a notification with the initial value fairly + // quickly after this is set. A value of nil means the Endpoints + // object doesn't exist or is empty. To stop watching this + // EndPoints object, close the stopWatching channel. + // If the underlying topo.Server encounters an error watching the node, + // it should retry on a regular basis until it can succeed. + // The initial error returned by this method is meant to catch + // the obvious bad cases (invalid cell, invalid tabletType, ...) + // that are never going to work. Mutiple notifications with the + // same contents may be sent (for instance when the serving graph + // is rebuilt, but the content hasn't changed). + WatchEndPoints(cell, keyspace, shard string, tabletType TabletType) (notifications <-chan *EndPoints, stopWatching chan<- struct{}, err error) + // UpdateSrvShard updates the serving records for a cell, // keyspace, shard. UpdateSrvShard(cell, keyspace, shard string, srvShard *SrvShard) error @@ -254,45 +266,36 @@ type Server interface { // LockKeyspaceForAction locks the keyspace in order to // perform the action described by contents. It will wait for - // the lock for at most duration. The wait can be interrupted - // if the interrupted channel is closed. It returns the lock - // path. + // the lock until at most ctx.Done(). The wait can be interrupted + // by cancelling the context. It returns the lock path. + // // Can return ErrTimeout or ErrInterrupted - LockKeyspaceForAction(keyspace, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) + LockKeyspaceForAction(ctx context.Context, keyspace, contents string) (string, error) // UnlockKeyspaceForAction unlocks a keyspace. UnlockKeyspaceForAction(keyspace, lockPath, results string) error // LockShardForAction locks the shard in order to // perform the action described by contents. It will wait for - // the lock for at most duration. The wait can be interrupted - // if the interrupted channel is closed. It returns the lock - // path. + // the lock until at most ctx.Done(). The wait can be interrupted + // by cancelling the context. It returns the lock path. + // // Can return ErrTimeout or ErrInterrupted - LockShardForAction(keyspace, shard, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) + LockShardForAction(ctx context.Context, keyspace, shard, contents string) (string, error) // UnlockShardForAction unlocks a shard. UnlockShardForAction(keyspace, shard, lockPath, results string) error +} - // - // Supporting the local agent process, local cell. - // - - // CreateTabletPidNode will keep a PID node up to date with - // this tablet's current PID, until 'done' is closed. - CreateTabletPidNode(tabletAlias TabletAlias, contents string, done chan struct{}) error - - // ValidateTabletPidNode makes sure a PID file exists for the tablet - ValidateTabletPidNode(tabletAlias TabletAlias) error - - // GetSubprocessFlags returns the flags required to run a - // subprocess that uses the same Server parameters as - // this process. - GetSubprocessFlags() []string +// Schemafier is a temporary interface for supporting vschema +// reads and writes. It will eventually be merged into Server. +type Schemafier interface { + SaveVSchema(string) error + GetVSchema() (string, error) } // Registry for Server implementations. -var serverImpls map[string]Server = make(map[string]Server) +var serverImpls = make(map[string]Server) // Which implementation to use var topoImplementation = flag.String("topo_implementation", "zookeeper", "the topology implementation to use") @@ -307,7 +310,7 @@ func RegisterServer(name string, ts Server) { serverImpls[name] = ts } -// Returns a specific Server by name, or nil. +// GetServerByName returns a specific Server by name, or nil. func GetServerByName(name string) Server { return serverImpls[name] } @@ -333,19 +336,10 @@ func GetServer() Server { return result } -// Close all registered Server. +// CloseServers closes all registered Server. func CloseServers() { for name, ts := range serverImpls { log.V(6).Infof("Closing topo.Server: %v", name) ts.Close() } } - -// GetSubprocessFlags returns all the flags required to launch a subprocess -// with the exact same topology server as the current process. -func GetSubprocessFlags() []string { - result := []string{ - "-topo_implementation", *topoImplementation, - } - return append(result, GetServer().GetSubprocessFlags()...) -} diff --git a/go/vt/topo/serving_graph.go b/go/vt/topo/serving_graph.go new file mode 100644 index 00000000000..203731439ab --- /dev/null +++ b/go/vt/topo/serving_graph.go @@ -0,0 +1,25 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package topo + +import ( + "golang.org/x/net/context" + + "github.com/youtube/vitess/go/trace" +) + +// UpdateEndPoints is a high level wrapper for TopoServer.UpdateEndPoints. +// It generates trace spans. +func UpdateEndPoints(ctx context.Context, ts Server, cell, keyspace, shard string, tabletType TabletType, addrs *EndPoints) error { + span := trace.NewSpanFromContext(ctx) + span.StartClient("TopoServer.UpdateEndPoints") + span.Annotate("cell", cell) + span.Annotate("keyspace", keyspace) + span.Annotate("shard", shard) + span.Annotate("tablet_type", string(tabletType)) + defer span.Finish() + + return ts.UpdateEndPoints(cell, keyspace, shard, tabletType, addrs) +} diff --git a/go/vt/topo/shard.go b/go/vt/topo/shard.go index ff846c61d9d..b2938606a8c 100644 --- a/go/vt/topo/shard.go +++ b/go/vt/topo/shard.go @@ -11,7 +11,7 @@ import ( "strings" "sync" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" log "github.com/golang/glog" @@ -69,8 +69,22 @@ func removeCells(cells, toRemove, fullList []string) ([]string, bool) { return leftoverCells, false } +// ParseKeyspaceShardString parse a "keyspace/shard" string and extract +// both keyspace and shard. It also returns empty keyspace and shard if +// input param looks like a old zk path +func ParseKeyspaceShardString(param string) (string, string, error) { + if param[0] == '/' { + return "", "", fmt.Errorf("Invalid keyspace/shard: %v, Note: old style zk path is no longer supported, please use a keyspace/shard instead", param) + } + keySpaceShard := strings.Split(param, "/") + if len(keySpaceShard) != 2 { + return "", "", fmt.Errorf("Invalid shard path: %v", param) + } + return keySpaceShard[0], keySpaceShard[1], nil +} + // SourceShard represents a data source for filtered replication -// accross shards. When this is used in a destination shard, the master +// across shards. When this is used in a destination shard, the master // of that shard will run filtered replication. type SourceShard struct { // Uid is the unique ID for this SourceShard object. @@ -129,7 +143,7 @@ type ShardServedType struct { Cells []string // nil means all cells } -// A pure data struct for information stored in topology server. This +// Shard is a pure data struct for information stored in topology server. This // node is used to present a controlled view of the shard, unaware of // every management action. It also contains configuration data for a // shard. @@ -236,6 +250,18 @@ func NewShardInfo(keyspace, shard string, value *Shard, version int64) *ShardInf } } +// GetShard is a high level function to read shard data. +// It generates trace spans. +func GetShard(ctx context.Context, ts Server, keyspace, shard string) (*ShardInfo, error) { + span := trace.NewSpanFromContext(ctx) + span.StartClient("TopoServer.GetShard") + span.Annotate("keyspace", keyspace) + span.Annotate("shard", shard) + defer span.Finish() + + return ts.GetShard(keyspace, shard) +} + // UpdateShard updates the shard data, with the right version func UpdateShard(ctx context.Context, ts Server, si *ShardInfo) error { span := trace.NewSpanFromContext(ctx) @@ -256,6 +282,25 @@ func UpdateShard(ctx context.Context, ts Server, si *ShardInfo) error { return err } +// UpdateShardFields is a high level helper to read a shard record, call an +// update function on it, and then write it back. If the write fails due to +// a version mismatch, it will re-read the record and retry the update. +// If the update succeeds, it returns the updated ShardInfo. +func UpdateShardFields(ctx context.Context, ts Server, keyspace, shard string, update func(*Shard) error) (*ShardInfo, error) { + for { + si, err := GetShard(ctx, ts, keyspace, shard) + if err != nil { + return nil, err + } + if err = update(si.Shard); err != nil { + return nil, err + } + if err = UpdateShard(ctx, ts, si); err != ErrBadVersion { + return si, err + } + } +} + // CreateShard creates a new shard and tries to fill in the right information. func CreateShard(ts Server, keyspace, shard string) error { @@ -281,7 +326,7 @@ func CreateShard(ts Server, keyspace, shard string) error { } for _, si := range sis { if key.KeyRangesIntersect(si.KeyRange, keyRange) { - for t, _ := range si.ServedTypesMap { + for t := range si.ServedTypesMap { delete(s.ServedTypesMap, t) } } @@ -499,17 +544,25 @@ func InCellList(cell string, cells []string) bool { // tablet aliases in the given shard. // It can return ErrPartialResult if some cells were not fetched, // in which case the result only contains the cells that were fetched. -func FindAllTabletAliasesInShard(ts Server, keyspace, shard string) ([]TabletAlias, error) { - return FindAllTabletAliasesInShardByCell(ts, keyspace, shard, nil) +func FindAllTabletAliasesInShard(ctx context.Context, ts Server, keyspace, shard string) ([]TabletAlias, error) { + return FindAllTabletAliasesInShardByCell(ctx, ts, keyspace, shard, nil) } -// FindAllTabletAliasesInShard uses the replication graph to find all the +// FindAllTabletAliasesInShardByCell uses the replication graph to find all the // tablet aliases in the given shard. // It can return ErrPartialResult if some cells were not fetched, // in which case the result only contains the cells that were fetched. -func FindAllTabletAliasesInShardByCell(ts Server, keyspace, shard string, cells []string) ([]TabletAlias, error) { +func FindAllTabletAliasesInShardByCell(ctx context.Context, ts Server, keyspace, shard string, cells []string) ([]TabletAlias, error) { + span := trace.NewSpanFromContext(ctx) + span.StartLocal("topo.FindAllTabletAliasesInShardbyCell") + span.Annotate("keyspace", keyspace) + span.Annotate("shard", shard) + span.Annotate("num_cells", len(cells)) + defer span.Finish() + ctx = trace.NewContext(ctx, span) + // read the shard information to find the cells - si, err := ts.GetShard(keyspace, shard) + si, err := GetShard(ctx, ts, keyspace, shard) if err != nil { return nil, err } @@ -541,9 +594,6 @@ func FindAllTabletAliasesInShardByCell(ts Server, keyspace, shard string, cells mutex.Lock() for _, rl := range sri.ReplicationLinks { resultAsMap[rl.TabletAlias] = true - if !rl.Parent.IsZero() && InCellList(rl.Parent.Cell, cells) { - resultAsMap[rl.Parent] = true - } } mutex.Unlock() }(cell) @@ -565,24 +615,24 @@ func FindAllTabletAliasesInShardByCell(ts Server, keyspace, shard string, cells // GetTabletMapForShard returns the tablets for a shard. It can return // ErrPartialResult if it couldn't read all the cells, or all // the individual tablets, in which case the map is valid, but partial. -func GetTabletMapForShard(ts Server, keyspace, shard string) (map[TabletAlias]*TabletInfo, error) { - return GetTabletMapForShardByCell(ts, keyspace, shard, nil) +func GetTabletMapForShard(ctx context.Context, ts Server, keyspace, shard string) (map[TabletAlias]*TabletInfo, error) { + return GetTabletMapForShardByCell(ctx, ts, keyspace, shard, nil) } // GetTabletMapForShardByCell returns the tablets for a shard. It can return // ErrPartialResult if it couldn't read all the cells, or all // the individual tablets, in which case the map is valid, but partial. -func GetTabletMapForShardByCell(ts Server, keyspace, shard string, cells []string) (map[TabletAlias]*TabletInfo, error) { +func GetTabletMapForShardByCell(ctx context.Context, ts Server, keyspace, shard string, cells []string) (map[TabletAlias]*TabletInfo, error) { // if we get a partial result, we keep going. It most likely means // a cell is out of commission. - aliases, err := FindAllTabletAliasesInShardByCell(ts, keyspace, shard, cells) + aliases, err := FindAllTabletAliasesInShardByCell(ctx, ts, keyspace, shard, cells) if err != nil && err != ErrPartialResult { return nil, err } // get the tablets for the cells we were able to reach, forward // ErrPartialResult from FindAllTabletAliasesInShard - result, gerr := GetTabletMap(ts, aliases) + result, gerr := GetTabletMap(ctx, ts, aliases) if gerr == nil && err != nil { gerr = err } diff --git a/go/vt/topo/shard_test.go b/go/vt/topo/shard_test.go index 012d0e55f95..d3da838c743 100644 --- a/go/vt/topo/shard_test.go +++ b/go/vt/topo/shard_test.go @@ -59,6 +59,29 @@ func TestRemoveCells(t *testing.T) { } } +func TestParseKeyspaceShardString(t *testing.T) { + zkPath := "/zk/tablet" + keyspace := "key01" + shard := "shard0" + tabletAlias := keyspace + "/" + shard + + if _, _, err := ParseKeyspaceShardString(zkPath); err == nil { + t.Fatalf("zk path: %s should cause error.", zkPath) + } + k, s, err := ParseKeyspaceShardString(tabletAlias) + if err != nil { + t.Fatalf("Failed to parse valid tablet alias: %s", tabletAlias) + } + if keyspace != k { + t.Fatalf("keyspace parsed from tablet alias %s is %s, but expect %s", + tabletAlias, k, keyspace) + } + if shard != s { + t.Fatalf("shard parsed from tablet alias %s is %s, but expect %s", + tabletAlias, s, shard) + } +} + func TestUpdateSourceBlacklistedTables(t *testing.T) { si := NewShardInfo("ks", "sh", &Shard{ Cells: []string{"first", "second", "third"}, diff --git a/go/vt/topo/srvkeyspace_bson.go b/go/vt/topo/srvkeyspace_bson.go index 79388584635..ab5a383b49b 100644 --- a/go/vt/topo/srvkeyspace_bson.go +++ b/go/vt/topo/srvkeyspace_bson.go @@ -33,21 +33,12 @@ func (srvKeyspace *SrvKeyspace) MarshalBson(buf *bytes2.ChunkedWriter, key strin } lenWriter.Close() } - // []SrvShard - { - bson.EncodePrefix(buf, bson.Array, "Shards") - lenWriter := bson.NewLenWriter(buf) - for _i, _v2 := range srvKeyspace.Shards { - _v2.MarshalBson(buf, bson.Itoa(_i)) - } - lenWriter.Close() - } // []TabletType { bson.EncodePrefix(buf, bson.Array, "TabletTypes") lenWriter := bson.NewLenWriter(buf) - for _i, _v3 := range srvKeyspace.TabletTypes { - _v3.MarshalBson(buf, bson.Itoa(_i)) + for _i, _v2 := range srvKeyspace.TabletTypes { + _v2.MarshalBson(buf, bson.Itoa(_i)) } lenWriter.Close() } @@ -57,8 +48,8 @@ func (srvKeyspace *SrvKeyspace) MarshalBson(buf *bytes2.ChunkedWriter, key strin { bson.EncodePrefix(buf, bson.Object, "ServedFrom") lenWriter := bson.NewLenWriter(buf) - for _k, _v4 := range srvKeyspace.ServedFrom { - bson.EncodeString(buf, string(_k), _v4) + for _k, _v3 := range srvKeyspace.ServedFrom { + bson.EncodeString(buf, string(_k), _v3) } lenWriter.Close() } @@ -100,21 +91,6 @@ func (srvKeyspace *SrvKeyspace) UnmarshalBson(buf *bytes.Buffer, kind byte) { srvKeyspace.Partitions[_k] = _v1 } } - case "Shards": - // []SrvShard - if kind != bson.Null { - if kind != bson.Array { - panic(bson.NewBsonError("unexpected kind %v for srvKeyspace.Shards", kind)) - } - bson.Next(buf, 4) - srvKeyspace.Shards = make([]SrvShard, 0, 8) - for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { - bson.SkipIndex(buf) - var _v2 SrvShard - _v2.UnmarshalBson(buf, kind) - srvKeyspace.Shards = append(srvKeyspace.Shards, _v2) - } - } case "TabletTypes": // []TabletType if kind != bson.Null { @@ -125,9 +101,9 @@ func (srvKeyspace *SrvKeyspace) UnmarshalBson(buf *bytes.Buffer, kind byte) { srvKeyspace.TabletTypes = make([]TabletType, 0, 8) for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { bson.SkipIndex(buf) - var _v3 TabletType - _v3.UnmarshalBson(buf, kind) - srvKeyspace.TabletTypes = append(srvKeyspace.TabletTypes, _v3) + var _v2 TabletType + _v2.UnmarshalBson(buf, kind) + srvKeyspace.TabletTypes = append(srvKeyspace.TabletTypes, _v2) } } case "ShardingColumnName": @@ -144,9 +120,9 @@ func (srvKeyspace *SrvKeyspace) UnmarshalBson(buf *bytes.Buffer, kind byte) { srvKeyspace.ServedFrom = make(map[TabletType]string) for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { _k := TabletType(bson.ReadCString(buf)) - var _v4 string - _v4 = bson.DecodeString(buf, kind) - srvKeyspace.ServedFrom[_k] = _v4 + var _v3 string + _v3 = bson.DecodeString(buf, kind) + srvKeyspace.ServedFrom[_k] = _v3 } } case "SplitShardCount": diff --git a/go/vt/topo/srvshard.go b/go/vt/topo/srvshard.go index dff2a6f6a2f..3df69cb7bac 100644 --- a/go/vt/topo/srvshard.go +++ b/go/vt/topo/srvshard.go @@ -18,7 +18,7 @@ const SHARD_ZERO = "0" // SrvShard contains a roll-up of the shard in the local namespace. // In zk, it is under /zk//vt/ns// type SrvShard struct { - // Copied / infered from Shard + // Copied / inferred from Shard Name string KeyRange key.KeyRange ServedTypes []TabletType @@ -34,6 +34,8 @@ type SrvShard struct { version int64 } +//go:generate bsongen -file $GOFILE -type SrvShard -o srvshard_bson.go + // SrvShardArray is used for sorting SrvShard arrays type SrvShardArray []SrvShard @@ -77,6 +79,8 @@ type KeyspacePartition struct { Shards []SrvShard } +//go:generate bsongen -file $GOFILE -type KeyspacePartition -o keyspace_partition_bson.go + // HasShard returns true if this KeyspacePartition has the shard with // the given name in it. func (kp *KeyspacePartition) HasShard(name string) bool { @@ -96,10 +100,6 @@ type SrvKeyspace struct { // Shards to use per type, only contains complete partitions. Partitions map[TabletType]*KeyspacePartition - // This list will be deprecated as soon as Partitions is used. - // List of non-overlapping shards sorted by range. - Shards []SrvShard - // List of available tablet types for this keyspace in this cell. // May not have a server for every shard, but we have some. TabletTypes []TabletType @@ -114,6 +114,8 @@ type SrvKeyspace struct { version int64 } +//go:generate bsongen -file $GOFILE -type SrvKeyspace -o srvkeyspace_bson.go + // NewSrvKeyspace returns an empty SrvKeyspace with the given version. func NewSrvKeyspace(version int64) *SrvKeyspace { return &SrvKeyspace{ diff --git a/go/vt/topo/srvshard_test.go b/go/vt/topo/srvshard_test.go index 8e28ca466e4..8d75e928cfc 100644 --- a/go/vt/topo/srvshard_test.go +++ b/go/vt/topo/srvshard_test.go @@ -14,7 +14,6 @@ import ( type reflectSrvKeyspace struct { Partitions map[string]*KeyspacePartition - Shards []SrvShard TabletTypes []TabletType ShardingColumnName string ShardingColumnType key.KeyspaceIdType @@ -26,7 +25,6 @@ type reflectSrvKeyspace struct { type extraSrvKeyspace struct { Extra int Partitions map[TabletType]*KeyspacePartition - Shards []SrvShard TabletTypes []TabletType ShardingColumnName string ShardingColumnType key.KeyspaceIdType @@ -73,7 +71,6 @@ func TestSrvKeySpace(t *testing.T) { }, }, }, - Shards: []SrvShard{}, TabletTypes: []TabletType{TYPE_MASTER}, ShardingColumnName: "video_id", ShardingColumnType: key.KIT_UINT64, diff --git a/go/vt/topo/tablet.go b/go/vt/topo/tablet.go index 2270eab2ace..2da862c6b30 100644 --- a/go/vt/topo/tablet.go +++ b/go/vt/topo/tablet.go @@ -12,10 +12,11 @@ import ( "strings" "sync" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/jscfg" + "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/trace" "github.com/youtube/vitess/go/vt/key" ) @@ -29,6 +30,14 @@ const ( // Default name for databases is the prefix plus keyspace vtDbPrefix = "vt_" + + // ReplicationLag is the key in the health map to indicate high + // replication lag + ReplicationLag = "replication_lag" + + // ReplicationLagHigh is the value in the health map to indicate high + // replication lag + ReplicationLagHigh = "high" ) // TabletAlias is the minimum required information to locate a tablet. @@ -52,9 +61,9 @@ func (ta TabletAlias) String() string { return fmtAlias(ta.Cell, ta.Uid) } -// TabletUidStr returns a string version of the uid -func (ta TabletAlias) TabletUidStr() string { - return tabletUidStr(ta.Uid) +// TabletUIDStr returns a string version of the uid +func (ta TabletAlias) TabletUIDStr() string { + return tabletUIDStr(ta.Uid) } // ParseTabletAliasString returns a TabletAlias for the input string, @@ -66,7 +75,7 @@ func ParseTabletAliasString(aliasStr string) (result TabletAlias, err error) { return } result.Cell = nameParts[0] - result.Uid, err = ParseUid(nameParts[1]) + result.Uid, err = ParseUID(nameParts[1]) if err != nil { err = fmt.Errorf("invalid tablet uid %v: %v", aliasStr, err) return @@ -74,12 +83,12 @@ func ParseTabletAliasString(aliasStr string) (result TabletAlias, err error) { return } -func tabletUidStr(uid uint32) string { +func tabletUIDStr(uid uint32) string { return fmt.Sprintf("%010d", uid) } -// ParseUid parses just the uid (a number) -func ParseUid(value string) (uint32, error) { +// ParseUID parses just the uid (a number) +func ParseUID(value string) (uint32, error) { uid, err := strconv.ParseUint(value, 10, 32) if err != nil { return 0, fmt.Errorf("bad tablet uid %v", err) @@ -88,7 +97,7 @@ func ParseUid(value string) (uint32, error) { } func fmtAlias(cell string, uid uint32) string { - return fmt.Sprintf("%v-%v", cell, tabletUidStr(uid)) + return fmt.Sprintf("%v-%v", cell, tabletUIDStr(uid)) } // TabletAliasList is used mainly for sorting @@ -120,6 +129,8 @@ func (tal TabletAliasList) Swap(i, j int) { // - the uptime expectancy type TabletType string +//go:generate bsongen -file $GOFILE -type TabletType -o tablet_type_bson.go + const ( // idle - no keyspace, shard or type assigned TYPE_IDLE = TabletType("idle") @@ -177,6 +188,7 @@ const ( TYPE_SCRAP = TabletType("scrap") ) +// AllTabletTypes lists all the possible tablet types var AllTabletTypes = []TabletType{TYPE_IDLE, TYPE_MASTER, TYPE_REPLICA, @@ -194,6 +206,7 @@ var AllTabletTypes = []TabletType{TYPE_IDLE, TYPE_SCRAP, } +// SlaveTabletTypes list all the types that are replication slaves var SlaveTabletTypes = []TabletType{ TYPE_REPLICA, TYPE_RDONLY, @@ -257,7 +270,7 @@ func IsTrivialTypeChange(oldTabletType, newTabletType TabletType) bool { // IsValidTypeChange returns if we should we allow this transition at // all. Most transitions are allowed, but some don't make sense under -// any circumstances. If a transistion could be forced, don't disallow +// any circumstances. If a transition could be forced, don't disallow // it here. func IsValidTypeChange(oldTabletType, newTabletType TabletType) bool { switch oldTabletType { @@ -329,25 +342,21 @@ func IsSlaveType(tt TabletType) bool { type TabletState string const ( - // The normal state for a master + // STATE_READ_WRITE is the normal state for a master STATE_READ_WRITE = TabletState("ReadWrite") - // The normal state for a slave, or temporarily a master. Not - // to be confused with type, which implies a workload. + // STATE_READ_ONLY is the normal state for a slave, or temporarily a master. + // Not to be confused with type, which implies a workload. STATE_READ_ONLY = TabletState("ReadOnly") ) // Tablet is a pure data struct for information serialized into json // and stored into topo.Server type Tablet struct { - // Parent is the globally unique alias for our replication - // parent - IsZero() if this tablet has no parent - Parent TabletAlias - // What is this tablet? Alias TabletAlias - // Locaiton of the tablet + // Location of the tablet Hostname string IPAddr string @@ -396,15 +405,17 @@ func (tablet *Tablet) EndPoint() (*EndPoint, error) { return nil, err } - // TODO(szopa): Rename _vtocc to vt. - entry.NamedPortMap = map[string]int{ - "_vtocc": tablet.Portmap["vt"], + entry.NamedPortMap = map[string]int{} + + if port, ok := tablet.Portmap["vt"]; ok { + entry.NamedPortMap["_vtocc"] = port + entry.NamedPortMap["vt"] = port } if port, ok := tablet.Portmap["mysql"]; ok { - entry.NamedPortMap["_mysql"] = port + entry.NamedPortMap["mysql"] = port } if port, ok := tablet.Portmap["vts"]; ok { - entry.NamedPortMap["_vts"] = port + entry.NamedPortMap["vts"] = port } if len(tablet.Health) > 0 { @@ -416,19 +427,19 @@ func (tablet *Tablet) EndPoint() (*EndPoint, error) { return entry, nil } -// Addr returns hostname:vt port +// Addr returns hostname:vt port. func (tablet *Tablet) Addr() string { - return fmt.Sprintf("%v:%v", tablet.Hostname, tablet.Portmap["vt"]) + return netutil.JoinHostPort(tablet.Hostname, tablet.Portmap["vt"]) } -// MysqlAddr returns hostname:mysql port +// MysqlAddr returns hostname:mysql port. func (tablet *Tablet) MysqlAddr() string { - return fmt.Sprintf("%v:%v", tablet.Hostname, tablet.Portmap["mysql"]) + return netutil.JoinHostPort(tablet.Hostname, tablet.Portmap["mysql"]) } -// MysqlIpAddr returns ip:mysql port -func (tablet *Tablet) MysqlIpAddr() string { - return fmt.Sprintf("%v:%v", tablet.IPAddr, tablet.Portmap["mysql"]) +// MysqlIPAddr returns ip:mysql port. +func (tablet *Tablet) MysqlIPAddr() string { + return netutil.JoinHostPort(tablet.IPAddr, tablet.Portmap["mysql"]) } // DbName is usually implied by keyspace. Having the shard information in the @@ -443,33 +454,40 @@ func (tablet *Tablet) DbName() string { return vtDbPrefix + tablet.Keyspace } +// IsInServingGraph returns if this tablet is in the serving graph func (tablet *Tablet) IsInServingGraph() bool { return IsInServingGraph(tablet.Type) } +// IsRunningQueryService returns if this tablet should be running +// the query service. func (tablet *Tablet) IsRunningQueryService() bool { return IsRunningQueryService(tablet.Type) } +// IsInReplicationGraph returns if this tablet is in the replication graph. func (tablet *Tablet) IsInReplicationGraph() bool { return IsInReplicationGraph(tablet.Type) } +// IsSlaveType returns if this tablet's type is a slave func (tablet *Tablet) IsSlaveType() bool { return IsSlaveType(tablet.Type) } -// Was this tablet ever assigned data? A "scrap" node will show up as assigned -// even though its data cannot be used for serving. +// IsAssigned returns if this tablet ever assigned data? A "scrap" node will +// show up as assigned even though its data cannot be used for serving. func (tablet *Tablet) IsAssigned() bool { return tablet.Keyspace != "" && tablet.Shard != "" } +// String returns a string describing the tablet. func (tablet *Tablet) String() string { return fmt.Sprintf("Tablet{%v}", tablet.Alias) } -func (tablet *Tablet) Json() string { +// JSON returns a json verison of the tablet. +func (tablet *Tablet) JSON() string { return jscfg.ToJson(tablet) } @@ -491,13 +509,7 @@ func (tablet *Tablet) Complete() error { switch tablet.Type { case TYPE_MASTER: tablet.State = STATE_READ_WRITE - if tablet.Parent.Uid != NO_TABLET { - return fmt.Errorf("master cannot have parent: %v", tablet.Parent.Uid) - } case TYPE_IDLE: - if tablet.Parent.Uid != NO_TABLET { - return fmt.Errorf("idle cannot have parent: %v", tablet.Parent.Uid) - } fallthrough default: tablet.State = STATE_READ_ONLY @@ -525,6 +537,17 @@ func NewTabletInfo(tablet *Tablet, version int64) *TabletInfo { return &TabletInfo{version: version, Tablet: tablet} } +// GetTablet is a high level function to read tablet data. +// It generates trace spans. +func GetTablet(ctx context.Context, ts Server, alias TabletAlias) (*TabletInfo, error) { + span := trace.NewSpanFromContext(ctx) + span.StartClient("TopoServer.GetTablet") + span.Annotate("tablet", alias.String()) + defer span.Finish() + + return ts.GetTablet(alias) +} + // UpdateTablet updates the tablet data only - not associated replication paths. func UpdateTablet(ctx context.Context, ts Server, tablet *TabletInfo) error { span := trace.NewSpanFromContext(ctx) @@ -544,6 +567,17 @@ func UpdateTablet(ctx context.Context, ts Server, tablet *TabletInfo) error { return err } +// UpdateTabletFields is a high level wrapper for TopoServer.UpdateTabletFields +// that generates trace spans. +func UpdateTabletFields(ctx context.Context, ts Server, alias TabletAlias, update func(*Tablet) error) error { + span := trace.NewSpanFromContext(ctx) + span.StartClient("TopoServer.UpdateTabletFields") + span.Annotate("tablet", alias.String()) + defer span.Finish() + + return ts.UpdateTabletFields(alias, update) +} + // Validate makes sure a tablet is represented correctly in the topology server. func Validate(ts Server, tabletAlias TabletAlias) error { // read the tablet record, make sure it parses @@ -552,11 +586,6 @@ func Validate(ts Server, tabletAlias TabletAlias) error { return err } - // make sure the Server is good for this tablet - if err = ts.ValidateTablet(tabletAlias); err != nil { - return err - } - // Some tablets have no information to generate valid replication paths. // We have three cases to handle: // - we are a master, in which case we may have an entry or not @@ -574,16 +603,12 @@ func Validate(ts Server, tabletAlias TabletAlias) error { return nil } - rl, err := si.GetReplicationLink(tabletAlias) + _, err = si.GetReplicationLink(tabletAlias) if err != nil { log.Warningf("master tablet %v with no ReplicationLink entry, assuming it's because of transition", tabletAlias) return nil } - if rl.Parent != tablet.Parent { - return fmt.Errorf("tablet %v has parent %v but has %v in shard replication object", tabletAlias, tablet.Parent, rl.Parent) - } - } else if tablet.IsInReplicationGraph() { if err = ts.ValidateShard(tablet.Keyspace, tablet.Shard); err != nil { return err @@ -594,15 +619,11 @@ func Validate(ts Server, tabletAlias TabletAlias) error { return err } - rl, err := si.GetReplicationLink(tabletAlias) + _, err = si.GetReplicationLink(tabletAlias) if err != nil { return fmt.Errorf("tablet %v not found in cell %v shard replication: %v", tabletAlias, tablet.Alias.Cell, err) } - if rl.Parent != tablet.Parent { - return fmt.Errorf("tablet %v has parent %v but has %v in shard replication object", tabletAlias, tablet.Parent, rl.Parent) - } - } else if tablet.IsAssigned() { // this case is to make sure a scrap node that used to be in // a replication graph doesn't leave a node behind. @@ -642,7 +663,7 @@ func CreateTablet(ts Server, tablet *Tablet) error { // UpdateTabletReplicationData creates or updates the replication // graph data for a tablet func UpdateTabletReplicationData(ctx context.Context, ts Server, tablet *Tablet) error { - return UpdateShardReplicationRecord(ctx, ts, tablet.Keyspace, tablet.Shard, tablet.Alias, tablet.Parent) + return UpdateShardReplicationRecord(ctx, ts, tablet.Keyspace, tablet.Shard, tablet.Alias) } // DeleteTabletReplicationData deletes replication data. @@ -654,7 +675,12 @@ func DeleteTabletReplicationData(ts Server, tablet *Tablet) error { // and returns them all in a map. // If error is ErrPartialResult, the results in the dictionary are // incomplete, meaning some tablets couldn't be read. -func GetTabletMap(ts Server, tabletAliases []TabletAlias) (map[TabletAlias]*TabletInfo, error) { +func GetTabletMap(ctx context.Context, ts Server, tabletAliases []TabletAlias) (map[TabletAlias]*TabletInfo, error) { + span := trace.NewSpanFromContext(ctx) + span.StartLocal("topo.GetTabletMap") + span.Annotate("num_tablets", len(tabletAliases)) + defer span.Finish() + wg := sync.WaitGroup{} mutex := sync.Mutex{} diff --git a/go/vt/topo/test/faketopo/faketopo.go b/go/vt/topo/test/faketopo/faketopo.go index 2e2573cd89f..7441ff0119e 100644 --- a/go/vt/topo/test/faketopo/faketopo.go +++ b/go/vt/topo/test/faketopo/faketopo.go @@ -1,4 +1,4 @@ -// faketopo contains utitlities for tests that have to interact with a +// Package faketopo contains utitlities for tests that have to interact with a // Vitess topology. package faketopo @@ -12,10 +12,14 @@ import ( "github.com/youtube/vitess/go/vt/mysqlctl" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/wrangler" + "golang.org/x/net/context" ) const ( - TestShard = "0" + // TestShard is the shard we use in tests + TestShard = "0" + + // TestKeyspace is the keyspace we use in tests TestKeyspace = "test_keyspace" ) @@ -47,7 +51,7 @@ type Fixture struct { // New creates a topology fixture. func New(t *testing.T, logger logutil.Logger, ts topo.Server, cells []string) *Fixture { - wr := wrangler.New(logger, ts, 1*time.Second, 1*time.Second) + wr := wrangler.New(logger, ts, 1*time.Second) return &Fixture{ T: t, @@ -74,7 +78,7 @@ func (fix *Fixture) MakeMySQLMaster(uid int) { if id == uid { tablet.mysql.MasterAddr = "" } else { - tablet.mysql.MasterAddr = newMaster.MysqlIpAddr() + tablet.mysql.MasterAddr = newMaster.MysqlIPAddr() } } } @@ -95,16 +99,13 @@ func (fix *Fixture) AddTablet(uid int, cell string, tabletType topo.TabletType, Shard: TestShard, KeyRange: newKeyRange(TestShard), } - if master != nil { - tablet.Parent = master.Alias - } - if err := fix.Wrangler.InitTablet(tablet, true, true, false); err != nil { + if err := fix.Wrangler.InitTablet(context.Background(), tablet, true, true, false); err != nil { fix.Fatalf("CreateTablet: %v", err) } mysqlDaemon := &mysqlctl.FakeMysqlDaemon{} if master != nil { - mysqlDaemon.MasterAddr = master.MysqlIpAddr() + mysqlDaemon.MasterAddr = master.MysqlIPAddr() } mysqlDaemon.MysqlPort = 3334 + 10*uid diff --git a/go/vt/topo/test/keyspace.go b/go/vt/topo/test/keyspace.go index 6cacef100bc..c7532032c43 100644 --- a/go/vt/topo/test/keyspace.go +++ b/go/vt/topo/test/keyspace.go @@ -1,4 +1,5 @@ -// package test contains utilities to test topo.Server +// TODO(sougou): The comments below look obsolete. Need to verify. +// Package test contains utilities to test topo.Server // implementations. If you are testing your implementation, you will // want to call CheckAll in your test method. For an example, look at // the tests in github.com/youtube/vitess/go/vt/zktopo. @@ -64,6 +65,10 @@ func CheckKeyspace(t *testing.T, ts topo.Server) { t.Errorf("GetKeyspaces: want %v, got %v", []string{"test_keyspace", "test_keyspace2"}, keyspaces) } + // Call delete shards and make sure the keyspace still exists. + if err := ts.DeleteKeyspaceShards("test_keyspace2"); err != nil { + t.Errorf("DeleteKeyspaceShards: %v", err) + } ki, err := ts.GetKeyspace("test_keyspace2") if err != nil { t.Fatalf("GetKeyspace: %v", err) diff --git a/go/vt/topo/test/lock.go b/go/vt/topo/test/lock.go index 193a323c22b..4bb39a95d01 100644 --- a/go/vt/topo/test/lock.go +++ b/go/vt/topo/test/lock.go @@ -1,4 +1,4 @@ -// package test contains utilities to test topo.Server +// Package test contains utilities to test topo.Server // implementations. If you are testing your implementation, you will // want to call CheckAll in your test method. For an example, look at // the tests in github.com/youtube/vitess/go/vt/zktopo. @@ -9,30 +9,45 @@ import ( "time" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) +// timeUntilLockIsTaken is the time to wait until a lock is taken. +// We haven't found a better simpler way to guarantee a routine is stuck +// waiting for a topo lock than sleeping that amount. +var timeUntilLockIsTaken = 10 * time.Millisecond + +// CheckKeyspaceLock checks we can take a keyspace lock as expected. func CheckKeyspaceLock(t *testing.T, ts topo.Server) { if err := ts.CreateKeyspace("test_keyspace", &topo.Keyspace{}); err != nil { t.Fatalf("CreateKeyspace: %v", err) } - interrupted := make(chan struct{}, 1) - lockPath, err := ts.LockKeyspaceForAction("test_keyspace", "fake-content", 5*time.Second, interrupted) + checkKeyspaceLockTimeout(t, ts) + checkKeyspaceLockMissing(t, ts) + checkKeyspaceLockUnblocks(t, ts) +} + +func checkKeyspaceLockTimeout(t *testing.T, ts topo.Server) { + ctx, ctxCancel := context.WithCancel(context.Background()) + lockPath, err := ts.LockKeyspaceForAction(ctx, "test_keyspace", "fake-content") if err != nil { t.Fatalf("LockKeyspaceForAction: %v", err) } // test we can't take the lock again - if _, err := ts.LockKeyspaceForAction("test_keyspace", "unused-fake-content", time.Second/10, interrupted); err != topo.ErrTimeout { + fastCtx, cancel := context.WithTimeout(ctx, timeUntilLockIsTaken) + if _, err := ts.LockKeyspaceForAction(fastCtx, "test_keyspace", "unused-fake-content"); err != topo.ErrTimeout { t.Errorf("LockKeyspaceForAction(again): %v", err) } + cancel() // test we can interrupt taking the lock go func() { - time.Sleep(time.Second / 10) - close(interrupted) + time.Sleep(timeUntilLockIsTaken) + ctxCancel() }() - if _, err := ts.LockKeyspaceForAction("test_keyspace", "unused-fake-content", 5*time.Second, interrupted); err != topo.ErrInterrupted { + if _, err := ts.LockKeyspaceForAction(ctx, "test_keyspace", "unused-fake-content"); err != topo.ErrInterrupted { t.Errorf("LockKeyspaceForAction(interrupted): %v", err) } @@ -44,14 +59,62 @@ func CheckKeyspaceLock(t *testing.T, ts topo.Server) { if err := ts.UnlockKeyspaceForAction("test_keyspace", lockPath, "fake-results"); err == nil { t.Error("UnlockKeyspaceForAction(again) worked") } +} - // test we can't lock a non-existing keyspace - interrupted = make(chan struct{}, 1) - if _, err := ts.LockKeyspaceForAction("test_keyspace_666", "fake-content", 5*time.Second, interrupted); err == nil { - t.Fatalf("LockKeyspaceForAction(test_keyspace_666) worked for non-existing keyspace") +// checkKeyspaceLockMissing makes sure we can't lock a non-existing keyspace +func checkKeyspaceLockMissing(t *testing.T, ts topo.Server) { + ctx := context.Background() + if _, err := ts.LockKeyspaceForAction(ctx, "test_keyspace_666", "fake-content"); err == nil { + t.Errorf("LockKeyspaceForAction(test_keyspace_666) worked for non-existing keyspace") } } +// checkKeyspaceLockUnblocks makes sure that a routine waiting on a lock +// is unblocked when another routine frees the lock +func checkKeyspaceLockUnblocks(t *testing.T, ts topo.Server) { + unblock := make(chan struct{}) + finished := make(chan struct{}) + + // as soon as we're unblocked, we try to lock the keyspace + go func() { + <-unblock + ctx := context.Background() + lockPath, err := ts.LockKeyspaceForAction(ctx, "test_keyspace", "fake-content") + if err != nil { + t.Fatalf("LockKeyspaceForAction(test_keyspace) failed: %v", err) + } + if err = ts.UnlockKeyspaceForAction("test_keyspace", lockPath, "fake-results"); err != nil { + t.Errorf("UnlockKeyspaceForAction(test_keyspace): %v", err) + } + close(finished) + }() + + // lock the keyspace + ctx := context.Background() + lockPath2, err := ts.LockKeyspaceForAction(ctx, "test_keyspace", "fake-content") + if err != nil { + t.Fatalf("LockKeyspaceForAction(test_keyspace) failed: %v", err) + } + + // unblock the go routine so it starts waiting + close(unblock) + + // sleep for a while so we're sure the go routine is blocking + time.Sleep(timeUntilLockIsTaken) + + if err = ts.UnlockKeyspaceForAction("test_keyspace", lockPath2, "fake-results"); err != nil { + t.Fatalf("UnlockKeyspaceForAction(test_keyspace): %v", err) + } + + timeout := time.After(10 * time.Second) + select { + case <-finished: + case <-timeout: + t.Fatalf("unlocking timed out") + } +} + +// CheckShardLock checks we can take a shard lock func CheckShardLock(t *testing.T, ts topo.Server) { if err := ts.CreateKeyspace("test_keyspace", &topo.Keyspace{}); err != nil { t.Fatalf("CreateKeyspace: %v", err) @@ -60,23 +123,31 @@ func CheckShardLock(t *testing.T, ts topo.Server) { t.Fatalf("CreateShard: %v", err) } - interrupted := make(chan struct{}, 1) - lockPath, err := ts.LockShardForAction("test_keyspace", "10-20", "fake-content", 5*time.Second, interrupted) + checkShardLockTimeout(t, ts) + checkShardLockMissing(t, ts) + checkShardLockUnblocks(t, ts) +} + +func checkShardLockTimeout(t *testing.T, ts topo.Server) { + ctx, ctxCancel := context.WithCancel(context.Background()) + lockPath, err := ts.LockShardForAction(ctx, "test_keyspace", "10-20", "fake-content") if err != nil { t.Fatalf("LockShardForAction: %v", err) } // test we can't take the lock again - if _, err := ts.LockShardForAction("test_keyspace", "10-20", "unused-fake-content", time.Second/2, interrupted); err != topo.ErrTimeout { + fastCtx, cancel := context.WithTimeout(ctx, timeUntilLockIsTaken) + if _, err := ts.LockShardForAction(fastCtx, "test_keyspace", "10-20", "unused-fake-content"); err != topo.ErrTimeout { t.Errorf("LockShardForAction(again): %v", err) } + cancel() // test we can interrupt taking the lock go func() { - time.Sleep(time.Second / 2) - close(interrupted) + time.Sleep(timeUntilLockIsTaken) + ctxCancel() }() - if _, err := ts.LockShardForAction("test_keyspace", "10-20", "unused-fake-content", 5*time.Second, interrupted); err != topo.ErrInterrupted { + if _, err := ts.LockShardForAction(ctx, "test_keyspace", "10-20", "unused-fake-content"); err != topo.ErrInterrupted { t.Errorf("LockShardForAction(interrupted): %v", err) } @@ -88,54 +159,157 @@ func CheckShardLock(t *testing.T, ts topo.Server) { if err := ts.UnlockShardForAction("test_keyspace", "10-20", lockPath, "fake-results"); err == nil { t.Error("UnlockShardForAction(again) worked") } +} +func checkShardLockMissing(t *testing.T, ts topo.Server) { // test we can't lock a non-existing shard - interrupted = make(chan struct{}, 1) - if _, err := ts.LockShardForAction("test_keyspace", "20-30", "fake-content", 5*time.Second, interrupted); err == nil { - t.Fatalf("LockShardForAction(test_keyspace/20-30) worked for non-existing shard") + ctx := context.Background() + if _, err := ts.LockShardForAction(ctx, "test_keyspace", "20-30", "fake-content"); err == nil { + t.Errorf("LockShardForAction(test_keyspace/20-30) worked for non-existing shard") } } +// checkShardLockUnblocks makes sure that a routine waiting on a lock +// is unblocked when another routine frees the lock +func checkShardLockUnblocks(t *testing.T, ts topo.Server) { + unblock := make(chan struct{}) + finished := make(chan struct{}) + + // as soon as we're unblocked, we try to lock the shard + go func() { + <-unblock + ctx := context.Background() + lockPath, err := ts.LockShardForAction(ctx, "test_keyspace", "10-20", "fake-content") + if err != nil { + t.Fatalf("LockShardForAction(test_keyspace, 10-20) failed: %v", err) + } + if err = ts.UnlockShardForAction("test_keyspace", "10-20", lockPath, "fake-results"); err != nil { + t.Errorf("UnlockShardForAction(test_keyspace, 10-20): %v", err) + } + close(finished) + }() + + // lock the shard + ctx := context.Background() + lockPath2, err := ts.LockShardForAction(ctx, "test_keyspace", "10-20", "fake-content") + if err != nil { + t.Fatalf("LockShardForAction(test_keyspace, 10-20) failed: %v", err) + } + + // unblock the go routine so it starts waiting + close(unblock) + + // sleep for a while so we're sure the go routine is blocking + time.Sleep(timeUntilLockIsTaken) + + if err = ts.UnlockShardForAction("test_keyspace", "10-20", lockPath2, "fake-results"); err != nil { + t.Fatalf("UnlockShardForAction(test_keyspace, 10-20): %v", err) + } + + timeout := time.After(10 * time.Second) + select { + case <-finished: + case <-timeout: + t.Fatalf("unlocking timed out") + } +} + +// CheckSrvShardLock tests we can take a SrvShard lock func CheckSrvShardLock(t *testing.T, ts topo.Server) { + checkSrvShardLockGeneral(t, ts) + checkSrvShardLockUnblocks(t, ts) +} + +func checkSrvShardLockGeneral(t *testing.T, ts topo.Server) { + cell := getLocalCell(t, ts) + // make sure we can create the lock even if no directory exists - interrupted := make(chan struct{}, 1) - lockPath, err := ts.LockSrvShardForAction("test", "test_keyspace", "10-20", "fake-content", 5*time.Second, interrupted) + ctx, ctxCancel := context.WithCancel(context.Background()) + lockPath, err := ts.LockSrvShardForAction(ctx, cell, "test_keyspace", "10-20", "fake-content") if err != nil { t.Fatalf("LockSrvShardForAction: %v", err) } - if err := ts.UnlockSrvShardForAction("test", "test_keyspace", "10-20", lockPath, "fake-results"); err != nil { - t.Errorf("UnlockShardForAction(): %v", err) + if err := ts.UnlockSrvShardForAction(cell, "test_keyspace", "10-20", lockPath, "fake-results"); err != nil { + t.Errorf("UnlockShardForAction: %v", err) } // now take the lock again after the root exists - lockPath, err = ts.LockSrvShardForAction("test", "test_keyspace", "10-20", "fake-content", 5*time.Second, interrupted) + lockPath, err = ts.LockSrvShardForAction(ctx, cell, "test_keyspace", "10-20", "fake-content") if err != nil { t.Fatalf("LockSrvShardForAction: %v", err) } // test we can't take the lock again - if _, err := ts.LockSrvShardForAction("test", "test_keyspace", "10-20", "unused-fake-content", time.Second/2, interrupted); err != topo.ErrTimeout { + fastCtx, cancel := context.WithTimeout(ctx, timeUntilLockIsTaken) + if _, err := ts.LockSrvShardForAction(fastCtx, cell, "test_keyspace", "10-20", "unused-fake-content"); err != topo.ErrTimeout { t.Errorf("LockSrvShardForAction(again): %v", err) } + cancel() // test we can interrupt taking the lock go func() { - time.Sleep(time.Second / 2) - close(interrupted) + time.Sleep(timeUntilLockIsTaken) + ctxCancel() }() - if _, err := ts.LockSrvShardForAction("test", "test_keyspace", "10-20", "unused-fake-content", 5*time.Second, interrupted); err != topo.ErrInterrupted { + if _, err := ts.LockSrvShardForAction(ctx, cell, "test_keyspace", "10-20", "unused-fake-content"); err != topo.ErrInterrupted { t.Errorf("LockSrvShardForAction(interrupted): %v", err) } // unlock now - if err := ts.UnlockSrvShardForAction("test", "test_keyspace", "10-20", lockPath, "fake-results"); err != nil { + if err := ts.UnlockSrvShardForAction(cell, "test_keyspace", "10-20", lockPath, "fake-results"); err != nil { t.Errorf("UnlockSrvShardForAction(): %v", err) } // test we can't unlock again - if err := ts.UnlockSrvShardForAction("test", "test_keyspace", "10-20", lockPath, "fake-results"); err == nil { + if err := ts.UnlockSrvShardForAction(cell, "test_keyspace", "10-20", lockPath, "fake-results"); err == nil { t.Error("UnlockSrvShardForAction(again) worked") } } + +// checkSrvShardLockUnblocks makes sure that a routine waiting on a lock +// is unblocked when another routine frees the lock +func checkSrvShardLockUnblocks(t *testing.T, ts topo.Server) { + cell := getLocalCell(t, ts) + unblock := make(chan struct{}) + finished := make(chan struct{}) + + // as soon as we're unblocked, we try to lock the shard + go func() { + <-unblock + ctx := context.Background() + lockPath, err := ts.LockSrvShardForAction(ctx, cell, "test_keyspace", "10-20", "fake-content") + if err != nil { + t.Fatalf("LockSrvShardForAction(test, test_keyspace, 10-20) failed: %v", err) + } + if err = ts.UnlockSrvShardForAction(cell, "test_keyspace", "10-20", lockPath, "fake-results"); err != nil { + t.Errorf("UnlockSrvShardForAction(test, test_keyspace, 10-20): %v", err) + } + close(finished) + }() + + // lock the shard + ctx := context.Background() + lockPath2, err := ts.LockSrvShardForAction(ctx, cell, "test_keyspace", "10-20", "fake-content") + if err != nil { + t.Fatalf("LockSrvShardForAction(test, test_keyspace, 10-20) failed: %v", err) + } + + // unblock the go routine so it starts waiting + close(unblock) + + // sleep for a while so we're sure the go routine is blocking + time.Sleep(timeUntilLockIsTaken) + + if err = ts.UnlockSrvShardForAction(cell, "test_keyspace", "10-20", lockPath2, "fake-results"); err != nil { + t.Fatalf("UnlockSrvShardForAction(test, test_keyspace, 10-20): %v", err) + } + + timeout := time.After(10 * time.Second) + select { + case <-finished: + case <-timeout: + t.Fatalf("unlocking timed out") + } +} diff --git a/go/vt/topo/test/replication.go b/go/vt/topo/test/replication.go index 013fbd9bef5..3011ec1cef5 100644 --- a/go/vt/topo/test/replication.go +++ b/go/vt/topo/test/replication.go @@ -1,4 +1,4 @@ -// package test contains utilities to test topo.Server +// Package test contains utilities to test topo.Server // implementations. If you are testing your implementation, you will // want to call CheckAll in your test method. For an example, look at // the tests in github.com/youtube/vitess/go/vt/zktopo. @@ -10,6 +10,7 @@ import ( "github.com/youtube/vitess/go/vt/topo" ) +// CheckShardReplication tests ShardReplication objects func CheckShardReplication(t *testing.T, ts topo.Server) { cell := getLocalCell(t, ts) if _, err := ts.GetShardReplication(cell, "test_keyspace", "-10"); err != topo.ErrNoNode { @@ -23,10 +24,6 @@ func CheckShardReplication(t *testing.T, ts topo.Server) { Cell: "c1", Uid: 1, }, - Parent: topo.TabletAlias{ - Cell: "c2", - Uid: 2, - }, }, }, } @@ -42,9 +39,7 @@ func CheckShardReplication(t *testing.T, ts topo.Server) { } else { if len(sri.ReplicationLinks) != 1 || sri.ReplicationLinks[0].TabletAlias.Cell != "c1" || - sri.ReplicationLinks[0].TabletAlias.Uid != 1 || - sri.ReplicationLinks[0].Parent.Cell != "c2" || - sri.ReplicationLinks[0].Parent.Uid != 2 { + sri.ReplicationLinks[0].TabletAlias.Uid != 1 { t.Errorf("GetShardReplication(new guy) returned wrong value: %v", *sri) } } @@ -55,10 +50,6 @@ func CheckShardReplication(t *testing.T, ts topo.Server) { Cell: "c3", Uid: 3, }, - Parent: topo.TabletAlias{ - Cell: "c4", - Uid: 4, - }, }) return nil }); err != nil { @@ -71,12 +62,8 @@ func CheckShardReplication(t *testing.T, ts topo.Server) { if len(sri.ReplicationLinks) != 2 || sri.ReplicationLinks[0].TabletAlias.Cell != "c1" || sri.ReplicationLinks[0].TabletAlias.Uid != 1 || - sri.ReplicationLinks[0].Parent.Cell != "c2" || - sri.ReplicationLinks[0].Parent.Uid != 2 || sri.ReplicationLinks[1].TabletAlias.Cell != "c3" || - sri.ReplicationLinks[1].TabletAlias.Uid != 3 || - sri.ReplicationLinks[1].Parent.Cell != "c4" || - sri.ReplicationLinks[1].Parent.Uid != 4 { + sri.ReplicationLinks[1].TabletAlias.Uid != 3 { t.Errorf("GetShardReplication(new guy) returned wrong value: %v", *sri) } } diff --git a/go/vt/topo/test/serving.go b/go/vt/topo/test/serving.go index f8a43c2a11f..01a3f0bfe23 100644 --- a/go/vt/topo/test/serving.go +++ b/go/vt/topo/test/serving.go @@ -1,17 +1,20 @@ -// package test contains utilities to test topo.Server +// Package test contains utilities to test topo.Server // implementations. If you are testing your implementation, you will // want to call CheckAll in your test method. For an example, look at // the tests in github.com/youtube/vitess/go/vt/zktopo. package test import ( + "reflect" "testing" "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) -func CheckServingGraph(t *testing.T, ts topo.Server) { +// CheckServingGraph makes sure the serving graph functions work properly. +func CheckServingGraph(ctx context.Context, t *testing.T, ts topo.Server) { cell := getLocalCell(t, ts) // test individual cell/keyspace/shard/type entries @@ -27,18 +30,34 @@ func CheckServingGraph(t *testing.T, ts topo.Server) { topo.EndPoint{ Uid: 1, Host: "host1", - NamedPortMap: map[string]int{"_vt": 1234, "_mysql": 1235, "_vts": 1236}, + NamedPortMap: map[string]int{"vt": 1234, "mysql": 1235, "vts": 1236}, }, }, } - if err := ts.UpdateEndPoints(cell, "test_keyspace", "-10", topo.TYPE_MASTER, &endPoints); err != nil { - t.Errorf("UpdateEndPoints(master): %v", err) + if err := topo.UpdateEndPoints(ctx, ts, cell, "test_keyspace", "-10", topo.TYPE_MASTER, &endPoints); err != nil { + t.Fatalf("UpdateEndPoints(master): %v", err) } if types, err := ts.GetSrvTabletTypesPerShard(cell, "test_keyspace", "-10"); err != nil || len(types) != 1 || types[0] != topo.TYPE_MASTER { t.Errorf("GetSrvTabletTypesPerShard(1): %v %v", err, types) } + // Delete the SrvShard (need to delete endpoints first). + if err := ts.DeleteEndPoints(cell, "test_keyspace", "-10", topo.TYPE_MASTER); err != nil { + t.Errorf("DeleteEndPoints: %v", err) + } + if err := ts.DeleteSrvShard(cell, "test_keyspace", "-10"); err != nil { + t.Errorf("DeleteSrvShard: %v", err) + } + if _, err := ts.GetSrvShard(cell, "test_keyspace", "-10"); err != topo.ErrNoNode { + t.Errorf("GetSrvShard(deleted) got %v, want ErrNoNode", err) + } + + // Re-add endpoints. + if err := topo.UpdateEndPoints(ctx, ts, cell, "test_keyspace", "-10", topo.TYPE_MASTER, &endPoints); err != nil { + t.Fatalf("UpdateEndPoints(master): %v", err) + } + addrs, err := ts.GetEndPoints(cell, "test_keyspace", "-10", topo.TYPE_MASTER) if err != nil { t.Errorf("GetEndPoints: %v", err) @@ -46,21 +65,21 @@ func CheckServingGraph(t *testing.T, ts topo.Server) { if len(addrs.Entries) != 1 || addrs.Entries[0].Uid != 1 { t.Errorf("GetEndPoints(1): %v", addrs) } - if pm := addrs.Entries[0].NamedPortMap; pm["_vt"] != 1234 || pm["_mysql"] != 1235 || pm["_vts"] != 1236 { + if pm := addrs.Entries[0].NamedPortMap; pm["vt"] != 1234 || pm["mysql"] != 1235 || pm["vts"] != 1236 { t.Errorf("GetSrcTabletType(1).NamedPortmap: want %v, got %v", endPoints.Entries[0].NamedPortMap, pm) } if err := ts.UpdateTabletEndpoint(cell, "test_keyspace", "-10", topo.TYPE_REPLICA, &topo.EndPoint{Uid: 2, Host: "host2"}); err != nil { - t.Errorf("UpdateTabletEndpoint(invalid): %v", err) + t.Fatalf("UpdateTabletEndpoint(invalid): %v", err) } if err := ts.UpdateTabletEndpoint(cell, "test_keyspace", "-10", topo.TYPE_MASTER, &topo.EndPoint{Uid: 1, Host: "host2"}); err != nil { - t.Errorf("UpdateTabletEndpoint(master): %v", err) + t.Fatalf("UpdateTabletEndpoint(master): %v", err) } if addrs, err := ts.GetEndPoints(cell, "test_keyspace", "-10", topo.TYPE_MASTER); err != nil || len(addrs.Entries) != 1 || addrs.Entries[0].Uid != 1 { t.Errorf("GetEndPoints(2): %v %v", err, addrs) } if err := ts.UpdateTabletEndpoint(cell, "test_keyspace", "-10", topo.TYPE_MASTER, &topo.EndPoint{Uid: 3, Host: "host3"}); err != nil { - t.Errorf("UpdateTabletEndpoint(master): %v", err) + t.Fatalf("UpdateTabletEndpoint(master): %v", err) } if addrs, err := ts.GetEndPoints(cell, "test_keyspace", "-10", topo.TYPE_MASTER); err != nil || len(addrs.Entries) != 2 { t.Errorf("GetEndPoints(2): %v %v", err, addrs) @@ -79,7 +98,7 @@ func CheckServingGraph(t *testing.T, ts topo.Server) { TabletTypes: []topo.TabletType{topo.TYPE_REPLICA, topo.TYPE_RDONLY}, } if err := ts.UpdateSrvShard(cell, "test_keyspace", "-10", &srvShard); err != nil { - t.Errorf("UpdateSrvShard(1): %v", err) + t.Fatalf("UpdateSrvShard(1): %v", err) } if _, err := ts.GetSrvShard(cell, "test_keyspace", "666"); err != topo.ErrNoNode { t.Errorf("GetSrvShard(invalid): %v", err) @@ -135,7 +154,7 @@ func CheckServingGraph(t *testing.T, ts topo.Server) { // check that updating a SrvKeyspace out of the blue works if err := ts.UpdateSrvKeyspace(cell, "unknown_keyspace_so_far", &srvKeyspace); err != nil { - t.Errorf("UpdateSrvKeyspace(2): %v", err) + t.Fatalf("UpdateSrvKeyspace(2): %v", err) } if k, err := ts.GetSrvKeyspace(cell, "unknown_keyspace_so_far"); err != nil || len(k.TabletTypes) != 1 || @@ -150,3 +169,104 @@ func CheckServingGraph(t *testing.T, ts topo.Server) { t.Errorf("GetSrvKeyspace(out of the blue): %v %v", err, *k) } } + +// CheckWatchEndPoints makes sure WatchEndPoints works as expected +func CheckWatchEndPoints(ctx context.Context, t *testing.T, ts topo.Server) { + cell := getLocalCell(t, ts) + keyspace := "test_keyspace" + shard := "-10" + tabletType := topo.TYPE_MASTER + + // start watching, should get nil first + notifications, stopWatching, err := ts.WatchEndPoints(cell, keyspace, shard, tabletType) + if err != nil { + t.Fatalf("WatchEndPoints failed: %v", err) + } + ep, ok := <-notifications + if !ok || ep != nil { + t.Fatalf("first value is wrong: %v %v", ep, ok) + } + + // update the endpoints, should get a notification + endPoints := topo.EndPoints{ + Entries: []topo.EndPoint{ + topo.EndPoint{ + Uid: 1, + Host: "host1", + NamedPortMap: map[string]int{"vt": 1234, "mysql": 1235, "vts": 1236}, + }, + }, + } + if err := topo.UpdateEndPoints(ctx, ts, cell, keyspace, shard, tabletType, &endPoints); err != nil { + t.Fatalf("UpdateEndPoints failed: %v", err) + } + for { + ep, ok := <-notifications + if !ok { + t.Fatalf("watch channel is closed???") + } + if ep == nil { + // duplicate notification of the first value, that's OK + continue + } + // non-empty value, that one should be ours + if !reflect.DeepEqual(&endPoints, ep) { + t.Fatalf("first value is wrong: %v %v", ep, ok) + } + break + } + + // delete the endpoints, should get a notification + if err := ts.DeleteEndPoints(cell, keyspace, shard, tabletType); err != nil { + t.Fatalf("DeleteEndPoints failed: %v", err) + } + for { + ep, ok := <-notifications + if !ok { + t.Fatalf("watch channel is closed???") + } + if ep == nil { + break + } + + // duplicate notification of the first value, that's OK, + // but value better be good. + if !reflect.DeepEqual(&endPoints, ep) { + t.Fatalf("duplicate notification value is bad: %v", ep) + } + } + + // re-create the value, a bit different, should get a notification + endPoints.Entries[0].Uid = 2 + if err := topo.UpdateEndPoints(ctx, ts, cell, keyspace, shard, tabletType, &endPoints); err != nil { + t.Fatalf("UpdateEndPoints failed: %v", err) + } + for { + ep, ok := <-notifications + if !ok { + t.Fatalf("watch channel is closed???") + } + if ep == nil { + // duplicate notification of the closed value, that's OK + continue + } + // non-empty value, that one should be ours + if !reflect.DeepEqual(&endPoints, ep) { + t.Fatalf("value after delete / re-create is wrong: %v %v", ep, ok) + } + break + } + + // close the stopWatching channel, should eventually get a closed + // notifications channel too + close(stopWatching) + for { + ep, ok := <-notifications + if !ok { + break + } + if !reflect.DeepEqual(&endPoints, ep) { + t.Fatalf("duplicate notification value is bad: %v", ep) + } + } +} diff --git a/go/vt/topo/test/shard.go b/go/vt/topo/test/shard.go index a7422bd3041..5e9c7514ff8 100644 --- a/go/vt/topo/test/shard.go +++ b/go/vt/topo/test/shard.go @@ -1,4 +1,4 @@ -// package test contains utilities to test topo.Server +// Package test contains utilities to test topo.Server // implementations. If you are testing your implementation, you will // want to call CheckAll in your test method. For an example, look at // the tests in github.com/youtube/vitess/go/vt/zktopo. @@ -8,7 +8,7 @@ import ( "encoding/json" "testing" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/vt/topo" ) @@ -25,7 +25,8 @@ func shardEqual(left, right *topo.Shard) (bool, error) { return string(lj) == string(rj), nil } -func CheckShard(t *testing.T, ts topo.Server) { +// CheckShard verifies the Shard operations work correctly +func CheckShard(ctx context.Context, t *testing.T, ts topo.Server) { if err := ts.CreateKeyspace("test_keyspace", &topo.Keyspace{}); err != nil { t.Fatalf("CreateKeyspace: %v", err) } @@ -37,11 +38,27 @@ func CheckShard(t *testing.T, ts topo.Server) { t.Errorf("CreateShard called second time, got: %v", err) } - if _, err := ts.GetShard("test_keyspace", "666"); err != topo.ErrNoNode { + // Delete shard and see if we can re-create it. + if err := ts.DeleteShard("test_keyspace", "b0-c0"); err != nil { + t.Fatalf("DeleteShard: %v", err) + } + if err := topo.CreateShard(ts, "test_keyspace", "b0-c0"); err != nil { + t.Fatalf("CreateShard: %v", err) + } + + // Delete ALL shards. + if err := ts.DeleteKeyspaceShards("test_keyspace"); err != nil { + t.Fatalf("DeleteKeyspaceShards: %v", err) + } + if err := topo.CreateShard(ts, "test_keyspace", "b0-c0"); err != nil { + t.Fatalf("CreateShard: %v", err) + } + + if _, err := topo.GetShard(ctx, ts, "test_keyspace", "666"); err != topo.ErrNoNode { t.Errorf("GetShard(666): %v", err) } - shardInfo, err := ts.GetShard("test_keyspace", "b0-c0") + shardInfo, err := topo.GetShard(ctx, ts, "test_keyspace", "b0-c0") if err != nil { t.Errorf("GetShard: %v", err) } @@ -74,11 +91,34 @@ func CheckShard(t *testing.T, ts topo.Server) { DisableQueryService: true, }, } - if err := topo.UpdateShard(context.TODO(), ts, shardInfo); err != nil { + if err := topo.UpdateShard(ctx, ts, shardInfo); err != nil { t.Errorf("UpdateShard: %v", err) } - updatedShardInfo, err := ts.GetShard("test_keyspace", "b0-c0") + other := topo.TabletAlias{Cell: "ny", Uid: 82873} + _, err = topo.UpdateShardFields(ctx, ts, "test_keyspace", "b0-c0", func(shard *topo.Shard) error { + shard.MasterAlias = other + return nil + }) + if err != nil { + t.Fatalf("UpdateShardFields error: %v", err) + } + si, err := topo.GetShard(ctx, ts, "test_keyspace", "b0-c0") + if err != nil { + t.Fatalf("GetShard: %v", err) + } + if si.MasterAlias != other { + t.Fatalf("shard.MasterAlias = %v, want %v", si.MasterAlias, other) + } + _, err = topo.UpdateShardFields(ctx, ts, "test_keyspace", "b0-c0", func(shard *topo.Shard) error { + shard.MasterAlias = master + return nil + }) + if err != nil { + t.Fatalf("UpdateShardFields error: %v", err) + } + + updatedShardInfo, err := topo.GetShard(ctx, ts, "test_keyspace", "b0-c0") if err != nil { t.Fatalf("GetShard: %v", err) } @@ -102,4 +142,8 @@ func CheckShard(t *testing.T, ts topo.Server) { t.Errorf("GetShardNames(666): %v", err) } + // test ValidateShard + if err := ts.ValidateShard("test_keyspace", "b0-c0"); err != nil { + t.Errorf("ValidateShard(test_keyspace, b0-c0) failed: %v", err) + } } diff --git a/go/vt/topo/test/tablet.go b/go/vt/topo/test/tablet.go index b3b669efaba..e228b5e5380 100644 --- a/go/vt/topo/test/tablet.go +++ b/go/vt/topo/test/tablet.go @@ -1,4 +1,4 @@ -// package test contains utilities to test topo.Server +// Package test contains utilities to test topo.Server // implementations. If you are testing your implementation, you will // want to call CheckAll in your test method. For an example, look at // the tests in github.com/youtube/vitess/go/vt/zktopo. @@ -7,9 +7,8 @@ package test import ( "encoding/json" "testing" - "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/vt/topo" ) @@ -89,7 +88,7 @@ func CheckTablet(ctx context.Context, t *testing.T, ts topo.Server) { t.Errorf("ti.State: want %v, got %v", want, ti.State) } - if err := ts.UpdateTabletFields(tablet.Alias, func(t *topo.Tablet) error { + if err := topo.UpdateTabletFields(ctx, ts, tablet.Alias, func(t *topo.Tablet) error { t.State = topo.STATE_READ_WRITE return nil }); err != nil { @@ -116,48 +115,3 @@ func CheckTablet(ctx context.Context, t *testing.T, ts topo.Server) { } } - -func CheckPid(t *testing.T, ts topo.Server) { - cell := getLocalCell(t, ts) - tablet := &topo.Tablet{ - Alias: topo.TabletAlias{Cell: cell, Uid: 1}, - Hostname: "localhost", - Portmap: map[string]int{ - "vt": 3333, - }, - - Parent: topo.TabletAlias{}, - Keyspace: "test_keyspace", - Type: topo.TYPE_MASTER, - State: topo.STATE_READ_WRITE, - KeyRange: newKeyRange("-10"), - } - if err := ts.CreateTablet(tablet); err != nil { - t.Fatalf("CreateTablet: %v", err) - } - tabletAlias := topo.TabletAlias{Cell: cell, Uid: 1} - - done := make(chan struct{}, 1) - if err := ts.CreateTabletPidNode(tabletAlias, "contents", done); err != nil { - t.Errorf("ts.CreateTabletPidNode: %v", err) - } - - // wait for up to 30 seconds for the pid to appear - timeout := 30 - for { - err := ts.ValidateTabletPidNode(tabletAlias) - if err == nil { - // exists, we're good - break - } - - timeout -= 1 - if timeout == 0 { - t.Fatalf("ts.ValidateTabletPidNode: %v", err) - } - t.Logf("Waiting for ValidateTabletPidNode to succeed %v/30", timeout) - time.Sleep(time.Second) - } - - close(done) -} diff --git a/go/vt/topo/test/testing.go b/go/vt/topo/test/testing.go index da14123671a..25fac0cca67 100644 --- a/go/vt/topo/test/testing.go +++ b/go/vt/topo/test/testing.go @@ -1,4 +1,4 @@ -// package test contains utilities to test topo.Server +// Package test contains utilities to test topo.Server // implementations. If you are testing your implementation, you will // want to call CheckAll in your test method. For an example, look at // the tests in github.com/youtube/vitess/go/vt/zktopo. diff --git a/go/vt/topo/test/vschema.go b/go/vt/topo/test/vschema.go new file mode 100644 index 00000000000..2fe424304ad --- /dev/null +++ b/go/vt/topo/test/vschema.go @@ -0,0 +1,62 @@ +// Copyright 2015, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package test + +import ( + "strings" + "testing" + + "github.com/youtube/vitess/go/vt/topo" +) + +func CheckVSchema(t *testing.T, ts topo.Server) { + schemafier, ok := ts.(topo.Schemafier) + if !ok { + t.Errorf("%T is not a Schemafier", ts) + return + } + got, err := schemafier.GetVSchema() + if err != nil { + t.Error(err) + } + want := "{}" + if got != want { + t.Errorf("GetVSchema: %s, want %s", got, want) + } + + err = schemafier.SaveVSchema(`{ "Keyspaces": {}}`) + if err != nil { + t.Error(err) + } + + got, err = schemafier.GetVSchema() + if err != nil { + t.Error(err) + } + want = `{ "Keyspaces": {}}` + if got != want { + t.Errorf("GetVSchema: %s, want %s", got, want) + } + + err = schemafier.SaveVSchema(`{ "Keyspaces": { "aa": { "Sharded": false}}}`) + if err != nil { + t.Error(err) + } + + got, err = schemafier.GetVSchema() + if err != nil { + t.Error(err) + } + want = `{ "Keyspaces": { "aa": { "Sharded": false}}}` + if got != want { + t.Errorf("GetVSchema: %s, want %s", got, want) + } + + err = schemafier.SaveVSchema("invalid") + want = "Unmarshal failed:" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("SaveVSchema: %v, must start with %s", err, want) + } +} diff --git a/go/vt/topo/toporeader.go b/go/vt/topo/toporeader.go index 9969835b61e..18d70454141 100644 --- a/go/vt/topo/toporeader.go +++ b/go/vt/topo/toporeader.go @@ -1,7 +1,7 @@ package topo import ( - "code.google.com/p/go.net/context" + "golang.org/x/net/context" ) // TopoReader returns read only information about the topology. @@ -14,6 +14,10 @@ type TopoReader interface { // particular cell (as specified by the GetSrvKeyspaceArgs). GetSrvKeyspace(context.Context, *GetSrvKeyspaceArgs, *SrvKeyspace) error + // GetSrvShard returns information about a shard in a + // particular cell and keyspace (as specified by the GetSrvShardArgs). + GetSrvShard(context.Context, *GetSrvShardArgs, *SrvShard) error + // GetEndPoints returns addresses for a tablet type in a shard // in a keyspace (as specified in GetEndPointsArgs). GetEndPoints(context.Context, *GetEndPointsArgs, *EndPoints) error @@ -24,17 +28,32 @@ type GetSrvKeyspaceNamesArgs struct { Cell string } +//go:generate bsongen -file $GOFILE -type GetSrvKeyspaceNamesArgs -o get_srv_keyspace_names_args_bson.go + // GetSrvKeyspaceArgs is the parameters for TopoReader.GetSrvKeyspace type GetSrvKeyspaceArgs struct { Cell string Keyspace string } +//go:generate bsongen -file $GOFILE -type GetSrvKeyspaceArgs -o get_srv_keyspace_args_bson.go + +// GetSrvShardArgs is the parameters for TopoReader.GetSrvShard +type GetSrvShardArgs struct { + Cell string + Keyspace string + Shard string +} + +//go:generate bsongen -file $GOFILE -type GetSrvShardArgs -o get_srv_shard_args_bson.go + // SrvKeyspaceNames is the response for TopoReader.GetSrvKeyspaceNames type SrvKeyspaceNames struct { Entries []string } +//go:generate bsongen -file $GOFILE -type SrvKeyspaceNames -o srv_keyspace_names_bson.go + // GetEndPointsArgs is the parameters for TopoReader.GetEndPoints type GetEndPointsArgs struct { Cell string @@ -42,3 +61,5 @@ type GetEndPointsArgs struct { Shard string TabletType TabletType } + +//go:generate bsongen -file $GOFILE -type GetEndPointsArgs -o get_end_points_args_bson.go diff --git a/go/vt/topotools/events/snapshot.go b/go/vt/topotools/events/snapshot.go deleted file mode 100644 index 9a6e13b47eb..00000000000 --- a/go/vt/topotools/events/snapshot.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2014, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package events - -import ( - base "github.com/youtube/vitess/go/vt/events" - "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" - "github.com/youtube/vitess/go/vt/topo" -) - -// MultiSnapshot is an event that triggers when a tablet has completed a -// filtered snapshot. -type MultiSnapshot struct { - base.StatusUpdater - - Tablet topo.Tablet - Args actionnode.MultiSnapshotArgs -} - -// MultiRestore is an event that triggers when a tablet has completed a filtered -// snapshot restore. -type MultiRestore struct { - base.StatusUpdater - - Tablet topo.Tablet - Args actionnode.MultiRestoreArgs -} diff --git a/go/vt/topotools/events/snapshot_syslog.go b/go/vt/topotools/events/snapshot_syslog.go deleted file mode 100644 index eba27c41b94..00000000000 --- a/go/vt/topotools/events/snapshot_syslog.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2014, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package events - -import ( - "fmt" - "log/syslog" - - "github.com/youtube/vitess/go/event/syslogger" -) - -// Syslog writes a MultiSnapshot event to syslog. -func (ev *MultiSnapshot) Syslog() (syslog.Priority, string) { - return syslog.LOG_INFO, fmt.Sprintf("%s/%s/%s [multisnapshot] %s", - ev.Tablet.Keyspace, ev.Tablet.Shard, ev.Tablet.Alias, ev.Status) -} - -var _ syslogger.Syslogger = (*MultiSnapshot)(nil) // compile-time interface check - -// Syslog writes a MultiRestore event to syslog. -func (ev *MultiRestore) Syslog() (syslog.Priority, string) { - return syslog.LOG_INFO, fmt.Sprintf("%s/%s/%s [multirestore] %s", - ev.Tablet.Keyspace, ev.Tablet.Shard, ev.Tablet.Alias, ev.Status) -} - -var _ syslogger.Syslogger = (*MultiRestore)(nil) // compile-time interface check diff --git a/go/vt/topotools/events/snapshot_syslog_test.go b/go/vt/topotools/events/snapshot_syslog_test.go deleted file mode 100644 index f0d306fe50a..00000000000 --- a/go/vt/topotools/events/snapshot_syslog_test.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2014, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package events - -import ( - "log/syslog" - "testing" - - base "github.com/youtube/vitess/go/vt/events" - "github.com/youtube/vitess/go/vt/topo" -) - -func TestMultiSnapshotSyslog(t *testing.T) { - wantSev, wantMsg := syslog.LOG_INFO, "keyspace-123/shard-123/cell-0000012345 [multisnapshot] status" - ev := &MultiSnapshot{ - Tablet: topo.Tablet{ - Keyspace: "keyspace-123", - Shard: "shard-123", - Alias: topo.TabletAlias{Cell: "cell", Uid: 12345}, - }, - StatusUpdater: base.StatusUpdater{Status: "status"}, - } - gotSev, gotMsg := ev.Syslog() - - if gotSev != wantSev { - t.Errorf("wrong severity: got %v, want %v", gotSev, wantSev) - } - if gotMsg != wantMsg { - t.Errorf("wrong message: got %v, want %v", gotMsg, wantMsg) - } -} - -func TestMultiRestoreSyslog(t *testing.T) { - wantSev, wantMsg := syslog.LOG_INFO, "keyspace-123/shard-123/cell-0000012345 [multirestore] status" - ev := &MultiRestore{ - Tablet: topo.Tablet{ - Keyspace: "keyspace-123", - Shard: "shard-123", - Alias: topo.TabletAlias{Cell: "cell", Uid: 12345}, - }, - StatusUpdater: base.StatusUpdater{Status: "status"}, - } - gotSev, gotMsg := ev.Syslog() - - if gotSev != wantSev { - t.Errorf("wrong severity: got %v, want %v", gotSev, wantSev) - } - if gotMsg != wantMsg { - t.Errorf("wrong message: got %v, want %v", gotMsg, wantMsg) - } -} diff --git a/go/vt/topotools/rebuild.go b/go/vt/topotools/rebuild.go index 1e26aaf6b93..bece585303a 100644 --- a/go/vt/topotools/rebuild.go +++ b/go/vt/topotools/rebuild.go @@ -10,26 +10,26 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/trace" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // UseSrvShardLocks is a deprecated flag. We leave it here until it's removed // from all invocations of the tools. var UseSrvShardLocks = flag.Bool("use_srv_shard_locks", true, "DEPRECATED: If true, takes the SrvShard lock for each shard being rebuilt") -// Update shard file with new master, replicas, etc. +// RebuildShard updates the SrvShard objects and underlying serving graph. // // Re-read from TopologyServer to make sure we are using the side // effects of all actions. // // This function locks individual SvrShard paths, so it doesn't need a lock // on the shard. -func RebuildShard(ctx context.Context, log logutil.Logger, ts topo.Server, keyspace, shard string, cells []string, timeout time.Duration, interrupted chan struct{}) (*topo.ShardInfo, error) { +func RebuildShard(ctx context.Context, log logutil.Logger, ts topo.Server, keyspace, shard string, cells []string, lockTimeout time.Duration) (*topo.ShardInfo, error) { log.Infof("RebuildShard %v/%v", keyspace, shard) span := trace.NewSpanFromContext(ctx) @@ -65,7 +65,9 @@ func RebuildShard(ctx context.Context, log logutil.Logger, ts topo.Server, keysp // Lock the SrvShard so we don't race with other rebuilds of the same // shard in the same cell (e.g. from our peer tablets). actionNode := actionnode.RebuildSrvShard() - lockPath, err := actionNode.LockSrvShard(ctx, ts, cell, keyspace, shard, timeout, interrupted) + lockCtx, cancel := context.WithTimeout(ctx, lockTimeout) + lockPath, err := actionNode.LockSrvShard(lockCtx, ts, cell, keyspace, shard) + cancel() if err != nil { rec.RecordError(err) return @@ -81,9 +83,6 @@ func RebuildShard(ctx context.Context, log logutil.Logger, ts topo.Server, keysp // add all relevant tablets to the map for _, rl := range sri.ReplicationLinks { tabletsAsMap[rl.TabletAlias] = true - if rl.Parent.Cell == cell { - tabletsAsMap[rl.Parent] = true - } } // convert the map to a list @@ -93,7 +92,7 @@ func RebuildShard(ctx context.Context, log logutil.Logger, ts topo.Server, keysp } // read all the Tablet records - tablets, err := topo.GetTabletMap(ts, aliases) + tablets, err := topo.GetTabletMap(ctx, ts, aliases) switch err { case nil: // keep going, we're good @@ -108,7 +107,7 @@ func RebuildShard(ctx context.Context, log logutil.Logger, ts topo.Server, keysp rebuildErr := rebuildCellSrvShard(ctx, log, ts, shardInfo, cell, tablets) // and unlock - if err := actionNode.UnlockSrvShard(ts, cell, keyspace, shard, lockPath, rebuildErr); err != nil { + if err := actionNode.UnlockSrvShard(ctx, ts, cell, keyspace, shard, lockPath, rebuildErr); err != nil { rec.RecordError(err) } }(cell) @@ -142,9 +141,7 @@ func rebuildCellSrvShard(ctx context.Context, log logutil.Logger, ts topo.Server if !tablet.IsInReplicationGraph() { // only valid case is a scrapped master in the // catastrophic reparent case - if tablet.Parent.Uid != topo.NO_TABLET { - log.Warningf("Tablet %v should not be in the replication graph, please investigate (it is being ignored in the rebuild)", tablet.Alias) - } + log.Warningf("Tablet %v should not be in the replication graph, please investigate (it is being ignored in the rebuild)", tablet.Alias) continue } diff --git a/go/vt/topotools/rebuild_test.go b/go/vt/topotools/rebuild_test.go index 05cb9ff01a0..e4758066965 100644 --- a/go/vt/topotools/rebuild_test.go +++ b/go/vt/topotools/rebuild_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/topo" @@ -24,8 +24,6 @@ func TestRebuildShardRace(t *testing.T) { ctx := context.Background() cells := []string{"test_cell"} logger := logutil.NewMemoryLogger() - timeout := 10 * time.Second - interrupted := make(chan struct{}) // Set up topology. ts := zktopo.NewTestServer(t, cells) @@ -38,7 +36,7 @@ func TestRebuildShardRace(t *testing.T) { f.AddTablet(2, "test_cell", topo.TYPE_REPLICA, master) // Do an initial rebuild. - if _, err := RebuildShard(ctx, logger, f.Topo, keyspace, shard, cells, timeout, interrupted); err != nil { + if _, err := RebuildShard(ctx, logger, f.Topo, keyspace, shard, cells, time.Minute); err != nil { t.Fatalf("RebuildShard: %v", err) } @@ -80,7 +78,7 @@ func TestRebuildShardRace(t *testing.T) { t.Fatalf("UpdateTablet: %v", err) } go func() { - if _, err := RebuildShard(ctx, logger, f.Topo, keyspace, shard, cells, timeout, interrupted); err != nil { + if _, err := RebuildShard(ctx, logger, f.Topo, keyspace, shard, cells, time.Minute); err != nil { t.Fatalf("RebuildShard: %v", err) } close(done) @@ -96,7 +94,7 @@ func TestRebuildShardRace(t *testing.T) { if err := topo.UpdateTablet(ctx, ts, replicaInfo); err != nil { t.Fatalf("UpdateTablet: %v", err) } - if _, err := RebuildShard(ctx, logger, f.Topo, keyspace, shard, cells, timeout, interrupted); err != nil { + if _, err := RebuildShard(ctx, logger, f.Topo, keyspace, shard, cells, time.Minute); err != nil { t.Fatalf("RebuildShard: %v", err) } diff --git a/go/vt/topotools/reparent.go b/go/vt/topotools/reparent.go index 4ad7f91a85a..c68012c8fec 100644 --- a/go/vt/topotools/reparent.go +++ b/go/vt/topotools/reparent.go @@ -5,13 +5,12 @@ package topotools // This file contains utility functions for reparenting. It is used by -// the wrangler, and by the new master tablet for -// ShardExternallyReparented. +// the new master tablet for TabletExternallyReparented. import ( "sync" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" @@ -50,12 +49,11 @@ func RestartSlavesExternal(ts topo.Server, log logutil.Logger, slaveTabletMap, m if err != nil { // the old master can be annoying if left // around in the replication graph, so if we - // can't restart it, we just scrap it. + // can't restart it, we just make it spare. // We don't rebuild the Shard just yet though. log.Warningf("Old master %v is not restarting in time, forcing it to spare: %v", ti.Alias, err) ti.Type = topo.TYPE_SPARE - ti.Parent = masterElectTabletAlias if err := topo.UpdateTablet(context.TODO(), ts, ti); err != nil { log.Warningf("Failed to change old master %v to spare: %v", ti.Alias, err) } diff --git a/go/vt/topotools/tablet.go b/go/vt/topotools/tablet.go index 3fd59200755..6501810f813 100644 --- a/go/vt/topotools/tablet.go +++ b/go/vt/topotools/tablet.go @@ -13,7 +13,7 @@ level. In particular, it cannot depend on: topotools is used by wrangler, so it ends up in all tools using wrangler (vtctl, vtctld, ...). It is also included by vttablet, so it contains: - most of the logic to rebuild a shard serving graph (helthcheck module) -- some of the logic to perform a ShardExternallyReparented (RPC call +- some of the logic to perform a TabletExternallyReparented (RPC call to master vttablet to let it know it's the master). */ @@ -23,12 +23,10 @@ package topotools import ( "fmt" - "sync" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" log "github.com/golang/glog" - "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/hook" "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/topo" @@ -50,7 +48,7 @@ func ConfigureTabletHook(hk *hook.Hook, tabletAlias topo.TabletAlias) { // probably dead. So if 'force' is true, we will also remove pending // remote actions. And if 'force' is false, we also run an optional // hook. -func Scrap(ts topo.Server, tabletAlias topo.TabletAlias, force bool) error { +func Scrap(ctx context.Context, ts topo.Server, tabletAlias topo.TabletAlias, force bool) error { tablet, err := ts.GetTablet(tabletAlias) if err != nil { return err @@ -60,9 +58,8 @@ func Scrap(ts topo.Server, tabletAlias topo.TabletAlias, force bool) error { // be there anyway. wasAssigned := tablet.IsAssigned() tablet.Type = topo.TYPE_SCRAP - tablet.Parent = topo.TabletAlias{} // Update the tablet first, since that is canonical. - err = topo.UpdateTablet(context.TODO(), ts, tablet) + err = topo.UpdateTablet(ctx, ts, tablet) if err != nil { return err } @@ -102,7 +99,7 @@ func Scrap(ts topo.Server, tabletAlias topo.TabletAlias, force bool) error { // - if health is nil, we don't touch the Tablet's Health record. // - if health is an empty map, we clear the Tablet's Health record. // - if health has values, we overwrite the Tablet's Health record. -func ChangeType(ts topo.Server, tabletAlias topo.TabletAlias, newType topo.TabletType, health map[string]string, runHooks bool) error { +func ChangeType(ctx context.Context, ts topo.Server, tabletAlias topo.TabletAlias, newType topo.TabletType, health map[string]string, runHooks bool) error { tablet, err := ts.GetTablet(tabletAlias) if err != nil { return err @@ -124,35 +121,6 @@ func ChangeType(ts topo.Server, tabletAlias topo.TabletAlias, newType topo.Table tablet.Type = newType if newType == topo.TYPE_IDLE { - if tablet.Parent.IsZero() { - si, err := ts.GetShard(tablet.Keyspace, tablet.Shard) - if err != nil { - return err - } - rec := concurrency.AllErrorRecorder{} - wg := sync.WaitGroup{} - for _, cell := range si.Cells { - wg.Add(1) - go func(cell string) { - defer wg.Done() - sri, err := ts.GetShardReplication(cell, tablet.Keyspace, tablet.Shard) - if err != nil { - log.Warningf("Cannot check cell %v for extra replication paths, assuming it's good", cell) - return - } - for _, rl := range sri.ReplicationLinks { - if rl.Parent == tabletAlias { - rec.RecordError(fmt.Errorf("Still have a ReplicationLink in cell %v", cell)) - } - } - }(cell) - } - wg.Wait() - if rec.HasErrors() { - return rec.Error() - } - } - tablet.Parent = topo.TabletAlias{} tablet.Keyspace = "" tablet.Shard = "" tablet.KeyRange = key.KeyRange{} @@ -165,5 +133,5 @@ func ChangeType(ts topo.Server, tabletAlias topo.TabletAlias, newType topo.Table tablet.Health = health } } - return topo.UpdateTablet(context.TODO(), ts, tablet) + return topo.UpdateTablet(ctx, ts, tablet) } diff --git a/go/vt/topotools/topology.go b/go/vt/topotools/topology.go index 821ce92006a..cf64498999d 100644 --- a/go/vt/topotools/topology.go +++ b/go/vt/topotools/topology.go @@ -2,12 +2,16 @@ package topotools import ( "fmt" + "net" "sort" "strconv" "strings" "sync" + "golang.org/x/net/context" + log "github.com/golang/glog" + "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/topo" ) @@ -21,12 +25,17 @@ type TabletNode struct { } // ShortName returns a displayable representation of the host name. +// If the host is an IP address instead of a name, it is not shortened. func (tn *TabletNode) ShortName() string { + if net.ParseIP(tn.Host) != nil { + return netutil.JoinHostPort(tn.Host, tn.Port) + } + hostPart := strings.SplitN(tn.Host, ".", 2)[0] if tn.Port == 0 { return hostPart } - return fmt.Sprintf("%v:%v", hostPart, tn.Port) + return netutil.JoinHostPort(hostPart, tn.Port) } func newTabletNodeFromTabletInfo(ti *topo.TabletInfo) *TabletNode { @@ -130,7 +139,7 @@ func (ks *KeyspaceNodes) hasOnlyNumericShardNames() bool { // TabletTypes returns a slice of tablet type names this ks // contains. func (ks KeyspaceNodes) TabletTypes() []topo.TabletType { - contained := make([]topo.TabletType, 0) + var contained []topo.TabletType for _, t := range topo.AllTabletTypes { if ks.HasType(t) { contained = append(contained, t) @@ -158,7 +167,7 @@ type Topology struct { } // DbTopology returns the Topology for the topo server. -func DbTopology(ts topo.Server) (*Topology, error) { +func DbTopology(ctx context.Context, ts topo.Server) (*Topology, error) { topology := &Topology{ Assigned: make(map[string]*KeyspaceNodes), Idle: make([]*TabletNode, 0), @@ -166,7 +175,7 @@ func DbTopology(ts topo.Server) (*Topology, error) { Partial: false, } - tabletInfos, err := GetAllTabletsAccrossCells(ts) + tabletInfos, err := GetAllTabletsAcrossCells(ctx, ts) switch err { case nil: // we're good, no error diff --git a/go/vt/topotools/topology_test.go b/go/vt/topotools/topology_test.go new file mode 100644 index 00000000000..e96e8a86693 --- /dev/null +++ b/go/vt/topotools/topology_test.go @@ -0,0 +1,88 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package topotools + +import ( + "reflect" + "sort" + "testing" + + "github.com/youtube/vitess/go/vt/topo" +) + +func TestTabletNodeShortName(t *testing.T) { + table := map[TabletNode]string{ + TabletNode{Host: "hostname", Port: 0}: "hostname", + TabletNode{Host: "hostname", Port: 123}: "hostname:123", + TabletNode{Host: "hostname.domain", Port: 456}: "hostname:456", + TabletNode{Host: "12.34.56.78", Port: 555}: "12.34.56.78:555", + TabletNode{Host: "::1", Port: 789}: "[::1]:789", + } + for input, want := range table { + if got := input.ShortName(); got != want { + t.Errorf("ShortName(%v:%v) = %q, want %q", input.Host, input.Port, got, want) + } + } +} + +func TestNumericShardNodesList(t *testing.T) { + input := numericShardNodesList{ + &ShardNodes{Name: "3"}, + &ShardNodes{Name: "5"}, + &ShardNodes{Name: "4"}, + &ShardNodes{Name: "7"}, + &ShardNodes{Name: "10"}, + &ShardNodes{Name: "1"}, + &ShardNodes{Name: "0"}, + } + want := numericShardNodesList{ + &ShardNodes{Name: "0"}, + &ShardNodes{Name: "1"}, + &ShardNodes{Name: "3"}, + &ShardNodes{Name: "4"}, + &ShardNodes{Name: "5"}, + &ShardNodes{Name: "7"}, + &ShardNodes{Name: "10"}, + } + sort.Sort(input) + if !reflect.DeepEqual(input, want) { + t.Errorf("Sort(numericShardNodesList) failed") + } +} + +func TestRangeShardNodesList(t *testing.T) { + input := rangeShardNodesList{ + &ShardNodes{Name: "50-60"}, + &ShardNodes{Name: "80-"}, + &ShardNodes{Name: "70-80"}, + &ShardNodes{Name: "30-40"}, + &ShardNodes{Name: "-10"}, + } + want := rangeShardNodesList{ + &ShardNodes{Name: "-10"}, + &ShardNodes{Name: "30-40"}, + &ShardNodes{Name: "50-60"}, + &ShardNodes{Name: "70-80"}, + &ShardNodes{Name: "80-"}, + } + sort.Sort(input) + if !reflect.DeepEqual(input, want) { + t.Errorf("Sort(rangeShardNodesList) failed") + } +} + +func TestKeyspaceNodesTabletTypes(t *testing.T) { + input := KeyspaceNodes{ + ShardNodes: []*ShardNodes{ + &ShardNodes{TabletNodes: TabletNodesByType{topo.TYPE_REPLICA: nil}}, + &ShardNodes{TabletNodes: TabletNodesByType{topo.TYPE_MASTER: nil, topo.TYPE_REPLICA: nil}}, + }, + } + want := topo.MakeStringTypeList([]topo.TabletType{topo.TYPE_REPLICA, topo.TYPE_MASTER}) + got := topo.MakeStringTypeList(input.TabletTypes()) + if !reflect.DeepEqual(got, want) { + t.Errorf("KeyspaceNodes.TabletTypes() = %v, want %v", got, want) + } +} diff --git a/go/vt/topotools/utils.go b/go/vt/topotools/utils.go index fb20f550a0d..f20fc07a0b9 100644 --- a/go/vt/topotools/utils.go +++ b/go/vt/topotools/utils.go @@ -9,6 +9,8 @@ import ( "sort" "sync" + "golang.org/x/net/context" + log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/topo" ) @@ -24,14 +26,14 @@ func FindTabletByIPAddrAndPort(tabletMap map[topo.TabletAlias]*topo.TabletInfo, } // GetAllTablets returns a sorted list of tablets. -func GetAllTablets(ts topo.Server, cell string) ([]*topo.TabletInfo, error) { +func GetAllTablets(ctx context.Context, ts topo.Server, cell string) ([]*topo.TabletInfo, error) { aliases, err := ts.GetTabletsByCell(cell) if err != nil { return nil, err } sort.Sort(topo.TabletAliasList(aliases)) - tabletMap, err := topo.GetTabletMap(ts, aliases) + tabletMap, err := topo.GetTabletMap(ctx, ts, aliases) if err != nil { // we got another error than topo.ErrNoNode return nil, err @@ -51,9 +53,9 @@ func GetAllTablets(ts topo.Server, cell string) ([]*topo.TabletInfo, error) { return tablets, nil } -// GetAllTabletsAccrossCells returns all tablets from known cells. +// GetAllTabletsAcrossCells returns all tablets from known cells. // If it returns topo.ErrPartialResult, then the list is valid, but partial. -func GetAllTabletsAccrossCells(ts topo.Server) ([]*topo.TabletInfo, error) { +func GetAllTabletsAcrossCells(ctx context.Context, ts topo.Server) ([]*topo.TabletInfo, error) { cells, err := ts.GetKnownCells() if err != nil { return nil, err @@ -65,15 +67,15 @@ func GetAllTabletsAccrossCells(ts topo.Server) ([]*topo.TabletInfo, error) { wg.Add(len(cells)) for i, cell := range cells { go func(i int, cell string) { - results[i], errors[i] = GetAllTablets(ts, cell) + results[i], errors[i] = GetAllTablets(ctx, ts, cell) wg.Done() }(i, cell) } wg.Wait() err = nil - allTablets := make([]*topo.TabletInfo, 0) - for i, _ := range cells { + var allTablets []*topo.TabletInfo + for i := range cells { if errors[i] == nil { allTablets = append(allTablets, results[i]...) } else { @@ -95,7 +97,7 @@ func SortedTabletMap(tabletMap map[topo.TabletAlias]*topo.TabletInfo) (map[topo. for alias, ti := range tabletMap { if ti.Type != topo.TYPE_MASTER && ti.Type != topo.TYPE_SCRAP { slaveMap[alias] = ti - } else if ti.Parent.Uid == topo.NO_TABLET { + } else { masterMap[alias] = ti } } @@ -114,7 +116,7 @@ func CopyMapKeys(m interface{}, typeHint interface{}) interface{} { return keys.Interface() } -// CopyMapKeys copies values from from map m into a new slice with the +// CopyMapValues copies values from from map m into a new slice with the // type specified by typeHint. Reflection can't make a new slice type // just based on the key type AFAICT. func CopyMapValues(m interface{}, typeHint interface{}) interface{} { diff --git a/go/vt/vtctl/gorpcvtctlserver/server.go b/go/vt/vtctl/gorpcvtctlserver/server.go index d71a529aa62..5faf4d9f181 100644 --- a/go/vt/vtctl/gorpcvtctlserver/server.go +++ b/go/vt/vtctl/gorpcvtctlserver/server.go @@ -11,13 +11,13 @@ package gorpcvtctlserver import ( "sync" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/vt/logutil" "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtctl" "github.com/youtube/vitess/go/vt/vtctl/gorpcproto" "github.com/youtube/vitess/go/vt/wrangler" + "golang.org/x/net/context" ) // VtctlServer is our RPC server @@ -27,7 +27,7 @@ type VtctlServer struct { // ExecuteVtctlCommand is the server side method that will execute the query, // and stream the results. -func (s *VtctlServer) ExecuteVtctlCommand(context context.Context, query *gorpcproto.ExecuteVtctlCommandArgs, sendReply func(interface{}) error) error { +func (s *VtctlServer) ExecuteVtctlCommand(ctx context.Context, query *gorpcproto.ExecuteVtctlCommandArgs, sendReply func(interface{}) error) error { // create a logger, send the result back to the caller logstream := logutil.NewChannelLogger(10) logger := logutil.NewTeeLogger(logstream, logutil.NewConsoleLogger()) @@ -47,10 +47,13 @@ func (s *VtctlServer) ExecuteVtctlCommand(context context.Context, query *gorpcp }() // create the wrangler - wr := wrangler.New(logger, s.ts, query.ActionTimeout, query.LockTimeout) + wr := wrangler.New(logger, s.ts, query.LockTimeout) + // FIXME(alainjobart) use a single context, copy the source info from it + ctx, cancel := context.WithTimeout(context.TODO(), query.ActionTimeout) // execute the command - err := vtctl.RunCommand(wr, query.Args) + err := vtctl.RunCommand(ctx, wr, query.Args) + cancel() // close the log channel, and wait for them all to be sent close(logstream) diff --git a/go/vt/vtctl/plugin_etcdtopo.go b/go/vt/vtctl/plugin_etcdtopo.go new file mode 100644 index 00000000000..5975353ba97 --- /dev/null +++ b/go/vt/vtctl/plugin_etcdtopo.go @@ -0,0 +1,11 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vtctl + +// This plugin imports etcdtopo to register the etcd implementation of TopoServer. + +import ( + _ "github.com/youtube/vitess/go/vt/etcdtopo" +) diff --git a/go/vt/vtctl/plugin_zktopo.go b/go/vt/vtctl/plugin_zktopo.go index 724bcc8fdac..6453dffd368 100644 --- a/go/vt/vtctl/plugin_zktopo.go +++ b/go/vt/vtctl/plugin_zktopo.go @@ -10,12 +10,14 @@ package vtctl import ( "flag" "fmt" + "strings" "sync" "github.com/youtube/vitess/go/sync2" "github.com/youtube/vitess/go/vt/wrangler" "github.com/youtube/vitess/go/vt/zktopo" "github.com/youtube/vitess/go/zk" + "golang.org/x/net/context" ) func init() { @@ -38,8 +40,6 @@ func init() { "", "(requires zktopo.Server)\n" + "Export the serving graph entries to the zkns format."}) - - resolveWildcards = zkResolveWildcards } func zkResolveWildcards(wr *wrangler.Wrangler, args []string) ([]string, error) { @@ -50,17 +50,17 @@ func zkResolveWildcards(wr *wrangler.Wrangler, args []string) ([]string, error) return zk.ResolveWildcards(zkts.GetZConn(), args) } -func commandPruneActionLogs(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandPruneActionLogs(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { keepCount := subFlags.Int("keep-count", 10, "count to keep") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() == 0 { - return fmt.Errorf("action PruneActionLogs requires ...") + return fmt.Errorf("action PruneActionLogs requires [...]") } - paths, err := resolveWildcards(wr, subFlags.Args()) + paths, err := zkResolveWildcards(wr, subFlags.Args()) if err != nil { return err } @@ -92,30 +92,54 @@ func commandPruneActionLogs(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args return nil } -func commandExportZkns(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandExportZkns(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { return fmt.Errorf("action ExportZkns requires ") } - cell, err := vtPathToCell(subFlags.Arg(0)) + cell, err := zkVtPathToCell(subFlags.Arg(0)) if err != nil { return err } return wr.ExportZkns(cell) } -func commandExportZknsForKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandExportZknsForKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { return fmt.Errorf("action ExportZknsForKeyspace requires ") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) + keyspace, err := zkKeyspaceParamToKeyspace(subFlags.Arg(0)) if err != nil { return err } - return wr.ExportZknsForKeyspace(keyspace) + return wr.ExportZknsForKeyspace(ctx, keyspace) +} + +func zkVtPathToCell(param string) (string, error) { + if param[0] == '/' { + // old zookeeper replication path like /zk//vt + zkPathParts := strings.Split(param, "/") + if len(zkPathParts) != 4 || zkPathParts[0] != "" || zkPathParts[1] != "zk" || zkPathParts[3] != "vt" { + return "", fmt.Errorf("Invalid vt path: %v", param) + } + return zkPathParts[2], nil + } + return param, nil +} + +func zkKeyspaceParamToKeyspace(param string) (string, error) { + if param[0] == '/' { + // old zookeeper path, convert to new-style string keyspace + zkPathParts := strings.Split(param, "/") + if len(zkPathParts) != 6 || zkPathParts[0] != "" || zkPathParts[1] != "zk" || zkPathParts[2] != "global" || zkPathParts[3] != "vt" || zkPathParts[4] != "keyspaces" { + return "", fmt.Errorf("Invalid keyspace path: %v", param) + } + return zkPathParts[5], nil + } + return param, nil } diff --git a/go/vt/vtctl/reparent.go b/go/vt/vtctl/reparent.go index b785dbb9076..449a9264746 100644 --- a/go/vt/vtctl/reparent.go +++ b/go/vt/vtctl/reparent.go @@ -7,39 +7,39 @@ package vtctl import ( "flag" "fmt" + "time" - "code.google.com/p/go.net/context" - - _ "github.com/youtube/vitess/go/vt/logutil" + "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/wrangler" + "golang.org/x/net/context" ) func init() { addCommand("Tablets", command{ "DemoteMaster", commandDemoteMaster, - "", + "", "Demotes a master tablet."}) addCommand("Tablets", command{ "ReparentTablet", commandReparentTablet, - "", + "", "Reparent a tablet to the current master in the shard. This only works if the current slave position matches the last known reparent action."}) addCommand("Shards", command{ "ReparentShard", commandReparentShard, - "[-force] [-leave-master-read-only] ", + "[-force] [-leave-master-read-only] ", "Specify which shard to reparent and which tablet should be the new master."}) } -func commandDemoteMaster(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandDemoteMaster(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action DemoteMaster requires ") + return fmt.Errorf("action DemoteMaster requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -47,40 +47,41 @@ func commandDemoteMaster(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []s if err != nil { return err } - return wr.TabletManagerClient().DemoteMaster(context.TODO(), tabletInfo, wr.ActionTimeout()) + return wr.TabletManagerClient().DemoteMaster(ctx, tabletInfo) } -func commandReparentTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandReparentTablet(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ReparentTablet requires ") + return fmt.Errorf("action ReparentTablet requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } - return wr.ReparentTablet(tabletAlias) + return wr.ReparentTablet(ctx, tabletAlias) } -func commandReparentShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandReparentShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { leaveMasterReadOnly := subFlags.Bool("leave-master-read-only", false, "leaves the master read-only after reparenting") force := subFlags.Bool("force", false, "will force the reparent even if the master is already correct") + waitSlaveTimeout := subFlags.Duration("wait_slave_timeout", 30*time.Second, "time to wait for slaves to catch up in reparenting") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action ReparentShard requires ") + return fmt.Errorf("action ReparentShard requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(1)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(1)) if err != nil { return err } - return wr.ReparentShard(keyspace, shard, tabletAlias, *leaveMasterReadOnly, *force) + return wr.ReparentShard(ctx, keyspace, shard, tabletAlias, *leaveMasterReadOnly, *force, *waitSlaveTimeout) } diff --git a/go/vt/vtctl/vtctl.go b/go/vt/vtctl/vtctl.go index cb7bf240a65..c7adb040bc7 100644 --- a/go/vt/vtctl/vtctl.go +++ b/go/vt/vtctl/vtctl.go @@ -15,10 +15,10 @@ import ( "strings" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/flagutil" "github.com/youtube/vitess/go/jscfg" + "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/vt/client2" hk "github.com/youtube/vitess/go/vt/hook" "github.com/youtube/vitess/go/vt/key" @@ -28,16 +28,17 @@ import ( "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" "github.com/youtube/vitess/go/vt/wrangler" + "golang.org/x/net/context" ) var ( - // Error returned for an unknown command + // ErrUnknownCommand is returned for an unknown command ErrUnknownCommand = errors.New("unknown command") ) type command struct { name string - method func(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error + method func(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error params string help string // if help is empty, won't list the command } @@ -56,101 +57,98 @@ var commands = []commandGroup{ "Valid :\n" + " " + strings.Join(topo.MakeStringTypeList(topo.AllTabletTypes), " ")}, command{"GetTablet", commandGetTablet, - "", + "", "Outputs the json version of Tablet to stdout."}, command{"UpdateTabletAddrs", commandUpdateTabletAddrs, - "[-hostname ] [-ip-addr ] [-mysql-port ] [-vt-port ] [-vts-port ] ", + "[-hostname ] [-ip-addr ] [-mysql-port ] [-vt-port ] [-vts-port ] ", "Updates the addresses of a tablet."}, command{"ScrapTablet", commandScrapTablet, - "[-force] [-skip-rebuild] ", + "[-force] [-skip-rebuild] ", "Scraps a tablet."}, command{"DeleteTablet", commandDeleteTablet, - " ...", + " ...", "Deletes scrapped tablet(s) from the topology."}, command{"SetReadOnly", commandSetReadOnly, - "[]", + "[]", "Sets the tablet as ReadOnly."}, command{"SetReadWrite", commandSetReadWrite, - "[]", + "[]", "Sets the tablet as ReadWrite."}, command{"ChangeSlaveType", commandChangeSlaveType, - "[-force] [-dry-run] ", + "[-force] [-dry-run] ", "Change the db type for this tablet if possible. This is mostly for arranging replicas - it will not convert a master.\n" + "NOTE: This will automatically update the serving graph.\n" + "Valid :\n" + " " + strings.Join(topo.MakeStringTypeList(topo.SlaveTabletTypes), " ")}, command{"Ping", commandPing, - "", + "", "Check that the agent is awake and responding to RPCs. Can be blocked by other in-flight operations."}, command{"RefreshState", commandRefreshState, - "", + "", "Asks a remote tablet to reload its tablet record."}, command{"RunHealthCheck", commandRunHealthCheck, " ", "Asks a remote tablet to run a health check with the providd target type."}, + command{"HealthStream", commandHealthStream, + "", + "Streams the health status out of a tablet."}, command{"Query", commandQuery, " ", "Send a SQL query to a tablet."}, command{"Sleep", commandSleep, - " ", + " ", "Block the action queue for the specified duration (mostly for testing)."}, command{"Snapshot", commandSnapshot, - "[-force] [-server-mode] [-concurrency=4] ", + "[-force] [-server-mode] [-concurrency=4] ", "Stop mysqld and copy compressed data aside."}, command{"SnapshotSourceEnd", commandSnapshotSourceEnd, - "[-slave-start] [-read-write] ", + "[-slave-start] [-read-write] ", "Restart Mysql and restore original server type." + "Valid :\n" + " " + strings.Join(topo.MakeStringTypeList(topo.AllTabletTypes), " ")}, command{"Restore", commandRestore, - "[-fetch-concurrency=3] [-fetch-retry-count=3] [-dont-wait-for-slave-start] []", + "[-fetch-concurrency=3] [-fetch-retry-count=3] [-dont-wait-for-slave-start] []", "Copy the given snaphot from the source tablet and restart replication to the new master path (or uses the if not specified). If is 'default', uses the default value.\n" + "NOTE: This does not wait for replication to catch up. The destination tablet must be 'idle' to begin with. It will transition to 'spare' once the restore is complete."}, command{"Clone", commandClone, - "[-force] [-concurrency=4] [-fetch-concurrency=3] [-fetch-retry-count=3] [-server-mode] ...", + "[-force] [-concurrency=4] [-fetch-concurrency=3] [-fetch-retry-count=3] [-server-mode] ...", "This performs Snapshot and then Restore on all the targets in parallel. The advantage of having separate actions is that one snapshot can be used for many restores, and it's then easier to spread them over time."}, - command{"MultiSnapshot", commandMultiSnapshot, - "[-force] [-concurrency=8] [-skip-slave-restart] [-maximum-file-size=134217728] -spec='-' [-tables=''] [-exclude_tables=''] ", - "Locks mysqld and copy compressed data aside."}, - command{"MultiRestore", commandMultiRestore, - "[-force] [-concurrency=4] [-fetch-concurrency=4] [-insert-table-concurrency=4] [-fetch-retry-count=3] [-strategy=] ...", - "Restores a snapshot from multiple hosts."}, command{"ExecuteHook", commandExecuteHook, - " [ ...]", + " [ ...]", "This runs the specified hook on the given tablet."}, command{"ExecuteFetch", commandExecuteFetch, - "[--max_rows=10000] [--want_fields] [--disable_binlogs] ", + "[--max_rows=10000] [--want_fields] [--disable_binlogs] ", "Runs the given sql command as a DBA on the remote tablet"}, }, }, commandGroup{ "Shards", []command{ command{"CreateShard", commandCreateShard, - "[-force] [-parent] ", + "[-force] [-parent] ", "Creates the given shard"}, command{"GetShard", commandGetShard, - "", + "", "Outputs the json version of Shard to stdout."}, command{"RebuildShardGraph", commandRebuildShardGraph, - "[-cells=a,b] ... (/zk/global/vt/keyspaces//shards/)", + "[-cells=a,b] ... ", "Rebuild the replication graph and shard serving data in zk. This may trigger an update to all connected clients."}, - command{"ShardExternallyReparented", commandShardExternallyReparented, - "[-use_rpc] ", + command{"TabletExternallyReparented", commandTabletExternallyReparented, + "", "Changes metadata to acknowledge a shard master change performed by an external tool."}, command{"ValidateShard", commandValidateShard, - "[-ping-tablets] ", + "[-ping-tablets] ", "Validate all nodes reachable from this shard are consistent."}, command{"ShardReplicationPositions", commandShardReplicationPositions, - "", + "", "Show slave status on all machines in the shard graph."}, command{"ListShardTablets", commandListShardTablets, - ")", + ")", "List all tablets in a given shard."}, command{"SetShardServedTypes", commandSetShardServedTypes, - " [,,...]", + " [,,...]", "Sets a given shard's served types. Does not rebuild any serving graph."}, command{"SetShardTabletControl", commandSetShardTabletControl, - "[--cells=c1,c2,...] [--blacklisted_tables=t1,t2,...] [--remove] [--disable_query_service] ", + "[--cells=c1,c2,...] [--blacklisted_tables=t1,t2,...] [--remove] [--disable_query_service] ", "Sets the TabletControl record for a shard and type. Only use this for an emergency fix, or after a finished vertical split. MigrateServedFrom and MigrateServedType will set this field appropriately already. Always specify blacklisted_tables for vertical splits, never for horizontal splits."}, command{"SourceShardDelete", commandSourceShardDelete, " ", @@ -158,116 +156,126 @@ var commands = []commandGroup{ command{"SourceShardAdd", commandSourceShardAdd, "[--key_range=] [--tables=] ", "Adds the SourceShard record with the provided index. This is meant as an emergency function. Does not RefreshState the shard master."}, - command{"ShardMultiRestore", commandShardMultiRestore, - "[-force] [-concurrency=4] [-fetch-concurrency=4] [-insert-table-concurrency=4] [-fetch-retry-count=3] [-strategy=] [-tables=,,...] ...", - "Restore multi-snapshots on all the tablets of a shard."}, command{"ShardReplicationAdd", commandShardReplicationAdd, - " ", + " ", "HIDDEN Adds an entry to the replication graph in the given cell"}, command{"ShardReplicationRemove", commandShardReplicationRemove, - " ", + " ", "HIDDEN Removes an entry to the replication graph in the given cell"}, command{"ShardReplicationFix", commandShardReplicationFix, - " ", + " ", "Walks through a ShardReplication object and fixes the first error it encrounters"}, command{"RemoveShardCell", commandRemoveShardCell, - "[-force] ", + "[-force] ", "Removes the cell in the shard's Cells list."}, command{"DeleteShard", commandDeleteShard, - " ...", + " ...", "Deletes the given shard(s)"}, }, }, commandGroup{ "Keyspaces", []command{ command{"CreateKeyspace", commandCreateKeyspace, - "[-sharding_column_name=name] [-sharding_column_type=type] [-served_from=tablettype1:ks1,tablettype2,ks2,...] [-split_shard_count=N] [-force] ", + "[-sharding_column_name=name] [-sharding_column_type=type] [-served_from=tablettype1:ks1,tablettype2,ks2,...] [-split_shard_count=N] [-force] ", "Creates the given keyspace"}, command{"GetKeyspace", commandGetKeyspace, - "", + "", "Outputs the json version of Keyspace to stdout."}, command{"SetKeyspaceShardingInfo", commandSetKeyspaceShardingInfo, - "[-force] [-split_shard_count=N] [] []", + "[-force] [-split_shard_count=N] [] []", "Updates the sharding info for a keyspace"}, command{"SetKeyspaceServedFrom", commandSetKeyspaceServedFrom, "[-source=] [-remove] [-cells=c1,c2,...] ", "Manually change the ServedFromMap. Only use this for an emergency fix. MigrateServedFrom will set this field appropriately already. Does not rebuild the serving graph."}, command{"RebuildKeyspaceGraph", commandRebuildKeyspaceGraph, - "[-cells=a,b] ... (/zk/global/vt/keyspaces/)", + "[-cells=a,b] ...", "Rebuild the serving data for all shards in this keyspace. This may trigger an update to all connected clients."}, command{"ValidateKeyspace", commandValidateKeyspace, - "[-ping-tablets] ", + "[-ping-tablets] ", "Validate all nodes reachable from this keyspace are consistent."}, command{"MigrateServedTypes", commandMigrateServedTypes, - "[-cells=c1,c2,...] [-reverse] [-skip-refresh-state] ", + "[-cells=c1,c2,...] [-reverse] [-skip-refresh-state] ", "Migrates a serving type from the source shard to the shards it replicates to. Will also rebuild the serving graph. keyspace/shard can be any of the involved shards in the migration."}, command{"MigrateServedFrom", commandMigrateServedFrom, - "[-cells=c1,c2,...] [-reverse] ", + "[-cells=c1,c2,...] [-reverse] ", "Makes the destination keyspace/shard serve the given type. Will also rebuild the serving graph."}, + command{"FindAllShardsInKeyspace", commandFindAllShardsInKeyspace, + "", + "Displays all the shards in a keyspace."}, }, }, commandGroup{ "Generic", []command{ command{"Resolve", commandResolve, "..:", - "Read a list of addresses that can answer this query. The port name is usually _mysql or _vtocc."}, + "Read a list of addresses that can answer this query. The port name is usually mysql or vt."}, command{"Validate", commandValidate, "[-ping-tablets]", "Validate all nodes reachable from global replication graph and all tablets in all discoverable cells are consistent."}, command{"RebuildReplicationGraph", commandRebuildReplicationGraph, - ",... ,,...", + ",... ,,...", "HIDDEN This takes the Thor's hammer approach of recovery and should only be used in emergencies. cell1,cell2,... are the canonical source of data for the system. This function uses that canonical data to recover the replication graph, at which point further auditing with Validate can reveal any remaining issues."}, command{"ListAllTablets", commandListAllTablets, - "", + "", "List all tablets in an awk-friendly way."}, command{"ListTablets", commandListTablets, - " ...", + " ...", "List specified tablets in an awk-friendly way."}, }, }, commandGroup{ "Schema, Version, Permissions", []command{ command{"GetSchema", commandGetSchema, - "[-tables=,,...] [-exclude_tables=,,...] [-include-views] ", + "[-tables=,,...] [-exclude_tables=,,...] [-include-views] ", "Display the full schema for a tablet, or just the schema for the provided tables."}, command{"ReloadSchema", commandReloadSchema, - "", + "", "Asks a remote tablet to reload its schema."}, command{"ValidateSchemaShard", commandValidateSchemaShard, - "[-exclude_tables=''] [-include-views] ", + "[-exclude_tables=''] [-include-views] ", "Validate the master schema matches all the slaves."}, command{"ValidateSchemaKeyspace", commandValidateSchemaKeyspace, - "[-exclude_tables=''] [-include-views] ", + "[-exclude_tables=''] [-include-views] ", "Validate the master schema from shard 0 matches all the other tablets in the keyspace."}, command{"PreflightSchema", commandPreflightSchema, - "{-sql= || -sql-file=} ", + "{-sql= || -sql-file=} ", "Apply the schema change to a temporary database to gather before and after schema and validate the change. The sql can be inlined or read from a file."}, command{"ApplySchema", commandApplySchema, - "[-force] {-sql= || -sql-file=} [-skip-preflight] [-stop-replication] ", + "[-force] {-sql= || -sql-file=} [-skip-preflight] [-stop-replication] ", "Apply the schema change to the specified tablet (allowing replication by default). The sql can be inlined or read from a file. Note this doesn't change any tablet state (doesn't go into 'schema' type)."}, command{"ApplySchemaShard", commandApplySchemaShard, - "[-force] {-sql= || -sql-file=} [-simple] [-new-parent=] ", + "[-force] {-sql= || -sql-file=} [-simple] [-new-parent=] ", "Apply the schema change to the specified shard. If simple is specified, we just apply on the live master. Otherwise we will need to do the shell game. So we will apply the schema change to every single slave. if new_parent is set, we will also reparent (otherwise the master won't be touched at all). Using the force flag will cause a bunch of checks to be ignored, use with care."}, command{"ApplySchemaKeyspace", commandApplySchemaKeyspace, - "[-force] {-sql= || -sql-file=} [-simple] ", + "[-force] {-sql= || -sql-file=} [-simple] ", "Apply the schema change to the specified keyspace. If simple is specified, we just apply on the live masters. Otherwise we will need to do the shell game on each shard. So we will apply the schema change to every single slave (running in parallel on all shards, but on one host at a time in a given shard). We will not reparent at the end, so the masters won't be touched at all. Using the force flag will cause a bunch of checks to be ignored, use with care."}, + command{"CopySchemaShard", commandCopySchemaShard, + "[-tables=,,...] [-exclude_tables=,,...] [-include-views] ", + "Copy the schema from a source tablet to the specified shard. The schema is applied directly on the master of the destination shard, and is propogated to the replicas through binlogs"}, command{"ValidateVersionShard", commandValidateVersionShard, - "", + "", "Validate the master version matches all the slaves."}, command{"ValidateVersionKeyspace", commandValidateVersionKeyspace, - "", + "", "Validate the master version from shard 0 matches all the other tablets in the keyspace."}, command{"GetPermissions", commandGetPermissions, - "", + "", "Display the permissions for a tablet."}, command{"ValidatePermissionsShard", commandValidatePermissionsShard, - "", + "", "Validate the master permissions match all the slaves."}, command{"ValidatePermissionsKeyspace", commandValidatePermissionsKeyspace, - "", + "", "Validate the master permissions from shard 0 match all the other tablets in the keyspace."}, + + command{"GetVSchema", commandGetVSchema, + "", + "Display the VTGate routing schema."}, + command{"ApplyVSchema", commandApplyVSchema, + "{-vschema= || -vschema_file=}", + "Apply the VTGate routing schema."}, }, }, commandGroup{ @@ -279,17 +287,17 @@ var commands = []commandGroup{ "", "Outputs a list of keyspace names."}, command{"GetSrvShard", commandGetSrvShard, - " ", + " ", "Outputs the json version of SrvShard to stdout."}, command{"GetEndPoints", commandGetEndPoints, - " ", + " ", "Outputs the json version of EndPoints to stdout."}, }, }, commandGroup{ "Replication Graph", []command{ command{"GetShardReplication", commandGetShardReplication, - " ", + " ", "Outputs the json version of ShardReplication to stdout."}, }, }, @@ -305,10 +313,6 @@ func addCommand(groupName string, c command) { panic(fmt.Errorf("Trying to add to missing group %v", groupName)) } -var resolveWildcards = func(wr *wrangler.Wrangler, args []string) ([]string, error) { - return args, nil -} - func fmtMapAwkable(m map[string]string) string { pairs := make([]string, len(m)) i := 0 @@ -341,16 +345,16 @@ func fmtAction(action *actionnode.ActionNode) string { return fmt.Sprintf("%v %v %v %v %v", action.Path, action.Action, state, action.ActionGuid, action.Error) } -func listTabletsByShard(wr *wrangler.Wrangler, keyspace, shard string) error { - tabletAliases, err := topo.FindAllTabletAliasesInShard(wr.TopoServer(), keyspace, shard) +func listTabletsByShard(ctx context.Context, wr *wrangler.Wrangler, keyspace, shard string) error { + tabletAliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.TopoServer(), keyspace, shard) if err != nil { return err } - return dumpTablets(wr, tabletAliases) + return dumpTablets(ctx, wr, tabletAliases) } -func dumpAllTablets(wr *wrangler.Wrangler, zkVtPath string) error { - tablets, err := topotools.GetAllTablets(wr.TopoServer(), zkVtPath) +func dumpAllTablets(ctx context.Context, wr *wrangler.Wrangler, zkVtPath string) error { + tablets, err := topotools.GetAllTablets(ctx, wr.TopoServer(), zkVtPath) if err != nil { return err } @@ -360,8 +364,8 @@ func dumpAllTablets(wr *wrangler.Wrangler, zkVtPath string) error { return nil } -func dumpTablets(wr *wrangler.Wrangler, tabletAliases []topo.TabletAlias) error { - tabletMap, err := topo.GetTabletMap(wr.TopoServer(), tabletAliases) +func dumpTablets(ctx context.Context, wr *wrangler.Wrangler, tabletAliases []topo.TabletAlias) error { + tabletMap, err := topo.GetTabletMap(ctx, wr.TopoServer(), tabletAliases) if err != nil { return err } @@ -424,23 +428,9 @@ func getFileParam(flag, flagFile, name string) (string, error) { return string(data), nil } -func keyspaceParamToKeyspace(param string) (string, error) { - if param[0] == '/' { - // old zookeeper path, convert to new-style string keyspace - zkPathParts := strings.Split(param, "/") - if len(zkPathParts) != 6 || zkPathParts[0] != "" || zkPathParts[1] != "zk" || zkPathParts[2] != "global" || zkPathParts[3] != "vt" || zkPathParts[4] != "keyspaces" { - return "", fmt.Errorf("Invalid keyspace path: %v", param) - } - return zkPathParts[5], nil - } - return param, nil -} - // keyspaceParamsToKeyspaces builds a list of keyspaces. // It supports topology-based wildcards, and plain wildcards. // For instance: -// /zk/global/vt/keyspaces/one // using plugin_zktopo -// /zk/global/vt/keyspaces/* // using plugin_zktopo // us* // using plain matching // * // using plain matching func keyspaceParamsToKeyspaces(wr *wrangler.Wrangler, params []string) ([]string, error) { @@ -448,16 +438,8 @@ func keyspaceParamsToKeyspaces(wr *wrangler.Wrangler, params []string) ([]string for _, param := range params { if param[0] == '/' { // this is a topology-specific path - zkPaths, err := resolveWildcards(wr, params) - if err != nil { - return nil, fmt.Errorf("Failed to resolve wildcard: %v", err) - } - for _, zkPath := range zkPaths { - subResult, err := keyspaceParamToKeyspace(zkPath) - if err != nil { - return nil, err - } - result = append(result, subResult) + for _, path := range params { + result = append(result, path) } } else { // this is not a path, so assume a keyspace name, @@ -472,26 +454,9 @@ func keyspaceParamsToKeyspaces(wr *wrangler.Wrangler, params []string) ([]string return result, nil } -func shardParamToKeyspaceShard(param string) (string, string, error) { - if param[0] == '/' { - // old zookeeper path, convert to new-style - zkPathParts := strings.Split(param, "/") - if len(zkPathParts) != 8 || zkPathParts[0] != "" || zkPathParts[1] != "zk" || zkPathParts[2] != "global" || zkPathParts[3] != "vt" || zkPathParts[4] != "keyspaces" || zkPathParts[6] != "shards" { - return "", "", fmt.Errorf("Invalid shard path: %v", param) - } - return zkPathParts[5], zkPathParts[7], nil - } - zkPathParts := strings.Split(param, "/") - if len(zkPathParts) != 2 { - return "", "", fmt.Errorf("Invalid shard path: %v", param) - } - return zkPathParts[0], zkPathParts[1], nil -} - // shardParamsToKeyspaceShards builds a list of keyspace/shard pairs. // It supports topology-based wildcards, and plain wildcards. // For instance: -// /zk/global/vt/keyspaces/*/shards/* // using plugin_zktopo // user/* // using plain matching // */0 // using plain matching func shardParamsToKeyspaceShards(wr *wrangler.Wrangler, params []string) ([]topo.KeyspaceShard, error) { @@ -499,12 +464,8 @@ func shardParamsToKeyspaceShards(wr *wrangler.Wrangler, params []string) ([]topo for _, param := range params { if param[0] == '/' { // this is a topology-specific path - zkPaths, err := resolveWildcards(wr, params) - if err != nil { - return nil, fmt.Errorf("Failed to resolve wildcard: %v", err) - } - for _, zkPath := range zkPaths { - keyspace, shard, err := shardParamToKeyspaceShard(zkPath) + for _, path := range params { + keyspace, shard, err := topo.ParseKeyspaceShardString(path) if err != nil { return nil, err } @@ -523,31 +484,13 @@ func shardParamsToKeyspaceShards(wr *wrangler.Wrangler, params []string) ([]topo return result, nil } -// tabletParamToTabletAlias takes either an old style ZK tablet path or a -// new style tablet alias as a string, and returns a TabletAlias. -func tabletParamToTabletAlias(param string) (topo.TabletAlias, error) { - if param[0] == '/' { - // old zookeeper path, convert to new-style string tablet alias - zkPathParts := strings.Split(param, "/") - if len(zkPathParts) != 6 || zkPathParts[0] != "" || zkPathParts[1] != "zk" || zkPathParts[3] != "vt" || zkPathParts[4] != "tablets" { - return topo.TabletAlias{}, fmt.Errorf("Invalid tablet path: %v", param) - } - param = zkPathParts[2] + "-" + zkPathParts[5] - } - result, err := topo.ParseTabletAliasString(param) - if err != nil { - return topo.TabletAlias{}, fmt.Errorf("Invalid tablet alias %v: %v", param, err) - } - return result, nil -} - // tabletParamsToTabletAliases takes multiple params and converts them // to tablet aliases. func tabletParamsToTabletAliases(params []string) ([]topo.TabletAlias, error) { result := make([]topo.TabletAlias, len(params)) var err error for i, param := range params { - result[i], err = tabletParamToTabletAlias(param) + result[i], err = topo.ParseTabletAliasString(param) if err != nil { return nil, err } @@ -555,41 +498,6 @@ func tabletParamsToTabletAliases(params []string) ([]topo.TabletAlias, error) { return result, nil } -// tabletRepParamToTabletAlias takes either an old style ZK tablet replication -// path or a new style tablet alias as a string, and returns a -// TabletAlias. -func tabletRepParamToTabletAlias(param string) (topo.TabletAlias, error) { - if param[0] == '/' { - // old zookeeper replication path, e.g. - // /zk/global/vt/keyspaces/ruser/shards/10-20/nyc-0000200278 - // convert to new-style string tablet alias - zkPathParts := strings.Split(param, "/") - if len(zkPathParts) != 9 || zkPathParts[0] != "" || zkPathParts[1] != "zk" || zkPathParts[2] != "global" || zkPathParts[3] != "vt" || zkPathParts[4] != "keyspaces" || zkPathParts[6] != "shards" { - return topo.TabletAlias{}, fmt.Errorf("Invalid tablet replication path: %v", param) - } - param = zkPathParts[8] - } - result, err := topo.ParseTabletAliasString(param) - if err != nil { - return topo.TabletAlias{}, fmt.Errorf("Invalid tablet alias %v: %v", param, err) - } - return result, nil -} - -// vtPathToCell takes either an old style ZK vt path /zk//vt or -// a new style cell and returns the cell name -func vtPathToCell(param string) (string, error) { - if param[0] == '/' { - // old zookeeper replication path like /zk//vt - zkPathParts := strings.Split(param, "/") - if len(zkPathParts) != 4 || zkPathParts[0] != "" || zkPathParts[1] != "zk" || zkPathParts[3] != "vt" { - return "", fmt.Errorf("Invalid vt path: %v", param) - } - return zkPathParts[2], nil - } - return param, nil -} - // parseTabletType parses the string tablet type and verifies // it is an accepted one func parseTabletType(param string, types []topo.TabletType) (topo.TabletType, error) { @@ -600,7 +508,7 @@ func parseTabletType(param string, types []topo.TabletType) (topo.TabletType, er return tabletType, nil } -func commandInitTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandInitTablet(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { var ( dbNameOverride = subFlags.String("db-name-override", "", "override the name of the db used by vttablet") force = subFlags.Bool("force", false, "will overwrite the node if it already exists") @@ -612,7 +520,6 @@ func commandInitTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []str vtsPort = subFlags.Int("vts_port", 0, "encrypted port for the vttablet process") keyspace = subFlags.String("keyspace", "", "keyspace this tablet belongs to") shard = subFlags.String("shard", "", "shard this tablet belongs to") - parentAlias = subFlags.String("parent_alias", "", "alias of the mysql parent tablet for this tablet") tags flagutil.StringMapValue ) subFlags.Var(&tags, "tags", "comma separated list of key:value pairs used to tag the tablet") @@ -623,7 +530,7 @@ func commandInitTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []str if subFlags.NArg() != 2 { return fmt.Errorf("action InitTablet requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -652,25 +559,19 @@ func commandInitTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []str if *vtsPort != 0 { tablet.Portmap["vts"] = *vtsPort } - if *parentAlias != "" { - tablet.Parent, err = tabletRepParamToTabletAlias(*parentAlias) - if err != nil { - return err - } - } - return wr.InitTablet(tablet, *force, *parent, *update) + return wr.InitTablet(ctx, tablet, *force, *parent, *update) } -func commandGetTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandGetTablet(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action GetTablet requires ") + return fmt.Errorf("action GetTablet requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -681,7 +582,7 @@ func commandGetTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []stri return err } -func commandUpdateTabletAddrs(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandUpdateTabletAddrs(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { hostname := subFlags.String("hostname", "", "fully qualified host name") ipAddr := subFlags.String("ip-addr", "", "IP address") mysqlPort := subFlags.Int("mysql-port", 0, "mysql port") @@ -692,13 +593,13 @@ func commandUpdateTabletAddrs(wr *wrangler.Wrangler, subFlags *flag.FlagSet, arg } if subFlags.NArg() != 1 { - return fmt.Errorf("action UpdateTabletAddrs requires ") + return fmt.Errorf("action UpdateTabletAddrs requires ") } if *ipAddr != "" && net.ParseIP(*ipAddr) == nil { return fmt.Errorf("malformed address: %v", *ipAddr) } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -727,29 +628,29 @@ func commandUpdateTabletAddrs(wr *wrangler.Wrangler, subFlags *flag.FlagSet, arg }) } -func commandScrapTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandScrapTablet(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "writes the scrap state in to zk, no questions asked, if a tablet is offline") skipRebuild := subFlags.Bool("skip-rebuild", false, "do not rebuild the shard and keyspace graph after scrapping") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ScrapTablet requires ") + return fmt.Errorf("action ScrapTablet requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } - return wr.Scrap(tabletAlias, *force, *skipRebuild) + return wr.Scrap(ctx, tabletAlias, *force, *skipRebuild) } -func commandDeleteTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandDeleteTablet(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() == 0 { - return fmt.Errorf("action DeleteTablet requires at least one ...") + return fmt.Errorf("action DeleteTablet requires at least one ") } tabletAliases, err := tabletParamsToTabletAliases(subFlags.Args()) @@ -764,15 +665,15 @@ func commandDeleteTablet(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []s return nil } -func commandSetReadOnly(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSetReadOnly(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action SetReadOnly requires ") + return fmt.Errorf("action SetReadOnly requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -780,18 +681,18 @@ func commandSetReadOnly(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []st if err != nil { return fmt.Errorf("failed reading tablet %v: %v", tabletAlias, err) } - return wr.TabletManagerClient().SetReadOnly(context.TODO(), ti, wr.ActionTimeout()) + return wr.TabletManagerClient().SetReadOnly(ctx, ti) } -func commandSetReadWrite(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSetReadWrite(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action SetReadWrite requires ") + return fmt.Errorf("action SetReadWrite requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -799,10 +700,10 @@ func commandSetReadWrite(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []s if err != nil { return fmt.Errorf("failed reading tablet %v: %v", tabletAlias, err) } - return wr.TabletManagerClient().SetReadWrite(context.TODO(), ti, wr.ActionTimeout()) + return wr.TabletManagerClient().SetReadWrite(ctx, ti) } -func commandChangeSlaveType(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandChangeSlaveType(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will change the type in zookeeper, and not run hooks") dryRun := subFlags.Bool("dry-run", false, "just list the proposed change") @@ -810,10 +711,10 @@ func commandChangeSlaveType(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action ChangeSlaveType requires ") + return fmt.Errorf("action ChangeSlaveType requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -834,17 +735,17 @@ func commandChangeSlaveType(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args wr.Logger().Printf("+ %v\n", fmtTabletAwkable(ti)) return nil } - return wr.ChangeType(tabletAlias, newType, *force) + return wr.ChangeType(ctx, tabletAlias, newType, *force) } -func commandPing(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandPing(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action Ping requires ") + return fmt.Errorf("action Ping requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -852,17 +753,17 @@ func commandPing(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) e if err != nil { return err } - return wr.TabletManagerClient().Ping(context.TODO(), tabletInfo, wr.ActionTimeout()) + return wr.TabletManagerClient().Ping(ctx, tabletInfo) } -func commandRefreshState(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandRefreshState(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action RefreshState requires ") + return fmt.Errorf("action RefreshState requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -870,17 +771,17 @@ func commandRefreshState(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []s if err != nil { return err } - return wr.TabletManagerClient().RefreshState(context.TODO(), tabletInfo, wr.ActionTimeout()) + return wr.TabletManagerClient().RefreshState(ctx, tabletInfo) } -func commandRunHealthCheck(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandRunHealthCheck(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { return fmt.Errorf("action RunHealthCheck requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -892,10 +793,35 @@ func commandRunHealthCheck(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args [ if err != nil { return err } - return wr.TabletManagerClient().RunHealthCheck(context.TODO(), tabletInfo, servedType, wr.ActionTimeout()) + return wr.TabletManagerClient().RunHealthCheck(ctx, tabletInfo, servedType) } -func commandQuery(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandHealthStream(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { + if err := subFlags.Parse(args); err != nil { + return err + } + if subFlags.NArg() != 1 { + return fmt.Errorf("action HealthStream ") + } + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) + if err != nil { + return err + } + tabletInfo, err := wr.TopoServer().GetTablet(tabletAlias) + if err != nil { + return err + } + c, errFunc, err := wr.TabletManagerClient().HealthStream(ctx, tabletInfo) + if err != nil { + return err + } + for hsr := range c { + wr.Logger().Printf("%v\n", jscfg.ToJson(hsr)) + } + return errFunc() +} + +func commandQuery(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } @@ -905,14 +831,14 @@ func commandQuery(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) return kquery(wr, subFlags.Arg(0), subFlags.Arg(1), subFlags.Arg(2)) } -func commandSleep(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSleep(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action Sleep requires ") + return fmt.Errorf("action Sleep requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -924,20 +850,20 @@ func commandSleep(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) if err != nil { return err } - return wr.TabletManagerClient().Sleep(context.TODO(), ti, duration, wr.ActionTimeout()) + return wr.TabletManagerClient().Sleep(ctx, ti, duration) } -func commandSnapshotSourceEnd(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSnapshotSourceEnd(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { slaveStartRequired := subFlags.Bool("slave-start", false, "will restart replication") readWrite := subFlags.Bool("read-write", false, "will make the server read-write") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action SnapshotSourceEnd requires ") + return fmt.Errorf("action SnapshotSourceEnd requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -945,10 +871,10 @@ func commandSnapshotSourceEnd(wr *wrangler.Wrangler, subFlags *flag.FlagSet, arg if err != nil { return err } - return wr.SnapshotSourceEnd(tabletAlias, *slaveStartRequired, !(*readWrite), tabletType) + return wr.SnapshotSourceEnd(ctx, tabletAlias, *slaveStartRequired, !(*readWrite), tabletType) } -func commandSnapshot(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSnapshot(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will force the snapshot for a master, and turn it into a backup") serverMode := subFlags.Bool("server-mode", false, "will symlink the data files and leave mysqld stopped") concurrency := subFlags.Int("concurrency", 4, "how many compression/checksum jobs to run simultaneously") @@ -956,14 +882,14 @@ func commandSnapshot(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []strin return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action Snapshot requires ") + return fmt.Errorf("action Snapshot requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } - sr, originalType, err := wr.Snapshot(tabletAlias, *force, *concurrency, *serverMode) + sr, originalType, err := wr.Snapshot(ctx, tabletAlias, *force, *concurrency, *serverMode) if err == nil { log.Infof("Manifest: %v", sr.ManifestPath) log.Infof("ParentAlias: %v", sr.ParentAlias) @@ -976,7 +902,7 @@ func commandSnapshot(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []strin return err } -func commandRestore(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandRestore(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { dontWaitForSlaveStart := subFlags.Bool("dont-wait-for-slave-start", false, "won't wait for replication to start (useful when restoring from snapshot source that is the replication master)") fetchConcurrency := subFlags.Int("fetch-concurrency", 3, "how many files to fetch simultaneously") fetchRetryCount := subFlags.Int("fetch-retry-count", 3, "how many times to retry a failed transfer") @@ -984,27 +910,27 @@ func commandRestore(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string return err } if subFlags.NArg() != 3 && subFlags.NArg() != 4 { - return fmt.Errorf("action Restore requires []") + return fmt.Errorf("action Restore requires []") } - srcTabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + srcTabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } - dstTabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(2)) + dstTabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(2)) if err != nil { return err } parentAlias := srcTabletAlias if subFlags.NArg() == 4 { - parentAlias, err = tabletParamToTabletAlias(subFlags.Arg(3)) + parentAlias, err = topo.ParseTabletAliasString(subFlags.Arg(3)) if err != nil { return err } } - return wr.Restore(srcTabletAlias, subFlags.Arg(1), dstTabletAlias, parentAlias, *fetchConcurrency, *fetchRetryCount, false, *dontWaitForSlaveStart) + return wr.Restore(ctx, srcTabletAlias, subFlags.Arg(1), dstTabletAlias, parentAlias, *fetchConcurrency, *fetchRetryCount, false, *dontWaitForSlaveStart) } -func commandClone(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandClone(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will force the snapshot for a master, and turn it into a backup") concurrency := subFlags.Int("concurrency", 4, "how many compression/checksum jobs to run simultaneously") fetchConcurrency := subFlags.Int("fetch-concurrency", 3, "how many files to fetch simultaneously") @@ -1014,92 +940,24 @@ func commandClone(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) return err } if subFlags.NArg() < 2 { - return fmt.Errorf("action Clone requires ...") + return fmt.Errorf("action Clone requires [...]") } - srcTabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + srcTabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } dstTabletAliases := make([]topo.TabletAlias, subFlags.NArg()-1) for i := 1; i < subFlags.NArg(); i++ { - dstTabletAliases[i-1], err = tabletParamToTabletAlias(subFlags.Arg(i)) - if err != nil { - return err - } - } - return wr.Clone(srcTabletAlias, dstTabletAliases, *force, *concurrency, *fetchConcurrency, *fetchRetryCount, *serverMode) -} - -func commandMultiRestore(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { - fetchRetryCount := subFlags.Int("fetch-retry-count", 3, "how many times to retry a failed transfer") - concurrency := subFlags.Int("concurrency", 8, "how many concurrent jobs to run simultaneously") - fetchConcurrency := subFlags.Int("fetch-concurrency", 4, "how many files to fetch simultaneously") - insertTableConcurrency := subFlags.Int("insert-table-concurrency", 4, "how many tables to load into a single destination table simultaneously") - strategy := subFlags.String("strategy", "", "which strategy to use for restore, use 'mysqlctl multirestore -help' for more info") - if err := subFlags.Parse(args); err != nil { - return err - } - - if subFlags.NArg() < 2 { - return fmt.Errorf("MultiRestore requires ... %v", args) - } - destination, err := tabletParamToTabletAlias(subFlags.Arg(0)) - if err != nil { - return err - } - sources := make([]topo.TabletAlias, subFlags.NArg()-1) - for i := 1; i < subFlags.NArg(); i++ { - sources[i-1], err = tabletParamToTabletAlias(subFlags.Arg(i)) + dstTabletAliases[i-1], err = topo.ParseTabletAliasString(subFlags.Arg(i)) if err != nil { return err } } - return wr.MultiRestore(destination, sources, *concurrency, *fetchConcurrency, *insertTableConcurrency, *fetchRetryCount, *strategy) -} - -func commandMultiSnapshot(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { - force := subFlags.Bool("force", false, "will force the snapshot for a master, and turn it into a backup") - concurrency := subFlags.Int("concurrency", 8, "how many compression jobs to run simultaneously") - spec := subFlags.String("spec", "-", "shard specification") - tablesString := subFlags.String("tables", "", "dump only this comma separated list of table regexp") - excludeTablesString := subFlags.String("exclude_tables", "", "comma separated list of regexps for tables to exclude") - skipSlaveRestart := subFlags.Bool("skip-slave-restart", false, "after the snapshot is done, do not restart slave replication") - maximumFilesize := subFlags.Uint64("maximum-file-size", 128*1024*1024, "the maximum size for an uncompressed data file") - if err := subFlags.Parse(args); err != nil { - return err - } - if subFlags.NArg() != 1 { - return fmt.Errorf("action MultiSnapshot requires ") - } - - shards, err := key.ParseShardingSpec(*spec) - if err != nil { - return fmt.Errorf("multisnapshot failed: %v", err) - } - var tables []string - if *tablesString != "" { - tables = strings.Split(*tablesString, ",") - } - var excludeTables []string - if *excludeTablesString != "" { - excludeTables = strings.Split(*excludeTablesString, ",") - } - - source, err := tabletParamToTabletAlias(subFlags.Arg(0)) - if err != nil { - return err - } - filenames, parentAlias, err := wr.MultiSnapshot(shards, source, *concurrency, tables, excludeTables, *force, *skipSlaveRestart, *maximumFilesize) - - if err == nil { - log.Infof("manifest locations: %v", filenames) - log.Infof("ParentAlias: %v", parentAlias) - } - return err + return wr.Clone(ctx, srcTabletAlias, dstTabletAliases, *force, *concurrency, *fetchConcurrency, *fetchRetryCount, *serverMode) } -func commandExecuteFetch(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandExecuteFetch(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { maxRows := subFlags.Int("max_rows", 10000, "maximum number of rows to allow in reset") wantFields := subFlags.Bool("want_fields", false, "also get the field names") disableBinlogs := subFlags.Bool("disable_binlogs", false, "disable writing to binlogs during the query") @@ -1107,52 +965,52 @@ func commandExecuteFetch(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []s return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action ExecuteFetch requires ") + return fmt.Errorf("action ExecuteFetch requires ") } - alias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + alias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } query := subFlags.Arg(1) - qr, err := wr.ExecuteFetch(alias, query, *maxRows, *wantFields, *disableBinlogs) + qr, err := wr.ExecuteFetch(ctx, alias, query, *maxRows, *wantFields, *disableBinlogs) if err == nil { wr.Logger().Printf("%v\n", jscfg.ToJson(qr)) } return err } -func commandExecuteHook(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandExecuteHook(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() < 2 { - return fmt.Errorf("action ExecuteHook requires ") + return fmt.Errorf("action ExecuteHook requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } hook := &hk.Hook{Name: subFlags.Arg(1), Parameters: subFlags.Args()[2:]} - hr, err := wr.ExecuteHook(tabletAlias, hook) + hr, err := wr.ExecuteHook(ctx, tabletAlias, hook) if err == nil { log.Infof(hr.String()) } return err } -func commandCreateShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandCreateShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will keep going even if the keyspace already exists") parent := subFlags.Bool("parent", false, "creates the parent keyspace if it doesn't exist") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action CreateShard requires ") + return fmt.Errorf("action CreateShard requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } @@ -1170,15 +1028,15 @@ func commandCreateShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []st return err } -func commandGetShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandGetShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action GetShard requires ") + return fmt.Errorf("action GetShard requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } @@ -1189,13 +1047,13 @@ func commandGetShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []strin return err } -func commandRebuildShardGraph(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandRebuildShardGraph(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { cells := subFlags.String("cells", "", "comma separated list of cells to update") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() == 0 { - return fmt.Errorf("action RebuildShardGraph requires at least one ") + return fmt.Errorf("action RebuildShardGraph requires at least one ") } var cellArray []string @@ -1208,68 +1066,60 @@ func commandRebuildShardGraph(wr *wrangler.Wrangler, subFlags *flag.FlagSet, arg return err } for _, ks := range keyspaceShards { - if _, err := wr.RebuildShardGraph(ks.Keyspace, ks.Shard, cellArray); err != nil { + if _, err := wr.RebuildShardGraph(ctx, ks.Keyspace, ks.Shard, cellArray); err != nil { return err } } return nil } -func commandShardExternallyReparented(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { - useRpc := subFlags.Bool("use_rpc", false, "send an RPC call to the new master instead of doing the operation internally") +func commandTabletExternallyReparented(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } - if subFlags.NArg() != 2 { - return fmt.Errorf("action ShardExternallyReparented requires ") + if subFlags.NArg() != 1 { + return fmt.Errorf("action TabletExternallyReparented requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(1)) + ti, err := wr.TopoServer().GetTablet(tabletAlias) if err != nil { return err } - if *useRpc { - ti, err := wr.TopoServer().GetTablet(tabletAlias) - if err != nil { - return err - } - return wr.TabletManagerClient().TabletExternallyReparented(context.TODO(), ti, "", wr.ActionTimeout()) - } - return wr.ShardExternallyReparented(keyspace, shard, tabletAlias) + return wr.TabletManagerClient().TabletExternallyReparented(ctx, ti, "") } -func commandValidateShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandValidateShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { pingTablets := subFlags.Bool("ping-tablets", true, "ping all tablets during validate") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ValidateShard requires ") + return fmt.Errorf("action ValidateShard requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } - return wr.ValidateShard(keyspace, shard, *pingTablets) + return wr.ValidateShard(ctx, keyspace, shard, *pingTablets) } -func commandShardReplicationPositions(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandShardReplicationPositions(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ShardReplicationPositions requires ") + return fmt.Errorf("action ShardReplicationPositions requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } - tablets, stats, err := wr.ShardReplicationStatuses(keyspace, shard) + tablets, stats, err := wr.ShardReplicationStatuses(ctx, keyspace, shard) if tablets == nil { return err } @@ -1290,30 +1140,30 @@ func commandShardReplicationPositions(wr *wrangler.Wrangler, subFlags *flag.Flag return nil } -func commandListShardTablets(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandListShardTablets(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ListShardTablets requires ") + return fmt.Errorf("action ListShardTablets requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } - return listTabletsByShard(wr, keyspace, shard) + return listTabletsByShard(ctx, wr, keyspace, shard) } -func commandSetShardServedTypes(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSetShardServedTypes(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { cellsStr := subFlags.String("cells", "", "comma separated list of cells to update") remove := subFlags.Bool("remove", false, "will remove the served type") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action SetShardServedTypes requires ") + return fmt.Errorf("action SetShardServedTypes requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } @@ -1326,21 +1176,21 @@ func commandSetShardServedTypes(wr *wrangler.Wrangler, subFlags *flag.FlagSet, a cells = strings.Split(*cellsStr, ",") } - return wr.SetShardServedTypes(keyspace, shard, cells, servedType, *remove) + return wr.SetShardServedTypes(ctx, keyspace, shard, cells, servedType, *remove) } -func commandSetShardTabletControl(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSetShardTabletControl(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { cellsStr := subFlags.String("cells", "", "comma separated list of cells to update") tablesStr := subFlags.String("tables", "", "comma separated list of tables to replicate (used for vertical split)") remove := subFlags.Bool("remove", false, "will remove cells for vertical splits (requires tables)") - disableQueryService := subFlags.Bool("disableQueryService", false, "will disable query service on the provided nodes") + disableQueryService := subFlags.Bool("disable_query_service", false, "will disable query service on the provided nodes") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action SetShardTabletControl requires ") + return fmt.Errorf("action SetShardTabletControl requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } @@ -1357,10 +1207,10 @@ func commandSetShardTabletControl(wr *wrangler.Wrangler, subFlags *flag.FlagSet, cells = strings.Split(*cellsStr, ",") } - return wr.SetShardTabletControl(keyspace, shard, tabletType, cells, *remove, *disableQueryService, tables) + return wr.SetShardTabletControl(ctx, keyspace, shard, tabletType, cells, *remove, *disableQueryService, tables) } -func commandSourceShardDelete(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSourceShardDelete(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } @@ -1368,7 +1218,7 @@ func commandSourceShardDelete(wr *wrangler.Wrangler, subFlags *flag.FlagSet, arg if subFlags.NArg() < 2 { return fmt.Errorf("SourceShardDelete requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } @@ -1376,10 +1226,10 @@ func commandSourceShardDelete(wr *wrangler.Wrangler, subFlags *flag.FlagSet, arg if err != nil { return err } - return wr.SourceShardDelete(keyspace, shard, uint32(uid)) + return wr.SourceShardDelete(ctx, keyspace, shard, uint32(uid)) } -func commandSourceShardAdd(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSourceShardAdd(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { keyRange := subFlags.String("key_range", "", "key range to use for the SourceShard") tablesStr := subFlags.String("tables", "", "comma separated list of tables to replicate (used for vertical split)") if err := subFlags.Parse(args); err != nil { @@ -1388,7 +1238,7 @@ func commandSourceShardAdd(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args [ if subFlags.NArg() != 3 { return fmt.Errorf("SourceShardAdd requires ... %v", args) - } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) - if err != nil { - return err - } - sources := make([]topo.TabletAlias, subFlags.NArg()-1) - for i := 1; i < subFlags.NArg(); i++ { - sources[i-1], err = tabletParamToTabletAlias(subFlags.Arg(i)) - if err != nil { - return err - } - } - var tableArray []string - if *tables != "" { - tableArray = strings.Split(*tables, ",") - } - return wr.ShardMultiRestore(keyspace, shard, sources, tableArray, *concurrency, *fetchConcurrency, *insertTableConcurrency, *fetchRetryCount, *strategy) + return wr.SourceShardAdd(ctx, keyspace, shard, uint32(uid), skeyspace, sshard, kr, tables) } -func commandShardReplicationAdd(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandShardReplicationAdd(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } - if subFlags.NArg() != 3 { - return fmt.Errorf("action ShardReplicationAdd requires ") + if subFlags.NArg() != 2 { + return fmt.Errorf("action ShardReplicationAdd requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) - if err != nil { - return err - } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(1)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } - parentAlias, err := tabletParamToTabletAlias(subFlags.Arg(2)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(1)) if err != nil { return err } - return topo.UpdateShardReplicationRecord(context.TODO(), wr.TopoServer(), keyspace, shard, tabletAlias, parentAlias) + return topo.UpdateShardReplicationRecord(ctx, wr.TopoServer(), keyspace, shard, tabletAlias) } -func commandShardReplicationRemove(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandShardReplicationRemove(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action ShardReplicationRemove requires ") + return fmt.Errorf("action ShardReplicationRemove requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(1)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(1)) if err != nil { return err } return topo.RemoveShardReplicationRecord(wr.TopoServer(), tabletAlias.Cell, keyspace, shard, tabletAlias) } -func commandShardReplicationFix(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandShardReplicationFix(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action ShardReplicationRemove requires ") + return fmt.Errorf("action ShardReplicationRemove requires ") } cell := subFlags.Arg(0) - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(1)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(1)) if err != nil { return err } return topo.FixShardReplication(wr.TopoServer(), wr.Logger(), cell, keyspace, shard) } -func commandRemoveShardCell(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandRemoveShardCell(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will keep going even we can't reach the cell's topology server to check for tablets") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action RemoveShardCell requires ") + return fmt.Errorf("action RemoveShardCell requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } - return wr.RemoveShardCell(keyspace, shard, subFlags.Arg(1), *force) + return wr.RemoveShardCell(ctx, keyspace, shard, subFlags.Arg(1), *force) } -func commandDeleteShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandDeleteShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() == 0 { - return fmt.Errorf("action DeleteShard requires ...") + return fmt.Errorf("action DeleteShard requires [...]") } keyspaceShards, err := shardParamsToKeyspaceShards(wr, subFlags.Args()) @@ -1532,7 +1346,7 @@ func commandDeleteShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []st return err } for _, ks := range keyspaceShards { - err := wr.DeleteShard(ks.Keyspace, ks.Shard) + err := wr.DeleteShard(ctx, ks.Keyspace, ks.Shard) switch err { case nil: // keep going @@ -1545,7 +1359,7 @@ func commandDeleteShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []st return nil } -func commandCreateKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandCreateKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { shardingColumnName := subFlags.String("sharding_column_name", "", "column to use for sharding operations") shardingColumnType := subFlags.String("sharding_column_type", "", "type of the column to use for sharding operations") splitShardCount := subFlags.Int("split_shard_count", 0, "number of shards to use for data splits") @@ -1556,13 +1370,10 @@ func commandCreateKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args [ return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action CreateKeyspace requires ") + return fmt.Errorf("action CreateKeyspace requires ") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) - if err != nil { - return err - } + keyspace := subFlags.Arg(0) kit := key.KeyspaceIdType(*shardingColumnType) if !key.IsKeyspaceIdTypeInList(kit, key.AllKeyspaceIdTypes) { return fmt.Errorf("invalid sharding_column_type") @@ -1584,7 +1395,7 @@ func commandCreateKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args [ } } } - err = wr.TopoServer().CreateKeyspace(keyspace, ki) + err := wr.TopoServer().CreateKeyspace(keyspace, ki) if *force && err == topo.ErrNodeExists { log.Infof("keyspace %v already exists (ignoring error with -force)", keyspace) err = nil @@ -1592,18 +1403,15 @@ func commandCreateKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args [ return err } -func commandGetKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandGetKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action GetKeyspace requires ") + return fmt.Errorf("action GetKeyspace requires ") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) - if err != nil { - return err - } + keyspace := subFlags.Arg(0) keyspaceInfo, err := wr.TopoServer().GetKeyspace(keyspace) if err == nil { wr.Logger().Printf("%v\n", jscfg.ToJson(keyspaceInfo)) @@ -1611,20 +1419,17 @@ func commandGetKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []st return err } -func commandSetKeyspaceShardingInfo(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSetKeyspaceShardingInfo(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will update the fields even if they're already set, use with care") splitShardCount := subFlags.Int("split_shard_count", 0, "number of shards to use for data splits") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() > 3 || subFlags.NArg() < 1 { - return fmt.Errorf("action SetKeyspaceShardingInfo requires [] []") + return fmt.Errorf("action SetKeyspaceShardingInfo requires [] []") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) - if err != nil { - return err - } + keyspace := subFlags.Arg(0) columnName := "" if subFlags.NArg() >= 2 { columnName = subFlags.Arg(1) @@ -1637,10 +1442,10 @@ func commandSetKeyspaceShardingInfo(wr *wrangler.Wrangler, subFlags *flag.FlagSe } } - return wr.SetKeyspaceShardingInfo(keyspace, columnName, kit, int32(*splitShardCount), *force) + return wr.SetKeyspaceShardingInfo(ctx, keyspace, columnName, kit, int32(*splitShardCount), *force) } -func commandSetKeyspaceServedFrom(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandSetKeyspaceServedFrom(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { source := subFlags.String("source", "", "source keyspace name") remove := subFlags.Bool("remove", false, "remove the served from record instead of adding it") cellsStr := subFlags.String("cells", "", "comma separated list of cells to affect") @@ -1650,10 +1455,7 @@ func commandSetKeyspaceServedFrom(wr *wrangler.Wrangler, subFlags *flag.FlagSet, if subFlags.NArg() != 2 { return fmt.Errorf("action SetKeyspaceServedFrom requires ") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) - if err != nil { - return err - } + keyspace := subFlags.Arg(0) servedType, err := parseTabletType(subFlags.Arg(1), []topo.TabletType{topo.TYPE_MASTER, topo.TYPE_REPLICA, topo.TYPE_RDONLY}) if err != nil { return err @@ -1663,16 +1465,16 @@ func commandSetKeyspaceServedFrom(wr *wrangler.Wrangler, subFlags *flag.FlagSet, cells = strings.Split(*cellsStr, ",") } - return wr.SetKeyspaceServedFrom(keyspace, servedType, cells, *source, *remove) + return wr.SetKeyspaceServedFrom(ctx, keyspace, servedType, cells, *source, *remove) } -func commandRebuildKeyspaceGraph(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandRebuildKeyspaceGraph(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { cells := subFlags.String("cells", "", "comma separated list of cells to update") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() == 0 { - return fmt.Errorf("action RebuildKeyspaceGraph requires at least one ") + return fmt.Errorf("action RebuildKeyspaceGraph requires at least one ") } var cellArray []string @@ -1685,41 +1487,39 @@ func commandRebuildKeyspaceGraph(wr *wrangler.Wrangler, subFlags *flag.FlagSet, return err } for _, keyspace := range keyspaces { - if err := wr.RebuildKeyspaceGraph(keyspace, cellArray); err != nil { + if err := wr.RebuildKeyspaceGraph(ctx, keyspace, cellArray); err != nil { return err } } return nil } -func commandValidateKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandValidateKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { pingTablets := subFlags.Bool("ping-tablets", false, "ping all tablets during validate") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ValidateKeyspace requires ") + return fmt.Errorf("action ValidateKeyspace requires ") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) - if err != nil { - return err - } - return wr.ValidateKeyspace(keyspace, *pingTablets) + keyspace := subFlags.Arg(0) + return wr.ValidateKeyspace(ctx, keyspace, *pingTablets) } -func commandMigrateServedTypes(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandMigrateServedTypes(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { cellsStr := subFlags.String("cells", "", "comma separated list of cells to update") reverse := subFlags.Bool("reverse", false, "move the served type back instead of forward, use in case of trouble") skipReFreshState := subFlags.Bool("skip-refresh-state", false, "do not refresh the state of the source tablets after the migration (will need to be done manually, replica and rdonly only)") + filteredReplicationWaitTime := subFlags.Duration("filtered_replication_wait_time", 30*time.Second, "maximum time to wait for filtered replication to catch up on master migrations") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action MigrateServedTypes requires ") + return fmt.Errorf("action MigrateServedTypes requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } @@ -1734,20 +1534,21 @@ func commandMigrateServedTypes(wr *wrangler.Wrangler, subFlags *flag.FlagSet, ar if *cellsStr != "" { cells = strings.Split(*cellsStr, ",") } - return wr.MigrateServedTypes(keyspace, shard, cells, servedType, *reverse, *skipReFreshState) + return wr.MigrateServedTypes(ctx, keyspace, shard, cells, servedType, *reverse, *skipReFreshState, *filteredReplicationWaitTime) } -func commandMigrateServedFrom(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandMigrateServedFrom(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { reverse := subFlags.Bool("reverse", false, "move the served from back instead of forward, use in case of trouble") cellsStr := subFlags.String("cells", "", "comma separated list of cells to update") + filteredReplicationWaitTime := subFlags.Duration("filtered_replication_wait_time", 30*time.Second, "maximum time to wait for filtered replication to catch up on master migrations") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action MigrateServedFrom requires ") + return fmt.Errorf("action MigrateServedFrom requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } @@ -1759,10 +1560,27 @@ func commandMigrateServedFrom(wr *wrangler.Wrangler, subFlags *flag.FlagSet, arg if *cellsStr != "" { cells = strings.Split(*cellsStr, ",") } - return wr.MigrateServedFrom(keyspace, shard, servedType, cells, *reverse) + return wr.MigrateServedFrom(ctx, keyspace, shard, servedType, cells, *reverse, *filteredReplicationWaitTime) +} + +func commandFindAllShardsInKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { + if err := subFlags.Parse(args); err != nil { + return err + } + if subFlags.NArg() != 1 { + return fmt.Errorf("action FindAllShardsInKeyspace requires ") + } + + keyspace := subFlags.Arg(0) + result, err := topo.FindAllShardsInKeyspace(wr.TopoServer(), keyspace) + if err == nil { + wr.Logger().Printf("%v\n", jscfg.ToJson(result)) + } + return err + } -func commandResolve(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandResolve(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } @@ -1789,12 +1607,12 @@ func commandResolve(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string return err } for _, addr := range addrs { - wr.Logger().Printf("%v:%v\n", addr.Target, addr.Port) + wr.Logger().Printf("%v\n", netutil.JoinHostPort(addr.Target, int(addr.Port))) } return nil } -func commandValidate(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandValidate(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { pingTablets := subFlags.Bool("ping-tablets", false, "ping all tablets during validate") if err := subFlags.Parse(args); err != nil { return err @@ -1803,78 +1621,60 @@ func commandValidate(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []strin if subFlags.NArg() != 0 { log.Warningf("action Validate doesn't take any parameter any more") } - return wr.Validate(*pingTablets) + return wr.Validate(ctx, *pingTablets) } -func commandRebuildReplicationGraph(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandRebuildReplicationGraph(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { // This is sort of a nuclear option. if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() < 2 { - return fmt.Errorf("action RebuildReplicationGraph requires ,,... ,...") - } - - cellParams := strings.Split(subFlags.Arg(0), ",") - resolvedCells, err := resolveWildcards(wr, cellParams) - if err != nil { - return err - } - cells := make([]string, 0, len(cellParams)) - for _, cell := range resolvedCells { - c, err := vtPathToCell(cell) - if err != nil { - return err - } - cells = append(cells, c) + return fmt.Errorf("action RebuildReplicationGraph requires ,,... ,[,...]") } + cells := strings.Split(subFlags.Arg(0), ",") keyspaceParams := strings.Split(subFlags.Arg(1), ",") keyspaces, err := keyspaceParamsToKeyspaces(wr, keyspaceParams) if err != nil { return err } - return wr.RebuildReplicationGraph(cells, keyspaces) + return wr.RebuildReplicationGraph(ctx, cells, keyspaces) } -func commandListAllTablets(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandListAllTablets(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ListAllTablets requires ") + return fmt.Errorf("action ListAllTablets requires ") } - cell, err := vtPathToCell(subFlags.Arg(0)) - if err != nil { - return err - } - return dumpAllTablets(wr, cell) + cell := subFlags.Arg(0) + return dumpAllTablets(ctx, wr, cell) } -func commandListTablets(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandListTablets(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() == 0 { - return fmt.Errorf("action ListTablets requires ...") + return fmt.Errorf("action ListTablets requires ") } - zkPaths, err := resolveWildcards(wr, subFlags.Args()) - if err != nil { - return err - } - aliases := make([]topo.TabletAlias, len(zkPaths)) - for i, zkPath := range zkPaths { - aliases[i], err = tabletParamToTabletAlias(zkPath) + paths := subFlags.Args() + aliases := make([]topo.TabletAlias, len(paths)) + var err error + for i, path := range paths { + aliases[i], err = topo.ParseTabletAliasString(path) if err != nil { return err } } - return dumpTablets(wr, aliases) + return dumpTablets(ctx, wr, aliases) } -func commandGetSchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandGetSchema(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { tables := subFlags.String("tables", "", "comma separated list of regexps for tables to gather schema information for") excludeTables := subFlags.String("exclude_tables", "", "comma separated list of regexps for tables to exclude") includeViews := subFlags.Bool("include-views", false, "include views in the output") @@ -1883,9 +1683,9 @@ func commandGetSchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []stri return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action GetSchema requires ") + return fmt.Errorf("action GetSchema requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -1898,7 +1698,7 @@ func commandGetSchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []stri excludeTableArray = strings.Split(*excludeTables, ",") } - sd, err := wr.GetSchema(tabletAlias, tableArray, excludeTableArray, *includeViews) + sd, err := wr.GetSchema(ctx, tabletAlias, tableArray, excludeTableArray, *includeViews) if err == nil { if *tableNamesOnly { for _, td := range sd.TableDefinitions { @@ -1911,31 +1711,31 @@ func commandGetSchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []stri return err } -func commandReloadSchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandReloadSchema(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ReloadSchema requires ") + return fmt.Errorf("action ReloadSchema requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } - return wr.ReloadSchema(tabletAlias) + return wr.ReloadSchema(ctx, tabletAlias) } -func commandValidateSchemaShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandValidateSchemaShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { excludeTables := subFlags.String("exclude_tables", "", "comma separated list of regexps for tables to exclude") includeViews := subFlags.Bool("include-views", false, "include views in the validation") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ValidateSchemaShard requires ") + return fmt.Errorf("action ValidateSchemaShard requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } @@ -1943,31 +1743,28 @@ func commandValidateSchemaShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, a if *excludeTables != "" { excludeTableArray = strings.Split(*excludeTables, ",") } - return wr.ValidateSchemaShard(keyspace, shard, excludeTableArray, *includeViews) + return wr.ValidateSchemaShard(ctx, keyspace, shard, excludeTableArray, *includeViews) } -func commandValidateSchemaKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandValidateSchemaKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { excludeTables := subFlags.String("exclude_tables", "", "comma separated list of regexps for tables to exclude") includeViews := subFlags.Bool("include-views", false, "include views in the validation") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ValidateSchemaKeyspace requires ") + return fmt.Errorf("action ValidateSchemaKeyspace requires ") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) - if err != nil { - return err - } + keyspace := subFlags.Arg(0) var excludeTableArray []string if *excludeTables != "" { excludeTableArray = strings.Split(*excludeTables, ",") } - return wr.ValidateSchemaKeyspace(keyspace, excludeTableArray, *includeViews) + return wr.ValidateSchemaKeyspace(ctx, keyspace, excludeTableArray, *includeViews) } -func commandPreflightSchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandPreflightSchema(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { sql := subFlags.String("sql", "", "sql command") sqlFile := subFlags.String("sql-file", "", "file containing the sql commands") if err := subFlags.Parse(args); err != nil { @@ -1975,9 +1772,9 @@ func commandPreflightSchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args } if subFlags.NArg() != 1 { - return fmt.Errorf("action PreflightSchema requires ") + return fmt.Errorf("action PreflightSchema requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -1985,14 +1782,14 @@ func commandPreflightSchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args if err != nil { return err } - scr, err := wr.PreflightSchema(tabletAlias, change) + scr, err := wr.PreflightSchema(ctx, tabletAlias, change) if err == nil { log.Infof(scr.String()) } return err } -func commandApplySchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandApplySchema(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will apply the schema even if preflight schema doesn't match") sql := subFlags.String("sql", "", "sql command") sqlFile := subFlags.String("sql-file", "", "file containing the sql commands") @@ -2003,9 +1800,9 @@ func commandApplySchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []st } if subFlags.NArg() != 1 { - return fmt.Errorf("action ApplySchema requires ") + return fmt.Errorf("action ApplySchema requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } @@ -2020,7 +1817,7 @@ func commandApplySchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []st // do the preflight to get before and after schema if !(*skipPreflight) { - scr, err := wr.PreflightSchema(tabletAlias, sc.Sql) + scr, err := wr.PreflightSchema(ctx, tabletAlias, sc.Sql) if err != nil { return fmt.Errorf("preflight failed: %v", err) } @@ -2030,27 +1827,28 @@ func commandApplySchema(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []st sc.Force = *force } - scr, err := wr.ApplySchema(tabletAlias, sc) + scr, err := wr.ApplySchema(ctx, tabletAlias, sc) if err == nil { log.Infof(scr.String()) } return err } -func commandApplySchemaShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandApplySchemaShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will apply the schema even if preflight schema doesn't match") sql := subFlags.String("sql", "", "sql command") sqlFile := subFlags.String("sql-file", "", "file containing the sql commands") simple := subFlags.Bool("simple", false, "just apply change on master and let replication do the rest") newParent := subFlags.String("new-parent", "", "will reparent to this tablet after the change") + waitSlaveTimeout := subFlags.Duration("wait_slave_timeout", 30*time.Second, "time to wait for slaves to catch up in reparenting") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ApplySchemaShard requires ") + return fmt.Errorf("action ApplySchemaShard requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } @@ -2060,7 +1858,7 @@ func commandApplySchemaShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args } var newParentAlias topo.TabletAlias if *newParent != "" { - newParentAlias, err = tabletParamToTabletAlias(*newParent) + newParentAlias, err = topo.ParseTabletAliasString(*newParent) if err != nil { return err } @@ -2070,119 +1868,188 @@ func commandApplySchemaShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args return fmt.Errorf("new_parent for action ApplySchemaShard can only be specified for complex schema upgrades") } - scr, err := wr.ApplySchemaShard(keyspace, shard, change, newParentAlias, *simple, *force) + scr, err := wr.ApplySchemaShard(ctx, keyspace, shard, change, newParentAlias, *simple, *force, *waitSlaveTimeout) if err == nil { log.Infof(scr.String()) } return err } -func commandApplySchemaKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandApplySchemaKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { force := subFlags.Bool("force", false, "will apply the schema even if preflight schema doesn't match") sql := subFlags.String("sql", "", "sql command") sqlFile := subFlags.String("sql-file", "", "file containing the sql commands") simple := subFlags.Bool("simple", false, "just apply change on master and let replication do the rest") + waitSlaveTimeout := subFlags.Duration("wait_slave_timeout", 30*time.Second, "time to wait for slaves to catch up in reparenting") if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ApplySchemaKeyspace requires ") + return fmt.Errorf("action ApplySchemaKeyspace requires ") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) - if err != nil { - return err - } + keyspace := subFlags.Arg(0) change, err := getFileParam(*sql, *sqlFile, "sql") if err != nil { return err } - scr, err := wr.ApplySchemaKeyspace(keyspace, change, *simple, *force) + scr, err := wr.ApplySchemaKeyspace(ctx, keyspace, change, *simple, *force, *waitSlaveTimeout) if err == nil { log.Infof(scr.String()) } return err } -func commandValidateVersionShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandCopySchemaShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { + tables := subFlags.String("tables", "", "comma separated list of regexps for tables to gather schema information for") + excludeTables := subFlags.String("exclude_tables", "", "comma separated list of regexps for tables to exclude") + includeViews := subFlags.Bool("include-views", true, "include views in the output") if err := subFlags.Parse(args); err != nil { return err } - if subFlags.NArg() != 1 { - return fmt.Errorf("action ValidateVersionShard requires ") + + if subFlags.NArg() != 2 { + return fmt.Errorf("action CopySchemaShard requires a source and a destination ") + } + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) + if err != nil { + return err + } + var tableArray []string + if *tables != "" { + tableArray = strings.Split(*tables, ",") + } + var excludeTableArray []string + if *excludeTables != "" { + excludeTableArray = strings.Split(*excludeTables, ",") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(1)) if err != nil { return err } - return wr.ValidateVersionShard(keyspace, shard) + + return wr.CopySchemaShard(ctx, tabletAlias, tableArray, excludeTableArray, *includeViews, keyspace, shard) } -func commandValidateVersionKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandValidateVersionShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ValidateVersionKeyspace requires ") + return fmt.Errorf("action ValidateVersionShard requires ") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } - return wr.ValidateVersionKeyspace(keyspace) + return wr.ValidateVersionShard(ctx, keyspace, shard) } -func commandGetPermissions(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandValidateVersionKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action GetPermissions requires ") + return fmt.Errorf("action ValidateVersionKeyspace requires ") } - tabletAlias, err := tabletParamToTabletAlias(subFlags.Arg(0)) + + keyspace := subFlags.Arg(0) + return wr.ValidateVersionKeyspace(ctx, keyspace) +} + +func commandGetPermissions(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { + if err := subFlags.Parse(args); err != nil { + return err + } + if subFlags.NArg() != 1 { + return fmt.Errorf("action GetPermissions requires ") + } + tabletAlias, err := topo.ParseTabletAliasString(subFlags.Arg(0)) if err != nil { return err } - p, err := wr.GetPermissions(tabletAlias) + p, err := wr.GetPermissions(ctx, tabletAlias) if err == nil { log.Infof("%v", p.String()) // they can contain '%' } return err } -func commandValidatePermissionsShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandValidatePermissionsShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ValidatePermissionsShard requires ") + return fmt.Errorf("action ValidatePermissionsShard requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(0)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(0)) if err != nil { return err } - return wr.ValidatePermissionsShard(keyspace, shard) + return wr.ValidatePermissionsShard(ctx, keyspace, shard) } -func commandValidatePermissionsKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandValidatePermissionsKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 1 { - return fmt.Errorf("action ValidatePermissionsKeyspace requires ") + return fmt.Errorf("action ValidatePermissionsKeyspace requires ") } - keyspace, err := keyspaceParamToKeyspace(subFlags.Arg(0)) + keyspace := subFlags.Arg(0) + return wr.ValidatePermissionsKeyspace(ctx, keyspace) +} + +func commandGetVSchema(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { + if err := subFlags.Parse(args); err != nil { + return err + } + if subFlags.NArg() != 0 { + return fmt.Errorf("action GetVSchema does not require additional arguments") + } + ts := wr.TopoServer() + schemafier, ok := ts.(topo.Schemafier) + if !ok { + return fmt.Errorf("%T does no support the vschema operations", ts) + } + schema, err := schemafier.GetVSchema() if err != nil { return err } - return wr.ValidatePermissionsKeyspace(keyspace) + wr.Logger().Printf("%s\n", schema) + return nil +} + +func commandApplyVSchema(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { + vschema := subFlags.String("vschema", "", "VTGate routing schema") + vschemaFile := subFlags.String("vschema_file", "", "VTGate routing schema file") + if err := subFlags.Parse(args); err != nil { + return err + } + if (*vschema == "") == (*vschemaFile == "") { + return fmt.Errorf("action ApplyVSchema requires either vschema or vschema_file") + } + ts := wr.TopoServer() + schemafier, ok := ts.(topo.Schemafier) + if !ok { + return fmt.Errorf("%T does not support vschema operations", ts) + } + s := *vschema + if *vschemaFile != "" { + schema, err := ioutil.ReadFile(*vschemaFile) + if err != nil { + return err + } + s = string(schema) + } + return schemafier.SaveVSchema(s) } -func commandGetSrvKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandGetSrvKeyspace(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } @@ -2197,7 +2064,7 @@ func commandGetSrvKeyspace(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args [ return err } -func commandGetSrvKeyspaceNames(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandGetSrvKeyspaceNames(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } @@ -2215,15 +2082,15 @@ func commandGetSrvKeyspaceNames(wr *wrangler.Wrangler, subFlags *flag.FlagSet, a return nil } -func commandGetSrvShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandGetSrvShard(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action GetSrvShard requires ") + return fmt.Errorf("action GetSrvShard requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(1)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(1)) if err != nil { return err } @@ -2234,15 +2101,15 @@ func commandGetSrvShard(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []st return err } -func commandGetEndPoints(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandGetEndPoints(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 3 { - return fmt.Errorf("action GetEndPoints requires ") + return fmt.Errorf("action GetEndPoints requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(1)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(1)) if err != nil { return err } @@ -2254,15 +2121,15 @@ func commandGetEndPoints(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []s return err } -func commandGetShardReplication(wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { +func commandGetShardReplication(ctx context.Context, wr *wrangler.Wrangler, subFlags *flag.FlagSet, args []string) error { if err := subFlags.Parse(args); err != nil { return err } if subFlags.NArg() != 2 { - return fmt.Errorf("action GetShardReplication requires ") + return fmt.Errorf("action GetShardReplication requires ") } - keyspace, shard, err := shardParamToKeyspaceShard(subFlags.Arg(1)) + keyspace, shard, err := topo.ParseKeyspaceShardString(subFlags.Arg(1)) if err != nil { return err } @@ -2325,7 +2192,7 @@ func sortReplicatingTablets(tablets []*topo.TabletInfo, stats []*myproto.Replica // RunCommand will execute the command using the provided wrangler. // It will return the actionPath to wait on for long remote actions if // applicable. -func RunCommand(wr *wrangler.Wrangler, args []string) error { +func RunCommand(ctx context.Context, wr *wrangler.Wrangler, args []string) error { if len(args) == 0 { wr.Logger().Printf("No command specified. Please see the list below:\n\n") PrintAllCommands(wr.Logger()) @@ -2344,7 +2211,7 @@ func RunCommand(wr *wrangler.Wrangler, args []string) error { wr.Logger().Printf("%s\n\n", cmd.help) subFlags.PrintDefaults() } - return cmd.method(wr, subFlags, args[1:]) + return cmd.method(ctx, wr, subFlags, args[1:]) } } } diff --git a/go/vt/vtgate/balancer.go b/go/vt/vtgate/balancer.go index 5432976977e..7ef9baeb5cf 100644 --- a/go/vt/vtgate/balancer.go +++ b/go/vt/vtgate/balancer.go @@ -38,7 +38,7 @@ type addressStatus struct { balancer *Balancer } -// NewBalancer creates a Balancer. getAddreses is the function +// NewBalancer creates a Balancer. getAddresses is the function // it will use to refresh the list of addresses if one of the // nodes has been marked down. The list of addresses is shuffled. // retryDelay specifies the minimum time a node will be marked down diff --git a/go/vt/vtgate/gorpcvtgateconn/conn.go b/go/vt/vtgate/gorpcvtgateconn/conn.go new file mode 100644 index 00000000000..d37d7f9d3b2 --- /dev/null +++ b/go/vt/vtgate/gorpcvtgateconn/conn.go @@ -0,0 +1,138 @@ +// Copyright 2015, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package gorpcvtgateconn provides go rpc connectivity for VTGate. +package gorpcvtgateconn + +import ( + "fmt" + "strings" + "time" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/rpcplus" + "github.com/youtube/vitess/go/rpcwrap/bsonrpc" + "github.com/youtube/vitess/go/vt/rpc" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/vtgate/proto" + "github.com/youtube/vitess/go/vt/vtgate/vtgateconn" + "golang.org/x/net/context" +) + +func init() { + vtgateconn.RegisterDialer("gorpc", dial) +} + +type vtgateConn struct { + rpcConn *rpcplus.Client + session *proto.Session + address string + timeout time.Duration +} + +func dial(ctx context.Context, address string, timeout time.Duration) (vtgateconn.VTGateConn, error) { + var network string + if strings.Contains(address, "/") { + network = "unix" + } else { + network = "tcp" + } + conn := &vtgateConn{ + address: address, + timeout: timeout, + } + var err error + conn.rpcConn, err = bsonrpc.DialHTTP(network, address, timeout, nil) + if err != nil { + return nil, err + } + return conn, nil +} + +func (conn *vtgateConn) Execute(ctx context.Context, query string, bindVars map[string]interface{}, tabletType topo.TabletType) (*mproto.QueryResult, error) { + request := proto.Query{ + Sql: query, + BindVariables: bindVars, + TabletType: tabletType, + Session: conn.session, + } + var result proto.QueryResult + if err := conn.rpcConn.Call(ctx, "VTGate.Execute", request, &result); err != nil { + return nil, fmt.Errorf("execute: %v", err) + } + conn.session = result.Session + if result.Error != "" { + return nil, fmt.Errorf("execute: %s", result.Error) + } + return result.Result, nil +} + +func (conn *vtgateConn) ExecuteBatch(ctx context.Context, queries []tproto.BoundQuery, tabletType topo.TabletType) (*tproto.QueryResultList, error) { + return nil, fmt.Errorf("not implemented yet") +} + +func (conn *vtgateConn) StreamExecute(ctx context.Context, query string, bindVars map[string]interface{}, tabletType topo.TabletType) (<-chan *mproto.QueryResult, vtgateconn.ErrFunc) { + req := &proto.Query{ + Sql: query, + BindVariables: bindVars, + TabletType: tabletType, + Session: conn.session, + } + sr := make(chan *proto.QueryResult, 10) + c := conn.rpcConn.StreamGo("VTGate.StreamExecute", req, sr) + srout := make(chan *mproto.QueryResult, 1) + go func() { + defer close(srout) + for r := range sr { + srout <- r.Result + } + }() + return srout, func() error { return c.Error } +} + +func (conn *vtgateConn) Begin(ctx context.Context) error { + if conn.session != nil { + return fmt.Errorf("begin: already in a transaction") + } + session := &proto.Session{} + if err := conn.rpcConn.Call(ctx, "VTGate.Begin", &rpc.Unused{}, session); err != nil { + return fmt.Errorf("begin: %v", err) + } + conn.session = session + return nil +} + +func (conn *vtgateConn) Commit(ctx context.Context) error { + if conn.session == nil { + return fmt.Errorf("commit: not in transaction") + } + defer func() { conn.session = nil }() + if err := conn.rpcConn.Call(ctx, "VTGate.Commit", conn.session, &rpc.Unused{}); err != nil { + return fmt.Errorf("commit: %v", err) + } + return nil +} + +func (conn *vtgateConn) Rollback(ctx context.Context) error { + if conn.session == nil { + return nil + } + defer func() { conn.session = nil }() + if err := conn.rpcConn.Call(ctx, "VTGate.Rollback", conn.session, &rpc.Unused{}); err != nil { + return fmt.Errorf("rollback: %v", err) + } + return nil +} + +func (conn *vtgateConn) SplitQuery(ctx context.Context, query tproto.BoundQuery, splitCount int) ([]tproto.QuerySplit, error) { + return nil, fmt.Errorf("not implemented yet") +} + +func (conn *vtgateConn) Close() { + if conn.session != nil { + conn.Rollback(context.Background()) + } + conn.rpcConn.Close() +} diff --git a/go/vt/vtgate/gorpcvtgateconn/conn_test.go b/go/vt/vtgate/gorpcvtgateconn/conn_test.go new file mode 100644 index 00000000000..8e70d87319c --- /dev/null +++ b/go/vt/vtgate/gorpcvtgateconn/conn_test.go @@ -0,0 +1,275 @@ +// Copyright 2015, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gorpcvtgateconn + +import ( + "fmt" + "reflect" + "sync" + "testing" + "time" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/vtgateconn" + "github.com/youtube/vitess/go/vt/vttest" + "golang.org/x/net/context" +) + +const ( + actionSelect = iota + actionCommit + actionRollback +) + +var once sync.Once + +func initEnv() { + once.Do(func() { + err := vttest.LocalLaunch( + []string{"0"}, + 1, + 0, + "test_keyspace", + "create table test_table(id int auto_increment, val varchar(128), primary key(id))", + `{"Keyspaces":{"test_keyspace":{"Tables":{"test_table":""}}}}`, + ) + if err != nil { + vttest.LocalTeardown() + panic(err) + } + }) +} + +/* +func TestMain(m *testing.M) { + r := m.Run() + vttest.LocalTeardown() + os.Exit(r) +} +*/ + +func TestExecuteCommit(t *testing.T) { + t.Skip("skipping for go1.3") + if testing.Short() { + t.Skip("skipping integration test in short mode.") + } + initEnv() + + conn := testDial(t) + defer conn.Close() + + result, err := testExec(conn, "insert into test_table(val) values ('abcd')", nil, actionCommit) + if err != nil { + t.Error(err) + } + if result.InsertId == 0 { + t.Errorf("InsertId: 0, want non-zero") + } + if result.RowsAffected != 1 { + t.Errorf("RowsAffected: %d, want 1", result.RowsAffected) + } + + result, err = testExec(conn, "select * from test_table", nil, actionSelect) + if err != nil { + t.Error(err) + } + wantFields := []mproto.Field{ + {Name: "id", Type: 3}, + {Name: "val", Type: 253}, + } + wantVal := "abcd" + if !reflect.DeepEqual(result.Fields, wantFields) { + t.Errorf("Fields: \n%#v, want \n%#v", result.Fields, wantFields) + } + gotVal := result.Rows[0][1].String() + if gotVal != wantVal { + t.Errorf("val: %q, want %q", gotVal, wantVal) + } + + _, err = testExec(conn, "delete from test_table", nil, actionCommit) + if err != nil { + t.Error(err) + } + if result.RowsAffected != 1 { + t.Errorf("RowsAffected: %d, want 1", result.RowsAffected) + } +} + +func TestStreamExecute(t *testing.T) { + t.Skip("skipping for go1.3") + if testing.Short() { + t.Skip("skipping integration test in short mode.") + } + initEnv() + + conn := testDial(t) + defer conn.Close() + + _, err := testExec(conn, "insert into test_table(val) values ('abcd')", nil, actionCommit) + if err != nil { + t.Error(err) + } + + ch, errFunc := conn.StreamExecute(context.Background(), "select * from test_table", nil, "master") + + result := <-ch + wantFields := []mproto.Field{ + {Name: "id", Type: 3}, + {Name: "val", Type: 253}, + } + if !reflect.DeepEqual(result.Fields, wantFields) { + t.Errorf("Fields: \n%#v, want \n%#v", result.Fields, wantFields) + } + + result = <-ch + wantVal := "abcd" + gotVal := result.Rows[0][1].String() + if gotVal != wantVal { + t.Errorf("val: %q, want %q", gotVal, wantVal) + } + + for result = range ch { + fmt.Errorf("Result: %+v, want closed channel", result) + } + if err := errFunc(); err != nil { + t.Error(err) + } + + _, err = testExec(conn, "delete from test_table", nil, actionCommit) + if err != nil { + t.Error(err) + } +} + +func TestRollback(t *testing.T) { + t.Skip("skipping for go1.3") + if testing.Short() { + t.Skip("skipping integration test in short mode.") + } + initEnv() + + conn := testDial(t) + defer conn.Close() + + _, err := testExec(conn, "insert into test_table(val) values ('abcd')", nil, actionRollback) + if err != nil { + t.Error(err) + } + + result, err := testExec(conn, "select * from test_table", nil, actionSelect) + if err != nil { + t.Error(err) + } + if result.RowsAffected != 0 { + t.Errorf("RowsAffected: %d, want 0", result.RowsAffected) + } +} + +func TestExecuteUnimplemented(t *testing.T) { + t.Skip("skipping for go1.3") + if testing.Short() { + t.Skip("skipping integration test in short mode.") + } + initEnv() + + conn := testDial(t) + defer conn.Close() + + _, err := conn.ExecuteBatch(nil, nil, "") + want := "not implemented yet" + if err.Error() != want { + t.Errorf("ExecuteBatch: %v, want %q", err, want) + } + + _, err = conn.SplitQuery(nil, proto.BoundQuery{}, 1) + if err.Error() != want { + t.Errorf("SplitQuery: %v, want %q", err, want) + } +} + +func TestExecuteFail(t *testing.T) { + t.Skip("skipping for go1.3") + if testing.Short() { + t.Skip("skipping integration test in short mode.") + } + initEnv() + + conn := testDial(t) + defer conn.Close() + + _, err := testExec(conn, "select * from notable", nil, actionSelect) + want := "execute: cannot route query: select * from notable: table notable not found" + if err == nil || err.Error() != want { + t.Errorf("err: %v, want %q", err, want) + } +} + +func TestBadTx(t *testing.T) { + t.Skip("skipping for go1.3") + if testing.Short() { + t.Skip("skipping integration test in short mode.") + } + initEnv() + + conn := testDial(t) + defer conn.Close() + ctx := context.Background() + + err := conn.Commit(ctx) + want := "commit: not in transaction" + if err == nil || err.Error() != want { + t.Errorf("Commit: %v, want %v", err, want) + } + + err = conn.Rollback(ctx) + if err != nil { + t.Error(err) + } + + err = conn.Begin(ctx) + if err != nil { + t.Error(err) + } + err = conn.Begin(ctx) + want = "begin: already in a transaction" + if err == nil || err.Error() != want { + t.Errorf("Begin: %v, want %v", err, want) + } +} + +func testDial(t *testing.T) vtgateconn.VTGateConn { + // TODO(sougou): Fetch port from the launch output. + conn, err := dial(nil, "localhost:15007", time.Duration(3*time.Second)) + if err != nil { + t.Fatal(err) + } + return conn +} + +func testExec(conn vtgateconn.VTGateConn, query string, bindVars map[string]interface{}, action int) (*mproto.QueryResult, error) { + ctx := context.Background() + var err error + if action == actionCommit || action == actionRollback { + err = conn.Begin(ctx) + if err != nil { + return nil, err + } + } + result, err := conn.Execute(ctx, query, nil, "master") + if err != nil { + return nil, err + } + switch action { + case actionCommit: + err = conn.Commit(ctx) + case actionRollback: + err = conn.Rollback(ctx) + } + if err != nil { + return nil, err + } + return result, nil +} diff --git a/go/vt/vtgate/gorpcvtgateservice/server.go b/go/vt/vtgate/gorpcvtgateservice/server.go index bcb8eaadadd..8785fff837e 100644 --- a/go/vt/vtgate/gorpcvtgateservice/server.go +++ b/go/vt/vtgate/gorpcvtgateservice/server.go @@ -6,17 +6,21 @@ package gorpcvtgateservice import ( - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/vt/rpc" "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/vtgate" "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" ) type VTGate struct { server *vtgate.VTGate } +func (vtg *VTGate) Execute(ctx context.Context, query *proto.Query, reply *proto.QueryResult) error { + return vtg.server.Execute(ctx, query, reply) +} + func (vtg *VTGate) ExecuteShard(ctx context.Context, query *proto.QueryShard, reply *proto.QueryResult) error { return vtg.server.ExecuteShard(ctx, query, reply) } @@ -41,6 +45,12 @@ func (vtg *VTGate) ExecuteBatchKeyspaceIds(ctx context.Context, batchQuery *prot return vtg.server.ExecuteBatchKeyspaceIds(ctx, batchQuery, reply) } +func (vtg *VTGate) StreamExecute(ctx context.Context, query *proto.Query, sendReply func(interface{}) error) error { + return vtg.server.StreamExecute(ctx, query, func(value *proto.QueryResult) error { + return sendReply(value) + }) +} + func (vtg *VTGate) StreamExecuteShard(ctx context.Context, query *proto.QueryShard, sendReply func(interface{}) error) error { return vtg.server.StreamExecuteShard(ctx, query, func(value *proto.QueryResult) error { return sendReply(value) diff --git a/go/vt/vtgate/hash_index.go b/go/vt/vtgate/hash_index.go deleted file mode 100644 index 4639d27fcae..00000000000 --- a/go/vt/vtgate/hash_index.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2014, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package vtgate - -// This is a V3 file. Do not intermix with V2. - -import ( - "crypto/cipher" - "crypto/des" - "encoding/binary" - "fmt" - - "github.com/youtube/vitess/go/sqltypes" - "github.com/youtube/vitess/go/vt/key" - "github.com/youtube/vitess/go/vt/topo" -) - -type HashIndex struct { - keyspace string - serv SrvTopoServer - cell string -} - -func NewHashIndex(keyspace string, serv SrvTopoServer, cell string) *HashIndex { - return &HashIndex{ - keyspace: keyspace, - serv: serv, - cell: cell, - } -} - -func (hindex *HashIndex) Resolve(tabletType topo.TabletType, shardKeys []interface{}) (newKeyspace string, shards []string, err error) { - newKeyspace, allShards, err := getKeyspaceShards(hindex.serv, hindex.cell, hindex.keyspace, tabletType) - if err != nil { - return "", nil, err - } - shards = make([]string, 0, len(shardKeys)) - for _, shardKey := range shardKeys { - num, err := getNumber(shardKey) - if err != nil { - return "", nil, err - } - shard, err := getShardForKeyspaceId(allShards, vhash(num)) - if err != nil { - return "", nil, err - } - shards = append(shards, shard) - } - return newKeyspace, shards, nil -} - -func getNumber(v interface{}) (uint64, error) { - switch v := v.(type) { - case int: - return uint64(v), nil - case int32: - return uint64(v), nil - case int64: - return uint64(v), nil - case uint: - return uint64(v), nil - case uint32: - return uint64(v), nil - case uint64: - return v, nil - case sqltypes.Value: - result, err := v.ParseUint64() - if err != nil { - return 0, fmt.Errorf("error parsing %v: %v", v, err) - } - return result, nil - } - return 0, fmt.Errorf("unexpected type for %v: %T", v, v) -} - -var ( - block3DES cipher.Block - iv3DES = make([]byte, 8) -) - -func init() { - var err error - block3DES, err = des.NewTripleDESCipher(make([]byte, 24)) - if err != nil { - panic(err) - } -} - -func vhash(shardKey uint64) key.KeyspaceId { - var keybytes, hashed [8]byte - binary.BigEndian.PutUint64(keybytes[:], shardKey) - encrypter := cipher.NewCBCEncrypter(block3DES, iv3DES) - encrypter.CryptBlocks(hashed[:], keybytes[:]) - return key.KeyspaceId(hashed[:]) -} diff --git a/go/vt/vtgate/hash_index_test.go b/go/vt/vtgate/hash_index_test.go deleted file mode 100644 index 6298698760f..00000000000 --- a/go/vt/vtgate/hash_index_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package vtgate - -import ( - "reflect" - "testing" - - "github.com/youtube/vitess/go/sqltypes" - - "github.com/youtube/vitess/go/vt/topo" -) - -func TestConvert(t *testing.T) { - cases := []struct { - in uint64 - out string - }{ - {1, "\x16k@\xb4J\xbaK\xd6"}, - {0, "\x8c\xa6M\xe9\xc1\xb1#\xa7"}, - {11, "\xae\xfcDI\x1c\xfeGL"}, - {0x100000000000000, "\r\x9f'\x9b\xa5\xd8r`"}, - {0x800000000000000, " \xb9\xe7g\xb2\xfb\x14V"}, - {11, "\xae\xfcDI\x1c\xfeGL"}, - {0, "\x8c\xa6M\xe9\xc1\xb1#\xa7"}, - } - for _, c := range cases { - got := string(vhash(c.in)) - want := c.out - if got != want { - t.Errorf("For %d: got: %#v, want %q", c.in, got, want) - } - } -} - -func BenchmarkConvert(b *testing.B) { - for i := 0; i < b.N; i++ { - vhash(uint64(i)) - } -} - -func TestHashResolve(t *testing.T) { - hind := NewHashIndex(TEST_SHARDED, new(sandboxTopo), "") - nn, _ := sqltypes.BuildNumeric("11") - ks, shards, err := hind.Resolve(topo.TabletType("master"), []interface{}{1, int32(2), int64(3), uint(4), uint32(5), uint64(6), nn}) - if err != nil { - t.Error(err) - } - want := []string{"-20", "-20", "40-60", "c0-e0", "60-80", "e0-", "a0-c0"} - if !reflect.DeepEqual(shards, want) { - t.Errorf("got\n%#v, want\n%#v", shards, want) - } - if ks != TEST_SHARDED { - t.Errorf("got %v, want TEST_SHARDED", ks) - } - _, _, err = hind.Resolve(topo.TabletType("master"), []interface{}{"aa"}) - wantErr := "unexpected type for aa: string" - if err == nil || err.Error() != wantErr { - t.Errorf("got %v, want %v", err, wantErr) - } -} diff --git a/go/vt/vtgate/planbuilder/dml.go b/go/vt/vtgate/planbuilder/dml.go index 2c6890acd0e..694f41c16eb 100644 --- a/go/vt/vtgate/planbuilder/dml.go +++ b/go/vt/vtgate/planbuilder/dml.go @@ -4,7 +4,12 @@ package planbuilder -import "github.com/youtube/vitess/go/vt/sqlparser" +import ( + "bytes" + "fmt" + + "github.com/youtube/vitess/go/vt/sqlparser" +) func buildUpdatePlan(upd *sqlparser.Update, schema *Schema) *Plan { plan := &Plan{ @@ -12,42 +17,40 @@ func buildUpdatePlan(upd *sqlparser.Update, schema *Schema) *Plan { Rewritten: generateQuery(upd), } tablename := sqlparser.GetTableName(upd.Table) - plan.Table, plan.Reason = schema.LookupTable(tablename) + plan.Table, plan.Reason = schema.FindTable(tablename) if plan.Reason != "" { return plan } - if plan.Table.Keyspace.ShardingScheme == Unsharded { + if !plan.Table.Keyspace.Sharded { plan.ID = UpdateUnsharded return plan } - getWhereRouting(upd.Where, plan) + getWhereRouting(upd.Where, plan, true) switch plan.ID { - case SelectSingleShardKey: - plan.ID = UpdateSingleShardKey - case SelectSingleLookup: - plan.ID = UpdateSingleLookup - case SelectMultiShardKey, SelectMultiLookup, SelectScatter: + case SelectEqual: + plan.ID = UpdateEqual + case SelectIN, SelectScatter, SelectKeyrange: plan.ID = NoPlan - plan.Reason = "too complex" + plan.Reason = "update has multi-shard where clause" return plan default: panic("unexpected") } - if isIndexChanging(upd.Exprs, plan.Table.Indexes) { + if isIndexChanging(upd.Exprs, plan.Table.ColVindexes) { plan.ID = NoPlan plan.Reason = "index is changing" } return plan } -func isIndexChanging(setClauses sqlparser.UpdateExprs, indexes []*Index) bool { - indexCols := make([]string, len(indexes)) - for i, index := range indexes { - indexCols[i] = index.Column +func isIndexChanging(setClauses sqlparser.UpdateExprs, colVindexes []*ColVindex) bool { + vindexCols := make([]string, len(colVindexes)) + for i, index := range colVindexes { + vindexCols[i] = index.Col } for _, assignment := range setClauses { - if sqlparser.StringIn(string(assignment.Name.Name), indexCols...) { + if sqlparser.StringIn(string(assignment.Name.Name), vindexCols...) { return true } } @@ -60,26 +63,43 @@ func buildDeletePlan(del *sqlparser.Delete, schema *Schema) *Plan { Rewritten: generateQuery(del), } tablename := sqlparser.GetTableName(del.Table) - plan.Table, plan.Reason = schema.LookupTable(tablename) + plan.Table, plan.Reason = schema.FindTable(tablename) if plan.Reason != "" { return plan } - if plan.Table.Keyspace.ShardingScheme == Unsharded { + if !plan.Table.Keyspace.Sharded { plan.ID = DeleteUnsharded return plan } - getWhereRouting(del.Where, plan) + getWhereRouting(del.Where, plan, true) switch plan.ID { - case SelectSingleShardKey: - plan.ID = DeleteSingleShardKey - case SelectSingleLookup: - plan.ID = DeleteSingleLookup - case SelectMultiShardKey, SelectMultiLookup, SelectScatter: + case SelectEqual: + plan.ID = DeleteEqual + plan.Subquery = generateDeleteSubquery(del, plan.Table) + case SelectIN, SelectScatter, SelectKeyrange: plan.ID = NoPlan - plan.Reason = "too complex" + plan.Reason = "delete has multi-shard where clause" default: panic("unexpected") } return plan } + +func generateDeleteSubquery(del *sqlparser.Delete, table *Table) string { + if len(table.Owned) == 0 { + return "" + } + buf := bytes.NewBuffer(nil) + buf.WriteString("select ") + prefix := "" + for _, cv := range table.Owned { + buf.WriteString(prefix) + buf.WriteString(cv.Col) + prefix = ", " + } + fmt.Fprintf(buf, " from %s", table.Name) + buf.WriteString(sqlparser.String(del.Where)) + buf.WriteString(" for update") + return buf.String() +} diff --git a/go/vt/vtgate/planbuilder/insert.go b/go/vt/vtgate/planbuilder/insert.go index 70494a7ae7f..165a2364d4f 100644 --- a/go/vt/vtgate/planbuilder/insert.go +++ b/go/vt/vtgate/planbuilder/insert.go @@ -16,11 +16,11 @@ func buildInsertPlan(ins *sqlparser.Insert, schema *Schema) *Plan { Rewritten: generateQuery(ins), } tablename := sqlparser.GetTableName(ins.Table) - plan.Table, plan.Reason = schema.LookupTable(tablename) + plan.Table, plan.Reason = schema.FindTable(tablename) if plan.Reason != "" { return plan } - if plan.Table.Keyspace.ShardingScheme == Unsharded { + if !plan.Table.Keyspace.Sharded { plan.ID = InsertUnsharded return plan } @@ -53,45 +53,39 @@ func buildInsertPlan(ins *sqlparser.Insert, schema *Schema) *Plan { plan.Reason = "column list doesn't match values" return plan } - indexes := schema.Tables[tablename].Indexes + colVindexes := schema.Tables[tablename].ColVindexes plan.ID = InsertSharded - plan.Values = make([]interface{}, 0, len(indexes)) - for _, index := range indexes { + plan.Values = make([]interface{}, 0, len(colVindexes)) + for _, index := range colVindexes { if err := buildIndexPlan(ins, tablename, index, plan); err != nil { plan.ID = NoPlan plan.Reason = err.Error() return plan } } - // Query was rewritten plan.Rewritten = generateQuery(ins) return plan } -func buildIndexPlan(ins *sqlparser.Insert, tablename string, index *Index, plan *Plan) error { +func buildIndexPlan(ins *sqlparser.Insert, tablename string, colVindex *ColVindex, plan *Plan) error { pos := -1 for i, column := range ins.Columns { - if index.Column == sqlparser.GetColName(column.(*sqlparser.NonStarExpr).Expr) { + if colVindex.Col == sqlparser.GetColName(column.(*sqlparser.NonStarExpr).Expr) { pos = i break } } - if pos == -1 && index.Owner == tablename && index.IsAutoInc { + if pos == -1 { pos = len(ins.Columns) - ins.Columns = append(ins.Columns, &sqlparser.NonStarExpr{Expr: &sqlparser.ColName{Name: []byte(index.Column)}}) + ins.Columns = append(ins.Columns, &sqlparser.NonStarExpr{Expr: &sqlparser.ColName{Name: []byte(colVindex.Col)}}) ins.Rows.(sqlparser.Values)[0] = append(ins.Rows.(sqlparser.Values)[0].(sqlparser.ValTuple), &sqlparser.NullVal{}) } - if pos == -1 { - return fmt.Errorf("must supply value for indexed column: %s", index.Column) - } row := ins.Rows.(sqlparser.Values)[0].(sqlparser.ValTuple) - val, err := sqlparser.AsInterface(row[pos]) + val, err := asInterface(row[pos]) if err != nil { - return fmt.Errorf("could not convert val: %s, pos: %d", row[pos], pos) + return fmt.Errorf("could not convert val: %s, pos: %d: %v", sqlparser.String(row[pos]), pos, err) } plan.Values = append(plan.Values.([]interface{}), val) - if index.Owner == tablename && index.IsAutoInc { - row[pos] = sqlparser.ValArg([]byte(fmt.Sprintf(":_%s", index.Column))) - } + row[pos] = sqlparser.ValArg([]byte(fmt.Sprintf(":_%s", colVindex.Col))) return nil } diff --git a/go/vt/vtgate/planbuilder/plan.go b/go/vt/vtgate/planbuilder/plan.go index dc5ed3231cb..973c7a52a03 100644 --- a/go/vt/vtgate/planbuilder/plan.go +++ b/go/vt/vtgate/planbuilder/plan.go @@ -5,65 +5,120 @@ package planbuilder import ( + "encoding/json" "fmt" "github.com/youtube/vitess/go/vt/sqlparser" ) +// PlanID is number representing the plan id. type PlanID int +// The following constants define all the PlanID values. const ( NoPlan = PlanID(iota) SelectUnsharded - SelectSingleShardKey - SelectMultiShardKey - SelectSingleLookup - SelectMultiLookup + SelectEqual + SelectIN + SelectKeyrange SelectScatter UpdateUnsharded - UpdateSingleShardKey - UpdateSingleLookup + UpdateEqual DeleteUnsharded - DeleteSingleShardKey - DeleteSingleLookup + DeleteEqual InsertUnsharded InsertSharded NumPlans ) -type Plan struct { - ID PlanID - Reason string - Table *Table - Original string - Rewritten string - Index *Index - Values interface{} -} - -func (pln *Plan) Size() int { - return 1 -} - // Must exactly match order of plan constants. -var planName = []string{ +var planName = [NumPlans]string{ "NoPlan", "SelectUnsharded", - "SelectSingleShardKey", - "SelectMultiShardKey", - "SelectSingleLookup", - "SelectMultiLookup", + "SelectEqual", + "SelectIN", + "SelectKeyrange", "SelectScatter", "UpdateUnsharded", - "UpdateSingleShardKey", - "UpdateSingleLookup", + "UpdateEqual", "DeleteUnsharded", - "DeleteSingleShardKey", - "DeleteSingleLookup", + "DeleteEqual", "InsertUnsharded", "InsertSharded", } +// Plan represents the routing strategy for a given query. +type Plan struct { + ID PlanID + // Reason usually contains a string describing the reason + // for why a certain plan was (or not) chosen. + Reason string + Table *Table + // Original is the original query. + Original string + // Rewritten is the rewritten query. This is empty for + // all Unsharded plans since the Original query is sufficient. + Rewritten string + // Subquery is used for DeleteUnsharded to fetch the column values + // for owned vindexes so they can be deleted. + Subquery string + ColVindex *ColVindex + // Values is a single or a list of values that are used + // for making routing decisions. + Values interface{} +} + +// Size is defined so that Plan can be given to an LRUCache. +func (pln *Plan) Size() int { + return 1 +} + +// MarshalJSON serializes the Plan into a JSON representation. +func (pln *Plan) MarshalJSON() ([]byte, error) { + var tname, vindexName, col string + if pln.Table != nil { + tname = pln.Table.Name + } + if pln.ColVindex != nil { + vindexName = pln.ColVindex.Name + col = pln.ColVindex.Col + } + marshalPlan := struct { + ID PlanID + Reason string + Table string + Original string + Rewritten string + Subquery string + Vindex string + Col string + Values interface{} + }{ + ID: pln.ID, + Reason: pln.Reason, + Table: tname, + Original: pln.Original, + Rewritten: pln.Rewritten, + Subquery: pln.Subquery, + Vindex: vindexName, + Col: col, + Values: pln.Values, + } + return json.Marshal(marshalPlan) +} + +// IsMulti returns true if the SELECT query can potentially +// be sent to more than one shard. +func (pln *Plan) IsMulti() bool { + if pln.ID == SelectIN || pln.ID == SelectScatter { + return true + } + if pln.ID == SelectEqual && !IsUnique(pln.ColVindex.Vindex) { + return true + } + return false +} + func (id PlanID) String() string { if id < 0 || id >= NumPlans { return "" @@ -71,6 +126,8 @@ func (id PlanID) String() string { return planName[id] } +// PlanByName returns the PlanID from the plan name. +// If it cannot be found, then it returns NumPlans. func PlanByName(s string) (id PlanID, ok bool) { for i, v := range planName { if v == s { @@ -80,14 +137,12 @@ func PlanByName(s string) (id PlanID, ok bool) { return NumPlans, false } -func (id PlanID) IsMulti() bool { - return id == SelectMultiShardKey || id == SelectMultiLookup || id == SelectScatter -} - +// MarshalJSON serializes the plan id as a JSON string. func (id PlanID) MarshalJSON() ([]byte, error) { return ([]byte)(fmt.Sprintf("\"%s\"", id.String())), nil } +// BuildPlan builds a plan for a query based on the specified schema. func BuildPlan(query string, schema *Schema) *Plan { statement, err := sqlparser.Parse(query) if err != nil { @@ -99,7 +154,7 @@ func BuildPlan(query string, schema *Schema) *Plan { } noplan := &Plan{ ID: NoPlan, - Reason: "too complex", + Reason: "cannot build a plan for this construct", Original: query, } var plan *Plan diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go index 85911119eea..ced8163b290 100644 --- a/go/vt/vtgate/planbuilder/plan_test.go +++ b/go/vt/vtgate/planbuilder/plan_test.go @@ -17,8 +17,54 @@ import ( "testing" "github.com/youtube/vitess/go/testfiles" + "github.com/youtube/vitess/go/vt/key" ) +// hashIndex satisfies Functional, Unique. +type hashIndex struct{} + +func (*hashIndex) Cost() int { return 1 } +func (*hashIndex) Verify(VCursor, interface{}, key.KeyspaceId) (bool, error) { + return false, nil +} +func (*hashIndex) Map(VCursor, []interface{}) ([]key.KeyspaceId, error) { return nil, nil } +func (*hashIndex) Create(VCursor, interface{}) error { return nil } +func (*hashIndex) Delete(VCursor, []interface{}, key.KeyspaceId) error { return nil } + +func newHashIndex(map[string]interface{}) (Vindex, error) { return &hashIndex{}, nil } + +// lookupIndex satisfies Lookup, Unique. +type lookupIndex struct{} + +func (*lookupIndex) Cost() int { return 2 } +func (*lookupIndex) Verify(VCursor, interface{}, key.KeyspaceId) (bool, error) { + return false, nil +} +func (*lookupIndex) Map(VCursor, []interface{}) ([]key.KeyspaceId, error) { return nil, nil } +func (*lookupIndex) Create(VCursor, interface{}, key.KeyspaceId) error { return nil } +func (*lookupIndex) Delete(VCursor, []interface{}, key.KeyspaceId) error { return nil } + +func newLookupIndex(map[string]interface{}) (Vindex, error) { return &lookupIndex{}, nil } + +// multiIndex satisfies Lookup, NonUnique. +type multiIndex struct{} + +func (*multiIndex) Cost() int { return 3 } +func (*multiIndex) Verify(VCursor, interface{}, key.KeyspaceId) (bool, error) { + return false, nil +} +func (*multiIndex) Map(VCursor, []interface{}) ([][]key.KeyspaceId, error) { return nil, nil } +func (*multiIndex) Create(VCursor, interface{}, key.KeyspaceId) error { return nil } +func (*multiIndex) Delete(VCursor, []interface{}, key.KeyspaceId) error { return nil } + +func newMultiIndex(map[string]interface{}) (Vindex, error) { return &multiIndex{}, nil } + +func init() { + Register("hash", newHashIndex) + Register("lookup", newLookupIndex) + Register("multi", newMultiIndex) +} + func TestPlanName(t *testing.T) { id, ok := PlanByName("SelectUnsharded") if !ok { @@ -38,7 +84,7 @@ func TestPlanName(t *testing.T) { } func TestPlan(t *testing.T) { - schema, err := LoadSchemaJSON(locateFile("schema_test.json")) + schema, err := LoadFile(locateFile("schema_test.json")) if err != nil { t.Fatal(err) } @@ -52,7 +98,7 @@ func testFile(t *testing.T, filename string, schema *Schema) { plan := BuildPlan(tcase.input, schema) if plan.ID == NoPlan { plan.Rewritten = "" - plan.Index = nil + plan.ColVindex = nil plan.Values = nil } bout, err := json.Marshal(plan) @@ -63,7 +109,6 @@ func testFile(t *testing.T, filename string, schema *Schema) { if out != tcase.output { t.Error(fmt.Sprintf("File: %s, Line:%v\n%s\n%s", filename, tcase.lineno, tcase.output, out)) } - //fmt.Printf("%s\n%s\n\n", tcase.input, out) } } @@ -104,14 +149,13 @@ func iterateExecFile(name string) (testCaseIterator chan testCase) { if err != nil { if err != io.EOF { fmt.Printf("Line: %d\n", lineno) - panic(fmt.Errorf("Error reading file %s: %s", name, err.Error())) + panic(fmt.Errorf("error reading file %s: %s", name, err.Error())) } break } lineno++ input := string(binput) if input == "" || input == "\n" || input[0] == '#' || strings.HasPrefix(input, "Length:") { - //fmt.Printf("%s\n", input) continue } err = json.Unmarshal(binput, &input) @@ -126,7 +170,7 @@ func iterateExecFile(name string) (testCaseIterator chan testCase) { lineno++ if err != nil { fmt.Printf("Line: %d\n", lineno) - panic(fmt.Errorf("Error reading file %s: %s", name, err.Error())) + panic(fmt.Errorf("error reading file %s: %s", name, err.Error())) } output = append(output, l...) if l[0] == '}' { diff --git a/go/vt/vtgate/planbuilder/registrar.go b/go/vt/vtgate/planbuilder/registrar.go new file mode 100644 index 00000000000..780fa172a86 --- /dev/null +++ b/go/vt/vtgate/planbuilder/registrar.go @@ -0,0 +1,136 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package planbuilder + +import ( + "fmt" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/vt/key" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" +) + +// This file defines interfaces and registration for vindexes. + +// A VCursor is an interface that allows you to execute queries +// in the current context and session of a VTGate request. Vindexes +// can use this interface to execute lookup queries. +type VCursor interface { + Execute(query *tproto.BoundQuery) (*mproto.QueryResult, error) +} + +// Vindex defines the interface required to register a vindex. +// Additional to these functions, a vindex also needs +// to satisfy the Unique or NonUnique interface. +type Vindex interface { + // Cost is used by planbuilder to prioritize vindexes. + // The cost can be 0 if the id is basically a keyspace id. + // The cost can be 1 if the id can be hashed to a keyspace id. + // The cost can be 2 or above if the id needs to be looked up + // from an external data source. These guidelines are subject + // to change in the future. + Cost() int + + // Verify must be implented by all vindexes. It should return + // true if the id can be mapped to the keyspace id. + Verify(cursor VCursor, id interface{}, ks key.KeyspaceId) (bool, error) +} + +// Unique defines the interface for a unique vindex. +// For a vindex to be unique, an id has to map to at most +// one keyspace id. +type Unique interface { + Map(cursor VCursor, ids []interface{}) ([]key.KeyspaceId, error) +} + +// NonUnique defines the interface for a non-unique vindex. +// This means that an id can map to multiple keyspace ids. +type NonUnique interface { + Map(cursor VCursor, ids []interface{}) ([][]key.KeyspaceId, error) +} + +// IsUnique returns true if the Vindex is Unique. +func IsUnique(v Vindex) bool { + _, ok := v.(Unique) + return ok +} + +// A Reversible vindex is one that can perform a +// reverse lookup from a keyspace id to an id. This +// is optional. If present, VTGate can use it to +// fill column values based on the target keyspace id. +type Reversible interface { + ReverseMap(cursor VCursor, ks key.KeyspaceId) (interface{}, error) +} + +// A Functional vindex is an index that can compute +// the keyspace id from the id without a lookup. This +// means that the creation of a functional vindex entry +// does not take the keyspace id as input. In general, +// the main reason to support creation functions for +// functional indexes is for auto-generating ids. +// A Functional vindex is also required to be Unique. +// If it's not unique, we cannot determine the target shard +// for an insert operation. +type Functional interface { + Create(VCursor, interface{}) error + Delete(VCursor, []interface{}, key.KeyspaceId) error + Unique +} + +// A FunctionalGenerator vindex is a Functional vindex +// that can generate new ids. +type FunctionalGenerator interface { + Functional + Generate(cursor VCursor) (id int64, err error) +} + +// A Lookup vindex is one that needs to lookup +// a previously stored map to compute the keyspace +// id from an id. This means that the creation of +// a lookup vindex entry requires a keyspace id as +// input. +// A Lookup vindex need not be unique because the +// keyspace_id, which must be supplied, can be used +// to determine the target shard for an insert operation. +type Lookup interface { + Create(VCursor, interface{}, key.KeyspaceId) error + Delete(VCursor, []interface{}, key.KeyspaceId) error +} + +// A LookupGenerator vindex is a Lookup that can +// generate new ids. +type LookupGenerator interface { + Lookup + Generate(VCursor, key.KeyspaceId) (id int64, err error) +} + +// A NewVindexFunc is a function that creates a Vindex based on the +// properties specified in the input map. Every vindex must +// register a NewVindexFunc under a unique vindexType. +type NewVindexFunc func(map[string]interface{}) (Vindex, error) + +var registry = make(map[string]NewVindexFunc) + +// Register registers a vindex under the specified vindexType. +// A duplicate vindexType will generate a panic. +// New vindexes will be created using these functions at the +// time of schema loading. +func Register(vindexType string, newVindexFunc NewVindexFunc) { + if _, ok := registry[vindexType]; ok { + panic(fmt.Sprintf("%s is already registered", vindexType)) + } + registry[vindexType] = newVindexFunc +} + +// CreateVindex creates a vindex of the specified type using the +// supplied params. The type must have been previously registered. +func CreateVindex(vindexType string, params map[string]interface{}) (Vindex, error) { + f, ok := registry[vindexType] + if !ok { + return nil, fmt.Errorf("vindexType %s not found", vindexType) + } + return f(params) +} diff --git a/go/vt/vtgate/planbuilder/schema.go b/go/vt/vtgate/planbuilder/schema.go index 0c4c6f484a6..0e6c23d1f2e 100644 --- a/go/vt/vtgate/planbuilder/schema.go +++ b/go/vt/vtgate/planbuilder/schema.go @@ -7,20 +7,8 @@ package planbuilder import ( "encoding/json" "fmt" - - "github.com/youtube/vitess/go/jscfg" -) - -// Keyspace types. -const ( - Unsharded = iota - HashSharded -) - -// Index types. -const ( - ShardKey = iota - Lookup + "io/ioutil" + "sort" ) // Schema represents the denormalized version of SchemaFormal, @@ -31,87 +19,111 @@ type Schema struct { // Table represnts a table in Schema. type Table struct { - Name string - Keyspace *Keyspace - Indexes []*Index -} - -// MarshalJSON should only be used for testing. -func (t *Table) MarshalJSON() ([]byte, error) { - return json.Marshal(t.Name) + Name string + Keyspace *Keyspace + ColVindexes []*ColVindex + Ordered []*ColVindex + Owned []*ColVindex } // Keyspace contains the keyspcae info for each Table. type Keyspace struct { - Name string - // ShardingScheme is Unsharded or HashSharded. - ShardingScheme int + Name string + Sharded bool } -// Index contains the index info for each index of a table. -type Index struct { - // Type is ShardKey or Lookup. - Type int - Column string - Name string - From, To string - Owner string - IsAutoInc bool +// ColVindex contains the index info for each index of a table. +type ColVindex struct { + Col string + Type string + Name string + Owned bool + Vindex Vindex } // BuildSchema builds a Schema from a SchemaFormal. func BuildSchema(source *SchemaFormal) (schema *Schema, err error) { - allindexes := make(map[string]string) schema = &Schema{Tables: make(map[string]*Table)} for ksname, ks := range source.Keyspaces { keyspace := &Keyspace{ - Name: ksname, - ShardingScheme: ks.ShardingScheme, + Name: ksname, + Sharded: ks.Sharded, + } + vindexes := make(map[string]Vindex) + for vname, vindexInfo := range ks.Vindexes { + vindex, err := CreateVindex(vindexInfo.Type, vindexInfo.Params) + if err != nil { + return nil, err + } + switch vindex.(type) { + case Unique: + case NonUnique: + default: + return nil, fmt.Errorf("vindex %s needs to be Unique or NonUnique", vname) + } + vindexes[vname] = vindex } - for tname, table := range ks.Tables { + for tname, cname := range ks.Tables { if _, ok := schema.Tables[tname]; ok { return nil, fmt.Errorf("table %s has multiple definitions", tname) } t := &Table{ Name: tname, Keyspace: keyspace, - Indexes: make([]*Index, 0, len(table.IndexColumns)), } - for i, ind := range table.IndexColumns { - idx, ok := ks.Indexes[ind.IndexName] + if !keyspace.Sharded { + schema.Tables[tname] = t + continue + } + class, ok := ks.Classes[cname] + if !ok { + return nil, fmt.Errorf("class %s not found for table %s", cname, tname) + } + for i, ind := range class.ColVindexes { + vindexInfo, ok := ks.Vindexes[ind.Name] if !ok { - return nil, fmt.Errorf("index %s not found for table %s", ind.IndexName, tname) + return nil, fmt.Errorf("vindex %s not found for class %s", ind.Name, cname) + } + columnVindex := &ColVindex{ + Col: ind.Col, + Type: vindexInfo.Type, + Name: ind.Name, + Owned: vindexInfo.Owner == tname, + Vindex: vindexes[ind.Name], } - if i == 0 && idx.Type != ShardKey { - return nil, fmt.Errorf("first index is not ShardKey for table %s", tname) + if i == 0 { + // Perform Primary vindex check. + if _, ok := columnVindex.Vindex.(Unique); !ok { + return nil, fmt.Errorf("primary index %s is not Unique for class %s", ind.Name, cname) + } + if columnVindex.Owned { + if _, ok := columnVindex.Vindex.(Functional); !ok { + return nil, fmt.Errorf("primary owned index %s is not Functional for class %s", ind.Name, cname) + } + } + } else { + // Perform non-primary vindex check. + if columnVindex.Owned { + if _, ok := columnVindex.Vindex.(Lookup); !ok { + return nil, fmt.Errorf("non-primary owned index %s is not Lookup for class %s", ind.Name, cname) + } + } } - switch prevks := allindexes[ind.IndexName]; prevks { - case "": - allindexes[ind.IndexName] = ksname - case ksname: - // We're good. - default: - return nil, fmt.Errorf("index %s used in more than one keyspace: %s %s", ind.IndexName, prevks, ksname) + t.ColVindexes = append(t.ColVindexes, columnVindex) + if columnVindex.Owned { + t.Owned = append(t.Owned, columnVindex) } - t.Indexes = append(t.Indexes, &Index{ - Type: idx.Type, - Column: ind.Column, - Name: ind.IndexName, - From: idx.From, - To: idx.To, - Owner: idx.Owner, - IsAutoInc: idx.IsAutoInc, - }) } + t.Ordered = colVindexSorted(t.ColVindexes) schema.Tables[tname] = t } } return schema, nil } -// LookupTable returns a pointer to the Table if found. +// FindTable returns a pointer to the Table if found. // Otherwise, it returns a reason, which is equivalent to an error. -func (schema *Schema) LookupTable(tablename string) (table *Table, reason string) { +func (schema *Schema) FindTable(tablename string) (table *Table, reason string) { if tablename == "" { return nil, "complex table expression" } @@ -122,6 +134,22 @@ func (schema *Schema) LookupTable(tablename string) (table *Table, reason string return table, "" } +// ByCost provides the interface needed for ColVindexes to +// be sorted by cost order. +type ByCost []*ColVindex + +func (bc ByCost) Len() int { return len(bc) } +func (bc ByCost) Swap(i, j int) { bc[i], bc[j] = bc[j], bc[i] } +func (bc ByCost) Less(i, j int) bool { return bc[i].Vindex.Cost() < bc[j].Vindex.Cost() } + +func colVindexSorted(cvs []*ColVindex) (sorted []*ColVindex) { + for _, cv := range cvs { + sorted = append(sorted, cv) + } + sort.Sort(ByCost(sorted)) + return sorted +} + // SchemaFormal is the formal representation of the schema // as loaded from the source. type SchemaFormal struct { @@ -131,41 +159,47 @@ type SchemaFormal struct { // KeyspaceFormal is the keyspace info for each keyspace // as loaded from the source. type KeyspaceFormal struct { - ShardingScheme int - Indexes map[string]IndexFormal - Tables map[string]TableFormal + Sharded bool + Vindexes map[string]VindexFormal + Classes map[string]ClassFormal + Tables map[string]string } -// IndexFormal is the info for each index as loaded from +// VindexFormal is the info for each index as loaded from // the source. -type IndexFormal struct { - // Type is ShardKey or Lookup. - Type int - From, To string - Owner string - IsAutoInc bool +type VindexFormal struct { + Type string + Params map[string]interface{} + Owner string } -// TableFormal is the info for each table as loaded from +// ClassFormal is the info for each table class as loaded from // the source. -type TableFormal struct { - IndexColumns []IndexColumnFormal +type ClassFormal struct { + ColVindexes []ColVindexFormal } -// IndexColumnFormal is the info for each indexed column +// ColVindexFormal is the info for each indexed column // of a table as loaded from the source. -type IndexColumnFormal struct { - Column string - IndexName string +type ColVindexFormal struct { + Col string + Name string +} + +// LoadFile creates a new Schema from a JSON file. +func LoadFile(filename string) (schema *Schema, err error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("ReadFile failed: %v %v", filename, err) + } + return NewSchema(data) } -// LoadSchemaJSON loads the formal representation of a schema -// from a JSON file and returns the more usable denormalized -// representaion (Schema) for it. -func LoadSchemaJSON(filename string) (schema *Schema, err error) { +// NewSchema creates a new Schema from a JSON byte array. +func NewSchema(data []byte) (schema *Schema, err error) { var source SchemaFormal - if err := jscfg.ReadJson(filename, &source); err != nil { - return nil, err + if err := json.Unmarshal(data, &source); err != nil { + return nil, fmt.Errorf("Unmarshal failed: %v %s %v", source, data, err) } return BuildSchema(&source) } diff --git a/go/vt/vtgate/planbuilder/schema_test.go b/go/vt/vtgate/planbuilder/schema_test.go index af6c4e5b7bc..8ca09ca40ed 100644 --- a/go/vt/vtgate/planbuilder/schema_test.go +++ b/go/vt/vtgate/planbuilder/schema_test.go @@ -5,106 +5,566 @@ package planbuilder import ( + "reflect" + "strings" "testing" + + "github.com/youtube/vitess/go/vt/key" ) -var ( - idx1 = map[string]IndexFormal{ - "idx1": {}, +// stFU satisfies Functional, Unique. +type stFU struct { + Params map[string]interface{} +} + +func (*stFU) Cost() int { return 1 } +func (*stFU) Verify(VCursor, interface{}, key.KeyspaceId) (bool, error) { return false, nil } +func (*stFU) Map(VCursor, []interface{}) ([]key.KeyspaceId, error) { return nil, nil } +func (*stFU) Create(VCursor, interface{}) error { return nil } +func (*stFU) Delete(VCursor, []interface{}, key.KeyspaceId) error { return nil } + +func NewSTFU(params map[string]interface{}) (Vindex, error) { + return &stFU{Params: params}, nil +} + +// stF satisfies Functional, but no Map. Invalid vindex. +type stF struct { + Params map[string]interface{} +} + +func (*stF) Cost() int { return 0 } +func (*stF) Verify(VCursor, interface{}, key.KeyspaceId) (bool, error) { return false, nil } + +func NewSTF(params map[string]interface{}) (Vindex, error) { + return &stF{Params: params}, nil +} + +// stLN satisfies Lookup, NonUnique. +type stLN struct { + Params map[string]interface{} +} + +func (*stLN) Cost() int { return 0 } +func (*stLN) Verify(VCursor, interface{}, key.KeyspaceId) (bool, error) { return false, nil } +func (*stLN) Map(VCursor, []interface{}) ([][]key.KeyspaceId, error) { return nil, nil } +func (*stLN) Create(VCursor, interface{}, key.KeyspaceId) error { return nil } +func (*stLN) Delete(VCursor, []interface{}, key.KeyspaceId) error { return nil } + +func NewSTLN(params map[string]interface{}) (Vindex, error) { + return &stLN{Params: params}, nil +} + +// stLU satisfies Lookup, Unique. +type stLU struct { + Params map[string]interface{} +} + +func (*stLU) Cost() int { return 2 } +func (*stLU) Verify(VCursor, interface{}, key.KeyspaceId) (bool, error) { return false, nil } +func (*stLU) Map(VCursor, []interface{}) ([]key.KeyspaceId, error) { return nil, nil } +func (*stLU) Create(VCursor, interface{}, key.KeyspaceId) error { return nil } +func (*stLU) Delete(VCursor, []interface{}, key.KeyspaceId) error { return nil } + +func NewSTLU(params map[string]interface{}) (Vindex, error) { + return &stLU{Params: params}, nil +} + +func init() { + Register("stfu", NewSTFU) + Register("stf", NewSTF) + Register("stln", NewSTLN) + Register("stlu", NewSTLU) +} + +func TestUnshardedSchema(t *testing.T) { + good := SchemaFormal{ + Keyspaces: map[string]KeyspaceFormal{ + "unsharded": { + Tables: map[string]string{ + "t1": "", + }, + }, + }, + } + got, err := BuildSchema(&good) + if err != nil { + t.Error(err) } - idx2 = map[string]IndexFormal{ - "idx2": { - Type: Lookup, + want := &Schema{ + Tables: map[string]*Table{ + "t1": &Table{ + Name: "t1", + Keyspace: &Keyspace{ + Name: "unsharded", + }, + ColVindexes: nil, + }, }, } - t1 = map[string]TableFormal{ - "t1": { - IndexColumns: []IndexColumnFormal{ - {IndexName: "idx1"}, + if !reflect.DeepEqual(got, want) { + t.Errorf("BuildSchema:s\n%v, want\n%v", got, want) + } +} + +func TestShardedSchemaOwned(t *testing.T) { + good := SchemaFormal{ + Keyspaces: map[string]KeyspaceFormal{ + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stfu1": { + Type: "stfu", + Params: map[string]interface{}{ + "stfu1": 1, + }, + Owner: "t1", + }, + "stln1": { + Type: "stln", + Owner: "t1", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "stfu1", + }, { + Col: "c2", + Name: "stln1", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, }, }, } - t2idx1 = map[string]TableFormal{ - "t2": { - IndexColumns: []IndexColumnFormal{ - {IndexName: "idx1"}, + got, err := BuildSchema(&good) + if err != nil { + t.Error(err) + } + want := &Schema{ + Tables: map[string]*Table{ + "t1": &Table{ + Name: "t1", + Keyspace: &Keyspace{ + Name: "sharded", + Sharded: true, + }, + ColVindexes: []*ColVindex{ + &ColVindex{ + Col: "c1", + Type: "stfu", + Name: "stfu1", + Owned: true, + Vindex: &stFU{ + Params: map[string]interface{}{ + "stfu1": 1, + }, + }, + }, + &ColVindex{ + Col: "c2", + Type: "stln", + Name: "stln1", + Owned: true, + Vindex: &stLN{}, + }, + }, }, }, } - t1idx2 = map[string]TableFormal{ - "t1": { - IndexColumns: []IndexColumnFormal{ - {IndexName: "idx2"}, + want.Tables["t1"].Ordered = []*ColVindex{ + want.Tables["t1"].ColVindexes[1], + want.Tables["t1"].ColVindexes[0], + } + want.Tables["t1"].Owned = want.Tables["t1"].ColVindexes + if !reflect.DeepEqual(got, want) { + t.Errorf("BuildSchema:s\n%v, want\n%v", got, want) + } +} + +func TestShardedSchemaNotOwned(t *testing.T) { + good := SchemaFormal{ + Keyspaces: map[string]KeyspaceFormal{ + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stlu1": { + Type: "stlu", + Owner: "", + }, + "stfu1": { + Type: "stfu", + Owner: "", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "stlu1", + }, { + Col: "c2", + Name: "stfu1", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, }, }, } -) + got, err := BuildSchema(&good) + if err != nil { + t.Error(err) + } + want := &Schema{ + Tables: map[string]*Table{ + "t1": &Table{ + Name: "t1", + Keyspace: &Keyspace{ + Name: "sharded", + Sharded: true, + }, + ColVindexes: []*ColVindex{ + &ColVindex{ + Col: "c1", + Type: "stlu", + Name: "stlu1", + Owned: false, + Vindex: &stLU{}, + }, + &ColVindex{ + Col: "c2", + Type: "stfu", + Name: "stfu1", + Owned: false, + Vindex: &stFU{}, + }, + }, + }, + }, + } + want.Tables["t1"].Ordered = []*ColVindex{ + want.Tables["t1"].ColVindexes[1], + want.Tables["t1"].ColVindexes[0], + } + if !reflect.DeepEqual(got, want) { + t.Errorf("BuildSchema:s\n%v, want\n%v", got, want) + } +} -func TestBuildSchemaErrors(t *testing.T) { - badschema := SchemaFormal{ +func TestLoadSchemaFail(t *testing.T) { + _, err := LoadFile("bogus file name") + want := "ReadFile failed" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("LoadFile: \n%q, should start with \n%q", err, want) + } + + _, err = NewSchema([]byte("{,}")) + want = "Unmarshal failed" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("LoadFile: \n%q, should start with \n%q", err, want) + } +} + +func TestBuildSchemaClassNotFoundFail(t *testing.T) { + bad := SchemaFormal{ Keyspaces: map[string]KeyspaceFormal{ - "ks1": { - Indexes: idx1, - Tables: t1, + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stfu": { + Type: "stfu", + }, + }, + Classes: map[string]ClassFormal{ + "notexist": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "noexist", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, }, - "ks2": { - Indexes: idx1, - Tables: t2idx1, + }, + } + _, err := BuildSchema(&bad) + want := "class t1 not found for table t1" + if err == nil || err.Error() != want { + t.Errorf("BuildSchema: %v, want %v", err, want) + } +} + +func TestBuildSchemaVindexNotFoundFail(t *testing.T) { + bad := SchemaFormal{ + Keyspaces: map[string]KeyspaceFormal{ + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "noexist": { + Type: "noexist", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "noexist", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, }, }, } - _, err := BuildSchema(&badschema) - want := "index idx1 used in more than one keyspace: ks1 ks2" - wantother := "index idx1 used in more than one keyspace: ks2 ks1" - if err == nil || (err.Error() != want && err.Error() != wantother) { - t.Errorf("got %v, want %s", err, want) + _, err := BuildSchema(&bad) + want := "vindexType noexist not found" + if err == nil || err.Error() != want { + t.Errorf("BuildSchema: %v, want %v", err, want) } +} - badschema.Keyspaces["ks2"] = KeyspaceFormal{ - Indexes: idx1, - Tables: t1, +func TestBuildSchemaInvalidVindexFail(t *testing.T) { + bad := SchemaFormal{ + Keyspaces: map[string]KeyspaceFormal{ + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stf": { + Type: "stf", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "stf", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, + }, + }, } - _, err = BuildSchema(&badschema) - want = "table t1 has multiple definitions" + _, err := BuildSchema(&bad) + want := "vindex stf needs to be Unique or NonUnique" if err == nil || err.Error() != want { - t.Errorf("got %v, want %s", err, want) + t.Errorf("BuildSchema: %v, want %v", err, want) } +} - badschema.Keyspaces["ks2"] = KeyspaceFormal{ - Indexes: idx1, - Tables: t1, +func TestBuildSchemaDupTableFail(t *testing.T) { + bad := SchemaFormal{ + Keyspaces: map[string]KeyspaceFormal{ + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stfu": { + Type: "stfu", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "stfu", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, + }, + "sharded1": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stfu": { + Type: "stfu", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "stfu", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, + }, + }, } - _, err = BuildSchema(&badschema) - want = "table t1 has multiple definitions" + _, err := BuildSchema(&bad) + want := "table t1 has multiple definitions" if err == nil || err.Error() != want { - t.Errorf("got %v, want %s", err, want) + t.Errorf("BuildSchema: %v, want %v", err, want) } +} - badschema = SchemaFormal{ +func TestBuildSchemaNoindexFail(t *testing.T) { + bad := SchemaFormal{ Keyspaces: map[string]KeyspaceFormal{ - "ks1": { - Indexes: idx2, - Tables: t1, + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stfu": { + Type: "stfu", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "notexist", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, }, }, } - _, err = BuildSchema(&badschema) - want = "index idx1 not found for table t1" + _, err := BuildSchema(&bad) + want := "vindex notexist not found for class t1" if err == nil || err.Error() != want { - t.Errorf("got %v, want %s", err, want) + t.Errorf("BuildSchema: %v, want %v", err, want) } +} + +func TestBuildSchemaNotUniqueFail(t *testing.T) { + bad := SchemaFormal{ + Keyspaces: map[string]KeyspaceFormal{ + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stln": { + Type: "stln", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "stln", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, + }, + }, + } + _, err := BuildSchema(&bad) + want := "primary index stln is not Unique for class t1" + if err == nil || err.Error() != want { + t.Errorf("BuildSchema: %v, want %v", err, want) + } +} + +func TestBuildSchemaPrimaryNonFunctionalFail(t *testing.T) { + bad := SchemaFormal{ + Keyspaces: map[string]KeyspaceFormal{ + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stlu": { + Type: "stlu", + Owner: "t1", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "stlu", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, + }, + }, + } + _, err := BuildSchema(&bad) + want := "primary owned index stlu is not Functional for class t1" + if err == nil || err.Error() != want { + t.Errorf("BuildSchema: %v, want %v", err, want) + } +} - badschema = SchemaFormal{ +func TestBuildSchemaNonPrimaryLookupFail(t *testing.T) { + bad := SchemaFormal{ Keyspaces: map[string]KeyspaceFormal{ - "ks1": { - Indexes: idx2, - Tables: t1idx2, + "sharded": { + Sharded: true, + Vindexes: map[string]VindexFormal{ + "stlu": { + Type: "stlu", + }, + "stfu": { + Type: "stfu", + Owner: "t1", + }, + }, + Classes: map[string]ClassFormal{ + "t1": { + ColVindexes: []ColVindexFormal{ + { + Col: "c1", + Name: "stlu", + }, { + Col: "c2", + Name: "stfu", + }, + }, + }, + }, + Tables: map[string]string{ + "t1": "t1", + }, }, }, } - _, err = BuildSchema(&badschema) - want = "first index is not ShardKey for table t1" + _, err := BuildSchema(&bad) + want := "non-primary owned index stfu is not Lookup for class t1" if err == nil || err.Error() != want { - t.Errorf("got %v, want %s", err, want) + t.Errorf("BuildSchema: %v, want %v", err, want) } } diff --git a/go/vt/vtgate/planbuilder/select.go b/go/vt/vtgate/planbuilder/select.go index 2f6786fc7b5..9284cebd2af 100644 --- a/go/vt/vtgate/planbuilder/select.go +++ b/go/vt/vtgate/planbuilder/select.go @@ -7,30 +7,27 @@ package planbuilder import "github.com/youtube/vitess/go/vt/sqlparser" func buildSelectPlan(sel *sqlparser.Select, schema *Schema) *Plan { - plan := &Plan{ - ID: NoPlan, - Rewritten: generateQuery(sel), - } + plan := &Plan{ID: NoPlan} tablename, _ := analyzeFrom(sel.From) - plan.Table, plan.Reason = schema.LookupTable(tablename) + plan.Table, plan.Reason = schema.FindTable(tablename) if plan.Reason != "" { return plan } - if plan.Table.Keyspace.ShardingScheme == Unsharded { + if !plan.Table.Keyspace.Sharded { plan.ID = SelectUnsharded return plan } - getWhereRouting(sel.Where, plan) - if plan.ID.IsMulti() { - if hasAggregates(sel.SelectExprs) || sel.Distinct != "" || sel.GroupBy != nil || sel.Having != nil || sel.OrderBy != nil || sel.Limit != nil { + getWhereRouting(sel.Where, plan, false) + if plan.IsMulti() { + if hasPostProcessing(sel) { plan.ID = NoPlan - plan.Reason = "too complex" + plan.Reason = "multi-shard query has post-processing constructs" return plan } - // The where clause changes if it's Multi. - plan.Rewritten = generateQuery(sel) } + // The where clause might have changed. + plan.Rewritten = generateQuery(sel) return plan } @@ -113,3 +110,7 @@ func exprHasAggregates(node sqlparser.Expr) bool { panic("unexpected") } } + +func hasPostProcessing(sel *sqlparser.Select) bool { + return hasAggregates(sel.SelectExprs) || sel.Distinct != "" || sel.GroupBy != nil || sel.Having != nil || sel.OrderBy != nil || sel.Limit != nil +} diff --git a/go/vt/vtgate/planbuilder/where.go b/go/vt/vtgate/planbuilder/where.go index df9c41407ae..857a229f5fe 100644 --- a/go/vt/vtgate/planbuilder/where.go +++ b/go/vt/vtgate/planbuilder/where.go @@ -4,15 +4,25 @@ package planbuilder -import "github.com/youtube/vitess/go/vt/sqlparser" +import ( + "fmt" + "strconv" + + "github.com/youtube/vitess/go/vt/sqlparser" +) + +// ListVarName is the bind var name used for plans +// that require VTGate to compute custom list values, +// like for IN clauses. +const ListVarName = "_vals" // getWhereRouting fills the plan fields for the where clause of a SELECT // statement. It gets reused for DML planning also, where the select plan is // replaced with the appropriate DML plan after the fact. -func getWhereRouting(where *sqlparser.Where, plan *Plan) { +// onlyUnique matches only Unique indexes. +func getWhereRouting(where *sqlparser.Where, plan *Plan, onlyUnique bool) { if where == nil { plan.ID = SelectScatter - plan.Reason = "no where clause" return } if hasSubquery(where.Expr) { @@ -20,16 +30,29 @@ func getWhereRouting(where *sqlparser.Where, plan *Plan) { plan.Reason = "has subquery" return } - for _, index := range plan.Table.Indexes { - if planID, values := getMatch(where.Expr, index); planID != SelectScatter { + values, err := getKeyrangeMatch(where) + if err != nil { + plan.ID = NoPlan + plan.Reason = err.Error() + return + } + if values != nil { + plan.ID = SelectKeyrange + plan.Values = values + return + } + for _, index := range plan.Table.Ordered { + if onlyUnique && !IsUnique(index.Vindex) { + continue + } + if planID, values := getMatch(where.Expr, index.Col); planID != SelectScatter { plan.ID = planID - plan.Index = index + plan.ColVindex = index plan.Values = values return } } plan.ID = SelectScatter - plan.Reason = "no index match" } func hasSubquery(node sqlparser.Expr) bool { @@ -52,7 +75,7 @@ func hasSubquery(node sqlparser.Expr) bool { return true case sqlparser.StrVal, sqlparser.NumVal, sqlparser.ValArg, *sqlparser.NullVal, *sqlparser.ColName, sqlparser.ValTuple, - sqlparser.ListArg: + sqlparser.ListArg, *sqlparser.KeyrangeExpr: return false case *sqlparser.Subquery: return true @@ -87,54 +110,102 @@ func hasSubquery(node sqlparser.Expr) bool { } } -func getMatch(node sqlparser.BoolExpr, index *Index) (planID PlanID, values interface{}) { +func getKeyrangeMatch(where *sqlparser.Where) (values interface{}, err error) { + where.Expr, values, err = getKeyrangeFromBool(where.Expr) + return values, err +} + +func getKeyrangeFromBool(node sqlparser.BoolExpr) (newnode sqlparser.BoolExpr, values interface{}, err error) { + switch node := node.(type) { + case *sqlparser.AndExpr: + node.Left, values, err = getKeyrangeFromBool(node.Left) + if err != nil { + return node, nil, err + } + // Left node was a keyrange expr. + // Eliminate Left node by returning the Right node. + if node.Left == nil { + return node.Right, values, nil + } + // A child of Left node was a keyrange expr. + // So, we root node. + if values != nil { + return node, values, nil + } + node.Right, values, err = getKeyrangeFromBool(node.Right) + if err != nil { + return node, nil, err + } + // Right node was a keyrange expr. + // Eliminate Right node by returning the Left node. + if node.Right == nil { + return node.Left, values, nil + } + return node, values, nil + case *sqlparser.ParenBoolExpr: + node.Expr, values, err = getKeyrangeFromBool(node.Expr) + if err != nil { + return node, nil, err + } + // Eliminate root parenthesis if sub-expr was a keyrange expr. + // This goes recursively up. + if node.Expr == nil { + return nil, values, nil + } + return node, values, nil + case *sqlparser.KeyrangeExpr: + vals := make([]interface{}, 2) + vals[0], err = asInterface(node.Start) + if err != nil { + return node, nil, fmt.Errorf("invalid keyrange: %v", err) + } + vals[1], err = asInterface(node.End) + if err != nil { + return node, nil, fmt.Errorf("invalid keyrange: %v", err) + } + return nil, vals, nil + } + return node, nil, nil +} + +func getMatch(node sqlparser.BoolExpr, col string) (planID PlanID, values interface{}) { switch node := node.(type) { case *sqlparser.AndExpr: - if planID, values = getMatch(node.Left, index); planID != SelectScatter { + if planID, values = getMatch(node.Left, col); planID != SelectScatter { return planID, values } - if planID, values = getMatch(node.Right, index); planID != SelectScatter { + if planID, values = getMatch(node.Right, col); planID != SelectScatter { return planID, values } case *sqlparser.ParenBoolExpr: - return getMatch(node.Expr, index) + return getMatch(node.Expr, col) case *sqlparser.ComparisonExpr: switch node.Operator { case "=": - if !nameMatch(node.Left, index.Column) { + if !nameMatch(node.Left, col) { return SelectScatter, nil } if !sqlparser.IsValue(node.Right) { return SelectScatter, nil } - val, err := sqlparser.AsInterface(node.Right) + val, err := asInterface(node.Right) if err != nil { return SelectScatter, nil } - if index.Type == ShardKey { - planID = SelectSingleShardKey - } else { - planID = SelectSingleLookup - } - return planID, val + return SelectEqual, val case "in": - if !nameMatch(node.Left, index.Column) { + if !nameMatch(node.Left, col) { return SelectScatter, nil } if !sqlparser.IsSimpleTuple(node.Right) { return SelectScatter, nil } - val, err := sqlparser.AsInterface(node.Right) + val, err := asInterface(node.Right) if err != nil { return SelectScatter, nil } - node.Right = sqlparser.ListArg("::_vals") - if index.Type == ShardKey { - planID = SelectMultiShardKey - } else { - planID = SelectMultiLookup - } - return planID, val + node.Right = sqlparser.ListArg("::" + ListVarName) + return SelectIN, val } } return SelectScatter, nil @@ -150,3 +221,40 @@ func nameMatch(node sqlparser.ValExpr, col string) bool { } return true } + +// asInterface is similar to sqlparser.AsInterface, but it converts +// numeric and string types to native go types. +func asInterface(node sqlparser.ValExpr) (interface{}, error) { + switch node := node.(type) { + case sqlparser.ValTuple: + vals := make([]interface{}, 0, len(node)) + for _, val := range node { + v, err := asInterface(val) + if err != nil { + return nil, err + } + vals = append(vals, v) + } + return vals, nil + case sqlparser.ValArg: + return string(node), nil + case sqlparser.ListArg: + return string(node), nil + case sqlparser.StrVal: + return []byte(node), nil + case sqlparser.NumVal: + val := string(node) + signed, err := strconv.ParseInt(val, 0, 64) + if err == nil { + return signed, nil + } + unsigned, err := strconv.ParseUint(val, 0, 64) + if err == nil { + return unsigned, nil + } + return nil, err + case *sqlparser.NullVal: + return nil, nil + } + return nil, fmt.Errorf("%v is not a value", sqlparser.String(node)) +} diff --git a/go/vt/vtgate/proto/vtgate_proto.go b/go/vt/vtgate/proto/vtgate_proto.go index 4cc769d7ff2..72109e90b15 100644 --- a/go/vt/vtgate/proto/vtgate_proto.go +++ b/go/vt/vtgate/proto/vtgate_proto.go @@ -21,6 +21,8 @@ type Session struct { ShardSessions []*ShardSession } +//go:generate bsongen -file $GOFILE -type Session -o session_bson.go + func (session *Session) String() string { return fmt.Sprintf("InTransaction: %v, ShardSession: %+v", session.InTransaction, session.ShardSessions) } @@ -33,6 +35,8 @@ type ShardSession struct { TransactionId int64 } +//go:generate bsongen -file $GOFILE -type ShardSession -o shard_session_bson.go + func (shardSession *ShardSession) String() string { return fmt.Sprintf("Keyspace: %v, Shard: %v, TabletType: %v, TransactionId: %v", shardSession.Keyspace, shardSession.Shard, shardSession.TabletType, shardSession.TransactionId) } @@ -45,6 +49,8 @@ type Query struct { Session *Session } +//go:generate bsongen -file $GOFILE -type Query -o query_bson.go + // QueryShard represents a query request for the // specified list of shards. type QueryShard struct { @@ -56,6 +62,8 @@ type QueryShard struct { Session *Session } +//go:generate bsongen -file $GOFILE -type QueryShard -o query_shard_bson.go + // KeyspaceIdQuery represents a query request for the // specified list of keyspace IDs. type KeyspaceIdQuery struct { @@ -67,6 +75,8 @@ type KeyspaceIdQuery struct { Session *Session } +//go:generate bsongen -file $GOFILE -type KeyspaceIdQuery -o keyspace_id_query_bson.go + // KeyRangeQuery represents a query request for the // specified list of keyranges. type KeyRangeQuery struct { @@ -78,12 +88,16 @@ type KeyRangeQuery struct { Session *Session } +//go:generate bsongen -file $GOFILE -type KeyRangeQuery -o key_range_query_bson.go + // EntityId represents a tuple of external_id and keyspace_id type EntityId struct { ExternalID interface{} KeyspaceID kproto.KeyspaceId } +//go:generate bsongen -file $GOFILE -type EntityId -o entity_id_bson.go + // EntityIdsQuery represents a query request for the specified KeyspaceId map. type EntityIdsQuery struct { Sql string @@ -95,6 +109,8 @@ type EntityIdsQuery struct { Session *Session } +//go:generate bsongen -file $GOFILE -type EntityIdsQuery -o entity_ids_query_bson.go + // QueryResult is mproto.QueryResult+Session (for now). type QueryResult struct { Result *mproto.QueryResult @@ -102,6 +118,8 @@ type QueryResult struct { Error string } +//go:generate bsongen -file $GOFILE -type QueryResult -o query_result_bson.go + // BatchQueryShard represents a batch query request // for the specified shards. type BatchQueryShard struct { @@ -112,6 +130,8 @@ type BatchQueryShard struct { Session *Session } +//go:generate bsongen -file $GOFILE -type BatchQueryShard -o batch_query_shard_bson.go + // KeyspaceIdBatchQuery represents a batch query request // for the specified keyspace IDs. type KeyspaceIdBatchQuery struct { @@ -122,6 +142,8 @@ type KeyspaceIdBatchQuery struct { Session *Session } +//go:generate bsongen -file $GOFILE -type KeyspaceIdBatchQuery -o keyspace_id_batch_query_bson.go + // QueryResultList is mproto.QueryResultList+Session type QueryResultList struct { List []mproto.QueryResult diff --git a/go/vt/vtgate/request_context.go b/go/vt/vtgate/request_context.go new file mode 100644 index 00000000000..e3f41ab42e3 --- /dev/null +++ b/go/vt/vtgate/request_context.go @@ -0,0 +1,36 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vtgate + +import ( + mproto "github.com/youtube/vitess/go/mysql/proto" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" +) + +type requestContext struct { + ctx context.Context + query *proto.Query + router *Router +} + +func newRequestContext(ctx context.Context, query *proto.Query, router *Router) *requestContext { + return &requestContext{ + ctx: ctx, + query: query, + router: router, + } +} + +func (vc *requestContext) Execute(boundQuery *tproto.BoundQuery) (*mproto.QueryResult, error) { + q := &proto.Query{ + Sql: boundQuery.Sql, + BindVariables: boundQuery.BindVariables, + TabletType: vc.query.TabletType, + Session: vc.query.Session, + } + return vc.router.Execute(vc.ctx, q) +} diff --git a/go/vt/vtgate/resolver.go b/go/vt/vtgate/resolver.go index d61780ff476..2df27405e6a 100644 --- a/go/vt/vtgate/resolver.go +++ b/go/vt/vtgate/resolver.go @@ -13,12 +13,12 @@ import ( "strings" "time" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" "github.com/youtube/vitess/go/vt/tabletserver/tabletconn" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" ) var ( @@ -28,6 +28,9 @@ var ( closeBracket = []byte(")") kwAnd = []byte(" and ") kwWhere = []byte(" where ") + insert_dml = "insert" + update_dml = "update" + delete_dml = "delete" ) // Resolver is the layer to resolve KeyspaceIds and KeyRanges @@ -55,36 +58,43 @@ func (res *Resolver) InitializeConnections(ctx context.Context) error { // ExecuteKeyspaceIds executes a non-streaming query based on KeyspaceIds. // It retries query if new keyspace/shards are re-resolved after a retryable error. -func (res *Resolver) ExecuteKeyspaceIds(context context.Context, query *proto.KeyspaceIdQuery) (*mproto.QueryResult, error) { +// This throws an error if a dml spans multiple keyspace_ids. Resharding depends +// on being able to uniquely route a write. +func (res *Resolver) ExecuteKeyspaceIds(ctx context.Context, query *proto.KeyspaceIdQuery) (*mproto.QueryResult, error) { + if isDml(query.Sql) && len(query.KeyspaceIds) > 1 { + return nil, fmt.Errorf("DML should not span multiple keyspace_ids") + } mapToShards := func(keyspace string) (string, []string, error) { return mapKeyspaceIdsToShards( + ctx, res.scatterConn.toposerv, res.scatterConn.cell, keyspace, query.TabletType, query.KeyspaceIds) } - return res.Execute(context, query.Sql, query.BindVariables, query.Keyspace, query.TabletType, query.Session, mapToShards) + return res.Execute(ctx, query.Sql, query.BindVariables, query.Keyspace, query.TabletType, query.Session, mapToShards) } // ExecuteKeyRanges executes a non-streaming query based on KeyRanges. // It retries query if new keyspace/shards are re-resolved after a retryable error. -func (res *Resolver) ExecuteKeyRanges(context context.Context, query *proto.KeyRangeQuery) (*mproto.QueryResult, error) { +func (res *Resolver) ExecuteKeyRanges(ctx context.Context, query *proto.KeyRangeQuery) (*mproto.QueryResult, error) { mapToShards := func(keyspace string) (string, []string, error) { return mapKeyRangesToShards( + ctx, res.scatterConn.toposerv, res.scatterConn.cell, keyspace, query.TabletType, query.KeyRanges) } - return res.Execute(context, query.Sql, query.BindVariables, query.Keyspace, query.TabletType, query.Session, mapToShards) + return res.Execute(ctx, query.Sql, query.BindVariables, query.Keyspace, query.TabletType, query.Session, mapToShards) } // Execute executes a non-streaming query based on shards resolved by given func. // It retries query if new keyspace/shards are re-resolved after a retryable error. func (res *Resolver) Execute( - context context.Context, + ctx context.Context, sql string, bindVars map[string]interface{}, keyspace string, @@ -98,7 +108,7 @@ func (res *Resolver) Execute( } for { qr, err := res.scatterConn.Execute( - context, + ctx, sql, bindVars, keyspace, @@ -136,10 +146,11 @@ func (res *Resolver) Execute( // ExecuteEntityIds executes a non-streaming query based on given KeyspaceId map. // It retries query if new keyspace/shards are re-resolved after a retryable error. func (res *Resolver) ExecuteEntityIds( - context context.Context, + ctx context.Context, query *proto.EntityIdsQuery, ) (*mproto.QueryResult, error) { newKeyspace, shardIDMap, err := mapEntityIdsToShards( + ctx, res.scatterConn.toposerv, res.scatterConn.cell, query.Keyspace, @@ -152,7 +163,7 @@ func (res *Resolver) ExecuteEntityIds( shards, sqls, bindVars := buildEntityIds(shardIDMap, query.Sql, query.EntityColumnName, query.BindVariables) for { qr, err := res.scatterConn.ExecuteEntityIds( - context, + ctx, shards, sqls, bindVars, @@ -162,6 +173,7 @@ func (res *Resolver) ExecuteEntityIds( if connError, ok := err.(*ShardConnError); ok && connError.Code == tabletconn.ERR_RETRY { resharding := false newKeyspace, newShardIDMap, err := mapEntityIdsToShards( + ctx, res.scatterConn.toposerv, res.scatterConn.cell, query.Keyspace, @@ -197,22 +209,23 @@ func (res *Resolver) ExecuteEntityIds( // ExecuteBatchKeyspaceIds executes a group of queries based on KeyspaceIds. // It retries query if new keyspace/shards are re-resolved after a retryable error. -func (res *Resolver) ExecuteBatchKeyspaceIds(context context.Context, query *proto.KeyspaceIdBatchQuery) (*tproto.QueryResultList, error) { +func (res *Resolver) ExecuteBatchKeyspaceIds(ctx context.Context, query *proto.KeyspaceIdBatchQuery) (*tproto.QueryResultList, error) { mapToShards := func(keyspace string) (string, []string, error) { return mapKeyspaceIdsToShards( + ctx, res.scatterConn.toposerv, res.scatterConn.cell, keyspace, query.TabletType, query.KeyspaceIds) } - return res.ExecuteBatch(context, query.Queries, query.Keyspace, query.TabletType, query.Session, mapToShards) + return res.ExecuteBatch(ctx, query.Queries, query.Keyspace, query.TabletType, query.Session, mapToShards) } // ExecuteBatch executes a group of queries based on shards resolved by given func. // It retries query if new keyspace/shards are re-resolved after a retryable error. func (res *Resolver) ExecuteBatch( - context context.Context, + ctx context.Context, queries []tproto.BoundQuery, keyspace string, tabletType topo.TabletType, @@ -225,7 +238,7 @@ func (res *Resolver) ExecuteBatch( } for { qrs, err := res.scatterConn.ExecuteBatch( - context, + ctx, queries, keyspace, shards, @@ -265,16 +278,17 @@ func (res *Resolver) ExecuteBatch( // one shard since it cannot merge-sort the results to guarantee ordering of // response which is needed for checkpointing. // The api supports supplying multiple KeyspaceIds to make it future proof. -func (res *Resolver) StreamExecuteKeyspaceIds(context context.Context, query *proto.KeyspaceIdQuery, sendReply func(*mproto.QueryResult) error) error { +func (res *Resolver) StreamExecuteKeyspaceIds(ctx context.Context, query *proto.KeyspaceIdQuery, sendReply func(*mproto.QueryResult) error) error { mapToShards := func(keyspace string) (string, []string, error) { return mapKeyspaceIdsToShards( + ctx, res.scatterConn.toposerv, res.scatterConn.cell, query.Keyspace, query.TabletType, query.KeyspaceIds) } - return res.StreamExecute(context, query.Sql, query.BindVariables, query.Keyspace, query.TabletType, query.Session, mapToShards, sendReply) + return res.StreamExecute(ctx, query.Sql, query.BindVariables, query.Keyspace, query.TabletType, query.Session, mapToShards, sendReply) } // StreamExecuteKeyRanges executes a streaming query on the specified KeyRanges. @@ -283,16 +297,17 @@ func (res *Resolver) StreamExecuteKeyspaceIds(context context.Context, query *pr // one shard since it cannot merge-sort the results to guarantee ordering of // response which is needed for checkpointing. // The api supports supplying multiple keyranges to make it future proof. -func (res *Resolver) StreamExecuteKeyRanges(context context.Context, query *proto.KeyRangeQuery, sendReply func(*mproto.QueryResult) error) error { +func (res *Resolver) StreamExecuteKeyRanges(ctx context.Context, query *proto.KeyRangeQuery, sendReply func(*mproto.QueryResult) error) error { mapToShards := func(keyspace string) (string, []string, error) { return mapKeyRangesToShards( + ctx, res.scatterConn.toposerv, res.scatterConn.cell, query.Keyspace, query.TabletType, query.KeyRanges) } - return res.StreamExecute(context, query.Sql, query.BindVariables, query.Keyspace, query.TabletType, query.Session, mapToShards, sendReply) + return res.StreamExecute(ctx, query.Sql, query.BindVariables, query.Keyspace, query.TabletType, query.Session, mapToShards, sendReply) } // StreamExecuteShard executes a streaming query on shards resolved by given func. @@ -300,7 +315,7 @@ func (res *Resolver) StreamExecuteKeyRanges(context context.Context, query *prot // one shard since it cannot merge-sort the results to guarantee ordering of // response which is needed for checkpointing. func (res *Resolver) StreamExecute( - context context.Context, + ctx context.Context, sql string, bindVars map[string]interface{}, keyspace string, @@ -314,7 +329,7 @@ func (res *Resolver) StreamExecute( return err } err = res.scatterConn.StreamExecute( - context, + ctx, sql, bindVars, keyspace, @@ -326,13 +341,13 @@ func (res *Resolver) StreamExecute( } // Commit commits a transaction. -func (res *Resolver) Commit(context context.Context, inSession *proto.Session) error { - return res.scatterConn.Commit(context, NewSafeSession(inSession)) +func (res *Resolver) Commit(ctx context.Context, inSession *proto.Session) error { + return res.scatterConn.Commit(ctx, NewSafeSession(inSession)) } // Rollback rolls back a transaction. -func (res *Resolver) Rollback(context context.Context, inSession *proto.Session) error { - return res.scatterConn.Rollback(context, NewSafeSession(inSession)) +func (res *Resolver) Rollback(ctx context.Context, inSession *proto.Session) error { + return res.scatterConn.Rollback(ctx, NewSafeSession(inSession)) } // StrsEquals compares contents of two string slices. @@ -411,3 +426,15 @@ func insertSqlClause(querySql, clause string) string { } return b.String() } + +func isDml(querySql string) bool { + var sqlKW string + if i := strings.Index(querySql, " "); i >= 0 { + sqlKW = querySql[:i] + } + sqlKW = strings.ToLower(sqlKW) + if sqlKW == insert_dml || sqlKW == update_dml || sqlKW == delete_dml { + return true + } + return false +} diff --git a/go/vt/vtgate/resolver_test.go b/go/vt/vtgate/resolver_test.go index 1c30d34f622..47cfe08b39d 100644 --- a/go/vt/vtgate/resolver_test.go +++ b/go/vt/vtgate/resolver_test.go @@ -15,11 +15,11 @@ import ( "time" mproto "github.com/youtube/vitess/go/mysql/proto" - "github.com/youtube/vitess/go/vt/context" "github.com/youtube/vitess/go/vt/key" tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" ) // This file uses the sandbox_test framework. @@ -41,7 +41,7 @@ func TestResolverExecuteKeyspaceIds(t *testing.T) { TabletType: topo.TYPE_MASTER, } res := NewResolver(new(sandboxTopo), "", "aa", 1*time.Millisecond, 0, 1*time.Millisecond) - return res.ExecuteKeyspaceIds(&context.DummyContext{}, query) + return res.ExecuteKeyspaceIds(context.Background(), query) }) } @@ -62,7 +62,7 @@ func TestResolverExecuteKeyRanges(t *testing.T) { TabletType: topo.TYPE_MASTER, } res := NewResolver(new(sandboxTopo), "", "aa", 1*time.Millisecond, 0, 1*time.Millisecond) - return res.ExecuteKeyRanges(&context.DummyContext{}, query) + return res.ExecuteKeyRanges(context.Background(), query) }) } @@ -93,7 +93,7 @@ func TestResolverExecuteEntityIds(t *testing.T) { TabletType: topo.TYPE_MASTER, } res := NewResolver(new(sandboxTopo), "", "aa", 1*time.Millisecond, 0, 1*time.Millisecond) - return res.ExecuteEntityIds(&context.DummyContext{}, query) + return res.ExecuteEntityIds(context.Background(), query) }) } @@ -114,7 +114,7 @@ func TestResolverExecuteBatchKeyspaceIds(t *testing.T) { TabletType: topo.TYPE_MASTER, } res := NewResolver(new(sandboxTopo), "", "aa", 1*time.Millisecond, 0, 1*time.Millisecond) - qrs, err := res.ExecuteBatchKeyspaceIds(&context.DummyContext{}, query) + qrs, err := res.ExecuteBatchKeyspaceIds(context.Background(), query) if err != nil { return nil, err } @@ -145,7 +145,7 @@ func TestResolverStreamExecuteKeyspaceIds(t *testing.T) { testResolverStreamGeneric(t, "TestResolverStreamExecuteKeyspaceIds", func() (*mproto.QueryResult, error) { res := NewResolver(new(sandboxTopo), "", "aa", 1*time.Millisecond, 0, 1*time.Millisecond) qr := new(mproto.QueryResult) - err = res.StreamExecuteKeyspaceIds(&context.DummyContext{}, query, func(r *mproto.QueryResult) error { + err = res.StreamExecuteKeyspaceIds(context.Background(), query, func(r *mproto.QueryResult) error { appendResult(qr, r) return nil }) @@ -155,7 +155,7 @@ func TestResolverStreamExecuteKeyspaceIds(t *testing.T) { query.KeyspaceIds = []key.KeyspaceId{kid10, kid15, kid25} res := NewResolver(new(sandboxTopo), "", "aa", 1*time.Millisecond, 0, 1*time.Millisecond) qr := new(mproto.QueryResult) - err = res.StreamExecuteKeyspaceIds(&context.DummyContext{}, query, func(r *mproto.QueryResult) error { + err = res.StreamExecuteKeyspaceIds(context.Background(), query, func(r *mproto.QueryResult) error { appendResult(qr, r) return nil }) @@ -187,7 +187,7 @@ func TestResolverStreamExecuteKeyRanges(t *testing.T) { testResolverStreamGeneric(t, "TestResolverStreamExecuteKeyRanges", func() (*mproto.QueryResult, error) { res := NewResolver(new(sandboxTopo), "", "aa", 1*time.Millisecond, 0, 1*time.Millisecond) qr := new(mproto.QueryResult) - err = res.StreamExecuteKeyRanges(&context.DummyContext{}, query, func(r *mproto.QueryResult) error { + err = res.StreamExecuteKeyRanges(context.Background(), query, func(r *mproto.QueryResult) error { appendResult(qr, r) return nil }) @@ -198,7 +198,7 @@ func TestResolverStreamExecuteKeyRanges(t *testing.T) { query.KeyRanges = []key.KeyRange{key.KeyRange{Start: kid10, End: kid25}} res := NewResolver(new(sandboxTopo), "", "aa", 1*time.Millisecond, 0, 1*time.Millisecond) qr := new(mproto.QueryResult) - err = res.StreamExecuteKeyRanges(&context.DummyContext{}, query, func(r *mproto.QueryResult) error { + err = res.StreamExecuteKeyRanges(context.Background(), query, func(r *mproto.QueryResult) error { appendResult(qr, r) return nil }) @@ -477,3 +477,33 @@ func TestResolverBuildEntityIds(t *testing.T) { t.Errorf("want %+v, got %+v", wantBindVars, bindVars) } } + +func TestResolverDmlOnMultipleKeyspaceIds(t *testing.T) { + kid10, err := key.HexKeyspaceId("10").Unhex() + if err != nil { + t.Errorf("Error encoding keyspace id") + } + kid25, err := key.HexKeyspaceId("25").Unhex() + if err != nil { + t.Errorf("Error encoding keyspace id") + } + query := &proto.KeyspaceIdQuery{ + Sql: "update table set a = b", + Keyspace: "TestResolverExecuteKeyspaceIds", + KeyspaceIds: []key.KeyspaceId{kid10, kid25}, + TabletType: topo.TYPE_MASTER, + } + res := NewResolver(new(sandboxTopo), "", "aa", 1*time.Millisecond, 0, 1*time.Millisecond) + + s := createSandbox("TestResolverDmlOnMultipleKeyspaceIds") + sbc0 := &sandboxConn{} + s.MapTestConn("-20", sbc0) + sbc1 := &sandboxConn{} + s.MapTestConn("20-40", sbc1) + + errStr := "DML should not span multiple keyspace_ids" + _, err = res.ExecuteKeyspaceIds(context.Background(), query) + if err == nil { + t.Errorf("want %v, got nil", errStr) + } +} diff --git a/go/vt/vtgate/router.go b/go/vt/vtgate/router.go index eb830964843..3970ee6d770 100644 --- a/go/vt/vtgate/router.go +++ b/go/vt/vtgate/router.go @@ -9,11 +9,17 @@ package vtgate import ( "fmt" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" - "github.com/youtube/vitess/go/sqltypes" + "github.com/youtube/vitess/go/vt/key" + "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtgate/planbuilder" "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" +) + +const ( + ksidName = "keyspace_id" + dmlPostfix = " /* _routing keyspace_id:%v */" ) // Router is the layer to route queries to the correct shards @@ -25,6 +31,23 @@ type Router struct { scatterConn *ScatterConn } +type scatterParams struct { + query, ks string + shardVars map[string]map[string]interface{} +} + +func newScatterParams(query, ks string, bv map[string]interface{}, shards []string) *scatterParams { + shardVars := make(map[string]map[string]interface{}, len(shards)) + for _, shard := range shards { + shardVars[shard] = bv + } + return &scatterParams{ + query: query, + ks: ks, + shardVars: shardVars, + } +} + // NewRouter creates a new Router. func NewRouter(serv SrvTopoServer, cell string, schema *planbuilder.Schema, statsName string, scatterConn *ScatterConn) *Router { return &Router{ @@ -36,37 +59,279 @@ func NewRouter(serv SrvTopoServer, cell string, schema *planbuilder.Schema, stat } // Execute routes a non-streaming query. -func (rtr *Router) Execute(context context.Context, query *proto.Query) (*mproto.QueryResult, error) { +func (rtr *Router) Execute(ctx context.Context, query *proto.Query) (*mproto.QueryResult, error) { + if query.BindVariables == nil { + query.BindVariables = make(map[string]interface{}) + } + vcursor := newRequestContext(ctx, query, rtr) plan := rtr.planner.GetPlan(string(query.Sql)) + + switch plan.ID { + case planbuilder.UpdateEqual: + return rtr.execUpdateEqual(vcursor, plan) + case planbuilder.DeleteEqual: + return rtr.execDeleteEqual(vcursor, plan) + case planbuilder.InsertSharded: + return rtr.execInsertSharded(vcursor, plan) + } + + var err error + var params *scatterParams switch plan.ID { - case planbuilder.SelectSingleShardKey: - return rtr.execSelectSingleShardKey(context, query, plan) + case planbuilder.SelectUnsharded, planbuilder.UpdateUnsharded, + planbuilder.DeleteUnsharded, planbuilder.InsertUnsharded: + params, err = rtr.paramsUnsharded(vcursor, plan) + case planbuilder.SelectEqual: + params, err = rtr.paramsSelectEqual(vcursor, plan) + case planbuilder.SelectIN: + params, err = rtr.paramsSelectIN(vcursor, plan) + case planbuilder.SelectKeyrange: + params, err = rtr.paramsSelectKeyrange(vcursor, plan) + case planbuilder.SelectScatter: + params, err = rtr.paramsSelectScatter(vcursor, plan) default: - return nil, fmt.Errorf("plan unimplemented") + return nil, fmt.Errorf("cannot route query: %s: %s", query.Sql, plan.Reason) + } + if err != nil { + return nil, err } + return rtr.scatterConn.ExecuteMulti( + ctx, + params.query, + params.ks, + params.shardVars, + query.TabletType, + NewSafeSession(vcursor.query.Session), + ) } -func (rtr *Router) execSelectSingleShardKey(context context.Context, query *proto.Query, plan *planbuilder.Plan) (*mproto.QueryResult, error) { - hind := NewHashIndex(plan.Table.Keyspace.Name, rtr.serv, rtr.cell) - keys, err := resolveKeys([]interface{}{plan.Values}, query.BindVariables) +// StreamExecute executes a streaming query. +func (rtr *Router) StreamExecute(ctx context.Context, query *proto.Query, sendReply func(*mproto.QueryResult) error) error { + if query.BindVariables == nil { + query.BindVariables = make(map[string]interface{}) + } + vcursor := newRequestContext(ctx, query, rtr) + plan := rtr.planner.GetPlan(string(query.Sql)) + + var err error + var params *scatterParams + switch plan.ID { + case planbuilder.SelectUnsharded: + params, err = rtr.paramsUnsharded(vcursor, plan) + case planbuilder.SelectEqual: + params, err = rtr.paramsSelectEqual(vcursor, plan) + case planbuilder.SelectIN: + params, err = rtr.paramsSelectIN(vcursor, plan) + case planbuilder.SelectKeyrange: + params, err = rtr.paramsSelectKeyrange(vcursor, plan) + case planbuilder.SelectScatter: + params, err = rtr.paramsSelectScatter(vcursor, plan) + default: + return fmt.Errorf("query %q cannot be used for streaming", query.Sql) + } if err != nil { - return nil, err + return err + } + return rtr.scatterConn.StreamExecuteMulti( + ctx, + params.query, + params.ks, + params.shardVars, + query.TabletType, + NewSafeSession(vcursor.query.Session), + sendReply, + ) +} + +func (rtr *Router) paramsUnsharded(vcursor *requestContext, plan *planbuilder.Plan) (*scatterParams, error) { + ks, allShards, err := getKeyspaceShards(vcursor.ctx, rtr.serv, rtr.cell, plan.Table.Keyspace.Name, vcursor.query.TabletType) + if err != nil { + return nil, fmt.Errorf("paramsUnsharded: %v", err) + } + if len(allShards) != 1 { + return nil, fmt.Errorf("unsharded keyspace %s has multiple shards", ks) + } + return newScatterParams(vcursor.query.Sql, ks, vcursor.query.BindVariables, []string{allShards[0].ShardName()}), nil +} + +func (rtr *Router) paramsSelectEqual(vcursor *requestContext, plan *planbuilder.Plan) (*scatterParams, error) { + keys, err := rtr.resolveKeys([]interface{}{plan.Values}, vcursor.query.BindVariables) + if err != nil { + return nil, fmt.Errorf("paramsSelectEqual: %v", err) + } + ks, routing, err := rtr.resolveShards(vcursor, keys, plan) + if err != nil { + return nil, fmt.Errorf("paramsSelectEqual: %v", err) + } + return newScatterParams(plan.Rewritten, ks, vcursor.query.BindVariables, routing.Shards()), nil +} + +func (rtr *Router) paramsSelectIN(vcursor *requestContext, plan *planbuilder.Plan) (*scatterParams, error) { + keys, err := rtr.resolveKeys(plan.Values.([]interface{}), vcursor.query.BindVariables) + if err != nil { + return nil, fmt.Errorf("paramsSelectIN: %v", err) + } + ks, routing, err := rtr.resolveShards(vcursor, keys, plan) + if err != nil { + return nil, fmt.Errorf("paramsSelectEqual: %v", err) + } + return &scatterParams{ + query: plan.Rewritten, + ks: ks, + shardVars: routing.ShardVars(vcursor.query.BindVariables), + }, nil +} + +func (rtr *Router) paramsSelectKeyrange(vcursor *requestContext, plan *planbuilder.Plan) (*scatterParams, error) { + keys, err := rtr.resolveKeys(plan.Values.([]interface{}), vcursor.query.BindVariables) + if err != nil { + return nil, fmt.Errorf("paramsSelectKeyrange: %v", err) + } + kr, err := getKeyRange(keys) + if err != nil { + return nil, fmt.Errorf("paramsSelectKeyrange: %v", err) + } + ks, shards, err := mapExactShards(vcursor.ctx, rtr.serv, rtr.cell, plan.Table.Keyspace.Name, vcursor.query.TabletType, kr) + if err != nil { + return nil, fmt.Errorf("paramsSelectKeyrange: %v", err) } - ks, shards, err := hind.Resolve(query.TabletType, keys) if len(shards) != 1 { - panic("unexpected") + return nil, fmt.Errorf("keyrange must match exactly one shard: %+v", keys) + } + return newScatterParams(plan.Rewritten, ks, vcursor.query.BindVariables, shards), nil +} + +func getKeyRange(keys []interface{}) (key.KeyRange, error) { + var ksids []key.KeyspaceId + for _, k := range keys { + switch k := k.(type) { + case string: + ksids = append(ksids, key.KeyspaceId(k)) + default: + return key.KeyRange{}, fmt.Errorf("expecting strings for keyrange: %+v", keys) + } } + return key.KeyRange{ + Start: ksids[0], + End: ksids[1], + }, nil +} + +func (rtr *Router) paramsSelectScatter(vcursor *requestContext, plan *planbuilder.Plan) (*scatterParams, error) { + ks, allShards, err := getKeyspaceShards(vcursor.ctx, rtr.serv, rtr.cell, plan.Table.Keyspace.Name, vcursor.query.TabletType) + if err != nil { + return nil, fmt.Errorf("paramsSelectScatter: %v", err) + } + var shards []string + for _, shard := range allShards { + shards = append(shards, shard.ShardName()) + } + return newScatterParams(plan.Rewritten, ks, vcursor.query.BindVariables, shards), nil +} + +func (rtr *Router) execUpdateEqual(vcursor *requestContext, plan *planbuilder.Plan) (*mproto.QueryResult, error) { + keys, err := rtr.resolveKeys([]interface{}{plan.Values}, vcursor.query.BindVariables) + if err != nil { + return nil, fmt.Errorf("execUpdateEqual: %v", err) + } + ks, shard, ksid, err := rtr.resolveSingleShard(vcursor, keys[0], plan) + if err != nil { + return nil, fmt.Errorf("execUpdateEqual: %v", err) + } + if ksid == key.MinKey { + return &mproto.QueryResult{}, nil + } + vcursor.query.BindVariables[ksidName] = string(ksid) + rewritten := plan.Rewritten + fmt.Sprintf(dmlPostfix, ksid) return rtr.scatterConn.Execute( - context, - query.Sql, - query.BindVariables, + vcursor.ctx, + rewritten, + vcursor.query.BindVariables, ks, - shards, - query.TabletType, - NewSafeSession(query.Session)) + []string{shard}, + vcursor.query.TabletType, + NewSafeSession(vcursor.query.Session)) +} + +func (rtr *Router) execDeleteEqual(vcursor *requestContext, plan *planbuilder.Plan) (*mproto.QueryResult, error) { + keys, err := rtr.resolveKeys([]interface{}{plan.Values}, vcursor.query.BindVariables) + if err != nil { + return nil, fmt.Errorf("execDeleteEqual: %v", err) + } + ks, shard, ksid, err := rtr.resolveSingleShard(vcursor, keys[0], plan) + if err != nil { + return nil, fmt.Errorf("execDeleteEqual: %v", err) + } + if ksid == key.MinKey { + return &mproto.QueryResult{}, nil + } + if plan.Subquery != "" { + err = rtr.deleteVindexEntries(vcursor, plan, ks, shard, ksid) + if err != nil { + return nil, fmt.Errorf("execDeleteEqual: %v", err) + } + } + vcursor.query.BindVariables[ksidName] = string(ksid) + rewritten := plan.Rewritten + fmt.Sprintf(dmlPostfix, ksid) + return rtr.scatterConn.Execute( + vcursor.ctx, + rewritten, + vcursor.query.BindVariables, + ks, + []string{shard}, + vcursor.query.TabletType, + NewSafeSession(vcursor.query.Session)) +} + +func (rtr *Router) execInsertSharded(vcursor *requestContext, plan *planbuilder.Plan) (*mproto.QueryResult, error) { + input := plan.Values.([]interface{}) + keys, err := rtr.resolveKeys(input, vcursor.query.BindVariables) + if err != nil { + return nil, fmt.Errorf("execInsertSharded: %v", err) + } + ksid, generated, err := rtr.handlePrimary(vcursor, keys[0], plan.Table.ColVindexes[0], vcursor.query.BindVariables) + if err != nil { + return nil, fmt.Errorf("execInsertSharded: %v", err) + } + ks, shard, err := rtr.getRouting(vcursor.ctx, plan.Table.Keyspace.Name, vcursor.query.TabletType, ksid) + if err != nil { + return nil, fmt.Errorf("execInsertSharded: %v", err) + } + for i := 1; i < len(keys); i++ { + newgen, err := rtr.handleNonPrimary(vcursor, keys[i], plan.Table.ColVindexes[i], vcursor.query.BindVariables, ksid) + if err != nil { + return nil, err + } + if newgen != 0 { + if generated != 0 { + return nil, fmt.Errorf("insert generated more than one value") + } + generated = newgen + } + } + vcursor.query.BindVariables[ksidName] = string(ksid) + rewritten := plan.Rewritten + fmt.Sprintf(dmlPostfix, ksid) + result, err := rtr.scatterConn.Execute( + vcursor.ctx, + rewritten, + vcursor.query.BindVariables, + ks, + []string{shard}, + vcursor.query.TabletType, + NewSafeSession(vcursor.query.Session)) + if err != nil { + return nil, fmt.Errorf("execInsertSharded: %v", err) + } + if generated != 0 { + if result.InsertId != 0 { + return nil, fmt.Errorf("vindex and db generated a value each for insert") + } + result.InsertId = uint64(generated) + } + return result, nil } -func resolveKeys(vals []interface{}, bindVars map[string]interface{}) (keys []interface{}, err error) { +func (rtr *Router) resolveKeys(vals []interface{}, bindVars map[string]interface{}) (keys []interface{}, err error) { keys = make([]interface{}, 0, len(vals)) for _, val := range vals { switch val := val.(type) { @@ -76,11 +341,215 @@ func resolveKeys(vals []interface{}, bindVars map[string]interface{}) (keys []in return nil, fmt.Errorf("could not find bind var %s", val) } keys = append(keys, v) - case sqltypes.Value: - keys = append(keys, val) + case []byte: + keys = append(keys, string(val)) default: - panic("unexpected") + keys = append(keys, val) } } return keys, nil } + +func (rtr *Router) resolveShards(vcursor *requestContext, vindexKeys []interface{}, plan *planbuilder.Plan) (newKeyspace string, routing routingMap, err error) { + newKeyspace, allShards, err := getKeyspaceShards(vcursor.ctx, rtr.serv, rtr.cell, plan.Table.Keyspace.Name, vcursor.query.TabletType) + if err != nil { + return "", nil, err + } + routing = make(routingMap) + switch mapper := plan.ColVindex.Vindex.(type) { + case planbuilder.Unique: + ksids, err := mapper.Map(vcursor, vindexKeys) + if err != nil { + return "", nil, err + } + for i, ksid := range ksids { + if ksid == key.MinKey { + continue + } + shard, err := getShardForKeyspaceId(allShards, ksid) + if err != nil { + return "", nil, err + } + routing.Add(shard, vindexKeys[i]) + } + case planbuilder.NonUnique: + ksidss, err := mapper.Map(vcursor, vindexKeys) + if err != nil { + return "", nil, err + } + for i, ksids := range ksidss { + for _, ksid := range ksids { + shard, err := getShardForKeyspaceId(allShards, ksid) + if err != nil { + return "", nil, err + } + routing.Add(shard, vindexKeys[i]) + } + } + default: + panic("unexpected") + } + return newKeyspace, routing, nil +} + +func (rtr *Router) resolveSingleShard(vcursor *requestContext, vindexKey interface{}, plan *planbuilder.Plan) (newKeyspace, shard string, ksid key.KeyspaceId, err error) { + newKeyspace, allShards, err := getKeyspaceShards(vcursor.ctx, rtr.serv, rtr.cell, plan.Table.Keyspace.Name, vcursor.query.TabletType) + if err != nil { + return "", "", "", err + } + mapper := plan.ColVindex.Vindex.(planbuilder.Unique) + ksids, err := mapper.Map(vcursor, []interface{}{vindexKey}) + if err != nil { + return "", "", "", err + } + ksid = ksids[0] + if ksid == key.MinKey { + return "", "", ksid, nil + } + shard, err = getShardForKeyspaceId(allShards, ksid) + if err != nil { + return "", "", "", err + } + return newKeyspace, shard, ksid, nil +} + +func (rtr *Router) deleteVindexEntries(vcursor *requestContext, plan *planbuilder.Plan, ks, shard string, ksid key.KeyspaceId) error { + result, err := rtr.scatterConn.Execute( + vcursor.ctx, + plan.Subquery, + vcursor.query.BindVariables, + ks, + []string{shard}, + vcursor.query.TabletType, + NewSafeSession(vcursor.query.Session)) + if err != nil { + return err + } + if len(result.Rows) == 0 { + return nil + } + for i, colVindex := range plan.Table.Owned { + keys := make(map[interface{}]bool) + for _, row := range result.Rows { + k, err := mproto.Convert(result.Fields[i].Type, row[i]) + if err != nil { + return err + } + switch k := k.(type) { + case []byte: + keys[string(k)] = true + default: + keys[k] = true + } + } + var ids []interface{} + for k := range keys { + ids = append(ids, k) + } + switch vindex := colVindex.Vindex.(type) { + case planbuilder.Functional: + if err = vindex.Delete(vcursor, ids, ksid); err != nil { + return err + } + case planbuilder.Lookup: + if err = vindex.Delete(vcursor, ids, ksid); err != nil { + return err + } + default: + panic("unexpceted") + } + } + return nil +} + +func (rtr *Router) handlePrimary(vcursor *requestContext, vindexKey interface{}, colVindex *planbuilder.ColVindex, bv map[string]interface{}) (ksid key.KeyspaceId, generated int64, err error) { + if colVindex.Owned { + if vindexKey == nil { + generator, ok := colVindex.Vindex.(planbuilder.FunctionalGenerator) + if !ok { + return "", 0, fmt.Errorf("value must be supplied for column %s", colVindex.Col) + } + generated, err = generator.Generate(vcursor) + vindexKey = generated + if err != nil { + return "", 0, err + } + } else { + err = colVindex.Vindex.(planbuilder.Functional).Create(vcursor, vindexKey) + if err != nil { + return "", 0, err + } + } + } + if vindexKey == nil { + return "", 0, fmt.Errorf("value must be supplied for column %s", colVindex.Col) + } + mapper := colVindex.Vindex.(planbuilder.Unique) + ksids, err := mapper.Map(vcursor, []interface{}{vindexKey}) + if err != nil { + return "", 0, err + } + ksid = ksids[0] + if ksid == key.MinKey { + return "", 0, fmt.Errorf("could not map %v to a keyspace id", vindexKey) + } + bv["_"+colVindex.Col] = vindexKey + return ksid, generated, nil +} + +func (rtr *Router) handleNonPrimary(vcursor *requestContext, vindexKey interface{}, colVindex *planbuilder.ColVindex, bv map[string]interface{}, ksid key.KeyspaceId) (generated int64, err error) { + if colVindex.Owned { + if vindexKey == nil { + generator, ok := colVindex.Vindex.(planbuilder.LookupGenerator) + if !ok { + return 0, fmt.Errorf("value must be supplied for column %s", colVindex.Col) + } + generated, err = generator.Generate(vcursor, ksid) + vindexKey = generated + if err != nil { + return 0, err + } + } else { + err = colVindex.Vindex.(planbuilder.Lookup).Create(vcursor, vindexKey, ksid) + if err != nil { + return 0, err + } + } + } else { + if vindexKey == nil { + reversible, ok := colVindex.Vindex.(planbuilder.Reversible) + if !ok { + return 0, fmt.Errorf("value must be supplied for column %s", colVindex.Col) + } + vindexKey, err = reversible.ReverseMap(vcursor, ksid) + if err != nil { + return 0, err + } + if vindexKey == nil { + return 0, fmt.Errorf("could not compute value for column %v", colVindex.Col) + } + } else { + ok, err := colVindex.Vindex.Verify(vcursor, vindexKey, ksid) + if err != nil { + return 0, err + } + if !ok { + return 0, fmt.Errorf("value %v for column %s does not map to keyspace id %v", vindexKey, colVindex.Col, ksid) + } + } + } + bv["_"+colVindex.Col] = vindexKey + return generated, nil +} + +func (rtr *Router) getRouting(ctx context.Context, keyspace string, tabletType topo.TabletType, ksid key.KeyspaceId) (newKeyspace, shard string, err error) { + newKeyspace, allShards, err := getKeyspaceShards(ctx, rtr.serv, rtr.cell, keyspace, tabletType) + if err != nil { + return "", "", err + } + shard, err = getShardForKeyspaceId(allShards, ksid) + if err != nil { + return "", "", err + } + return newKeyspace, shard, nil +} diff --git a/go/vt/vtgate/router_dml_test.go b/go/vt/vtgate/router_dml_test.go new file mode 100644 index 00000000000..8418bf7ce5f --- /dev/null +++ b/go/vt/vtgate/router_dml_test.go @@ -0,0 +1,657 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vtgate + +import ( + "reflect" + "strings" + "testing" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/sqltypes" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + _ "github.com/youtube/vitess/go/vt/vtgate/vindexes" +) + +func TestUpdateEqual(t *testing.T) { + router, sbc1, sbc2, sbclookup := createRouterEnv() + + _, err := routerExec(router, "update user set a=2 where id = 1", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "update user set a = 2 where id = 1 /* _routing keyspace_id:166b40b44aba4bd6 */", + BindVariables: map[string]interface{}{ + "keyspace_id": "\x16k@\xb4J\xbaK\xd6", + }, + }} + if !reflect.DeepEqual(sbc1.Queries, wantQueries) { + t.Errorf("sbc1.Queries: %+v, want %+v\n", sbc1.Queries, wantQueries) + } + if sbc2.Queries != nil { + t.Errorf("sbc2.Queries: %+v, want nil\n", sbc2.Queries) + } + + sbc1.Queries = nil + _, err = routerExec(router, "update user set a=2 where id = 3", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "update user set a = 2 where id = 3 /* _routing keyspace_id:4eb190c9a2fa169c */", + BindVariables: map[string]interface{}{ + "keyspace_id": "N\xb1\x90É¢\xfa\x16\x9c", + }, + }} + if !reflect.DeepEqual(sbc2.Queries, wantQueries) { + t.Errorf("sbc2.Queries: %+v, want %+v\n", sbc2.Queries, wantQueries) + } + if sbc1.Queries != nil { + t.Errorf("sbc1.Queries: %+v, want nil\n", sbc1.Queries) + } + + sbc1.Queries = nil + sbc2.Queries = nil + sbclookup.setResults([]*mproto.QueryResult{&mproto.QueryResult{}}) + _, err = routerExec(router, "update music set a=2 where id = 2", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select user_id from music_user_map where music_id = :music_id", + BindVariables: map[string]interface{}{ + "music_id": int64(2), + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } + if sbc2.Queries != nil { + t.Errorf("sbc2.Queries: %+v, want nil\n", sbc2.Queries) + } + if sbc1.Queries != nil { + t.Errorf("sbc1.Queries: %+v, want nil\n", sbc1.Queries) + } +} + +func TestUpdateEqualFail(t *testing.T) { + router, _, _, _ := createRouterEnv() + s := getSandbox("TestRouter") + + _, err := routerExec(router, "update user set a=2 where id = :aa", nil) + want := "execUpdateEqual: could not find bind var :aa" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + s.SrvKeyspaceMustFail = 1 + _, err = routerExec(router, "update user set a=2 where id = :id", map[string]interface{}{ + "id": 1, + }) + want = "execUpdateEqual: keyspace TestRouter fetch error: topo error GetSrvKeyspace" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + _, err = routerExec(router, "update user set a=2 where id = :id", map[string]interface{}{ + "id": "aa", + }) + want = "execUpdateEqual: hash.Map: unexpected type for aa: string" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + s.ShardSpec = "80-" + _, err = routerExec(router, "update user set a=2 where id = :id", map[string]interface{}{ + "id": 1, + }) + want = "execUpdateEqual: KeyspaceId 166b40b44aba4bd6 didn't match any shards" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + s.ShardSpec = DefaultShardSpec +} + +func TestDeleteEqual(t *testing.T) { + router, sbc, _, sbclookup := createRouterEnv() + + sbc.setResults([]*mproto.QueryResult{&mproto.QueryResult{ + Fields: []mproto.Field{ + {"id", 3}, + {"name", 253}, + }, + RowsAffected: 1, + InsertId: 0, + Rows: [][]sqltypes.Value{{ + {sqltypes.Numeric("1")}, + {sqltypes.String("myname")}, + }}, + }}) + _, err := routerExec(router, "delete from user where id = 1", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "select id, name from user where id = 1 for update", + BindVariables: map[string]interface{}{}, + }, { + Sql: "delete from user where id = 1 /* _routing keyspace_id:166b40b44aba4bd6 */", + BindVariables: map[string]interface{}{ + "keyspace_id": "\x16k@\xb4J\xbaK\xd6", + }, + }} + if !reflect.DeepEqual(sbc.Queries, wantQueries) { + t.Errorf("sbc.Queries: %+v, want %+v\n", sbc.Queries, wantQueries) + } + + wantQueries = []tproto.BoundQuery{{ + Sql: "delete from user_idx where id in ::id", + BindVariables: map[string]interface{}{ + "id": []interface{}{int64(1)}, + }, + }, { + Sql: "delete from name_user_map where name in ::name and user_id = :user_id", + BindVariables: map[string]interface{}{ + "user_id": int64(1), + "name": []interface{}{"myname"}, + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } + + sbc.Queries = nil + sbclookup.Queries = nil + sbc.setResults([]*mproto.QueryResult{&mproto.QueryResult{}}) + _, err = routerExec(router, "delete from user where id = 1", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select id, name from user where id = 1 for update", + BindVariables: map[string]interface{}{}, + }, { + Sql: "delete from user where id = 1 /* _routing keyspace_id:166b40b44aba4bd6 */", + BindVariables: map[string]interface{}{ + "keyspace_id": "\x16k@\xb4J\xbaK\xd6", + }, + }} + if !reflect.DeepEqual(sbc.Queries, wantQueries) { + t.Errorf("sbc.Queries: %+v, want %+v\n", sbc.Queries, wantQueries) + } + if sbclookup.Queries != nil { + t.Errorf("sbclookup.Queries: %+v, want nil\n", sbclookup.Queries) + } + + sbc.Queries = nil + sbclookup.Queries = nil + sbclookup.setResults([]*mproto.QueryResult{&mproto.QueryResult{}}) + _, err = routerExec(router, "delete from music where id = 1", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select user_id from music_user_map where music_id = :music_id", + BindVariables: map[string]interface{}{ + "music_id": int64(1), + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } + if sbc.Queries != nil { + t.Errorf("sbc.Queries: %+v, want nil\n", sbc.Queries) + } +} + +func TestDeleteEqualFail(t *testing.T) { + router, _, _, _ := createRouterEnv() + s := getSandbox("TestRouter") + + _, err := routerExec(router, "delete from user where id = :aa", nil) + want := "execDeleteEqual: could not find bind var :aa" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + s.SrvKeyspaceMustFail = 1 + _, err = routerExec(router, "delete from user where id = :id", map[string]interface{}{ + "id": 1, + }) + want = "execDeleteEqual: keyspace TestRouter fetch error: topo error GetSrvKeyspace" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + _, err = routerExec(router, "delete from user where id = :id", map[string]interface{}{ + "id": "aa", + }) + want = "execDeleteEqual: hash.Map: unexpected type for aa: string" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + s.ShardSpec = "80-" + _, err = routerExec(router, "delete from user where id = :id", map[string]interface{}{ + "id": 1, + }) + want = "execDeleteEqual: KeyspaceId 166b40b44aba4bd6 didn't match any shards" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + s.ShardSpec = DefaultShardSpec +} + +func TestDeleteVindexFail(t *testing.T) { + router, sbc, _, sbclookup := createRouterEnv() + + sbc.mustFailServer = 1 + _, err := routerExec(router, "delete from user where id = 1", nil) + want := "execDeleteEqual: shard, host: TestRouter.-20.master" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + sbc.setResults([]*mproto.QueryResult{&mproto.QueryResult{ + Fields: []mproto.Field{ + {"id", 3}, + {"name", 253}, + }, + RowsAffected: 1, + InsertId: 0, + Rows: [][]sqltypes.Value{{ + {sqltypes.String("foo")}, + {sqltypes.String("myname")}, + }}, + }}) + _, err = routerExec(router, "delete from user where id = 1", nil) + want = `execDeleteEqual: strconv.ParseUint: parsing "foo": invalid syntax` + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + sbclookup.mustFailServer = 1 + _, err = routerExec(router, "delete from user where id = 1", nil) + want = "execDeleteEqual: hash.Delete: shard, host: TestUnsharded.0.master" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + sbclookup.mustFailServer = 1 + _, err = routerExec(router, "delete from music where user_id = 1", nil) + want = "execDeleteEqual: lookup.Delete: shard, host: TestUnsharded.0.master" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } +} + +func TestInsertSharded(t *testing.T) { + router, sbc1, sbc2, sbclookup := createRouterEnv() + + _, err := routerExec(router, "insert into user(id, v, name) values (1, 2, 'myname')", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "insert into user(id, v, name) values (:_id, 2, :_name) /* _routing keyspace_id:166b40b44aba4bd6 */", + BindVariables: map[string]interface{}{ + "keyspace_id": "\x16k@\xb4J\xbaK\xd6", + "_id": int64(1), + "_name": "myname", + }, + }} + if !reflect.DeepEqual(sbc1.Queries, wantQueries) { + t.Errorf("sbc1.Queries: %+v, want %+v\n", sbc1.Queries, wantQueries) + } + if sbc2.Queries != nil { + t.Errorf("sbc2.Queries: %+v, want nil\n", sbc2.Queries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "insert into user_idx(id) values(:id)", + BindVariables: map[string]interface{}{ + "id": int64(1), + }, + }, { + Sql: "insert into name_user_map(name, user_id) values(:name, :user_id)", + BindVariables: map[string]interface{}{ + "name": "myname", + "user_id": int64(1), + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } + + sbc1.Queries = nil + sbclookup.Queries = nil + _, err = routerExec(router, "insert into user(id, v, name) values (3, 2, 'myname2')", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "insert into user(id, v, name) values (:_id, 2, :_name) /* _routing keyspace_id:4eb190c9a2fa169c */", + BindVariables: map[string]interface{}{ + "keyspace_id": "N\xb1\x90É¢\xfa\x16\x9c", + "_id": int64(3), + "_name": "myname2", + }, + }} + if !reflect.DeepEqual(sbc2.Queries, wantQueries) { + t.Errorf("sbc2.Queries: %+v, want %+v\n", sbc2.Queries, wantQueries) + } + if sbc1.Queries != nil { + t.Errorf("sbc1.Queries: %+v, want nil\n", sbc1.Queries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "insert into user_idx(id) values(:id)", + BindVariables: map[string]interface{}{ + "id": int64(3), + }, + }, { + Sql: "insert into name_user_map(name, user_id) values(:name, :user_id)", + BindVariables: map[string]interface{}{ + "name": "myname2", + "user_id": int64(3), + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } +} + +func TestInsertGenerator(t *testing.T) { + router, sbc, _, sbclookup := createRouterEnv() + + sbclookup.setResults([]*mproto.QueryResult{&mproto.QueryResult{RowsAffected: 1, InsertId: 1}}) + result, err := routerExec(router, "insert into user(v, name) values (2, 'myname')", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "insert into user(v, name, id) values (2, :_name, :_id) /* _routing keyspace_id:166b40b44aba4bd6 */", + BindVariables: map[string]interface{}{ + "keyspace_id": "\x16k@\xb4J\xbaK\xd6", + "_id": int64(1), + "_name": "myname", + }, + }} + if !reflect.DeepEqual(sbc.Queries, wantQueries) { + t.Errorf("sbc.Queries: %+v, want %+v\n", sbc.Queries, wantQueries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "insert into user_idx(id) values(:id)", + BindVariables: map[string]interface{}{ + "id": nil, + }, + }, { + Sql: "insert into name_user_map(name, user_id) values(:name, :user_id)", + BindVariables: map[string]interface{}{ + "name": "myname", + "user_id": int64(1), + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } + wantResult := *singleRowResult + wantResult.InsertId = 1 + if !reflect.DeepEqual(result, &wantResult) { + t.Errorf("result: %+v, want %+v", result, &wantResult) + } +} + +func TestInsertLookupOwned(t *testing.T) { + router, sbc, _, sbclookup := createRouterEnv() + + _, err := routerExec(router, "insert into music(user_id, id) values (2, 3)", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "insert into music(user_id, id) values (:_user_id, :_id) /* _routing keyspace_id:06e7ea22ce92708f */", + BindVariables: map[string]interface{}{ + "keyspace_id": "\x06\xe7\xea\"Î’p\x8f", + "_user_id": int64(2), + "_id": int64(3), + }, + }} + if !reflect.DeepEqual(sbc.Queries, wantQueries) { + t.Errorf("sbc.Queries: %+v, want %+v\n", sbc.Queries, wantQueries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "insert into music_user_map(music_id, user_id) values(:music_id, :user_id)", + BindVariables: map[string]interface{}{ + "music_id": int64(3), + "user_id": int64(2), + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } +} + +func TestInsertLookupOwnedGenerator(t *testing.T) { + router, sbc, _, sbclookup := createRouterEnv() + + sbclookup.setResults([]*mproto.QueryResult{&mproto.QueryResult{RowsAffected: 1, InsertId: 1}}) + result, err := routerExec(router, "insert into music(user_id) values (2)", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "insert into music(user_id, id) values (:_user_id, :_id) /* _routing keyspace_id:06e7ea22ce92708f */", + BindVariables: map[string]interface{}{ + "keyspace_id": "\x06\xe7\xea\"Î’p\x8f", + "_user_id": int64(2), + "_id": int64(1), + }, + }} + if !reflect.DeepEqual(sbc.Queries, wantQueries) { + t.Errorf("sbc.Queries: %+v, want %+v\n", sbc.Queries, wantQueries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "insert into music_user_map(music_id, user_id) values(:music_id, :user_id)", + BindVariables: map[string]interface{}{ + "music_id": nil, + "user_id": int64(2), + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } + wantResult := *singleRowResult + wantResult.InsertId = 1 + if !reflect.DeepEqual(result, &wantResult) { + t.Errorf("result: %+v, want %+v", result, &wantResult) + } +} + +func TestInsertLookupUnowned(t *testing.T) { + router, sbc, _, sbclookup := createRouterEnv() + + _, err := routerExec(router, "insert into music_extra(user_id, music_id) values (2, 3)", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "insert into music_extra(user_id, music_id) values (:_user_id, :_music_id) /* _routing keyspace_id:06e7ea22ce92708f */", + BindVariables: map[string]interface{}{ + "keyspace_id": "\x06\xe7\xea\"Î’p\x8f", + "_user_id": int64(2), + "_music_id": int64(3), + }, + }} + if !reflect.DeepEqual(sbc.Queries, wantQueries) { + t.Errorf("sbc.Queries: %+v, want %+v\n", sbc.Queries, wantQueries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select music_id from music_user_map where music_id = :music_id and user_id = :user_id", + BindVariables: map[string]interface{}{ + "music_id": int64(3), + "user_id": int64(2), + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } +} + +func TestInsertLookupUnownedUnsupplied(t *testing.T) { + router, sbc, _, sbclookup := createRouterEnv() + + _, err := routerExec(router, "insert into music_extra_reversed(music_id) values (3)", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "insert into music_extra_reversed(music_id, user_id) values (:_music_id, :_user_id) /* _routing keyspace_id:166b40b44aba4bd6 */", + BindVariables: map[string]interface{}{ + "keyspace_id": "\x16k@\xb4J\xbaK\xd6", + "_user_id": int64(1), + "_music_id": int64(3), + }, + }} + if !reflect.DeepEqual(sbc.Queries, wantQueries) { + t.Errorf("sbc.Queries: %+v, want %+v\n", sbc.Queries, wantQueries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select user_id from music_user_map where music_id = :music_id", + BindVariables: map[string]interface{}{ + "music_id": int64(3), + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } +} + +func TestInsertFail(t *testing.T) { + router, sbc, _, sbclookup := createRouterEnv() + + _, err := routerExec(router, "insert into user(id, v, name) values (:aa, 2, 'myname')", nil) + want := "execInsertSharded: could not find bind var :aa" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + sbclookup.mustFailServer = 1 + _, err = routerExec(router, "insert into user(id, v, name) values (null, 2, 'myname')", nil) + want = "execInsertSharded: hash.Generate" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + sbclookup.mustFailServer = 1 + _, err = routerExec(router, "insert into user(id, v, name) values (1, 2, 'myname')", nil) + want = "execInsertSharded: hash.Create" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + _, err = routerExec(router, "insert into ksid_table(keyspace_id) values (null)", nil) + want = "execInsertSharded: value must be supplied for column keyspace_id" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + sbclookup.mustFailServer = 1 + _, err = routerExec(router, "insert into music_extra_reversed(music_id, user_id) values (1, 1)", nil) + want = "execInsertSharded: lookup.Map" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + sbclookup.setResults([]*mproto.QueryResult{&mproto.QueryResult{}}) + _, err = routerExec(router, "insert into music_extra_reversed(music_id, user_id) values (1, 1)", nil) + want = "execInsertSharded: could not map 1 to a keyspace id" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + getSandbox("TestRouter").SrvKeyspaceMustFail = 1 + _, err = routerExec(router, "insert into user(id, v, name) values (1, 2, 'myname')", nil) + want = "execInsertSharded: keyspace TestRouter fetch error: topo error GetSrvKeyspace" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + getSandbox("TestRouter").ShardSpec = "80-" + _, err = routerExec(router, "insert into user(id, v, name) values (1, 2, 'myname')", nil) + want = "execInsertSharded: KeyspaceId 166b40b44aba4bd6 didn't match any shards" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + getSandbox("TestRouter").ShardSpec = DefaultShardSpec + + sbclookup.mustFailServer = 1 + _, err = routerExec(router, "insert into music(user_id, id) values (1, null)", nil) + want = "lookup.Generate: shard, host: TestUnsharded.0.master" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + sbclookup.mustFailServer = 1 + _, err = routerExec(router, "insert into music(user_id, id) values (1, 2)", nil) + want = "lookup.Create: shard, host: TestUnsharded.0.master" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + _, err = routerExec(router, "insert into music_extra(user_id, music_id) values (1, null)", nil) + want = "value must be supplied for column music_id" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + _, err = routerExec(router, "insert into music_extra_reversed(music_id, user_id) values (1, 'aa')", nil) + want = "hash.Verify: unexpected type for aa: string" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + _, err = routerExec(router, "insert into music_extra_reversed(music_id, user_id) values (1, 3)", nil) + want = "value 3 for column user_id does not map to keyspace id 166b40b44aba4bd6" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + sbc.mustFailServer = 1 + _, err = routerExec(router, "insert into user(id, v, name) values (1, 2, 'myname')", nil) + want = "execInsertSharded: shard, host: TestRouter.-20.master" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + sbclookup.setResults([]*mproto.QueryResult{ + &mproto.QueryResult{RowsAffected: 1, InsertId: 1}, + &mproto.QueryResult{RowsAffected: 1, InsertId: 1}, + }) + _, err = routerExec(router, "insert into multi_autoinc_table(id1, id2) values (null, null)", nil) + want = "insert generated more than one value" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + _, err = routerExec(router, "insert into noauto_table(id) values (null)", nil) + want = "execInsertSharded: value must be supplied for column id" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + _, err = routerExec(router, "insert into user(id, v, name) values (1, 2, null)", nil) + want = "value must be supplied for column name" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + sbc.setResults([]*mproto.QueryResult{&mproto.QueryResult{RowsAffected: 1, InsertId: 1}}) + sbclookup.setResults([]*mproto.QueryResult{&mproto.QueryResult{RowsAffected: 1, InsertId: 1}}) + _, err = routerExec(router, "insert into user(id, v, name) values (null, 2, 'myname')", nil) + want = "vindex and db generated a value each for insert" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } +} diff --git a/go/vt/vtgate/router_framework_test.go b/go/vt/vtgate/router_framework_test.go new file mode 100644 index 00000000000..2bc8358e2b3 --- /dev/null +++ b/go/vt/vtgate/router_framework_test.go @@ -0,0 +1,263 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vtgate + +import ( + "io/ioutil" + "os" + "time" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" + "github.com/youtube/vitess/go/vt/vtgate/proto" + _ "github.com/youtube/vitess/go/vt/vtgate/vindexes" + "golang.org/x/net/context" +) + +var routerSchema = createTestSchema(` +{ + "Keyspaces": { + "TestRouter": { + "Sharded": true, + "Vindexes": { + "user_index": { + "Type": "hash_autoinc", + "Owner": "user", + "Params": { + "Table": "user_idx", + "Column": "id" + } + }, + "music_user_map": { + "Type": "lookup_hash_unique_autoinc", + "Owner": "music", + "Params": { + "Table": "music_user_map", + "From": "music_id", + "To": "user_id" + } + }, + "name_user_map": { + "Type": "lookup_hash", + "Owner": "user", + "Params": { + "Table": "name_user_map", + "From": "name", + "To": "user_id" + } + }, + "idx1": { + "Type": "hash_autoinc", + "Owner": "multi_autoinc_table", + "Params": { + "Table": "idx1", + "Column": "id1" + } + }, + "idx2": { + "Type": "lookup_hash_autoinc", + "Owner": "multi_autoinc_table", + "Params": { + "Table": "idx2", + "From": "id", + "To": "val" + } + }, + "idx_noauto": { + "Type": "hash", + "Owner": "noauto_table" + }, + "keyspace_id": { + "Type": "numeric" + } + }, + "Classes": { + "user": { + "ColVindexes": [ + { + "Col": "id", + "Name": "user_index" + }, + { + "Col": "name", + "Name": "name_user_map" + } + ] + }, + "user_extra": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "user_index" + } + ] + }, + "music": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "user_index" + }, + { + "Col": "id", + "Name": "music_user_map" + } + ] + }, + "music_extra": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "user_index" + }, + { + "Col": "music_id", + "Name": "music_user_map" + } + ] + }, + "music_extra_reversed": { + "ColVindexes": [ + { + "Col": "music_id", + "Name": "music_user_map" + }, + { + "Col": "user_id", + "Name": "user_index" + } + ] + }, + "multi_autoinc_table": { + "ColVindexes": [ + { + "Col": "id1", + "Name": "idx1" + }, + { + "Col": "id2", + "Name": "idx2" + } + ] + }, + "noauto_table": { + "ColVindexes": [ + { + "Col": "id", + "Name": "idx_noauto" + } + ] + }, + "ksid_table": { + "ColVindexes": [ + { + "Col": "keyspace_id", + "Name": "keyspace_id" + } + ] + } + }, + "Tables": { + "user": "user", + "user_extra": "user_extra", + "music": "music", + "music_extra": "music_extra", + "music_extra_reversed": "music_extra_reversed", + "multi_autoinc_table": "multi_autoinc_table", + "noauto_table": "noauto_table", + "ksid_table": "ksid_table" + } + }, + "TestBadSharding": { + "Sharded": false, + "Tables": { + "sharded_table": "" + } + }, + "TestUnsharded": { + "Sharded": false, + "Tables": { + "user_idx": "", + "music_user_map": "", + "name_user_map": "", + "idx1": "", + "idx2": "" + } + } + } +} +`) + +// createTestSchema creates a schema based on the JSON specs. +// It panics on failure. +func createTestSchema(schemaJSON string) *planbuilder.Schema { + f, err := ioutil.TempFile("", "vtgate_schema") + if err != nil { + panic(err) + } + fname := f.Name() + f.Close() + defer os.Remove(fname) + + err = ioutil.WriteFile(fname, []byte(schemaJSON), 0644) + if err != nil { + panic(err) + } + schema, err := planbuilder.LoadFile(fname) + if err != nil { + panic(err) + } + return schema +} + +func createRouterEnv() (router *Router, sbc1, sbc2, sbclookup *sandboxConn) { + s := createSandbox("TestRouter") + sbc1 = &sandboxConn{} + sbc2 = &sandboxConn{} + s.MapTestConn("-20", sbc1) + s.MapTestConn("40-60", sbc2) + + l := createSandbox(KsTestUnsharded) + sbclookup = &sandboxConn{} + l.MapTestConn("0", sbclookup) + + createSandbox("TestBadSharding") + + serv := new(sandboxTopo) + scatterConn := NewScatterConn(serv, "", "aa", 1*time.Second, 10, 1*time.Millisecond) + router = NewRouter(serv, "aa", routerSchema, "", scatterConn) + return router, sbc1, sbc2, sbclookup +} + +func routerExec(router *Router, sql string, bv map[string]interface{}) (*mproto.QueryResult, error) { + return router.Execute(context.Background(), &proto.Query{ + Sql: sql, + BindVariables: bv, + TabletType: topo.TYPE_MASTER, + }) +} + +func routerStream(router *Router, q *proto.Query) (qr *mproto.QueryResult, err error) { + results := make(chan *mproto.QueryResult, 10) + err = router.StreamExecute(context.Background(), q, func(qr *mproto.QueryResult) error { + results <- qr + return nil + }) + close(results) + if err != nil { + return nil, err + } + first := true + for r := range results { + if first { + qr = &mproto.QueryResult{Fields: r.Fields} + first = false + } + qr.Rows = append(qr.Rows, r.Rows...) + qr.RowsAffected += r.RowsAffected + } + return qr, nil +} diff --git a/go/vt/vtgate/router_select_test.go b/go/vt/vtgate/router_select_test.go new file mode 100644 index 00000000000..ff568a38602 --- /dev/null +++ b/go/vt/vtgate/router_select_test.go @@ -0,0 +1,617 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vtgate + +import ( + "reflect" + "strings" + "testing" + "time" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/sqltypes" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/vtgate/proto" + _ "github.com/youtube/vitess/go/vt/vtgate/vindexes" +) + +func TestUnsharded(t *testing.T) { + router, _, _, sbclookup := createRouterEnv() + + _, err := routerExec(router, "select * from music_user_map where id = 1", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "select * from music_user_map where id = 1", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } + + _, err = routerExec(router, "update music_user_map set id = 1", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select * from music_user_map where id = 1", + BindVariables: map[string]interface{}{}, + }, { + Sql: "update music_user_map set id = 1", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } + + sbclookup.Queries = nil + _, err = routerExec(router, "delete from music_user_map", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "delete from music_user_map", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } + + sbclookup.Queries = nil + _, err = routerExec(router, "insert into music_user_map values(1)", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "insert into music_user_map values(1)", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } +} + +func TestStreamUnsharded(t *testing.T) { + router, _, _, _ := createRouterEnv() + + q := proto.Query{ + Sql: "select * from music_user_map where id = 1", + TabletType: topo.TYPE_MASTER, + } + result, err := routerStream(router, &q) + if err != nil { + t.Error(err) + } + wantResult := singleRowResult + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } +} + +func TestUnshardedFail(t *testing.T) { + router, _, _, _ := createRouterEnv() + + getSandbox(KsTestUnsharded).SrvKeyspaceMustFail = 1 + _, err := routerExec(router, "select * from music_user_map where id = 1", nil) + want := "paramsUnsharded: keyspace TestUnsharded fetch error: topo error GetSrvKeyspace" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + _, err = routerExec(router, "select * from sharded_table where id = 1", nil) + want = "unsharded keyspace TestBadSharding has multiple shards" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } +} + +func TestStreamUnshardedFail(t *testing.T) { + router, _, _, _ := createRouterEnv() + + getSandbox(KsTestUnsharded).SrvKeyspaceMustFail = 1 + q := proto.Query{ + Sql: "select * from music_user_map where id = 1", + TabletType: topo.TYPE_MASTER, + } + _, err := routerStream(router, &q) + want := "paramsUnsharded: keyspace TestUnsharded fetch error: topo error GetSrvKeyspace" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + q = proto.Query{ + Sql: "update music_user_map set a = 1 where id = 1", + TabletType: topo.TYPE_MASTER, + } + _, err = routerStream(router, &q) + want = `query "update music_user_map set a = 1 where id = 1" cannot be used for streaming` + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } +} + +func TestSelectEqual(t *testing.T) { + router, sbc1, sbc2, sbclookup := createRouterEnv() + + _, err := routerExec(router, "select * from user where id = 1", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "select * from user where id = 1", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbc1.Queries, wantQueries) { + t.Errorf("sbc1.Queries: %+v, want %+v\n", sbc1.Queries, wantQueries) + } + if sbc2.Queries != nil { + t.Errorf("sbc2.Queries: %+v, want nil\n", sbc2.Queries) + } + + sbc1.Queries = nil + _, err = routerExec(router, "select * from user where id = 3", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select * from user where id = 3", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbc2.Queries, wantQueries) { + t.Errorf("sbc2.Queries: %+v, want %+v\n", sbc2.Queries, wantQueries) + } + if sbc1.ExecCount != 1 { + t.Errorf("sbc1.ExecCount: %v, want 1\n", sbc1.ExecCount) + } + if sbc1.Queries != nil { + t.Errorf("sbc1.Queries: %+v, want nil\n", sbc1.Queries) + } + + sbc2.Queries = nil + _, err = routerExec(router, "select * from user where name = 'foo'", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select * from user where name = 'foo'", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbc1.Queries, wantQueries) { + t.Errorf("sbc1.Queries: %+v, want %+v\n", sbc1.Queries, wantQueries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select user_id from name_user_map where name = :name", + BindVariables: map[string]interface{}{ + "name": "foo", + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } +} + +func TestSelectEqualNotFound(t *testing.T) { + router, _, _, sbclookup := createRouterEnv() + + sbclookup.setResults([]*mproto.QueryResult{&mproto.QueryResult{}}) + result, err := routerExec(router, "select * from music where id = 1", nil) + if err != nil { + t.Error(err) + } + wantResult := &mproto.QueryResult{} + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } + + sbclookup.setResults([]*mproto.QueryResult{&mproto.QueryResult{}}) + result, err = routerExec(router, "select * from user where name = 'foo'", nil) + if err != nil { + t.Error(err) + } + wantResult = &mproto.QueryResult{} + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } +} + +func TestStreamSelectEqual(t *testing.T) { + router, _, _, _ := createRouterEnv() + + q := proto.Query{ + Sql: "select * from user where id = 1", + TabletType: topo.TYPE_MASTER, + } + result, err := routerStream(router, &q) + if err != nil { + t.Error(err) + } + wantResult := singleRowResult + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } +} + +func TestSelectEqualFail(t *testing.T) { + router, _, _, sbclookup := createRouterEnv() + s := getSandbox("TestRouter") + + _, err := routerExec(router, "select * from user where id = (select count(*) from music)", nil) + want := "cannot route query: select * from user where id = (select count(*) from music): has subquery" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + _, err = routerExec(router, "select * from user where id = :aa", nil) + want = "paramsSelectEqual: could not find bind var :aa" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + s.SrvKeyspaceMustFail = 1 + _, err = routerExec(router, "select * from user where id = 1", nil) + want = "paramsSelectEqual: keyspace TestRouter fetch error: topo error GetSrvKeyspace" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + sbclookup.mustFailServer = 1 + _, err = routerExec(router, "select * from music where id = 1", nil) + want = "paramsSelectEqual: lookup.Map" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + s.ShardSpec = "80-" + _, err = routerExec(router, "select * from user where id = 1", nil) + want = "paramsSelectEqual: KeyspaceId 166b40b44aba4bd6 didn't match any shards" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + s.ShardSpec = DefaultShardSpec + + sbclookup.mustFailServer = 1 + _, err = routerExec(router, "select * from user where name = 'foo'", nil) + want = "paramsSelectEqual: lookup.Map" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + + s.ShardSpec = "80-" + _, err = routerExec(router, "select * from user where name = 'foo'", nil) + want = "paramsSelectEqual: KeyspaceId 166b40b44aba4bd6 didn't match any shards" + if err == nil || !strings.HasPrefix(err.Error(), want) { + t.Errorf("routerExec: %v, want prefix %v", err, want) + } + s.ShardSpec = DefaultShardSpec +} + +func TestSelectIN(t *testing.T) { + router, sbc1, sbc2, sbclookup := createRouterEnv() + + _, err := routerExec(router, "select * from user where id in (1)", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "select * from user where id in ::_vals", + BindVariables: map[string]interface{}{ + "_vals": []interface{}{int64(1)}, + }, + }} + if !reflect.DeepEqual(sbc1.Queries, wantQueries) { + t.Errorf("sbc1.Queries: %+v, want %+v\n", sbc1.Queries, wantQueries) + } + if sbc2.Queries != nil { + t.Errorf("sbc2.Queries: %+v, want nil\n", sbc2.Queries) + } + + sbc1.Queries = nil + _, err = routerExec(router, "select * from user where id in (1, 3)", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select * from user where id in ::_vals", + BindVariables: map[string]interface{}{ + "_vals": []interface{}{int64(1)}, + }, + }} + if !reflect.DeepEqual(sbc1.Queries, wantQueries) { + t.Errorf("sbc1.Queries: %+v, want %+v\n", sbc1.Queries, wantQueries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select * from user where id in ::_vals", + BindVariables: map[string]interface{}{ + "_vals": []interface{}{int64(3)}, + }, + }} + if !reflect.DeepEqual(sbc2.Queries, wantQueries) { + t.Errorf("sbc2.Queries: %+v, want %+v\n", sbc2.Queries, wantQueries) + } + + sbc1.Queries = nil + sbc2.Queries = nil + _, err = routerExec(router, "select * from user where name = 'foo'", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select * from user where name = 'foo'", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbc1.Queries, wantQueries) { + t.Errorf("sbc1.Queries: %+v, want %+v\n", sbc1.Queries, wantQueries) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select user_id from name_user_map where name = :name", + BindVariables: map[string]interface{}{ + "name": "foo", + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } +} + +func TestStreamSelectIN(t *testing.T) { + router, _, _, sbclookup := createRouterEnv() + + q := proto.Query{ + Sql: "select * from user where id in (1)", + TabletType: topo.TYPE_MASTER, + } + result, err := routerStream(router, &q) + if err != nil { + t.Error(err) + } + wantResult := singleRowResult + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } + + q.Sql = "select * from user where id in (1, 3)" + result, err = routerStream(router, &q) + if err != nil { + t.Error(err) + } + wantResult = &mproto.QueryResult{ + Fields: singleRowResult.Fields, + Rows: [][]sqltypes.Value{ + singleRowResult.Rows[0], + singleRowResult.Rows[0], + }, + RowsAffected: 2, + } + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } + + q.Sql = "select * from user where name = 'foo'" + result, err = routerStream(router, &q) + if err != nil { + t.Error(err) + } + wantResult = singleRowResult + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } + + wantQueries := []tproto.BoundQuery{{ + Sql: "select user_id from name_user_map where name = :name", + BindVariables: map[string]interface{}{ + "name": "foo", + }, + }} + if !reflect.DeepEqual(sbclookup.Queries, wantQueries) { + t.Errorf("sbclookup.Queries: %+v, want %+v\n", sbclookup.Queries, wantQueries) + } +} + +func TestSelectINFail(t *testing.T) { + router, _, _, _ := createRouterEnv() + + _, err := routerExec(router, "select * from user where id in (:aa)", nil) + want := "paramsSelectIN: could not find bind var :aa" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + getSandbox("TestRouter").SrvKeyspaceMustFail = 1 + _, err = routerExec(router, "select * from user where id in (1)", nil) + want = "paramsSelectEqual: keyspace TestRouter fetch error: topo error GetSrvKeyspace" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } +} + +func TestSelectKeyrange(t *testing.T) { + router, sbc1, sbc2, _ := createRouterEnv() + + _, err := routerExec(router, "select * from user where keyrange('', '\x20')", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "select * from user", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbc1.Queries, wantQueries) { + t.Errorf("sbc1.Queries: %+v, want %+v\n", sbc1.Queries, wantQueries) + } + if sbc2.Queries != nil { + t.Errorf("sbc2.Queries: %+v, want nil\n", sbc2.Queries) + } + + sbc1.Queries = nil + _, err = routerExec(router, "select * from user where keyrange('\x40', '\x60')", nil) + if err != nil { + t.Error(err) + } + wantQueries = []tproto.BoundQuery{{ + Sql: "select * from user", + BindVariables: map[string]interface{}{}, + }} + if !reflect.DeepEqual(sbc2.Queries, wantQueries) { + t.Errorf("sbc2.Queries: %+v, want %+v\n", sbc2.Queries, wantQueries) + } + if sbc1.Queries != nil { + t.Errorf("sbc1.Queries: %+v, want nil\n", sbc1.Queries) + } +} + +func TestStreamSelectKeyrange(t *testing.T) { + router, _, _, _ := createRouterEnv() + + q := proto.Query{ + Sql: "select * from user where keyrange('', '\x20')", + TabletType: topo.TYPE_MASTER, + } + result, err := routerStream(router, &q) + if err != nil { + t.Error(err) + } + wantResult := singleRowResult + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } + + q.Sql = "select * from user where keyrange('\x40', '\x60')" + result, err = routerStream(router, &q) + if err != nil { + t.Error(err) + } + wantResult = singleRowResult + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } +} + +func TestSelectKeyrangeFail(t *testing.T) { + router, _, _, _ := createRouterEnv() + + _, err := routerExec(router, "select * from user where keyrange('', :aa)", nil) + want := "paramsSelectKeyrange: could not find bind var :aa" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + _, err = routerExec(router, "select * from user where keyrange('', :aa)", map[string]interface{}{ + "aa": 1, + }) + want = "paramsSelectKeyrange: expecting strings for keyrange: [ 1]" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + _, err = routerExec(router, "select * from user where keyrange('', :aa)", map[string]interface{}{ + "aa": "\x21", + }) + want = "paramsSelectKeyrange: keyrange {Start: , End: 21} does not exactly match shards" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } + + _, err = routerExec(router, "select * from user where keyrange('', :aa)", map[string]interface{}{ + "aa": "\x40", + }) + want = "keyrange must match exactly one shard: [ @]" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } +} + +func TestSelectScatter(t *testing.T) { + // Special setup: Don't use createRouterEnv. + s := createSandbox("TestRouter") + shards := []string{"-20", "20-40", "40-60", "60-80", "80-a0", "a0-c0", "c0-e0", "e0-"} + var conns []*sandboxConn + for _, shard := range shards { + sbc := &sandboxConn{} + conns = append(conns, sbc) + s.MapTestConn(shard, sbc) + } + serv := new(sandboxTopo) + scatterConn := NewScatterConn(serv, "", "aa", 1*time.Second, 10, 1*time.Millisecond) + router := NewRouter(serv, "aa", routerSchema, "", scatterConn) + + _, err := routerExec(router, "select * from user", nil) + if err != nil { + t.Error(err) + } + wantQueries := []tproto.BoundQuery{{ + Sql: "select * from user", + BindVariables: map[string]interface{}{}, + }} + for _, conn := range conns { + if !reflect.DeepEqual(conn.Queries, wantQueries) { + t.Errorf("conn.Queries = %#v, want %#v", conn.Queries, wantQueries) + } + } +} + +func TestStreamSelectScatter(t *testing.T) { + // Special setup: Don't use createRouterEnv. + s := createSandbox("TestRouter") + shards := []string{"-20", "20-40", "40-60", "60-80", "80-a0", "a0-c0", "c0-e0", "e0-"} + var conns []*sandboxConn + for _, shard := range shards { + sbc := &sandboxConn{} + conns = append(conns, sbc) + s.MapTestConn(shard, sbc) + } + serv := new(sandboxTopo) + scatterConn := NewScatterConn(serv, "", "aa", 1*time.Second, 10, 1*time.Millisecond) + router := NewRouter(serv, "aa", routerSchema, "", scatterConn) + + q := proto.Query{ + Sql: "select * from user", + TabletType: topo.TYPE_MASTER, + } + result, err := routerStream(router, &q) + if err != nil { + t.Error(err) + } + wantResult := &mproto.QueryResult{ + Fields: singleRowResult.Fields, + Rows: [][]sqltypes.Value{ + singleRowResult.Rows[0], + singleRowResult.Rows[0], + singleRowResult.Rows[0], + singleRowResult.Rows[0], + singleRowResult.Rows[0], + singleRowResult.Rows[0], + singleRowResult.Rows[0], + singleRowResult.Rows[0], + }, + RowsAffected: 8, + } + if !reflect.DeepEqual(result, wantResult) { + t.Errorf("result: %+v, want %+v", result, wantResult) + } +} + +func TestSelectScatterFail(t *testing.T) { + // Special setup: Don't use createRouterEnv. + s := createSandbox("TestRouter") + s.SrvKeyspaceMustFail = 1 + shards := []string{"-20", "20-40", "40-60", "60-80", "80-a0", "a0-c0", "c0-e0", "e0-"} + var conns []*sandboxConn + for _, shard := range shards { + sbc := &sandboxConn{} + conns = append(conns, sbc) + s.MapTestConn(shard, sbc) + } + serv := new(sandboxTopo) + scatterConn := NewScatterConn(serv, "", "aa", 1*time.Second, 10, 1*time.Millisecond) + router := NewRouter(serv, "aa", routerSchema, "", scatterConn) + + _, err := routerExec(router, "select * from user", nil) + want := "paramsSelectScatter: keyspace TestRouter fetch error: topo error GetSrvKeyspace" + if err == nil || err.Error() != want { + t.Errorf("routerExec: %v, want %v", err, want) + } +} diff --git a/go/vt/vtgate/router_test.go b/go/vt/vtgate/router_test.go deleted file mode 100644 index 1e3f6d8abd3..00000000000 --- a/go/vt/vtgate/router_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2014, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package vtgate - -import ( - "path" - "testing" - "time" - - "github.com/youtube/vitess/go/testfiles" - "github.com/youtube/vitess/go/vt/context" - "github.com/youtube/vitess/go/vt/topo" - "github.com/youtube/vitess/go/vt/vtgate/planbuilder" - "github.com/youtube/vitess/go/vt/vtgate/proto" -) - -type VTGateSchemaNormalized struct { - Keyspaces map[string]struct { - ShardingScheme int - Indexes map[string]struct { - // Type is ShardKey or Lookup. - Type int - From, To string - Owner string - IsAutoInc bool - } - Tables map[string]struct { - IndexColumns []struct { - Column string - IndexName string - } - } - } -} - -func TestSelectSingleShardKey(t *testing.T) { - schema, err := planbuilder.LoadSchemaJSON(locateFile("router_test.json")) - if err != nil { - t.Fatal(err) - } - s := createSandbox("TestRouter") - sbc1 := &sandboxConn{} - sbc2 := &sandboxConn{} - s.MapTestConn("-20", sbc1) - s.MapTestConn("40-60", sbc2) - serv := new(sandboxTopo) - scatterConn := NewScatterConn(serv, "", "aa", 1*time.Second, 10, 1*time.Millisecond) - router := NewRouter(serv, "aa", schema, "", scatterConn) - q := proto.Query{ - Sql: "select * from user where id = 1", - TabletType: topo.TYPE_MASTER, - } - _, err = router.Execute(&context.DummyContext{}, &q) - if err != nil { - t.Errorf("want nil, got %v", err) - } - if sbc1.ExecCount != 1 { - t.Errorf("want 1, got %v\n", sbc1.ExecCount) - } - if sbc2.ExecCount != 0 { - t.Errorf("want 0, got %v\n", sbc2.ExecCount) - } - q.Sql = "select * from user where id = 3" - _, err = router.Execute(&context.DummyContext{}, &q) - if err != nil { - t.Errorf("want nil, got %v", err) - } - if sbc1.ExecCount != 1 { - t.Errorf("want 1, got %v\n", sbc1.ExecCount) - } - if sbc2.ExecCount != 1 { - t.Errorf("want 1, got %v\n", sbc2.ExecCount) - } -} - -func locateFile(name string) string { - if path.IsAbs(name) { - return name - } - return testfiles.Locate("vtgate/" + name) -} diff --git a/go/vt/vtgate/routing_map.go b/go/vt/vtgate/routing_map.go new file mode 100644 index 00000000000..5dd615ec368 --- /dev/null +++ b/go/vt/vtgate/routing_map.go @@ -0,0 +1,41 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vtgate + +import "github.com/youtube/vitess/go/vt/vtgate/planbuilder" + +type routingMap map[string][]interface{} + +func (rtm routingMap) Add(shard string, id interface{}) { + ids := rtm[shard] + // Check for duplicates + for _, current := range ids { + if id == current { + return + } + } + rtm[shard] = append(ids, id) +} + +func (rtm routingMap) Shards() []string { + shards := make([]string, 0, len(rtm)) + for k := range rtm { + shards = append(shards, k) + } + return shards +} + +func (rtm routingMap) ShardVars(bv map[string]interface{}) map[string]map[string]interface{} { + shardVars := make(map[string]map[string]interface{}, len(rtm)) + for shard, vals := range rtm { + newbv := make(map[string]interface{}, len(bv)+1) + for k, v := range bv { + newbv[k] = v + } + newbv[planbuilder.ListVarName] = vals + shardVars[shard] = newbv + } + return shardVars +} diff --git a/go/vt/vtgate/sandbox_test.go b/go/vt/vtgate/sandbox_test.go index 846fe9685f6..6fcfde69424 100644 --- a/go/vt/vtgate/sandbox_test.go +++ b/go/vt/vtgate/sandbox_test.go @@ -10,7 +10,6 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/sqltypes" "github.com/youtube/vitess/go/sync2" @@ -18,20 +17,21 @@ import ( tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" "github.com/youtube/vitess/go/vt/tabletserver/tabletconn" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // sandbox_test.go provides a sandbox for unit testing VTGate. const ( - TEST_SHARDED = "TestSharded" - TEST_UNSHARDED = "TestUnshared" - TEST_UNSHARDED_SERVED_FROM = "TestUnshardedServedFrom" + KsTestSharded = "TestSharded" + KsTestUnsharded = "TestUnsharded" + KsTestUnshardedServedFrom = "TestUnshardedServedFrom" ) func init() { sandboxMap = make(map[string]*sandbox) - createSandbox(TEST_SHARDED) - createSandbox(TEST_UNSHARDED) + createSandbox(KsTestSharded) + createSandbox(KsTestUnsharded) tabletconn.RegisterDialer("sandbox", sandboxDialer) flag.Set("tablet_protocol", "sandbox") } @@ -110,7 +110,16 @@ func (s *sandbox) Reset() { s.SrvKeyspaceCallback = nil } -func (s *sandbox) MapTestConn(shard string, conn *sandboxConn) { +// a sandboxableConn is a tablet.TabletConn that allows you +// to set the endPoint. MapTestConn uses it to set some good +// defaults. This way, you have the option of calling MapTestConn +// with variables other than sandboxConn. +type sandboxableConn interface { + tabletconn.TabletConn + setEndPoint(topo.EndPoint) +} + +func (s *sandbox) MapTestConn(shard string, conn sandboxableConn) { s.sandmu.Lock() defer s.sandmu.Unlock() conns, ok := s.TestConns[shard] @@ -118,19 +127,23 @@ func (s *sandbox) MapTestConn(shard string, conn *sandboxConn) { conns = make(map[uint32]tabletconn.TabletConn) } uid := uint32(len(conns)) - conn.uid = uid + conn.setEndPoint(topo.EndPoint{ + Uid: uid, + Host: shard, + NamedPortMap: map[string]int{"vt": 1}, + }) conns[uid] = conn s.TestConns[shard] = conns } -func (s *sandbox) DeleteTestConn(shard string, conn *sandboxConn) { +func (s *sandbox) DeleteTestConn(shard string, conn tabletconn.TabletConn) { s.sandmu.Lock() defer s.sandmu.Unlock() conns, ok := s.TestConns[shard] if !ok { panic(fmt.Sprintf("unknown shard: %v", shard)) } - delete(conns, conn.uid) + delete(conns, conn.EndPoint().Uid) s.TestConns[shard] = conns } @@ -237,22 +250,26 @@ func (sct *sandboxTopo) GetSrvKeyspace(context context.Context, cell, keyspace s return nil, fmt.Errorf("topo error GetSrvKeyspace") } switch keyspace { - case TEST_UNSHARDED_SERVED_FROM: + case KsTestUnshardedServedFrom: servedFromKeyspace, err := createUnshardedKeyspace() if err != nil { return nil, err } servedFromKeyspace.ServedFrom = map[topo.TabletType]string{ - topo.TYPE_RDONLY: TEST_UNSHARDED, - topo.TYPE_MASTER: TEST_UNSHARDED} + topo.TYPE_RDONLY: KsTestUnsharded, + topo.TYPE_MASTER: KsTestUnsharded} return servedFromKeyspace, nil - case TEST_UNSHARDED: + case KsTestUnsharded: return createUnshardedKeyspace() } return createShardedSrvKeyspace(sand.ShardSpec, sand.KeyspaceServedFrom) } +func (sct *sandboxTopo) GetSrvShard(context context.Context, cell, keyspace, shard string) (*topo.SrvShard, error) { + return nil, fmt.Errorf("Unsupported") +} + func (sct *sandboxTopo) GetEndPoints(context context.Context, cell, keyspace, shard string, tabletType topo.TabletType) (*topo.EndPoints, error) { sand := getSandbox(keyspace) sand.EndPointCounter++ @@ -265,13 +282,8 @@ func (sct *sandboxTopo) GetEndPoints(context context.Context, cell, keyspace, sh } conns := sand.TestConns[shard] ep := &topo.EndPoints{} - for i := range conns { - ep.Entries = append(ep.Entries, - topo.EndPoint{ - Uid: i, - Host: shard, - NamedPortMap: map[string]int{"vt": 1}, - }) + for _, conn := range conns { + ep.Entries = append(ep.Entries, conn.EndPoint()) } return ep, nil } @@ -290,13 +302,11 @@ func sandboxDialer(context context.Context, endPoint topo.EndPoint, keyspace, sh panic(fmt.Sprintf("can't find shard %v", shard)) } tconn := conns[endPoint.Uid] - tconn.(*sandboxConn).endPoint = endPoint return tconn, nil } // sandboxConn satisfies the TabletConn interface type sandboxConn struct { - uid uint32 endPoint topo.EndPoint mustFailRetry int mustFailFatal int @@ -317,11 +327,16 @@ type sandboxConn struct { RollbackCount sync2.AtomicInt64 CloseCount sync2.AtomicInt64 - // BindVars keeps track of the bind vars that were sent. - BindVars []string + // Queries stores the requests received. + Queries []tproto.BoundQuery + + // results specifies the results to be returned. + // They're consumed as results are returned. If there are + // no results left, singleRowResult is returned. + results []*mproto.QueryResult // transaction id generator - TransactionId sync2.AtomicInt64 + TransactionID sync2.AtomicInt64 } func (sbc *sandboxConn) getError() error { @@ -355,18 +370,27 @@ func (sbc *sandboxConn) getError() error { return nil } +func (sbc *sandboxConn) setResults(r []*mproto.QueryResult) { + sbc.results = r +} + func (sbc *sandboxConn) Execute(context context.Context, query string, bindVars map[string]interface{}, transactionID int64) (*mproto.QueryResult, error) { sbc.ExecCount.Add(1) - for k, _ := range bindVars { - sbc.BindVars = append(sbc.BindVars, k) - } + bv := make(map[string]interface{}) + for k, v := range bindVars { + bv[k] = v + } + sbc.Queries = append(sbc.Queries, tproto.BoundQuery{ + Sql: query, + BindVariables: bv, + }) if sbc.mustDelay != 0 { time.Sleep(sbc.mustDelay) } if err := sbc.getError(); err != nil { return nil, err } - return singleRowResult, nil + return sbc.getNextResult(), nil } func (sbc *sandboxConn) ExecuteBatch(context context.Context, queries []tproto.BoundQuery, transactionID int64) (*tproto.QueryResultList, error) { @@ -380,21 +404,26 @@ func (sbc *sandboxConn) ExecuteBatch(context context.Context, queries []tproto.B qrl := &tproto.QueryResultList{} qrl.List = make([]mproto.QueryResult, 0, len(queries)) for _ = range queries { - qrl.List = append(qrl.List, *singleRowResult) + qrl.List = append(qrl.List, *(sbc.getNextResult())) } return qrl, nil } func (sbc *sandboxConn) StreamExecute(context context.Context, query string, bindVars map[string]interface{}, transactionID int64) (<-chan *mproto.QueryResult, tabletconn.ErrFunc, error) { sbc.ExecCount.Add(1) - for k, _ := range bindVars { - sbc.BindVars = append(sbc.BindVars, k) - } + bv := make(map[string]interface{}) + for k, v := range bindVars { + bv[k] = v + } + sbc.Queries = append(sbc.Queries, tproto.BoundQuery{ + Sql: query, + BindVariables: bv, + }) if sbc.mustDelay != 0 { time.Sleep(sbc.mustDelay) } ch := make(chan *mproto.QueryResult, 1) - ch <- singleRowResult + ch <- sbc.getNextResult() close(ch) err := sbc.getError() return ch, func() error { return err }, err @@ -410,7 +439,7 @@ func (sbc *sandboxConn) Begin(context context.Context) (int64, error) { if err != nil { return 0, err } - return sbc.TransactionId.Add(1), nil + return sbc.TransactionID.Add(1), nil } func (sbc *sandboxConn) Commit(context context.Context, transactionID int64) error { @@ -459,6 +488,19 @@ func (sbc *sandboxConn) EndPoint() topo.EndPoint { return sbc.endPoint } +func (sbc *sandboxConn) setEndPoint(ep topo.EndPoint) { + sbc.endPoint = ep +} + +func (sbc *sandboxConn) getNextResult() *mproto.QueryResult { + if len(sbc.results) != 0 { + r := sbc.results[0] + sbc.results = sbc.results[1:] + return r + } + return singleRowResult +} + var singleRowResult = &mproto.QueryResult{ Fields: []mproto.Field{ {"id", 3}, diff --git a/go/vt/vtgate/scatter_conn.go b/go/vt/vtgate/scatter_conn.go index 07ed88e4faa..4c880647d4c 100644 --- a/go/vt/vtgate/scatter_conn.go +++ b/go/vt/vtgate/scatter_conn.go @@ -10,7 +10,6 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/stats" "github.com/youtube/vitess/go/sync2" @@ -20,6 +19,7 @@ import ( "github.com/youtube/vitess/go/vt/tabletserver/tabletconn" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" ) var idGen sync2.AtomicInt64 @@ -184,6 +184,7 @@ func (stc *ScatterConn) ExecuteMulti( return qr, nil } +// ExecuteEntityIds executes queries that are shard specific. func (stc *ScatterConn) ExecuteEntityIds( context context.Context, shards []string, @@ -290,12 +291,21 @@ func (stc *ScatterConn) StreamExecute( return errFunc() }) var replyErr error + fieldSent := false for innerqr := range results { // We still need to finish pumping if replyErr != nil { continue } - replyErr = sendReply(innerqr.(*mproto.QueryResult)) + mqr := innerqr.(*mproto.QueryResult) + // only send field info once for scattered streaming + if len(mqr.Fields) > 0 && len(mqr.Rows) == 0 { + if fieldSent { + continue + } + fieldSent = true + } + replyErr = sendReply(mqr) } if replyErr != nil { allErrors.RecordError(replyErr) @@ -332,12 +342,21 @@ func (stc *ScatterConn) StreamExecuteMulti( return errFunc() }) var replyErr error + fieldSent := false for innerqr := range results { // We still need to finish pumping if replyErr != nil { continue } - replyErr = sendReply(innerqr.(*mproto.QueryResult)) + mqr := innerqr.(*mproto.QueryResult) + // only send field info once for scattered streaming + if len(mqr.Fields) > 0 && len(mqr.Rows) == 0 { + if fieldSent { + continue + } + fieldSent = true + } + replyErr = sendReply(mqr) } if replyErr != nil { allErrors.RecordError(replyErr) @@ -347,6 +366,9 @@ func (stc *ScatterConn) StreamExecuteMulti( // Commit commits the current transaction. There are no retries on this operation. func (stc *ScatterConn) Commit(context context.Context, session *SafeSession) (err error) { + if session == nil { + return fmt.Errorf("cannot commit: empty session") + } if !session.InTransaction() { return fmt.Errorf("cannot commit: not in transaction") } @@ -367,6 +389,9 @@ func (stc *ScatterConn) Commit(context context.Context, session *SafeSession) (e // Rollback rolls back the current transaction. There are no retries on this operation. func (stc *ScatterConn) Rollback(context context.Context, session *SafeSession) (err error) { + if session == nil { + return nil + } for _, shardSession := range session.ShardSessions { sdc := stc.getConnection(context, shardSession.Keyspace, shardSession.Shard, shardSession.TabletType) sdc.Rollback(context, shardSession.TransactionId) @@ -491,12 +516,12 @@ func (stc *ScatterConn) multiGo( defer stc.timings.Record([]string{name, keyspace, shard, string(tabletType)}, startTime) sdc := stc.getConnection(context, keyspace, shard, tabletType) - transactionId, err := stc.updateSession(context, sdc, keyspace, shard, tabletType, session) + transactionID, err := stc.updateSession(context, sdc, keyspace, shard, tabletType, session) if err != nil { allErrors.RecordError(err) return } - err = action(sdc, transactionId, results) + err = action(sdc, transactionID, results) if err != nil { allErrors.RecordError(err) return @@ -540,7 +565,7 @@ func (stc *ScatterConn) updateSession( keyspace, shard string, tabletType topo.TabletType, session *SafeSession, -) (transactionId int64, err error) { +) (transactionID int64, err error) { if !session.InTransaction() { return 0, nil } @@ -548,11 +573,11 @@ func (stc *ScatterConn) updateSession( // Find and Append. The higher level functions ensure that no // duplicate (keyspace, shard, tabletType) tuples can execute // this at the same time. - transactionId = session.Find(keyspace, shard, tabletType) - if transactionId != 0 { - return transactionId, nil + transactionID = session.Find(keyspace, shard, tabletType) + if transactionID != 0 { + return transactionID, nil } - transactionId, err = sdc.Begin(context) + transactionID, err = sdc.Begin(context) if err != nil { return 0, err } @@ -560,9 +585,9 @@ func (stc *ScatterConn) updateSession( Keyspace: keyspace, TabletType: tabletType, Shard: shard, - TransactionId: transactionId, + TransactionId: transactionID, }) - return transactionId, nil + return transactionID, nil } func getShards(shardVars map[string]map[string]interface{}) []string { diff --git a/go/vt/vtgate/scatter_conn_test.go b/go/vt/vtgate/scatter_conn_test.go index ffc23a22f7e..f4519b2f402 100644 --- a/go/vt/vtgate/scatter_conn_test.go +++ b/go/vt/vtgate/scatter_conn_test.go @@ -12,9 +12,9 @@ import ( mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/sqltypes" - "github.com/youtube/vitess/go/vt/context" tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" ) // This file uses the sandbox_test framework. @@ -22,7 +22,7 @@ import ( func TestScatterConnExecute(t *testing.T) { testScatterConnGeneric(t, "TestScatterConnExecute", func(shards []string) (*mproto.QueryResult, error) { stc := NewScatterConn(new(sandboxTopo), "", "aa", 1*time.Millisecond, 3, 1*time.Millisecond) - return stc.Execute(&context.DummyContext{}, "query", nil, "TestScatterConnExecute", shards, "", nil) + return stc.Execute(context.Background(), "query", nil, "TestScatterConnExecute", shards, "", nil) }) } @@ -33,7 +33,7 @@ func TestScatterConnExecuteMulti(t *testing.T) { for _, shard := range shards { shardVars[shard] = nil } - return stc.ExecuteMulti(&context.DummyContext{}, "query", "TestScatterConnExecute", shardVars, "", nil) + return stc.ExecuteMulti(context.Background(), "query", "TestScatterConnExecute", shardVars, "", nil) }) } @@ -41,7 +41,7 @@ func TestScatterConnExecuteBatch(t *testing.T) { testScatterConnGeneric(t, "TestScatterConnExecuteBatch", func(shards []string) (*mproto.QueryResult, error) { stc := NewScatterConn(new(sandboxTopo), "", "aa", 1*time.Millisecond, 3, 1*time.Millisecond) queries := []tproto.BoundQuery{{"query", nil}} - qrs, err := stc.ExecuteBatch(&context.DummyContext{}, queries, "TestScatterConnExecuteBatch", shards, "", nil) + qrs, err := stc.ExecuteBatch(context.Background(), queries, "TestScatterConnExecuteBatch", shards, "", nil) if err != nil { return nil, err } @@ -53,7 +53,7 @@ func TestScatterConnStreamExecute(t *testing.T) { testScatterConnGeneric(t, "TestScatterConnStreamExecute", func(shards []string) (*mproto.QueryResult, error) { stc := NewScatterConn(new(sandboxTopo), "", "aa", 1*time.Millisecond, 3, 1*time.Millisecond) qr := new(mproto.QueryResult) - err := stc.StreamExecute(&context.DummyContext{}, "query", nil, "TestScatterConnStreamExecute", shards, "", nil, func(r *mproto.QueryResult) error { + err := stc.StreamExecute(context.Background(), "query", nil, "TestScatterConnStreamExecute", shards, "", nil, func(r *mproto.QueryResult) error { appendResult(qr, r) return nil }) @@ -69,7 +69,7 @@ func TestScatterConnStreamExecuteMulti(t *testing.T) { for _, shard := range shards { shardVars[shard] = nil } - err := stc.StreamExecuteMulti(&context.DummyContext{}, "query", "TestScatterConnStreamExecute", shardVars, "", nil, func(r *mproto.QueryResult) error { + err := stc.StreamExecuteMulti(context.Background(), "query", "TestScatterConnStreamExecute", shardVars, "", nil, func(r *mproto.QueryResult) error { appendResult(qr, r) return nil }) @@ -173,23 +173,23 @@ func TestMultiExecs(t *testing.T) { "bv1": 1, }, } - _, _ = stc.ExecuteMulti(&context.DummyContext{}, "query", "TestMultiExecs", shardVars, "", nil) - if sbc0.BindVars[0] != "bv0" { - t.Errorf("got %s, want bv0", sbc0.BindVars[0]) + _, _ = stc.ExecuteMulti(context.Background(), "query", "TestMultiExecs", shardVars, "", nil) + if !reflect.DeepEqual(sbc0.Queries[0].BindVariables, shardVars["0"]) { + t.Errorf("got %+v, want %+v", sbc0.Queries[0].BindVariables, shardVars["0"]) } - if sbc1.BindVars[0] != "bv1" { - t.Errorf("got %s, want bv0", sbc1.BindVars[0]) + if !reflect.DeepEqual(sbc1.Queries[0].BindVariables, shardVars["1"]) { + t.Errorf("got %+v, want %+v", sbc0.Queries[0].BindVariables, shardVars["1"]) } - sbc0.BindVars = nil - sbc1.BindVars = nil - _ = stc.StreamExecuteMulti(&context.DummyContext{}, "query", "TestMultiExecs", shardVars, "", nil, func(*mproto.QueryResult) error { + sbc0.Queries = nil + sbc1.Queries = nil + _ = stc.StreamExecuteMulti(context.Background(), "query", "TestMultiExecs", shardVars, "", nil, func(*mproto.QueryResult) error { return nil }) - if sbc0.BindVars[0] != "bv0" { - t.Errorf("got %s, want bv0", sbc0.BindVars[0]) + if !reflect.DeepEqual(sbc0.Queries[0].BindVariables, shardVars["0"]) { + t.Errorf("got %+v, want %+v", sbc0.Queries[0].BindVariables, shardVars["0"]) } - if sbc1.BindVars[0] != "bv1" { - t.Errorf("got %s, want bv0", sbc1.BindVars[0]) + if !reflect.DeepEqual(sbc1.Queries[0].BindVariables, shardVars["1"]) { + t.Errorf("got %+v, want %+v", sbc0.Queries[0].BindVariables, shardVars["1"]) } } @@ -198,7 +198,7 @@ func TestScatterConnStreamExecuteSendError(t *testing.T) { sbc := &sandboxConn{} s.MapTestConn("0", sbc) stc := NewScatterConn(new(sandboxTopo), "", "aa", 1*time.Millisecond, 3, 1*time.Millisecond) - err := stc.StreamExecute(&context.DummyContext{}, "query", nil, "TestScatterConnStreamExecuteSendError", []string{"0"}, "", nil, func(*mproto.QueryResult) error { + err := stc.StreamExecute(context.Background(), "query", nil, "TestScatterConnStreamExecuteSendError", []string{"0"}, "", nil, func(*mproto.QueryResult) error { return fmt.Errorf("send error") }) want := "send error" @@ -208,6 +208,29 @@ func TestScatterConnStreamExecuteSendError(t *testing.T) { } } +func TestScatterCommitRollbackIncorrectSession(t *testing.T) { + s := createSandbox("TestScatterCommitRollbackIncorrectSession") + sbc0 := &sandboxConn{} + s.MapTestConn("0", sbc0) + stc := NewScatterConn(new(sandboxTopo), "", "aa", 1*time.Millisecond, 3, 1*time.Millisecond) + + // nil session + err := stc.Commit(context.Background(), nil) + if err == nil { + t.Errorf("want error, got nil") + } + err = stc.Rollback(context.Background(), nil) + if err != nil { + t.Errorf("want nil, got %v", err) + } + // not in transaction + session := NewSafeSession(&proto.Session{}) + err = stc.Commit(context.Background(), session) + if err == nil { + t.Errorf("want error, got nil") + } +} + func TestScatterConnCommitSuccess(t *testing.T) { s := createSandbox("TestScatterConnCommitSuccess") sbc0 := &sandboxConn{} @@ -218,7 +241,7 @@ func TestScatterConnCommitSuccess(t *testing.T) { // Sequence the executes to ensure commit order session := NewSafeSession(&proto.Session{InTransaction: true}) - stc.Execute(&context.DummyContext{}, "query1", nil, "TestScatterConnCommitSuccess", []string{"0"}, "", session) + stc.Execute(context.Background(), "query1", nil, "TestScatterConnCommitSuccess", []string{"0"}, "", session) wantSession := proto.Session{ InTransaction: true, ShardSessions: []*proto.ShardSession{{ @@ -231,7 +254,7 @@ func TestScatterConnCommitSuccess(t *testing.T) { if !reflect.DeepEqual(wantSession, *session.Session) { t.Errorf("want\n%+v, got\n%+v", wantSession, *session.Session) } - stc.Execute(&context.DummyContext{}, "query1", nil, "TestScatterConnCommitSuccess", []string{"0", "1"}, "", session) + stc.Execute(context.Background(), "query1", nil, "TestScatterConnCommitSuccess", []string{"0", "1"}, "", session) wantSession = proto.Session{ InTransaction: true, ShardSessions: []*proto.ShardSession{{ @@ -250,7 +273,7 @@ func TestScatterConnCommitSuccess(t *testing.T) { t.Errorf("want\n%+v, got\n%+v", wantSession, *session.Session) } sbc0.mustFailServer = 1 - err := stc.Commit(&context.DummyContext{}, session) + err := stc.Commit(context.Background(), session) if err == nil { t.Errorf("want error, got nil") } @@ -276,9 +299,9 @@ func TestScatterConnRollback(t *testing.T) { // Sequence the executes to ensure commit order session := NewSafeSession(&proto.Session{InTransaction: true}) - stc.Execute(&context.DummyContext{}, "query1", nil, "TestScatterConnRollback", []string{"0"}, "", session) - stc.Execute(&context.DummyContext{}, "query1", nil, "TestScatterConnRollback", []string{"0", "1"}, "", session) - err := stc.Rollback(&context.DummyContext{}, session) + stc.Execute(context.Background(), "query1", nil, "TestScatterConnRollback", []string{"0"}, "", session) + stc.Execute(context.Background(), "query1", nil, "TestScatterConnRollback", []string{"0", "1"}, "", session) + err := stc.Rollback(context.Background(), session) if err != nil { t.Errorf("want nil, got %v", err) } @@ -299,7 +322,7 @@ func TestScatterConnClose(t *testing.T) { sbc := &sandboxConn{} s.MapTestConn("0", sbc) stc := NewScatterConn(new(sandboxTopo), "", "aa", 1*time.Millisecond, 3, 1*time.Millisecond) - stc.Execute(&context.DummyContext{}, "query1", nil, "TestScatterConnClose", []string{"0"}, "", nil) + stc.Execute(context.Background(), "query1", nil, "TestScatterConnClose", []string{"0"}, "", nil) stc.Close() if sbc.CloseCount != 1 { t.Errorf("want 1, got %d", sbc.CloseCount) diff --git a/go/vt/vtgate/schema_editor/app.js b/go/vt/vtgate/schema_editor/app.js new file mode 100644 index 00000000000..8fddb838db2 --- /dev/null +++ b/go/vt/vtgate/schema_editor/app.js @@ -0,0 +1,39 @@ +/** + * Copyright 2014, Google Inc. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ +'use strict'; + +angular.module('app', ['ngRoute']) +.factory('vindexInfo', vindexInfo) +.factory('curSchema', curSchema) +.controller('KeyspaceController', KeyspaceController) +.controller('SidebarController', SidebarController) +.controller('ClassController', ClassController) +.controller('LoadController', LoadController) +.controller('SubmitController', SubmitController) +.config(['$routeProvider', function($routeProvider) { + $routeProvider + .when('/',{ + templateUrl: "editor/keyspace.html", + controller: "KeyspaceController" + }) + .when('/editor/:keyspaceName',{ + templateUrl: "editor/keyspace.html", + controller: "KeyspaceController" + }) + .when('/editor/:keyspaceName/class/:className',{ + templateUrl: "editor/class/class.html", + controller: "ClassController" + }) + .when('/load',{ + templateUrl: "load/load.html", + controller: "LoadController" + }) + .when('/submit',{ + templateUrl: "submit/submit.html", + controller: "SubmitController" + }) + .otherwise({redirectTo: '/'}); +}]); \ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/curschema.js b/go/vt/vtgate/schema_editor/curschema.js new file mode 100644 index 00000000000..377bffb1114 --- /dev/null +++ b/go/vt/vtgate/schema_editor/curschema.js @@ -0,0 +1,216 @@ +/** + * Copyright 2014, Google Inc. All rights reserved. Use of this source code is + * governed by a BSD-style license that can be found in the LICENSE file. + */ +'use strict'; + +function curSchema(vindexInfo) { + var data = {}; + + data.reset = function() { + data.keyspaces = copyKeyspaces(data.original, vindexInfo); + data.tables = computeTables(data.keyspaces); + }; + + data.deleteKeyspace = function(keyspaceName) { + delete data.keyspaces[keyspaceName]; + data.tables = computeTables(data.keyspaces); + }; + + data.addTable = function(keyspaceName, tableName, className) { + data.keyspaces[keyspaceName].Tables[tableName] = className; + data.tables = computeTables(data.keyspaces); + }; + + data.deleteTable = function(keyspaceName, tableName) { + delete data.keyspaces[keyspaceName].Tables[tableName]; + data.tables = computeTables(data.keyspaces); + }; + + data.validClasses = function(keyspace, tableName) { + var valid = []; + if (!keyspace) { + return []; + } + for ( var className in keyspace.Classes) { + if (data.classHasError(keyspace, tableName, className)) { + continue; + } + valid.push(className); + } + return valid; + }; + + data.classHasError = function(keyspace, tableName, className) { + if (!(className in keyspace.Classes)) { + return "class not found"; + } + var klass = keyspace.Classes[className]; + for (var i = 0; i < klass.ColVindexes.length; i++) { + var classError = data.vindexHasError(keyspace, className, i); + if (classError) { + return "invalid class"; + } + var vindex = keyspace.Vindexes[klass.ColVindexes[i].Name]; + if (vindex.Owner != tableName) { + continue; + } + if (i == 0) { + if (vindexInfo.Types[vindex.Type].Type != "functional") { + return "owned primary vindex must be functional"; + } + } else { + if (vindexInfo.Types[vindex.Type].Type != "lookup") { + return "owned non-primary vindex must be lookup"; + } + } + } + return ""; + }; + + data.validVindexes = function(keyspace, className, index) { + var valid = []; + for ( var vindexName in keyspace.Vindexes) { + // Duplicated from vindexHasError. + if (index == 0) { + var vindexTypeName = keyspace.Vindexes[vindexName].Type; + if (!vindexInfo.Types[vindexTypeName].Unique) { + continue; + } + } + valid.push(vindexName); + } + return valid; + }; + + data.vindexHasError = function(keyspace, className, index) { + var vindexName = keyspace.Classes[className].ColVindexes[index].Name; + if (!(vindexName in keyspace.Vindexes)) { + return "vindex not found"; + } + if (index == 0) { + var vindexTypeName = keyspace.Vindexes[vindexName].Type; + if (!vindexInfo.Types[vindexTypeName].Unique) { + return "primary vindex must be unique"; + } + } + return ""; + }; + + data.init = function(original) { + data.original = original; + data.reset(); + }; + + data.init({}); + return data; +} + +function SetSharded(keyspace, sharded) { + if (sharded) { + keyspace.Sharded = true; + if (!keyspace["Classes"]) { + keyspace.Classes = {}; + } + if (!keyspace["Vindexes"]) { + keyspace.Vindexes = {}; + } + } else { + keyspace.Sharded = false; + for ( var tableName in keyspace.Tables) { + keyspace.Tables[tableName] = ""; + } + delete keyspace["Classes"]; + delete keyspace["Vindexes"]; + } +}; + +function AddKeyspace(keyspaces, keyspaceName, sharded) { + var keyspace = {}; + keyspace.Tables = {}; + SetSharded(keyspace, sharded); + keyspaces[keyspaceName] = keyspace; +}; + +function CopyParams(original, type, vindexInfo) { + var params = {}; + var vparams = vindexInfo.Types[type].Params; + for (var i = 0; i < vparams.length; i++) { + params[vparams[i]] = original[vparams[i]]; + } + return params; +} + +function copyKeyspaces(original, vindexInfo) { + var copied = {}; + for ( var key in original) { + copied[key] = {}; + var keyspace = copied[key]; + if (original[key].Sharded) { + keyspace.Sharded = true; + keyspace.Vindexes = copyVindexes(original[key].Vindexes, vindexInfo); + keyspace.Classes = copyClasses(original[key].Classes); + keyspace.Tables = copyTables(original[key].Tables); + } else { + keyspace.Sharded = false; + keyspace.Tables = {}; + for (key in original[key].Tables) { + keyspace.Tables[key] = ""; + } + } + } + return copied; +} + +function copyVindexes(original, vindexInfo) { + var copied = {}; + for ( var key in original) { + if (!vindexInfo.Types[original[key].Type]) { + continue; + } + copied[key] = {}; + var vindex = copied[key]; + vindex.Type = original[key].Type; + vindex.Owner = original[key].Owner; + vindex.Params = CopyParams(original[key].Params, original[key].Type, vindexInfo); + } + return copied; +} + +function copyClasses(original) { + var copied = {}; + for ( var key in original) { + copied[key] = {"ColVindexes": []}; + for (var i = 0; i < original[key].ColVindexes.length; i++) { + copied[key].ColVindexes.push({ + "Col": original[key].ColVindexes[i].Col, + "Name": original[key].ColVindexes[i].Name + }); + } + } + return copied; +} + +function copyTables(original) { + var copied = {}; + for ( var key in original) { + copied[key] = original[key]; + } + return copied; +} + +function computeTables(keyspaces) { + var tables = {}; + for ( var ks in keyspaces) { + for ( var table in keyspaces[ks].Tables) { + if (table in tables) { + tables[table].push(ks); + } else { + tables[table] = [ + ks + ]; + } + } + } + return tables; +} diff --git a/go/vt/vtgate/schema_editor/editor/class/class.html b/go/vt/vtgate/schema_editor/editor/class/class.html new file mode 100644 index 00000000000..2d797ba4fe9 --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/class/class.html @@ -0,0 +1,75 @@ +
+
+
+
+
+

Class: + {{keyspaceName}}.{{className}}

+
+
+ + + + + + + + + + + + +
{{colVindex.Col}} +
(empty)
+
+ + +
+
+ +
+ + +
+
+
+
+ {{classEditor.err}} + +
+
+ +
+
+
+
diff --git a/go/vt/vtgate/schema_editor/editor/class/class.js b/go/vt/vtgate/schema_editor/editor/class/class.js new file mode 100644 index 00000000000..a81cee5f55b --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/class/class.js @@ -0,0 +1,66 @@ +/** + * Copyright 2014, Google Inc. All rights reserved. Use of this source code is + * governed by a BSD-style license that can be found in the LICENSE file. + */ +'use strict'; + +function ClassController($scope, $routeParams, vindexInfo, curSchema) { + init(); + + function init() { + $scope.curSchema = curSchema; + $scope.vindexInfo = vindexInfo; + if (!$routeParams.keyspaceName || !curSchema.keyspaces[$routeParams.keyspaceName]) { + return; + } + $scope.keyspaceName = $routeParams.keyspaceName; + $scope.keyspace = curSchema.keyspaces[$routeParams.keyspaceName]; + if (!$routeParams.className || !$scope.keyspace.Classes[$routeParams.className]) { + return; + } + $scope.className = $routeParams.className; + $scope.klass = $scope.keyspace.Classes[$routeParams.className]; + $scope.classEditor = {}; + } + + $scope.setName = function($colVindex, $vindex) { + $colVindex.Name = $vindex; + $scope.clearClassError(); + }; + + $scope.addColVindex = function($colName, $vindex) { + if (!$colName) { + $scope.classEditor.err = "empty column name"; + return; + } + for (var i = 0; i < $scope.klass.length; i++) { + if ($colName == $scope.klass[i].Col) { + $scope.classEditor.err = $colName + " already exists"; + return; + } + } + ; + $scope.klass.ColVindexes.push({ + "Col": $colName, + "Name": $vindex + }); + $scope.clearClassError(); + }; + + $scope.deleteColVindex = function(index) { + $scope.klass.ColVindexes.splice(index, 1); + $scope.clearClassError(); + }; + + $scope.clearClassError = function() { + $scope.classEditor.err = ""; + }; + + $scope.validVindexes = function($className, $index) { + return curSchema.validVindexes($scope.keyspace, $className, $index); + }; + + $scope.vindexHasError = function($className, $index) { + return curSchema.vindexHasError($scope.keyspace, $className, $index); + }; +} diff --git a/go/vt/vtgate/schema_editor/editor/classes.html b/go/vt/vtgate/schema_editor/editor/classes.html new file mode 100644 index 00000000000..c67758c1303 --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/classes.html @@ -0,0 +1,60 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassColumnVindex
{{className}}
(empty)
+
{{className}} + {{colVindex.Col}} +
(empty)
+
{{colVindex.Name}} + +
+
+ +
+
+
+ {{classesEditor.err}} + +
+
+
\ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/editor/keyspace.html b/go/vt/vtgate/schema_editor/editor/keyspace.html new file mode 100644 index 00000000000..363167025e3 --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/keyspace.html @@ -0,0 +1,33 @@ +
+
+
+
+

+ Keyspace: {{keyspaceName}} + + + + + + + +

+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/editor/keyspace.js b/go/vt/vtgate/schema_editor/editor/keyspace.js new file mode 100644 index 00000000000..b37e78d8d55 --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/keyspace.js @@ -0,0 +1,158 @@ +/** + * Copyright 2014, Google Inc. All rights reserved. Use of this source code is + * governed by a BSD-style license that can be found in the LICENSE file. + */ +'use strict'; + +function KeyspaceController($scope, $routeParams, vindexInfo, curSchema) { + init(); + + function init() { + $scope.curSchema = curSchema; + $scope.vindexInfo = vindexInfo; + if (!$routeParams.keyspaceName || !curSchema.keyspaces[$routeParams.keyspaceName]) { + return; + } + $scope.keyspaceName = $routeParams.keyspaceName; + $scope.keyspace = curSchema.keyspaces[$routeParams.keyspaceName]; + if ($scope.keyspace.Sharded) { + $scope.vindexNames = Object.keys($scope.keyspace.Vindexes); + } + $scope.tablesEditor = {}; + $scope.classesEditor = {}; + $scope.vindexEditor = {}; + } + + $scope.setSharded = function($sharded) { + SetSharded($scope.keyspace, $sharded); + }; + + $scope.deleteKeyspace = function($keyspaceName) { + curSchema.deleteKeyspace($keyspaceName); + }; + + $scope.tableHasError = function($tableName) { + var table = curSchema.tables[$tableName]; + if (table && table.length > 1) { + return $tableName + " duplicated in " + curSchema.tables[$tableName]; + } + }; + + $scope.addTable = function($tableName, $className) { + if (!$tableName) { + $scope.tablesEditor.err = "empty table name"; + return; + } + if ($tableName in curSchema.tables) { + $scope.tablesEditor.err = $tableName + " already exists in " + curSchema.tables[$tableName]; + return; + + } + curSchema.addTable($scope.keyspaceName, $tableName, $className); + $scope.tablesEditor.newTableName = ""; + $scope.clearTableError(); + }; + + $scope.setTableClass = function($tableName, $className) { + $scope.keyspace.Tables[$tableName] = $className; + $scope.clearTableError(); + }; + + $scope.deleteTable = function($tableName) { + curSchema.deleteTable($scope.keyspaceName, $tableName); + $scope.clearTableError(); + }; + + $scope.validClasses = function($tableName) { + return curSchema.validClasses($scope.keyspace, $tableName); + }; + + $scope.classHasError = function($tableName, $className) { + return curSchema.classHasError($scope.keyspace, $tableName, $className); + }; + + $scope.clearTableError = function() { + $scope.tablesEditor.err = ""; + }; + + $scope.addClass = function($className) { + if (!$className) { + $scope.classesEditor.err = "empty class name"; + return; + } + if ($className in $scope.keyspace.Classes) { + $scope.classesEditor.err = $className + " already exists"; + return; + } + $scope.keyspace.Classes[$className] = {"ColVindexes": []}; + $scope.classesEditor.newClassName = ""; + $scope.clearClassesError(); + window.location.href = "#/editor/" + $scope.keyspaceName + "/class/" + $className; + }; + + $scope.deleteClass = function($className) { + delete $scope.keyspace.Classes[$className]; + $scope.clearClassesError(); + }; + + $scope.clearClassesError = function() { + $scope.classesEditor.err = ""; + }; + + $scope.vindexHasError = function($className, $index) { + return curSchema.vindexHasError($scope.keyspace, $className, $index); + }; + + $scope.setVindexType = function($vindex, $vindexType) { + $vindex.Type = $vindexType; + $vindex.Params = CopyParams($vindex.Params, $vindexType, vindexInfo); + $scope.clearVindexError(); + }; + + $scope.deleteVindex = function($vindexName) { + delete $scope.keyspace.Vindexes[$vindexName]; + $scope.clearVindexError(); + }; + + $scope.ownerHasWarning = function($owner, $vindexName) { + if (!$owner) { + return ""; + } + var className = $scope.keyspace.Tables[$owner]; + if (!className) { + return "table not found"; + } + var klass = $scope.keyspace.Classes[className]; + if (!klass) { + return "table has invalid class"; + } + for (var i = 0; i < klass.length; i++) { + if (klass[i].Name == $vindexName) { + return ""; + } + } + return "table does not contain this index"; + }; + + $scope.addVindex = function($vindexName, $vindexType) { + if (!$vindexName) { + $scope.vindexEditor.err = "empty vindex name"; + return; + } + if ($vindexName in $scope.keyspace.Vindexes) { + $scope.vindexEditor.err = $vindexName + " already exists"; + return; + } + var newVindex = { + "Params": {} + }; + $scope.setVindexType(newVindex, $vindexType); + $scope.keyspace.Vindexes[$vindexName] = newVindex; + $scope.vindexEditor.vindexName = ""; + $scope.clearVindexError(); + }; + + $scope.clearVindexError = function() { + $scope.vindexEditor.err = ""; + }; +} diff --git a/go/vt/vtgate/schema_editor/editor/sidebar.html b/go/vt/vtgate/schema_editor/editor/sidebar.html new file mode 100644 index 00000000000..b9a215fc20b --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/sidebar.html @@ -0,0 +1,41 @@ + diff --git a/go/vt/vtgate/schema_editor/editor/sidebar.js b/go/vt/vtgate/schema_editor/editor/sidebar.js new file mode 100644 index 00000000000..55f9c58de09 --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/sidebar.js @@ -0,0 +1,36 @@ +/** + * Copyright 2014, Google Inc. All rights reserved. Use of this source code is + * governed by a BSD-style license that can be found in the LICENSE file. + */ +'use strict'; + +function SidebarController($scope, $routeParams, curSchema) { + init(); + + function init() { + $scope.curSchema = curSchema; + $scope.keyspaceEditor = {}; + } + $scope.addKeyspace = function($keyspaceName, $sharded) { + if (!$keyspaceName) { + $scope.keyspaceEditor.err = "empty keyspace name"; + return; + } + if ($keyspaceName in curSchema.keyspaces) { + $scope.keyspaceEditor.err = $keyspaceName + " already exists"; + return; + } + AddKeyspace(curSchema.keyspaces, $keyspaceName, $sharded); + $scope.clearKeyspaceError(); + window.location.href = "#/editor/" + $keyspaceName; + }; + + $scope.reset = function() { + curSchema.reset(); + $scope.clearKeyspaceError(); + }; + + $scope.clearKeyspaceError = function() { + $scope.keyspaceEditor.err = ""; + }; +} \ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/editor/tables.html b/go/vt/vtgate/schema_editor/editor/tables.html new file mode 100644 index 00000000000..5dd1dc0598b --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/tables.html @@ -0,0 +1,63 @@ +
+ + + + + + + + + + + + + + + + + + +
TableClass
{{tableName}} + + + +
+
+ +
+ + +
+
+
+
+ {{tablesEditor.err}} + +
+
+
\ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/editor/unsharded_tables.html b/go/vt/vtgate/schema_editor/editor/unsharded_tables.html new file mode 100644 index 00000000000..e2976a64886 --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/unsharded_tables.html @@ -0,0 +1,41 @@ +
+ + + + + + + + + + + + + + + + +
Table
{{tableName}} +
+
+ +
+ +
+
+
+
+ {{tablesEditor.err}} + +
+
+
\ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/editor/vindexes.html b/go/vt/vtgate/schema_editor/editor/vindexes.html new file mode 100644 index 00000000000..d4e351c3a6d --- /dev/null +++ b/go/vt/vtgate/schema_editor/editor/vindexes.html @@ -0,0 +1,72 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
VindexTypeOwnerParams
{{vindexName}} + + + + + + +
{{paramName}}
+
+
+ +
+ + +
+
+
+
+ {{vindexEditor.err}} + +
+
+
\ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/index.html b/go/vt/vtgate/schema_editor/index.html new file mode 100644 index 00000000000..8073632550c --- /dev/null +++ b/go/vt/vtgate/schema_editor/index.html @@ -0,0 +1,78 @@ + + + + + + + + +VTGate schema editor + + + +
+ + + + + + + + + + + + + + diff --git a/go/vt/vtgate/schema_editor/load/load.html b/go/vt/vtgate/schema_editor/load/load.html new file mode 100644 index 00000000000..b65b591be4f --- /dev/null +++ b/go/vt/vtgate/schema_editor/load/load.html @@ -0,0 +1,21 @@ +
+
+
+
+ + +
+
+ {{loader.err}} + +
+
+

Loaded data

+
{{loadedJSON}}
+
+
+
\ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/load/load.js b/go/vt/vtgate/schema_editor/load/load.js new file mode 100644 index 00000000000..8b709e07b47 --- /dev/null +++ b/go/vt/vtgate/schema_editor/load/load.js @@ -0,0 +1,153 @@ +/** + * Copyright 2015, Google Inc. All rights reserved. Use of this source code is + * governed by a BSD-style license that can be found in the LICENSE file. + */ +'use strict'; + +function LoadController($scope, $http, curSchema) { + init(); + + function init() { + $scope.loader = {}; + } + + function setLoaded(loaded) { + $scope.loadedJSON = angular.toJson(loaded, true); + curSchema.init(loaded.Keyspaces); + $scope.clearLoaderError(); + } + + $scope.loadFromTopo = function() { + $http.get("/vschema").success(function(data, status, headers, config) { + try { + var parser = new DOMParser(); + var xmlDoc = parser.parseFromString(data, "text/xml"); + var err = xmlDoc.getElementById("err"); + if (err) { + $scope.loader.err = err.innerHTML; + return; + } + var initialJSON = xmlDoc.getElementById("vschema").innerHTML; + setLoaded(angular.fromJson(initialJSON)); + } catch (err) { + $scope.loader.err = err.message; + } + }).error(function(data, status, headers, config) { + $scope.loader.err = data; + }); + }; + + $scope.loadTestData = function() { + var testData = { + "user": { + "Sharded": true, + "Vindexes": { + "user_index": { + "Type": "hash_autoinc", + "Owner": "user", + "Params": { + "Table": "user_lookup", + "Column": "user_id" + } + }, + "music_user_map": { + "Type": "lookup_hash_unique_autoinc", + "Owner": "music_extra", + "Params": { + "Table": "music_user_map", + "From": "music_id", + "To": "user_id" + } + }, + "name_user_map": { + "Type": "lookup_hash", + "Owner": "user", + "Params": { + "Table": "name_user_map", + "From": "name", + "To": "user_id" + } + }, + "user_extra_index": { + "Type": "hash", + "Owner": "user_extra", + "Params": { + "Table": "user_extra_lookup", + "Column": "user_extra_id" + } + } + }, + "Classes": { + "user": { + "ColVindexes": [ + { + "Col": "id", + "Name": "user_index" + }, { + "Col": "", + "Name": "name_user_map" + }, { + "Col": "third", + "Name": "name_user_map" + } + ] + }, + "user_extra": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "user_index" + }, { + "Col": "id", + "Name": "user_extra_index" + } + ] + }, + "music": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "name_user_map" + }, { + "Col": "id", + "Name": "user_index" + } + ] + }, + "music_extra": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "music_user_map" + }, { + "Col": "music_id", + "Name": "user_index1" + } + ] + } + }, + "Tables": { + "user": "aa", + "user_extra": "user_extra", + "music": "music", + "music_extra": "music_extra", + "very_very_long_name": "music_extra" + } + }, + "main": { + "Tables": { + "main1": "aa", + "main2": "", + "music_extra": "" + } + } + }; + setLoaded({ + "Keyspaces": testData + }); + }; + + $scope.clearLoaderError = function() { + $scope.loader.err = ""; + }; +} \ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/submit/submit.html b/go/vt/vtgate/schema_editor/submit/submit.html new file mode 100644 index 00000000000..62b94a3d689 --- /dev/null +++ b/go/vt/vtgate/schema_editor/submit/submit.html @@ -0,0 +1,29 @@ +
+
+
+
+ +
+
+ {{submitter.err}} + +
+
+ {{submitter.ok}} + +
+
+

Original

+
{{originalJSON}}
+
+
+

Edited

+
{{keyspacesJSON}}
+
+
+
\ No newline at end of file diff --git a/go/vt/vtgate/schema_editor/submit/submit.js b/go/vt/vtgate/schema_editor/submit/submit.js new file mode 100644 index 00000000000..342583b594a --- /dev/null +++ b/go/vt/vtgate/schema_editor/submit/submit.js @@ -0,0 +1,48 @@ +/** + * Copyright 2014, Google Inc. All rights reserved. Use of this source code is + * governed by a BSD-style license that can be found in the LICENSE file. + */ +'use strict'; + +function SubmitController($scope, $http, curSchema) { + init(); + + function init() { + $scope.keyspacesJSON = angular.toJson({ + "Keyspaces": curSchema.keyspaces + }, true); + $scope.originalJSON = angular.toJson({ + "Keyspaces": curSchema.original + }, true); + $scope.submitter = {}; + } + + $scope.submitToTopo = function() { + try { + $http({ + method: 'POST', + url: '/vschema', + data: "vschema=" + $scope.keyspacesJSON, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }).success(function(data, status, headers, config) { + var parser = new DOMParser(); + var xmlDoc = parser.parseFromString(data, "text/xml"); + var err = xmlDoc.getElementById("err"); + if (err) { + $scope.submitter.err = err.innerHTML; + return; + } + $scope.submitter.ok = "Submitted!"; + }); + } catch (err) { + $scope.submitter.err = err.message; + } + }; + + $scope.clearSubmitter = function() { + $scope.submitter.err = ""; + $scope.submitter.ok = ""; + }; +} diff --git a/go/vt/vtgate/schema_editor/vindex_info.js b/go/vt/vtgate/schema_editor/vindex_info.js new file mode 100644 index 00000000000..52cc2954027 --- /dev/null +++ b/go/vt/vtgate/schema_editor/vindex_info.js @@ -0,0 +1,60 @@ +/** + * Copyright 2014, Google Inc. All rights reserved. Use of this source code is + * governed by a BSD-style license that can be found in the LICENSE file. + */ +'use strict'; + +function vindexInfo() { + var info = {}; + info.Types = { + "numeric": { + "Type": "functional", + "Unique": true, + "Params": [] + }, + "hash": { + "Type": "functional", + "Unique": true, + "Params": [ + "Table", "Column" + ] + }, + "hash_autoinc": { + "Type": "functional", + "Unique": true, + "Params": [ + "Table", "Column" + ] + }, + "lookup_hash": { + "Type": "lookup", + "Unique": false, + "Params": [ + "Table", "From", "To" + ] + }, + "lookup_hash_unique": { + "Type": "lookup", + "Unique": true, + "Params": [ + "Table", "From", "To" + ] + }, + "lookup_hash_autoinc": { + "Type": "lookup", + "Unique": false, + "Params": [ + "Table", "From", "To" + ] + }, + "lookup_hash_unique_autoinc": { + "Type": "lookup", + "Unique": true, + "Params": [ + "Table", "From", "To" + ] + } + }; + info.TypeNames = Object.keys(info.Types); + return info; +} \ No newline at end of file diff --git a/go/vt/vtgate/shard_conn.go b/go/vt/vtgate/shard_conn.go index abc0d5302da..281737127e0 100644 --- a/go/vt/vtgate/shard_conn.go +++ b/go/vt/vtgate/shard_conn.go @@ -9,11 +9,11 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" "github.com/youtube/vitess/go/vt/tabletserver/tabletconn" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // ShardConn represents a load balanced connection to a group @@ -281,8 +281,7 @@ func (sdc *ShardConn) markDown(conn tabletconn.TabletConn, reason string) { } sdc.balancer.MarkDown(conn.EndPoint().Uid, reason) - // Launch as goroutine so we don't block - go sdc.conn.Close() + sdc.conn.Close() sdc.conn = nil } diff --git a/go/vt/vtgate/shard_conn_test.go b/go/vt/vtgate/shard_conn_test.go index 8b118abd8b5..4fb8ec55e1f 100644 --- a/go/vt/vtgate/shard_conn_test.go +++ b/go/vt/vtgate/shard_conn_test.go @@ -9,72 +9,72 @@ import ( "testing" "time" - "github.com/youtube/vitess/go/vt/context" tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "golang.org/x/net/context" ) // This file uses the sandbox_test framework. func TestShardConnExecute(t *testing.T) { testShardConnGeneric(t, "TestShardConnExecute", func() error { - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnExecute", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) - _, err := sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnExecute", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) + _, err := sdc.Execute(context.Background(), "query", nil, 0) return err }) testShardConnTransact(t, "TestShardConnExecute", func() error { - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnExecute", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) - _, err := sdc.Execute(&context.DummyContext{}, "query", nil, 1) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnExecute", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) + _, err := sdc.Execute(context.Background(), "query", nil, 1) return err }) } func TestShardConnExecuteBatch(t *testing.T) { testShardConnGeneric(t, "TestShardConnExecuteBatch", func() error { - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnExecuteBatch", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnExecuteBatch", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) queries := []tproto.BoundQuery{{"query", nil}} - _, err := sdc.ExecuteBatch(&context.DummyContext{}, queries, 0) + _, err := sdc.ExecuteBatch(context.Background(), queries, 0) return err }) testShardConnTransact(t, "TestShardConnExecuteBatch", func() error { - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnExecuteBatch", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnExecuteBatch", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) queries := []tproto.BoundQuery{{"query", nil}} - _, err := sdc.ExecuteBatch(&context.DummyContext{}, queries, 1) + _, err := sdc.ExecuteBatch(context.Background(), queries, 1) return err }) } func TestShardConnExecuteStream(t *testing.T) { testShardConnGeneric(t, "TestShardConnExecuteStream", func() error { - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnExecuteStream", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) - _, errfunc := sdc.StreamExecute(&context.DummyContext{}, "query", nil, 0) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnExecuteStream", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) + _, errfunc := sdc.StreamExecute(context.Background(), "query", nil, 0) return errfunc() }) testShardConnTransact(t, "TestShardConnExecuteStream", func() error { - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnExecuteStream", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) - _, errfunc := sdc.StreamExecute(&context.DummyContext{}, "query", nil, 1) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnExecuteStream", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) + _, errfunc := sdc.StreamExecute(context.Background(), "query", nil, 1) return errfunc() }) } func TestShardConnBegin(t *testing.T) { testShardConnGeneric(t, "TestShardConnBegin", func() error { - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnBegin", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) - _, err := sdc.Begin(&context.DummyContext{}) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnBegin", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) + _, err := sdc.Begin(context.Background()) return err }) } func TestShardConnCommit(t *testing.T) { testShardConnTransact(t, "TestShardConnCommit", func() error { - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnCommit", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) - return sdc.Commit(&context.DummyContext{}, 1) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnCommit", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) + return sdc.Commit(context.Background(), 1) }) } func TestShardConnRollback(t *testing.T) { testShardConnTransact(t, "TestShardConnRollback", func() error { - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnRollback", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) - return sdc.Rollback(&context.DummyContext{}, 1) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnRollback", "0", "", 1*time.Millisecond, 3, 1*time.Millisecond) + return sdc.Rollback(context.Background(), 1) }) } @@ -244,9 +244,9 @@ func TestShardConnBeginOther(t *testing.T) { s := createSandbox("TestShardConnBeginOther") sbc := &sandboxConn{mustFailTxPool: 1} s.MapTestConn("0", sbc) - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnBeginOther", "0", "", 10*time.Millisecond, 3, 1*time.Millisecond) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnBeginOther", "0", "", 10*time.Millisecond, 3, 1*time.Millisecond) startTime := time.Now() - _, err := sdc.Begin(&context.DummyContext{}) + _, err := sdc.Begin(context.Background()) // If transaction pool is full, Begin should wait and retry. if time.Now().Sub(startTime) < (10 * time.Millisecond) { t.Errorf("want >10ms, got %v", time.Now().Sub(startTime)) @@ -269,8 +269,8 @@ func TestShardConnReconnect(t *testing.T) { retryCount := 5 s := createSandbox("TestShardConnReconnect") // case 1: resolved 0 endpoint, return error - sdc := NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) - _, err := sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc := NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + _, err := sdc.Execute(context.Background(), "query", nil, 0) if err == nil { t.Errorf("want error, got nil") } @@ -283,9 +283,9 @@ func TestShardConnReconnect(t *testing.T) { s.DialMustFail = 1 sbc := &sandboxConn{} s.MapTestConn("0", sbc) - sdc = NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + sdc = NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) timeStart := time.Now() - sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc.Execute(context.Background(), "query", nil, 0) timeDuration := time.Now().Sub(timeStart) if timeDuration < retryDelay { t.Errorf("want no spam delay %v, got %v", retryDelay, timeDuration) @@ -301,9 +301,9 @@ func TestShardConnReconnect(t *testing.T) { s.Reset() sbc = &sandboxConn{mustFailConn: 1} s.MapTestConn("0", sbc) - sdc = NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + sdc = NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) timeStart = time.Now() - sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc.Execute(context.Background(), "query", nil, 0) timeDuration = time.Now().Sub(timeStart) if timeDuration < retryDelay { t.Errorf("want no spam delay %v, got %v", retryDelay, timeDuration) @@ -324,9 +324,9 @@ func TestShardConnReconnect(t *testing.T) { s.MapTestConn("0", sbc0) s.MapTestConn("0", sbc1) s.MapTestConn("0", sbc2) - sdc = NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + sdc = NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) timeStart = time.Now() - sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc.Execute(context.Background(), "query", nil, 0) timeDuration = time.Now().Sub(timeStart) if timeDuration >= retryDelay { t.Errorf("want no delay, got %v", timeDuration) @@ -353,9 +353,9 @@ func TestShardConnReconnect(t *testing.T) { s.MapTestConn("0", sbc0) s.MapTestConn("0", sbc1) s.MapTestConn("0", sbc2) - sdc = NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + sdc = NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) timeStart = time.Now() - sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc.Execute(context.Background(), "query", nil, 0) timeDuration = time.Now().Sub(timeStart) if timeDuration >= retryDelay { t.Errorf("want no delay, got %v", timeDuration) @@ -386,9 +386,9 @@ func TestShardConnReconnect(t *testing.T) { s.MapTestConn("0", sbc0) s.MapTestConn("0", sbc1) s.MapTestConn("0", sbc2) - sdc = NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + sdc = NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) timeStart = time.Now() - sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc.Execute(context.Background(), "query", nil, 0) timeDuration = time.Now().Sub(timeStart) if timeDuration >= retryDelay { t.Errorf("want no delay, got %v", timeDuration) @@ -419,9 +419,9 @@ func TestShardConnReconnect(t *testing.T) { s.MapTestConn("0", sbc0) s.MapTestConn("0", sbc1) s.MapTestConn("0", sbc2) - sdc = NewShardConn(&context.DummyContext{}, new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + sdc = NewShardConn(context.Background(), new(sandboxTopo), "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) timeStart = time.Now() - sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc.Execute(context.Background(), "query", nil, 0) timeDuration = time.Now().Sub(timeStart) if timeDuration < retryDelay { t.Errorf("want no spam delay %v, got %v", retryDelay, timeDuration) @@ -466,9 +466,9 @@ func TestShardConnReconnect(t *testing.T) { } countGetEndPoints++ } - sdc = NewShardConn(&context.DummyContext{}, &sandboxTopo{callbackGetEndPoints: onGetEndPoints}, "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + sdc = NewShardConn(context.Background(), &sandboxTopo{callbackGetEndPoints: onGetEndPoints}, "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) timeStart = time.Now() - sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc.Execute(context.Background(), "query", nil, 0) timeDuration = time.Now().Sub(timeStart) if timeDuration >= retryDelay { t.Errorf("want no delay, got %v", timeDuration) @@ -516,9 +516,9 @@ func TestShardConnReconnect(t *testing.T) { } countGetEndPoints++ } - sdc = NewShardConn(&context.DummyContext{}, &sandboxTopo{callbackGetEndPoints: onGetEndPoints}, "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + sdc = NewShardConn(context.Background(), &sandboxTopo{callbackGetEndPoints: onGetEndPoints}, "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) timeStart = time.Now() - sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc.Execute(context.Background(), "query", nil, 0) timeDuration = time.Now().Sub(timeStart) if timeDuration < retryDelay { t.Errorf("want no spam delay %v, got %v", retryDelay, timeDuration) @@ -572,9 +572,9 @@ func TestShardConnReconnect(t *testing.T) { } countGetEndPoints++ } - sdc = NewShardConn(&context.DummyContext{}, &sandboxTopo{callbackGetEndPoints: onGetEndPoints}, "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) + sdc = NewShardConn(context.Background(), &sandboxTopo{callbackGetEndPoints: onGetEndPoints}, "aa", "TestShardConnReconnect", "0", "", retryDelay, retryCount, 1*time.Millisecond) timeStart = time.Now() - sdc.Execute(&context.DummyContext{}, "query", nil, 0) + sdc.Execute(context.Background(), "query", nil, 0) timeDuration = time.Now().Sub(timeStart) if timeDuration >= retryDelay { t.Errorf("want no delay, got %v", timeDuration) diff --git a/go/vt/vtgate/srv_topo_server.go b/go/vt/vtgate/srv_topo_server.go index b1238a651fb..775fa1109a1 100644 --- a/go/vt/vtgate/srv_topo_server.go +++ b/go/vt/vtgate/srv_topo_server.go @@ -15,10 +15,9 @@ import ( log "github.com/golang/glog" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/stats" - "github.com/youtube/vitess/go/vt/health" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) var ( @@ -41,6 +40,8 @@ type SrvTopoServer interface { GetSrvKeyspace(context context.Context, cell, keyspace string) (*topo.SrvKeyspace, error) + GetSrvShard(context context.Context, cell, keyspace, shard string) (*topo.SrvShard, error) + GetEndPoints(context context.Context, cell, keyspace, shard string, tabletType topo.TabletType) (*topo.EndPoints, error) } @@ -59,6 +60,7 @@ type ResilientSrvTopoServer struct { mutex sync.Mutex srvKeyspaceNamesCache map[string]*srvKeyspaceNamesEntry srvKeyspaceCache map[string]*srvKeyspaceEntry + srvShardCache map[string]*srvShardEntry endPointsCache map[string]*endPointsEntry // GetEndPoints stats. @@ -123,6 +125,21 @@ type srvKeyspaceEntry struct { lastErrorContext context.Context } +type srvShardEntry struct { + // unmutable values + cell string + keyspace string + shard string + + // the mutex protects any access to this structure (read or write) + mutex sync.Mutex + + insertionTime time.Time + value *topo.SrvShard + lastError error + lastErrorContext context.Context +} + type endPointsEntry struct { // unmutable values cell string @@ -147,7 +164,7 @@ type endPointsEntry struct { func endPointIsHealthy(ep topo.EndPoint) bool { // if we are behind on replication, we're not 100% healthy - return ep.Health == nil || ep.Health[health.ReplicationLag] != health.ReplicationLagHigh + return ep.Health == nil || ep.Health[topo.ReplicationLag] != topo.ReplicationLagHigh } // filterUnhealthyServers removes the unhealthy servers from the list, @@ -189,6 +206,7 @@ func NewResilientSrvTopoServer(base topo.Server, counterPrefix string) *Resilien srvKeyspaceNamesCache: make(map[string]*srvKeyspaceNamesEntry), srvKeyspaceCache: make(map[string]*srvKeyspaceEntry), + srvShardCache: make(map[string]*srvShardEntry), endPointsCache: make(map[string]*endPointsEntry), endPointCounters: newEndPointCounters(counterPrefix), @@ -292,6 +310,56 @@ func (server *ResilientSrvTopoServer) GetSrvKeyspace(context context.Context, ce return result, err } +// GetSrvShard returns SrvShard object for the given cell, keyspace, and shard. +func (server *ResilientSrvTopoServer) GetSrvShard(context context.Context, cell, keyspace, shard string) (*topo.SrvShard, error) { + server.counts.Add(queryCategory, 1) + + // find the entry in the cache, add it if not there + key := cell + "." + keyspace + "." + shard + server.mutex.Lock() + entry, ok := server.srvShardCache[key] + if !ok { + entry = &srvShardEntry{ + cell: cell, + keyspace: keyspace, + shard: shard, + } + server.srvShardCache[key] = entry + } + server.mutex.Unlock() + + // Lock the entry, and do everything holding the lock. This + // means two concurrent requests will only issue one + // underlying query. + entry.mutex.Lock() + defer entry.mutex.Unlock() + + // If the entry is fresh enough, return it + if time.Now().Sub(entry.insertionTime) < server.cacheTTL { + return entry.value, entry.lastError + } + + // not in cache or too old, get the real value + result, err := server.topoServer.GetSrvShard(cell, keyspace, shard) + if err != nil { + if entry.insertionTime.IsZero() { + server.counts.Add(errorCategory, 1) + log.Errorf("GetSrvShard(%v, %v, %v, %v) failed: %v (no cached value, caching and returning error)", context, cell, keyspace, shard, err) + } else { + server.counts.Add(cachedCategory, 1) + log.Warningf("GetSrvShard(%v, %v, %v, %v) failed: %v (returning cached value: %v %v)", context, cell, keyspace, shard, err, entry.value, entry.lastError) + return entry.value, entry.lastError + } + } + + // save the value we got and the current time in the cache + entry.insertionTime = time.Now() + entry.value = result + entry.lastError = err + entry.lastErrorContext = context + return result, err +} + // GetEndPoints return all endpoints for the given cell, keyspace, shard, and tablet type. func (server *ResilientSrvTopoServer) GetEndPoints(context context.Context, cell, keyspace, shard string, tabletType topo.TabletType) (result *topo.EndPoints, err error) { shard = strings.ToLower(shard) @@ -489,6 +557,62 @@ func (skcsl SrvKeyspaceCacheStatusList) Swap(i, j int) { skcsl[i], skcsl[j] = skcsl[j], skcsl[i] } +// SrvShardCacheStatus is the current value for a SrvShard object +type SrvShardCacheStatus struct { + Cell string + Keyspace string + Shard string + Value *topo.SrvShard + LastError error + LastErrorContext context.Context +} + +// StatusAsHTML returns an HTML version of our status. +// It works best if there is data in the cache. +func (st *SrvShardCacheStatus) StatusAsHTML() template.HTML { + if st.Value == nil { + return template.HTML("No Data") + } + + result := "Name: " + st.Value.Name + "
" + result += "KeyRange: " + st.Value.KeyRange.String() + "
" + + result += "ServedTypes:" + for _, servedType := range st.Value.ServedTypes { + result += " " + string(servedType) + } + result += "
" + + result += "MasterCell: " + st.Value.MasterCell + "
" + + result += "TabletTypes:" + for _, tabletType := range st.Value.TabletTypes { + result += " " + string(tabletType) + } + result += "
" + + return template.HTML(result) +} + +// SrvShardCacheStatusList is used for sorting +type SrvShardCacheStatusList []*SrvShardCacheStatus + +// Len is part of sort.Interface +func (sscsl SrvShardCacheStatusList) Len() int { + return len(sscsl) +} + +// Less is part of sort.Interface +func (sscsl SrvShardCacheStatusList) Less(i, j int) bool { + return sscsl[i].Cell+"."+sscsl[i].Keyspace < + sscsl[j].Cell+"."+sscsl[j].Keyspace +} + +// Swap is part of sort.Interface +func (sscsl SrvShardCacheStatusList) Swap(i, j int) { + sscsl[i], sscsl[j] = sscsl[j], sscsl[i] +} + // EndPointsCacheStatus is the current value for an EndPoints object type EndPointsCacheStatus struct { Cell string @@ -547,6 +671,7 @@ func (epcsl EndPointsCacheStatusList) Swap(i, j int) { type ResilientSrvTopoServerCacheStatus struct { SrvKeyspaceNames SrvKeyspaceNamesCacheStatusList SrvKeyspaces SrvKeyspaceCacheStatusList + SrvShards SrvShardCacheStatusList EndPoints EndPointsCacheStatusList } @@ -578,6 +703,19 @@ func (server *ResilientSrvTopoServer) CacheStatus() *ResilientSrvTopoServerCache entry.mutex.Unlock() } + for _, entry := range server.srvShardCache { + entry.mutex.Lock() + result.SrvShards = append(result.SrvShards, &SrvShardCacheStatus{ + Cell: entry.cell, + Keyspace: entry.keyspace, + Shard: entry.shard, + Value: entry.value, + LastError: entry.lastError, + LastErrorContext: entry.lastErrorContext, + }) + entry.mutex.Unlock() + } + for _, entry := range server.endPointsCache { entry.mutex.Lock() result.EndPoints = append(result.EndPoints, &EndPointsCacheStatus{ @@ -598,6 +736,7 @@ func (server *ResilientSrvTopoServer) CacheStatus() *ResilientSrvTopoServerCache // do the sorting without the mutex sort.Sort(result.SrvKeyspaceNames) sort.Sort(result.SrvKeyspaces) + sort.Sort(result.SrvShards) sort.Sort(result.EndPoints) return result diff --git a/go/vt/vtgate/srv_topo_server_test.go b/go/vt/vtgate/srv_topo_server_test.go index 64af01e5da8..5d2fc6ab16d 100644 --- a/go/vt/vtgate/srv_topo_server_test.go +++ b/go/vt/vtgate/srv_topo_server_test.go @@ -8,11 +8,9 @@ import ( "fmt" "reflect" "testing" - "time" - "github.com/youtube/vitess/go/vt/context" - "github.com/youtube/vitess/go/vt/health" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) func TestFilterUnhealthy(t *testing.T) { @@ -100,7 +98,7 @@ func TestFilterUnhealthy(t *testing.T) { topo.EndPoint{ Uid: 4, Health: map[string]string{ - health.ReplicationLag: health.ReplicationLagHigh, + topo.ReplicationLag: topo.ReplicationLagHigh, }, }, topo.EndPoint{ @@ -139,19 +137,19 @@ func TestFilterUnhealthy(t *testing.T) { topo.EndPoint{ Uid: 1, Health: map[string]string{ - health.ReplicationLag: health.ReplicationLagHigh, + topo.ReplicationLag: topo.ReplicationLagHigh, }, }, topo.EndPoint{ Uid: 2, Health: map[string]string{ - health.ReplicationLag: health.ReplicationLagHigh, + topo.ReplicationLag: topo.ReplicationLagHigh, }, }, topo.EndPoint{ Uid: 3, Health: map[string]string{ - health.ReplicationLag: health.ReplicationLagHigh, + topo.ReplicationLag: topo.ReplicationLagHigh, }, }, }, @@ -161,19 +159,19 @@ func TestFilterUnhealthy(t *testing.T) { topo.EndPoint{ Uid: 1, Health: map[string]string{ - health.ReplicationLag: health.ReplicationLagHigh, + topo.ReplicationLag: topo.ReplicationLagHigh, }, }, topo.EndPoint{ Uid: 2, Health: map[string]string{ - health.ReplicationLag: health.ReplicationLagHigh, + topo.ReplicationLag: topo.ReplicationLagHigh, }, }, topo.EndPoint{ Uid: 3, Health: map[string]string{ - health.ReplicationLag: health.ReplicationLagHigh, + topo.ReplicationLag: topo.ReplicationLagHigh, }, }, }, @@ -235,7 +233,6 @@ func (ft *fakeTopo) UpdateTabletFields(tabletAlias topo.TabletAlias, update func return nil } func (ft *fakeTopo) DeleteTablet(alias topo.TabletAlias) error { return nil } -func (ft *fakeTopo) ValidateTablet(alias topo.TabletAlias) error { return nil } func (ft *fakeTopo) GetTablet(alias topo.TabletAlias) (*topo.TabletInfo, error) { return nil, nil } func (ft *fakeTopo) GetTabletsByCell(cell string) ([]topo.TabletAlias, error) { return nil, nil } func (ft *fakeTopo) UpdateShardReplicationFields(cell, keyspace, shard string, update func(*topo.ShardReplication) error) error { @@ -245,7 +242,7 @@ func (ft *fakeTopo) GetShardReplication(cell, keyspace, shard string) (*topo.Sha return nil, nil } func (ft *fakeTopo) DeleteShardReplication(cell, keyspace, shard string) error { return nil } -func (ft *fakeTopo) LockSrvShardForAction(cell, keyspace, shard, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +func (ft *fakeTopo) LockSrvShardForAction(ctx context.Context, cell, keyspace, shard, contents string) (string, error) { return "", nil } func (ft *fakeTopo) UnlockSrvShardForAction(cell, keyspace, shard, lockPath, results string) error { @@ -260,6 +257,9 @@ func (ft *fakeTopo) UpdateEndPoints(cell, keyspace, shard string, tabletType top func (ft *fakeTopo) DeleteEndPoints(cell, keyspace, shard string, tabletType topo.TabletType) error { return nil } +func (ft *fakeTopo) WatchEndPoints(cell, keyspace, shard string, tabletType topo.TabletType) (notifications <-chan *topo.EndPoints, stopWatching chan<- struct{}, err error) { + return nil, nil, nil +} func (ft *fakeTopo) UpdateSrvShard(cell, keyspace, shard string, srvShard *topo.SrvShard) error { return nil } @@ -271,19 +271,14 @@ func (ft *fakeTopo) UpdateSrvKeyspace(cell, keyspace string, srvKeyspace *topo.S func (ft *fakeTopo) UpdateTabletEndpoint(cell, keyspace, shard string, tabletType topo.TabletType, addr *topo.EndPoint) error { return nil } -func (ft *fakeTopo) LockKeyspaceForAction(keyspace, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +func (ft *fakeTopo) LockKeyspaceForAction(ctx context.Context, keyspace, contents string) (string, error) { return "", nil } func (ft *fakeTopo) UnlockKeyspaceForAction(keyspace, lockPath, results string) error { return nil } -func (ft *fakeTopo) LockShardForAction(keyspace, shard, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +func (ft *fakeTopo) LockShardForAction(ctx context.Context, keyspace, shard, contents string) (string, error) { return "", nil } func (ft *fakeTopo) UnlockShardForAction(keyspace, shard, lockPath, results string) error { return nil } -func (ft *fakeTopo) CreateTabletPidNode(tabletAlias topo.TabletAlias, contents string, done chan struct{}) error { - return nil -} -func (ft *fakeTopo) ValidateTabletPidNode(tabletAlias topo.TabletAlias) error { return nil } -func (ft *fakeTopo) GetSubprocessFlags() []string { return nil } type fakeTopoRemoteMaster struct { fakeTopo @@ -329,7 +324,7 @@ func TestRemoteMaster(t *testing.T) { rsts.enableRemoteMaster = true // remote cell for master - ep, err := rsts.GetEndPoints(&context.DummyContext{}, "cell3", "test_ks", "1", topo.TYPE_MASTER) + ep, err := rsts.GetEndPoints(context.Background(), "cell3", "test_ks", "1", topo.TYPE_MASTER) if err != nil { t.Fatalf("GetEndPoints got unexpected error: %v", err) } @@ -342,23 +337,23 @@ func TestRemoteMaster(t *testing.T) { } // no remote cell for non-master - ep, err = rsts.GetEndPoints(&context.DummyContext{}, "cell3", "test_ks", "0", topo.TYPE_REPLICA) + ep, err = rsts.GetEndPoints(context.Background(), "cell3", "test_ks", "0", topo.TYPE_REPLICA) if err == nil { t.Fatalf("GetEndPoints did not return an error") } // no remote cell for master rsts.enableRemoteMaster = false - ep, err = rsts.GetEndPoints(&context.DummyContext{}, "cell3", "test_ks", "2", topo.TYPE_MASTER) + ep, err = rsts.GetEndPoints(context.Background(), "cell3", "test_ks", "2", topo.TYPE_MASTER) if err == nil { t.Fatalf("GetEndPoints did not return an error") } // use cached value from above - ep, err = rsts.GetEndPoints(&context.DummyContext{}, "cell3", "test_ks", "1", topo.TYPE_MASTER) + ep, err = rsts.GetEndPoints(context.Background(), "cell3", "test_ks", "1", topo.TYPE_MASTER) if err != nil { t.Fatalf("GetEndPoints got unexpected error: %v", err) } - ep, err = rsts.GetEndPoints(&context.DummyContext{}, "cell1", "test_ks", "1", topo.TYPE_MASTER) + ep, err = rsts.GetEndPoints(context.Background(), "cell1", "test_ks", "1", topo.TYPE_MASTER) if ep.Entries[0].Uid != 0 { t.Fatalf("GetEndPoints got %v want 0", ep.Entries[0].Uid) } @@ -370,7 +365,7 @@ func TestCacheWithErrors(t *testing.T) { rsts := NewResilientSrvTopoServer(ft, "TestCacheWithErrors") // ask for the known keyspace, that populates the cache - _, err := rsts.GetSrvKeyspace(&context.DummyContext{}, "", "test_ks") + _, err := rsts.GetSrvKeyspace(context.Background(), "", "test_ks") if err != nil { t.Fatalf("GetSrvKeyspace got unexpected error: %v", err) } @@ -378,14 +373,14 @@ func TestCacheWithErrors(t *testing.T) { // now make the topo server fail, and ask again, should get cached // value, not even ask underlying guy ft.keyspace = "another_test_ks" - _, err = rsts.GetSrvKeyspace(&context.DummyContext{}, "", "test_ks") + _, err = rsts.GetSrvKeyspace(context.Background(), "", "test_ks") if err != nil { t.Fatalf("GetSrvKeyspace got unexpected error: %v", err) } // now reduce TTL to nothing, so we won't use cache, and ask again rsts.cacheTTL = 0 - _, err = rsts.GetSrvKeyspace(&context.DummyContext{}, "", "test_ks") + _, err = rsts.GetSrvKeyspace(context.Background(), "", "test_ks") if err != nil { t.Fatalf("GetSrvKeyspace got unexpected error: %v", err) } @@ -397,7 +392,7 @@ func TestCachedErrors(t *testing.T) { rsts := NewResilientSrvTopoServer(ft, "TestCachedErrors") // ask for an unknown keyspace, should get an error - _, err := rsts.GetSrvKeyspace(&context.DummyContext{}, "", "unknown_ks") + _, err := rsts.GetSrvKeyspace(context.Background(), "", "unknown_ks") if err == nil { t.Fatalf("First GetSrvKeyspace didn't return an error") } @@ -406,7 +401,7 @@ func TestCachedErrors(t *testing.T) { } // ask again, should get an error and use cache - _, err = rsts.GetSrvKeyspace(&context.DummyContext{}, "", "unknown_ks") + _, err = rsts.GetSrvKeyspace(context.Background(), "", "unknown_ks") if err == nil { t.Fatalf("Second GetSrvKeyspace didn't return an error") } @@ -416,7 +411,7 @@ func TestCachedErrors(t *testing.T) { // ask again after expired cache, should get an error rsts.cacheTTL = 0 - _, err = rsts.GetSrvKeyspace(&context.DummyContext{}, "", "unknown_ks") + _, err = rsts.GetSrvKeyspace(context.Background(), "", "unknown_ks") if err == nil { t.Fatalf("Third GetSrvKeyspace didn't return an error") } diff --git a/go/vt/vtgate/topo_utils.go b/go/vt/vtgate/topo_utils.go index 9c8a0542774..09c1a2b36cc 100644 --- a/go/vt/vtgate/topo_utils.go +++ b/go/vt/vtgate/topo_utils.go @@ -7,14 +7,14 @@ package vtgate import ( "fmt" - "github.com/youtube/vitess/go/vt/context" "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" ) -func mapKeyspaceIdsToShards(topoServ SrvTopoServer, cell, keyspace string, tabletType topo.TabletType, keyspaceIds []key.KeyspaceId) (string, []string, error) { - keyspace, allShards, err := getKeyspaceShards(topoServ, cell, keyspace, tabletType) +func mapKeyspaceIdsToShards(ctx context.Context, topoServ SrvTopoServer, cell, keyspace string, tabletType topo.TabletType, keyspaceIds []key.KeyspaceId) (string, []string, error) { + keyspace, allShards, err := getKeyspaceShards(ctx, topoServ, cell, keyspace, tabletType) if err != nil { return "", nil, err } @@ -33,8 +33,8 @@ func mapKeyspaceIdsToShards(topoServ SrvTopoServer, cell, keyspace string, table return keyspace, res, nil } -func getKeyspaceShards(topoServ SrvTopoServer, cell, keyspace string, tabletType topo.TabletType) (string, []topo.SrvShard, error) { - srvKeyspace, err := topoServ.GetSrvKeyspace(&context.DummyContext{}, cell, keyspace) +func getKeyspaceShards(ctx context.Context, topoServ SrvTopoServer, cell, keyspace string, tabletType topo.TabletType) (string, []topo.SrvShard, error) { + srvKeyspace, err := topoServ.GetSrvKeyspace(ctx, cell, keyspace) if err != nil { return "", nil, fmt.Errorf("keyspace %v fetch error: %v", keyspace, err) } @@ -42,7 +42,7 @@ func getKeyspaceShards(topoServ SrvTopoServer, cell, keyspace string, tabletType // check if the keyspace has been redirected for this tabletType. if servedFrom, ok := srvKeyspace.ServedFrom[tabletType]; ok { keyspace = servedFrom - srvKeyspace, err = topoServ.GetSrvKeyspace(&context.DummyContext{}, cell, keyspace) + srvKeyspace, err = topoServ.GetSrvKeyspace(ctx, cell, keyspace) if err != nil { return "", nil, fmt.Errorf("keyspace %v fetch error: %v", keyspace, err) } @@ -68,8 +68,8 @@ func getShardForKeyspaceId(allShards []topo.SrvShard, keyspaceId key.KeyspaceId) return "", fmt.Errorf("KeyspaceId %v didn't match any shards %+v", keyspaceId, allShards) } -func mapEntityIdsToShards(topoServ SrvTopoServer, cell, keyspace string, entityIds []proto.EntityId, tabletType topo.TabletType) (string, map[string][]interface{}, error) { - keyspace, allShards, err := getKeyspaceShards(topoServ, cell, keyspace, tabletType) +func mapEntityIdsToShards(ctx context.Context, topoServ SrvTopoServer, cell, keyspace string, entityIds []proto.EntityId, tabletType topo.TabletType) (string, map[string][]interface{}, error) { + keyspace, allShards, err := getKeyspaceShards(ctx, topoServ, cell, keyspace, tabletType) if err != nil { return "", nil, err } @@ -87,8 +87,8 @@ func mapEntityIdsToShards(topoServ SrvTopoServer, cell, keyspace string, entityI // This function implements the restriction of handling one keyrange // and one shard since streaming doesn't support merge sorting the results. // The input/output api is generic though. -func mapKeyRangesToShards(topoServ SrvTopoServer, cell, keyspace string, tabletType topo.TabletType, krs []key.KeyRange) (string, []string, error) { - keyspace, allShards, err := getKeyspaceShards(topoServ, cell, keyspace, tabletType) +func mapKeyRangesToShards(ctx context.Context, topoServ SrvTopoServer, cell, keyspace string, tabletType topo.TabletType, krs []key.KeyRange) (string, []string, error) { + keyspace, allShards, err := getKeyspaceShards(ctx, topoServ, cell, keyspace, tabletType) if err != nil { return "", nil, err } @@ -127,3 +127,27 @@ func resolveKeyRangeToShards(allShards []topo.SrvShard, kr key.KeyRange) ([]stri } return shards, nil } + +// mapExactShards maps a keyrange to shards only if there's a complete +// match. If there's any partial match the function returns no match. +func mapExactShards(ctx context.Context, topoServ SrvTopoServer, cell, keyspace string, tabletType topo.TabletType, kr key.KeyRange) (newkeyspace string, shards []string, err error) { + keyspace, allShards, err := getKeyspaceShards(ctx, topoServ, cell, keyspace, tabletType) + if err != nil { + return "", nil, err + } + shardnum := 0 + for shardnum < len(allShards) { + if kr.Start == allShards[shardnum].KeyRange.Start { + break + } + shardnum++ + } + for shardnum < len(allShards) { + shards = append(shards, allShards[shardnum].ShardName()) + if kr.End == allShards[shardnum].KeyRange.End { + return keyspace, shards, nil + } + shardnum++ + } + return keyspace, nil, fmt.Errorf("keyrange %v does not exactly match shards", kr) +} diff --git a/go/vt/vtgate/topo_utils_test.go b/go/vt/vtgate/topo_utils_test.go index 3c87d4820b9..3212837f7d1 100644 --- a/go/vt/vtgate/topo_utils_test.go +++ b/go/vt/vtgate/topo_utils_test.go @@ -10,6 +10,7 @@ import ( "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) func TestKeyRangeToShardMap(t *testing.T) { @@ -19,19 +20,19 @@ func TestKeyRangeToShardMap(t *testing.T) { keyRange string shards []string }{ - {keyspace: TEST_SHARDED, keyRange: "20-40", shards: []string{"20-40"}}, + {keyspace: KsTestSharded, keyRange: "20-40", shards: []string{"20-40"}}, // check for partial keyrange, spanning one shard - {keyspace: TEST_SHARDED, keyRange: "10-18", shards: []string{"-20"}}, + {keyspace: KsTestSharded, keyRange: "10-18", shards: []string{"-20"}}, // check for keyrange intersecting with multiple shards - {keyspace: TEST_SHARDED, keyRange: "10-40", shards: []string{"-20", "20-40"}}, + {keyspace: KsTestSharded, keyRange: "10-40", shards: []string{"-20", "20-40"}}, // check for keyrange intersecting with multiple shards - {keyspace: TEST_SHARDED, keyRange: "1c-2a", shards: []string{"-20", "20-40"}}, + {keyspace: KsTestSharded, keyRange: "1c-2a", shards: []string{"-20", "20-40"}}, // check for keyrange where kr.End is Max Key "" - {keyspace: TEST_SHARDED, keyRange: "80-", shards: []string{"80-a0", "a0-c0", "c0-e0", "e0-"}}, + {keyspace: KsTestSharded, keyRange: "80-", shards: []string{"80-a0", "a0-c0", "c0-e0", "e0-"}}, // test for sharded, non-partial keyrange spanning the entire space. - {keyspace: TEST_SHARDED, keyRange: "", shards: []string{"-20", "20-40", "40-60", "60-80", "80-a0", "a0-c0", "c0-e0", "e0-"}}, + {keyspace: KsTestSharded, keyRange: "", shards: []string{"-20", "20-40", "40-60", "60-80", "80-a0", "a0-c0", "c0-e0", "e0-"}}, // test for unsharded, non-partial keyrange spanning the entire space. - {keyspace: TEST_UNSHARDED, keyRange: "", shards: []string{"0"}}, + {keyspace: KsTestUnsharded, keyRange: "", shards: []string{"0"}}, } for _, testCase := range testCases { @@ -46,7 +47,7 @@ func TestKeyRangeToShardMap(t *testing.T) { } keyRange = krArray[0] } - _, allShards, err := getKeyspaceShards(ts, "", testCase.keyspace, topo.TYPE_MASTER) + _, allShards, err := getKeyspaceShards(context.Background(), ts, "", testCase.keyspace, topo.TYPE_MASTER) gotShards, err := resolveKeyRangeToShards(allShards, keyRange) if err != nil { t.Errorf("want nil, got %v", err) @@ -56,3 +57,46 @@ func TestKeyRangeToShardMap(t *testing.T) { } } } + +func TestMapExactShards(t *testing.T) { + ts := new(sandboxTopo) + var testCases = []struct { + keyspace string + keyRange string + shards []string + err string + }{ + {keyspace: KsTestSharded, keyRange: "20-40", shards: []string{"20-40"}}, + // check for partial keyrange, spanning one shard + {keyspace: KsTestSharded, keyRange: "10-18", shards: nil, err: "keyrange {Start: 10, End: 18} does not exactly match shards"}, + // check for keyrange intersecting with multiple shards + {keyspace: KsTestSharded, keyRange: "10-40", shards: nil, err: "keyrange {Start: 10, End: 40} does not exactly match shards"}, + // check for keyrange intersecting with multiple shards + {keyspace: KsTestSharded, keyRange: "1c-2a", shards: nil, err: "keyrange {Start: 1c, End: 2a} does not exactly match shards"}, + // check for keyrange where kr.End is Max Key "" + {keyspace: KsTestSharded, keyRange: "80-", shards: []string{"80-a0", "a0-c0", "c0-e0", "e0-"}}, + // test for sharded, non-partial keyrange spanning the entire space. + {keyspace: KsTestSharded, keyRange: "", shards: []string{"-20", "20-40", "40-60", "60-80", "80-a0", "a0-c0", "c0-e0", "e0-"}}, + } + + for _, testCase := range testCases { + var keyRange key.KeyRange + var err error + if testCase.keyRange == "" { + keyRange = key.KeyRange{Start: "", End: ""} + } else { + krArray, err := key.ParseShardingSpec(testCase.keyRange) + if err != nil { + t.Errorf("Got error while parsing sharding spec %v", err) + } + keyRange = krArray[0] + } + _, gotShards, err := mapExactShards(context.Background(), ts, "", testCase.keyspace, topo.TYPE_MASTER, keyRange) + if err != nil && err.Error() != testCase.err { + t.Errorf("gotShards: %v, want %s", err, testCase.err) + } + if !reflect.DeepEqual(testCase.shards, gotShards) { + t.Errorf("want \n%#v, got \n%#v", testCase.shards, gotShards) + } + } +} diff --git a/go/vt/vtgate/vertical_split_test.go b/go/vt/vtgate/vertical_split_test.go index 30e2319d670..3dc799c348c 100644 --- a/go/vt/vtgate/vertical_split_test.go +++ b/go/vt/vtgate/vertical_split_test.go @@ -9,10 +9,10 @@ import ( "time" mproto "github.com/youtube/vitess/go/mysql/proto" - "github.com/youtube/vitess/go/vt/context" tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" ) // This file uses the sandbox_test framework. @@ -20,7 +20,7 @@ import ( func TestExecuteKeyspaceAlias(t *testing.T) { testVerticalSplitGeneric(t, func(shards []string) (*mproto.QueryResult, error) { stc := NewScatterConn(new(sandboxTopo), "", "aa", 1*time.Millisecond, 3, 1*time.Millisecond) - return stc.Execute(&context.DummyContext{}, "query", nil, TEST_UNSHARDED_SERVED_FROM, shards, topo.TYPE_RDONLY, nil) + return stc.Execute(context.Background(), "query", nil, KsTestUnshardedServedFrom, shards, topo.TYPE_RDONLY, nil) }) } @@ -28,7 +28,7 @@ func TestBatchExecuteKeyspaceAlias(t *testing.T) { testVerticalSplitGeneric(t, func(shards []string) (*mproto.QueryResult, error) { stc := NewScatterConn(new(sandboxTopo), "", "aa", 1*time.Millisecond, 3, 1*time.Millisecond) queries := []tproto.BoundQuery{{"query", nil}} - qrs, err := stc.ExecuteBatch(&context.DummyContext{}, queries, TEST_UNSHARDED_SERVED_FROM, shards, topo.TYPE_RDONLY, nil) + qrs, err := stc.ExecuteBatch(context.Background(), queries, KsTestUnshardedServedFrom, shards, topo.TYPE_RDONLY, nil) if err != nil { return nil, err } @@ -40,7 +40,7 @@ func TestStreamExecuteKeyspaceAlias(t *testing.T) { testVerticalSplitGeneric(t, func(shards []string) (*mproto.QueryResult, error) { stc := NewScatterConn(new(sandboxTopo), "", "aa", 1*time.Millisecond, 3, 1*time.Millisecond) qr := new(mproto.QueryResult) - err := stc.StreamExecute(&context.DummyContext{}, "query", nil, TEST_UNSHARDED_SERVED_FROM, shards, topo.TYPE_RDONLY, nil, func(r *mproto.QueryResult) error { + err := stc.StreamExecute(context.Background(), "query", nil, KsTestUnshardedServedFrom, shards, topo.TYPE_RDONLY, nil, func(r *mproto.QueryResult) error { appendResult(qr, r) return nil }) @@ -49,7 +49,7 @@ func TestStreamExecuteKeyspaceAlias(t *testing.T) { } func TestInTransactionKeyspaceAlias(t *testing.T) { - s := createSandbox(TEST_UNSHARDED_SERVED_FROM) + s := createSandbox(KsTestUnshardedServedFrom) sbc := &sandboxConn{mustFailRetry: 3} s.MapTestConn("0", sbc) @@ -57,13 +57,13 @@ func TestInTransactionKeyspaceAlias(t *testing.T) { session := NewSafeSession(&proto.Session{ InTransaction: true, ShardSessions: []*proto.ShardSession{{ - Keyspace: TEST_UNSHARDED_SERVED_FROM, + Keyspace: KsTestUnshardedServedFrom, Shard: "0", TabletType: topo.TYPE_MASTER, TransactionId: 1, }}, }) - _, err := stc.Execute(&context.DummyContext{}, "query", nil, TEST_UNSHARDED_SERVED_FROM, []string{"0"}, topo.TYPE_MASTER, session) + _, err := stc.Execute(context.Background(), "query", nil, KsTestUnshardedServedFrom, []string{"0"}, topo.TYPE_MASTER, session) want := "shard, host: TestUnshardedServedFrom.0.master, {Uid:0 Host:0 NamedPortMap:map[vt:1] Health:map[]}, retry: err" if err == nil || err.Error() != want { t.Errorf("want '%v', got '%v'", want, err) @@ -77,7 +77,7 @@ func TestInTransactionKeyspaceAlias(t *testing.T) { func testVerticalSplitGeneric(t *testing.T, f func(shards []string) (*mproto.QueryResult, error)) { // Retry Error, for keyspace that is redirected should succeed. - s := createSandbox(TEST_UNSHARDED_SERVED_FROM) + s := createSandbox(KsTestUnshardedServedFrom) sbc := &sandboxConn{mustFailRetry: 3} s.MapTestConn("0", sbc) _, err := f([]string{"0"}) diff --git a/go/vt/vtgate/vindexes/hash.go b/go/vt/vtgate/vindexes/hash.go new file mode 100644 index 00000000000..c6e80d326bd --- /dev/null +++ b/go/vt/vtgate/vindexes/hash.go @@ -0,0 +1,212 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "crypto/cipher" + "crypto/des" + "encoding/binary" + "fmt" + + "github.com/youtube/vitess/go/vt/key" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +// Hash defines vindex that hashes an int64 to a KeyspaceId +// by using null-key 3DES hash. It's Unique, Reversible and +// Functional. +type Hash struct { + hv HashAuto +} + +// NewHash creates a new Hash. +func NewHash(m map[string]interface{}) (planbuilder.Vindex, error) { + h := &Hash{} + h.hv.Init(m) + return h, nil +} + +// Cost returns the cost of this index as 1. +func (vind *Hash) Cost() int { + return vind.hv.Cost() +} + +// Map returns the corresponding KeyspaceId values for the given ids. +func (vind *Hash) Map(_ planbuilder.VCursor, ids []interface{}) ([]key.KeyspaceId, error) { + return vind.hv.Map(nil, ids) +} + +// Verify returns true if id maps to ksid. +func (vind *Hash) Verify(_ planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) (bool, error) { + return vind.hv.Verify(nil, id, ksid) +} + +// ReverseMap returns the id from ksid. +func (vind *Hash) ReverseMap(_ planbuilder.VCursor, ksid key.KeyspaceId) (interface{}, error) { + return vind.hv.ReverseMap(nil, ksid) +} + +// Create reserves the id by inserting it into the vindex table. +func (vind *Hash) Create(vcursor planbuilder.VCursor, id interface{}) error { + return vind.hv.Create(vcursor, id) +} + +// Delete deletes the entry from the vindex table. +func (vind *Hash) Delete(vcursor planbuilder.VCursor, ids []interface{}, _ key.KeyspaceId) error { + return vind.hv.Delete(vcursor, ids, "") +} + +// HashAuto defines vindex that hashes an int64 to a KeyspaceId +// by using null-key 3DES hash. It's Unique, Reversible and +// Functional. Additionally, it's also a FunctionalGenerator +// because it's capable of generating new values from a vindex table +// with a single unique autoinc column. +type HashAuto struct { + Table, Column string + ins, del string +} + +// NewHashAuto creates a new HashAuto. +func NewHashAuto(m map[string]interface{}) (planbuilder.Vindex, error) { + hva := &HashAuto{} + hva.Init(m) + return hva, nil +} + +// Init initializes HashAuto. +func (vind *HashAuto) Init(m map[string]interface{}) { + get := func(name string) string { + v, _ := m[name].(string) + return v + } + t := get("Table") + c := get("Column") + vind.Table = t + vind.Column = c + vind.ins = fmt.Sprintf("insert into %s(%s) values(:%s)", t, c, c) + vind.del = fmt.Sprintf("delete from %s where %s in ::%s", t, c, c) +} + +// Cost returns the cost of this index as 1. +func (vind *HashAuto) Cost() int { + return 1 +} + +// Map returns the corresponding KeyspaceId values for the given ids. +func (vind *HashAuto) Map(_ planbuilder.VCursor, ids []interface{}) ([]key.KeyspaceId, error) { + out := make([]key.KeyspaceId, 0, len(ids)) + for _, id := range ids { + num, err := getNumber(id) + if err != nil { + return nil, fmt.Errorf("hash.Map: %v", err) + } + out = append(out, vhash(num)) + } + return out, nil +} + +// Verify returns true if id maps to ksid. +func (vind *HashAuto) Verify(_ planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) (bool, error) { + num, err := getNumber(id) + if err != nil { + return false, fmt.Errorf("hash.Verify: %v", err) + } + return vhash(num) == ksid, nil +} + +// ReverseMap returns the id from ksid. +func (vind *HashAuto) ReverseMap(_ planbuilder.VCursor, ksid key.KeyspaceId) (interface{}, error) { + return vunhash(ksid) +} + +// Create reserves the id by inserting it into the vindex table. +func (vind *HashAuto) Create(vcursor planbuilder.VCursor, id interface{}) error { + bq := &tproto.BoundQuery{ + Sql: vind.ins, + BindVariables: map[string]interface{}{ + vind.Column: id, + }, + } + if _, err := vcursor.Execute(bq); err != nil { + return fmt.Errorf("hash.Create: %v", err) + } + return nil +} + +// Generate generates a new id by using the autoinc of the vindex table. +func (vind *HashAuto) Generate(vcursor planbuilder.VCursor) (id int64, err error) { + bq := &tproto.BoundQuery{ + Sql: vind.ins, + BindVariables: map[string]interface{}{ + vind.Column: nil, + }, + } + result, err := vcursor.Execute(bq) + if err != nil { + return 0, fmt.Errorf("hash.Generate: %v", err) + } + return int64(result.InsertId), err +} + +// Delete deletes the entry from the vindex table. +func (vind *HashAuto) Delete(vcursor planbuilder.VCursor, ids []interface{}, _ key.KeyspaceId) error { + bq := &tproto.BoundQuery{ + Sql: vind.del, + BindVariables: map[string]interface{}{ + vind.Column: ids, + }, + } + if _, err := vcursor.Execute(bq); err != nil { + return fmt.Errorf("hash.Delete: %v", err) + } + return nil +} + +func getNumber(v interface{}) (int64, error) { + switch v := v.(type) { + case int: + return int64(v), nil + case int32: + return int64(v), nil + case int64: + return v, nil + case uint: + return int64(v), nil + case uint32: + return int64(v), nil + case uint64: + return int64(v), nil + } + return 0, fmt.Errorf("unexpected type for %v: %T", v, v) +} + +var block3DES cipher.Block + +func init() { + var err error + block3DES, err = des.NewTripleDESCipher(make([]byte, 24)) + if err != nil { + panic(err) + } + planbuilder.Register("hash", NewHash) + planbuilder.Register("hash_autoinc", NewHashAuto) +} + +func vhash(shardKey int64) key.KeyspaceId { + var keybytes, hashed [8]byte + binary.BigEndian.PutUint64(keybytes[:], uint64(shardKey)) + block3DES.Encrypt(hashed[:], keybytes[:]) + return key.KeyspaceId(hashed[:]) +} + +func vunhash(k key.KeyspaceId) (int64, error) { + if len(k) != 8 { + return 0, fmt.Errorf("invalid keyspace id: %v", k) + } + var unhashed [8]byte + block3DES.Decrypt(unhashed[:], []byte(k)) + return int64(binary.BigEndian.Uint64(unhashed[:])), nil +} diff --git a/go/vt/vtgate/vindexes/hash_auto_test.go b/go/vt/vtgate/vindexes/hash_auto_test.go new file mode 100644 index 00000000000..8690c544eb3 --- /dev/null +++ b/go/vt/vtgate/vindexes/hash_auto_test.go @@ -0,0 +1,250 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "errors" + "fmt" + "reflect" + "strings" + "testing" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/sqltypes" + "github.com/youtube/vitess/go/vt/key" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +var hashAuto planbuilder.Vindex + +func init() { + hv, err := planbuilder.CreateVindex("hash_autoinc", map[string]interface{}{"Table": "t", "Column": "c"}) + if err != nil { + panic(err) + } + hashAuto = hv +} + +func TestHashAutoConvert(t *testing.T) { + cases := []struct { + in int64 + out string + }{ + {1, "\x16k@\xb4J\xbaK\xd6"}, + {0, "\x8c\xa6M\xe9\xc1\xb1#\xa7"}, + {11, "\xae\xfcDI\x1c\xfeGL"}, + {0x100000000000000, "\r\x9f'\x9b\xa5\xd8r`"}, + {0x800000000000000, " \xb9\xe7g\xb2\xfb\x14V"}, + {11, "\xae\xfcDI\x1c\xfeGL"}, + {0, "\x8c\xa6M\xe9\xc1\xb1#\xa7"}, + } + for _, c := range cases { + got := string(vhash(c.in)) + want := c.out + if got != want { + t.Errorf("vhash(%d): %#v, want %q", c.in, got, want) + } + back, err := vunhash(key.KeyspaceId(got)) + if err != nil { + t.Error(err) + } + if back != c.in { + t.Errorf("vunhash(%q): %d, want %d", got, back, c.in) + } + } +} + +func TestHashAutoFail(t *testing.T) { + _, err := vunhash(key.KeyspaceId("aa")) + want := "invalid keyspace id: 6161" + if err == nil || err.Error() != want { + t.Errorf(`vunhash("aa"): %v, want %s`, err, want) + } +} + +func BenchmarkHashConvert(b *testing.B) { + for i := 0; i < b.N; i++ { + vhash(int64(i)) + } +} + +func TestHashAutoCost(t *testing.T) { + if hashAuto.Cost() != 1 { + t.Errorf("Cost(): %d, want 1", hashAuto.Cost()) + } +} + +func TestHashAutoMap(t *testing.T) { + got, err := hashAuto.(planbuilder.Unique).Map(nil, []interface{}{1, int32(2), int64(3), uint(4), uint32(5), uint64(6)}) + if err != nil { + t.Error(err) + } + want := []key.KeyspaceId{ + "\x16k@\xb4J\xbaK\xd6", + "\x06\xe7\xea\"Βp\x8f", + "N\xb1\x90ɢ\xfa\x16\x9c", + "\xd2\xfd\x88g\xd5\r-\xfe", + "p\xbb\x02<\x81\f\xa8z", + "\xf0\x98H\n\xc4ľq", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Map(): %#v, want %+v", got, want) + } +} + +func TestHashAutoMapFail(t *testing.T) { + _, err := hashAuto.(planbuilder.Unique).Map(nil, []interface{}{1.1}) + want := "hash.Map: unexpected type for 1.1: float64" + if err == nil || err.Error() != want { + t.Errorf("hashAuto.Map: %v, want %v", err, want) + } +} + +func TestHashAutoVerify(t *testing.T) { + success, err := hashAuto.Verify(nil, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if !success { + t.Errorf("Verify(): %+v, want true", success) + } +} + +func TestHashAutoVerifyFail(t *testing.T) { + _, err := hashAuto.Verify(nil, 1.1, "\x16k@\xb4J\xbaK\xd6") + want := "hash.Verify: unexpected type for 1.1: float64" + if err == nil || err.Error() != want { + t.Errorf("hashAuto.Verify: %v, want %v", err, want) + } +} + +func TestHashAutoReverseMap(t *testing.T) { + got, err := hashAuto.(planbuilder.Reversible).ReverseMap(nil, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if got.(int64) != 1 { + t.Errorf("ReverseMap(): %+v, want 1", got) + } +} + +type vcursor struct { + mustFail bool + numRows int + result *mproto.QueryResult + query *tproto.BoundQuery +} + +func (vc *vcursor) Execute(query *tproto.BoundQuery) (*mproto.QueryResult, error) { + vc.query = query + if vc.mustFail { + return nil, errors.New("execute failed") + } + switch { + case strings.HasPrefix(query.Sql, "select"): + if vc.result != nil { + return vc.result, nil + } + result := &mproto.QueryResult{ + Fields: []mproto.Field{{ + Type: mproto.VT_LONG, + }}, + RowsAffected: uint64(vc.numRows), + } + for i := 0; i < vc.numRows; i++ { + result.Rows = append(result.Rows, []sqltypes.Value{ + sqltypes.MakeNumeric([]byte(fmt.Sprintf("%d", i+1))), + }) + } + return result, nil + case strings.HasPrefix(query.Sql, "insert"): + return &mproto.QueryResult{InsertId: 1}, nil + case strings.HasPrefix(query.Sql, "delete"): + return &mproto.QueryResult{}, nil + } + panic("unexpected") +} + +func TestHashAutoCreate(t *testing.T) { + vc := &vcursor{} + err := hashAuto.(planbuilder.Functional).Create(vc, 1) + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "insert into t(c) values(:c)", + BindVariables: map[string]interface{}{ + "c": 1, + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestHashAutoCreateFail(t *testing.T) { + vc := &vcursor{mustFail: true} + err := hashAuto.(planbuilder.Functional).Create(vc, 1) + want := "hash.Create: execute failed" + if err == nil || err.Error() != want { + t.Errorf("hashAuto.Create: %v, want %v", err, want) + } +} + +func TestHashAutoGenerate(t *testing.T) { + vc := &vcursor{} + got, err := hashAuto.(planbuilder.FunctionalGenerator).Generate(vc) + if err != nil { + t.Error(err) + } + if got != 1 { + t.Errorf("Generate(): %+v, want 1", got) + } + wantQuery := &tproto.BoundQuery{ + Sql: "insert into t(c) values(:c)", + BindVariables: map[string]interface{}{ + "c": nil, + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestHashAutoGenerateFail(t *testing.T) { + vc := &vcursor{mustFail: true} + _, err := hashAuto.(planbuilder.FunctionalGenerator).Generate(vc) + want := "hash.Generate: execute failed" + if err == nil || err.Error() != want { + t.Errorf("hashAuto.Generate: %v, want %v", err, want) + } +} + +func TestHashAutoDelete(t *testing.T) { + vc := &vcursor{} + err := hashAuto.(planbuilder.Functional).Delete(vc, []interface{}{1}, "") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "delete from t where c in ::c", + BindVariables: map[string]interface{}{ + "c": []interface{}{1}, + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestHashAutoDeleteFail(t *testing.T) { + vc := &vcursor{mustFail: true} + err := hashAuto.(planbuilder.Functional).Delete(vc, []interface{}{1}, "") + want := "hash.Delete: execute failed" + if err == nil || err.Error() != want { + t.Errorf("hashAuto.Delete: %v, want %v", err, want) + } +} diff --git a/go/vt/vtgate/vindexes/hash_test.go b/go/vt/vtgate/vindexes/hash_test.go new file mode 100644 index 00000000000..f4ce7ed1944 --- /dev/null +++ b/go/vt/vtgate/vindexes/hash_test.go @@ -0,0 +1,109 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "reflect" + "testing" + + "github.com/youtube/vitess/go/vt/key" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +var hash planbuilder.Vindex + +func init() { + hv, err := planbuilder.CreateVindex("hash", map[string]interface{}{"Table": "t", "Column": "c"}) + if err != nil { + panic(err) + } + hash = hv +} + +func TestHashCost(t *testing.T) { + if hash.Cost() != 1 { + t.Errorf("Cost(): %d, want 1", hash.Cost()) + } +} + +func TestHashMap(t *testing.T) { + got, err := hash.(planbuilder.Unique).Map(nil, []interface{}{1, int32(2), int64(3), uint(4), uint32(5), uint64(6)}) + if err != nil { + t.Error(err) + } + want := []key.KeyspaceId{ + "\x16k@\xb4J\xbaK\xd6", + "\x06\xe7\xea\"Βp\x8f", + "N\xb1\x90ɢ\xfa\x16\x9c", + "\xd2\xfd\x88g\xd5\r-\xfe", + "p\xbb\x02<\x81\f\xa8z", + "\xf0\x98H\n\xc4ľq", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Map(): %#v, want %+v", got, want) + } +} + +func TestHashVerify(t *testing.T) { + success, err := hash.Verify(nil, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if !success { + t.Errorf("Verify(): %+v, want true", success) + } +} + +func TestHashReverseMap(t *testing.T) { + got, err := hash.(planbuilder.Reversible).ReverseMap(nil, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if got.(int64) != 1 { + t.Errorf("ReverseMap(): %+v, want 1", got) + } +} + +func TestHashCreate(t *testing.T) { + vc := &vcursor{} + err := hash.(planbuilder.Functional).Create(vc, 1) + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "insert into t(c) values(:c)", + BindVariables: map[string]interface{}{ + "c": 1, + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestHashGenerate(t *testing.T) { + _, ok := hash.(planbuilder.FunctionalGenerator) + if ok { + t.Errorf("hash.(planbuilder.FunctionalGenerator): true, want false") + } +} + +func TestHashDelete(t *testing.T) { + vc := &vcursor{} + err := hash.(planbuilder.Functional).Delete(vc, []interface{}{1}, "") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "delete from t where c in ::c", + BindVariables: map[string]interface{}{ + "c": []interface{}{1}, + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} diff --git a/go/vt/vtgate/vindexes/lookup_hash.go b/go/vt/vtgate/vindexes/lookup_hash.go new file mode 100644 index 00000000000..0cfc2384763 --- /dev/null +++ b/go/vt/vtgate/vindexes/lookup_hash.go @@ -0,0 +1,369 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "fmt" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/vt/key" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +func init() { + planbuilder.Register("lookup_hash", NewLookupHash) + planbuilder.Register("lookup_hash_autoinc", NewLookupHashAuto) + planbuilder.Register("lookup_hash_unique", NewLookupHashUnique) + planbuilder.Register("lookup_hash_unique_autoinc", NewLookupHashUniqueAuto) +} + +//==================================================================== + +// LookupHash defines a vindex that uses a lookup table. +// The table is expected to define the id column as unique. It's +// NonUnique and a Lookup. +type LookupHash struct { + lkp lookup +} + +// NewLookupHash creates a LookupHash vindex. +func NewLookupHash(m map[string]interface{}) (planbuilder.Vindex, error) { + lhu := &LookupHash{} + lhu.lkp.Init(m) + return lhu, nil +} + +// Cost returns the cost of this vindex as 20. +func (vind *LookupHash) Cost() int { + return 20 +} + +// Map returns the corresponding KeyspaceId values for the given ids. +func (vind *LookupHash) Map(vcursor planbuilder.VCursor, ids []interface{}) ([][]key.KeyspaceId, error) { + return vind.lkp.Map2(vcursor, ids) +} + +// Verify returns true if id maps to ksid. +func (vind *LookupHash) Verify(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) (bool, error) { + return vind.lkp.Verify(vcursor, id, ksid) +} + +// Create reserves the id by inserting it into the vindex table. +func (vind *LookupHash) Create(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) error { + return vind.lkp.Create(vcursor, id, ksid) +} + +// Delete deletes the entry from the vindex table. +func (vind *LookupHash) Delete(vcursor planbuilder.VCursor, ids []interface{}, ksid key.KeyspaceId) error { + return vind.lkp.Delete(vcursor, ids, ksid) +} + +//==================================================================== + +// LookupHashAuto defines a vindex that uses a lookup table. +// The table is expected to define the id column as unique. It's +// NonUnique and a Lookup. It's also a LookupGenerator, because it +// can use the autoinc capabilities of the lookup table. +type LookupHashAuto struct { + lkp lookup +} + +// NewLookupHashAuto creates a new LookupHashAuto. +func NewLookupHashAuto(m map[string]interface{}) (planbuilder.Vindex, error) { + h := &LookupHashAuto{} + h.lkp.Init(m) + return h, nil +} + +// Cost returns the cost of this index as 20. +func (vind *LookupHashAuto) Cost() int { + return 20 +} + +// Map returns the corresponding KeyspaceId values for the given ids. +func (vind *LookupHashAuto) Map(vcursor planbuilder.VCursor, ids []interface{}) ([][]key.KeyspaceId, error) { + return vind.lkp.Map2(vcursor, ids) +} + +// Verify returns true if id maps to ksid. +func (vind *LookupHashAuto) Verify(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) (bool, error) { + return vind.lkp.Verify(vcursor, id, ksid) +} + +// Create reserves the id by inserting it into the vindex table. +func (vind *LookupHashAuto) Create(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) error { + return vind.lkp.Create(vcursor, id, ksid) +} + +// Generate reserves the id by inserting it into the vindex table. +func (vind *LookupHashAuto) Generate(vcursor planbuilder.VCursor, ksid key.KeyspaceId) (id int64, err error) { + return vind.lkp.Generate(vcursor, ksid) +} + +// Delete deletes the entry from the vindex table. +func (vind *LookupHashAuto) Delete(vcursor planbuilder.VCursor, ids []interface{}, ksid key.KeyspaceId) error { + return vind.lkp.Delete(vcursor, ids, ksid) +} + +//==================================================================== + +// LookupHashUnique defines a vindex that uses a lookup table. +// The table is expected to define the id column as unique. It's +// Unique and a Lookup. +type LookupHashUnique struct { + lkp lookup +} + +// NewLookupHashUnique creates a LookupHashUnique vindex. +func NewLookupHashUnique(m map[string]interface{}) (planbuilder.Vindex, error) { + lhu := &LookupHashUnique{} + lhu.lkp.Init(m) + return lhu, nil +} + +// Cost returns the cost of this vindex as 10. +func (vind *LookupHashUnique) Cost() int { + return 10 +} + +// Map returns the corresponding KeyspaceId values for the given ids. +func (vind *LookupHashUnique) Map(vcursor planbuilder.VCursor, ids []interface{}) ([]key.KeyspaceId, error) { + return vind.lkp.Map1(vcursor, ids) +} + +// Verify returns true if id maps to ksid. +func (vind *LookupHashUnique) Verify(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) (bool, error) { + return vind.lkp.Verify(vcursor, id, ksid) +} + +// Create reserves the id by inserting it into the vindex table. +func (vind *LookupHashUnique) Create(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) error { + return vind.lkp.Create(vcursor, id, ksid) +} + +// Delete deletes the entry from the vindex table. +func (vind *LookupHashUnique) Delete(vcursor planbuilder.VCursor, ids []interface{}, ksid key.KeyspaceId) error { + return vind.lkp.Delete(vcursor, ids, ksid) +} + +//==================================================================== + +// LookupHashUniqueAuto defines a vindex that uses a lookup table. +// The table is expected to define the id column as unique. It's +// Unique and a Lookup. It's also a LookupGenerator, because it +// can use the autoinc capabilities of the lookup table. +type LookupHashUniqueAuto struct { + lkp lookup +} + +// NewLookupHashUniqueAuto creates a new LookupHashUniqueAuto. +func NewLookupHashUniqueAuto(m map[string]interface{}) (planbuilder.Vindex, error) { + h := &LookupHashUniqueAuto{} + h.lkp.Init(m) + return h, nil +} + +// Cost returns the cost of this index as 10. +func (vind *LookupHashUniqueAuto) Cost() int { + return 10 +} + +// Map returns the corresponding KeyspaceId values for the given ids. +func (vind *LookupHashUniqueAuto) Map(vcursor planbuilder.VCursor, ids []interface{}) ([]key.KeyspaceId, error) { + return vind.lkp.Map1(vcursor, ids) +} + +// Verify returns true if id maps to ksid. +func (vind *LookupHashUniqueAuto) Verify(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) (bool, error) { + return vind.lkp.Verify(vcursor, id, ksid) +} + +// Create reserves the id by inserting it into the vindex table. +func (vind *LookupHashUniqueAuto) Create(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) error { + return vind.lkp.Create(vcursor, id, ksid) +} + +// Generate reserves the id by inserting it into the vindex table. +func (vind *LookupHashUniqueAuto) Generate(vcursor planbuilder.VCursor, ksid key.KeyspaceId) (id int64, err error) { + return vind.lkp.Generate(vcursor, ksid) +} + +// Delete deletes the entry from the vindex table. +func (vind *LookupHashUniqueAuto) Delete(vcursor planbuilder.VCursor, ids []interface{}, ksid key.KeyspaceId) error { + return vind.lkp.Delete(vcursor, ids, ksid) +} + +//==================================================================== + +// lookup implements the functions for the Lookup vindexes. +type lookup struct { + Table, From, To string + sel, ver, ins, del string +} + +func (lkp *lookup) Init(m map[string]interface{}) { + get := func(name string) string { + v, _ := m[name].(string) + return v + } + t := get("Table") + from := get("From") + to := get("To") + + lkp.Table = t + lkp.From = from + lkp.To = to + lkp.sel = fmt.Sprintf("select %s from %s where %s = :%s", to, t, from, from) + lkp.ver = fmt.Sprintf("select %s from %s where %s = :%s and %s = :%s", from, t, from, from, to, to) + lkp.ins = fmt.Sprintf("insert into %s(%s, %s) values(:%s, :%s)", t, from, to, from, to) + lkp.del = fmt.Sprintf("delete from %s where %s in ::%s and %s = :%s", t, from, from, to, to) +} + +// Map1 is for a unique vindex. +func (lkp *lookup) Map1(vcursor planbuilder.VCursor, ids []interface{}) ([]key.KeyspaceId, error) { + out := make([]key.KeyspaceId, 0, len(ids)) + bq := &tproto.BoundQuery{ + Sql: lkp.sel, + } + for _, id := range ids { + bq.BindVariables = map[string]interface{}{ + lkp.From: id, + } + result, err := vcursor.Execute(bq) + if err != nil { + return nil, fmt.Errorf("lookup.Map: %v", err) + } + if len(result.Rows) == 0 { + out = append(out, "") + continue + } + if len(result.Rows) != 1 { + return nil, fmt.Errorf("lookup.Map: unexpected multiple results from vindex %s: %v", lkp.Table, id) + } + inum, err := mproto.Convert(result.Fields[0].Type, result.Rows[0][0]) + if err != nil { + return nil, fmt.Errorf("lookup.Map: %v", err) + } + num, err := getNumber(inum) + if err != nil { + return nil, fmt.Errorf("lookup.Map: %v", err) + } + out = append(out, vhash(num)) + } + return out, nil +} + +// Map2 is for a non-unique vindex. +func (lkp *lookup) Map2(vcursor planbuilder.VCursor, ids []interface{}) ([][]key.KeyspaceId, error) { + out := make([][]key.KeyspaceId, 0, len(ids)) + bq := &tproto.BoundQuery{ + Sql: lkp.sel, + } + for _, id := range ids { + bq.BindVariables = map[string]interface{}{ + lkp.From: id, + } + result, err := vcursor.Execute(bq) + if err != nil { + return nil, fmt.Errorf("lookup.Map: %v", err) + } + var ksids []key.KeyspaceId + for _, row := range result.Rows { + inum, err := mproto.Convert(result.Fields[0].Type, row[0]) + if err != nil { + return nil, fmt.Errorf("lookup.Map: %v", err) + } + num, err := getNumber(inum) + if err != nil { + return nil, fmt.Errorf("lookup.Map: %v", err) + } + ksids = append(ksids, vhash(num)) + } + out = append(out, ksids) + } + return out, nil +} + +// Verify returns true if id maps to ksid. +func (lkp *lookup) Verify(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) (bool, error) { + val, err := vunhash(ksid) + if err != nil { + return false, fmt.Errorf("lookup.Verify: %v", err) + } + bq := &tproto.BoundQuery{ + Sql: lkp.ver, + BindVariables: map[string]interface{}{ + lkp.From: id, + lkp.To: val, + }, + } + result, err := vcursor.Execute(bq) + if err != nil { + return false, fmt.Errorf("lookup.Verify: %v", err) + } + if len(result.Rows) == 0 { + return false, nil + } + return true, nil +} + +// Create creates an association between id and ksid by inserting a row in the vindex table. +func (lkp *lookup) Create(vcursor planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) error { + val, err := vunhash(ksid) + if err != nil { + return fmt.Errorf("lookup.Create: %v", err) + } + bq := &tproto.BoundQuery{ + Sql: lkp.ins, + BindVariables: map[string]interface{}{ + lkp.From: id, + lkp.To: val, + }, + } + if _, err := vcursor.Execute(bq); err != nil { + return fmt.Errorf("lookup.Create: %v", err) + } + return nil +} + +// Generate generates an id and associates the ksid to the new id. +func (lkp *lookup) Generate(vcursor planbuilder.VCursor, ksid key.KeyspaceId) (id int64, err error) { + val, err := vunhash(ksid) + if err != nil { + return 0, fmt.Errorf("lookup.Generate: %v", err) + } + bq := &tproto.BoundQuery{ + Sql: lkp.ins, + BindVariables: map[string]interface{}{ + lkp.From: nil, + lkp.To: val, + }, + } + result, err := vcursor.Execute(bq) + if err != nil { + return 0, fmt.Errorf("lookup.Generate: %v", err) + } + return int64(result.InsertId), err +} + +// Delete deletes the association between ids and ksid. +func (lkp *lookup) Delete(vcursor planbuilder.VCursor, ids []interface{}, ksid key.KeyspaceId) error { + val, err := vunhash(ksid) + if err != nil { + return fmt.Errorf("lookup.Delete: %v", err) + } + bq := &tproto.BoundQuery{ + Sql: lkp.del, + BindVariables: map[string]interface{}{ + lkp.From: ids, + lkp.To: val, + }, + } + if _, err := vcursor.Execute(bq); err != nil { + return fmt.Errorf("lookup.Delete: %v", err) + } + return nil +} diff --git a/go/vt/vtgate/vindexes/lookup_hash_auto_test.go b/go/vt/vtgate/vindexes/lookup_hash_auto_test.go new file mode 100644 index 00000000000..c10c19df5c5 --- /dev/null +++ b/go/vt/vtgate/vindexes/lookup_hash_auto_test.go @@ -0,0 +1,175 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "reflect" + "testing" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/sqltypes" + "github.com/youtube/vitess/go/vt/key" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +var lha planbuilder.Vindex + +func init() { + h, err := planbuilder.CreateVindex("lookup_hash_autoinc", map[string]interface{}{"Table": "t", "From": "fromc", "To": "toc"}) + if err != nil { + panic(err) + } + lha = h +} + +func TestLookupHashAutoCost(t *testing.T) { + if lha.Cost() != 20 { + t.Errorf("Cost(): %d, want 20", lha.Cost()) + } +} + +func TestLookupHashAutoMap(t *testing.T) { + vc := &vcursor{numRows: 2} + got, err := lha.(planbuilder.NonUnique).Map(vc, []interface{}{1, int32(2)}) + if err != nil { + t.Error(err) + } + want := [][]key.KeyspaceId{{ + "\x16k@\xb4J\xbaK\xd6", + "\x06\xe7\xea\"Βp\x8f", + }, { + "\x16k@\xb4J\xbaK\xd6", + "\x06\xe7\xea\"Βp\x8f", + }} + if !reflect.DeepEqual(got, want) { + t.Errorf("Map(): %#v, want %+v", got, want) + } +} + +func TestLookupHashAutoMapFail(t *testing.T) { + vc := &vcursor{mustFail: true} + _, err := lha.(planbuilder.NonUnique).Map(vc, []interface{}{1, int32(2)}) + want := "lookup.Map: execute failed" + if err == nil || err.Error() != want { + t.Errorf("lha.Map: %v, want %v", err, want) + } +} + +func TestLookupHashAutoMapBadData(t *testing.T) { + result := &mproto.QueryResult{ + Fields: []mproto.Field{{ + Type: mproto.VT_INT24, + }}, + Rows: [][]sqltypes.Value{ + []sqltypes.Value{ + sqltypes.MakeFractional([]byte("1.1")), + }, + }, + RowsAffected: 1, + } + vc := &vcursor{result: result} + _, err := lha.(planbuilder.NonUnique).Map(vc, []interface{}{1, int32(2)}) + want := `lookup.Map: strconv.ParseUint: parsing "1.1": invalid syntax` + if err == nil || err.Error() != want { + t.Errorf("lha.Map: %v, want %v", err, want) + } + + result.Fields = []mproto.Field{{ + Type: mproto.VT_FLOAT, + }} + vc = &vcursor{result: result} + _, err = lha.(planbuilder.NonUnique).Map(vc, []interface{}{1, int32(2)}) + want = `lookup.Map: unexpected type for 1.1: float64` + if err == nil || err.Error() != want { + t.Errorf("lha.Map: %v, want %v", err, want) + } +} + +func TestLookupHashAutoVerify(t *testing.T) { + vc := &vcursor{numRows: 1} + success, err := lha.Verify(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if !success { + t.Errorf("Verify(): %+v, want true", success) + } +} + +func TestLookupHashAutoVerifyNomatch(t *testing.T) { + vc := &vcursor{} + success, err := lha.Verify(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if success { + t.Errorf("Verify(): %+v, want false", success) + } +} + +func TestLookupHashAutoCreate(t *testing.T) { + vc := &vcursor{} + err := lha.(planbuilder.Lookup).Create(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "insert into t(fromc, toc) values(:fromc, :toc)", + BindVariables: map[string]interface{}{ + "fromc": 1, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestLookupHashAutoGenerate(t *testing.T) { + vc := &vcursor{} + got, err := lha.(planbuilder.LookupGenerator).Generate(vc, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if got != 1 { + t.Errorf("Generate(): %+v, want 1", got) + } + wantQuery := &tproto.BoundQuery{ + Sql: "insert into t(fromc, toc) values(:fromc, :toc)", + BindVariables: map[string]interface{}{ + "fromc": nil, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestLookupHashAutoReverse(t *testing.T) { + _, ok := lha.(planbuilder.Reversible) + if ok { + t.Errorf("lha.(planbuilder.Reversible): true, want false") + } +} + +func TestLookupHashAutoDelete(t *testing.T) { + vc := &vcursor{} + err := lha.(planbuilder.Lookup).Delete(vc, []interface{}{1}, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "delete from t where fromc in ::fromc and toc = :toc", + BindVariables: map[string]interface{}{ + "fromc": []interface{}{1}, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} diff --git a/go/vt/vtgate/vindexes/lookup_hash_test.go b/go/vt/vtgate/vindexes/lookup_hash_test.go new file mode 100644 index 00000000000..7bef8e9c173 --- /dev/null +++ b/go/vt/vtgate/vindexes/lookup_hash_test.go @@ -0,0 +1,109 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "reflect" + "testing" + + "github.com/youtube/vitess/go/vt/key" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +var lhm planbuilder.Vindex + +func init() { + h, err := planbuilder.CreateVindex("lookup_hash", map[string]interface{}{"Table": "t", "From": "fromc", "To": "toc"}) + if err != nil { + panic(err) + } + lhm = h +} + +func TestLookupHashCost(t *testing.T) { + if lhm.Cost() != 20 { + t.Errorf("Cost(): %d, want 20", lhm.Cost()) + } +} + +func TestLookupHashMap(t *testing.T) { + vc := &vcursor{numRows: 2} + got, err := lhm.(planbuilder.NonUnique).Map(vc, []interface{}{1, int32(2)}) + if err != nil { + t.Error(err) + } + want := [][]key.KeyspaceId{{ + "\x16k@\xb4J\xbaK\xd6", + "\x06\xe7\xea\"Βp\x8f", + }, { + "\x16k@\xb4J\xbaK\xd6", + "\x06\xe7\xea\"Βp\x8f", + }} + if !reflect.DeepEqual(got, want) { + t.Errorf("Map(): %#v, want %+v", got, want) + } +} + +func TestLookupHashVerify(t *testing.T) { + vc := &vcursor{numRows: 1} + success, err := lhm.Verify(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if !success { + t.Errorf("Verify(): %+v, want true", success) + } +} + +func TestLookupHashCreate(t *testing.T) { + vc := &vcursor{} + err := lhm.(planbuilder.Lookup).Create(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "insert into t(fromc, toc) values(:fromc, :toc)", + BindVariables: map[string]interface{}{ + "fromc": 1, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestLookupHashGenerate(t *testing.T) { + _, ok := lhm.(planbuilder.LookupGenerator) + if ok { + t.Errorf("lhm.(planbuilder.LookupGenerator): true, want false") + } +} + +func TestLookupHashReverse(t *testing.T) { + _, ok := lhm.(planbuilder.Reversible) + if ok { + t.Errorf("lhm.(planbuilder.Reversible): true, want false") + } +} + +func TestLookupHashDelete(t *testing.T) { + vc := &vcursor{} + err := lhm.(planbuilder.Lookup).Delete(vc, []interface{}{1}, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "delete from t where fromc in ::fromc and toc = :toc", + BindVariables: map[string]interface{}{ + "fromc": []interface{}{1}, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} diff --git a/go/vt/vtgate/vindexes/lookup_hash_unique_auto_test.go b/go/vt/vtgate/vindexes/lookup_hash_unique_auto_test.go new file mode 100644 index 00000000000..dd2e409664e --- /dev/null +++ b/go/vt/vtgate/vindexes/lookup_hash_unique_auto_test.go @@ -0,0 +1,255 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "reflect" + "testing" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/sqltypes" + "github.com/youtube/vitess/go/vt/key" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +var lhua planbuilder.Vindex + +func init() { + h, err := planbuilder.CreateVindex("lookup_hash_unique_autoinc", map[string]interface{}{"Table": "t", "From": "fromc", "To": "toc"}) + if err != nil { + panic(err) + } + lhua = h +} + +func TestLookupHashUniqueAutoCost(t *testing.T) { + if lhua.Cost() != 10 { + t.Errorf("Cost(): %d, want 10", lhua.Cost()) + } +} + +func TestLookupHashUniqueAutoMap(t *testing.T) { + vc := &vcursor{numRows: 1} + got, err := lhua.(planbuilder.Unique).Map(vc, []interface{}{1, int32(2)}) + if err != nil { + t.Error(err) + } + want := []key.KeyspaceId{ + "\x16k@\xb4J\xbaK\xd6", + "\x16k@\xb4J\xbaK\xd6", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Map(): %#v, want %+v", got, want) + } +} + +func TestLookupHashUniqueAutoMapNomatch(t *testing.T) { + vc := &vcursor{} + got, err := lhua.(planbuilder.Unique).Map(vc, []interface{}{1, int32(2)}) + if err != nil { + t.Error(err) + } + want := []key.KeyspaceId{"", ""} + if !reflect.DeepEqual(got, want) { + t.Errorf("Map(): %#v, want %+v", got, want) + } +} + +func TestLookupHashUniqueAutoMapFail(t *testing.T) { + vc := &vcursor{mustFail: true} + _, err := lhua.(planbuilder.Unique).Map(vc, []interface{}{1, int32(2)}) + want := "lookup.Map: execute failed" + if err == nil || err.Error() != want { + t.Errorf("lhua.Map: %v, want %v", err, want) + } +} + +func TestLookupHashUniqueAutoMapBadData(t *testing.T) { + result := &mproto.QueryResult{ + Fields: []mproto.Field{{ + Type: mproto.VT_INT24, + }}, + Rows: [][]sqltypes.Value{ + []sqltypes.Value{ + sqltypes.MakeFractional([]byte("1.1")), + }, + }, + RowsAffected: 1, + } + vc := &vcursor{result: result} + _, err := lhua.(planbuilder.Unique).Map(vc, []interface{}{1, int32(2)}) + want := `lookup.Map: strconv.ParseUint: parsing "1.1": invalid syntax` + if err == nil || err.Error() != want { + t.Errorf("lhua.Map: %v, want %v", err, want) + } + + result.Fields = []mproto.Field{{ + Type: mproto.VT_FLOAT, + }} + vc = &vcursor{result: result} + _, err = lhua.(planbuilder.Unique).Map(vc, []interface{}{1, int32(2)}) + want = `lookup.Map: unexpected type for 1.1: float64` + if err == nil || err.Error() != want { + t.Errorf("lhua.Map: %v, want %v", err, want) + } + + vc = &vcursor{numRows: 2} + _, err = lhua.(planbuilder.Unique).Map(vc, []interface{}{1, int32(2)}) + want = `lookup.Map: unexpected multiple results from vindex t: 1` + if err == nil || err.Error() != want { + t.Errorf("lhua.Map: %v, want %v", err, want) + } +} + +func TestLookupHashUniqueAutoVerify(t *testing.T) { + vc := &vcursor{numRows: 1} + success, err := lhua.Verify(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if !success { + t.Errorf("Verify(): %+v, want true", success) + } +} + +func TestLookupHashUniqueAutoVerifyNomatch(t *testing.T) { + vc := &vcursor{} + success, err := lhua.Verify(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if success { + t.Errorf("Verify(): %+v, want false", success) + } +} + +func TestLookupHashUniqueAutoVerifyFail(t *testing.T) { + vc := &vcursor{mustFail: true} + + _, err := lhua.Verify(vc, 1, "\x16k@\xb4J\xbaK\xd6") + want := "lookup.Verify: execute failed" + if err == nil || err.Error() != want { + t.Errorf("lhua.Verify: %v, want %v", err, want) + } + + _, err = lhua.Verify(vc, 1, "aa") + want = "lookup.Verify: invalid keyspace id: 6161" + if err == nil || err.Error() != want { + t.Errorf("lhua.Verify: %v, want %v", err, want) + } +} + +func TestLookupHashUniqueAutoCreate(t *testing.T) { + vc := &vcursor{} + err := lhua.(planbuilder.Lookup).Create(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "insert into t(fromc, toc) values(:fromc, :toc)", + BindVariables: map[string]interface{}{ + "fromc": 1, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestLookupHashUniqueAutoCreateFail(t *testing.T) { + vc := &vcursor{mustFail: true} + + err := lhua.(planbuilder.Lookup).Create(vc, 1, "\x16k@\xb4J\xbaK\xd6") + want := "lookup.Create: execute failed" + if err == nil || err.Error() != want { + t.Errorf("lhua.Create: %v, want %v", err, want) + } + + err = lhua.(planbuilder.Lookup).Create(vc, 1, "aa") + want = "lookup.Create: invalid keyspace id: 6161" + if err == nil || err.Error() != want { + t.Errorf("lhua.Create: %v, want %v", err, want) + } +} + +func TestLookupHashUniqueAutoGenerate(t *testing.T) { + vc := &vcursor{} + got, err := lhua.(planbuilder.LookupGenerator).Generate(vc, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if got != 1 { + t.Errorf("Generate(): %+v, want 1", got) + } + wantQuery := &tproto.BoundQuery{ + Sql: "insert into t(fromc, toc) values(:fromc, :toc)", + BindVariables: map[string]interface{}{ + "fromc": nil, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestLookupHashUniqueAutoGenerateFail(t *testing.T) { + vc := &vcursor{mustFail: true} + + _, err := lhua.(planbuilder.LookupGenerator).Generate(vc, "\x16k@\xb4J\xbaK\xd6") + want := "lookup.Generate: execute failed" + if err == nil || err.Error() != want { + t.Errorf("lhua.Generate: %v, want %v", err, want) + } + + _, err = lhua.(planbuilder.LookupGenerator).Generate(vc, "aa") + want = "lookup.Generate: invalid keyspace id: 6161" + if err == nil || err.Error() != want { + t.Errorf("lhua.Generate: %v, want %v", err, want) + } +} + +func TestLookupHashUniqueAutoReverse(t *testing.T) { + _, ok := lhua.(planbuilder.Reversible) + if ok { + t.Errorf("lhua.(planbuilder.Reversible): true, want false") + } +} + +func TestLookupHashUniqueAutoDelete(t *testing.T) { + vc := &vcursor{} + err := lhua.(planbuilder.Lookup).Delete(vc, []interface{}{1}, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "delete from t where fromc in ::fromc and toc = :toc", + BindVariables: map[string]interface{}{ + "fromc": []interface{}{1}, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestLookupHashUniqueAutoDeleteFail(t *testing.T) { + vc := &vcursor{mustFail: true} + + err := lhua.(planbuilder.Lookup).Delete(vc, []interface{}{1}, "\x16k@\xb4J\xbaK\xd6") + want := "lookup.Delete: execute failed" + if err == nil || err.Error() != want { + t.Errorf("lhua.Delete: %v, want %v", err, want) + } + + err = lhua.(planbuilder.Lookup).Delete(vc, []interface{}{1}, "aa") + want = "lookup.Delete: invalid keyspace id: 6161" + if err == nil || err.Error() != want { + t.Errorf("lhua.Delete: %v, want %v", err, want) + } +} diff --git a/go/vt/vtgate/vindexes/lookup_hash_unique_test.go b/go/vt/vtgate/vindexes/lookup_hash_unique_test.go new file mode 100644 index 00000000000..80e9bbce830 --- /dev/null +++ b/go/vt/vtgate/vindexes/lookup_hash_unique_test.go @@ -0,0 +1,106 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "reflect" + "testing" + + "github.com/youtube/vitess/go/vt/key" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +var lhu planbuilder.Vindex + +func init() { + h, err := planbuilder.CreateVindex("lookup_hash_unique", map[string]interface{}{"Table": "t", "From": "fromc", "To": "toc"}) + if err != nil { + panic(err) + } + lhu = h +} + +func TestLookupHashUniqueCost(t *testing.T) { + if lhu.Cost() != 10 { + t.Errorf("Cost(): %d, want 10", lhu.Cost()) + } +} + +func TestLookupHashUniqueMap(t *testing.T) { + vc := &vcursor{numRows: 1} + got, err := lhu.(planbuilder.Unique).Map(vc, []interface{}{1, int32(2)}) + if err != nil { + t.Error(err) + } + want := []key.KeyspaceId{ + "\x16k@\xb4J\xbaK\xd6", + "\x16k@\xb4J\xbaK\xd6", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Map(): %#v, want %+v", got, want) + } +} + +func TestLookupHashUniqueVerify(t *testing.T) { + vc := &vcursor{numRows: 1} + success, err := lhu.Verify(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + if !success { + t.Errorf("Verify(): %+v, want true", success) + } +} + +func TestLookupHashUniqueCreate(t *testing.T) { + vc := &vcursor{} + err := lhu.(planbuilder.Lookup).Create(vc, 1, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "insert into t(fromc, toc) values(:fromc, :toc)", + BindVariables: map[string]interface{}{ + "fromc": 1, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} + +func TestLookupHashUniqueGenerate(t *testing.T) { + _, ok := lhu.(planbuilder.LookupGenerator) + if ok { + t.Errorf("lhu.(planbuilder.LookupGenerator): true, want false") + } +} + +func TestLookupHashUniqueReverse(t *testing.T) { + _, ok := lhu.(planbuilder.Reversible) + if ok { + t.Errorf("lhu.(planbuilder.Reversible): true, want false") + } +} + +func TestLookupHashUniqueDelete(t *testing.T) { + vc := &vcursor{} + err := lhu.(planbuilder.Lookup).Delete(vc, []interface{}{1}, "\x16k@\xb4J\xbaK\xd6") + if err != nil { + t.Error(err) + } + wantQuery := &tproto.BoundQuery{ + Sql: "delete from t where fromc in ::fromc and toc = :toc", + BindVariables: map[string]interface{}{ + "fromc": []interface{}{1}, + "toc": int64(1), + }, + } + if !reflect.DeepEqual(vc.query, wantQuery) { + t.Errorf("vc.query = %#v, want %#v", vc.query, wantQuery) + } +} diff --git a/go/vt/vtgate/vindexes/numeric.go b/go/vt/vtgate/vindexes/numeric.go new file mode 100644 index 00000000000..0eef8a6818f --- /dev/null +++ b/go/vt/vtgate/vindexes/numeric.go @@ -0,0 +1,65 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "encoding/binary" + "fmt" + + "github.com/youtube/vitess/go/vt/key" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +// Numeric defines a bit-pattern mapping of a uint64 to the KeyspaceId. +// It's Unique and Reversible. +type Numeric struct{} + +// NewNumeric creates a Numeric vindex. +func NewNumeric(_ map[string]interface{}) (planbuilder.Vindex, error) { + return Numeric{}, nil +} + +// Cost returns the cost of this vindex as 0. +func (Numeric) Cost() int { + return 0 +} + +// Verify returns true if id and ksid match. +func (Numeric) Verify(_ planbuilder.VCursor, id interface{}, ksid key.KeyspaceId) (bool, error) { + var keybytes [8]byte + num, err := getNumber(id) + if err != nil { + return false, fmt.Errorf("Numeric.Verify: %v", err) + } + binary.BigEndian.PutUint64(keybytes[:], uint64(num)) + return key.KeyspaceId(keybytes[:]) == ksid, nil +} + +// Map returns the associated keyspae ids for the given ids. +func (Numeric) Map(_ planbuilder.VCursor, ids []interface{}) ([]key.KeyspaceId, error) { + var keybytes [8]byte + out := make([]key.KeyspaceId, 0, len(ids)) + for _, id := range ids { + num, err := getNumber(id) + if err != nil { + return nil, fmt.Errorf("Numeric.Map: %v", err) + } + binary.BigEndian.PutUint64(keybytes[:], uint64(num)) + out = append(out, key.KeyspaceId(keybytes[:])) + } + return out, nil +} + +// ReverseMap returns the associated id for the ksid. +func (Numeric) ReverseMap(_ planbuilder.VCursor, ksid key.KeyspaceId) (interface{}, error) { + if len(ksid) != 8 { + return nil, fmt.Errorf("Numeric.ReverseMap: length of keyspace is not 8: %d", len(ksid)) + } + return binary.BigEndian.Uint64([]byte(ksid)), nil +} + +func init() { + planbuilder.Register("numeric", NewNumeric) +} diff --git a/go/vt/vtgate/vindexes/numeric_test.go b/go/vt/vtgate/vindexes/numeric_test.go new file mode 100644 index 00000000000..37353fc58ac --- /dev/null +++ b/go/vt/vtgate/vindexes/numeric_test.go @@ -0,0 +1,109 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vindexes + +import ( + "reflect" + "testing" + + "github.com/youtube/vitess/go/vt/key" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" +) + +var numeric planbuilder.Vindex + +func init() { + numeric, _ = planbuilder.CreateVindex("numeric", nil) +} + +func TestNumericCost(t *testing.T) { + if numeric.Cost() != 0 { + t.Errorf("Cost(): %d, want 0", numeric.Cost()) + } +} + +func TestNumericMap(t *testing.T) { + got, err := numeric.(planbuilder.Unique).Map(nil, []interface{}{1, int32(2), int64(3), uint(4), uint32(5), uint64(6)}) + if err != nil { + t.Error(err) + } + want := []key.KeyspaceId{ + "\x00\x00\x00\x00\x00\x00\x00\x01", + "\x00\x00\x00\x00\x00\x00\x00\x02", + "\x00\x00\x00\x00\x00\x00\x00\x03", + "\x00\x00\x00\x00\x00\x00\x00\x04", + "\x00\x00\x00\x00\x00\x00\x00\x05", + "\x00\x00\x00\x00\x00\x00\x00\x06", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Map(): %#v, want %+v", got, want) + } +} + +func TestNumericMapBadData(t *testing.T) { + _, err := numeric.(planbuilder.Unique).Map(nil, []interface{}{1.1}) + want := `Numeric.Map: unexpected type for 1.1: float64` + if err == nil || err.Error() != want { + t.Errorf("numeric.Map: %v, want %v", err, want) + } +} + +func TestNumericVerify(t *testing.T) { + success, err := numeric.Verify(nil, 1, "\x00\x00\x00\x00\x00\x00\x00\x01") + if err != nil { + t.Error(err) + } + if !success { + t.Errorf("Verify(): %+v, want true", success) + } +} + +func TestNumericVerifyBadData(t *testing.T) { + _, err := numeric.Verify(nil, 1.1, "\x00\x00\x00\x00\x00\x00\x00\x01") + want := `Numeric.Verify: unexpected type for 1.1: float64` + if err == nil || err.Error() != want { + t.Errorf("numeric.Map: %v, want %v", err, want) + } +} + +func TestNumericCreate(t *testing.T) { + _, ok := numeric.(planbuilder.Functional) + if ok { + t.Errorf("numeric.(planbuilder.Functional): true, want false") + } + _, ok = numeric.(planbuilder.Lookup) + if ok { + t.Errorf("numeric.(planbuilder.Lookup): true, want false") + } +} + +func TestNumericGenerate(t *testing.T) { + _, ok := numeric.(planbuilder.FunctionalGenerator) + if ok { + t.Errorf("numeric.(planbuilder.FunctionalGenerator): true, want false") + } + _, ok = numeric.(planbuilder.LookupGenerator) + if ok { + t.Errorf("numeric.(planbuilder.LookupGenerator): true, want false") + } +} + +func TestNumericReverseMap(t *testing.T) { + got, err := numeric.(planbuilder.Reversible).ReverseMap(nil, "\x00\x00\x00\x00\x00\x00\x00\x01") + if err != nil { + t.Error(err) + } + if got.(uint64) != 1 { + t.Errorf("ReverseMap(): %+v, want 1", got) + } +} + +func TestNumericReverseMapBadData(t *testing.T) { + _, err := numeric.(planbuilder.Reversible).ReverseMap(nil, "aa") + want := `Numeric.ReverseMap: length of keyspace is not 8: 2` + if err == nil || err.Error() != want { + t.Errorf("numeric.Map: %v, want %v", err, want) + } +} diff --git a/go/vt/vtgate/vtgate.go b/go/vt/vtgate/vtgate.go index 713b6584e64..2ec6f7518da 100644 --- a/go/vt/vtgate/vtgate.go +++ b/go/vt/vtgate/vtgate.go @@ -13,7 +13,6 @@ import ( "strings" "time" - "code.google.com/p/go.net/context" log "github.com/golang/glog" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/stats" @@ -21,24 +20,29 @@ import ( "github.com/youtube/vitess/go/tb" kproto "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/logutil" + "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" "github.com/youtube/vitess/go/vt/vtgate/proto" + _ "github.com/youtube/vitess/go/vt/vtgate/vindexes" + "golang.org/x/net/context" ) const errDupKey = "errno 1062" +const errTxPoolFull = "tx_pool_full" var ( - RpcVTGate *VTGate + rpcVTGate *VTGate - QPSByOperation *stats.Rates - QPSByKeyspace *stats.Rates - QPSByDbType *stats.Rates + qpsByOperation *stats.Rates + qpsByKeyspace *stats.Rates + qpsByDbType *stats.Rates - ErrorsByOperation *stats.Rates - ErrorsByKeyspace *stats.Rates - ErrorsByDbType *stats.Rates + errorsByOperation *stats.Rates + errorsByKeyspace *stats.Rates + errorsByDbType *stats.Rates - ErrTooManyInFlight = errors.New("request_backlog: too many requests in flight") + errTooManyInFlight = errors.New("request_backlog: too many requests in flight") // Error counters should be global so they can be set from anywhere normalErrors *stats.MultiCounters @@ -50,6 +54,7 @@ var ( // can be created. type VTGate struct { resolver *Resolver + router *Router timings *stats.MultiTimings rowsReturned *stats.MultiCounters @@ -57,27 +62,31 @@ type VTGate struct { inFlight sync2.AtomicInt64 // the throttled loggers for all errors, one per API entry + logExecute *logutil.ThrottledLogger logExecuteShard *logutil.ThrottledLogger logExecuteKeyspaceIds *logutil.ThrottledLogger logExecuteKeyRanges *logutil.ThrottledLogger logExecuteEntityIds *logutil.ThrottledLogger logExecuteBatchShard *logutil.ThrottledLogger logExecuteBatchKeyspaceIds *logutil.ThrottledLogger + logStreamExecute *logutil.ThrottledLogger logStreamExecuteKeyspaceIds *logutil.ThrottledLogger logStreamExecuteKeyRanges *logutil.ThrottledLogger logStreamExecuteShard *logutil.ThrottledLogger } -// registration mechanism +// RegisterVTGate defines the type of registration mechanism. type RegisterVTGate func(*VTGate) +// RegisterVTGates stores register funcs for VTGate server. var RegisterVTGates []RegisterVTGate -func Init(serv SrvTopoServer, cell string, retryDelay time.Duration, retryCount int, timeout time.Duration, maxInFlight int) { - if RpcVTGate != nil { +// Init initializes VTGate server. +func Init(serv SrvTopoServer, schema *planbuilder.Schema, cell string, retryDelay time.Duration, retryCount int, timeout time.Duration, maxInFlight int) { + if rpcVTGate != nil { log.Fatalf("VTGate already initialized") } - RpcVTGate = &VTGate{ + rpcVTGate = &VTGate{ resolver: NewResolver(serv, "VttabletCall", cell, retryDelay, retryCount, timeout), timings: stats.NewMultiTimings("VtgateApi", []string{"Operation", "Keyspace", "DbType"}), rowsReturned: stats.NewMultiCounters("VtgateApiRowsReturned", []string{"Operation", "Keyspace", "DbType"}), @@ -85,30 +94,34 @@ func Init(serv SrvTopoServer, cell string, retryDelay time.Duration, retryCount maxInFlight: int64(maxInFlight), inFlight: 0, + logExecute: logutil.NewThrottledLogger("Execute", 5*time.Second), logExecuteShard: logutil.NewThrottledLogger("ExecuteShard", 5*time.Second), logExecuteKeyspaceIds: logutil.NewThrottledLogger("ExecuteKeyspaceIds", 5*time.Second), logExecuteKeyRanges: logutil.NewThrottledLogger("ExecuteKeyRanges", 5*time.Second), logExecuteEntityIds: logutil.NewThrottledLogger("ExecuteEntityIds", 5*time.Second), logExecuteBatchShard: logutil.NewThrottledLogger("ExecuteBatchShard", 5*time.Second), logExecuteBatchKeyspaceIds: logutil.NewThrottledLogger("ExecuteBatchKeyspaceIds", 5*time.Second), + logStreamExecute: logutil.NewThrottledLogger("StreamExecute", 5*time.Second), logStreamExecuteKeyspaceIds: logutil.NewThrottledLogger("StreamExecuteKeyspaceIds", 5*time.Second), logStreamExecuteKeyRanges: logutil.NewThrottledLogger("StreamExecuteKeyRanges", 5*time.Second), logStreamExecuteShard: logutil.NewThrottledLogger("StreamExecuteShard", 5*time.Second), } + // Resuse resolver's scatterConn. + rpcVTGate.router = NewRouter(serv, cell, schema, "VTGateRouter", rpcVTGate.resolver.scatterConn) normalErrors = stats.NewMultiCounters("VtgateApiErrorCounts", []string{"Operation", "Keyspace", "DbType"}) infoErrors = stats.NewCounters("VtgateInfoErrorCounts") internalErrors = stats.NewCounters("VtgateInternalErrorCounts") - QPSByOperation = stats.NewRates("QPSByOperation", stats.CounterForDimension(RpcVTGate.timings, "Operation"), 15, 1*time.Minute) - QPSByKeyspace = stats.NewRates("QPSByKeyspace", stats.CounterForDimension(RpcVTGate.timings, "Keyspace"), 15, 1*time.Minute) - QPSByDbType = stats.NewRates("QPSByDbType", stats.CounterForDimension(RpcVTGate.timings, "DbType"), 15, 1*time.Minute) + qpsByOperation = stats.NewRates("QPSByOperation", stats.CounterForDimension(rpcVTGate.timings, "Operation"), 15, 1*time.Minute) + qpsByKeyspace = stats.NewRates("QPSByKeyspace", stats.CounterForDimension(rpcVTGate.timings, "Keyspace"), 15, 1*time.Minute) + qpsByDbType = stats.NewRates("QPSByDbType", stats.CounterForDimension(rpcVTGate.timings, "DbType"), 15, 1*time.Minute) - ErrorsByOperation = stats.NewRates("ErrorsByOperation", stats.CounterForDimension(normalErrors, "Operation"), 15, 1*time.Minute) - ErrorsByKeyspace = stats.NewRates("ErrorsByKeyspace", stats.CounterForDimension(normalErrors, "Keyspace"), 15, 1*time.Minute) - ErrorsByDbType = stats.NewRates("ErrorsByDbType", stats.CounterForDimension(normalErrors, "DbType"), 15, 1*time.Minute) + errorsByOperation = stats.NewRates("ErrorsByOperation", stats.CounterForDimension(normalErrors, "Operation"), 15, 1*time.Minute) + errorsByKeyspace = stats.NewRates("ErrorsByKeyspace", stats.CounterForDimension(normalErrors, "Keyspace"), 15, 1*time.Minute) + errorsByDbType = stats.NewRates("ErrorsByDbType", stats.CounterForDimension(normalErrors, "DbType"), 15, 1*time.Minute) for _, f := range RegisterVTGates { - f(RpcVTGate) + f(rpcVTGate) } } @@ -128,8 +141,33 @@ func (vtg *VTGate) InitializeConnections(ctx context.Context) (err error) { return nil } +// Execute executes a non-streaming query by routing based on the values in the query. +func (vtg *VTGate) Execute(ctx context.Context, query *proto.Query, reply *proto.QueryResult) (err error) { + defer handlePanic(&err) + + startTime := time.Now() + statsKey := []string{"Execute", "Any", string(query.TabletType)} + defer vtg.timings.Record(statsKey, startTime) + + x := vtg.inFlight.Add(1) + defer vtg.inFlight.Add(-1) + if 0 < vtg.maxInFlight && vtg.maxInFlight < x { + return errTooManyInFlight + } + + qr, err := vtg.router.Execute(ctx, query) + if err == nil { + reply.Result = qr + vtg.rowsReturned.Add(statsKey, int64(len(qr.Rows))) + } else { + reply.Error = handleExecuteError(err, statsKey, query, vtg.logExecute) + } + reply.Session = query.Session + return nil +} + // ExecuteShard executes a non-streaming query on the specified shards. -func (vtg *VTGate) ExecuteShard(context context.Context, query *proto.QueryShard, reply *proto.QueryResult) (err error) { +func (vtg *VTGate) ExecuteShard(ctx context.Context, query *proto.QueryShard, reply *proto.QueryResult) (err error) { defer handlePanic(&err) startTime := time.Now() @@ -139,11 +177,11 @@ func (vtg *VTGate) ExecuteShard(context context.Context, query *proto.QueryShard x := vtg.inFlight.Add(1) defer vtg.inFlight.Add(-1) if 0 < vtg.maxInFlight && vtg.maxInFlight < x { - return ErrTooManyInFlight + return errTooManyInFlight } qr, err := vtg.resolver.Execute( - context, + ctx, query.Sql, query.BindVariables, query.Keyspace, @@ -157,20 +195,14 @@ func (vtg *VTGate) ExecuteShard(context context.Context, query *proto.QueryShard reply.Result = qr vtg.rowsReturned.Add(statsKey, int64(len(qr.Rows))) } else { - reply.Error = err.Error() - if strings.Contains(reply.Error, errDupKey) { - infoErrors.Add("DupKey", 1) - } else { - normalErrors.Add(statsKey, 1) - vtg.logExecuteShard.Errorf("%v, query: %+v", err, query) - } + reply.Error = handleExecuteError(err, statsKey, query, vtg.logExecuteShard) } reply.Session = query.Session return nil } // ExecuteKeyspaceIds executes a non-streaming query based on the specified keyspace ids. -func (vtg *VTGate) ExecuteKeyspaceIds(context context.Context, query *proto.KeyspaceIdQuery, reply *proto.QueryResult) (err error) { +func (vtg *VTGate) ExecuteKeyspaceIds(ctx context.Context, query *proto.KeyspaceIdQuery, reply *proto.QueryResult) (err error) { defer handlePanic(&err) startTime := time.Now() @@ -180,28 +212,22 @@ func (vtg *VTGate) ExecuteKeyspaceIds(context context.Context, query *proto.Keys x := vtg.inFlight.Add(1) defer vtg.inFlight.Add(-1) if 0 < vtg.maxInFlight && vtg.maxInFlight < x { - return ErrTooManyInFlight + return errTooManyInFlight } - qr, err := vtg.resolver.ExecuteKeyspaceIds(context, query) + qr, err := vtg.resolver.ExecuteKeyspaceIds(ctx, query) if err == nil { reply.Result = qr vtg.rowsReturned.Add(statsKey, int64(len(qr.Rows))) } else { - reply.Error = err.Error() - if strings.Contains(reply.Error, errDupKey) { - infoErrors.Add("DupKey", 1) - } else { - normalErrors.Add(statsKey, 1) - vtg.logExecuteKeyspaceIds.Errorf("%v, query: %+v", err, query) - } + reply.Error = handleExecuteError(err, statsKey, query, vtg.logExecuteKeyspaceIds) } reply.Session = query.Session return nil } // ExecuteKeyRanges executes a non-streaming query based on the specified keyranges. -func (vtg *VTGate) ExecuteKeyRanges(context context.Context, query *proto.KeyRangeQuery, reply *proto.QueryResult) (err error) { +func (vtg *VTGate) ExecuteKeyRanges(ctx context.Context, query *proto.KeyRangeQuery, reply *proto.QueryResult) (err error) { defer handlePanic(&err) startTime := time.Now() @@ -211,28 +237,22 @@ func (vtg *VTGate) ExecuteKeyRanges(context context.Context, query *proto.KeyRan x := vtg.inFlight.Add(1) defer vtg.inFlight.Add(-1) if 0 < vtg.maxInFlight && vtg.maxInFlight < x { - return ErrTooManyInFlight + return errTooManyInFlight } - qr, err := vtg.resolver.ExecuteKeyRanges(context, query) + qr, err := vtg.resolver.ExecuteKeyRanges(ctx, query) if err == nil { reply.Result = qr vtg.rowsReturned.Add(statsKey, int64(len(qr.Rows))) } else { - reply.Error = err.Error() - if strings.Contains(reply.Error, errDupKey) { - infoErrors.Add("DupKey", 1) - } else { - normalErrors.Add(statsKey, 1) - vtg.logExecuteKeyRanges.Errorf("%v, query: %+v", err, query) - } + reply.Error = handleExecuteError(err, statsKey, query, vtg.logExecuteKeyRanges) } reply.Session = query.Session return nil } // ExecuteEntityIds excutes a non-streaming query based on given KeyspaceId map. -func (vtg *VTGate) ExecuteEntityIds(context context.Context, query *proto.EntityIdsQuery, reply *proto.QueryResult) (err error) { +func (vtg *VTGate) ExecuteEntityIds(ctx context.Context, query *proto.EntityIdsQuery, reply *proto.QueryResult) (err error) { defer handlePanic(&err) startTime := time.Now() @@ -242,28 +262,22 @@ func (vtg *VTGate) ExecuteEntityIds(context context.Context, query *proto.Entity x := vtg.inFlight.Add(1) defer vtg.inFlight.Add(-1) if 0 < vtg.maxInFlight && vtg.maxInFlight < x { - return ErrTooManyInFlight + return errTooManyInFlight } - qr, err := vtg.resolver.ExecuteEntityIds(context, query) + qr, err := vtg.resolver.ExecuteEntityIds(ctx, query) if err == nil { reply.Result = qr vtg.rowsReturned.Add(statsKey, int64(len(qr.Rows))) } else { - reply.Error = err.Error() - if strings.Contains(reply.Error, errDupKey) { - infoErrors.Add("DupKey", 1) - } else { - normalErrors.Add(statsKey, 1) - vtg.logExecuteEntityIds.Errorf("%v, query: %+v", err, query) - } + reply.Error = handleExecuteError(err, statsKey, query, vtg.logExecuteEntityIds) } reply.Session = query.Session return nil } // ExecuteBatchShard executes a group of queries on the specified shards. -func (vtg *VTGate) ExecuteBatchShard(context context.Context, batchQuery *proto.BatchQueryShard, reply *proto.QueryResultList) (err error) { +func (vtg *VTGate) ExecuteBatchShard(ctx context.Context, batchQuery *proto.BatchQueryShard, reply *proto.QueryResultList) (err error) { defer handlePanic(&err) startTime := time.Now() @@ -273,11 +287,11 @@ func (vtg *VTGate) ExecuteBatchShard(context context.Context, batchQuery *proto. x := vtg.inFlight.Add(1) defer vtg.inFlight.Add(-1) if 0 < vtg.maxInFlight && vtg.maxInFlight < x { - return ErrTooManyInFlight + return errTooManyInFlight } qrs, err := vtg.resolver.ExecuteBatch( - context, + ctx, batchQuery.Queries, batchQuery.Keyspace, batchQuery.TabletType, @@ -294,20 +308,14 @@ func (vtg *VTGate) ExecuteBatchShard(context context.Context, batchQuery *proto. } vtg.rowsReturned.Add(statsKey, rowCount) } else { - reply.Error = err.Error() - if strings.Contains(reply.Error, errDupKey) { - infoErrors.Add("DupKey", 1) - } else { - normalErrors.Add(statsKey, 1) - vtg.logExecuteBatchShard.Errorf("%v, queries: %+v", err, batchQuery) - } + reply.Error = handleExecuteError(err, statsKey, batchQuery, vtg.logExecuteBatchShard) } reply.Session = batchQuery.Session return nil } // ExecuteBatchKeyspaceIds executes a group of queries based on the specified keyspace ids. -func (vtg *VTGate) ExecuteBatchKeyspaceIds(context context.Context, query *proto.KeyspaceIdBatchQuery, reply *proto.QueryResultList) (err error) { +func (vtg *VTGate) ExecuteBatchKeyspaceIds(ctx context.Context, query *proto.KeyspaceIdBatchQuery, reply *proto.QueryResultList) (err error) { defer handlePanic(&err) startTime := time.Now() @@ -317,11 +325,11 @@ func (vtg *VTGate) ExecuteBatchKeyspaceIds(context context.Context, query *proto x := vtg.inFlight.Add(1) defer vtg.inFlight.Add(-1) if 0 < vtg.maxInFlight && vtg.maxInFlight < x { - return ErrTooManyInFlight + return errTooManyInFlight } qrs, err := vtg.resolver.ExecuteBatchKeyspaceIds( - context, + ctx, query) if err == nil { reply.List = qrs.List @@ -331,25 +339,58 @@ func (vtg *VTGate) ExecuteBatchKeyspaceIds(context context.Context, query *proto } vtg.rowsReturned.Add(statsKey, rowCount) } else { - reply.Error = err.Error() - if strings.Contains(reply.Error, errDupKey) { - infoErrors.Add("DupKey", 1) - } else { - normalErrors.Add(statsKey, 1) - vtg.logExecuteBatchKeyspaceIds.Errorf("%v, query: %+v", err, query) - } + reply.Error = handleExecuteError(err, statsKey, query, vtg.logExecuteBatchKeyspaceIds) } reply.Session = query.Session return nil } +// StreamExecute executes a streaming query by routing based on the values in the query. +func (vtg *VTGate) StreamExecute(ctx context.Context, query *proto.Query, sendReply func(*proto.QueryResult) error) (err error) { + defer handlePanic(&err) + + startTime := time.Now() + statsKey := []string{"StreamExecute", "Any", string(query.TabletType)} + defer vtg.timings.Record(statsKey, startTime) + + x := vtg.inFlight.Add(1) + defer vtg.inFlight.Add(-1) + if 0 < vtg.maxInFlight && vtg.maxInFlight < x { + return errTooManyInFlight + } + + var rowCount int64 + err = vtg.router.StreamExecute( + ctx, + query, + func(mreply *mproto.QueryResult) error { + reply := new(proto.QueryResult) + reply.Result = mreply + rowCount += int64(len(mreply.Rows)) + // Note we don't populate reply.Session here, + // as it may change incrementaly as responses are sent. + return sendReply(reply) + }) + vtg.rowsReturned.Add(statsKey, rowCount) + + if err != nil { + normalErrors.Add(statsKey, 1) + vtg.logStreamExecute.Errorf("%v, query: %+v", err, query) + } + // Now we can send the final Sessoin info. + if query.Session != nil { + sendReply(&proto.QueryResult{Session: query.Session}) + } + return formatError(err) +} + // StreamExecuteKeyspaceIds executes a streaming query on the specified KeyspaceIds. // The KeyspaceIds are resolved to shards using the serving graph. // This function currently temporarily enforces the restriction of executing on // one shard since it cannot merge-sort the results to guarantee ordering of // response which is needed for checkpointing. // The api supports supplying multiple KeyspaceIds to make it future proof. -func (vtg *VTGate) StreamExecuteKeyspaceIds(context context.Context, query *proto.KeyspaceIdQuery, sendReply func(*proto.QueryResult) error) (err error) { +func (vtg *VTGate) StreamExecuteKeyspaceIds(ctx context.Context, query *proto.KeyspaceIdQuery, sendReply func(*proto.QueryResult) error) (err error) { defer handlePanic(&err) startTime := time.Now() @@ -359,12 +400,12 @@ func (vtg *VTGate) StreamExecuteKeyspaceIds(context context.Context, query *prot x := vtg.inFlight.Add(1) defer vtg.inFlight.Add(-1) if 0 < vtg.maxInFlight && vtg.maxInFlight < x { - return ErrTooManyInFlight + return errTooManyInFlight } var rowCount int64 err = vtg.resolver.StreamExecuteKeyspaceIds( - context, + ctx, query, func(mreply *mproto.QueryResult) error { reply := new(proto.QueryResult) @@ -384,7 +425,7 @@ func (vtg *VTGate) StreamExecuteKeyspaceIds(context context.Context, query *prot if query.Session != nil { sendReply(&proto.QueryResult{Session: query.Session}) } - return err + return formatError(err) } // StreamExecuteKeyRanges executes a streaming query on the specified KeyRanges. @@ -393,7 +434,7 @@ func (vtg *VTGate) StreamExecuteKeyspaceIds(context context.Context, query *prot // one shard since it cannot merge-sort the results to guarantee ordering of // response which is needed for checkpointing. // The api supports supplying multiple keyranges to make it future proof. -func (vtg *VTGate) StreamExecuteKeyRanges(context context.Context, query *proto.KeyRangeQuery, sendReply func(*proto.QueryResult) error) (err error) { +func (vtg *VTGate) StreamExecuteKeyRanges(ctx context.Context, query *proto.KeyRangeQuery, sendReply func(*proto.QueryResult) error) (err error) { defer handlePanic(&err) startTime := time.Now() @@ -403,12 +444,12 @@ func (vtg *VTGate) StreamExecuteKeyRanges(context context.Context, query *proto. x := vtg.inFlight.Add(1) defer vtg.inFlight.Add(-1) if 0 < vtg.maxInFlight && vtg.maxInFlight < x { - return ErrTooManyInFlight + return errTooManyInFlight } var rowCount int64 err = vtg.resolver.StreamExecuteKeyRanges( - context, + ctx, query, func(mreply *mproto.QueryResult) error { reply := new(proto.QueryResult) @@ -428,11 +469,11 @@ func (vtg *VTGate) StreamExecuteKeyRanges(context context.Context, query *proto. if query.Session != nil { sendReply(&proto.QueryResult{Session: query.Session}) } - return err + return formatError(err) } // StreamExecuteShard executes a streaming query on the specified shards. -func (vtg *VTGate) StreamExecuteShard(context context.Context, query *proto.QueryShard, sendReply func(*proto.QueryResult) error) (err error) { +func (vtg *VTGate) StreamExecuteShard(ctx context.Context, query *proto.QueryShard, sendReply func(*proto.QueryResult) error) (err error) { defer handlePanic(&err) startTime := time.Now() @@ -442,12 +483,12 @@ func (vtg *VTGate) StreamExecuteShard(context context.Context, query *proto.Quer x := vtg.inFlight.Add(1) defer vtg.inFlight.Add(-1) if 0 < vtg.maxInFlight && vtg.maxInFlight < x { - return ErrTooManyInFlight + return errTooManyInFlight } var rowCount int64 err = vtg.resolver.StreamExecute( - context, + ctx, query.Sql, query.BindVariables, query.Keyspace, @@ -474,26 +515,26 @@ func (vtg *VTGate) StreamExecuteShard(context context.Context, query *proto.Quer if query.Session != nil { sendReply(&proto.QueryResult{Session: query.Session}) } - return err + return formatError(err) } // Begin begins a transaction. It has to be concluded by a Commit or Rollback. -func (vtg *VTGate) Begin(context context.Context, outSession *proto.Session) (err error) { +func (vtg *VTGate) Begin(ctx context.Context, outSession *proto.Session) (err error) { defer handlePanic(&err) outSession.InTransaction = true return nil } // Commit commits a transaction. -func (vtg *VTGate) Commit(context context.Context, inSession *proto.Session) (err error) { +func (vtg *VTGate) Commit(ctx context.Context, inSession *proto.Session) (err error) { defer handlePanic(&err) - return vtg.resolver.Commit(context, inSession) + return formatError(vtg.resolver.Commit(ctx, inSession)) } // Rollback rolls back a transaction. -func (vtg *VTGate) Rollback(context context.Context, inSession *proto.Session) (err error) { +func (vtg *VTGate) Rollback(ctx context.Context, inSession *proto.Session) (err error) { defer handlePanic(&err) - return vtg.resolver.Rollback(context, inSession) + return formatError(vtg.resolver.Rollback(ctx, inSession)) } // SplitQuery splits a query into sub queries by appending keyranges and @@ -502,10 +543,10 @@ func (vtg *VTGate) Rollback(context context.Context, inSession *proto.Session) ( // original query. Number of sub queries will be a multiple of N that is // greater than or equal to SplitQueryRequest.SplitCount, where N is the // number of shards. -func (vtg *VTGate) SplitQuery(context context.Context, req *proto.SplitQueryRequest, reply *proto.SplitQueryResult) (err error) { +func (vtg *VTGate) SplitQuery(ctx context.Context, req *proto.SplitQueryRequest, reply *proto.SplitQueryResult) (err error) { defer handlePanic(&err) sc := vtg.resolver.scatterConn - keyspace, shards, err := getKeyspaceShards(sc.toposerv, sc.cell, req.Keyspace, topo.TYPE_RDONLY) + keyspace, shards, err := getKeyspaceShards(ctx, sc.toposerv, sc.cell, req.Keyspace, topo.TYPE_RDONLY) if err != nil { return err } @@ -514,7 +555,7 @@ func (vtg *VTGate) SplitQuery(context context.Context, req *proto.SplitQueryRequ keyRangeByShard[shard.ShardName()] = shard.KeyRange } perShardSplitCount := int(math.Ceil(float64(req.SplitCount) / float64(len(shards)))) - splits, err := vtg.resolver.scatterConn.SplitQuery(context, req.Query, perShardSplitCount, keyRangeByShard, keyspace) + splits, err := vtg.resolver.scatterConn.SplitQuery(ctx, req.Query, perShardSplitCount, keyRangeByShard, keyspace) if err != nil { return err } @@ -522,10 +563,30 @@ func (vtg *VTGate) SplitQuery(context context.Context, req *proto.SplitQueryRequ return nil } +func handleExecuteError(err error, statsKey []string, query interface{}, logger *logutil.ThrottledLogger) string { + errStr := err.Error() + ", vtgate: " + servenv.ListeningURL.String() + if strings.Contains(errStr, errDupKey) { + infoErrors.Add("DupKey", 1) + } else if strings.Contains(errStr, errTxPoolFull) { + normalErrors.Add(statsKey, 1) + } else { + normalErrors.Add(statsKey, 1) + logger.Errorf("%v, query: %+v", err, query) + } + return errStr +} + +func formatError(err error) error { + if err == nil { + return nil + } + return fmt.Errorf("%v, vtgate: %v", err, servenv.ListeningURL.String()) +} + func handlePanic(err *error) { if x := recover(); x != nil { log.Errorf("Uncaught panic:\n%v\n%s", x, tb.Stack(4)) - *err = fmt.Errorf("uncaught panic: %v", x) + *err = fmt.Errorf("uncaught panic: %v, vtgate: %v", x, servenv.ListeningURL.String()) internalErrors.Add("Panic", 1) } } diff --git a/go/vt/vtgate/vtgate_test.go b/go/vt/vtgate/vtgate_test.go index 6637988d142..d2c6a7402d8 100644 --- a/go/vt/vtgate/vtgate_test.go +++ b/go/vt/vtgate/vtgate_test.go @@ -10,18 +10,82 @@ import ( "testing" "time" - "github.com/youtube/vitess/go/vt/context" "github.com/youtube/vitess/go/vt/key" kproto "github.com/youtube/vitess/go/vt/key" tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/vtgate/proto" + "golang.org/x/net/context" ) // This file uses the sandbox_test framework. func init() { - Init(new(sandboxTopo), "aa", 1*time.Second, 10, 1*time.Millisecond, 0) + schema := createTestSchema(` +{ + "Keyspaces": { + "TestUnsharded": { + "Sharded": false, + "Tables": { + "t1": "" + } + } + } +} +`) + Init(new(sandboxTopo), schema, "aa", 1*time.Second, 10, 1*time.Millisecond, 0) +} + +func TestVTGateExecute(t *testing.T) { + sandbox := createSandbox(KsTestUnsharded) + sbc := &sandboxConn{} + sandbox.MapTestConn("0", sbc) + q := proto.Query{ + Sql: "select * from t1", + TabletType: topo.TYPE_MASTER, + } + qr := new(proto.QueryResult) + err := rpcVTGate.Execute(context.Background(), &q, qr) + if err != nil { + t.Errorf("want nil, got %v", err) + } + wantqr := new(proto.QueryResult) + wantqr.Result = singleRowResult + if !reflect.DeepEqual(wantqr, qr) { + t.Errorf("want \n%+v, got \n%+v", singleRowResult, qr) + } + if qr.Session != nil { + t.Errorf("want nil, got %+v\n", qr.Session) + } + + q.Session = new(proto.Session) + rpcVTGate.Begin(context.Background(), q.Session) + if !q.Session.InTransaction { + t.Errorf("want true, got false") + } + rpcVTGate.Execute(context.Background(), &q, qr) + wantSession := &proto.Session{ + InTransaction: true, + ShardSessions: []*proto.ShardSession{{ + Keyspace: KsTestUnsharded, + Shard: "0", + TabletType: topo.TYPE_MASTER, + TransactionId: 1, + }}, + } + if !reflect.DeepEqual(wantSession, q.Session) { + t.Errorf("want \n%+v, got \n%+v", wantSession, q.Session) + } + + rpcVTGate.Commit(context.Background(), q.Session) + if sbc.CommitCount != 1 { + t.Errorf("want 1, got %d", sbc.CommitCount) + } + + q.Session = new(proto.Session) + rpcVTGate.Begin(context.Background(), q.Session) + rpcVTGate.Execute(context.Background(), &q, qr) + rpcVTGate.Rollback(context.Background(), q.Session) } func TestVTGateExecuteShard(t *testing.T) { @@ -34,7 +98,7 @@ func TestVTGateExecuteShard(t *testing.T) { Shards: []string{"0"}, } qr := new(proto.QueryResult) - err := RpcVTGate.ExecuteShard(&context.DummyContext{}, &q, qr) + err := rpcVTGate.ExecuteShard(context.Background(), &q, qr) if err != nil { t.Errorf("want nil, got %v", err) } @@ -48,11 +112,11 @@ func TestVTGateExecuteShard(t *testing.T) { } q.Session = new(proto.Session) - RpcVTGate.Begin(&context.DummyContext{}, q.Session) + rpcVTGate.Begin(context.Background(), q.Session) if !q.Session.InTransaction { t.Errorf("want true, got false") } - RpcVTGate.ExecuteShard(&context.DummyContext{}, &q, qr) + rpcVTGate.ExecuteShard(context.Background(), &q, qr) wantSession := &proto.Session{ InTransaction: true, ShardSessions: []*proto.ShardSession{{ @@ -65,15 +129,15 @@ func TestVTGateExecuteShard(t *testing.T) { t.Errorf("want \n%+v, got \n%+v", wantSession, q.Session) } - RpcVTGate.Commit(&context.DummyContext{}, q.Session) + rpcVTGate.Commit(context.Background(), q.Session) if sbc.CommitCount != 1 { t.Errorf("want 1, got %d", sbc.CommitCount) } q.Session = new(proto.Session) - RpcVTGate.Begin(&context.DummyContext{}, q.Session) - RpcVTGate.ExecuteShard(&context.DummyContext{}, &q, qr) - RpcVTGate.Rollback(&context.DummyContext{}, q.Session) + rpcVTGate.Begin(context.Background(), q.Session) + rpcVTGate.ExecuteShard(context.Background(), &q, qr) + rpcVTGate.Rollback(context.Background(), q.Session) /* // Flaky: This test should be run manually. runtime.Gosched() @@ -101,7 +165,7 @@ func TestVTGateExecuteKeyspaceIds(t *testing.T) { } // Test for successful execution qr := new(proto.QueryResult) - err = RpcVTGate.ExecuteKeyspaceIds(&context.DummyContext{}, &q, qr) + err = rpcVTGate.ExecuteKeyspaceIds(context.Background(), &q, qr) if err != nil { t.Errorf("want nil, got %v", err) } @@ -118,11 +182,11 @@ func TestVTGateExecuteKeyspaceIds(t *testing.T) { } // Test for successful execution in transaction q.Session = new(proto.Session) - RpcVTGate.Begin(&context.DummyContext{}, q.Session) + rpcVTGate.Begin(context.Background(), q.Session) if !q.Session.InTransaction { t.Errorf("want true, got false") } - RpcVTGate.ExecuteKeyspaceIds(&context.DummyContext{}, &q, qr) + rpcVTGate.ExecuteKeyspaceIds(context.Background(), &q, qr) wantSession := &proto.Session{ InTransaction: true, ShardSessions: []*proto.ShardSession{{ @@ -135,7 +199,7 @@ func TestVTGateExecuteKeyspaceIds(t *testing.T) { if !reflect.DeepEqual(wantSession, q.Session) { t.Errorf("want \n%+v, got \n%+v", wantSession, q.Session) } - RpcVTGate.Commit(&context.DummyContext{}, q.Session) + rpcVTGate.Commit(context.Background(), q.Session) if sbc1.CommitCount.Get() != 1 { t.Errorf("want 1, got %d", sbc1.CommitCount.Get()) } @@ -145,7 +209,7 @@ func TestVTGateExecuteKeyspaceIds(t *testing.T) { t.Errorf("want nil, got %+v", err) } q.KeyspaceIds = []key.KeyspaceId{kid10, kid30} - RpcVTGate.ExecuteKeyspaceIds(&context.DummyContext{}, &q, qr) + rpcVTGate.ExecuteKeyspaceIds(context.Background(), &q, qr) if qr.Result.RowsAffected != 2 { t.Errorf("want 2, got %v", qr.Result.RowsAffected) } @@ -166,7 +230,7 @@ func TestVTGateExecuteKeyRanges(t *testing.T) { } // Test for successful execution qr := new(proto.QueryResult) - err = RpcVTGate.ExecuteKeyRanges(&context.DummyContext{}, &q, qr) + err = rpcVTGate.ExecuteKeyRanges(context.Background(), &q, qr) if err != nil { t.Errorf("want nil, got %v", err) } @@ -183,11 +247,11 @@ func TestVTGateExecuteKeyRanges(t *testing.T) { } // Test for successful execution in transaction q.Session = new(proto.Session) - RpcVTGate.Begin(&context.DummyContext{}, q.Session) + rpcVTGate.Begin(context.Background(), q.Session) if !q.Session.InTransaction { t.Errorf("want true, got false") } - err = RpcVTGate.ExecuteKeyRanges(&context.DummyContext{}, &q, qr) + err = rpcVTGate.ExecuteKeyRanges(context.Background(), &q, qr) if err != nil { t.Errorf("want nil, got %v", err) } @@ -203,14 +267,14 @@ func TestVTGateExecuteKeyRanges(t *testing.T) { if !reflect.DeepEqual(wantSession, q.Session) { t.Errorf("want \n%+v, got \n%+v", wantSession, q.Session) } - RpcVTGate.Commit(&context.DummyContext{}, q.Session) + rpcVTGate.Commit(context.Background(), q.Session) if sbc1.CommitCount.Get() != 1 { t.Errorf("want 1, got %v", sbc1.CommitCount.Get()) } // Test for multiple shards kr, err = key.ParseKeyRangeParts("10", "30") q.KeyRanges = []key.KeyRange{kr} - RpcVTGate.ExecuteKeyRanges(&context.DummyContext{}, &q, qr) + rpcVTGate.ExecuteKeyRanges(context.Background(), &q, qr) if qr.Result.RowsAffected != 2 { t.Errorf("want 2, got %v", qr.Result.RowsAffected) } @@ -240,7 +304,7 @@ func TestVTGateExecuteEntityIds(t *testing.T) { } // Test for successful execution qr := new(proto.QueryResult) - err = RpcVTGate.ExecuteEntityIds(&context.DummyContext{}, &q, qr) + err = rpcVTGate.ExecuteEntityIds(context.Background(), &q, qr) if err != nil { t.Errorf("want nil, got %v", err) } @@ -257,11 +321,11 @@ func TestVTGateExecuteEntityIds(t *testing.T) { } // Test for successful execution in transaction q.Session = new(proto.Session) - RpcVTGate.Begin(&context.DummyContext{}, q.Session) + rpcVTGate.Begin(context.Background(), q.Session) if !q.Session.InTransaction { t.Errorf("want true, got false") } - RpcVTGate.ExecuteEntityIds(&context.DummyContext{}, &q, qr) + rpcVTGate.ExecuteEntityIds(context.Background(), &q, qr) wantSession := &proto.Session{ InTransaction: true, ShardSessions: []*proto.ShardSession{{ @@ -274,7 +338,7 @@ func TestVTGateExecuteEntityIds(t *testing.T) { if !reflect.DeepEqual(wantSession, q.Session) { t.Errorf("want \n%+v, got \n%+v", wantSession, q.Session) } - RpcVTGate.Commit(&context.DummyContext{}, q.Session) + rpcVTGate.Commit(context.Background(), q.Session) if sbc1.CommitCount.Get() != 1 { t.Errorf("want 1, got %d", sbc1.CommitCount.Get()) } @@ -284,7 +348,7 @@ func TestVTGateExecuteEntityIds(t *testing.T) { t.Errorf("want nil, got %+v", err) } q.EntityKeyspaceIDs = append(q.EntityKeyspaceIDs, proto.EntityId{ExternalID: "id2", KeyspaceID: kid30}) - RpcVTGate.ExecuteEntityIds(&context.DummyContext{}, &q, qr) + rpcVTGate.ExecuteEntityIds(context.Background(), &q, qr) if qr.Result.RowsAffected != 2 { t.Errorf("want 2, got %v", qr.Result.RowsAffected) } @@ -306,7 +370,7 @@ func TestVTGateExecuteBatchShard(t *testing.T) { Shards: []string{"-20", "20-40"}, } qrl := new(proto.QueryResultList) - err := RpcVTGate.ExecuteBatchShard(&context.DummyContext{}, &q, qrl) + err := rpcVTGate.ExecuteBatchShard(context.Background(), &q, qrl) if err != nil { t.Errorf("want nil, got %v", err) } @@ -321,8 +385,8 @@ func TestVTGateExecuteBatchShard(t *testing.T) { } q.Session = new(proto.Session) - RpcVTGate.Begin(&context.DummyContext{}, q.Session) - RpcVTGate.ExecuteBatchShard(&context.DummyContext{}, &q, qrl) + rpcVTGate.Begin(context.Background(), q.Session) + rpcVTGate.ExecuteBatchShard(context.Background(), &q, qrl) if len(q.Session.ShardSessions) != 2 { t.Errorf("want 2, got %d", len(q.Session.ShardSessions)) } @@ -353,7 +417,7 @@ func TestVTGateExecuteBatchKeyspaceIds(t *testing.T) { TabletType: topo.TYPE_MASTER, } qrl := new(proto.QueryResultList) - err = RpcVTGate.ExecuteBatchKeyspaceIds(&context.DummyContext{}, &q, qrl) + err = rpcVTGate.ExecuteBatchKeyspaceIds(context.Background(), &q, qrl) if err != nil { t.Errorf("want nil, got %v", err) } @@ -368,13 +432,37 @@ func TestVTGateExecuteBatchKeyspaceIds(t *testing.T) { } q.Session = new(proto.Session) - RpcVTGate.Begin(&context.DummyContext{}, q.Session) - RpcVTGate.ExecuteBatchKeyspaceIds(&context.DummyContext{}, &q, qrl) + rpcVTGate.Begin(context.Background(), q.Session) + rpcVTGate.ExecuteBatchKeyspaceIds(context.Background(), &q, qrl) if len(q.Session.ShardSessions) != 2 { t.Errorf("want 2, got %d", len(q.Session.ShardSessions)) } } +func TestVTGateStreamExecute(t *testing.T) { + sandbox := createSandbox(KsTestUnsharded) + sbc := &sandboxConn{} + sandbox.MapTestConn("0", sbc) + q := proto.Query{ + Sql: "select * from t1", + TabletType: topo.TYPE_MASTER, + } + var qrs []*proto.QueryResult + err := rpcVTGate.StreamExecute(context.Background(), &q, func(r *proto.QueryResult) error { + qrs = append(qrs, r) + return nil + }) + if err != nil { + t.Errorf("want nil, got %v", err) + } + row := new(proto.QueryResult) + row.Result = singleRowResult + want := []*proto.QueryResult{row} + if !reflect.DeepEqual(want, qrs) { + t.Errorf("want \n%+v, got \n%+v", want, qrs) + } +} + func TestVTGateStreamExecuteKeyspaceIds(t *testing.T) { s := createSandbox("TestVTGateStreamExecuteKeyspaceIds") sbc := &sandboxConn{} @@ -393,7 +481,7 @@ func TestVTGateStreamExecuteKeyspaceIds(t *testing.T) { } // Test for successful execution var qrs []*proto.QueryResult - err = RpcVTGate.StreamExecuteKeyspaceIds(&context.DummyContext{}, &sq, func(r *proto.QueryResult) error { + err = rpcVTGate.StreamExecuteKeyspaceIds(context.Background(), &sq, func(r *proto.QueryResult) error { qrs = append(qrs, r) return nil }) @@ -410,8 +498,8 @@ func TestVTGateStreamExecuteKeyspaceIds(t *testing.T) { // Test for successful execution in transaction sq.Session = new(proto.Session) qrs = nil - RpcVTGate.Begin(&context.DummyContext{}, sq.Session) - err = RpcVTGate.StreamExecuteKeyspaceIds(&context.DummyContext{}, &sq, func(r *proto.QueryResult) error { + rpcVTGate.Begin(context.Background(), sq.Session) + err = rpcVTGate.StreamExecuteKeyspaceIds(context.Background(), &sq, func(r *proto.QueryResult) error { qrs = append(qrs, r) return nil }) @@ -432,7 +520,7 @@ func TestVTGateStreamExecuteKeyspaceIds(t *testing.T) { if !reflect.DeepEqual(want, qrs) { t.Errorf("want\n%#v\ngot\n%#v", want, qrs) } - RpcVTGate.Commit(&context.DummyContext{}, sq.Session) + rpcVTGate.Commit(context.Background(), sq.Session) if sbc.CommitCount.Get() != 1 { t.Errorf("want 1, got %d", sbc.CommitCount.Get()) } @@ -444,7 +532,7 @@ func TestVTGateStreamExecuteKeyspaceIds(t *testing.T) { t.Errorf("want nil, got %+v", err) } sq.KeyspaceIds = []key.KeyspaceId{kid10, kid15} - err = RpcVTGate.StreamExecuteKeyspaceIds(&context.DummyContext{}, &sq, func(r *proto.QueryResult) error { + err = rpcVTGate.StreamExecuteKeyspaceIds(context.Background(), &sq, func(r *proto.QueryResult) error { qrs = append(qrs, r) return nil }) @@ -463,7 +551,7 @@ func TestVTGateStreamExecuteKeyspaceIds(t *testing.T) { t.Errorf("want nil, got %+v", err) } sq.KeyspaceIds = []key.KeyspaceId{kid10, kid30} - err = RpcVTGate.StreamExecuteKeyspaceIds(&context.DummyContext{}, &sq, func(r *proto.QueryResult) error { + err = rpcVTGate.StreamExecuteKeyspaceIds(context.Background(), &sq, func(r *proto.QueryResult) error { qrs = append(qrs, r) return nil }) @@ -487,7 +575,7 @@ func TestVTGateStreamExecuteKeyRanges(t *testing.T) { } // Test for successful execution var qrs []*proto.QueryResult - err = RpcVTGate.StreamExecuteKeyRanges(&context.DummyContext{}, &sq, func(r *proto.QueryResult) error { + err = rpcVTGate.StreamExecuteKeyRanges(context.Background(), &sq, func(r *proto.QueryResult) error { qrs = append(qrs, r) return nil }) @@ -503,8 +591,8 @@ func TestVTGateStreamExecuteKeyRanges(t *testing.T) { sq.Session = new(proto.Session) qrs = nil - RpcVTGate.Begin(&context.DummyContext{}, sq.Session) - err = RpcVTGate.StreamExecuteKeyRanges(&context.DummyContext{}, &sq, func(r *proto.QueryResult) error { + rpcVTGate.Begin(context.Background(), sq.Session) + err = rpcVTGate.StreamExecuteKeyRanges(context.Background(), &sq, func(r *proto.QueryResult) error { qrs = append(qrs, r) return nil }) @@ -529,7 +617,7 @@ func TestVTGateStreamExecuteKeyRanges(t *testing.T) { // Test for successful execution - multiple shards kr, err = key.ParseKeyRangeParts("10", "40") sq.KeyRanges = []key.KeyRange{kr} - err = RpcVTGate.StreamExecuteKeyRanges(&context.DummyContext{}, &sq, func(r *proto.QueryResult) error { + err = rpcVTGate.StreamExecuteKeyRanges(context.Background(), &sq, func(r *proto.QueryResult) error { qrs = append(qrs, r) return nil }) @@ -550,7 +638,7 @@ func TestVTGateStreamExecuteShard(t *testing.T) { } // Test for successful execution var qrs []*proto.QueryResult - err := RpcVTGate.StreamExecuteShard(&context.DummyContext{}, &q, func(r *proto.QueryResult) error { + err := rpcVTGate.StreamExecuteShard(context.Background(), &q, func(r *proto.QueryResult) error { qrs = append(qrs, r) return nil }) @@ -566,8 +654,8 @@ func TestVTGateStreamExecuteShard(t *testing.T) { q.Session = new(proto.Session) qrs = nil - RpcVTGate.Begin(&context.DummyContext{}, q.Session) - err = RpcVTGate.StreamExecuteShard(&context.DummyContext{}, &q, func(r *proto.QueryResult) error { + rpcVTGate.Begin(context.Background(), q.Session) + err = rpcVTGate.StreamExecuteShard(context.Background(), &q, func(r *proto.QueryResult) error { qrs = append(qrs, r) return nil }) @@ -607,7 +695,7 @@ func TestVTGateSplitQuery(t *testing.T) { SplitCount: splitCount, } result := new(proto.SplitQueryResult) - err := RpcVTGate.SplitQuery(&context.DummyContext{}, &req, result) + err := rpcVTGate.SplitQuery(context.Background(), &req, result) if err != nil { t.Errorf("want nil, got %v", err) } diff --git a/go/vt/vtgate/vtgateconn/vtgateconn.go b/go/vt/vtgate/vtgateconn/vtgateconn.go new file mode 100644 index 00000000000..4bf67dce384 --- /dev/null +++ b/go/vt/vtgate/vtgateconn/vtgateconn.go @@ -0,0 +1,89 @@ +// Copyright 2015, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vtgateconn + +import ( + "flag" + "time" + + log "github.com/golang/glog" + mproto "github.com/youtube/vitess/go/mysql/proto" + tproto "github.com/youtube/vitess/go/vt/tabletserver/proto" + "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" +) + +var ( + vtgateProtocol = flag.String("vtgate_protocol", "gorpc", "how to talk to vtgate") +) + +// ServerError represents an error that was returned from +// a vtgate server. +type ServerError struct { + Code int + Err string +} + +func (e *ServerError) Error() string { return e.Err } + +// OperationalError represents an error due to a failure to +// communicate with vtgate. +type OperationalError string + +func (e OperationalError) Error() string { return string(e) } + +// DialerFunc represents a function that will return a VTGateConn object that can communicate with a VTGate. +type DialerFunc func(ctx context.Context, address string, timeout time.Duration) (VTGateConn, error) + +// VTGateConn defines the interface for a vtgate client. It should +// not be concurrently used across goroutines. +type VTGateConn interface { + // Execute executes a non-streaming query on vtgate. + Execute(ctx context.Context, query string, bindVars map[string]interface{}, tabletType topo.TabletType) (*mproto.QueryResult, error) + + // ExecuteBatch executes a group of queries. + ExecuteBatch(ctx context.Context, queries []tproto.BoundQuery, tabletType topo.TabletType) (*tproto.QueryResultList, error) + + // StreamExecute executes a streaming query on vtgate. It returns a channel, ErrFunc and error. + // If error is non-nil, it means that the StreamExecute failed to send the request. Otherwise, + // you can pull values from the channel till it's closed. Following this, you can call ErrFunc + // to see if the stream ended normally or due to a failure. + StreamExecute(ctx context.Context, query string, bindVars map[string]interface{}, tabletType topo.TabletType) (<-chan *mproto.QueryResult, ErrFunc) + + // Transaction support + Begin(ctx context.Context) error + Commit(ctx context.Context) error + Rollback(ctx context.Context) error + + // Close must be called for releasing resources. + Close() + + // SplitQuery splits a query into equally sized smaller queries by + // appending primary key range clauses to the original query + SplitQuery(ctx context.Context, query tproto.BoundQuery, splitCount int) ([]tproto.QuerySplit, error) +} + +// ErrFunc is used to check for streaming errors. +type ErrFunc func() error + +var dialers = make(map[string]DialerFunc) + +// RegisterDialer is meant to be used by Dialer implementations +// to self register. +func RegisterDialer(name string, dialer DialerFunc) { + if _, ok := dialers[name]; ok { + log.Fatalf("Dialer %s already exists", name) + } + dialers[name] = dialer +} + +// GetDialer returns the dialer to use, described by the command line flag +func GetDialer() DialerFunc { + td, ok := dialers[*vtgateProtocol] + if !ok { + log.Fatalf("No dialer registered for VTGate protocol %s", *vtgateProtocol) + } + return td +} diff --git a/go/vt/vttest/local_cluster.go b/go/vt/vttest/local_cluster.go new file mode 100644 index 00000000000..fea06ac18ca --- /dev/null +++ b/go/vt/vttest/local_cluster.go @@ -0,0 +1,84 @@ +// Copyright 2015, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package vttest provides the functionality to bring +// up a test cluster. +package vttest + +import ( + "encoding/json" + "errors" + "os" + "os/exec" + "strings" +) + +var ( + curShardNames []string + curReplicas int + curRdonly int + curKeyspace string + curSchema string + curVSchema string +) + +func run(shardNames []string, replicas, rdonly int, keyspace, schema, vschema, op string) error { + curShardNames = shardNames + curReplicas = replicas + curRdonly = rdonly + curKeyspace = keyspace + curSchema = schema + curVSchema = vschema + + vttop := os.Getenv("VTTOP") + if vttop == "" { + return errors.New("VTTOP not set") + } + cfg, err := json.Marshal(map[string]int{ + "replica": replicas, + "rdonly": rdonly, + }) + if err != nil { + return err + } + cmd := exec.Command( + "python", + vttop+"/test/java_vtgate_test_helper.py", + "--shards", + strings.Join(shardNames, ","), + "--tablet-config", + string(cfg), + "--keyspace", + keyspace, + ) + if schema != "" { + cmd.Args = append(cmd.Args, "--schema", schema) + } + if vschema != "" { + cmd.Args = append(cmd.Args, "--vschema", vschema) + } + cmd.Args = append(cmd.Args, op) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// LocalLaunch launches the cluster. Only one cluster can be active at a time. +func LocalLaunch(shardNames []string, replicas, rdonly int, keyspace, schema, vschema string) error { + err := run(shardNames, replicas, rdonly, keyspace, schema, vschema, "setup") + if err != nil { + LocalTeardown() + } + return err +} + +// LocalTeardown shuts down the previously launched cluster. +func LocalTeardown() error { + if curShardNames == nil { + return nil + } + err := run(curShardNames, curReplicas, curRdonly, curKeyspace, curSchema, curVSchema, "teardown") + curShardNames = nil + return err +} diff --git a/go/vt/worker/clone_utils.go b/go/vt/worker/clone_utils.go index fbc315a4be8..37819004e15 100644 --- a/go/vt/worker/clone_utils.go +++ b/go/vt/worker/clone_utils.go @@ -13,7 +13,7 @@ import ( "text/template" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/sqltypes" @@ -28,45 +28,67 @@ import ( // tableStatus keeps track of the status for a given table type tableStatus struct { - name string - rowCount uint64 + name string + isView bool // all subsequent fields are protected by the mutex - mu sync.Mutex - state string - copiedRows uint64 + mu sync.Mutex + rowCount uint64 // set to approximate value, until copy ends + copiedRows uint64 // actual count of copied rows + threadCount int // how many concurrent threads will copy the data + threadsStarted int // how many threads have started + threadsDone int // how many threads are done } -func (ts *tableStatus) setState(state string) { +func (ts *tableStatus) setThreadCount(threadCount int) { ts.mu.Lock() - ts.state = state + ts.threadCount = threadCount + ts.mu.Unlock() +} + +func (ts *tableStatus) threadStarted() { + ts.mu.Lock() + ts.threadsStarted++ + ts.mu.Unlock() +} + +func (ts *tableStatus) threadDone() { + ts.mu.Lock() + ts.threadsDone++ ts.mu.Unlock() } func (ts *tableStatus) addCopiedRows(copiedRows int) { ts.mu.Lock() ts.copiedRows += uint64(copiedRows) - if ts.copiedRows >= ts.rowCount { - // FIXME(alainjobart) this is not accurate, as the - // total row count is approximate - ts.state = "finished the copy" + if ts.copiedRows > ts.rowCount { + // since rowCount is not accurate, update it if we go past it. + ts.rowCount = ts.copiedRows } ts.mu.Unlock() } -func formatTableStatuses(tableStatuses []tableStatus, startTime time.Time) ([]string, time.Time) { +func formatTableStatuses(tableStatuses []*tableStatus, startTime time.Time) ([]string, time.Time) { copiedRows := uint64(0) rowCount := uint64(0) result := make([]string, len(tableStatuses)) for i, ts := range tableStatuses { ts.mu.Lock() - if ts.rowCount > 0 { - result[i] = fmt.Sprintf("%v: %v (%v/%v)", ts.name, ts.state, ts.copiedRows, ts.rowCount) - copiedRows += ts.copiedRows - rowCount += ts.rowCount + if ts.isView { + // views are not copied + result[i] = fmt.Sprintf("%v is a view", ts.name) + } else if ts.threadsStarted == 0 { + // we haven't started yet + result[i] = fmt.Sprintf("%v: copy not started (estimating %v rows)", ts.name, ts.rowCount) + } else if ts.threadsDone == ts.threadCount { + // we are done with the copy + result[i] = fmt.Sprintf("%v: copy done, copied %v rows", ts.name, ts.rowCount) } else { - result[i] = fmt.Sprintf("%v: %v", ts.name, ts.state) + // copy is running + result[i] = fmt.Sprintf("%v: copy running using %v threads (%v/%v rows)", ts.name, ts.threadsStarted-ts.threadsDone, ts.copiedRows, ts.rowCount) } + copiedRows += ts.copiedRows + rowCount += ts.rowCount ts.mu.Unlock() } now := time.Now() @@ -77,6 +99,47 @@ func formatTableStatuses(tableStatuses []tableStatus, startTime time.Time) ([]st return result, eta } +// executeFetchWithRetries will attempt to run ExecuteFetch for a single command, with a reasonably small timeout. +// If will keep retrying the ExecuteFetch (for a finite but longer duration) if it fails due to a timeout or a +// retriable application error. +func executeFetchWithRetries(ctx context.Context, wr *wrangler.Wrangler, ti *topo.TabletInfo, command string, disableBinLogs bool) error { + retryDuration := 2 * time.Hour + // We should keep retrying up until the retryCtx runs out + retryCtx, retryCancel := context.WithTimeout(ctx, retryDuration) + defer retryCancel() + for { + tryCtx, cancel := context.WithTimeout(retryCtx, 2*time.Minute) + _, err := wr.TabletManagerClient().ExecuteFetch(tryCtx, ti, command, 0, false, disableBinLogs) + cancel() + switch { + case err == nil: + // success! + return nil + case wr.TabletManagerClient().IsTimeoutError(err): + wr.Logger().Infof("ExecuteFetch failed on %v; will retry because it was a timeout error: %v", ti, err) + case strings.Contains(err.Error(), "retry: "): + wr.Logger().Infof("ExecuteFetch failed on %v; will retry because it was a retriable application failure: %v", ti, err) + default: + return err + } + t := time.NewTimer(30 * time.Second) + // don't leak memory if the timer isn't triggered + defer t.Stop() + + select { + case <-retryCtx.Done(): + if retryCtx.Err() == context.DeadlineExceeded { + return fmt.Errorf("failed to connect to destination tablet %v after retrying for %v", ti, retryDuration) + } + return fmt.Errorf("interrupted while trying to run %v on tablet %v", command, ti) + case <-t.C: + // Re-resolve and retry 30s after the failure + // TODO(aaijazi): Re-resolve before retrying + } + + } +} + // fillStringTemplate returns the string template filled func fillStringTemplate(tmpl string, vars interface{}) (string, error) { myTemplate := template.Must(template.New("").Parse(tmpl)) @@ -88,25 +151,17 @@ func fillStringTemplate(tmpl string, vars interface{}) (string, error) { } // runSqlCommands will send the sql commands to the remote tablet. -func runSqlCommands(wr *wrangler.Wrangler, ti *topo.TabletInfo, commands []string, abort chan struct{}, disableBinLogs bool) error { +func runSqlCommands(ctx context.Context, wr *wrangler.Wrangler, ti *topo.TabletInfo, commands []string, disableBinLogs bool) error { for _, command := range commands { command, err := fillStringTemplate(command, map[string]string{"DatabaseName": ti.DbName()}) if err != nil { return fmt.Errorf("fillStringTemplate failed: %v", err) } - _, err = wr.TabletManagerClient().ExecuteFetch(context.TODO(), ti, command, 0, false, disableBinLogs, 30*time.Second) + err = executeFetchWithRetries(ctx, wr, ti, command, disableBinLogs) if err != nil { return err } - - // check on abort - select { - case <-abort: - return nil - default: - break - } } return nil @@ -119,7 +174,7 @@ func runSqlCommands(wr *wrangler.Wrangler, ti *topo.TabletInfo, commands []strin // "", "value1", "value2", "" // A non-split tablet will just return: // "", "" -func findChunks(wr *wrangler.Wrangler, ti *topo.TabletInfo, td *myproto.TableDefinition, minTableSizeForSplit uint64, sourceReaderCount int) ([]string, error) { +func findChunks(ctx context.Context, wr *wrangler.Wrangler, ti *topo.TabletInfo, td *myproto.TableDefinition, minTableSizeForSplit uint64, sourceReaderCount int) ([]string, error) { result := []string{"", ""} // eliminate a few cases we don't split tables for @@ -134,7 +189,9 @@ func findChunks(wr *wrangler.Wrangler, ti *topo.TabletInfo, td *myproto.TableDef // get the min and max of the leading column of the primary key query := fmt.Sprintf("SELECT MIN(%v), MAX(%v) FROM %v.%v", td.PrimaryKeyColumns[0], td.PrimaryKeyColumns[0], ti.DbName(), td.Name) - qr, err := wr.TabletManagerClient().ExecuteFetch(context.TODO(), ti, query, 1, true, false, 30*time.Second) + ctx, cancel := context.WithTimeout(ctx, 60*time.Second) + qr, err := wr.TabletManagerClient().ExecuteFetch(ctx, ti, query, 1, true, false) + cancel() if err != nil { wr.Logger().Infof("Not splitting table %v into multiple chunks: %v", td.Name, err) return result, nil @@ -288,7 +345,7 @@ func makeValueString(fields []mproto.Field, rows [][]sqltypes.Value) string { // executeFetchLoop loops over the provided insertChannel // and sends the commands to the provided tablet. -func executeFetchLoop(wr *wrangler.Wrangler, ti *topo.TabletInfo, insertChannel chan string, abort chan struct{}, disableBinLogs bool) error { +func executeFetchLoop(ctx context.Context, wr *wrangler.Wrangler, ti *topo.TabletInfo, insertChannel chan string, disableBinLogs bool) error { for { select { case cmd, ok := <-insertChannel: @@ -297,14 +354,14 @@ func executeFetchLoop(wr *wrangler.Wrangler, ti *topo.TabletInfo, insertChannel return nil } cmd = "INSERT INTO `" + ti.DbName() + "`." + cmd - _, err := wr.TabletManagerClient().ExecuteFetch(context.TODO(), ti, cmd, 0, false, disableBinLogs, 30*time.Second) + err := executeFetchWithRetries(ctx, wr, ti, cmd, disableBinLogs) if err != nil { return fmt.Errorf("ExecuteFetch failed: %v", err) } - case <-abort: - // FIXME(alainjobart): note this select case - // could be starved here, and we might miss - // the abort in some corner cases. + case <-ctx.Done(): + // Doesn't really matter if this select gets starved, because the other case + // will also return an error due to executeFetch's context being closed. This case + // does prevent us from blocking indefinitely on inserChannel when the worker is canceled. return nil } } diff --git a/go/vt/worker/diff_utils.go b/go/vt/worker/diff_utils.go index 1cfe4c88cb8..dc1b12028bd 100644 --- a/go/vt/worker/diff_utils.go +++ b/go/vt/worker/diff_utils.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/sqltypes" @@ -31,7 +31,7 @@ type QueryResultReader struct { // NewQueryResultReaderForTablet creates a new QueryResultReader for // the provided tablet / sql query -func NewQueryResultReaderForTablet(ts topo.Server, tabletAlias topo.TabletAlias, sql string) (*QueryResultReader, error) { +func NewQueryResultReaderForTablet(ctx context.Context, ts topo.Server, tabletAlias topo.TabletAlias, sql string) (*QueryResultReader, error) { tablet, err := ts.GetTablet(tabletAlias) if err != nil { return nil, err @@ -42,12 +42,12 @@ func NewQueryResultReaderForTablet(ts topo.Server, tabletAlias topo.TabletAlias, return nil, err } - conn, err := tabletconn.GetDialer()(context.TODO(), *endPoint, tablet.Keyspace, tablet.Shard, 30*time.Second) + conn, err := tabletconn.GetDialer()(ctx, *endPoint, tablet.Keyspace, tablet.Shard, 30*time.Second) if err != nil { return nil, err } - sr, clientErrFn, err := conn.StreamExecute(context.TODO(), sql, make(map[string]interface{}), 0) + sr, clientErrFn, err := conn.StreamExecute(ctx, sql, make(map[string]interface{}), 0) if err != nil { return nil, err } @@ -97,17 +97,17 @@ func uint64FromKeyspaceId(keyspaceId key.KeyspaceId) string { // TableScan returns a QueryResultReader that gets all the rows from a // table, ordered by Primary Key. The returned columns are ordered // with the Primary Key columns in front. -func TableScan(log logutil.Logger, ts topo.Server, tabletAlias topo.TabletAlias, tableDefinition *myproto.TableDefinition) (*QueryResultReader, error) { +func TableScan(ctx context.Context, log logutil.Logger, ts topo.Server, tabletAlias topo.TabletAlias, tableDefinition *myproto.TableDefinition) (*QueryResultReader, error) { sql := fmt.Sprintf("SELECT %v FROM %v ORDER BY %v", strings.Join(orderedColumns(tableDefinition), ", "), tableDefinition.Name, strings.Join(tableDefinition.PrimaryKeyColumns, ", ")) log.Infof("SQL query for %v/%v: %v", tabletAlias, tableDefinition.Name, sql) - return NewQueryResultReaderForTablet(ts, tabletAlias, sql) + return NewQueryResultReaderForTablet(ctx, ts, tabletAlias, sql) } // TableScanByKeyRange returns a QueryResultReader that gets all the // rows from a table that match the supplied KeyRange, ordered by // Primary Key. The returned columns are ordered with the Primary Key // columns in front. -func TableScanByKeyRange(log logutil.Logger, ts topo.Server, tabletAlias topo.TabletAlias, tableDefinition *myproto.TableDefinition, keyRange key.KeyRange, keyspaceIdType key.KeyspaceIdType) (*QueryResultReader, error) { +func TableScanByKeyRange(ctx context.Context, log logutil.Logger, ts topo.Server, tabletAlias topo.TabletAlias, tableDefinition *myproto.TableDefinition, keyRange key.KeyRange, keyspaceIdType key.KeyspaceIdType) (*QueryResultReader, error) { where := "" switch keyspaceIdType { case key.KIT_UINT64: @@ -146,7 +146,7 @@ func TableScanByKeyRange(log logutil.Logger, ts topo.Server, tabletAlias topo.Ta sql := fmt.Sprintf("SELECT %v FROM %v %vORDER BY %v", strings.Join(orderedColumns(tableDefinition), ", "), tableDefinition.Name, where, strings.Join(tableDefinition.PrimaryKeyColumns, ", ")) log.Infof("SQL query for %v/%v: %v", tabletAlias, tableDefinition.Name, sql) - return NewQueryResultReaderForTablet(ts, tabletAlias, sql) + return NewQueryResultReaderForTablet(ctx, ts, tabletAlias, sql) } func (qrr *QueryResultReader) Error() error { @@ -174,7 +174,7 @@ func NewRowReader(queryResultReader *QueryResultReader) *RowReader { // Next will return: // (row, nil) for the next row // (nil, nil) for EOF -// (nil, error) if an error occured +// (nil, error) if an error occurred func (rr *RowReader) Next() ([]sqltypes.Value, error) { if rr.currentResult == nil || rr.currentIndex == len(rr.currentResult.Rows) { var ok bool @@ -259,6 +259,7 @@ func RowsEqual(left, right []sqltypes.Value) int { // -1 if left is smaller than right // 0 if left and right are equal // +1 if left is bigger than right +// TODO: This can panic if types for left and right don't match. func CompareRows(fields []mproto.Field, compareCount int, left, right []sqltypes.Value) (int, error) { for i := 0; i < compareCount; i++ { fieldType := fields[i].Type @@ -278,6 +279,13 @@ func CompareRows(fields []mproto.Field, compareCount int, left, right []sqltypes } else if l > r { return 1, nil } + case uint64: + r := rv.(uint64) + if l < r { + return -1, nil + } else if l > r { + return 1, nil + } case float64: r := rv.(float64) if l < r { diff --git a/go/vt/worker/row_splitter.go b/go/vt/worker/row_splitter.go index 80e0a3f1789..1b16677faf0 100644 --- a/go/vt/worker/row_splitter.go +++ b/go/vt/worker/row_splitter.go @@ -7,13 +7,14 @@ package worker import ( "fmt" + mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/sqltypes" "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/topo" ) // RowSplitter is a helper class to split rows into multiple -// subsets targetted to different shards. +// subsets targeted to different shards. type RowSplitter struct { Type key.KeyspaceIdType ValueIndex int @@ -33,15 +34,19 @@ func NewRowSplitter(shardInfos []*topo.ShardInfo, typ key.KeyspaceIdType, valueI return result } +// StartSplit starts a new split. Split can then be called multiple times. +func (rs *RowSplitter) StartSplit() [][][]sqltypes.Value { + return make([][][]sqltypes.Value, len(rs.KeyRanges)) +} + // Split will split the rows into subset for each distribution -func (rs *RowSplitter) Split(rows [][]sqltypes.Value) ([][][]sqltypes.Value, error) { - result := make([][][]sqltypes.Value, len(rs.KeyRanges)) +func (rs *RowSplitter) Split(result [][][]sqltypes.Value, rows [][]sqltypes.Value) error { if rs.Type == key.KIT_UINT64 { for _, row := range rows { v := sqltypes.MakeNumeric(row[rs.ValueIndex].Raw()) i, err := v.ParseUint64() if err != nil { - return nil, fmt.Errorf("Non numerical value: %v", err) + return fmt.Errorf("Non numerical value: %v", err) } k := key.Uint64Key(i).KeyspaceId() for i, kr := range rs.KeyRanges { @@ -62,5 +67,25 @@ func (rs *RowSplitter) Split(rows [][]sqltypes.Value) ([][][]sqltypes.Value, err } } } - return result, nil + return nil +} + +// Send will send the rows to the list of channels. Returns true if aborted. +func (rs *RowSplitter) Send(fields []mproto.Field, result [][][]sqltypes.Value, baseCmd string, insertChannels [][]chan string, abort <-chan struct{}) bool { + for i, cs := range insertChannels { + // one of the chunks might be empty, so no need + // to send data in that case + if len(result[i]) > 0 { + cmd := baseCmd + makeValueString(fields, result[i]) + for _, c := range cs { + // also check on abort, so we don't wait forever + select { + case c <- cmd: + case <-abort: + return true + } + } + } + } + return false } diff --git a/go/vt/worker/row_splitter_test.go b/go/vt/worker/row_splitter_test.go index 533b10c2cf3..3c7a367c1e7 100644 --- a/go/vt/worker/row_splitter_test.go +++ b/go/vt/worker/row_splitter_test.go @@ -57,8 +57,8 @@ func TestRowSplitterUint64(t *testing.T) { // basic split rows := [][]sqltypes.Value{row0, row1, row2, row2, row1, row2, row0} - result, err := rs.Split(rows) - if err != nil { + result := rs.StartSplit() + if err := rs.Split(result, rows); err != nil { t.Fatalf("Split failed: %v", err) } if len(result) != 3 { @@ -108,8 +108,8 @@ func TestRowSplitterString(t *testing.T) { // basic split rows := [][]sqltypes.Value{row0, row1, row2, row2, row1, row2, row0} - result, err := rs.Split(rows) - if err != nil { + result := rs.StartSplit() + if err := rs.Split(result, rows); err != nil { t.Fatalf("Split failed: %v", err) } if len(result) != 3 { diff --git a/go/vt/worker/split_clone.go b/go/vt/worker/split_clone.go index 1f3dcae5cf0..d3a5fb1d157 100644 --- a/go/vt/worker/split_clone.go +++ b/go/vt/worker/split_clone.go @@ -11,9 +11,10 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/event" + "github.com/youtube/vitess/go/jscfg" "github.com/youtube/vitess/go/sync2" "github.com/youtube/vitess/go/vt/binlog/binlogplayer" "github.com/youtube/vitess/go/vt/mysqlctl" @@ -46,9 +47,12 @@ type SplitCloneWorker struct { excludeTables []string strategy *mysqlctl.SplitStrategy sourceReaderCount int + destinationPackCount int minTableSizeForSplit uint64 destinationWriterCount int cleaner *wrangler.Cleaner + ctx context.Context + ctxCancel context.CancelFunc // all subsequent fields are protected by the mutex mu sync.Mutex @@ -68,20 +72,24 @@ type SplitCloneWorker struct { destinationAliases [][]topo.TabletAlias destinationTablets []map[topo.TabletAlias]*topo.TabletInfo destinationMasterAliases []topo.TabletAlias + // aliases of tablets that need to have their schema reloaded + reloadAliases [][]topo.TabletAlias + reloadTablets []map[topo.TabletAlias]*topo.TabletInfo // populated during stateSCCopy - tableStatus []tableStatus + tableStatus []*tableStatus startTime time.Time ev *events.SplitClone } // NewSplitCloneWorker returns a new SplitCloneWorker object. -func NewSplitCloneWorker(wr *wrangler.Wrangler, cell, keyspace, shard string, excludeTables []string, strategyStr string, sourceReaderCount int, minTableSizeForSplit uint64, destinationWriterCount int) (Worker, error) { +func NewSplitCloneWorker(wr *wrangler.Wrangler, cell, keyspace, shard string, excludeTables []string, strategyStr string, sourceReaderCount, destinationPackCount int, minTableSizeForSplit uint64, destinationWriterCount int) (Worker, error) { strategy, err := mysqlctl.NewSplitStrategy(wr.Logger(), strategyStr) if err != nil { return nil, err } + ctx, cancel := context.WithCancel(context.Background()) return &SplitCloneWorker{ wr: wr, cell: cell, @@ -90,9 +98,12 @@ func NewSplitCloneWorker(wr *wrangler.Wrangler, cell, keyspace, shard string, ex excludeTables: excludeTables, strategy: strategy, sourceReaderCount: sourceReaderCount, + destinationPackCount: destinationPackCount, minTableSizeForSplit: minTableSizeForSplit, destinationWriterCount: destinationWriterCount, cleaner: &wrangler.Cleaner{}, + ctx: ctx, + ctxCancel: cancel, state: stateSCNotSarted, ev: &events.SplitClone{ @@ -177,9 +188,17 @@ func (scw *SplitCloneWorker) StatusAsText() string { return result } -func (scw *SplitCloneWorker) CheckInterrupted() bool { +// Cancel is part of the Worker interface +func (scw *SplitCloneWorker) Cancel() { + scw.ctxCancel() +} + +func (scw *SplitCloneWorker) checkInterrupted() bool { select { - case <-interrupted: + case <-scw.ctx.Done(): + if scw.ctx.Err() == context.DeadlineExceeded { + return false + } scw.recordError(topo.ErrInterrupted) return true default: @@ -216,23 +235,31 @@ func (scw *SplitCloneWorker) run() error { if err := scw.init(); err != nil { return fmt.Errorf("init() failed: %v", err) } - if scw.CheckInterrupted() { + if scw.checkInterrupted() { return topo.ErrInterrupted } // second state: find targets if err := scw.findTargets(); err != nil { + // A canceled context can appear to cause an application error + if scw.checkInterrupted() { + return topo.ErrInterrupted + } return fmt.Errorf("findTargets() failed: %v", err) } - if scw.CheckInterrupted() { + if scw.checkInterrupted() { return topo.ErrInterrupted } // third state: copy data if err := scw.copy(); err != nil { + // A canceled context can appear to cause an application error + if scw.checkInterrupted() { + return topo.ErrInterrupted + } return fmt.Errorf("copy() failed: %v", err) } - if scw.CheckInterrupted() { + if scw.checkInterrupted() { return topo.ErrInterrupted } @@ -262,6 +289,7 @@ func (scw *SplitCloneWorker) init() error { if os == nil { return fmt.Errorf("the specified shard %v/%v is not in any overlapping shard", scw.keyspace, scw.shard) } + scw.wr.Logger().Infof("Found overlapping shards: %v\n", jscfg.ToJson(os)) // one side should have served types, the other one none, // figure out wich is which, then double check them all @@ -302,7 +330,7 @@ func (scw *SplitCloneWorker) findTargets() error { // find an appropriate endpoint in the source shards scw.sourceAliases = make([]topo.TabletAlias, len(scw.sourceShards)) for i, si := range scw.sourceShards { - scw.sourceAliases[i], err = findChecker(scw.wr, scw.cleaner, scw.cell, si.Keyspace(), si.ShardName()) + scw.sourceAliases[i], err = findChecker(scw.ctx, scw.wr, scw.cleaner, scw.cell, si.Keyspace(), si.ShardName()) if err != nil { return fmt.Errorf("cannot find checker for %v/%v/%v: %v", scw.cell, si.Keyspace(), si.ShardName(), err) } @@ -317,11 +345,14 @@ func (scw *SplitCloneWorker) findTargets() error { return fmt.Errorf("cannot read tablet %v: %v", alias, err) } - if err := scw.wr.TabletManagerClient().StopSlave(context.TODO(), scw.sourceTablets[i], 30*time.Second); err != nil { + ctx, cancel := context.WithTimeout(scw.ctx, 60*time.Second) + err := scw.wr.TabletManagerClient().StopSlave(ctx, scw.sourceTablets[i]) + cancel() + if err != nil { return fmt.Errorf("cannot stop replication on tablet %v", alias) } - wrangler.RecordStartSlaveAction(scw.cleaner, scw.sourceTablets[i], 30*time.Second) + wrangler.RecordStartSlaveAction(scw.cleaner, scw.sourceTablets[i]) action, err := wrangler.FindChangeSlaveTypeActionByTarget(scw.cleaner, alias) if err != nil { return fmt.Errorf("cannot find ChangeSlaveType action for %v: %v", alias, err) @@ -329,66 +360,45 @@ func (scw *SplitCloneWorker) findTargets() error { action.TabletType = topo.TYPE_SPARE } - if scw.strategy.WriteMastersOnly { - return scw.findMasterTargets() - } else { - return scw.findAllTargets() - } + return scw.findMasterTargets() } // findMasterTargets looks up the masters for all destination shards, and set the destinations appropriately. // It should be used if vtworker will only want to write to masters. func (scw *SplitCloneWorker) findMasterTargets() error { - scw.destinationAliases = make([][]topo.TabletAlias, len(scw.destinationShards)) - scw.destinationTablets = make([]map[topo.TabletAlias]*topo.TabletInfo, len(scw.destinationShards)) - scw.destinationMasterAliases = make([]topo.TabletAlias, len(scw.destinationShards)) - - for shardIndex, si := range scw.destinationShards { - // keep a local copy of MasterAlias, so that it can't be changed out from under us - // TODO(aaijazi): find out if this is actually necessary - masterAlias := si.MasterAlias - if masterAlias.IsZero() { - return fmt.Errorf("no master in destination shard") - } - scw.wr.Logger().Infof("Found master alias %v in shard %v/%v", masterAlias, si.Keyspace(), si.ShardName()) - scw.destinationMasterAliases[shardIndex] = masterAlias - scw.destinationAliases[shardIndex] = []topo.TabletAlias{masterAlias} - mt, err := scw.wr.TopoServer().GetTablet(masterAlias) - if err != nil { - return fmt.Errorf("cannot read master tablet from alias %v in %v/%v", masterAlias, si.Keyspace(), si.ShardName()) - } - scw.destinationTablets[shardIndex] = map[topo.TabletAlias]*topo.TabletInfo{masterAlias: mt} - } - - return nil -} - -// findAllTargets finds all the target aliases and tablets in the destination shards -// It should be used if vtworker will write to all tablets in the destination shards, not just masters. -func (scw *SplitCloneWorker) findAllTargets() error { var err error scw.destinationAliases = make([][]topo.TabletAlias, len(scw.destinationShards)) scw.destinationTablets = make([]map[topo.TabletAlias]*topo.TabletInfo, len(scw.destinationShards)) scw.destinationMasterAliases = make([]topo.TabletAlias, len(scw.destinationShards)) + + scw.reloadAliases = make([][]topo.TabletAlias, len(scw.destinationShards)) + scw.reloadTablets = make([]map[topo.TabletAlias]*topo.TabletInfo, len(scw.destinationShards)) + for shardIndex, si := range scw.destinationShards { - scw.destinationAliases[shardIndex], err = topo.FindAllTabletAliasesInShard(scw.wr.TopoServer(), si.Keyspace(), si.ShardName()) + ctx, cancel := context.WithTimeout(scw.ctx, 60*time.Second) + scw.reloadAliases[shardIndex], err = topo.FindAllTabletAliasesInShard(ctx, scw.wr.TopoServer(), si.Keyspace(), si.ShardName()) + cancel() if err != nil { - return fmt.Errorf("cannot find all target tablets in %v/%v: %v", si.Keyspace(), si.ShardName(), err) + return fmt.Errorf("cannot find all reload target tablets in %v/%v: %v", si.Keyspace(), si.ShardName(), err) } - scw.wr.Logger().Infof("Found %v target aliases in shard %v/%v", len(scw.destinationAliases[shardIndex]), si.Keyspace(), si.ShardName()) + scw.wr.Logger().Infof("Found %v reload target aliases in shard %v/%v", len(scw.reloadAliases[shardIndex]), si.Keyspace(), si.ShardName()) // get the TabletInfo for all targets - scw.destinationTablets[shardIndex], err = topo.GetTabletMap(scw.wr.TopoServer(), scw.destinationAliases[shardIndex]) + ctx, cancel = context.WithTimeout(scw.ctx, 60*time.Second) + scw.reloadTablets[shardIndex], err = topo.GetTabletMap(ctx, scw.wr.TopoServer(), scw.reloadAliases[shardIndex]) + cancel() if err != nil { - return fmt.Errorf("cannot read all target tablets in %v/%v: %v", si.Keyspace(), si.ShardName(), err) + return fmt.Errorf("cannot read all reload target tablets in %v/%v: %v", si.Keyspace(), si.ShardName(), err) } // find and validate the master - for tabletAlias, ti := range scw.destinationTablets[shardIndex] { + for tabletAlias, ti := range scw.reloadTablets[shardIndex] { if ti.Type == topo.TYPE_MASTER { if scw.destinationMasterAliases[shardIndex].IsZero() { scw.destinationMasterAliases[shardIndex] = tabletAlias + scw.destinationAliases[shardIndex] = []topo.TabletAlias{tabletAlias} + scw.destinationTablets[shardIndex] = map[topo.TabletAlias]*topo.TabletInfo{tabletAlias: ti} } else { return fmt.Errorf("multiple masters in destination shard: %v and %v at least", scw.destinationMasterAliases[shardIndex], tabletAlias) } @@ -397,15 +407,16 @@ func (scw *SplitCloneWorker) findAllTargets() error { if scw.destinationMasterAliases[shardIndex].IsZero() { return fmt.Errorf("no master in destination shard") } + scw.wr.Logger().Infof("Found target master alias %v in shard %v/%v", scw.destinationMasterAliases[shardIndex], si.Keyspace(), si.ShardName()) } return nil } // copy phase: -// - get schema on the sources, filter tables -// - create tables on all destinations -// - copy the data +// - copy the data from source tablets to destination masters (wtih replication on) +// Assumes that the schema has already been created on each destination tablet +// (probably from vtctl's CopySchemaShard) func (scw *SplitCloneWorker) copy() error { scw.setState(stateSCCopy) @@ -414,7 +425,9 @@ func (scw *SplitCloneWorker) copy() error { // on all source shards. Furthermore, we estimate the number of rows // in each source shard for each table to be about the same // (rowCount is used to estimate an ETA) - sourceSchemaDefinition, err := scw.wr.GetSchema(scw.sourceAliases[0], nil, scw.excludeTables, true) + ctx, cancel := context.WithTimeout(scw.ctx, 60*time.Second) + sourceSchemaDefinition, err := scw.wr.GetSchema(ctx, scw.sourceAliases[0], nil, scw.excludeTables, true) + cancel() if err != nil { return fmt.Errorf("cannot get schema from source %v: %v", scw.sourceAliases[0], err) } @@ -423,36 +436,20 @@ func (scw *SplitCloneWorker) copy() error { } scw.wr.Logger().Infof("Source tablet 0 has %v tables to copy", len(sourceSchemaDefinition.TableDefinitions)) scw.mu.Lock() - scw.tableStatus = make([]tableStatus, len(sourceSchemaDefinition.TableDefinitions)) + scw.tableStatus = make([]*tableStatus, len(sourceSchemaDefinition.TableDefinitions)) for i, td := range sourceSchemaDefinition.TableDefinitions { - scw.tableStatus[i].name = td.Name - scw.tableStatus[i].rowCount = td.RowCount * uint64(len(scw.sourceAliases)) + scw.tableStatus[i] = &tableStatus{ + name: td.Name, + rowCount: td.RowCount * uint64(len(scw.sourceAliases)), + } } scw.startTime = time.Now() scw.mu.Unlock() - // Create all the commands to create the destination schema: - // - createDbCmds will create the database and the tables - // - createViewCmds will create the views - // - alterTablesCmds will modify the tables at the end if needed - // (all need template substitution for {{.DatabaseName}}) - createDbCmds := make([]string, 0, len(sourceSchemaDefinition.TableDefinitions)+1) - createDbCmds = append(createDbCmds, sourceSchemaDefinition.DatabaseSchema) - createViewCmds := make([]string, 0, 16) - alterTablesCmds := make([]string, 0, 16) + // Find the column index for the sharding columns in all the databases, and count rows columnIndexes := make([]int, len(sourceSchemaDefinition.TableDefinitions)) for tableIndex, td := range sourceSchemaDefinition.TableDefinitions { if td.Type == myproto.TABLE_BASE_TABLE { - // build the create and alter statements - create, alter, err := mysqlctl.MakeSplitCreateTableSql(scw.wr.Logger(), td.Schema, "{{.DatabaseName}}", td.Name, scw.strategy) - if err != nil { - return fmt.Errorf("MakeSplitCreateTableSql(%v) returned: %v", td.Name, err) - } - createDbCmds = append(createDbCmds, create) - if alter != "" { - alterTablesCmds = append(alterTablesCmds, alter) - } - // find the column to split on columnIndexes[tableIndex] = -1 for i, name := range td.Columns { @@ -466,44 +463,37 @@ func (scw *SplitCloneWorker) copy() error { } scw.tableStatus[tableIndex].mu.Lock() - scw.tableStatus[tableIndex].state = "before table creation" scw.tableStatus[tableIndex].rowCount = td.RowCount scw.tableStatus[tableIndex].mu.Unlock() } else { scw.tableStatus[tableIndex].mu.Lock() - createViewCmds = append(createViewCmds, td.Schema) - scw.tableStatus[tableIndex].state = "before view creation" - scw.tableStatus[tableIndex].rowCount = 0 + scw.tableStatus[tableIndex].isView = true scw.tableStatus[tableIndex].mu.Unlock() } } - // For each destination tablet (in parallel): - // - create the schema - // - setup the channels to send SQL data chunks + // In parallel, setup the channels to send SQL data chunks to for each destination tablet: // - // mu protects the abort channel for closing, and firstError + // mu protects the context for cancelation, and firstError mu := sync.Mutex{} - abort := make(chan struct{}) var firstError error processError := func(format string, args ...interface{}) { scw.wr.Logger().Errorf(format, args...) mu.Lock() - if abort != nil { - close(abort) - abort = nil + if !scw.checkInterrupted() { + scw.Cancel() firstError = fmt.Errorf(format, args...) } mu.Unlock() } - // if we're writing only to masters, we need to enable bin logs so that replication happens - disableBinLogs := !scw.strategy.WriteMastersOnly + // since we're writing only to masters, we need to enable bin logs so that replication happens + disableBinLogs := false insertChannels := make([][]chan string, len(scw.destinationShards)) destinationWaitGroup := sync.WaitGroup{} - for shardIndex, _ := range scw.destinationShards { + for shardIndex := range scw.destinationShards { insertChannels[shardIndex] = make([]chan string, len(scw.destinationAliases[shardIndex])) for i, tabletAlias := range scw.destinationAliases[shardIndex] { // we create one channel per destination tablet. It @@ -513,26 +503,12 @@ func (scw *SplitCloneWorker) copy() error { // destinationWriterCount go routines reading from it. insertChannels[shardIndex][i] = make(chan string, scw.destinationWriterCount*2) - destinationWaitGroup.Add(1) go func(ti *topo.TabletInfo, insertChannel chan string) { - defer destinationWaitGroup.Done() - scw.wr.Logger().Infof("Creating tables on tablet %v", ti.Alias) - if err := runSqlCommands(scw.wr, ti, createDbCmds, abort, disableBinLogs); err != nil { - processError("createDbCmds failed: %v", err) - return - } - if len(createViewCmds) > 0 { - scw.wr.Logger().Infof("Creating views on tablet %v", ti.Alias) - if err := runSqlCommands(scw.wr, ti, createViewCmds, abort, disableBinLogs); err != nil { - processError("createViewCmds failed: %v", err) - return - } - } for j := 0; j < scw.destinationWriterCount; j++ { destinationWaitGroup.Add(1) go func() { defer destinationWaitGroup.Done() - if err := executeFetchLoop(scw.wr, ti, insertChannel, abort, disableBinLogs); err != nil { + if err := executeFetchLoop(scw.ctx, scw.wr, ti, insertChannel, disableBinLogs); err != nil { processError("executeFetchLoop failed: %v", err) } }() @@ -544,7 +520,7 @@ func (scw *SplitCloneWorker) copy() error { // Now for each table, read data chunks and send them to all // insertChannels sourceWaitGroup := sync.WaitGroup{} - for shardIndex, _ := range scw.sourceShards { + for shardIndex := range scw.sourceShards { sema := sync2.NewSemaphore(scw.sourceReaderCount, 0) for tableIndex, td := range sourceSchemaDefinition.TableDefinitions { if td.Type == myproto.TABLE_VIEW { @@ -553,10 +529,11 @@ func (scw *SplitCloneWorker) copy() error { rowSplitter := NewRowSplitter(scw.destinationShards, scw.keyspaceInfo.ShardingColumnType, columnIndexes[tableIndex]) - chunks, err := findChunks(scw.wr, scw.sourceTablets[shardIndex], td, scw.minTableSizeForSplit, scw.sourceReaderCount) + chunks, err := findChunks(scw.ctx, scw.wr, scw.sourceTablets[shardIndex], td, scw.minTableSizeForSplit, scw.sourceReaderCount) if err != nil { return err } + scw.tableStatus[tableIndex].setThreadCount(len(chunks) - 1) for chunkIndex := 0; chunkIndex < len(chunks)-1; chunkIndex++ { sourceWaitGroup.Add(1) @@ -566,25 +543,29 @@ func (scw *SplitCloneWorker) copy() error { sema.Acquire() defer sema.Release() + scw.tableStatus[tableIndex].threadStarted() + // build the query, and start the streaming selectSQL := buildSQLFromChunks(scw.wr, td, chunks, chunkIndex, scw.sourceAliases[shardIndex].String()) - qrr, err := NewQueryResultReaderForTablet(scw.wr.TopoServer(), scw.sourceAliases[shardIndex], selectSQL) + qrr, err := NewQueryResultReaderForTablet(scw.ctx, scw.wr.TopoServer(), scw.sourceAliases[shardIndex], selectSQL) if err != nil { processError("NewQueryResultReaderForTablet failed: %v", err) return } + defer qrr.Close() // process the data - if err := scw.processData(td, tableIndex, qrr, rowSplitter, insertChannels, abort); err != nil { + if err := scw.processData(td, tableIndex, qrr, rowSplitter, insertChannels, scw.destinationPackCount, scw.ctx.Done()); err != nil { processError("processData failed: %v", err) } + scw.tableStatus[tableIndex].threadDone() }(td, tableIndex, chunkIndex) } } } sourceWaitGroup.Wait() - for shardIndex, _ := range scw.destinationShards { + for shardIndex := range scw.destinationShards { for _, c := range insertChannels[shardIndex] { close(c) } @@ -594,26 +575,6 @@ func (scw *SplitCloneWorker) copy() error { return firstError } - // do the post-copy alters if any - if len(alterTablesCmds) > 0 { - for shardIndex, _ := range scw.destinationShards { - for _, tabletAlias := range scw.destinationAliases[shardIndex] { - destinationWaitGroup.Add(1) - go func(ti *topo.TabletInfo) { - defer destinationWaitGroup.Done() - scw.wr.Logger().Infof("Altering tables on tablet %v", ti.Alias) - if err := runSqlCommands(scw.wr, ti, alterTablesCmds, abort, disableBinLogs); err != nil { - processError("alterTablesCmds failed on tablet %v: %v", ti.Alias, err) - } - }(scw.destinationTablets[shardIndex][tabletAlias]) - } - } - destinationWaitGroup.Wait() - if firstError != nil { - return firstError - } - } - // then create and populate the blp_checkpoint table if scw.strategy.PopulateBlpCheckpoint { queries := make([]string, 0, 4) @@ -624,8 +585,10 @@ func (scw *SplitCloneWorker) copy() error { } // get the current position from the sources - for shardIndex, _ := range scw.sourceShards { - status, err := scw.wr.TabletManagerClient().SlaveStatus(context.TODO(), scw.sourceTablets[shardIndex], 30*time.Second) + for shardIndex := range scw.sourceShards { + ctx, cancel := context.WithTimeout(scw.ctx, 60*time.Second) + status, err := scw.wr.TabletManagerClient().SlaveStatus(ctx, scw.sourceTablets[shardIndex]) + cancel() if err != nil { return err } @@ -633,13 +596,13 @@ func (scw *SplitCloneWorker) copy() error { queries = append(queries, binlogplayer.PopulateBlpCheckpoint(0, status.Position, time.Now().Unix(), flags)) } - for shardIndex, _ := range scw.destinationShards { + for shardIndex := range scw.destinationShards { for _, tabletAlias := range scw.destinationAliases[shardIndex] { destinationWaitGroup.Add(1) go func(ti *topo.TabletInfo) { defer destinationWaitGroup.Done() scw.wr.Logger().Infof("Making and populating blp_checkpoint table on tablet %v", ti.Alias) - if err := runSqlCommands(scw.wr, ti, queries, abort, disableBinLogs); err != nil { + if err := runSqlCommands(scw.ctx, scw.wr, ti, queries, disableBinLogs); err != nil { processError("blp_checkpoint queries failed on tablet %v: %v", ti.Alias, err) } }(scw.destinationTablets[shardIndex][tabletAlias]) @@ -660,7 +623,10 @@ func (scw *SplitCloneWorker) copy() error { } else { for _, si := range scw.destinationShards { scw.wr.Logger().Infof("Setting SourceShard on shard %v/%v", si.Keyspace(), si.ShardName()) - if err := scw.wr.SetSourceShards(si.Keyspace(), si.ShardName(), scw.sourceAliases, nil); err != nil { + ctx, cancel := context.WithTimeout(scw.ctx, 60*time.Second) + err := scw.wr.SetSourceShards(ctx, si.Keyspace(), si.ShardName(), scw.sourceAliases, nil) + cancel() + if err != nil { return fmt.Errorf("Failed to set source shards: %v", err) } } @@ -669,16 +635,19 @@ func (scw *SplitCloneWorker) copy() error { // And force a schema reload on all destination tablets. // The master tablet will end up starting filtered replication // at this point. - for shardIndex, _ := range scw.destinationShards { - for _, tabletAlias := range scw.destinationAliases[shardIndex] { + for shardIndex := range scw.destinationShards { + for _, tabletAlias := range scw.reloadAliases[shardIndex] { destinationWaitGroup.Add(1) go func(ti *topo.TabletInfo) { defer destinationWaitGroup.Done() scw.wr.Logger().Infof("Reloading schema on tablet %v", ti.Alias) - if err := scw.wr.TabletManagerClient().ReloadSchema(context.TODO(), ti, 30*time.Second); err != nil { + ctx, cancel := context.WithTimeout(scw.ctx, 60*time.Second) + err := scw.wr.TabletManagerClient().ReloadSchema(ctx, ti) + cancel() + if err != nil { processError("ReloadSchema failed on tablet %v: %v", ti.Alias, err) } - }(scw.destinationTablets[shardIndex][tabletAlias]) + }(scw.reloadTablets[shardIndex][tabletAlias]) } } destinationWaitGroup.Wait() @@ -687,45 +656,61 @@ func (scw *SplitCloneWorker) copy() error { // processData pumps the data out of the provided QueryResultReader. // It returns any error the source encounters. -func (scw *SplitCloneWorker) processData(td *myproto.TableDefinition, tableIndex int, qrr *QueryResultReader, rowSplitter *RowSplitter, insertChannels [][]chan string, abort chan struct{}) error { +func (scw *SplitCloneWorker) processData(td *myproto.TableDefinition, tableIndex int, qrr *QueryResultReader, rowSplitter *RowSplitter, insertChannels [][]chan string, destinationPackCount int, abort <-chan struct{}) error { baseCmd := td.Name + "(" + strings.Join(td.Columns, ", ") + ") VALUES " + sr := rowSplitter.StartSplit() + packCount := 0 for { select { case r, ok := <-qrr.Output: if !ok { - return qrr.Error() + // we are done, see if there was an error + err := qrr.Error() + if err != nil { + return err + } + + // send the remainder if any (ignoring + // the return value, we don't care + // here if we're aborted) + if packCount > 0 { + rowSplitter.Send(qrr.Fields, sr, baseCmd, insertChannels, abort) + } + return nil } // Split the rows by keyspace_id, and insert // each chunk into each destination - sr, err := rowSplitter.Split(r.Rows) - if err != nil { + if err := rowSplitter.Split(sr, r.Rows); err != nil { return fmt.Errorf("RowSplitter failed for table %v: %v", td.Name, err) } + scw.tableStatus[tableIndex].addCopiedRows(len(r.Rows)) + + // see if we reach the destination pack count + packCount++ + if packCount < destinationPackCount { + continue + } // send the rows to be inserted - scw.tableStatus[tableIndex].addCopiedRows(len(r.Rows)) - for i, cs := range insertChannels { - // one of the chunks might be empty, so no need - // to send data in that case - if len(sr[i]) > 0 { - cmd := baseCmd + makeValueString(qrr.Fields, sr[i]) - for _, c := range cs { - // also check on abort, so we don't wait forever - select { - case c <- cmd: - case <-abort: - return nil - } - } - } + if aborted := rowSplitter.Send(qrr.Fields, sr, baseCmd, insertChannels, abort); aborted { + return nil } + + // and reset our row buffer + sr = rowSplitter.StartSplit() + packCount = 0 + + case <-abort: + return nil + } + // the abort case might be starved above, so we check again; this means that the loop + // will run at most once before the abort case is triggered. + select { case <-abort: - // FIXME(alainjobart): note this select case - // could be starved here, and we might miss - // the abort in some corner cases. return nil + default: } } } diff --git a/go/vt/worker/split_clone_test.go b/go/vt/worker/split_clone_test.go index a91195b17a5..a32cff55bfc 100644 --- a/go/vt/worker/split_clone_test.go +++ b/go/vt/worker/split_clone_test.go @@ -12,7 +12,6 @@ import ( "testing" "time" - "code.google.com/p/go.net/context" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/sqltypes" "github.com/youtube/vitess/go/vt/dbconnpool" @@ -26,6 +25,7 @@ import ( "github.com/youtube/vitess/go/vt/wrangler" "github.com/youtube/vitess/go/vt/wrangler/testlib" "github.com/youtube/vitess/go/vt/zktopo" + "golang.org/x/net/context" ) // This is a local SqlQuery RCP implementation to support the tests @@ -111,27 +111,7 @@ type FakePoolConnection struct { ExpectedExecuteFetchIndex int } -func NewFakePoolConnectionQueryBinlogOff(t *testing.T, query string) *FakePoolConnection { - return &FakePoolConnection{ - t: t, - ExpectedExecuteFetch: []ExpectedExecuteFetch{ - ExpectedExecuteFetch{ - Query: "SET sql_log_bin = OFF", - QueryResult: &mproto.QueryResult{}, - }, - ExpectedExecuteFetch{ - Query: query, - QueryResult: &mproto.QueryResult{}, - }, - ExpectedExecuteFetch{ - Query: "SET sql_log_bin = ON", - QueryResult: &mproto.QueryResult{}, - }, - }, - } -} - -func NewFakePoolConnectionQueryBinlogOn(t *testing.T, query string) *FakePoolConnection { +func NewFakePoolConnectionQuery(t *testing.T, query string) *FakePoolConnection { return &FakePoolConnection{ t: t, ExpectedExecuteFetch: []ExpectedExecuteFetch{ @@ -171,7 +151,7 @@ func (fpc *FakePoolConnection) ExecuteStreamFetch(query string, callback func(*m return nil } -func (fpc *FakePoolConnection) Id() int64 { +func (fpc *FakePoolConnection) ID() int64 { return 1 } @@ -186,6 +166,10 @@ func (fpc *FakePoolConnection) IsClosed() bool { func (fpc *FakePoolConnection) Recycle() { } +func (fpc *FakePoolConnection) Reconnect() error { + return nil +} + // on the source rdonly guy, should only have one query to find min & max func SourceRdonlyFactory(t *testing.T) func() (dbconnpool.PoolConnection, error) { return func() (dbconnpool.PoolConnection, error) { @@ -219,66 +203,46 @@ func SourceRdonlyFactory(t *testing.T) func() (dbconnpool.PoolConnection, error) } // on the destinations -func DestinationsFactory(t *testing.T, rowCount int64, disableBinLogs bool) func() (dbconnpool.PoolConnection, error) { +func DestinationsFactory(t *testing.T, insertCount int64) func() (dbconnpool.PoolConnection, error) { var queryIndex int64 = -1 - var newFakePoolConnectionFactory func(*testing.T, string) *FakePoolConnection - if disableBinLogs { - newFakePoolConnectionFactory = NewFakePoolConnectionQueryBinlogOff - } else { - newFakePoolConnectionFactory = NewFakePoolConnectionQueryBinlogOn - } - return func() (dbconnpool.PoolConnection, error) { qi := atomic.AddInt64(&queryIndex, 1) switch { - case qi == 0: - return newFakePoolConnectionFactory(t, "CREATE DATABASE `vt_ks` /*!40100 DEFAULT CHARACTER SET utf8 */"), nil - case qi == 1: - return newFakePoolConnectionFactory(t, "CREATE TABLE `vt_ks`.`resharding1` (\n"+ - " `id` bigint(20) NOT NULL,\n"+ - " `msg` varchar(64) DEFAULT NULL,\n"+ - " `keyspace_id` bigint(20) unsigned NOT NULL,\n"+ - " PRIMARY KEY (`id`),\n"+ - " KEY `by_msg` (`msg`)\n"+ - ") ENGINE=InnoDB DEFAULT CHARSET=utf8"), nil - case qi >= 2 && qi < rowCount+2: - return newFakePoolConnectionFactory(t, "INSERT INTO `vt_ks`.table1(id, msg, keyspace_id) VALUES (*"), nil - case qi == rowCount+2: - return newFakePoolConnectionFactory(t, "ALTER TABLE `vt_ks`.`table1` MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT"), nil - case qi == rowCount+3: - return newFakePoolConnectionFactory(t, "CREATE DATABASE IF NOT EXISTS _vt"), nil - case qi == rowCount+4: - return newFakePoolConnectionFactory(t, "CREATE TABLE IF NOT EXISTS _vt.blp_checkpoint (\n"+ + case qi < insertCount: + return NewFakePoolConnectionQuery(t, "INSERT INTO `vt_ks`.table1(id, msg, keyspace_id) VALUES (*"), nil + case qi == insertCount: + return NewFakePoolConnectionQuery(t, "CREATE DATABASE IF NOT EXISTS _vt"), nil + case qi == insertCount+1: + return NewFakePoolConnectionQuery(t, "CREATE TABLE IF NOT EXISTS _vt.blp_checkpoint (\n"+ " source_shard_uid INT(10) UNSIGNED NOT NULL,\n"+ " pos VARCHAR(250) DEFAULT NULL,\n"+ " time_updated BIGINT UNSIGNED NOT NULL,\n"+ " transaction_timestamp BIGINT UNSIGNED NOT NULL,\n"+ " flags VARCHAR(250) DEFAULT NULL,\n"+ " PRIMARY KEY (source_shard_uid)) ENGINE=InnoDB"), nil - case qi == rowCount+5: - return newFakePoolConnectionFactory(t, "INSERT INTO _vt.blp_checkpoint (source_shard_uid, pos, time_updated, transaction_timestamp, flags) VALUES (0, 'MariaDB/12-34-5678', *"), nil + case qi == insertCount+2: + return NewFakePoolConnectionQuery(t, "INSERT INTO _vt.blp_checkpoint (source_shard_uid, pos, time_updated, transaction_timestamp, flags) VALUES (0, 'MariaDB/12-34-5678', *"), nil } return nil, fmt.Errorf("Unexpected connection") } } -func TestSplitCloneWriteMastersOnly(t *testing.T) { - testSplitClone(t, "-populate_blp_checkpoint -delay_auto_increment -write_masters_only") -} - -func TestSplitCloneBinlogDisabled(t *testing.T) { - testSplitClone(t, "-populate_blp_checkpoint -delay_auto_increment") +func TestSplitClonePopulateBlpCheckpoint(t *testing.T) { + testSplitClone(t, "-populate_blp_checkpoint") } func testSplitClone(t *testing.T, strategy string) { ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) - wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Minute, time.Second) + wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Second) sourceMaster := testlib.NewFakeTablet(t, wr, "cell1", 0, topo.TYPE_MASTER, testlib.TabletKeyspaceShard(t, "ks", "-80")) - sourceRdonly := testlib.NewFakeTablet(t, wr, "cell1", 1, + sourceRdonly1 := testlib.NewFakeTablet(t, wr, "cell1", 1, + topo.TYPE_RDONLY, testlib.TabletKeyspaceShard(t, "ks", "-80"), + testlib.TabletParent(sourceMaster.Tablet.Alias)) + sourceRdonly2 := testlib.NewFakeTablet(t, wr, "cell1", 2, topo.TYPE_RDONLY, testlib.TabletKeyspaceShard(t, "ks", "-80"), testlib.TabletParent(sourceMaster.Tablet.Alias)) @@ -294,56 +258,63 @@ func testSplitClone(t *testing.T, strategy string) { topo.TYPE_RDONLY, testlib.TabletKeyspaceShard(t, "ks", "40-80"), testlib.TabletParent(rightMaster.Tablet.Alias)) - for _, ft := range []*testlib.FakeTablet{sourceMaster, sourceRdonly, leftMaster, leftRdonly, rightMaster, rightRdonly} { + for _, ft := range []*testlib.FakeTablet{sourceMaster, sourceRdonly1, sourceRdonly2, leftMaster, leftRdonly, rightMaster, rightRdonly} { ft.StartActionLoop(t, wr) defer ft.StopActionLoop(t) } // add the topo and schema data we'll need + ctx := context.Background() if err := topo.CreateShard(ts, "ks", "80-"); err != nil { t.Fatalf("CreateShard(\"-80\") failed: %v", err) } - if err := wr.SetKeyspaceShardingInfo("ks", "keyspace_id", key.KIT_UINT64, 4, false); err != nil { + if err := wr.SetKeyspaceShardingInfo(ctx, "ks", "keyspace_id", key.KIT_UINT64, 4, false); err != nil { t.Fatalf("SetKeyspaceShardingInfo failed: %v", err) } - if err := wr.RebuildKeyspaceGraph("ks", nil); err != nil { + if err := wr.RebuildKeyspaceGraph(ctx, "ks", nil); err != nil { t.Fatalf("RebuildKeyspaceGraph failed: %v", err) } - gwrk, err := NewSplitCloneWorker(wr, "cell1", "ks", "-80", nil, strategy, 10, 1, 10) + gwrk, err := NewSplitCloneWorker(wr, "cell1", "ks", "-80", nil, strategy, 10 /*sourceReaderCount*/, 4 /*destinationPackCount*/, 1 /*minTableSizeForSplit*/, 10 /*destinationWriterCount*/) if err != nil { t.Errorf("Worker creation failed: %v", err) } wrk := gwrk.(*SplitCloneWorker) - // the only strategy that enables bin logs - disableBinLogs := !wrk.strategy.WriteMastersOnly - - sourceRdonly.FakeMysqlDaemon.Schema = &myproto.SchemaDefinition{ - DatabaseSchema: "CREATE DATABASE `{{.DatabaseName}}` /*!40100 DEFAULT CHARACTER SET utf8 */", - TableDefinitions: []*myproto.TableDefinition{ - &myproto.TableDefinition{ - Name: "table1", - Schema: "CREATE TABLE `resharding1` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `msg` varchar(64) DEFAULT NULL,\n `keyspace_id` bigint(20) unsigned NOT NULL,\n PRIMARY KEY (`id`),\n KEY `by_msg` (`msg`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8", - Columns: []string{"id", "msg", "keyspace_id"}, - PrimaryKeyColumns: []string{"id"}, - Type: myproto.TABLE_BASE_TABLE, - DataLength: 2048, - RowCount: 100, + + for _, sourceRdonly := range []*testlib.FakeTablet{sourceRdonly1, sourceRdonly2} { + sourceRdonly.FakeMysqlDaemon.Schema = &myproto.SchemaDefinition{ + DatabaseSchema: "", + TableDefinitions: []*myproto.TableDefinition{ + &myproto.TableDefinition{ + Name: "table1", + Columns: []string{"id", "msg", "keyspace_id"}, + PrimaryKeyColumns: []string{"id"}, + Type: myproto.TABLE_BASE_TABLE, + // This informs how many rows we can pack into a single insert + DataLength: 2048, + }, }, - }, - Version: "unused", - } - sourceRdonly.FakeMysqlDaemon.DbaConnectionFactory = SourceRdonlyFactory(t) - sourceRdonly.FakeMysqlDaemon.CurrentSlaveStatus = &myproto.ReplicationStatus{ - Position: myproto.ReplicationPosition{ - GTIDSet: myproto.MariadbGTID{Domain: 12, Server: 34, Sequence: 5678}, - }, + } + sourceRdonly.FakeMysqlDaemon.DbaConnectionFactory = SourceRdonlyFactory(t) + sourceRdonly.FakeMysqlDaemon.CurrentSlaveStatus = &myproto.ReplicationStatus{ + Position: myproto.ReplicationPosition{ + GTIDSet: myproto.MariadbGTID{Domain: 12, Server: 34, Sequence: 5678}, + }, + } + sourceRdonly.RPCServer.Register(&SqlQuery{t: t}) } - sourceRdonly.RpcServer.Register(&SqlQuery{t: t}) - leftMaster.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t, 50, disableBinLogs) - leftRdonly.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t, 50, disableBinLogs) - rightMaster.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t, 50, disableBinLogs) - rightRdonly.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t, 50, disableBinLogs) + + // We read 100 source rows. sourceReaderCount is set to 10, so + // we'll have 100/10=10 rows per table chunk. + // destinationPackCount is set to 4, so we take 4 source rows + // at once. So we'll process 4 + 4 + 2 rows to get to 10. + // That means 3 insert statements on each target (each + // containing half of the rows, i.e. 2 + 2 + 1 rows). So 3 * 10 + // = 30 insert statements on each destination. + leftMaster.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t, 30) + leftRdonly.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t, 30) + rightMaster.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t, 30) + rightRdonly.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t, 30) wrk.Run() status := wrk.StatusAsText() diff --git a/go/vt/worker/split_diff.go b/go/vt/worker/split_diff.go index 72196aa43fa..2c6c471e65b 100644 --- a/go/vt/worker/split_diff.go +++ b/go/vt/worker/split_diff.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/sync2" blproto "github.com/youtube/vitess/go/vt/binlog/proto" @@ -37,11 +37,13 @@ const ( // SplitDiffWorker executes a diff between a destination shard and its // source shards in a shard split case. type SplitDiffWorker struct { - wr *wrangler.Wrangler - cell string - keyspace string - shard string - cleaner *wrangler.Cleaner + wr *wrangler.Wrangler + cell string + keyspace string + shard string + cleaner *wrangler.Cleaner + ctx context.Context + ctxCancel context.CancelFunc // all subsequent fields are protected by the mutex mu sync.Mutex @@ -63,14 +65,17 @@ type SplitDiffWorker struct { destinationSchemaDefinition *myproto.SchemaDefinition } -// NewSplitDiff returns a new SplitDiffWorker object. +// NewSplitDiffWorker returns a new SplitDiffWorker object. func NewSplitDiffWorker(wr *wrangler.Wrangler, cell, keyspace, shard string) Worker { + ctx, cancel := context.WithCancel(context.Background()) return &SplitDiffWorker{ - wr: wr, - cell: cell, - keyspace: keyspace, - shard: shard, - cleaner: &wrangler.Cleaner{}, + wr: wr, + cell: cell, + keyspace: keyspace, + shard: shard, + cleaner: &wrangler.Cleaner{}, + ctx: ctx, + ctxCancel: cancel, state: stateSDNotSarted, } @@ -89,6 +94,7 @@ func (sdw *SplitDiffWorker) recordError(err error) { sdw.mu.Unlock() } +// StatusAsHTML is part of the Worker interface func (sdw *SplitDiffWorker) StatusAsHTML() template.HTML { sdw.mu.Lock() defer sdw.mu.Unlock() @@ -106,6 +112,7 @@ func (sdw *SplitDiffWorker) StatusAsHTML() template.HTML { return template.HTML(result) } +// StatusAsText is part of the Worker interface func (sdw *SplitDiffWorker) StatusAsText() string { sdw.mu.Lock() defer sdw.mu.Unlock() @@ -122,9 +129,17 @@ func (sdw *SplitDiffWorker) StatusAsText() string { return result } -func (sdw *SplitDiffWorker) CheckInterrupted() bool { +// Cancel is part of the Worker interface +func (sdw *SplitDiffWorker) Cancel() { + sdw.ctxCancel() +} + +func (sdw *SplitDiffWorker) checkInterrupted() bool { select { - case <-interrupted: + case <-sdw.ctx.Done(): + if sdw.ctx.Err() == context.DeadlineExceeded { + return false + } sdw.recordError(topo.ErrInterrupted) return true default: @@ -161,7 +176,7 @@ func (sdw *SplitDiffWorker) run() error { if err := sdw.init(); err != nil { return fmt.Errorf("init() failed: %v", err) } - if sdw.CheckInterrupted() { + if sdw.checkInterrupted() { return topo.ErrInterrupted } @@ -169,20 +184,26 @@ func (sdw *SplitDiffWorker) run() error { if err := sdw.findTargets(); err != nil { return fmt.Errorf("findTargets() failed: %v", err) } - if sdw.CheckInterrupted() { + if sdw.checkInterrupted() { return topo.ErrInterrupted } // third phase: synchronize replication if err := sdw.synchronizeReplication(); err != nil { + if sdw.checkInterrupted() { + return topo.ErrInterrupted + } return fmt.Errorf("synchronizeReplication() failed: %v", err) } - if sdw.CheckInterrupted() { + if sdw.checkInterrupted() { return topo.ErrInterrupted } // fourth phase: diff if err := sdw.diff(); err != nil { + if sdw.checkInterrupted() { + return topo.ErrInterrupted + } return fmt.Errorf("diff() failed: %v", err) } @@ -223,7 +244,7 @@ func (sdw *SplitDiffWorker) findTargets() error { // find an appropriate endpoint in destination shard var err error - sdw.destinationAlias, err = findChecker(sdw.wr, sdw.cleaner, sdw.cell, sdw.keyspace, sdw.shard) + sdw.destinationAlias, err = findChecker(sdw.ctx, sdw.wr, sdw.cleaner, sdw.cell, sdw.keyspace, sdw.shard) if err != nil { return fmt.Errorf("cannot find checker for %v/%v/%v: %v", sdw.cell, sdw.keyspace, sdw.shard, err) } @@ -231,7 +252,7 @@ func (sdw *SplitDiffWorker) findTargets() error { // find an appropriate endpoint in the source shards sdw.sourceAliases = make([]topo.TabletAlias, len(sdw.shardInfo.SourceShards)) for i, ss := range sdw.shardInfo.SourceShards { - sdw.sourceAliases[i], err = findChecker(sdw.wr, sdw.cleaner, sdw.cell, sdw.keyspace, ss.Shard) + sdw.sourceAliases[i], err = findChecker(sdw.ctx, sdw.wr, sdw.cleaner, sdw.cell, sdw.keyspace, ss.Shard) if err != nil { return fmt.Errorf("cannot find checker for %v/%v/%v: %v", sdw.cell, sdw.keyspace, ss.Shard, err) } @@ -268,11 +289,13 @@ func (sdw *SplitDiffWorker) synchronizeReplication() error { // 1 - stop the master binlog replication, get its current position sdw.wr.Logger().Infof("Stopping master binlog replication on %v", sdw.shardInfo.MasterAlias) - blpPositionList, err := sdw.wr.TabletManagerClient().StopBlp(context.TODO(), masterInfo, 30*time.Second) + ctx, cancel := context.WithTimeout(sdw.ctx, 60*time.Second) + blpPositionList, err := sdw.wr.TabletManagerClient().StopBlp(ctx, masterInfo) + cancel() if err != nil { return fmt.Errorf("StopBlp for %v failed: %v", sdw.shardInfo.MasterAlias, err) } - wrangler.RecordStartBlpAction(sdw.cleaner, masterInfo, 30*time.Second) + wrangler.RecordStartBlpAction(sdw.cleaner, masterInfo) // 2 - stop all the source 'checker' at a binlog position // higher than the destination master @@ -294,7 +317,9 @@ func (sdw *SplitDiffWorker) synchronizeReplication() error { // stop replication sdw.wr.Logger().Infof("Stopping slave[%v] %v at a minimum of %v", i, sdw.sourceAliases[i], blpPos.Position) - stoppedAt, err := sdw.wr.TabletManagerClient().StopSlaveMinimum(context.TODO(), sourceTablet, blpPos.Position, 30*time.Second) + ctx, cancel := context.WithTimeout(sdw.ctx, 60*time.Second) + stoppedAt, err := sdw.wr.TabletManagerClient().StopSlaveMinimum(ctx, sourceTablet, blpPos.Position, 30*time.Second) + cancel() if err != nil { return fmt.Errorf("cannot stop slave %v at right binlog position %v: %v", sdw.sourceAliases[i], blpPos.Position, err) } @@ -303,7 +328,7 @@ func (sdw *SplitDiffWorker) synchronizeReplication() error { // change the cleaner actions from ChangeSlaveType(rdonly) // to StartSlave() + ChangeSlaveType(spare) - wrangler.RecordStartSlaveAction(sdw.cleaner, sourceTablet, 30*time.Second) + wrangler.RecordStartSlaveAction(sdw.cleaner, sourceTablet) action, err := wrangler.FindChangeSlaveTypeActionByTarget(sdw.cleaner, sdw.sourceAliases[i]) if err != nil { return fmt.Errorf("cannot find ChangeSlaveType action for %v: %v", sdw.sourceAliases[i], err) @@ -314,7 +339,9 @@ func (sdw *SplitDiffWorker) synchronizeReplication() error { // 3 - ask the master of the destination shard to resume filtered // replication up to the new list of positions sdw.wr.Logger().Infof("Restarting master %v until it catches up to %v", sdw.shardInfo.MasterAlias, stopPositionList) - masterPos, err := sdw.wr.TabletManagerClient().RunBlpUntil(context.TODO(), masterInfo, &stopPositionList, 30*time.Second) + ctx, cancel = context.WithTimeout(sdw.ctx, 60*time.Second) + masterPos, err := sdw.wr.TabletManagerClient().RunBlpUntil(ctx, masterInfo, &stopPositionList, 30*time.Second) + cancel() if err != nil { return fmt.Errorf("RunBlpUntil for %v until %v failed: %v", sdw.shardInfo.MasterAlias, stopPositionList, err) } @@ -326,11 +353,13 @@ func (sdw *SplitDiffWorker) synchronizeReplication() error { if err != nil { return err } - _, err = sdw.wr.TabletManagerClient().StopSlaveMinimum(context.TODO(), destinationTablet, masterPos, 30*time.Second) + ctx, cancel = context.WithTimeout(sdw.ctx, 60*time.Second) + _, err = sdw.wr.TabletManagerClient().StopSlaveMinimum(ctx, destinationTablet, masterPos, 30*time.Second) + cancel() if err != nil { return fmt.Errorf("StopSlaveMinimum for %v at %v failed: %v", sdw.destinationAlias, masterPos, err) } - wrangler.RecordStartSlaveAction(sdw.cleaner, destinationTablet, 30*time.Second) + wrangler.RecordStartSlaveAction(sdw.cleaner, destinationTablet) action, err := wrangler.FindChangeSlaveTypeActionByTarget(sdw.cleaner, sdw.destinationAlias) if err != nil { return fmt.Errorf("cannot find ChangeSlaveType action for %v: %v", sdw.destinationAlias, err) @@ -339,10 +368,12 @@ func (sdw *SplitDiffWorker) synchronizeReplication() error { // 5 - restart filtered replication on destination master sdw.wr.Logger().Infof("Restarting filtered replication on master %v", sdw.shardInfo.MasterAlias) - err = sdw.wr.TabletManagerClient().StartBlp(context.TODO(), masterInfo, 30*time.Second) + ctx, cancel = context.WithTimeout(sdw.ctx, 60*time.Second) + err = sdw.wr.TabletManagerClient().StartBlp(ctx, masterInfo) if err := sdw.cleaner.RemoveActionByName(wrangler.StartBlpActionName, sdw.shardInfo.MasterAlias.String()); err != nil { sdw.wr.Logger().Warningf("Cannot find cleaning action %v/%v: %v", wrangler.StartBlpActionName, sdw.shardInfo.MasterAlias.String(), err) } + cancel() if err != nil { return fmt.Errorf("StartBlp failed for %v: %v", sdw.shardInfo.MasterAlias, err) } @@ -365,7 +396,9 @@ func (sdw *SplitDiffWorker) diff() error { wg.Add(1) go func() { var err error - sdw.destinationSchemaDefinition, err = sdw.wr.GetSchema(sdw.destinationAlias, nil, nil, false) + ctx, cancel := context.WithTimeout(sdw.ctx, 60*time.Second) + sdw.destinationSchemaDefinition, err = sdw.wr.GetSchema(ctx, sdw.destinationAlias, nil, nil, false) + cancel() rec.RecordError(err) sdw.wr.Logger().Infof("Got schema from destination %v", sdw.destinationAlias) wg.Done() @@ -374,7 +407,9 @@ func (sdw *SplitDiffWorker) diff() error { wg.Add(1) go func(i int, sourceAlias topo.TabletAlias) { var err error - sdw.sourceSchemaDefinitions[i], err = sdw.wr.GetSchema(sourceAlias, nil, nil, false) + ctx, cancel := context.WithTimeout(sdw.ctx, 60*time.Second) + sdw.sourceSchemaDefinitions[i], err = sdw.wr.GetSchema(ctx, sourceAlias, nil, nil, false) + cancel() rec.RecordError(err) sdw.wr.Logger().Infof("Got schema from source[%v] %v", i, sourceAlias) wg.Done() @@ -420,14 +455,14 @@ func (sdw *SplitDiffWorker) diff() error { sdw.wr.Logger().Errorf("Source shard doesn't overlap with destination????: %v", err) return } - sourceQueryResultReader, err := TableScanByKeyRange(sdw.wr.Logger(), sdw.wr.TopoServer(), sdw.sourceAliases[0], tableDefinition, overlap, sdw.keyspaceInfo.ShardingColumnType) + sourceQueryResultReader, err := TableScanByKeyRange(sdw.ctx, sdw.wr.Logger(), sdw.wr.TopoServer(), sdw.sourceAliases[0], tableDefinition, overlap, sdw.keyspaceInfo.ShardingColumnType) if err != nil { sdw.wr.Logger().Errorf("TableScanByKeyRange(source) failed: %v", err) return } defer sourceQueryResultReader.Close() - destinationQueryResultReader, err := TableScanByKeyRange(sdw.wr.Logger(), sdw.wr.TopoServer(), sdw.destinationAlias, tableDefinition, key.KeyRange{}, sdw.keyspaceInfo.ShardingColumnType) + destinationQueryResultReader, err := TableScanByKeyRange(sdw.ctx, sdw.wr.Logger(), sdw.wr.TopoServer(), sdw.destinationAlias, tableDefinition, key.KeyRange{}, sdw.keyspaceInfo.ShardingColumnType) if err != nil { sdw.wr.Logger().Errorf("TableScanByKeyRange(destination) failed: %v", err) return diff --git a/go/vt/worker/sqldiffer.go b/go/vt/worker/sqldiffer.go index fa96b2876ff..e6a03b2cb29 100644 --- a/go/vt/worker/sqldiffer.go +++ b/go/vt/worker/sqldiffer.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/wrangler" @@ -24,13 +24,13 @@ type sqlDiffWorkerState string const ( // all the states for the worker - SQLDiffNotSarted sqlDiffWorkerState = "not started" - SQLDiffDone sqlDiffWorkerState = "done" - SQLDiffError sqlDiffWorkerState = "error" - SQLDiffFindTargets sqlDiffWorkerState = "finding target instances" - SQLDiffSynchronizeReplication sqlDiffWorkerState = "synchronizing replication" - SQLDiffRunning sqlDiffWorkerState = "running the diff" - SQLDiffCleanUp sqlDiffWorkerState = "cleaning up" + sqlDiffNotSarted sqlDiffWorkerState = "not started" + sqlDiffDone sqlDiffWorkerState = "done" + sqlDiffError sqlDiffWorkerState = "error" + sqlDiffFindTargets sqlDiffWorkerState = "finding target instances" + sqlDiffSynchronizeReplication sqlDiffWorkerState = "synchronizing replication" + sqlDiffRunning sqlDiffWorkerState = "running the diff" + sqlDiffCleanUp sqlDiffWorkerState = "cleaning up" ) func (state sqlDiffWorkerState) String() string { @@ -50,10 +50,12 @@ type SourceSpec struct { // database: any row in the subset spec needs to have a conuterpart in // the superset spec. type SQLDiffWorker struct { - wr *wrangler.Wrangler - cell string - shard string - cleaner *wrangler.Cleaner + wr *wrangler.Wrangler + cell string + shard string + cleaner *wrangler.Cleaner + ctx context.Context + ctxCancel context.CancelFunc // alias in the following 2 fields is during // SQLDifferFindTargets, read-only after that. @@ -70,13 +72,17 @@ type SQLDiffWorker struct { // NewSQLDiffWorker returns a new SQLDiffWorker object. func NewSQLDiffWorker(wr *wrangler.Wrangler, cell string, superset, subset SourceSpec) Worker { + ctx, cancel := context.WithCancel(context.Background()) return &SQLDiffWorker{ - wr: wr, - cell: cell, - superset: superset, - subset: subset, - cleaner: new(wrangler.Cleaner), - state: SQLDiffNotSarted, + wr: wr, + cell: cell, + superset: superset, + subset: subset, + cleaner: new(wrangler.Cleaner), + ctx: ctx, + ctxCancel: cancel, + + state: sqlDiffNotSarted, } } @@ -90,10 +96,11 @@ func (worker *SQLDiffWorker) recordError(err error) { worker.mu.Lock() defer worker.mu.Unlock() - worker.state = SQLDiffError + worker.state = sqlDiffError worker.err = err } +// StatusAsHTML is part of the Worker interface func (worker *SQLDiffWorker) StatusAsHTML() template.HTML { worker.mu.Lock() defer worker.mu.Unlock() @@ -101,17 +108,18 @@ func (worker *SQLDiffWorker) StatusAsHTML() template.HTML { result := "Working on: " + worker.subset.Keyspace + "/" + worker.subset.Shard + "
\n" result += "State: " + worker.state.String() + "
\n" switch worker.state { - case SQLDiffError: + case sqlDiffError: result += "Error: " + worker.err.Error() + "
\n" - case SQLDiffRunning: + case sqlDiffRunning: result += "Running...
\n" - case SQLDiffDone: + case sqlDiffDone: result += "Success.
\n" } return template.HTML(result) } +// StatusAsText is part of the Worker interface func (worker *SQLDiffWorker) StatusAsText() string { worker.mu.Lock() defer worker.mu.Unlock() @@ -119,19 +127,27 @@ func (worker *SQLDiffWorker) StatusAsText() string { result := "Working on: " + worker.subset.Keyspace + "/" + worker.subset.Shard + "\n" result += "State: " + worker.state.String() + "\n" switch worker.state { - case SQLDiffError: + case sqlDiffError: result += "Error: " + worker.err.Error() + "\n" - case SQLDiffRunning: + case sqlDiffRunning: result += "Running...\n" - case SQLDiffDone: + case sqlDiffDone: result += "Success.\n" } return result } -func (worker *SQLDiffWorker) CheckInterrupted() bool { +// Cancel is part of the Worker interface +func (worker *SQLDiffWorker) Cancel() { + worker.ctxCancel() +} + +func (worker *SQLDiffWorker) checkInterrupted() bool { select { - case <-interrupted: + case <-worker.ctx.Done(): + if worker.ctx.Err() == context.DeadlineExceeded { + return false + } worker.recordError(topo.ErrInterrupted) return true default: @@ -143,7 +159,7 @@ func (worker *SQLDiffWorker) CheckInterrupted() bool { func (worker *SQLDiffWorker) Run() { err := worker.run() - worker.setState(SQLDiffCleanUp) + worker.setState(sqlDiffCleanUp) cerr := worker.cleaner.CleanUp(worker.wr) if cerr != nil { if err != nil { @@ -156,7 +172,7 @@ func (worker *SQLDiffWorker) Run() { worker.recordError(err) return } - worker.setState(SQLDiffDone) + worker.setState(sqlDiffDone) } func (worker *SQLDiffWorker) Error() error { @@ -168,15 +184,18 @@ func (worker *SQLDiffWorker) run() error { if err := worker.findTargets(); err != nil { return err } - if worker.CheckInterrupted() { + if worker.checkInterrupted() { return topo.ErrInterrupted } // second phase: synchronize replication if err := worker.synchronizeReplication(); err != nil { + if worker.checkInterrupted() { + return topo.ErrInterrupted + } return err } - if worker.CheckInterrupted() { + if worker.checkInterrupted() { return topo.ErrInterrupted } @@ -193,17 +212,17 @@ func (worker *SQLDiffWorker) run() error { // - find one rdonly in subset // - mark them all as 'checker' pointing back to us func (worker *SQLDiffWorker) findTargets() error { - worker.setState(SQLDiffFindTargets) + worker.setState(sqlDiffFindTargets) // find an appropriate endpoint in superset var err error - worker.superset.alias, err = findChecker(worker.wr, worker.cleaner, worker.cell, worker.superset.Keyspace, worker.superset.Shard) + worker.superset.alias, err = findChecker(worker.ctx, worker.wr, worker.cleaner, worker.cell, worker.superset.Keyspace, worker.superset.Shard) if err != nil { return err } // find an appropriate endpoint in subset - worker.subset.alias, err = findChecker(worker.wr, worker.cleaner, worker.cell, worker.subset.Keyspace, worker.subset.Shard) + worker.subset.alias, err = findChecker(worker.ctx, worker.wr, worker.cleaner, worker.cell, worker.subset.Keyspace, worker.subset.Shard) if err != nil { return err } @@ -217,7 +236,7 @@ func (worker *SQLDiffWorker) findTargets() error { // 3 - ask the superset slave to stop replication // Note this is not 100% correct, but good enough for now func (worker *SQLDiffWorker) synchronizeReplication() error { - worker.setState(SQLDiffSynchronizeReplication) + worker.setState(sqlDiffSynchronizeReplication) // stop replication on subset slave worker.wr.Logger().Infof("Stopping replication on subset slave %v", worker.subset.alias) @@ -225,16 +244,19 @@ func (worker *SQLDiffWorker) synchronizeReplication() error { if err != nil { return err } - if err := worker.wr.TabletManagerClient().StopSlave(context.TODO(), subsetTablet, 30*time.Second); err != nil { + ctx, cancel := context.WithTimeout(worker.ctx, 60*time.Second) + err = worker.wr.TabletManagerClient().StopSlave(ctx, subsetTablet) + cancel() + if err != nil { return fmt.Errorf("Cannot stop slave %v: %v", worker.subset.alias, err) } - if worker.CheckInterrupted() { + if worker.checkInterrupted() { return topo.ErrInterrupted } // change the cleaner actions from ChangeSlaveType(rdonly) // to StartSlave() + ChangeSlaveType(spare) - wrangler.RecordStartSlaveAction(worker.cleaner, subsetTablet, 30*time.Second) + wrangler.RecordStartSlaveAction(worker.cleaner, subsetTablet) action, err := wrangler.FindChangeSlaveTypeActionByTarget(worker.cleaner, worker.subset.alias) if err != nil { return fmt.Errorf("cannot find ChangeSlaveType action for %v: %v", worker.subset.alias, err) @@ -243,7 +265,7 @@ func (worker *SQLDiffWorker) synchronizeReplication() error { // sleep for a few seconds time.Sleep(5 * time.Second) - if worker.CheckInterrupted() { + if worker.checkInterrupted() { return topo.ErrInterrupted } @@ -253,13 +275,16 @@ func (worker *SQLDiffWorker) synchronizeReplication() error { if err != nil { return err } - if err := worker.wr.TabletManagerClient().StopSlave(context.TODO(), supersetTablet, 30*time.Second); err != nil { + ctx, cancel = context.WithTimeout(worker.ctx, 60*time.Second) + err = worker.wr.TabletManagerClient().StopSlave(ctx, supersetTablet) + cancel() + if err != nil { return fmt.Errorf("Cannot stop slave %v: %v", worker.superset.alias, err) } // change the cleaner actions from ChangeSlaveType(rdonly) // to StartSlave() + ChangeSlaveType(spare) - wrangler.RecordStartSlaveAction(worker.cleaner, supersetTablet, 30*time.Second) + wrangler.RecordStartSlaveAction(worker.cleaner, supersetTablet) action, err = wrangler.FindChangeSlaveTypeActionByTarget(worker.cleaner, worker.superset.alias) if err != nil { return fmt.Errorf("cannot find ChangeSlaveType action for %v: %v", worker.superset.alias, err) @@ -275,19 +300,19 @@ func (worker *SQLDiffWorker) synchronizeReplication() error { // - for each table in destination, run a diff pipeline. func (worker *SQLDiffWorker) diff() error { - worker.setState(SQLDiffRunning) + worker.setState(sqlDiffRunning) // run the diff worker.wr.Logger().Infof("Running the diffs...") - supersetQueryResultReader, err := NewQueryResultReaderForTablet(worker.wr.TopoServer(), worker.superset.alias, worker.superset.SQL) + supersetQueryResultReader, err := NewQueryResultReaderForTablet(worker.ctx, worker.wr.TopoServer(), worker.superset.alias, worker.superset.SQL) if err != nil { worker.wr.Logger().Errorf("NewQueryResultReaderForTablet(superset) failed: %v", err) return err } defer supersetQueryResultReader.Close() - subsetQueryResultReader, err := NewQueryResultReaderForTablet(worker.wr.TopoServer(), worker.subset.alias, worker.subset.SQL) + subsetQueryResultReader, err := NewQueryResultReaderForTablet(worker.ctx, worker.wr.TopoServer(), worker.subset.alias, worker.subset.SQL) if err != nil { worker.wr.Logger().Errorf("NewQueryResultReaderForTablet(subset) failed: %v", err) return err diff --git a/go/vt/worker/topo_utils.go b/go/vt/worker/topo_utils.go index bf6d730e0aa..b9d1cdf89de 100644 --- a/go/vt/worker/topo_utils.go +++ b/go/vt/worker/topo_utils.go @@ -5,38 +5,53 @@ package worker import ( + "flag" "fmt" + "math/rand" "time" "github.com/youtube/vitess/go/vt/servenv" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/wrangler" + "golang.org/x/net/context" ) -// findHealthyEndPoint returns the first healthy endpoint. -func findHealthyEndPoint(wr *wrangler.Wrangler, cell, keyspace, shard string) (topo.TabletAlias, error) { +var ( + minHealthyEndPoints = flag.Int("min_healthy_rdonly_endpoints", 2, "minimum number of healthy rdonly endpoints required for checker") +) + +// findHealthyRdonlyEndPoint returns a random healthy endpoint. +// Since we don't want to use them all, we require at least +// minHealthyEndPoints servers to be healthy. +func findHealthyRdonlyEndPoint(wr *wrangler.Wrangler, cell, keyspace, shard string) (topo.TabletAlias, error) { endPoints, err := wr.TopoServer().GetEndPoints(cell, keyspace, shard, topo.TYPE_RDONLY) if err != nil { return topo.TabletAlias{}, fmt.Errorf("GetEndPoints(%v,%v,%v,rdonly) failed: %v", cell, keyspace, shard, err) } + healthyEndpoints := make([]topo.EndPoint, 0, len(endPoints.Entries)) for _, entry := range endPoints.Entries { if len(entry.Health) == 0 { - // first healthy server is what we want - return topo.TabletAlias{ - Cell: cell, - Uid: entry.Uid, - }, nil + healthyEndpoints = append(healthyEndpoints, entry) } } - return topo.TabletAlias{}, fmt.Errorf("No endpoint to chose from in (%v,%v/%v)", cell, keyspace, shard) + if len(healthyEndpoints) < *minHealthyEndPoints { + return topo.TabletAlias{}, fmt.Errorf("Not enough endpoints to chose from in (%v,%v/%v), have %v healthy ones, need at least %v", cell, keyspace, shard, len(healthyEndpoints), *minHealthyEndPoints) + } + + // random server in the list is what we want + index := rand.Intn(len(healthyEndpoints)) + return topo.TabletAlias{ + Cell: cell, + Uid: healthyEndpoints[index].Uid, + }, nil } // findChecker: // - find a rdonly instance in the keyspace / shard // - mark it as checker // - tag it with our worker process -func findChecker(wr *wrangler.Wrangler, cleaner *wrangler.Cleaner, cell, keyspace, shard string) (topo.TabletAlias, error) { - tabletAlias, err := findHealthyEndPoint(wr, cell, keyspace, shard) +func findChecker(ctx context.Context, wr *wrangler.Wrangler, cleaner *wrangler.Cleaner, cell, keyspace, shard string) (topo.TabletAlias, error) { + tabletAlias, err := findHealthyRdonlyEndPoint(wr, cell, keyspace, shard) if err != nil { return topo.TabletAlias{}, err } @@ -60,8 +75,10 @@ func findChecker(wr *wrangler.Wrangler, cleaner *wrangler.Cleaner, cell, keyspac defer wrangler.RecordTabletTagAction(cleaner, tabletAlias, "worker", "") wr.Logger().Infof("Changing tablet %v to 'checker'", tabletAlias) - wr.ResetActionTimeout(30 * time.Second) - if err := wr.ChangeType(tabletAlias, topo.TYPE_CHECKER, false /*force*/); err != nil { + ctx, cancel := context.WithTimeout(ctx, 60*time.Second) + err = wr.ChangeType(ctx, tabletAlias, topo.TYPE_CHECKER, false /*force*/) + cancel() + if err != nil { return topo.TabletAlias{}, err } @@ -71,3 +88,7 @@ func findChecker(wr *wrangler.Wrangler, cleaner *wrangler.Cleaner, cell, keyspac wrangler.RecordChangeSlaveTypeAction(cleaner, tabletAlias, topo.TYPE_RDONLY) return tabletAlias, nil } + +func init() { + rand.Seed(time.Now().UnixNano()) +} diff --git a/go/vt/worker/vertical_split_clone.go b/go/vt/worker/vertical_split_clone.go index 6dc39f2cade..39f73c44372 100644 --- a/go/vt/worker/vertical_split_clone.go +++ b/go/vt/worker/vertical_split_clone.go @@ -11,9 +11,10 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/event" + "github.com/youtube/vitess/go/sqltypes" "github.com/youtube/vitess/go/sync2" "github.com/youtube/vitess/go/vt/binlog/binlogplayer" "github.com/youtube/vitess/go/vt/mysqlctl" @@ -45,9 +46,12 @@ type VerticalSplitCloneWorker struct { tables []string strategy *mysqlctl.SplitStrategy sourceReaderCount int + destinationPackCount int minTableSizeForSplit uint64 destinationWriterCount int cleaner *wrangler.Cleaner + ctx context.Context + ctxCancel context.CancelFunc // all subsequent fields are protected by the mutex mu sync.Mutex @@ -65,20 +69,24 @@ type VerticalSplitCloneWorker struct { destinationAliases []topo.TabletAlias destinationTablets map[topo.TabletAlias]*topo.TabletInfo destinationMasterAlias topo.TabletAlias + // aliases of tablets that need to have their schema reloaded + reloadAliases []topo.TabletAlias + reloadTablets map[topo.TabletAlias]*topo.TabletInfo // populated during stateVSCCopy - tableStatus []tableStatus + tableStatus []*tableStatus startTime time.Time ev *events.VerticalSplitClone } // NewVerticalSplitCloneWorker returns a new VerticalSplitCloneWorker object. -func NewVerticalSplitCloneWorker(wr *wrangler.Wrangler, cell, destinationKeyspace, destinationShard string, tables []string, strategyStr string, sourceReaderCount int, minTableSizeForSplit uint64, destinationWriterCount int) (Worker, error) { +func NewVerticalSplitCloneWorker(wr *wrangler.Wrangler, cell, destinationKeyspace, destinationShard string, tables []string, strategyStr string, sourceReaderCount, destinationPackCount int, minTableSizeForSplit uint64, destinationWriterCount int) (Worker, error) { strategy, err := mysqlctl.NewSplitStrategy(wr.Logger(), strategyStr) if err != nil { return nil, err } + ctx, cancel := context.WithCancel(context.Background()) return &VerticalSplitCloneWorker{ wr: wr, cell: cell, @@ -87,9 +95,12 @@ func NewVerticalSplitCloneWorker(wr *wrangler.Wrangler, cell, destinationKeyspac tables: tables, strategy: strategy, sourceReaderCount: sourceReaderCount, + destinationPackCount: destinationPackCount, minTableSizeForSplit: minTableSizeForSplit, destinationWriterCount: destinationWriterCount, cleaner: &wrangler.Cleaner{}, + ctx: ctx, + ctxCancel: cancel, state: stateVSCNotSarted, ev: &events.VerticalSplitClone{ @@ -166,9 +177,17 @@ func (vscw *VerticalSplitCloneWorker) StatusAsText() string { return result } -func (vscw *VerticalSplitCloneWorker) CheckInterrupted() bool { +// Cancel is part of the Worker interface +func (vscw *VerticalSplitCloneWorker) Cancel() { + vscw.ctxCancel() +} + +func (vscw *VerticalSplitCloneWorker) checkInterrupted() bool { select { - case <-interrupted: + case <-vscw.ctx.Done(): + if vscw.ctx.Err() == context.DeadlineExceeded { + return false + } vscw.recordError(topo.ErrInterrupted) return true default: @@ -205,23 +224,31 @@ func (vscw *VerticalSplitCloneWorker) run() error { if err := vscw.init(); err != nil { return fmt.Errorf("init() failed: %v", err) } - if vscw.CheckInterrupted() { + if vscw.checkInterrupted() { return topo.ErrInterrupted } // second state: find targets if err := vscw.findTargets(); err != nil { + // A canceled context can appear to cause an application error + if vscw.checkInterrupted() { + return topo.ErrInterrupted + } return fmt.Errorf("findTargets() failed: %v", err) } - if vscw.CheckInterrupted() { + if vscw.checkInterrupted() { return topo.ErrInterrupted } // third state: copy data if err := vscw.copy(); err != nil { + // A canceled context can appear to cause an application error + if vscw.checkInterrupted() { + return topo.ErrInterrupted + } return fmt.Errorf("copy() failed: %v", err) } - if vscw.CheckInterrupted() { + if vscw.checkInterrupted() { return topo.ErrInterrupted } @@ -246,15 +273,15 @@ func (vscw *VerticalSplitCloneWorker) init() error { servingTypes := []topo.TabletType{topo.TYPE_MASTER, topo.TYPE_REPLICA, topo.TYPE_RDONLY} servedFrom := "" for _, st := range servingTypes { - if sf, ok := destinationKeyspaceInfo.ServedFromMap[st]; !ok { + sf, ok := destinationKeyspaceInfo.ServedFromMap[st] + if !ok { return fmt.Errorf("destination keyspace %v is serving type %v", vscw.destinationKeyspace, st) + } + if servedFrom == "" { + servedFrom = sf.Keyspace } else { - if servedFrom == "" { - servedFrom = sf.Keyspace - } else { - if servedFrom != sf.Keyspace { - return fmt.Errorf("destination keyspace %v is serving from multiple source keyspaces %v and %v", vscw.destinationKeyspace, servedFrom, sf.Keyspace) - } + if servedFrom != sf.Keyspace { + return fmt.Errorf("destination keyspace %v is serving from multiple source keyspaces %v and %v", vscw.destinationKeyspace, servedFrom, sf.Keyspace) } } } @@ -272,7 +299,7 @@ func (vscw *VerticalSplitCloneWorker) findTargets() error { // find an appropriate endpoint in the source shard var err error - vscw.sourceAlias, err = findChecker(vscw.wr, vscw.cleaner, vscw.cell, vscw.sourceKeyspace, "0") + vscw.sourceAlias, err = findChecker(vscw.ctx, vscw.wr, vscw.cleaner, vscw.cell, vscw.sourceKeyspace, "0") if err != nil { return fmt.Errorf("cannot find checker for %v/%v/0: %v", vscw.cell, vscw.sourceKeyspace, err) } @@ -285,35 +312,51 @@ func (vscw *VerticalSplitCloneWorker) findTargets() error { } // stop replication on it - if err := vscw.wr.TabletManagerClient().StopSlave(context.TODO(), vscw.sourceTablet, 30*time.Second); err != nil { + ctx, cancel := context.WithTimeout(vscw.ctx, 60*time.Second) + err = vscw.wr.TabletManagerClient().StopSlave(ctx, vscw.sourceTablet) + cancel() + if err != nil { return fmt.Errorf("cannot stop replication on tablet %v", vscw.sourceAlias) } - wrangler.RecordStartSlaveAction(vscw.cleaner, vscw.sourceTablet, 30*time.Second) + wrangler.RecordStartSlaveAction(vscw.cleaner, vscw.sourceTablet) action, err := wrangler.FindChangeSlaveTypeActionByTarget(vscw.cleaner, vscw.sourceAlias) if err != nil { return fmt.Errorf("cannot find ChangeSlaveType action for %v: %v", vscw.sourceAlias, err) } action.TabletType = topo.TYPE_SPARE + return vscw.findMasterTargets() +} + +// findMasterTargets looks up the master for the destination shard, and set the destinations appropriately. +// It should be used if vtworker will only want to write to masters. +func (vscw *VerticalSplitCloneWorker) findMasterTargets() error { + var err error // find all the targets in the destination keyspace / shard - vscw.destinationAliases, err = topo.FindAllTabletAliasesInShard(vscw.wr.TopoServer(), vscw.destinationKeyspace, vscw.destinationShard) + ctx, cancel := context.WithTimeout(vscw.ctx, 60*time.Second) + vscw.reloadAliases, err = topo.FindAllTabletAliasesInShard(ctx, vscw.wr.TopoServer(), vscw.destinationKeyspace, vscw.destinationShard) + cancel() if err != nil { - return fmt.Errorf("cannot find all target tablets in %v/%v: %v", vscw.destinationKeyspace, vscw.destinationShard, err) + return fmt.Errorf("cannot find all reload target tablets in %v/%v: %v", vscw.destinationKeyspace, vscw.destinationShard, err) } - vscw.wr.Logger().Infof("Found %v target aliases", len(vscw.destinationAliases)) + vscw.wr.Logger().Infof("Found %v reload target aliases", len(vscw.reloadAliases)) // get the TabletInfo for all targets - vscw.destinationTablets, err = topo.GetTabletMap(vscw.wr.TopoServer(), vscw.destinationAliases) + ctx, cancel = context.WithTimeout(vscw.ctx, 60*time.Second) + vscw.reloadTablets, err = topo.GetTabletMap(ctx, vscw.wr.TopoServer(), vscw.reloadAliases) + cancel() if err != nil { - return fmt.Errorf("cannot read all target tablets in %v/%v: %v", vscw.destinationKeyspace, vscw.destinationShard, err) + return fmt.Errorf("cannot read all reload target tablets in %v/%v: %v", vscw.destinationKeyspace, vscw.destinationShard, err) } // find and validate the master - for tabletAlias, ti := range vscw.destinationTablets { + for tabletAlias, ti := range vscw.reloadTablets { if ti.Type == topo.TYPE_MASTER { if vscw.destinationMasterAlias.IsZero() { vscw.destinationMasterAlias = tabletAlias + vscw.destinationAliases = []topo.TabletAlias{tabletAlias} + vscw.destinationTablets = map[topo.TabletAlias]*topo.TabletInfo{tabletAlias: ti} } else { return fmt.Errorf("multiple masters in destination shard: %v and %v at least", vscw.destinationMasterAlias, tabletAlias) } @@ -322,19 +365,22 @@ func (vscw *VerticalSplitCloneWorker) findTargets() error { if vscw.destinationMasterAlias.IsZero() { return fmt.Errorf("no master in destination shard") } + vscw.wr.Logger().Infof("Found target master alias %v in shard %v/%v", vscw.destinationMasterAlias, vscw.destinationKeyspace, vscw.destinationShard) return nil } // copy phase: -// - get schema on the source, filter tables -// - create tables on all destinations -// - copy the data +// - copy the data from source tablets to destination masters (wtih replication on) +// Assumes that the schema has already been created on each destination tablet +// (probably from vtctl's CopySchemaShard) func (vscw *VerticalSplitCloneWorker) copy() error { vscw.setState(stateVSCCopy) // get source schema - sourceSchemaDefinition, err := vscw.wr.GetSchema(vscw.sourceAlias, vscw.tables, nil, true) + ctx, cancel := context.WithTimeout(vscw.ctx, 60*time.Second) + sourceSchemaDefinition, err := vscw.wr.GetSchema(ctx, vscw.sourceAlias, vscw.tables, nil, true) + cancel() if err != nil { return fmt.Errorf("cannot get schema from source %v: %v", vscw.sourceAlias, err) } @@ -343,64 +389,46 @@ func (vscw *VerticalSplitCloneWorker) copy() error { } vscw.wr.Logger().Infof("Source tablet has %v tables to copy", len(sourceSchemaDefinition.TableDefinitions)) vscw.mu.Lock() - vscw.tableStatus = make([]tableStatus, len(sourceSchemaDefinition.TableDefinitions)) + vscw.tableStatus = make([]*tableStatus, len(sourceSchemaDefinition.TableDefinitions)) for i, td := range sourceSchemaDefinition.TableDefinitions { - vscw.tableStatus[i].name = td.Name - vscw.tableStatus[i].rowCount = td.RowCount + vscw.tableStatus[i] = &tableStatus{ + name: td.Name, + rowCount: td.RowCount, + } } vscw.startTime = time.Now() vscw.mu.Unlock() - // Create all the commands to create the destination schema: - // - createDbCmds will create the database and the tables - // - createViewCmds will create the views - // - alterTablesCmds will modify the tables at the end if needed - // (all need template substitution for {{.DatabaseName}}) - createDbCmds := make([]string, 0, len(sourceSchemaDefinition.TableDefinitions)+1) - createDbCmds = append(createDbCmds, sourceSchemaDefinition.DatabaseSchema) - createViewCmds := make([]string, 0, 16) - alterTablesCmds := make([]string, 0, 16) + // Count rows for i, td := range sourceSchemaDefinition.TableDefinitions { vscw.tableStatus[i].mu.Lock() if td.Type == myproto.TABLE_BASE_TABLE { - create, alter, err := mysqlctl.MakeSplitCreateTableSql(vscw.wr.Logger(), td.Schema, "{{.DatabaseName}}", td.Name, vscw.strategy) - if err != nil { - return fmt.Errorf("MakeSplitCreateTableSql(%v) returned: %v", td.Name, err) - } - createDbCmds = append(createDbCmds, create) - if alter != "" { - alterTablesCmds = append(alterTablesCmds, alter) - } - vscw.tableStatus[i].state = "before table creation" vscw.tableStatus[i].rowCount = td.RowCount } else { - createViewCmds = append(createViewCmds, td.Schema) - vscw.tableStatus[i].state = "before view creation" - vscw.tableStatus[i].rowCount = 0 + vscw.tableStatus[i].isView = true } vscw.tableStatus[i].mu.Unlock() } - // For each destination tablet (in parallel): - // - create the schema - // - setup the channels to send SQL data chunks + // In parallel, setup the channels to send SQL data chunks to for each destination tablet. // - // mu protects the abort channel for closing, and firstError + // mu protects the context for cancelation, and firstError mu := sync.Mutex{} - abort := make(chan struct{}) var firstError error processError := func(format string, args ...interface{}) { vscw.wr.Logger().Errorf(format, args...) mu.Lock() - if abort != nil { - close(abort) - abort = nil + if !vscw.checkInterrupted() { + vscw.Cancel() firstError = fmt.Errorf(format, args...) } mu.Unlock() } + // since we're writing only to masters, we need to enable bin logs so that replication happens + disableBinLogs := false + insertChannels := make([]chan string, len(vscw.destinationAliases)) destinationWaitGroup := sync.WaitGroup{} for i, tabletAlias := range vscw.destinationAliases { @@ -411,27 +439,13 @@ func (vscw *VerticalSplitCloneWorker) copy() error { // destinationWriterCount go routines reading from it. insertChannels[i] = make(chan string, vscw.destinationWriterCount*2) - destinationWaitGroup.Add(1) go func(ti *topo.TabletInfo, insertChannel chan string) { - defer destinationWaitGroup.Done() - vscw.wr.Logger().Infof("Creating tables on tablet %v", ti.Alias) - if err := runSqlCommands(vscw.wr, ti, createDbCmds, abort, true); err != nil { - processError("createDbCmds failed: %v", err) - return - } - if len(createViewCmds) > 0 { - vscw.wr.Logger().Infof("Creating views on tablet %v", ti.Alias) - if err := runSqlCommands(vscw.wr, ti, createViewCmds, abort, true); err != nil { - processError("createViewCmds failed: %v", err) - return - } - } for j := 0; j < vscw.destinationWriterCount; j++ { destinationWaitGroup.Add(1) go func() { defer destinationWaitGroup.Done() - if err := executeFetchLoop(vscw.wr, ti, insertChannel, abort, true); err != nil { + if err := executeFetchLoop(vscw.ctx, vscw.wr, ti, insertChannel, disableBinLogs); err != nil { processError("executeFetchLoop failed: %v", err) } }() @@ -445,15 +459,14 @@ func (vscw *VerticalSplitCloneWorker) copy() error { sema := sync2.NewSemaphore(vscw.sourceReaderCount, 0) for tableIndex, td := range sourceSchemaDefinition.TableDefinitions { if td.Type == myproto.TABLE_VIEW { - vscw.tableStatus[tableIndex].setState("view created") continue } - vscw.tableStatus[tableIndex].setState("before copy") - chunks, err := findChunks(vscw.wr, vscw.sourceTablet, td, vscw.minTableSizeForSplit, vscw.sourceReaderCount) + chunks, err := findChunks(vscw.ctx, vscw.wr, vscw.sourceTablet, td, vscw.minTableSizeForSplit, vscw.sourceReaderCount) if err != nil { return err } + vscw.tableStatus[tableIndex].setThreadCount(len(chunks) - 1) for chunkIndex := 0; chunkIndex < len(chunks)-1; chunkIndex++ { sourceWaitGroup.Add(1) @@ -463,20 +476,22 @@ func (vscw *VerticalSplitCloneWorker) copy() error { sema.Acquire() defer sema.Release() - vscw.tableStatus[tableIndex].setState("started the copy") + vscw.tableStatus[tableIndex].threadStarted() // build the query, and start the streaming selectSQL := buildSQLFromChunks(vscw.wr, td, chunks, chunkIndex, vscw.sourceAlias.String()) - qrr, err := NewQueryResultReaderForTablet(vscw.wr.TopoServer(), vscw.sourceAlias, selectSQL) + qrr, err := NewQueryResultReaderForTablet(vscw.ctx, vscw.wr.TopoServer(), vscw.sourceAlias, selectSQL) if err != nil { processError("NewQueryResultReaderForTablet failed: %v", err) return } + defer qrr.Close() // process the data - if err := vscw.processData(td, tableIndex, qrr, insertChannels, abort); err != nil { + if err := vscw.processData(td, tableIndex, qrr, insertChannels, vscw.destinationPackCount, vscw.ctx.Done()); err != nil { processError("QueryResultReader failed: %v", err) } + vscw.tableStatus[tableIndex].threadDone() }(td, tableIndex, chunkIndex) } } @@ -490,28 +505,12 @@ func (vscw *VerticalSplitCloneWorker) copy() error { return firstError } - // do the post-copy alters if any - if len(alterTablesCmds) > 0 { - for _, tabletAlias := range vscw.destinationAliases { - destinationWaitGroup.Add(1) - go func(ti *topo.TabletInfo) { - defer destinationWaitGroup.Done() - vscw.wr.Logger().Infof("Altering tables on tablet %v", ti.Alias) - if err := runSqlCommands(vscw.wr, ti, alterTablesCmds, abort, true); err != nil { - processError("alterTablesCmds failed on tablet %v: %v", ti.Alias, err) - } - }(vscw.destinationTablets[tabletAlias]) - } - destinationWaitGroup.Wait() - if firstError != nil { - return firstError - } - } - // then create and populate the blp_checkpoint table if vscw.strategy.PopulateBlpCheckpoint { // get the current position from the source - status, err := vscw.wr.TabletManagerClient().SlaveStatus(context.TODO(), vscw.sourceTablet, 30*time.Second) + ctx, cancel := context.WithTimeout(vscw.ctx, 60*time.Second) + status, err := vscw.wr.TabletManagerClient().SlaveStatus(ctx, vscw.sourceTablet) + cancel() if err != nil { return err } @@ -528,7 +527,7 @@ func (vscw *VerticalSplitCloneWorker) copy() error { go func(ti *topo.TabletInfo) { defer destinationWaitGroup.Done() vscw.wr.Logger().Infof("Making and populating blp_checkpoint table on tablet %v", ti.Alias) - if err := runSqlCommands(vscw.wr, ti, queries, abort, true); err != nil { + if err := runSqlCommands(vscw.ctx, vscw.wr, ti, queries, disableBinLogs); err != nil { processError("blp_checkpoint queries failed on tablet %v: %v", ti.Alias, err) } }(vscw.destinationTablets[tabletAlias]) @@ -544,7 +543,10 @@ func (vscw *VerticalSplitCloneWorker) copy() error { vscw.wr.Logger().Infof("Skipping setting SourceShard on destination shard.") } else { vscw.wr.Logger().Infof("Setting SourceShard on shard %v/%v", vscw.destinationKeyspace, vscw.destinationShard) - if err := vscw.wr.SetSourceShards(vscw.destinationKeyspace, vscw.destinationShard, []topo.TabletAlias{vscw.sourceAlias}, vscw.tables); err != nil { + ctx, cancel := context.WithTimeout(vscw.ctx, 60*time.Second) + err := vscw.wr.SetSourceShards(ctx, vscw.destinationKeyspace, vscw.destinationShard, []topo.TabletAlias{vscw.sourceAlias}, vscw.tables) + cancel() + if err != nil { return fmt.Errorf("Failed to set source shards: %v", err) } } @@ -552,15 +554,18 @@ func (vscw *VerticalSplitCloneWorker) copy() error { // And force a schema reload on all destination tablets. // The master tablet will end up starting filtered replication // at this point. - for _, tabletAlias := range vscw.destinationAliases { + for _, tabletAlias := range vscw.reloadAliases { destinationWaitGroup.Add(1) go func(ti *topo.TabletInfo) { defer destinationWaitGroup.Done() vscw.wr.Logger().Infof("Reloading schema on tablet %v", ti.Alias) - if err := vscw.wr.TabletManagerClient().ReloadSchema(context.TODO(), ti, 30*time.Second); err != nil { + ctx, cancel := context.WithTimeout(vscw.ctx, 30*time.Second) + err := vscw.wr.TabletManagerClient().ReloadSchema(ctx, ti) + cancel() + if err != nil { processError("ReloadSchema failed on tablet %v: %v", ti.Alias, err) } - }(vscw.destinationTablets[tabletAlias]) + }(vscw.reloadTablets[tabletAlias]) } destinationWaitGroup.Wait() return firstError @@ -568,19 +573,48 @@ func (vscw *VerticalSplitCloneWorker) copy() error { // processData pumps the data out of the provided QueryResultReader. // It returns any error the source encounters. -func (vscw *VerticalSplitCloneWorker) processData(td *myproto.TableDefinition, tableIndex int, qrr *QueryResultReader, insertChannels []chan string, abort chan struct{}) error { +func (vscw *VerticalSplitCloneWorker) processData(td *myproto.TableDefinition, tableIndex int, qrr *QueryResultReader, insertChannels []chan string, destinationPackCount int, abort <-chan struct{}) error { // process the data baseCmd := td.Name + "(" + strings.Join(td.Columns, ", ") + ") VALUES " + var rows [][]sqltypes.Value + packCount := 0 + for { select { case r, ok := <-qrr.Output: if !ok { - return qrr.Error() + // we are done, see if there was an error + err := qrr.Error() + if err != nil { + return err + } + + // send the remainder if any + if packCount > 0 { + cmd := baseCmd + makeValueString(qrr.Fields, rows) + for _, c := range insertChannels { + select { + case c <- cmd: + case <-abort: + return nil + } + } + } + return nil } - // send the rows to be inserted + // add the rows to our current result + rows = append(rows, r.Rows...) vscw.tableStatus[tableIndex].addCopiedRows(len(r.Rows)) - cmd := baseCmd + makeValueString(qrr.Fields, r.Rows) + + // see if we reach the destination pack count + packCount++ + if packCount < destinationPackCount { + continue + } + + // send the rows to be inserted + cmd := baseCmd + makeValueString(qrr.Fields, rows) for _, c := range insertChannels { select { case c <- cmd: @@ -588,11 +622,20 @@ func (vscw *VerticalSplitCloneWorker) processData(td *myproto.TableDefinition, t return nil } } + + // and reset our row buffer + rows = nil + packCount = 0 + + case <-abort: + return nil + } + // the abort case might be starved above, so we check again; this means that the loop + // will run at most once before the abort case is triggered. + select { case <-abort: - // FIXME(alainjobart): note this select case - // could be starved here, and we might miss - // the abort in some corner cases. return nil + default: } } } diff --git a/go/vt/worker/vertical_split_diff.go b/go/vt/worker/vertical_split_diff.go index 57bd370d128..72428aec77f 100644 --- a/go/vt/worker/vertical_split_diff.go +++ b/go/vt/worker/vertical_split_diff.go @@ -11,7 +11,7 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" "github.com/youtube/vitess/go/sync2" blproto "github.com/youtube/vitess/go/vt/binlog/proto" @@ -37,11 +37,13 @@ const ( // VerticalSplitDiffWorker executes a diff between a destination shard and its // source shards in a shard split case. type VerticalSplitDiffWorker struct { - wr *wrangler.Wrangler - cell string - keyspace string - shard string - cleaner *wrangler.Cleaner + wr *wrangler.Wrangler + cell string + keyspace string + shard string + cleaner *wrangler.Cleaner + ctx context.Context + ctxCancel context.CancelFunc // all subsequent fields are protected by the mutex mu sync.Mutex @@ -63,14 +65,17 @@ type VerticalSplitDiffWorker struct { destinationSchemaDefinition *myproto.SchemaDefinition } -// NewVerticalSplitDiff returns a new VerticalSplitDiffWorker object. +// NewVerticalSplitDiffWorker returns a new VerticalSplitDiffWorker object. func NewVerticalSplitDiffWorker(wr *wrangler.Wrangler, cell, keyspace, shard string) Worker { + ctx, cancel := context.WithCancel(context.Background()) return &VerticalSplitDiffWorker{ - wr: wr, - cell: cell, - keyspace: keyspace, - shard: shard, - cleaner: &wrangler.Cleaner{}, + wr: wr, + cell: cell, + keyspace: keyspace, + shard: shard, + cleaner: &wrangler.Cleaner{}, + ctx: ctx, + ctxCancel: cancel, state: stateVSDNotSarted, } @@ -89,6 +94,7 @@ func (vsdw *VerticalSplitDiffWorker) recordError(err error) { vsdw.mu.Unlock() } +// StatusAsHTML is part of the Worker interface. func (vsdw *VerticalSplitDiffWorker) StatusAsHTML() template.HTML { vsdw.mu.Lock() defer vsdw.mu.Unlock() @@ -106,6 +112,7 @@ func (vsdw *VerticalSplitDiffWorker) StatusAsHTML() template.HTML { return template.HTML(result) } +// StatusAsText is part of the Worker interface. func (vsdw *VerticalSplitDiffWorker) StatusAsText() string { vsdw.mu.Lock() defer vsdw.mu.Unlock() @@ -122,9 +129,17 @@ func (vsdw *VerticalSplitDiffWorker) StatusAsText() string { return result } -func (vsdw *VerticalSplitDiffWorker) CheckInterrupted() bool { +// Cancel is part of the Worker interface +func (vsdw *VerticalSplitDiffWorker) Cancel() { + vsdw.ctxCancel() +} + +func (vsdw *VerticalSplitDiffWorker) checkInterrupted() bool { select { - case <-interrupted: + case <-vsdw.ctx.Done(): + if vsdw.ctx.Err() == context.DeadlineExceeded { + return false + } vsdw.recordError(topo.ErrInterrupted) return true default: @@ -161,7 +176,7 @@ func (vsdw *VerticalSplitDiffWorker) run() error { if err := vsdw.init(); err != nil { return fmt.Errorf("init() failed: %v", err) } - if vsdw.CheckInterrupted() { + if vsdw.checkInterrupted() { return topo.ErrInterrupted } @@ -169,20 +184,26 @@ func (vsdw *VerticalSplitDiffWorker) run() error { if err := vsdw.findTargets(); err != nil { return fmt.Errorf("findTargets() failed: %v", err) } - if vsdw.CheckInterrupted() { + if vsdw.checkInterrupted() { return topo.ErrInterrupted } // third phase: synchronize replication if err := vsdw.synchronizeReplication(); err != nil { + if vsdw.checkInterrupted() { + return topo.ErrInterrupted + } return fmt.Errorf("synchronizeReplication() failed: %v", err) } - if vsdw.CheckInterrupted() { + if vsdw.checkInterrupted() { return topo.ErrInterrupted } // fourth phase: diff if err := vsdw.diff(); err != nil { + if vsdw.checkInterrupted() { + return topo.ErrInterrupted + } return fmt.Errorf("diff() failed: %v", err) } @@ -232,13 +253,13 @@ func (vsdw *VerticalSplitDiffWorker) findTargets() error { // find an appropriate endpoint in destination shard var err error - vsdw.destinationAlias, err = findChecker(vsdw.wr, vsdw.cleaner, vsdw.cell, vsdw.keyspace, vsdw.shard) + vsdw.destinationAlias, err = findChecker(vsdw.ctx, vsdw.wr, vsdw.cleaner, vsdw.cell, vsdw.keyspace, vsdw.shard) if err != nil { return fmt.Errorf("cannot find checker for %v/%v/%v: %v", vsdw.cell, vsdw.keyspace, vsdw.shard, err) } // find an appropriate endpoint in the source shard - vsdw.sourceAlias, err = findChecker(vsdw.wr, vsdw.cleaner, vsdw.cell, vsdw.shardInfo.SourceShards[0].Keyspace, vsdw.shardInfo.SourceShards[0].Shard) + vsdw.sourceAlias, err = findChecker(vsdw.ctx, vsdw.wr, vsdw.cleaner, vsdw.cell, vsdw.shardInfo.SourceShards[0].Keyspace, vsdw.shardInfo.SourceShards[0].Shard) if err != nil { return fmt.Errorf("cannot find checker for %v/%v/%v: %v", vsdw.cell, vsdw.shardInfo.SourceShards[0].Keyspace, vsdw.shardInfo.SourceShards[0].Shard, err) } @@ -274,11 +295,13 @@ func (vsdw *VerticalSplitDiffWorker) synchronizeReplication() error { // 1 - stop the master binlog replication, get its current position vsdw.wr.Logger().Infof("Stopping master binlog replication on %v", vsdw.shardInfo.MasterAlias) - blpPositionList, err := vsdw.wr.TabletManagerClient().StopBlp(context.TODO(), masterInfo, 30*time.Second) + ctx, cancel := context.WithTimeout(vsdw.ctx, 60*time.Second) + blpPositionList, err := vsdw.wr.TabletManagerClient().StopBlp(ctx, masterInfo) + cancel() if err != nil { return fmt.Errorf("StopBlp on master %v failed: %v", vsdw.shardInfo.MasterAlias, err) } - wrangler.RecordStartBlpAction(vsdw.cleaner, masterInfo, 30*time.Second) + wrangler.RecordStartBlpAction(vsdw.cleaner, masterInfo) // 2 - stop the source 'checker' at a binlog position // higher than the destination master @@ -298,7 +321,9 @@ func (vsdw *VerticalSplitDiffWorker) synchronizeReplication() error { if err != nil { return err } - stoppedAt, err := vsdw.wr.TabletManagerClient().StopSlaveMinimum(context.TODO(), sourceTablet, pos.Position, 30*time.Second) + ctx, cancel = context.WithTimeout(vsdw.ctx, 60*time.Second) + stoppedAt, err := vsdw.wr.TabletManagerClient().StopSlaveMinimum(ctx, sourceTablet, pos.Position, 30*time.Second) + cancel() if err != nil { return fmt.Errorf("cannot stop slave %v at right binlog position %v: %v", vsdw.sourceAlias, pos.Position, err) } @@ -307,7 +332,7 @@ func (vsdw *VerticalSplitDiffWorker) synchronizeReplication() error { // change the cleaner actions from ChangeSlaveType(rdonly) // to StartSlave() + ChangeSlaveType(spare) - wrangler.RecordStartSlaveAction(vsdw.cleaner, sourceTablet, 30*time.Second) + wrangler.RecordStartSlaveAction(vsdw.cleaner, sourceTablet) action, err := wrangler.FindChangeSlaveTypeActionByTarget(vsdw.cleaner, vsdw.sourceAlias) if err != nil { return fmt.Errorf("cannot find ChangeSlaveType action for %v: %v", vsdw.sourceAlias, err) @@ -317,7 +342,9 @@ func (vsdw *VerticalSplitDiffWorker) synchronizeReplication() error { // 3 - ask the master of the destination shard to resume filtered // replication up to the new list of positions vsdw.wr.Logger().Infof("Restarting master %v until it catches up to %v", vsdw.shardInfo.MasterAlias, stopPositionList) - masterPos, err := vsdw.wr.TabletManagerClient().RunBlpUntil(context.TODO(), masterInfo, &stopPositionList, 30*time.Second) + ctx, cancel = context.WithTimeout(vsdw.ctx, 60*time.Second) + masterPos, err := vsdw.wr.TabletManagerClient().RunBlpUntil(ctx, masterInfo, &stopPositionList, 30*time.Second) + cancel() if err != nil { return fmt.Errorf("RunBlpUntil on %v until %v failed: %v", vsdw.shardInfo.MasterAlias, stopPositionList, err) } @@ -329,11 +356,13 @@ func (vsdw *VerticalSplitDiffWorker) synchronizeReplication() error { if err != nil { return err } - _, err = vsdw.wr.TabletManagerClient().StopSlaveMinimum(context.TODO(), destinationTablet, masterPos, 30*time.Second) + ctx, cancel = context.WithTimeout(vsdw.ctx, 60*time.Second) + _, err = vsdw.wr.TabletManagerClient().StopSlaveMinimum(ctx, destinationTablet, masterPos, 30*time.Second) + cancel() if err != nil { return fmt.Errorf("StopSlaveMinimum on %v at %v failed: %v", vsdw.destinationAlias, masterPos, err) } - wrangler.RecordStartSlaveAction(vsdw.cleaner, destinationTablet, 30*time.Second) + wrangler.RecordStartSlaveAction(vsdw.cleaner, destinationTablet) action, err = wrangler.FindChangeSlaveTypeActionByTarget(vsdw.cleaner, vsdw.destinationAlias) if err != nil { return fmt.Errorf("cannot find ChangeSlaveType action for %v: %v", vsdw.destinationAlias, err) @@ -342,10 +371,12 @@ func (vsdw *VerticalSplitDiffWorker) synchronizeReplication() error { // 5 - restart filtered replication on destination master vsdw.wr.Logger().Infof("Restarting filtered replication on master %v", vsdw.shardInfo.MasterAlias) - err = vsdw.wr.TabletManagerClient().StartBlp(context.TODO(), masterInfo, 30*time.Second) + ctx, cancel = context.WithTimeout(vsdw.ctx, 60*time.Second) + err = vsdw.wr.TabletManagerClient().StartBlp(ctx, masterInfo) if err := vsdw.cleaner.RemoveActionByName(wrangler.StartBlpActionName, vsdw.shardInfo.MasterAlias.String()); err != nil { vsdw.wr.Logger().Warningf("Cannot find cleaning action %v/%v: %v", wrangler.StartBlpActionName, vsdw.shardInfo.MasterAlias.String(), err) } + cancel() if err != nil { return fmt.Errorf("StartBlp on %v failed: %v", vsdw.shardInfo.MasterAlias, err) } @@ -367,7 +398,9 @@ func (vsdw *VerticalSplitDiffWorker) diff() error { wg.Add(1) go func() { var err error - vsdw.destinationSchemaDefinition, err = vsdw.wr.GetSchema(vsdw.destinationAlias, nil, nil, false) + ctx, cancel := context.WithTimeout(vsdw.ctx, 60*time.Second) + vsdw.destinationSchemaDefinition, err = vsdw.wr.GetSchema(ctx, vsdw.destinationAlias, nil, nil, false) + cancel() rec.RecordError(err) vsdw.wr.Logger().Infof("Got schema from destination %v", vsdw.destinationAlias) wg.Done() @@ -375,7 +408,9 @@ func (vsdw *VerticalSplitDiffWorker) diff() error { wg.Add(1) go func() { var err error - vsdw.sourceSchemaDefinition, err = vsdw.wr.GetSchema(vsdw.sourceAlias, nil, nil, false) + ctx, cancel := context.WithTimeout(vsdw.ctx, 60*time.Second) + vsdw.sourceSchemaDefinition, err = vsdw.wr.GetSchema(ctx, vsdw.sourceAlias, nil, nil, false) + cancel() rec.RecordError(err) vsdw.wr.Logger().Infof("Got schema from source %v", vsdw.sourceAlias) wg.Done() @@ -434,14 +469,14 @@ func (vsdw *VerticalSplitDiffWorker) diff() error { defer sem.Release() vsdw.wr.Logger().Infof("Starting the diff on table %v", tableDefinition.Name) - sourceQueryResultReader, err := TableScan(vsdw.wr.Logger(), vsdw.wr.TopoServer(), vsdw.sourceAlias, tableDefinition) + sourceQueryResultReader, err := TableScan(vsdw.ctx, vsdw.wr.Logger(), vsdw.wr.TopoServer(), vsdw.sourceAlias, tableDefinition) if err != nil { vsdw.wr.Logger().Errorf("TableScan(source) failed: %v", err) return } defer sourceQueryResultReader.Close() - destinationQueryResultReader, err := TableScan(vsdw.wr.Logger(), vsdw.wr.TopoServer(), vsdw.destinationAlias, tableDefinition) + destinationQueryResultReader, err := TableScan(vsdw.ctx, vsdw.wr.Logger(), vsdw.wr.TopoServer(), vsdw.destinationAlias, tableDefinition) if err != nil { vsdw.wr.Logger().Errorf("TableScan(destination) failed: %v", err) return diff --git a/go/vt/worker/worker.go b/go/vt/worker/worker.go index a1acb760a01..16f416d4e34 100644 --- a/go/vt/worker/worker.go +++ b/go/vt/worker/worker.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. /* -'worker' package contains the framework, utility methods and core +Package worker contains the framework, utility methods and core functions for long running actions. 'vtworker' binary will use these. */ package worker @@ -22,18 +22,14 @@ type Worker interface { // Run is the main entry point for the worker. It will be called // in a go routine. - // When the SignalInterrupt() is called, Run should exit as soon as - // possible. + // When Cancel() is called, Run should exit as soon as possible. Run() + // Cancel should attempt to force the Worker to exit as soon as possible. + // Note that cleanup actions may still run after cancellation. + Cancel() + // Error returns the error status of the job, if any. // It will only be called after Run() has completed. Error() error } - -// signal handling -var interrupted = make(chan struct{}) - -func SignalInterrupt() { - close(interrupted) -} diff --git a/go/vt/wrangler/cleaner.go b/go/vt/wrangler/cleaner.go index 2170756ce26..32ee43579c1 100644 --- a/go/vt/wrangler/cleaner.go +++ b/go/vt/wrangler/cleaner.go @@ -9,18 +9,16 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" - - log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // Cleaner remembers a list of cleanup steps to perform. Just record // action cleanup steps, and execute them at the end in reverse // order, with various guarantees. type Cleaner struct { - // folowing members protected by lock + // following members protected by lock mu sync.Mutex actions []cleanerActionReference } @@ -34,7 +32,7 @@ type cleanerActionReference struct { // CleanerAction is the interface that clean-up actions need to implement type CleanerAction interface { - CleanUp(wr *Wrangler) error + CleanUp(context.Context, *Wrangler) error } // Record will add a cleaning action to the list @@ -56,9 +54,12 @@ type cleanUpHelper struct { // If an action on a target fails, it will not run the next action on // the same target. // We return the aggregate errors for all cleanups. +// CleanUp uses its own context, with a timeout of 5 minutes, so that clean up action will run even if the original context times out. // TODO(alainjobart) Actions should run concurrently on a per target // basis. They are then serialized on each target. func (cleaner *Cleaner) CleanUp(wr *Wrangler) error { + // we use a background context so we're not dependent on the original context timeout + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) actionMap := make(map[string]*cleanUpHelper) rec := concurrency.AllErrorRecorder{} cleaner.mu.Lock() @@ -72,19 +73,20 @@ func (cleaner *Cleaner) CleanUp(wr *Wrangler) error { actionMap[actionReference.target] = helper } if helper.err != nil { - log.Warningf("previous action failed on target %v, no running %v", actionReference.target, actionReference.name) + wr.Logger().Warningf("previous action failed on target %v, no running %v", actionReference.target, actionReference.name) continue } - err := actionReference.action.CleanUp(wr) + err := actionReference.action.CleanUp(ctx, wr) if err != nil { helper.err = err rec.RecordError(err) - log.Errorf("action %v failed on %v: %v", actionReference.name, actionReference.target, err) + wr.Logger().Errorf("action %v failed on %v: %v", actionReference.name, actionReference.target, err) } else { - log.Infof("action %v successfull on %v", actionReference.name, actionReference.target) + wr.Logger().Infof("action %v successful on %v", actionReference.name, actionReference.target) } } cleaner.mu.Unlock() + cancel() return rec.Error() } @@ -131,6 +133,8 @@ type ChangeSlaveTypeAction struct { TabletType topo.TabletType } +// ChangeSlaveTypeActionName is the name of the action to change a slave type +// (can be used to find such an action by name) const ChangeSlaveTypeActionName = "ChangeSlaveTypeAction" // RecordChangeSlaveTypeAction records a new ChangeSlaveTypeAction @@ -156,9 +160,8 @@ func FindChangeSlaveTypeActionByTarget(cleaner *Cleaner, tabletAlias topo.Tablet } // CleanUp is part of CleanerAction interface. -func (csta ChangeSlaveTypeAction) CleanUp(wr *Wrangler) error { - wr.ResetActionTimeout(30 * time.Second) - return wr.ChangeType(csta.TabletAlias, csta.TabletType, false) +func (csta ChangeSlaveTypeAction) CleanUp(ctx context.Context, wr *Wrangler) error { + return wr.ChangeType(ctx, csta.TabletAlias, csta.TabletType, false) } // @@ -173,6 +176,7 @@ type TabletTagAction struct { Value string } +// TabletTagActionName is the name of the Tag action const TabletTagActionName = "TabletTagAction" // RecordTabletTagAction records a new TabletTagAction @@ -186,7 +190,7 @@ func RecordTabletTagAction(cleaner *Cleaner, tabletAlias topo.TabletAlias, name, } // CleanUp is part of CleanerAction interface. -func (tta TabletTagAction) CleanUp(wr *Wrangler) error { +func (tta TabletTagAction) CleanUp(ctx context.Context, wr *Wrangler) error { return wr.TopoServer().UpdateTabletFields(tta.TabletAlias, func(tablet *topo.Tablet) error { if tablet.Tags == nil { tablet.Tags = make(map[string]string) @@ -207,23 +211,22 @@ func (tta TabletTagAction) CleanUp(wr *Wrangler) error { // StartSlaveAction will restart binlog replication on a server type StartSlaveAction struct { TabletInfo *topo.TabletInfo - WaitTime time.Duration } +// StartSlaveActionName is the name of the slave start action const StartSlaveActionName = "StartSlaveAction" // RecordStartSlaveAction records a new StartSlaveAction // into the specified Cleaner -func RecordStartSlaveAction(cleaner *Cleaner, tabletInfo *topo.TabletInfo, waitTime time.Duration) { +func RecordStartSlaveAction(cleaner *Cleaner, tabletInfo *topo.TabletInfo) { cleaner.Record(StartSlaveActionName, tabletInfo.Alias.String(), &StartSlaveAction{ TabletInfo: tabletInfo, - WaitTime: waitTime, }) } // CleanUp is part of CleanerAction interface. -func (sba StartSlaveAction) CleanUp(wr *Wrangler) error { - return wr.TabletManagerClient().StartSlave(context.TODO(), sba.TabletInfo, sba.WaitTime) +func (sba StartSlaveAction) CleanUp(ctx context.Context, wr *Wrangler) error { + return wr.TabletManagerClient().StartSlave(ctx, sba.TabletInfo) } // @@ -233,21 +236,20 @@ func (sba StartSlaveAction) CleanUp(wr *Wrangler) error { // StartBlpAction will restart binlog replication on a server type StartBlpAction struct { TabletInfo *topo.TabletInfo - WaitTime time.Duration } +// StartBlpActionName is the name of the action to start binlog player const StartBlpActionName = "StartBlpAction" // RecordStartBlpAction records a new StartBlpAction // into the specified Cleaner -func RecordStartBlpAction(cleaner *Cleaner, tabletInfo *topo.TabletInfo, waitTime time.Duration) { +func RecordStartBlpAction(cleaner *Cleaner, tabletInfo *topo.TabletInfo) { cleaner.Record(StartBlpActionName, tabletInfo.Alias.String(), &StartBlpAction{ TabletInfo: tabletInfo, - WaitTime: waitTime, }) } // CleanUp is part of CleanerAction interface. -func (sba StartBlpAction) CleanUp(wr *Wrangler) error { - return wr.TabletManagerClient().StartBlp(context.TODO(), sba.TabletInfo, sba.WaitTime) +func (sba StartBlpAction) CleanUp(ctx context.Context, wr *Wrangler) error { + return wr.TabletManagerClient().StartBlp(ctx, sba.TabletInfo) } diff --git a/go/vt/wrangler/clone.go b/go/vt/wrangler/clone.go index fec64b55b90..0a8dfdd1d3c 100644 --- a/go/vt/wrangler/clone.go +++ b/go/vt/wrangler/clone.go @@ -8,11 +8,10 @@ import ( "fmt" "sync" - "code.google.com/p/go.net/context" - "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // Snapshot takes a tablet snapshot. @@ -29,7 +28,7 @@ import ( // // If error is nil, returns the SnapshotReply from the remote host, // and the original type the server was before the snapshot. -func (wr *Wrangler) Snapshot(tabletAlias topo.TabletAlias, forceMasterSnapshot bool, snapshotConcurrency int, serverMode bool) (*actionnode.SnapshotReply, topo.TabletType, error) { +func (wr *Wrangler) Snapshot(ctx context.Context, tabletAlias topo.TabletAlias, forceMasterSnapshot bool, snapshotConcurrency int, serverMode bool) (*actionnode.SnapshotReply, topo.TabletType, error) { // read the tablet to be able to RPC to it, and also to get its // original type ti, err := wr.ts.GetTablet(tabletAlias) @@ -44,7 +43,7 @@ func (wr *Wrangler) Snapshot(tabletAlias topo.TabletAlias, forceMasterSnapshot b ServerMode: serverMode, ForceMasterSnapshot: forceMasterSnapshot, } - logStream, errFunc, err := wr.tmc.Snapshot(context.TODO(), ti, args, wr.ActionTimeout()) + logStream, errFunc, err := wr.tmc.Snapshot(ctx, ti, args) if err != nil { return nil, "", err } @@ -57,7 +56,7 @@ func (wr *Wrangler) Snapshot(tabletAlias topo.TabletAlias, forceMasterSnapshot b // SnapshotSourceEnd will change the tablet back to its original type // once it's done serving backups. -func (wr *Wrangler) SnapshotSourceEnd(tabletAlias topo.TabletAlias, slaveStartRequired, readWrite bool, originalType topo.TabletType) (err error) { +func (wr *Wrangler) SnapshotSourceEnd(ctx context.Context, tabletAlias topo.TabletAlias, slaveStartRequired, readWrite bool, originalType topo.TabletType) (err error) { var ti *topo.TabletInfo ti, err = wr.ts.GetTablet(tabletAlias) if err != nil { @@ -69,12 +68,12 @@ func (wr *Wrangler) SnapshotSourceEnd(tabletAlias topo.TabletAlias, slaveStartRe ReadOnly: !readWrite, OriginalType: originalType, } - return wr.tmc.SnapshotSourceEnd(context.TODO(), ti, args, wr.ActionTimeout()) + return wr.tmc.SnapshotSourceEnd(ctx, ti, args) } // ReserveForRestore will make sure a tablet is ready to be used as a restore // target. -func (wr *Wrangler) ReserveForRestore(srcTabletAlias, dstTabletAlias topo.TabletAlias) (err error) { +func (wr *Wrangler) ReserveForRestore(ctx context.Context, srcTabletAlias, dstTabletAlias topo.TabletAlias) (err error) { // read our current tablet, verify its state before sending it // to the tablet itself tablet, err := wr.ts.GetTablet(dstTabletAlias) @@ -88,12 +87,12 @@ func (wr *Wrangler) ReserveForRestore(srcTabletAlias, dstTabletAlias topo.Tablet args := &actionnode.ReserveForRestoreArgs{ SrcTabletAlias: srcTabletAlias, } - return wr.tmc.ReserveForRestore(context.TODO(), tablet, args, wr.ActionTimeout()) + return wr.tmc.ReserveForRestore(ctx, tablet, args) } // UnreserveForRestore switches the tablet back to its original state, // the restore won't happen. -func (wr *Wrangler) UnreserveForRestore(dstTabletAlias topo.TabletAlias) (err error) { +func (wr *Wrangler) UnreserveForRestore(ctx context.Context, dstTabletAlias topo.TabletAlias) (err error) { tablet, err := wr.ts.GetTablet(dstTabletAlias) if err != nil { return err @@ -103,11 +102,11 @@ func (wr *Wrangler) UnreserveForRestore(dstTabletAlias topo.TabletAlias) (err er return err } - return wr.ChangeType(tablet.Alias, topo.TYPE_IDLE, false) + return wr.ChangeType(ctx, tablet.Alias, topo.TYPE_IDLE, false) } // Restore actually performs the restore action on a tablet. -func (wr *Wrangler) Restore(srcTabletAlias topo.TabletAlias, srcFilePath string, dstTabletAlias, parentAlias topo.TabletAlias, fetchConcurrency, fetchRetryCount int, wasReserved, dontWaitForSlaveStart bool) error { +func (wr *Wrangler) Restore(ctx context.Context, srcTabletAlias topo.TabletAlias, srcFilePath string, dstTabletAlias, parentAlias topo.TabletAlias, fetchConcurrency, fetchRetryCount int, wasReserved, dontWaitForSlaveStart bool) error { // read our current tablet, verify its state before sending it // to the tablet itself tablet, err := wr.ts.GetTablet(dstTabletAlias) @@ -133,7 +132,7 @@ func (wr *Wrangler) Restore(srcTabletAlias topo.TabletAlias, srcFilePath string, if err != nil { return fmt.Errorf("Cannot read shard: %v", err) } - if err := wr.updateShardCellsAndMaster(si, tablet.Alias, topo.TYPE_SPARE, false); err != nil { + if err := wr.updateShardCellsAndMaster(ctx, si, tablet.Alias, topo.TYPE_SPARE, false); err != nil { return err } @@ -147,7 +146,7 @@ func (wr *Wrangler) Restore(srcTabletAlias topo.TabletAlias, srcFilePath string, WasReserved: wasReserved, DontWaitForSlaveStart: dontWaitForSlaveStart, } - logStream, errFunc, err := wr.tmc.Restore(context.TODO(), tablet, args, wr.ActionTimeout()) + logStream, errFunc, err := wr.tmc.Restore(ctx, tablet, args) if err != nil { return err } @@ -165,9 +164,9 @@ func (wr *Wrangler) Restore(srcTabletAlias topo.TabletAlias, srcFilePath string, } // UnreserveForRestoreMulti calls UnreserveForRestore on all targets. -func (wr *Wrangler) UnreserveForRestoreMulti(dstTabletAliases []topo.TabletAlias) { +func (wr *Wrangler) UnreserveForRestoreMulti(ctx context.Context, dstTabletAliases []topo.TabletAlias) { for _, dstTabletAlias := range dstTabletAliases { - ufrErr := wr.UnreserveForRestore(dstTabletAlias) + ufrErr := wr.UnreserveForRestore(ctx, dstTabletAlias) if ufrErr != nil { wr.Logger().Errorf("Failed to UnreserveForRestore destination tablet after failed source snapshot: %v", ufrErr) } else { @@ -178,15 +177,15 @@ func (wr *Wrangler) UnreserveForRestoreMulti(dstTabletAliases []topo.TabletAlias // Clone will do all the necessary actions to copy all the data from a // source to a set of destinations. -func (wr *Wrangler) Clone(srcTabletAlias topo.TabletAlias, dstTabletAliases []topo.TabletAlias, forceMasterSnapshot bool, snapshotConcurrency, fetchConcurrency, fetchRetryCount int, serverMode bool) error { +func (wr *Wrangler) Clone(ctx context.Context, srcTabletAlias topo.TabletAlias, dstTabletAliases []topo.TabletAlias, forceMasterSnapshot bool, snapshotConcurrency, fetchConcurrency, fetchRetryCount int, serverMode bool) error { // make sure the destination can be restored into (otherwise // there is no point in taking the snapshot in the first place), // and reserve it. reserved := make([]topo.TabletAlias, 0, len(dstTabletAliases)) for _, dstTabletAlias := range dstTabletAliases { - err := wr.ReserveForRestore(srcTabletAlias, dstTabletAlias) + err := wr.ReserveForRestore(ctx, srcTabletAlias, dstTabletAlias) if err != nil { - wr.UnreserveForRestoreMulti(reserved) + wr.UnreserveForRestoreMulti(ctx, reserved) return err } reserved = append(reserved, dstTabletAlias) @@ -195,10 +194,10 @@ func (wr *Wrangler) Clone(srcTabletAlias topo.TabletAlias, dstTabletAliases []to // take the snapshot, or put the server in SnapshotSource mode // srcFilePath, parentAlias, slaveStartRequired, readWrite - sr, originalType, err := wr.Snapshot(srcTabletAlias, forceMasterSnapshot, snapshotConcurrency, serverMode) + sr, originalType, err := wr.Snapshot(ctx, srcTabletAlias, forceMasterSnapshot, snapshotConcurrency, serverMode) if err != nil { // The snapshot failed so un-reserve the destinations and return - wr.UnreserveForRestoreMulti(reserved) + wr.UnreserveForRestoreMulti(ctx, reserved) return err } @@ -210,7 +209,7 @@ func (wr *Wrangler) Clone(srcTabletAlias topo.TabletAlias, dstTabletAliases []to for _, dstTabletAlias := range dstTabletAliases { wg.Add(1) go func(dstTabletAlias topo.TabletAlias) { - e := wr.Restore(srcTabletAlias, sr.ManifestPath, dstTabletAlias, sr.ParentAlias, fetchConcurrency, fetchRetryCount, true, serverMode && originalType == topo.TYPE_MASTER) + e := wr.Restore(ctx, srcTabletAlias, sr.ManifestPath, dstTabletAlias, sr.ParentAlias, fetchConcurrency, fetchRetryCount, true, serverMode && originalType == topo.TYPE_MASTER) rec.RecordError(e) wg.Done() }(dstTabletAlias) @@ -220,7 +219,7 @@ func (wr *Wrangler) Clone(srcTabletAlias topo.TabletAlias, dstTabletAliases []to // in any case, fix the server if serverMode { - resetErr := wr.SnapshotSourceEnd(srcTabletAlias, sr.SlaveStartRequired, sr.ReadOnly, originalType) + resetErr := wr.SnapshotSourceEnd(ctx, srcTabletAlias, sr.SlaveStartRequired, sr.ReadOnly, originalType) if resetErr != nil { if err == nil { // If there is no other error, this matters. diff --git a/go/vt/wrangler/hook.go b/go/vt/wrangler/hook.go index 5f3bba0c437..4eb19fe50f5 100644 --- a/go/vt/wrangler/hook.go +++ b/go/vt/wrangler/hook.go @@ -8,14 +8,14 @@ import ( "fmt" "strings" - "code.google.com/p/go.net/context" - log "github.com/golang/glog" hk "github.com/youtube/vitess/go/vt/hook" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) -func (wr *Wrangler) ExecuteHook(tabletAlias topo.TabletAlias, hook *hk.Hook) (hookResult *hk.HookResult, err error) { +// ExecuteHook will run the hook on the tablet +func (wr *Wrangler) ExecuteHook(ctx context.Context, tabletAlias topo.TabletAlias, hook *hk.Hook) (hookResult *hk.HookResult, err error) { if strings.Contains(hook.Name, "/") { return nil, fmt.Errorf("hook name cannot have a '/' in it") } @@ -23,17 +23,19 @@ func (wr *Wrangler) ExecuteHook(tabletAlias topo.TabletAlias, hook *hk.Hook) (ho if err != nil { return nil, err } - return wr.ExecuteTabletInfoHook(ti, hook) + return wr.ExecuteTabletInfoHook(ctx, ti, hook) } -func (wr *Wrangler) ExecuteTabletInfoHook(ti *topo.TabletInfo, hook *hk.Hook) (hookResult *hk.HookResult, err error) { - return wr.tmc.ExecuteHook(context.TODO(), ti, hook, wr.ActionTimeout()) +// ExecuteTabletInfoHook will run the hook on the tablet described by +// TabletInfo +func (wr *Wrangler) ExecuteTabletInfoHook(ctx context.Context, ti *topo.TabletInfo, hook *hk.Hook) (hookResult *hk.HookResult, err error) { + return wr.tmc.ExecuteHook(ctx, ti, hook) } -// Execute a hook and returns an error only if the hook failed, not if -// the hook doesn't exist. -func (wr *Wrangler) ExecuteOptionalTabletInfoHook(ti *topo.TabletInfo, hook *hk.Hook) (err error) { - hr, err := wr.ExecuteTabletInfoHook(ti, hook) +// ExecuteOptionalTabletInfoHook executes a hook and returns an error +// only if the hook failed, not if the hook doesn't exist. +func (wr *Wrangler) ExecuteOptionalTabletInfoHook(ctx context.Context, ti *topo.TabletInfo, hook *hk.Hook) (err error) { + hr, err := wr.ExecuteTabletInfoHook(ctx, ti, hook) if err != nil { return err } diff --git a/go/vt/wrangler/keyspace.go b/go/vt/wrangler/keyspace.go index 1a23f6623a1..de8b6a22e0e 100644 --- a/go/vt/wrangler/keyspace.go +++ b/go/vt/wrangler/keyspace.go @@ -7,8 +7,8 @@ package wrangler import ( "fmt" "sync" + "time" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/event" blproto "github.com/youtube/vitess/go/vt/binlog/proto" "github.com/youtube/vitess/go/vt/concurrency" @@ -18,29 +18,32 @@ import ( "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" "github.com/youtube/vitess/go/vt/topotools/events" + "golang.org/x/net/context" ) // keyspace related methods for Wrangler -func (wr *Wrangler) lockKeyspace(keyspace string, actionNode *actionnode.ActionNode) (lockPath string, err error) { - return actionNode.LockKeyspace(context.TODO(), wr.ts, keyspace, wr.lockTimeout, interrupted) +func (wr *Wrangler) lockKeyspace(ctx context.Context, keyspace string, actionNode *actionnode.ActionNode) (lockPath string, err error) { + ctx, cancel := context.WithTimeout(ctx, wr.lockTimeout) + defer cancel() + return actionNode.LockKeyspace(ctx, wr.ts, keyspace) } -func (wr *Wrangler) unlockKeyspace(keyspace string, actionNode *actionnode.ActionNode, lockPath string, actionError error) error { - return actionNode.UnlockKeyspace(wr.ts, keyspace, lockPath, actionError) +func (wr *Wrangler) unlockKeyspace(ctx context.Context, keyspace string, actionNode *actionnode.ActionNode, lockPath string, actionError error) error { + return actionNode.UnlockKeyspace(ctx, wr.ts, keyspace, lockPath, actionError) } // SetKeyspaceShardingInfo locks a keyspace and sets its ShardingColumnName // and ShardingColumnType -func (wr *Wrangler) SetKeyspaceShardingInfo(keyspace, shardingColumnName string, shardingColumnType key.KeyspaceIdType, splitShardCount int32, force bool) error { +func (wr *Wrangler) SetKeyspaceShardingInfo(ctx context.Context, keyspace, shardingColumnName string, shardingColumnType key.KeyspaceIdType, splitShardCount int32, force bool) error { actionNode := actionnode.SetKeyspaceShardingInfo() - lockPath, err := wr.lockKeyspace(keyspace, actionNode) + lockPath, err := wr.lockKeyspace(ctx, keyspace, actionNode) if err != nil { return err } err = wr.setKeyspaceShardingInfo(keyspace, shardingColumnName, shardingColumnType, splitShardCount, force) - return wr.unlockKeyspace(keyspace, actionNode, lockPath, err) + return wr.unlockKeyspace(ctx, keyspace, actionNode, lockPath, err) } @@ -74,7 +77,7 @@ func (wr *Wrangler) setKeyspaceShardingInfo(keyspace, shardingColumnName string, // MigrateServedTypes is used during horizontal splits to migrate a // served type from a list of shards to another. -func (wr *Wrangler) MigrateServedTypes(keyspace, shard string, cells []string, servedType topo.TabletType, reverse, skipReFreshState bool) error { +func (wr *Wrangler) MigrateServedTypes(ctx context.Context, keyspace, shard string, cells []string, servedType topo.TabletType, reverse, skipReFreshState bool, filteredReplicationWaitTime time.Duration) error { if servedType == topo.TYPE_MASTER { // we cannot migrate a master back, since when master migration // is done, the source shards are dead @@ -134,7 +137,7 @@ func (wr *Wrangler) MigrateServedTypes(keyspace, shard string, cells []string, s actionNode := actionnode.MigrateServedTypes(servedType) sourceLockPath := make([]string, len(sourceShards)) for i, si := range sourceShards { - sourceLockPath[i], err = wr.lockShard(si.Keyspace(), si.ShardName(), actionNode) + sourceLockPath[i], err = wr.lockShard(ctx, si.Keyspace(), si.ShardName(), actionNode) if err != nil { wr.Logger().Errorf("Failed to lock source shard %v/%v, may need to unlock other shards manually", si.Keyspace(), si.ShardName()) return err @@ -142,7 +145,7 @@ func (wr *Wrangler) MigrateServedTypes(keyspace, shard string, cells []string, s } destinationLockPath := make([]string, len(destinationShards)) for i, si := range destinationShards { - destinationLockPath[i], err = wr.lockShard(si.Keyspace(), si.ShardName(), actionNode) + destinationLockPath[i], err = wr.lockShard(ctx, si.Keyspace(), si.ShardName(), actionNode) if err != nil { wr.Logger().Errorf("Failed to lock destination shard %v/%v, may need to unlock other shards manually", si.Keyspace(), si.ShardName()) return err @@ -153,29 +156,36 @@ func (wr *Wrangler) MigrateServedTypes(keyspace, shard string, cells []string, s rec := concurrency.AllErrorRecorder{} // execute the migration - rec.RecordError(wr.migrateServedTypes(keyspace, sourceShards, destinationShards, cells, servedType, reverse)) + rec.RecordError(wr.migrateServedTypes(ctx, keyspace, sourceShards, destinationShards, cells, servedType, reverse, filteredReplicationWaitTime)) // unlock the shards, we're done for i := len(destinationShards) - 1; i >= 0; i-- { - rec.RecordError(wr.unlockShard(destinationShards[i].Keyspace(), destinationShards[i].ShardName(), actionNode, destinationLockPath[i], nil)) + rec.RecordError(wr.unlockShard(ctx, destinationShards[i].Keyspace(), destinationShards[i].ShardName(), actionNode, destinationLockPath[i], nil)) } for i := len(sourceShards) - 1; i >= 0; i-- { - rec.RecordError(wr.unlockShard(sourceShards[i].Keyspace(), sourceShards[i].ShardName(), actionNode, sourceLockPath[i], nil)) + rec.RecordError(wr.unlockShard(ctx, sourceShards[i].Keyspace(), sourceShards[i].ShardName(), actionNode, sourceLockPath[i], nil)) } // rebuild the keyspace serving graph if there was no error if !rec.HasErrors() { - rec.RecordError(wr.RebuildKeyspaceGraph(keyspace, nil)) + rec.RecordError(wr.RebuildKeyspaceGraph(ctx, keyspace, nil)) } - // Send a refresh to the source tablets we just disabled, iff: + // Send a refresh to the tablets we just disabled, iff: // - we're not migrating a master - // - it is not a reverse migration - // - we dont' have any errors + // - we don't have any errors // - we're not told to skip the refresh - if servedType != topo.TYPE_MASTER && !reverse && !rec.HasErrors() && !skipReFreshState { - for _, si := range sourceShards { - rec.RecordError(wr.RefreshTablesByShard(si, servedType, cells)) + if servedType != topo.TYPE_MASTER && !rec.HasErrors() && !skipReFreshState { + var refreshShards []*topo.ShardInfo + if reverse { + // For a backwards migration, we just disabled query service on the destination shards + refreshShards = destinationShards + } else { + // For a forwards migration, we just disabled query service on the source shards + refreshShards = sourceShards + } + for _, si := range refreshShards { + rec.RecordError(wr.RefreshTablesByShard(ctx, si, servedType, cells)) } } @@ -195,7 +205,7 @@ func removeType(tabletType topo.TabletType, types []topo.TabletType) ([]topo.Tab return result, found } -func (wr *Wrangler) getMastersPosition(shards []*topo.ShardInfo) (map[*topo.ShardInfo]myproto.ReplicationPosition, error) { +func (wr *Wrangler) getMastersPosition(ctx context.Context, shards []*topo.ShardInfo) (map[*topo.ShardInfo]myproto.ReplicationPosition, error) { mu := sync.Mutex{} result := make(map[*topo.ShardInfo]myproto.ReplicationPosition) @@ -212,7 +222,7 @@ func (wr *Wrangler) getMastersPosition(shards []*topo.ShardInfo) (map[*topo.Shar return } - pos, err := wr.tmc.MasterPosition(context.TODO(), ti, wr.ActionTimeout()) + pos, err := wr.tmc.MasterPosition(ctx, ti) if err != nil { rec.RecordError(err) return @@ -228,7 +238,7 @@ func (wr *Wrangler) getMastersPosition(shards []*topo.ShardInfo) (map[*topo.Shar return result, rec.Error() } -func (wr *Wrangler) waitForFilteredReplication(sourcePositions map[*topo.ShardInfo]myproto.ReplicationPosition, destinationShards []*topo.ShardInfo) error { +func (wr *Wrangler) waitForFilteredReplication(ctx context.Context, sourcePositions map[*topo.ShardInfo]myproto.ReplicationPosition, destinationShards []*topo.ShardInfo, waitTime time.Duration) error { wg := sync.WaitGroup{} rec := concurrency.AllErrorRecorder{} for _, si := range destinationShards { @@ -256,7 +266,7 @@ func (wr *Wrangler) waitForFilteredReplication(sourcePositions map[*topo.ShardIn return } - if err := wr.tmc.WaitBlpPosition(context.TODO(), tablet, blpPosition, wr.ActionTimeout()); err != nil { + if err := wr.tmc.WaitBlpPosition(ctx, tablet, blpPosition, waitTime); err != nil { rec.RecordError(err) } else { wr.Logger().Infof("%v caught up", si.MasterAlias) @@ -269,7 +279,7 @@ func (wr *Wrangler) waitForFilteredReplication(sourcePositions map[*topo.ShardIn } // refreshMasters will just RPC-ping all the masters with RefreshState -func (wr *Wrangler) refreshMasters(shards []*topo.ShardInfo) error { +func (wr *Wrangler) refreshMasters(ctx context.Context, shards []*topo.ShardInfo) error { wg := sync.WaitGroup{} rec := concurrency.AllErrorRecorder{} for _, si := range shards { @@ -283,7 +293,7 @@ func (wr *Wrangler) refreshMasters(shards []*topo.ShardInfo) error { return } - if err := wr.tmc.RefreshState(context.TODO(), ti, wr.ActionTimeout()); err != nil { + if err := wr.tmc.RefreshState(ctx, ti); err != nil { rec.RecordError(err) } else { wr.Logger().Infof("%v responded", si.MasterAlias) @@ -295,7 +305,7 @@ func (wr *Wrangler) refreshMasters(shards []*topo.ShardInfo) error { } // migrateServedTypes operates with all concerned shards locked. -func (wr *Wrangler) migrateServedTypes(keyspace string, sourceShards, destinationShards []*topo.ShardInfo, cells []string, servedType topo.TabletType, reverse bool) (err error) { +func (wr *Wrangler) migrateServedTypes(ctx context.Context, keyspace string, sourceShards, destinationShards []*topo.ShardInfo, cells []string, servedType topo.TabletType, reverse bool, filteredReplicationWaitTime time.Duration) (err error) { // re-read all the shards so we are up to date wr.Logger().Infof("Re-reading all shards") @@ -335,22 +345,22 @@ func (wr *Wrangler) migrateServedTypes(keyspace string, sourceShards, destinatio if err := si.UpdateDisableQueryService(topo.TYPE_MASTER, nil, true); err != nil { return err } - if err := topo.UpdateShard(context.TODO(), wr.ts, si); err != nil { + if err := topo.UpdateShard(ctx, wr.ts, si); err != nil { return err } } - if err := wr.refreshMasters(sourceShards); err != nil { + if err := wr.refreshMasters(ctx, sourceShards); err != nil { return err } event.DispatchUpdate(ev, "getting positions of source masters") - masterPositions, err := wr.getMastersPosition(sourceShards) + masterPositions, err := wr.getMastersPosition(ctx, sourceShards) if err != nil { return err } event.DispatchUpdate(ev, "waiting for destination masters to catch up") - if err := wr.waitForFilteredReplication(masterPositions, destinationShards); err != nil { + if err := wr.waitForFilteredReplication(ctx, masterPositions, destinationShards, filteredReplicationWaitTime); err != nil { return err } @@ -385,38 +395,64 @@ func (wr *Wrangler) migrateServedTypes(keyspace string, sourceShards, destinatio } } } + // We remember if we need to refresh the state of the destination tablets + // so their query service will be enabled. + needToRefreshDestinationTablets := false for _, si := range destinationShards { if err := si.UpdateServedTypesMap(servedType, cells, reverse); err != nil { return err } + if tc, ok := si.TabletControlMap[servedType]; !reverse && ok && tc.DisableQueryService { + // This is a forwards migration, and the destination query service was already in a disabled state. + // We need to enable and force a refresh, otherwise it's possible that both the source and destination + // will have query service disabled at the same time, and queries would have nowhere to go. + if err := si.UpdateDisableQueryService(servedType, cells, false); err != nil { + return err + } + needToRefreshDestinationTablets = true + } + if reverse && servedType != topo.TYPE_MASTER { + // this is a backwards migration, we need to disable + // query service on the destination shards. + // (we're not allowed to reverse a master migration) + if err := si.UpdateDisableQueryService(servedType, cells, true); err != nil { + return err + } + } } // All is good, we can save the shards now event.DispatchUpdate(ev, "updating source shards") for _, si := range sourceShards { - if err := topo.UpdateShard(context.TODO(), wr.ts, si); err != nil { + if err := topo.UpdateShard(ctx, wr.ts, si); err != nil { return err } } if needToRefreshSourceTablets { event.DispatchUpdate(ev, "refreshing source shard tablets so they restart their query service") for _, si := range sourceShards { - wr.RefreshTablesByShard(si, servedType, cells) + wr.RefreshTablesByShard(ctx, si, servedType, cells) } } event.DispatchUpdate(ev, "updating destination shards") for _, si := range destinationShards { - if err := topo.UpdateShard(context.TODO(), wr.ts, si); err != nil { + if err := topo.UpdateShard(ctx, wr.ts, si); err != nil { return err } } + if needToRefreshDestinationTablets { + event.DispatchUpdate(ev, "refreshing destination shard tablets so they restart their query service") + for _, si := range destinationShards { + wr.RefreshTablesByShard(ctx, si, servedType, cells) + } + } // And tell the new shards masters they can now be read-write. // Invoking a remote action will also make the tablet stop filtered // replication. if servedType == topo.TYPE_MASTER { event.DispatchUpdate(ev, "setting destination masters read-write") - if err := wr.refreshMasters(destinationShards); err != nil { + if err := wr.refreshMasters(ctx, destinationShards); err != nil { return err } } @@ -427,7 +463,7 @@ func (wr *Wrangler) migrateServedTypes(keyspace string, sourceShards, destinatio // MigrateServedFrom is used during vertical splits to migrate a // served type from a keyspace to another. -func (wr *Wrangler) MigrateServedFrom(keyspace, shard string, servedType topo.TabletType, cells []string, reverse bool) error { +func (wr *Wrangler) MigrateServedFrom(ctx context.Context, keyspace, shard string, servedType topo.TabletType, cells []string, reverse bool, filteredReplicationWaitTime time.Duration) error { // read the destination keyspace, check it ki, err := wr.ts.GetKeyspace(keyspace) if err != nil { @@ -454,24 +490,24 @@ func (wr *Wrangler) MigrateServedFrom(keyspace, shard string, servedType topo.Ta // lock the keyspace and shards actionNode := actionnode.MigrateServedFrom(servedType) - keyspaceLockPath, err := wr.lockKeyspace(keyspace, actionNode) + keyspaceLockPath, err := wr.lockKeyspace(ctx, keyspace, actionNode) if err != nil { wr.Logger().Errorf("Failed to lock destination keyspace %v", keyspace) return err } - destinationShardLockPath, err := wr.lockShard(keyspace, shard, actionNode) + destinationShardLockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { wr.Logger().Errorf("Failed to lock destination shard %v/%v", keyspace, shard) - wr.unlockKeyspace(keyspace, actionNode, keyspaceLockPath, nil) + wr.unlockKeyspace(ctx, keyspace, actionNode, keyspaceLockPath, nil) return err } sourceKeyspace := si.SourceShards[0].Keyspace sourceShard := si.SourceShards[0].Shard - sourceShardLockPath, err := wr.lockShard(sourceKeyspace, sourceShard, actionNode) + sourceShardLockPath, err := wr.lockShard(ctx, sourceKeyspace, sourceShard, actionNode) if err != nil { wr.Logger().Errorf("Failed to lock source shard %v/%v", sourceKeyspace, sourceShard) - wr.unlockShard(keyspace, shard, actionNode, destinationShardLockPath, nil) - wr.unlockKeyspace(keyspace, actionNode, keyspaceLockPath, nil) + wr.unlockShard(ctx, keyspace, shard, actionNode, destinationShardLockPath, nil) + wr.unlockKeyspace(ctx, keyspace, actionNode, keyspaceLockPath, nil) return err } @@ -479,21 +515,21 @@ func (wr *Wrangler) MigrateServedFrom(keyspace, shard string, servedType topo.Ta rec := concurrency.AllErrorRecorder{} // execute the migration - rec.RecordError(wr.migrateServedFrom(ki, si, servedType, cells, reverse)) + rec.RecordError(wr.migrateServedFrom(ctx, ki, si, servedType, cells, reverse, filteredReplicationWaitTime)) - rec.RecordError(wr.unlockShard(sourceKeyspace, sourceShard, actionNode, sourceShardLockPath, nil)) - rec.RecordError(wr.unlockShard(keyspace, shard, actionNode, destinationShardLockPath, nil)) - rec.RecordError(wr.unlockKeyspace(keyspace, actionNode, keyspaceLockPath, nil)) + rec.RecordError(wr.unlockShard(ctx, sourceKeyspace, sourceShard, actionNode, sourceShardLockPath, nil)) + rec.RecordError(wr.unlockShard(ctx, keyspace, shard, actionNode, destinationShardLockPath, nil)) + rec.RecordError(wr.unlockKeyspace(ctx, keyspace, actionNode, keyspaceLockPath, nil)) // rebuild the keyspace serving graph if there was no error if rec.Error() == nil { - rec.RecordError(wr.RebuildKeyspaceGraph(keyspace, cells)) + rec.RecordError(wr.RebuildKeyspaceGraph(ctx, keyspace, cells)) } return rec.Error() } -func (wr *Wrangler) migrateServedFrom(ki *topo.KeyspaceInfo, destinationShard *topo.ShardInfo, servedType topo.TabletType, cells []string, reverse bool) (err error) { +func (wr *Wrangler) migrateServedFrom(ctx context.Context, ki *topo.KeyspaceInfo, destinationShard *topo.ShardInfo, servedType topo.TabletType, cells []string, reverse bool, filteredReplicationWaitTime time.Duration) (err error) { // re-read and update keyspace info record ki, err = wr.ts.GetKeyspace(ki.KeyspaceName()) @@ -539,16 +575,16 @@ func (wr *Wrangler) migrateServedFrom(ki *topo.KeyspaceInfo, destinationShard *t }() if servedType == topo.TYPE_MASTER { - err = wr.masterMigrateServedFrom(ki, sourceShard, destinationShard, tables, ev) + err = wr.masterMigrateServedFrom(ctx, ki, sourceShard, destinationShard, tables, ev, filteredReplicationWaitTime) } else { - err = wr.replicaMigrateServedFrom(ki, sourceShard, destinationShard, servedType, cells, reverse, tables, ev) + err = wr.replicaMigrateServedFrom(ctx, ki, sourceShard, destinationShard, servedType, cells, reverse, tables, ev) } event.DispatchUpdate(ev, "finished") return } // replicaMigrateServedFrom handles the slave (replica, rdonly) migration. -func (wr *Wrangler) replicaMigrateServedFrom(ki *topo.KeyspaceInfo, sourceShard *topo.ShardInfo, destinationShard *topo.ShardInfo, servedType topo.TabletType, cells []string, reverse bool, tables []string, ev *events.MigrateServedFrom) error { +func (wr *Wrangler) replicaMigrateServedFrom(ctx context.Context, ki *topo.KeyspaceInfo, sourceShard *topo.ShardInfo, destinationShard *topo.ShardInfo, servedType topo.TabletType, cells []string, reverse bool, tables []string, ev *events.MigrateServedFrom) error { // Save the destination keyspace (its ServedFrom has been changed) event.DispatchUpdate(ev, "updating keyspace") if err := topo.UpdateKeyspace(wr.ts, ki); err != nil { @@ -560,14 +596,14 @@ func (wr *Wrangler) replicaMigrateServedFrom(ki *topo.KeyspaceInfo, sourceShard if err := sourceShard.UpdateSourceBlacklistedTables(servedType, cells, reverse, tables); err != nil { return fmt.Errorf("UpdateSourceBlacklistedTables(%v/%v) failed: %v", sourceShard.Keyspace(), sourceShard.ShardName(), err) } - if err := topo.UpdateShard(context.TODO(), wr.ts, sourceShard); err != nil { + if err := topo.UpdateShard(ctx, wr.ts, sourceShard); err != nil { return fmt.Errorf("UpdateShard(%v/%v) failed: %v", sourceShard.Keyspace(), sourceShard.ShardName(), err) } // Now refresh the source servers so they reload their // blacklisted table list event.DispatchUpdate(ev, "refreshing sources tablets state so they update their blacklisted tables") - if err := wr.RefreshTablesByShard(sourceShard, servedType, cells); err != nil { + if err := wr.RefreshTablesByShard(ctx, sourceShard, servedType, cells); err != nil { return err } @@ -584,7 +620,7 @@ func (wr *Wrangler) replicaMigrateServedFrom(ki *topo.KeyspaceInfo, sourceShard // - Clear SourceShard on the destination Shard // - Refresh the destination master, so its stops its filtered // replication and starts accepting writes -func (wr *Wrangler) masterMigrateServedFrom(ki *topo.KeyspaceInfo, sourceShard *topo.ShardInfo, destinationShard *topo.ShardInfo, tables []string, ev *events.MigrateServedFrom) error { +func (wr *Wrangler) masterMigrateServedFrom(ctx context.Context, ki *topo.KeyspaceInfo, sourceShard *topo.ShardInfo, destinationShard *topo.ShardInfo, tables []string, ev *events.MigrateServedFrom, filteredReplicationWaitTime time.Duration) error { // Read the data we need sourceMasterTabletInfo, err := wr.ts.GetTablet(sourceShard.MasterAlias) if err != nil { @@ -600,29 +636,29 @@ func (wr *Wrangler) masterMigrateServedFrom(ki *topo.KeyspaceInfo, sourceShard * if err := sourceShard.UpdateSourceBlacklistedTables(topo.TYPE_MASTER, nil, false, tables); err != nil { return fmt.Errorf("UpdateSourceBlacklistedTables(%v/%v) failed: %v", sourceShard.Keyspace(), sourceShard.ShardName(), err) } - if err := topo.UpdateShard(context.TODO(), wr.ts, sourceShard); err != nil { + if err := topo.UpdateShard(ctx, wr.ts, sourceShard); err != nil { return fmt.Errorf("UpdateShard(%v/%v) failed: %v", sourceShard.Keyspace(), sourceShard.ShardName(), err) } // Now refresh the blacklisted table list on the source master event.DispatchUpdate(ev, "refreshing source master so it updates its blacklisted tables") - if err := wr.tmc.RefreshState(context.TODO(), sourceMasterTabletInfo, wr.ActionTimeout()); err != nil { + if err := wr.tmc.RefreshState(ctx, sourceMasterTabletInfo); err != nil { return err } // get the position event.DispatchUpdate(ev, "getting master position") - masterPosition, err := wr.tmc.MasterPosition(context.TODO(), sourceMasterTabletInfo, wr.ActionTimeout()) + masterPosition, err := wr.tmc.MasterPosition(ctx, sourceMasterTabletInfo) if err != nil { return err } // wait for it event.DispatchUpdate(ev, "waiting for destination master to catch up to source master") - if err := wr.tmc.WaitBlpPosition(context.TODO(), destinationMasterTabletInfo, blproto.BlpPosition{ + if err := wr.tmc.WaitBlpPosition(ctx, destinationMasterTabletInfo, blproto.BlpPosition{ Uid: 0, Position: masterPosition, - }, wr.ActionTimeout()); err != nil { + }, filteredReplicationWaitTime); err != nil { return err } @@ -635,7 +671,7 @@ func (wr *Wrangler) masterMigrateServedFrom(ki *topo.KeyspaceInfo, sourceShard * // Update the destination shard (no more source shard) event.DispatchUpdate(ev, "updating destination shard") destinationShard.SourceShards = nil - if err := topo.UpdateShard(context.TODO(), wr.ts, destinationShard); err != nil { + if err := topo.UpdateShard(ctx, wr.ts, destinationShard); err != nil { return err } @@ -643,7 +679,7 @@ func (wr *Wrangler) masterMigrateServedFrom(ki *topo.KeyspaceInfo, sourceShard * // Invoking a remote action will also make the tablet stop filtered // replication. event.DispatchUpdate(ev, "setting destination shard masters read-write") - if err := wr.refreshMasters([]*topo.ShardInfo{destinationShard}); err != nil { + if err := wr.refreshMasters(ctx, []*topo.ShardInfo{destinationShard}); err != nil { return err } @@ -651,15 +687,15 @@ func (wr *Wrangler) masterMigrateServedFrom(ki *topo.KeyspaceInfo, sourceShard * } // SetKeyspaceServedFrom locks a keyspace and changes its ServerFromMap -func (wr *Wrangler) SetKeyspaceServedFrom(keyspace string, servedType topo.TabletType, cells []string, sourceKeyspace string, remove bool) error { +func (wr *Wrangler) SetKeyspaceServedFrom(ctx context.Context, keyspace string, servedType topo.TabletType, cells []string, sourceKeyspace string, remove bool) error { actionNode := actionnode.SetKeyspaceServedFrom() - lockPath, err := wr.lockKeyspace(keyspace, actionNode) + lockPath, err := wr.lockKeyspace(ctx, keyspace, actionNode) if err != nil { return err } err = wr.setKeyspaceServedFrom(keyspace, servedType, cells, sourceKeyspace, remove) - return wr.unlockKeyspace(keyspace, actionNode, lockPath, err) + return wr.unlockKeyspace(ctx, keyspace, actionNode, lockPath, err) } func (wr *Wrangler) setKeyspaceServedFrom(keyspace string, servedType topo.TabletType, cells []string, sourceKeyspace string, remove bool) error { @@ -676,8 +712,9 @@ func (wr *Wrangler) setKeyspaceServedFrom(keyspace string, servedType topo.Table // RefreshTablesByShard calls RefreshState on all the tables of a // given type in a shard. It would work for the master, but the // discovery wouldn't be very efficient. -func (wr *Wrangler) RefreshTablesByShard(si *topo.ShardInfo, tabletType topo.TabletType, cells []string) error { - tabletMap, err := topo.GetTabletMapForShardByCell(wr.ts, si.Keyspace(), si.ShardName(), cells) +func (wr *Wrangler) RefreshTablesByShard(ctx context.Context, si *topo.ShardInfo, tabletType topo.TabletType, cells []string) error { + wr.Logger().Infof("RefreshTablesByShard called on shard %v/%v", si.Keyspace(), si.ShardName()) + tabletMap, err := topo.GetTabletMapForShardByCell(ctx, wr.ts, si.Keyspace(), si.ShardName(), cells) switch err { case nil: // keep going @@ -696,7 +733,8 @@ func (wr *Wrangler) RefreshTablesByShard(si *topo.ShardInfo, tabletType topo.Tab wg.Add(1) go func(ti *topo.TabletInfo) { - if err := wr.tmc.RefreshState(context.TODO(), ti, wr.ActionTimeout()); err != nil { + wr.Logger().Infof("Calling RefreshState on tablet %v", ti.Alias) + if err := wr.tmc.RefreshState(ctx, ti); err != nil { wr.Logger().Warningf("RefreshTablesByShard: failed to refresh %v: %v", ti.Alias, err) } wg.Done() diff --git a/go/vt/wrangler/permissions.go b/go/vt/wrangler/permissions.go index 74ae5ba90ec..e68412079d8 100644 --- a/go/vt/wrangler/permissions.go +++ b/go/vt/wrangler/permissions.go @@ -9,28 +9,28 @@ import ( "sort" "sync" - "code.google.com/p/go.net/context" - log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/concurrency" myproto "github.com/youtube/vitess/go/vt/mysqlctl/proto" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) -func (wr *Wrangler) GetPermissions(tabletAlias topo.TabletAlias) (*myproto.Permissions, error) { +// GetPermissions returns the permissions set on a remote tablet +func (wr *Wrangler) GetPermissions(ctx context.Context, tabletAlias topo.TabletAlias) (*myproto.Permissions, error) { tablet, err := wr.ts.GetTablet(tabletAlias) if err != nil { return nil, err } - return wr.tmc.GetPermissions(context.TODO(), tablet, wr.ActionTimeout()) + return wr.tmc.GetPermissions(ctx, tablet) } -// helper method to asynchronously diff a permissions -func (wr *Wrangler) diffPermissions(masterPermissions *myproto.Permissions, masterAlias topo.TabletAlias, alias topo.TabletAlias, wg *sync.WaitGroup, er concurrency.ErrorRecorder) { +// diffPermissions is a helper method to asynchronously diff a permissions +func (wr *Wrangler) diffPermissions(ctx context.Context, masterPermissions *myproto.Permissions, masterAlias topo.TabletAlias, alias topo.TabletAlias, wg *sync.WaitGroup, er concurrency.ErrorRecorder) { defer wg.Done() log.Infof("Gathering permissions for %v", alias) - slavePermissions, err := wr.GetPermissions(alias) + slavePermissions, err := wr.GetPermissions(ctx, alias) if err != nil { er.RecordError(err) return @@ -40,7 +40,9 @@ func (wr *Wrangler) diffPermissions(masterPermissions *myproto.Permissions, mast myproto.DiffPermissions(masterAlias.String(), masterPermissions, alias.String(), slavePermissions, er) } -func (wr *Wrangler) ValidatePermissionsShard(keyspace, shard string) error { +// ValidatePermissionsShard validates all the permissions are the same +// in a shard +func (wr *Wrangler) ValidatePermissionsShard(ctx context.Context, keyspace, shard string) error { si, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err @@ -51,14 +53,14 @@ func (wr *Wrangler) ValidatePermissionsShard(keyspace, shard string) error { return fmt.Errorf("No master in shard %v/%v", keyspace, shard) } log.Infof("Gathering permissions for master %v", si.MasterAlias) - masterPermissions, err := wr.GetPermissions(si.MasterAlias) + masterPermissions, err := wr.GetPermissions(ctx, si.MasterAlias) if err != nil { return err } // read all the aliases in the shard, that is all tablets that are // replicating from the master - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shard) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shard) if err != nil { return err } @@ -71,7 +73,7 @@ func (wr *Wrangler) ValidatePermissionsShard(keyspace, shard string) error { continue } wg.Add(1) - go wr.diffPermissions(masterPermissions, si.MasterAlias, alias, &wg, &er) + go wr.diffPermissions(ctx, masterPermissions, si.MasterAlias, alias, &wg, &er) } wg.Wait() if er.HasErrors() { @@ -80,7 +82,9 @@ func (wr *Wrangler) ValidatePermissionsShard(keyspace, shard string) error { return nil } -func (wr *Wrangler) ValidatePermissionsKeyspace(keyspace string) error { +// ValidatePermissionsKeyspace validates all the permissions are the same +// in a keyspace +func (wr *Wrangler) ValidatePermissionsKeyspace(ctx context.Context, keyspace string) error { // find all the shards shards, err := wr.ts.GetShardNames(keyspace) if err != nil { @@ -93,7 +97,7 @@ func (wr *Wrangler) ValidatePermissionsKeyspace(keyspace string) error { } sort.Strings(shards) if len(shards) == 1 { - return wr.ValidatePermissionsShard(keyspace, shards[0]) + return wr.ValidatePermissionsShard(ctx, keyspace, shards[0]) } // find the reference permissions using the first shard's master @@ -106,7 +110,7 @@ func (wr *Wrangler) ValidatePermissionsKeyspace(keyspace string) error { } referenceAlias := si.MasterAlias log.Infof("Gathering permissions for reference master %v", referenceAlias) - referencePermissions, err := wr.GetPermissions(si.MasterAlias) + referencePermissions, err := wr.GetPermissions(ctx, si.MasterAlias) if err != nil { return err } @@ -115,7 +119,7 @@ func (wr *Wrangler) ValidatePermissionsKeyspace(keyspace string) error { er := concurrency.AllErrorRecorder{} wg := sync.WaitGroup{} for _, shard := range shards { - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shard) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shard) if err != nil { er.RecordError(err) continue @@ -127,7 +131,7 @@ func (wr *Wrangler) ValidatePermissionsKeyspace(keyspace string) error { } wg.Add(1) - go wr.diffPermissions(referencePermissions, referenceAlias, alias, &wg, &er) + go wr.diffPermissions(ctx, referencePermissions, referenceAlias, alias, &wg, &er) } } wg.Wait() diff --git a/go/vt/wrangler/rebuild.go b/go/vt/wrangler/rebuild.go index 59d3caa5f21..218b789fb9f 100644 --- a/go/vt/wrangler/rebuild.go +++ b/go/vt/wrangler/rebuild.go @@ -8,32 +8,32 @@ import ( "fmt" "sync" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" + "golang.org/x/net/context" ) -// Rebuild the serving and replication rollup data data while locking +// RebuildShardGraph rebuilds the serving and replication rollup data data while locking // out other changes. -func (wr *Wrangler) RebuildShardGraph(keyspace, shard string, cells []string) (*topo.ShardInfo, error) { - return topotools.RebuildShard(context.TODO(), wr.logger, wr.ts, keyspace, shard, cells, wr.lockTimeout, interrupted) +func (wr *Wrangler) RebuildShardGraph(ctx context.Context, keyspace, shard string, cells []string) (*topo.ShardInfo, error) { + return topotools.RebuildShard(ctx, wr.logger, wr.ts, keyspace, shard, cells, wr.lockTimeout) } -// Rebuild the serving graph data while locking out other changes. +// RebuildKeyspaceGraph rebuilds the serving graph data while locking out other changes. // If some shards were recently read / updated, pass them in the cache so // we don't read them again (and possible get stale replicated data) -func (wr *Wrangler) RebuildKeyspaceGraph(keyspace string, cells []string) error { +func (wr *Wrangler) RebuildKeyspaceGraph(ctx context.Context, keyspace string, cells []string) error { actionNode := actionnode.RebuildKeyspace() - lockPath, err := wr.lockKeyspace(keyspace, actionNode) + lockPath, err := wr.lockKeyspace(ctx, keyspace, actionNode) if err != nil { return err } - err = wr.rebuildKeyspace(keyspace, cells) - return wr.unlockKeyspace(keyspace, actionNode, lockPath, err) + err = wr.rebuildKeyspace(ctx, keyspace, cells) + return wr.unlockKeyspace(ctx, keyspace, actionNode, lockPath, err) } // findCellsForRebuild will find all the cells in the given keyspace @@ -46,7 +46,6 @@ func (wr *Wrangler) findCellsForRebuild(ki *topo.KeyspaceInfo, shardMap map[stri } if _, ok := srvKeyspaceMap[cell]; !ok { srvKeyspaceMap[cell] = &topo.SrvKeyspace{ - Shards: make([]topo.SrvShard, 0, 16), ShardingColumnName: ki.ShardingColumnName, ShardingColumnType: ki.ShardingColumnType, ServedFrom: ki.ComputeCellServedFrom(cell), @@ -63,7 +62,7 @@ func (wr *Wrangler) findCellsForRebuild(ki *topo.KeyspaceInfo, shardMap map[stri // // Take data from the global keyspace and rebuild the local serving // copies in each cell. -func (wr *Wrangler) rebuildKeyspace(keyspace string, cells []string) error { +func (wr *Wrangler) rebuildKeyspace(ctx context.Context, keyspace string, cells []string) error { wr.logger.Infof("rebuildKeyspace %v", keyspace) ki, err := wr.ts.GetKeyspace(keyspace) @@ -84,7 +83,7 @@ func (wr *Wrangler) rebuildKeyspace(keyspace string, cells []string) error { for _, shard := range shards { wg.Add(1) go func(shard string) { - if shardInfo, err := wr.RebuildShardGraph(keyspace, shard, cells); err != nil { + if shardInfo, err := wr.RebuildShardGraph(ctx, keyspace, shard, cells); err != nil { rec.RecordError(fmt.Errorf("RebuildShardGraph failed: %v/%v %v", keyspace, shard, err)) } else { mu.Lock() @@ -182,7 +181,6 @@ func (wr *Wrangler) rebuildKeyspace(keyspace string, cells []string) error { func (wr *Wrangler) checkPartitions(cell string, srvKeyspace *topo.SrvKeyspace) error { // now check them all - first := true for tabletType, partition := range srvKeyspace.Partitions { topo.SrvShardArray(partition.Shards).Sort() @@ -199,23 +197,26 @@ func (wr *Wrangler) checkPartitions(cell string, srvKeyspace *topo.SrvKeyspace) return fmt.Errorf("non-contiguous KeyRange values for %v in cell %v at shard %v to %v: %v != %v", tabletType, cell, i, i+1, partition.Shards[i].KeyRange.End.Hex(), partition.Shards[i+1].KeyRange.Start.Hex()) } } - - // backfill Shards - if first { - first = false - srvKeyspace.Shards = partition.Shards - } } return nil } -// This is a quick and dirty tool to resurrect the TopologyServer data from the +func strInList(sl []string, s string) bool { + for _, x := range sl { + if x == s { + return true + } + } + return false +} + +// RebuildReplicationGraph is a quick and dirty tool to resurrect the TopologyServer data from the // canonical data stored in the tablet nodes. // // cells: local vt cells to scan for all tablets // keyspaces: list of keyspaces to rebuild -func (wr *Wrangler) RebuildReplicationGraph(cells []string, keyspaces []string) error { +func (wr *Wrangler) RebuildReplicationGraph(ctx context.Context, cells []string, keyspaces []string) error { if cells == nil || len(cells) == 0 { return fmt.Errorf("must specify cells to rebuild replication graph") } @@ -225,7 +226,7 @@ func (wr *Wrangler) RebuildReplicationGraph(cells []string, keyspaces []string) allTablets := make([]*topo.TabletInfo, 0, 1024) for _, cell := range cells { - tablets, err := topotools.GetAllTablets(wr.ts, cell) + tablets, err := topotools.GetAllTablets(ctx, wr.ts, cell) if err != nil { return err } @@ -266,7 +267,7 @@ func (wr *Wrangler) RebuildReplicationGraph(cells []string, keyspaces []string) } } mu.Unlock() - err := topo.UpdateTabletReplicationData(context.TODO(), wr.ts, ti.Tablet) + err := topo.UpdateTabletReplicationData(ctx, wr.ts, ti.Tablet) if err != nil { mu.Lock() hasErr = true @@ -281,7 +282,7 @@ func (wr *Wrangler) RebuildReplicationGraph(cells []string, keyspaces []string) wg.Add(1) go func(keyspace string) { defer wg.Done() - if err := wr.RebuildKeyspaceGraph(keyspace, nil); err != nil { + if err := wr.RebuildKeyspaceGraph(ctx, keyspace, nil); err != nil { mu.Lock() hasErr = true mu.Unlock() diff --git a/go/vt/wrangler/reparent.go b/go/vt/wrangler/reparent.go index c4c560beee3..cacb17e5df9 100644 --- a/go/vt/wrangler/reparent.go +++ b/go/vt/wrangler/reparent.go @@ -66,18 +66,14 @@ On X: (promoted slave) import ( "fmt" - - "code.google.com/p/go.net/context" + "time" myproto "github.com/youtube/vitess/go/vt/mysqlctl/proto" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" "github.com/youtube/vitess/go/vt/topotools/events" -) - -const ( - SLAVE_STATUS_DEADLINE = 10e9 + "golang.org/x/net/context" ) // ReparentShard creates the reparenting action and launches a goroutine @@ -88,28 +84,28 @@ const ( // though all the other necessary updates have been made. // forceReparentToCurrentMaster: mostly for test setups, this can // cause data loss. -func (wr *Wrangler) ReparentShard(keyspace, shard string, masterElectTabletAlias topo.TabletAlias, leaveMasterReadOnly, forceReparentToCurrentMaster bool) error { +func (wr *Wrangler) ReparentShard(ctx context.Context, keyspace, shard string, masterElectTabletAlias topo.TabletAlias, leaveMasterReadOnly, forceReparentToCurrentMaster bool, waitSlaveTimeout time.Duration) error { // lock the shard actionNode := actionnode.ReparentShard(masterElectTabletAlias) - lockPath, err := wr.lockShard(keyspace, shard, actionNode) + lockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { return err } // do the work - err = wr.reparentShardLocked(keyspace, shard, masterElectTabletAlias, leaveMasterReadOnly, forceReparentToCurrentMaster) + err = wr.reparentShardLocked(ctx, keyspace, shard, masterElectTabletAlias, leaveMasterReadOnly, forceReparentToCurrentMaster, waitSlaveTimeout) // and unlock - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } -func (wr *Wrangler) reparentShardLocked(keyspace, shard string, masterElectTabletAlias topo.TabletAlias, leaveMasterReadOnly, forceReparentToCurrentMaster bool) error { +func (wr *Wrangler) reparentShardLocked(ctx context.Context, keyspace, shard string, masterElectTabletAlias topo.TabletAlias, leaveMasterReadOnly, forceReparentToCurrentMaster bool, waitSlaveTimeout time.Duration) error { shardInfo, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err } - tabletMap, err := topo.GetTabletMapForShard(wr.ts, keyspace, shard) + tabletMap, err := topo.GetTabletMapForShard(ctx, wr.ts, keyspace, shard) if err != nil { return err } @@ -135,9 +131,9 @@ func (wr *Wrangler) reparentShardLocked(keyspace, shard string, masterElectTable } if !shardInfo.MasterAlias.IsZero() && !forceReparentToCurrentMaster { - err = wr.reparentShardGraceful(ev, shardInfo, slaveTabletMap, masterTabletMap, masterElectTablet, leaveMasterReadOnly) + err = wr.reparentShardGraceful(ctx, ev, shardInfo, slaveTabletMap, masterTabletMap, masterElectTablet, leaveMasterReadOnly, waitSlaveTimeout) } else { - err = wr.reparentShardBrutal(ev, shardInfo, slaveTabletMap, masterTabletMap, masterElectTablet, leaveMasterReadOnly, forceReparentToCurrentMaster) + err = wr.reparentShardBrutal(ctx, ev, shardInfo, slaveTabletMap, masterTabletMap, masterElectTablet, leaveMasterReadOnly, forceReparentToCurrentMaster, waitSlaveTimeout) } if err == nil { @@ -148,7 +144,7 @@ func (wr *Wrangler) reparentShardLocked(keyspace, shard string, masterElectTable } // ShardReplicationStatuses returns the ReplicationStatus for each tablet in a shard. -func (wr *Wrangler) ShardReplicationStatuses(keyspace, shard string) ([]*topo.TabletInfo, []*myproto.ReplicationStatus, error) { +func (wr *Wrangler) ShardReplicationStatuses(ctx context.Context, keyspace, shard string) ([]*topo.TabletInfo, []*myproto.ReplicationStatus, error) { shardInfo, err := wr.ts.GetShard(keyspace, shard) if err != nil { return nil, nil, err @@ -156,30 +152,30 @@ func (wr *Wrangler) ShardReplicationStatuses(keyspace, shard string) ([]*topo.Ta // lock the shard actionNode := actionnode.CheckShard() - lockPath, err := wr.lockShard(keyspace, shard, actionNode) + lockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { return nil, nil, err } - tabletMap, posMap, err := wr.shardReplicationStatuses(shardInfo) - return tabletMap, posMap, wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + tabletMap, posMap, err := wr.shardReplicationStatuses(ctx, shardInfo) + return tabletMap, posMap, wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } -func (wr *Wrangler) shardReplicationStatuses(shardInfo *topo.ShardInfo) ([]*topo.TabletInfo, []*myproto.ReplicationStatus, error) { +func (wr *Wrangler) shardReplicationStatuses(ctx context.Context, shardInfo *topo.ShardInfo) ([]*topo.TabletInfo, []*myproto.ReplicationStatus, error) { // FIXME(msolomon) this assumes no hierarchical replication, which is currently the case. - tabletMap, err := topo.GetTabletMapForShard(wr.ts, shardInfo.Keyspace(), shardInfo.ShardName()) + tabletMap, err := topo.GetTabletMapForShard(ctx, wr.ts, shardInfo.Keyspace(), shardInfo.ShardName()) if err != nil { return nil, nil, err } tablets := topotools.CopyMapValues(tabletMap, []*topo.TabletInfo{}).([]*topo.TabletInfo) - stats, err := wr.tabletReplicationStatuses(tablets) + stats, err := wr.tabletReplicationStatuses(ctx, tablets) return tablets, stats, err } // ReparentTablet attempts to reparent this tablet to the current // master, based on the current replication position. If there is no // match, it will fail. -func (wr *Wrangler) ReparentTablet(tabletAlias topo.TabletAlias) error { +func (wr *Wrangler) ReparentTablet(ctx context.Context, tabletAlias topo.TabletAlias) error { // Get specified tablet. // Get current shard master tablet. // Sanity check they are in the same keyspace/shard. @@ -213,13 +209,13 @@ func (wr *Wrangler) ReparentTablet(tabletAlias topo.TabletAlias) error { return fmt.Errorf("master %v and potential slave not in same keyspace/shard", shardInfo.MasterAlias) } - status, err := wr.tmc.SlaveStatus(context.TODO(), ti, wr.ActionTimeout()) + status, err := wr.tmc.SlaveStatus(ctx, ti) if err != nil { return err } wr.Logger().Infof("slave tablet position: %v %v %v", tabletAlias, ti.MysqlAddr(), status.Position) - rsd, err := wr.tmc.ReparentPosition(context.TODO(), masterTi, &status.Position, wr.ActionTimeout()) + rsd, err := wr.tmc.ReparentPosition(ctx, masterTi, &status.Position) if err != nil { return err } @@ -228,5 +224,5 @@ func (wr *Wrangler) ReparentTablet(tabletAlias topo.TabletAlias) error { // An orphan is already in the replication graph but it is // disconnected, hence we have to force this action. rsd.Force = ti.Type == topo.TYPE_LAG_ORPHAN - return wr.tmc.RestartSlave(context.TODO(), ti, rsd, wr.ActionTimeout()) + return wr.tmc.RestartSlave(ctx, ti, rsd) } diff --git a/go/vt/wrangler/reparent_action.go b/go/vt/wrangler/reparent_action.go index 0d2c0f4544e..52171eb7841 100644 --- a/go/vt/wrangler/reparent_action.go +++ b/go/vt/wrangler/reparent_action.go @@ -7,12 +7,12 @@ import ( "sync" "time" - "code.google.com/p/go.net/context" "github.com/youtube/vitess/go/vt/hook" myproto "github.com/youtube/vitess/go/vt/mysqlctl/proto" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" + "golang.org/x/net/context" ) // helper struct to queue up results @@ -24,20 +24,9 @@ type rpcContext struct { // Check all the tablets replication positions to find if some // will have a problem, and suggest a fix for them. -func (wr *Wrangler) checkSlaveReplication(tabletMap map[topo.TabletAlias]*topo.TabletInfo, masterTabletUid uint32) error { +func (wr *Wrangler) checkSlaveReplication(ctx context.Context, tabletMap map[topo.TabletAlias]*topo.TabletInfo, masterTabletUID uint32, waitSlaveTimeout time.Duration) error { wr.logger.Infof("Checking all replication positions will allow the transition:") - masterIsDead := masterTabletUid == topo.NO_TABLET - - // Check everybody has the right master. If there is no master - // (crash) just check that everyone has the same parent. - for _, tablet := range tabletMap { - if masterTabletUid == topo.NO_TABLET { - masterTabletUid = tablet.Parent.Uid - } - if tablet.Parent.Uid != masterTabletUid { - return fmt.Errorf("tablet %v not slaved correctly, expected %v, found %v", tablet.Alias, masterTabletUid, tablet.Parent.Uid) - } - } + masterIsDead := masterTabletUID == topo.NO_TABLET // now check all the replication positions will allow us to proceed if masterIsDead { @@ -65,7 +54,7 @@ func (wr *Wrangler) checkSlaveReplication(tabletMap map[topo.TabletAlias]*topo.T return } - status, err := wr.tmc.SlaveStatus(context.TODO(), tablet, wr.ActionTimeout()) + status, err := wr.tmc.SlaveStatus(ctx, tablet) if err != nil { if tablet.Type == topo.TYPE_BACKUP { wr.logger.Warningf(" failed to get slave position from backup tablet %v, either wait for backup to finish or scrap tablet (%v)", tablet.Alias, err) @@ -82,14 +71,14 @@ func (wr *Wrangler) checkSlaveReplication(tabletMap map[topo.TabletAlias]*topo.T return } - var dur time.Duration = time.Duration(uint(time.Second) * status.SecondsBehindMaster) - if dur > wr.ActionTimeout() { - err = fmt.Errorf("slave is too far behind to complete reparent in time (%v>%v), either increase timeout using 'vtctl -wait-time XXX ReparentShard ...' or scrap tablet %v", dur, wr.ActionTimeout(), tablet.Alias) + var dur = time.Second * time.Duration(status.SecondsBehindMaster) + if dur > waitSlaveTimeout { + err = fmt.Errorf("slave is too far behind to complete reparent in time (%v>%v), either increase timeout using 'vtctl ReparentShard -wait_slave_timeout=XXX ...' or scrap tablet %v", dur, waitSlaveTimeout, tablet.Alias) wr.logger.Errorf(" %v", err) return } - wr.logger.Infof(" slave is %v behind master (<%v), reparent should work for %v", dur, wr.ActionTimeout(), tablet.Alias) + wr.logger.Infof(" slave is %v behind master (<%v), reparent should work for %v", dur, waitSlaveTimeout, tablet.Alias) } }(tablet) } @@ -100,33 +89,33 @@ func (wr *Wrangler) checkSlaveReplication(tabletMap map[topo.TabletAlias]*topo.T // Check all the tablets to see if we can proceed with reparenting. // masterPosition is supplied from the demoted master if we are doing // this gracefully. -func (wr *Wrangler) checkSlaveConsistency(tabletMap map[uint32]*topo.TabletInfo, masterPosition myproto.ReplicationPosition) error { +func (wr *Wrangler) checkSlaveConsistency(ctx context.Context, tabletMap map[uint32]*topo.TabletInfo, masterPosition myproto.ReplicationPosition, waitSlaveTimeout time.Duration) error { wr.logger.Infof("checkSlaveConsistency %v %#v", topotools.MapKeys(tabletMap), masterPosition) // FIXME(msolomon) Something still feels clumsy here and I can't put my finger on it. calls := make(chan *rpcContext, len(tabletMap)) f := func(ti *topo.TabletInfo) { - ctx := &rpcContext{tablet: ti} + rpcCtx := &rpcContext{tablet: ti} defer func() { - calls <- ctx + calls <- rpcCtx }() if !masterPosition.IsZero() { // If the master position is known, do our best to wait for replication to catch up. - status, err := wr.tmc.WaitSlavePosition(context.TODO(), ti, masterPosition, wr.ActionTimeout()) + status, err := wr.tmc.WaitSlavePosition(ctx, ti, masterPosition, waitSlaveTimeout) if err != nil { - ctx.err = err + rpcCtx.err = err return } - ctx.status = status + rpcCtx.status = status } else { // If the master is down, just get the slave status. - status, err := wr.tmc.SlaveStatus(context.TODO(), ti, wr.ActionTimeout()) + status, err := wr.tmc.SlaveStatus(ctx, ti) if err != nil { - ctx.err = err + rpcCtx.err = err return } - ctx.status = status + rpcCtx.status = status } } @@ -138,15 +127,15 @@ func (wr *Wrangler) checkSlaveConsistency(tabletMap map[uint32]*topo.TabletInfo, // map positions to tablets positionMap := make(map[string][]uint32) for i := 0; i < len(tabletMap); i++ { - ctx := <-calls + rpcCtx := <-calls mapKey := "unavailable-tablet-error" - if ctx.err == nil { - mapKey = ctx.status.Position.String() + if rpcCtx.err == nil { + mapKey = rpcCtx.status.Position.String() } if _, ok := positionMap[mapKey]; !ok { positionMap[mapKey] = make([]uint32, 0, 32) } - positionMap[mapKey] = append(positionMap[mapKey], ctx.tablet.Alias.Uid) + positionMap[mapKey] = append(positionMap[mapKey], rpcCtx.tablet.Alias.Uid) } if len(positionMap) == 1 { @@ -180,10 +169,10 @@ func (wr *Wrangler) checkSlaveConsistency(tabletMap map[uint32]*topo.TabletInfo, } // Shut off all replication. -func (wr *Wrangler) stopSlaves(tabletMap map[topo.TabletAlias]*topo.TabletInfo) error { +func (wr *Wrangler) stopSlaves(ctx context.Context, tabletMap map[topo.TabletAlias]*topo.TabletInfo) error { errs := make(chan error, len(tabletMap)) f := func(ti *topo.TabletInfo) { - err := wr.tmc.StopSlave(context.TODO(), ti, wr.ActionTimeout()) + err := wr.tmc.StopSlave(ctx, ti) if err != nil { wr.logger.Infof("StopSlave failed: %v", err) } @@ -208,7 +197,7 @@ func (wr *Wrangler) stopSlaves(tabletMap map[topo.TabletAlias]*topo.TabletInfo) // tabletReplicationStatuses returns the ReplicationStatus of each tablet in // tablets. It handles masters and slaves, but it's up to the caller to // guarantee all tablets are in the same shard. -func (wr *Wrangler) tabletReplicationStatuses(tablets []*topo.TabletInfo) ([]*myproto.ReplicationStatus, error) { +func (wr *Wrangler) tabletReplicationStatuses(ctx context.Context, tablets []*topo.TabletInfo) ([]*myproto.ReplicationStatus, error) { wr.logger.Infof("tabletReplicationStatuses: %v", tablets) calls := make([]*rpcContext, len(tablets)) wg := sync.WaitGroup{} @@ -216,16 +205,16 @@ func (wr *Wrangler) tabletReplicationStatuses(tablets []*topo.TabletInfo) ([]*my f := func(idx int) { defer wg.Done() ti := tablets[idx] - ctx := &rpcContext{tablet: ti} - calls[idx] = ctx + rpcCtx := &rpcContext{tablet: ti} + calls[idx] = rpcCtx if ti.Type == topo.TYPE_MASTER { - pos, err := wr.tmc.MasterPosition(context.TODO(), ti, wr.ActionTimeout()) - ctx.err = err + pos, err := wr.tmc.MasterPosition(ctx, ti) + rpcCtx.err = err if err == nil { - ctx.status = &myproto.ReplicationStatus{Position: pos} + rpcCtx.status = &myproto.ReplicationStatus{Position: pos} } } else if ti.IsSlaveType() { - ctx.status, ctx.err = wr.tmc.SlaveStatus(context.TODO(), ti, wr.ActionTimeout()) + rpcCtx.status, rpcCtx.err = wr.tmc.SlaveStatus(ctx, ti) } } @@ -243,15 +232,15 @@ func (wr *Wrangler) tabletReplicationStatuses(tablets []*topo.TabletInfo) ([]*my someErrors := false stats := make([]*myproto.ReplicationStatus, len(tablets)) - for i, ctx := range calls { - if ctx == nil { + for i, rpcCtx := range calls { + if rpcCtx == nil { continue } - if ctx.err != nil { - wr.logger.Warningf("could not get replication status for tablet %v %v", ctx.tablet.Alias, ctx.err) + if rpcCtx.err != nil { + wr.logger.Warningf("could not get replication status for tablet %v %v", rpcCtx.tablet.Alias, rpcCtx.err) someErrors = true } else { - stats[i] = ctx.status + stats[i] = rpcCtx.status } } if someErrors { @@ -260,26 +249,26 @@ func (wr *Wrangler) tabletReplicationStatuses(tablets []*topo.TabletInfo) ([]*my return stats, nil } -func (wr *Wrangler) demoteMaster(ti *topo.TabletInfo) (myproto.ReplicationPosition, error) { +func (wr *Wrangler) demoteMaster(ctx context.Context, ti *topo.TabletInfo) (myproto.ReplicationPosition, error) { wr.logger.Infof("demote master %v", ti.Alias) - if err := wr.tmc.DemoteMaster(context.TODO(), ti, wr.ActionTimeout()); err != nil { + if err := wr.tmc.DemoteMaster(ctx, ti); err != nil { return myproto.ReplicationPosition{}, err } - return wr.tmc.MasterPosition(context.TODO(), ti, wr.ActionTimeout()) + return wr.tmc.MasterPosition(ctx, ti) } -func (wr *Wrangler) promoteSlave(ti *topo.TabletInfo) (rsd *actionnode.RestartSlaveData, err error) { +func (wr *Wrangler) promoteSlave(ctx context.Context, ti *topo.TabletInfo) (rsd *actionnode.RestartSlaveData, err error) { wr.logger.Infof("promote slave %v", ti.Alias) - return wr.tmc.PromoteSlave(context.TODO(), ti, wr.ActionTimeout()) + return wr.tmc.PromoteSlave(ctx, ti) } -func (wr *Wrangler) restartSlaves(slaveTabletMap map[topo.TabletAlias]*topo.TabletInfo, rsd *actionnode.RestartSlaveData) (majorityRestart bool, err error) { +func (wr *Wrangler) restartSlaves(ctx context.Context, slaveTabletMap map[topo.TabletAlias]*topo.TabletInfo, rsd *actionnode.RestartSlaveData) (majorityRestart bool, err error) { wg := new(sync.WaitGroup) slaves := topotools.CopyMapValues(slaveTabletMap, []*topo.TabletInfo{}).([]*topo.TabletInfo) errs := make([]error, len(slaveTabletMap)) f := func(i int) { - errs[i] = wr.restartSlave(slaves[i], rsd) + errs[i] = wr.restartSlave(ctx, slaves[i], rsd) if errs[i] != nil { // FIXME(msolomon) Don't bail early, just mark this phase as // failed. We might decide to proceed if enough of these @@ -318,28 +307,28 @@ func (wr *Wrangler) restartSlaves(slaveTabletMap map[topo.TabletAlias]*topo.Tabl return } -func (wr *Wrangler) restartSlave(ti *topo.TabletInfo, rsd *actionnode.RestartSlaveData) (err error) { +func (wr *Wrangler) restartSlave(ctx context.Context, ti *topo.TabletInfo, rsd *actionnode.RestartSlaveData) (err error) { wr.logger.Infof("restart slave %v", ti.Alias) - return wr.tmc.RestartSlave(context.TODO(), ti, rsd, wr.ActionTimeout()) + return wr.tmc.RestartSlave(ctx, ti, rsd) } -func (wr *Wrangler) checkMasterElect(ti *topo.TabletInfo) error { +func (wr *Wrangler) checkMasterElect(ctx context.Context, ti *topo.TabletInfo) error { // Check the master-elect is fit for duty - call out for hardware checks. // if the server was already serving live traffic, it's probably good if ti.IsInServingGraph() { return nil } - return wr.ExecuteOptionalTabletInfoHook(ti, hook.NewSimpleHook("preflight_serving_type")) + return wr.ExecuteOptionalTabletInfoHook(ctx, ti, hook.NewSimpleHook("preflight_serving_type")) } -func (wr *Wrangler) finishReparent(si *topo.ShardInfo, masterElect *topo.TabletInfo, majorityRestart, leaveMasterReadOnly bool) error { +func (wr *Wrangler) finishReparent(ctx context.Context, si *topo.ShardInfo, masterElect *topo.TabletInfo, majorityRestart, leaveMasterReadOnly bool) error { // If the majority of slaves restarted, move ahead. if majorityRestart { if leaveMasterReadOnly { wr.logger.Warningf("leaving master-elect read-only, change with: vtctl SetReadWrite %v", masterElect.Alias) } else { wr.logger.Infof("marking master-elect read-write %v", masterElect.Alias) - if err := wr.tmc.SetReadWrite(context.TODO(), masterElect, wr.ActionTimeout()); err != nil { + if err := wr.tmc.SetReadWrite(ctx, masterElect); err != nil { wr.logger.Warningf("master master-elect read-write failed, leaving master-elect read-only, change with: vtctl SetReadWrite %v", masterElect.Alias) } } @@ -349,7 +338,7 @@ func (wr *Wrangler) finishReparent(si *topo.ShardInfo, masterElect *topo.TabletI // save the new master in the shard info si.MasterAlias = masterElect.Alias - if err := topo.UpdateShard(context.TODO(), wr.ts, si); err != nil { + if err := topo.UpdateShard(ctx, wr.ts, si); err != nil { wr.logger.Errorf("Failed to save new master into shard: %v", err) return err } @@ -357,15 +346,15 @@ func (wr *Wrangler) finishReparent(si *topo.ShardInfo, masterElect *topo.TabletI // We rebuild all the cells, as we may have taken tablets in and // out of the graph. wr.logger.Infof("rebuilding shard serving graph data") - _, err := topotools.RebuildShard(context.TODO(), wr.logger, wr.ts, masterElect.Keyspace, masterElect.Shard, nil, wr.lockTimeout, interrupted) + _, err := wr.RebuildShardGraph(ctx, masterElect.Keyspace, masterElect.Shard, nil) return err } -func (wr *Wrangler) breakReplication(slaveMap map[topo.TabletAlias]*topo.TabletInfo, masterElect *topo.TabletInfo) error { +func (wr *Wrangler) breakReplication(ctx context.Context, slaveMap map[topo.TabletAlias]*topo.TabletInfo, masterElect *topo.TabletInfo) error { // We are forcing a reparenting. Make sure that all slaves stop so // no data is accidentally replicated through before we call RestartSlave. wr.logger.Infof("stop slaves %v", masterElect.Alias) - err := wr.stopSlaves(slaveMap) + err := wr.stopSlaves(ctx, slaveMap) if err != nil { return err } @@ -373,7 +362,7 @@ func (wr *Wrangler) breakReplication(slaveMap map[topo.TabletAlias]*topo.TabletI // Force slaves to break, just in case they were not advertised in // the replication graph. wr.logger.Infof("break slaves %v", masterElect.Alias) - return wr.tmc.BreakSlaves(context.TODO(), masterElect, wr.ActionTimeout()) + return wr.tmc.BreakSlaves(ctx, masterElect) } func (wr *Wrangler) restartableTabletMap(slaves map[topo.TabletAlias]*topo.TabletInfo) map[uint32]*topo.TabletInfo { diff --git a/go/vt/wrangler/reparent_brutal.go b/go/vt/wrangler/reparent_brutal.go index ebc1d245bff..73832069fe2 100644 --- a/go/vt/wrangler/reparent_brutal.go +++ b/go/vt/wrangler/reparent_brutal.go @@ -2,12 +2,14 @@ package wrangler import ( "fmt" + "time" "github.com/youtube/vitess/go/event" myproto "github.com/youtube/vitess/go/vt/mysqlctl/proto" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" "github.com/youtube/vitess/go/vt/topotools/events" + "golang.org/x/net/context" ) // reparentShardBrutal executes a brutal reparent. @@ -18,7 +20,7 @@ import ( // // The ev parameter is an event struct prefilled with information that the // caller has on hand, which would be expensive for us to re-query. -func (wr *Wrangler) reparentShardBrutal(ev *events.Reparent, si *topo.ShardInfo, slaveTabletMap, masterTabletMap map[topo.TabletAlias]*topo.TabletInfo, masterElectTablet *topo.TabletInfo, leaveMasterReadOnly, force bool) (err error) { +func (wr *Wrangler) reparentShardBrutal(ctx context.Context, ev *events.Reparent, si *topo.ShardInfo, slaveTabletMap, masterTabletMap map[topo.TabletAlias]*topo.TabletInfo, masterElectTablet *topo.TabletInfo, leaveMasterReadOnly, force bool, waitSlaveTimeout time.Duration) (err error) { event.DispatchUpdate(ev, "starting brutal") defer func() { @@ -38,34 +40,34 @@ func (wr *Wrangler) reparentShardBrutal(ev *events.Reparent, si *topo.ShardInfo, if !force { // Make sure all tablets have the right parent and reasonable positions. event.DispatchUpdate(ev, "checking slave replication positions") - if err := wr.checkSlaveReplication(slaveTabletMap, topo.NO_TABLET); err != nil { + if err := wr.checkSlaveReplication(ctx, slaveTabletMap, topo.NO_TABLET, waitSlaveTimeout); err != nil { return err } // Check the master-elect is fit for duty - call out for hardware checks. event.DispatchUpdate(ev, "checking that new master is ready to serve") - if err := wr.checkMasterElect(masterElectTablet); err != nil { + if err := wr.checkMasterElect(ctx, masterElectTablet); err != nil { return err } event.DispatchUpdate(ev, "checking slave consistency") wr.logger.Infof("check slaves %v/%v", masterElectTablet.Keyspace, masterElectTablet.Shard) restartableSlaveTabletMap := wr.restartableTabletMap(slaveTabletMap) - err = wr.checkSlaveConsistency(restartableSlaveTabletMap, myproto.ReplicationPosition{}) + err = wr.checkSlaveConsistency(ctx, restartableSlaveTabletMap, myproto.ReplicationPosition{}, waitSlaveTimeout) if err != nil { return err } } else { event.DispatchUpdate(ev, "stopping slave replication") wr.logger.Infof("forcing reparent to same master %v", masterElectTablet.Alias) - err := wr.breakReplication(slaveTabletMap, masterElectTablet) + err := wr.breakReplication(ctx, slaveTabletMap, masterElectTablet) if err != nil { return err } } event.DispatchUpdate(ev, "promoting new master") - rsd, err := wr.promoteSlave(masterElectTablet) + rsd, err := wr.promoteSlave(ctx, masterElectTablet) if err != nil { // FIXME(msolomon) This suggests that the master-elect is dead. // We need to classify certain errors as temporary and retry. @@ -77,7 +79,7 @@ func (wr *Wrangler) reparentShardBrutal(ev *events.Reparent, si *topo.ShardInfo, delete(masterTabletMap, masterElectTablet.Alias) event.DispatchUpdate(ev, "restarting slaves") - majorityRestart, restartSlaveErr := wr.restartSlaves(slaveTabletMap, rsd) + majorityRestart, restartSlaveErr := wr.restartSlaves(ctx, slaveTabletMap, rsd) if !force { for _, failedMaster := range masterTabletMap { @@ -85,14 +87,14 @@ func (wr *Wrangler) reparentShardBrutal(ev *events.Reparent, si *topo.ShardInfo, wr.logger.Infof("scrap dead master %v", failedMaster.Alias) // The master is dead so execute the action locally instead of // enqueing the scrap action for an arbitrary amount of time. - if scrapErr := topotools.Scrap(wr.ts, failedMaster.Alias, false); scrapErr != nil { + if scrapErr := topotools.Scrap(ctx, wr.ts, failedMaster.Alias, false); scrapErr != nil { wr.logger.Warningf("scrapping failed master failed: %v", scrapErr) } } } event.DispatchUpdate(ev, "rebuilding shard serving graph") - err = wr.finishReparent(si, masterElectTablet, majorityRestart, leaveMasterReadOnly) + err = wr.finishReparent(ctx, si, masterElectTablet, majorityRestart, leaveMasterReadOnly) if err != nil { return err } diff --git a/go/vt/wrangler/reparent_external.go b/go/vt/wrangler/reparent_external.go deleted file mode 100644 index 948f843e757..00000000000 --- a/go/vt/wrangler/reparent_external.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2013, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package wrangler - -import ( - "fmt" - - "code.google.com/p/go.net/context" - "github.com/youtube/vitess/go/event" - "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" - "github.com/youtube/vitess/go/vt/topo" - "github.com/youtube/vitess/go/vt/topotools" - "github.com/youtube/vitess/go/vt/topotools/events" -) - -// ShardExternallyReparented updates the topology after the master -// tablet in a keyspace/shard has changed. We trust that whoever made -// the change completed the change successfully. We lock the shard -// while doing the update. We will then rebuild the serving graph in -// the cells that need it (the old master cell and the new master cell) -func (wr *Wrangler) ShardExternallyReparented(keyspace, shard string, masterElectTabletAlias topo.TabletAlias) error { - // grab the shard lock - actionNode := actionnode.ShardExternallyReparented(masterElectTabletAlias) - lockPath, err := wr.lockShard(keyspace, shard, actionNode) - if err != nil { - return err - } - - // do the work - err = wr.shardExternallyReparentedLocked(keyspace, shard, masterElectTabletAlias) - - // release the lock in any case - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) -} - -func (wr *Wrangler) shardExternallyReparentedLocked(keyspace, shard string, masterElectTabletAlias topo.TabletAlias) (err error) { - // read the shard, make sure the master is not already good. - shardInfo, err := wr.ts.GetShard(keyspace, shard) - if err != nil { - return err - } - if shardInfo.MasterAlias == masterElectTabletAlias { - return fmt.Errorf("master-elect tablet %v is already master", masterElectTabletAlias) - } - - // Read the tablets, make sure the master elect is known to us. - // Note we will keep going with a partial tablet map, which usually - // happens when a cell is not reachable. After these checks, the - // guarantees we'll have are: - // - global cell is reachable (we just locked and read the shard) - // - the local cell that contains the new master is reachable - // (as we're going to check the new master is in the list) - // That should be enough. - tabletMap, err := topo.GetTabletMapForShard(wr.ts, keyspace, shard) - switch err { - case nil: - // keep going - case topo.ErrPartialResult: - wr.logger.Warningf("Got topo.ErrPartialResult from GetTabletMapForShard, may need to re-init some tablets") - default: - return err - } - masterElectTablet, ok := tabletMap[masterElectTabletAlias] - if !ok { - return fmt.Errorf("master-elect tablet %v not found in replication graph %v/%v %v", masterElectTabletAlias, keyspace, shard, topotools.MapKeys(tabletMap)) - } - - // Create reusable Reparent event with available info - ev := &events.Reparent{ - ShardInfo: *shardInfo, - NewMaster: *masterElectTablet.Tablet, - } - - if oldMasterTablet, ok := tabletMap[shardInfo.MasterAlias]; ok { - ev.OldMaster = *oldMasterTablet.Tablet - } - - defer func() { - if err != nil { - event.DispatchUpdate(ev, "failed: "+err.Error()) - } - }() - - // sort the tablets, and handle them - slaveTabletMap, masterTabletMap := topotools.SortedTabletMap(tabletMap) - err = wr.reparentShardExternal(ev, slaveTabletMap, masterTabletMap, masterElectTablet) - if err != nil { - wr.logger.Infof("Skipping shard rebuild with failed reparent") - return err - } - - // Compute the list of Cells we need to rebuild: old master and - // all other cells if reparenting to another cell. - cells := []string{shardInfo.MasterAlias.Cell} - if shardInfo.MasterAlias.Cell != masterElectTabletAlias.Cell { - cells = nil - } - - // now update the master record in the shard object - event.DispatchUpdate(ev, "updating shard record") - wr.logger.Infof("Updating Shard's MasterAlias record") - shardInfo.MasterAlias = masterElectTabletAlias - if err = topo.UpdateShard(context.TODO(), wr.ts, shardInfo); err != nil { - return err - } - - // and rebuild the shard serving graph - event.DispatchUpdate(ev, "rebuilding shard serving graph") - wr.logger.Infof("Rebuilding shard serving graph data") - if _, err = topotools.RebuildShard(context.TODO(), wr.logger, wr.ts, masterElectTablet.Keyspace, masterElectTablet.Shard, cells, wr.lockTimeout, interrupted); err != nil { - return err - } - - event.DispatchUpdate(ev, "finished") - return nil -} - -// reparentShardExternal handles an external reparent. -// -// The ev parameter is an event struct prefilled with information that the -// caller has on hand, which would be expensive for us to re-query. -func (wr *Wrangler) reparentShardExternal(ev *events.Reparent, slaveTabletMap, masterTabletMap map[topo.TabletAlias]*topo.TabletInfo, masterElectTablet *topo.TabletInfo) error { - event.DispatchUpdate(ev, "starting external") - - // we fix the new master in the replication graph - event.DispatchUpdate(ev, "checking if new master was promoted") - err := wr.slaveWasPromoted(masterElectTablet) - if err != nil { - // This suggests that the master-elect is dead. This is bad. - return fmt.Errorf("slaveWasPromoted(%v) failed: %v", masterElectTablet, err) - } - - // Once the slave is promoted, remove it from our maps - delete(slaveTabletMap, masterElectTablet.Alias) - delete(masterTabletMap, masterElectTablet.Alias) - - // Then fix all the slaves, including the old master. This - // last step is very likely to time out for some tablets (one - // random guy is dead, the old master is dead, ...). We - // execute them all in parallel until we get to - // wr.ActionTimeout(). After this, no other action with a - // timeout is executed, so even if we got to the timeout, - // we're still good. - event.DispatchUpdate(ev, "restarting slaves") - ctx := context.TODO() - topotools.RestartSlavesExternal(wr.ts, wr.logger, slaveTabletMap, masterTabletMap, masterElectTablet.Alias, func(ti *topo.TabletInfo, swra *actionnode.SlaveWasRestartedArgs) error { - wr.logger.Infof("slaveWasRestarted(%v)", ti.Alias) - return wr.tmc.SlaveWasRestarted(ctx, ti, swra, wr.ActionTimeout()) - }) - return nil -} - -func (wr *Wrangler) slaveWasPromoted(ti *topo.TabletInfo) error { - wr.logger.Infof("slaveWasPromoted(%v)", ti.Alias) - return wr.tmc.SlaveWasPromoted(context.TODO(), ti, wr.ActionTimeout()) -} diff --git a/go/vt/wrangler/reparent_graceful.go b/go/vt/wrangler/reparent_graceful.go index 880f1c8380b..a3eda491109 100644 --- a/go/vt/wrangler/reparent_graceful.go +++ b/go/vt/wrangler/reparent_graceful.go @@ -7,19 +7,19 @@ package wrangler import ( "fmt" "strings" - - "code.google.com/p/go.net/context" + "time" "github.com/youtube/vitess/go/event" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" "github.com/youtube/vitess/go/vt/topotools/events" + "golang.org/x/net/context" ) // reparentShardGraceful executes a graceful reparent. // The ev parameter is an event struct prefilled with information that the // caller has on hand, which would be expensive for us to re-query. -func (wr *Wrangler) reparentShardGraceful(ev *events.Reparent, si *topo.ShardInfo, slaveTabletMap, masterTabletMap map[topo.TabletAlias]*topo.TabletInfo, masterElectTablet *topo.TabletInfo, leaveMasterReadOnly bool) (err error) { +func (wr *Wrangler) reparentShardGraceful(ctx context.Context, ev *events.Reparent, si *topo.ShardInfo, slaveTabletMap, masterTabletMap map[topo.TabletAlias]*topo.TabletInfo, masterElectTablet *topo.TabletInfo, leaveMasterReadOnly bool, waitSlaveTimeout time.Duration) (err error) { event.DispatchUpdate(ev, "starting graceful") defer func() { @@ -41,10 +41,6 @@ func (wr *Wrangler) reparentShardGraceful(ev *events.Reparent, si *topo.ShardInf masterTablet = v } - if masterTablet.Parent.Uid != topo.NO_TABLET { - return fmt.Errorf("master tablet should not have a ParentUid: %v %v", masterTablet.Parent.Uid, masterTablet.Alias) - } - if masterTablet.Type != topo.TYPE_MASTER { return fmt.Errorf("master tablet should not be type: %v %v", masterTablet.Type, masterTablet.Alias) } @@ -57,26 +53,26 @@ func (wr *Wrangler) reparentShardGraceful(ev *events.Reparent, si *topo.ShardInf return fmt.Errorf("master elect tablet not in replication graph %v %v/%v %v", masterElectTablet.Alias, masterTablet.Keyspace, masterTablet.Shard, topotools.MapKeys(slaveTabletMap)) } - if err := wr.ValidateShard(masterTablet.Keyspace, masterTablet.Shard, true); err != nil { + if err := wr.ValidateShard(ctx, masterTablet.Keyspace, masterTablet.Shard, true); err != nil { return fmt.Errorf("ValidateShard verification failed: %v, if the master is dead, run: vtctl ScrapTablet -force %v", err, masterTablet.Alias) } // Make sure all tablets have the right parent and reasonable positions. event.DispatchUpdate(ev, "checking slave replication positions") - err = wr.checkSlaveReplication(slaveTabletMap, masterTablet.Alias.Uid) + err = wr.checkSlaveReplication(ctx, slaveTabletMap, masterTablet.Alias.Uid, waitSlaveTimeout) if err != nil { return err } // Check the master-elect is fit for duty - call out for hardware checks. event.DispatchUpdate(ev, "checking that new master is ready to serve") - err = wr.checkMasterElect(masterElectTablet) + err = wr.checkMasterElect(ctx, masterElectTablet) if err != nil { return err } event.DispatchUpdate(ev, "demoting old master") - masterPosition, err := wr.demoteMaster(masterTablet) + masterPosition, err := wr.demoteMaster(ctx, masterTablet) if err != nil { // FIXME(msolomon) This suggests that the master is dead and we // need to take steps. We could either pop a prompt, or make @@ -87,13 +83,13 @@ func (wr *Wrangler) reparentShardGraceful(ev *events.Reparent, si *topo.ShardInf event.DispatchUpdate(ev, "checking slave consistency") wr.logger.Infof("check slaves %v/%v", masterTablet.Keyspace, masterTablet.Shard) restartableSlaveTabletMap := wr.restartableTabletMap(slaveTabletMap) - err = wr.checkSlaveConsistency(restartableSlaveTabletMap, masterPosition) + err = wr.checkSlaveConsistency(ctx, restartableSlaveTabletMap, masterPosition, waitSlaveTimeout) if err != nil { return fmt.Errorf("check slave consistency failed %v, demoted master is still read only, run: vtctl SetReadWrite %v", err, masterTablet.Alias) } event.DispatchUpdate(ev, "promoting new master") - rsd, err := wr.promoteSlave(masterElectTablet) + rsd, err := wr.promoteSlave(ctx, masterElectTablet) if err != nil { // FIXME(msolomon) This suggests that the master-elect is dead. // We need to classify certain errors as temporary and retry. @@ -104,7 +100,7 @@ func (wr *Wrangler) reparentShardGraceful(ev *events.Reparent, si *topo.ShardInf delete(slaveTabletMap, masterElectTablet.Alias) event.DispatchUpdate(ev, "restarting slaves") - majorityRestart, restartSlaveErr := wr.restartSlaves(slaveTabletMap, rsd) + majorityRestart, restartSlaveErr := wr.restartSlaves(ctx, slaveTabletMap, rsd) // For now, scrap the old master regardless of how many // slaves restarted. @@ -113,13 +109,13 @@ func (wr *Wrangler) reparentShardGraceful(ev *events.Reparent, si *topo.ShardInf // it as new replica. event.DispatchUpdate(ev, "scrapping old master") wr.logger.Infof("scrap demoted master %v", masterTablet.Alias) - if scrapErr := wr.tmc.Scrap(context.TODO(), masterTablet, wr.ActionTimeout()); scrapErr != nil { + if scrapErr := wr.tmc.Scrap(ctx, masterTablet); scrapErr != nil { // The sub action is non-critical, so just warn. wr.logger.Warningf("scrap demoted master failed: %v", scrapErr) } event.DispatchUpdate(ev, "rebuilding shard serving graph") - err = wr.finishReparent(si, masterElectTablet, majorityRestart, leaveMasterReadOnly) + err = wr.finishReparent(ctx, si, masterElectTablet, majorityRestart, leaveMasterReadOnly) if err != nil { return err } diff --git a/go/vt/wrangler/schema.go b/go/vt/wrangler/schema.go index 74efd398034..e609e23f738 100644 --- a/go/vt/wrangler/schema.go +++ b/go/vt/wrangler/schema.go @@ -5,12 +5,15 @@ package wrangler import ( + "bytes" "fmt" + "html/template" "sort" "strings" "sync" + "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/concurrency" @@ -22,30 +25,30 @@ import ( ) // GetSchema uses an RPC to get the schema from a remote tablet -func (wr *Wrangler) GetSchema(tabletAlias topo.TabletAlias, tables, excludeTables []string, includeViews bool) (*myproto.SchemaDefinition, error) { +func (wr *Wrangler) GetSchema(ctx context.Context, tabletAlias topo.TabletAlias, tables, excludeTables []string, includeViews bool) (*myproto.SchemaDefinition, error) { ti, err := wr.ts.GetTablet(tabletAlias) if err != nil { return nil, err } - return wr.tmc.GetSchema(context.TODO(), ti, tables, excludeTables, includeViews, wr.ActionTimeout()) + return wr.tmc.GetSchema(ctx, ti, tables, excludeTables, includeViews) } // ReloadSchema forces the remote tablet to reload its schema. -func (wr *Wrangler) ReloadSchema(tabletAlias topo.TabletAlias) error { +func (wr *Wrangler) ReloadSchema(ctx context.Context, tabletAlias topo.TabletAlias) error { ti, err := wr.ts.GetTablet(tabletAlias) if err != nil { return err } - return wr.tmc.ReloadSchema(context.TODO(), ti, wr.ActionTimeout()) + return wr.tmc.ReloadSchema(ctx, ti) } // helper method to asynchronously diff a schema -func (wr *Wrangler) diffSchema(masterSchema *myproto.SchemaDefinition, masterTabletAlias, alias topo.TabletAlias, excludeTables []string, includeViews bool, wg *sync.WaitGroup, er concurrency.ErrorRecorder) { +func (wr *Wrangler) diffSchema(ctx context.Context, masterSchema *myproto.SchemaDefinition, masterTabletAlias, alias topo.TabletAlias, excludeTables []string, includeViews bool, wg *sync.WaitGroup, er concurrency.ErrorRecorder) { defer wg.Done() log.Infof("Gathering schema for %v", alias) - slaveSchema, err := wr.GetSchema(alias, nil, excludeTables, includeViews) + slaveSchema, err := wr.GetSchema(ctx, alias, nil, excludeTables, includeViews) if err != nil { er.RecordError(err) return @@ -56,7 +59,7 @@ func (wr *Wrangler) diffSchema(masterSchema *myproto.SchemaDefinition, masterTab } // ValidateSchemaShard will diff the schema from all the tablets in the shard. -func (wr *Wrangler) ValidateSchemaShard(keyspace, shard string, excludeTables []string, includeViews bool) error { +func (wr *Wrangler) ValidateSchemaShard(ctx context.Context, keyspace, shard string, excludeTables []string, includeViews bool) error { si, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err @@ -67,14 +70,14 @@ func (wr *Wrangler) ValidateSchemaShard(keyspace, shard string, excludeTables [] return fmt.Errorf("No master in shard %v/%v", keyspace, shard) } log.Infof("Gathering schema for master %v", si.MasterAlias) - masterSchema, err := wr.GetSchema(si.MasterAlias, nil, excludeTables, includeViews) + masterSchema, err := wr.GetSchema(ctx, si.MasterAlias, nil, excludeTables, includeViews) if err != nil { return err } // read all the aliases in the shard, that is all tablets that are // replicating from the master - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shard) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shard) if err != nil { return err } @@ -88,7 +91,7 @@ func (wr *Wrangler) ValidateSchemaShard(keyspace, shard string, excludeTables [] } wg.Add(1) - go wr.diffSchema(masterSchema, si.MasterAlias, alias, excludeTables, includeViews, &wg, &er) + go wr.diffSchema(ctx, masterSchema, si.MasterAlias, alias, excludeTables, includeViews, &wg, &er) } wg.Wait() if er.HasErrors() { @@ -97,9 +100,9 @@ func (wr *Wrangler) ValidateSchemaShard(keyspace, shard string, excludeTables [] return nil } -// ValidateSchemaShard will diff the schema from all the tablets in +// ValidateSchemaKeyspace will diff the schema from all the tablets in // the keyspace. -func (wr *Wrangler) ValidateSchemaKeyspace(keyspace string, excludeTables []string, includeViews bool) error { +func (wr *Wrangler) ValidateSchemaKeyspace(ctx context.Context, keyspace string, excludeTables []string, includeViews bool) error { // find all the shards shards, err := wr.ts.GetShardNames(keyspace) if err != nil { @@ -112,7 +115,7 @@ func (wr *Wrangler) ValidateSchemaKeyspace(keyspace string, excludeTables []stri } sort.Strings(shards) if len(shards) == 1 { - return wr.ValidateSchemaShard(keyspace, shards[0], excludeTables, includeViews) + return wr.ValidateSchemaShard(ctx, keyspace, shards[0], excludeTables, includeViews) } // find the reference schema using the first shard's master @@ -125,7 +128,7 @@ func (wr *Wrangler) ValidateSchemaKeyspace(keyspace string, excludeTables []stri } referenceAlias := si.MasterAlias log.Infof("Gathering schema for reference master %v", referenceAlias) - referenceSchema, err := wr.GetSchema(referenceAlias, nil, excludeTables, includeViews) + referenceSchema, err := wr.GetSchema(ctx, referenceAlias, nil, excludeTables, includeViews) if err != nil { return err } @@ -135,7 +138,7 @@ func (wr *Wrangler) ValidateSchemaKeyspace(keyspace string, excludeTables []stri wg := sync.WaitGroup{} // first diff the slaves in the reference shard 0 - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shards[0]) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shards[0]) if err != nil { return err } @@ -146,7 +149,7 @@ func (wr *Wrangler) ValidateSchemaKeyspace(keyspace string, excludeTables []stri } wg.Add(1) - go wr.diffSchema(referenceSchema, referenceAlias, alias, excludeTables, includeViews, &wg, &er) + go wr.diffSchema(ctx, referenceSchema, referenceAlias, alias, excludeTables, includeViews, &wg, &er) } // then diffs all tablets in the other shards @@ -162,7 +165,7 @@ func (wr *Wrangler) ValidateSchemaKeyspace(keyspace string, excludeTables []stri continue } - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shard) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shard) if err != nil { er.RecordError(err) continue @@ -170,7 +173,7 @@ func (wr *Wrangler) ValidateSchemaKeyspace(keyspace string, excludeTables []stri for _, alias := range aliases { wg.Add(1) - go wr.diffSchema(referenceSchema, referenceAlias, alias, excludeTables, includeViews, &wg, &er) + go wr.diffSchema(ctx, referenceSchema, referenceAlias, alias, excludeTables, includeViews, &wg, &er) } } wg.Wait() @@ -181,29 +184,30 @@ func (wr *Wrangler) ValidateSchemaKeyspace(keyspace string, excludeTables []stri } // PreflightSchema will try a schema change on the remote tablet. -func (wr *Wrangler) PreflightSchema(tabletAlias topo.TabletAlias, change string) (*myproto.SchemaChangeResult, error) { +func (wr *Wrangler) PreflightSchema(ctx context.Context, tabletAlias topo.TabletAlias, change string) (*myproto.SchemaChangeResult, error) { ti, err := wr.ts.GetTablet(tabletAlias) if err != nil { return nil, err } - return wr.tmc.PreflightSchema(context.TODO(), ti, change, wr.ActionTimeout()) + return wr.tmc.PreflightSchema(ctx, ti, change) } // ApplySchema will apply a schema change on the remote tablet. -func (wr *Wrangler) ApplySchema(tabletAlias topo.TabletAlias, sc *myproto.SchemaChange) (*myproto.SchemaChangeResult, error) { +func (wr *Wrangler) ApplySchema(ctx context.Context, tabletAlias topo.TabletAlias, sc *myproto.SchemaChange) (*myproto.SchemaChangeResult, error) { ti, err := wr.ts.GetTablet(tabletAlias) if err != nil { return nil, err } - return wr.tmc.ApplySchema(context.TODO(), ti, sc, wr.ActionTimeout()) + return wr.tmc.ApplySchema(ctx, ti, sc) } +// ApplySchemaShard applies a shcema change on a shard. // Note for 'complex' mode (the 'simple' mode is easy enough that we // don't need to handle recovery that much): this method is able to // recover if interrupted in the middle, because it knows which server // has the schema change already applied, and will just pass through them // very quickly. -func (wr *Wrangler) ApplySchemaShard(keyspace, shard, change string, newParentTabletAlias topo.TabletAlias, simple, force bool) (*myproto.SchemaChangeResult, error) { +func (wr *Wrangler) ApplySchemaShard(ctx context.Context, keyspace, shard, change string, newParentTabletAlias topo.TabletAlias, simple, force bool, waitSlaveTimeout time.Duration) (*myproto.SchemaChangeResult, error) { // read the shard shardInfo, err := wr.ts.GetShard(keyspace, shard) if err != nil { @@ -219,43 +223,43 @@ func (wr *Wrangler) ApplySchemaShard(keyspace, shard, change string, newParentTa if err != nil { return nil, err } - preflight, err := wr.PreflightSchema(shardInfo.MasterAlias, change) + preflight, err := wr.PreflightSchema(ctx, shardInfo.MasterAlias, change) if err != nil { return nil, err } - return wr.lockAndApplySchemaShard(shardInfo, preflight, keyspace, shard, shardInfo.MasterAlias, change, newParentTabletAlias, simple, force) + return wr.lockAndApplySchemaShard(ctx, shardInfo, preflight, keyspace, shard, shardInfo.MasterAlias, change, newParentTabletAlias, simple, force, waitSlaveTimeout) } -func (wr *Wrangler) lockAndApplySchemaShard(shardInfo *topo.ShardInfo, preflight *myproto.SchemaChangeResult, keyspace, shard string, masterTabletAlias topo.TabletAlias, change string, newParentTabletAlias topo.TabletAlias, simple, force bool) (*myproto.SchemaChangeResult, error) { +func (wr *Wrangler) lockAndApplySchemaShard(ctx context.Context, shardInfo *topo.ShardInfo, preflight *myproto.SchemaChangeResult, keyspace, shard string, masterTabletAlias topo.TabletAlias, change string, newParentTabletAlias topo.TabletAlias, simple, force bool, waitSlaveTimeout time.Duration) (*myproto.SchemaChangeResult, error) { // get a shard lock actionNode := actionnode.ApplySchemaShard(masterTabletAlias, change, simple) - lockPath, err := wr.lockShard(keyspace, shard, actionNode) + lockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { return nil, err } - scr, err := wr.applySchemaShard(shardInfo, preflight, masterTabletAlias, change, newParentTabletAlias, simple, force) - return scr, wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + scr, err := wr.applySchemaShard(ctx, shardInfo, preflight, masterTabletAlias, change, newParentTabletAlias, simple, force, waitSlaveTimeout) + return scr, wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } -// local structure used to keep track of what we're doing -type TabletStatus struct { +// tabletStatus is a local structure used to keep track of what we're doing +type tabletStatus struct { ti *topo.TabletInfo lastError error beforeSchema *myproto.SchemaDefinition } -func (wr *Wrangler) applySchemaShard(shardInfo *topo.ShardInfo, preflight *myproto.SchemaChangeResult, masterTabletAlias topo.TabletAlias, change string, newParentTabletAlias topo.TabletAlias, simple, force bool) (*myproto.SchemaChangeResult, error) { +func (wr *Wrangler) applySchemaShard(ctx context.Context, shardInfo *topo.ShardInfo, preflight *myproto.SchemaChangeResult, masterTabletAlias topo.TabletAlias, change string, newParentTabletAlias topo.TabletAlias, simple, force bool, waitSlaveTimeout time.Duration) (*myproto.SchemaChangeResult, error) { // find all the shards we need to handle - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, shardInfo.Keyspace(), shardInfo.ShardName()) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, shardInfo.Keyspace(), shardInfo.ShardName()) if err != nil { return nil, err } - // build the array of TabletStatus we're going to use - statusArray := make([]*TabletStatus, 0, len(aliases)-1) + // build the array of tabletStatus we're going to use + statusArray := make([]*tabletStatus, 0, len(aliases)-1) for _, alias := range aliases { if alias == masterTabletAlias { // we skip the master @@ -278,7 +282,7 @@ func (wr *Wrangler) applySchemaShard(shardInfo *topo.ShardInfo, preflight *mypro continue } - statusArray = append(statusArray, &TabletStatus{ti: ti}) + statusArray = append(statusArray, &tabletStatus{ti: ti}) } // get schema on all tablets. @@ -286,8 +290,8 @@ func (wr *Wrangler) applySchemaShard(shardInfo *topo.ShardInfo, preflight *mypro wg := &sync.WaitGroup{} for _, status := range statusArray { wg.Add(1) - go func(status *TabletStatus) { - status.beforeSchema, status.lastError = wr.tmc.GetSchema(context.TODO(), status.ti, nil, nil, false, wr.ActionTimeout()) + go func(status *tabletStatus) { + status.beforeSchema, status.lastError = wr.tmc.GetSchema(ctx, status.ti, nil, nil, false) wg.Done() }(status) } @@ -302,13 +306,13 @@ func (wr *Wrangler) applySchemaShard(shardInfo *topo.ShardInfo, preflight *mypro // simple or complex? if simple { - return wr.applySchemaShardSimple(statusArray, preflight, masterTabletAlias, change, force) + return wr.applySchemaShardSimple(ctx, statusArray, preflight, masterTabletAlias, change, force) } - return wr.applySchemaShardComplex(statusArray, shardInfo, preflight, masterTabletAlias, change, newParentTabletAlias, force) + return wr.applySchemaShardComplex(ctx, statusArray, shardInfo, preflight, masterTabletAlias, change, newParentTabletAlias, force, waitSlaveTimeout) } -func (wr *Wrangler) applySchemaShardSimple(statusArray []*TabletStatus, preflight *myproto.SchemaChangeResult, masterTabletAlias topo.TabletAlias, change string, force bool) (*myproto.SchemaChangeResult, error) { +func (wr *Wrangler) applySchemaShardSimple(ctx context.Context, statusArray []*tabletStatus, preflight *myproto.SchemaChangeResult, masterTabletAlias topo.TabletAlias, change string, force bool) (*myproto.SchemaChangeResult, error) { // check all tablets have the same schema as the master's // BeforeSchema. If not, we shouldn't proceed log.Infof("Checking schema on all tablets") @@ -326,10 +330,10 @@ func (wr *Wrangler) applySchemaShardSimple(statusArray []*TabletStatus, prefligh // we're good, just send to the master log.Infof("Applying schema change to master in simple mode") sc := &myproto.SchemaChange{Sql: change, Force: force, AllowReplication: true, BeforeSchema: preflight.BeforeSchema, AfterSchema: preflight.AfterSchema} - return wr.ApplySchema(masterTabletAlias, sc) + return wr.ApplySchema(ctx, masterTabletAlias, sc) } -func (wr *Wrangler) applySchemaShardComplex(statusArray []*TabletStatus, shardInfo *topo.ShardInfo, preflight *myproto.SchemaChangeResult, masterTabletAlias topo.TabletAlias, change string, newParentTabletAlias topo.TabletAlias, force bool) (*myproto.SchemaChangeResult, error) { +func (wr *Wrangler) applySchemaShardComplex(ctx context.Context, statusArray []*tabletStatus, shardInfo *topo.ShardInfo, preflight *myproto.SchemaChangeResult, masterTabletAlias topo.TabletAlias, change string, newParentTabletAlias topo.TabletAlias, force bool, waitSlaveTimeout time.Duration) (*myproto.SchemaChangeResult, error) { // apply the schema change to all replica / slave tablets for _, status := range statusArray { // if already applied, we skip this guy @@ -357,7 +361,7 @@ func (wr *Wrangler) applySchemaShardComplex(statusArray []*TabletStatus, shardIn typeChangeRequired := ti.Tablet.IsInServingGraph() if typeChangeRequired { // note we want to update the serving graph there - err = wr.changeTypeInternal(ti.Alias, topo.TYPE_SCHEMA_UPGRADE) + err = wr.changeTypeInternal(ctx, ti.Alias, topo.TYPE_SCHEMA_UPGRADE) if err != nil { return nil, err } @@ -366,14 +370,14 @@ func (wr *Wrangler) applySchemaShardComplex(statusArray []*TabletStatus, shardIn // apply the schema change log.Infof("Applying schema change to slave %v in complex mode", status.ti.Alias) sc := &myproto.SchemaChange{Sql: change, Force: force, AllowReplication: false, BeforeSchema: preflight.BeforeSchema, AfterSchema: preflight.AfterSchema} - _, err = wr.ApplySchema(status.ti.Alias, sc) + _, err = wr.ApplySchema(ctx, status.ti.Alias, sc) if err != nil { return nil, err } // put this guy back into the serving graph if typeChangeRequired { - err = wr.changeTypeInternal(ti.Alias, ti.Tablet.Type) + err = wr.changeTypeInternal(ctx, ti.Alias, ti.Tablet.Type) if err != nil { return nil, err } @@ -383,7 +387,7 @@ func (wr *Wrangler) applySchemaShardComplex(statusArray []*TabletStatus, shardIn // if newParentTabletAlias is passed in, use that as the new master if !newParentTabletAlias.IsZero() { log.Infof("Reparenting with new master set to %v", newParentTabletAlias) - tabletMap, err := topo.GetTabletMapForShard(wr.ts, shardInfo.Keyspace(), shardInfo.ShardName()) + tabletMap, err := topo.GetTabletMapForShard(ctx, wr.ts, shardInfo.Keyspace(), shardInfo.ShardName()) if err != nil { return nil, err } @@ -404,7 +408,7 @@ func (wr *Wrangler) applySchemaShardComplex(statusArray []*TabletStatus, shardIn ev.OldMaster = *oldMasterTablet.Tablet } - err = wr.reparentShardGraceful(ev, shardInfo, slaveTabletMap, masterTabletMap, newMasterTablet /*leaveMasterReadOnly*/, false) + err = wr.reparentShardGraceful(ctx, ev, shardInfo, slaveTabletMap, masterTabletMap, newMasterTablet /*leaveMasterReadOnly*/, false, waitSlaveTimeout) if err != nil { return nil, err } @@ -420,24 +424,24 @@ func (wr *Wrangler) applySchemaShardComplex(statusArray []*TabletStatus, shardIn return &myproto.SchemaChangeResult{BeforeSchema: preflight.BeforeSchema, AfterSchema: preflight.AfterSchema}, nil } -// apply a schema change to an entire keyspace. +// ApplySchemaKeyspace applies a schema change to an entire keyspace. // take a keyspace lock to do this. // first we will validate the Preflight works the same on all shard masters // and fail if not (unless force is specified) // if simple, we just do it on all masters. // if complex, we do the shell game in parallel on all shards -func (wr *Wrangler) ApplySchemaKeyspace(keyspace string, change string, simple, force bool) (*myproto.SchemaChangeResult, error) { +func (wr *Wrangler) ApplySchemaKeyspace(ctx context.Context, keyspace string, change string, simple, force bool, waitSlaveTimeout time.Duration) (*myproto.SchemaChangeResult, error) { actionNode := actionnode.ApplySchemaKeyspace(change, simple) - lockPath, err := wr.lockKeyspace(keyspace, actionNode) + lockPath, err := wr.lockKeyspace(ctx, keyspace, actionNode) if err != nil { return nil, err } - scr, err := wr.applySchemaKeyspace(keyspace, change, simple, force) - return scr, wr.unlockKeyspace(keyspace, actionNode, lockPath, err) + scr, err := wr.applySchemaKeyspace(ctx, keyspace, change, simple, force, waitSlaveTimeout) + return scr, wr.unlockKeyspace(ctx, keyspace, actionNode, lockPath, err) } -func (wr *Wrangler) applySchemaKeyspace(keyspace string, change string, simple, force bool) (*myproto.SchemaChangeResult, error) { +func (wr *Wrangler) applySchemaKeyspace(ctx context.Context, keyspace string, change string, simple, force bool, waitSlaveTimeout time.Duration) (*myproto.SchemaChangeResult, error) { shards, err := wr.ts.GetShardNames(keyspace) if err != nil { return nil, err @@ -449,7 +453,7 @@ func (wr *Wrangler) applySchemaKeyspace(keyspace string, change string, simple, } if len(shards) == 1 { log.Infof("Only one shard in keyspace %v, using ApplySchemaShard", keyspace) - return wr.ApplySchemaShard(keyspace, shards[0], change, topo.TabletAlias{}, simple, force) + return wr.ApplySchemaShard(ctx, keyspace, shards[0], change, topo.TabletAlias{}, simple, force, waitSlaveTimeout) } // Get schema on all shard masters in parallel @@ -477,7 +481,7 @@ func (wr *Wrangler) applySchemaKeyspace(keyspace string, change string, simple, return } - beforeSchemas[i], err = wr.GetSchema(shardInfos[i].MasterAlias, nil, nil, false) + beforeSchemas[i], err = wr.GetSchema(ctx, shardInfos[i].MasterAlias, nil, nil, false) }(i, shard) } wg.Wait() @@ -505,7 +509,7 @@ func (wr *Wrangler) applySchemaKeyspace(keyspace string, change string, simple, // this assumes shard 0 master doesn't have the schema upgrade applied // if it does, we'll have to fix the slaves and other shards manually. log.Infof("Running Preflight on Shard 0 Master") - preflight, err := wr.PreflightSchema(shardInfos[0].MasterAlias, change) + preflight, err := wr.PreflightSchema(ctx, shardInfos[0].MasterAlias, change) if err != nil { return nil, err } @@ -518,7 +522,7 @@ func (wr *Wrangler) applySchemaKeyspace(keyspace string, change string, simple, go func(i int, shard string) { defer wg.Done() - _, err := wr.lockAndApplySchemaShard(shardInfos[i], preflight, keyspace, shard, shardInfos[i].MasterAlias, change, topo.TabletAlias{}, simple, force) + _, err := wr.lockAndApplySchemaShard(ctx, shardInfos[i], preflight, keyspace, shard, shardInfos[i].MasterAlias, change, topo.TabletAlias{}, simple, force, waitSlaveTimeout) if err != nil { mu.Lock() applyErr = err @@ -534,3 +538,60 @@ func (wr *Wrangler) applySchemaKeyspace(keyspace string, change string, simple, return &myproto.SchemaChangeResult{BeforeSchema: preflight.BeforeSchema, AfterSchema: preflight.AfterSchema}, nil } + +// CopySchemaShard copies the schema from a source tablet to the +// specified shard. The schema is applied directly on the master of +// the destination shard, and is propogated to the replicas through +// binlogs. +func (wr *Wrangler) CopySchemaShard(ctx context.Context, srcTabletAlias topo.TabletAlias, tables, excludeTables []string, includeViews bool, keyspace, shard string) error { + sd, err := wr.GetSchema(ctx, srcTabletAlias, tables, excludeTables, includeViews) + if err != nil { + return err + } + shardInfo, err := wr.ts.GetShard(keyspace, shard) + if err != nil { + return err + } + tabletInfo, err := wr.ts.GetTablet(shardInfo.MasterAlias) + if err != nil { + return err + } + createSql := sd.ToSQLStrings() + + for _, sqlLine := range createSql { + err = wr.applySqlShard(ctx, tabletInfo, sqlLine) + if err != nil { + return err + } + } + return nil +} + +// applySqlShard applies a given SQL change on a given tablet alias. It allows executing arbitrary +// SQL statements, but doesn't return any results, so it's only useful for SQL statements +// that would be run for their effects (e.g., CREATE). +// It works by applying the SQL statement on the shard's master tablet with replication turned on. +// Thus it should be used only for changes that can be applies on a live instance without causing issues; +// it shouldn't be used for anything that will require a pivot. +// The SQL statement string is expected to have {{.DatabaseName}} in place of the actual db name. +func (wr *Wrangler) applySqlShard(ctx context.Context, tabletInfo *topo.TabletInfo, change string) error { + filledChange, err := fillStringTemplate(change, map[string]string{"DatabaseName": tabletInfo.DbName()}) + if err != nil { + return fmt.Errorf("fillStringTemplate failed: %v", err) + } + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + // Need to make sure that we enable binlog, since we're only applying the statement on masters. + _, err = wr.tmc.ExecuteFetch(ctx, tabletInfo, filledChange, 0, false, false) + return err +} + +// fillStringTemplate returns the string template filled +func fillStringTemplate(tmpl string, vars interface{}) (string, error) { + myTemplate := template.Must(template.New("").Parse(tmpl)) + data := new(bytes.Buffer) + if err := myTemplate.Execute(data, vars); err != nil { + return "", err + } + return data.String(), nil +} diff --git a/go/vt/wrangler/shard.go b/go/vt/wrangler/shard.go index 38daecead6c..3bcfb04fa34 100644 --- a/go/vt/wrangler/shard.go +++ b/go/vt/wrangler/shard.go @@ -7,26 +7,27 @@ package wrangler import ( "fmt" - "code.google.com/p/go.net/context" - log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/key" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // shard related methods for Wrangler -func (wr *Wrangler) lockShard(keyspace, shard string, actionNode *actionnode.ActionNode) (lockPath string, err error) { - return actionNode.LockShard(context.TODO(), wr.ts, keyspace, shard, wr.lockTimeout, interrupted) +func (wr *Wrangler) lockShard(ctx context.Context, keyspace, shard string, actionNode *actionnode.ActionNode) (lockPath string, err error) { + ctx, cancel := context.WithTimeout(ctx, wr.lockTimeout) + defer cancel() + return actionNode.LockShard(ctx, wr.ts, keyspace, shard) } -func (wr *Wrangler) unlockShard(keyspace, shard string, actionNode *actionnode.ActionNode, lockPath string, actionError error) error { - return actionNode.UnlockShard(wr.ts, keyspace, shard, lockPath, actionError) +func (wr *Wrangler) unlockShard(ctx context.Context, keyspace, shard string, actionNode *actionnode.ActionNode, lockPath string, actionError error) error { + return actionNode.UnlockShard(ctx, wr.ts, keyspace, shard, lockPath, actionError) } // updateShardCellsAndMaster will update the 'Cells' and possibly // MasterAlias records for the shard, if needed. -func (wr *Wrangler) updateShardCellsAndMaster(si *topo.ShardInfo, tabletAlias topo.TabletAlias, tabletType topo.TabletType, force bool) error { +func (wr *Wrangler) updateShardCellsAndMaster(ctx context.Context, si *topo.ShardInfo, tabletAlias topo.TabletAlias, tabletType topo.TabletType, force bool) error { // See if we need to update the Shard: // - add the tablet's cell to the shard's Cells if needed // - change the master if needed @@ -44,7 +45,7 @@ func (wr *Wrangler) updateShardCellsAndMaster(si *topo.ShardInfo, tabletAlias to actionNode := actionnode.UpdateShard() keyspace := si.Keyspace() shard := si.ShardName() - lockPath, err := wr.lockShard(keyspace, shard, actionNode) + lockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { return err } @@ -52,7 +53,7 @@ func (wr *Wrangler) updateShardCellsAndMaster(si *topo.ShardInfo, tabletAlias to // re-read the shard with the lock si, err = wr.ts.GetShard(keyspace, shard) if err != nil { - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } // update it @@ -63,7 +64,7 @@ func (wr *Wrangler) updateShardCellsAndMaster(si *topo.ShardInfo, tabletAlias to } if tabletType == topo.TYPE_MASTER && si.MasterAlias != tabletAlias { if !si.MasterAlias.IsZero() && !force { - return wr.unlockShard(keyspace, shard, actionNode, lockPath, fmt.Errorf("creating this tablet would override old master %v in shard %v/%v", si.MasterAlias, keyspace, shard)) + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, fmt.Errorf("creating this tablet would override old master %v in shard %v/%v", si.MasterAlias, keyspace, shard)) } si.MasterAlias = tabletAlias wasUpdated = true @@ -71,30 +72,30 @@ func (wr *Wrangler) updateShardCellsAndMaster(si *topo.ShardInfo, tabletAlias to if wasUpdated { // write it back - if err := topo.UpdateShard(context.TODO(), wr.ts, si); err != nil { - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + if err := topo.UpdateShard(ctx, wr.ts, si); err != nil { + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } } // and unlock - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } // SetShardServedTypes changes the ServedTypes parameter of a shard. // It does not rebuild any serving graph or do any consistency check (yet). -func (wr *Wrangler) SetShardServedTypes(keyspace, shard string, cells []string, servedType topo.TabletType, remove bool) error { +func (wr *Wrangler) SetShardServedTypes(ctx context.Context, keyspace, shard string, cells []string, servedType topo.TabletType, remove bool) error { actionNode := actionnode.SetShardServedTypes(cells, servedType) - lockPath, err := wr.lockShard(keyspace, shard, actionNode) + lockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { return err } - err = wr.setShardServedTypes(keyspace, shard, cells, servedType, remove) - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + err = wr.setShardServedTypes(ctx, keyspace, shard, cells, servedType, remove) + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } -func (wr *Wrangler) setShardServedTypes(keyspace, shard string, cells []string, servedType topo.TabletType, remove bool) error { +func (wr *Wrangler) setShardServedTypes(ctx context.Context, keyspace, shard string, cells []string, servedType topo.TabletType, remove bool) error { si, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err @@ -103,7 +104,7 @@ func (wr *Wrangler) setShardServedTypes(keyspace, shard string, cells []string, if err := si.UpdateServedTypesMap(servedType, cells, remove); err != nil { return err } - return topo.UpdateShard(context.TODO(), wr.ts, si) + return topo.UpdateShard(ctx, wr.ts, si) } // SetShardTabletControl changes the TabletControl records @@ -112,23 +113,23 @@ func (wr *Wrangler) setShardServedTypes(keyspace, shard string, cells []string, // - if disableQueryService is set, tables has to be empty // - if disableQueryService is not set, and tables is empty, we remove // the TabletControl record for the cells -func (wr *Wrangler) SetShardTabletControl(keyspace, shard string, tabletType topo.TabletType, cells []string, remove, disableQueryService bool, tables []string) error { +func (wr *Wrangler) SetShardTabletControl(ctx context.Context, keyspace, shard string, tabletType topo.TabletType, cells []string, remove, disableQueryService bool, tables []string) error { if disableQueryService && len(tables) > 0 { return fmt.Errorf("SetShardTabletControl cannot have both DisableQueryService set and tables set") } actionNode := actionnode.UpdateShard() - lockPath, err := wr.lockShard(keyspace, shard, actionNode) + lockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { return err } - err = wr.setShardTabletControl(keyspace, shard, tabletType, cells, remove, disableQueryService, tables) - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + err = wr.setShardTabletControl(ctx, keyspace, shard, tabletType, cells, remove, disableQueryService, tables) + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } -func (wr *Wrangler) setShardTabletControl(keyspace, shard string, tabletType topo.TabletType, cells []string, remove, disableQueryService bool, tables []string) error { +func (wr *Wrangler) setShardTabletControl(ctx context.Context, keyspace, shard string, tabletType topo.TabletType, cells []string, remove, disableQueryService bool, tables []string) error { shardInfo, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err @@ -145,19 +146,19 @@ func (wr *Wrangler) setShardTabletControl(keyspace, shard string, tabletType top return fmt.Errorf("UpdateSourceBlacklistedTables(%v/%v) failed: %v", shardInfo.Keyspace(), shardInfo.ShardName(), err) } } - return topo.UpdateShard(context.TODO(), wr.ts, shardInfo) + return topo.UpdateShard(ctx, wr.ts, shardInfo) } // DeleteShard will do all the necessary changes in the topology server // to entirely remove a shard. It can only work if there are no tablets // in that shard. -func (wr *Wrangler) DeleteShard(keyspace, shard string) error { +func (wr *Wrangler) DeleteShard(ctx context.Context, keyspace, shard string) error { shardInfo, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err } - tabletMap, err := topo.GetTabletMapForShard(wr.ts, keyspace, shard) + tabletMap, err := topo.GetTabletMapForShard(ctx, wr.ts, keyspace, shard) if err != nil { return err } @@ -168,7 +169,7 @@ func (wr *Wrangler) DeleteShard(keyspace, shard string) error { // remove the replication graph and serving graph in each cell for _, cell := range shardInfo.Cells { if err := wr.ts.DeleteShardReplication(cell, keyspace, shard); err != nil { - log.Warningf("Cannot delete ShardReplication in cell %v for %v/%v: %v", cell, keyspace, shard, err) + wr.Logger().Warningf("Cannot delete ShardReplication in cell %v for %v/%v: %v", cell, keyspace, shard, err) } for _, t := range topo.AllTabletTypes { @@ -177,12 +178,12 @@ func (wr *Wrangler) DeleteShard(keyspace, shard string) error { } if err := wr.ts.DeleteEndPoints(cell, keyspace, shard, t); err != nil && err != topo.ErrNoNode { - log.Warningf("Cannot delete EndPoints in cell %v for %v/%v/%v: %v", cell, keyspace, shard, t, err) + wr.Logger().Warningf("Cannot delete EndPoints in cell %v for %v/%v/%v: %v", cell, keyspace, shard, t, err) } } if err := wr.ts.DeleteSrvShard(cell, keyspace, shard); err != nil && err != topo.ErrNoNode { - log.Warningf("Cannot delete SrvShard in cell %v for %v/%v: %v", cell, keyspace, shard, err) + wr.Logger().Warningf("Cannot delete SrvShard in cell %v for %v/%v: %v", cell, keyspace, shard, err) } } @@ -194,18 +195,18 @@ func (wr *Wrangler) DeleteShard(keyspace, shard string) error { // specified, it will remove the cell even when the tablet map cannot // be retrieved. This is intended to be used when a cell is completely // down and its topology server cannot even be reached. -func (wr *Wrangler) RemoveShardCell(keyspace, shard, cell string, force bool) error { +func (wr *Wrangler) RemoveShardCell(ctx context.Context, keyspace, shard, cell string, force bool) error { actionNode := actionnode.UpdateShard() - lockPath, err := wr.lockShard(keyspace, shard, actionNode) + lockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { return err } - err = wr.removeShardCell(keyspace, shard, cell, force) - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + err = wr.removeShardCell(ctx, keyspace, shard, cell, force) + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } -func (wr *Wrangler) removeShardCell(keyspace, shard, cell string, force bool) error { +func (wr *Wrangler) removeShardCell(ctx context.Context, keyspace, shard, cell string, force bool) error { shardInfo, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err @@ -243,11 +244,11 @@ func (wr *Wrangler) removeShardCell(keyspace, shard, cell string, force bool) er if !force { return err } - log.Warningf("Cannot get ShardReplication from cell %v, assuming cell topo server is down, and forcing the removal", cell) + wr.Logger().Warningf("Cannot get ShardReplication from cell %v, assuming cell topo server is down, and forcing the removal", cell) } // now we can update the shard - log.Infof("Removing cell %v from shard %v/%v", cell, keyspace, shard) + wr.Logger().Infof("Removing cell %v from shard %v/%v", cell, keyspace, shard) newCells := make([]string, 0, len(shardInfo.Cells)-1) for _, c := range shardInfo.Cells { if c != cell { @@ -256,21 +257,22 @@ func (wr *Wrangler) removeShardCell(keyspace, shard, cell string, force bool) er } shardInfo.Cells = newCells - return topo.UpdateShard(context.TODO(), wr.ts, shardInfo) + return topo.UpdateShard(ctx, wr.ts, shardInfo) } -func (wr *Wrangler) SourceShardDelete(keyspace, shard string, uid uint32) error { +// SourceShardDelete will delete a SourceShard inside a shard, by index. +func (wr *Wrangler) SourceShardDelete(ctx context.Context, keyspace, shard string, uid uint32) error { actionNode := actionnode.UpdateShard() - lockPath, err := wr.lockShard(keyspace, shard, actionNode) + lockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { return err } - err = wr.sourceShardDelete(keyspace, shard, uid) - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + err = wr.sourceShardDelete(ctx, keyspace, shard, uid) + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } -func (wr *Wrangler) sourceShardDelete(keyspace, shard string, uid uint32) error { +func (wr *Wrangler) sourceShardDelete(ctx context.Context, keyspace, shard string, uid uint32) error { si, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err @@ -288,21 +290,22 @@ func (wr *Wrangler) sourceShardDelete(keyspace, shard string, uid uint32) error newSourceShards = nil } si.SourceShards = newSourceShards - return topo.UpdateShard(context.TODO(), wr.ts, si) + return topo.UpdateShard(ctx, wr.ts, si) } -func (wr *Wrangler) SourceShardAdd(keyspace, shard string, uid uint32, skeyspace, sshard string, keyRange key.KeyRange, tables []string) error { +// SourceShardAdd will add a new SourceShard inside a shard +func (wr *Wrangler) SourceShardAdd(ctx context.Context, keyspace, shard string, uid uint32, skeyspace, sshard string, keyRange key.KeyRange, tables []string) error { actionNode := actionnode.UpdateShard() - lockPath, err := wr.lockShard(keyspace, shard, actionNode) + lockPath, err := wr.lockShard(ctx, keyspace, shard, actionNode) if err != nil { return err } - err = wr.sourceShardAdd(keyspace, shard, uid, skeyspace, sshard, keyRange, tables) - return wr.unlockShard(keyspace, shard, actionNode, lockPath, err) + err = wr.sourceShardAdd(ctx, keyspace, shard, uid, skeyspace, sshard, keyRange, tables) + return wr.unlockShard(ctx, keyspace, shard, actionNode, lockPath, err) } -func (wr *Wrangler) sourceShardAdd(keyspace, shard string, uid uint32, skeyspace, sshard string, keyRange key.KeyRange, tables []string) error { +func (wr *Wrangler) sourceShardAdd(ctx context.Context, keyspace, shard string, uid uint32, skeyspace, sshard string, keyRange key.KeyRange, tables []string) error { si, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err @@ -322,5 +325,5 @@ func (wr *Wrangler) sourceShardAdd(keyspace, shard string, uid uint32, skeyspace KeyRange: keyRange, Tables: tables, }) - return topo.UpdateShard(context.TODO(), wr.ts, si) + return topo.UpdateShard(ctx, wr.ts, si) } diff --git a/go/vt/wrangler/split.go b/go/vt/wrangler/split.go index 9046b68863c..8775407e02c 100644 --- a/go/vt/wrangler/split.go +++ b/go/vt/wrangler/split.go @@ -6,210 +6,14 @@ package wrangler import ( "fmt" - "sync" - "code.google.com/p/go.net/context" - - log "github.com/golang/glog" - "github.com/youtube/vitess/go/event" - cc "github.com/youtube/vitess/go/vt/concurrency" - "github.com/youtube/vitess/go/vt/key" - "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" - "github.com/youtube/vitess/go/vt/topotools/events" + "golang.org/x/net/context" ) -// replaceError replaces original with recent if recent is not nil, -// logging original if it wasn't nil. This should be used in deferred -// cleanup functions if they change the returned error. -func replaceError(original, recent error) error { - if recent == nil { - return original - } - if original != nil { - log.Errorf("One of multiple error: %v", original) - } - return recent -} - -// prepareToSnapshot changes the type of the tablet to backup (when -// the original type is master, it will proceed only if -// forceMasterSnapshot is true). It returns a function that will -// restore the original state. -func (wr *Wrangler) prepareToSnapshot(ti *topo.TabletInfo, forceMasterSnapshot bool) (restoreAfterSnapshot func() error, err error) { - originalType := ti.Tablet.Type - - if ti.Tablet.Type == topo.TYPE_MASTER && forceMasterSnapshot { - // In this case, we don't bother recomputing the serving graph. - // All queries will have to fail anyway. - log.Infof("force change type master -> backup: %v", ti.Alias) - // There is a legitimate reason to force in the case of a single - // master. - ti.Tablet.Type = topo.TYPE_BACKUP - err = topo.UpdateTablet(context.TODO(), wr.ts, ti) - } else { - err = wr.ChangeType(ti.Alias, topo.TYPE_BACKUP, false) - } - - if err != nil { - return - } - - restoreAfterSnapshot = func() (err error) { - log.Infof("change type after snapshot: %v %v", ti.Alias, originalType) - - if ti.Tablet.Parent.Uid == topo.NO_TABLET && forceMasterSnapshot { - log.Infof("force change type backup -> master: %v", ti.Alias) - ti.Tablet.Type = topo.TYPE_MASTER - return topo.UpdateTablet(context.TODO(), wr.ts, ti) - } - - return wr.ChangeType(ti.Alias, originalType, false) - } - - return -} - -func (wr *Wrangler) MultiRestore(dstTabletAlias topo.TabletAlias, sources []topo.TabletAlias, concurrency, fetchConcurrency, insertTableConcurrency, fetchRetryCount int, strategy string) (err error) { - var ti *topo.TabletInfo - ti, err = wr.ts.GetTablet(dstTabletAlias) - if err != nil { - return - } - - args := &actionnode.MultiRestoreArgs{SrcTabletAliases: sources, Concurrency: concurrency, FetchConcurrency: fetchConcurrency, InsertTableConcurrency: insertTableConcurrency, FetchRetryCount: fetchRetryCount, Strategy: strategy} - ev := &events.MultiRestore{ - Tablet: *ti.Tablet, - Args: *args, - } - event.DispatchUpdate(ev, "starting") - defer func() { - if err != nil { - event.DispatchUpdate(ev, "failed: "+err.Error()) - } - }() - - logStream, errFunc, err := wr.tmc.MultiRestore(context.TODO(), ti, args, wr.ActionTimeout()) - if err != nil { - return err - } - for e := range logStream { - wr.Logger().Infof("MultiRestore(%v): %v", dstTabletAlias, e) - } - if err := errFunc(); err != nil { - return err - } - - event.DispatchUpdate(ev, "finished") - return nil -} - -func (wr *Wrangler) MultiSnapshot(keyRanges []key.KeyRange, tabletAlias topo.TabletAlias, concurrency int, tables, excludeTables []string, forceMasterSnapshot, skipSlaveRestart bool, maximumFilesize uint64) (manifests []string, parent topo.TabletAlias, err error) { - var ti *topo.TabletInfo - ti, err = wr.ts.GetTablet(tabletAlias) - if err != nil { - return - } - - args := &actionnode.MultiSnapshotArgs{KeyRanges: keyRanges, Concurrency: concurrency, Tables: tables, ExcludeTables: excludeTables, SkipSlaveRestart: skipSlaveRestart, MaximumFilesize: maximumFilesize} - ev := &events.MultiSnapshot{ - Tablet: *ti.Tablet, - Args: *args, - } - if len(tables) > 0 { - event.DispatchUpdate(ev, "starting table") - } else { - event.DispatchUpdate(ev, "starting keyrange") - } - defer func() { - if err != nil { - event.DispatchUpdate(ev, "failed: "+err.Error()) - } - }() - - restoreAfterSnapshot, err := wr.prepareToSnapshot(ti, forceMasterSnapshot) - if err != nil { - return - } - defer func() { - err = replaceError(err, restoreAfterSnapshot()) - }() - - // execute the remote action, log the results, save the error - logStream, errFunc, err := wr.tmc.MultiSnapshot(context.TODO(), ti, args, wr.ActionTimeout()) - if err != nil { - return nil, topo.TabletAlias{}, err - } - for e := range logStream { - wr.Logger().Infof("MultiSnapshot(%v): %v", tabletAlias, e) - } - var reply *actionnode.MultiSnapshotReply - if reply, err = errFunc(); err != nil { - return - } - - event.DispatchUpdate(ev, "finished") - return reply.ManifestPaths, reply.ParentAlias, nil -} - -func (wr *Wrangler) ShardMultiRestore(keyspace, shard string, sources []topo.TabletAlias, tables []string, concurrency, fetchConcurrency, insertTableConcurrency, fetchRetryCount int, strategy string) error { - - // check parameters - if len(tables) > 0 && len(sources) > 1 { - return fmt.Errorf("ShardMultiRestore can only handle one source when tables are specified") - } - - // lock the shard to perform the changes we need done - actionNode := actionnode.ShardMultiRestore(&actionnode.MultiRestoreArgs{ - SrcTabletAliases: sources, - Concurrency: concurrency, - FetchConcurrency: fetchConcurrency, - InsertTableConcurrency: insertTableConcurrency, - FetchRetryCount: fetchRetryCount, - Strategy: strategy}) - lockPath, err := wr.lockShard(keyspace, shard, actionNode) - if err != nil { - return err - } - - mrErr := wr.SetSourceShards(keyspace, shard, sources, tables) - err = wr.unlockShard(keyspace, shard, actionNode, lockPath, mrErr) - if err != nil { - if mrErr != nil { - log.Errorf("unlockShard got error back: %v", err) - return mrErr - } - return err - } - if mrErr != nil { - return mrErr - } - - // find all tablets in the shard - destTablets, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shard) - if err != nil { - return err - } - - // now launch MultiRestore on all tablets we need to do - rec := cc.AllErrorRecorder{} - wg := sync.WaitGroup{} - for _, tabletAlias := range destTablets { - wg.Add(1) - go func(tabletAlias topo.TabletAlias) { - log.Infof("Starting multirestore on tablet %v", tabletAlias) - err := wr.MultiRestore(tabletAlias, sources, concurrency, fetchConcurrency, insertTableConcurrency, fetchRetryCount, strategy) - log.Infof("Multirestore on tablet %v is done (err=%v)", tabletAlias, err) - rec.RecordError(err) - wg.Done() - }(tabletAlias) - } - wg.Wait() - - return rec.Error() -} - -func (wr *Wrangler) SetSourceShards(keyspace, shard string, sources []topo.TabletAlias, tables []string) error { +// SetSourceShards is a utility function to override the SourceShards fields +// on a Shard. +func (wr *Wrangler) SetSourceShards(ctx context.Context, keyspace, shard string, sources []topo.TabletAlias, tables []string) error { // read the shard shardInfo, err := wr.ts.GetShard(keyspace, shard) if err != nil { @@ -223,7 +27,7 @@ func (wr *Wrangler) SetSourceShards(keyspace, shard string, sources []topo.Table } // read the source tablets - sourceTablets, err := topo.GetTabletMap(wr.TopoServer(), sources) + sourceTablets, err := topo.GetTabletMap(ctx, wr.TopoServer(), sources) if err != nil { return err } @@ -245,7 +49,7 @@ func (wr *Wrangler) SetSourceShards(keyspace, shard string, sources []topo.Table } // and write the shard - if err = topo.UpdateShard(context.TODO(), wr.ts, shardInfo); err != nil { + if err = topo.UpdateShard(ctx, wr.ts, shardInfo); err != nil { return err } diff --git a/go/vt/wrangler/tablet.go b/go/vt/wrangler/tablet.go index 3651d45d928..4d1b159f203 100644 --- a/go/vt/wrangler/tablet.go +++ b/go/vt/wrangler/tablet.go @@ -7,12 +7,11 @@ package wrangler import ( "fmt" - "code.google.com/p/go.net/context" - log "github.com/golang/glog" mproto "github.com/youtube/vitess/go/mysql/proto" "github.com/youtube/vitess/go/vt/tabletmanager/actionnode" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" + "golang.org/x/net/context" ) // Tablet related methods for wrangler @@ -24,7 +23,7 @@ import ( // update is true, and a tablet with the same ID exists, update it. // If Force is true, and a tablet with the same ID already exists, it // will be scrapped and deleted, and then recreated. -func (wr *Wrangler) InitTablet(tablet *topo.Tablet, force, createShardAndKeyspace, update bool) error { +func (wr *Wrangler) InitTablet(ctx context.Context, tablet *topo.Tablet, force, createShardAndKeyspace, update bool) error { if err := tablet.Complete(); err != nil { return err } @@ -53,16 +52,8 @@ func (wr *Wrangler) InitTablet(tablet *topo.Tablet, force, createShardAndKeyspac return fmt.Errorf("creating this tablet would override old master %v in shard %v/%v", si.MasterAlias, tablet.Keyspace, tablet.Shard) } - // see if we specified a parent, otherwise get it from the shard - if tablet.Parent.IsZero() && tablet.Type.IsSlaveType() { - if si.MasterAlias.IsZero() { - return fmt.Errorf("trying to create tablet %v in shard %v/%v without a master", tablet.Alias, tablet.Keyspace, tablet.Shard) - } - tablet.Parent = si.MasterAlias - } - // update the shard record if needed - if err := wr.updateShardCellsAndMaster(si, tablet.Alias, tablet.Type, force); err != nil { + if err := wr.updateShardCellsAndMaster(ctx, si, tablet.Alias, tablet.Type, force); err != nil { return err } } @@ -73,20 +64,20 @@ func (wr *Wrangler) InitTablet(tablet *topo.Tablet, force, createShardAndKeyspac if update || force { oldTablet, err := wr.ts.GetTablet(tablet.Alias) if err != nil { - log.Warningf("failed reading tablet %v: %v", tablet.Alias, err) + wr.Logger().Warningf("failed reading tablet %v: %v", tablet.Alias, err) } else { if oldTablet.Keyspace == tablet.Keyspace && oldTablet.Shard == tablet.Shard { *(oldTablet.Tablet) = *tablet - if err := topo.UpdateTablet(context.TODO(), wr.ts, oldTablet); err != nil { - log.Warningf("failed updating tablet %v: %v", tablet.Alias, err) + if err := topo.UpdateTablet(ctx, wr.ts, oldTablet); err != nil { + wr.Logger().Warningf("failed updating tablet %v: %v", tablet.Alias, err) // now fall through the Scrap case } else { if !tablet.IsInReplicationGraph() { return nil } - if err := topo.UpdateTabletReplicationData(context.TODO(), wr.ts, tablet); err != nil { - log.Warningf("failed updating tablet replication data for %v: %v", tablet.Alias, err) + if err := topo.UpdateTabletReplicationData(ctx, wr.ts, tablet); err != nil { + wr.Logger().Warningf("failed updating tablet replication data for %v: %v", tablet.Alias, err) // now fall through the Scrap case } else { return nil @@ -96,13 +87,13 @@ func (wr *Wrangler) InitTablet(tablet *topo.Tablet, force, createShardAndKeyspac } } if force { - if err = wr.Scrap(tablet.Alias, force, false); err != nil { - log.Errorf("failed scrapping tablet %v: %v", tablet.Alias, err) + if err = wr.Scrap(ctx, tablet.Alias, force, false); err != nil { + wr.Logger().Errorf("failed scrapping tablet %v: %v", tablet.Alias, err) return err } if err := wr.ts.DeleteTablet(tablet.Alias); err != nil { // we ignore this - log.Errorf("failed deleting tablet %v: %v", tablet.Alias, err) + wr.Logger().Errorf("failed deleting tablet %v: %v", tablet.Alias, err) } return topo.CreateTablet(wr.ts, tablet) } @@ -115,7 +106,7 @@ func (wr *Wrangler) InitTablet(tablet *topo.Tablet, force, createShardAndKeyspac // // If we scrap the master for a shard, we will clear its record // from the Shard object (only if that was the right master) -func (wr *Wrangler) Scrap(tabletAlias topo.TabletAlias, force, skipRebuild bool) error { +func (wr *Wrangler) Scrap(ctx context.Context, tabletAlias topo.TabletAlias, force, skipRebuild bool) error { // load the tablet, see if we'll need to rebuild ti, err := wr.ts.GetTablet(tabletAlias) if err != nil { @@ -125,27 +116,27 @@ func (wr *Wrangler) Scrap(tabletAlias topo.TabletAlias, force, skipRebuild bool) wasMaster := ti.Type == topo.TYPE_MASTER if force { - err = topotools.Scrap(wr.ts, ti.Alias, force) + err = topotools.Scrap(ctx, wr.ts, ti.Alias, force) } else { - err = wr.tmc.Scrap(context.TODO(), ti, wr.ActionTimeout()) + err = wr.tmc.Scrap(ctx, ti) } if err != nil { return err } if !rebuildRequired { - log.Infof("Rebuild not required") + wr.Logger().Infof("Rebuild not required") return nil } if skipRebuild { - log.Warningf("Rebuild required, but skipping it") + wr.Logger().Warningf("Rebuild required, but skipping it") return nil } // update the Shard object if the master was scrapped if wasMaster { actionNode := actionnode.UpdateShard() - lockPath, err := wr.lockShard(ti.Keyspace, ti.Shard, actionNode) + lockPath, err := wr.lockShard(ctx, ti.Keyspace, ti.Shard, actionNode) if err != nil { return err } @@ -153,7 +144,7 @@ func (wr *Wrangler) Scrap(tabletAlias topo.TabletAlias, force, skipRebuild bool) // read the shard with the lock si, err := wr.ts.GetShard(ti.Keyspace, ti.Shard) if err != nil { - return wr.unlockShard(ti.Keyspace, ti.Shard, actionNode, lockPath, err) + return wr.unlockShard(ctx, ti.Keyspace, ti.Shard, actionNode, lockPath, err) } // update it if the right alias is there @@ -161,38 +152,38 @@ func (wr *Wrangler) Scrap(tabletAlias topo.TabletAlias, force, skipRebuild bool) si.MasterAlias = topo.TabletAlias{} // write it back - if err := topo.UpdateShard(context.TODO(), wr.ts, si); err != nil { - return wr.unlockShard(ti.Keyspace, ti.Shard, actionNode, lockPath, err) + if err := topo.UpdateShard(ctx, wr.ts, si); err != nil { + return wr.unlockShard(ctx, ti.Keyspace, ti.Shard, actionNode, lockPath, err) } } else { - log.Warningf("Scrapping master %v from shard %v/%v but master in Shard object was %v", tabletAlias, ti.Keyspace, ti.Shard, si.MasterAlias) + wr.Logger().Warningf("Scrapping master %v from shard %v/%v but master in Shard object was %v", tabletAlias, ti.Keyspace, ti.Shard, si.MasterAlias) } // and unlock - if err := wr.unlockShard(ti.Keyspace, ti.Shard, actionNode, lockPath, err); err != nil { + if err := wr.unlockShard(ctx, ti.Keyspace, ti.Shard, actionNode, lockPath, err); err != nil { return err } } // and rebuild the original shard / keyspace - _, err = wr.RebuildShardGraph(ti.Keyspace, ti.Shard, []string{ti.Alias.Cell}) + _, err = wr.RebuildShardGraph(ctx, ti.Keyspace, ti.Shard, []string{ti.Alias.Cell}) return err } -// Change the type of tablet and recompute all necessary derived paths in the +// ChangeType changes the type of tablet and recompute all necessary derived paths in the // serving graph. If force is true, it will bypass the RPC action // system and make the data change directly, and not run the remote // hooks. // // Note we don't update the master record in the Shard here, as we // can't ChangeType from and out of master anyway. -func (wr *Wrangler) ChangeType(tabletAlias topo.TabletAlias, tabletType topo.TabletType, force bool) error { - rebuildRequired, cell, keyspace, shard, err := wr.ChangeTypeNoRebuild(tabletAlias, tabletType, force) +func (wr *Wrangler) ChangeType(ctx context.Context, tabletAlias topo.TabletAlias, tabletType topo.TabletType, force bool) error { + rebuildRequired, cell, keyspace, shard, err := wr.ChangeTypeNoRebuild(ctx, tabletAlias, tabletType, force) if err != nil { return err } if rebuildRequired { - _, err = wr.RebuildShardGraph(keyspace, shard, []string{cell}) + _, err = wr.RebuildShardGraph(ctx, keyspace, shard, []string{cell}) return err } return nil @@ -206,7 +197,7 @@ func (wr *Wrangler) ChangeType(tabletAlias topo.TabletAlias, tabletType topo.Tab // // Note we don't update the master record in the Shard here, as we // can't ChangeType from and out of master anyway. -func (wr *Wrangler) ChangeTypeNoRebuild(tabletAlias topo.TabletAlias, tabletType topo.TabletType, force bool) (rebuildRequired bool, cell, keyspace, shard string, err error) { +func (wr *Wrangler) ChangeTypeNoRebuild(ctx context.Context, tabletAlias topo.TabletAlias, tabletType topo.TabletType, force bool) (rebuildRequired bool, cell, keyspace, shard string, err error) { // Load tablet to find keyspace and shard assignment. // Don't load after the ChangeType which might have unassigned // the tablet. @@ -216,11 +207,11 @@ func (wr *Wrangler) ChangeTypeNoRebuild(tabletAlias topo.TabletAlias, tabletType } if force { - if err := topotools.ChangeType(wr.ts, tabletAlias, tabletType, nil, false); err != nil { + if err := topotools.ChangeType(ctx, wr.ts, tabletAlias, tabletType, nil, false); err != nil { return false, "", "", "", err } } else { - if err := wr.tmc.ChangeType(context.TODO(), ti, tabletType, wr.ActionTimeout()); err != nil { + if err := wr.tmc.ChangeType(ctx, ti, tabletType); err != nil { return false, "", "", "", err } } @@ -241,7 +232,7 @@ func (wr *Wrangler) ChangeTypeNoRebuild(tabletAlias topo.TabletAlias, tabletType // same as ChangeType, but assume we already have the shard lock, // and do not have the option to force anything. -func (wr *Wrangler) changeTypeInternal(tabletAlias topo.TabletAlias, dbType topo.TabletType) error { +func (wr *Wrangler) changeTypeInternal(ctx context.Context, tabletAlias topo.TabletAlias, dbType topo.TabletType) error { ti, err := wr.ts.GetTablet(tabletAlias) if err != nil { return err @@ -249,13 +240,13 @@ func (wr *Wrangler) changeTypeInternal(tabletAlias topo.TabletAlias, dbType topo rebuildRequired := ti.Tablet.IsInServingGraph() // change the type - if err := wr.tmc.ChangeType(context.TODO(), ti, dbType, wr.ActionTimeout()); err != nil { + if err := wr.tmc.ChangeType(ctx, ti, dbType); err != nil { return err } // rebuild if necessary if rebuildRequired { - _, err = topotools.RebuildShard(context.TODO(), wr.logger, wr.ts, ti.Keyspace, ti.Shard, []string{ti.Alias.Cell}, wr.lockTimeout, interrupted) + _, err = wr.RebuildShardGraph(ctx, ti.Keyspace, ti.Shard, []string{ti.Alias.Cell}) if err != nil { return err } @@ -278,10 +269,10 @@ func (wr *Wrangler) DeleteTablet(tabletAlias topo.TabletAlias) error { } // ExecuteFetch will get data from a remote tablet -func (wr *Wrangler) ExecuteFetch(tabletAlias topo.TabletAlias, query string, maxRows int, wantFields, disableBinlogs bool) (*mproto.QueryResult, error) { +func (wr *Wrangler) ExecuteFetch(ctx context.Context, tabletAlias topo.TabletAlias, query string, maxRows int, wantFields, disableBinlogs bool) (*mproto.QueryResult, error) { ti, err := wr.ts.GetTablet(tabletAlias) if err != nil { return nil, err } - return wr.tmc.ExecuteFetch(context.TODO(), ti, query, maxRows, wantFields, disableBinlogs, wr.ActionTimeout()) + return wr.tmc.ExecuteFetch(ctx, ti, query, maxRows, wantFields, disableBinlogs) } diff --git a/go/vt/wrangler/testlib/copy_schema_shard_test.go b/go/vt/wrangler/testlib/copy_schema_shard_test.go new file mode 100644 index 00000000000..dc7310f8756 --- /dev/null +++ b/go/vt/wrangler/testlib/copy_schema_shard_test.go @@ -0,0 +1,170 @@ +// Copyright 2014, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testlib + +import ( + "fmt" + "sync/atomic" + "testing" + "time" + + mproto "github.com/youtube/vitess/go/mysql/proto" + "github.com/youtube/vitess/go/vt/dbconnpool" + "github.com/youtube/vitess/go/vt/logutil" + myproto "github.com/youtube/vitess/go/vt/mysqlctl/proto" + _ "github.com/youtube/vitess/go/vt/tabletmanager/gorpctmclient" + _ "github.com/youtube/vitess/go/vt/tabletserver/gorpctabletconn" + "github.com/youtube/vitess/go/vt/topo" + "github.com/youtube/vitess/go/vt/wrangler" + "github.com/youtube/vitess/go/vt/zktopo" + "golang.org/x/net/context" +) + +type ExpectedExecuteFetch struct { + Query string + MaxRows int + WantFields bool + QueryResult *mproto.QueryResult + Error error +} + +// FakePoolConnection implements dbconnpool.PoolConnection +type FakePoolConnection struct { + t *testing.T + Closed bool + + ExpectedExecuteFetch []ExpectedExecuteFetch + ExpectedExecuteFetchIndex int +} + +func NewFakePoolConnectionQuery(t *testing.T, query string) *FakePoolConnection { + return &FakePoolConnection{ + t: t, + ExpectedExecuteFetch: []ExpectedExecuteFetch{ + ExpectedExecuteFetch{ + Query: query, + QueryResult: &mproto.QueryResult{}, + }, + }, + } +} + +func (fpc *FakePoolConnection) ExecuteFetch(query string, maxrows int, wantfields bool) (*mproto.QueryResult, error) { + if fpc.ExpectedExecuteFetchIndex >= len(fpc.ExpectedExecuteFetch) { + fpc.t.Errorf("got unexpected out of bound fetch: %v >= %v", fpc.ExpectedExecuteFetchIndex, len(fpc.ExpectedExecuteFetch)) + return nil, fmt.Errorf("unexpected out of bound fetch") + } + expected := fpc.ExpectedExecuteFetch[fpc.ExpectedExecuteFetchIndex].Query + if query != expected { + fpc.t.Errorf("got unexpected query: %v != %v", query, expected) + return nil, fmt.Errorf("unexpected query") + } + fpc.t.Logf("ExecuteFetch: %v", query) + defer func() { + fpc.ExpectedExecuteFetchIndex++ + }() + return fpc.ExpectedExecuteFetch[fpc.ExpectedExecuteFetchIndex].QueryResult, nil +} + +func (fpc *FakePoolConnection) ExecuteStreamFetch(query string, callback func(*mproto.QueryResult) error, streamBufferSize int) error { + return nil +} + +func (fpc *FakePoolConnection) ID() int64 { + return 1 +} + +func (fpc *FakePoolConnection) Close() { + fpc.Closed = true +} + +func (fpc *FakePoolConnection) IsClosed() bool { + return fpc.Closed +} + +func (fpc *FakePoolConnection) Recycle() { +} + +func (fpc *FakePoolConnection) Reconnect() error { + return nil +} + +// on the destinations +func DestinationsFactory(t *testing.T) func() (dbconnpool.PoolConnection, error) { + var queryIndex int64 = -1 + + return func() (dbconnpool.PoolConnection, error) { + qi := atomic.AddInt64(&queryIndex, 1) + switch { + case qi == 0: + return NewFakePoolConnectionQuery(t, "CREATE DATABASE `vt_ks` /*!40100 DEFAULT CHARACTER SET utf8 */"), nil + case qi == 1: + return NewFakePoolConnectionQuery(t, "CREATE TABLE `vt_ks`.`resharding1` (\n"+ + " `id` bigint(20) NOT NULL AUTO_INCREMENT,\n"+ + " `msg` varchar(64) DEFAULT NULL,\n"+ + " `keyspace_id` bigint(20) unsigned NOT NULL,\n"+ + " PRIMARY KEY (`id`),\n"+ + " KEY `by_msg` (`msg`)\n"+ + ") ENGINE=InnoDB DEFAULT CHARSET=utf8"), nil + case qi == 2: + return NewFakePoolConnectionQuery(t, "CREATE TABLE `view1` (\n"+ + " `id` bigint(20) NOT NULL AUTO_INCREMENT,\n"+ + " `msg` varchar(64) DEFAULT NULL,\n"+ + " `keyspace_id` bigint(20) unsigned NOT NULL,\n"+ + " PRIMARY KEY (`id`),\n"+ + " KEY `by_msg` (`msg`)\n"+ + ") ENGINE=InnoDB DEFAULT CHARSET=utf8"), nil + } + + return nil, fmt.Errorf("Unexpected connection") + } +} + +func TestCopySchemaShard(t *testing.T) { + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) + wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Second) + + sourceMaster := NewFakeTablet(t, wr, "cell1", 0, + topo.TYPE_MASTER, TabletKeyspaceShard(t, "ks", "-80")) + sourceRdonly := NewFakeTablet(t, wr, "cell1", 1, + topo.TYPE_RDONLY, TabletKeyspaceShard(t, "ks", "-80"), + TabletParent(sourceMaster.Tablet.Alias)) + + destinationMaster := NewFakeTablet(t, wr, "cell1", 10, + topo.TYPE_MASTER, TabletKeyspaceShard(t, "ks", "-40")) + // one destination RdOnly, so we know that schema copies propogate from masters + destinationRdonly := NewFakeTablet(t, wr, "cell1", 11, + topo.TYPE_RDONLY, TabletKeyspaceShard(t, "ks", "-40"), + TabletParent(destinationMaster.Tablet.Alias)) + + for _, ft := range []*FakeTablet{sourceMaster, sourceRdonly, destinationMaster, destinationRdonly} { + ft.StartActionLoop(t, wr) + defer ft.StopActionLoop(t) + } + + sourceRdonly.FakeMysqlDaemon.Schema = &myproto.SchemaDefinition{ + DatabaseSchema: "CREATE DATABASE `{{.DatabaseName}}` /*!40100 DEFAULT CHARACTER SET utf8 */", + TableDefinitions: []*myproto.TableDefinition{ + &myproto.TableDefinition{ + Name: "table1", + Schema: "CREATE TABLE `resharding1` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `msg` varchar(64) DEFAULT NULL,\n `keyspace_id` bigint(20) unsigned NOT NULL,\n PRIMARY KEY (`id`),\n KEY `by_msg` (`msg`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8", + Type: myproto.TABLE_BASE_TABLE, + }, + &myproto.TableDefinition{ + Name: "view1", + Schema: "CREATE TABLE `view1` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `msg` varchar(64) DEFAULT NULL,\n `keyspace_id` bigint(20) unsigned NOT NULL,\n PRIMARY KEY (`id`),\n KEY `by_msg` (`msg`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8", + Type: myproto.TABLE_VIEW, + }, + }, + } + + destinationMaster.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t) + destinationRdonly.FakeMysqlDaemon.DbaConnectionFactory = DestinationsFactory(t) + + if err := wr.CopySchemaShard(context.Background(), sourceRdonly.Tablet.Alias, nil, nil, true, "ks", "-40"); err != nil { + t.Fatalf("CopySchemaShard failed: %v", err) + } + +} diff --git a/go/vt/wrangler/testlib/fake_tablet.go b/go/vt/wrangler/testlib/fake_tablet.go index a6de0a90b0e..5582cddcec3 100644 --- a/go/vt/wrangler/testlib/fake_tablet.go +++ b/go/vt/wrangler/testlib/fake_tablet.go @@ -4,7 +4,7 @@ /* Package testlib contains utility methods to include in unit tests to -deal with topology common tasks, liek fake tablets and action loops. +deal with topology common tasks, like fake tablets and action loops. */ package testlib @@ -21,6 +21,7 @@ import ( "github.com/youtube/vitess/go/vt/tabletmanager/gorpctmserver" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/wrangler" + "golang.org/x/net/context" ) // This file contains utility methods for unit tests. @@ -40,7 +41,7 @@ type FakeTablet struct { // the tablet, and closed / cleared when we stop it. Agent *tabletmanager.ActionAgent Listener net.Listener - RpcServer *rpcplus.Server + RPCServer *rpcplus.Server } // TabletOption is an interface for changing tablet parameters. @@ -51,7 +52,8 @@ type TabletOption func(tablet *topo.Tablet) // TabletParent is the tablet option to set the parent alias func TabletParent(parent topo.TabletAlias) TabletOption { return func(tablet *topo.Tablet) { - tablet.Parent = parent + // save the parent alias uid into the portmap as a hack + tablet.Portmap["parent_uid"] = int(parent.Uid) } } @@ -67,7 +69,7 @@ func TabletKeyspaceShard(t *testing.T, keyspace, shard string) TabletOption { } } -// CreateTestTablet creates the test tablet in the topology. 'uid' +// NewFakeTablet creates the test tablet in the topology. 'uid' // has to be between 0 and 99. All the tablet info will be derived // from that. Look at the implementation if you need values. // Use TabletOption implementations if you need to change values at creation. @@ -96,14 +98,16 @@ func NewFakeTablet(t *testing.T, wr *wrangler.Wrangler, cell string, uid uint32, for _, option := range options { option(tablet) } - if err := wr.InitTablet(tablet, false, true, false); err != nil { + puid, ok := tablet.Portmap["parent_uid"] + delete(tablet.Portmap, "parent_uid") + if err := wr.InitTablet(context.Background(), tablet, false, true, false); err != nil { t.Fatalf("cannot create tablet %v: %v", uid, err) } // create a FakeMysqlDaemon with the right information by default fakeMysqlDaemon := &mysqlctl.FakeMysqlDaemon{} - if !tablet.Parent.IsZero() { - fakeMysqlDaemon.MasterAddr = fmt.Sprintf("%v.0.0.1:%v", 100+tablet.Parent.Uid, 3300+int(tablet.Parent.Uid)) + if ok { + fakeMysqlDaemon.MasterAddr = fmt.Sprintf("%v.0.0.1:%v", 100+puid, 3300+puid) } fakeMysqlDaemon.MysqlPort = 3300 + int(uid) @@ -130,16 +134,16 @@ func (ft *FakeTablet) StartActionLoop(t *testing.T, wr *wrangler.Wrangler) { // create a test agent on that port, and re-read the record // (it has new ports and IP) - ft.Agent = tabletmanager.NewTestActionAgent(wr.TopoServer(), ft.Tablet.Alias, port, ft.FakeMysqlDaemon) + ft.Agent = tabletmanager.NewTestActionAgent(context.TODO(), wr.TopoServer(), ft.Tablet.Alias, port, ft.FakeMysqlDaemon) ft.Tablet = ft.Agent.Tablet().Tablet // create the RPC server - ft.RpcServer = rpcplus.NewServer() - gorpctmserver.RegisterForTest(ft.RpcServer, ft.Agent) + ft.RPCServer = rpcplus.NewServer() + gorpctmserver.RegisterForTest(ft.RPCServer, ft.Agent) // create the HTTP server, serve the server from it handler := http.NewServeMux() - bsonrpc.ServeCustomRPC(handler, ft.RpcServer, false) + bsonrpc.ServeCustomRPC(handler, ft.RPCServer, false) httpServer := http.Server{ Handler: handler, } diff --git a/go/vt/wrangler/testlib/reparent_external_test.go b/go/vt/wrangler/testlib/reparent_external_test.go index 32636adc960..13d9b9408ee 100644 --- a/go/vt/wrangler/testlib/reparent_external_test.go +++ b/go/vt/wrangler/testlib/reparent_external_test.go @@ -5,24 +5,38 @@ package testlib import ( - "strings" + "fmt" "testing" "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" + "github.com/youtube/vitess/go/event" "github.com/youtube/vitess/go/vt/logutil" + "github.com/youtube/vitess/go/vt/tabletmanager" _ "github.com/youtube/vitess/go/vt/tabletmanager/gorpctmclient" + "github.com/youtube/vitess/go/vt/tabletmanager/tmclient" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topotools" + "github.com/youtube/vitess/go/vt/topotools/events" "github.com/youtube/vitess/go/vt/wrangler" "github.com/youtube/vitess/go/vt/zktopo" ) -func TestShardExternallyReparented(t *testing.T) { +func TestTabletExternallyReparented(t *testing.T) { + testTabletExternallyReparented(t, false /* falst */) +} + +func TestTabletExternallyReparentedFast(t *testing.T) { + testTabletExternallyReparented(t, true /* fast */) +} + +func testTabletExternallyReparented(t *testing.T, fast bool) { + tabletmanager.SetReparentFlags(fast, time.Minute /* finalizeTimeout */) + ctx := context.Background() ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) - wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Minute, time.Second) + wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Second) // Create an old master, a new master, two good slaves, one bad slave oldMaster := NewFakeTablet(t, wr, "cell1", 0, topo.TYPE_MASTER) @@ -48,7 +62,7 @@ func TestShardExternallyReparented(t *testing.T) { // Slightly unrelated test: make sure we can find the tablets // even with a datacenter being down. - tabletMap, err := topo.GetTabletMapForShardByCell(ts, "test_keyspace", "0", []string{"cell1"}) + tabletMap, err := topo.GetTabletMapForShardByCell(ctx, ts, "test_keyspace", "0", []string{"cell1"}) if err != nil { t.Fatalf("GetTabletMapForShardByCell should have worked but got: %v", err) } @@ -66,13 +80,13 @@ func TestShardExternallyReparented(t *testing.T) { } // Make sure the master is not exported in other cells - tabletMap, err = topo.GetTabletMapForShardByCell(ts, "test_keyspace", "0", []string{"cell2"}) + tabletMap, err = topo.GetTabletMapForShardByCell(ctx, ts, "test_keyspace", "0", []string{"cell2"}) master, err = topotools.FindTabletByIPAddrAndPort(tabletMap, oldMaster.Tablet.IPAddr, "vt", oldMaster.Tablet.Portmap["vt"]) if err != topo.ErrNoNode { t.Fatalf("FindTabletByIPAddrAndPort(master) worked in cell2: %v %v", err, master) } - tabletMap, err = topo.GetTabletMapForShard(ts, "test_keyspace", "0") + tabletMap, err = topo.GetTabletMapForShard(ctx, ts, "test_keyspace", "0") if err != topo.ErrPartialResult { t.Fatalf("GetTabletMapForShard should have returned ErrPartialResult but got: %v", err) } @@ -81,19 +95,6 @@ func TestShardExternallyReparented(t *testing.T) { t.Fatalf("FindTabletByIPAddrAndPort(master) failed: %v %v", err, master) } - // First test: reparent to the same master, make sure it works - // as expected. - if err := wr.ShardExternallyReparented("test_keyspace", "0", oldMaster.Tablet.Alias); err == nil { - t.Fatalf("ShardExternallyReparented(same master) should have failed") - } else { - if !strings.Contains(err.Error(), "already master") { - t.Fatalf("ShardExternallyReparented(same master) should have failed with an error that contains 'already master' but got: %v", err) - } - } - - // Second test: reparent to the replica, and pretend the old - // master is still good to go. - // On the elected master, we will respond to // TABLET_ACTION_SLAVE_WAS_PROMOTED newMaster.FakeMysqlDaemon.MasterAddr = "" @@ -102,17 +103,17 @@ func TestShardExternallyReparented(t *testing.T) { // On the old master, we will only respond to // TABLET_ACTION_SLAVE_WAS_RESTARTED. - oldMaster.FakeMysqlDaemon.MasterAddr = newMaster.Tablet.MysqlIpAddr() + oldMaster.FakeMysqlDaemon.MasterAddr = newMaster.Tablet.MysqlIPAddr() oldMaster.StartActionLoop(t, wr) defer oldMaster.StopActionLoop(t) // On the good slaves, we will respond to // TABLET_ACTION_SLAVE_WAS_RESTARTED. - goodSlave1.FakeMysqlDaemon.MasterAddr = newMaster.Tablet.MysqlIpAddr() + goodSlave1.FakeMysqlDaemon.MasterAddr = newMaster.Tablet.MysqlIPAddr() goodSlave1.StartActionLoop(t, wr) defer goodSlave1.StopActionLoop(t) - goodSlave2.FakeMysqlDaemon.MasterAddr = newMaster.Tablet.MysqlIpAddr() + goodSlave2.FakeMysqlDaemon.MasterAddr = newMaster.Tablet.MysqlIPAddr() goodSlave2.StartActionLoop(t, wr) defer goodSlave2.StopActionLoop(t) @@ -122,16 +123,42 @@ func TestShardExternallyReparented(t *testing.T) { badSlave.StartActionLoop(t, wr) defer badSlave.StopActionLoop(t) + // First test: reparent to the same master, make sure it works + // as expected. + tmc := tmclient.NewTabletManagerClient() + ti, err := ts.GetTablet(oldMaster.Tablet.Alias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if err := tmc.TabletExternallyReparented(context.Background(), ti, ""); err != nil { + t.Fatalf("TabletExternallyReparented(same master) should have worked") + } + + // Second test: reparent to a replica, and pretend the old + // master is still good to go. + // This tests a bad case; the new designated master is a slave, // but we should do what we're told anyway - if err := wr.ShardExternallyReparented("test_keyspace", "0", goodSlave1.Tablet.Alias); err != nil { - t.Fatalf("ShardExternallyReparented(slave) error: %v", err) + ti, err = ts.GetTablet(goodSlave1.Tablet.Alias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if err := tmc.TabletExternallyReparented(context.Background(), ti, ""); err != nil { + t.Fatalf("TabletExternallyReparented(slave) error: %v", err) } // This tests the good case, where everything works as planned - t.Logf("ShardExternallyReparented(new master) expecting success") - if err := wr.ShardExternallyReparented("test_keyspace", "0", newMaster.Tablet.Alias); err != nil { - t.Fatalf("ShardExternallyReparented(replica) failed: %v", err) + t.Logf("TabletExternallyReparented(new master) expecting success") + ti, err = ts.GetTablet(newMaster.Tablet.Alias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + waitID := makeWaitID() + if err := tmc.TabletExternallyReparented(context.Background(), ti, waitID); err != nil { + t.Fatalf("TabletExternallyReparented(replica) failed: %v", err) + } + if fast { + waitForExternalReparent(t, waitID) } // Now double-check the serving graph is good. @@ -145,12 +172,22 @@ func TestShardExternallyReparented(t *testing.T) { } } -// TestShardExternallyReparentedWithDifferentMysqlPort makes sure +// TestTabletExternallyReparentedWithDifferentMysqlPort makes sure // that if mysql is restarted on the master-elect tablet and has a different // port, we pick it up correctly. -func TestShardExternallyReparentedWithDifferentMysqlPort(t *testing.T) { +func TestTabletExternallyReparentedWithDifferentMysqlPort(t *testing.T) { + testTabletExternallyReparentedWithDifferentMysqlPort(t, false /* fast */) +} + +func TestTabletExternallyReparentedWithDifferentMysqlPortFast(t *testing.T) { + testTabletExternallyReparentedWithDifferentMysqlPort(t, true /* fast */) +} + +func testTabletExternallyReparentedWithDifferentMysqlPort(t *testing.T, fast bool) { + tabletmanager.SetReparentFlags(fast, time.Minute /* finalizeTimeout */) + ts := zktopo.NewTestServer(t, []string{"cell1"}) - wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Minute, time.Second) + wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Second) // Create an old master, a new master, two good slaves, one bad slave oldMaster := NewFakeTablet(t, wr, "cell1", 0, topo.TYPE_MASTER) @@ -183,17 +220,32 @@ func TestShardExternallyReparentedWithDifferentMysqlPort(t *testing.T) { defer goodSlave.StopActionLoop(t) // This tests the good case, where everything works as planned - t.Logf("ShardExternallyReparented(new master) expecting success") - if err := wr.ShardExternallyReparented("test_keyspace", "0", newMaster.Tablet.Alias); err != nil { - t.Fatalf("ShardExternallyReparented(replica) failed: %v", err) + t.Logf("TabletExternallyReparented(new master) expecting success") + tmc := tmclient.NewTabletManagerClient() + ti, err := ts.GetTablet(newMaster.Tablet.Alias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if err := tmc.TabletExternallyReparented(context.Background(), ti, ""); err != nil { + t.Fatalf("TabletExternallyReparented(replica) failed: %v", err) } } -// TestShardExternallyReparentedContinueOnUnexpectedMaster makes sure +// TestTabletExternallyReparentedContinueOnUnexpectedMaster makes sure // that we ignore mysql's master if the flag is set -func TestShardExternallyReparentedContinueOnUnexpectedMaster(t *testing.T) { +func TestTabletExternallyReparentedContinueOnUnexpectedMaster(t *testing.T) { + testTabletExternallyReparentedContinueOnUnexpectedMaster(t, false /* fast */) +} + +func TestTabletExternallyReparentedContinueOnUnexpectedMasterFast(t *testing.T) { + testTabletExternallyReparentedContinueOnUnexpectedMaster(t, true /* fast */) +} + +func testTabletExternallyReparentedContinueOnUnexpectedMaster(t *testing.T, fast bool) { + tabletmanager.SetReparentFlags(fast, time.Minute /* finalizeTimeout */) + ts := zktopo.NewTestServer(t, []string{"cell1"}) - wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Minute, time.Second) + wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Second) // Create an old master, a new master, two good slaves, one bad slave oldMaster := NewFakeTablet(t, wr, "cell1", 0, topo.TYPE_MASTER) @@ -222,25 +274,39 @@ func TestShardExternallyReparentedContinueOnUnexpectedMaster(t *testing.T) { defer goodSlave.StopActionLoop(t) // This tests the good case, where everything works as planned - t.Logf("ShardExternallyReparented(new master) expecting success") - // temporary failure still: - if err := wr.ShardExternallyReparented("test_keyspace", "0", newMaster.Tablet.Alias); err != nil { - t.Fatalf("ShardExternallyReparented(replica) failed: %v", err) + t.Logf("TabletExternallyReparented(new master) expecting success") + tmc := tmclient.NewTabletManagerClient() + ti, err := ts.GetTablet(newMaster.Tablet.Alias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + if err := tmc.TabletExternallyReparented(context.Background(), ti, ""); err != nil { + t.Fatalf("TabletExternallyReparented(replica) failed: %v", err) } } -func TestShardExternallyReparentedFailedOldMaster(t *testing.T) { +func TestTabletExternallyReparentedFailedOldMaster(t *testing.T) { + testTabletExternallyReparentedFailedOldMaster(t, false /* fast */) +} + +func TestTabletExternallyReparentedFailedOldMasterFast(t *testing.T) { + testTabletExternallyReparentedFailedOldMaster(t, true /* fast */) +} + +func testTabletExternallyReparentedFailedOldMaster(t *testing.T, fast bool) { + tabletmanager.SetReparentFlags(fast, time.Minute /* finalizeTimeout */) + ts := zktopo.NewTestServer(t, []string{"cell1", "cell2"}) - wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Minute, time.Second) + wr := wrangler.New(logutil.NewConsoleLogger(), ts, time.Second) - // Create an old master, a new master, two good slaves + // Create an old master, a new master, and a good slave. oldMaster := NewFakeTablet(t, wr, "cell1", 0, topo.TYPE_MASTER) newMaster := NewFakeTablet(t, wr, "cell1", 1, topo.TYPE_REPLICA, TabletParent(oldMaster.Tablet.Alias)) goodSlave := NewFakeTablet(t, wr, "cell1", 2, topo.TYPE_REPLICA, TabletParent(oldMaster.Tablet.Alias)) - // Reparent to a replica, and pretend the old master is not responding + // Reparent to a replica, and pretend the old master is not responding. // On the elected master, we will respond to // TABLET_ACTION_SLAVE_WAS_PROMOTED @@ -254,14 +320,23 @@ func TestShardExternallyReparentedFailedOldMaster(t *testing.T) { // On the good slave, we will respond to // TABLET_ACTION_SLAVE_WAS_RESTARTED. - goodSlave.FakeMysqlDaemon.MasterAddr = newMaster.Tablet.MysqlIpAddr() + goodSlave.FakeMysqlDaemon.MasterAddr = newMaster.Tablet.MysqlIPAddr() goodSlave.StartActionLoop(t, wr) defer goodSlave.StopActionLoop(t) // The reparent should work as expected here - t.Logf("ShardExternallyReparented(new master) expecting success") - if err := wr.ShardExternallyReparented("test_keyspace", "0", newMaster.Tablet.Alias); err != nil { - t.Fatalf("ShardExternallyReparented(replica) failed: %v", err) + t.Logf("TabletExternallyReparented(new master) expecting success") + tmc := tmclient.NewTabletManagerClient() + ti, err := ts.GetTablet(newMaster.Tablet.Alias) + if err != nil { + t.Fatalf("GetTablet failed: %v", err) + } + waitID := makeWaitID() + if err := tmc.TabletExternallyReparented(context.Background(), ti, waitID); err != nil { + t.Fatalf("TabletExternallyReparented(replica) failed: %v", err) + } + if fast { + waitForExternalReparent(t, waitID) } // Now double-check the serving graph is good. @@ -282,7 +357,44 @@ func TestShardExternallyReparentedFailedOldMaster(t *testing.T) { if tablet.Type != topo.TYPE_SPARE { t.Fatalf("old master should be spare but is: %v", tablet.Type) } - if tablet.Parent != newMaster.Tablet.Alias { - t.Fatalf("old master has the wrong master, got %v expected %v", tablet.Parent, newMaster.Tablet.Alias) +} + +var externalReparents = make(map[string]chan struct{}) + +// makeWaitID generates a unique externalID that can be passed to +// TabletExternallyReparented, and then to waitForExternalReparent. +func makeWaitID() string { + id := fmt.Sprintf("wait id %v", len(externalReparents)) + externalReparents[id] = make(chan struct{}) + return id +} + +func init() { + event.AddListener(func(ev *events.Reparent) { + if ev.Status == "finished" { + if c, ok := externalReparents[ev.ExternalID]; ok { + close(c) + } + } + }) +} + +// waitForExternalReparent waits up to a fixed duration for the external +// reparent with the given ID to finish. The ID must have been previously +// generated by makeWaitID(). +// +// In fast mode, the TabletExternallyReparented RPC returns as soon as the +// new master is visible in the serving graph. Before checking things like +// replica endpoints and old master status, we should wait for the finalize +// stage, which happens in the background. +func waitForExternalReparent(t *testing.T, externalID string) { + timer := time.NewTimer(10 * time.Second) + defer timer.Stop() + + select { + case <-externalReparents[externalID]: + return + case <-timer.C: + t.Fatalf("deadline exceeded waiting for finalized external reparent %q", externalID) } } diff --git a/go/vt/wrangler/validator.go b/go/vt/wrangler/validator.go index 9047172a857..f7f366e79d3 100644 --- a/go/vt/wrangler/validator.go +++ b/go/vt/wrangler/validator.go @@ -8,12 +8,10 @@ import ( "fmt" "strings" "sync" - "time" - - "code.google.com/p/go.net/context" log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // As with all distributed systems, things can skew. These functions @@ -24,75 +22,44 @@ import ( // // This may eventually move into a separate package. -type vresult struct { - name string - err error -} - -func (wr *Wrangler) waitForResults(wg *sync.WaitGroup, results chan vresult) error { - timer := time.NewTimer(wr.ActionTimeout()) - done := make(chan bool, 1) +// waitForResults will wait for all the errors to come back. +// There is no timeout, as individual calls will use the context and timeout +// and fail at the end anyway. +func (wr *Wrangler) waitForResults(wg *sync.WaitGroup, results chan error) error { go func() { wg.Wait() - done <- true + close(results) }() - var err error -wait: - for { - select { - case vd := <-results: - log.Infof("checking %v", vd.name) - if vd.err != nil { - err = fmt.Errorf("some validation errors - see log") - log.Errorf("%v: %v", vd.name, vd.err) - } - case <-timer.C: - err = fmt.Errorf("timed out during validate") - break wait - case <-done: - // To prevent a false positive, once we are 'done', - // drain the result channel completely. - for { - select { - case vd := <-results: - log.Infof("checking %v", vd.name) - if vd.err != nil { - err = fmt.Errorf("some validation errors - see log") - log.Errorf("%v: %v", vd.name, vd.err) - } - default: - break wait - } - } - } + var finalErr error + for err := range results { + finalErr = fmt.Errorf("some validation errors - see log") + log.Errorf("%v", err) } - - return err + return finalErr } // Validate all tablets in all discoverable cells, even if they are // not in the replication graph. -func (wr *Wrangler) validateAllTablets(wg *sync.WaitGroup, results chan<- vresult) { - +func (wr *Wrangler) validateAllTablets(ctx context.Context, wg *sync.WaitGroup, results chan<- error) { cellSet := make(map[string]bool, 16) keyspaces, err := wr.ts.GetKeyspaces() if err != nil { - results <- vresult{"TopologyServer.GetKeyspaces", err} + results <- fmt.Errorf("TopologyServer.GetKeyspaces failed: %v", err) return } for _, keyspace := range keyspaces { shards, err := wr.ts.GetShardNames(keyspace) if err != nil { - results <- vresult{"TopologyServer.GetShardNames(" + keyspace + ")", err} + results <- fmt.Errorf("TopologyServer.GetShardNames(%v) failed: %v", keyspace, err) return } for _, shard := range shards { - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shard) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shard) if err != nil { - results <- vresult{"TopologyServer.FindAllTabletAliasesInShard(" + keyspace + "," + shard + ")", err} + results <- fmt.Errorf("TopologyServer.FindAllTabletAliasesInShard(%v, %v) failed: %v", keyspace, shard, err) return } for _, alias := range aliases { @@ -104,60 +71,67 @@ func (wr *Wrangler) validateAllTablets(wg *sync.WaitGroup, results chan<- vresul for cell := range cellSet { aliases, err := wr.ts.GetTabletsByCell(cell) if err != nil { - results <- vresult{"GetTabletsByCell(" + cell + ")", err} - } else { - for _, alias := range aliases { - wg.Add(1) - go func(alias topo.TabletAlias) { - results <- vresult{alias.String(), topo.Validate(wr.ts, alias)} - wg.Done() - }(alias) - } + results <- fmt.Errorf("TopologyServer.GetTabletsByCell(%v) failed: %v", cell, err) + continue + } + + for _, alias := range aliases { + wg.Add(1) + go func(alias topo.TabletAlias) { + defer wg.Done() + if err := topo.Validate(wr.ts, alias); err != nil { + results <- fmt.Errorf("Validate(%v) failed: %v", alias, err) + } else { + wr.Logger().Infof("tablet %v is valid", alias) + } + }(alias) } } } -func (wr *Wrangler) validateKeyspace(keyspace string, pingTablets bool, wg *sync.WaitGroup, results chan<- vresult) { +func (wr *Wrangler) validateKeyspace(ctx context.Context, keyspace string, pingTablets bool, wg *sync.WaitGroup, results chan<- error) { // Validate replication graph by traversing each shard. shards, err := wr.ts.GetShardNames(keyspace) if err != nil { - results <- vresult{"TopologyServer.GetShardNames(" + keyspace + ")", err} + results <- fmt.Errorf("TopologyServer.GetShardNames(%v) failed: %v", keyspace, err) + return } for _, shard := range shards { wg.Add(1) go func(shard string) { - wr.validateShard(keyspace, shard, pingTablets, wg, results) - wg.Done() + defer wg.Done() + wr.validateShard(ctx, keyspace, shard, pingTablets, wg, results) }(shard) } } // FIXME(msolomon) This validate presumes the master is up and running. // Even when that isn't true, there are validation processes that might be valuable. -func (wr *Wrangler) validateShard(keyspace, shard string, pingTablets bool, wg *sync.WaitGroup, results chan<- vresult) { +func (wr *Wrangler) validateShard(ctx context.Context, keyspace, shard string, pingTablets bool, wg *sync.WaitGroup, results chan<- error) { shardInfo, err := wr.ts.GetShard(keyspace, shard) if err != nil { - results <- vresult{keyspace + "/" + shard, err} + results <- fmt.Errorf("TopologyServer.GetShard(%v, %v) failed: %v", keyspace, shard, err) return } - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shard) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shard) if err != nil { - results <- vresult{keyspace + "/" + shard, err} + results <- fmt.Errorf("TopologyServer.FindAllTabletAliasesInShard(%v, %v) failed: %v", keyspace, shard, err) + return } - tabletMap, _ := topo.GetTabletMap(wr.ts, aliases) + tabletMap, _ := topo.GetTabletMap(ctx, wr.ts, aliases) var masterAlias topo.TabletAlias for _, alias := range aliases { tabletInfo, ok := tabletMap[alias] if !ok { - results <- vresult{alias.String(), fmt.Errorf("tablet not found in map")} + results <- fmt.Errorf("tablet %v not found in map", alias) continue } - if tabletInfo.Parent.Uid == topo.NO_TABLET { + if tabletInfo.Type == topo.TYPE_MASTER { if masterAlias.Cell != "" { - results <- vresult{alias.String(), fmt.Errorf("tablet already has a master %v", masterAlias)} + results <- fmt.Errorf("shard %v/%v already has master %v but found other master %v", keyspace, shard, masterAlias, alias) } else { masterAlias = alias } @@ -165,22 +139,26 @@ func (wr *Wrangler) validateShard(keyspace, shard string, pingTablets bool, wg * } if masterAlias.Cell == "" { - results <- vresult{keyspace + "/" + shard, fmt.Errorf("no master for shard")} + results <- fmt.Errorf("no master for shard %v/%v", keyspace, shard) } else if shardInfo.MasterAlias != masterAlias { - results <- vresult{keyspace + "/" + shard, fmt.Errorf("master mismatch for shard: found %v, expected %v", masterAlias, shardInfo.MasterAlias)} + results <- fmt.Errorf("master mismatch for shard %v/%v: found %v, expected %v", keyspace, shard, masterAlias, shardInfo.MasterAlias) } for _, alias := range aliases { wg.Add(1) go func(alias topo.TabletAlias) { - results <- vresult{alias.String(), topo.Validate(wr.ts, alias)} - wg.Done() + defer wg.Done() + if err := topo.Validate(wr.ts, alias); err != nil { + results <- fmt.Errorf("Validate(%v) failed: %v", alias, err) + } else { + wr.Logger().Infof("tablet %v is valid", alias) + } }(alias) } if pingTablets { - wr.validateReplication(shardInfo, tabletMap, results) - wr.pingTablets(tabletMap, wg, results) + wr.validateReplication(ctx, shardInfo, tabletMap, results) + wr.pingTablets(ctx, tabletMap, wg, results) } return @@ -194,50 +172,35 @@ func normalizeIP(ip string) string { return ip } -func strInList(sl []string, s string) bool { - for _, x := range sl { - if x == s { - return true - } - } - return false -} - -func (wr *Wrangler) validateReplication(shardInfo *topo.ShardInfo, tabletMap map[topo.TabletAlias]*topo.TabletInfo, results chan<- vresult) { +func (wr *Wrangler) validateReplication(ctx context.Context, shardInfo *topo.ShardInfo, tabletMap map[topo.TabletAlias]*topo.TabletInfo, results chan<- error) { masterTablet, ok := tabletMap[shardInfo.MasterAlias] if !ok { - results <- vresult{shardInfo.MasterAlias.String(), fmt.Errorf("master not in tablet map")} + results <- fmt.Errorf("master %v not in tablet map", shardInfo.MasterAlias) return } - slaveList, err := wr.tmc.GetSlaves(context.TODO(), masterTablet, wr.ActionTimeout()) + slaveList, err := wr.tmc.GetSlaves(ctx, masterTablet) if err != nil { - results <- vresult{shardInfo.MasterAlias.String(), err} + results <- fmt.Errorf("GetSlaves(%v) failed: %v", masterTablet, err) return } if len(slaveList) == 0 { - results <- vresult{shardInfo.MasterAlias.String(), fmt.Errorf("no slaves found")} - return - } - - // Some addresses don't resolve in all locations, just use IP address - if err != nil { - results <- vresult{shardInfo.MasterAlias.String(), fmt.Errorf("resolve slaves failed: %v", err)} + results <- fmt.Errorf("no slaves of tablet %v found", shardInfo.MasterAlias) return } - tabletIpMap := make(map[string]*topo.Tablet) - slaveIpMap := make(map[string]bool) + tabletIPMap := make(map[string]*topo.Tablet) + slaveIPMap := make(map[string]bool) for _, tablet := range tabletMap { - tabletIpMap[normalizeIP(tablet.IPAddr)] = tablet.Tablet + tabletIPMap[normalizeIP(tablet.IPAddr)] = tablet.Tablet } // See if every slave is in the replication graph. for _, slaveAddr := range slaveList { - if tabletIpMap[normalizeIP(slaveAddr)] == nil { - results <- vresult{shardInfo.Keyspace() + "/" + shardInfo.ShardName(), fmt.Errorf("slave not in replication graph: %v (mysql instance without vttablet?)", slaveAddr)} + if tabletIPMap[normalizeIP(slaveAddr)] == nil { + results <- fmt.Errorf("slave %v not in replication graph for shard %v/%v (mysql instance without vttablet?)", slaveAddr, shardInfo.Keyspace(), shardInfo.ShardName()) } - slaveIpMap[normalizeIP(slaveAddr)] = true + slaveIPMap[normalizeIP(slaveAddr)] = true } // See if every entry in the replication graph is connected to the master. @@ -246,78 +209,76 @@ func (wr *Wrangler) validateReplication(shardInfo *topo.ShardInfo, tabletMap map continue } - if !slaveIpMap[normalizeIP(tablet.IPAddr)] { - results <- vresult{tablet.Alias.String(), fmt.Errorf("slave not replicating: %v %q", tablet.IPAddr, slaveList)} + if !slaveIPMap[normalizeIP(tablet.IPAddr)] { + results <- fmt.Errorf("slave %v not replicating: %v %q", tablet.Alias, tablet.IPAddr, slaveList) } } } -func (wr *Wrangler) pingTablets(tabletMap map[topo.TabletAlias]*topo.TabletInfo, wg *sync.WaitGroup, results chan<- vresult) { +func (wr *Wrangler) pingTablets(ctx context.Context, tabletMap map[topo.TabletAlias]*topo.TabletInfo, wg *sync.WaitGroup, results chan<- error) { for tabletAlias, tabletInfo := range tabletMap { wg.Add(1) go func(tabletAlias topo.TabletAlias, tabletInfo *topo.TabletInfo) { defer wg.Done() - if err := wr.ts.ValidateTabletPidNode(tabletAlias); err != nil { - results <- vresult{tabletAlias.String(), fmt.Errorf("no pid node on %v: %v", tabletInfo.Hostname, err)} - return - } - - if err := wr.tmc.Ping(context.TODO(), tabletInfo, wr.ActionTimeout()); err != nil { - results <- vresult{tabletAlias.String(), fmt.Errorf("Ping failed: %v %v", err, tabletInfo.Hostname)} + if err := wr.tmc.Ping(ctx, tabletInfo); err != nil { + results <- fmt.Errorf("Ping(%v) failed: %v %v", tabletAlias, err, tabletInfo.Hostname) } }(tabletAlias, tabletInfo) } } // Validate a whole TopologyServer tree -func (wr *Wrangler) Validate(pingTablets bool) error { +func (wr *Wrangler) Validate(ctx context.Context, pingTablets bool) error { // Results from various actions feed here. - results := make(chan vresult, 16) + results := make(chan error, 16) wg := &sync.WaitGroup{} // Validate all tablets in all cells, even if they are not discoverable // by the replication graph. wg.Add(1) go func() { - wr.validateAllTablets(wg, results) - wg.Done() + defer wg.Done() + wr.validateAllTablets(ctx, wg, results) }() // Validate replication graph by traversing each keyspace and then each shard. keyspaces, err := wr.ts.GetKeyspaces() if err != nil { - results <- vresult{"TopologyServer.GetKeyspaces", err} + results <- fmt.Errorf("GetKeyspaces failed: %v", err) } else { for _, keyspace := range keyspaces { wg.Add(1) go func(keyspace string) { - wr.validateKeyspace(keyspace, pingTablets, wg, results) - wg.Done() + defer wg.Done() + wr.validateKeyspace(ctx, keyspace, pingTablets, wg, results) }(keyspace) } } return wr.waitForResults(wg, results) } -func (wr *Wrangler) ValidateKeyspace(keyspace string, pingTablets bool) error { +// ValidateKeyspace will validate a bunch of information in a keyspace +// is correct. +func (wr *Wrangler) ValidateKeyspace(ctx context.Context, keyspace string, pingTablets bool) error { wg := &sync.WaitGroup{} - results := make(chan vresult, 16) + results := make(chan error, 16) wg.Add(1) go func() { - wr.validateKeyspace(keyspace, pingTablets, wg, results) - wg.Done() + defer wg.Done() + wr.validateKeyspace(ctx, keyspace, pingTablets, wg, results) }() return wr.waitForResults(wg, results) } -func (wr *Wrangler) ValidateShard(keyspace, shard string, pingTablets bool) error { +// ValidateShard will validate a bunch of information in a shard is correct. +func (wr *Wrangler) ValidateShard(ctx context.Context, keyspace, shard string, pingTablets bool) error { wg := &sync.WaitGroup{} - results := make(chan vresult, 16) + results := make(chan error, 16) wg.Add(1) go func() { - wr.validateShard(keyspace, shard, pingTablets, wg, results) - wg.Done() + defer wg.Done() + wr.validateShard(ctx, keyspace, shard, pingTablets, wg, results) }() return wr.waitForResults(wg, results) } diff --git a/go/vt/wrangler/version.go b/go/vt/wrangler/version.go index ade441bee2a..c0c7c05c5b2 100644 --- a/go/vt/wrangler/version.go +++ b/go/vt/wrangler/version.go @@ -10,27 +10,16 @@ import ( "io/ioutil" "net/http" "sort" - "strings" "sync" log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/concurrency" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) -type debugVars struct { - Version string -} - -func (wr *Wrangler) GetVersion(tabletAlias topo.TabletAlias) (string, error) { - // read the tablet from TopologyServer to get the address to connect to - tablet, err := wr.ts.GetTablet(tabletAlias) - if err != nil { - return "", err - } - - // build the url, get debug/vars - resp, err := http.Get("http://" + tablet.Addr() + "/debug/vars") +var getVersionFromTablet = func(tabletAddr string) (string, error) { + resp, err := http.Get("http://" + tabletAddr + "/debug/vars") if err != nil { return "", err } @@ -40,23 +29,34 @@ func (wr *Wrangler) GetVersion(tabletAlias topo.TabletAlias) (string, error) { return "", err } - // convert json - vars := debugVars{} + var vars struct { + BuildHost string + BuildUser string + BuildTimestamp int64 + BuildGitRev string + } err = json.Unmarshal(body, &vars) if err != nil { return "", err } - // split the version into date and md5 - parts := strings.Split(vars.Version, " ") - if len(parts) != 2 { - // can't understand this, oh well - return vars.Version, nil + version := fmt.Sprintf("%v", vars) + return version, nil +} + +// GetVersion returns the version string from a tablet +func (wr *Wrangler) GetVersion(tabletAlias topo.TabletAlias) (string, error) { + tablet, err := wr.ts.GetTablet(tabletAlias) + if err != nil { + return "", err } - version := parts[1] + version, err := getVersionFromTablet(tablet.Addr()) + if err != nil { + return "", err + } log.Infof("Tablet %v is running version '%v'", tabletAlias, version) - return version, nil + return version, err } // helper method to asynchronously get and diff a version @@ -74,7 +74,9 @@ func (wr *Wrangler) diffVersion(masterVersion string, masterAlias topo.TabletAli } } -func (wr *Wrangler) ValidateVersionShard(keyspace, shard string) error { +// ValidateVersionShard validates all versions are the same in all +// tablets in a shard +func (wr *Wrangler) ValidateVersionShard(ctx context.Context, keyspace, shard string) error { si, err := wr.ts.GetShard(keyspace, shard) if err != nil { return err @@ -92,7 +94,7 @@ func (wr *Wrangler) ValidateVersionShard(keyspace, shard string) error { // read all the aliases in the shard, that is all tablets that are // replicating from the master - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shard) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shard) if err != nil { return err } @@ -115,7 +117,9 @@ func (wr *Wrangler) ValidateVersionShard(keyspace, shard string) error { return nil } -func (wr *Wrangler) ValidateVersionKeyspace(keyspace string) error { +// ValidateVersionKeyspace validates all versions are the same in all +// tablets in a keyspace +func (wr *Wrangler) ValidateVersionKeyspace(ctx context.Context, keyspace string) error { // find all the shards shards, err := wr.ts.GetShardNames(keyspace) if err != nil { @@ -128,7 +132,7 @@ func (wr *Wrangler) ValidateVersionKeyspace(keyspace string) error { } sort.Strings(shards) if len(shards) == 1 { - return wr.ValidateVersionShard(keyspace, shards[0]) + return wr.ValidateVersionShard(ctx, keyspace, shards[0]) } // find the reference version using the first shard's master @@ -150,7 +154,7 @@ func (wr *Wrangler) ValidateVersionKeyspace(keyspace string) error { er := concurrency.AllErrorRecorder{} wg := sync.WaitGroup{} for _, shard := range shards { - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shard) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shard) if err != nil { er.RecordError(err) continue diff --git a/go/vt/wrangler/wrangler.go b/go/vt/wrangler/wrangler.go index c7e8e8ae9e4..680852f11f9 100644 --- a/go/vt/wrangler/wrangler.go +++ b/go/vt/wrangler/wrangler.go @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// wrangler contains the Wrangler object to manage complex topology actions. +// Package wrangler contains the Wrangler object to manage complex +// topology actions. package wrangler import ( @@ -18,44 +19,39 @@ var ( // DefaultActionTimeout is a good default for interactive // remote actions. We usually take a lock then do an action, // so basing this to be greater than DefaultLockTimeout is good. + // Use this as the default value for Context that need a deadline. DefaultActionTimeout = actionnode.DefaultLockTimeout * 4 ) // Wrangler manages complex actions on the topology, like reparents, // snapshots, restores, ... -// It is not a thread safe structure. Two go routines cannot usually -// call wrangler methods at the same time (they probably would not -// have the same logger, and definitely not the same timeouts / -// deadlines). +// +// FIXME(alainjobart) take the context out of this structure. +// We want the context to come from the outside on every call. +// +// Multiple go routines can use the same Wrangler at the same time, +// provided they want to share the same logger / topo server / lock timeout. type Wrangler struct { logger logutil.Logger ts topo.Server tmc tmclient.TabletManagerClient - deadline time.Time lockTimeout time.Duration } // New creates a new Wrangler object. // -// actionTimeout: how long should we wait for an action to complete? -// - if using wrangler for just one action, this is set properly -// upon wrangler creation. -// - if re-using wrangler multiple times, call ResetActionTimeout before -// every action. -// // lockTimeout: how long should we wait for the initial lock to start -// a complex action? This is distinct from actionTimeout because most +// a complex action? This is distinct from the context timeout because most // of the time, we want to immediately know that our action will // fail. However, automated action will need some time to arbitrate // the locks. -func New(logger logutil.Logger, ts topo.Server, actionTimeout, lockTimeout time.Duration) *Wrangler { - return &Wrangler{logger, ts, tmclient.NewTabletManagerClient(), time.Now().Add(actionTimeout), lockTimeout} -} - -// ActionTimeout returns the timeout to use so the action finishes before -// the deadline. -func (wr *Wrangler) ActionTimeout() time.Duration { - return wr.deadline.Sub(time.Now()) +func New(logger logutil.Logger, ts topo.Server, lockTimeout time.Duration) *Wrangler { + return &Wrangler{ + logger: logger, + ts: ts, + tmc: tmclient.NewTabletManagerClient(), + lockTimeout: lockTimeout, + } } // TopoServer returns the topo.Server this wrangler is using. @@ -79,20 +75,3 @@ func (wr *Wrangler) SetLogger(logger logutil.Logger) { func (wr *Wrangler) Logger() logutil.Logger { return wr.logger } - -// ResetActionTimeout should be used before every action on a wrangler -// object that is going to be re-used: -// - vtctl will not call this, as it does one action -// - vtctld will call this, as it re-uses the same wrangler for actions -func (wr *Wrangler) ResetActionTimeout(actionTimeout time.Duration) { - wr.deadline = time.Now().Add(actionTimeout) -} - -// signal handling -var interrupted = make(chan struct{}) - -// SignalInterrupt needs to be called when a signal interrupts the current -// process. -func SignalInterrupt() { - close(interrupted) -} diff --git a/go/vt/wrangler/zkns.go b/go/vt/wrangler/zkns.go index 5325c3e2931..d89a89c7f2e 100644 --- a/go/vt/wrangler/zkns.go +++ b/go/vt/wrangler/zkns.go @@ -12,10 +12,11 @@ import ( "github.com/youtube/vitess/go/vt/zktopo" "github.com/youtube/vitess/go/zk" "github.com/youtube/vitess/go/zk/zkns" + "golang.org/x/net/context" "launchpad.net/gozk/zookeeper" ) -// Export addresses from the VT serving graph to a legacy zkns server. +// ExportZkns exports addresses from the VT serving graph to a legacy zkns server. // Note these functions only work with a zktopo. func (wr *Wrangler) ExportZkns(cell string) error { zkTopo, ok := wr.ts.(*zktopo.Server) @@ -51,8 +52,8 @@ func (wr *Wrangler) ExportZkns(cell string) error { return nil } -// Export addresses from the VT serving graph to a legacy zkns server. -func (wr *Wrangler) ExportZknsForKeyspace(keyspace string) error { +// ExportZknsForKeyspace exports addresses from the VT serving graph to a legacy zkns server. +func (wr *Wrangler) ExportZknsForKeyspace(ctx context.Context, keyspace string) error { zkTopo, ok := wr.ts.(*zktopo.Server) if !ok { return fmt.Errorf("ExportZknsForKeyspace only works with zktopo") @@ -65,7 +66,7 @@ func (wr *Wrangler) ExportZknsForKeyspace(keyspace string) error { } // Scan the first shard to discover which cells need local serving data. - aliases, err := topo.FindAllTabletAliasesInShard(wr.ts, keyspace, shardNames[0]) + aliases, err := topo.FindAllTabletAliasesInShard(ctx, wr.ts, keyspace, shardNames[0]) if err != nil { return err } @@ -164,13 +165,13 @@ func (wr *Wrangler) exportVtnsToZkns(zconn zk.Conn, vtnsAddrPath, zknsAddrPath s for i, entry := range addrs.Entries { zknsAddrPath := fmt.Sprintf("%v/%v", zknsAddrPath, i) zknsPaths = append(zknsPaths, zknsAddrPath) - zknsAddr := zkns.ZknsAddr{Host: entry.Host, Port: entry.NamedPortMap["_mysql"], NamedPortMap: entry.NamedPortMap} - err := WriteAddr(zconn, zknsAddrPath, &zknsAddr) + zknsAddr := zkns.ZknsAddr{Host: entry.Host, Port: entry.NamedPortMap["mysql"], NamedPortMap: entry.NamedPortMap} + err := writeAddr(zconn, zknsAddrPath, &zknsAddr) if err != nil { return nil, err } defaultAddrs.Endpoints = append(defaultAddrs.Endpoints, zknsAddrPath) - vtoccAddrs.Endpoints = append(vtoccAddrs.Endpoints, zknsAddrPath+":_vtocc") + vtoccAddrs.Endpoints = append(vtoccAddrs.Endpoints, zknsAddrPath+":vt") } // Prune any zkns entries that are no longer referenced by the @@ -192,31 +193,32 @@ func (wr *Wrangler) exportVtnsToZkns(zconn zk.Conn, vtnsAddrPath, zknsAddrPath s } // Write the VDNS entries for both vtocc and mysql - vtoccVdnsPath := fmt.Sprintf("%v/_vtocc.vdns", zknsAddrPath) + vtoccVdnsPath := fmt.Sprintf("%v/vt.vdns", zknsAddrPath) zknsPaths = append(zknsPaths, vtoccVdnsPath) - if err = WriteAddrs(zconn, vtoccVdnsPath, &vtoccAddrs); err != nil { + if err = writeAddrs(zconn, vtoccVdnsPath, &vtoccAddrs); err != nil { return nil, err } defaultVdnsPath := fmt.Sprintf("%v.vdns", zknsAddrPath) zknsPaths = append(zknsPaths, defaultVdnsPath) - if err = WriteAddrs(zconn, defaultVdnsPath, &defaultAddrs); err != nil { + if err = writeAddrs(zconn, defaultVdnsPath, &defaultAddrs); err != nil { return nil, err } return zknsPaths, nil } +// LegacyZknsAddrs is what we write to ZK to use for zkns type LegacyZknsAddrs struct { Endpoints []string `json:"endpoints"` } -func WriteAddr(zconn zk.Conn, zkPath string, addr *zkns.ZknsAddr) error { +func writeAddr(zconn zk.Conn, zkPath string, addr *zkns.ZknsAddr) error { data := jscfg.ToJson(addr) _, err := zk.CreateOrUpdate(zconn, zkPath, data, 0, zookeeper.WorldACL(zookeeper.PERM_ALL), true) return err } -func WriteAddrs(zconn zk.Conn, zkPath string, addrs *LegacyZknsAddrs) error { +func writeAddrs(zconn zk.Conn, zkPath string, addrs *LegacyZknsAddrs) error { data := jscfg.ToJson(addrs) _, err := zk.CreateOrUpdate(zconn, zkPath, data, 0, zookeeper.WorldACL(zookeeper.PERM_ALL), true) return err diff --git a/go/vt/zktopo/agent.go b/go/vt/zktopo/agent.go deleted file mode 100644 index 5de1db45dc3..00000000000 --- a/go/vt/zktopo/agent.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2013, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zktopo - -import ( - "path" - - "github.com/youtube/vitess/go/vt/topo" - "github.com/youtube/vitess/go/zk" -) - -/* -This file contains the code to support the local agent process for zktopo.Server -*/ - -func (zkts *Server) CreateTabletPidNode(tabletAlias topo.TabletAlias, contents string, done chan struct{}) error { - zkTabletPath := TabletPathForAlias(tabletAlias) - path := path.Join(zkTabletPath, "pid") - return zk.CreatePidNode(zkts.zconn, path, contents, done) -} - -func (zkts *Server) ValidateTabletPidNode(tabletAlias topo.TabletAlias) error { - zkTabletPath := TabletPathForAlias(tabletAlias) - path := path.Join(zkTabletPath, "pid") - _, _, err := zkts.zconn.Get(path) - return err -} - -func (zkts *Server) GetSubprocessFlags() []string { - return zk.GetZkSubprocessFlags() -} diff --git a/go/vt/zktopo/cell.go b/go/vt/zktopo/cell.go index c748bc496c0..321050fac85 100644 --- a/go/vt/zktopo/cell.go +++ b/go/vt/zktopo/cell.go @@ -15,7 +15,7 @@ This file contains the cell management methods of zktopo.Server */ func (zkts *Server) GetKnownCells() ([]string, error) { - cellsWithGlobal, err := zk.ZkKnownCells(false) + cellsWithGlobal, err := zk.ZkKnownCells() if err != nil { return cellsWithGlobal, err } diff --git a/go/vt/zktopo/lock.go b/go/vt/zktopo/lock.go index 4776876b332..3f142692965 100644 --- a/go/vt/zktopo/lock.go +++ b/go/vt/zktopo/lock.go @@ -13,6 +13,7 @@ import ( log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/zk" + "golang.org/x/net/context" "launchpad.net/gozk/zookeeper" ) @@ -22,21 +23,42 @@ This file contains the lock management code for zktopo.Server // lockForAction creates the action node in zookeeper, waits for the // queue lock, displays a nice error message if it cant get it -func (zkts *Server) lockForAction(actionDir, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +func (zkts *Server) lockForAction(ctx context.Context, actionDir, contents string) (string, error) { // create the action path actionPath, err := zkts.zconn.Create(actionDir, contents, zookeeper.SEQUENCE|zookeeper.EPHEMERAL, zookeeper.WorldACL(zk.PERM_FILE)) if err != nil { return "", err } + // get the timeout from the context + var timeout time.Duration + deadline, ok := ctx.Deadline() + if !ok { + // enforce a default timeout + timeout = 30 * time.Second + } else { + timeout = deadline.Sub(time.Now()) + } + + // get the interrupted channel from context or don't interrupt + interrupted := ctx.Done() + if interrupted == nil { + interrupted = make(chan struct{}) + } + err = zk.ObtainQueueLock(zkts.zconn, actionPath, timeout, interrupted) if err != nil { var errToReturn error switch err { - case zk.ErrInterrupted: - errToReturn = topo.ErrInterrupted case zk.ErrTimeout: errToReturn = topo.ErrTimeout + case zk.ErrInterrupted: + // the context failed, get the error from it + if ctx.Err() == context.DeadlineExceeded { + errToReturn = topo.ErrTimeout + } else { + errToReturn = topo.ErrInterrupted + } default: errToReturn = fmt.Errorf("failed to obtain action lock: %v %v", actionPath, err) } @@ -84,42 +106,42 @@ func (zkts *Server) unlockForAction(lockPath, results string) error { return zk.DeleteRecursive(zkts.zconn, lockPath, -1) } -func (zkts *Server) LockKeyspaceForAction(keyspace, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +func (zkts *Server) LockKeyspaceForAction(ctx context.Context, keyspace, contents string) (string, error) { // Action paths end in a trailing slash to that when we create // sequential nodes, they are created as children, not siblings. actionDir := path.Join(globalKeyspacesPath, keyspace, "action") + "/" - return zkts.lockForAction(actionDir, contents, timeout, interrupted) + return zkts.lockForAction(ctx, actionDir, contents) } func (zkts *Server) UnlockKeyspaceForAction(keyspace, lockPath, results string) error { return zkts.unlockForAction(lockPath, results) } -func (zkts *Server) LockShardForAction(keyspace, shard, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +func (zkts *Server) LockShardForAction(ctx context.Context, keyspace, shard, contents string) (string, error) { // Action paths end in a trailing slash to that when we create // sequential nodes, they are created as children, not siblings. actionDir := path.Join(globalKeyspacesPath, keyspace, "shards", shard, "action") + "/" - return zkts.lockForAction(actionDir, contents, timeout, interrupted) + return zkts.lockForAction(ctx, actionDir, contents) } func (zkts *Server) UnlockShardForAction(keyspace, shard, lockPath, results string) error { return zkts.unlockForAction(lockPath, results) } -func (zkts *Server) LockSrvShardForAction(cell, keyspace, shard, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +func (zkts *Server) LockSrvShardForAction(ctx context.Context, cell, keyspace, shard, contents string) (string, error) { // Action paths end in a trailing slash to that when we create // sequential nodes, they are created as children, not siblings. actionDir := path.Join(zkPathForVtShard(cell, keyspace, shard), "action") // if we can't create the lock file because the directory doesn't exist, // create it - p, err := zkts.lockForAction(actionDir+"/", contents, timeout, interrupted) + p, err := zkts.lockForAction(ctx, actionDir+"/", contents) if err != nil && zookeeper.IsError(err, zookeeper.ZNONODE) { _, err = zk.CreateRecursive(zkts.zconn, actionDir, "", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) if err != nil && !zookeeper.IsError(err, zookeeper.ZNODEEXISTS) { return "", err } - p, err = zkts.lockForAction(actionDir+"/", contents, timeout, interrupted) + p, err = zkts.lockForAction(ctx, actionDir+"/", contents) } return p, err } diff --git a/go/vt/zktopo/server.go b/go/vt/zktopo/server.go index 0c129ff3277..bd187702248 100644 --- a/go/vt/zktopo/server.go +++ b/go/vt/zktopo/server.go @@ -8,7 +8,6 @@ import ( "fmt" "path" "sort" - "time" "github.com/youtube/vitess/go/stats" "github.com/youtube/vitess/go/vt/topo" @@ -21,10 +20,12 @@ type Server struct { zconn zk.Conn } +// Close is part of topo.Server interface. func (zkts *Server) Close() { zkts.zconn.Close() } +// GetZConn returns the zookeeper connection for this Server. func (zkts *Server) GetZConn() zk.Conn { return zkts.zconn } @@ -37,7 +38,7 @@ func NewServer(zconn zk.Conn) *Server { } func init() { - zconn := zk.NewMetaConn(false) + zconn := zk.NewMetaConn() stats.PublishJSONFunc("ZkMetaConn", zconn.String) topo.RegisterServer("zookeeper", NewServer(zconn)) } @@ -46,10 +47,6 @@ func init() { // These helper methods are for ZK specific things // -func (zkts *Server) ShardActionPath(keyspace, shard string) string { - return "/zk/global/vt/keyspaces/" + keyspace + "/shards/" + shard + "/action" -} - // PurgeActions removes all queued actions, leaving the action node // itself in place. // @@ -87,37 +84,6 @@ func (zkts *Server) PurgeActions(zkActionPath string, canBePurged func(data stri return nil } -// StaleActions returns a list of queued actions that have been -// sitting for more than some amount of time. -func (zkts *Server) StaleActions(zkActionPath string, maxStaleness time.Duration, isStale func(data string) bool) ([]string, error) { - if path.Base(zkActionPath) != "action" { - return nil, fmt.Errorf("not action path: %v", zkActionPath) - } - - children, _, err := zkts.zconn.Children(zkActionPath) - if err != nil { - return nil, err - } - - staleActions := make([]string, 0, 16) - // Purge newer items first so the action queues don't try to process something. - sort.Strings(children) - for i := 0; i < len(children); i++ { - actionPath := path.Join(zkActionPath, children[i]) - data, stat, err := zkts.zconn.Get(actionPath) - if err != nil && !zookeeper.IsError(err, zookeeper.ZNONODE) { - return nil, fmt.Errorf("stale action err: %v", err) - } - if stat == nil || time.Since(stat.MTime()) <= maxStaleness { - continue - } - if isStale(data) { - staleActions = append(staleActions, data) - } - } - return staleActions, nil -} - // PruneActionLogs prunes old actionlog entries. Returns how many // entries were purged (even if there was an error). // diff --git a/go/vt/zktopo/serving_graph.go b/go/vt/zktopo/serving_graph.go index 33f365ffc94..3f5e894df08 100644 --- a/go/vt/zktopo/serving_graph.go +++ b/go/vt/zktopo/serving_graph.go @@ -9,13 +9,21 @@ import ( "fmt" "path" "sort" + "time" + log "github.com/golang/glog" "github.com/youtube/vitess/go/jscfg" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/zk" "launchpad.net/gozk/zookeeper" ) +// WatchSleepDuration is how many seconds interval to poll for in case +// the directory that contains a file to watch doesn't exist, or a watch +// is broken. It is exported so individual test and main programs +// can change it. +var WatchSleepDuration = 30 * time.Second + /* This file contains the serving graph management code of zktopo.Server */ @@ -35,6 +43,7 @@ func zkPathForVtName(cell, keyspace, shard string, tabletType topo.TabletType) s return path.Join(zkPathForVtShard(cell, keyspace, shard), string(tabletType)) } +// GetSrvTabletTypesPerShard is part of the topo.Server interface func (zkts *Server) GetSrvTabletTypesPerShard(cell, keyspace, shard string) ([]topo.TabletType, error) { zkSgShardPath := zkPathForVtShard(cell, keyspace, shard) children, _, err := zkts.zconn.Children(zkSgShardPath) @@ -55,6 +64,7 @@ func (zkts *Server) GetSrvTabletTypesPerShard(cell, keyspace, shard string) ([]t return result, nil } +// UpdateEndPoints is part of the topo.Server interface func (zkts *Server) UpdateEndPoints(cell, keyspace, shard string, tabletType topo.TabletType, addrs *topo.EndPoints) error { path := zkPathForVtName(cell, keyspace, shard, tabletType) data := jscfg.ToJson(addrs) @@ -72,6 +82,7 @@ func (zkts *Server) UpdateEndPoints(cell, keyspace, shard string, tabletType top return err } +// GetEndPoints is part of the topo.Server interface func (zkts *Server) GetEndPoints(cell, keyspace, shard string, tabletType topo.TabletType) (*topo.EndPoints, error) { path := zkPathForVtName(cell, keyspace, shard, tabletType) data, _, err := zkts.zconn.Get(path) @@ -90,6 +101,7 @@ func (zkts *Server) GetEndPoints(cell, keyspace, shard string, tabletType topo.T return result, nil } +// DeleteEndPoints is part of the topo.Server interface func (zkts *Server) DeleteEndPoints(cell, keyspace, shard string, tabletType topo.TabletType) error { path := zkPathForVtName(cell, keyspace, shard, tabletType) err := zkts.zconn.Delete(path, -1) @@ -102,6 +114,7 @@ func (zkts *Server) DeleteEndPoints(cell, keyspace, shard string, tabletType top return nil } +// UpdateSrvShard is part of the topo.Server interface func (zkts *Server) UpdateSrvShard(cell, keyspace, shard string, srvShard *topo.SrvShard) error { path := zkPathForVtShard(cell, keyspace, shard) data := jscfg.ToJson(srvShard) @@ -109,6 +122,7 @@ func (zkts *Server) UpdateSrvShard(cell, keyspace, shard string, srvShard *topo. return err } +// GetSrvShard is part of the topo.Server interface func (zkts *Server) GetSrvShard(cell, keyspace, shard string) (*topo.SrvShard, error) { path := zkPathForVtShard(cell, keyspace, shard) data, stat, err := zkts.zconn.Get(path) @@ -127,6 +141,7 @@ func (zkts *Server) GetSrvShard(cell, keyspace, shard string) (*topo.SrvShard, e return srvShard, nil } +// DeleteSrvShard is part of the topo.Server interface func (zkts *Server) DeleteSrvShard(cell, keyspace, shard string) error { path := zkPathForVtShard(cell, keyspace, shard) err := zkts.zconn.Delete(path, -1) @@ -139,6 +154,7 @@ func (zkts *Server) DeleteSrvShard(cell, keyspace, shard string) error { return nil } +// UpdateSrvKeyspace is part of the topo.Server interface func (zkts *Server) UpdateSrvKeyspace(cell, keyspace string, srvKeyspace *topo.SrvKeyspace) error { path := zkPathForVtKeyspace(cell, keyspace) data := jscfg.ToJson(srvKeyspace) @@ -149,6 +165,7 @@ func (zkts *Server) UpdateSrvKeyspace(cell, keyspace string, srvKeyspace *topo.S return err } +// GetSrvKeyspace is part of the topo.Server interface func (zkts *Server) GetSrvKeyspace(cell, keyspace string) (*topo.SrvKeyspace, error) { path := zkPathForVtKeyspace(cell, keyspace) data, stat, err := zkts.zconn.Get(path) @@ -167,6 +184,7 @@ func (zkts *Server) GetSrvKeyspace(cell, keyspace string) (*topo.SrvKeyspace, er return srvKeyspace, nil } +// GetSrvKeyspaceNames is part of the topo.Server interface func (zkts *Server) GetSrvKeyspaceNames(cell string) ([]string, error) { children, _, err := zkts.zconn.Children(zkPathForCell(cell)) if err != nil { @@ -180,14 +198,14 @@ func (zkts *Server) GetSrvKeyspaceNames(cell string) ([]string, error) { return children, nil } -var skipUpdateErr = fmt.Errorf("skip update") +var errSkipUpdate = fmt.Errorf("skip update") func (zkts *Server) updateTabletEndpoint(oldValue string, oldStat zk.Stat, addr *topo.EndPoint) (newValue string, err error) { if oldStat == nil { // The incoming object doesn't exist - we haven't been placed in the serving // graph yet, so don't update. Assume the next process that rebuilds the graph // will get the updated tablet location. - return "", skipUpdateErr + return "", errSkipUpdate } var addrs *topo.EndPoints @@ -220,14 +238,95 @@ func (zkts *Server) updateTabletEndpoint(oldValue string, oldStat zk.Stat, addr return jscfg.ToJson(addrs), nil } +// UpdateTabletEndpoint is part of the topo.Server interface func (zkts *Server) UpdateTabletEndpoint(cell, keyspace, shard string, tabletType topo.TabletType, addr *topo.EndPoint) error { path := zkPathForVtName(cell, keyspace, shard, tabletType) f := func(oldValue string, oldStat zk.Stat) (string, error) { return zkts.updateTabletEndpoint(oldValue, oldStat, addr) } err := zkts.zconn.RetryChange(path, 0, zookeeper.WorldACL(zookeeper.PERM_ALL), f) - if err == skipUpdateErr || zookeeper.IsError(err, zookeeper.ZNONODE) { + if err == errSkipUpdate || zookeeper.IsError(err, zookeeper.ZNONODE) { err = nil } return err } + +// WatchEndPoints is part of the topo.Server interface +func (zkts *Server) WatchEndPoints(cell, keyspace, shard string, tabletType topo.TabletType) (<-chan *topo.EndPoints, chan<- struct{}, error) { + filePath := zkPathForVtName(cell, keyspace, shard, tabletType) + + notifications := make(chan *topo.EndPoints, 10) + stopWatching := make(chan struct{}) + + // waitOrInterrupted will return true if stopWatching is triggered + waitOrInterrupted := func() bool { + timer := time.After(WatchSleepDuration) + select { + case <-stopWatching: + close(notifications) + return true + case <-timer: + } + return false + } + + go func() { + for { + // set the watch + data, _, watch, err := zkts.zconn.GetW(filePath) + if err != nil { + if zookeeper.IsError(err, zookeeper.ZNONODE) { + // the parent directory doesn't exist + notifications <- nil + } + + log.Errorf("Cannot set watch on %v, waiting for %v to retry: %v", filePath, WatchSleepDuration, err) + if waitOrInterrupted() { + return + } + continue + } + + // get the initial value, send it, or send nil if no + // data + var ep *topo.EndPoints + sendIt := true + if len(data) > 0 { + ep = &topo.EndPoints{} + if err := json.Unmarshal([]byte(data), ep); err != nil { + log.Errorf("EndPoints unmarshal failed: %v %v", data, err) + sendIt = false + } + } + if sendIt { + notifications <- ep + } + + // now act on the watch + select { + case event, ok := <-watch: + if !ok { + log.Warningf("watch on %v was closed, waiting for %v to retry", filePath, WatchSleepDuration) + if waitOrInterrupted() { + return + } + continue + } + + if !event.Ok() { + log.Warningf("received a non-OK event for %v, waiting for %v to retry", filePath, WatchSleepDuration) + if waitOrInterrupted() { + return + } + } + case <-stopWatching: + // user is not interested any more + close(notifications) + return + } + } + }() + + return notifications, stopWatching, nil + +} diff --git a/go/vt/zktopo/tablet.go b/go/vt/zktopo/tablet.go index 7e5d2a224df..3369f2f20b7 100644 --- a/go/vt/zktopo/tablet.go +++ b/go/vt/zktopo/tablet.go @@ -7,7 +7,6 @@ package zktopo import ( "encoding/json" "fmt" - "path" "sort" "github.com/youtube/vitess/go/event" @@ -22,19 +21,16 @@ import ( This file contains the tablet management parts of zktopo.Server */ +// TabletPathForAlias converts a tablet alias to the zk path func TabletPathForAlias(alias topo.TabletAlias) string { - return fmt.Sprintf("/zk/%v/vt/tablets/%v", alias.Cell, alias.TabletUidStr()) -} - -func TabletActionPathForAlias(alias topo.TabletAlias) string { - return fmt.Sprintf("/zk/%v/vt/tablets/%v/action", alias.Cell, alias.TabletUidStr()) + return fmt.Sprintf("/zk/%v/vt/tablets/%v", alias.Cell, alias.TabletUIDStr()) } func tabletDirectoryForCell(cell string) string { return fmt.Sprintf("/zk/%v/vt/tablets", cell) } -func tabletFromJson(data string) (*topo.Tablet, error) { +func tabletFromJSON(data string) (*topo.Tablet, error) { t := &topo.Tablet{} err := json.Unmarshal([]byte(data), t) if err != nil { @@ -43,19 +39,20 @@ func tabletFromJson(data string) (*topo.Tablet, error) { return t, nil } -func tabletInfoFromJson(data string, version int64) (*topo.TabletInfo, error) { - tablet, err := tabletFromJson(data) +func tabletInfoFromJSON(data string, version int64) (*topo.TabletInfo, error) { + tablet, err := tabletFromJSON(data) if err != nil { return nil, err } return topo.NewTabletInfo(tablet, version), nil } +// CreateTablet is part of the topo.Server interface func (zkts *Server) CreateTablet(tablet *topo.Tablet) error { zkTabletPath := TabletPathForAlias(tablet.Alias) // Create /zk//vt/tablets/ - _, err := zk.CreateRecursive(zkts.zconn, zkTabletPath, tablet.Json(), 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) + _, err := zk.CreateRecursive(zkts.zconn, zkTabletPath, tablet.JSON(), 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) if err != nil { if zookeeper.IsError(err, zookeeper.ZNODEEXISTS) { err = topo.ErrNodeExists @@ -63,20 +60,6 @@ func (zkts *Server) CreateTablet(tablet *topo.Tablet) error { return err } - // Create /zk//vt/tablets//action - tap := path.Join(zkTabletPath, "action") - _, err = zkts.zconn.Create(tap, "", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) - if err != nil { - return err - } - - // Create /zk//vt/tablets//actionlog - talp := path.Join(zkTabletPath, "actionlog") - _, err = zkts.zconn.Create(talp, "", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)) - if err != nil { - return err - } - event.Dispatch(&events.TabletChange{ Tablet: *tablet, Status: "created", @@ -84,9 +67,10 @@ func (zkts *Server) CreateTablet(tablet *topo.Tablet) error { return nil } +// UpdateTablet is part of the topo.Server interface func (zkts *Server) UpdateTablet(tablet *topo.TabletInfo, existingVersion int64) (int64, error) { zkTabletPath := TabletPathForAlias(tablet.Alias) - stat, err := zkts.zconn.Set(zkTabletPath, tablet.Json(), int(existingVersion)) + stat, err := zkts.zconn.Set(zkTabletPath, tablet.JSON(), int(existingVersion)) if err != nil { if zookeeper.IsError(err, zookeeper.ZBADVERSION) { err = topo.ErrBadVersion @@ -104,6 +88,7 @@ func (zkts *Server) UpdateTablet(tablet *topo.TabletInfo, existingVersion int64) return int64(stat.Version()), nil } +// UpdateTabletFields is part of the topo.Server interface func (zkts *Server) UpdateTabletFields(tabletAlias topo.TabletAlias, update func(*topo.Tablet) error) error { // Store the last tablet value so we can log it if the change succeeds. var lastTablet *topo.Tablet @@ -114,7 +99,7 @@ func (zkts *Server) UpdateTabletFields(tabletAlias topo.TabletAlias, update func return "", fmt.Errorf("no data for tablet addr update: %v", tabletAlias) } - tablet, err := tabletFromJson(oldValue) + tablet, err := tabletFromJSON(oldValue) if err != nil { return "", err } @@ -141,6 +126,7 @@ func (zkts *Server) UpdateTabletFields(tabletAlias topo.TabletAlias, update func return nil } +// DeleteTablet is part of the topo.Server interface func (zkts *Server) DeleteTablet(alias topo.TabletAlias) error { // We need to find out the keyspace and shard names because those are required // in the TabletChange event. @@ -171,21 +157,7 @@ func (zkts *Server) DeleteTablet(alias topo.TabletAlias) error { return nil } -func (zkts *Server) ValidateTablet(alias topo.TabletAlias) error { - zkTabletPath := TabletPathForAlias(alias) - zkPaths := []string{ - path.Join(zkTabletPath, "action"), - path.Join(zkTabletPath, "actionlog"), - } - - for _, zkPath := range zkPaths { - if _, _, err := zkts.zconn.Get(zkPath); err != nil { - return err - } - } - return nil -} - +// GetTablet is part of the topo.Server interface func (zkts *Server) GetTablet(alias topo.TabletAlias) (*topo.TabletInfo, error) { zkTabletPath := TabletPathForAlias(alias) data, stat, err := zkts.zconn.Get(zkTabletPath) @@ -195,9 +167,10 @@ func (zkts *Server) GetTablet(alias topo.TabletAlias) (*topo.TabletInfo, error) } return nil, err } - return tabletInfoFromJson(data, int64(stat.Version())) + return tabletInfoFromJSON(data, int64(stat.Version())) } +// GetTabletsByCell is part of the topo.Server interface func (zkts *Server) GetTabletsByCell(cell string) ([]topo.TabletAlias, error) { zkTabletsPath := tabletDirectoryForCell(cell) children, _, err := zkts.zconn.Children(zkTabletsPath) @@ -212,7 +185,7 @@ func (zkts *Server) GetTabletsByCell(cell string) ([]topo.TabletAlias, error) { result := make([]topo.TabletAlias, len(children)) for i, child := range children { result[i].Cell = cell - result[i].Uid, err = topo.ParseUid(child) + result[i].Uid, err = topo.ParseUID(child) if err != nil { return nil, err } diff --git a/go/vt/zktopo/testserver.go b/go/vt/zktopo/testserver.go index 884fae7b2ed..14b3391b4b8 100644 --- a/go/vt/zktopo/testserver.go +++ b/go/vt/zktopo/testserver.go @@ -3,11 +3,11 @@ package zktopo import ( "fmt" "testing" - "time" "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/zk" "github.com/youtube/vitess/go/zk/fakezk" + "golang.org/x/net/context" "launchpad.net/gozk/zookeeper" ) @@ -39,9 +39,23 @@ func (s *TestServer) GetKnownCells() ([]string, error) { // LockSrvShardForAction should override the function defined by the underlying // topo.Server. -func (s *TestServer) LockSrvShardForAction(cell, keyspace, shard, contents string, timeout time.Duration, interrupted chan struct{}) (string, error) { +func (s *TestServer) LockSrvShardForAction(ctx context.Context, cell, keyspace, shard, contents string) (string, error) { if s.HookLockSrvShardForAction != nil { s.HookLockSrvShardForAction() } - return s.Server.LockSrvShardForAction(cell, keyspace, shard, contents, timeout, interrupted) + return s.Server.LockSrvShardForAction(ctx, cell, keyspace, shard, contents) +} + +// TODO(sougou): Remove these two functions after they're +// migrated into topo.Server. +// SaveVSchema has to be redefined here. +// Otherwise the test type assertion fails. +func (s *TestServer) SaveVSchema(vschema string) error { + return s.Server.(topo.Schemafier).SaveVSchema(vschema) +} + +// GetVSchema has to be redefined here. +// Otherwise the test type assertion fails. +func (s *TestServer) GetVSchema() (string, error) { + return s.Server.(topo.Schemafier).GetVSchema() } diff --git a/go/vt/zktopo/testserver_test.go b/go/vt/zktopo/testserver_test.go index 85d7544736e..01eacc7bc19 100644 --- a/go/vt/zktopo/testserver_test.go +++ b/go/vt/zktopo/testserver_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/youtube/vitess/go/vt/topo" + "golang.org/x/net/context" ) // TestHookLockSrvShardForAction makes sure that changes to the upstream @@ -23,7 +24,8 @@ func TestHookLockSrvShardForAction(t *testing.T) { triggered = true } - topo.Server(ts).LockSrvShardForAction(cells[0], "keyspace", "shard", "contents", 0, nil) + ctx := context.Background() + topo.Server(ts).LockSrvShardForAction(ctx, cells[0], "keyspace", "shard", "contents") if !triggered { t.Errorf("HookLockSrvShardForAction wasn't triggered") diff --git a/go/vt/zktopo/vschema.go b/go/vt/zktopo/vschema.go new file mode 100644 index 00000000000..33cbe604467 --- /dev/null +++ b/go/vt/zktopo/vschema.go @@ -0,0 +1,45 @@ +// Copyright 2015, Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package zktopo + +import ( + "github.com/youtube/vitess/go/vt/vtgate/planbuilder" + // vindexes needs to be imported so that they register + // themselves against vtgate/planbuilder. This will allow + // us to sanity check the schema being uploaded. + _ "github.com/youtube/vitess/go/vt/vtgate/vindexes" + "github.com/youtube/vitess/go/zk" + "launchpad.net/gozk/zookeeper" +) + +/* +This file contains the vschema management code for zktopo.Server +*/ + +const ( + globalVSchemaPath = "/zk/global/vt/vschema" +) + +// SaveVSchema saves the JSON vschema into the topo. +func (zkts *Server) SaveVSchema(vschema string) error { + _, err := planbuilder.NewSchema([]byte(vschema)) + if err != nil { + return err + } + _, err = zk.CreateOrUpdate(zkts.zconn, globalVSchemaPath, vschema, 0, zookeeper.WorldACL(zookeeper.PERM_ALL), true) + return err +} + +// GetVSchema fetches the JSON vschema from the topo. +func (zkts *Server) GetVSchema() (string, error) { + data, _, err := zkts.zconn.Get(globalVSchemaPath) + if err != nil { + if zookeeper.IsError(err, zookeeper.ZNONODE) { + return "{}", nil + } + return "", err + } + return data, nil +} diff --git a/go/vt/zktopo/zktopo_test.go b/go/vt/zktopo/zktopo_test.go index e779cee4a00..de4cb09d166 100644 --- a/go/vt/zktopo/zktopo_test.go +++ b/go/vt/zktopo/zktopo_test.go @@ -1,11 +1,16 @@ package zktopo import ( + "path" "testing" + "time" - "code.google.com/p/go.net/context" + "golang.org/x/net/context" + "github.com/youtube/vitess/go/vt/topo" "github.com/youtube/vitess/go/vt/topo/test" + "github.com/youtube/vitess/go/zk" + "launchpad.net/gozk/zookeeper" ) func TestKeyspace(t *testing.T) { @@ -17,7 +22,7 @@ func TestKeyspace(t *testing.T) { func TestShard(t *testing.T) { ts := NewTestServer(t, []string{"test"}) defer ts.Close() - test.CheckShard(t, ts) + test.CheckShard(context.Background(), t, ts) } func TestTablet(t *testing.T) { @@ -35,7 +40,14 @@ func TestShardReplication(t *testing.T) { func TestServingGraph(t *testing.T) { ts := NewTestServer(t, []string{"test"}) defer ts.Close() - test.CheckServingGraph(t, ts) + test.CheckServingGraph(context.Background(), t, ts) +} + +func TestWatchEndPoints(t *testing.T) { + WatchSleepDuration = 2 * time.Millisecond + ts := NewTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckWatchEndPoints(context.Background(), t, ts) } func TestKeyspaceLock(t *testing.T) { @@ -64,8 +76,68 @@ func TestSrvShardLock(t *testing.T) { test.CheckSrvShardLock(t, ts) } -func TestPid(t *testing.T) { +func TestVSchema(t *testing.T) { + ts := NewTestServer(t, []string{"test"}) + defer ts.Close() + test.CheckVSchema(t, ts) +} + +// TestPurgeActions is a ZK specific unit test +func TestPurgeActions(t *testing.T) { + ts := NewTestServer(t, []string{"test"}) + defer ts.Close() + + if err := ts.CreateKeyspace("test_keyspace", &topo.Keyspace{}); err != nil { + t.Fatalf("CreateKeyspace: %v", err) + } + + actionPath := path.Join(globalKeyspacesPath, "test_keyspace", "action") + zkts := ts.Server.(*Server) + + if _, err := zk.CreateRecursive(zkts.zconn, actionPath+"/topurge", "purgeme", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)); err != nil { + t.Fatalf("CreateRecursive(topurge): %v", err) + } + if _, err := zk.CreateRecursive(zkts.zconn, actionPath+"/tokeep", "keepme", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)); err != nil { + t.Fatalf("CreateRecursive(tokeep): %v", err) + } + + if err := zkts.PurgeActions(actionPath, func(data string) bool { + return data == "purgeme" + }); err != nil { + t.Fatalf("PurgeActions(tokeep): %v", err) + } + + actions, _, err := zkts.zconn.Children(actionPath) + if err != nil || len(actions) != 1 || actions[0] != "tokeep" { + t.Errorf("PurgeActions kept the wrong things: %v %v", err, actions) + } +} + +// TestPruneActionLogs is a ZK specific unit test +func TestPruneActionLogs(t *testing.T) { ts := NewTestServer(t, []string{"test"}) defer ts.Close() - test.CheckPid(t, ts) + + if err := ts.CreateKeyspace("test_keyspace", &topo.Keyspace{}); err != nil { + t.Fatalf("CreateKeyspace: %v", err) + } + + actionLogPath := path.Join(globalKeyspacesPath, "test_keyspace", "actionlog") + zkts := ts.Server.(*Server) + + if _, err := zk.CreateRecursive(zkts.zconn, actionLogPath+"/0", "first", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)); err != nil { + t.Fatalf("CreateRecursive(stale): %v", err) + } + if _, err := zk.CreateRecursive(zkts.zconn, actionLogPath+"/1", "second", 0, zookeeper.WorldACL(zookeeper.PERM_ALL)); err != nil { + t.Fatalf("CreateRecursive(fresh): %v", err) + } + + if count, err := zkts.PruneActionLogs(actionLogPath, 1); err != nil || count != 1 { + t.Fatalf("PruneActionLogs: %v %v", err, count) + } + + actionLogs, _, err := zkts.zconn.Children(actionLogPath) + if err != nil || len(actionLogs) != 1 || actionLogs[0] != "1" { + t.Errorf("PruneActionLogs kept the wrong things: %v %v", err, actionLogs) + } } diff --git a/go/zk/config.go b/go/zk/config.go index 1d624191c26..de070d58651 100644 --- a/go/zk/config.go +++ b/go/zk/config.go @@ -28,7 +28,7 @@ var ( localCell = flag.String("zk.local-cell", "", "closest zk cell used for /zk/local paths") localAddrs = flag.String("zk.local-addrs", "", "list of zookeeper servers (host:port, ...)") globalAddrs = flag.String("zk.global-addrs", "", "list of global zookeeper servers (host:port, ...)") - baseTimeout = flag.Duration("zk.base-timeout", DEFAULT_BASE_TIMEOUT, "zk or zkocc base timeout (see zkconn.go and zkoccconn.go)") + baseTimeout = flag.Duration("zk.base-timeout", DEFAULT_BASE_TIMEOUT, "zk base timeout (see zkconn.go)") connectTimeout = flag.Duration("zk.connect-timeout", 30*time.Second, "zk connect timeout") ) @@ -69,6 +69,7 @@ func letterPrefix(str string) string { return str } +// ZkCellFromZkPath extracts the cell name from a zkPath. func ZkCellFromZkPath(zkPath string) (string, error) { pathParts := strings.Split(zkPath, "/") if len(pathParts) < 3 { @@ -78,8 +79,8 @@ func ZkCellFromZkPath(zkPath string) (string, error) { return "", fmt.Errorf("path should start with /%v: %v", MagicPrefix, zkPath) } cell := pathParts[2] - if strings.Contains(cell, "-") { - return "", fmt.Errorf("invalid cell name %v", cell) + if cell == "" || strings.Contains(cell, "-") { + return "", fmt.Errorf("invalid cell name %q", cell) } return cell, nil } @@ -110,7 +111,7 @@ func getCellAddrMap() (map[string]string, error) { return nil, fmt.Errorf("no config file paths found") } -func ZkPathToZkAddr(zkPath string, useCache bool) (string, error) { +func ZkPathToZkAddr(zkPath string) (string, error) { cell, err := ZkCellFromZkPath(zkPath) if err != nil { return "", err @@ -131,9 +132,6 @@ func ZkPathToZkAddr(zkPath string, useCache bool) (string, error) { cell = GuessLocalCell() + "-global" } } - if useCache { - cell += ":_zkocc" - } addr := cellAddrMap[cell] if addr != "" { @@ -145,7 +143,7 @@ func ZkPathToZkAddr(zkPath string, useCache bool) (string, error) { // returns all the known cells, alphabetically ordered. It will // include 'global' if there is a dc-specific global cell or a global cell -func ZkKnownCells(useCache bool) ([]string, error) { +func ZkKnownCells() ([]string, error) { localCell := GuessLocalCell() cellAddrMap, err := getCellAddrMap() if err != nil { @@ -154,34 +152,22 @@ func ZkKnownCells(useCache bool) ([]string, error) { result := make([]string, 0, len(cellAddrMap)) foundGlobal := false for cell := range cellAddrMap { - // figure out cell name and if it's zkocc - isZkoccCell := false - name := cell - if strings.HasSuffix(name, ":_zkocc") { - name = name[:len(name)-7] - isZkoccCell = true - } - if (useCache && !isZkoccCell) || (!useCache && isZkoccCell) { - // this is not the zkocc you're looking for - continue - } - // handle global, we just remember it - if name == "global" { + if cell == "global" { foundGlobal = true continue } // skip global cells, remember if we have our global - if strings.HasSuffix(name, "-global") { - if name == localCell+"-global" { + if strings.HasSuffix(cell, "-global") { + if cell == localCell+"-global" { foundGlobal = true } continue } // non-global cell - result = append(result, name) + result = append(result, cell) } if foundGlobal { result = append(result, "global") diff --git a/go/zk/config_test.go b/go/zk/config_test.go index 32139ee17e6..c4c5b1ed71b 100644 --- a/go/zk/config_test.go +++ b/go/zk/config_test.go @@ -29,9 +29,8 @@ func TestZkConfig(t *testing.T) { t.Logf("fakeAddr: %v", fakeAddr) configMap := map[string]string{ - fakeCell: fakeAddr, - fakeCell + "z:_zkocc": "localhost:2182", - fakeCell + "-global": "localhost:2183", + fakeCell: fakeAddr, + fakeCell + "-global": "localhost:2183", } t.Logf("configMap: %+v", configMap) @@ -52,7 +51,7 @@ func TestZkConfig(t *testing.T) { // test ZkPathToZkAddr for _, path := range []string{"/zk/" + fakeCell, "/zk/" + fakeCell + "/", "/zk/local", "/zk/local/"} { - zkAddr, err := ZkPathToZkAddr(path, false) + zkAddr, err := ZkPathToZkAddr(path) if err != nil { t.Errorf("ZkPathToZkAddr(%v, false): %v", path, err.Error()) } @@ -62,7 +61,7 @@ func TestZkConfig(t *testing.T) { } // test ZkKnownCells - knownCells, err := ZkKnownCells(false) + knownCells, err := ZkKnownCells() if err != nil { t.Errorf("unexpected error from ZkKnownCells(): %v", err) } @@ -71,11 +70,21 @@ func TestZkConfig(t *testing.T) { if len(knownCells) != 2 || knownCells[0] != expectedKnownCells[0] || knownCells[1] != expectedKnownCells[1] { t.Errorf("ZkKnownCells(false) failed, expected %v got %v", expectedKnownCells, knownCells) } - knownCells, err = ZkKnownCells(true) - if err != nil { - t.Errorf("unexpected error from ZkKnownCells(): %v", err) +} + +func TestZkCellFromZkPathInvalid(t *testing.T) { + // The following paths should be rejected so the invalid cell name doesn't + // cause problems down the line. + inputs := []string{ + "/zk", + "bad/zk/path", + "/wrongprefix/cell", + "/zk//emptycellname", + "/zk/bad-cell-name/", } - if len(knownCells) != 1 || knownCells[0] != fakeCell+"z" { - t.Errorf("ZkKnownCells(true) failed, expected %v got %v", []string{fakeCell}, knownCells) + for _, input := range inputs { + if _, err := ZkCellFromZkPath(input); err == nil { + t.Errorf("expected error for ZkCellFromZkPath(%q), got none", input) + } } } diff --git a/go/zk/conn_cache.go b/go/zk/conn_cache.go index 8a5ec7be3ba..8f6dc08af57 100644 --- a/go/zk/conn_cache.go +++ b/go/zk/conn_cache.go @@ -47,7 +47,6 @@ type cachedConn struct { type ConnCache struct { mutex sync.Mutex zconnCellMap map[string]*cachedConn // map cell name to connection - useZkocc bool } func (cc *ConnCache) setState(zcell string, conn *cachedConn, state int64) { @@ -86,17 +85,13 @@ func (cc *ConnCache) ConnForPath(zkPath string) (cn Conn, err error) { return conn.zconn, nil } - zkAddr, err := ZkPathToZkAddr(zkPath, cc.useZkocc) + zkAddr, err := ZkPathToZkAddr(zkPath) if err != nil { return nil, &zookeeper.Error{Op: "dial", Code: zookeeper.ZSYSTEMERROR, SystemError: err, Path: zkPath} } cc.setState(zcell, conn, CONNECTING) - if cc.useZkocc { - conn.zconn, err = DialZkocc(zkAddr, *baseTimeout) - } else { - conn.zconn, err = cc.newZookeeperConn(zkAddr, zcell) - } + conn.zconn, err = cc.newZookeeperConn(zkAddr, zcell) if conn.zconn != nil { cc.setState(zcell, conn, CONNECTED) } else { @@ -187,8 +182,8 @@ func (cc *ConnCache) String() string { return b.String() } -func NewConnCache(useZkocc bool) *ConnCache { +func NewConnCache() *ConnCache { return &ConnCache{ zconnCellMap: make(map[string]*cachedConn), - useZkocc: useZkocc} + } } diff --git a/go/zk/fakezk/fakezk.go b/go/zk/fakezk/fakezk.go index 31bccb520dc..142da94a0fc 100644 --- a/go/zk/fakezk/fakezk.go +++ b/go/zk/fakezk/fakezk.go @@ -229,7 +229,6 @@ func (conn *zconn) Create(zkPath, value string, flags int, aclv []zookeeper.ACL) delete(conn.existWatches, zkPath) for _, watch := range watches { watch <- event - } } childrenEvent := zookeeper.Event{ diff --git a/go/zk/global.go b/go/zk/global.go index 41e4d604531..a2de2b73f56 100644 --- a/go/zk/global.go +++ b/go/zk/global.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -/* Emulate a "global" namespace across n zk quorums. */ +// Package zk emulates a "global" namespace across n zk quorums. package zk import ( diff --git a/go/zk/metaconn.go b/go/zk/metaconn.go index 54000c9e4cb..ced3e2b9507 100644 --- a/go/zk/metaconn.go +++ b/go/zk/metaconn.go @@ -134,7 +134,7 @@ func (conn *MetaConn) Children(path string) (children []string, stat Stat, err e if path == ("/" + MagicPrefix) { // NOTE(msolo) There is a slight hack there - but there really is // no valid stat for the top level path. - children, err = ZkKnownCells(false) + children, err = ZkKnownCells() return } var zconn Conn @@ -275,6 +275,6 @@ func (conn *MetaConn) String() string { return conn.connCache.String() } -func NewMetaConn(useZkocc bool) *MetaConn { - return &MetaConn{NewConnCache(useZkocc)} +func NewMetaConn() *MetaConn { + return &MetaConn{NewConnCache()} } diff --git a/go/zk/zkctl/zkctl.go b/go/zk/zkctl/zkctl.go index d8cf7ea25bd..d85a321cb0b 100644 --- a/go/zk/zkctl/zkctl.go +++ b/go/zk/zkctl/zkctl.go @@ -123,23 +123,17 @@ func (zkd *Zkd) Shutdown() error { if err != nil { return err } - err = syscall.Kill(pid, 9) + err = syscall.Kill(pid, syscall.SIGKILL) if err != nil && err != syscall.ESRCH { return err } - proc, _ := os.FindProcess(pid) - processIsDead := false for i := 0; i < ShutdownWaitTime; i++ { - if proc.Signal(syscall.Signal(0)) == syscall.ESRCH { - processIsDead = true - break + if syscall.Kill(pid, syscall.SIGKILL) == syscall.ESRCH { + return nil } time.Sleep(time.Second) } - if !processIsDead { - return fmt.Errorf("Shutdown didn't kill process %v", pid) - } - return nil + return fmt.Errorf("Shutdown didn't kill process %v", pid) } func (zkd *Zkd) makeCfg() (string, error) { diff --git a/go/zk/zkctl/zkctl_test.go b/go/zk/zkctl/zkctl_test.go index 7dee25c07ff..e0aa604e6b4 100644 --- a/go/zk/zkctl/zkctl_test.go +++ b/go/zk/zkctl/zkctl_test.go @@ -42,7 +42,7 @@ func TestLifeCycleGlobal(t *testing.T) { testLifeCycle(t, "1255@voltron:2890:3890:2183", 1255) } -func testLifeCycle(t *testing.T, config string, myId uint32) { +func testLifeCycle(t *testing.T, config string, myID uint32) { currentVtDataRoot := os.Getenv("VTDATAROOT") vtDataRoot := path.Join(os.TempDir(), fmt.Sprintf("VTDATAROOT_%v", getUUID(t))) if err := os.Setenv("VTDATAROOT", vtDataRoot); err != nil { @@ -57,7 +57,7 @@ func testLifeCycle(t *testing.T, config string, myId uint32) { t.Errorf("cannot remove test VTDATAROOT directory: %v", err) } }() - zkConf := MakeZkConfigFromString(config, myId) + zkConf := MakeZkConfigFromString(config, myID) zkd := NewZkd(zkConf) if err := zkd.Init(); err != nil { t.Fatalf("Init() err: %v", err) diff --git a/go/zk/zknode_bson.go b/go/zk/zknode_bson.go deleted file mode 100644 index d649296fedc..00000000000 --- a/go/zk/zknode_bson.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zk - -// DO NOT EDIT. -// FILE GENERATED BY BSONGEN. - -import ( - "bytes" - - "github.com/youtube/vitess/go/bson" - "github.com/youtube/vitess/go/bytes2" -) - -// MarshalBson bson-encodes ZkNode. -func (zkNode *ZkNode) MarshalBson(buf *bytes2.ChunkedWriter, key string) { - bson.EncodeOptionalPrefix(buf, bson.Object, key) - lenWriter := bson.NewLenWriter(buf) - - bson.EncodeString(buf, "Path", zkNode.Path) - bson.EncodeString(buf, "Data", zkNode.Data) - zkNode.Stat.MarshalBson(buf, "Stat") - // []string - { - bson.EncodePrefix(buf, bson.Array, "Children") - lenWriter := bson.NewLenWriter(buf) - for _i, _v1 := range zkNode.Children { - bson.EncodeString(buf, bson.Itoa(_i), _v1) - } - lenWriter.Close() - } - bson.EncodeBool(buf, "Cached", zkNode.Cached) - bson.EncodeBool(buf, "Stale", zkNode.Stale) - - lenWriter.Close() -} - -// UnmarshalBson bson-decodes into ZkNode. -func (zkNode *ZkNode) UnmarshalBson(buf *bytes.Buffer, kind byte) { - switch kind { - case bson.EOO, bson.Object: - // valid - case bson.Null: - return - default: - panic(bson.NewBsonError("unexpected kind %v for ZkNode", kind)) - } - bson.Next(buf, 4) - - for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { - switch bson.ReadCString(buf) { - case "Path": - zkNode.Path = bson.DecodeString(buf, kind) - case "Data": - zkNode.Data = bson.DecodeString(buf, kind) - case "Stat": - zkNode.Stat.UnmarshalBson(buf, kind) - case "Children": - // []string - if kind != bson.Null { - if kind != bson.Array { - panic(bson.NewBsonError("unexpected kind %v for zkNode.Children", kind)) - } - bson.Next(buf, 4) - zkNode.Children = make([]string, 0, 8) - for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { - bson.SkipIndex(buf) - var _v1 string - _v1 = bson.DecodeString(buf, kind) - zkNode.Children = append(zkNode.Children, _v1) - } - } - case "Cached": - zkNode.Cached = bson.DecodeBool(buf, kind) - case "Stale": - zkNode.Stale = bson.DecodeBool(buf, kind) - default: - bson.Skip(buf, kind) - } - } -} diff --git a/go/zk/zknodev_bson.go b/go/zk/zknodev_bson.go deleted file mode 100644 index 3155fd556fc..00000000000 --- a/go/zk/zknodev_bson.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zk - -// DO NOT EDIT. -// FILE GENERATED BY BSONGEN. - -import ( - "bytes" - - "github.com/youtube/vitess/go/bson" - "github.com/youtube/vitess/go/bytes2" -) - -// MarshalBson bson-encodes ZkNodeV. -func (zkNodeV *ZkNodeV) MarshalBson(buf *bytes2.ChunkedWriter, key string) { - bson.EncodeOptionalPrefix(buf, bson.Object, key) - lenWriter := bson.NewLenWriter(buf) - - // []*ZkNode - { - bson.EncodePrefix(buf, bson.Array, "Nodes") - lenWriter := bson.NewLenWriter(buf) - for _i, _v1 := range zkNodeV.Nodes { - // *ZkNode - if _v1 == nil { - bson.EncodePrefix(buf, bson.Null, bson.Itoa(_i)) - } else { - (*_v1).MarshalBson(buf, bson.Itoa(_i)) - } - } - lenWriter.Close() - } - - lenWriter.Close() -} - -// UnmarshalBson bson-decodes into ZkNodeV. -func (zkNodeV *ZkNodeV) UnmarshalBson(buf *bytes.Buffer, kind byte) { - switch kind { - case bson.EOO, bson.Object: - // valid - case bson.Null: - return - default: - panic(bson.NewBsonError("unexpected kind %v for ZkNodeV", kind)) - } - bson.Next(buf, 4) - - for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { - switch bson.ReadCString(buf) { - case "Nodes": - // []*ZkNode - if kind != bson.Null { - if kind != bson.Array { - panic(bson.NewBsonError("unexpected kind %v for zkNodeV.Nodes", kind)) - } - bson.Next(buf, 4) - zkNodeV.Nodes = make([]*ZkNode, 0, 8) - for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { - bson.SkipIndex(buf) - var _v1 *ZkNode - // *ZkNode - if kind != bson.Null { - _v1 = new(ZkNode) - (*_v1).UnmarshalBson(buf, kind) - } - zkNodeV.Nodes = append(zkNodeV.Nodes, _v1) - } - } - default: - bson.Skip(buf, kind) - } - } -} diff --git a/go/zk/zkns/pdns/pdns.go b/go/zk/zkns/pdns/pdns.go index ec270cda307..da52df02381 100644 --- a/go/zk/zkns/pdns/pdns.go +++ b/go/zk/zkns/pdns/pdns.go @@ -1,4 +1,5 @@ -// To be used with PowerDNS (pdns) as a "pipe backend" CoProcess. +// Package pdns provides code to be used with PowerDNS (pdns) as a +// "pipe backend" CoProcess. // // Protocol description: // http://downloads.powerdns.com/documentation/html/backends-detail.html#PIPEBACKEND. diff --git a/go/zk/zkns/pdns/pdns_test.go b/go/zk/zkns/pdns/pdns_test.go index 49561efe332..42eef95171f 100644 --- a/go/zk/zkns/pdns/pdns_test.go +++ b/go/zk/zkns/pdns/pdns_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "testing" + "time" "github.com/youtube/vitess/go/netutil" "github.com/youtube/vitess/go/zk" @@ -118,13 +119,91 @@ type TestZkConn struct { data map[string]string } +type ZkStat struct { + czxid int64 `bson:"Czxid"` + mzxid int64 `bson:"Mzxid"` + cTime time.Time `bson:"CTime"` + mTime time.Time `bson:"MTime"` + version int `bson:"Version"` + cVersion int `bson:"CVersion"` + aVersion int `bson:"AVersion"` + ephemeralOwner int64 `bson:"EphemeralOwner"` + dataLength int `bson:"DataLength"` + numChildren int `bson:"NumChildren"` + pzxid int64 `bson:"Pzxid"` +} + +type ZkPath struct { + Path string +} + +type ZkPathV struct { + Paths []string +} + +type ZkNode struct { + Path string + Data string + Stat ZkStat + Children []string +} + +type ZkNodeV struct { + Nodes []*ZkNode +} + +// ZkStat methods to match zk.Stat interface +func (zkStat *ZkStat) Czxid() int64 { + return zkStat.czxid +} + +func (zkStat *ZkStat) Mzxid() int64 { + return zkStat.mzxid +} + +func (zkStat *ZkStat) CTime() time.Time { + return zkStat.cTime +} + +func (zkStat *ZkStat) MTime() time.Time { + return zkStat.mTime +} + +func (zkStat *ZkStat) Version() int { + return zkStat.version +} + +func (zkStat *ZkStat) CVersion() int { + return zkStat.cVersion +} + +func (zkStat *ZkStat) AVersion() int { + return zkStat.aVersion +} + +func (zkStat *ZkStat) EphemeralOwner() int64 { + return zkStat.ephemeralOwner +} + +func (zkStat *ZkStat) DataLength() int { + return zkStat.dataLength +} + +func (zkStat *ZkStat) NumChildren() int { + return zkStat.numChildren +} + +func (zkStat *ZkStat) Pzxid() int64 { + return zkStat.pzxid +} + func (conn *TestZkConn) Get(path string) (data string, stat zk.Stat, err error) { data, ok := conn.data[path] if !ok { err = &zookeeper.Error{Op: "TestZkConn: node doesn't exist", Code: zookeeper.ZNONODE, Path: path} return } - s := &zk.ZkStat{} + s := &ZkStat{} return data, s, nil } @@ -143,7 +222,7 @@ func (conn *TestZkConn) ChildrenW(path string) (children []string, stat zk.Stat, func (conn *TestZkConn) Exists(path string) (stat zk.Stat, err error) { _, ok := conn.data[path] if ok { - return &zk.ZkStat{}, nil + return &ZkStat{}, nil } return nil, nil } diff --git a/go/zk/zkns/zkns.go b/go/zk/zkns/zkns.go index 9fe1b630dc3..c430d41231a 100644 --- a/go/zk/zkns/zkns.go +++ b/go/zk/zkns/zkns.go @@ -79,11 +79,6 @@ func (zaddrs *ZknsAddrs) IsValidSRV() bool { if zaddr.Host == "" || zaddr.IPv4 != "" || len(zaddr.NamedPortMap) == 0 { return false } - for portName, _ := range zaddr.NamedPortMap { - if portName != "" && portName[0] != '_' { - return false - } - } } return true } diff --git a/go/zk/zkocc/cache.go b/go/zk/zkocc/cache.go deleted file mode 100644 index a74325a2ec0..00000000000 --- a/go/zk/zkocc/cache.go +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// cache for zkocc -package zkocc - -import ( - "fmt" - "sort" - "sync" - "time" - - log "github.com/golang/glog" - "github.com/youtube/vitess/go/zk" - "launchpad.net/gozk/zookeeper" -) - -// The cache is a map of entry. The mutex on the cache only protects the -// map itself, not individual entries. Each entry has a mutex too. -// Once an entry is added to the map, it is never removed. - -// error used for stale entries -var ( - errStale = fmt.Errorf("Stale entry") -) - -// The states for the structure are: -// xxxError == nil && xxxTime.IsZero() -// we were never interested in this value, never asked, never failed -// xxxError == nil && !xxxTime.IsZero() -// we asked for the value, and got it with no error -// xxxError != nil && xxxTime.IsZero() -// the first time we asked for it, we got an error, and we never got -// a good value after that. -// xxxError != nil && !xxxTime.IsZero() -// entry is stale: we got a value a long time ago, and we then marked it -// stale, and were never able to recover it. -type zkCacheEntry struct { - // the mutex protects any access to this structure (read or write) - mutex sync.Mutex - node zk.ZkNode // node has data, children and stat - - dataTime time.Time // time we last got the data at - dataError error - - childrenTime time.Time // time we last got the children at - childrenError error -} - -func (entry *zkCacheEntry) processEvent(watch <-chan zookeeper.Event) { - for event := range watch { - // mark the cache so we know what to do - // note we ignore session events here, as they're handled - // at the cell level - switch event.Type { - case zookeeper.EVENT_DELETED: - // invalidate both caches - entry.mutex.Lock() - entry.dataTime = time.Time{} - entry.childrenTime = time.Time{} - entry.mutex.Unlock() - case zookeeper.EVENT_CHANGED: - // invalidate the data cache - entry.mutex.Lock() - entry.dataTime = time.Time{} - entry.mutex.Unlock() - case zookeeper.EVENT_CHILD: - // invalidate the children cache - entry.mutex.Lock() - entry.childrenTime = time.Time{} - entry.mutex.Unlock() - } - } -} - -func (entry *zkCacheEntry) get(zcell *zkCell, path string, reply *zk.ZkNode) error { - entry.mutex.Lock() - defer entry.mutex.Unlock() - - cached := true - if entry.dataTime.IsZero() { - // the value is not set yet - - // if another process got an error getting it, and we - // never got a value, we will let the background - // refresh try to do better - if entry.dataError != nil { - return entry.dataError - } - - // let's ask for it - // first get the connection - zconn, err := zcell.getConnection() - if err != nil { - entry.dataError = err - log.Warningf("ZK connection error for path %v: %v", path, err) - zcell.zkrStats.otherErrors.Add(zcell.cellName, 1) - return err - } - - // get the value into the cache - entry.node.Path = path - var stat zk.Stat - var watch <-chan zookeeper.Event - entry.node.Data, stat, watch, err = zconn.GetW(path) - if err != nil { - entry.dataError = err - log.Warningf("ZK error for path %v: %v", path, err) - if zookeeper.IsError(err, zookeeper.ZNONODE) { - zcell.zkrStats.nodeNotFoundErrors.Add(zcell.cellName, 1) - } else { - zcell.zkrStats.otherErrors.Add(zcell.cellName, 1) - } - return err - } - zcell.zkrStats.zkReads.Add(zcell.cellName, 1) - entry.node.Stat.FromZookeeperStat(stat) - entry.dataTime = time.Now() - - // set up the update channel - go entry.processEvent(watch) - - cached = false - } else { - // update the stats - if entry.dataError != nil { - // we have an error, so the entry is stale - zcell.zkrStats.staleReads.Add(zcell.cellName, 1) - } else { - zcell.zkrStats.cacheReads.Add(zcell.cellName, 1) - } - } - - // we have a good value, return it - *reply = entry.node - reply.Children = nil - reply.Cached = cached - reply.Stale = entry.dataError != nil - return nil -} - -func (entry *zkCacheEntry) children(zcell *zkCell, path string, reply *zk.ZkNode) error { - entry.mutex.Lock() - defer entry.mutex.Unlock() - - cached := true - if entry.childrenTime.IsZero() { - // the value is not set yet - - // if another process got an error getting it, and we - // never got a value, we will let the background - // refresh try to do better - if entry.childrenError != nil { - return entry.childrenError - } - - // let's ask for it - // first get the connection - zconn, err := zcell.getConnection() - if err != nil { - entry.childrenError = err - log.Warningf("ZK connection error for path %v: %v", path, err) - zcell.zkrStats.otherErrors.Add(zcell.cellName, 1) - return err - } - - // get the value into the cache - entry.node.Path = path - var stat zk.Stat - var watch <-chan zookeeper.Event - entry.node.Children, stat, watch, err = zconn.ChildrenW(path) - if err != nil { - entry.childrenError = err - log.Warningf("ZK error for path %v: %v", path, err) - if zookeeper.IsError(err, zookeeper.ZNONODE) { - zcell.zkrStats.nodeNotFoundErrors.Add(zcell.cellName, 1) - } else { - zcell.zkrStats.otherErrors.Add(zcell.cellName, 1) - } - return err - } - sort.Strings(entry.node.Children) - zcell.zkrStats.zkReads.Add(zcell.cellName, 1) - entry.node.Stat.FromZookeeperStat(stat) - entry.childrenTime = time.Now() - - // set up the update channel - go entry.processEvent(watch) - - cached = false - } else { - // update the stats - if entry.childrenError != nil { - zcell.zkrStats.staleReads.Add(zcell.cellName, 1) - } else { - zcell.zkrStats.cacheReads.Add(zcell.cellName, 1) - } - } - - // we have a good value, return it - *reply = entry.node - reply.Data = "" - reply.Cached = cached - reply.Stale = entry.childrenError != nil - return nil -} - -func (entry *zkCacheEntry) checkForRefresh(refreshThreshold time.Time) (shouldBeDataRefreshed, shouldBeChildrenRefreshed bool) { - entry.mutex.Lock() - defer entry.mutex.Unlock() - - if entry.dataTime.IsZero() { - // the entry was never set - // if we had an error getting it, it means we asked for it, - // see if we can get a good value - if entry.dataError != nil { - if zookeeper.IsError(entry.dataError, zookeeper.ZNONODE) { - // we had no node, we can try next time a client asks - entry.dataError = nil - // at this point, we have both - // dataTime.IsZero() and - // entry.dataError = nil, as if we - // never asked for it - } else { - // we had a valid error, let's try again - shouldBeDataRefreshed = true - } - } - } else { - // 1. we got a value at some point, then it got stale - // 2. we got a value a long time ago, refresh it - shouldBeDataRefreshed = entry.dataError != nil || entry.dataTime.Before(refreshThreshold) - } - - if entry.childrenTime.IsZero() { - // the entry was never set - // if we had an error getting it, it means we asked for it, - // see if we can get a good value - if entry.childrenError != nil { - if zookeeper.IsError(entry.childrenError, zookeeper.ZNONODE) { - // we had no node, we can try next time a client asks - entry.childrenError = nil - // at this point, we have both - // childrenTime.IsZero() and - // entry.childrenError = nil, as if we - // never asked for it - } else { - shouldBeChildrenRefreshed = true - // we had a valid error, let's try again - } - } - } else { - // 1. we got a value at some point, then it got stale - // 2. we got a value a long time ago, refresh it - shouldBeChildrenRefreshed = entry.childrenError != nil || entry.childrenTime.Before(refreshThreshold) - } - return -} - -func (entry *zkCacheEntry) markForRefresh() { - entry.mutex.Lock() - if !entry.dataTime.IsZero() && entry.dataError == nil { - entry.dataError = errStale - } - if !entry.childrenTime.IsZero() && entry.childrenError == nil { - entry.childrenError = errStale - } - entry.mutex.Unlock() -} - -func (entry *zkCacheEntry) updateData(data string, stat *zk.ZkStat, watch <-chan zookeeper.Event) { - entry.mutex.Lock() - entry.dataTime = time.Now() - entry.dataError = nil - entry.node.Data = data - entry.node.Stat = *stat - entry.mutex.Unlock() - - go entry.processEvent(watch) -} - -func (entry *zkCacheEntry) updateChildren(children []string, stat *zk.ZkStat, watch <-chan zookeeper.Event) { - entry.mutex.Lock() - entry.childrenTime = time.Now() - entry.childrenError = nil - entry.node.Stat = *stat - entry.node.Children = children - entry.mutex.Unlock() - - go entry.processEvent(watch) -} - -// the ZkCache is a map from resolved zk path (where 'local' has been replaced -// with the cell name) to the entry -type ZkCache struct { - Cache map[string]*zkCacheEntry - mutex sync.Mutex -} - -func newZkCache() *ZkCache { - return &ZkCache{Cache: make(map[string]*zkCacheEntry)} -} - -func (zkc *ZkCache) getEntry(path string) *zkCacheEntry { - zkc.mutex.Lock() - result, ok := zkc.Cache[path] - if !ok { - result = &zkCacheEntry{node: zk.ZkNode{Path: path}} - zkc.Cache[path] = result - } - zkc.mutex.Unlock() - return result -} - -// marks the entire cache as needing a refresh -// all entries will be stale after this -func (zkc *ZkCache) markForRefresh() { - zkc.mutex.Lock() - defer zkc.mutex.Unlock() - for _, entry := range zkc.Cache { - entry.markForRefresh() - } -} - -// return a few values that need to be refreshed -func (zkc *ZkCache) refreshSomeValues(zconn zk.Conn, maxToRefresh int) { - // build a list of a few values we want to refresh - refreshThreshold := time.Now().Add(-10 * time.Minute) - - // range will randomize the traversal order, so we will not always try - // the same entries in the same order - dataEntries := make([]*zkCacheEntry, 0, maxToRefresh) - childrenEntries := make([]*zkCacheEntry, 0, maxToRefresh) - zkc.mutex.Lock() - for _, entry := range zkc.Cache { - shouldBeDataRefreshed, shouldBeChildrenRefreshed := entry.checkForRefresh(refreshThreshold) - if shouldBeDataRefreshed { - dataEntries = append(dataEntries, entry) - } - if shouldBeChildrenRefreshed { - childrenEntries = append(childrenEntries, entry) - } - - // check if we have enough work to do - if len(dataEntries) == maxToRefresh || len(childrenEntries) == maxToRefresh { - break - } - } - zkc.mutex.Unlock() - - // now refresh the values - for _, entry := range dataEntries { - data, stat, watch, err := zconn.GetW(entry.node.Path) - if err == nil { - zkStat := &zk.ZkStat{} - zkStat.FromZookeeperStat(stat) - entry.updateData(data, zkStat, watch) - } else if zookeeper.IsError(err, zookeeper.ZCLOSING) { - // connection is closing, no point in asking for more - log.Warningf("failed to refresh cache: %v (and stopping refresh)", err.Error()) - return - } else { - // individual failure - log.Warningf("failed to refresh cache: %v", err.Error()) - } - } - - for _, entry := range childrenEntries { - children, stat, watch, err := zconn.ChildrenW(entry.node.Path) - if err == nil { - zkStat := &zk.ZkStat{} - zkStat.FromZookeeperStat(stat) - entry.updateChildren(children, zkStat, watch) - } else if zookeeper.IsError(err, zookeeper.ZCLOSING) { - // connection is closing, no point in asking for more - log.Warningf("failed to refresh cache: %v (and stopping refresh)", err.Error()) - return - } else { - // individual failure - log.Warningf("failed to refresh cache: %v", err.Error()) - } - } -} diff --git a/go/zk/zkocc/cell.go b/go/zk/zkocc/cell.go deleted file mode 100644 index 9eff95350a5..00000000000 --- a/go/zk/zkocc/cell.go +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zkocc - -import ( - "flag" - "fmt" - "sync" - "time" - - log "github.com/golang/glog" - "github.com/youtube/vitess/go/stats" - "github.com/youtube/vitess/go/zk" - "launchpad.net/gozk/zookeeper" -) - -// a zkCell object represents a zookeeper cell, with a cache and a connection -// to the real cell. -var ( - baseTimeout = flag.Duration("base-timeout", 30*time.Second, "zookeeper base time out") - connectTimeout = flag.Duration("connect-timeout", 30*time.Second, "zookeeper connection time out") - reconnectInterval = flag.Int("reconnect-interval", 3, "how many seconds to wait between reconnect attempts") - refreshInterval = flag.Duration("cache-refresh-interval", 1*time.Second, "how many seconds to wait between cache refreshes") - refreshCount = flag.Int("cache-refresh-count", 10, "how many entries to refresh at every tick") -) - -// Our state. We need this to be independent as we want to decorelate the -// connection from what clients are asking for. -// For instance, if a cell is not used often, and gets disconnected, -// we want to reconnect in the background, independently of the clients. -// Also we want to support a BACKOFF mode for fast client failure -// reporting while protecting the server from high rates of connections. -const ( - // DISCONNECTED: initial state of the cell. - // connect will only work in that state, and will go to CONNECTING - CELL_DISCONNECTED = iota - - // CONNECTING: a 'connect' function started the connection process. - // It will then go to CONNECTED or BACKOFF. Only one connect - // function will run at a time. - // requests will be blocked until the state changes (if it goes to - // CONNECTED, request will then try to get the value, if it goes to - // CELL_BACKOFF, they will fail) - CELL_CONNECTING - - // steady state, when all is good and dandy. - CELL_CONNECTED - - // BACKOFF: we're waiting for a bit before trying to reconnect. - // a go routine will go to DISCONNECTED and start login soon. - // we're failing all requests in this state. - CELL_BACKOFF -) - -var stateNames = map[int64]string{ - CELL_DISCONNECTED: "Disconnected", - - CELL_CONNECTING: "Connecting", - CELL_CONNECTED: "Connected", - CELL_BACKOFF: "BackOff", -} - -type zkCell struct { - // set at creation - cellName string - zkAddr string - zcache *ZkCache - zkrStats *zkrStats - - // connection related variables - mutex sync.Mutex // For connection & state only - zconn zk.Conn - state int64 - ready *sync.Cond // will be signaled at connection time - lastErr error // last connection error -} - -func newZkCell(name, zkaddr string, zkrstats *zkrStats) *zkCell { - result := &zkCell{cellName: name, zkAddr: zkaddr, zcache: newZkCache(), zkrStats: zkrstats} - result.ready = sync.NewCond(&result.mutex) - stats.Publish("Zcell"+name, stats.StringFunc(func() string { - - result.mutex.Lock() - defer result.mutex.Unlock() - return stateNames[result.state] - })) - go result.backgroundRefresher() - return result -} - -// background routine to initiate a connection sequence -// only connect if state == CELL_DISCONNECTED -// will change state to CELL_CONNECTING during the connection process -// will then change to CELL_CONNECTED (and braodcast the cond) -// or to CELL_BACKOFF (and schedule a new reconnection soon) -func (zcell *zkCell) connect() { - // change our state, we're working on connecting - zcell.mutex.Lock() - if zcell.state != CELL_DISCONNECTED { - // someone else is already connecting - zcell.mutex.Unlock() - return - } - zcell.state = CELL_CONNECTING - zcell.mutex.Unlock() - - // now connect - zconn, session, err := zk.DialZkTimeout(zcell.zkAddr, *baseTimeout, *connectTimeout) - if err == nil { - zcell.zconn = zconn - go zcell.handleSessionEvents(session) - } - - // and change our state - zcell.mutex.Lock() - if zcell.state != CELL_CONNECTING { - panic(fmt.Errorf("Unexpected state: %v", zcell.state)) - } - if err == nil { - log.Infof("zk cell conn: cell %v connected", zcell.cellName) - zcell.state = CELL_CONNECTED - zcell.lastErr = nil - - } else { - log.Infof("zk cell conn: cell %v connection failed: %v", zcell.cellName, err) - zcell.state = CELL_BACKOFF - zcell.lastErr = err - - go func() { - // we're going to try to reconnect at some point - // FIXME(alainjobart) backoff algorithm? - <-time.NewTimer(time.Duration(*reconnectInterval) * time.Second).C - - // switch back to DISCONNECTED, and trigger a connect - zcell.mutex.Lock() - zcell.state = CELL_DISCONNECTED - zcell.mutex.Unlock() - zcell.connect() - }() - } - - // we broadcast on the condition to get everybody unstuck, - // whether we succeeded to connect or not - zcell.ready.Broadcast() - zcell.mutex.Unlock() -} - -// the state transitions from the library are not that obvious: -// - If the server connection is delayed (as with using pkill -STOP -// on the process), the client will get a STATE_CONNECTING message, -// and then most likely after that a STATE_EXPIRED_SESSION event. -// We lost all of our watches, we need to reset them. -// - If the server connection dies, and cannot be re-established -// (server was restarted), the client will get a STATE_CONNECTING message, -// and then a STATE_CONNECTED when the connection is re-established. -// The watches will still be valid. -// - If the server connection dies, and a new server comes in (different -// server root), the client will never connect again (it will try though!). -// So we'll only get a STATE_CONNECTING and nothing else. The watches -// won't be valid at all any more. -// Given all these cases, the simpler for now is to always consider a -// STATE_CONNECTING message as a cache invalidation, close the connection -// and start over. -// (alainjobart: Note I've never seen a STATE_CLOSED message) -func (zcell *zkCell) handleSessionEvents(session <-chan zookeeper.Event) { - for event := range session { - log.Infof("zk cell conn: cell %v received: %v", zcell.cellName, event) - switch event.State { - case zookeeper.STATE_EXPIRED_SESSION, zookeeper.STATE_CONNECTING: - zcell.zconn.Close() - fallthrough - case zookeeper.STATE_CLOSED: - zcell.mutex.Lock() - zcell.state = CELL_DISCONNECTED - zcell.zconn = nil - zcell.zcache.markForRefresh() - // for a closed connection, no backoff at first retry - // if connect fails again, then we'll backoff - go zcell.connect() - zcell.mutex.Unlock() - log.Warningf("zk cell conn: session for cell %v ended: %v", zcell.cellName, event) - return - default: - log.Infof("zk conn cache: session for cell %v event: %v", zcell.cellName, event) - } - } -} - -func (zcell *zkCell) getConnection() (zk.Conn, error) { - zcell.mutex.Lock() - defer zcell.mutex.Unlock() - - switch zcell.state { - case CELL_CONNECTED: - // we are already connected, just return the connection - return zcell.zconn, nil - case CELL_DISCONNECTED: - // trigger the connection sequence and wait for connection - go zcell.connect() - fallthrough - case CELL_CONNECTING: - for zcell.state != CELL_CONNECTED && zcell.state != CELL_BACKOFF { - zcell.ready.Wait() - } - if zcell.state == CELL_CONNECTED { - return zcell.zconn, nil - } - } - - // we are in BACKOFF or failed to connect - return nil, zcell.lastErr -} - -// runs in the background and refreshes the cache if we're in connected state -func (zcell *zkCell) backgroundRefresher() { - ticker := time.NewTicker(*refreshInterval) - for _ = range ticker.C { - // grab a valid connection - zcell.mutex.Lock() - // not connected, what can we do? - if zcell.state != CELL_CONNECTED { - zcell.mutex.Unlock() - continue - } - zconn := zcell.zconn - zcell.mutex.Unlock() - - // get a few values to refresh, and ask for them - zcell.zcache.refreshSomeValues(zconn, *refreshCount) - } -} diff --git a/go/zk/zkocc/rpc.go b/go/zk/zkocc/rpc.go deleted file mode 100644 index b5a9a603605..00000000000 --- a/go/zk/zkocc/rpc.go +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zkocc - -import ( - "bytes" - "errors" - "fmt" - "strings" - "sync" - - log "github.com/golang/glog" - "github.com/youtube/vitess/go/stats" - "github.com/youtube/vitess/go/zk" -) - -// zkocc -// -// Cache open zk connections and allow cheap read requests. -// Cache data coming back from zk. -// * Use node notifications for data invalidation and freshness. -// * Force invalidation after some amount of time with some amount of skew. -// * Insulate this process from periodic zk cell failures. -// * On reconnect, re-read all paths. -// * Report ZkNode as stale when things are disconnected? - -type zkrStats struct { - zkReads *stats.Counters - cacheReads *stats.Counters - staleReads *stats.Counters - nodeNotFoundErrors *stats.Counters - otherErrors *stats.Counters -} - -func newZkrStats() *zkrStats { - zs := &zkrStats{} - zs.zkReads = stats.NewCounters("ZkReaderZkReads") - zs.cacheReads = stats.NewCounters("ZkReaderCacheReads") - zs.staleReads = stats.NewCounters("ZkReaderStaleReads") - zs.nodeNotFoundErrors = stats.NewCounters("ZkReaderNodeNotFoundErrors") - zs.otherErrors = stats.NewCounters("ZkReaderOtherErrors") - return zs -} - -// ZkReader is the main object receiving RPC calls -type ZkReader struct { - mutex sync.Mutex - zcell map[string]*zkCell - resolveLocal bool - localCell string - - // stats - rpcCalls *stats.Int //sync2.AtomicInt32 - unknownCellErrors *stats.Int //sync2.AtomicInt32 - zkrStats *zkrStats -} - -var ( - ErrPartialRead = errors.New("zkocc: partial read") - ErrLocalCell = errors.New("zkocc: cannot resolve local cell") -) - -func NewZkReader(resolveLocal bool, preload []string) *ZkReader { - zkr := &ZkReader{zcell: make(map[string]*zkCell), resolveLocal: resolveLocal} - if resolveLocal { - zkr.localCell = zk.GuessLocalCell() - } - zkr.rpcCalls = stats.NewInt("ZkReaderRpcCalls") - zkr.unknownCellErrors = stats.NewInt("ZkReaderUnknownCellErrors") - zkr.zkrStats = newZkrStats() - - stats.PublishJSONFunc("ZkReader", zkr.statsJSON) - - // start some cells - for _, cellName := range preload { - _, path, err := zkr.getCell("/zk/" + cellName) - if err != nil { - log.Errorf("Cell " + cellName + " could not be preloaded: " + err.Error()) - } else { - log.Infof("Cell " + cellName + " preloaded for: " + path) - } - } - return zkr -} - -func (zkr *ZkReader) getCell(path string) (*zkCell, string, error) { - zkr.mutex.Lock() - defer zkr.mutex.Unlock() - cellName, err := zk.ZkCellFromZkPath(path) - if err != nil { - return nil, "", err - } - - // the 'local' cell has to be resolved, and the path fixed - resolvedPath := path - if cellName == "local" { - if zkr.resolveLocal { - cellName = zkr.localCell - parts := strings.Split(path, "/") - parts[2] = cellName - resolvedPath = strings.Join(parts, "/") - } else { - return nil, "", ErrLocalCell - } - } - - cell, ok := zkr.zcell[cellName] - if !ok { - zkaddr, err := zk.ZkPathToZkAddr(path, false) - if err != nil { - return nil, "", err - } - cell = newZkCell(cellName, zkaddr, zkr.zkrStats) - zkr.zcell[cellName] = cell - } - return cell, resolvedPath, nil -} - -func handleError(err *error) { - if x := recover(); x != nil { - log.Errorf("rpc panic: %v", x) - terr, ok := x.(error) - if !ok { - *err = fmt.Errorf("rpc panic: %v", x) - return - } - *err = terr - } -} - -func (zkr *ZkReader) get(req *zk.ZkPath, reply *zk.ZkNode) (err error) { - // get the cell - cell, path, err := zkr.getCell(req.Path) - if err != nil { - log.Warningf("Unknown cell for path %v: %v", req.Path, err) - zkr.unknownCellErrors.Add(1) - return err - } - - // get the entry - entry := cell.zcache.getEntry(path) - - // and fill it in if we can - return entry.get(cell, path, reply) -} - -func (zkr *ZkReader) Get(req *zk.ZkPath, reply *zk.ZkNode) (err error) { - defer handleError(&err) - zkr.rpcCalls.Add(1) - - return zkr.get(req, reply) -} - -func (zkr *ZkReader) GetV(req *zk.ZkPathV, reply *zk.ZkNodeV) (err error) { - defer handleError(&err) - zkr.rpcCalls.Add(1) - - wg := sync.WaitGroup{} - mu := sync.Mutex{} - - reply.Nodes = make([]*zk.ZkNode, len(req.Paths)) - errors := make([]error, 0, len(req.Paths)) - for i, zkPath := range req.Paths { - wg.Add(1) - go func(i int, zkPath string) { - zp := &zk.ZkPath{Path: zkPath} - zn := &zk.ZkNode{} - err := zkr.get(zp, zn) - if err != nil { - mu.Lock() - errors = append(errors, err) - mu.Unlock() - } else { - reply.Nodes[i] = zn - } - wg.Done() - }(i, zkPath) - } - wg.Wait() - mu.Lock() - defer mu.Unlock() - if len(errors) > 0 { - // this won't transmit the responses we actually got, - // just return an error for them all. Look at logs to - // figure out what went wrong. - return ErrPartialRead - } - return nil -} - -func (zkr *ZkReader) Children(req *zk.ZkPath, reply *zk.ZkNode) (err error) { - defer handleError(&err) - zkr.rpcCalls.Add(1) - - // get the cell - cell, path, err := zkr.getCell(req.Path) - if err != nil { - log.Warningf("Unknown cell for path %v: %v", req.Path, err) - zkr.unknownCellErrors.Add(1) - return err - } - - // get the entry - entry := cell.zcache.getEntry(path) - - // and fill it in if we can - return entry.children(cell, path, reply) -} - -func (zkr *ZkReader) statsJSON() string { - zkr.mutex.Lock() - defer zkr.mutex.Unlock() - - b := bytes.NewBuffer(make([]byte, 0, 4096)) - fmt.Fprintf(b, "{") - fmt.Fprintf(b, "\"RpcCalls\": %v,", zkr.rpcCalls.Get()) - fmt.Fprintf(b, "\"UnknownCellErrors\": %v", zkr.unknownCellErrors.Get()) - var zkReads int64 - var cacheReads int64 - var staleReads int64 - var nodeNotFoundErrors int64 - var otherErrors int64 - mapZkReads := zkr.zkrStats.zkReads.Counts() - mapCacheReads := zkr.zkrStats.cacheReads.Counts() - mapStaleReads := zkr.zkrStats.staleReads.Counts() - mapNodeNotFoundErrors := zkr.zkrStats.nodeNotFoundErrors.Counts() - mapOtherErrors := zkr.zkrStats.otherErrors.Counts() - for name, zcell := range zkr.zcell { - fmt.Fprintf(b, ", \"%v\": {", name) - fmt.Fprintf(b, "\"CacheReads\": %v,", mapCacheReads[name]) - fmt.Fprintf(b, "\"NodeNotFoundErrors\": %v,", mapNodeNotFoundErrors[name]) - fmt.Fprintf(b, "\"OtherErrors\": %v,", mapOtherErrors[name]) - fmt.Fprintf(b, "\"StaleReads\": %v,", mapStaleReads[name]) - zcell.mutex.Lock() - fmt.Fprintf(b, "\"State\": %q,", stateNames[zcell.state]) - zcell.mutex.Unlock() - fmt.Fprintf(b, "\"ZkReads\": %v", mapZkReads[name]) - fmt.Fprintf(b, "}") - zkReads += mapZkReads[name] - cacheReads += mapCacheReads[name] - staleReads += mapStaleReads[name] - nodeNotFoundErrors += mapNodeNotFoundErrors[name] - otherErrors += mapOtherErrors[name] - } - fmt.Fprintf(b, ", \"total\": {") - fmt.Fprintf(b, "\"CacheReads\": %v,", cacheReads) - fmt.Fprintf(b, "\"NodeNotFoundErrors\": %v,", nodeNotFoundErrors) - fmt.Fprintf(b, "\"OtherErrors\": %v,", otherErrors) - fmt.Fprintf(b, "\"StaleReads\": %v,", staleReads) - fmt.Fprintf(b, "\"ZkReads\": %v", zkReads) - fmt.Fprintf(b, "}") - - fmt.Fprintf(b, "}") - return b.String() -} diff --git a/go/zk/zkocc_rpc.go b/go/zk/zkocc_rpc.go deleted file mode 100644 index 6c8b6b3dc40..00000000000 --- a/go/zk/zkocc_rpc.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zk - -import ( - rpc "github.com/youtube/vitess/go/rpcplus" -) - -// defines the RPC services for zkocc -// the service name to use is 'ZkReader' -type ZkReader interface { - Get(req *ZkPath, reply *ZkNode) error - GetV(req *ZkPathV, reply *ZkNodeV) error - Children(req *ZkPath, reply *ZkNode) error -} - -// helper method to register the server (does interface checking) -func RegisterZkReader(zkReader ZkReader) { - rpc.Register(zkReader) -} diff --git a/go/zk/zkocc_structs.go b/go/zk/zkocc_structs.go deleted file mode 100644 index 6f50ca77e46..00000000000 --- a/go/zk/zkocc_structs.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zk - -// contains the structures used for RPC calls to zkocc. - -import ( - "time" -) - -type ZkStat struct { - czxid int64 `bson:"Czxid"` - mzxid int64 `bson:"Mzxid"` - cTime time.Time `bson:"CTime"` - mTime time.Time `bson:"MTime"` - version int `bson:"Version"` - cVersion int `bson:"CVersion"` - aVersion int `bson:"AVersion"` - ephemeralOwner int64 `bson:"EphemeralOwner"` - dataLength int `bson:"DataLength"` - numChildren int `bson:"NumChildren"` - pzxid int64 `bson:"Pzxid"` -} - -type ZkPath struct { - Path string -} - -type ZkPathV struct { - Paths []string -} - -type ZkNode struct { - Path string - Data string - Stat ZkStat - Children []string - Cached bool // the response comes from the zkocc cache - Stale bool // the response is stale because we're not connected -} - -type ZkNodeV struct { - Nodes []*ZkNode -} - -// ZkStat methods to match zk.Stat interface -func (zkStat *ZkStat) Czxid() int64 { - return zkStat.czxid -} - -func (zkStat *ZkStat) Mzxid() int64 { - return zkStat.mzxid -} - -func (zkStat *ZkStat) CTime() time.Time { - return zkStat.cTime -} - -func (zkStat *ZkStat) MTime() time.Time { - return zkStat.mTime -} - -func (zkStat *ZkStat) Version() int { - return zkStat.version -} - -func (zkStat *ZkStat) CVersion() int { - return zkStat.cVersion -} - -func (zkStat *ZkStat) AVersion() int { - return zkStat.aVersion -} - -func (zkStat *ZkStat) EphemeralOwner() int64 { - return zkStat.ephemeralOwner -} - -func (zkStat *ZkStat) DataLength() int { - return zkStat.dataLength -} - -func (zkStat *ZkStat) NumChildren() int { - return zkStat.numChildren -} - -func (zkStat *ZkStat) Pzxid() int64 { - return zkStat.pzxid -} - -// helper method -func (zkStat *ZkStat) FromZookeeperStat(zStat Stat) { - zkStat.czxid = zStat.Czxid() - zkStat.mzxid = zStat.Mzxid() - zkStat.cTime = zStat.CTime() - zkStat.mTime = zStat.MTime() - zkStat.version = zStat.Version() - zkStat.cVersion = zStat.CVersion() - zkStat.aVersion = zStat.AVersion() - zkStat.ephemeralOwner = zStat.EphemeralOwner() - zkStat.dataLength = zStat.DataLength() - zkStat.numChildren = zStat.NumChildren() - zkStat.pzxid = zStat.Pzxid() -} diff --git a/go/zk/zkoccconn.go b/go/zk/zkoccconn.go deleted file mode 100644 index 5fb21647a64..00000000000 --- a/go/zk/zkoccconn.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zk - -import ( - "fmt" - "math/rand" - "strings" - "time" - - "code.google.com/p/go.net/context" - - log "github.com/golang/glog" - "github.com/youtube/vitess/go/rpcplus" - "github.com/youtube/vitess/go/rpcwrap/bsonrpc" - "launchpad.net/gozk/zookeeper" -) - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -type ZkoccUnimplementedError string - -func (e ZkoccUnimplementedError) Error() string { - return string("ZkoccConn doesn't implement " + e) -} - -// ZkoccConn is a client class that implements zk.Conn -// but uses a RPC client to talk to a zkocc process -type ZkoccConn struct { - rpcClient *rpcplus.Client -} - -// From the addr (of the form server1:port1,server2:port2,server3:port3:...) -// splits it on commas, randomizes the list, and tries to connect -// to the servers, stopping at the first successful connection -func DialZkocc(addr string, connectTimeout time.Duration) (zkocc *ZkoccConn, err error) { - servers := strings.Split(addr, ",") - perm := rand.Perm(len(servers)) - for _, index := range perm { - server := servers[index] - - rpcClient, err := bsonrpc.DialHTTP("tcp", server, connectTimeout, nil) - if err == nil { - return &ZkoccConn{rpcClient: rpcClient}, nil - } - log.Infof("zk conn cache: zkocc connection to %v failed: %v", server, err) - } - return nil, fmt.Errorf("zkocc connect failed: %v", addr) -} - -func (conn *ZkoccConn) Get(path string) (data string, stat Stat, err error) { - zkPath := &ZkPath{path} - zkNode := &ZkNode{} - if err := conn.rpcClient.Call(context.TODO(), "ZkReader.Get", zkPath, zkNode); err != nil { - return "", nil, err - } - return zkNode.Data, &zkNode.Stat, nil -} - -func (conn *ZkoccConn) GetW(path string) (data string, stat Stat, watch <-chan zookeeper.Event, err error) { - panic(ZkoccUnimplementedError("GetW")) -} - -func (conn *ZkoccConn) Children(path string) (children []string, stat Stat, err error) { - zkPath := &ZkPath{path} - zkNode := &ZkNode{} - if err := conn.rpcClient.Call(context.TODO(), "ZkReader.Children", zkPath, zkNode); err != nil { - return nil, nil, err - } - return zkNode.Children, &zkNode.Stat, nil -} - -func (conn *ZkoccConn) ChildrenW(path string) (children []string, stat Stat, watch <-chan zookeeper.Event, err error) { - panic(ZkoccUnimplementedError("ChildrenW")) -} - -// implement Exists using Get -// FIXME(alainjobart) Maybe we should add Exists in rpc API? -func (conn *ZkoccConn) Exists(path string) (stat Stat, err error) { - zkPath := &ZkPath{path} - zkNode := &ZkNode{} - if err := conn.rpcClient.Call(context.TODO(), "ZkReader.Get", zkPath, zkNode); err != nil { - return nil, err - } - return &zkNode.Stat, nil -} - -func (conn *ZkoccConn) ExistsW(path string) (stat Stat, watch <-chan zookeeper.Event, err error) { - panic(ZkoccUnimplementedError("ExistsW")) -} - -func (conn *ZkoccConn) Create(path, value string, flags int, aclv []zookeeper.ACL) (pathCreated string, err error) { - panic(ZkoccUnimplementedError("Create")) -} - -func (conn *ZkoccConn) Set(path, value string, version int) (stat Stat, err error) { - panic(ZkoccUnimplementedError("Set")) -} - -func (conn *ZkoccConn) Delete(path string, version int) (err error) { - panic(ZkoccUnimplementedError("Delete")) -} - -func (conn *ZkoccConn) Close() error { - return conn.rpcClient.Close() -} - -func (conn *ZkoccConn) RetryChange(path string, flags int, acl []zookeeper.ACL, changeFunc ChangeFunc) error { - panic(ZkoccUnimplementedError("RetryChange")) -} - -// might want to add ACL in RPC code -func (conn *ZkoccConn) ACL(path string) ([]zookeeper.ACL, Stat, error) { - panic(ZkoccUnimplementedError("ACL")) -} - -func (conn *ZkoccConn) SetACL(path string, aclv []zookeeper.ACL, version int) error { - panic(ZkoccUnimplementedError("SetACL")) -} diff --git a/go/zk/zkpath_bson.go b/go/zk/zkpath_bson.go deleted file mode 100644 index 030e47e1ec2..00000000000 --- a/go/zk/zkpath_bson.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zk - -// DO NOT EDIT. -// FILE GENERATED BY BSONGEN. - -import ( - "bytes" - - "github.com/youtube/vitess/go/bson" - "github.com/youtube/vitess/go/bytes2" -) - -// MarshalBson bson-encodes ZkPath. -func (zkPath *ZkPath) MarshalBson(buf *bytes2.ChunkedWriter, key string) { - bson.EncodeOptionalPrefix(buf, bson.Object, key) - lenWriter := bson.NewLenWriter(buf) - - bson.EncodeString(buf, "Path", zkPath.Path) - - lenWriter.Close() -} - -// UnmarshalBson bson-decodes into ZkPath. -func (zkPath *ZkPath) UnmarshalBson(buf *bytes.Buffer, kind byte) { - switch kind { - case bson.EOO, bson.Object: - // valid - case bson.Null: - return - default: - panic(bson.NewBsonError("unexpected kind %v for ZkPath", kind)) - } - bson.Next(buf, 4) - - for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { - switch bson.ReadCString(buf) { - case "Path": - zkPath.Path = bson.DecodeString(buf, kind) - default: - bson.Skip(buf, kind) - } - } -} diff --git a/go/zk/zkpathv_bson.go b/go/zk/zkpathv_bson.go deleted file mode 100644 index c5ddfa7dc9a..00000000000 --- a/go/zk/zkpathv_bson.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zk - -// DO NOT EDIT. -// FILE GENERATED BY BSONGEN. - -import ( - "bytes" - - "github.com/youtube/vitess/go/bson" - "github.com/youtube/vitess/go/bytes2" -) - -// MarshalBson bson-encodes ZkPathV. -func (zkPathV *ZkPathV) MarshalBson(buf *bytes2.ChunkedWriter, key string) { - bson.EncodeOptionalPrefix(buf, bson.Object, key) - lenWriter := bson.NewLenWriter(buf) - - // []string - { - bson.EncodePrefix(buf, bson.Array, "Paths") - lenWriter := bson.NewLenWriter(buf) - for _i, _v1 := range zkPathV.Paths { - bson.EncodeString(buf, bson.Itoa(_i), _v1) - } - lenWriter.Close() - } - - lenWriter.Close() -} - -// UnmarshalBson bson-decodes into ZkPathV. -func (zkPathV *ZkPathV) UnmarshalBson(buf *bytes.Buffer, kind byte) { - switch kind { - case bson.EOO, bson.Object: - // valid - case bson.Null: - return - default: - panic(bson.NewBsonError("unexpected kind %v for ZkPathV", kind)) - } - bson.Next(buf, 4) - - for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { - switch bson.ReadCString(buf) { - case "Paths": - // []string - if kind != bson.Null { - if kind != bson.Array { - panic(bson.NewBsonError("unexpected kind %v for zkPathV.Paths", kind)) - } - bson.Next(buf, 4) - zkPathV.Paths = make([]string, 0, 8) - for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { - bson.SkipIndex(buf) - var _v1 string - _v1 = bson.DecodeString(buf, kind) - zkPathV.Paths = append(zkPathV.Paths, _v1) - } - } - default: - bson.Skip(buf, kind) - } - } -} diff --git a/go/zk/zkstat_bson.go b/go/zk/zkstat_bson.go deleted file mode 100644 index 2e5bf5d0787..00000000000 --- a/go/zk/zkstat_bson.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2012, Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package zk - -// DO NOT EDIT. -// FILE GENERATED BY BSONGEN. - -import ( - "bytes" - - "github.com/youtube/vitess/go/bson" - "github.com/youtube/vitess/go/bytes2" -) - -// MarshalBson bson-encodes ZkStat. -func (zkStat *ZkStat) MarshalBson(buf *bytes2.ChunkedWriter, key string) { - bson.EncodeOptionalPrefix(buf, bson.Object, key) - lenWriter := bson.NewLenWriter(buf) - - bson.EncodeInt64(buf, "Czxid", zkStat.czxid) - bson.EncodeInt64(buf, "Mzxid", zkStat.mzxid) - bson.EncodeTime(buf, "CTime", zkStat.cTime) - bson.EncodeTime(buf, "MTime", zkStat.mTime) - bson.EncodeInt(buf, "Version", zkStat.version) - bson.EncodeInt(buf, "CVersion", zkStat.cVersion) - bson.EncodeInt(buf, "AVersion", zkStat.aVersion) - bson.EncodeInt64(buf, "EphemeralOwner", zkStat.ephemeralOwner) - bson.EncodeInt(buf, "DataLength", zkStat.dataLength) - bson.EncodeInt(buf, "NumChildren", zkStat.numChildren) - bson.EncodeInt64(buf, "Pzxid", zkStat.pzxid) - - lenWriter.Close() -} - -// UnmarshalBson bson-decodes into ZkStat. -func (zkStat *ZkStat) UnmarshalBson(buf *bytes.Buffer, kind byte) { - switch kind { - case bson.EOO, bson.Object: - // valid - case bson.Null: - return - default: - panic(bson.NewBsonError("unexpected kind %v for ZkStat", kind)) - } - bson.Next(buf, 4) - - for kind := bson.NextByte(buf); kind != bson.EOO; kind = bson.NextByte(buf) { - switch bson.ReadCString(buf) { - case "Czxid": - zkStat.czxid = bson.DecodeInt64(buf, kind) - case "Mzxid": - zkStat.mzxid = bson.DecodeInt64(buf, kind) - case "CTime": - zkStat.cTime = bson.DecodeTime(buf, kind) - case "MTime": - zkStat.mTime = bson.DecodeTime(buf, kind) - case "Version": - zkStat.version = bson.DecodeInt(buf, kind) - case "CVersion": - zkStat.cVersion = bson.DecodeInt(buf, kind) - case "AVersion": - zkStat.aVersion = bson.DecodeInt(buf, kind) - case "EphemeralOwner": - zkStat.ephemeralOwner = bson.DecodeInt64(buf, kind) - case "DataLength": - zkStat.dataLength = bson.DecodeInt(buf, kind) - case "NumChildren": - zkStat.numChildren = bson.DecodeInt(buf, kind) - case "Pzxid": - zkStat.pzxid = bson.DecodeInt64(buf, kind) - default: - bson.Skip(buf, kind) - } - } -} diff --git a/go/zk/zkutil.go b/go/zk/zkutil.go index dce5cf72042..c825e00015c 100644 --- a/go/zk/zkutil.go +++ b/go/zk/zkutil.go @@ -206,7 +206,7 @@ func resolveRecursive(zconn Conn, parts []string, toplevel bool) ([]string, erro var children []string var err error if i == 2 { - children, err = ZkKnownCells(false) + children, err = ZkKnownCells() if err != nil { return children, err } @@ -341,7 +341,7 @@ func DeleteRecursive(zconn Conn, zkPath string, version int) error { // numbering that don't hold when the data in a lock is modified. // if the provided 'interrupted' chan is closed, we'll just stop waiting // and return an interruption error -func ObtainQueueLock(zconn Conn, zkPath string, wait time.Duration, interrupted chan struct{}) error { +func ObtainQueueLock(zconn Conn, zkPath string, wait time.Duration, interrupted <-chan struct{}) error { queueNode := path.Dir(zkPath) lockNode := path.Base(zkPath) @@ -391,71 +391,6 @@ trylock: return fmt.Errorf("zkutil: empty queue node: %v", queueNode) } -// Close the release channel when you want to clean up nicely. -func CreatePidNode(zconn Conn, zkPath string, contents string, done chan struct{}) error { - // On the first try, assume the cluster is up and running, that will - // help hunt down any config issues present at startup - if _, err := zconn.Create(zkPath, contents, zookeeper.EPHEMERAL, zookeeper.WorldACL(PERM_FILE)); err != nil { - if zookeeper.IsError(err, zookeeper.ZNODEEXISTS) { - err = zconn.Delete(zkPath, -1) - } - if err != nil { - return fmt.Errorf("zkutil: failed deleting pid node: %v: %v", zkPath, err) - } - _, err = zconn.Create(zkPath, contents, zookeeper.EPHEMERAL, zookeeper.WorldACL(PERM_FILE)) - if err != nil { - return fmt.Errorf("zkutil: failed creating pid node: %v: %v", zkPath, err) - } - } - - go func() { - for { - _, _, watch, err := zconn.GetW(zkPath) - if err != nil { - if zookeeper.IsError(err, zookeeper.ZNONODE) { - _, err = zconn.Create(zkPath, contents, zookeeper.EPHEMERAL, zookeeper.WorldACL(zookeeper.PERM_ALL)) - if err != nil { - log.Warningf("failed recreating pid node: %v: %v", zkPath, err) - } else { - log.Infof("recreated pid node: %v", zkPath) - continue - } - } else { - log.Warningf("failed reading pid node: %v", err) - } - } else { - select { - case event := <-watch: - if event.Ok() && event.Type == zookeeper.EVENT_DELETED { - // Most likely another process has started up. However, - // there is a chance that an ephemeral node is deleted by - // the session expiring, yet that same session gets a watch - // notification. This seems like buggy behavior, but rather - // than race too hard on the node, just wait a bit and see - // if the situation resolves itself. - log.Warningf("pid deleted: %v", zkPath) - } else { - log.Infof("pid node event: %v", event) - } - // break here and wait for a bit before attempting - case <-done: - log.Infof("pid watcher stopped on done: %v", zkPath) - return - } - } - select { - // No one likes a thundering herd, least of all zookeeper. - case <-time.After(5*time.Second + time.Duration(rand.Int63n(55e9))): - case <-done: - log.Infof("pid watcher stopped on done: %v", zkPath) - return - } - } - }() - - return nil -} - // ZLocker is an interface for a lock that can fail. type ZLocker interface { Lock() error @@ -477,7 +412,7 @@ type zMutex struct { ephemeral bool } -// CreateMutex initializes an unaquired mutex. A mutex is released only +// CreateMutex initializes an unacquired mutex. A mutex is released only // by Unlock. You can clean up a mutex with delete, but you should be // careful doing so. func CreateMutex(zconn Conn, zkPath string) ZLocker { diff --git a/go/zk/zkutil_test.go b/go/zk/zkutil_test.go index 727d9e4536c..0a847691960 100644 --- a/go/zk/zkutil_test.go +++ b/go/zk/zkutil_test.go @@ -20,6 +20,65 @@ type TestZkConn struct { children map[string][]string } +type ZkStat struct { + czxid int64 + mzxid int64 + cTime time.Time + mTime time.Time + version int + cVersion int + aVersion int + ephemeralOwner int64 + dataLength int + numChildren int + pzxid int64 +} + +// ZkStat methods to match zk.Stat interface +func (zkStat *ZkStat) Czxid() int64 { + return zkStat.czxid +} + +func (zkStat *ZkStat) Mzxid() int64 { + return zkStat.mzxid +} + +func (zkStat *ZkStat) CTime() time.Time { + return zkStat.cTime +} + +func (zkStat *ZkStat) MTime() time.Time { + return zkStat.mTime +} + +func (zkStat *ZkStat) Version() int { + return zkStat.version +} + +func (zkStat *ZkStat) CVersion() int { + return zkStat.cVersion +} + +func (zkStat *ZkStat) AVersion() int { + return zkStat.aVersion +} + +func (zkStat *ZkStat) EphemeralOwner() int64 { + return zkStat.ephemeralOwner +} + +func (zkStat *ZkStat) DataLength() int { + return zkStat.dataLength +} + +func (zkStat *ZkStat) NumChildren() int { + return zkStat.numChildren +} + +func (zkStat *ZkStat) Pzxid() int64 { + return zkStat.pzxid +} + func (conn *TestZkConn) Get(path string) (data string, stat Stat, err error) { panic("Should not be used") } diff --git a/java/gorpc/pom.xml b/java/gorpc/pom.xml index ff0417047f0..999d4d6f201 100644 --- a/java/gorpc/pom.xml +++ b/java/gorpc/pom.xml @@ -1,81 +1,91 @@ - 4.0.0 - com.youtube.vitess - gorpc-client - 0.0.1-SNAPSHOT - - - junit - junit - 4.11 - test - - - org.mongodb - bson - 2.12.3 - - - commons-lang - commons-lang - 2.6 - - - com.google.code.gson - gson - 2.3 - - - log4j - log4j - 1.2.17 - - - org.mongodb - mongo-java-driver - 2.12.3 - - - com.google.guava - guava - 11.0.2 - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.7 - 1.7 - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.17 - - ${surefireArgLine} - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.13 - - ${failsafeArgLine} - - - - - integration-test - verify - - - - - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + com.youtube.vitess + gorpc-client + 0.0.1-SNAPSHOT + + + repo + https://github.com/youtube/mvn-repo/raw/master/releases + + + snapshot-repo + https://github.com/youtube/mvn-repo/raw/master/snapshots + + + + + junit + junit + 4.11 + test + + + org.mongodb + bson + 2.12.3 + + + commons-lang + commons-lang + 2.6 + + + com.google.code.gson + gson + 2.3 + + + log4j + log4j + 1.2.17 + + + org.mongodb + mongo-java-driver + 2.12.3 + + + com.google.guava + guava + 11.0.2 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.17 + + ${surefireArgLine} + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.13 + + ${failsafeArgLine} + + + + + integration-test + verify + + + + + + diff --git a/java/vtgate-client/README.md b/java/vtgate-client/README.md new file mode 100644 index 00000000000..71d139371d8 --- /dev/null +++ b/java/vtgate-client/README.md @@ -0,0 +1,21 @@ +VtGate Java Driver +================== + +Add the following repository and dependency to your pom.xml to use this client library. + +``` + + + youtube-snapshots + https://github.com/youtube/mvn-repo/raw/master/snapshots + + +``` + +``` + + com.youtube.vitess + vtgate-client + 0.0.1-SNAPSHOT + +``` diff --git a/java/vtgate-client/pom.xml b/java/vtgate-client/pom.xml index 4fef0117fbc..273205dcde0 100644 --- a/java/vtgate-client/pom.xml +++ b/java/vtgate-client/pom.xml @@ -1,99 +1,118 @@ - 4.0.0 - com.youtube.vitess - vtgate-client - 0.0.1-SNAPSHOT - - - junit - junit - 4.11 - test - - - com.youtube.vitess - gorpc-client - 0.0.1-SNAPSHOT - - - log4j - log4j - 1.2.17 - - - commons-codec - commons-codec - 1.9 - - - org.apache.hadoop - hadoop-client - 2.5.1 - - - joda-time - joda-time - 2.5 - - - org.apache.hadoop - hadoop-mapreduce-client-jobclient - 2.5.1 - - test-jar - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.7 - 1.7 - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.17 - - ${surefireArgLine} - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.13 - - ${failsafeArgLine} - - - - - integration-test - verify - - - - - - org.apache.maven.plugins - maven-shade-plugin - - false - - - - package - - shade - - - - - - - \ No newline at end of file + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + com.youtube.vitess + vtgate-client + 0.0.1-SNAPSHOT + + UTF-8 + + + + repo + https://github.com/youtube/mvn-repo/raw/master/releases + + + snapshot-repo + https://github.com/youtube/mvn-repo/raw/master/snapshots + + + + + youtube-snapshots + https://github.com/youtube/mvn-repo/raw/master/snapshots + + + + + junit + junit + 4.11 + test + + + com.youtube.vitess + gorpc-client + 0.0.1-SNAPSHOT + + + log4j + log4j + 1.2.17 + + + commons-codec + commons-codec + 1.9 + + + org.apache.hadoop + hadoop-client + 2.5.1 + + + joda-time + joda-time + 2.5 + + + org.apache.hadoop + hadoop-mapreduce-client-jobclient + 2.5.1 + + test-jar + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.17 + + ${surefireArgLine} + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.13 + + ${failsafeArgLine} + + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-shade-plugin + + false + + + + package + + shade + + + + + + + diff --git a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/SplitQueryRequest.java b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/SplitQueryRequest.java index 4c08a88bb4a..35b7e080f25 100644 --- a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/SplitQueryRequest.java +++ b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/SplitQueryRequest.java @@ -3,12 +3,12 @@ public class SplitQueryRequest { private String sql; private String keyspace; - private int splitsPerShard; + private int splitCount; - public SplitQueryRequest(String sql, String keyspace, int splitsPerShard) { + public SplitQueryRequest(String sql, String keyspace, int splitCount) { this.sql = sql; this.keyspace = keyspace; - this.splitsPerShard = splitsPerShard; + this.splitCount = splitCount; } public String getSql() { @@ -19,7 +19,7 @@ public String getKeyspace() { return keyspace; } - public int getSplitsPerShard() { - return splitsPerShard; + public int getSplitCount() { + return splitCount; } } diff --git a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/VtGate.java b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/VtGate.java index ce4381dce8a..aa16bac5444 100644 --- a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/VtGate.java +++ b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/VtGate.java @@ -70,6 +70,9 @@ public Cursor execute(Query query) throws DatabaseException, ConnectionException query.setSession(session); } QueryResponse response = client.execute(query); + if (response.getSession() != null) { + session = response.getSession(); + } String error = response.getError(); if (error != null) { if (error.contains(INTEGRITY_ERROR_MSG)) { @@ -77,9 +80,6 @@ public Cursor execute(Query query) throws DatabaseException, ConnectionException } throw new DatabaseException(response.getError()); } - if (response.getSession() != null) { - session = response.getSession(); - } if (query.isStreaming()) { return new StreamCursor(response.getResult(), client); } @@ -91,6 +91,9 @@ public List execute(BatchQuery query) throws DatabaseException, Connecti query.setSession(session); } BatchQueryResponse response = client.batchExecute(query); + if (response.getSession() != null) { + session = response.getSession(); + } String error = response.getError(); if (error != null) { if (error.contains(INTEGRITY_ERROR_MSG)) { @@ -98,9 +101,6 @@ public List execute(BatchQuery query) throws DatabaseException, Connecti } throw new DatabaseException(response.getError()); } - if (response.getSession() != null) { - session = response.getSession(); - } List cursors = new LinkedList<>(); for (QueryResult qr : response.getResults()) { cursors.add(new CursorImpl(qr)); @@ -114,9 +114,9 @@ public List execute(BatchQuery query) throws DatabaseException, Connecti * instances. Batch jobs or MapReduce jobs that needs to scan all rows can use these queries to * parallelize full table scans. */ - public Map splitQuery(String keyspace, String sql, int splitsPerShard) + public Map splitQuery(String keyspace, String sql, int splitCount) throws ConnectionException, DatabaseException { - SplitQueryRequest req = new SplitQueryRequest(sql, keyspace, splitsPerShard); + SplitQueryRequest req = new SplitQueryRequest(sql, keyspace, splitCount); SplitQueryResponse response = client.splitQuery(req); if (response.getError() != null) { throw new DatabaseException(response.getError()); diff --git a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessConf.java b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessConf.java index e0a7bff31fe..7bc27e51a10 100644 --- a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessConf.java +++ b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessConf.java @@ -1,11 +1,7 @@ package com.youtube.vitess.vtgate.hadoop; -import org.apache.commons.lang.StringUtils; import org.apache.hadoop.conf.Configuration; -import java.util.Arrays; -import java.util.List; - /** * Collection of configuration properties used for {@link VitessInputFormat} */ @@ -13,9 +9,8 @@ public class VitessConf { public static final String HOSTS = "vitess.vtgate.hosts"; public static final String CONN_TIMEOUT_MS = "vitess.vtgate.conn_timeout_ms"; public static final String INPUT_KEYSPACE = "vitess.vtgate.hadoop.keyspace"; - public static final String INPUT_TABLE = "vitess.vtgate.hadoop.input_table"; - public static final String INPUT_COLUMNS = "vitess.vtgate.hadoop.input_columns"; - public static final String SPLITS_PER_SHARD = "vitess.vtgate.hadoop.splits_per_shard"; + public static final String INPUT_QUERY = "vitess.vtgate.hadoop.input_query"; + public static final String SPLITS = "vitess.vtgate.hadoop.splits"; public static final String HOSTS_DELIM = ","; private Configuration conf; @@ -48,27 +43,19 @@ public void setKeyspace(String keyspace) { conf.set(INPUT_KEYSPACE, keyspace); } - public String getInputTable() { - return conf.get(INPUT_TABLE); - } - - public void setInputTable(String table) { - conf.set(INPUT_TABLE, table); - } - - public List getInputColumns() { - return Arrays.asList(StringUtils.split(conf.get(INPUT_COLUMNS), HOSTS_DELIM)); + public String getInputQuery() { + return conf.get(INPUT_QUERY); } - public void setInputColumns(List columns) { - conf.set(INPUT_COLUMNS, StringUtils.join(columns, HOSTS_DELIM)); + public void setInputQuery(String query) { + conf.set(INPUT_QUERY, query); } - public int getSplitsPerShard() { - return conf.getInt(SPLITS_PER_SHARD, 1); + public int getSplits() { + return conf.getInt(SPLITS, 1); } - public void setSplitsPerShard(int splitsPerShard) { - conf.setInt(SPLITS_PER_SHARD, splitsPerShard); + public void setSplits(int splits) { + conf.setInt(SPLITS, splits); } } diff --git a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessInputFormat.java b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessInputFormat.java index 349f0c23bfb..a7034854610 100644 --- a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessInputFormat.java +++ b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessInputFormat.java @@ -2,13 +2,12 @@ import com.youtube.vitess.vtgate.Exceptions.ConnectionException; import com.youtube.vitess.vtgate.Exceptions.DatabaseException; -import com.youtube.vitess.vtgate.KeyspaceId; import com.youtube.vitess.vtgate.Query; import com.youtube.vitess.vtgate.VtGate; import com.youtube.vitess.vtgate.hadoop.writables.KeyspaceIdWritable; import com.youtube.vitess.vtgate.hadoop.writables.RowWritable; -import org.apache.commons.lang.StringUtils; +import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.InputFormat; import org.apache.hadoop.mapreduce.InputSplit; import org.apache.hadoop.mapreduce.Job; @@ -26,20 +25,15 @@ * {@link VitessInputSplit}) are fetched from VtGate via an RPC. map() calls are supplied with a * {@link KeyspaceIdWritable}, {@link RowWritable} pair. */ -public class VitessInputFormat extends InputFormat { +public class VitessInputFormat extends InputFormat { @Override public List getSplits(JobContext context) { try { VitessConf conf = new VitessConf(context.getConfiguration()); VtGate vtgate = VtGate.connect(conf.getHosts(), conf.getTimeoutMs()); - List columns = conf.getInputColumns(); - if (!columns.contains(KeyspaceId.COL_NAME)) { - columns.add(KeyspaceId.COL_NAME); - } - String sql = "select " + StringUtils.join(columns, ',') + " from " + conf.getInputTable(); Map queries = - vtgate.splitQuery(conf.getKeyspace(), sql, conf.getSplitsPerShard()); + vtgate.splitQuery(conf.getKeyspace(), conf.getInputQuery(), conf.getSplits()); List splits = new LinkedList<>(); for (Query query : queries.keySet()) { Long size = queries.get(query); @@ -56,7 +50,7 @@ public List getSplits(JobContext context) { } } - public RecordReader createRecordReader(InputSplit split, + public RecordReader createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException { return new VitessRecordReader(); } @@ -67,15 +61,13 @@ public RecordReader createRecordReader(InputSpl public static void setInput(Job job, String hosts, String keyspace, - String table, - List columns, - int splitsPerShard) { + String query, + int splits) { job.setInputFormatClass(VitessInputFormat.class); VitessConf vtConf = new VitessConf(job.getConfiguration()); vtConf.setHosts(hosts); vtConf.setKeyspace(keyspace); - vtConf.setInputTable(table); - vtConf.setInputColumns(columns); - vtConf.setSplitsPerShard(splitsPerShard); + vtConf.setInputQuery(query); + vtConf.setSplits(splits); } } diff --git a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessRecordReader.java b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessRecordReader.java index 39383bd22b1..dcf63a33dcd 100644 --- a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessRecordReader.java +++ b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/VitessRecordReader.java @@ -2,25 +2,22 @@ import com.youtube.vitess.vtgate.Exceptions.ConnectionException; import com.youtube.vitess.vtgate.Exceptions.DatabaseException; -import com.youtube.vitess.vtgate.Exceptions.InvalidFieldException; -import com.youtube.vitess.vtgate.KeyspaceId; import com.youtube.vitess.vtgate.Row; import com.youtube.vitess.vtgate.VtGate; import com.youtube.vitess.vtgate.cursor.Cursor; -import com.youtube.vitess.vtgate.hadoop.writables.KeyspaceIdWritable; import com.youtube.vitess.vtgate.hadoop.writables.RowWritable; +import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.InputSplit; import org.apache.hadoop.mapreduce.RecordReader; import org.apache.hadoop.mapreduce.TaskAttemptContext; import java.io.IOException; -public class VitessRecordReader extends RecordReader { +public class VitessRecordReader extends RecordReader { private VitessInputSplit split; private VtGate vtgate; private VitessConf vtConf; - private KeyspaceIdWritable kidWritable; private RowWritable rowWritable; private long rowsProcessed = 0; private Cursor cursor; @@ -53,8 +50,8 @@ public void close() throws IOException { } @Override - public KeyspaceIdWritable getCurrentKey() throws IOException, InterruptedException { - return kidWritable; + public NullWritable getCurrentKey() throws IOException, InterruptedException { + return NullWritable.get(); } @Override @@ -89,12 +86,6 @@ public boolean nextKeyValue() throws IOException, InterruptedException { Row row = cursor.next(); rowWritable = new RowWritable(row); rowsProcessed++; - try { - KeyspaceId keyspaceId = KeyspaceId.valueOf(row.getULong(KeyspaceId.COL_NAME)); - kidWritable = new KeyspaceIdWritable(keyspaceId); - } catch (InvalidFieldException e) { - throw new RuntimeException(e); - } return true; } } diff --git a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/writables/RowWritable.java b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/writables/RowWritable.java index fa3432781b9..1c8ecdeccf8 100644 --- a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/writables/RowWritable.java +++ b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/hadoop/writables/RowWritable.java @@ -8,6 +8,7 @@ import com.youtube.vitess.vtgate.Row.Cell; import com.youtube.vitess.vtgate.utils.GsonAdapters; +import org.apache.commons.net.util.Base64; import org.apache.hadoop.io.Writable; import org.apache.hadoop.io.WritableComparable; @@ -27,8 +28,10 @@ public class RowWritable implements Writable { // Row contains UnsignedLong and Class objects which need custom adapters private Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(byte[].class, GsonAdapters.BYTE_ARRAY) .registerTypeAdapter(UnsignedLong.class, GsonAdapters.UNSIGNED_LONG) - .registerTypeAdapter(Class.class, GsonAdapters.CLASS).create(); + .registerTypeAdapter(Class.class, GsonAdapters.CLASS) + .create(); public RowWritable() { @@ -53,7 +56,11 @@ public void write(DataOutput out) throws IOException { public void writeCell(DataOutput out, Cell cell) throws IOException { out.writeUTF(cell.getName()); out.writeUTF(cell.getType().getName()); - out.writeUTF(cell.getValue().toString()); + if (cell.getType().equals(byte[].class)){ + out.writeUTF(Base64.encodeBase64String((byte[])cell.getValue())); + } else{ + out.writeUTF(cell.getValue().toString()); + } } @Override @@ -98,6 +105,9 @@ public Cell readCell(DataInput in) throws IOException { if (clazz.equals(String.class)) { val = value; } + if (clazz.equals(byte[].class)) { + val = Base64.decodeBase64(value); + } if (val == null) { throw new RuntimeException("unknown type in RowWritable: " + clazz); } diff --git a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/rpcclient/gorpc/Bsonify.java b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/rpcclient/gorpc/Bsonify.java index e4e0b9c8499..c4e60dfe9c4 100644 --- a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/rpcclient/gorpc/Bsonify.java +++ b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/rpcclient/gorpc/Bsonify.java @@ -186,7 +186,7 @@ public static BSONObject splitQueryRequestToBson(SplitQueryRequest request) { BSONObject b = new BasicBSONObject(); b.put("Keyspace", request.getKeyspace()); b.put("Query", query); - b.put("SplitsPerShard", request.getSplitsPerShard()); + b.put("SplitCount", request.getSplitCount()); return b; } diff --git a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/rpcclient/gorpc/GoRpcClient.java b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/rpcclient/gorpc/GoRpcClient.java index 057cd79ec5c..f7ee8448c65 100644 --- a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/rpcclient/gorpc/GoRpcClient.java +++ b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/rpcclient/gorpc/GoRpcClient.java @@ -109,7 +109,7 @@ public void close() throws ConnectionException { @Override public SplitQueryResponse splitQuery(SplitQueryRequest request) throws ConnectionException { - String callMethod = "VTGate.GetMRSplits"; + String callMethod = "VTGate.SplitQuery"; Response response = call(callMethod, Bsonify.splitQueryRequestToBson(request)); return Bsonify.bsonToSplitQueryResponse((BSONObject) response.getReply()); } diff --git a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/utils/GsonAdapters.java b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/utils/GsonAdapters.java index 484a27f65d2..f0dac1f6758 100644 --- a/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/utils/GsonAdapters.java +++ b/java/vtgate-client/src/main/java/com/youtube/vitess/vtgate/utils/GsonAdapters.java @@ -1,13 +1,23 @@ package com.youtube.vitess.vtgate.utils; import com.google.common.primitives.UnsignedLong; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; +import org.apache.commons.codec.binary.Base64; + import java.io.IOException; +import java.lang.reflect.Type; /** * Custom GSON adapters for {@link UnsignedLong} and {@link Class} types @@ -52,4 +62,18 @@ public void write(JsonWriter out, Class value) throws IOException { out.value(value.getName()); } }; + + public static final Object BYTE_ARRAY = new ByteArrayAdapter(); + + private static class ByteArrayAdapter implements JsonSerializer, JsonDeserializer { + @Override + public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Base64.decodeBase64(json.getAsString()); + } + + @Override + public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(Base64.encodeBase64String(src)); + } + } } diff --git a/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/FailuresIT.java b/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/FailuresIT.java index a0225c5505a..aa3acab9a9b 100644 --- a/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/FailuresIT.java +++ b/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/FailuresIT.java @@ -19,6 +19,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -51,6 +52,7 @@ public void createTable() throws Exception { } @Test + @Ignore("causing other tests to fail") public void testIntegrityException() throws Exception { VtGate vtgate = VtGate.connect("localhost:" + testEnv.port, 0); String insertSql = "insert into vtgate_test(id, keyspace_id) values (:id, :keyspace_id)"; diff --git a/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/VtGateIT.java b/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/VtGateIT.java index 17e5bd0e8ce..a4a55ef600d 100644 --- a/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/VtGateIT.java +++ b/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/VtGateIT.java @@ -20,6 +20,7 @@ import com.youtube.vitess.vtgate.integration.util.TestEnv; import com.youtube.vitess.vtgate.integration.util.Util; +import org.apache.commons.codec.binary.Hex; import org.joda.time.DateTime; import org.junit.AfterClass; import org.junit.Assert; @@ -33,10 +34,12 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Set; @RunWith(JUnit4.class) public class VtGateIT { @@ -312,6 +315,90 @@ public void testBatchExecuteKeyspaceIds() throws Exception { } + @Test + public void testSplitQuery() throws Exception { + // Insert 20 rows per shard + for (String shardName : testEnv.shardKidMap.keySet()) { + Util.insertRowsInShard(testEnv, shardName, 20); + } + Util.waitForTablet("rdonly", 40, 3, testEnv); + VtGate vtgate = VtGate.connect("localhost:" + testEnv.port, 0); + Map queries = + vtgate.splitQuery("test_keyspace", "select id,keyspace_id from vtgate_test", 1); + vtgate.close(); + + // Verify 2 splits, one per shard + Assert.assertEquals(2, queries.size()); + Set shardsInSplits = new HashSet<>(); + for (Query q : queries.keySet()) { + Assert.assertEquals("select id,keyspace_id from vtgate_test", q.getSql()); + Assert.assertEquals("test_keyspace", q.getKeyspace()); + Assert.assertEquals("rdonly", q.getTabletType()); + Assert.assertEquals(0, q.getBindVars().size()); + Assert.assertEquals(null, q.getKeyspaceIds()); + String start = Hex.encodeHexString(q.getKeyRanges().get(0).get("Start")); + String end = Hex.encodeHexString(q.getKeyRanges().get(0).get("End")); + shardsInSplits.add(start + "-" + end); + } + + // Verify the keyrange queries in splits cover the entire keyspace + Assert.assertTrue(shardsInSplits.containsAll(testEnv.shardKidMap.keySet())); + } + + @Test + public void testSplitQueryMultipleSplitsPerShard() throws Exception { + int rowCount = 30; + Util.insertRows(testEnv, 1, 30); + List expectedSqls = + Lists.newArrayList("select id, keyspace_id from vtgate_test where id < 10", + "select id, keyspace_id from vtgate_test where id < 11", + "select id, keyspace_id from vtgate_test where id >= 10 and id < 19", + "select id, keyspace_id from vtgate_test where id >= 11 and id < 19", + "select id, keyspace_id from vtgate_test where id >= 19", + "select id, keyspace_id from vtgate_test where id >= 19"); + Util.waitForTablet("rdonly", rowCount, 3, testEnv); + VtGate vtgate = VtGate.connect("localhost:" + testEnv.port, 0); + int splitCount = 6; + Map queries = + vtgate.splitQuery("test_keyspace", "select id,keyspace_id from vtgate_test", splitCount); + vtgate.close(); + + // Verify 6 splits, 3 per shard + Assert.assertEquals(splitCount, queries.size()); + Set shardsInSplits = new HashSet<>(); + for (Query q : queries.keySet()) { + String sql = q.getSql(); + Assert.assertTrue(expectedSqls.contains(sql)); + expectedSqls.remove(sql); + Assert.assertEquals("test_keyspace", q.getKeyspace()); + Assert.assertEquals("rdonly", q.getTabletType()); + Assert.assertEquals(0, q.getBindVars().size()); + Assert.assertEquals(null, q.getKeyspaceIds()); + String start = Hex.encodeHexString(q.getKeyRanges().get(0).get("Start")); + String end = Hex.encodeHexString(q.getKeyRanges().get(0).get("End")); + shardsInSplits.add(start + "-" + end); + } + + // Verify the keyrange queries in splits cover the entire keyspace + Assert.assertTrue(shardsInSplits.containsAll(testEnv.shardKidMap.keySet())); + Assert.assertTrue(expectedSqls.size() == 0); + } + + @Test + public void testSplitQueryInvalidTable() throws Exception { + VtGate vtgate = VtGate.connect("localhost:" + testEnv.port, 0); + try { + vtgate.splitQuery("test_keyspace", "select id from invalid_table", 1); + Assert.fail("failed to raise connection exception"); + } catch (ConnectionException e) { + Assert.assertTrue( + e.getMessage().contains("query validation error: can't find table in schema")); + } finally { + vtgate.close(); + } + } + + /** * Create env with two shards each having a master and replica */ @@ -322,7 +409,7 @@ static TestEnv getTestEnv() { shardKidMap.put("80-", Lists.newArrayList("9767889778372766922", "9742070682920810358", "10296850775085416642")); TestEnv env = new TestEnv(shardKidMap, "test_keyspace"); - env.addTablet("replica", 1); + env.addTablet("rdonly", 1); return env; } } diff --git a/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/hadoop/MapReduceIT.java b/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/hadoop/MapReduceIT.java index 2d2e2510421..d84b9970754 100644 --- a/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/hadoop/MapReduceIT.java +++ b/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/hadoop/MapReduceIT.java @@ -3,21 +3,21 @@ import com.google.common.collect.Lists; import com.google.common.primitives.UnsignedLong; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; -import com.youtube.vitess.vtgate.Exceptions.ConnectionException; +import com.youtube.vitess.vtgate.Exceptions.InvalidFieldException; import com.youtube.vitess.vtgate.KeyspaceId; -import com.youtube.vitess.vtgate.Query; -import com.youtube.vitess.vtgate.VtGate; import com.youtube.vitess.vtgate.hadoop.VitessInputFormat; import com.youtube.vitess.vtgate.hadoop.writables.KeyspaceIdWritable; import com.youtube.vitess.vtgate.hadoop.writables.RowWritable; import com.youtube.vitess.vtgate.integration.util.TestEnv; import com.youtube.vitess.vtgate.integration.util.Util; +import com.youtube.vitess.vtgate.utils.GsonAdapters; import junit.extensions.TestSetup; import junit.framework.TestSuite; -import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.binary.Base64; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; @@ -30,7 +30,6 @@ import org.apache.hadoop.mapreduce.Reducer; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat; -import org.junit.Test; import java.io.IOException; import java.util.HashMap; @@ -61,88 +60,6 @@ public void setUp() throws Exception { Util.createTable(testEnv); } - @Test - public void testSplitQuery() throws Exception { - // Insert 20 rows per shard - for (String shardName : testEnv.shardKidMap.keySet()) { - Util.insertRowsInShard(testEnv, shardName, 20); - } - Util.waitForTablet("rdonly", 40, 3, testEnv); - VtGate vtgate = VtGate.connect("localhost:" + testEnv.port, 0); - Map queries = - vtgate.splitQuery("test_keyspace", "select id,keyspace_id from vtgate_test", 1); - vtgate.close(); - - // Verify 2 splits, one per shard - assertEquals(2, queries.size()); - Set shardsInSplits = new HashSet<>(); - for (Query q : queries.keySet()) { - assertEquals("select id,keyspace_id from vtgate_test", q.getSql()); - assertEquals("test_keyspace", q.getKeyspace()); - assertEquals("rdonly", q.getTabletType()); - assertEquals(0, q.getBindVars().size()); - assertEquals(null, q.getKeyspaceIds()); - String start = Hex.encodeHexString(q.getKeyRanges().get(0).get("Start")); - String end = Hex.encodeHexString(q.getKeyRanges().get(0).get("End")); - shardsInSplits.add(start + "-" + end); - } - - // Verify the keyrange queries in splits cover the entire keyspace - assertTrue(shardsInSplits.containsAll(testEnv.shardKidMap.keySet())); - } - - @Test - public void testSplitQueryMultipleSplitsPerShard() throws Exception { - int rowCount = 30; - Util.insertRows(testEnv, 1, 30); - List expectedSqls = - Lists.newArrayList("select id, keyspace_id from vtgate_test where id < 10", - "select id, keyspace_id from vtgate_test where id < 11", - "select id, keyspace_id from vtgate_test where id >= 10 and id < 19", - "select id, keyspace_id from vtgate_test where id >= 11 and id < 19", - "select id, keyspace_id from vtgate_test where id >= 19", - "select id, keyspace_id from vtgate_test where id >= 19"); - Util.waitForTablet("rdonly", rowCount, 3, testEnv); - VtGate vtgate = VtGate.connect("localhost:" + testEnv.port, 0); - int splitsPerShard = 3; - Map queries = vtgate.splitQuery("test_keyspace", - "select id,keyspace_id from vtgate_test", splitsPerShard); - vtgate.close(); - - // Verify 6 splits, 3 per shard - assertEquals(2 * splitsPerShard, queries.size()); - Set shardsInSplits = new HashSet<>(); - for (Query q : queries.keySet()) { - String sql = q.getSql(); - assertTrue(expectedSqls.contains(sql)); - expectedSqls.remove(sql); - assertEquals("test_keyspace", q.getKeyspace()); - assertEquals("rdonly", q.getTabletType()); - assertEquals(0, q.getBindVars().size()); - assertEquals(null, q.getKeyspaceIds()); - String start = Hex.encodeHexString(q.getKeyRanges().get(0).get("Start")); - String end = Hex.encodeHexString(q.getKeyRanges().get(0).get("End")); - shardsInSplits.add(start + "-" + end); - } - - // Verify the keyrange queries in splits cover the entire keyspace - assertTrue(shardsInSplits.containsAll(testEnv.shardKidMap.keySet())); - assertTrue(expectedSqls.size() == 0); - } - - @Test - public void testSplitQueryInvalidTable() throws Exception { - VtGate vtgate = VtGate.connect("localhost:" + testEnv.port, 0); - try { - vtgate.splitQuery("test_keyspace", "select id from invalid_table", 1); - fail("failed to raise connection exception"); - } catch (ConnectionException e) { - assertTrue(e.getMessage().contains("query validation error: can't find table in schema")); - } finally { - vtgate.close(); - } - } - /** * Run a mapper only MR job and verify all the rows in the source table were outputted into HDFS. */ @@ -162,10 +79,9 @@ public void testDumpTableToHDFS() throws Exception { VitessInputFormat.setInput(job, "localhost:" + testEnv.port, testEnv.keyspace, - "vtgate_test", - Lists.newArrayList("keyspace_id", "name"), + "select keyspace_id, name from vtgate_test", 4); - job.setOutputKeyClass(KeyspaceIdWritable.class); + job.setOutputKeyClass(NullWritable.class); job.setOutputValueClass(RowWritable.class); job.setOutputFormatClass(TextOutputFormat.class); job.setNumReduceTasks(0); @@ -189,18 +105,22 @@ public void testDumpTableToHDFS() throws Exception { // Rows are keyspace ids are written as JSON since this is // TextOutputFormat. Parse and verify we've gotten all the keyspace // ids and rows. - Gson gson = new Gson(); + Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(byte[].class, GsonAdapters.BYTE_ARRAY) + .registerTypeAdapter(UnsignedLong.class, GsonAdapters.UNSIGNED_LONG) + .registerTypeAdapter(Class.class, GsonAdapters.CLASS) + .create(); for (String line : outputLines) { - System.out.println(line); String kidJson = line.split("\t")[0]; Map m = new HashMap<>(); m = gson.fromJson(kidJson, m.getClass()); actualKids.add(m.get("id")); String rowJson = line.split("\t")[1]; - Map>> map = new HashMap<>(); + Map>> map = new HashMap<>(); map = gson.fromJson(rowJson, map.getClass()); - actualNames.add(new String(map.get("contents").get("name").get("value"))); + actualNames.add( + new String(Base64.decodeBase64(map.get("contents").get("name").get("value")))); } Set expectedKids = new HashSet<>(); @@ -211,7 +131,7 @@ public void testDumpTableToHDFS() throws Exception { assertTrue(actualKids.containsAll(expectedKids)); Set expectedNames = new HashSet<>(); - for (int i = 0; i < rowsPerShard; i++) { + for (int i = 1; i <= rowsPerShard; i++) { expectedNames.add("name_" + i); } @@ -222,7 +142,6 @@ public void testDumpTableToHDFS() throws Exception { /** * Map all rows and aggregate by keyspace id at the reducer. */ - @Test public void testReducerAggregateRows() throws Exception { int rowsPerShard = 20; for (String shardName : testEnv.shardKidMap.keySet()) { @@ -238,8 +157,7 @@ public void testReducerAggregateRows() throws Exception { VitessInputFormat.setInput(job, "localhost:" + testEnv.port, testEnv.keyspace, - "vtgate_test", - Lists.newArrayList("keyspace_id", "name"), + "select keyspace_id, name from vtgate_test", 1); job.setMapOutputKeyClass(KeyspaceIdWritable.class); @@ -270,11 +188,17 @@ public void testReducerAggregateRows() throws Exception { } public static class TableMapper extends - Mapper { + Mapper { @Override - public void map(KeyspaceIdWritable key, RowWritable value, Context context) throws IOException, + public void map(NullWritable key, RowWritable value, Context context) throws IOException, InterruptedException { - context.write(key, value); + try { + KeyspaceId id = new KeyspaceId(); + id.setId(value.getRow().getULong("keyspace_id")); + context.write(new KeyspaceIdWritable(id), value); + } catch (InvalidFieldException e) { + throw new IOException(e); + } } } @@ -285,8 +209,9 @@ public void reduce(KeyspaceIdWritable key, Iterable values, Context throws IOException, InterruptedException { long count = 0; Iterator iter = values.iterator(); - while (iter.next() != null) { + while (iter.hasNext()) { count++; + iter.next(); } context.write(NullWritable.get(), new LongWritable(count)); } diff --git a/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/util/Util.java b/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/util/Util.java index 648243c2728..cea4ceb6306 100644 --- a/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/util/Util.java +++ b/java/vtgate-client/src/test/java/com/youtube/vitess/vtgate/integration/util/Util.java @@ -135,11 +135,15 @@ public static void waitForTablet(String tabletType, int rowCount, int attempts, String sql = "select * from vtgate_test"; VtGate vtgate = VtGate.connect("localhost:" + testEnv.port, 0); for (int i = 0; i < attempts; i++) { - Cursor cursor = vtgate.execute(new QueryBuilder(sql, testEnv.keyspace, tabletType) - .setKeyspaceIds(testEnv.getAllKeyspaceIds()).build()); - if (cursor.getRowsAffected() >= rowCount) { - vtgate.close(); - return; + try { + Cursor cursor = vtgate.execute(new QueryBuilder(sql, testEnv.keyspace, tabletType) + .setKeyspaceIds(testEnv.getAllKeyspaceIds()).build()); + if (cursor.getRowsAffected() >= rowCount) { + vtgate.close(); + return; + } + } catch (DatabaseException e) { + } Thread.sleep(1000); } diff --git a/misc/git/hooks/gofmt b/misc/git/hooks/gofmt index 44ef1298987..6306dd1905f 100755 --- a/misc/git/hooks/gofmt +++ b/misc/git/hooks/gofmt @@ -9,7 +9,7 @@ # it has execute permissions. # # This script does not handle file names that contain spaces. -gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$') +gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '^go/.*\.go$') [ -z "$gofiles" ] && exit 0 unformatted=$(goimports -l=true $gofiles 2>&1 | awk -F: '{print $1}') diff --git a/misc/git/hooks/golint b/misc/git/hooks/golint new file mode 100755 index 00000000000..c5b94c11287 --- /dev/null +++ b/misc/git/hooks/golint @@ -0,0 +1,45 @@ +#!/bin/bash +# Copyright 2012 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# git golint pre-commit hook +# +# To use, store as .git/hooks/pre-commit inside your repository and make sure +# it has execute permissions. +# +# This script does not handle file names that contain spaces. + +if [ -z "$(which golint)" ]; then + echo "golint not found, please run: go get github.com/golang/lint/golint" + exit 1 +fi + +gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '^go/.*\.go$') + +errors= + +# Run on one file at a time because a single invocation of golint +# with multiple files requires the files to all be in one package. +for gofile in $gofiles +do + errcount=$(golint $gofile | wc -l) + if [ "$errcount" -gt "0" ]; then + errors=YES + echo "$errcount suggestions for: golint $gofile" + fi +done + +[ -z "$errors" ] && exit 0 + +# git doesn't give us access to user input, so let's steal it. +exec < /dev/tty + +echo +echo "Lint suggestions were found. They're not enforced, but we're pausing" +echo "to let you know before they get clobbered in the scrollback buffer." +echo +read -r -p 'Press enter to cancel, or type "ack" to continue: ' +if [ "$REPLY" != "ack" ]; then + exit 1 +fi diff --git a/misc/git/hooks/govet b/misc/git/hooks/govet index 19cc55fd1b5..b98f471a363 100755 --- a/misc/git/hooks/govet +++ b/misc/git/hooks/govet @@ -9,7 +9,7 @@ # it has execute permissions. # # This script does not handle file names that contain spaces. -gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$') +gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '^go/.*\.go$') # If any checks are found to be useless, they can be disabled here. # See the output of "go tool vet" for a list of flags. diff --git a/php/bsonrpc.php b/php/bsonrpc.php new file mode 100644 index 00000000000..010acd94d0c --- /dev/null +++ b/php/bsonrpc.php @@ -0,0 +1,42 @@ +write(bson_encode($req->header)); + if ($req->body === NULL) + $this->write(bson_encode(array())); + else + $this->write(bson_encode($req->body)); + } + + protected function read_response() { + // Read the header. + $data = $this->read_n(self::LEN_PACK_SIZE); + $len = unpack(self::LEN_PACK_FORMAT, $data)[1]; + $header = $data . $this->read_n($len - self::LEN_PACK_SIZE); + + // Read the body. + $data = $this->read_n(self::LEN_PACK_SIZE); + $len = unpack(self::LEN_PACK_FORMAT, $data)[1]; + $body = $data . $this->read_n($len - self::LEN_PACK_SIZE); + + // Decode and return. + return new GoRpcResponse(bson_decode($header), bson_decode($body)); + } +} diff --git a/php/gorpc.php b/php/gorpc.php new file mode 100644 index 00000000000..3695d8d9c52 --- /dev/null +++ b/php/gorpc.php @@ -0,0 +1,139 @@ +header = array('ServiceMethod' => $method, 'Seq' => $seq); + $this->body = $body; + } + + public function seq() { + return $this->header['Seq']; + } +} + +class GoRpcResponse { + public $header; + public $reply; + + public function __construct($header, $body) { + $this->header = $header; + $this->reply = $body; + } + + public function error() { + return $this->header['Error']; + } + + public function seq() { + return $this->header['Seq']; + } + + public function is_eos() { + return $this->error() == self::END_OF_STREAM; + } + + const END_OF_STREAM = 'EOS'; +} + +abstract class GoRpcClient { + protected $seq = 0; + protected $stream = NULL; + + abstract protected function send_request(GoRpcRequest $req); + abstract protected function read_response(); + + public function dial($addr, $path) { + // Connect to $addr. + $fp = stream_socket_client($addr, $errno, $errstr); + if ($fp === FALSE) + throw new GoRpcException("can't connect to $addr: $errstr ($errno)"); + $this->stream = $fp; + + // Initiate request for $path. + $this->write("CONNECT $path HTTP/1.0\n\n"); + + // Read until handshake is completed. + $data = ''; + while (strpos($data, "\n\n") === FALSE) + $data .= $this->read(1024); + } + + public function close() { + if ($this->stream !== NULL) { + fclose($this->stream); + $this->stream = NULL; + } + } + + public function call($method, $request) { + $req = new GoRpcRequest($this->next_seq(), $method, $request); + $this->send_request($req); + + $resp = $this->read_response(); + if ($resp->seq() != $req->seq()) + throw new GoRpcException("$method: request sequence mismatch"); + if ($resp->error()) + throw new GoRpcRemoteError("$method: " . $resp->error()); + + return $resp; + } + + public function stream_call($method, $request) { + $req = new GoRpcRequest($this->next_seq(), $method, $request); + $this->send_request($req); + } + + public function stream_next() { + $resp = $this->read_response(); + if ($resp->seq() != $this->seq) + throw new GoRpcException("$method: request sequence mismatch"); + if ($resp->is_eos()) + return FALSE; + if ($resp->error()) + throw new GoRpcRemoteError("$method: " . $resp->error()); + return $resp; + } + + protected function read($max_len) { + if (feof($this->stream)) + throw new GoRpcException("unexpected EOF while reading from stream"); + $packet = fread($this->stream, $max_len); + if ($packet === FALSE) + throw new GoRpcException("can't read from stream"); + return $packet; + } + + protected function read_n($target_len) { + // Read exactly $target_len bytes or bust. + $data = ''; + while (($len = strlen($data)) < $target_len) + $data .= $this->read($target_len - $len); + return $data; + } + + protected function write($data) { + if (fwrite($this->stream, $data) === FALSE) + throw new GoRpcException("can't write to stream"); + } + + protected function next_seq() { + return ++$this->seq; + } +} diff --git a/php/test.php b/php/test.php new file mode 100644 index 00000000000..52f715a22e2 --- /dev/null +++ b/php/test.php @@ -0,0 +1,20 @@ +beginTransaction(); +$dbh->exec('INSERT INTO user (name) VALUES (:name)', array('name' => 'user 1'), 'master'); +$dbh->exec('INSERT INTO user (name) VALUES (:name)', array('name' => 'user 2'), 'master'); +$dbh->commit(); + +$stmt = $dbh->query('SELECT * FROM user'); +print_r($stmt->fetchAll()); diff --git a/php/vtgatev3.php b/php/vtgatev3.php new file mode 100644 index 00000000000..5031a5fa6bf --- /dev/null +++ b/php/vtgatev3.php @@ -0,0 +1,93 @@ +res = $res; + } + + public function rowCount() { + return $this->res['RowsAffected']; + } + + public function fetch() { + if ($this->i >= count($this->res['Rows'])) + return FALSE; + return $this->res['Rows'][$this->i++]; + } + + public function fetchAll() { + return $this->res['Rows']; + } + + public function closeCursor() { + $this->res = NULL; + } +} + +class VTGateConnection { + protected $rpc = NULL; + protected $session = NULL; + + public function __construct($addr) { + $this->rpc = new BsonRpcClient(); + $this->rpc->dial($addr); + } + + public function beginTransaction() { + $resp = $this->rpc->call('VTGate.Begin', NULL); + $this->session = $resp->reply; + } + + public function commit() { + $session = $this->session; + $this->session = NULL; + $this->rpc->call('VTGate.Commit', $session); + } + + public function rollBack() { + $session = $this->session; + $this->session = NULL; + $this->rpc->call('VTGate.Rollback', $session); + } + + public function execute($sql, $bind_vars, $tablet_type) { + $req = array( + 'Sql' => $sql, + 'BindVariables' => $bind_vars, + 'TabletType' => $tablet_type, + ); + if ($this->session) + $req['Session'] = $this->session; + + $resp = $this->rpc->call('VTGate.Execute', $req); + + $reply = $resp->reply; + if (array_key_exists('Session', $reply) && $reply['Session']) + $this->session = $reply['Session']; + if (array_key_exists('Error', $reply) && $reply['Error']) + throw new GoRpcRemoteError('exec: ' . $reply['Error']); + + return $reply['Result']; + } + + public function exec($sql, $bind_vars, $tablet_type) { + $res = $this->execute($sql, $bind_vars, $tablet_type); + return $res['RowsAffected']; + } + + public function query($sql, $bind_vars, $tablet_type) { + $res = $this->execute($sql, $bind_vars, $tablet_type); + return new VTGateStatement($res); + } +} diff --git a/py/cbson/cbson.c b/py/cbson/cbson.c index 451fb739731..e1224bf1967 100644 --- a/py/cbson/cbson.c +++ b/py/cbson/cbson.c @@ -148,7 +148,7 @@ static int buf_iter_from_buffer(PyObject* buffer_obj, BufIter* buf_iter, Py_buff /* ------------------------------------------------------------------------ */ -/* decode_element is the function prototype for parsing differnt +/* decode_element is the function prototype for parsing different * values in the bson stream */ typedef PyObject* (*decode_element)(BufIter* buf_iter); diff --git a/py/vtdb/cursorv3.py b/py/vtdb/cursorv3.py new file mode 100644 index 00000000000..377e89448d5 --- /dev/null +++ b/py/vtdb/cursorv3.py @@ -0,0 +1,191 @@ +# Copyright 2012, Google Inc. All rights reserved. +# Use of this source code is governed by a BSD-style license that can +# be found in the LICENSE file. + +from vtdb import cursor +from vtdb import dbexceptions + + +class Cursor(object): + _conn = None + tablet_type = None + arraysize = 1 + lastrowid = None + rowcount = 0 + results = None + description = None + index = None + + def __init__(self, connection, tablet_type): + self._conn = connection + self.tablet_type = tablet_type + + def close(self): + self.results = None + + def commit(self): + return self._conn.commit() + + def begin(self): + return self._conn.begin() + + def rollback(self): + return self._conn.rollback() + + def execute(self, sql, bind_variables): + self.rowcount = 0 + self.results = None + self.description = None + self.lastrowid = None + + sql_check = sql.strip().lower() + if sql_check == 'begin': + self.begin() + return + elif sql_check == 'commit': + self.commit() + return + elif sql_check == 'rollback': + self.rollback() + return + + self.results, self.rowcount, self.lastrowid, self.description = self._conn._execute( + sql, + bind_variables, + self.tablet_type) + self.index = 0 + return self.rowcount + + def fetchone(self): + if self.results is None: + raise dbexceptions.ProgrammingError('fetch called before execute') + + if self.index >= len(self.results): + return None + self.index += 1 + return self.results[self.index-1] + + def fetchmany(self, size=None): + if self.results is None: + raise dbexceptions.ProgrammingError('fetch called before execute') + + if self.index >= len(self.results): + return [] + if size is None: + size = self.arraysize + res = self.results[self.index:self.index+size] + self.index += size + return res + + def fetchall(self): + if self.results is None: + raise dbexceptions.ProgrammingError('fetch called before execute') + return self.fetchmany(len(self.results)-self.index) + + def callproc(self): + raise dbexceptions.NotSupportedError + + def executemany(self, *pargs): + raise dbexceptions.NotSupportedError + + def nextset(self): + raise dbexceptions.NotSupportedError + + def setinputsizes(self, sizes): + pass + + def setoutputsize(self, size, column=None): + pass + + @property + def rownumber(self): + return self.index + + def __iter__(self): + return self + + def next(self): + val = self.fetchone() + if val is None: + raise StopIteration + return val + + +class StreamCursor(Cursor): + arraysize = 1 + conversions = None + connection = None + description = None + index = None + fetchmany_done = False + + def execute(self, sql, bind_variables, **kargs): + self.description = None + x, y, z, self.description = self._conn._stream_execute( + sql, + bind_variables, + self.tablet_type) + self.index = 0 + return 0 + + def fetchone(self): + if self.description is None: + raise dbexceptions.ProgrammingError('fetch called before execute') + + self.index += 1 + return self._conn._stream_next() + + # fetchmany can be called until it returns no rows. Returning less rows + # than what we asked for is also an indication we ran out, but the cursor + # API in PEP249 is silent about that. + def fetchmany(self, size=None): + if size is None: + size = self.arraysize + result = [] + if self.fetchmany_done: + self.fetchmany_done = False + return result + for i in xrange(size): + row = self.fetchone() + if row is None: + self.fetchmany_done = True + break + result.append(row) + return result + + def fetchall(self): + result = [] + while True: + row = self.fetchone() + if row is None: + break + result.append(row) + return result + + def callproc(self): + raise dbexceptions.NotSupportedError + + def executemany(self, *pargs): + raise dbexceptions.NotSupportedError + + def nextset(self): + raise dbexceptions.NotSupportedError + + def setinputsizes(self, sizes): + pass + + def setoutputsize(self, size, column=None): + pass + + @property + def rownumber(self): + return self.index + + def __iter__(self): + return self + + def next(self): + val = self.fetchone() + if val is None: + raise StopIteration + return val diff --git a/py/vtdb/database_context.py b/py/vtdb/database_context.py new file mode 100644 index 00000000000..c46687a4d06 --- /dev/null +++ b/py/vtdb/database_context.py @@ -0,0 +1,321 @@ +"""Database Context sets the environment for accessing database via vtgate. + +This modules has classes and methods that initialize the vitess system. +This includes - +* DatabaseContext is the primary object that sets the mode of operation, +logging handlers, connection to VTGate etc. +* Error logging helpers +* Database Operation context managers and decorators that set the local +context for different types of read operations and write transaction. This +governs the tablet_type and aid with cursor creation and transaction +management. +""" + +# Copyright 2013 Google Inc. All Rights Reserved. +# Use of this source code is governed by a BSD-style license that can +# be found in the LICENSE file. + +import contextlib +import functools +import logging + +from vtdb import dbexceptions +from vtdb import shard_constants +from vtdb import vtdb_logger +from vtdb import vtgatev2 + + +#TODO: verify that these values make sense. +DEFAULT_CONNECTION_TIMEOUT = 5.0 +DEFAULT_QUERY_TIMEOUT = 15.0 + +__app_read_only_mode_method = lambda:False +__vtgate_connect_method = vtgatev2.connect +#TODO: perhaps make vtgate addrs also a registeration mechanism ? +#TODO: add mechansim to refresh vtgate addrs. + + +class DatabaseContext(object): + """Global Database Context for client db operations via VTGate. + + This is the main entry point for db access. Responsibilities include - + * Setting the operational environment. + * Manages connection to VTGate. + * Manages the database transaction. + * Error logging and handling. + + Attributes: + lag_tolerant_mode: This directs all replica traffic to batch replicas. + This is done for applications that have a OLAP workload and also higher tolerance + for replication lag. + vtgate_addrs: vtgate server endpoints + master_access_disabled: Disallow master access for application running in non-master + capable cells. + event_logger: Logs events and errors of note. Defaults to vtdb_logger. + transaction_stack_depth: This allows nesting of transactions and makes + commit rpc to VTGate when the outer-most commits. + vtgate_connection: Connection to VTGate. + """ + + def __init__(self, vtgate_addrs=None, lag_tolerant_mode=False, master_access_disabled=False): + self.vtgate_addrs = vtgate_addrs + self.lag_tolerant_mode = lag_tolerant_mode + self.master_access_disabled = master_access_disabled + self.vtgate_connection = None + self.change_master_read_to_replica = False + self._transaction_stack_depth = 0 + self.connection_timeout = DEFAULT_CONNECTION_TIMEOUT + self.query_timeout = DEFAULT_QUERY_TIMEOUT + self.event_logger = vtdb_logger.get_logger() + self._tablet_type = None + + @property + def tablet_type(self): + return self._tablet_type + + @property + def in_transaction(self): + return self._transaction_stack_depth > 0 + + @property + def in_db_operation(self): + return (self._tablet_type is not None) + + def get_vtgate_connection(self): + """Returns the cached vtgate connection or creates a new one. + + Transactions and some of the consistency guarantees rely on vtgate + connections being sticky hence this class caches the connection. + """ + if self.vtgate_connection is not None and not self.vtgate_connection.is_closed(): + return self.vtgate_connection + + #TODO: the connect method needs to be extended to include query n txn timeouts as well + #FIXME: what is the best way of passing other params ? + connect_method = get_vtgate_connect_method() + self.vtgate_connection = connect_method(self.vtgate_addrs, self.connection_timeout) + return self.vtgate_connection + + def degrade_master_read_to_replica(self): + self.change_master_read_to_replica = True + + def start_transaction(self): + conn = self.get_vtgate_connection() + if self._transaction_stack_depth == 0: + conn.begin() + self._transaction_stack_depth += 1 + + def commit(self): + if self._transaction_stack_depth: + self._transaction_stack_depth -= 1 + + if self._transaction_stack_depth != 0: + return + + if self.vtgate_connection is None: + return + self.vtgate_connection.commit() + + def rollback(self): + self._transaction_stack_depth = 0 + try: + if self.vtgate_connection is not None: + self.vtgate_connection.rollback() + except dbexceptions.OperationalError: + self.vtgate_connection.close() + self.vtgate_connection = None + except Exception as e: + raise + + def close(self): + if self._transaction_stack_depth: + self.rollback() + self.vtgate_connection.close() + + def read_from_master_setup(self): + self._tablet_type = shard_constants.TABLET_TYPE_MASTER + if self.master_access_disabled: + raise dbexceptions.Error("Master access is disabled.") + if app_read_only_mode() and self.change_master_read_to_replica: + self._tablet_type = shard_constants.TABLET_TYPE_REPLICA + + def read_from_replica_setup(self): + self._tablet_type = shard_constants.TABLET_TYPE_REPLICA + + # During a write transaction, all reads are promoted to + # read from master. + if self._transaction_stack_depth > 0: + self._tablet_type = shard_constants.TABLET_TYPE_MASTER + elif self.lag_tolerant_mode: + self._tablet_type = shard_constants.TABLET_TYPE_BATCH + + def write_transaction_setup(self): + if self.master_access_disabled: + raise dbexceptions.Error("Cannot write, master access is disabled.") + self._tablet_type = shard_constants.TABLET_TYPE_MASTER + + def close_db_operation(self): + self._tablet_type = None + + def create_cursor(self, writable, table_class, **cursor_kargs): + if not self.in_db_operation: + raise dbexceptions.ProgrammingError( + "Cannot execute queries outside db operations context.") + + cursor = table_class.create_vtgate_cursor(self.get_vtgate_connection(), + self.tablet_type, + writable, + **cursor_kargs) + + return cursor + + +class DBOperationBase(object): + """Base class for database read and write operations. + + Attributes: + dc: database context object. + writable: Indicates whether this is part of write transaction. + """ + def __init__(self, db_context): + self.dc = db_context + self.writable = False + + def get_cursor(self, **cursor_kargs): + """This returns the create_cursor method of DatabaseContext with + the writable attribute from the instance of DBOperationBase's + derived classes.""" + return functools.partial(self.dc.create_cursor, self.writable, **cursor_kargs) + + +class ReadFromMaster(DBOperationBase): + """Context Manager for reading from master.""" + def __enter__(self): + self.dc.read_from_master_setup() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.dc.close_db_operation() + if exc_type is None: + return True + if isinstance(exc_type, dbexceptions.OperationalError): + self.dc.event_logger.vtgatev2_exception(exc_value) + self.dc.vtgate_connection.close() + self.dc.vtgate_connection = None + + +class ReadFromReplica(DBOperationBase): + """Context Manager for reading from lag-sensitive or lag-tolerant replica.""" + def __enter__(self): + self.dc.read_from_replica_setup() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.dc.close_db_operation() + if exc_type is None: + return True + if isinstance(exc_type, dbexceptions.OperationalError): + self.dc.event_logger.vtgatev2_exception(exc_value) + self.dc.vtgate_connection.close() + self.dc.vtgate_connection = None + + +class WriteTransaction(DBOperationBase): + """Context Manager for write transactions.""" + def __enter__(self): + self.writable = True + self.dc.write_transaction_setup() + self.dc.start_transaction() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.dc.close_db_operation() + if exc_type is None: + self.dc.commit() + return True + + if isinstance(exc_type, dbexceptions.OperationalError): + self.dc.vtgate_connection.close() + self.dc.vtgate_connection = None + else: + if self.dc.vtgate_connection is not None: + self.dc.rollback() + if isinstance(exc_type, dbexceptions.IntegrityError): + self.dc.event_logger.integrity_error(exc_value) + else: + self.dc.event_logger.vtgatev2_exception(exc_value) + + +def read_from_master(method): + """Decorator to read from the master db. + + Args: + method: Method to decorate. This creates the appropriate cursor + and calls the underlying vtgate rpc. + Returns: + The decorated method. + """ + @functools.wraps(method) + def _read_from_master(*pargs, **kargs): + dc = open_context() + with ReadFromMaster(dc) as context: + cursor_method = context.get_cursor() + if pargs[1:]: + return method(cursor_method, *pargs[1:], **kargs) + else: + return method(cursor_method, **kargs) + return _read_from_master + + +def register_app_read_only_mode_method(func): + """Register function to temporarily disable master access. + + This is different from the master_acces_disabled mode as this + is supposed to be a transient state. + + Args: + func: method that determines this condition. + """ + global __app_read_only_mode_method + __app_read_only_mode_method = func + + +def app_read_only_mode(): + """This method returns the result of the registered + method for __app_read_only_mode_method. + + Returns: + Output from __app_read_only_mode_method. Default False. + """ + global __app_read_only_mode_method + return __app_read_only_mode_method() + + +def register_create_vtgate_connection_method(connect_method): + """Register connection creation method for vtgate.""" + global __vtgate_connect_method + __vtgate_connect_method = connect_method + +def get_vtgate_connect_method(): + """Returns the vtgate connection creation method.""" + global __vtgate_connect_method + return __vtgate_connect_method + +# The global object is for legacy application. +__database_context = None + +def open_context(*pargs, **kargs): + """Returns the existing global database context or creates a new one.""" + global __database_context + + if __database_context is None: + __database_context = DatabaseContext(*pargs, **kargs) + return __database_context + + +def close(): + """Close the global database context and close any open connections.""" + global __database_context + if __database_context is not None: + __database_context.close() + __database_context = None diff --git a/py/vtdb/db_object.py b/py/vtdb/db_object.py new file mode 100644 index 00000000000..49967bc0f9c --- /dev/null +++ b/py/vtdb/db_object.py @@ -0,0 +1,270 @@ +"""Module containing the base class for database classes and decorator for db method. + +The base class DBObjectBase is the base class for all other database base classes. +It has methods for common database operations like select, insert, update and delete. +This module also contains the definition for ShardRouting which is used for determining +the routing of a query during cursor creation. +The module also has the db_class_method decorator and db_wrapper method which are +used for cursor creation and calling the database method. +""" +import functools +import logging +import struct + +from vtdb import database_context +from vtdb import dbexceptions +from vtdb import keyrange +from vtdb import keyrange_constants +from vtdb import shard_constants +from vtdb import sql_builder +from vtdb import vtgate_cursor + + +class ShardRouting(object): + """VTGate Shard Routing Class. + + Attributes: + keyspace: keyspace where the table resides. + sharding_key: sharding key of the table. + keyrange: keyrange for the query. + entity_column_name: the name of the lookup based entity used for routing. + entity_id_sharding_key_map: this map is used for in clause queries. + shard_name: this is used to route queries for custom sharded keyspaces. + """ + def __init__(self, keyspace): + # keyspace of the table. + self.keyspace = keyspace + # sharding_key, entity_column_name and entity_id_sharding_key + # are used primarily for routing range-sharded keyspace queries. + self.sharding_key = None + self.entity_column_name = None + self.entity_id_sharding_key_map = None + self.keyrange = None + self.shard_name = None + + +def _is_iterable_container(x): + return hasattr(x, '__iter__') + + +def create_cursor_from_params(vtgate_conn, tablet_type, is_dml, table_class): + """This method creates the cursor from the required params. + + This is mainly used for creating lookup cursor during create_shard_routing, + as there is no real cursor available. + + Args: + vtgate_conn: connection to vtgate server. + tablet_type: tablet type for the cursor. + is_dml: indicates writable cursor or not. + table_class: table for which the cursor is being created. + + Returns: + cursor + """ + cursor = table_class.create_vtgate_cursor(vtgate_conn, tablet_type, is_dml) + return cursor + + +def create_cursor_from_old_cursor(old_cursor, table_class): + """This method creates the cursor from an existing cursor. + + This is mainly used for creating lookup cursor during db operations on + other database tables. + + Args: + old_cursor: existing cursor from which important params are evaluated. + table_class: table for which the cursor is being created. + + Returns: + cursor + """ + cursor = table_class.create_vtgate_cursor(old_cursor._conn, + old_cursor.tablet_type, + old_cursor.is_writable()) + return cursor + + +def create_stream_cursor_from_cursor(original_cursor): + """ + This method creates streaming cursor from a regular cursor. + + Args: + original_cursor: Cursor of VTGateCursor type + + Returns: + Returns StreamVTGateCursor that is not writable. + """ + if not isinstance(original_cursor, vtgate_cursor.VTGateCursor): + raise dbexceptions.ProgrammingError( + "Original cursor should be of VTGateCursor type.") + stream_cursor = vtgate_cursor.StreamVTGateCursor( + original_cursor._conn, original_cursor.keyspace, + original_cursor.tablet_type, + keyspace_ids=original_cursor.keyspace_ids, + keyranges=original_cursor.keyranges, + writable=False) + return stream_cursor + + +def db_wrapper(method): + """Decorator that is used to create the appropriate cursor + for the table and call the database method with it. + + Args: + method: Method to decorate. + + Returns: + Decorated method. + """ + @functools.wraps(method) + def _db_wrapper(*pargs, **kargs): + table_class = pargs[0] + if not issubclass(table_class, DBObjectBase): + raise dbexceptions.ProgrammingError( + "table class '%s' is not inherited from DBObjectBase" % table_class) + cursor_method = pargs[1] + cursor = cursor_method(table_class) + if pargs[2:]: + return method(table_class, cursor, *pargs[2:], **kargs) + else: + return method(table_class, cursor, **kargs) + return _db_wrapper + + +def db_class_method(*pargs, **kargs): + """This function calls db_wrapper to create the appropriate cursor.""" + return classmethod(db_wrapper(*pargs, **kargs)) + + +class DBObjectBase(object): + """Base class for db classes. + + This abstracts sharding information and provides helper methods + for common database access operations. + """ + keyspace = None + sharding = None + table_name = None + + + @classmethod + def create_shard_routing(class_, *pargs, **kwargs): + """This method is used to create ShardRouting object which is + used for determining routing attributes for the vtgate cursor. + + Returns: + ShardRouting object. + """ + raise NotImplementedError + + @classmethod + def create_vtgate_cursor(class_, vtgate_conn, tablet_type, is_dml, **cursor_kargs): + """This creates the VTGateCursor object which is used to make + all the rpc calls to VTGate. + + Args: + vtgate_conn: connection to vtgate. + tablet_type: tablet type to connect to. + is_dml: Makes the cursor writable, enforces appropriate constraints. + + Returns: + VTGateCursor for the query. + """ + raise NotImplementedError + + @db_class_method + def select_by_columns(class_, cursor, where_column_value_pairs, + columns_list = None,order_by=None, group_by=None, + limit=None, **kwargs): + if class_.columns_list is None: + raise dbexceptions.ProgrammingError("DB class should define columns_list") + + if columns_list is None: + columns_list = class_.columns_list + query, bind_vars = sql_builder.select_by_columns_query(columns_list, + class_.table_name, + where_column_value_pairs, + order_by=order_by, + group_by=group_by, + limit=limit, + **kwargs) + + rowcount = cursor.execute(query, bind_vars) + rows = cursor.fetchall() + return [sql_builder.DBRow(columns_list, row) for row in rows] + + @db_class_method + def insert(class_, cursor, **bind_vars): + if class_.columns_list is None: + raise dbexceptions.ProgrammingError("DB class should define columns_list") + + query, bind_vars = sql_builder.insert_query(class_.table_name, + class_.columns_list, + **bind_vars) + cursor.execute(query, bind_vars) + return cursor.lastrowid + + @db_class_method + def update_columns(class_, cursor, where_column_value_pairs, + **update_columns): + + query, bind_vars = sql_builder.update_columns_query( + class_.table_name, where_column_value_pairs, **update_columns) + + return cursor.execute(query, bind_vars) + + @db_class_method + def delete_by_columns(class_, cursor, where_column_value_pairs, limit=None, + **columns): + if not where_column_value_pairs: + where_column_value_pairs = columns.items() + where_column_value_pairs.sort() + + if not where_column_value_pairs: + raise dbexceptions.ProgrammingError("deleting the whole table is not allowed") + + query, bind_vars = sql_builder.delete_by_columns_query(class_.table_name, + where_column_value_pairs, + limit=limit) + cursor.execute(query, bind_vars) + if cursor.rowcount == 0: + raise dbexceptions.DatabaseError("DB Row not found") + return cursor.rowcount + + @db_class_method + def select_by_columns_streaming(class_, cursor, where_column_value_pairs, + columns_list = None,order_by=None, group_by=None, + limit=None, fetch_size=100, **kwargs): + if class_.columns_list is None: + raise dbexceptions.ProgrammingError("DB class should define columns_list") + + if columns_list is None: + columns_list = class_.columns_list + query, bind_vars = sql_builder.select_by_columns_query(columns_list, + class_.table_name, + where_column_value_pairs, + order_by=order_by, + group_by=group_by, + limit=limit, + **kwargs) + + return class_._stream_fetch(cursor, query, bind_vars, fetch_size) + + @classmethod + def _stream_fetch(class_, cursor, query, bind_vars, fetch_size=100): + stream_cursor = create_stream_cursor_from_cursor(cursor) + stream_cursor.execute(query, bind_vars) + while True: + rows = stream_cursor.fetchmany(size=fetch_size) + + # NOTE: fetchmany returns an empty list when there are no more items. + # But an empty generator is still "true", so we have to count if we + # actually returned anything. + i = 0 + for r in rows: + i += 1 + yield sql_builder.DBRow(class_.columns_list, r) + if i == 0: + break + stream_cursor.close() diff --git a/py/vtdb/db_object_custom_sharded.py b/py/vtdb/db_object_custom_sharded.py new file mode 100644 index 00000000000..7637b988da9 --- /dev/null +++ b/py/vtdb/db_object_custom_sharded.py @@ -0,0 +1,50 @@ +"""Module containing base class for tables in custom sharded keyspace. + +Vitess sharding scheme is range-sharded. Vitess supports routing for +other sharding schemes by allowing explicit shard_name addressing. +This implementation is not fully complete as yet. +""" +import logging + +from vtdb import db_object +from vtdb import dbexceptions +from vtdb import shard_constants +from vtdb import vtgate_cursor + + +class DBObjectCustomSharded(db_object.DBObjectBase): + """Base class for custom-sharded db classes. + + This class is intended to support a custom sharding scheme, where the user + controls the routing of their queries by passing in the shard_name + explicitly.This provides helper methods for common database access operations. + """ + keyspace = None + sharding = shard_constants.CUSTOM_SHARDED + + table_name = None + columns_list = None + + @classmethod + def create_shard_routing(class_, *pargs, **kwargs): + routing = db_object.ShardRouting(keyspace) + routing.shard_name = kargs.get('shard_name') + if routing.shard_name is None: + dbexceptions.InternalError("For custom sharding, shard_name cannot be None.") + + if (_is_iterable_container(routing.shard_name) + and is_dml): + raise dbexceptions.InternalError( + "Writes are not allowed on multiple shards.") + return routing + + @classmethod + def create_vtgate_cursor(class_, vtgate_conn, tablet_type, is_dml, **cursor_kargs): + # FIXME:extend VTGateCursor's api to accept shard_names + # and allow queries based on that. + routing = class_.create_shard_routing(**cursor_kargs) + cursor = vtgate_cursor.VTGateCursor(vtgate_conn, class_.keyspace, + tablet_type, + keyranges=[routing.shard_name,], + writable=is_dml) + return cursor diff --git a/py/vtdb/db_object_lookup.py b/py/vtdb/db_object_lookup.py new file mode 100644 index 00000000000..2d5e78b974b --- /dev/null +++ b/py/vtdb/db_object_lookup.py @@ -0,0 +1,47 @@ +"""Module containing base class for lookup database tables. + +LookupDBObject defines the base class for lookup tables and defines +relevant methods. LookupDBObject inherits from DBObjectUnsharded and +extends the functionality for getting, creating, updating and deleting +the lookup relationship. +""" +import functools +import logging +import struct + +from vtdb import db_object +from vtdb import db_object_unsharded +from vtdb import dbexceptions +from vtdb import keyrange +from vtdb import keyrange_constants +from vtdb import shard_constants +from vtdb import vtgate_cursor + + +# TODO: is a generic Lookup interface for non-db based look classes needed ? +class LookupDBObject(db_object_unsharded.DBObjectUnsharded): + """This is an example implementation of lookup class where it is stored + in unsharded db. + """ + @classmethod + def get(class_, cursor, entity_id_column, entity_id): + where_column_value_pairs = [(entity_id_column, entity_id),] + rows = class_.select_by_columns(cursor, where_column_value_pairs) + return [row.__dict__ for row in rows] + + @classmethod + def create(class_, cursor, **bind_vars): + return class_.insert(cursor, **bind_vars) + + @classmethod + def update(class_, cursor, sharding_key_column_name, sharding_key, + entity_id_column, new_entity_id): + where_column_value_pairs = [(sharding_key_column_name, sharding_key),] + update_columns = {entity_id_column:new_entity_id} + return class_.update_columns(cursor, where_column_value_pairs, + **update_columns) + + @classmethod + def delete(class_, cursor, sharding_key_column_name, sharding_key): + where_column_value_pairs = [(sharding_key_column_name, sharding_key),] + return class_.delete_by_columns(cursor, where_column_value_pairs) diff --git a/py/vtdb/db_object_range_sharded.py b/py/vtdb/db_object_range_sharded.py new file mode 100644 index 00000000000..c2c7277176c --- /dev/null +++ b/py/vtdb/db_object_range_sharded.py @@ -0,0 +1,513 @@ +"""Module containing base classes for range-sharded database objects. + +There are two base classes for tables that live in range-sharded keyspace - +1. DBObjectRangeSharded - This should be used for tables that only reference lookup entities +but don't create or manage them. Please see examples in test/clientlib_tests/db_class_sharded.py. +2. DBObjectEntityRangeSharded - This inherits from DBObjectRangeSharded and is used for tables +and also create new lookup relationships. +This module also contains helper methods for cursor creation for accessing lookup tables +and methods for dml and select for the above mentioned base classes. +""" +import functools +import logging +import struct + +from vtdb import db_object +from vtdb import dbexceptions +from vtdb import keyrange +from vtdb import keyrange_constants +from vtdb import shard_constants +from vtdb import sql_builder +from vtdb import vtgate_cursor + + +# This creates a 64 binary packed string for keyspace_id. +# This is used for cursor creation so that keyspace_id can +# be passed as rpc param for vtgate. +pack_keyspace_id = struct.Struct('!Q').pack + + +# This unpacks the keyspace_id so that it can be used +# in bind variables. +def unpack_keyspace_id(kid): + return struct.Struct('!Q').unpack(kid)[0] + + + + +class DBObjectRangeSharded(db_object.DBObjectBase): + """Base class for range-sharded db classes. + + This provides default implementation of routing helper methods, cursor + creation and common database access operations. + """ + # keyspace of this table. This is needed for routing. + keyspace = None + + # sharding scheme for this base class. + sharding = shard_constants.RANGE_SHARDED + + # table name for the corresponding database table. This is used in query + # construction. + table_name = None + + # List of columns on the database table. This is used in query construction. + columns_list = None + + #FIXME: is this needed ? + id_column_name = None + + # sharding_key_column_name defines column name for sharding key for this + # table. + sharding_key_column_name = None + + # entity_id_lookup_map defines entity lookup relationship. It is a map of + # column names for this table to lookup class that contains the mapping of + # this entity to sharding key for this keyspace. + entity_id_lookup_map = None + + # column_lookup_name_map defines the map of column names from this table + # to the corresponding lookup table. This should be used when column_names + # in the main table and lookup table is different. This is used in + # conjunction with sharding_key_column_name and entity_id_lookup_map. + column_lookup_name_map = None + + @classmethod + def create_shard_routing(class_, *pargs, **kargs): + """This creates the ShardRouting object based on the kargs. + This prunes the routing kargs so as not to interfere with the + actual database method. + + Args: + *pargs: Positional arguments + **kargs: Routing key-value params. These are used to determine routing. + There are two mutually exclusive mechanisms to indicate routing. + 1. entity_id_map {"entity_id_column": entity_id_value} where entity_id_column + could be the sharding key or a lookup based entity column of this table. This + helps determine the keyspace_ids for the cursor. + 2. keyrange - This helps determine the keyrange for the cursor. + + Returns: + ShardRouting object and modified kargs + """ + lookup_cursor_method = pargs[0] + routing = db_object.ShardRouting(class_.keyspace) + entity_id_map = None + + entity_id_map = kargs.get("entity_id_map", None) + if entity_id_map is None: + kr = None + key_range = kargs.get("keyrange", None) + if isinstance(key_range, keyrange.KeyRange): + kr = key_range + else: + kr = keyrange.KeyRange(key_range) + if kr is not None: + routing.keyrange = kr + # Both entity_id_map and keyrange have been evaluated. Return. + return routing + + # entity_id_map is not None + if len(entity_id_map) != 1: + dbexceptions.ProgrammingError("Invalid entity_id_map '%s'" % entity_id_map) + + entity_id_col = entity_id_map.keys()[0] + entity_id = entity_id_map[entity_id_col] + + #TODO: the current api means that if a table doesn't have the sharding key column name + # then it cannot pass in sharding key for routing purposes. Will this cause + # extra load on lookup db/cache ? This is cleaner from a design perspective. + if entity_id_col == class_.sharding_key_column_name: + # Routing using sharding key. + routing.sharding_key = entity_id + if not class_.is_sharding_key_valid(routing.sharding_key): + raise dbexceptions.InternalError("Invalid sharding_key %s" % routing.sharding_key) + else: + # Routing using lookup based entity. + routing.entity_column_name = entity_id_col + routing.entity_id_sharding_key_map = class_.lookup_sharding_key_from_entity_id( + lookup_cursor_method, entity_id_col, entity_id) + + return routing + + @classmethod + def create_vtgate_cursor(class_, vtgate_conn, tablet_type, is_dml, **cursor_kargs): + cursor_method = functools.partial(db_object.create_cursor_from_params, + vtgate_conn, tablet_type, False) + routing = class_.create_shard_routing(cursor_method, **cursor_kargs) + if is_dml: + if routing.sharding_key is None or db_object._is_iterable_container(routing.sharding_key): + dbexceptions.InternalError( + "Writes require unique sharding_key") + + keyspace_ids = None + keyranges = None + if routing.sharding_key is not None: + keyspace_ids = [] + if db_object._is_iterable_container(routing.sharding_key): + for sk in routing.sharding_key: + kid = class_.sharding_key_to_keyspace_id(sk) + keyspace_ids.append(pack_keyspace_id(kid)) + else: + kid = class_.sharding_key_to_keyspace_id(routing.sharding_key) + keyspace_ids = [pack_keyspace_id(kid),] + elif routing.entity_id_sharding_key_map is not None: + keyspace_ids = [] + for sharding_key in routing.entity_id_sharding_key_map.values(): + keyspace_ids.append(pack_keyspace_id(class_.sharding_key_to_keyspace_id(sharding_key))) + elif routing.keyrange: + keyranges = [routing.keyrange,] + + cursor = vtgate_cursor.VTGateCursor(vtgate_conn, + class_.keyspace, + tablet_type, + keyspace_ids=keyspace_ids, + keyranges=keyranges, + writable=is_dml) + cursor.routing = routing + return cursor + + @classmethod + def get_lookup_column_name(class_, column_name): + """Return the lookup column name for a column name from this table. + If the entry doesn't exist it is assumed that the column_name is same. + """ + return class_.column_lookup_name_map.get(column_name, column_name) + + @classmethod + def lookup_sharding_key_from_entity_id(class_, cursor_method, entity_id_column, entity_id): + """This method is used to map any entity id to sharding key. + + Args: + entity_id_column: Non-sharding key indexes that can be used for query routing. + entity_id: entity id value. + + Returns: + sharding key to be used for routing. + """ + lookup_column = class_.get_lookup_column_name(entity_id_column) + lookup_class = class_.entity_id_lookup_map[lookup_column] + rows = lookup_class.get(cursor_method, entity_id_column, entity_id) + + sk_lookup_column = class_.get_lookup_column_name(class_.sharding_key_column_name) + entity_id_sharding_key_map = {} + for row in rows: + en_id = row[lookup_column] + sk = row[sk_lookup_column] + entity_id_sharding_key_map[en_id] = sk + + return entity_id_sharding_key_map + + @db_object.db_class_method + def select_by_ids(class_, cursor, where_column_value_pairs, + columns_list = None,order_by=None, group_by=None, + limit=None, **kwargs): + """This method is used to perform in-clause queries. + + Such queries can cause vtgate to scatter over multiple shards. + This uses execute_entity_ids method of vtgate cursor and the entity + column and the associated entity_keyspace_id_map is computed based + on the routing used - sharding_key or entity_id_map. + """ + + if class_.columns_list is None: + raise dbexceptions.ProgrammingError("DB class should define columns_list") + + if columns_list is None: + columns_list = class_.columns_list + query, bind_vars = sql_builder.select_by_columns_query(columns_list, + class_.table_name, + where_column_value_pairs, + order_by=order_by, + group_by=group_by, + limit=limit, + **kwargs) + + entity_col_name = None + entity_id_keyspace_id_map = {} + if cursor.routing.sharding_key is not None: + # If the in-clause is based on sharding key + entity_col_name = class_.sharding_key_column_name + if db_object._is_iterable_container(cursor.routing.sharding_key): + for sk in list(cursor.routing.sharding_key): + entity_id_keyspace_id_map[sk] = pack_keyspace_id(class_.sharding_key_to_keyspace_id(sk)) + else: + sk = cursor.routing.sharding_key + entity_id_keyspace_id_map[sk] = pack_keyspace_id(class_.sharding_key_to_keyspace_id(sk)) + elif cursor.routing.entity_id_sharding_key_map is not None: + # If the in-clause is based on entity column + entity_col_name = cursor.routing.entity_column_name + for en_id, sk in cursor.routing.entity_id_sharding_key_map.iteritems(): + entity_id_keyspace_id_map[en_id] = pack_keyspace_id(class_.sharding_key_to_keyspace_id(sk)) + else: + dbexceptions.ProgrammingError("Invalid routing method used.") + + # cursor.routing.entity_column_name is set while creating shard routing. + rowcount = cursor.execute_entity_ids(query, bind_vars, + entity_id_keyspace_id_map, + entity_col_name) + rows = cursor.fetchall() + return [sql_builder.DBRow(columns_list, row) for row in rows] + + @classmethod + def is_sharding_key_valid(class_, sharding_key): + """Method to check the validity of sharding key for the table. + + Args: + sharding_key: sharding_key to be validated. + + Returns: + bool + """ + raise NotImplementedError + + @classmethod + def sharding_key_to_keyspace_id(class_, sharding_key): + """Method to create keyspace_id from sharding_key. + + Args: + sharding_key: sharding_key + + Returns: + keyspace_id + """ + raise NotImplementedError + + @db_object.db_class_method + def insert(class_, cursor, **bind_vars): + if class_.columns_list is None: + raise dbexceptions.ProgrammingError("DB class should define columns_list") + + keyspace_id = bind_vars.get('keyspace_id', None) + if keyspace_id is None: + kid = cursor.keyspace_ids[0] + keyspace_id = unpack_keyspace_id(kid) + bind_vars['keyspace_id'] = keyspace_id + + query, bind_vars = sql_builder.insert_query(class_.table_name, + class_.columns_list, + **bind_vars) + cursor.execute(query, bind_vars) + return cursor.lastrowid + + @classmethod + def _add_keyspace_id(class_, keyspace_id, where_column_value_pairs): + where_col_dict = dict(where_column_value_pairs) + if 'keyspace_id' not in where_col_dict: + where_column_value_pairs.append(('keyspace_id', keyspace_id)) + + return where_column_value_pairs + + @db_object.db_class_method + def update_columns(class_, cursor, where_column_value_pairs, + **update_columns): + + where_column_value_pairs = class_._add_keyspace_id( + unpack_keyspace_id(cursor.keyspace_ids[0]), + where_column_value_pairs) + + query, bind_vars = sql_builder.update_columns_query( + class_.table_name, where_column_value_pairs, **update_columns) + + rowcount = cursor.execute(query, bind_vars) + + # If the entity_id column is being updated, update lookup map. + if class_.entity_id_lookup_map is not None: + for entity_col in class_.entity_id_lookup_map.keys(): + if entity_col in update_columns: + class_.update_sharding_key_entity_id_lookup(cursor, sharding_key, + entity_col, + update_columns[entity_col]) + + return rowcount + + @db_object.db_class_method + def delete_by_columns(class_, cursor, where_column_value_pairs, limit=None): + + if not where_column_value_pairs: + raise dbexceptions.ProgrammingError("deleting the whole table is not allowed") + + where_column_value_pairs = class_._add_keyspace_id( + unpack_keyspace_id(cursor.keyspace_ids[0]), where_column_value_pairs) + + query, bind_vars = sql_builder.delete_by_columns_query(class_.table_name, + where_column_value_pairs, + limit=limit) + cursor.execute(query, bind_vars) + if cursor.rowcount == 0: + raise dbexceptions.DatabaseError("DB Row not found") + + return cursor.rowcount + + +class DBObjectEntityRangeSharded(DBObjectRangeSharded): + """Base class for sharded tables that also needs to create and manage lookup + entities. + + This provides default implementation of routing helper methods, cursor + creation and common database access operations. + """ + + @classmethod + def get_insert_id_from_lookup(class_, cursor_method, entity_id_col, **bind_vars): + """This method is used to map any entity id to sharding key. + + Args: + entity_id_column: Non-sharding key indexes that can be used for query routing. + entity_id: entity id value. + + Returns: + sharding key to be used for routing. + """ + lookup_class = class_.entity_id_lookup_map[entity_id_col] + new_bind_vars = {} + for col, value in bind_vars.iteritems(): + lookup_col = class_.get_lookup_column_name(col) + new_bind_vars[lookup_col] = value + return lookup_class.create(cursor_method, **new_bind_vars) + + @classmethod + def delete_sharding_key_entity_id_lookup(class_, cursor_method, + sharding_key): + sharding_key_lookup_column = class_.get_lookup_column_name(class_.sharding_key_column_name) + for lookup_class in class_.entity_id_lookup_map.values(): + lookup_class.delete(cursor_method, + sharding_key_lookup_column, + sharding_key) + + + @classmethod + def update_sharding_key_entity_id_lookup(class_, cursor_method, + sharding_key, entity_id_column, + new_entity_id): + sharding_key_lookup_column = class_.get_lookup_column_name(class_.sharding_key_column_name) + entity_id_lookup_column = class_.get_lookup_column_name(entity_id_column) + lookup_class = class_.entity_id_lookup_map[entity_id_column] + return lookup_class.update(cursor_method, + sharding_key_lookup_column, + sharding_key, + entity_id_lookup_column, + new_entity_id) + + + @db_object.db_class_method + def insert_primary(class_, cursor, **bind_vars): + if class_.columns_list is None: + raise dbexceptions.ProgrammingError("DB class should define columns_list") + + query, bind_vars = sql_builder.insert_query(class_.table_name, + class_.columns_list, + **bind_vars) + cursor.execute(query, bind_vars) + return cursor.lastrowid + + + @classmethod + def insert(class_, cursor_method, **bind_vars): + """ This method creates the lookup relationship as well as the insert + in the primary table. The creation of the lookup entry also creates the + primary key for the row in the primary table. + + The lookup relationship is determined by class_.column_lookup_name_map and the bind + variables passed in. There are two types of entities - + 1. Table for which the entity that is also the primary sharding key for this keyspace. + 2. Entity table that creates a new entity and needs to create a lookup between + that entity and sharding key. + """ + if class_.sharding_key_column_name is None: + raise dbexceptions.ProgrammingError( + "sharding_key_column_name empty for DBObjectEntityRangeSharded") + + # Used for insert into class_.table_name + new_inserted_key = None + # Used for routing the insert_primary + entity_id_map = {} + + + # Create the lookup entry first + if class_.sharding_key_column_name in bind_vars: + # Secondary entity creation + sharding_key = bind_vars[class_.sharding_key_column_name] + entity_col = class_.entity_id_lookup_map.keys()[0] + lookup_bind_vars = {class_.sharding_key_column_name, sharding_key} + entity_id = class_.get_insert_id_from_lookup(cursor_method, entity_col, + **lookup_bind_vars) + bind_vars[entity_col] = entity_id + new_inserted_key = entity_id + entity_id_map[entity_col] = entity_id + else: + # Primary sharding key creation + # FIXME: what if class_.entity_id_lookup_map was empty ? + # there would need to be some table on which there was an auto-inc + # to generate the primary sharding key. + entity_col = class_.entity_id_lookup_map.keys()[0] + entity_id = bind_vars[entity_col] + lookup_bind_vars = {entity_col: entity_id} + sharding_key = class_.get_insert_id_from_lookup(cursor_method, entity_col, + **lookup_bind_vars) + bind_vars[class_.sharding_key_column_name] = sharding_key + new_inserted_key = sharding_key + entity_id_map[class_.sharding_key_column_name] = sharding_key + + # FIXME: is the not value check correct ? + if 'keyspace_id' not in bind_vars or not bind_vars['keyspace_id']: + keyspace_id = class_.sharding_key_to_keyspace_id(sharding_key) + bind_vars['keyspace_id'] = keyspace_id + + # entity_id_map is used for routing and hence passed to cursor_method + #bind_vars['entity_id_map'] = entity_id_map + new_cursor = functools.partial(cursor_method, entity_id_map=entity_id_map) + class_.insert_primary(new_cursor, **bind_vars) + return new_inserted_key + + @db_object.db_class_method + def update_columns(class_, cursor, where_column_value_pairs, + **update_columns): + + sharding_key = cursor.routing.sharding_key + if sharding_key is None: + raise dbexceptions.ProgrammingError("sharding_key cannot be empty") + + # update the primary table first. + query, bind_vars = sql_builder.update_columns_query( + class_.table_name, where_column_value_pairs, **update_columns) + + rowcount = cursor.execute(query, bind_vars) + + # If the entity_id column is being updated, update lookup map. + lookup_cursor_method = functools.partial( + db_object.create_cursor_from_old_cursor, cursor) + for entity_col in class_.entity_id_lookup_map.keys(): + if entity_col in update_columns: + class_.update_sharding_key_entity_id_lookup(lookup_cursor_method, + sharding_key, + entity_col, + update_columns[entity_col]) + + return rowcount + + @db_object.db_class_method + def delete_by_columns(class_, cursor, where_column_value_pairs, + limit=None): + sharding_key = cursor.routing.sharding_key + if sharding_key is None: + raise dbexceptions.ProgrammingError("sharding_key cannot be empty") + + if not where_column_value_pairs: + raise dbexceptions.ProgrammingError("deleting the whole table is not allowed") + + query, bind_vars = sql_builder.delete_by_columns_query(class_.table_name, + where_column_value_pairs, + limit=limit) + cursor.execute(query, bind_vars) + if cursor.rowcount == 0: + raise dbexceptions.DatabaseError("DB Row not found") + + rowcount = cursor.rowcount + + #delete the lookup map. + lookup_cursor_method = functools.partial( + db_object.create_cursor_from_old_cursor, cursor) + class_.delete_sharding_key_entity_id_lookup(lookup_cursor_method, sharding_key) + + return rowcount diff --git a/py/vtdb/db_object_unsharded.py b/py/vtdb/db_object_unsharded.py new file mode 100644 index 00000000000..43b5b4721c0 --- /dev/null +++ b/py/vtdb/db_object_unsharded.py @@ -0,0 +1,52 @@ +"""Module containing base class for tables in unsharded keyspace. + +DBObjectUnsharded inherits from DBObjectBase, the implementation +for the common database operations is defined in DBObjectBase. +DBObjectUnsharded defines the cursor creation methods for the same. +""" +import functools +import logging +import struct + +from vtdb import db_object +from vtdb import dbexceptions +from vtdb import keyrange +from vtdb import keyrange_constants +from vtdb import shard_constants +from vtdb import vtgate_cursor + + +class DBObjectUnsharded(db_object.DBObjectBase): + """Base class for unsharded db classes. + + This provides default implementation of routing helper methods and cursor + creation. The common database access operations are defined in the base class. + """ + keyspace = None + sharding = shard_constants.UNSHARDED + + table_name = None + columns_list = None + + + @classmethod + def create_shard_routing(class_, *pargs, **kwargs): + routing = db_object.ShardRouting(class_.keyspace) + routing.keyrange = keyrange.KeyRange(keyrange_constants.NON_PARTIAL_KEYRANGE) + return routing + + @classmethod + def create_vtgate_cursor(class_, vtgate_conn, tablet_type, is_dml, **cursor_kargs): + routing = class_.create_shard_routing(**cursor_kargs) + if routing.keyrange is not None: + keyranges = [routing.keyrange,] + else: + dbexceptions.ProgrammingError("Empty Keyrange") + + cursor = vtgate_cursor.VTGateCursor(vtgate_conn, + class_.keyspace, + tablet_type, + keyranges=keyranges, + writable=is_dml) + + return cursor diff --git a/py/vtdb/field_types.py b/py/vtdb/field_types.py index 403fcbf0771..a074dbc09d2 100755 --- a/py/vtdb/field_types.py +++ b/py/vtdb/field_types.py @@ -79,7 +79,7 @@ class List(list): NoneType = type(None) # FIXME(msolomon) we could make a SqlLiteral ABC and just type check. -# That doens't seem dramatically better than __sql_literal__ but it might +# That doesn't seem dramatically better than __sql_literal__ but it might # be move self-documenting. def convert_bind_vars(bind_variables): diff --git a/py/vtdb/keyrange.py b/py/vtdb/keyrange.py index 21b27db6b91..39a7561d4e6 100644 --- a/py/vtdb/keyrange.py +++ b/py/vtdb/keyrange.py @@ -11,7 +11,7 @@ class KeyRange(codec.BSONCoding): - """Defintion of KeyRange object. + """Definition of KeyRange object. Vitess uses range based sharding. KeyRange denotes the range for the sharding key. This class also provides bson encoding diff --git a/py/vtdb/shard_constants.py b/py/vtdb/shard_constants.py new file mode 100644 index 00000000000..5c48ca1ba03 --- /dev/null +++ b/py/vtdb/shard_constants.py @@ -0,0 +1,13 @@ +"""Constants that describe different sharding schemes. + +Different sharding schemes govern different routing strategies +that are computed while create the correct cursor. +""" + +UNSHARDED = "UNSHARDED" +RANGE_SHARDED = "RANGE" +CUSTOM_SHARDED = "CUSTOM" + +TABLET_TYPE_MASTER = 'master' +TABLET_TYPE_REPLICA = 'replica' +TABLET_TYPE_BATCH = 'rdonly' diff --git a/py/vtdb/sql_builder.py b/py/vtdb/sql_builder.py new file mode 100644 index 00000000000..c126342a013 --- /dev/null +++ b/py/vtdb/sql_builder.py @@ -0,0 +1,676 @@ +"""Helper classes for building queries. + +Helper classes and fucntions for building queries. +""" + +import itertools +import pprint + +#TODO: add unit-tests for the methods and classes. +#TODO: integration with SQL Alchemy ? + +class DBRow(object): + + def __init__(self, column_names, row_tuple, **overrides): + self.__dict__ = dict(zip(column_names, row_tuple), **overrides) + + def __repr__(self): + return pprint.pformat(self.__dict__, 4) + + +def select_clause(select_columns, table_name, alias=None, cols=None, order_by_cols=None): + """Build the select clause for a query.""" + + if alias: + return 'SELECT %s FROM %s %s' % ( + colstr(select_columns, alias, cols, order_by_cols=order_by_cols), + table_name, alias) + return 'SELECT %s FROM %s' % ( + colstr(select_columns, alias, cols, order_by_cols=order_by_cols), + table_name) + + +def colstr(select_columns, alias=None, cols=None, bind=None, order_by_cols=None): + if not cols: + cols = select_columns + + # in the case of a scatter/gather, prepend these columns to facilitate an in-code + # sort - after that, we can just strip these off and process normally + if order_by_cols: + # avoid altering a class variable + cols = cols[:] + for order_col in reversed(order_by_cols): + if type(order_col) in (tuple, list): + cols.insert(0, order_col[0]) + else: + cols.insert(0, order_col) + + if not bind: + bind = cols + + def prefix(col): + if isinstance(col, SQLAggregate): + return col.sql() + + if alias and '.' not in col: + col = '%s.%s' % (alias, col) + + return col + return ', '.join([prefix(c) for c in cols if c in bind]) + + +def build_values_clause(columns, bind_values): + """Builds values clause for an insert query.""" + + clause_parts = [] + bind_list = [] + for column in columns: + if (column in ('time_created', 'time_updated') and + column not in bind_values): + bind_list.append(column) + clause_parts.append('%%(%s)s' % column) + bind_values[column] = int(time.time()) + elif column in bind_values: + bind_list.append(column) + if type(bind_values[column]) == MySQLFunction: + clause_parts.append(bind_values[column]) + bind_values.update(column.bind_vals) + else: + clause_parts.append('%%(%s)s' % column) + return ', '.join(clause_parts), bind_list + + +def build_in(column, items, alt_name=None, counter=None): + """Build SQL IN statement and bind hash for use with pyformat.""" + + if not items: + raise ValueError('Called with empty "items"') + + base = alt_name if alt_name else column + bind_list = make_bind_list(base, items, counter=counter) + + return ('%s IN (%s)' % (column, + str.join(',', ['%(' + pair[0] + ')s' + for pair in bind_list])), + dict(bind_list)) + + +def build_order_clause(order_by): + """order_by could be a list, tuple or string.""" + + if not order_by: + return '' + + if type(order_by) not in (tuple, list): + order_by = (order_by,) + + subclause_list = [] + for subclause in order_by: + if type(subclause) in (tuple, list): + subclause = ' '.join(subclause) + subclause_list.append(subclause) + + return 'ORDER BY %s' % ', '.join(subclause_list) + + +def build_group_clause(group_by): + """Build group_by clause for a query.""" + + if not group_by: + return '' + + if type(group_by) not in (tuple, list): + group_by = (group_by,) + + return 'GROUP BY %s' % ', '.join(group_by) + + +def build_limit_clause(limit): + """Build limit clause for a query.""" + + if not limit: + return '', {} + + if not isinstance(limit, tuple): + limit = (limit,) + + bind_vars = {'limit_row_count': limit[0]} + if len(limit) == 1: + return 'LIMIT %(limit_row_count)s', bind_vars + + bind_vars = {'limit_offset': limit[0], + 'limit_row_count': limit[1]} + return 'LIMIT %(limit_offset)s,%(limit_row_count)s', bind_vars + + +def build_where_clause(column_value_pairs): + """Build the where clause for a query.""" + + condition_list = [] + bind_vars = {} + + counter = itertools.count(1) + + def update_bindvars(newvars): + for k, v in newvars.iteritems(): + if k in bind_vars: + raise ValueError('Duplicate bind vars: cannot add %r to %r', + newvars, bind_vars) + bind_vars[k] = v + + for column, value in column_value_pairs: + if isinstance(value, (Flag, SQLOperator, NullSafeNotValue)): + clause, clause_bind_vars = value.build_sql(column, counter=counter) + update_bindvars(clause_bind_vars) + condition_list.append(clause) + elif isinstance(value, (tuple, list, set)): + if value: + in_clause, in_bind_variables = build_in(column, value, + counter=counter) + update_bindvars(in_bind_variables) + condition_list.append(in_clause) + else: + condition_list.append('1 = 0') + else: + bind_name = choose_bind_name(column, counter=counter) + update_bindvars({bind_name: value}) + condition_list.append('%s = %%(%s)s' % (column, bind_name)) + + if not bind_vars: + bind_vars = dict(column_value_pairs) + + where_clause = ' AND '.join(condition_list) + return where_clause, bind_vars + + +def select_by_columns_query(select_column_list, table_name, column_value_pairs=None, + order_by=None, group_by=None, limit=None, + for_update=False,client_aggregate=False, + vt_routing_info=None, **columns): + + # generate WHERE clause and bind variables + if not column_value_pairs: + column_value_pairs = columns.items() + column_value_pairs.sort() + + if client_aggregate: + clause_list = [select_clause(select_column_list, table_name, + order_by_cols=order_by)] + else: + clause_list = [select_clause(select_column_list, table_name)] + + if column_value_pairs: + where_clause, bind_vars = build_where_clause(column_value_pairs) + # add vt routing info + if vt_routing_info: + where_clause, bind_vars = vt_routing_info.update_where_clause( + where_clause, bind_vars) + clause_list += ['WHERE', where_clause] + else: + bind_vars = {} + + if group_by: + clause_list.append(build_group_clause(group_by)) + if order_by: + clause_list.append(build_order_clause(order_by)) + if limit: + clause, limit_bind_vars = build_limit_clause(limit) + clause_list.append(clause) + bind_vars.update(limit_bind_vars) + if for_update: + clause_list.append('FOR UPDATE') + + query = ' '.join(clause_list) + return query, bind_vars + +def update_columns_query(table_name, where_column_value_pairs=None, + update_column_value_pairs=None, limit=None, + order_by=None, **update_columns): + if not update_column_value_pairs: + update_column_value_pairs = update_columns.items() + update_column_value_pairs.sort() + + clause_list = [] + bind_vals = {} + for i, (column, value) in enumerate(update_column_value_pairs): + if isinstance(value, (Flag, Increment, MySQLFunction)): + clause, clause_bind_vals = value.build_update_sql(column) + clause_list.append(clause) + bind_vals.update(clause_bind_vals) + else: + clause_list.append('%s = %%(update_set_%s)s' % (column, i)) + bind_vals['update_set_%s' % i] = value + + if not clause_list: + # this would be invalid syntax anyway, let's raise a nicer exception + raise ValueError( + 'Expected nonempty update_column_value_pairs. Got: %r' + % update_column_value_pairs) + + set_clause = ', '.join(clause_list) + + if not where_column_value_pairs: + # same as above. We could allow for no where clause, + # but this is a notoriously error-prone construct, so, no. + raise ValueError( + 'Expected nonempty where_column_value_pairs. Got: %r' + % where_column_value_pairs) + + where_clause, where_bind_vals = build_where_clause(where_column_value_pairs) + bind_vals.update(where_bind_vals) + + query = ('UPDATE %(table)s SET %(set_clause)s WHERE %(where_clause)s' + % {'table': table_name, 'set_clause': set_clause, + 'where_clause': where_clause}) + + additional_clause = [] + if order_by: + additional_clause.append(build_order_clause(order_by)) + if limit: + limit_clause, limit_bind_vars = build_limit_clause(limit) + additional_clause.append(limit_clause) + bind_vals.update(limit_bind_vars) + + query += ' ' + ' '.join(additional_clause) + return query, bind_vals + + +def delete_by_columns_query(table_name, where_column_value_pairs=None, + limit=None): + where_clause, bind_vars = build_where_clause(where_column_value_pairs) + limit_clause, limit_bind_vars = build_limit_clause(limit) + bind_vars.update(limit_bind_vars) + + query = ( + 'DELETE FROM %(table_name)s WHERE %(where_clause)s %(limit_clause)s' % + {'table_name': table_name, 'where_clause': where_clause, + 'limit_clause': limit_clause}) + return query, bind_vars + + +def insert_query(table_name, columns_list, **bind_variables): + values_clause, bind_list = build_values_clause(columns_list, + bind_variables) + + + query = 'INSERT INTO %s (%s) VALUES (%s)' % (table_name, + colstr(columns_list, + bind=bind_list), + values_clause) + return query, bind_variables + + +def choose_bind_name(base, counter=None): + if counter: + base += '_%d' % counter.next() + return base + +def make_bind_list(column, values, counter=None): + result = [] + bind_names = [] + if counter is None: + counter = itertools.count(1) + for value in values: + bind_name = choose_bind_name(column, counter=counter) + bind_names.append(bind_name) + result.append((bind_name, value)) + return result + + + +class MySQLFunction(object): + + def __init__(self, func, bind_vals=()): + self.bind_vals = bind_vals + self.func = func + + def __str__(self): + return self.func + + def build_update_sql(self, column): + clause = '%s = %s' % (column, self.func) + return clause, self.bind_vals + + +class SQLAggregate(object): + + def __init__(self, function_name, column_name): + self.function_name = function_name + self.column_name = column_name + + def sql(self): + clause = '%(function_name)s(%(column_name)s)' % vars(self) + return clause + + +def Sum(column_name): + return SQLAggregate('SUM', column_name) + + +def Max(column_name): + return SQLAggregate('MAX', column_name) + + +def Min(column_name): + return SQLAggregate('MIN', column_name) + + +# A null-safe inequality operator. For any [column] and [value] we do +# "NOT [column] <=> [value]". +# +# This is a bit of a hack because our framework assumes all operators are +# binary in nature (whereas we need a combination of unary and binary +# operators). +# +# This is only enabled for use in the where clause. For use in select or +# update you'll need to do some additional work. +class NullSafeNotValue(object): + + def __init__(self, value): + self.value = value + + def build_sql(self, column_name, counter=None): + bind_name = choose_bind_name(column_name, counter=counter) + clause = 'NOT %(column_name)s <=> %%(%(bind_name)s)s' % vars() + bind_vars = {bind_name: self.value} + return clause, bind_vars + + +class SQLOperator(object): + """Base class for a column expression in a SQL WHERE clause.""" + + def __init__(self, value, op): + """Constructor. + + Args: + value: The value against which to compare the column, or an iterable of + values if appropriate for the operator. + op: The operator to use for comparison. + """ + + self.value = value + self.op = op + + def build_sql(self, column_name, counter=None): + """Render this expression as a SQL string. + + Args: + column_name: Name of the column being tested in this expression. + counter: Instance of itertools.count supplying numeric suffixes for + disambiguating bind_names, or None. (See choose_bind_name + for a discussion.) + + Returns: + clause: The SQL expression, including a placeholder for the value. + bind_vars: Dict mapping placeholder names to actual values. + """ + + op = self.op + + bind_name = choose_bind_name(column_name, counter=counter) + + clause = '%(column_name)s %(op)s %%(%(bind_name)s)s' % vars() + bind_vars = {bind_name: self.value} + + return clause, bind_vars + + +class NotValue(SQLOperator): + + def __init__(self, value): + super(NotValue, self).__init__(value, '!=') + + def build_sql(self, column_name, counter=None): + if self.value is None: + return '%s IS NOT NULL' % column_name, {} + return super(NotValue, self).build_sql(column_name, counter=counter) + + +class InValuesOperatorBase(SQLOperator): + + def __init__(self, op, *values): + super(InValuesOperatorBase, self).__init__(values, op) + + def build_sql(self, column_name, counter=None): + op = self.op + bind_list = make_bind_list(column_name, self.value, counter=counter) + in_clause = ', '.join(('%(' + key + ')s') for key, val in bind_list) + clause = '%(column_name)s %(op)s (%(in_clause)s)' % vars() + return clause, dict(bind_list) + + +# You rarely need to use InValues directly in your database classes. +# List and tuples are handled automatically by most database helper methods. +class InValues(InValuesOperatorBase): + + def __init__(self, *values): + super(InValues, self).__init__('IN', *values) + + +class NotInValues(InValuesOperatorBase): + + def __init__(self, *values): + super(NotInValues, self).__init__('NOT IN', *values) + + +class InValuesOrNull(InValues): + + def build_sql(self, column_name, counter=None): + clause, bind_vars = super(InValuesOrNull, self).build_sql(column_name, + counter=counter) + clause = '(%s OR %s IS NULL)' % (clause, column_name) + return clause, bind_vars + + +class BetweenValues(SQLOperator): + + def __init__(self, value0, value1): + if value0 < value1: + super(BetweenValues, self).__init__((value0, value1), 'BETWEEN') + else: + super(BetweenValues, self).__init__((value1, value0), 'BETWEEN') + + def build_sql(self, column_name, counter=None): + op = self.op + bind_list = make_bind_list(column_name, self.value, counter=counter) + between_clause = ' AND '.join(('%(' + key + ')s') for key, val in bind_list) + clause = '%(column_name)s %(op)s %(between_clause)s' % vars() + return clause, dict(bind_list) + + +class OrValues(SQLOperator): + + def __init__(self, *values): + if not values or len(values) == 1: + raise errors.IllegalArgumentException + + super(OrValues, self).__init__(values, 'OR') + + def build_sql(self, column_name, counter=None): + condition_list = [] + bind_vars = {} + if counter is None: + counter = itertools.count(1) + + for v in self.value: + if isinstance(v, (SQLOperator, Flag, NullSafeNotValue)): + clause, clause_bind_vars = v.build_sql(column_name, counter=counter) + bind_vars.update(clause_bind_vars) + condition_list.append(clause) + else: + bind_name = choose_bind_name(column_name, counter=counter) + bind_vars[bind_name] = v + condition_list.append('%s = %%(%s)s' % (column_name, bind_name)) + + or_clause = '((' + ') OR ('.join(condition_list) + '))' + return or_clause, bind_vars + + +class LikeValue(SQLOperator): + + def __init__(self, value): + super(LikeValue, self).__init__(value, 'LIKE') + + +class GreaterThanValue(SQLOperator): + + def __init__(self, value): + super(GreaterThanValue, self).__init__(value, '>') + + +class GreaterThanOrEqualToValue(SQLOperator): + + def __init__(self, value): + super(GreaterThanOrEqualToValue, self).__init__(value, '>=') + + +class LessThanValue(SQLOperator): + + def __init__(self, value): + super(LessThanValue, self).__init__(value, '<') + + +class LessThanOrEqualToValue(SQLOperator): + + def __init__(self, value): + super(LessThanOrEqualToValue, self).__init__(value, '<=') + + +class ModuloEquals(SQLOperator): + """column % modulus = value.""" + + def __init__(self, modulus, value): + super(ModuloEquals, self).__init__(value, '%') + self.modulus = modulus + + def build_sql(self, column, counter=None): + mod_bind_name = choose_bind_name('modulus', counter=counter) + val_bind_name = choose_bind_name(column, counter=counter) + sql = '(%(column)s %%%% %%(%(mod_bind_name)s)s) = %%(%(val_bind_name)s)s' + return (sql % {'column': column, + 'mod_bind_name': mod_bind_name, + 'val_bind_name': val_bind_name}, + {mod_bind_name: self.modulus, + val_bind_name: self.value}) + + +class Expression(SQLOperator): + + def build_sql(self, column_name, counter=None): + op = self.op + value = str(self.value) + clause = '%(column_name)s %(op)s %(value)s' % vars() + return clause, {} + + +class IsNullOrEmptyString(SQLOperator): + + def __init__(self): + super(IsNullOrEmptyString, self).__init__('', '') + + def build_sql(self, column_name, counter=None): + # mysql treats '' the same as ' ' + return "(%s IS NULL OR %s = '')" % (column_name, column_name), {} + + +class IsNullValue(SQLOperator): + + def __init__(self): + super(IsNullValue, self).__init__('NULL', 'IS') + + def build_sql(self, column_name, counter=None): + return '%s IS NULL' % column_name, {} + + +class IsNotNullValue(SQLOperator): + + def __init__(self): + super(IsNotNullValue, self).__init__('NULL', 'IS NOT') + + def build_sql(self, column_name, counter=None): + return '%s IS NOT NULL' % column_name, {} + + +class Flag(object): + + def __init__(self, flags_present=0x0, flags_absent=0x0): + if flags_present & flags_absent: + raise errors.InternalError( + 'flags_present (0x%016x) and flags_absent (0x%016x)' + ' overlap: 0x%016x' % ( + flags_present, flags_absent, flags_present & flags_absent)) + self.mask = flags_present | flags_absent + self.value = flags_present + self.flags_to_remove = flags_absent + self.flags_to_add = flags_present + + def __repr__(self): + return '%s(flags_present=0x%X, flags_absent=0x%X)' % ( + self.__class__.__name__, self.flags_to_add, self.flags_to_remove) + + def __or__(self, other): + return Flag(flags_present=self.flags_to_add | other.flags_to_add, + flags_absent=self.flags_to_remove | other.flags_to_remove) + + # Beware: this doesn't switch the present and absent flags, it makes + # an object that *clears all the flags* that the operand would touch. + def __invert__(self): + return Flag(flags_absent=self.mask) + + def __eq__(self, other): + if not isinstance(other, Flag): + return False + + return (self.mask == other.mask + and self.value == other.value + and self.flags_to_add == other.flags_to_add + and self.flags_to_remove == other.flags_to_remove) + + def sql(self, column_name='flags'): + return '%s & %s = %s' % (column_name, self.mask, self.value) + + def build_sql(self, column_name='flags', counter=None): + bind_name_mask = choose_bind_name(column_name + '_mask', counter=counter) + bind_name_value = choose_bind_name(column_name + '_value', counter=counter) + + clause = '{column_name} & %({bind_name_mask})s = %({bind_name_value})s'.format( + bind_name_mask=bind_name_mask, bind_name_value=bind_name_value, + column_name=column_name) + + bind_vars = { + bind_name_mask: self.mask, + bind_name_value: self.value + } + return clause, bind_vars + + def update_sql(self, column_name='flags'): + return '%s = (%s | %s) & ~%s' % ( + column_name, column_name, self.flags_to_add, self.flags_to_remove) + + def build_update_sql(self, column_name='flags'): + clause = ('%(column_name)s = (%(column_name)s | ' + '%%(update_%(column_name)s_add)s) & ' + '~%%(update_%(column_name)s_remove)s') % vars( ) + bind_vars = { + 'update_%s_add' % column_name: self.flags_to_add, 'update_%s_remove' % + column_name: self.flags_to_remove} + return clause, bind_vars + + +def make_flag(flag_mask, value): + if value: + return Flag(flags_present=flag_mask) + else: + return Flag(flags_absent=flag_mask) + + +class Increment(object): + + def __init__(self, amount): + self.amount = amount + + def build_update_sql(self, column_name): + clause = ('%(column_name)s = (%(column_name)s + ' + '%%(update_%(column_name)s_amount)s)') % vars() + bind_vars = {'update_%s_amount' % column_name: self.amount} + return clause, bind_vars diff --git a/py/vtdb/topo_utils.py b/py/vtdb/topo_utils.py index c024b66c72b..00d0758d6b5 100644 --- a/py/vtdb/topo_utils.py +++ b/py/vtdb/topo_utils.py @@ -33,11 +33,11 @@ def __init__(self, keyspace_name, shard, db_type, addr, timeout, encrypted, user def get_db_params_for_tablet_conn(topo_client, keyspace_name, shard, db_type, timeout, encrypted, user, password): db_params_list = [] - encrypted_service = '_vts' + encrypted_service = 'vts' if encrypted: service = encrypted_service else: - service = '_vtocc' + service = 'vt' db_key = "%s.%s.%s:%s" % (keyspace_name, shard, db_type, service) # This will read the cached keyspace. keyspace_object = topology.get_keyspace(keyspace_name) @@ -68,7 +68,7 @@ def get_db_params_for_tablet_conn(topo_client, keyspace_name, shard, db_type, ti for entry in end_points_data['Entries']: if service in entry['NamedPortMap']: host_port = (entry['Host'], entry['NamedPortMap'][service], - service == '_vts') + service == 'vts') host_port_list.append(host_port) if encrypted and encrypted_service in entry['NamedPortMap']: host_port = (entry['Host'], entry['NamedPortMap'][encrypted_service], diff --git a/py/vtdb/topology.py b/py/vtdb/topology.py index 8e8d9a2e714..8a4da3f8719 100644 --- a/py/vtdb/topology.py +++ b/py/vtdb/topology.py @@ -8,6 +8,10 @@ # 1. resolve a "db key" into a set of parameters that can be used to connect # 2. resolve the full topology of all databases # +# Note: This is meant to be used to resolve vttablet backends directly using +# vtclient.py. This module will be subsumed by vtgate and hence will be retired +# soon. +# import logging import random @@ -129,13 +133,13 @@ def get_host_port_by_name(topo_client, db_key, encrypted=False): if len(parts) == 2: service = parts[1] else: - service = '_mysql' + service = 'mysql' host_port_list = [] encrypted_host_port_list = [] - if service == '_vtocc' and encrypted: - encrypted_service = '_vts' + if service == 'vt' and encrypted: + encrypted_service = 'vts' db_key = parts[0] ks, shard, tablet_type = db_key.split('.') try: @@ -152,7 +156,7 @@ def get_host_port_by_name(topo_client, db_key, encrypted=False): for entry in data['Entries']: if service in entry['NamedPortMap']: host_port = (entry['Host'], entry['NamedPortMap'][service], - service == '_vts') + service == 'vts') host_port_list.append(host_port) if encrypted and encrypted_service in entry['NamedPortMap']: host_port = (entry['Host'], entry['NamedPortMap'][encrypted_service], @@ -187,4 +191,3 @@ def get_keyrange_from_shard_name(keyspace, shard_name, db_type): else: kr = keyrange.KeyRange(shard_name) return kr - diff --git a/py/vtdb/vtgate_cursor.py b/py/vtdb/vtgate_cursor.py index efea4713c74..9a5730975bc 100644 --- a/py/vtdb/vtgate_cursor.py +++ b/py/vtdb/vtgate_cursor.py @@ -27,6 +27,7 @@ class VTGateCursor(object): keyspace_ids = None keyranges = None _writable = None + routing = None def __init__(self, connection, keyspace, tablet_type, keyspace_ids=None, keyranges=None, writable=False): self._conn = connection @@ -79,16 +80,6 @@ def execute(self, sql, bind_variables, **kargs): if not self.is_writable(): raise dbexceptions.DatabaseError('DML on a non-writable cursor', sql) - # FIXME(shrutip): these checks maybe better on vtgate server. - if topology.is_sharded_keyspace(self.keyspace, self.tablet_type): - if self.keyspace_ids is None or len(self.keyspace_ids) != 1: - raise dbexceptions.ProgrammingError('DML on zero or multiple keyspace ids is not allowed: %r' - % self.keyspace_ids) - else: - if not self.keyranges or str(self.keyranges[0]) != keyrange_constants.NON_PARTIAL_KEYRANGE: - raise dbexceptions.ProgrammingError('Keyrange not correct for non-sharded keyspace: %r' - % self.keyranges) - self.results, self.rowcount, self.lastrowid, self.description = self._conn._execute( sql, bind_variables, diff --git a/py/vtdb/vtgatev2.py b/py/vtdb/vtgatev2.py index a306feac391..55088de4207 100644 --- a/py/vtdb/vtgatev2.py +++ b/py/vtdb/vtgatev2.py @@ -142,10 +142,15 @@ def close(self): def is_closed(self): return self.client.is_closed() - def cursor(self, cursorclass=None, *pargs, **kwargs): + def cursor(self, *pargs, **kwargs): + cursorclass = None + if 'cursorclass' in kwargs: + cursorclass = kwargs['cursorclass'] + del kwargs['cursorclass'] + if cursorclass is None: cursorclass = vtgate_cursor.VTGateCursor - return cursorclass(*pargs, **kwargs) + return cursorclass(self, *pargs, **kwargs) def begin(self): try: @@ -376,10 +381,6 @@ def _stream_next(self): self.session = self._stream_result.reply['Session'] self._stream_result = None continue - # An extra fields message if it is scatter over streaming, ignore it - if not self._stream_result.reply['Result']['Rows']: - self._stream_result = None - continue except gorpc.GoRpcError as e: raise convert_exception(e, str(self)) except: @@ -414,9 +415,9 @@ def get_params_for_vtgate_conn(vtgate_addrs, timeout, encrypted=False, user=None db_params_list = [] addrs = [] if isinstance(vtgate_addrs, dict): - service = '_vt' + service = 'vt' if encrypted: - service = '_vts' + service = 'vts' if service not in vtgate_addrs: raise Exception("required vtgate service addrs %s not exist" % service) addrs = vtgate_addrs[service] diff --git a/py/vtdb/vtgate.py b/py/vtdb/vtgatev3.py similarity index 66% rename from py/vtdb/vtgate.py rename to py/vtdb/vtgatev3.py index f6b43d19196..e9ed32b4719 100644 --- a/py/vtdb/vtgate.py +++ b/py/vtdb/vtgatev3.py @@ -4,39 +4,71 @@ from itertools import izip import logging +import random import re from net import bsonrpc from net import gorpc -from vtdb import cursor from vtdb import dbexceptions from vtdb import field_types - -# This is the shard name for when the keyrange covers the entire space -# for unsharded database. -SHARD_ZERO = "0" +from vtdb import vtdb_logger +from vtdb import cursorv3 _errno_pattern = re.compile('\(errno (\d+)\)') -# Map specific errors to specific classes. -_errno_map = { - 1062: dbexceptions.IntegrityError, -} +def log_exception(method): + """Decorator for logging the exception from vtgatev2. + + The convert_exception method interprets and recasts the exceptions + raised by lower-layer. The inner function calls the appropriate vtdb_logger + method based on the exception raised. + + Args: + exc: exception raised by calling code + args: additional args for the exception. + + Returns: + Decorated method. + """ + def _log_exception(exc, *args): + logger_object = vtdb_logger.get_logger() + + new_exception = method(exc, *args) + + if isinstance(new_exception, dbexceptions.IntegrityError): + logger_object.integrity_error(new_exception) + else: + logger_object.vtgatev2_exception(new_exception) + return new_exception + return _log_exception + + +def handle_app_error(exc_args): + msg = str(exc_args[0]).lower() + if msg.startswith('request_backlog'): + return dbexceptions.RequestBacklog(exc_args) + match = _errno_pattern.search(msg) + if match: + mysql_errno = int(match.group(1)) + # Prune the error message to truncate the query string + # returned by mysql as it contains bind variables. + if mysql_errno == 1062: + parts = _errno_pattern.split(msg) + pruned_msg = msg[:msg.find(parts[2])] + new_args = (pruned_msg,) + tuple(exc_args[1:]) + return dbexceptions.IntegrityError(new_args) + return dbexceptions.DatabaseError(exc_args) + + +@log_exception def convert_exception(exc, *args): new_args = exc.args + args if isinstance(exc, gorpc.TimeoutError): return dbexceptions.TimeoutError(new_args) elif isinstance(exc, gorpc.AppError): - msg = str(exc[0]).lower() - if msg.startswith('request_backlog'): - return dbexceptions.RequestBacklog(new_args) - match = _errno_pattern.search(msg) - if match: - mysql_errno = int(match.group(1)) - return _errno_map.get(mysql_errno, dbexceptions.DatabaseError)(new_args) - return dbexceptions.DatabaseError(new_args) + return handle_app_error(new_args) elif isinstance(exc, gorpc.ProgrammingError): return dbexceptions.ProgrammingError(new_args) elif isinstance(exc, gorpc.GoRpcError): @@ -44,28 +76,32 @@ def convert_exception(exc, *args): return exc -# A simple, direct connection to the vttablet query server. -# This is shard-unaware and only handles the most basic communication. -# If something goes wrong, this object should be thrown away and a new one instantiated. -class VtgateConnection(object): +def _create_req(sql, new_binds, tablet_type): + new_binds = field_types.convert_bind_vars(new_binds) + req = { + 'Sql': sql, + 'BindVariables': new_binds, + 'TabletType': tablet_type, + } + return req + + +# This utilizes the V3 API of VTGate. +class VTGateConnection(object): session = None - tablet_type = None - cursorclass = cursor.TabletCursor _stream_fields = None _stream_conversions = None _stream_result = None _stream_result_index = None - def __init__(self, addr, tablet_type, keyspace, shard, timeout, user=None, password=None, encrypted=False, keyfile=None, certfile=None): + def __init__(self, addr, timeout, user=None, password=None, encrypted=False, keyfile=None, certfile=None): self.addr = addr - self.tablet_type = tablet_type - self.keyspace = keyspace - self.shard = shard self.timeout = timeout self.client = bsonrpc.BsonRpcClient(addr, timeout, user, password, encrypted=encrypted, keyfile=keyfile, certfile=certfile) + self.logger_object = vtdb_logger.get_logger() def __str__(self): - return '' % (self.addr, self.tablet_type, self.keyspace, self.shard) + return '' % self.addr def dial(self): try: @@ -83,6 +119,16 @@ def close(self): def is_closed(self): return self.client.is_closed() + def cursor(self, *pargs, **kwargs): + cursorclass = None + if 'cursorclass' in kwargs: + cursorclass = kwargs['cursorclass'] + del kwargs['cursorclass'] + + if cursorclass is None: + cursorclass = cursorv3.Cursor + return cursorclass(self, *pargs, **kwargs) + def begin(self): try: response = self.client.call('VTGate.Begin', None) @@ -106,9 +152,6 @@ def rollback(self): except gorpc.GoRpcError as e: raise convert_exception(e, str(self)) - def cursor(self, cursorclass=None, **kargs): - return (cursorclass or self.cursorclass)(self, **kargs) - def _add_session(self, req): if self.session: req['Session'] = self.session @@ -117,15 +160,8 @@ def _update_session(self, response): if 'Session' in response.reply and response.reply['Session']: self.session = response.reply['Session'] - def _execute(self, sql, bind_variables): - new_binds = field_types.convert_bind_vars(bind_variables) - req = { - 'Sql': sql, - 'BindVariables': new_binds, - 'Keyspace': self.keyspace, - 'TabletType': self.tablet_type, - 'Shards': [self.shard], - } + def _execute(self, sql, bind_variables, tablet_type): + req = _create_req(sql, bind_variables, tablet_type) self._add_session(req) fields = [] @@ -134,12 +170,11 @@ def _execute(self, sql, bind_variables): rowcount = 0 lastrowid = 0 try: - response = self.client.call('VTGate.ExecuteShard', req) + response = self.client.call('VTGate.Execute', req) self._update_session(response) reply = response.reply - # TODO(sougou): Simplify this check after all servers are deployed if 'Error' in response.reply and response.reply['Error']: - raise gorpc.AppError(response.reply['Error'], 'VTGate.ExecuteShard') + raise gorpc.AppError(response.reply['Error'], 'VTGate.Execute') if 'Result' in reply: res = reply['Result'] @@ -153,13 +188,15 @@ def _execute(self, sql, bind_variables): rowcount = res['RowsAffected'] lastrowid = res['InsertId'] except gorpc.GoRpcError as e: - raise convert_exception(e, str(self), sql, bind_variables) + self.logger_object.log_private_data(bind_variables) + raise convert_exception(e, str(self), sql) except: logging.exception('gorpc low-level error') raise return results, rowcount, lastrowid, fields - def _execute_batch(self, sql_list, bind_variables_list): + + def _execute_batch(self, sql_list, bind_variables_list, tablet_type): query_list = [] for sql, bind_vars in zip(sql_list, bind_variables_list): query = {} @@ -172,15 +209,13 @@ def _execute_batch(self, sql_list, bind_variables_list): try: req = { 'Queries': query_list, - 'Keyspace': self.keyspace, - 'TabletType': self.tablet_type, - 'Shards': [self.shard], + 'TabletType': tablet_type, } self._add_session(req) - response = self.client.call('VTGate.ExecuteBatchShard', req) + response = self.client.call('VTGate.ExecuteBatch', req) self._update_session(response) if 'Error' in response.reply and response.reply['Error']: - raise gorpc.AppError(response.reply['Error'], 'VTGate.ExecuteBatchShard') + raise gorpc.AppError(response.reply['Error'], 'VTGate.ExecuteBatch') for reply in response.reply['List']: fields = [] conversions = [] @@ -198,7 +233,8 @@ def _execute_batch(self, sql_list, bind_variables_list): lastrowid = reply['InsertId'] rowsets.append((results, rowcount, lastrowid, fields)) except gorpc.GoRpcError as e: - raise convert_exception(e, str(self), sql_list, bind_variables_list) + self.logger_object.log_private_data(bind_variables_list) + raise convert_exception(e, str(self), sql_list) except: logging.exception('gorpc low-level error') raise @@ -207,15 +243,8 @@ def _execute_batch(self, sql_list, bind_variables_list): # we return the fields for the response, and the column conversions # the conversions will need to be passed back to _stream_next # (that way we avoid using a member variable here for such a corner case) - def _stream_execute(self, sql, bind_variables): - new_binds = field_types.convert_bind_vars(bind_variables) - req = { - 'Sql': sql, - 'BindVariables': new_binds, - 'Keyspace': self.keyspace, - 'TabletType': self.tablet_type, - 'Shards': [self.shard], - } + def _stream_execute(self, sql, bind_variables, tablet_type): + req = _create_req(sql, bind_variables, tablet_type) self._add_session(req) self._stream_fields = [] @@ -223,7 +252,7 @@ def _stream_execute(self, sql, bind_variables): self._stream_result = None self._stream_result_index = 0 try: - self.client.stream_call('VTGate.StreamExecuteShard', req) + self.client.stream_call('VTGate.StreamExecute', req) first_response = self.client.stream_next() reply = first_response.reply['Result'] @@ -231,7 +260,8 @@ def _stream_execute(self, sql, bind_variables): self._stream_fields.append((field['Name'], field['Type'])) self._stream_conversions.append(field_types.conversions.get(field['Type'])) except gorpc.GoRpcError as e: - raise convert_exception(e, str(self), sql, bind_variables) + self.logger_object.log_private_data(bind_variables) + raise convert_exception(e, str(self), sql) except: logging.exception('gorpc low-level error') raise @@ -250,12 +280,14 @@ def _stream_next(self): self._stream_result_index = None return None # A session message, if any comes separately with no rows - # TODO(sougou) get rid of this check. After all the server - # changes, there will always be a 'Session' in the reply. if 'Session' in self._stream_result.reply and self._stream_result.reply['Session']: self.session = self._stream_result.reply['Session'] self._stream_result = None continue + # An extra fields message if it is scatter over streaming, ignore it + if not self._stream_result.reply['Result']['Rows']: + self._stream_result = None + continue except gorpc.GoRpcError as e: raise convert_exception(e, str(self)) except: @@ -272,6 +304,7 @@ def _stream_next(self): return row + def _make_row(row, conversions): converted_row = [] for conversion_func, field_data in izip(conversions, row): @@ -285,7 +318,7 @@ def _make_row(row, conversions): return converted_row -def connect(*pargs, **kargs): - conn = VtgateConnection(*pargs, **kargs) +def connect(addr, timeout, **kwargs): + conn = VTGateConnection(addr, timeout, **kwargs) conn.dial() return conn diff --git a/test/binlog.py b/test/binlog.py index 83d5f4ebe02..10d415aa891 100755 --- a/test/binlog.py +++ b/test/binlog.py @@ -22,6 +22,8 @@ src_master = tablet.Tablet() src_replica = tablet.Tablet() +src_rdonly1 = tablet.Tablet() +src_rdonly2 = tablet.Tablet() dst_master = tablet.Tablet() dst_replica = tablet.Tablet() @@ -33,6 +35,8 @@ def setUpModule(): setup_procs = [ src_master.init_mysql(), src_replica.init_mysql(), + src_rdonly1.init_mysql(), + src_rdonly2.init_mysql(), dst_master.init_mysql(), dst_replica.init_mysql(), ] @@ -47,19 +51,20 @@ def setUpModule(): src_master.init_tablet('master', 'test_keyspace', '0') src_replica.init_tablet('replica', 'test_keyspace', '0') + src_rdonly1.init_tablet('rdonly', 'test_keyspace', '0') + src_rdonly2.init_tablet('rdonly', 'test_keyspace', '0') utils.run_vtctl(['RebuildShardGraph', 'test_keyspace/0']) utils.validate_topology() utils.run_vtctl(['RebuildKeyspaceGraph', 'test_keyspace'], auto_log=True) - src_master.create_db('vt_test_keyspace') - src_master.start_vttablet(wait_for_state=None) - src_replica.create_db('vt_test_keyspace') - src_replica.start_vttablet(wait_for_state=None) + for t in [src_master, src_replica, src_rdonly1, src_rdonly2]: + t.create_db('vt_test_keyspace') + t.start_vttablet(wait_for_state=None) - src_master.wait_for_vttablet_state('SERVING') - src_replica.wait_for_vttablet_state('SERVING') + for t in [src_master, src_replica, src_rdonly1, src_rdonly2]: + t.wait_for_vttablet_state('SERVING') utils.run_vtctl(['ReparentShard', '-force', 'test_keyspace/0', src_master.tablet_alias], auto_log=True) @@ -89,12 +94,20 @@ def setUpModule(): dst_master.tablet_alias], auto_log=True) utils.run_vtctl(['RebuildKeyspaceGraph', 'test_keyspace'], auto_log=True) - # Start binlog stream from src_replica to dst_master. - logging.debug("Starting binlog stream...") - utils.run_vtctl(['MultiSnapshot', src_replica.tablet_alias], auto_log=True) - src_replica.wait_for_binlog_server_state("Enabled") - utils.run_vtctl(['ShardMultiRestore', '-strategy=-populate_blp_checkpoint', - 'test_keyspace/1', src_replica.tablet_alias], auto_log=True) + # copy the schema + utils.run_vtctl(['CopySchemaShard', src_replica.tablet_alias, + 'test_keyspace/1'], auto_log=True) + + # run the clone worked (this is a degenerate case, source and destination + # both have the full keyrange. Happens to work correctly). + logging.debug("Running the clone worker to start binlog stream...") + utils.run_vtworker(['--cell', 'test_nj', + 'SplitClone', + '--strategy=-populate_blp_checkpoint', + '--source_reader_count', '10', + '--min_table_size_for_split', '1', + 'test_keyspace/0'], + auto_log=True) dst_master.wait_for_binlog_player_count(1) # Wait for dst_replica to be ready. @@ -108,11 +121,14 @@ def tearDownModule(): if utils.options.skip_teardown: return - tablet.kill_tablets([src_master, src_replica, dst_master, dst_replica]) + tablet.kill_tablets([src_master, src_replica, src_rdonly1, src_rdonly2, + dst_master, dst_replica]) teardown_procs = [ src_master.teardown_mysql(), src_replica.teardown_mysql(), + src_rdonly1.teardown_mysql(), + src_rdonly2.teardown_mysql(), dst_master.teardown_mysql(), dst_replica.teardown_mysql(), ] @@ -124,6 +140,8 @@ def tearDownModule(): src_master.remove_tree() src_replica.remove_tree() + src_rdonly1.remove_tree() + src_rdonly2.remove_tree() dst_master.remove_tree() dst_replica.remove_tree() diff --git a/test/client_test.py b/test/client_test.py new file mode 100644 index 00000000000..007768851dc --- /dev/null +++ b/test/client_test.py @@ -0,0 +1,443 @@ +"""Test environment for client library tests. + +This module has functions for creating keyspaces, tablets for the client +library test. +""" +#!/usr/bin/env python +# coding: utf-8 + +import hashlib +import logging +import struct +import threading +import time +import traceback +import unittest + +import environment +import tablet +import utils +from clientlib_tests import topo_schema +from clientlib_tests import db_class_unsharded +from clientlib_tests import db_class_sharded +from clientlib_tests import db_class_lookup + +from vtdb import database_context +from vtdb import keyrange +from vtdb import keyrange_constants +from vtdb import keyspace +from vtdb import dbexceptions +from vtdb import shard_constants +from vtdb import vtdb_logger +from vtdb import vtgatev2 +from vtdb import vtgate_cursor +from zk import zkocc + +conn_class = vtgatev2 +__tablets = None + +vtgate_server = None +vtgate_port = None + +shard_names = ['-80', '80-'] +shard_kid_map = {'-80': [527875958493693904, 626750931627689502, + 345387386794260318, 332484755310826578, + 1842642426274125671, 1326307661227634652, + 1761124146422844620, 1661669973250483744, + 3361397649937244239, 2444880764308344533], + '80-': [9767889778372766922, 9742070682920810358, + 10296850775085416642, 9537430901666854108, + 10440455099304929791, 11454183276974683945, + 11185910247776122031, 10460396697869122981, + 13379616110062597001, 12826553979133932576], + } + +pack_kid = struct.Struct('!Q').pack + +def setUpModule(): + global vtgate_server, vtgate_port + logging.debug("in setUpModule") + try: + environment.topo_server().setup() + setup_topology() + + # start mysql instance external to the test + global __tablets + setup_procs = [] + for tablet in __tablets: + setup_procs.append(tablet.init_mysql()) + utils.wait_procs(setup_procs) + create_db() + start_tablets() + vtgate_server, vtgate_port = utils.vtgate_start() + except: + tearDownModule() + raise + +def tearDownModule(): + global vtgate_server + global __tablets + logging.debug("in tearDownModule") + if utils.options.skip_teardown: + return + logging.debug("Tearing down the servers and setup") + utils.vtgate_kill(vtgate_server) + if __tablets is not None: + tablet.kill_tablets(__tablets) + teardown_procs = [] + for t in __tablets: + teardown_procs.append(t.teardown_mysql()) + utils.wait_procs(teardown_procs, raise_on_error=False) + + environment.topo_server().teardown() + + utils.kill_sub_processes() + utils.remove_tmp_files() + + if __tablets is not None: + for t in __tablets: + t.remove_tree() + +def setup_topology(): + global __tablets + if __tablets is None: + __tablets = [] + + keyspaces = topo_schema.keyspaces + for ks in keyspaces: + ks_name = ks[0] + ks_type = ks[1] + utils.run_vtctl(['CreateKeyspace', ks_name]) + if ks_type == shard_constants.UNSHARDED: + shard_master = tablet.Tablet() + shard_replica = tablet.Tablet() + shard_master.init_tablet('master', keyspace=ks_name, shard='0') + __tablets.append(shard_master) + shard_replica.init_tablet('replica', keyspace=ks_name, shard='0') + __tablets.append(shard_replica) + elif ks_type == shard_constants.RANGE_SHARDED: + utils.run_vtctl(['SetKeyspaceShardingInfo', '-force', ks_name, + 'keyspace_id', 'uint64']) + for shard_name in shard_names: + shard_master = tablet.Tablet() + shard_replica = tablet.Tablet() + shard_master.init_tablet('master', keyspace=ks_name, shard=shard_name) + __tablets.append(shard_master) + shard_replica.init_tablet('replica', keyspace=ks_name, shard=shard_name) + __tablets.append(shard_replica) + utils.run_vtctl(['RebuildKeyspaceGraph', ks_name], auto_log=True) + + +def create_db(): + global __tablets + for t in __tablets: + t.create_db(t.dbname) + ks_name = t.keyspace + for table_tuple in topo_schema.keyspace_table_map[ks_name]: + t.mquery(t.dbname, table_tuple[1]) + +def start_tablets(): + global __tablets + # start tablets + for t in __tablets: + t.start_vttablet(wait_for_state=None) + + # wait for them to come in serving state + for t in __tablets: + t.wait_for_vttablet_state('SERVING') + + # ReparentShard for master tablets + for t in __tablets: + if t.tablet_type == 'master': + utils.run_vtctl(['ReparentShard', '-force', t.keyspace+'/'+t.shard, + t.tablet_alias], auto_log=True) + + for ks in topo_schema.keyspaces: + ks_name = ks[0] + ks_type = ks[1] + utils.run_vtctl(['RebuildKeyspaceGraph', ks_name], + auto_log=True) + if ks_type == shard_constants.RANGE_SHARDED: + utils.check_srv_keyspace('test_nj', ks_name, + 'Partitions(master): -80 80-\n' + + 'Partitions(replica): -80 80-\n' + + 'TabletTypes: master,replica') + + +def get_connection(user=None, password=None): + global vtgate_port + timeout = 10.0 + conn = None + vtgate_addrs = {"vt": ["localhost:%s" % (vtgate_port),]} + conn = conn_class.connect(vtgate_addrs, timeout, + user=user, password=password) + return conn + +def get_keyrange(shard_name): + kr = None + if shard_name == keyrange_constants.SHARD_ZERO: + kr = keyrange.KeyRange(keyrange_constants.NON_PARTIAL_KEYRANGE) + else: + kr = keyrange.KeyRange(shard_name) + return kr + + +def _delete_all(keyspace, shard_name, table_name): + vtgate_conn = get_connection() + # This write is to set up the test with fresh insert + # and hence performing it directly on the connection. + vtgate_conn.begin() + vtgate_conn._execute("delete from %s" % table_name, {}, + keyspace, 'master', + keyranges=[get_keyrange(shard_name)]) + vtgate_conn.commit() + + +def restart_vtgate(extra_args={}): + global vtgate_server, vtgate_port + utils.vtgate_kill(vtgate_server) + vtgate_server, vtgate_port = utils.vtgate_start(vtgate_port, extra_args=extra_args) + +def populate_table(): + keyspace = "KS_UNSHARDED" + _delete_all(keyspace, keyrange_constants.SHARD_ZERO, 'vt_unsharded') + vtgate_conn = get_connection() + cursor = vtgate_conn.cursor(keyspace, 'master', keyranges=[get_keyrange(keyrange_constants.SHARD_ZERO),],writable=True) + cursor.begin() + for x in xrange(10): + cursor.execute('insert into vt_unsharded (id, msg) values (%s, %s)' % (str(x), 'msg'), {}) + cursor.commit() + +class TestUnshardedTable(unittest.TestCase): + + def setUp(self): + self.vtgate_addrs = {"vt": ["localhost:%s" % (vtgate_port),]} + self.dc = database_context.DatabaseContext(self.vtgate_addrs) + with database_context.WriteTransaction(self.dc) as context: + for x in xrange(10): + db_class_unsharded.VtUnsharded.insert(context.get_cursor(), + id=x, msg=str(x)) + + def tearDown(self): + _delete_all("KS_UNSHARDED", "0", 'vt_unsharded') + + def test_read(self): + with database_context.ReadFromMaster(self.dc) as context: + rows = db_class_unsharded.VtUnsharded.select_by_id( + context.get_cursor(), 2) + self.assertEqual(len(rows), 1, "wrong number of rows fetched") + self.assertEqual(rows[0].id, 2, "wrong row fetched") + + def test_update_and_read(self): + where_column_value_pairs = [('id', 2)] + with database_context.WriteTransaction(self.dc) as context: + db_class_unsharded.VtUnsharded.update_columns(context.get_cursor(), + where_column_value_pairs, + msg="test update") + + with database_context.ReadFromMaster(self.dc) as context: + rows = db_class_unsharded.VtUnsharded.select_by_id(context.get_cursor(), 2) + self.assertEqual(len(rows), 1, "wrong number of rows fetched") + self.assertEqual(rows[0].msg, "test update", "wrong row fetched") + + def test_delete_and_read(self): + where_column_value_pairs = [('id', 2)] + with database_context.WriteTransaction(self.dc) as context: + db_class_unsharded.VtUnsharded.delete_by_columns(context.get_cursor(), + where_column_value_pairs) + + with database_context.ReadFromMaster(self.dc) as context: + rows = db_class_unsharded.VtUnsharded.select_by_id(context.get_cursor(), 2) + self.assertEqual(len(rows), 0, "wrong number of rows fetched") + + +class TestRangeSharded(unittest.TestCase): + def populate_tables(self): + # vt_user + user_id_list = [] + # This should create the lookup entries and sharding key. + with database_context.WriteTransaction(self.dc) as context: + for x in xrange(20): + # vt_user - EntityRangeSharded + user_id = db_class_sharded.VtUser.insert(context.get_cursor(), + username="user%s" % x, msg=str(x)) + user_id_list.append(user_id) + + # vt_user_email - RangeSharded + email = 'user%s@google.com' % x + m = hashlib.md5() + m.update(email) + email_hash = m.digest() + entity_id_map={'user_id':user_id} + db_class_sharded.VtUserEmail.insert( + context.get_cursor(entity_id_map=entity_id_map), + user_id=user_id, email=email, + email_hash=email_hash) + # vt_song + # vt_song_detail + return user_id_list + + def setUp(self): + self.vtgate_addrs = {"vt": ["localhost:%s" % (vtgate_port),]} + self.dc = database_context.DatabaseContext(self.vtgate_addrs) + self.user_id_list = self.populate_tables() + + def tearDown(self): + with database_context.WriteTransaction(self.dc) as context: + for uid in self.user_id_list: + try: + db_class_sharded.VtUser.delete_by_columns(context.get_cursor(entity_id_map={'id':uid}), + [('id', uid),]) + db_class_sharded.VtUserEmail.delete_by_columns(context.get_cursor(entity_id_map={'user_id':uid}), + [('user_id', uid),]) + except dbexceptions.DatabaseError as e: + if str(e) == "DB Row not found": + pass + + def test_sharding_key_read(self): + with database_context.ReadFromMaster(self.dc) as context: + where_column_value_pairs = [('id', self.user_id_list[0]),] + rows = db_class_sharded.VtUser.select_by_columns( + context.get_cursor(entity_id_map={'id':self.user_id_list[0]}), + where_column_value_pairs) + self.assertEqual(len(rows), 1, "wrong number of rows fetched") + + where_column_value_pairs = [('user_id', self.user_id_list[0]),] + rows = db_class_sharded.VtUserEmail.select_by_columns( + context.get_cursor(entity_id_map={'user_id':self.user_id_list[0]}), + where_column_value_pairs) + self.assertEqual(len(rows), 1, "wrong number of rows fetched") + + def test_entity_id_read(self): + with database_context.ReadFromMaster(self.dc) as context: + entity_id_map = {'username': 'user0'} + rows = db_class_sharded.VtUser.select_by_columns( + context.get_cursor(entity_id_map=entity_id_map), + [('id', self.user_id_list[0]),]) + self.assertEqual(len(rows), 1, "wrong number of rows fetched") + + def test_in_clause_read(self): + with database_context.ReadFromMaster(self.dc) as context: + user_id_list = [self.user_id_list[0], self.user_id_list[1]] + + where_column_value_pairs = (('id', user_id_list),) + entity_id_map = dict(where_column_value_pairs) + rows = db_class_sharded.VtUser.select_by_ids( + context.get_cursor(entity_id_map=entity_id_map), + where_column_value_pairs) + self.assertEqual(len(rows), 2, "wrong number of rows fetched") + self.assertEqual(user_id_list, [row.id for row in rows], "wrong rows fetched") + + username_list = [row.username for row in rows] + where_column_value_pairs = (('username', username_list),) + entity_id_map = dict(where_column_value_pairs) + rows = db_class_sharded.VtUser.select_by_ids( + context.get_cursor(entity_id_map=entity_id_map), + where_column_value_pairs) + self.assertEqual(len(rows), 2, "wrong number of rows fetched") + self.assertEqual(username_list, [row.username for row in rows], "wrong rows fetched") + + where_column_value_pairs = (('user_id', user_id_list),) + entity_id_map = dict(where_column_value_pairs) + rows = db_class_sharded.VtUserEmail.select_by_ids( + context.get_cursor(entity_id_map=entity_id_map), + where_column_value_pairs) + self.assertEqual(len(rows), 2, "wrong number of rows fetched") + self.assertEqual(user_id_list, [row.user_id for row in rows], "wrong rows fetched") + + def test_keyrange_read(self): + where_column_value_pairs = [] + with database_context.ReadFromMaster(self.dc) as context: + rows1 = db_class_sharded.VtUser.select_by_columns( + context.get_cursor(keyrange='-80'), where_column_value_pairs) + rows2 = db_class_sharded.VtUser.select_by_columns( + context.get_cursor(keyrange='80-'), where_column_value_pairs) + fetched_rows = len(rows1) + len(rows2) + expected = len(self.user_id_list) + self.assertEqual(fetched_rows, expected, "wrong number of rows fetched expected:%d got:%d" % (expected, fetched_rows)) + + def test_scatter_read(self): + where_column_value_pairs = [] + with database_context.ReadFromMaster(self.dc) as context: + rows = db_class_sharded.VtUser.select_by_columns( + context.get_cursor(keyrange=keyrange_constants.NON_PARTIAL_KEYRANGE), + where_column_value_pairs) + self.assertEqual(len(rows), len(self.user_id_list), "wrong number of rows fetched, expecting %d got %d" % (len(self.user_id_list), len(rows))) + + def test_streaming_read(self): + where_column_value_pairs = [] + with database_context.ReadFromMaster(self.dc) as context: + rows = db_class_sharded.VtUser.select_by_columns_streaming( + context.get_cursor(keyrange=keyrange_constants.NON_PARTIAL_KEYRANGE), + where_column_value_pairs) + got_user_id_list = [] + for r in rows: + got_user_id_list.append(r.id) + self.assertEqual(len(got_user_id_list), len(self.user_id_list), "wrong number of rows fetched") + + def update_columns(self): + with database_context.WriteTransaction(self.dc) as context: + user_id = self.user_id_list[1] + where_column_value_pairs = [('id', user_id),] + entity_id_map = {'id': user_id} + new_username = 'new_user%s' % user_id + db_class_sharded.VtUser.update_columns(context.get_cursor(entity_id_map=entity_id_map), + where_column_value_pairs, + username=new_username) + # verify the updated value. + where_column_value_pairs = [('id', user_id),] + rows = db_class_sharded.VtUser.select_by_columns( + context.get_cursor(entity_id_map={'id': user_id}), + where_column_value_pairs) + self.assertEqual(len(rows), 1, "wrong number of rows fetched") + self.assertEqual(new_username, rows[0].username) + + where_column_value_pairs = [('user_id', user_id),] + entity_id_map = {'user_id': user_id} + new_email = 'new_user%s@google.com' % user_id + m = hashlib.md5() + m.update(new_email) + email_hash = m.digest() + db_class_sharded.VtUserEmail.update_columns(context.get_cursor(entity_id_map={'user_id':user_id}), + where_column_value_pairs, + email=new_email, + email_hash=email_hash) + + # verify the updated value. + with database_context.ReadFromMaster(self.dc) as context: + where_column_value_pairs = [('user_id', user_id),] + entity_id_map = dict(where_column_value_pairs) + rows = db_class_sharded.VtUserEmail.select_by_ids( + context.get_cursor(entity_id_map=entity_id_map), + where_column_value_pairs) + self.assertEqual(len(rows), 1, "wrong number of rows fetched") + self.assertEqual(new_email, rows[0].email) + + def delete_columns(self): + user_id = self.user_id_list[-1] + with database_context.WriteTransaction(self.dc) as context: + where_column_value_pairs = [('id', user_id),] + entity_id_map = {'id': user_id} + db_class_sharded.VtUser.delete_by_columns(context.get_cursor(entity_id_map=entity_id_map), + where_column_value_pairs) + + where_column_value_pairs = [('user_id', user_id),] + entity_id_map = {'user_id': user_id} + db_class_sharded.VtUserEmail.delete_by_columns(context.get_cursor(entity_id_map=entity_id_map), + where_column_value_pairs) + + with database_context.ReadFromMaster(self.dc) as context: + rows = db_class_sharded.VtUser.select_by_columns( + context.get_cursor(entity_id_map=entity_id_map), + where_column_value_pairs) + self.assertEqual(len(rows), 0, "wrong number of rows fetched") + + rows = db_class_sharded.VtUserEmail.select_by_ids( + context.get_cursor(entity_id_map=entity_id_map), + where_column_value_pairs) + self.assertEqual(len(rows), 0, "wrong number of rows fetched") + self.user_id_list = self.user_id_list[:-1] + + +if __name__ == '__main__': + utils.main() diff --git a/test/clientlib_tests/__init__.py b/test/clientlib_tests/__init__.py new file mode 100644 index 00000000000..36e40b6542f --- /dev/null +++ b/test/clientlib_tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2012, Google Inc. All rights reserved. +# Use of this source code is governed by a BSD-style license that can +# be found in the LICENSE file. + diff --git a/test/clientlib_tests/db_class_lookup.py b/test/clientlib_tests/db_class_lookup.py new file mode 100644 index 00000000000..3b7552892a6 --- /dev/null +++ b/test/clientlib_tests/db_class_lookup.py @@ -0,0 +1,24 @@ +"""DB Classes for testing client library. + +These classes are used for testing various types of scenarios like +- sharding schemes +- schema types +- lookup + +This module contains the schema and class definitions. +""" + +import topo_schema +from vtdb import db_object_lookup + + +class VtUsernameLookup(db_object_lookup.LookupDBObject): + keyspace = topo_schema.KS_LOOKUP[0] + table_name = "vt_username_lookup" + columns_list = ['user_id', 'username'] + + +class VtSongUserLookup(db_object_lookup.LookupDBObject): + keyspace = topo_schema.KS_LOOKUP[0] + table_name = "vt_song_user_lookup" + columns_list = ['song_id', 'user_id'] diff --git a/test/clientlib_tests/db_class_sharded.py b/test/clientlib_tests/db_class_sharded.py new file mode 100644 index 00000000000..5e36b7e66e1 --- /dev/null +++ b/test/clientlib_tests/db_class_sharded.py @@ -0,0 +1,81 @@ +"""DB Classes for tables in sharded keyspace. + +This provides examples of sharded db class and its interaction with +lookup tables. +""" + +import logging +from Crypto.Cipher import DES3 +import struct + +import db_class_lookup +import topo_schema +from vtdb import db_object_range_sharded + + +pack_kid = struct.Struct('!Q').pack +unpack_kid = struct.Struct('!Q').unpack +encryption_key = '\xff'*24 + + +def create_keyspace_id(sharding_key): + data = pack_kid(sharding_key) + crypter = DES3.new(encryption_key, DES3.MODE_ECB) + encrypted = crypter.encrypt(data) + return unpack_kid(encrypted)[0] + + +class VtRangeBase(db_object_range_sharded.DBObjectRangeSharded): + @classmethod + def sharding_key_to_keyspace_id(class_, sharding_key): + keyspace_id = create_keyspace_id(sharding_key) + return keyspace_id + + @classmethod + def is_sharding_key_valid(class_, sharding_key): + return True + + +class VtEntityRangeBase(db_object_range_sharded.DBObjectEntityRangeSharded): + @classmethod + def sharding_key_to_keyspace_id(class_, sharding_key): + keyspace_id = create_keyspace_id(sharding_key) + return keyspace_id + + @classmethod + def is_sharding_key_valid(class_, sharding_key): + return True + + +class VtUser(VtEntityRangeBase): + keyspace = topo_schema.KS_RANGE_SHARDED[0] + table_name = "vt_user" + columns_list = ["id", "username", "msg", "keyspace_id"] + sharding_key_column_name = "id" + entity_id_lookup_map = {"username": db_class_lookup.VtUsernameLookup} + column_lookup_name_map = {"id":"user_id"} + + +class VtSong(VtEntityRangeBase): + keyspace = topo_schema.KS_RANGE_SHARDED[0] + table_name = "vt_song" + columns_list = ["id", "user_id", "title", "keyspace_id"] + sharding_key_column_name = "user_id" + entity_id_lookup_map = {"id": db_class_lookup.VtSongUserLookup} + column_lookup_name_map = {"id":"song_id"} + + +class VtUserEmail(VtRangeBase): + keyspace = topo_schema.KS_RANGE_SHARDED[0] + table_name = "vt_user_email" + columns_list = ["user_id", "email", "email_hash", "keyspace_id"] + sharding_key_column_name = "user_id" + entity_id_lookup_map = None + + +class VtSongDetail(VtRangeBase): + keyspace = topo_schema.KS_RANGE_SHARDED[0] + table_name = "vt_song_detail" + columns_list = ["song_id", "album_name", "artist", "keyspace_id"] + sharding_key_column_name = None + entity_id_lookup_map = {"song_id": db_class_lookup.VtSongUserLookup} diff --git a/test/clientlib_tests/db_class_unsharded.py b/test/clientlib_tests/db_class_unsharded.py new file mode 100644 index 00000000000..c61bbca45da --- /dev/null +++ b/test/clientlib_tests/db_class_unsharded.py @@ -0,0 +1,19 @@ +"""DB Classes for tables in unsharded keyspace. + +This provides examples of unsharded db class +and its related methods. +""" + +import topo_schema +from vtdb import db_object_unsharded +from vtdb import database_context + +class VtUnsharded(db_object_unsharded.DBObjectUnsharded): + keyspace = topo_schema.KS_UNSHARDED[0] + table_name = "vt_unsharded" + columns_list = ['id', 'msg'] + + @classmethod + def select_by_id(class_, cursor, id_val): + where_column_value_pairs = [('id', id_val),] + return class_.select_by_columns(cursor, where_column_value_pairs) diff --git a/test/clientlib_tests/topo_schema.py b/test/clientlib_tests/topo_schema.py new file mode 100644 index 00000000000..d72f37b20a9 --- /dev/null +++ b/test/clientlib_tests/topo_schema.py @@ -0,0 +1,84 @@ +"""This module contains keyspace and schema definitions for the client tests. + +The keyspaces define various sharding schemes. And tables represent +different schema types and relationships. +""" + +from vtdb import shard_constants + +KS_UNSHARDED = ("KS_UNSHARDED", shard_constants.UNSHARDED) +KS_RANGE_SHARDED = ("KS_RANGE_SHARDED", shard_constants.RANGE_SHARDED) +KS_LOOKUP = ("KS_LOOKUP", shard_constants.UNSHARDED) + +#KS_UNSHARDED tables +create_vt_unsharded = '''create table vt_unsharded ( +id bigint, +msg varchar(64), +primary key (id) +) Engine=InnoDB''' + +#KS_RANGE_SHARDED tables +#entity user, entity_id username, lookup vt_username_lookup +create_vt_user = '''create table vt_user ( +id bigint, +username varchar(64), +msg varchar(64), +keyspace_id bigint(20) unsigned NOT NULL, +primary key (id), +key idx_username (username) +) Engine=InnoDB''' + +create_vt_user_email = '''create table vt_user_email ( +user_id bigint(20) NOT NULL, +email varchar(60) NOT NULL, +email_hash binary(20) NOT NULL, +keyspace_id bigint(20) unsigned NOT NULL, +PRIMARY KEY (user_id), +KEY email_hash (email_hash(4)) +) ENGINE=InnoDB''' + +#entity song, entity_id id, lookup vt_song_user_lookup +create_vt_song = '''create table vt_song ( +id bigint, +user_id bigint, +title varchar(64), +keyspace_id bigint(20) unsigned NOT NULL, +primary key (user_id, id), +unique key id_idx (id) +) Engine=InnoDB''' + +create_vt_song_detail = '''create table vt_song_detail ( +song_id bigint, +album_name varchar(64), +artist varchar(64), +keyspace_id bigint(20) unsigned NOT NULL, +primary key (song_id) +) Engine=InnoDB''' + +#KS_LOOKUP tables +create_vt_username_lookup = '''create table vt_username_lookup ( +user_id bigint(20) NOT NULL AUTO_INCREMENT, +username varchar(20) NOT NULL, +primary key (user_id), +unique key idx_username (username) +) ENGINE=InnoDB''' + +create_vt_song_user_lookup = '''create table vt_song_user_lookup ( +song_id bigint(20) NOT NULL AUTO_INCREMENT, +user_id varchar(20) NOT NULL, +primary key (song_id) +) ENGINE=InnoDB''' + + +keyspaces = [KS_UNSHARDED, KS_RANGE_SHARDED, KS_LOOKUP] + +keyspace_table_map = {KS_UNSHARDED[0]: [('vt_unsharded', create_vt_unsharded),], + KS_RANGE_SHARDED[0]: [('vt_user', create_vt_user), + ('vt_user_email', create_vt_user_email), + ('vt_song', create_vt_song), + ('vt_song_detail', create_vt_song_detail), + ], + KS_LOOKUP[0]: [('vt_username_lookup', create_vt_username_lookup), + ('vt_song_user_lookup', create_vt_song_user_lookup), + ], + } diff --git a/test/clone.py b/test/clone.py index 0eddd5cc6d2..cba945b1da3 100755 --- a/test/clone.py +++ b/test/clone.py @@ -32,8 +32,8 @@ def setUpModule(): tablet_31981.init_mysql(), ] if use_mysqlctld: - tablet_62344.wait_for_mysql_socket() - tablet_31981.wait_for_mysql_socket() + tablet_62344.wait_for_mysqlctl_socket() + tablet_31981.wait_for_mysqlctl_socket() else: utils.wait_procs(setup_procs) except: @@ -48,12 +48,13 @@ def tearDownModule(): # Try to terminate mysqlctld gracefully, so it kills its mysqld. for proc in setup_procs: utils.kill_sub_process(proc, soft=True) + teardown_procs = setup_procs else: teardown_procs = [ tablet_62344.teardown_mysql(), tablet_31981.teardown_mysql(), ] - utils.wait_procs(teardown_procs, raise_on_error=False) + utils.wait_procs(teardown_procs, raise_on_error=False) environment.topo_server().teardown() utils.kill_sub_processes() @@ -192,7 +193,10 @@ def _test_vtctl_snapshot_restore(self, server_mode): results['ReadOnly'] != 'true' or results['OriginalType'] != 'master'): self.fail("Bad values returned by Snapshot: %s" % err) - tablet_31981.init_tablet('idle', start=True) + + # try to init + start in one go + tablet_31981.start_vttablet(wait_for_state='NOT_SERVING', + init_tablet_type='idle') # do not specify a MANIFEST, see if 'default' works call(["touch", "/tmp/vtSimulateFetchFailures"]) diff --git a/test/demo.py b/test/demo.py new file mode 100755 index 00000000000..5b81f5c0d3e --- /dev/null +++ b/test/demo.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2015, Google Inc. All rights reserved. +# Use of this source code is governed by a BSD-style license that can +# be found in the LICENSE file. +""" +This program launches and shuts down a demo vitess cluster. +""" + +import json +import optparse + +import environment +import keyspace_util +import utils + + +vschema = '''{ + "Keyspaces": { + "user": { + "Sharded": true, + "Vindexes": { + "user_idx": { + "Type": "hash_autoinc", + "Params": { + "Table": "user_idx", + "Column": "user_id" + }, + "Owner": "user" + }, + "name_user_idx": { + "Type": "lookup_hash", + "Params": { + "Table": "name_user_idx", + "From": "name", + "To": "user_id" + }, + "Owner": "user" + }, + "music_user_idx": { + "Type": "lookup_hash_unique_autoinc", + "Params": { + "Table": "music_user_idx", + "From": "music_id", + "To": "user_id" + }, + "Owner": "music" + }, + "keyspace_idx": { + "Type": "numeric" + } + }, + "Classes": { + "user": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "user_idx" + }, + { + "Col": "name", + "Name": "name_user_idx" + } + ] + }, + "user_extra": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "user_idx" + } + ] + }, + "music": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "user_idx" + }, + { + "Col": "music_id", + "Name": "music_user_idx" + } + ] + }, + "music_extra": { + "ColVindexes": [ + { + "Col": "music_id", + "Name": "music_user_idx" + }, + { + "Col": "keyspace_id", + "Name": "keyspace_idx" + } + ] + } + }, + "Tables": { + "user": "user", + "user_extra": "user_extra", + "music": "music", + "music_extra": "music_extra" + } + }, + "lookup": { + "Sharded": false, + "Tables": { + "user_idx": "", + "music_user_idx": "", + "name_user_idx": "" + } + } + } +}''' + +# Verify valid json +json.loads(vschema) + +def main(): + parser = optparse.OptionParser(usage="usage: %prog [options]") + utils.add_options(parser) + (options, args) = parser.parse_args() + options.debug = True + utils.set_options(options) + env = keyspace_util.TestEnv() + vtgate_server=None + try: + environment.topo_server().setup() + env.launch( + "user", + shards=["-80", "80-"], + ddls=[ + 'create table user(user_id bigint, name varchar(128), primary key(user_id))', + 'create table user_extra(user_id bigint, extra varchar(128), primary key(user_id))', + 'create table music(user_id bigint, music_id bigint, primary key(user_id, music_id))', + 'create table music_extra(music_id bigint, keyspace_id bigint unsigned, primary key(music_id))', + ], + ) + env.launch( + "lookup", + ddls=[ + 'create table user_idx(user_id bigint not null auto_increment, primary key(user_id))', + 'create table name_user_idx(name varchar(128), user_id bigint, primary key(name, user_id))', + 'create table music_user_idx(music_id bigint not null auto_increment, user_id bigint, primary key(music_id))', + ], + ) + utils.apply_vschema(vschema) + vtgate_server, vtgate_port = utils.vtgate_start(cache_ttl='500s') + utils.Vtctld().start() + print "vtgate:", vtgate_port + print "vtctld:", utils.vtctld.port + utils.pause("the cluster is up, press enter to shut it down...") + finally: + utils.vtgate_kill(vtgate_server) + env.teardown() + utils.kill_sub_processes() + utils.remove_tmp_files() + environment.topo_server().teardown() + + +if __name__ == '__main__': + main() diff --git a/test/environment.py b/test/environment.py index 4e22a2759ce..9b70e95a3c9 100644 --- a/test/environment.py +++ b/test/environment.py @@ -5,13 +5,23 @@ import os import socket import subprocess +import sys # Import the topo implementations that you want registered as options for the # --topo-server-flavor flag. import topo_flavor.zookeeper +import topo_flavor.etcd from topo_flavor.server import topo_server +# sanity check the environment +if os.environ['USER'] == 'root': + sys.stderr.write('ERROR: Vitess and its dependencies (mysqld and memcached) should not be run as root.\n') + sys.exit(1) +if 'VTTOP' not in os.environ: + sys.stderr.write('ERROR: Vitess environment not set up. Please run "source dev.env" first.\n') + sys.exit(1) + # vttop is the toplevel of the vitess source tree vttop = os.environ['VTTOP'] @@ -90,7 +100,7 @@ def prog_compile(name): return compiled_progs.append(name) logging.debug('Compiling %s', name) - run(['go', 'install'], cwd=os.path.join(vttop, 'go', 'cmd', name)) + run(['godep', 'go', 'install'], cwd=os.path.join(vttop, 'go', 'cmd', name)) # binary management: returns the full path for a binary # this should typically not be used outside this file, unless you want to bypass @@ -118,3 +128,7 @@ def binary_argstr(name): # binary management for the MySQL distribution. def mysql_binary_path(name): return os.path.join(vt_mysql_root, 'bin', name) + +# add environment-specific command-line options +def add_options(parser): + pass diff --git a/test/fake_zkocc_config.json b/test/fake_zkocc_config.json index 6e9539bfdc9..33284e320aa 100644 --- a/test/fake_zkocc_config.json +++ b/test/fake_zkocc_config.json @@ -23,8 +23,8 @@ "host": "127.0.0.1", "port": 0, "named_port_map": { - "_mysql": 3306, - "_vtocc": 6711 + "mysql": 3306, + "vt": 6711 } } ] diff --git a/test/initial_sharding.py b/test/initial_sharding.py index 83bb5525294..8ca04793f15 100755 --- a/test/initial_sharding.py +++ b/test/initial_sharding.py @@ -8,7 +8,7 @@ # - we start with a keyspace with a single shard and a single table # - we add and populate the sharding key # - we set the sharding key in the topology -# - we backup / restore into 2 instances +# - we clone into 2 instances # - we enable filtered replication # - we move all serving types # - we scrap the source tablets @@ -27,24 +27,23 @@ import utils import tablet -use_clone_worker = False keyspace_id_type = keyrange_constants.KIT_UINT64 pack_keyspace_id = struct.Struct('!Q').pack # initial shard, covers everything shard_master = tablet.Tablet() shard_replica = tablet.Tablet() -shard_rdonly = tablet.Tablet() +shard_rdonly1 = tablet.Tablet() # split shards # range "" - 80 shard_0_master = tablet.Tablet() shard_0_replica = tablet.Tablet() -shard_0_rdonly = tablet.Tablet() +shard_0_rdonly1 = tablet.Tablet() # range 80 - "" shard_1_master = tablet.Tablet() shard_1_replica = tablet.Tablet() -shard_1_rdonly = tablet.Tablet() +shard_1_rdonly1 = tablet.Tablet() def setUpModule(): @@ -54,13 +53,13 @@ def setUpModule(): setup_procs = [ shard_master.init_mysql(), shard_replica.init_mysql(), - shard_rdonly.init_mysql(), + shard_rdonly1.init_mysql(), shard_0_master.init_mysql(), shard_0_replica.init_mysql(), - shard_0_rdonly.init_mysql(), + shard_0_rdonly1.init_mysql(), shard_1_master.init_mysql(), shard_1_replica.init_mysql(), - shard_1_rdonly.init_mysql(), + shard_1_rdonly1.init_mysql(), ] utils.wait_procs(setup_procs) except: @@ -75,13 +74,13 @@ def tearDownModule(): teardown_procs = [ shard_master.teardown_mysql(), shard_replica.teardown_mysql(), - shard_rdonly.teardown_mysql(), + shard_rdonly1.teardown_mysql(), shard_0_master.teardown_mysql(), shard_0_replica.teardown_mysql(), - shard_0_rdonly.teardown_mysql(), + shard_0_rdonly1.teardown_mysql(), shard_1_master.teardown_mysql(), shard_1_replica.teardown_mysql(), - shard_1_rdonly.teardown_mysql(), + shard_1_rdonly1.teardown_mysql(), ] utils.wait_procs(teardown_procs, raise_on_error=False) @@ -91,13 +90,13 @@ def tearDownModule(): shard_master.remove_tree() shard_replica.remove_tree() - shard_rdonly.remove_tree() + shard_rdonly1.remove_tree() shard_0_master.remove_tree() shard_0_replica.remove_tree() - shard_0_rdonly.remove_tree() + shard_0_rdonly1.remove_tree() shard_1_master.remove_tree() shard_1_replica.remove_tree() - shard_1_rdonly.remove_tree() + shard_1_rdonly1.remove_tree() class TestInitialSharding(unittest.TestCase): @@ -219,24 +218,24 @@ def _is_value_present_and_correct(self, tablet, table, id, msg, keyspace_id): def _check_startup_values(self): # check first value is in the right shard - for t in [shard_0_master, shard_0_replica, shard_0_rdonly]: + for t in [shard_0_master, shard_0_replica, shard_0_rdonly1]: self._check_value(t, 'resharding1', 1, 'msg1', 0x1000000000000000) - for t in [shard_1_master, shard_1_replica, shard_1_rdonly]: + for t in [shard_1_master, shard_1_replica, shard_1_rdonly1]: self._check_value(t, 'resharding1', 1, 'msg1', 0x1000000000000000, should_be_here=False) # check second value is in the right shard - for t in [shard_0_master, shard_0_replica, shard_0_rdonly]: + for t in [shard_0_master, shard_0_replica, shard_0_rdonly1]: self._check_value(t, 'resharding1', 2, 'msg2', 0x9000000000000000, should_be_here=False) - for t in [shard_1_master, shard_1_replica, shard_1_rdonly]: + for t in [shard_1_master, shard_1_replica, shard_1_rdonly1]: self._check_value(t, 'resharding1', 2, 'msg2', 0x9000000000000000) # check third value is in the right shard too - for t in [shard_0_master, shard_0_replica, shard_0_rdonly]: + for t in [shard_0_master, shard_0_replica, shard_0_rdonly1]: self._check_value(t, 'resharding1', 3, 'msg3', 0xD000000000000000, should_be_here=False) - for t in [shard_1_master, shard_1_replica, shard_1_rdonly]: + for t in [shard_1_master, shard_1_replica, shard_1_rdonly1]: self._check_value(t, 'resharding1', 3, 'msg3', 0xD000000000000000) def _insert_lots(self, count, base=0): @@ -289,19 +288,19 @@ def test_resharding(self): shard_master.init_tablet( 'master', 'test_keyspace', '0') shard_replica.init_tablet('replica', 'test_keyspace', '0') - shard_rdonly.init_tablet( 'rdonly', 'test_keyspace', '0') + shard_rdonly1.init_tablet( 'rdonly', 'test_keyspace', '0') utils.run_vtctl(['RebuildKeyspaceGraph', 'test_keyspace'], auto_log=True) # create databases so vttablet can start behaving normally - for t in [shard_master, shard_replica, shard_rdonly]: + for t in [shard_master, shard_replica, shard_rdonly1]: t.create_db('vt_test_keyspace') t.start_vttablet(wait_for_state=None) # wait for the tablets shard_master.wait_for_vttablet_state('SERVING') shard_replica.wait_for_vttablet_state('SERVING') - shard_rdonly.wait_for_vttablet_state('SERVING') + shard_rdonly1.wait_for_vttablet_state('SERVING') # reparent to make the tablets work utils.run_vtctl(['ReparentShard', '-force', 'test_keyspace/0', @@ -319,18 +318,18 @@ def test_resharding(self): # create the split shards shard_0_master.init_tablet( 'master', 'test_keyspace', '-80') shard_0_replica.init_tablet('replica', 'test_keyspace', '-80') - shard_0_rdonly.init_tablet( 'rdonly', 'test_keyspace', '-80') + shard_0_rdonly1.init_tablet( 'rdonly', 'test_keyspace', '-80') shard_1_master.init_tablet( 'master', 'test_keyspace', '80-') shard_1_replica.init_tablet('replica', 'test_keyspace', '80-') - shard_1_rdonly.init_tablet( 'rdonly', 'test_keyspace', '80-') + shard_1_rdonly1.init_tablet( 'rdonly', 'test_keyspace', '80-') # start vttablet on the split shards (no db created, # so they're all not serving) - for t in [shard_0_master, shard_0_replica, shard_0_rdonly, - shard_1_master, shard_1_replica, shard_1_rdonly]: + for t in [shard_0_master, shard_0_replica, shard_0_rdonly1, + shard_1_master, shard_1_replica, shard_1_rdonly1]: t.start_vttablet(wait_for_state=None) - for t in [shard_0_master, shard_0_replica, shard_0_rdonly, - shard_1_master, shard_1_replica, shard_1_rdonly]: + for t in [shard_0_master, shard_0_replica, shard_0_rdonly1, + shard_1_master, shard_1_replica, shard_1_rdonly1]: t.wait_for_vttablet_state('NOT_SERVING') utils.run_vtctl(['ReparentShard', '-force', 'test_keyspace/-80', @@ -347,35 +346,25 @@ def test_resharding(self): 'TabletTypes: master,rdonly,replica', keyspace_id_type=keyspace_id_type) - if use_clone_worker: - # the worker will do snapshot / restore - utils.run_vtworker(['--cell', 'test_nj', - '--command_display_interval', '10ms', - 'SplitClone', - '--exclude_tables' ,'unrelated', - '--strategy=-populate_blp_checkpoint -write_masters_only', - '--source_reader_count', '10', - '--min_table_size_for_split', '1', - 'test_keyspace/0'], - auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_rdonly.tablet_alias, 'rdonly'], + # we need to create the schema, and the worker will do data copying + for keyspace_shard in ('test_keyspace/-80', 'test_keyspace/80-'): + utils.run_vtctl(['CopySchemaShard', + '--exclude_tables', 'unrelated', + shard_rdonly1.tablet_alias, + keyspace_shard], auto_log=True) - else: - # take the snapshot for the split - utils.run_vtctl(['MultiSnapshot', '--spec=-80-', - shard_replica.tablet_alias], auto_log=True) - - # wait for tablet's binlog server service to be enabled after snapshot - shard_replica.wait_for_binlog_server_state("Enabled") - - # perform the restore. - utils.run_vtctl(['ShardMultiRestore', '-strategy=-populate_blp_checkpoint', - 'test_keyspace/-80', shard_replica.tablet_alias], - auto_log=True) - utils.run_vtctl(['ShardMultiRestore', '-strategy=-populate_blp_checkpoint', - 'test_keyspace/80-', shard_replica.tablet_alias], - auto_log=True) + utils.run_vtworker(['--cell', 'test_nj', + '--command_display_interval', '10ms', + 'SplitClone', + '--exclude_tables' ,'unrelated', + '--strategy=-populate_blp_checkpoint', + '--source_reader_count', '10', + '--min_table_size_for_split', '1', + 'test_keyspace/0'], + auto_log=True) + utils.run_vtctl(['ChangeSlaveType', shard_rdonly1.tablet_alias, 'rdonly'], + auto_log=True) # check the startup values are in the right place self._check_startup_values() @@ -404,17 +393,17 @@ def test_resharding(self): logging.debug("Running vtworker SplitDiff for -80") utils.run_vtworker(['-cell', 'test_nj', 'SplitDiff', 'test_keyspace/-80'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_rdonly.tablet_alias, 'rdonly'], + utils.run_vtctl(['ChangeSlaveType', shard_rdonly1.tablet_alias, 'rdonly'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_0_rdonly.tablet_alias, 'rdonly'], + utils.run_vtctl(['ChangeSlaveType', shard_0_rdonly1.tablet_alias, 'rdonly'], auto_log=True) logging.debug("Running vtworker SplitDiff for 80-") utils.run_vtworker(['-cell', 'test_nj', 'SplitDiff', 'test_keyspace/80-'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_rdonly.tablet_alias, 'rdonly'], + utils.run_vtctl(['ChangeSlaveType', shard_rdonly1.tablet_alias, 'rdonly'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_1_rdonly.tablet_alias, 'rdonly'], + utils.run_vtctl(['ChangeSlaveType', shard_1_rdonly1.tablet_alias, 'rdonly'], auto_log=True) utils.pause("Good time to test vtworker for diffs") @@ -434,6 +423,9 @@ def test_resharding(self): keyspace_id_type=keyspace_id_type) # then serve replica from the split shards + source_tablet = shard_replica + destination_tablets = [shard_0_replica, shard_1_replica] + utils.run_vtctl(['MigrateServedTypes', 'test_keyspace/0', 'replica'], auto_log=True) utils.check_srv_keyspace('test_nj', 'test_keyspace', @@ -446,14 +438,21 @@ def test_resharding(self): # move replica back and forth utils.run_vtctl(['MigrateServedTypes', '-reverse', 'test_keyspace/0', 'replica'], auto_log=True) + # After a backwards migration, queryservice should be enabled on source and disabled on destinations + utils.check_tablet_query_service(self, source_tablet, True, False) + utils.check_tablet_query_services(self, destination_tablets, False, True) utils.check_srv_keyspace('test_nj', 'test_keyspace', 'Partitions(master): -\n' + 'Partitions(rdonly): -80 80-\n' + 'Partitions(replica): -\n' + 'TabletTypes: master,rdonly,replica', keyspace_id_type=keyspace_id_type) + utils.run_vtctl(['MigrateServedTypes', 'test_keyspace/0', 'replica'], auto_log=True) + # After a forwards migration, queryservice should be disabled on source and enabled on destinations + utils.check_tablet_query_service(self, source_tablet, False, True) + utils.check_tablet_query_services(self, destination_tablets, True, False) utils.check_srv_keyspace('test_nj', 'test_keyspace', 'Partitions(master): -\n' + 'Partitions(rdonly): -80 80-\n' + @@ -461,6 +460,7 @@ def test_resharding(self): 'TabletTypes: master,rdonly,replica', keyspace_id_type=keyspace_id_type) + # then serve master from the split shards utils.run_vtctl(['MigrateServedTypes', 'test_keyspace/0', 'master'], auto_log=True) @@ -479,10 +479,10 @@ def test_resharding(self): utils.run_vtctl(['DeleteShard', 'test_keyspace/0'], expect_fail=True) # scrap the original tablets in the original shard - for t in [shard_master, shard_replica, shard_rdonly]: + for t in [shard_master, shard_replica, shard_rdonly1]: utils.run_vtctl(['ScrapTablet', t.tablet_alias], auto_log=True) - tablet.kill_tablets([shard_master, shard_replica, shard_rdonly]) - for t in [shard_master, shard_replica, shard_rdonly]: + tablet.kill_tablets([shard_master, shard_replica, shard_rdonly1]) + for t in [shard_master, shard_replica, shard_rdonly1]: utils.run_vtctl(['DeleteTablet', t.tablet_alias], auto_log=True) # rebuild the serving graph, all mentions of the old shards shoud be gone @@ -492,8 +492,8 @@ def test_resharding(self): utils.run_vtctl(['DeleteShard', 'test_keyspace/0'], auto_log=True) # kill everything else - tablet.kill_tablets([shard_0_master, shard_0_replica, shard_0_rdonly, - shard_1_master, shard_1_replica, shard_1_rdonly]) + tablet.kill_tablets([shard_0_master, shard_0_replica, shard_0_rdonly1, + shard_1_master, shard_1_replica, shard_1_rdonly1]) if __name__ == '__main__': utils.main() diff --git a/test/initial_sharding_bytes_vtworker.py b/test/initial_sharding_bytes_vtworker.py deleted file mode 100755 index 79fe3c41ae4..00000000000 --- a/test/initial_sharding_bytes_vtworker.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2014, Google Inc. All rights reserved. -# Use of this source code is governed by a BSD-style license that can -# be found in the LICENSE file. - -import utils -import initial_sharding - -from vtdb import keyrange_constants - -# this test is the same as initial_sharding_bytes.py, but it uses vtworker to -# do the clone. -if __name__ == '__main__': - initial_sharding.use_clone_worker = True - initial_sharding.keyspace_id_type = keyrange_constants.KIT_BYTES - utils.main(initial_sharding) diff --git a/test/initial_sharding_vtworker.py b/test/initial_sharding_vtworker.py deleted file mode 100755 index 233e14017dc..00000000000 --- a/test/initial_sharding_vtworker.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2014, Google Inc. All rights reserved. -# Use of this source code is governed by a BSD-style license that can -# be found in the LICENSE file. - -import utils -import initial_sharding - -# this test is the same as initial_sharding.py, but it uses vtworker to -# do the clone. -if __name__ == '__main__': - initial_sharding.use_clone_worker = True - utils.main(initial_sharding) diff --git a/test/java_vtgate_test_helper.py b/test/java_vtgate_test_helper.py index ae49b75bed1..5d23da7b949 100644 --- a/test/java_vtgate_test_helper.py +++ b/test/java_vtgate_test_helper.py @@ -12,8 +12,8 @@ - start VtGate instance Usage: -java_vtgate_test_helper.py --shards=-80,80- --tablet-config='{"rdonly":1, "replica":1}' --keyspace=test_keyspace setup -starts 1 VtGate and 6 vttablets - 1 master, replica and rdonly each per shard +java_vtgate_test_helper.py --shards=-80,80- --tablet-config='{"rdonly":1, "replica":1}' --keyspace=test_keyspace --vtgate-port=11111 setup +starts 1 VtGate on the specified port and 6 vttablets - 1 master, replica and rdonly each per shard java_vtgate_test_helper.py --shards=-80,80- --tablet-config='{"rdonly":1, "replica":1}' --keyspace=test_keyspace teardown shuts down the tablets and VtGate instances @@ -41,6 +41,9 @@ class TestEnv(object): vtgate_port = None def __init__(self, options): self.keyspace = options.keyspace + self.schema = options.schema + self.vschema = options.vschema + self.vtgate_port = options.vtgate_port self.tablets = [] tablet_config = json.loads(options.tablet_config) for shard in options.shards.split(','): @@ -60,14 +63,24 @@ def set_up(self): utils.run_vtctl(['RebuildKeyspaceGraph', self.keyspace], auto_log=True) for t in self.tablets: t.create_db('vt_' + self.keyspace) - t.start_vttablet(wait_for_state=None) + t.start_vttablet( + wait_for_state=None, + extra_args=['-queryserver-config-schema-reload-time', '1'], + ) for t in self.tablets: t.wait_for_vttablet_state('SERVING') for t in self.tablets: if t.type == "master": utils.run_vtctl(['ReparentShard', '-force', self.keyspace+'/'+t.shard, t.tablet_alias], auto_log=True) utils.run_vtctl(['RebuildKeyspaceGraph', self.keyspace], auto_log=True) - self.vtgate_server, self.vtgate_port = utils.vtgate_start(cache_ttl='500s') + if self.schema: + utils.run_vtctl(['ApplySchemaKeyspace', '-simple', '-sql', self.schema, self.keyspace]) + if self.vschema: + if self.vschema[0] == '{': + utils.run_vtctl(['ApplyVSchema', "-vschema", self.vschema]) + else: + utils.run_vtctl(['ApplyVSchema', "-vschema_file", self.vschema]) + self.vtgate_server, self.vtgate_port = utils.vtgate_start(cache_ttl='500s', vtport=self.vtgate_port) vtgate_client = zkocc.ZkOccConnection("localhost:%u" % self.vtgate_port, "test_nj", 30.0) topology.read_topology(vtgate_client) except: @@ -86,17 +99,22 @@ def shutdown(self): t.remove_tree() -def main(): +def parse_args(): + global options, args parser = optparse.OptionParser(usage="usage: %prog [options]") parser.add_option("--shards", action="store", type="string", help="comma separated list of shard names, e.g: '-80,80-'") parser.add_option("--tablet-config", action="store", type="string", help="json config for for non-master tablets. e.g {'replica':2, 'rdonly':1}") parser.add_option("--keyspace", action="store", type="string") + parser.add_option("--schema", action="store", type="string") + parser.add_option("--vschema", action="store", type="string") + parser.add_option("--vtgate-port", action="store", type="int") utils.add_options(parser) (options, args) = parser.parse_args() utils.set_options(options) - + +def main(): env = TestEnv(options) if args[0] == 'setup': env.set_up() @@ -109,5 +127,6 @@ def main(): if __name__ == '__main__': + parse_args() main() diff --git a/test/keyspace_test.py b/test/keyspace_test.py index 3db31398296..5b513e9b9b2 100755 --- a/test/keyspace_test.py +++ b/test/keyspace_test.py @@ -27,16 +27,13 @@ # range "" - 80 shard_0_master = tablet.Tablet() shard_0_replica = tablet.Tablet() -shard_0_rdonly = tablet.Tablet() # range 80 - "" shard_1_master = tablet.Tablet() shard_1_replica = tablet.Tablet() -shard_1_rdonly = tablet.Tablet() # shard for UNSHARDED_KEYSPACE unsharded_master = tablet.Tablet() unsharded_replica = tablet.Tablet() -unsharded_rdonly = tablet.Tablet() vtgate_server = None vtgate_port = None @@ -69,13 +66,10 @@ def setUpModule(): setup_procs = [ shard_0_master.init_mysql(), shard_0_replica.init_mysql(), - shard_0_rdonly.init_mysql(), shard_1_master.init_mysql(), shard_1_replica.init_mysql(), - shard_1_rdonly.init_mysql(), unsharded_master.init_mysql(), unsharded_replica.init_mysql(), - unsharded_rdonly.init_mysql(), ] utils.wait_procs(setup_procs) setup_tablets() @@ -90,18 +84,15 @@ def tearDownModule(): global vtgate_server utils.vtgate_kill(vtgate_server) - tablet.kill_tablets([shard_0_master, shard_0_replica, shard_0_rdonly, - shard_1_master, shard_1_replica, shard_1_rdonly]) + tablet.kill_tablets([shard_0_master, shard_0_replica, + shard_1_master, shard_1_replica]) teardown_procs = [ shard_0_master.teardown_mysql(), shard_0_replica.teardown_mysql(), - shard_0_rdonly.teardown_mysql(), shard_1_master.teardown_mysql(), shard_1_replica.teardown_mysql(), - shard_1_rdonly.teardown_mysql(), unsharded_master.teardown_mysql(), unsharded_replica.teardown_mysql(), - unsharded_rdonly.teardown_mysql(), ] utils.wait_procs(teardown_procs, raise_on_error=False) @@ -111,15 +102,10 @@ def tearDownModule(): shard_0_master.remove_tree() shard_0_replica.remove_tree() - shard_0_rdonly.remove_tree() shard_1_master.remove_tree() shard_1_replica.remove_tree() - shard_1_rdonly.remove_tree() - shard_1_rdonly.remove_tree() - shard_1_rdonly.remove_tree() unsharded_master.remove_tree() unsharded_replica.remove_tree() - unsharded_rdonly.remove_tree() def setup_tablets(): global vtgate_server @@ -136,14 +122,12 @@ def setup_sharded_keyspace(): 'keyspace_id', 'uint64']) shard_0_master.init_tablet('master', keyspace=SHARDED_KEYSPACE, shard='-80') shard_0_replica.init_tablet('replica', keyspace=SHARDED_KEYSPACE, shard='-80') - shard_0_rdonly.init_tablet('rdonly', keyspace=SHARDED_KEYSPACE, shard='-80') shard_1_master.init_tablet('master', keyspace=SHARDED_KEYSPACE, shard='80-') shard_1_replica.init_tablet('replica', keyspace=SHARDED_KEYSPACE, shard='80-') - shard_1_rdonly.init_tablet('rdonly', keyspace=SHARDED_KEYSPACE, shard='80-') utils.run_vtctl(['RebuildKeyspaceGraph', SHARDED_KEYSPACE,], auto_log=True) - for t in [shard_0_master, shard_0_replica, shard_0_rdonly, shard_1_master, shard_1_replica, shard_1_rdonly]: + for t in [shard_0_master, shard_0_replica, shard_1_master, shard_1_replica]: t.create_db('vt_test_keyspace_sharded') t.mquery(shard_0_master.dbname, create_vt_insert_test) t.start_vttablet(wait_for_state=None) @@ -161,9 +145,8 @@ def setup_sharded_keyspace(): utils.check_srv_keyspace('test_nj', SHARDED_KEYSPACE, 'Partitions(master): -80 80-\n' + - 'Partitions(rdonly): -80 80-\n' + 'Partitions(replica): -80 80-\n' + - 'TabletTypes: master,rdonly,replica') + 'TabletTypes: master,replica') def setup_unsharded_keyspace(): @@ -172,16 +155,15 @@ def setup_unsharded_keyspace(): 'keyspace_id', 'uint64']) unsharded_master.init_tablet('master', keyspace=UNSHARDED_KEYSPACE, shard='0') unsharded_replica.init_tablet('replica', keyspace=UNSHARDED_KEYSPACE, shard='0') - unsharded_rdonly.init_tablet('rdonly', keyspace=UNSHARDED_KEYSPACE, shard='0') utils.run_vtctl(['RebuildKeyspaceGraph', UNSHARDED_KEYSPACE,], auto_log=True) - for t in [unsharded_master, unsharded_replica, unsharded_rdonly]: + for t in [unsharded_master, unsharded_replica]: t.create_db('vt_test_keyspace_unsharded') t.mquery(unsharded_master.dbname, create_vt_insert_test) t.start_vttablet(wait_for_state=None) - for t in [unsharded_master, unsharded_replica, unsharded_rdonly]: + for t in [unsharded_master, unsharded_replica]: t.wait_for_vttablet_state('SERVING') utils.run_vtctl(['ReparentShard', '-force', '%s/0' % UNSHARDED_KEYSPACE, @@ -192,12 +174,11 @@ def setup_unsharded_keyspace(): utils.check_srv_keyspace('test_nj', UNSHARDED_KEYSPACE, 'Partitions(master): -\n' + - 'Partitions(rdonly): -\n' + 'Partitions(replica): -\n' + - 'TabletTypes: master,rdonly,replica') + 'TabletTypes: master,replica') -ALL_DB_TYPES = ['master', 'replica', 'rdonly'] +ALL_DB_TYPES = ['master', 'replica'] class TestKeyspace(unittest.TestCase): def _read_keyspace(self, keyspace_name): diff --git a/test/keyspace_util.py b/test/keyspace_util.py new file mode 100644 index 00000000000..d934f2e8740 --- /dev/null +++ b/test/keyspace_util.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2015, Google Inc. All rights reserved. +# Use of this source code is governed by a BSD-style license that can +# be found in the LICENSE file. +""" +This module allows you to bring up and tear down keyspaces. +""" + +import os + +import environment +import tablet +import utils + +class TestEnv(object): + def __init__(self): + self.tablet_map={} + + def launch(self, keyspace, shards=None, replica_count=0, rdonly_count=0, ddls=None): + self.tablets=[] + utils.run_vtctl(['CreateKeyspace', keyspace]) + if not shards or shards[0] == "0": + shards = ["0"] + else: + utils.run_vtctl(['SetKeyspaceShardingInfo', '-force', keyspace, 'keyspace_id', 'uint64']) + + for shard in shards: + procs = [] + procs.append(self._start_tablet(keyspace, shard, "master", None)) + for i in xrange(replica_count): + procs.append(self._start_tablet(keyspace, shard, "replica", i)) + for i in xrange(rdonly_count): + procs.append(self._start_tablet(keyspace, shard, "rdonly", i)) + utils.wait_procs(procs) + + utils.run_vtctl(['RebuildKeyspaceGraph', keyspace], auto_log=True) + + for t in self.tablets: + t.create_db('vt_' + keyspace) + t.start_vttablet( + wait_for_state=None, + extra_args=['-queryserver-config-schema-reload-time', '1'], + ) + for t in self.tablets: + t.wait_for_vttablet_state('SERVING') + for t in self.tablets: + if t.tablet_type == "master": + utils.run_vtctl(['ReparentShard', '-force', keyspace+'/'+t.shard, t.tablet_alias], auto_log=True) + # Force read-write even if there are no replicas. + utils.run_vtctl(['SetReadWrite', t.tablet_alias], auto_log=True) + + utils.run_vtctl(['RebuildKeyspaceGraph', keyspace], auto_log=True) + + for ddl in ddls: + fname = os.path.join(environment.tmproot, "ddl.sql") + with open(fname, "w") as f: + f.write(ddl) + utils.run_vtctl(['ApplySchemaKeyspace', '-simple', '-sql-file', fname, keyspace]) + + def teardown(self): + all_tablets = self.tablet_map.values() + tablet.kill_tablets(all_tablets) + teardown_procs = [t.teardown_mysql() for t in all_tablets] + utils.wait_procs(teardown_procs, raise_on_error=False) + for t in all_tablets: + t.remove_tree() + + def _start_tablet(self, keyspace, shard, tablet_type, index): + t = tablet.Tablet() + self.tablets.append(t) + if tablet_type == "master": + key = "%s.%s.%s" %(keyspace, shard, tablet_type) + else: + key = "%s.%s.%s.%s" %(keyspace, shard, tablet_type, index) + self.tablet_map[key] = t + proc = t.init_mysql() + t.init_tablet(tablet_type, keyspace=keyspace, shard=shard) + return proc diff --git a/test/primecache.py b/test/primecache.py index a33eb4648ba..43368fcd106 100755 --- a/test/primecache.py +++ b/test/primecache.py @@ -152,7 +152,6 @@ def test_primecache(self): args = environment.binary_args('vtprimecache') + [ '-db-config-dba-uname', 'vt_dba', '-db-config-dba-charset', 'utf8', - '-db-config-dba-dbname', 'vt_test_keyspace', '-db-config-app-uname', 'vt_app', '-db-config-app-charset', 'utf8', '-db-config-app-dbname', 'vt_test_keyspace', diff --git a/test/protocols_flavor.py b/test/protocols_flavor.py index e2acb76781c..657b6d48e6a 100644 --- a/test/protocols_flavor.py +++ b/test/protocols_flavor.py @@ -42,7 +42,7 @@ def tabletconn_protocol_flags(self): return ['-tablet_protocol', 'gorpc'] def rpc_timeout_message(self): - return 'Timeout waiting for' + return 'timeout waiting for' __knows_protocols_flavor_map = { 'gorpc': GoRpcProtocolsFlavor, diff --git a/test/queryservice_test.py b/test/queryservice_test.py index 185f50f840d..0560b6ae7d1 100755 --- a/test/queryservice_test.py +++ b/test/queryservice_test.py @@ -21,7 +21,7 @@ from topo_flavor.server import set_topo_server_flavor -if __name__ == "__main__": +def main(): parser = optparse.OptionParser(usage="usage: %prog [options] [test_names]") parser.add_option("-m", "--memcache", action="store_true", default=False, help="starts a memcache d, and tests rowcache") @@ -33,6 +33,9 @@ logging.getLogger().setLevel(logging.ERROR) utils.set_options(options) + run_tests(options, args) + +def run_tests(options, args): suite = unittest.TestSuite() if args: if args[0] == 'teardown': @@ -72,3 +75,6 @@ if options.keep_logs: print("Leaving temporary files behind (--keep-logs), please " "clean up before next run: " + os.environ["VTDATAROOT"]) + +if __name__ == "__main__": + main() diff --git a/test/queryservice_tests/cache_cases1.py b/test/queryservice_tests/cache_cases1.py index f4f5cadf254..5d02d0b81bc 100644 --- a/test/queryservice_tests/cache_cases1.py +++ b/test/queryservice_tests/cache_cases1.py @@ -16,6 +16,7 @@ def __init__(self, **kwargs): rewritten=[ "select * from vtocc_cached1 where 1 != 1", "select eid, name, foo from vtocc_cached1 where eid in (1)"], + rowcount=1, cache_misses=1), # (1) is in cache @@ -23,6 +24,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select * from vtocc_cached1 where eid = 1", result=[(1L, 'a', 'abcd')], + rowcount=1, rewritten=[], cache_hits=1), # (1) @@ -31,6 +33,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select * from vtocc_cached1 where eid in (1, 3, 6)", result=[(1L, 'a', 'abcd'), (3L, 'c', 'abcd')], + rowcount=2, rewritten=[ "select * from vtocc_cached1 where 1 != 1", "select eid, name, foo from vtocc_cached1 where eid in (3, 6)"], @@ -43,6 +46,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select * from vtocc_cached1 where eid in (1, 3, 6) limit 0", result=[], + rowcount=0, rewritten=["select * from vtocc_cached1 where 1 != 1"], cache_hits=0, cache_misses=0, @@ -53,6 +57,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select * from vtocc_cached1 where eid in (1, 3, 6) limit 1", result=[(1L, 'a', 'abcd')], + rowcount=1, rewritten=[ "select * from vtocc_cached1 where 1 != 1", "select eid, name, foo from vtocc_cached1 where eid in (6)"], @@ -66,6 +71,7 @@ def __init__(self, **kwargs): sql="select * from vtocc_cached1 where eid in (1, 3, 6) limit :a", bindings={"a": 1}, result=[(1L, 'a', 'abcd')], + rowcount=1, rewritten=[ "select * from vtocc_cached1 where 1 != 1", "select eid, name, foo from vtocc_cached1 where eid in (6)"], @@ -77,6 +83,7 @@ def __init__(self, **kwargs): Case1(doc="SELECT_SUBQUERY (1, 2)", sql="select * from vtocc_cached1 where name = 'a'", result=[(1L, 'a', 'abcd'), (2L, 'a', 'abcd')], + rowcount=2, rewritten=[ "select * from vtocc_cached1 where 1 != 1", "select eid from vtocc_cached1 use index (aname1) where name = 'a' limit 10001", @@ -89,6 +96,7 @@ def __init__(self, **kwargs): query_plan="PASS_SELECT", sql="select eid, name from vtocc_cached1 where name = 'a'", result=[(1L, 'a'), (2L, 'a')], + rowcount=2, rewritten=[ "select eid, name from vtocc_cached1 where 1 != 1", "select eid, name from vtocc_cached1 where name = 'a' limit 10001"]), @@ -97,6 +105,7 @@ def __init__(self, **kwargs): Case1(doc="SELECT_SUBQUERY (1, 2)", sql="select * from vtocc_cached1 where name = 'a'", result=[(1L, 'a', 'abcd'), (2L, 'a', 'abcd')], + rowcount=2, rewritten=["select eid from vtocc_cached1 use index (aname1) where name = 'a' limit 10001"], cache_hits=2), # (1, 2, 3) @@ -105,6 +114,7 @@ def __init__(self, **kwargs): query_plan="SELECT_SUBQUERY", sql="select * from vtocc_cached1 where name between 'd' and 'e'", result=[(4L, 'd', 'abcd'), (5L, 'e', 'efgh')], + rowcount=2, rewritten=[ "select * from vtocc_cached1 where 1 != 1", "select eid from vtocc_cached1 use index (aname1) where name between 'd' and 'e' limit 10001", @@ -117,6 +127,7 @@ def __init__(self, **kwargs): query_plan="PASS_SELECT", sql="select * from vtocc_cached1 where foo='abcd'", result=[(1L, 'a', 'abcd'), (2L, 'a', 'abcd'), (3L, 'c', 'abcd'), (4L, 'd', 'abcd')], + rowcount=4, rewritten=[ "select * from vtocc_cached1 where 1 != 1", "select * from vtocc_cached1 where foo = 'abcd' limit 10001"], diff --git a/test/queryservice_tests/cache_cases2.py b/test/queryservice_tests/cache_cases2.py index f2932981838..0b5130cbf04 100644 --- a/test/queryservice_tests/cache_cases2.py +++ b/test/queryservice_tests/cache_cases2.py @@ -15,6 +15,7 @@ def __init__(self, **kwargs): sql="select * from vtocc_cached2 where eid = 2 and bid = :bid", bindings={"bid": None}, result=[], + rowcount=0, rewritten=[ "select * from vtocc_cached2 where 1 != 1", "select eid, bid, name, foo from vtocc_cached2 where (eid = 2 and bid = null)"], @@ -24,6 +25,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select * from vtocc_cached2 where eid = 2 and bid = 'foo'", result=[(2, 'foo', 'abcd2', 'efgh')], + rowcount=1, rewritten=[ "select * from vtocc_cached2 where 1 != 1", "select eid, bid, name, foo from vtocc_cached2 where (eid = 2 and bid = 'foo')"], @@ -34,6 +36,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select bid, eid, name, foo from vtocc_cached2 where eid = 2 and bid = 'foo'", result=[('foo', 2, 'abcd2', 'efgh')], + rowcount=1, rewritten=["select bid, eid, name, foo from vtocc_cached2 where 1 != 1"], cache_hits=1), # (2.foo) @@ -42,6 +45,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select bid, eid, name, foo from vtocc_cached2 where eid = 3 and bid = 'foo'", result=[], + rowcount=0, rewritten=[ "select bid, eid, name, foo from vtocc_cached2 where 1 != 1", "select eid, bid, name, foo from vtocc_cached2 where (eid = 3 and bid = 'foo')"], @@ -51,6 +55,7 @@ def __init__(self, **kwargs): Case2(doc="out of order columns list", sql="select bid, eid from vtocc_cached2 where eid = 1 and bid = 'foo'", result=[('foo', 1)], + rowcount=1, rewritten=[ "select bid, eid from vtocc_cached2 where 1 != 1", "select eid, bid, name, foo from vtocc_cached2 where (eid = 1 and bid = 'foo')"], @@ -60,6 +65,7 @@ def __init__(self, **kwargs): Case2(doc="out of order columns list, use cache", sql="select bid, eid from vtocc_cached2 where eid = 1 and bid = 'foo'", result=[('foo', 1)], + rowcount=1, rewritten=[], cache_hits=1), # (1.foo, 2.foo) @@ -68,6 +74,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select eid, bid, name, foo from vtocc_cached2 where eid = 1 and bid in('absent1', 'absent2')", result=[], + rowcount=0, rewritten=[ "select eid, bid, name, foo from vtocc_cached2 where 1 != 1", "select eid, bid, name, foo from vtocc_cached2 where (eid = 1 and bid = 'absent1') or (eid = 1 and bid = 'absent2')"], @@ -81,6 +88,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select eid, bid, name, foo from vtocc_cached2 where eid = 1 and bid in('foo', 'bar')", result=[(1L, 'foo', 'abcd1', 'efgh'), (1L, 'bar', 'abcd1', 'efgh')], + rowcount=2, rewritten=[ "select eid, bid, name, foo from vtocc_cached2 where 1 != 1", "select eid, bid, name, foo from vtocc_cached2 where (eid = 1 and bid = 'bar')"], @@ -94,6 +102,7 @@ def __init__(self, **kwargs): query_plan="PK_IN", sql="select eid, bid, name, foo from vtocc_cached2 where eid = 1 and bid in('foo', 'bar')", result=[(1L, 'foo', 'abcd1', 'efgh'), (1L, 'bar', 'abcd1', 'efgh')], + rowcount=2, rewritten=[], cache_hits=2, cache_misses=0, @@ -105,6 +114,7 @@ def __init__(self, **kwargs): query_plan="SELECT_SUBQUERY", sql="select eid, bid, name, foo from vtocc_cached2 where eid = 2 and name='abcd2'", result=[(2L, 'foo', 'abcd2', 'efgh'), (2L, 'bar', 'abcd2', 'efgh')], + rowcount=2, rewritten=[ "select eid, bid, name, foo from vtocc_cached2 where 1 != 1", "select eid, bid from vtocc_cached2 use index (aname2) where eid = 2 and name = 'abcd2' limit 10001", @@ -118,6 +128,7 @@ def __init__(self, **kwargs): Case2(doc="verify 1.bar is in cache", sql="select bid, eid from vtocc_cached2 where eid = 1 and bid = 'bar'", result=[('bar', 1)], + rowcount=1, rewritten=[ "select bid, eid from vtocc_cached2 where 1 != 1"], cache_hits=1), @@ -131,6 +142,7 @@ def __init__(self, **kwargs): cache_invalidations=2), Case2(sql="select * from vtocc_cached2 where eid = 1 and bid = 'bar'", result=[(1L, 'bar', 'abcd1', 'fghi')], + rowcount=1, rewritten=[ "select * from vtocc_cached2 where 1 != 1", "select eid, bid, name, foo from vtocc_cached2 where (eid = 1 and bid = 'bar')"], @@ -144,6 +156,7 @@ def __init__(self, **kwargs): 'rollback', Case2(sql="select * from vtocc_cached2 where eid = 1 and bid = 'bar'", result=[(1L, 'bar', 'abcd1', 'fghi')], + rowcount=1, rewritten=[], cache_hits=1)]), # (1.foo, 1.bar, 2.foo, 2.bar) @@ -156,6 +169,7 @@ def __init__(self, **kwargs): cache_invalidations=1), Case2(sql="select * from vtocc_cached2 where eid = 1 and bid = 'bar'", result=[], + rowcount=0, rewritten="select eid, bid, name, foo from vtocc_cached2 where (eid = 1 and bid = 'bar')", cache_absent=1), "begin", @@ -167,6 +181,7 @@ def __init__(self, **kwargs): Case2(doc="Verify 1.foo is in cache", sql="select * from vtocc_cached2 where eid = 1 and bid = 'foo'", result=[(1, 'foo', 'abcd1', 'efgh')], + rowcount=1, rewritten=["select * from vtocc_cached2 where 1 != 1"], cache_hits=1), # (1.foo, 2.foo, 2.bar) @@ -177,6 +192,7 @@ def __init__(self, **kwargs): Case2(doc="Verify cache is empty after DDL", sql="select * from vtocc_cached2 where eid = 1 and bid = 'foo'", result=[(1, 'foo', 'abcd1', 'efgh')], + rowcount=1, rewritten=[ "select * from vtocc_cached2 where 1 != 1", "select eid, bid, name, foo from vtocc_cached2 where (eid = 1 and bid = 'foo')"], @@ -186,6 +202,7 @@ def __init__(self, **kwargs): Case2(doc="Verify row is cached", sql="select * from vtocc_cached2 where eid = 1 and bid = 'foo'", result=[(1, 'foo', 'abcd1', 'efgh')], + rowcount=1, rewritten=[], cache_hits=1), # (1.foo) diff --git a/test/queryservice_tests/cases_framework.py b/test/queryservice_tests/cases_framework.py index 223429d23ed..5dc4a0f8407 100644 --- a/test/queryservice_tests/cases_framework.py +++ b/test/queryservice_tests/cases_framework.py @@ -37,6 +37,7 @@ def __init__(self, line): self.query_sources, self.mysql_response_time, self.waiting_for_connection_time, + self.rowcount, self.size_of_response, self.cache_hits, self.cache_misses, @@ -78,6 +79,10 @@ def check_original_sql(self, case): if sql != eval(self.original_sql): return self.fail('wrong sql', case.sql, self.original_sql) + def check_rowcount(self, case): + if case.rowcount is not None and int(self.rowcount) != case.rowcount: + return self.fail("Bad rowcount", case.rowcount, self.rowcount) + def check_cache_hits(self, case): if case.cache_hits is not None and int(self.cache_hits) != case.cache_hits: return self.fail("Bad Cache Hits", case.cache_hits, self.cache_hits) @@ -117,7 +122,7 @@ def check_number_of_queries(self, case): class Case(object): def __init__(self, sql, bindings=None, result=None, rewritten=None, doc='', - cache_table=None, query_plan=None, cache_hits=None, + rowcount=None, cache_table=None, query_plan=None, cache_hits=None, cache_misses=None, cache_absent=None, cache_invalidations=None, remote_address="[::1]"): # For all cache_* parameters, a number n means "check this value @@ -129,6 +134,7 @@ def __init__(self, sql, bindings=None, result=None, rewritten=None, doc='', if isinstance(rewritten, basestring): rewritten = [rewritten] self.rewritten = rewritten + self.rowcount = rowcount self.doc = doc self.query_plan = query_plan self.cache_table = cache_table diff --git a/test/queryservice_tests/nocache_cases.py b/test/queryservice_tests/nocache_cases.py index 68c3dbe5794..9457b63b93c 100644 --- a/test/queryservice_tests/nocache_cases.py +++ b/test/queryservice_tests/nocache_cases.py @@ -7,6 +7,7 @@ Case(doc='union', sql='select /* union */ eid, id from vtocc_a union select eid, id from vtocc_b', result=[(1L, 1L), (1L, 2L)], + rowcount=2, rewritten=[ 'select eid, id from vtocc_a where 1 != 1 union select eid, id from vtocc_b where 1 != 1', 'select /* union */ eid, id from vtocc_a union select eid, id from vtocc_b']), @@ -14,6 +15,7 @@ Case(doc='double union', sql='select /* double union */ eid, id from vtocc_a union select eid, id from vtocc_b union select eid, id from vtocc_d', result=[(1L, 1L), (1L, 2L)], + rowcount=2, rewritten=[ 'select eid, id from vtocc_a where 1 != 1 union select eid, id from vtocc_b where 1 != 1 union select eid, id from vtocc_d where 1 != 1', 'select /* double union */ eid, id from vtocc_a union select eid, id from vtocc_b union select eid, id from vtocc_d']), @@ -28,12 +30,14 @@ Case(doc='group by', sql='select /* group by */ eid, sum(id) from vtocc_a group by eid', result=[(1L, 3L)], + rowcount=1, rewritten=[ 'select eid, sum(id) from vtocc_a where 1 != 1', 'select /* group by */ eid, sum(id) from vtocc_a group by eid limit 10001']), Case(doc='having', sql='select /* having */ sum(id) from vtocc_a having sum(id) = 3', result=[(3L,)], + rowcount=1, rewritten=[ 'select sum(id) from vtocc_a where 1 != 1', 'select /* having */ sum(id) from vtocc_a having sum(id) = 3 limit 10001']), @@ -42,12 +46,14 @@ sql='select /* limit */ eid, id from vtocc_a limit :a', bindings={"a": 1}, result=[(1L, 1L)], + rowcount=1, rewritten=[ 'select eid, id from vtocc_a where 1 != 1', 'select /* limit */ eid, id from vtocc_a limit 1']), Case(doc='multi-table', sql='select /* multi-table */ a.eid, a.id, b.eid, b.id from vtocc_a as a, vtocc_b as b order by a.eid, a.id, b.eid, b.id', result=[(1L, 1L, 1L, 1L), (1L, 1L, 1L, 2L), (1L, 2L, 1L, 1L), (1L, 2L, 1L, 2L)], + rowcount=4, rewritten=[ 'select a.eid, a.id, b.eid, b.id from vtocc_a as a, vtocc_b as b where 1 != 1', 'select /* multi-table */ a.eid, a.id, b.eid, b.id from vtocc_a as a, vtocc_b as b order by a.eid asc, a.id asc, b.eid asc, b.id asc limit 10001']), @@ -55,6 +61,7 @@ Case(doc='join', sql='select /* join */ a.eid, a.id, b.eid, b.id from vtocc_a as a join vtocc_b as b on a.eid = b.eid and a.id = b.id', result=[(1L, 1L, 1L, 1L), (1L, 2L, 1L, 2L)], + rowcount=2, rewritten=[ 'select a.eid, a.id, b.eid, b.id from vtocc_a as a join vtocc_b as b where 1 != 1', 'select /* join */ a.eid, a.id, b.eid, b.id from vtocc_a as a join vtocc_b as b on a.eid = b.eid and a.id = b.id limit 10001']), @@ -62,6 +69,7 @@ Case(doc='straight_join', sql='select /* straight_join */ a.eid, a.id, b.eid, b.id from vtocc_a as a straight_join vtocc_b as b on a.eid = b.eid and a.id = b.id', result=[(1L, 1L, 1L, 1L), (1L, 2L, 1L, 2L)], + rowcount=2, rewritten=[ 'select a.eid, a.id, b.eid, b.id from vtocc_a as a straight_join vtocc_b as b where 1 != 1', 'select /* straight_join */ a.eid, a.id, b.eid, b.id from vtocc_a as a straight_join vtocc_b as b on a.eid = b.eid and a.id = b.id limit 10001']), @@ -69,6 +77,7 @@ Case(doc='cross join', sql='select /* cross join */ a.eid, a.id, b.eid, b.id from vtocc_a as a cross join vtocc_b as b on a.eid = b.eid and a.id = b.id', result=[(1L, 1L, 1L, 1L), (1L, 2L, 1L, 2L)], + rowcount=2, rewritten=[ 'select a.eid, a.id, b.eid, b.id from vtocc_a as a cross join vtocc_b as b where 1 != 1', 'select /* cross join */ a.eid, a.id, b.eid, b.id from vtocc_a as a cross join vtocc_b as b on a.eid = b.eid and a.id = b.id limit 10001']), @@ -76,6 +85,7 @@ Case(doc='natural join', sql='select /* natural join */ a.eid, a.id, b.eid, b.id from vtocc_a as a natural join vtocc_b as b', result=[(1L, 1L, 1L, 1L), (1L, 2L, 1L, 2L)], + rowcount=2, rewritten=[ 'select a.eid, a.id, b.eid, b.id from vtocc_a as a natural join vtocc_b as b where 1 != 1', 'select /* natural join */ a.eid, a.id, b.eid, b.id from vtocc_a as a natural join vtocc_b as b limit 10001']), @@ -83,6 +93,7 @@ Case(doc='left join', sql='select /* left join */ a.eid, a.id, b.eid, b.id from vtocc_a as a left join vtocc_b as b on a.eid = b.eid and a.id = b.id', result=[(1L, 1L, 1L, 1L), (1L, 2L, 1L, 2L)], + rowcount=2, rewritten=[ 'select a.eid, a.id, b.eid, b.id from vtocc_a as a left join vtocc_b as b on 1 != 1 where 1 != 1', 'select /* left join */ a.eid, a.id, b.eid, b.id from vtocc_a as a left join vtocc_b as b on a.eid = b.eid and a.id = b.id limit 10001']), @@ -90,6 +101,7 @@ Case(doc='right join', sql='select /* right join */ a.eid, a.id, b.eid, b.id from vtocc_a as a right join vtocc_b as b on a.eid = b.eid and a.id = b.id', result=[(1L, 1L, 1L, 1L), (1L, 2L, 1L, 2L)], + rowcount=2, rewritten=[ 'select a.eid, a.id, b.eid, b.id from vtocc_a as a right join vtocc_b as b on 1 != 1 where 1 != 1', 'select /* right join */ a.eid, a.id, b.eid, b.id from vtocc_a as a right join vtocc_b as b on a.eid = b.eid and a.id = b.id limit 10001']), @@ -98,12 +110,14 @@ Case(doc='complex select list', sql='select /* complex select list */ eid+1, id from vtocc_a', result=[(2L, 1L), (2L, 2L)], + rowcount=2, rewritten=[ 'select eid+1, id from vtocc_a where 1 != 1', 'select /* complex select list */ eid+1, id from vtocc_a limit 10001']), Case(doc="*", sql='select /* * */ * from vtocc_a', result=[(1L, 1L, 'abcd', 'efgh'), (1L, 2L, 'bcde', 'fghi')], + rowcount=2, rewritten=[ 'select * from vtocc_a where 1 != 1', 'select /* * */ * from vtocc_a limit 10001']), @@ -111,6 +125,7 @@ Case(doc='table alias', sql='select /* table alias */ a.eid from vtocc_a as a where a.eid=1', result=[(1L,), (1L,)], + rowcount=2, rewritten=[ 'select a.eid from vtocc_a as a where 1 != 1', 'select /* table alias */ a.eid from vtocc_a as a where a.eid = 1 limit 10001']), @@ -118,6 +133,7 @@ Case(doc='parenthesised col', sql='select /* parenthesised col */ (eid) from vtocc_a where eid = 1 and id = 1', result=[(1L,)], + rowcount=1, rewritten=[ 'select (eid) from vtocc_a where 1 != 1', 'select /* parenthesised col */ (eid) from vtocc_a where eid = 1 and id = 1 limit 10001']), @@ -126,6 +142,7 @@ ['begin', Case(sql='select /* for update */ eid from vtocc_a where eid = 1 and id = 1 for update', result=[(1L,)], + rowcount=1, rewritten=[ 'select eid from vtocc_a where 1 != 1', 'select /* for update */ eid from vtocc_a where eid = 1 and id = 1 limit 10001 for update']), @@ -135,6 +152,7 @@ ['begin', Case(sql='select /* for update */ eid from vtocc_a where eid = 1 and id = 1 lock in share mode', result=[(1L,)], + rowcount=1, rewritten=[ 'select eid from vtocc_a where 1 != 1', 'select /* for update */ eid from vtocc_a where eid = 1 and id = 1 limit 10001 lock in share mode']), @@ -143,6 +161,7 @@ Case(doc='complex where', sql='select /* complex where */ id from vtocc_a where id+1 = 2', result=[(1L,)], + rowcount=1, rewritten=[ 'select id from vtocc_a where 1 != 1', 'select /* complex where */ id from vtocc_a where id+1 = 2 limit 10001']), @@ -150,6 +169,7 @@ Case(doc='complex where (non-value operand)', sql='select /* complex where (non-value operand) */ eid, id from vtocc_a where eid = id', result=[(1L, 1L)], + rowcount=1, rewritten=[ 'select eid, id from vtocc_a where 1 != 1', 'select /* complex where (non-value operand) */ eid, id from vtocc_a where eid = id limit 10001']), @@ -157,6 +177,7 @@ Case(doc='(condition)', sql='select /* (condition) */ * from vtocc_a where (eid = 1)', result=[(1L, 1L, 'abcd', 'efgh'), (1L, 2L, 'bcde', 'fghi')], + rowcount=2, rewritten=[ 'select * from vtocc_a where 1 != 1', 'select /* (condition) */ * from vtocc_a where (eid = 1) limit 10001']), @@ -164,12 +185,14 @@ Case(doc='inequality', sql='select /* inequality */ * from vtocc_a where id > 1', result=[(1L, 2L, 'bcde', 'fghi')], + rowcount=1, rewritten=[ 'select * from vtocc_a where 1 != 1', 'select /* inequality */ * from vtocc_a where id > 1 limit 10001']), Case(doc='in', sql='select /* in */ * from vtocc_a where id in (1, 2)', result=[(1L, 1L, 'abcd', 'efgh'), (1L, 2L, 'bcde', 'fghi')], + rowcount=2, rewritten=[ 'select * from vtocc_a where 1 != 1', 'select /* in */ * from vtocc_a where id in (1, 2) limit 10001']), @@ -177,6 +200,7 @@ Case(doc='between', sql='select /* between */ * from vtocc_a where id between 1 and 2', result=[(1L, 1L, 'abcd', 'efgh'), (1L, 2L, 'bcde', 'fghi')], + rowcount=2, rewritten=[ 'select * from vtocc_a where 1 != 1', 'select /* between */ * from vtocc_a where id between 1 and 2 limit 10001']), @@ -184,18 +208,21 @@ Case(doc='order', sql='select /* order */ * from vtocc_a order by id desc', result=[(1L, 2L, 'bcde', 'fghi'), (1L, 1L, 'abcd', 'efgh')], + rowcount=2, rewritten=[ 'select * from vtocc_a where 1 != 1', 'select /* order */ * from vtocc_a order by id desc limit 10001']), Case(doc='select in select list', sql='select (select eid from vtocc_a where id = 1), eid from vtocc_a where id = 2', result=[(1L, 1L)], + rowcount=1, rewritten=[ 'select (select eid from vtocc_a where 1 != 1), eid from vtocc_a where 1 != 1', 'select (select eid from vtocc_a where id = 1), eid from vtocc_a where id = 2 limit 10001']), Case(doc='select in from clause', sql='select eid from (select eid from vtocc_a where id=2) as a', result=[(1L,)], + rowcount=1, rewritten=[ 'select eid from (select eid from vtocc_a where 1 != 1) as a where 1 != 1', 'select eid from (select eid from vtocc_a where id = 2) as a limit 10001']), @@ -205,19 +232,23 @@ ['begin', Case(sql='select * from vtocc_a where eid = 2 and id = 1', result=[], + rowcount=0, rewritten=[ "select * from vtocc_a where 1 != 1", "select * from vtocc_a where eid = 2 and id = 1 limit 10001"]), Case(sql='select * from vtocc_a where eid = 2 and id = 1', result=[], + rowcount=0, rewritten=["select * from vtocc_a where eid = 2 and id = 1 limit 10001"]), Case(sql="select :bv from vtocc_a where eid = 2 and id = 1", bindings={'bv': 1}, result=[], + rowcount=0, rewritten=["select 1 from vtocc_a where eid = 2 and id = 1 limit 10001"]), Case(sql="select :bv from vtocc_a where eid = 2 and id = 1", bindings={'bv': 'abcd'}, result=[], + rowcount=0, rewritten=["select 'abcd' from vtocc_a where eid = 2 and id = 1 limit 10001"]), 'commit']), @@ -225,7 +256,8 @@ 'simple insert', ['begin', Case(sql="insert /* simple */ into vtocc_a values (2, 1, 'aaaa', 'bbbb')", - rewritten="insert /* simple */ into vtocc_a values (2, 1, 'aaaa', 'bbbb') /* _stream vtocc_a (eid id ) (2 1 )"), + rewritten="insert /* simple */ into vtocc_a values (2, 1, 'aaaa', 'bbbb') /* _stream vtocc_a (eid id ) (2 1 )", + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid = 2 and id = 1', result=[(2L, 1L, 'aaaa', 'bbbb')]), @@ -237,7 +269,8 @@ 'qualified insert', ['begin', Case(sql="insert /* qualified */ into vtocc_a(eid, id, name, foo) values (3, 1, 'aaaa', 'cccc')", - rewritten="insert /* qualified */ into vtocc_a(eid, id, name, foo) values (3, 1, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (3 1 )"), + rewritten="insert /* qualified */ into vtocc_a(eid, id, name, foo) values (3, 1, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (3 1 )", + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid = 3 and id = 1', result=[(3L, 1L, 'aaaa', 'cccc')]), @@ -249,7 +282,8 @@ 'insert with qualified column name', ['begin', Case(sql="insert /* qualified */ into vtocc_a(vtocc_a.eid, id, name, foo) values (4, 1, 'aaaa', 'cccc')", - rewritten="insert /* qualified */ into vtocc_a(vtocc_a.eid, id, name, foo) values (4, 1, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (4 1 )"), + rewritten="insert /* qualified */ into vtocc_a(vtocc_a.eid, id, name, foo) values (4, 1, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (4 1 )", + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid = 4 and id = 1', result=[(4L, 1L, 'aaaa', 'cccc')]), @@ -262,7 +296,8 @@ ['alter table vtocc_e auto_increment = 1', 'begin', Case(sql="insert /* auto_increment */ into vtocc_e(name, foo) values ('aaaa', 'cccc')", - rewritten="insert /* auto_increment */ into vtocc_e(name, foo) values ('aaaa', 'cccc') /* _stream vtocc_e (eid id name ) (null 1 'YWFhYQ==' )"), + rewritten="insert /* auto_increment */ into vtocc_e(name, foo) values ('aaaa', 'cccc') /* _stream vtocc_e (eid id name ) (null 1 'YWFhYQ==' )", + rowcount=1), 'commit', Case(sql='select * from vtocc_e', result=[(1L, 1L, 'aaaa', 'cccc')]), @@ -274,7 +309,8 @@ 'insert with number default value', ['begin', Case(sql="insert /* num default */ into vtocc_a(eid, name, foo) values (3, 'aaaa', 'cccc')", - rewritten="insert /* num default */ into vtocc_a(eid, name, foo) values (3, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (3 1 )"), + rewritten="insert /* num default */ into vtocc_a(eid, name, foo) values (3, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (3 1 )", + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid = 3 and id = 1', result=[(3L, 1L, 'aaaa', 'cccc')]), @@ -286,7 +322,8 @@ 'insert with string default value', ['begin', Case(sql="insert /* string default */ into vtocc_f(id) values (1)", - rewritten="insert /* string default */ into vtocc_f(id) values (1) /* _stream vtocc_f (vb ) ('YWI=' )"), + rewritten="insert /* string default */ into vtocc_f(id) values (1) /* _stream vtocc_f (vb ) ('YWI=' )", + rowcount=1), 'commit', Case(sql='select * from vtocc_f', result=[('ab', 1)]), @@ -299,7 +336,8 @@ ['begin', Case(sql="insert /* bind values */ into vtocc_a(eid, id, name, foo) values (:eid, :id, :name, :foo)", bindings={"eid": 4, "id": 1, "name": "aaaa", "foo": "cccc"}, - rewritten="insert /* bind values */ into vtocc_a(eid, id, name, foo) values (4, 1, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (4 1 )"), + rewritten="insert /* bind values */ into vtocc_a(eid, id, name, foo) values (4, 1, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (4 1 )", + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid = 4 and id = 1', result=[(4L, 1L, 'aaaa', 'cccc')]), @@ -312,7 +350,8 @@ ['begin', Case(sql="insert /* positional values */ into vtocc_a(eid, id, name, foo) values (?, ?, ?, ?)", bindings={"v1": 4, "v2": 1, "v3": "aaaa", "v4": "cccc"}, - rewritten="insert /* positional values */ into vtocc_a(eid, id, name, foo) values (4, 1, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (4 1 )"), + rewritten="insert /* positional values */ into vtocc_a(eid, id, name, foo) values (4, 1, 'aaaa', 'cccc') /* _stream vtocc_a (eid id ) (4 1 )", + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid = 4 and id = 1', result=[(4L, 1L, 'aaaa', 'cccc')]), @@ -324,7 +363,8 @@ 'out of sequence columns', ['begin', Case(sql="insert into vtocc_a(id, eid, foo, name) values (-1, 5, 'aaa', 'bbb')", - rewritten="insert into vtocc_a(id, eid, foo, name) values (-1, 5, 'aaa', 'bbb') /* _stream vtocc_a (eid id ) (5 -1 )"), + rewritten="insert into vtocc_a(id, eid, foo, name) values (-1, 5, 'aaa', 'bbb') /* _stream vtocc_a (eid id ) (5 -1 )", + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid = 5 and id = -1', result=[(5L, -1L, 'bbb', 'aaa')]), @@ -338,8 +378,8 @@ Case(sql="insert /* subquery */ into vtocc_a(eid, name, foo) select eid, name, foo from vtocc_c", rewritten =[ 'select eid, name, foo from vtocc_c limit 10001', - "insert /* subquery */ into vtocc_a(eid, name, foo) values (10, 'abcd', '20'), (11, 'bcde', '30') /* _stream vtocc_a (eid id ) (10 1 ) (11 1 )", - ]), + "insert /* subquery */ into vtocc_a(eid, name, foo) values (10, 'abcd', '20'), (11, 'bcde', '30') /* _stream vtocc_a (eid id ) (10 1 ) (11 1 )"], + rowcount=2), 'commit', Case(sql='select * from vtocc_a where eid in (10, 11)', result=[(10L, 1L, 'abcd', '20'), (11L, 1L, 'bcde', '30')]), @@ -348,8 +388,8 @@ Case(sql='insert into vtocc_e(id, name, foo) select eid, name, foo from vtocc_c', rewritten=[ 'select eid, name, foo from vtocc_c limit 10001', - "insert into vtocc_e(id, name, foo) values (10, 'abcd', '20'), (11, 'bcde', '30') /* _stream vtocc_e (eid id name ) (null 10 'YWJjZA==' ) (null 11 'YmNkZQ==' )", - ]), + "insert into vtocc_e(id, name, foo) values (10, 'abcd', '20'), (11, 'bcde', '30') /* _stream vtocc_e (eid id name ) (null 10 'YWJjZA==' ) (null 11 'YmNkZQ==' )"], + rowcount=2), 'commit', Case(sql='select eid, id, name, foo from vtocc_e', result=[(20L, 10L, 'abcd', '20'), (21L, 11L, 'bcde', '30')]), @@ -362,7 +402,8 @@ 'multi-value', ['begin', Case(sql="insert into vtocc_a(eid, id, name, foo) values (5, 1, '', ''), (7, 1, '', '')", - rewritten="insert into vtocc_a(eid, id, name, foo) values (5, 1, '', ''), (7, 1, '', '') /* _stream vtocc_a (eid id ) (5 1 ) (7 1 )"), + rewritten="insert into vtocc_a(eid, id, name, foo) values (5, 1, '', ''), (7, 1, '', '') /* _stream vtocc_a (eid id ) (5 1 ) (7 1 )", + rowcount=2), 'commit', Case(sql='select * from vtocc_a where eid>1', result=[(5L, 1L, '', ''), (7L, 1L, '', '')]), @@ -374,7 +415,8 @@ 'update', ['begin', Case(sql="update /* pk */ vtocc_a set foo='bar' where eid = 1 and id = 1", - rewritten="update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 )"), + rewritten="update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 )", + rowcount=1), 'commit', Case(sql='select foo from vtocc_a where id = 1', result=[('bar',)]), @@ -384,10 +426,11 @@ MultiCase( - 'single in', + 'single in update', ['begin', Case(sql="update /* pk */ vtocc_a set foo='bar' where eid = 1 and id in (1, 2)", - rewritten="update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) or (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 1 ) (1 2 )"), + rewritten="update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) or (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 1 ) (1 2 )", + rowcount=2), 'commit', Case(sql='select foo from vtocc_a where id = 1', result=[('bar',)]), @@ -397,12 +440,13 @@ 'commit']), MultiCase( - 'double in', + 'double in update', ['begin', Case(sql="update /* pk */ vtocc_a set foo='bar' where eid in (1) and id in (1, 2)", rewritten=[ 'select eid, id from vtocc_a where eid in (1) and id in (1, 2) limit 10001 for update', - "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) or (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 1 ) (1 2 )",]), + "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) or (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 1 ) (1 2 )",], + rowcount=2), 'commit', Case(sql='select foo from vtocc_a where id = 1', result=[('bar',)]), @@ -412,12 +456,13 @@ 'commit']), MultiCase( - 'double in 2', + 'double in 2 update', ['begin', Case(sql="update /* pk */ vtocc_a set foo='bar' where eid in (1, 2) and id in (1, 2)", rewritten=[ 'select eid, id from vtocc_a where eid in (1, 2) and id in (1, 2) limit 10001 for update', - "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) or (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 1 ) (1 2 )"]), + "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) or (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 1 ) (1 2 )"], + rowcount=2), 'commit', Case(sql='select foo from vtocc_a where id = 1', result=[('bar',)]), @@ -427,10 +472,11 @@ 'commit']), MultiCase( - 'pk change', + 'pk change update', ['begin', Case(sql="update vtocc_a set eid = 2 where eid = 1 and id = 1", - rewritten="update vtocc_a set eid = 2 where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 ) (2 1 )"), + rewritten="update vtocc_a set eid = 2 where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 ) (2 1 )", + rowcount=1), 'commit', Case(sql='select eid from vtocc_a where id = 1', result=[(2L,)]), @@ -439,10 +485,11 @@ 'commit']), MultiCase( - 'pk change with qualifed column name', + 'pk change with qualifed column name update', ['begin', Case(sql="update vtocc_a set vtocc_a.eid = 2 where eid = 1 and id = 1", - rewritten="update vtocc_a set vtocc_a.eid = 2 where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 ) (2 1 )"), + rewritten="update vtocc_a set vtocc_a.eid = 2 where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 ) (2 1 )", + rowcount=1), 'commit', Case(sql='select eid from vtocc_a where id = 1', result=[(2L,)]), @@ -451,12 +498,13 @@ 'commit']), MultiCase( - 'partial pk', + 'partial pk update', ['begin', Case(sql="update /* pk */ vtocc_a set foo='bar' where id = 1", rewritten=[ "select eid, id from vtocc_a where id = 1 limit 10001 for update", - "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 )"]), + "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 )"], + rowcount=1), 'commit', Case(sql='select foo from vtocc_a where id = 1', result=[('bar',)]), @@ -465,12 +513,13 @@ 'commit']), MultiCase( - 'limit', + 'limit update', ['begin', Case(sql="update /* pk */ vtocc_a set foo='bar' where eid = 1 limit 1", rewritten=[ "select eid, id from vtocc_a where eid = 1 limit 1 for update", - "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 )"]), + "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 1) /* _stream vtocc_a (eid id ) (1 1 )"], + rowcount=1), 'commit', Case(sql='select foo from vtocc_a where id = 1', result=[('bar',)]), @@ -479,12 +528,13 @@ 'commit']), MultiCase( - 'order by', + 'order by update', ['begin', Case(sql="update /* pk */ vtocc_a set foo='bar' where eid = 1 order by id desc limit 1", rewritten=[ "select eid, id from vtocc_a where eid = 1 order by id desc limit 1 for update", - "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 2 )"]), + "update /* pk */ vtocc_a set foo = 'bar' where (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 2 )"], + rowcount=1), 'commit', Case(sql='select foo from vtocc_a where id = 2', result=[('bar',)]), @@ -493,12 +543,13 @@ 'commit']), MultiCase( - 'missing where', + 'missing where update', ['begin', Case(sql="update vtocc_a set foo='bar'", rewritten=[ "select eid, id from vtocc_a limit 10001 for update", - "update vtocc_a set foo = 'bar' where (eid = 1 and id = 1) or (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 1 ) (1 2 )"]), + "update vtocc_a set foo = 'bar' where (eid = 1 and id = 1) or (eid = 1 and id = 2) /* _stream vtocc_a (eid id ) (1 1 ) (1 2 )"], + rowcount=2), 'commit', Case(sql='select * from vtocc_a', result=[(1L, 1L, 'abcd', 'bar'), (1L, 2L, 'bcde', 'bar')]), @@ -508,14 +559,15 @@ 'commit']), MultiCase( - 'single pk update one row', + 'single pk update one row update', ['begin', "insert into vtocc_f(vb,id) values ('a', 1), ('b', 2)", 'commit', 'begin', Case(sql="update vtocc_f set id=2 where vb='a'", rewritten=[ - "update vtocc_f set id = 2 where vb in ('a') /* _stream vtocc_f (vb ) ('YQ==' )"]), + "update vtocc_f set id = 2 where vb in ('a') /* _stream vtocc_f (vb ) ('YQ==' )"], + rowcount=1), 'commit', Case(sql='select * from vtocc_f', result=[('a', 2L), ('b', 2L)]), @@ -531,7 +583,8 @@ 'begin', Case(sql="update vtocc_f set id=3 where vb in ('a', 'b')", rewritten=[ - "update vtocc_f set id = 3 where vb in ('a', 'b') /* _stream vtocc_f (vb ) ('YQ==' ) ('Yg==' )"]), + "update vtocc_f set id = 3 where vb in ('a', 'b') /* _stream vtocc_f (vb ) ('YQ==' ) ('Yg==' )"], + rowcount=2), 'commit', Case(sql='select * from vtocc_f', result=[('a', 3L), ('b', 3L)]), @@ -548,7 +601,8 @@ Case(sql="update vtocc_f set id=4 where id >= 0", rewritten=[ "select vb from vtocc_f where id >= 0 limit 10001 for update", - "update vtocc_f set id = 4 where vb in ('a', 'b') /* _stream vtocc_f (vb ) ('YQ==' ) ('Yg==' )"]), + "update vtocc_f set id = 4 where vb in ('a', 'b') /* _stream vtocc_f (vb ) ('YQ==' ) ('Yg==' )"], + rowcount=2), 'commit', Case(sql='select * from vtocc_f', result=[('a', 4L), ('b', 4L)]), @@ -577,77 +631,84 @@ ['begin', "insert into vtocc_a(eid, id, name, foo) values (2, 1, '', '')", Case(sql="delete /* pk */ from vtocc_a where eid = 2 and id = 1", - rewritten="delete /* pk */ from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )"), + rewritten="delete /* pk */ from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )", + rowcount=1), 'commit', Case('select * from vtocc_a where eid=2', result=[])]), MultiCase( - 'single in', + 'single in delete', ['begin', "insert into vtocc_a(eid, id, name, foo) values (2, 1, '', '')", Case(sql="delete /* pk */ from vtocc_a where eid = 2 and id in (1, 2)", - rewritten="delete /* pk */ from vtocc_a where (eid = 2 and id = 1) or (eid = 2 and id = 2) /* _stream vtocc_a (eid id ) (2 1 ) (2 2 )"), + rewritten="delete /* pk */ from vtocc_a where (eid = 2 and id = 1) or (eid = 2 and id = 2) /* _stream vtocc_a (eid id ) (2 1 ) (2 2 )", + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid=2', result=[])]), MultiCase( - 'double in', + 'double in delete', ['begin', "insert into vtocc_a(eid, id, name, foo) values (2, 1, '', '')", Case(sql="delete /* pk */ from vtocc_a where eid in (2) and id in (1, 2)", rewritten=[ 'select eid, id from vtocc_a where eid in (2) and id in (1, 2) limit 10001 for update', - 'delete /* pk */ from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )',]), + 'delete /* pk */ from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )',], + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid=2', result=[])]), MultiCase( - 'double in 2', + 'double in 2 delete', ['begin', "insert into vtocc_a(eid, id, name, foo) values (2, 1, '', '')", Case(sql="delete /* pk */ from vtocc_a where eid in (2, 3) and id in (1, 2)", rewritten=[ 'select eid, id from vtocc_a where eid in (2, 3) and id in (1, 2) limit 10001 for update', - 'delete /* pk */ from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )']), + 'delete /* pk */ from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )'], + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid=2', result=[])]), MultiCase( - 'complex where', + 'complex where delete', ['begin', "insert into vtocc_a(eid, id, name, foo) values (2, 1, '', '')", Case(sql="delete from vtocc_a where eid = 1+1 and id = 1", rewritten=[ 'select eid, id from vtocc_a where eid = 1+1 and id = 1 limit 10001 for update', - "delete from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )"]), + "delete from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )"], + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid=2', result=[])]), MultiCase( - 'partial pk', + 'partial pk delete', ['begin', "insert into vtocc_a(eid, id, name, foo) values (2, 1, '', '')", Case(sql="delete from vtocc_a where eid = 2", rewritten=[ 'select eid, id from vtocc_a where eid = 2 limit 10001 for update', - "delete from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )"]), + "delete from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )"], + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid=2', result=[])]), MultiCase( - 'limit', + 'limit delete', ['begin', "insert into vtocc_a(eid, id, name, foo) values (2, 1, '', '')", Case(sql="delete from vtocc_a where eid = 2 limit 1", rewritten=[ 'select eid, id from vtocc_a where eid = 2 limit 1 for update', - "delete from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )"]), + "delete from vtocc_a where (eid = 2 and id = 1) /* _stream vtocc_a (eid id ) (2 1 )"], + rowcount=1), 'commit', Case(sql='select * from vtocc_a where eid=2', result=[])]), diff --git a/test/queryservice_tests/nocache_tests.py b/test/queryservice_tests/nocache_tests.py index 00bef24d070..9c705d7456b 100644 --- a/test/queryservice_tests/nocache_tests.py +++ b/test/queryservice_tests/nocache_tests.py @@ -7,6 +7,8 @@ import framework import nocache_cases +import environment +import utils class TestNocache(framework.TestCase): def test_data(self): @@ -85,7 +87,7 @@ def test_integrity_error(self): try: self.env.execute("insert into vtocc_test values(1, null, null, null)") except dbexceptions.IntegrityError as e: - self.assertContains(str(e), "error: Duplicate") + self.assertContains(str(e), "error: duplicate") else: self.fail("Did not receive exception") finally: @@ -458,8 +460,27 @@ def test_customrules(self): bv = {'asdfg': 1} try: self.env.execute("select * from vtocc_test where intval=:asdfg", bv) + self.fail("Bindvar asdfg should not be allowed by custom rule") except dbexceptions.DatabaseError as e: self.assertContains(str(e), "error: Query disallowed") + # Test dynamic custom rule for vttablet + if self.env.env == "vttablet": + if environment.topo_server().flavor() == 'zookeeper': + # Make a change to the rule + self.env.change_customrules() + time.sleep(3) + try: + self.env.execute("select * from vtocc_test where intval=:asdfg", bv) + except dbexceptions.DatabaseError as e: + self.fail("Bindvar asdfg should be allowed after a change of custom rule, Err=" + str(e)) + # Restore the rule + self.env.restore_customrules() + time.sleep(3) + try: + self.env.execute("select * from vtocc_test where intval=:asdfg", bv) + self.fail("Bindvar asdfg should not be allowed by custom rule") + except dbexceptions.DatabaseError as e: + self.assertContains(str(e), "error: Query disallowed") def test_health(self): self.assertEqual(self.env.health(), "ok") @@ -610,3 +631,33 @@ def test_table_acl_all_user_read_only(self): cu.execute("select * from vtocc_acl_all_user_read_only where key1=1", {}) cu.fetchall() cu.close() + + # This is a super-slow test. Uncomment and test if you change + # the server-side reconnect logic. + #def test_server_reconnect(self): + # self.env.execute("set vt_pool_size=1") + # self.env.execute("select * from vtocc_test limit :l", {"l": 1}) + # self.env.tablet.shutdown_mysql() + # time.sleep(5) + # self.env.tablet.start_mysql() + # time.sleep(5) + # self.env.execute("select * from vtocc_test limit :l", {"l": 1}) + # self.env.conn.begin() + # self.env.tablet.shutdown_mysql() + # time.sleep(5) + # self.env.tablet.start_mysql() + # time.sleep(5) + # with self.assertRaisesRegexp(dbexceptions.DatabaseError, ".*server has gone away.*"): + # self.env.execute("select * from vtocc_test limit :l", {"l": 1}) + # self.env.conn.rollback() + # self.env.execute("set vt_pool_size=16") + + # Super-slow test. + #def test_mysql_shutdown(self): + # self.env.execute("select * from vtocc_test limit :l", {"l": 1}) + # self.env.tablet.shutdown_mysql() + # time.sleep(5) + # with self.assertRaisesRegexp(dbexceptions.DatabaseError, '.*NOT_SERVING state.*'): + # self.env.execute("select * from vtocc_test limit :l", {"l": 1}) + # self.env.tablet.start_mysql() + # time.sleep(5) diff --git a/test/queryservice_tests/stream_tests.py b/test/queryservice_tests/stream_tests.py index ae58c7cf87d..b8266804da0 100644 --- a/test/queryservice_tests/stream_tests.py +++ b/test/queryservice_tests/stream_tests.py @@ -7,6 +7,7 @@ from vtdb import cursor from vtdb import dbexceptions +import environment import framework class TestStream(framework.TestCase): @@ -33,8 +34,28 @@ def test_customrules(self): try: self.env.execute("select * from vtocc_test where intval=:asdfg", bv, cursorclass=cursor.StreamCursor) + self.fail("Bindvar asdfg should not be allowed by custom rule") except dbexceptions.DatabaseError as e: self.assertContains(str(e), "error: Query disallowed") + # Test dynamic custom rule for vttablet + if self.env.env == "vttablet": + if environment.topo_server().flavor() == 'zookeeper': + # Make a change to the rule + self.env.change_customrules() + time.sleep(3) + try: + self.env.execute("select * from vtocc_test where intval=:asdfg", bv, + cursorclass=cursor.StreamCursor) + except dbexceptions.DatabaseError as e: + self.fail("Bindvar asdfg should be allowed after a change of custom rule, Err=" + str(e)) + self.env.restore_customrules() + time.sleep(3) + try: + self.env.execute("select * from vtocc_test where intval=:asdfg", bv, + cursorclass=cursor.StreamCursor) + self.fail("Bindvar asdfg should not be allowed by custom rule") + except dbexceptions.DatabaseError as e: + self.assertContains(str(e), "error: Query disallowed") def test_basic_stream(self): self._populate_vtocc_big_table(100) diff --git a/test/queryservice_tests/test_env.py b/test/queryservice_tests/test_env.py index 1cfa92239a1..5ffa284f85e 100644 --- a/test/queryservice_tests/test_env.py +++ b/test/queryservice_tests/test_env.py @@ -109,6 +109,33 @@ def create_customrules(self, filename): "Operator": "NOOP" }] }]""") + if self.env == "vttablet": + if environment.topo_server().flavor() == 'zookeeper': + utils.run(environment.binary_argstr('zk') + ' touch -p /zk/test_ca/config/customrules/testrules') + utils.run(environment.binary_argstr('zk') + ' cp ' + filename + ' /zk/test_ca/config/customrules/testrules') + + def change_customrules(self): + customrules = os.path.join(environment.tmproot, 'customrules.json') + with open(customrules, "w") as f: + f.write("""[{ + "Name": "r2", + "Description": "disallow bindvar 'gfdsa'", + "BindVarConds":[{ + "Name": "gfdsa", + "OnAbsent": false, + "Operator": "NOOP" + }] + }]""") + if self.env == "vttablet": + if environment.topo_server().flavor() == 'zookeeper': + utils.run(environment.binary_argstr('zk') + ' cp ' + customrules + ' /zk/test_ca/config/customrules/testrules') + + def restore_customrules(self): + customrules = os.path.join(environment.tmproot, 'customrules.json') + self.create_customrules(customrules) + if self.env == "vttablet": + if environment.topo_server().flavor() == 'zookeeper': + utils.run(environment.binary_argstr('zk') + ' cp ' + customrules + ' /zk/test_ca/config/customrules/testrules') def create_schema_override(self, filename): with open(filename, "w") as f: @@ -180,26 +207,36 @@ def setUp(self): mcu.close() customrules = os.path.join(environment.tmproot, 'customrules.json') - self.create_customrules(customrules) schema_override = os.path.join(environment.tmproot, 'schema_override.json') self.create_schema_override(schema_override) table_acl_config = os.path.join(environment.vttop, 'test', 'test_data', 'table_acl_config.json') if self.env == 'vttablet': environment.topo_server().setup() + self.create_customrules(customrules); utils.run_vtctl('CreateKeyspace -force test_keyspace') self.tablet.init_tablet('master', 'test_keyspace', '0') - self.tablet.start_vttablet( - memcache=self.memcache, - customrules=customrules, - schema_override=schema_override, - table_acl_config=table_acl_config, - auth=True, - ) + if environment.topo_server().flavor() == 'zookeeper': + self.tablet.start_vttablet( + memcache=self.memcache, + zkcustomrules='/zk/test_ca/config/customrules/testrules', + schema_override=schema_override, + table_acl_config=table_acl_config, + auth=True, + ) + else: + self.tablet.start_vttablet( + memcache=self.memcache, + filecustomrules=customrules, + schema_override=schema_override, + table_acl_config=table_acl_config, + auth=True, + ) else: + self.create_customrules(customrules); self.tablet.start_vtocc( memcache=self.memcache, - customrules=customrules, + filecustomrules=customrules, schema_override=schema_override, table_acl_config=table_acl_config, auth=True, diff --git a/test/reparent.py b/test/reparent.py index 0919511db44..2e15248f7b7 100755 --- a/test/reparent.py +++ b/test/reparent.py @@ -80,7 +80,7 @@ def _check_db_addr(self, shard, db_type, expected_port, cell='test_nj'): db_type]) self.assertEqual( len(ep['entries']), 1, 'Wrong number of entries: %s' % str(ep)) - port = ep['entries'][0]['named_port_map']['_vtocc'] + port = ep['entries'][0]['named_port_map']['vt'] self.assertEqual(port, expected_port, 'Unexpected port: %u != %u from %s' % (port, expected_port, str(ep))) @@ -171,9 +171,6 @@ def test_reparent_down_master(self): # Force the scrap action in zk even though tablet is not accessible. tablet_62344.scrap(force=True) - utils.run_vtctl(['ChangeSlaveType', '-force', tablet_62344.tablet_alias, - 'idle'], expect_fail=True) - # Re-run reparent operation, this should now proceed unimpeded. utils.run_vtctl(['ReparentShard', 'test_keyspace/0', tablet_62044.tablet_alias], auto_log=True) @@ -426,25 +423,24 @@ def _test_reparent_slave_offline(self, shard_id='0'): def test_reparent_from_outside(self): self._test_reparent_from_outside(brutal=False) + def test_reparent_from_outside_fast(self): + self._test_reparent_from_outside(brutal=False, fast=True) + def test_reparent_from_outside_brutal(self): self._test_reparent_from_outside(brutal=True) - def test_reparent_from_outside_rpc(self): - self._test_reparent_from_outside(brutal=False, rpc=True) + def test_reparent_from_outside_brutal_fast(self): + self._test_reparent_from_outside(brutal=True, fast=True) - def test_reparent_from_outside_brutal_rpc(self): - self._test_reparent_from_outside(brutal=True, rpc=True) - - def _test_reparent_from_outside(self, brutal=False, rpc=False): + def _test_reparent_from_outside(self, brutal=False, fast=False): """This test will start a master and 3 slaves. Then: - one slave will be the new master - one slave will be reparented to that new master - one slave will be busted and ded in the water - and we'll call ShardExternallyReparented. + and we'll call TabletExternallyReparented. Args: brutal: scraps the old master first - rpc: sends an RPC to the new master instead of doing the work. """ utils.run_vtctl(['CreateKeyspace', 'test_keyspace']) @@ -452,17 +448,21 @@ def _test_reparent_from_outside(self, brutal=False, rpc=False): for t in [tablet_62344, tablet_62044, tablet_41983, tablet_31981]: t.create_db('vt_test_keyspace') + extra_args = None + if fast: + extra_args = ['-fast_external_reparent'] + # Start up a master mysql and vttablet tablet_62344.init_tablet('master', 'test_keyspace', '0', start=True, - wait_for_start=False) + wait_for_start=False, extra_args=extra_args) # Create a few slaves for testing reparenting. tablet_62044.init_tablet('replica', 'test_keyspace', '0', start=True, - wait_for_start=False) + wait_for_start=False, extra_args=extra_args) tablet_41983.init_tablet('replica', 'test_keyspace', '0', start=True, - wait_for_start=False) + wait_for_start=False, extra_args=extra_args) tablet_31981.init_tablet('replica', 'test_keyspace', '0', start=True, - wait_for_start=False) + wait_for_start=False, extra_args=extra_args) # wait for all tablets to start for t in [tablet_62344, tablet_62044, tablet_41983, tablet_31981]: @@ -504,11 +504,7 @@ def _test_reparent_from_outside(self, brutal=False, rpc=False): tablet_62344.zk_tablet_path]) # update zk with the new graph - extra_args = [] - if rpc: - extra_args = ['-use_rpc'] - utils.run_vtctl(['ShardExternallyReparented'] + extra_args + - ['test_keyspace/0', tablet_62044.tablet_alias], + utils.run_vtctl(['TabletExternallyReparented', tablet_62044.tablet_alias], mode=utils.VTCTL_VTCTL, auto_log=True) self._test_reparent_from_outside_check(brutal) @@ -523,21 +519,21 @@ def _test_reparent_from_outside(self, brutal=False, rpc=False): def _test_reparent_from_outside_check(self, brutal): if environment.topo_server().flavor() != 'zookeeper': return + # make sure the shard replication graph is fine shard_replication = utils.run_vtctl_json(['GetShardReplication', 'test_nj', 'test_keyspace/0']) hashed_links = {} for rl in shard_replication['ReplicationLinks']: key = rl['TabletAlias']['Cell'] + '-' + str(rl['TabletAlias']['Uid']) - value = rl['Parent']['Cell'] + '-' + str(rl['Parent']['Uid']) - hashed_links[key] = value + hashed_links[key] = True logging.debug('Got replication links: %s', str(hashed_links)) expected_links = { - 'test_nj-41983': 'test_nj-62044', - 'test_nj-62044': '-0', + 'test_nj-41983': True, + 'test_nj-62044': True, } if not brutal: - expected_links['test_nj-62344'] = 'test_nj-62044' + expected_links['test_nj-62344'] = True self.assertEqual(expected_links, hashed_links, 'Got unexpected links: %s != %s' % (str(expected_links), str(hashed_links))) diff --git a/test/resharding.py b/test/resharding.py index f46054a0a2b..c9258bb5b59 100755 --- a/test/resharding.py +++ b/test/resharding.py @@ -19,8 +19,6 @@ import utils import tablet -use_clone_worker = False - keyspace_id_type = keyrange_constants.KIT_UINT64 pack_keyspace_id = struct.Struct('!Q').pack @@ -34,7 +32,7 @@ shard_1_slave1 = tablet.Tablet() shard_1_slave2 = tablet.Tablet() shard_1_ny_rdonly = tablet.Tablet(cell='ny') -shard_1_rdonly = tablet.Tablet() +shard_1_rdonly1 = tablet.Tablet() # split shards # range 80 - c0 @@ -44,7 +42,7 @@ # range c0 - "" shard_3_master = tablet.Tablet() shard_3_replica = tablet.Tablet() -shard_3_rdonly = tablet.Tablet() +shard_3_rdonly1 = tablet.Tablet() def setUpModule(): @@ -59,13 +57,13 @@ def setUpModule(): shard_1_slave1.init_mysql(), shard_1_slave2.init_mysql(), shard_1_ny_rdonly.init_mysql(), - shard_1_rdonly.init_mysql(), + shard_1_rdonly1.init_mysql(), shard_2_master.init_mysql(), shard_2_replica1.init_mysql(), shard_2_replica2.init_mysql(), shard_3_master.init_mysql(), shard_3_replica.init_mysql(), - shard_3_rdonly.init_mysql(), + shard_3_rdonly1.init_mysql(), ] utils.Vtctld().start() utils.wait_procs(setup_procs) @@ -86,13 +84,13 @@ def tearDownModule(): shard_1_slave1.teardown_mysql(), shard_1_slave2.teardown_mysql(), shard_1_ny_rdonly.teardown_mysql(), - shard_1_rdonly.teardown_mysql(), + shard_1_rdonly1.teardown_mysql(), shard_2_master.teardown_mysql(), shard_2_replica1.teardown_mysql(), shard_2_replica2.teardown_mysql(), shard_3_master.teardown_mysql(), shard_3_replica.teardown_mysql(), - shard_3_rdonly.teardown_mysql(), + shard_3_rdonly1.teardown_mysql(), ] utils.wait_procs(teardown_procs, raise_on_error=False) @@ -107,13 +105,13 @@ def tearDownModule(): shard_1_slave1.remove_tree() shard_1_slave2.remove_tree() shard_1_ny_rdonly.remove_tree() - shard_1_rdonly.remove_tree() + shard_1_rdonly1.remove_tree() shard_2_master.remove_tree() shard_2_replica1.remove_tree() shard_2_replica2.remove_tree() shard_3_master.remove_tree() shard_3_replica.remove_tree() - shard_3_rdonly.remove_tree() + shard_3_rdonly1.remove_tree() # InsertThread will insert a value into the timestamps table, and then @@ -322,7 +320,7 @@ def _check_startup_values(self): 0x9000000000000000, should_be_here=False) self._check_value(shard_3_replica, 'resharding1', 2, 'msg2', 0x9000000000000000, should_be_here=False) - self._check_value(shard_3_rdonly, 'resharding1', 2, 'msg2', + self._check_value(shard_3_rdonly1, 'resharding1', 2, 'msg2', 0x9000000000000000, should_be_here=False) # check second value is in the right shard too @@ -336,7 +334,7 @@ def _check_startup_values(self): 0xD000000000000000) self._check_value(shard_3_replica, 'resharding1', 3, 'msg3', 0xD000000000000000) - self._check_value(shard_3_rdonly, 'resharding1', 3, 'msg3', + self._check_value(shard_3_rdonly1, 'resharding1', 3, 'msg3', 0xD000000000000000) def _insert_lots(self, count, base=0): @@ -431,38 +429,6 @@ def _test_keyrange_constraints(self): {"keyspace_id": 0x9000000000000000}, ) - def _check_query_service(self, tablet, serving, tablet_control_disabled): - """_check_query_service will check that the query service is enabled - or disabled on the tablet. It will also check if the tablet control - status is the reason for being enabled / disabled. - - It will also run a remote RunHealthCheck to be sure it doesn't change - the serving state. - """ - tablet_vars = utils.get_vars(tablet.port) - if serving: - expected_state = 'SERVING' - else: - expected_state = 'NOT_SERVING' - self.assertEqual(tablet_vars['TabletStateName'], expected_state, 'tablet %s is not in the right serving state: got %s expected %s' % (tablet.tablet_alias, tablet_vars['TabletStateName'], expected_state)) - - status = tablet.get_status() - if tablet_control_disabled: - self.assertIn("Query Service disabled by TabletControl", status) - else: - self.assertNotIn("Query Service disabled by TabletControl", status) - - if tablet.tablet_type == 'rdonly': - utils.run_vtctl(['RunHealthCheck', tablet.tablet_alias, 'rdonly'], - auto_log=True) - - tablet_vars = utils.get_vars(tablet.port) - if serving: - expected_state = 'SERVING' - else: - expected_state = 'NOT_SERVING' - self.assertEqual(tablet_vars['TabletStateName'], expected_state, 'tablet %s is not in the right serving state after health check: got %s expected %s' % (tablet.tablet_alias, tablet_vars['TabletStateName'], expected_state)) - def test_resharding(self): utils.run_vtctl(['CreateKeyspace', '--sharding_column_name', 'bad_column', @@ -482,7 +448,7 @@ def test_resharding(self): shard_1_slave1.init_tablet('replica', 'test_keyspace', '80-') shard_1_slave2.init_tablet('spare', 'test_keyspace', '80-') shard_1_ny_rdonly.init_tablet('rdonly', 'test_keyspace', '80-') - shard_1_rdonly.init_tablet('rdonly', 'test_keyspace', '80-') + shard_1_rdonly1.init_tablet('rdonly', 'test_keyspace', '80-') utils.run_vtctl(['RebuildKeyspaceGraph', 'test_keyspace'], auto_log=True) @@ -493,8 +459,9 @@ def test_resharding(self): full_mycnf_args = keyspace_id_type == keyrange_constants.KIT_BYTES # create databases so vttablet can start behaving normally - for t in [shard_0_master, shard_0_replica, shard_0_ny_rdonly, shard_1_master, - shard_1_slave1, shard_1_slave2, shard_1_ny_rdonly, shard_1_rdonly]: + for t in [shard_0_master, shard_0_replica, shard_0_ny_rdonly, + shard_1_master, shard_1_slave1, shard_1_slave2, shard_1_ny_rdonly, + shard_1_rdonly1]: t.create_db('vt_test_keyspace') t.start_vttablet(wait_for_state=None, full_mycnf_args=full_mycnf_args) @@ -506,7 +473,7 @@ def test_resharding(self): shard_1_slave1.wait_for_vttablet_state('SERVING') shard_1_slave2.wait_for_vttablet_state('NOT_SERVING') # spare shard_1_ny_rdonly.wait_for_vttablet_state('SERVING') - shard_1_rdonly.wait_for_vttablet_state('SERVING') + shard_1_rdonly1.wait_for_vttablet_state('SERVING') # reparent to make the tablets work utils.run_vtctl(['ReparentShard', '-force', 'test_keyspace/-80', @@ -525,17 +492,17 @@ def test_resharding(self): shard_2_replica2.init_tablet('spare', 'test_keyspace', '80-c0') shard_3_master.init_tablet( 'master', 'test_keyspace', 'c0-') shard_3_replica.init_tablet( 'spare', 'test_keyspace', 'c0-') - shard_3_rdonly.init_tablet( 'rdonly', 'test_keyspace', 'c0-') + shard_3_rdonly1.init_tablet( 'rdonly', 'test_keyspace', 'c0-') # start vttablet on the split shards (no db created, # so they're all not serving) shard_3_master.start_vttablet(wait_for_state=None, target_tablet_type='replica') for t in [shard_2_master, shard_2_replica1, shard_2_replica2, - shard_3_replica, shard_3_rdonly]: + shard_3_replica, shard_3_rdonly1]: t.start_vttablet(wait_for_state=None) for t in [shard_2_master, shard_2_replica1, shard_2_replica2, - shard_3_master, shard_3_replica, shard_3_rdonly]: + shard_3_master, shard_3_replica, shard_3_rdonly1]: t.wait_for_vttablet_state('NOT_SERVING') utils.run_vtctl(['ReparentShard', '-force', 'test_keyspace/80-c0', @@ -552,77 +519,29 @@ def test_resharding(self): 'TabletTypes: master,rdonly,replica', keyspace_id_type=keyspace_id_type) - if use_clone_worker: - # the worker will do everything. We test with source_reader_count=10 - # (down from default=20) as connection pool is not big enough for 20. - # min_table_size_for_split is set to 1 as to force a split even on the - # small table we have. - utils.run_vtworker(['--cell', 'test_nj', - '--command_display_interval', '10ms', - 'SplitClone', - '--exclude_tables' ,'unrelated', - '--strategy=-populate_blp_checkpoint -write_masters_only', - '--source_reader_count', '10', - '--min_table_size_for_split', '1', - 'test_keyspace/80-c0'], - auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_1_rdonly.tablet_alias, 'rdonly'], + # the worker will do everything. We test with source_reader_count=10 + # (down from default=20) as connection pool is not big enough for 20. + # min_table_size_for_split is set to 1 as to force a split even on the + # small table we have. + # we need to create the schema, and the worker will do data copying + for keyspace_shard in ('test_keyspace/80-c0', 'test_keyspace/c0-'): + utils.run_vtctl(['CopySchemaShard', '--exclude_tables', 'unrelated', + shard_1_rdonly1.tablet_alias, keyspace_shard], auto_log=True) - # TODO(alainjobart): experiment with the dontStartBinlogPlayer option - - else: - # take the snapshot for the split - utils.run_vtctl(['MultiSnapshot', '--spec=80-c0-', - '--exclude_tables=unrelated', - shard_1_slave1.tablet_alias], auto_log=True) - - # the snapshot_copy hook will copy the snapshot files to - # VTDATAROOT/tmp/... as a test. We want to use these for one half, - # but not for the other, so we test both scenarios. - os.unlink(os.path.join(environment.tmproot, "snapshot-from-%s-for-%s.tar" % - (shard_1_slave1.tablet_alias, "80-c0"))) - - # wait for tablet's binlog server service to be enabled after snapshot - shard_1_slave1.wait_for_binlog_server_state("Enabled") - - # perform the restores: first one from source tablet. We removed the - # storage backup, so it's coming from the tablet itself. - # we also delay starting the binlog player, then enable it. - utils.run_vtctl(['ShardMultiRestore', - '-strategy=-populate_blp_checkpoint -dont_start_binlog_player', - 'test_keyspace/80-c0', shard_1_slave1.tablet_alias], - auto_log=True) + utils.run_vtworker(['--cell', 'test_nj', + '--command_display_interval', '10ms', + 'SplitClone', + '--exclude_tables' ,'unrelated', + '--strategy=-populate_blp_checkpoint', + '--source_reader_count', '10', + '--min_table_size_for_split', '1', + 'test_keyspace/80-'], + auto_log=True) + utils.run_vtctl(['ChangeSlaveType', shard_1_rdonly1.tablet_alias, + 'rdonly'], auto_log=True) - timeout = 10 - while True: - shard_2_master_status = shard_2_master.get_status() - if not "not starting because flag 'DontStart' is set" in shard_2_master_status: - timeout = utils.wait_step('shard 2 master has not failed starting yet', timeout) - continue - logging.debug("shard 2 master is waiting on flag removal, good") - break - - qr = utils.run_vtctl_json(['ExecuteFetch', shard_2_master.tablet_alias, 'update _vt.blp_checkpoint set flags="" where source_shard_uid=0']) - self.assertEqual(qr['RowsAffected'], 1) - - timeout = 10 - while True: - shard_2_master_status = shard_2_master.get_status() - if "not starting because flag 'DontStart' is set" in shard_2_master_status: - timeout = utils.wait_step('shard 2 master has not started replication yet', timeout) - continue - logging.debug("shard 2 master has started replication, good") - break - - # second restore from storage: to be sure, we stop vttablet, and restart - # it afterwards - shard_1_slave1.kill_vttablet() - utils.run_vtctl(['ShardMultiRestore', '-strategy=-populate_blp_checkpoint', - 'test_keyspace/c0-', shard_1_slave1.tablet_alias], - auto_log=True) - shard_1_slave1.start_vttablet(wait_for_state=None) - shard_1_slave1.wait_for_binlog_server_state("Enabled") + # TODO(alainjobart): experiment with the dontStartBinlogPlayer option # check the startup values are in the right place self._check_startup_values() @@ -658,9 +577,9 @@ def test_resharding(self): logging.debug("Running vtworker SplitDiff") utils.run_vtworker(['-cell', 'test_nj', 'SplitDiff', 'test_keyspace/c0-'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_1_rdonly.tablet_alias, 'rdonly'], + utils.run_vtctl(['ChangeSlaveType', shard_1_rdonly1.tablet_alias, 'rdonly'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_3_rdonly.tablet_alias, 'rdonly'], + utils.run_vtctl(['ChangeSlaveType', shard_3_rdonly1.tablet_alias, 'rdonly'], auto_log=True) utils.pause("Good time to test vtworker for diffs") @@ -698,9 +617,15 @@ def test_resharding(self): # check query service is off on master 2 and master 3, as filtered # replication is enabled. Even health check that is enabled on - # master 3 should not interfere. - self._check_query_service(shard_2_master, False, False) - self._check_query_service(shard_3_master, False, False) + # master 3 should not interfere (we run it to be sure). + utils.run_vtctl(['RunHealthCheck', shard_3_master.tablet_alias, 'replica'], + auto_log=True) + utils.check_tablet_query_service(self, shard_2_master, False, False) + utils.check_tablet_query_service(self, shard_3_master, False, False) + + # check the destination master 3 is healthy, even though its query + # service is not running (if not healthy this would exception out) + shard_3_master.get_healthz() # now serve rdonly from the split shards, in test_nj only utils.run_vtctl(['MigrateServedTypes', '--cells=test_nj', @@ -715,9 +640,9 @@ def test_resharding(self): 'Partitions(rdonly): -80 80-\n' + 'TabletTypes: rdonly', keyspace_id_type=keyspace_id_type) - self._check_query_service(shard_0_ny_rdonly, True, False) - self._check_query_service(shard_1_ny_rdonly, True, False) - self._check_query_service(shard_1_rdonly, False, True) + utils.check_tablet_query_service(self, shard_0_ny_rdonly, True, False) + utils.check_tablet_query_service(self, shard_1_ny_rdonly, True, False) + utils.check_tablet_query_service(self, shard_1_rdonly1, False, True) # now serve rdonly from the split shards, everywhere utils.run_vtctl(['MigrateServedTypes', 'test_keyspace/80-', 'rdonly'], @@ -732,11 +657,14 @@ def test_resharding(self): 'Partitions(rdonly): -80 80-c0 c0-\n' + 'TabletTypes: rdonly', keyspace_id_type=keyspace_id_type) - self._check_query_service(shard_0_ny_rdonly, True, False) - self._check_query_service(shard_1_ny_rdonly, False, True) - self._check_query_service(shard_1_rdonly, False, True) + utils.check_tablet_query_service(self, shard_0_ny_rdonly, True, False) + utils.check_tablet_query_service(self, shard_1_ny_rdonly, False, True) + utils.check_tablet_query_service(self, shard_1_rdonly1, False, True) # then serve replica from the split shards + source_tablet = shard_1_slave2 + destination_shards = ['test_keyspace/80-c0', 'test_keyspace/c0-'] + utils.run_vtctl(['MigrateServedTypes', 'test_keyspace/80-', 'replica'], auto_log=True) utils.check_srv_keyspace('test_nj', 'test_keyspace', @@ -745,27 +673,36 @@ def test_resharding(self): 'Partitions(replica): -80 80-c0 c0-\n' + 'TabletTypes: master,rdonly,replica', keyspace_id_type=keyspace_id_type) - self._check_query_service(shard_1_slave2, False, True) + utils.check_tablet_query_service(self, shard_1_slave2, False, True) # move replica back and forth utils.run_vtctl(['MigrateServedTypes', '-reverse', 'test_keyspace/80-', 'replica'], auto_log=True) + # After a backwards migration, queryservice should be enabled on source and disabled on destinations + utils.check_tablet_query_service(self, shard_1_slave2, True, False) + # Destination tablets would have query service disabled for other reasons than the migration, + # so check the shard record instead of the tablets directly + utils.check_shard_query_services(self, destination_shards, 'replica', False) utils.check_srv_keyspace('test_nj', 'test_keyspace', 'Partitions(master): -80 80-\n' + 'Partitions(rdonly): -80 80-c0 c0-\n' + 'Partitions(replica): -80 80-\n' + 'TabletTypes: master,rdonly,replica', keyspace_id_type=keyspace_id_type) - self._check_query_service(shard_1_slave2, True, False) + utils.run_vtctl(['MigrateServedTypes', 'test_keyspace/80-', 'replica'], auto_log=True) + # After a forwards migration, queryservice should be disabled on source and enabled on destinations + utils.check_tablet_query_service(self, shard_1_slave2, False, True) + # Destination tablets would have query service disabled for other reasons than the migration, + # so check the shard record instead of the tablets directly + utils.check_shard_query_services(self, destination_shards, 'replica', True) utils.check_srv_keyspace('test_nj', 'test_keyspace', 'Partitions(master): -80 80-\n' + 'Partitions(rdonly): -80 80-c0 c0-\n' + 'Partitions(replica): -80 80-c0 c0-\n' + 'TabletTypes: master,rdonly,replica', keyspace_id_type=keyspace_id_type) - self._check_query_service(shard_1_slave2, False, True) # reparent shard_2 to shard_2_replica1, then insert more data and # see it flow through still @@ -780,9 +717,9 @@ def test_resharding(self): logging.debug("Running vtworker SplitDiff") utils.run_vtworker(['-cell', 'test_nj', 'SplitDiff', 'test_keyspace/c0-'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_1_rdonly.tablet_alias, 'rdonly'], + utils.run_vtctl(['ChangeSlaveType', shard_1_rdonly1.tablet_alias, 'rdonly'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', shard_3_rdonly.tablet_alias, 'rdonly'], + utils.run_vtctl(['ChangeSlaveType', shard_3_rdonly1.tablet_alias, 'rdonly'], auto_log=True) # going to migrate the master now, check the delays @@ -817,7 +754,7 @@ def test_resharding(self): 'Partitions(replica): -80 80-c0 c0-\n' + 'TabletTypes: master,rdonly,replica', keyspace_id_type=keyspace_id_type) - self._check_query_service(shard_1_master, False, True) + utils.check_tablet_query_service(self, shard_1_master, False, True) # check the binlog players are gone now shard_2_master.wait_for_binlog_player_count(0) @@ -830,12 +767,12 @@ def test_resharding(self): # scrap the original tablets in the original shard for t in [shard_1_master, shard_1_slave1, shard_1_slave2, shard_1_ny_rdonly, - shard_1_rdonly]: + shard_1_rdonly1]: utils.run_vtctl(['ScrapTablet', t.tablet_alias], auto_log=True) tablet.kill_tablets([shard_1_master, shard_1_slave1, shard_1_slave2, - shard_1_ny_rdonly, shard_1_rdonly]) + shard_1_ny_rdonly, shard_1_rdonly1]) for t in [shard_1_master, shard_1_slave1, shard_1_slave2, shard_1_ny_rdonly, - shard_1_rdonly]: + shard_1_rdonly1]: utils.run_vtctl(['DeleteTablet', t.tablet_alias], auto_log=True) # rebuild the serving graph, all mentions of the old shards shoud be gone @@ -855,7 +792,7 @@ def test_resharding(self): # kill everything tablet.kill_tablets([shard_0_master, shard_0_replica, shard_0_ny_rdonly, shard_2_master, shard_2_replica1, shard_2_replica2, - shard_3_master, shard_3_replica, shard_3_rdonly]) + shard_3_master, shard_3_replica, shard_3_rdonly1]) if __name__ == '__main__': utils.main() diff --git a/test/resharding_bytes_vtworker.py b/test/resharding_bytes_vtworker.py deleted file mode 100755 index 000cf669716..00000000000 --- a/test/resharding_bytes_vtworker.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2014, Google Inc. All rights reserved. -# Use of this source code is governed by a BSD-style license that can -# be found in the LICENSE file. - -import utils -import resharding - -from vtdb import keyrange_constants - -# this test is the same as resharding_bytes.py, but it uses vtworker to -# do the clone. -if __name__ == '__main__': - resharding.keyspace_id_type = keyrange_constants.KIT_BYTES - resharding.use_clone_worker = True - utils.main(resharding) diff --git a/test/resharding_vtworker.py b/test/resharding_vtworker.py deleted file mode 100755 index e9b5ae141ac..00000000000 --- a/test/resharding_vtworker.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2014, Google Inc. All rights reserved. -# Use of this source code is governed by a BSD-style license that can -# be found in the LICENSE file. - -import utils -import resharding - -# this test is the same as resharding.py, but it uses vtworker to -# do the clone. -if __name__ == '__main__': - resharding.use_clone_worker = True - utils.main(resharding) diff --git a/test/schema.py b/test/schema.py index f7d7bbc8c80..b234c9564aa 100755 --- a/test/schema.py +++ b/test/schema.py @@ -14,6 +14,8 @@ shard_0_backup = tablet.Tablet() shard_1_master = tablet.Tablet() shard_1_replica1 = tablet.Tablet() +shard_2_master = tablet.Tablet() +shard_2_replica1 = tablet.Tablet() def setUpModule(): @@ -28,6 +30,8 @@ def setUpModule(): shard_0_backup.init_mysql(), shard_1_master.init_mysql(), shard_1_replica1.init_mysql(), + shard_2_master.init_mysql(), + shard_2_replica1.init_mysql(), ] utils.wait_procs(setup_procs) except: @@ -46,6 +50,8 @@ def tearDownModule(): shard_0_backup.teardown_mysql(), shard_1_master.teardown_mysql(), shard_1_replica1.teardown_mysql(), + shard_2_master.teardown_mysql(), + shard_2_replica1.teardown_mysql(), ] utils.wait_procs(teardown_procs, raise_on_error=False) @@ -60,6 +66,8 @@ def tearDownModule(): shard_0_backup.remove_tree() shard_1_master.remove_tree() shard_1_replica1.remove_tree() + shard_2_master.remove_tree() + shard_2_replica1.remove_tree() # statements to create the table create_vt_select_test = [ @@ -78,6 +86,12 @@ def _check_tables(self, tablet, expectedCount): 'Unexpected table count on %s (not %u): %s' % (tablet.tablet_alias, expectedCount, str(tables))) + def _check_db_not_created(self, tablet): + # Broadly catch all exceptions, since the exception being raised is internal to MySQL. + # We're strictly checking the error message though, so should be fine. + with self.assertRaisesRegexp(Exception, '(1049, "Unknown database \'vt_test_keyspace\'")'): + tables = tablet.mquery('vt_test_keyspace', 'show tables') + def test_complex_schema(self): utils.run_vtctl(['CreateKeyspace', 'test_keyspace']) @@ -89,6 +103,8 @@ def test_complex_schema(self): shard_0_backup.init_tablet( 'backup', 'test_keyspace', '0') shard_1_master.init_tablet( 'master', 'test_keyspace', '1') shard_1_replica1.init_tablet('replica', 'test_keyspace', '1') + shard_2_master.init_tablet( 'master', 'test_keyspace', '2') + shard_2_replica1.init_tablet('replica', 'test_keyspace', '2') utils.run_vtctl(['RebuildKeyspaceGraph', 'test_keyspace'], auto_log=True) @@ -101,6 +117,10 @@ def test_complex_schema(self): t.create_db('vt_test_keyspace') t.start_vttablet(wait_for_state=None) + # we intentionally don't want to create db on these tablets + shard_2_master.start_vttablet(wait_for_state=None) + shard_2_replica1.start_vttablet(wait_for_state=None) + # wait for the tablets to start shard_0_master.wait_for_vttablet_state('SERVING') shard_0_replica1.wait_for_vttablet_state('SERVING') @@ -109,13 +129,16 @@ def test_complex_schema(self): shard_0_backup.wait_for_vttablet_state('NOT_SERVING') shard_1_master.wait_for_vttablet_state('SERVING') shard_1_replica1.wait_for_vttablet_state('SERVING') + shard_2_master.wait_for_vttablet_state('NOT_SERVING') + shard_2_replica1.wait_for_vttablet_state('NOT_SERVING') # make sure all replication is good for t in [shard_0_master, shard_0_replica1, shard_0_replica2, - shard_0_rdonly, shard_0_backup, shard_1_master, shard_1_replica1]: + shard_0_rdonly, shard_0_backup, shard_1_master, shard_1_replica1, shard_2_master, shard_2_replica1]: t.reset_replication() utils.run_vtctl(['ReparentShard', '-force', 'test_keyspace/0', shard_0_master.tablet_alias], auto_log=True) utils.run_vtctl(['ReparentShard', '-force', 'test_keyspace/1', shard_1_master.tablet_alias], auto_log=True) + utils.run_vtctl(['ReparentShard', '-force', 'test_keyspace/2', shard_2_master.tablet_alias], auto_log=True) utils.run_vtctl(['ValidateKeyspace', '-ping-tablets', 'test_keyspace']) # check after all tablets are here and replication is fixed @@ -165,8 +188,27 @@ def test_complex_schema(self): self.assertEqual(len(s['TableDefinitions']), 1) self.assertEqual(s['TableDefinitions'][0]['Name'], 'vt_select_test0') + # CopySchemaShard is responsible for creating the db; one shouldn't exist before + # the command is run. + self._check_db_not_created(shard_2_master) + self._check_db_not_created(shard_2_replica1) + + utils.run_vtctl(['CopySchemaShard', + shard_0_replica1.tablet_alias, + 'test_keyspace/2'], + auto_log=True) + + # shard_2_master should look the same as the replica we copied from + self._check_tables(shard_2_master, 2) + self._check_tables(shard_2_replica1, 2) + # shard_2_replica1 should have gotten an identical schema applied to it via replication + self.assertEqual( + utils.run_vtctl_json(['GetSchema', shard_0_replica1.tablet_alias]), + utils.run_vtctl_json(['GetSchema', shard_2_replica1.tablet_alias]), + ) + # keyspace: try to apply a keyspace-wide schema change, should fail - # as the preflight would be different in both shards + # as the preflight would be different in shard1 vs the others out, err = utils.run_vtctl(['ApplySchemaKeyspace', '-sql='+create_vt_select_test[2], 'test_keyspace'], @@ -205,6 +247,8 @@ def test_complex_schema(self): self._check_tables(shard_0_backup, 3) self._check_tables(shard_1_master, 3) # current master self._check_tables(shard_1_replica1, 3) + self._check_tables(shard_2_master, 3) # current master + self._check_tables(shard_2_replica1, 3) # keyspace: apply a keyspace-wide complex schema change, should work too utils.run_vtctl(['ApplySchemaKeyspace', @@ -222,12 +266,14 @@ def test_complex_schema(self): self._check_tables(shard_0_backup, 4) self._check_tables(shard_1_master, 3) # current master self._check_tables(shard_1_replica1, 4) + self._check_tables(shard_2_master, 3) # current master + self._check_tables(shard_2_replica1, 4) utils.pause("Look at schema now!") tablet.kill_tablets([shard_0_master, shard_0_replica1, shard_0_replica2, shard_0_rdonly, shard_0_backup, shard_1_master, - shard_1_replica1]) + shard_1_replica1, shard_2_master, shard_2_replica1]) if __name__ == '__main__': utils.main() diff --git a/test/secure.py b/test/secure.py index 9f73fe66831..6ca06b805d9 100755 --- a/test/secure.py +++ b/test/secure.py @@ -148,7 +148,29 @@ def setUpModule(): vt_server_key = cert_dir + "/vt-server-key.pem" vt_server_cert = cert_dir + "/vt-server-cert.pem" vt_server_req = cert_dir + "/vt-server-req.pem" + vt_server_config = cert_dir + "/server.config" + with open(vt_server_config, 'w') as fd: + fd.write(""" +[ req ] + default_bits = 1024 + default_keyfile = keyfile.pem + distinguished_name = req_distinguished_name + attributes = req_attributes + prompt = no + output_password = mypass +[ req_distinguished_name ] + C = US + ST = California + L = Mountain View + O = Google + OU = Vitess + CN = Vitess Server + emailAddress = test@email.address +[ req_attributes ] + challengePassword = A challenge password +""") openssl(["req", "-newkey", "rsa:2048", "-days", "3600", "-nodes", "-batch", + "-config", vt_server_config, "-keyout", vt_server_key, "-out", vt_server_req]) openssl(["rsa", "-in", vt_server_key, "-out", vt_server_key]) openssl(["x509", "-req", @@ -209,7 +231,7 @@ def tearDownModule(): class TestSecure(unittest.TestCase): def test_secure(self): - zkocc_server = utils.zkocc_start() + vtgate_server, vtgate_port = utils.vtgate_start(cache_ttl='0s') # start the tablets shard_0_master.start_vttablet(cert=cert_dir + "/vt-server-cert.pem", @@ -229,19 +251,19 @@ def test_secure(self): utils.run_vtctl('ReparentShard -force test_keyspace/0 ' + shard_0_master.tablet_alias, auto_log=True) # then get the topology and check it - zkocc_client = zkocc.ZkOccConnection("localhost:%u" % environment.topo_server().zkocc_port_base, - "test_nj", 30.0) - topology.read_keyspaces(zkocc_client) + topo_client = zkocc.ZkOccConnection("localhost:%u" % vtgate_port, + "test_nj", 30.0) + topology.read_keyspaces(topo_client) - shard_0_master_addrs = topology.get_host_port_by_name(zkocc_client, "test_keyspace.0.master:_vts") + shard_0_master_addrs = topology.get_host_port_by_name(topo_client, "test_keyspace.0.master:vts") if len(shard_0_master_addrs) != 1: - self.fail('topology.get_host_port_by_name failed for "test_keyspace.0.master:_vts", got: %s' % " ".join(["%s:%u(%s)" % (h, p, str(e)) for (h, p, e) in shard_0_master_addrs])) + self.fail('topology.get_host_port_by_name failed for "test_keyspace.0.master:vts", got: %s' % " ".join(["%s:%u(%s)" % (h, p, str(e)) for (h, p, e) in shard_0_master_addrs])) if shard_0_master_addrs[0][2] != True: - self.fail('topology.get_host_port_by_name failed for "test_keyspace.0.master:_vts" is not encrypted') + self.fail('topology.get_host_port_by_name failed for "test_keyspace.0.master:vts" is not encrypted') logging.debug("shard 0 master addrs: %s", " ".join(["%s:%u(%s)" % (h, p, str(e)) for (h, p, e) in shard_0_master_addrs])) # make sure asking for optionally secure connections works too - auto_addrs = topology.get_host_port_by_name(zkocc_client, "test_keyspace.0.master:_vtocc", encrypted=True) + auto_addrs = topology.get_host_port_by_name(topo_client, "test_keyspace.0.master:vt", encrypted=True) if auto_addrs != shard_0_master_addrs: self.fail('topology.get_host_port_by_name doesn\'t resolve encrypted addresses properly: %s != %s' % (str(shard_0_master_addrs), str(auto_addrs))) @@ -338,25 +360,24 @@ def test_secure(self): utils.vtgate_kill(gate_proc) # kill everything - utils.kill_sub_process(zkocc_server) + utils.vtgate_kill(vtgate_server) def test_restart(self): - zkocc_server = utils.zkocc_start() - shard_0_master.create_db('vt_test_keyspace') proc1 = shard_0_master.start_vttablet(cert=cert_dir + "/vt-server-cert.pem", - key=cert_dir + "/vt-server-key.pem") - # Takes a bit longer for vttablet to serve the pid port - time.sleep(1.0) + key=cert_dir + "/vt-server-key.pem", + wait_for_state='SERVING') proc2 = shard_0_master.start_vttablet(cert=cert_dir + "/vt-server-cert.pem", - key=cert_dir + "/vt-server-key.pem") - time.sleep(1.0) - proc1.poll() - if proc1.returncode is None: - self.fail("proc1 still running") + key=cert_dir + "/vt-server-key.pem", + wait_for_state='SERVING') + timeout = 10.0 + while True: + proc1.poll() + if proc1.returncode is not None: + break + timeout = utils.wait_step("waiting for new vttablet to kill its predecessor", timeout) shard_0_master.kill_vttablet() - utils.kill_sub_process(zkocc_server) logging.debug("Done here") if __name__ == '__main__': diff --git a/test/sharded.py b/test/sharded.py index c7ef6de6b7f..b76f9902dc0 100755 --- a/test/sharded.py +++ b/test/sharded.py @@ -118,10 +118,6 @@ def test_sharding(self): '-sql=' + create_vt_select_test.replace("\n", ""), shard_0_replica.tablet_alias]) - if environment.topo_server().flavor() == 'zookeeper': - # start zkocc, we'll use it later, indirectly with the vtdb-zkocc driver - zkocc_server = utils.zkocc_start() - # start vtgate, we'll use it later vtgate_server, vtgate_port = utils.vtgate_start() @@ -186,22 +182,11 @@ def test_sharding(self): "2\ttest 2", "10\ttest 10"], driver="vtdb-zk-streaming") - self._check_rows(["Index\tid\tmsg", - "1\ttest 1", - "2\ttest 2", - "10\ttest 10"], - driver="vtdb-zkocc") - self._check_rows(["Index\tid\tmsg", - "1\ttest 1", - "2\ttest 2", - "10\ttest 10"], - driver="vtdb-zkocc-streaming") # make sure the schema checking works self._check_rows_schema_diff("vtdb") if environment.topo_server().flavor() == 'zookeeper': self._check_rows_schema_diff("vtdb-zk") - self._check_rows_schema_diff("vtdb-zkocc") # throw in some schema validation step # we created the schema differently, so it should show @@ -232,7 +217,7 @@ def test_sharding(self): lines = out.splitlines() for base in ['-80', '80-']: for db_type in ['master', 'replica']: - for sub_path in ['', '.vdns', '/0', '/_vtocc.vdns']: + for sub_path in ['', '.vdns', '/0', '/vt.vdns']: expected = '/zk/test_nj/zkns/vt/test_keyspace/' + base + '/' + db_type + sub_path if expected not in lines: self.fail('missing zkns part:\n%s\nin:%s' %(expected, out)) @@ -244,9 +229,9 @@ def test_sharding(self): "test_nj", 30.0) topology.read_keyspaces(vtgate_client) - shard_0_master_addrs = topology.get_host_port_by_name(vtgate_client, "test_keyspace.-80.master:_vtocc") + shard_0_master_addrs = topology.get_host_port_by_name(vtgate_client, "test_keyspace.-80.master:vt") if len(shard_0_master_addrs) != 1: - self.fail('topology.get_host_port_by_name failed for "test_keyspace.-80.master:_vtocc", got: %s' % " ".join(["%s:%u(%s)" % (h, p, str(e)) for (h, p, e) in shard_0_master_addrs])) + self.fail('topology.get_host_port_by_name failed for "test_keyspace.-80.master:vt", got: %s' % " ".join(["%s:%u(%s)" % (h, p, str(e)) for (h, p, e) in shard_0_master_addrs])) logging.debug("shard 0 master addrs: %s", " ".join(["%s:%u(%s)" % (h, p, str(e)) for (h, p, e) in shard_0_master_addrs])) # connect to shard -80 @@ -259,7 +244,7 @@ def test_sharding(self): 'wrong conn._execute output: %s' % str(results)) # connect to shard 80- - shard_1_master_addrs = topology.get_host_port_by_name(vtgate_client, "test_keyspace.80-.master:_vtocc") + shard_1_master_addrs = topology.get_host_port_by_name(vtgate_client, "test_keyspace.80-.master:vt") conn = tablet3.TabletConnection("%s:%u" % (shard_1_master_addrs[0][0], shard_1_master_addrs[0][1]), "", "test_keyspace", "80-", 10.0) @@ -280,8 +265,6 @@ def test_sharding(self): self.fail('unexpected exception: ' + str(e)) utils.vtgate_kill(vtgate_server) - if environment.topo_server().flavor() == 'zookeeper': - utils.kill_sub_process(zkocc_server) tablet.kill_tablets([shard_0_master, shard_0_replica, shard_1_master, shard_1_replica]) diff --git a/test/tablet.py b/test/tablet.py index 94d70fdf427..af4c4009331 100644 --- a/test/tablet.py +++ b/test/tablet.py @@ -4,6 +4,7 @@ import shutil import sys import time +import urllib2 import warnings # Dropping a table inexplicably produces a warning despite # the "IF EXISTS" clause. Squelch these warnings. @@ -87,8 +88,6 @@ def __init__(self, tablet_uid=None, port=None, mysql_port=None, cell=None, self.tablet_alias = 'test_%s-%010d' % (self.cell, self.tablet_uid) self.zk_tablet_path = ( '/zk/test_%s/vt/tablets/%010d' % (self.cell, self.tablet_uid)) - self.zk_pid = self.zk_tablet_path + '/pid' - self.checked_zk_pid = False def mysqlctl(self, cmd, extra_my_cnf=None, with_ports=False, verbose=False): extra_env = {} @@ -103,6 +102,7 @@ def mysqlctl(self, cmd, extra_my_cnf=None, with_ports=False, verbose=False): if with_ports: args.extend(['-port', str(self.port), '-mysql_port', str(self.mysql_port)]) + self._add_dbconfigs(args) if verbose: args.append('-alsologtostderr') args.extend(cmd) @@ -118,6 +118,7 @@ def mysqlctld(self, cmd, extra_my_cnf=None, with_ports=False, verbose=False): '-tablet_uid', str(self.tablet_uid), '-mysql_port', str(self.mysql_port), '-socket_file', os.path.join(self.tablet_dir, 'mysqlctl.sock')] + self._add_dbconfigs(args) if verbose: args.append('-alsologtostderr') args.extend(cmd) @@ -341,7 +342,7 @@ def flush(self): stderr=utils.devnull, stdout=utils.devnull) def _start_prog(self, binary, port=None, auth=False, memcache=False, - wait_for_state='SERVING', customrules=None, + wait_for_state='SERVING', filecustomrules=None, zkcustomrules=None, schema_override=None, cert=None, key=None, ca_cert=None, repl_extra_flags={}, table_acl_config=None, lameduck_period=None, security_policy=None, @@ -351,10 +352,7 @@ def _start_prog(self, binary, port=None, auth=False, memcache=False, args.extend(['-port', '%s' % (port or self.port), '-log_dir', environment.vtlogroot]) - dbconfigs = self._get_db_configs_file(repl_extra_flags) - for key1 in dbconfigs: - for key2 in dbconfigs[key1]: - args.extend(['-db-config-' + key1 + '-' + key2, dbconfigs[key1][key2]]) + self._add_dbconfigs(args, repl_extra_flags) if memcache: args.extend(['-rowcache-bin', environment.memcached_bin()]) @@ -369,8 +367,10 @@ def _start_prog(self, binary, port=None, auth=False, memcache=False, environment.vttop, 'test', 'test_data', 'authcredentials_test.json')]) - if customrules: - args.extend(['-customrules', customrules]) + if filecustomrules: + args.extend(['-filecustomrules', filecustomrules]) + if zkcustomrules: + args.extend(['-zkcustomrules', zkcustomrules]) if schema_override: args.extend(['-schema-override', schema_override]) @@ -410,12 +410,14 @@ def _start_prog(self, binary, port=None, auth=False, memcache=False, return self.proc def start_vttablet(self, port=None, auth=False, memcache=False, - wait_for_state='SERVING', customrules=None, + wait_for_state='SERVING', filecustomrules=None, zkcustomrules=None, schema_override=None, cert=None, key=None, ca_cert=None, repl_extra_flags={}, table_acl_config=None, lameduck_period=None, security_policy=None, target_tablet_type=None, full_mycnf_args=False, - extra_args=None, extra_env=None, include_mysql_port=True): + extra_args=None, extra_env=None, include_mysql_port=True, + init_tablet_type=None, init_keyspace=None, + init_shard=None, init_db_name_override=None): """Starts a vttablet process, and returns it. The process is also saved in self.proc, so it's easy to kill as well. @@ -461,16 +463,34 @@ def start_vttablet(self, port=None, auth=False, memcache=False, if include_mysql_port: args.extend(['-mycnf_mysql_port', str(self.mysql_port)]) if target_tablet_type: + self.tablet_type = target_tablet_type args.extend(['-target_tablet_type', target_tablet_type, '-health_check_interval', '2s', - '-allowed_replication_lag', '30']) + '-enable_replication_lag_check', + '-degraded_threshold', '5s']) + + # this is used to run InitTablet as part of the vttablet startup + if init_tablet_type: + self.tablet_type = init_tablet_type + args.extend(['-init_tablet_type', init_tablet_type]) + if init_keyspace: + self.keyspace = init_keyspace + self.shard = init_shard + args.extend(['-init_keyspace', init_keyspace, + '-init_shard', init_shard]) + if init_db_name_override: + self.dbname = init_db_name_override + args.extend(['-init_db_name_override', init_db_name_override]) + else: + self.dbname = 'vt_' + init_keyspace if extra_args: args.extend(extra_args) return self._start_prog(binary='vttablet', port=port, auth=auth, memcache=memcache, wait_for_state=wait_for_state, - customrules=customrules, + filecustomrules=filecustomrules, + zkcustomrules=zkcustomrules, schema_override=schema_override, cert=cert, key=key, ca_cert=ca_cert, repl_extra_flags=repl_extra_flags, @@ -479,7 +499,7 @@ def start_vttablet(self, port=None, auth=False, memcache=False, security_policy=security_policy, extra_env=extra_env) def start_vtocc(self, port=None, auth=False, memcache=False, - wait_for_state='SERVING', customrules=None, + wait_for_state='SERVING', filecustomrules=None, schema_override=None, cert=None, key=None, ca_cert=None, repl_extra_flags={}, table_acl_config=None, lameduck_period=None, security_policy=None, @@ -504,7 +524,7 @@ def start_vtocc(self, port=None, auth=False, memcache=False, return self._start_prog(binary='vtocc', port=port, auth=auth, memcache=memcache, wait_for_state=wait_for_state, - customrules=customrules, + filecustomrules=filecustomrules, schema_override=schema_override, cert=cert, key=key, ca_cert=ca_cert, repl_extra_flags=repl_extra_flags, @@ -514,12 +534,6 @@ def start_vtocc(self, port=None, auth=False, memcache=False, def wait_for_vttablet_state(self, expected, timeout=60.0, port=None): - # wait for zookeeper PID just to be sure we have it - if environment.topo_server().flavor() == 'zookeeper': - if not self.checked_zk_pid: - utils.run(environment.binary_args('zk') + ['wait', '-e', self.zk_pid], - stdout=utils.devnull) - self.checked_zk_pid = True self.wait_for_vtocc_state(expected, timeout=timeout, port=port) def wait_for_vtocc_state(self, expected, timeout=60.0, port=None): @@ -545,25 +559,30 @@ def wait_for_vtocc_state(self, expected, timeout=60.0, port=None): timeout = utils.wait_step('waiting for state %s' % expected, timeout, sleep_time=0.1) - def wait_for_mysql_socket(self, timeout=10.0): - socket_file = os.path.join(self.tablet_dir, 'mysql.sock') + def wait_for_mysqlctl_socket(self, timeout=10.0): + mysql_sock = os.path.join(self.tablet_dir, 'mysql.sock') + mysqlctl_sock = os.path.join(self.tablet_dir, 'mysqlctl.sock') while True: - if os.path.exists(socket_file): + if os.path.exists(mysql_sock) and os.path.exists(mysqlctl_sock): return - timeout = utils.wait_step('waiting for mysql socket file %s' % socket_file, timeout) + timeout = utils.wait_step('waiting for mysql and mysqlctl socket files: %s %s' % (mysql_sock, mysqlctl_sock), timeout) - def _get_db_configs_file(self, repl_extra_flags={}): + def _add_dbconfigs(self, args, repl_extra_flags={}): config = dict(self.default_db_config) if self.keyspace: config['app']['dbname'] = self.dbname - config['dba']['dbname'] = self.dbname config['repl']['dbname'] = self.dbname config['repl'].update(repl_extra_flags) - return config + for key1 in config: + for key2 in config[key1]: + args.extend(['-db-config-' + key1 + '-' + key2, config[key1][key2]]) def get_status(self): return utils.get_status(self.port) + def get_healthz(self): + return urllib2.urlopen('http://localhost:%u/healthz' % self.port).read() + def kill_vttablet(self): logging.debug('killing vttablet: %s', self.tablet_alias) if self.proc is not None: diff --git a/test/tabletmanager.py b/test/tabletmanager.py index 0125d1f2aff..44207d74b4c 100755 --- a/test/tabletmanager.py +++ b/test/tabletmanager.py @@ -13,6 +13,7 @@ import time import unittest import urllib +import urllib2 import environment import utils @@ -360,7 +361,7 @@ def test_scrap_and_reinit(self): # manually add a bogus entry to the replication graph, and check # it is removed by ShardReplicationFix utils.run_vtctl(['ShardReplicationAdd', 'test_keyspace/0', - 'test_nj-0000066666', 'test_nj-0000062344'], auto_log=True) + 'test_nj-0000066666'], auto_log=True) with_bogus = utils.run_vtctl_json(['GetShardReplication', 'test_nj', 'test_keyspace/0']) self.assertEqual(3, len(with_bogus['ReplicationLinks']), @@ -372,12 +373,17 @@ def test_scrap_and_reinit(self): self.assertEqual(2, len(after_scrap['ReplicationLinks']), 'wrong replication links after fix: %s' % str(after_fix)) - def test_health_check(self): - utils.run_vtctl(['CreateKeyspace', 'test_keyspace']) + def check_healthz(self, tablet, expected): + if expected: + self.assertEqual("ok\n", tablet.get_healthz()) + else: + with self.assertRaises(urllib2.HTTPError): + tablet.get_healthz() + def test_health_check(self): # one master, one replica that starts in spare + # (for the replica, we let vttablet do the InitTablet) tablet_62344.init_tablet('master', 'test_keyspace', '0') - tablet_62044.init_tablet('spare', 'test_keyspace', '0') for t in tablet_62344, tablet_62044: t.create_db('vt_test_keyspace') @@ -386,10 +392,13 @@ def test_health_check(self): target_tablet_type='replica') tablet_62044.start_vttablet(wait_for_state=None, target_tablet_type='replica', - lameduck_period='5s') + lameduck_period='5s', + init_keyspace='test_keyspace', + init_shard='0') tablet_62344.wait_for_vttablet_state('SERVING') tablet_62044.wait_for_vttablet_state('NOT_SERVING') + self.check_healthz(tablet_62044, False) utils.run_vtctl(['ReparentShard', '-force', 'test_keyspace/0', tablet_62344.tablet_alias]) @@ -402,56 +411,47 @@ def test_health_check(self): logging.debug("Slave tablet went to replica, good") break timeout = utils.wait_step('slave tablet going to replica', timeout) + self.check_healthz(tablet_62044, True) # make sure the master is still master ti = utils.run_vtctl_json(['GetTablet', tablet_62344.tablet_alias]) self.assertEqual(ti['Type'], 'master', "unexpected master type: %s" % ti['Type']) - # stop replication on the slave, see it trigger the slave going - # slightly unhealthy + # stop replication, make sure we go unhealthy. tablet_62044.mquery('', 'stop slave') timeout = 10 while True: ti = utils.run_vtctl_json(['GetTablet', tablet_62044.tablet_alias]) - if 'Health' in ti and ti['Health']: - if 'replication_lag' in ti['Health']: - if ti['Health']['replication_lag'] == 'high': - logging.debug("Slave tablet replication_lag went to high, good") - break - timeout = utils.wait_step('slave has high replication lag', timeout) + if ti['Type'] == "spare": + logging.debug("Slave tablet went to spare, good") + break + timeout = utils.wait_step('slave is spare', timeout) + self.check_healthz(tablet_62044, False) # make sure the serving graph was updated timeout = 10 while True: - ep = utils.run_vtctl_json(['GetEndPoints', 'test_nj', 'test_keyspace/0', - 'replica']) - if 'health' in ep['entries'][0] and ep['entries'][0]['health']: - if 'replication_lag' in ep['entries'][0]['health']: - if ep['entries'][0]['health']['replication_lag'] == 'high': - logging.debug("Replication lag parameter propagated to serving graph, good") - break - timeout = utils.wait_step('Replication lag parameter not propagated to serving graph', timeout) + try: + utils.run_vtctl_json(['GetEndPoints', 'test_nj', 'test_keyspace/0', + 'replica']) + except: + logging.debug("Tablet is gone from serving graph, good") + break + timeout = utils.wait_step('Stopped replication didn\'t trigger removal from serving graph', timeout) # make sure status web page is unhappy - self.assertIn('>unhappy', tablet_62044.get_status()) + self.assertIn('>unhealthy: replication_reporter: Replication is not running', tablet_62044.get_status()) - # make sure the vars is updated - v = utils.get_vars(tablet_62044.port) - self.assertEqual(v['LastHealthMapCount'], 1) - - # then restart replication, make sure we go back to healthy + # then restart replication, and write data, make sure we go back to healthy tablet_62044.mquery('', 'start slave') timeout = 10 while True: ti = utils.run_vtctl_json(['GetTablet', tablet_62044.tablet_alias]) - if 'Health' in ti and ti['Health']: - if 'replication_lag' in ti['Health']: - if ti['Health']['replication_lag'] == 'high': - timeout = utils.wait_step('slave has no replication lag', timeout) - continue - logging.debug("Slave tablet replication_lag is gone, good") - break + if ti['Type'] == "replica": + logging.debug("Slave tablet went to replica, good") + break + timeout = utils.wait_step('slave is not healthy', timeout) # make sure status web page is healthy self.assertIn('>healthy', tablet_62044.get_status()) @@ -502,6 +502,7 @@ def test_no_mysql_healthcheck(self): full_mycnf_args=True, include_mysql_port=False) for t in tablet_62344, tablet_62044: t.wait_for_vttablet_state('NOT_SERVING') + self.check_healthz(t, False) # restart mysqld start_procs = [ @@ -513,6 +514,16 @@ def test_no_mysql_healthcheck(self): # wait for the tablets to become healthy and fix their mysql port for t in tablet_62344, tablet_62044: t.wait_for_vttablet_state('SERVING') + + # we need to do one more health check here, so it sees the query service + # is now running, and turns green. + utils.run_vtctl(['RunHealthCheck', t.tablet_alias, 'replica'], + auto_log=True) + + # master will be healthy, slave's replication won't be running + self.check_healthz(tablet_62344, True) + self.check_healthz(tablet_62044, False) + for t in tablet_62344, tablet_62044: # wait for mysql port to show up timeout = 10 diff --git a/test/topo_flavor/etcd.py b/test/topo_flavor/etcd.py new file mode 100644 index 00000000000..5d7c74ce7aa --- /dev/null +++ b/test/topo_flavor/etcd.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +# Copyright 2014, Google Inc. All rights reserved. +# Use of this source code is governed by a BSD-style license that can +# be found in the LICENSE file. + +import os +import shutil + +import server + + +class EtcdCluster: + """Sets up a global or cell-local etcd cluster""" + + def __init__(self, name): + import environment + import utils + + self.port_base = environment.reserve_ports(2) + + self.name = name + self.hostname = 'localhost' + self.client_port = self.port_base + self.peer_port = self.port_base + 1 + self.client_addr = '%s:%u' % (self.hostname, self.client_port) + self.peer_addr = '%s:%u' % (self.hostname, self.peer_port) + self.api_url = 'http://%s/v2' % (self.client_addr) + + dirname = 'etcd_' + self.name + self.data_dir = os.path.join(environment.vtdataroot, dirname) + + self.proc = utils.run_bg([ + 'etcd', '-name', self.name, '-addr', + self.client_addr, '-peer-addr', + self.peer_addr, '-data-dir', self.data_dir], + stdout=open(os.path.join( + environment.vtlogroot, + dirname + '.stdout'), + 'w'), + stderr=open(os.path.join( + environment.vtlogroot, + dirname + '.stderr'), + 'w'),) + + +class EtcdTopoServer(server.TopoServer): + """Implementation of TopoServer for etcd""" + + clusters = {} + + def setup(self, add_bad_host=False): + import utils + + for cell in ['global', 'test_ca', 'test_nj', 'test_ny']: + self.clusters[cell] = EtcdCluster(cell) + + # Wait for global cluster to come up. + utils.curl( + self.clusters['global'].api_url + '/keys/vt', request='PUT', + data='dir=true', retry_timeout=10) + + # Add entries in global cell list. + for cell, cluster in self.clusters.iteritems(): + if cell != 'global': + utils.curl( + '%s/keys/vt/cells/%s' % + (self.clusters['global'].api_url, cell), request='PUT', + data='value=http://' + cluster.client_addr) + + def teardown(self): + import utils + + for cluster in self.clusters.itervalues(): + utils.kill_sub_process(cluster.proc) + if not utils.options.keep_logs: + shutil.rmtree(cluster.data_dir) + + def flags(self): + return [ + '-topo_implementation', 'etcd', + '-etcd_global_addrs', 'http://' + self.clusters['global'].client_addr, + ] + + def wipe(self): + import utils + + for cell, cluster in self.clusters.iteritems(): + if cell == 'global': + utils.curl( + cluster.api_url + '/keys/vt/keyspaces?recursive=true', + request='DELETE') + else: + utils.curl( + cluster.api_url + '/keys/vt/ns?recursive=true', request='DELETE') + utils.curl( + cluster.api_url + '/keys/vt/tablets?recursive=true', + request='DELETE') + utils.curl( + cluster.api_url + '/keys/vt/replication?recursive=true', + request='DELETE') + + +server.flavor_map['etcd'] = EtcdTopoServer() diff --git a/test/topo_flavor/zookeeper.py b/test/topo_flavor/zookeeper.py index ea0acd8cb0d..bff5fc322ba 100644 --- a/test/topo_flavor/zookeeper.py +++ b/test/topo_flavor/zookeeper.py @@ -6,7 +6,6 @@ import logging import os -import socket import json import server @@ -14,17 +13,28 @@ class ZkTopoServer(server.TopoServer): """Implementation of TopoServer for ZooKeeper""" + def __init__(self): + self.ports_assigned = False - def setup(self, add_bad_host=False): - from environment import reserve_ports, run, binary_args, vtlogroot, tmproot + def assign_ports(self): + """Assign ports if not already assigned""" - self.zk_port_base = reserve_ports(3) - self.zkocc_port_base = reserve_ports(3) + if self.ports_assigned: + return + + from environment import reserve_ports + import utils - self.hostname = socket.gethostname() + self.zk_port_base = reserve_ports(3) + self.hostname = utils.hostname self.zk_ports = ':'.join(str(self.zk_port_base + i) for i in range(3)) self.zk_client_port = self.zk_port_base + 2 + self.ports_assigned = True + + def setup(self, add_bad_host=False): + from environment import run, binary_args, vtlogroot, tmproot + self.assign_ports() run(binary_args('zkctl') + [ '-log_dir', vtlogroot, '-zk.cfg', '1@%s:%s' % (self.hostname, self.zk_ports), @@ -39,14 +49,6 @@ def setup(self, add_bad_host=False): 'test_ny': 'localhost:%u' % (self.zk_client_port), 'test_ca': ca_server, 'global': 'localhost:%u' % (self.zk_client_port), - 'test_nj:_zkocc': - 'localhost:%u,localhost:%u,localhost:%u' % tuple( - self.zkocc_port_base + i - for i in range( - 3)), - 'test_ny:_zkocc': 'localhost:%u' % (self.zkocc_port_base), - 'test_ca:_zkocc': 'localhost:%u' % (self.zkocc_port_base), - 'global:_zkocc': 'localhost:%u' % (self.zkocc_port_base), } json.dump(zk_cell_mapping, f) os.environ['ZK_CLIENT_CONFIG'] = config @@ -59,6 +61,7 @@ def teardown(self): from environment import run, binary_args, vtlogroot import utils + self.assign_ports() run(binary_args('zkctl') + [ '-log_dir', vtlogroot, '-zk.cfg', '1@%s:%s' % (self.hostname, self.zk_ports), diff --git a/test/update_stream.py b/test/update_stream.py index 601130e6511..6c252f6ded4 100755 --- a/test/update_stream.py +++ b/test/update_stream.py @@ -173,7 +173,7 @@ def _test_service_disabled(self): self._exec_vt_txn(self._populate_vt_insert_test) self._exec_vt_txn(['delete from vt_insert_test']) utils.run_vtctl(['ChangeSlaveType', replica_tablet.tablet_alias, 'spare']) - # time.sleep(20) + utils.wait_for_tablet_type(replica_tablet.tablet_alias, 'spare') replica_conn = self._get_replica_stream_conn() logging.debug('dialing replica update stream service') replica_conn.dial() @@ -202,9 +202,9 @@ def _test_service_enabled(self): logging.debug('_test_service_enabled starting @ %s', start_position) utils.run_vtctl(['ChangeSlaveType', replica_tablet.tablet_alias, 'replica']) logging.debug('sleeping a bit for the replica action to complete') - time.sleep(10) + utils.wait_for_tablet_type(replica_tablet.tablet_alias, 'replica', 30) thd = threading.Thread(target=self.perform_writes, name='write_thd', - args=(400,)) + args=(100,)) thd.daemon = True thd.start() replica_conn = self._get_replica_stream_conn() @@ -238,13 +238,12 @@ def _test_service_enabled(self): try: data = replica_conn.stream_start(start_position) utils.run_vtctl(['ChangeSlaveType', replica_tablet.tablet_alias, 'spare']) - #logging.debug("Sleeping a bit for the spare action to complete") - # time.sleep(20) + utils.wait_for_tablet_type(replica_tablet.tablet_alias, 'spare', 30) while data: data = replica_conn.stream_next() if data is not None and data['Category'] == 'POS': txn_count += 1 - logging.error('Test Service Switch: FAIL') + logging.debug('Test Service Switch: FAIL') return except dbexceptions.DatabaseError, e: self.assertEqual( @@ -277,8 +276,16 @@ def _exec_vt_txn(self, query_list=None): # from master and replica for the same writes. Also tests # transactions are retrieved properly. def test_stream_parity(self): - master_start_position = _get_master_current_position() - replica_start_position = _get_repl_current_position() + timeout = 30#s + while True: + master_start_position = _get_master_current_position() + replica_start_position = _get_repl_current_position() + if master_start_position == replica_start_position: + break + timeout = utils.wait_step( + "%s == %s" % (master_start_position, replica_start_position), + timeout + ) logging.debug('run_test_stream_parity starting @ %s', master_start_position) master_txn_count = 0 @@ -369,6 +376,7 @@ def test_service_switch(self): self._test_service_enabled() # The above tests leaves the service in disabled state, hence enabling it. utils.run_vtctl(['ChangeSlaveType', replica_tablet.tablet_alias, 'replica']) + utils.wait_for_tablet_type(replica_tablet.tablet_alias, 'replica', 30) def test_log_rotation(self): start_position = _get_master_current_position() diff --git a/test/utils.py b/test/utils.py index 331eb953b91..42b757b5c82 100644 --- a/test/utils.py +++ b/test/utils.py @@ -25,7 +25,7 @@ options = None devnull = open('/dev/null', 'w') -hostname = socket.gethostname() +hostname = socket.getaddrinfo(socket.getfqdn(), None, 0, 0, 0, socket.AI_CANONNAME)[0][3] class TestError(Exception): pass @@ -57,6 +57,7 @@ def flush(self): pass def add_options(parser): + environment.add_options(parser) parser.add_option('-d', '--debug', action='store_true', help='utils.pause() statements will wait for user input') parser.add_option('-k', '--keep-logs', action='store_true', @@ -66,7 +67,7 @@ def add_options(parser): parser.add_option('--skip-teardown', action='store_true') parser.add_option("--mysql-flavor") parser.add_option("--protocols-flavor") - parser.add_option("--topo-server-flavor") + parser.add_option("--topo-server-flavor", default="zookeeper") def set_options(opts): global options @@ -99,6 +100,9 @@ def main(mod=None): set_options(options) + run_tests(mod, args) + +def run_tests(mod, args): try: suite = unittest.TestSuite() if not args: @@ -196,6 +200,7 @@ def run(cmd, trap_output=False, raise_on_error=True, **kargs): stdout, stderr = proc.communicate() if proc.returncode: if raise_on_error: + pause("cmd fail: %s, pausing..." % (args)) raise TestError('cmd fail:', args, stdout, stderr) else: logging.debug('cmd fail: %s %s %s', str(args), stdout, stderr) @@ -320,19 +325,25 @@ def wait_for_vars(name, port, var=None): break timeout = wait_step('waiting for /debug/vars of %s' % name, timeout) -# zkocc helpers -def zkocc_start(cells=['test_nj'], extra_params=[]): - args = environment.binary_args('zkocc') + [ - '-port', str(environment.topo_server().zkocc_port_base), - '-stderrthreshold=ERROR', - ] + extra_params + cells - sp = run_bg(args) - wait_for_vars("zkocc", environment.topo_server().zkocc_port_base) - return sp +def apply_vschema(vschema): + fname = os.path.join(environment.tmproot, "vschema.json") + with open(fname, "w") as f: + f.write(vschema) + run_vtctl(['ApplyVSchema', "-vschema_file", fname]) -def zkocc_kill(sp): - kill_sub_process(sp) - sp.wait() +def wait_for_tablet_type(tablet_alias, expected_type, timeout=10): + """Waits for a given tablet's SlaveType to become the expected value. + + If the SlaveType does not become expected_type within timeout seconds, + it will raise a TestError. + """ + while True: + if run_vtctl_json(['GetTablet', tablet_alias])['Type'] == expected_type: + break + timeout = wait_step( + "%s's SlaveType to be %s" % (tablet_alias, expected_type), + timeout + ) # vtgate helpers, assuming it always restarts on the same port def vtgate_start(vtport=None, cell='test_nj', retry_delay=1, retry_count=1, @@ -367,6 +378,7 @@ def vtgate_start(vtport=None, cell='test_nj', retry_delay=1, retry_count=1, args.extend(['-ca_cert', ca_cert]) if socket_file: args.extend(['-socket_file', socket_file]) + if extra_args: args.extend(extra_args) @@ -450,6 +462,7 @@ def run_vtctl_json(clargs): def run_vtworker(clargs, log_level='', auto_log=False, expect_fail=False, **kwargs): args = environment.binary_args('vtworker') + [ '-log_dir', environment.vtlogroot, + '-min_healthy_rdonly_endpoints', '1', '-port', str(environment.reserve_ports(1))] args.extend(environment.topo_server().flags()) args.extend(protocols_flavor().tablet_manager_protocol_flags()) @@ -474,7 +487,6 @@ def run_vtworker(clargs, log_level='', auto_log=False, expect_fail=False, **kwar # - vttablet (default), vttablet-streaming # - vtdb, vtdb-streaming (default topo server) # - vtdb-zk, vtdb-zk-streaming (forced zk topo server) -# - vtdb-zkocc, vtdb-zkocc-streaming (forced zkocc topo server) # path is either: keyspace/shard for vttablet* or zk path for vtdb* def vtclient2(uid, path, query, bindvars=None, user=None, password=None, driver=None, verbose=False, raise_on_error=True): @@ -575,13 +587,83 @@ def check_srv_keyspace(cell, keyspace, expected, keyspace_id_type='uint64'): raise Exception("Got wrong ShardingColumnType in SrvKeyspace: %s" % str(ks)) +def check_shard_query_service(testcase, shard_name, tablet_type, expected_state): + """Makes assertions about the state of DisableQueryService in the shard record's TabletControlMap.""" + # We assume that query service should be enabled unless DisableQueryService is explicitly True + query_service_enabled = True + tablet_control_map = run_vtctl_json(['GetShard', shard_name]).get('TabletControlMap') + if tablet_control_map: + disable_query_service = tablet_control_map.get(tablet_type, {}).get('DisableQueryService') + + if disable_query_service: + query_service_enabled = False + + testcase.assertEqual( + query_service_enabled, + expected_state, + 'shard %s does not have the correct query service state: got %s but expected %s' % (shard_name, query_service_enabled, expected_state) + ) + +def check_shard_query_services(testcase, shard_names, tablet_type, expected_state): + for shard_names in shard_names: + check_shard_query_service(testcase, shard_names, tablet_type, expected_state) + +def check_tablet_query_service(testcase, tablet, serving, tablet_control_disabled): + """check_tablet_query_service will check that the query service is enabled + or disabled on the tablet. It will also check if the tablet control + status is the reason for being enabled / disabled. + + It will also run a remote RunHealthCheck to be sure it doesn't change + the serving state. + """ + tablet_vars = get_vars(tablet.port) + if serving: + expected_state = 'SERVING' + else: + expected_state = 'NOT_SERVING' + testcase.assertEqual(tablet_vars['TabletStateName'], expected_state, 'tablet %s is not in the right serving state: got %s expected %s' % (tablet.tablet_alias, tablet_vars['TabletStateName'], expected_state)) + + status = tablet.get_status() + if tablet_control_disabled: + testcase.assertIn("Query Service disabled by TabletControl", status) + else: + testcase.assertNotIn("Query Service disabled by TabletControl", status) + + if tablet.tablet_type == 'rdonly': + run_vtctl(['RunHealthCheck', tablet.tablet_alias, 'rdonly'], + auto_log=True) + + tablet_vars = get_vars(tablet.port) + testcase.assertEqual(tablet_vars['TabletStateName'], expected_state, 'tablet %s is not in the right serving state after health check: got %s expected %s' % (tablet.tablet_alias, tablet_vars['TabletStateName'], expected_state)) + +def check_tablet_query_services(testcase, tablets, serving, tablet_control_disabled): + for tablet in tablets: + check_tablet_query_service(testcase, tablet, serving, tablet_control_disabled) + def get_status(port): return urllib2.urlopen('http://localhost:%u%s' % (port, environment.status_url)).read() -def curl(url, background=False, **kwargs): +def curl(url, request=None, data=None, background=False, retry_timeout=0, **kwargs): + args = [environment.curl_bin, '--silent', '--no-buffer', '--location'] + if not background: + args.append('--show-error') + if request: + args.extend(['--request', request]) + if data: + args.extend(['--data', data]) + args.append(url) + if background: - return run_bg([environment.curl_bin, '-s', '-N', '-L', url], **kwargs) - return run([environment.curl_bin, '-s', '-N', '-L', url], **kwargs) + return run_bg(args, **kwargs) + + if retry_timeout > 0: + while True: + try: + return run(args, trap_output=True, **kwargs) + except TestError as e: + retry_timeout = wait_step('cmd: %s, error: %s' % (str(args), str(e)), retry_timeout) + + return run(args, trap_output=True, **kwargs) class VtctldError(Exception): pass @@ -612,6 +694,7 @@ def start(self): args = environment.binary_args('vtctld') + [ '-debug', '-templates', environment.vttop + '/go/cmd/vtctld/templates', + '-schema-editor-dir', environment.vttop + '/go/vt/vtgate', '-log_dir', environment.vtlogroot, '-port', str(self.port), ] + \ diff --git a/test/vertical_split.py b/test/vertical_split.py index 8e2c9a45b9d..8d6fb3d7018 100755 --- a/test/vertical_split.py +++ b/test/vertical_split.py @@ -22,17 +22,18 @@ VTGATE = "vtgate" VTGATE_PROTOCOL_TABLET = 'v0' client_type = TABLET -use_clone_worker = False # source keyspace, with 4 tables source_master = tablet.Tablet() source_replica = tablet.Tablet() -source_rdonly = tablet.Tablet() +source_rdonly1 = tablet.Tablet() +source_rdonly2 = tablet.Tablet() # destination keyspace, with just two tables destination_master = tablet.Tablet() destination_replica = tablet.Tablet() -destination_rdonly = tablet.Tablet() +destination_rdonly1 = tablet.Tablet() +destination_rdonly2 = tablet.Tablet() def setUpModule(): try: @@ -41,11 +42,12 @@ def setUpModule(): setup_procs = [ source_master.init_mysql(), source_replica.init_mysql(), - source_rdonly.init_mysql(), + source_rdonly1.init_mysql(), + source_rdonly2.init_mysql(), destination_master.init_mysql(), destination_replica.init_mysql(), - destination_rdonly.init_mysql(), - + destination_rdonly1.init_mysql(), + destination_rdonly2.init_mysql(), ] utils.Vtctld().start() utils.wait_procs(setup_procs) @@ -61,10 +63,12 @@ def tearDownModule(): teardown_procs = [ source_master.teardown_mysql(), source_replica.teardown_mysql(), - source_rdonly.teardown_mysql(), + source_rdonly1.teardown_mysql(), + source_rdonly2.teardown_mysql(), destination_master.teardown_mysql(), destination_replica.teardown_mysql(), - destination_rdonly.teardown_mysql(), + destination_rdonly1.teardown_mysql(), + destination_rdonly2.teardown_mysql(), ] utils.wait_procs(teardown_procs, raise_on_error=False) @@ -74,10 +78,12 @@ def tearDownModule(): source_master.remove_tree() source_replica.remove_tree() - source_rdonly.remove_tree() + source_rdonly1.remove_tree() + source_rdonly2.remove_tree() destination_master.remove_tree() destination_replica.remove_tree() - destination_rdonly.remove_tree() + destination_rdonly1.remove_tree() + destination_rdonly2.remove_tree() class TestVerticalSplit(unittest.TestCase): def setUp(self): @@ -87,7 +93,7 @@ def setUp(self): self.vtgate_addrs = None if client_type == VTGATE: global vtgate_addrs - self.vtgate_addrs = {"_vt": ["localhost:%s"%(self.vtgate_port),]} + self.vtgate_addrs = {"vt": ["localhost:%s"%(self.vtgate_port),]} self.insert_index = 0 # Lowering the keyspace refresh throttle so things are testable. @@ -247,7 +253,8 @@ def test_vertical_split(self): 'destination_keyspace']) source_master.init_tablet('master', 'source_keyspace', '0') source_replica.init_tablet('replica', 'source_keyspace', '0') - source_rdonly.init_tablet('rdonly', 'source_keyspace', '0') + source_rdonly1.init_tablet('rdonly', 'source_keyspace', '0') + source_rdonly2.init_tablet('rdonly', 'source_keyspace', '0') # rebuild destination keyspace to make sure there is a serving # graph entry, even though there is no tablet yet. @@ -260,7 +267,8 @@ def test_vertical_split(self): destination_master.init_tablet('master', 'destination_keyspace', '0') destination_replica.init_tablet('replica', 'destination_keyspace', '0') - destination_rdonly.init_tablet('rdonly', 'destination_keyspace', '0') + destination_rdonly1.init_tablet('rdonly', 'destination_keyspace', '0') + destination_rdonly2.init_tablet('rdonly', 'destination_keyspace', '0') utils.run_vtctl(['RebuildKeyspaceGraph', 'source_keyspace'], auto_log=True) utils.run_vtctl(['RebuildKeyspaceGraph', 'destination_keyspace'], @@ -270,18 +278,19 @@ def test_vertical_split(self): 'ServedFrom(replica): source_keyspace\n') # create databases so vttablet can start behaving normally - for t in [source_master, source_replica, source_rdonly]: + for t in [source_master, source_replica, source_rdonly1, source_rdonly2]: t.create_db('vt_source_keyspace') t.start_vttablet(wait_for_state=None) destination_master.start_vttablet(wait_for_state=None, target_tablet_type='replica') - for t in [destination_replica, destination_rdonly]: + for t in [destination_replica, destination_rdonly1, destination_rdonly2]: t.start_vttablet(wait_for_state=None) # wait for the tablets - for t in [source_master, source_replica, source_rdonly]: + for t in [source_master, source_replica, source_rdonly1, source_rdonly2]: t.wait_for_vttablet_state('SERVING') - for t in [destination_master, destination_replica, destination_rdonly]: + for t in [destination_master, destination_replica, destination_rdonly1, + destination_rdonly2]: t.wait_for_vttablet_state('NOT_SERVING') # reparent to make the tablets work @@ -310,35 +319,27 @@ def test_vertical_split(self): self._check_values(source_master, 'vt_source_keyspace', 'view1', moving1_first, 100) - if use_clone_worker: - # the worker will do everything. We test with source_reader_count=10 - # (down from default=20) as connection pool is not big enough for 20. - # min_table_size_for_split is set to 1 as to force a split even on the - # small table we have. - utils.run_vtworker(['--cell', 'test_nj', - '--command_display_interval', '10ms', - 'VerticalSplitClone', - '--tables', 'moving.*,view1', - '--strategy=-populate_blp_checkpoint', - '--source_reader_count', '10', - '--min_table_size_for_split', '1', - 'destination_keyspace/0'], - auto_log=True) - utils.run_vtctl(['ChangeSlaveType', source_rdonly.tablet_alias, 'rdonly'], - auto_log=True) + # the worker will do everything. We test with source_reader_count=10 + # (down from default=20) as connection pool is not big enough for 20. + # min_table_size_for_split is set to 1 as to force a split even on the + # small table we have. + utils.run_vtctl(['CopySchemaShard', '--tables', 'moving.*,view1', + source_rdonly1.tablet_alias, 'destination_keyspace/0'], + auto_log=True) - else: - # take the snapshot for the split - utils.run_vtctl(['MultiSnapshot', - '--tables', 'moving.*,view1', - source_rdonly.tablet_alias], auto_log=True) - - # perform the restore. - utils.run_vtctl(['ShardMultiRestore', - '--strategy=-populate_blp_checkpoint', - '--tables', 'moving.*,view1', - 'destination_keyspace/0', source_rdonly.tablet_alias], - auto_log=True) + utils.run_vtworker(['--cell', 'test_nj', + '--command_display_interval', '10ms', + 'VerticalSplitClone', + '--tables', 'moving.*,view1', + '--strategy=-populate_blp_checkpoint', + '--source_reader_count', '10', + '--min_table_size_for_split', '1', + 'destination_keyspace/0'], + auto_log=True) + utils.run_vtctl(['ChangeSlaveType', source_rdonly1.tablet_alias, + 'rdonly'], auto_log=True) + utils.run_vtctl(['ChangeSlaveType', source_rdonly2.tablet_alias, + 'rdonly'], auto_log=True) topology.refresh_keyspace(self.vtgate_client, 'destination_keyspace') @@ -366,9 +367,13 @@ def test_vertical_split(self): logging.debug("Running vtworker VerticalSplitDiff") utils.run_vtworker(['-cell', 'test_nj', 'VerticalSplitDiff', 'destination_keyspace/0'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', source_rdonly.tablet_alias, 'rdonly'], + utils.run_vtctl(['ChangeSlaveType', source_rdonly1.tablet_alias, 'rdonly'], auto_log=True) - utils.run_vtctl(['ChangeSlaveType', destination_rdonly.tablet_alias, + utils.run_vtctl(['ChangeSlaveType', source_rdonly2.tablet_alias, 'rdonly'], + auto_log=True) + utils.run_vtctl(['ChangeSlaveType', destination_rdonly1.tablet_alias, + 'rdonly'], auto_log=True) + utils.run_vtctl(['ChangeSlaveType', destination_rdonly2.tablet_alias, 'rdonly'], auto_log=True) utils.pause("Good time to test vtworker for diffs") @@ -399,7 +404,8 @@ def test_vertical_split(self): 'ServedFrom(replica): source_keyspace\n') self._check_blacklisted_tables(source_master, None) self._check_blacklisted_tables(source_replica, None) - self._check_blacklisted_tables(source_rdonly, None) + self._check_blacklisted_tables(source_rdonly1, None) + self._check_blacklisted_tables(source_rdonly2, None) # migrate test_nj only, using command line manual fix command, # and restore it back. @@ -425,7 +431,8 @@ def test_vertical_split(self): 'ServedFrom(replica): source_keyspace\n') self._check_blacklisted_tables(source_master, None) self._check_blacklisted_tables(source_replica, None) - self._check_blacklisted_tables(source_rdonly, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly1, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly2, ['moving.*', 'view1']) self._check_client_conn_redirection( 'source_keyspace', 'destination_keyspace', ['rdonly'], ['master', 'replica'], ['moving1', 'moving2']) @@ -436,7 +443,8 @@ def test_vertical_split(self): self._check_srv_keyspace('ServedFrom(master): source_keyspace\n') self._check_blacklisted_tables(source_master, None) self._check_blacklisted_tables(source_replica, ['moving.*', 'view1']) - self._check_blacklisted_tables(source_rdonly, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly1, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly2, ['moving.*', 'view1']) self._check_client_conn_redirection('source_keyspace', 'destination_keyspace', ['replica', 'rdonly'], ['master'], ['moving1', 'moving2']) # move replica back and forth @@ -446,13 +454,15 @@ def test_vertical_split(self): 'ServedFrom(replica): source_keyspace\n') self._check_blacklisted_tables(source_master, None) self._check_blacklisted_tables(source_replica, None) - self._check_blacklisted_tables(source_rdonly, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly1, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly2, ['moving.*', 'view1']) utils.run_vtctl(['MigrateServedFrom', 'destination_keyspace/0', 'replica'], auto_log=True) self._check_srv_keyspace('ServedFrom(master): source_keyspace\n') self._check_blacklisted_tables(source_master, None) self._check_blacklisted_tables(source_replica, ['moving.*', 'view1']) - self._check_blacklisted_tables(source_rdonly, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly1, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly2, ['moving.*', 'view1']) self._check_client_conn_redirection( 'source_keyspace', 'destination_keyspace', ['replica', 'rdonly'], ['master'], ['moving1', 'moving2']) @@ -463,7 +473,8 @@ def test_vertical_split(self): self._check_srv_keyspace('') self._check_blacklisted_tables(source_master, ['moving.*', 'view1']) self._check_blacklisted_tables(source_replica, ['moving.*', 'view1']) - self._check_blacklisted_tables(source_rdonly, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly1, ['moving.*', 'view1']) + self._check_blacklisted_tables(source_rdonly2, ['moving.*', 'view1']) self._check_client_conn_redirection( 'source_keyspace', 'destination_keyspace', ['replica', 'rdonly', 'master'], [], ['moving1', 'moving2']) @@ -497,9 +508,10 @@ def test_vertical_split(self): self._check_stats() # kill everything - tablet.kill_tablets([source_master, source_replica, source_rdonly, - destination_master, destination_replica, - destination_rdonly]) + tablet.kill_tablets([source_master, source_replica, source_rdonly1, + source_rdonly2, destination_master, + destination_replica, destination_rdonly1, + destination_rdonly2]) if __name__ == '__main__': utils.main() diff --git a/test/vertical_split_vtgate.py b/test/vertical_split_vtgate.py index 0a2b1ae9d7c..271c5b9f003 100755 --- a/test/vertical_split_vtgate.py +++ b/test/vertical_split_vtgate.py @@ -20,13 +20,13 @@ def tearDownModule(): class TestVerticalSplitVTGate(vertical_split.TestVerticalSplit): def _vtdb_conn(self): - conn = vtgatev2.connect(self.vtgate_addrs['_vt'], 30) + conn = vtgatev2.connect(self.vtgate_addrs['vt'], 30) return conn def _insert_values(self, table, count, db_type='master', keyspace='source_keyspace'): result = self.insert_index conn = self._vtdb_conn() - cursor = conn.cursor(None, conn, keyspace, db_type, keyranges=[keyrange.KeyRange(keyrange_constants.NON_PARTIAL_KEYRANGE)], writable=True) + cursor = conn.cursor(keyspace, db_type, keyranges=[keyrange.KeyRange(keyrange_constants.NON_PARTIAL_KEYRANGE)], writable=True) for i in xrange(count): conn.begin() cursor.execute("insert into %s (id, msg) values(%u, 'value %u')" % ( diff --git a/test/vertical_split_vtworker.py b/test/vertical_split_vtworker.py deleted file mode 100755 index 91497fdcdcd..00000000000 --- a/test/vertical_split_vtworker.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2014, Google Inc. All rights reserved. -# Use of this source code is governed by a BSD-style license that can -# be found in the LICENSE file. - -import utils -import vertical_split - -# this test is the same as vertical_split.py, but it uses vtworker to -# do the clone. -if __name__ == '__main__': - vertical_split.use_clone_worker = True - utils.main(vertical_split) diff --git a/test/vtctld_test.py b/test/vtctld_test.py index 7ea2e14ac66..413c58a318e 100755 --- a/test/vtctld_test.py +++ b/test/vtctld_test.py @@ -2,8 +2,10 @@ import json import logging import os +import socket import unittest import urllib2 +import re import environment import tablet @@ -167,7 +169,7 @@ def test_not_assigned(self): self.assertEqual(len(self.data["Scrap"]), 1) def test_partial(self): - utils.pause("You can now run a browser and connect to http://localhost:%u to manually check topology" % utils.vtctld.port) + utils.pause("You can now run a browser and connect to http://%s:%u to manually check topology" % (socket.getfqdn(), utils.vtctld.port)) self.assertEqual(self.data["Partial"], True) def test_explorer_redirects(self): @@ -211,7 +213,7 @@ def test_serving_graph(self): def test_tablet_status(self): # the vttablet that has a health check has a bit more, so using it shard_0_replica_status = shard_0_replica.get_status() - self.assertIn('Polling health information from MySQLReplicationLag(allowedLag=30)', shard_0_replica_status) + self.assertTrue(re.search(r'Polling health information from.+MySQLReplicationLag', shard_0_replica_status)) self.assertIn('Alias: test_nj', status) # vtctld link - utils.pause("You can now run a browser and connect to http://localhost:%u%s to manually check vtgate status page" % (vtgate_port, environment.status_url)) + utils.pause("You can now run a browser and connect to http://%s:%u%s to manually check vtgate status page" % (socket.getfqdn(), vtgate_port, environment.status_url)) if __name__ == '__main__': utils.main() diff --git a/test/vtdb_test.py b/test/vtdb_test.py index 1d07f4a7f60..207e653b7aa 100755 --- a/test/vtdb_test.py +++ b/test/vtdb_test.py @@ -160,7 +160,7 @@ def get_connection(db_type='master', shard_index=0, user=None, password=None): timeout = 10.0 conn = None shard = shard_names[shard_index] - vtgate_addrs = {"_vt": ["localhost:%s" % (vtgate_port),]} + vtgate_addrs = {"vt": ["localhost:%s" % (vtgate_port),]} vtgate_client = zkocc.ZkOccConnection("localhost:%u" % vtgate_port, "test_nj", 30.0) conn = vtclient.VtOCCConnection(vtgate_client, 'test_keyspace', shard, diff --git a/test/vtgatev2_test.py b/test/vtgatev2_test.py index 9fb698d3498..bde2061457a 100755 --- a/test/vtgatev2_test.py +++ b/test/vtgatev2_test.py @@ -24,7 +24,7 @@ from vtdb import vtdb_logger from vtdb import vtgatev2 from vtdb import vtgate_cursor - +from zk import zkocc conn_class = vtgatev2 @@ -157,7 +157,7 @@ def get_connection(user=None, password=None): global vtgate_port timeout = 10.0 conn = None - vtgate_addrs = {"_vt": ["localhost:%s" % (vtgate_port),]} + vtgate_addrs = {"vt": ["localhost:%s" % (vtgate_port),]} conn = conn_class.connect(vtgate_addrs, timeout, user=user, password=password) return conn @@ -171,21 +171,32 @@ def get_keyrange(shard_name): return kr +def _delete_all(shard_index, table_name): + vtgate_conn = get_connection() + # This write is to set up the test with fresh insert + # and hence performing it directly on the connection. + vtgate_conn.begin() + vtgate_conn._execute("delete from %s" % table_name, {}, + KEYSPACE_NAME, 'master', + keyranges=[get_keyrange(shard_names[shard_index])]) + vtgate_conn.commit() + + def do_write(count, shard_index): kid_list = shard_kid_map[shard_names[shard_index]] + _delete_all(shard_index, 'vt_insert_test') vtgate_conn = get_connection() - vtgate_conn.begin() - vtgate_conn._execute( - "delete from vt_insert_test", {}, - KEYSPACE_NAME, 'master', - keyranges=[get_keyrange(shard_names[shard_index])]) + for x in xrange(count): keyspace_id = kid_list[x%len(kid_list)] - vtgate_conn._execute( + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyspace_ids=[pack_kid(keyspace_id)], + writable=True) + cursor.begin() + cursor.execute( "insert into vt_insert_test (msg, keyspace_id) values (%(msg)s, %(keyspace_id)s)", - {'msg': 'test %s' % x, 'keyspace_id': keyspace_id}, - KEYSPACE_NAME, 'master', keyspace_ids=[pack_kid(keyspace_id)]) - vtgate_conn.commit() + {'msg': 'test %s' % x, 'keyspace_id': keyspace_id}) + cursor.commit() def restart_vtgate(extra_args={}): @@ -214,24 +225,22 @@ def test_connect(self): def test_writes(self): try: vtgate_conn = get_connection() + _delete_all(self.shard_index, 'vt_insert_test') count = 10 - vtgate_conn.begin() - vtgate_conn._execute( - "delete from vt_insert_test", {}, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange,]) kid_list = shard_kid_map[shard_names[self.shard_index]] for x in xrange(count): keyspace_id = kid_list[count%len(kid_list)] - vtgate_conn._execute( + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyspace_ids=[pack_kid(keyspace_id)], + writable=True) + cursor.begin() + cursor.execute( "insert into vt_insert_test (msg, keyspace_id) values (%(msg)s, %(keyspace_id)s)", - {'msg': 'test %s' % x, 'keyspace_id': keyspace_id}, - KEYSPACE_NAME, 'master', keyspace_ids=[pack_kid(keyspace_id)]) - vtgate_conn.commit() - results, rowcount = vtgate_conn._execute( - "select * from vt_insert_test", {}, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange])[:2] + {'msg': 'test %s' % x, 'keyspace_id': keyspace_id}) + cursor.commit() + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyranges=[self.keyrange]) + rowcount = cursor.execute("select * from vt_insert_test", {}) self.assertEqual(rowcount, count, "master fetch works") except Exception, e: logging.debug("Write failed with error %s" % str(e)) @@ -243,20 +252,22 @@ def test_query_routing(self): row_counts = [50, 75] for shard_index in [0,1]: do_write(row_counts[shard_index], shard_index) - master_conn = get_connection() + vtgate_conn = get_connection() for shard_index in [0,1]: # Fetch all rows in each shard - results, rowcount = master_conn._execute("select * from vt_insert_test", {}, - KEYSPACE_NAME, 'master', keyranges=[get_keyrange(shard_names[shard_index])])[:2] + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyranges=[get_keyrange(shard_names[shard_index])]) + rowcount = cursor.execute("select * from vt_insert_test", {}) # Verify row count self.assertEqual(rowcount, row_counts[shard_index]) # Verify keyspace id - for result in results: + for result in cursor.results: kid = result[2] self.assertTrue(kid in shard_kid_map[shard_names[shard_index]]) # Do a cross shard range query and assert all rows are fetched - results, rowcount = master_conn._execute("select * from vt_insert_test", {}, - KEYSPACE_NAME, 'master', keyranges=[get_keyrange('75-95')])[:2] + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyranges=[get_keyrange('75-95')]) + rowcount = cursor.execute("select * from vt_insert_test", {}) self.assertEqual(rowcount, row_counts[0] + row_counts[1]) except Exception, e: logging.debug("failed with error %s, %s" % (str(e), traceback.print_exc())) @@ -266,29 +277,28 @@ def test_rollback(self): try: vtgate_conn = get_connection() count = 10 - vtgate_conn.begin() - vtgate_conn._execute( - "delete from vt_insert_test", {}, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange]) + _delete_all(self.shard_index, 'vt_insert_test') kid_list = shard_kid_map[shard_names[self.shard_index]] for x in xrange(count): keyspace_id = kid_list[x%len(kid_list)] - vtgate_conn._execute( + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyspace_ids=[pack_kid(keyspace_id)], + writable=True) + cursor.begin() + cursor.execute( "insert into vt_insert_test (msg, keyspace_id) values (%(msg)s, %(keyspace_id)s)", - {'msg': 'test %s' % x, 'keyspace_id': keyspace_id}, - KEYSPACE_NAME, 'master', keyspace_ids=[pack_kid(keyspace_id)]) - vtgate_conn.commit() + {'msg': 'test %s' % x, 'keyspace_id': keyspace_id}) + cursor.commit() + vtgate_conn.begin() vtgate_conn._execute( "delete from vt_insert_test", {}, KEYSPACE_NAME, 'master', keyranges=[self.keyrange]) vtgate_conn.rollback() - results, rowcount = vtgate_conn._execute( - "select * from vt_insert_test", {}, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange])[:2] + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyranges=[self.keyrange]) + rowcount = cursor.execute("select * from vt_insert_test", {}) logging.debug("ROLLBACK TEST rowcount %d count %d" % (rowcount, count)) self.assertEqual(rowcount, count, "Fetched rows(%d) != inserted rows(%d), rollback didn't work" % (rowcount, count)) do_write(10, self.shard_index) @@ -299,25 +309,25 @@ def test_execute_entity_ids(self): try: vtgate_conn = get_connection() count = 10 - vtgate_conn.begin() - vtgate_conn._execute( - "delete from vt_a", {}, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange]) + _delete_all(self.shard_index, 'vt_a') eid_map = {} kid_list = shard_kid_map[shard_names[self.shard_index]] for x in xrange(count): keyspace_id = kid_list[x%len(kid_list)] eid_map[x] = pack_kid(keyspace_id) - vtgate_conn._execute( + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyspace_ids=[pack_kid(keyspace_id)], + writable=True) + cursor.begin() + cursor.execute( "insert into vt_a (eid, id, keyspace_id) \ values (%(eid)s, %(id)s, %(keyspace_id)s)", - {'eid': x, 'id': x, 'keyspace_id': keyspace_id}, - KEYSPACE_NAME, 'master', keyspace_ids=[pack_kid(keyspace_id)]) - vtgate_conn.commit() - results, rowcount, _, _ = vtgate_conn._execute_entity_ids( - "select * from vt_a", {}, - KEYSPACE_NAME, 'master', eid_map, 'id') + {'eid': x, 'id': x, 'keyspace_id': keyspace_id}) + cursor.commit() + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyspace_ids=eid_map.values()) + rowcount = cursor.execute_entity_ids("select * from vt_a", {}, eid_map, + 'id') self.assertEqual(rowcount, count, "entity_ids works") except Exception, e: self.fail("Execute entity ids failed with error %s %s" % @@ -328,38 +338,39 @@ def test_batch_read(self): try: vtgate_conn = get_connection() count = 10 - vtgate_conn.begin() - vtgate_conn._execute( - "delete from vt_insert_test", {}, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange]) + _delete_all(self.shard_index, 'vt_insert_test') kid_list = shard_kid_map[shard_names[self.shard_index]] for x in xrange(count): keyspace_id = kid_list[x%len(kid_list)] - vtgate_conn._execute( + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyspace_ids=[pack_kid(keyspace_id)], + writable=True) + cursor.begin() + cursor.execute( "insert into vt_insert_test (msg, keyspace_id) values (%(msg)s, %(keyspace_id)s)", - {'msg': 'test %s' % x, 'keyspace_id': keyspace_id}, - KEYSPACE_NAME, 'master', keyspace_ids=[pack_kid(keyspace_id)]) - vtgate_conn.commit() - vtgate_conn.begin() - vtgate_conn._execute( - "delete from vt_a", {}, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange]) + {'msg': 'test %s' % x, 'keyspace_id': keyspace_id}) + cursor.commit() + _delete_all(self.shard_index, 'vt_a') for x in xrange(count): keyspace_id = kid_list[x%len(kid_list)] - vtgate_conn._execute( + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyspace_ids=[pack_kid(keyspace_id)], + writable=True) + cursor.begin() + cursor.execute( "insert into vt_a (eid, id, keyspace_id) \ values (%(eid)s, %(id)s, %(keyspace_id)s)", - {'eid': x, 'id': x, 'keyspace_id': keyspace_id}, - KEYSPACE_NAME, 'master', keyspace_ids=[pack_kid(keyspace_id)]) - vtgate_conn.commit() - rowsets = vtgate_conn._execute_batch( - ["select * from vt_insert_test", - "select * from vt_a"], [{}, {}], - KEYSPACE_NAME, 'master', keyspace_ids=[pack_kid(kid) for kid in kid_list]) - self.assertEqual(rowsets[0][1], count) - self.assertEqual(rowsets[1][1], count) + {'eid': x, 'id': x, 'keyspace_id': keyspace_id}) + cursor.commit() + kid_list = [pack_kid(kid) for kid in kid_list] + cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyspace_ids=kid_list, + cursorclass=vtgate_cursor.BatchVTGateCursor) + cursor.execute("select * from vt_insert_test", {}) + cursor.execute("select * from vt_a", {}) + cursor.flush() + self.assertEqual(cursor.rowsets[0][1], count) + self.assertEqual(cursor.rowsets[1][1], count) except Exception, e: self.fail("Write failed with error %s %s" % (str(e), traceback.print_exc())) @@ -407,10 +418,9 @@ def test_streaming_fetchsubset(self): do_write(count, self.shard_index) # Fetch a subset of the total size. vtgate_conn = get_connection() - stream_cursor = vtgate_cursor.StreamVTGateCursor( - vtgate_conn, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange]) + stream_cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyranges=[self.keyrange], + cursorclass=vtgate_cursor.StreamVTGateCursor) stream_cursor.execute("select * from vt_insert_test", {}) fetch_size = 10 rows = stream_cursor.fetchmany(size=fetch_size) @@ -428,10 +438,9 @@ def test_streaming_fetchall(self): do_write(count, self.shard_index) # Fetch all. vtgate_conn = get_connection() - stream_cursor = vtgate_cursor.StreamVTGateCursor( - vtgate_conn, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange]) + stream_cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyranges=[self.keyrange], + cursorclass=vtgate_cursor.StreamVTGateCursor) stream_cursor.execute("select * from vt_insert_test", {}) rows = stream_cursor.fetchall() rowcount = 0 @@ -448,10 +457,9 @@ def test_streaming_fetchone(self): do_write(count, self.shard_index) # Fetch one. vtgate_conn = get_connection() - stream_cursor = vtgate_cursor.StreamVTGateCursor( - vtgate_conn, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange]) + stream_cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyranges=[self.keyrange], + cursorclass=vtgate_cursor.StreamVTGateCursor) stream_cursor.execute("select * from vt_insert_test", {}) rows = stream_cursor.fetchone() self.assertTrue(type(rows) == tuple, "Received a valid row") @@ -459,6 +467,26 @@ def test_streaming_fetchone(self): except Exception, e: self.fail("Failed with error %s %s" % (str(e), traceback.print_exc())) + def test_streaming_multishards(self): + try: + count = 100 + do_write(count, 0) + do_write(count, 1) + vtgate_conn = get_connection() + stream_cursor = vtgate_conn.cursor( + KEYSPACE_NAME, 'master', + keyranges=[keyrange.KeyRange(keyrange_constants.NON_PARTIAL_KEYRANGE)], + cursorclass=vtgate_cursor.StreamVTGateCursor) + stream_cursor.execute("select * from vt_insert_test", {}) + rows = stream_cursor.fetchall() + rowcount = 0 + for row in rows: + rowcount += 1 + self.assertEqual(rowcount, count*2) + stream_cursor.close() + except Exception, e: + self.fail("Failed with error %s %s" % (str(e), traceback.print_exc())) + def test_streaming_zero_results(self): try: vtgate_conn = get_connection() @@ -468,10 +496,9 @@ def test_streaming_zero_results(self): keyranges=[self.keyrange]) vtgate_conn.commit() # After deletion, should result zero. - stream_cursor = vtgate_cursor.StreamVTGateCursor( - vtgate_conn, - KEYSPACE_NAME, 'master', - keyranges=[self.keyrange]) + stream_cursor = vtgate_conn.cursor(KEYSPACE_NAME, 'master', + keyranges=[self.keyrange], + cursorclass=vtgate_cursor.StreamVTGateCursor) stream_cursor.execute("select * from vt_insert_test", {}) rows = stream_cursor.fetchall() rowcount = 0 @@ -599,9 +626,10 @@ def test_tablet_restart_stream_execute(self): vtgate_conn = get_connection() except Exception, e: self.fail("Connection to vtgate failed with error %s" % (str(e))) - stream_cursor = vtgate_cursor.StreamVTGateCursor( - vtgate_conn, KEYSPACE_NAME, 'replica', - keyranges=[self.keyrange]) + stream_cursor = vtgate_conn.cursor( + KEYSPACE_NAME, 'replica', + keyranges=[self.keyrange], + cursorclass=vtgate_cursor.StreamVTGateCursor) self.replica_tablet.kill_vttablet() with self.assertRaises(dbexceptions.DatabaseError): stream_cursor.execute("select * from vt_insert_test", {}) @@ -620,18 +648,18 @@ def test_vtgate_restart_stream_execute(self): except Exception, e: self.fail("Connection to vtgate failed with error %s" % (str(e))) stream_cursor = vtgate_conn.cursor( - vtgate_cursor.StreamVTGateCursor, vtgate_conn, KEYSPACE_NAME, 'replica', - keyranges=[self.keyrange]) + keyranges=[self.keyrange], + cursorclass=vtgate_cursor.StreamVTGateCursor) utils.vtgate_kill(vtgate_server) with self.assertRaises(dbexceptions.OperationalError): stream_cursor.execute("select * from vt_insert_test", {}) vtgate_server, vtgate_port = utils.vtgate_start(vtgate_port) vtgate_conn = get_connection() stream_cursor = vtgate_conn.cursor( - vtgate_cursor.StreamVTGateCursor, vtgate_conn, KEYSPACE_NAME, 'replica', - keyranges=[self.keyrange]) + keyranges=[self.keyrange], + cursorclass=vtgate_cursor.StreamVTGateCursor) try: stream_cursor.execute("select * from vt_insert_test", {}) except Exception, e: diff --git a/test/vtgatev3_test.py b/test/vtgatev3_test.py new file mode 100755 index 00000000000..102856e375b --- /dev/null +++ b/test/vtgatev3_test.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python +# coding: utf-8 + +import hmac +import json +import logging +import os +import struct +import threading +import time +import traceback +import unittest +import urllib + +import environment +import keyspace_util +import tablet +import utils + +from vtdb import cursorv3 +from vtdb import dbexceptions +from vtdb import vtgatev3 + +shard_0_master = tablet.Tablet() +shard_1_master = tablet.Tablet() +lookup_master = tablet.Tablet() + +vtgate_server = None +vtgate_port = None +keyspace_env = None + +create_vt_user = '''create table vt_user ( +id bigint, +name varchar(64), +primary key (id) +) Engine=InnoDB''' + +create_vt_user2 = '''create table vt_user2 ( +id bigint, +name varchar(64), +primary key (id) +) Engine=InnoDB''' + +create_vt_user_extra = '''create table vt_user_extra ( +user_id bigint, +email varchar(64), +primary key (user_id) +) Engine=InnoDB''' + +create_vt_music = '''create table vt_music ( +user_id bigint, +id bigint, +song varchar(64), +primary key (user_id, id) +) Engine=InnoDB''' + +create_vt_music_extra = '''create table vt_music_extra ( +music_id bigint, +user_id bigint, +artist varchar(64), +primary key (music_id) +) Engine=InnoDB''' + +create_vt_user_idx = '''create table vt_user_idx ( +id bigint auto_increment, +primary key (id) +) Engine=InnoDB''' + +create_name_user2_map = '''create table name_user2_map ( +name varchar(64), +user2_id bigint, +primary key (name, user2_id) +) Engine=InnoDB''' + +create_music_user_map = '''create table music_user_map ( +music_id bigint auto_increment, +user_id bigint, +primary key (music_id) +) Engine=InnoDB''' + +schema = '''{ + "Keyspaces": { + "user": { + "Sharded": true, + "Vindexes": { + "user_index": { + "Type": "hash_autoinc", + "Params": { + "Table": "vt_user_idx", + "Column": "id" + }, + "Owner": "vt_user" + }, + "name_user2_map": { + "Type": "lookup_hash", + "Params": { + "Table": "name_user2_map", + "From": "name", + "To": "user2_id" + }, + "Owner": "vt_user2" + }, + "music_user_map": { + "Type": "lookup_hash_unique_autoinc", + "Params": { + "Table": "music_user_map", + "From": "music_id", + "To": "user_id" + }, + "Owner": "vt_music" + } + }, + "Classes": { + "vt_user": { + "ColVindexes": [ + { + "Col": "id", + "Name": "user_index" + } + ] + }, + "vt_user2": { + "ColVindexes": [ + { + "Col": "id", + "Name": "user_index" + }, + { + "Col": "name", + "Name": "name_user2_map" + } + ] + }, + "vt_user_extra": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "user_index" + } + ] + }, + "vt_music": { + "ColVindexes": [ + { + "Col": "user_id", + "Name": "user_index" + }, + { + "Col": "id", + "Name": "music_user_map" + } + ] + }, + "vt_music_extra": { + "ColVindexes": [ + { + "Col": "music_id", + "Name": "music_user_map" + }, + { + "Col": "user_id", + "Name": "user_index" + } + ] + } + }, + "Tables": { + "vt_user": "vt_user", + "vt_user2": "vt_user2", + "vt_user_extra": "vt_user_extra", + "vt_music": "vt_music", + "vt_music_extra": "vt_music_extra" + } + }, + "lookup": { + "Sharded": false, + "Tables": { + "vt_user_idx": "", + "music_user_map": "", + "name_user2_map": "" + } + } + } +}''' + +# Verify valid json +json.loads(schema) + + +def setUpModule(): + global keyspace_env + global shard_0_master + global shard_1_master + global lookup_master + global vtgate_server + global vtgate_port + logging.debug("in setUpModule") + + try: + environment.topo_server().setup() + logging.debug("Setting up tablets") + keyspace_env = keyspace_util.TestEnv() + keyspace_env.launch( + "user", + shards=["-80", "80-"], + ddls=[ + create_vt_user, + create_vt_user2, + create_vt_user_extra, + create_vt_music, + create_vt_music_extra, + ], + ) + keyspace_env.launch( + "lookup", + ddls=[ + create_vt_user_idx, + create_music_user_map, + create_name_user2_map, + ], + ) + shard_0_master = keyspace_env.tablet_map["user.-80.master"] + shard_1_master = keyspace_env.tablet_map["user.80-.master"] + lookup_master = keyspace_env.tablet_map["lookup.0.master"] + + utils.apply_vschema(schema) + vtgate_server, vtgate_port = utils.vtgate_start() + except: + tearDownModule() + raise + +def tearDownModule(): + logging.debug("in tearDownModule") + if utils.options.skip_teardown: + return + logging.debug("Tearing down the servers and setup") + utils.vtgate_kill(vtgate_server) + keyspace_env.teardown() + + environment.topo_server().teardown() + + utils.kill_sub_processes() + utils.remove_tmp_files() + + +def get_connection(user=None, password=None): + global vtgate_port + timeout = 10.0 + return vtgatev3.connect("localhost:%s" % (vtgate_port), timeout, + user=user, password=password) + + +class TestVTGateFunctions(unittest.TestCase): + def setUp(self): + self.master_tablet = shard_1_master + + def test_user(self): + count = 4 + vtgate_conn = get_connection() + cursor = vtgate_conn.cursor('master') + + # Test insert + for x in xrange(count): + i = x+1 + cursor.begin() + cursor.execute( + "insert into vt_user (name) values (:name)", + {'name': 'test %s' % i}) + self.assertEqual((cursor.fetchall(), cursor.rowcount, cursor.lastrowid, cursor.description), ([], 1L, i, [])) + cursor.commit() + + # Test select equal + for x in xrange(count): + i = x+1 + cursor.execute("select * from vt_user where id = :id", {'id': i}) + self.assertEqual((cursor.fetchall(), cursor.rowcount, cursor.lastrowid, cursor.description), ([(i, "test %s" % i)], 1L, 0, [('id', 8L), ('name', 253L)])) + + # Test insert with no auto-inc, then auto-inc + vtgate_conn.begin() + result = vtgate_conn._execute( + "insert into vt_user (id, name) values (:id, :name)", + {'id': 6, 'name': 'test 6'}, + 'master') + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "insert into vt_user (name) values (:name)", + {'name': 'test 7'}, + 'master') + self.assertEqual(result, ([], 1L, 7L, [])) + vtgate_conn.commit() + + # Verify values in db + result = shard_0_master.mquery("vt_user", "select * from vt_user") + self.assertEqual(result, ((1L, 'test 1'), (2L, 'test 2'), (3L, 'test 3'))) + result = shard_1_master.mquery("vt_user", "select * from vt_user") + self.assertEqual(result, ((4L, 'test 4'), (6L, 'test 6'), (7L, 'test 7'))) + result = lookup_master.mquery("vt_lookup", "select * from vt_user_idx") + self.assertEqual(result, ((1L,), (2L,), (3L,), (4L,), (6L,), (7L,))) + + # Test IN clause + result = vtgate_conn._execute("select * from vt_user where id in (:a, :b)", {"a": 1, "b": 4}, 'master') + result[0].sort() + self.assertEqual(result, ([(1L, 'test 1'), (4L, 'test 4')], 2L, 0, [('id', 8L), ('name', 253L)])) + result = vtgate_conn._execute("select * from vt_user where id in (:a, :b)", {"a": 1, "b": 2}, 'master') + result[0].sort() + self.assertEqual(result, ([(1L, 'test 1'), (2L, 'test 2')], 2L, 0, [('id', 8L), ('name', 253L)])) + + # Test keyrange + result = vtgate_conn._execute("select * from vt_user where keyrange('', '\x80')", {}, 'master') + self.assertEqual(result, ([(1L, 'test 1'), (2L, 'test 2'), (3L, 'test 3')], 3L, 0, [('id', 8L), ('name', 253L)])) + result = vtgate_conn._execute("select * from vt_user where keyrange('\x80', '')", {}, 'master') + self.assertEqual(result, ([(4L, 'test 4'), (6L, 'test 6'), (7L, 'test 7')], 3L, 0, [('id', 8L), ('name', 253L)])) + + # Test scatter + result = vtgate_conn._execute("select * from vt_user", {}, 'master') + result[0].sort() + self.assertEqual(result, ( + [(1L, 'test 1'), (2L, 'test 2'), (3L, 'test 3'), (4L, 'test 4'), (6L, 'test 6'), (7L, 'test 7')], + 6L, + 0, + [('id', 8L), ('name', 253L)], + )) + + # Test stream + stream_cursor = vtgate_conn.cursor('master', cursorclass=cursorv3.StreamCursor) + stream_cursor.execute("select * from vt_user", {}) + self.assertEqual(cursor.description, [('id', 8L), ('name', 253L)]) + rows = [] + for row in stream_cursor: + rows.append(row) + rows.sort() + self.assertEqual(rows, [(1L, 'test 1'), (2L, 'test 2'), (3L, 'test 3'), (4L, 'test 4'), (6L, 'test 6'), (7L, 'test 7')]) + + # Test updates + vtgate_conn.begin() + result = vtgate_conn._execute( + "update vt_user set name = :name where id = :id", + {'id': 1, 'name': 'test one'}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "update vt_user set name = :name where id = :id", + {'id': 4, 'name': 'test four'}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_user") + self.assertEqual(result, ((1L, 'test one'), (2L, 'test 2'), (3L, 'test 3'))) + result = shard_1_master.mquery("vt_user", "select * from vt_user") + self.assertEqual(result, ((4L, 'test four'), (6L, 'test 6'), (7L, 'test 7'))) + + # Test deletes + vtgate_conn.begin() + result = vtgate_conn._execute( + "delete from vt_user where id = :id", + {'id': 1}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "delete from vt_user where id = :id", + {'id': 4}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_user") + self.assertEqual(result, ((2L, 'test 2'), (3L, 'test 3'))) + result = shard_1_master.mquery("vt_user", "select * from vt_user") + self.assertEqual(result, ((6L, 'test 6'), (7L, 'test 7'))) + result = lookup_master.mquery("vt_lookup", "select * from vt_user_idx") + self.assertEqual(result, ((2L,), (3L,), (6L,), (7L,))) + + def test_user2(self): + # user2 is for testing non-unique vindexes + vtgate_conn = get_connection() + vtgate_conn.begin() + result = vtgate_conn._execute( + "insert into vt_user2 (id, name) values (:id, :name)", + {'id': 1, 'name': 'name1'}, + 'master') + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "insert into vt_user2 (id, name) values (:id, :name)", + {'id': 7, 'name': 'name1'}, + 'master') + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "insert into vt_user2 (id, name) values (:id, :name)", + {'id': 2, 'name': 'name2'}, + 'master') + self.assertEqual(result, ([], 1L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_user2") + self.assertEqual(result, ((1L, 'name1'), (2L, 'name2'))) + result = shard_1_master.mquery("vt_user", "select * from vt_user2") + self.assertEqual(result, ((7L, 'name1'),)) + result = lookup_master.mquery("vt_lookup", "select * from name_user2_map") + self.assertEqual(result, (('name1', 1L), ('name1', 7L), ('name2', 2L))) + + # Test select by id + result = vtgate_conn._execute("select * from vt_user2 where id = :id", {'id': 1}, 'master') + self.assertEqual(result, ([(1, "name1")], 1L, 0, [('id', 8L), ('name', 253L)])) + + # Test select by lookup + result = vtgate_conn._execute("select * from vt_user2 where name = :name", {'name': 'name1'}, 'master') + result[0].sort() + self.assertEqual(result, ([(1, "name1"), (7, "name1")], 2L, 0, [('id', 8L), ('name', 253L)])) + + # Test IN clause using non-unique vindex + result = vtgate_conn._execute("select * from vt_user2 where name in ('name1', 'name2')", {}, 'master') + result[0].sort() + self.assertEqual(result, ([(1, "name1"), (2, "name2"), (7, "name1")], 3L, 0, [('id', 8L), ('name', 253L)])) + result = vtgate_conn._execute("select * from vt_user2 where name in ('name1')", {}, 'master') + result[0].sort() + self.assertEqual(result, ([(1, "name1"), (7, "name1")], 2L, 0, [('id', 8L), ('name', 253L)])) + + # Test delete + vtgate_conn.begin() + result = vtgate_conn._execute( + "delete from vt_user2 where id = :id", + {'id': 1}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "delete from vt_user2 where id = :id", + {'id': 2}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_user2") + self.assertEqual(result, ()) + result = shard_1_master.mquery("vt_user", "select * from vt_user2") + self.assertEqual(result, ((7L, 'name1'),)) + result = lookup_master.mquery("vt_lookup", "select * from name_user2_map") + self.assertEqual(result, (('name1', 7L),)) + + def test_user_extra(self): + # user_extra is for testing unowned functional vindex + count = 4 + vtgate_conn = get_connection() + for x in xrange(count): + i = x+1 + vtgate_conn.begin() + result = vtgate_conn._execute( + "insert into vt_user_extra (user_id, email) values (:user_id, :email)", + {'user_id': i, 'email': 'test %s' % i}, + 'master') + self.assertEqual(result, ([], 1L, 0L, [])) + vtgate_conn.commit() + for x in xrange(count): + i = x+1 + result = vtgate_conn._execute("select * from vt_user_extra where user_id = :user_id", {'user_id': i}, 'master') + self.assertEqual(result, ([(i, "test %s" % i)], 1L, 0, [('user_id', 8L), ('email', 253L)])) + result = shard_0_master.mquery("vt_user", "select * from vt_user_extra") + self.assertEqual(result, ((1L, 'test 1'), (2L, 'test 2'), (3L, 'test 3'))) + result = shard_1_master.mquery("vt_user", "select * from vt_user_extra") + self.assertEqual(result, ((4L, 'test 4'),)) + + vtgate_conn.begin() + result = vtgate_conn._execute( + "update vt_user_extra set email = :email where user_id = :user_id", + {'user_id': 1, 'email': 'test one'}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "update vt_user_extra set email = :email where user_id = :user_id", + {'user_id': 4, 'email': 'test four'}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_user_extra") + self.assertEqual(result, ((1L, 'test one'), (2L, 'test 2'), (3L, 'test 3'))) + result = shard_1_master.mquery("vt_user", "select * from vt_user_extra") + self.assertEqual(result, ((4L, 'test four'),)) + + vtgate_conn.begin() + result = vtgate_conn._execute( + "delete from vt_user_extra where user_id = :user_id", + {'user_id': 1}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "delete from vt_user_extra where user_id = :user_id", + {'user_id': 4}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_user_extra") + self.assertEqual(result, ((2L, 'test 2'), (3L, 'test 3'))) + result = shard_1_master.mquery("vt_user", "select * from vt_user_extra") + self.assertEqual(result, ()) + + def test_music(self): + # music is for testing owned lookup index + count = 4 + vtgate_conn = get_connection() + for x in xrange(count): + i = x+1 + vtgate_conn.begin() + result = vtgate_conn._execute( + "insert into vt_music (user_id, song) values (:user_id, :song)", + {'user_id': i, 'song': 'test %s' % i}, + 'master') + self.assertEqual(result, ([], 1L, i, [])) + vtgate_conn.commit() + for x in xrange(count): + i = x+1 + result = vtgate_conn._execute("select * from vt_music where id = :id", {'id': i}, 'master') + self.assertEqual(result, ([(i, i, "test %s" % i)], 1, 0, [('user_id', 8L), ('id', 8L), ('song', 253L)])) + vtgate_conn.begin() + result = vtgate_conn._execute( + "insert into vt_music (user_id, id, song) values (:user_id, :id, :song)", + {'user_id': 5, 'id': 6, 'song': 'test 6'}, + 'master') + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "insert into vt_music (user_id, song) values (:user_id, :song)", + {'user_id': 6, 'song': 'test 7'}, + 'master') + self.assertEqual(result, ([], 1L, 7L, [])) + result = vtgate_conn._execute( + "insert into vt_music (user_id, song) values (:user_id, :song)", + {'user_id': 6, 'song': 'test 8'}, + 'master') + self.assertEqual(result, ([], 1L, 8L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_music") + self.assertEqual(result, ((1L, 1L, 'test 1'), (2L, 2L, 'test 2'), (3L, 3L, 'test 3'), (5L, 6L, 'test 6'))) + result = shard_1_master.mquery("vt_user", "select * from vt_music") + self.assertEqual(result, ((4L, 4L, 'test 4'), (6L, 7L, 'test 7'), (6L, 8L, 'test 8'))) + result = lookup_master.mquery("vt_lookup", "select * from music_user_map") + self.assertEqual(result, ((1L, 1L), (2L, 2L), (3L, 3L), (4L, 4L), (6L, 5L), (7L, 6L), (8L, 6L))) + + vtgate_conn.begin() + result = vtgate_conn._execute( + "update vt_music set song = :song where id = :id", + {'id': 6, 'song': 'test six'}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "update vt_music set song = :song where id = :id", + {'id': 7, 'song': 'test seven'}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_music") + self.assertEqual(result, ((1L, 1L, 'test 1'), (2L, 2L, 'test 2'), (3L, 3L, 'test 3'), (5L, 6L, 'test six'))) + result = shard_1_master.mquery("vt_user", "select * from vt_music") + self.assertEqual(result, ((4L, 4L, 'test 4'), (6L, 7L, 'test seven'), (6L, 8L, 'test 8'))) + + vtgate_conn.begin() + result = vtgate_conn._execute( + "delete from vt_music where id = :id", + {'id': 3}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "delete from vt_music where user_id = :user_id", + {'user_id': 6}, + "master") + self.assertEqual(result, ([], 2L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_music") + self.assertEqual(result, ((1L, 1L, 'test 1'), (2L, 2L, 'test 2'), (5L, 6L, 'test six'))) + result = shard_1_master.mquery("vt_user", "select * from vt_music") + self.assertEqual(result, ((4L, 4L, 'test 4'),)) + result = lookup_master.mquery("vt_lookup", "select * from music_user_map") + self.assertEqual(result, ((1L, 1L), (2L, 2L), (4L, 4L), (6L, 5L))) + + def test_music_extra(self): + # music_extra is for testing unonwed lookup index + vtgate_conn = get_connection() + vtgate_conn.begin() + result = vtgate_conn._execute( + "insert into vt_music_extra (music_id, user_id, artist) values (:music_id, :user_id, :artist)", + {'music_id': 1, 'user_id': 1, 'artist': 'test 1'}, + 'master') + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "insert into vt_music_extra (music_id, artist) values (:music_id, :artist)", + {'music_id': 6, 'artist': 'test 6'}, + 'master') + self.assertEqual(result, ([], 1L, 0L, [])) + vtgate_conn.commit() + result = vtgate_conn._execute("select * from vt_music_extra where music_id = :music_id", {'music_id': 6}, 'master') + self.assertEqual(result, ([(6L, 5L, "test 6")], 1, 0, [('music_id', 8L), ('user_id', 8L), ('artist', 253L)])) + result = shard_0_master.mquery("vt_user", "select * from vt_music_extra") + self.assertEqual(result, ((1L, 1L, 'test 1'), (6L, 5L, 'test 6'))) + result = shard_1_master.mquery("vt_user", "select * from vt_music_extra") + self.assertEqual(result, ()) + + vtgate_conn.begin() + result = vtgate_conn._execute( + "update vt_music_extra set artist = :artist where music_id = :music_id", + {'music_id': 6, 'artist': 'test six'}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "update vt_music_extra set artist = :artist where music_id = :music_id", + {'music_id': 7, 'artist': 'test seven'}, + "master") + self.assertEqual(result, ([], 0L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_music_extra") + self.assertEqual(result, ((1L, 1L, 'test 1'), (6L, 5L, 'test six'))) + + vtgate_conn.begin() + result = vtgate_conn._execute( + "delete from vt_music_extra where music_id = :music_id", + {'music_id': 6}, + "master") + self.assertEqual(result, ([], 1L, 0L, [])) + result = vtgate_conn._execute( + "delete from vt_music_extra where music_id = :music_id", + {'music_id': 7}, + "master") + self.assertEqual(result, ([], 0L, 0L, [])) + vtgate_conn.commit() + result = shard_0_master.mquery("vt_user", "select * from vt_music_extra") + self.assertEqual(result, ((1L, 1L, 'test 1'),)) + + def test_insert_value_required(self): + vtgate_conn = get_connection() + try: + vtgate_conn.begin() + with self.assertRaisesRegexp(dbexceptions.DatabaseError, '.*value must be supplied.*'): + vtgate_conn._execute( + "insert into vt_user_extra (email) values (:email)", + {'email': 'test 10'}, + 'master') + finally: + vtgate_conn.rollback() + + +if __name__ == '__main__': + utils.main() diff --git a/test/vthook-copy_snapshot_from_storage.sh b/test/vthook-copy_snapshot_from_storage.sh deleted file mode 100755 index e942bef4ad6..00000000000 --- a/test/vthook-copy_snapshot_from_storage.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -# Copyright 2014, Google Inc. All rights reserved. -# Use of this source code is governed by a BSD-style license that can -# be found in the LICENSE file. - -# This script is a hook used during tests to copy snasphots from the central -# storage place. - -# find a suitable source, under VTDATAROOT/tmp -if [ "$VTDATAROOT" == "" ]; then - VTDATAROOT=/vt -fi -SOURCE=$VTDATAROOT/tmp/snapshot-from-$SOURCE_TABLET_ALIAS-for-$KEYRANGE.tar - -# no source -> can't copy it, exit -if ! test -e $SOURCE ; then - exit 1 -fi - -# and untar the directory from there to here -mkdir -p $SNAPSHOT_PATH || exit 1 -cd $SNAPSHOT_PATH && tar xvf $SOURCE -exit $? diff --git a/test/vthook-copy_snapshot_to_storage.sh b/test/vthook-copy_snapshot_to_storage.sh deleted file mode 100755 index f0ed480eb16..00000000000 --- a/test/vthook-copy_snapshot_to_storage.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Copyright 2014, Google Inc. All rights reserved. -# Use of this source code is governed by a BSD-style license that can -# be found in the LICENSE file. - -# This script is a hook used during tests to copy snasphots to a fake -# central storage place. - -# find a suitable destination, under VTDATAROOT/tmp -if [ "$VTDATAROOT" == "" ]; then - VTDATAROOT=/vt -fi -DESTINATION=$VTDATAROOT/tmp/snapshot-from-$TABLET_ALIAS-for-$KEYRANGE.tar - -# and tar the directory to there -cd $SNAPSHOT_PATH && tar cvf $DESTINATION * -exit $? diff --git a/test/zkocc_test.py b/test/zkocc_test.py index 6c5ee8a80b8..6c727c50a3c 100755 --- a/test/zkocc_test.py +++ b/test/zkocc_test.py @@ -43,18 +43,9 @@ class TopoOccTest(unittest.TestCase): def setUp(self): environment.topo_server().wipe() self.vtgate_zk, self.vtgate_zk_port = utils.vtgate_start() - if environment.topo_server().flavor() == 'zookeeper': - self.zkocc_server = utils.zkocc_start() - self.vtgate_zkocc, self.vtgate_zkocc_port = utils.vtgate_start(topo_impl="zkocc") - self.topo = zkocc.ZkOccConnection("localhost:%u" % environment.topo_server().zkocc_port_base, 'test_nj', 30) - self.topo.dial() def tearDown(self): utils.vtgate_kill(self.vtgate_zk) - if environment.topo_server().flavor() == 'zookeeper': - self.topo.close() - utils.zkocc_kill(self.zkocc_server) - utils.vtgate_kill(self.vtgate_zkocc) def rebuild(self): utils.run_vtctl(['RebuildKeyspaceGraph', 'test_keyspace'], auto_log=True) @@ -75,20 +66,6 @@ def test_get_srv_keyspace_names(self): self.assertEqual(err, "KeyspaceNames[0] = test_keyspace1\n" + "KeyspaceNames[1] = test_keyspace2\n") - if environment.topo_server().flavor() == 'zookeeper': - self.assertItemsEqual(self.topo.get_srv_keyspace_names('local'), ["test_keyspace1", "test_keyspace2"]) - - # zkocc API test - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u -mode getSrvKeyspaceNames test_nj' % environment.topo_server().zkocc_port_base, trap_output=True) - self.assertEqual(err, "KeyspaceNames[0] = test_keyspace1\n" + - "KeyspaceNames[1] = test_keyspace2\n") - - # vtgate zkocc API test - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u -mode getSrvKeyspaceNames test_nj' % self.vtgate_zkocc_port, trap_output=True) - self.assertEqual(err, "KeyspaceNames[0] = test_keyspace1\n" + - "KeyspaceNames[1] = test_keyspace2\n") - - def test_get_srv_keyspace(self): utils.run_vtctl('CreateKeyspace test_keyspace') t = tablet.Tablet(tablet_uid=1, cell="nj") @@ -104,49 +81,9 @@ def test_get_srv_keyspace(self): " Shards[0]={Start: , End: }\n" + "Partitions[replica] =\n" + " Shards[0]={Start: , End: }\n" + - "Shards[0]={Start: , End: }\n" + "TabletTypes[0] = master\n", "Got wrong content: %s" % err) - if environment.topo_server().flavor() == 'zookeeper': - reply = self.topo.get_srv_keyspace("test_nj", "test_keyspace") - self.assertEqual(reply['TabletTypes'], ['master']) - - # zkocc API test - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u -mode getSrvKeyspace test_nj test_keyspace' % environment.topo_server().zkocc_port_base, trap_output=True) - self.assertEqual(err, - "Partitions[master] =\n" + - " Shards[0]={Start: , End: }\n" + - "Partitions[rdonly] =\n" + - " Shards[0]={Start: , End: }\n" + - "Partitions[replica] =\n" + - " Shards[0]={Start: , End: }\n" + - "Shards[0]={Start: , End: }\n" + - "TabletTypes[0] = master\n", - "Got wrong content: %s" % err) - - # vtgate zkocc API test - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u -mode getSrvKeyspace test_nj test_keyspace' % self.vtgate_zkocc_port, trap_output=True) - self.assertEqual(err, "Partitions[master] =\n" + - " Shards[0]={Start: , End: }\n" + - "Partitions[rdonly] =\n" + - " Shards[0]={Start: , End: }\n" + - "Partitions[replica] =\n" + - " Shards[0]={Start: , End: }\n" + - "Shards[0]={Start: , End: }\n" + - "TabletTypes[0] = master\n", - "Got wrong content: %s" % err) - - - def test_get_srv_keyspace_local(self): - utils.run_vtctl('CreateKeyspace test_keyspace') - t = tablet.Tablet(tablet_uid=1, cell="nj") - t.init_tablet("master", "test_keyspace", "0") - t.update_addrs() - self.rebuild() - reply = self.topo.get_srv_keyspace("local", "test_keyspace") - self.assertEqual(reply['TabletTypes'], ['master']) - def test_get_end_points(self): utils.run_vtctl('CreateKeyspace test_keyspace') t = tablet.Tablet(tablet_uid=1, cell="nj") @@ -158,224 +95,15 @@ def test_get_end_points(self): out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u -mode getEndPoints test_nj test_keyspace 0 master' % self.vtgate_zk_port, trap_output=True) self.assertEqual(err, "Entries[0] = 1 localhost\n") - if environment.topo_server().flavor() == 'zookeeper': - self.assertEqual(len(self.topo.get_end_points("test_nj", "test_keyspace", "0", "master")['Entries']), 1) - # zkocc API test - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u -mode getEndPoints test_nj test_keyspace 0 master' % environment.topo_server().zkocc_port_base, trap_output=True) - self.assertEqual(err, "Entries[0] = 1 localhost\n") - - # vtgate zkocc API test - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u -mode getEndPoints test_nj test_keyspace 0 master' % self.vtgate_zkocc_port, trap_output=True) - self.assertEqual(err, "Entries[0] = 1 localhost\n") - - -def _format_time(timeFromBson): - (tz, val) = timeFromBson - t = datetime.datetime.fromtimestamp(val/1000) - return t.strftime("%Y-%m-%d %H:%M:%S") - - -class TestZkocc(unittest.TestCase): +class TestTopo(unittest.TestCase): longMessage = True def setUp(self): environment.topo_server().wipe() - if environment.topo_server().flavor() == 'zookeeper': - utils.run(environment.binary_argstr('zk')+' touch -p /zk/test_nj/vt/zkocc1') - utils.run(environment.binary_argstr('zk')+' touch -p /zk/test_nj/vt/zkocc2') - fd = tempfile.NamedTemporaryFile(dir=environment.tmproot, delete=False) - filename1 = fd.name - fd.write("Test data 1") - fd.close() - utils.run(environment.binary_argstr('zk')+' cp '+filename1+' /zk/test_nj/vt/zkocc1/data1') - - fd = tempfile.NamedTemporaryFile(dir=environment.tmproot, delete=False) - filename2 = fd.name - fd.write("Test data 2") - fd.close() - utils.run(environment.binary_argstr('zk')+' cp '+filename2+' /zk/test_nj/vt/zkocc1/data2') - - fd = tempfile.NamedTemporaryFile(dir=environment.tmproot, delete=False) - filename3 = fd.name - fd.write("Test data 3") - fd.close() - utils.run(environment.binary_argstr('zk')+' cp '+filename3+' /zk/test_nj/vt/zkocc1/data3') - - def _check_zk_output(self, cmd, expected): - # directly for sanity - out, err = utils.run(environment.binary_argstr('zk')+' ' + cmd, trap_output=True) - self.assertEqualNormalized(out, expected, 'unexpected direct zk output') - - # using zkocc - out, err = utils.run(environment.binary_argstr('zk')+' --zk.zkocc-addr=localhost:%u %s' % (environment.topo_server().zkocc_port_base, cmd), trap_output=True) - self.assertEqualNormalized(out, expected, 'unexpected zk zkocc output') - logging.debug("Matched: %s", out) - - - def assertEqualNormalized(self, actual, expected, msg=None): - self.assertEqual(re.sub(r'\s+', ' ', actual).strip(), re.sub(r'\s+', ' ', expected).strip(), msg) - - def test_zkocc(self): - # preload the test_nj cell - zkocc_14850 = utils.zkocc_start(extra_params=['-connect-timeout=2s', '-cache-refresh-interval=1s']) - time.sleep(1) - - # create a python client. The first address is bad, will test the retry logic - bad_port = environment.reserve_ports(3) - zkocc_client = zkocc.ZkOccConnection("localhost:%u,localhost:%u,localhost:%u" % (bad_port, environment.topo_server().zkocc_port_base, bad_port+1), "test_nj", 30) - zkocc_client.dial() - - # test failure for a python client that cannot connect - bad_zkocc_client = zkocc.ZkOccConnection("localhost:%u,localhost:%u" % (bad_port+2, bad_port), "test_nj", 30) - try: - bad_zkocc_client.dial() - self.fail('exception expected') - except zkocc.ZkOccError as e: - if not str(e).startswith("Cannot dial to any server, tried: "): - self.fail('unexpected exception: %s' % str(e)) - level = logging.getLogger().getEffectiveLevel() - logging.getLogger().setLevel(logging.ERROR) - - # FIXME(ryszard): This can be changed into a self.assertRaises. - try: - bad_zkocc_client.get("/zk/test_nj/vt/zkocc1/data1") - self.fail('exception expected') - except zkocc.ZkOccError as e: - if not str(e).startswith("Cannot dial to any server, tried: "): - self.fail('unexpected exception: %s' % str(e)) - - logging.getLogger().setLevel(level) - - # get test - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u /zk/test_nj/vt/zkocc1/data1' % environment.topo_server().zkocc_port_base, trap_output=True) - self.assertEqual(err, "/zk/test_nj/vt/zkocc1/data1 = Test data 1 (NumChildren=0, Version=0, Cached=false, Stale=false)\n") - - zk_data = zkocc_client.get("/zk/test_nj/vt/zkocc1/data1") - self.assertDictContainsSubset({'Data': "Test data 1", - 'Cached': True, - 'Stale': False,}, - zk_data) - self.assertDictContainsSubset({'NumChildren': 0, 'Version': 0}, zk_data['Stat']) - - # getv test - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u /zk/test_nj/vt/zkocc1/data1 /zk/test_nj/vt/zkocc1/data2 /zk/test_nj/vt/zkocc1/data3' % environment.topo_server().zkocc_port_base, trap_output=True) - self.assertEqualNormalized(err, """[0] /zk/test_nj/vt/zkocc1/data1 = Test data 1 (NumChildren=0, Version=0, Cached=true, Stale=false) - [1] /zk/test_nj/vt/zkocc1/data2 = Test data 2 (NumChildren=0, Version=0, Cached=false, Stale=false) - [2] /zk/test_nj/vt/zkocc1/data3 = Test data 3 (NumChildren=0, Version=0, Cached=false, Stale=false) - """) - zk_data = zkocc_client.getv(["/zk/test_nj/vt/zkocc1/data1", "/zk/test_nj/vt/zkocc1/data2", "/zk/test_nj/vt/zkocc1/data3"])['Nodes'] - self.assertEqual(len(zk_data), 3) - for i, d in enumerate(zk_data): - self.assertEqual(d['Data'], 'Test data %s' % (i + 1)) - self.assertTrue(d['Cached']) - self.assertFalse(d['Stale']) - self.assertDictContainsSubset({'NumChildren': 0, 'Version': 0}, d['Stat']) - - # children test - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u -mode children /zk/test_nj/vt' % environment.topo_server().zkocc_port_base, trap_output=True) - self.assertEqualNormalized(err, """Path = /zk/test_nj/vt - Child[0] = zkocc1 - Child[1] = zkocc2 - NumChildren = 2 - CVersion = 2 - Cached = false - Stale = false - """) - - # zk command tests - self._check_zk_output("cat /zk/test_nj/vt/zkocc1/data1", "Test data 1") - self._check_zk_output("ls -l /zk/test_nj/vt/zkocc1", """total: 3 - -rw-rw-rw- zk zk 11 %s data1 - -rw-rw-rw- zk zk 11 %s data2 - -rw-rw-rw- zk zk 11 %s data3 - """ % (_format_time(zk_data[0]['Stat']['MTime']), - _format_time(zk_data[1]['Stat']['MTime']), - _format_time(zk_data[2]['Stat']['MTime']))) - - # test /zk/local is not resolved and rejected - out, err = utils.run(environment.binary_argstr('zkclient2')+' -server localhost:%u /zk/local/vt/zkocc1/data1' % environment.topo_server().zkocc_port_base, trap_output=True, raise_on_error=False) - self.assertIn("zkocc: cannot resolve local cell", err) - - # start a background process to query the same value over and over again - # while we kill the zk server and restart it - outfd = tempfile.NamedTemporaryFile(dir=environment.tmproot, delete=False) - filename = outfd.name - querier = utils.run_bg('/bin/bash -c "while true ; do '+environment.binary_argstr('zkclient2')+' -server localhost:%u /zk/test_nj/vt/zkocc1/data1 ; sleep 0.1 ; done"' % environment.topo_server().zkocc_port_base, stderr=outfd.file) - outfd.close() - time.sleep(1) - - # kill zk server, sleep a bit, restart zk server, sleep a bit - utils.run(environment.binary_argstr('zkctl')+' -zk.cfg 1@'+utils.hostname+':%s shutdown' % environment.topo_server().zk_ports) - time.sleep(3) - utils.run(environment.binary_argstr('zkctl')+' -zk.cfg 1@'+utils.hostname+':%s start' % environment.topo_server().zk_ports) - time.sleep(3) - - utils.kill_sub_process(querier) - - logging.debug("Checking %s", filename) - fd = open(filename, "r") - state = 0 - for line in fd: - if line == "/zk/test_nj/vt/zkocc1/data1 = Test data 1 (NumChildren=0, Version=0, Cached=true, Stale=false)\n": - stale = False - elif line == "/zk/test_nj/vt/zkocc1/data1 = Test data 1 (NumChildren=0, Version=0, Cached=true, Stale=true)\n": - stale = True - else: - self.fail('unexpected line: %s' % line) - if state == 0: - if stale: - state = 1 - elif state == 1: - if not stale: - state = 2 - else: - if stale: - self.fail('unexpected stale state') - self.assertEqual(state, 2) - fd.close() - - utils.zkocc_kill(zkocc_14850) - - # check that after the server is gone, the python client fails correctly - level = logging.getLogger().getEffectiveLevel() - logging.getLogger().setLevel(logging.ERROR) - try: - zkocc_client.get("/zk/test_nj/vt/zkocc1/data1") - self.fail('exception expected') - except zkocc.ZkOccError as e: - if not str(e).startswith("Cannot dial to any server, tried: "): - self.fail('unexpected exception: %s', str(e)) - logging.getLogger().setLevel(level) - - def test_zkocc_qps(self): - # preload the test_nj cell - zkocc_14850 = utils.zkocc_start() - - qpser = utils.run_bg(environment.binary_argstr('zkclient2')+' -server localhost:%u -mode qps /zk/test_nj/vt/zkocc1/data1 /zk/test_nj/vt/zkocc1/data2' % environment.topo_server().zkocc_port_base) - qpser.wait() - - # get the zkocc vars, make sure we have what we need - v = utils.get_vars(environment.topo_server().zkocc_port_base) - if v['ZkReader']['test_nj']['State'] != 'Connected': - self.fail('invalid zk global state: ' + v['ZkReader']['test_nj']['State']) - - # some checks on performance / stats - rpcCalls = v['ZkReader']['RpcCalls'] - if rpcCalls < MIN_QPS * 10: - self.fail('QPS is too low: %u < %u' % (rpcCalls / 10, MIN_QPS)) - else: - logging.debug("Recorded qps: %u", rpcCalls / 10) - cacheReads = v['ZkReader']['test_nj']['CacheReads'] - if cacheReads < MIN_QPS * 10: - self.fail('Cache QPS is too low: %u < %u' % (cacheReads, MIN_QPS * 10)) - totalCacheReads = v['ZkReader']['total']['CacheReads'] - self.assertEqual(cacheReads, totalCacheReads, 'Rollup stats are wrong') - self.assertEqual(v['ZkReader']['UnknownCellErrors'], 0, 'unexpected UnknownCellErrors') - utils.zkocc_kill(zkocc_14850) # test_vtgate_qps can be run to profile vtgate: # Just run: - # ./zkocc_test.py -v TestZkocc.test_vtgate_qps --skip-teardown + # ./zkocc_test.py -v TestTopo.test_vtgate_qps --skip-teardown # Then run: # go tool pprof $VTROOT/bin/vtgate $VTDATAROOT/tmp/vtgate.pprof # (or with zkclient2 for the client side) @@ -394,7 +122,7 @@ def test_vtgate_qps(self): 'vtgate.pprof')]) qpser = utils.run_bg(environment.binary_args('zkclient2') + [ '-server', 'localhost:%u' % vtgate_port, - '-mode', 'qps2', + '-mode', 'qps', '-cpu_profile', os.path.join(environment.tmproot, 'zkclient2.pprof'), 'test_nj', 'test_keyspace']) qpser.wait() @@ -415,23 +143,6 @@ def test_fake_zkocc_connection(self): fkc.replace_zk_data("3306", "3310") fkc.replace_zk_data("127.0.0.1", "my.cool.hostname") - # old style API tests - keyspaces = fkc.children("/zk/testing/vt/ns") - self.assertEqual(keyspaces['Children'], ["test_keyspace"], "children doesn't work") - entry = fkc.get("/zk/testing/vt/ns/test_keyspace/0/master") - jentry = json.loads(entry['Data']) - self.assertEqual({ - "entries": [{ - "host": "my.cool.hostname", - "named_port_map": { - "_mysql": 3310, - "_vtocc": 6711, - }, - "uid": 0, - "port": 0, - }], - }, jentry, 'Entry fix-up is wrong: %s' % entry['Data']) - # new style API tests keyspaces = fkc.get_srv_keyspace_names('testing') self.assertEqual(keyspaces, ["test_keyspace"], "get_srv_keyspace_names doesn't work") @@ -446,7 +157,7 @@ def test_fake_zkocc_connection(self): end_points = fkc.get_end_points("testing", "test_keyspace", "0", "master") self.assertEqual({ 'entries': [{'host': 'my.cool.hostname', - 'named_port_map': {'_mysql': 3310, '_vtocc': 6711}, + 'named_port_map': {'mysql': 3310, 'vt': 6711}, 'port': 0, 'uid': 0}]}, end_points, "end points are wrong") diff --git a/third_party/go/launchpad.net/gozk/zookeeper/helpers.c b/third_party/go/launchpad.net/gozk/zookeeper/helpers.c index ece1c75f3e7..8d036cad750 100644 --- a/third_party/go/launchpad.net/gozk/zookeeper/helpers.c +++ b/third_party/go/launchpad.net/gozk/zookeeper/helpers.c @@ -86,5 +86,23 @@ void destroy_watch_data(watch_data *data) { watcher_fn watch_handler = _watch_handler; void_completion_t handle_void_completion = _handle_void_completion; +zhandle_t *zookeeper_init_int(const char *host, watcher_fn fn, + int recv_timeout, const clientid_t *clientid, unsigned long context, int flags) { + return zookeeper_init(host, fn, recv_timeout, clientid, (void*)context, flags); +} +int zoo_wget_int(zhandle_t *zh, const char *path, + watcher_fn watcher, unsigned long watcherCtx, + char *buffer, int* buffer_len, struct Stat *stat) { + return zoo_wget(zh, path, watcher, (void*)watcherCtx, buffer, buffer_len, stat); +} +int zoo_wget_children2_int(zhandle_t *zh, const char *path, + watcher_fn watcher, unsigned long watcherCtx, + struct String_vector *strings, struct Stat *stat) { + return zoo_wget_children2(zh, path, watcher, (void*)watcherCtx, strings, stat); +} +int zoo_wexists_int(zhandle_t *zh, const char *path, + watcher_fn watcher, unsigned long watcherCtx, struct Stat *stat) { + return zoo_wexists(zh, path, watcher, (void*)watcherCtx, stat); +} // vim:ts=4:sw=4:et diff --git a/third_party/go/launchpad.net/gozk/zookeeper/helpers.h b/third_party/go/launchpad.net/gozk/zookeeper/helpers.h index 130828799e6..72cfb8f3677 100644 --- a/third_party/go/launchpad.net/gozk/zookeeper/helpers.h +++ b/third_party/go/launchpad.net/gozk/zookeeper/helpers.h @@ -28,6 +28,22 @@ void destroy_watch_data(watch_data *data); extern watcher_fn watch_handler; extern void_completion_t handle_void_completion; +// The precise GC in Go 1.4+ doesn't like it when we cast arbitrary +// integers to unsafe.Pointer to pass to the void* context parameter. +// Below are helper functions that perform the cast in C so the Go GC +// doesn't try to interpret it as a pointer. + +zhandle_t *zookeeper_init_int(const char *host, watcher_fn fn, + int recv_timeout, const clientid_t *clientid, unsigned long context, int flags); +int zoo_wget_int(zhandle_t *zh, const char *path, + watcher_fn watcher, unsigned long watcherCtx, + char *buffer, int* buffer_len, struct Stat *stat); +int zoo_wget_children2_int(zhandle_t *zh, const char *path, + watcher_fn watcher, unsigned long watcherCtx, + struct String_vector *strings, struct Stat *stat); +int zoo_wexists_int(zhandle_t *zh, const char *path, + watcher_fn watcher, unsigned long watcherCtx, struct Stat *stat); + #endif // vim:ts=4:sw=4:et diff --git a/third_party/go/launchpad.net/gozk/zookeeper/zk.go b/third_party/go/launchpad.net/gozk/zookeeper/zk.go index 571eedafc0c..8f5fcfee428 100644 --- a/third_party/go/launchpad.net/gozk/zookeeper/zk.go +++ b/third_party/go/launchpad.net/gozk/zookeeper/zk.go @@ -447,7 +447,7 @@ func dial(servers string, recvTimeout time.Duration, clientId *ClientId) (*Conn, conn.sessionWatchId = watchId cservers := C.CString(servers) - handle, cerr := C.zookeeper_init(cservers, C.watch_handler, C.int(recvTimeout/1e6), cId, unsafe.Pointer(watchId), 0) + handle, cerr := C.zookeeper_init_int(cservers, C.watch_handler, C.int(recvTimeout/1e6), cId, C.ulong(watchId), 0) C.free(unsafe.Pointer(cservers)) if handle == nil { conn.closeAllWatches() @@ -535,7 +535,7 @@ func (conn *Conn) GetW(path string) (data string, stat *Stat, watch <-chan Event watchId, watchChannel := conn.createWatch(true) var cstat Stat - rc, cerr := C.zoo_wget(conn.handle, cpath, C.watch_handler, unsafe.Pointer(watchId), cbuffer, &cbufferLen, &cstat.c) + rc, cerr := C.zoo_wget_int(conn.handle, cpath, C.watch_handler, C.ulong(watchId), cbuffer, &cbufferLen, &cstat.c) if rc != C.ZOK { conn.forgetWatch(watchId) return "", nil, nil, zkError(rc, cerr, "getw", path) @@ -591,7 +591,7 @@ func (conn *Conn) ChildrenW(path string) (children []string, stat *Stat, watch < cvector := C.struct_String_vector{} var cstat Stat - rc, cerr := C.zoo_wget_children2(conn.handle, cpath, C.watch_handler, unsafe.Pointer(watchId), &cvector, &cstat.c) + rc, cerr := C.zoo_wget_children2_int(conn.handle, cpath, C.watch_handler, C.ulong(watchId), &cvector, &cstat.c) // Can't happen if rc != 0, but avoid potential memory leaks in the future. if cvector.count != 0 { @@ -665,7 +665,7 @@ func (conn *Conn) ExistsW(path string) (stat *Stat, watch <-chan Event, err erro watchId, watchChannel := conn.createWatch(true) var cstat Stat - rc, cerr := C.zoo_wexists(conn.handle, cpath, C.watch_handler, unsafe.Pointer(watchId), &cstat.c) + rc, cerr := C.zoo_wexists_int(conn.handle, cpath, C.watch_handler, C.ulong(watchId), &cstat.c) // We diverge a bit from the usual here: a ZNONODE is not an error // for an exists call, otherwise every Exists call would have to check @@ -944,7 +944,6 @@ func (conn *Conn) RetryChange(path string, flags int, acl []ACL, changeFunc Chan return err } } - panic("not reached") } // ----------------------------------------------------------------------- diff --git a/travis/dependencies.sh b/travis/dependencies.sh new file mode 100644 index 00000000000..8d8921ad3ab --- /dev/null +++ b/travis/dependencies.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -e + +# Add MariaDB repository +sudo apt-get install python-software-properties +sudo apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db +sudo add-apt-repository 'deb http://ftp.utexas.edu/mariadb/repo/10.0/ubuntu precise main' + +# Add New Relic repo +sudo sh -c 'echo deb http://apt.newrelic.com/debian/ newrelic non-free >> /etc/apt/sources.list.d/newrelic.list' +wget -O- https://download.newrelic.com/548C16BF.gpg | sudo apt-key add - + +sudo apt-get update + +# Install New relic to monitor perf metrics +# Travis will not set license key for forked pull +# requests, so skip the install. +if ! [ -z "$NEWRELIC_LICENSE_KEY" ]; then + sudo apt-get install newrelic-sysmond + sudo nrsysmond-config --set license_key=$NEWRELIC_LICENSE_KEY + sudo /etc/init.d/newrelic-sysmond start +fi + +# Remove pre-installed mysql +sudo apt-get purge mysql* mariadb* + +# MariaDB +sudo apt-get -f install libmariadbclient18 libmariadbclient-dev mariadb-client mariadb-server + +# Other dependencies +sudo apt-get install time automake libtool memcached python-dev python-mysqldb libssl-dev g++ mercurial git pkg-config bison bc + +# Java dependencies +wget https://dl.bintray.com/sbt/debian/sbt-0.13.6.deb +sudo dpkg -i sbt-0.13.6.deb +sudo apt-get -y install protobuf-compiler maven +echo "Y" | sudo apt-get -y install rsyslog +sudo service rsyslog restart +set +e