{"action":"created","comment":{"url":"https://api.github.com/repos/youtube/vitess/pulls/comments/30820921","id":30820921,"diff_hunk":"@@ -0,0 +1,548 @@\n+// Copyright 2015, Google Inc. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+package mysqlctl\n+\n+import (\n+\t\"bufio\"\n+\t\"encoding/json\"\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"io\"\n+\t\"io/ioutil\"\n+\t\"os\"\n+\t\"path\"\n+\t\"strings\"\n+\t\"sync\"\n+\n+\tlog \"github.com/golang/glog\"\n+\n+\t\"github.com/youtube/vitess/go/cgzip\"\n+\t\"github.com/youtube/vitess/go/sync2\"\n+\t\"github.com/youtube/vitess/go/vt/concurrency\"\n+\t\"github.com/youtube/vitess/go/vt/logutil\"\n+\t\"github.com/youtube/vitess/go/vt/mysqlctl/backupstorage\"\n+\t\"github.com/youtube/vitess/go/vt/mysqlctl/proto\"\n+)\n+\n+// This file handles the backup and restore related code\n+\n+const (\n+\t// the three bases for files to restore\n+\tbackupInnodbDataHomeDir     = \"InnoDBData\"\n+\tbackupInnodbLogGroupHomeDir = \"InnoDBLog\"\n+\tbackupData                  = \"Data\"\n+\n+\t// the manifest file name\n+\tbackupManifest = \"MANIFEST\"\n+)\n+\n+const (\n+\t// slaveStartDeadline is the deadline for starting a slave\n+\tslaveStartDeadline = 30\n+)\n+\n+var (\n+\t// ErrNoBackup is returned when there is no backup\n+\tErrNoBackup = errors.New(\"no available backup\")\n+)\n+\n+// FileEntry is one file to backup\n+type FileEntry struct {\n+\t// Base is one of:\n+\t// - backupInnodbDataHomeDir for files that go into Mycnf.InnodbDataHomeDir\n+\t// - backupInnodbLogGroupHomeDir for files that go into Mycnf.InnodbLogGroupHomeDir\n+\t// - backupData for files that go into Mycnf.DataDir\n+\tBase string\n+\n+\t// Name is the file name, relative to Base\n+\tName string\n+\n+\t// Hash is the hash of the gzip compressed data stored in the\n+\t// BackupStorage.\n+\tHash string\n+}\n+\n+func (fe *FileEntry) open(cnf *Mycnf, readOnly bool) (*os.File, error) {\n+\t// find the root to use\n+\tvar root string\n+\tswitch fe.Base {\n+\tcase backupInnodbDataHomeDir:\n+\t\troot = cnf.InnodbDataHomeDir\n+\tcase backupInnodbLogGroupHomeDir:\n+\t\troot = cnf.InnodbLogGroupHomeDir\n+\tcase backupData:\n+\t\troot = cnf.DataDir\n+\tdefault:\n+\t\treturn nil, fmt.Errorf(\"unknown base: %v\", fe.Base)\n+\t}\n+\n+\t// and open the file\n+\tname := path.Join(root, fe.Name)\n+\tvar fd *os.File\n+\tvar err error\n+\tif readOnly {\n+\t\tfd, err = os.Open(name)\n+\t} else {\n+\t\tdir := path.Dir(name)\n+\t\tif err := os.MkdirAll(dir, os.ModePerm); err != nil {\n+\t\t\treturn nil, fmt.Errorf(\"cannot create destination directory %v: %v\", dir, err)\n+\t\t}\n+\t\tfd, err = os.Create(name)\n+\t}\n+\tif err != nil {\n+\t\treturn nil, fmt.Errorf(\"cannot open source file %v: %v\", name, err)\n+\t}\n+\treturn fd, nil\n+}\n+\n+// BackupManifest represents the backup. It lists all the files, and\n+// the ReplicationPosition that the backup was taken at.\n+type BackupManifest struct {\n+\t// FileEntries contains all the files in the backup\n+\tFileEntries []FileEntry\n+\n+\t// ReplicationPosition is the position at which the backup was taken\n+\tReplicationPosition proto.ReplicationPosition\n+}\n+\n+// isDbDir returns true if the given directory contains a DB\n+func isDbDir(p string) bool {\n+\t// db.opt is there\n+\tif _, err := os.Stat(path.Join(p, \"db.opt\")); err == nil {\n+\t\treturn true\n+\t}\n+\n+\t// Look for at least one .frm file\n+\tfis, err := ioutil.ReadDir(p)\n+\tif err != nil {\n+\t\treturn false\n+\t}\n+\tfor _, fi := range fis {\n+\t\tif strings.HasSuffix(fi.Name(), \".frm\") {\n+\t\t\treturn true\n+\t\t}\n+\t}\n+\n+\treturn false\n+}\n+\n+func addDirectory(fes []FileEntry, base string, baseDir string, subDir string) ([]FileEntry, error) {\n+\tp := path.Join(baseDir, subDir)\n+\n+\tfis, err := ioutil.ReadDir(p)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tfor _, fi := range fis {\n+\t\tfes = append(fes, FileEntry{\n+\t\t\tBase: base,\n+\t\t\tName: path.Join(subDir, fi.Name()),\n+\t\t})\n+\t}\n+\treturn fes, nil\n+}\n+\n+func findFilesTobackup(cnf *Mycnf) ([]FileEntry, error) {\n+\tvar err error\n+\tvar result []FileEntry\n+\n+\t// first add inno db files\n+\tresult, err = addDirectory(result, backupInnodbDataHomeDir, cnf.InnodbDataHomeDir, \"\")\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tresult, err = addDirectory(result, backupInnodbLogGroupHomeDir, cnf.InnodbLogGroupHomeDir, \"\")\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\t// then add DB directories\n+\tfis, err := ioutil.ReadDir(cnf.DataDir)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tfor _, fi := range fis {\n+\t\tp := path.Join(cnf.DataDir, fi.Name())\n+\t\tif isDbDir(p) {\n+\t\t\tresult, err = addDirectory(result, backupData, cnf.DataDir, fi.Name())\n+\t\t\tif err != nil {\n+\t\t\t\treturn nil, err\n+\t\t\t}\n+\t\t}\n+\t}\n+\treturn result, nil\n+}\n+\n+// Backup is the main entry point for a backup:\n+// - uses the BackupStorage service to store a new backup\n+// - shuts down Mysqld during the backup\n+// - remember if we were replicating, restore the exact same state\n+func Backup(mysqld MysqlDaemon, logger logutil.Logger, bucket, name string, backupConcurrency int, hookExtraEnv map[string]string) error {\n+\n+\t// start the backup with the BackupStorage\n+\tbs := backupstorage.GetBackupStorage()\n+\tbh, err := bs.StartBackup(bucket, name)\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"StartBackup failed: %v\", err)\n+\t}\n+\n+\tif err = backup(mysqld, logger, bh, backupConcurrency, hookExtraEnv); err != nil {\n+\t\tif abortErr := bh.AbortBackup(); abortErr != nil {\n+\t\t\tlogger.Errorf(\"failed to abort backup: %v\", abortErr)\n+\t\t}\n+\t\treturn err\n+\t}\n+\treturn bh.EndBackup()\n+}\n+\n+func backup(mysqld MysqlDaemon, logger logutil.Logger, bh backupstorage.BackupHandle, backupConcurrency int, hookExtraEnv map[string]string) error {\n+\n+\t// save initial state so we can restore\n+\tslaveStartRequired := false\n+\tsourceIsMaster := false\n+\treadOnly := true\n+\tvar replicationPosition proto.ReplicationPosition\n+\n+\t// see if we need to restart replication after backup\n+\tlogger.Infof(\"getting current replication status\")\n+\tslaveStatus, err := mysqld.SlaveStatus()\n+\tswitch err {\n+\tcase nil:\n+\t\tslaveStartRequired = slaveStatus.SlaveRunning()\n+\tcase ErrNotSlave:\n+\t\t// keep going if we're the master, might be a degenerate case\n+\t\tsourceIsMaster = true\n+\tdefault:\n+\t\treturn fmt.Errorf(\"cannot get slave status: %v\", err)\n+\t}\n+\n+\t// get the read-only flag\n+\treadOnly, err = mysqld.IsReadOnly()\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"cannot get read only status: %v\", err)\n+\t}\n+\n+\t// get the replication position\n+\tif sourceIsMaster {\n+\t\tif !readOnly {\n+\t\t\tlogger.Infof(\"turning master read-onyl before backup\")\n+\t\t\tif err = mysqld.SetReadOnly(true); err != nil {\n+\t\t\t\treturn fmt.Errorf(\"cannot get read only status: %v\", err)\n+\t\t\t}\n+\t\t}\n+\t\treplicationPosition, err = mysqld.MasterPosition()\n+\t\tif err != nil {\n+\t\t\treturn fmt.Errorf(\"cannot get master position: %v\", err)\n+\t\t}\n+\t} else {\n+\t\tif err = StopSlave(mysqld, hookExtraEnv); err != nil {\n+\t\t\treturn fmt.Errorf(\"cannot stop slave: %v\", err)\n+\t\t}\n+\t\tvar slaveStatus proto.ReplicationStatus\n+\t\tslaveStatus, err = mysqld.SlaveStatus()\n+\t\tif err != nil {\n+\t\t\treturn fmt.Errorf(\"cannot get slave status: %v\", err)\n+\t\t}\n+\t\treplicationPosition = slaveStatus.Position\n+\t}\n+\tlogger.Infof(\"using replication position: %v\", replicationPosition)\n+\n+\t// shutdown mysqld\n+\tif err = mysqld.Shutdown(true, MysqlWaitTime); err != nil {\n+\t\treturn fmt.Errorf(\"cannot shutdown mysqld: %v\", err)\n+\t}\n+\n+\t// get the files to backup\n+\tfes, err := findFilesTobackup(mysqld.Cnf())\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"cannot find files to backup: %v\", err)\n+\t}\n+\tlogger.Infof(\"found %v files to backup\", len(fes))\n+\n+\t// backup everything\n+\tif err := backupFiles(mysqld, logger, bh, fes, replicationPosition, backupConcurrency); err != nil {\n+\t\treturn fmt.Errorf(\"cannot backup files: %v\", err)\n+\t}\n+\n+\t// Try to restart mysqld\n+\tif err := mysqld.Start(MysqlWaitTime); err != nil {\n+\t\treturn fmt.Errorf(\"cannot restart mysqld: %v\", err)\n+\t}\n+\n+\t// Restore original mysqld state that we saved above.\n+\tif slaveStartRequired {\n+\t\tlogger.Infof(\"restarting mysql replication\")\n+\t\tif err := StartSlave(mysqld, hookExtraEnv); err != nil {\n+\t\t\treturn fmt.Errorf(\"cannot restart slave: %v\", err)\n+\t\t}\n+\n+\t\t// this should be quick, but we might as well just wait\n+\t\tif err := WaitForSlaveStart(mysqld, slaveStartDeadline); err != nil {\n+\t\t\treturn fmt.Errorf(\"slave is not restarting: %v\", err)\n+\t\t}\n+\t}\n+\n+\t// And set read-only mode\n+\tlogger.Infof(\"resetting mysqld read-only to %v\", readOnly)\n+\tif err := mysqld.SetReadOnly(readOnly); err != nil {\n+\t\treturn err\n+\t}\n+\n+\treturn nil\n+}\n+\n+func backupFiles(mysqld MysqlDaemon, logger logutil.Logger, bh backupstorage.BackupHandle, fes []FileEntry, replicationPosition proto.ReplicationPosition, backupConcurrency int) error {\n+\n+\tsema := sync2.NewSemaphore(backupConcurrency, 0)\n+\trec := concurrency.AllErrorRecorder{}\n+\twg := sync.WaitGroup{}\n+\tfor i, fe := range fes {\n+\t\twg.Add(1)\n+\t\tgo func(i int, fe FileEntry) {\n+\t\t\tdefer wg.Done()\n+\n+\t\t\t// wait until we are ready to go, skip if we already\n+\t\t\t// encountered an error\n+\t\t\tsema.Acquire()\n+\t\t\tdefer sema.Release()\n+\t\t\tif rec.HasErrors() {\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\t// open the source file for reading\n+\t\t\tsource, err := fe.open(mysqld.Cnf(), true)\n+\t\t\tif err != nil {\n+\t\t\t\trec.RecordError(err)\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tdefer source.Close()\n+\n+\t\t\t// open the destination file for writing, and a buffer\n+\t\t\tname := fmt.Sprintf(\"%v\", i)\n+\t\t\twc, err := bh.AddFile(name)\n+\t\t\tif err != nil {\n+\t\t\t\trec.RecordError(fmt.Errorf(\"cannot add file: %v\", err))\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tdefer wc.Close()\n+\t\t\tdst := bufio.NewWriterSize(wc, 2*1024*1024)\n+\n+\t\t\t// create the hasher and the tee on top\n+\t\t\thasher := newHasher()\n+\t\t\ttee := io.MultiWriter(dst, hasher)\n+\n+\t\t\t// create the gzip compression filter\n+\t\t\tgzip, err := cgzip.NewWriterLevel(tee, cgzip.Z_BEST_SPEED)\n+\t\t\tif err != nil {\n+\t\t\t\trec.RecordError(fmt.Errorf(\"cannot create gziper: %v\", err))\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\t// copy from the source file to gzip to tee to output file and hasher\n+\t\t\t_, err = io.Copy(gzip, source)\n+\t\t\tif err != nil {\n+\t\t\t\trec.RecordError(fmt.Errorf(\"cannot copy data: %v\", err))\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\t// close gzip to flush it, after that the hash is good\n+\t\t\tif err = gzip.Close(); err != nil {\n+\t\t\t\trec.RecordError(fmt.Errorf(\"cannot close gzip: %v\", err))\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\t// flush the buffer to finish writing, save the hash\n+\t\t\tdst.Flush()\n+\t\t\tfes[i].Hash = hasher.HashString()\n+\t\t}(i, fe)\n+\t}\n+\n+\twg.Wait()\n+\tif rec.HasErrors() {\n+\t\treturn rec.Error()\n+\t}\n+\n+\t// open the MANIFEST\n+\twc, err := bh.AddFile(backupManifest)\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"cannot add %v to backup: %v\", backupManifest, err)\n+\t}\n+\tdefer wc.Close()\n+\n+\t// JSON-encode and write the MANIFEST\n+\tbm := &BackupManifest{\n+\t\tFileEntries:         fes,\n+\t\tReplicationPosition: replicationPosition,\n+\t}\n+\tdata, err := json.MarshalIndent(bm, \"\", \"  \")\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"cannot JSON encode %v: %v\", backupManifest, err)\n+\t}\n+\tif _, err := wc.Write([]byte(data)); err != nil {\n+\t\treturn fmt.Errorf(\"cannot write %v: %v\", backupManifest, err)\n+\t}\n+\n+\treturn nil\n+}\n+\n+// checkNoDB makes sure there is no vt_ db already there. Used by Restore,\n+// we do not wnat to destroy an existing DB.\n+func checkNoDB(mysqld MysqlDaemon) error {\n+\tqr, err := mysqld.FetchSuperQuery(\"SHOW DATABASES\")\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"checkNoDB failed: %v\", err)\n+\t}\n+\n+\tfor _, row := range qr.Rows {\n+\t\tif strings.HasPrefix(row[0].String(), \"vt_\") {\n+\t\t\tdbName := row[0].String()\n+\t\t\ttableQr, err := mysqld.FetchSuperQuery(\"SHOW TABLES FROM \" + dbName)\n+\t\t\tif err != nil {\n+\t\t\t\treturn fmt.Errorf(\"checkNoDB failed: %v\", err)\n+\t\t\t} else if len(tableQr.Rows) == 0 {\n+\t\t\t\t// no tables == empty db, all is well\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\treturn fmt.Errorf(\"checkNoDB failed, found active db %v\", dbName)\n+\t\t}\n+\t}\n+\n+\treturn nil\n+}\n+\n+// restoreFiles will copy all the files from the BackupStorage to the\n+// right place\n+func restoreFiles(cnf *Mycnf, bh backupstorage.BackupHandle, fes []FileEntry, restoreConcurrency int) error {\n+\tsema := sync2.NewSemaphore(restoreConcurrency, 0)\n+\trec := concurrency.AllErrorRecorder{}\n+\twg := sync.WaitGroup{}\n+\tfor i, fe := range fes {\n+\t\twg.Add(1)\n+\t\tgo func(i int, fe FileEntry) {\n+\t\t\tdefer wg.Done()\n+\n+\t\t\t// wait until we are ready to go, skip if we already\n+\t\t\t// encountered an error\n+\t\t\tsema.Acquire()\n+\t\t\tdefer sema.Release()\n+\t\t\tif rec.HasErrors() {\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\t// open the source file for reading\n+\t\t\tname := fmt.Sprintf(\"%v\", i)\n+\t\t\tsource, err := bh.ReadFile(name)\n+\t\t\tif err != nil {\n+\t\t\t\trec.RecordError(err)\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tdefer source.Close()\n+\n+\t\t\t// open the destination file for writing\n+\t\t\tdstFile, err := fe.open(cnf, false)\n+\t\t\tif err != nil {\n+\t\t\t\trec.RecordError(err)\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tdefer dstFile.Close()\n+\n+\t\t\t// create a buffering output\n+\t\t\tdst := bufio.NewWriterSize(dstFile, 2*1024*1024)\n+\n+\t\t\t// create hash to write the compressed data to\n+\t\t\thasher := newHasher()\n+\n+\t\t\t// create a Tee: we split the input into the hasher\n+\t\t\t// and into the gunziper\n+\t\t\ttee := io.TeeReader(source, hasher)\n+\n+\t\t\t// create the uncompresser\n+\t\t\tgz, err := cgzip.NewReader(tee)\n+\t\t\tif err != nil {\n+\t\t\t\trec.RecordError(err)\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tdefer gz.Close()\n+\n+\t\t\t// copy the data. Will also write to the hasher\n+\t\t\tif _, err = io.Copy(dst, gz); err != nil {\n+\t\t\t\trec.RecordError(err)\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\t// check the hash\n+\t\t\thash := hasher.HashString()\n+\t\t\tif hash != fe.Hash {\n+\t\t\t\trec.RecordError(fmt.Errorf(\"hash mismatch for %v, got %v expected %v\", fe.Name, hash, fe.Hash))\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\t// flush the buffer\n+\t\t\tdst.Flush()\n+\t\t}(i, fe)\n+\t}\n+\twg.Wait()\n+\treturn rec.Error()\n+}\n+\n+// Restore is the main entry point for backup restore.  If there is no\n+// appropriate backup on the BackupStorage, Restore logs an error\n+// and returns ErrNoBackup. Any other error is returned.\n+func Restore(mysqld MysqlDaemon, bucket string, restoreConcurrency int, hookExtraEnv map[string]string) (proto.ReplicationPosition, error) {\n+\t// find the right backup handle: most recent one, with a MANIFEST\n+\tlog.Infof(\"Restore: looking for a suitable backup to restore\")\n+\tbs := backupstorage.GetBackupStorage()\n+\tbhs, err := bs.ListBackups(bucket)\n+\tif err != nil {\n+\t\treturn proto.ReplicationPosition{}, fmt.Errorf(\"ListBackups failed: %v\", err)\n+\t}\n+\ttoRestore := len(bhs) - 1\n+\tvar bh backupstorage.BackupHandle\n+\tvar bm BackupManifest\n+\tfor toRestore >= 0 {","path":"go/vt/mysqlctl/backup.go","position":511,"original_position":505,"commit_id":"d89f042c5a9a32c40a614555490f9710a34b6762","original_commit_id":"cf568fdc28ec1de86bab9f7f0222cc3442530bc7","user":{"login":"alainjobart","id":4625714,"avatar_url":"https://avatars.githubusercontent.com/u/4625714?v=3","gravatar_id":"","url":"https://api.github.com/users/alainjobart","html_url":"https://github.com/alainjobart","followers_url":"https://api.github.com/users/alainjobart/followers","following_url":"https://api.github.com/users/alainjobart/following{/other_user}","gists_url":"https://api.github.com/users/alainjobart/gists{/gist_id}","starred_url":"https://api.github.com/users/alainjobart/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/alainjobart/subscriptions","organizations_url":"https://api.github.com/users/alainjobart/orgs","repos_url":"https://api.github.com/users/alainjobart/repos","events_url":"https://api.github.com/users/alainjobart/events{/privacy}","received_events_url":"https://api.github.com/users/alainjobart/received_events","type":"User","site_admin":false},"body":"Added a comment to say ListBackup returns them ordered. The other use case is 'vtctl ListBackups' which returns them in ascending order (old to new) so I think this is good. I need the loop anyway to find the latest correct one.","created_at":"2015-05-21T16:41:42Z","updated_at":"2015-05-21T16:41:42Z","html_url":"https://github.com/youtube/vitess/pull/705#discussion_r30820921","pull_request_url":"https://api.github.com/repos/youtube/vitess/pulls/705","_links":{"self":{"href":"https://api.github.com/repos/youtube/vitess/pulls/comments/30820921"},"html":{"href":"https://github.com/youtube/vitess/pull/705#discussion_r30820921"},"pull_request":{"href":"https://api.github.com/repos/youtube/vitess/pulls/705"}}},"pull_request":{"url":"https://api.github.com/repos/youtube/vitess/pulls/705","id":35875467,"html_url":"https://github.com/youtube/vitess/pull/705","diff_url":"https://github.com/youtube/vitess/pull/705.diff","patch_url":"https://github.com/youtube/vitess/pull/705.patch","issue_url":"https://api.github.com/repos/youtube/vitess/issues/705","number":705,"state":"open","locked":false,"title":"Resharding","user":{"login":"alainjobart","id":4625714,"avatar_url":"https://avatars.githubusercontent.com/u/4625714?v=3","gravatar_id":"","url":"https://api.github.com/users/alainjobart","html_url":"https://github.com/alainjobart","followers_url":"https://api.github.com/users/alainjobart/followers","following_url":"https://api.github.com/users/alainjobart/following{/other_user}","gists_url":"https://api.github.com/users/alainjobart/gists{/gist_id}","starred_url":"https://api.github.com/users/alainjobart/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/alainjobart/subscriptions","organizations_url":"https://api.github.com/users/alainjobart/orgs","repos_url":"https://api.github.com/users/alainjobart/repos","events_url":"https://api.github.com/users/alainjobart/events{/privacy}","received_events_url":"https://api.github.com/users/alainjobart/received_events","type":"User","site_admin":false},"body":"@enisoc ( @aaijazi if interested)\r\nOk here we go, adding new backup scheme, removing old one.","created_at":"2015-05-20T20:59:06Z","updated_at":"2015-05-21T16:41:42Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0279824deedda4914de5a36b0ba2507a14109507","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/youtube/vitess/pulls/705/commits","review_comments_url":"https://api.github.com/repos/youtube/vitess/pulls/705/comments","review_comment_url":"https://api.github.com/repos/youtube/vitess/pulls/comments{/number}","comments_url":"https://api.github.com/repos/youtube/vitess/issues/705/comments","statuses_url":"https://api.github.com/repos/youtube/vitess/statuses/d89f042c5a9a32c40a614555490f9710a34b6762","head":{"label":"youtube:resharding","ref":"resharding","sha":"d89f042c5a9a32c40a614555490f9710a34b6762","user":{"login":"youtube","id":4052902,"avatar_url":"https://avatars.githubusercontent.com/u/4052902?v=3","gravatar_id":"","url":"https://api.github.com/users/youtube","html_url":"https://github.com/youtube","followers_url":"https://api.github.com/users/youtube/followers","following_url":"https://api.github.com/users/youtube/following{/other_user}","gists_url":"https://api.github.com/users/youtube/gists{/gist_id}","starred_url":"https://api.github.com/users/youtube/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/youtube/subscriptions","organizations_url":"https://api.github.com/users/youtube/orgs","repos_url":"https://api.github.com/users/youtube/repos","events_url":"https://api.github.com/users/youtube/events{/privacy}","received_events_url":"https://api.github.com/users/youtube/received_events","type":"Organization","site_admin":false},"repo":{"id":11008207,"name":"vitess","full_name":"youtube/vitess","owner":{"login":"youtube","id":4052902,"avatar_url":"https://avatars.githubusercontent.com/u/4052902?v=3","gravatar_id":"","url":"https://api.github.com/users/youtube","html_url":"https://github.com/youtube","followers_url":"https://api.github.com/users/youtube/followers","following_url":"https://api.github.com/users/youtube/following{/other_user}","gists_url":"https://api.github.com/users/youtube/gists{/gist_id}","starred_url":"https://api.github.com/users/youtube/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/youtube/subscriptions","organizations_url":"https://api.github.com/users/youtube/orgs","repos_url":"https://api.github.com/users/youtube/repos","events_url":"https://api.github.com/users/youtube/events{/privacy}","received_events_url":"https://api.github.com/users/youtube/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/youtube/vitess","description":"vitess provides servers and tools which facilitate scaling of MySQL databases for large scale web services.","fork":false,"url":"https://api.github.com/repos/youtube/vitess","forks_url":"https://api.github.com/repos/youtube/vitess/forks","keys_url":"https://api.github.com/repos/youtube/vitess/keys{/key_id}","collaborators_url":"https://api.github.com/repos/youtube/vitess/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/youtube/vitess/teams","hooks_url":"https://api.github.com/repos/youtube/vitess/hooks","issue_events_url":"https://api.github.com/repos/youtube/vitess/issues/events{/number}","events_url":"https://api.github.com/repos/youtube/vitess/events","assignees_url":"https://api.github.com/repos/youtube/vitess/assignees{/user}","branches_url":"https://api.github.com/repos/youtube/vitess/branches{/branch}","tags_url":"https://api.github.com/repos/youtube/vitess/tags","blobs_url":"https://api.github.com/repos/youtube/vitess/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/youtube/vitess/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/youtube/vitess/git/refs{/sha}","trees_url":"https://api.github.com/repos/youtube/vitess/git/trees{/sha}","statuses_url":"https://api.github.com/repos/youtube/vitess/statuses/{sha}","languages_url":"https://api.github.com/repos/youtube/vitess/languages","stargazers_url":"https://api.github.com/repos/youtube/vitess/stargazers","contributors_url":"https://api.github.com/repos/youtube/vitess/contributors","subscribers_url":"https://api.github.com/repos/youtube/vitess/subscribers","subscription_url":"https://api.github.com/repos/youtube/vitess/subscription","commits_url":"https://api.github.com/repos/youtube/vitess/commits{/sha}","git_commits_url":"https://api.github.com/repos/youtube/vitess/git/commits{/sha}","comments_url":"https://api.github.com/repos/youtube/vitess/comments{/number}","issue_comment_url":"https://api.github.com/repos/youtube/vitess/issues/comments{/number}","contents_url":"https://api.github.com/repos/youtube/vitess/contents/{+path}","compare_url":"https://api.github.com/repos/youtube/vitess/compare/{base}...{head}","merges_url":"https://api.github.com/repos/youtube/vitess/merges","archive_url":"https://api.github.com/repos/youtube/vitess/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/youtube/vitess/downloads","issues_url":"https://api.github.com/repos/youtube/vitess/issues{/number}","pulls_url":"https://api.github.com/repos/youtube/vitess/pulls{/number}","milestones_url":"https://api.github.com/repos/youtube/vitess/milestones{/number}","notifications_url":"https://api.github.com/repos/youtube/vitess/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/youtube/vitess/labels{/name}","releases_url":"https://api.github.com/repos/youtube/vitess/releases{/id}","created_at":"2013-06-27T21:20:28Z","updated_at":"2015-05-21T14:51:51Z","pushed_at":"2015-05-21T16:40:22Z","git_url":"git://github.com/youtube/vitess.git","ssh_url":"git@github.com:youtube/vitess.git","clone_url":"https://github.com/youtube/vitess.git","svn_url":"https://github.com/youtube/vitess","homepage":null,"size":71096,"stargazers_count":2533,"watchers_count":2533,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":300,"mirror_url":null,"open_issues_count":18,"forks":300,"open_issues":18,"watchers":2533,"default_branch":"master"}},"base":{"label":"youtube:master","ref":"master","sha":"65aa8a6022e5a0103c5cab2d7662ef0ba41f83a4","user":{"login":"youtube","id":4052902,"avatar_url":"https://avatars.githubusercontent.com/u/4052902?v=3","gravatar_id":"","url":"https://api.github.com/users/youtube","html_url":"https://github.com/youtube","followers_url":"https://api.github.com/users/youtube/followers","following_url":"https://api.github.com/users/youtube/following{/other_user}","gists_url":"https://api.github.com/users/youtube/gists{/gist_id}","starred_url":"https://api.github.com/users/youtube/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/youtube/subscriptions","organizations_url":"https://api.github.com/users/youtube/orgs","repos_url":"https://api.github.com/users/youtube/repos","events_url":"https://api.github.com/users/youtube/events{/privacy}","received_events_url":"https://api.github.com/users/youtube/received_events","type":"Organization","site_admin":false},"repo":{"id":11008207,"name":"vitess","full_name":"youtube/vitess","owner":{"login":"youtube","id":4052902,"avatar_url":"https://avatars.githubusercontent.com/u/4052902?v=3","gravatar_id":"","url":"https://api.github.com/users/youtube","html_url":"https://github.com/youtube","followers_url":"https://api.github.com/users/youtube/followers","following_url":"https://api.github.com/users/youtube/following{/other_user}","gists_url":"https://api.github.com/users/youtube/gists{/gist_id}","starred_url":"https://api.github.com/users/youtube/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/youtube/subscriptions","organizations_url":"https://api.github.com/users/youtube/orgs","repos_url":"https://api.github.com/users/youtube/repos","events_url":"https://api.github.com/users/youtube/events{/privacy}","received_events_url":"https://api.github.com/users/youtube/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/youtube/vitess","description":"vitess provides servers and tools which facilitate scaling of MySQL databases for large scale web services.","fork":false,"url":"https://api.github.com/repos/youtube/vitess","forks_url":"https://api.github.com/repos/youtube/vitess/forks","keys_url":"https://api.github.com/repos/youtube/vitess/keys{/key_id}","collaborators_url":"https://api.github.com/repos/youtube/vitess/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/youtube/vitess/teams","hooks_url":"https://api.github.com/repos/youtube/vitess/hooks","issue_events_url":"https://api.github.com/repos/youtube/vitess/issues/events{/number}","events_url":"https://api.github.com/repos/youtube/vitess/events","assignees_url":"https://api.github.com/repos/youtube/vitess/assignees{/user}","branches_url":"https://api.github.com/repos/youtube/vitess/branches{/branch}","tags_url":"https://api.github.com/repos/youtube/vitess/tags","blobs_url":"https://api.github.com/repos/youtube/vitess/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/youtube/vitess/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/youtube/vitess/git/refs{/sha}","trees_url":"https://api.github.com/repos/youtube/vitess/git/trees{/sha}","statuses_url":"https://api.github.com/repos/youtube/vitess/statuses/{sha}","languages_url":"https://api.github.com/repos/youtube/vitess/languages","stargazers_url":"https://api.github.com/repos/youtube/vitess/stargazers","contributors_url":"https://api.github.com/repos/youtube/vitess/contributors","subscribers_url":"https://api.github.com/repos/youtube/vitess/subscribers","subscription_url":"https://api.github.com/repos/youtube/vitess/subscription","commits_url":"https://api.github.com/repos/youtube/vitess/commits{/sha}","git_commits_url":"https://api.github.com/repos/youtube/vitess/git/commits{/sha}","comments_url":"https://api.github.com/repos/youtube/vitess/comments{/number}","issue_comment_url":"https://api.github.com/repos/youtube/vitess/issues/comments{/number}","contents_url":"https://api.github.com/repos/youtube/vitess/contents/{+path}","compare_url":"https://api.github.com/repos/youtube/vitess/compare/{base}...{head}","merges_url":"https://api.github.com/repos/youtube/vitess/merges","archive_url":"https://api.github.com/repos/youtube/vitess/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/youtube/vitess/downloads","issues_url":"https://api.github.com/repos/youtube/vitess/issues{/number}","pulls_url":"https://api.github.com/repos/youtube/vitess/pulls{/number}","milestones_url":"https://api.github.com/repos/youtube/vitess/milestones{/number}","notifications_url":"https://api.github.com/repos/youtube/vitess/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/youtube/vitess/labels{/name}","releases_url":"https://api.github.com/repos/youtube/vitess/releases{/id}","created_at":"2013-06-27T21:20:28Z","updated_at":"2015-05-21T14:51:51Z","pushed_at":"2015-05-21T16:40:22Z","git_url":"git://github.com/youtube/vitess.git","ssh_url":"git@github.com:youtube/vitess.git","clone_url":"https://github.com/youtube/vitess.git","svn_url":"https://github.com/youtube/vitess","homepage":null,"size":71096,"stargazers_count":2533,"watchers_count":2533,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":300,"mirror_url":null,"open_issues_count":18,"forks":300,"open_issues":18,"watchers":2533,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/youtube/vitess/pulls/705"},"html":{"href":"https://github.com/youtube/vitess/pull/705"},"issue":{"href":"https://api.github.com/repos/youtube/vitess/issues/705"},"comments":{"href":"https://api.github.com/repos/youtube/vitess/issues/705/comments"},"review_comments":{"href":"https://api.github.com/repos/youtube/vitess/pulls/705/comments"},"review_comment":{"href":"https://api.github.com/repos/youtube/vitess/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/youtube/vitess/pulls/705/commits"},"statuses":{"href":"https://api.github.com/repos/youtube/vitess/statuses/d89f042c5a9a32c40a614555490f9710a34b6762"}}}}