{"action":"created","comment":{"url":"https://api.github.com/repos/influxdb/influxdb/pulls/comments/35016322","id":35016322,"diff_hunk":"@@ -0,0 +1,430 @@\n+package meta\n+\n+import (\n+\t\"encoding/binary\"\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"io\"\n+\t\"io/ioutil\"\n+\t\"log\"\n+\t\"net\"\n+\t\"time\"\n+\n+\t\"github.com/gogo/protobuf/proto\"\n+\t\"github.com/influxdb/influxdb/meta/internal\"\n+)\n+\n+// RPC handles request/response style messaging between cluster nodes\n+type RPC struct {\n+\tLogger         *log.Logger\n+\ttracingEnabled bool\n+\n+\tstore interface {\n+\t\tcachedData() *Data\n+\t\tIsLeader() bool\n+\t\tLeader() string\n+\t\tPeers() []string\n+\t\tAddPeer(host string) error\n+\t\tCreateNode(host string) (*NodeInfo, error)\n+\t\tNodeByHost(host string) (*NodeInfo, error)\n+\t\tWaitForDataChanged() error\n+\t}\n+}\n+\n+type JoinResult struct {\n+\tRaftEnabled bool\n+\tPeers       []string\n+\tNodeID      uint64\n+}\n+\n+type Reply interface {\n+\tGetHeader() *internal.ResponseHeader\n+}\n+\n+// proxyLeader proxies the connection to the current raft leader\n+func (r *RPC) proxyLeader(conn *net.TCPConn) {\n+\tif r.store.Leader() == \"\" {\n+\t\tr.sendError(conn, \"no leader\")\n+\t\treturn\n+\t}\n+\n+\tleaderConn, err := net.DialTimeout(\"tcp\", r.store.Leader(), 10*time.Second)\n+\tif err != nil {\n+\t\tr.sendError(conn, fmt.Sprintf(\"dial leader: %v\", err))\n+\t\treturn\n+\t}\n+\tdefer leaderConn.Close()\n+\n+\tleaderConn.Write([]byte{MuxRPCHeader})\n+\tif err := proxy(leaderConn.(*net.TCPConn), conn); err != nil {\n+\t\tr.sendError(conn, fmt.Sprintf(\"leader proxy error: %v\", err))\n+\t}\n+}\n+\n+// handleRPCConn reads a command from the connection and executes it.\n+func (r *RPC) handleRPCConn(conn net.Conn) {\n+\tdefer conn.Close()\n+\t// RPC connections should execute on the leader.  If we are not the leader,\n+\t// proxy the connection to the leader so that clients an connect to any node\n+\t// in the cluster.\n+\tr.traceCluster(\"rpc connection from: %v\", conn.RemoteAddr())\n+\n+\tif !r.store.IsLeader() {\n+\t\tr.proxyLeader(conn.(*net.TCPConn))\n+\t\treturn\n+\t}\n+\n+\t// Read and execute request.\n+\ttyp, resp, err := func() (internal.RPCType, proto.Message, error) {\n+\t\t// Read request size.\n+\t\tvar sz uint64\n+\t\tif err := binary.Read(conn, binary.BigEndian, &sz); err != nil {\n+\t\t\treturn internal.RPCType_Error, nil, fmt.Errorf(\"read size: %s\", err)\n+\t\t}\n+\n+\t\t// Read request.\n+\t\tbuf := make([]byte, sz)\n+\t\tif _, err := io.ReadFull(conn, buf); err != nil {\n+\t\t\treturn internal.RPCType_Error, nil, fmt.Errorf(\"read request: %s\", err)\n+\t\t}\n+\n+\t\t// Determine the RPC type\n+\t\trpcType := internal.RPCType(btou64(buf[0:8]))\n+\t\tbuf = buf[8:]\n+\n+\t\tr.traceCluster(\"recv %v request on: %v\", rpcType, conn.RemoteAddr())\n+\t\tswitch rpcType {\n+\t\tcase internal.RPCType_FetchData:\n+\t\t\tvar req internal.FetchDataRequest\n+\t\t\tif err := proto.Unmarshal(buf, &req); err != nil {\n+\t\t\t\treturn internal.RPCType_Error, nil, fmt.Errorf(\"fetch request unmarshal: %v\", err)\n+\t\t\t}\n+\t\t\tresp, err := r.handleFetchData(&req)\n+\t\t\treturn rpcType, resp, err\n+\t\tcase internal.RPCType_Join:\n+\t\t\tvar req internal.JoinRequest\n+\t\t\tif err := proto.Unmarshal(buf, &req); err != nil {\n+\t\t\t\treturn internal.RPCType_Error, nil, fmt.Errorf(\"join request unmarshal: %v\", err)\n+\t\t\t}\n+\t\t\tresp, err := r.handleJoinRequest(&req)\n+\t\t\treturn rpcType, resp, err\n+\t\tdefault:\n+\t\t\treturn internal.RPCType_Error, nil, fmt.Errorf(\"unknown rpc type:%v\", rpcType)\n+\t\t}\n+\t}()\n+\n+\t// Handle unexpected RPC errors\n+\tif err != nil {\n+\t\tresp = &internal.ErrorResponse{\n+\t\t\tHeader: &internal.ResponseHeader{\n+\t\t\t\tOK: proto.Bool(false),\n+\t\t\t},\n+\t\t}\n+\t\ttyp = internal.RPCType_Error\n+\t}\n+\n+\t// Set the status header and error message\n+\tif reply, ok := resp.(Reply); ok {\n+\t\treply.GetHeader().OK = proto.Bool(err == nil)\n+\t\tif err != nil {\n+\t\t\treply.GetHeader().Error = proto.String(err.Error())\n+\t\t}\n+\t}\n+\n+\tr.sendResponse(conn, typ, resp)\n+}\n+\n+func (r *RPC) sendResponse(conn net.Conn, typ internal.RPCType, resp proto.Message) {\n+\t// Marshal the response back to a protobuf\n+\tbuf, err := proto.Marshal(resp)\n+\tif err != nil {\n+\t\tr.Logger.Printf(\"unable to marshal response: %v\", err)\n+\t\treturn\n+\t}\n+\n+\t// Encode response back to connection.\n+\tif _, err := conn.Write(r.pack(typ, buf)); err != nil {\n+\t\tr.Logger.Printf(\"unable to write rpc response: %s\", err)\n+\t}\n+}\n+\n+func (r *RPC) sendError(conn net.Conn, msg string) {\n+\tr.traceCluster(msg)\n+\tresp := &internal.ErrorResponse{\n+\t\tHeader: &internal.ResponseHeader{\n+\t\t\tOK:    proto.Bool(false),\n+\t\t\tError: proto.String(msg),\n+\t\t},\n+\t}\n+\n+\tr.sendResponse(conn, internal.RPCType_Error, resp)\n+}\n+\n+// handleFetchData handles a request for the current nodes meta data\n+func (r *RPC) handleFetchData(req *internal.FetchDataRequest) (*internal.FetchDataResponse, error) {\n+\tvar (\n+\t\tb    []byte\n+\t\tdata *Data\n+\t\terr  error\n+\t)\n+\n+\tfor {\n+\t\tdata = r.store.cachedData()\n+\t\tif data.Index != req.GetIndex() {\n+\t\t\tb, err = data.MarshalBinary()\n+\t\t\tif err != nil {\n+\t\t\t\treturn nil, err\n+\t\t\t}\n+\t\t\tbreak\n+\t\t}\n+\n+\t\tif !req.GetBlocking() {\n+\t\t\tbreak\n+\t\t}\n+\n+\t\tif err := r.store.WaitForDataChanged(); err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t}\n+\n+\treturn &internal.FetchDataResponse{\n+\t\tHeader: &internal.ResponseHeader{\n+\t\t\tOK: proto.Bool(true),\n+\t\t},\n+\t\tIndex: proto.Uint64(data.Index),\n+\t\tTerm:  proto.Uint64(data.Term),\n+\t\tData:  b}, nil\n+}\n+\n+// handleJoinRequest handles a request to join the cluster\n+func (r *RPC) handleJoinRequest(req *internal.JoinRequest) (*internal.JoinResponse, error) {\n+\tr.traceCluster(\"join request from: %v\", *req.Addr)\n+\n+\tnode, err := func() (*NodeInfo, error) {\n+\t\t// attempt to create the node\n+\t\tnode, err := r.store.CreateNode(*req.Addr)\n+\t\t// if it exists, return the exting node\n+\t\tif err == ErrNodeExists {\n+\t\t\treturn r.store.NodeByHost(*req.Addr)\n+\t\t} else if err != nil {\n+\t\t\treturn nil, fmt.Errorf(\"create node: %v\", err)\n+\t\t}\n+\n+\t\t// FIXME: jwilder: adding raft nodes is tricky since going\n+\t\t// from 1 node (leader) to two kills the cluster because\n+\t\t// quorum is lost after adding the second node.  For now,\n+\t\t// can only add non-raft enabled nodes\n+\n+\t\t// If we have less than 3 nodes, add them as raft peers\n+\t\t// if len(r.store.Peers()) < MaxRaftNodes {\n+\t\t// \tif err = r.store.AddPeer(*req.Addr); err != nil {\n+\t\t// \t\treturn node, fmt.Errorf(\"add peer: %v\", err)\n+\t\t// \t}\n+\t\t// }\n+\t\treturn node, err\n+\t}()\n+\n+\tnodeID := uint64(0)\n+\tif node != nil {\n+\t\tnodeID = node.ID\n+\t}\n+\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\treturn &internal.JoinResponse{\n+\t\tHeader: &internal.ResponseHeader{\n+\t\t\tOK: proto.Bool(true),\n+\t\t},\n+\t\t//EnableRaft: proto.Bool(contains(r.store.Peers(), *req.Addr)),\n+\t\tEnableRaft: proto.Bool(false),\n+\t\tPeers:      r.store.Peers(),\n+\t\tNodeID:     proto.Uint64(nodeID),\n+\t}, err\n+\n+}\n+\n+// pack returns a TLV style byte slice encoding the size of the payload, the RPC type\n+// and the RPC data\n+func (r *RPC) pack(typ internal.RPCType, b []byte) []byte {\n+\tbuf := u64tob(uint64(len(b)) + 8)\n+\tbuf = append(buf, u64tob(uint64(typ))...)\n+\tbuf = append(buf, b...)\n+\treturn buf\n+}\n+\n+// fetchMetaData returns the latest copy of the meta store data from the current\n+// leader.\n+func (r *RPC) fetchMetaData(blocking bool) (*Data, error) {\n+\t// Retrieve the current known leader.\n+\tleader := r.store.Leader()\n+\tif leader == \"\" {\n+\t\treturn nil, errors.New(\"no leader\")\n+\t}\n+\n+\tvar index, term uint64\n+\tdata := r.store.cachedData()\n+\tif data != nil {\n+\t\tindex = data.Index\n+\t\tterm = data.Index\n+\t}\n+\tresp, err := r.call(leader, &internal.FetchDataRequest{\n+\t\tIndex:    proto.Uint64(index),\n+\t\tTerm:     proto.Uint64(term),\n+\t\tBlocking: proto.Bool(blocking),\n+\t})\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tswitch t := resp.(type) {\n+\tcase *internal.FetchDataResponse:\n+\t\t// If data is nil, then the term and index we sent matches the leader\n+\t\tif t.GetData() == nil {\n+\t\t\treturn nil, nil\n+\t\t}\n+\t\tms := &Data{}\n+\t\tif err := ms.UnmarshalBinary(t.GetData()); err != nil {\n+\t\t\treturn nil, fmt.Errorf(\"rpc unmarshal metadata: %v\", err)\n+\t\t}\n+\t\treturn ms, nil\n+\tcase *internal.ErrorResponse:\n+\t\treturn nil, fmt.Errorf(\"rpc failed: %s\", t.GetHeader().GetError())\n+\tdefault:\n+\t\treturn nil, fmt.Errorf(\"rpc failed: unknown response type: %v\", t.String())\n+\t}\n+}\n+\n+// join attempts to join a cluster at remoteAddr using localAddr as the current\n+// node's cluster address\n+func (r *RPC) join(localAddr, remoteAddr string) (*JoinResult, error) {\n+\treq := &internal.JoinRequest{\n+\t\tAddr: proto.String(localAddr),\n+\t}\n+\n+\tresp, err := r.call(remoteAddr, req)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tswitch t := resp.(type) {\n+\tcase *internal.JoinResponse:\n+\t\treturn &JoinResult{\n+\t\t\tRaftEnabled: t.GetEnableRaft(),\n+\t\t\tPeers:       t.GetPeers(),\n+\t\t\tNodeID:      t.GetNodeID(),\n+\t\t}, nil\n+\tcase *internal.ErrorResponse:\n+\t\treturn nil, fmt.Errorf(\"rpc failed: %s\", t.GetHeader().GetError())\n+\tdefault:\n+\t\treturn nil, fmt.Errorf(\"rpc failed: unknown response type: %v\", t.String())\n+\t}\n+}\n+\n+// call sends an encoded request to the remote leader and returns\n+// an encoded response value.\n+func (r *RPC) call(dest string, req proto.Message) (proto.Message, error) {\n+","path":"meta/rpc.go","position":328,"original_position":328,"commit_id":"754b4a1e30da80045a20554a63103ba7b5cc1793","original_commit_id":"754b4a1e30da80045a20554a63103ba7b5cc1793","user":{"login":"otoolep","id":536312,"avatar_url":"https://avatars.githubusercontent.com/u/536312?v=3","gravatar_id":"","url":"https://api.github.com/users/otoolep","html_url":"https://github.com/otoolep","followers_url":"https://api.github.com/users/otoolep/followers","following_url":"https://api.github.com/users/otoolep/following{/other_user}","gists_url":"https://api.github.com/users/otoolep/gists{/gist_id}","starred_url":"https://api.github.com/users/otoolep/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/otoolep/subscriptions","organizations_url":"https://api.github.com/users/otoolep/orgs","repos_url":"https://api.github.com/users/otoolep/repos","events_url":"https://api.github.com/users/otoolep/events{/privacy}","received_events_url":"https://api.github.com/users/otoolep/received_events","type":"User","site_admin":false},"body":"Nit: delete this blank line, I think.","created_at":"2015-07-20T16:50:12Z","updated_at":"2015-07-20T16:50:12Z","html_url":"https://github.com/influxdb/influxdb/pull/3372#discussion_r35016322","pull_request_url":"https://api.github.com/repos/influxdb/influxdb/pulls/3372","_links":{"self":{"href":"https://api.github.com/repos/influxdb/influxdb/pulls/comments/35016322"},"html":{"href":"https://github.com/influxdb/influxdb/pull/3372#discussion_r35016322"},"pull_request":{"href":"https://api.github.com/repos/influxdb/influxdb/pulls/3372"}}},"pull_request":{"url":"https://api.github.com/repos/influxdb/influxdb/pulls/3372","id":40277277,"html_url":"https://github.com/influxdb/influxdb/pull/3372","diff_url":"https://github.com/influxdb/influxdb/pull/3372.diff","patch_url":"https://github.com/influxdb/influxdb/pull/3372.patch","issue_url":"https://api.github.com/repos/influxdb/influxdb/issues/3372","number":3372,"state":"open","locked":false,"title":"Support joining nodes to an existing cluster","user":{"login":"jwilder","id":219935,"avatar_url":"https://avatars.githubusercontent.com/u/219935?v=3","gravatar_id":"","url":"https://api.github.com/users/jwilder","html_url":"https://github.com/jwilder","followers_url":"https://api.github.com/users/jwilder/followers","following_url":"https://api.github.com/users/jwilder/following{/other_user}","gists_url":"https://api.github.com/users/jwilder/gists{/gist_id}","starred_url":"https://api.github.com/users/jwilder/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jwilder/subscriptions","organizations_url":"https://api.github.com/users/jwilder/orgs","repos_url":"https://api.github.com/users/jwilder/repos","events_url":"https://api.github.com/users/jwilder/events{/privacy}","received_events_url":"https://api.github.com/users/jwilder/received_events","type":"User","site_admin":false},"body":"## Overview\r\n\r\nThis PR adds support for joining a new node to an existing cluster. It implements some of the functionality in #2966.\r\n\r\nThe way it works is that an existing cluster with an established raft leader must be running.  The new node should be started with `-join addr:port` where `addr` is the hostname/IP of any existing member and port is the cluster port (default 8088).  The new node will attempt to join the cluster and be assigned a node ID.  Future shards that are created will be distributed across the cluster and sometimes on the new node.\r\n\r\nQueries and writes can go to any node on any of the standard service ports.  Queries are not addressed by this PR and currently being worked on in other PRs.\r\n\r\n## Implementation Details\r\n\r\nWhen there are more than 3 nodes in a cluster, new nodes will not take part in the raft cluster.  All of the raft implementation is encapsulated in the `meta.Store`.  To keep the `meta.Store` implementation from having to check whether it's part of the raft cluster or not, the raft details have been pulled out into a `raftState` implementation.  The `meta.Store` now delegates raft related call to the `raftState`.  \r\n\r\nThere are two implementations of `raftState`: `localRaft` and `remoteRaft`. `localRaft` changes the behavior of the `meta.Store` to take part of the raft cluster.  `remoteRaft` causes the `meta.Store` to operate using a local cache with remote calls to the raft cluster.  In a subsequent PR, nodes will able to be promoted to raft cluster members and this state pattern will make it easier to change state dynamically.\r\n\r\nWhen a node is started with join options, it initiates a `JoinRequest` RPC to an existing members.  If the member is not the raft leader, the request is proxied to the current leader.  The response to the `JoinRequest` indicates the node ID, whether the node should join the raft cluster (currently always `false`), and the current set of raft peers.  If the node should join the raft cluster, it operates as before.  If not, the node calls a `FetchMetaData` RPC to the raft cluster (auto-proxied to leader) and then starts up.\r\n\r\nChanges to the meta-store in the raft cluster need to be propagated to non-raft members.  This is handled by blocking `FetchMetaData` calls initiated by each non-raft member.  They maintain a blocking call that waits for a `meta.Data` change.  When triggered, the updated `meta.Data` is returned and the non-raft member updates it's local cache if needed.  The blocking call is then repeated indefinitely.  This is similar to have zookeeper/etcd watches works.\r\n\r\n## Not implemented TODO\r\n\r\nThe following things are still not implemented:\r\n* Add new raft members - If you have a one node cluster, and add a new node, it should eventually add the first 3 nodes as part of the raft cluster.  This is more involved and needs more work.  For now, all new nodes are data-only nodes.\r\n* Join retries - If all the join addresses fail to join, the node will not retry need to be restarted to retry the join.\r\n* Leader RPC retries - remote calls to the raft cluster use a random member from the set of peers.  If that member is down, the call will fail and return an error.  Many of these could be retried automatically.\r\n* Removing nodes (raft or data-only).  ","created_at":"2015-07-17T22:27:33Z","updated_at":"2015-07-20T16:50:12Z","closed_at":null,"merged_at":null,"merge_commit_sha":"f57fd672b8fa0bb2b9e27d67c0427a76ea395886","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/influxdb/influxdb/pulls/3372/commits","review_comments_url":"https://api.github.com/repos/influxdb/influxdb/pulls/3372/comments","review_comment_url":"https://api.github.com/repos/influxdb/influxdb/pulls/comments{/number}","comments_url":"https://api.github.com/repos/influxdb/influxdb/issues/3372/comments","statuses_url":"https://api.github.com/repos/influxdb/influxdb/statuses/754b4a1e30da80045a20554a63103ba7b5cc1793","head":{"label":"influxdb:jw-cluster","ref":"jw-cluster","sha":"754b4a1e30da80045a20554a63103ba7b5cc1793","user":{"login":"influxdb","id":5713248,"avatar_url":"https://avatars.githubusercontent.com/u/5713248?v=3","gravatar_id":"","url":"https://api.github.com/users/influxdb","html_url":"https://github.com/influxdb","followers_url":"https://api.github.com/users/influxdb/followers","following_url":"https://api.github.com/users/influxdb/following{/other_user}","gists_url":"https://api.github.com/users/influxdb/gists{/gist_id}","starred_url":"https://api.github.com/users/influxdb/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/influxdb/subscriptions","organizations_url":"https://api.github.com/users/influxdb/orgs","repos_url":"https://api.github.com/users/influxdb/repos","events_url":"https://api.github.com/users/influxdb/events{/privacy}","received_events_url":"https://api.github.com/users/influxdb/received_events","type":"Organization","site_admin":false},"repo":{"id":13124802,"name":"influxdb","full_name":"influxdb/influxdb","owner":{"login":"influxdb","id":5713248,"avatar_url":"https://avatars.githubusercontent.com/u/5713248?v=3","gravatar_id":"","url":"https://api.github.com/users/influxdb","html_url":"https://github.com/influxdb","followers_url":"https://api.github.com/users/influxdb/followers","following_url":"https://api.github.com/users/influxdb/following{/other_user}","gists_url":"https://api.github.com/users/influxdb/gists{/gist_id}","starred_url":"https://api.github.com/users/influxdb/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/influxdb/subscriptions","organizations_url":"https://api.github.com/users/influxdb/orgs","repos_url":"https://api.github.com/users/influxdb/repos","events_url":"https://api.github.com/users/influxdb/events{/privacy}","received_events_url":"https://api.github.com/users/influxdb/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/influxdb/influxdb","description":"Scalable datastore for metrics, events, and real-time analytics","fork":false,"url":"https://api.github.com/repos/influxdb/influxdb","forks_url":"https://api.github.com/repos/influxdb/influxdb/forks","keys_url":"https://api.github.com/repos/influxdb/influxdb/keys{/key_id}","collaborators_url":"https://api.github.com/repos/influxdb/influxdb/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/influxdb/influxdb/teams","hooks_url":"https://api.github.com/repos/influxdb/influxdb/hooks","issue_events_url":"https://api.github.com/repos/influxdb/influxdb/issues/events{/number}","events_url":"https://api.github.com/repos/influxdb/influxdb/events","assignees_url":"https://api.github.com/repos/influxdb/influxdb/assignees{/user}","branches_url":"https://api.github.com/repos/influxdb/influxdb/branches{/branch}","tags_url":"https://api.github.com/repos/influxdb/influxdb/tags","blobs_url":"https://api.github.com/repos/influxdb/influxdb/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/influxdb/influxdb/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/influxdb/influxdb/git/refs{/sha}","trees_url":"https://api.github.com/repos/influxdb/influxdb/git/trees{/sha}","statuses_url":"https://api.github.com/repos/influxdb/influxdb/statuses/{sha}","languages_url":"https://api.github.com/repos/influxdb/influxdb/languages","stargazers_url":"https://api.github.com/repos/influxdb/influxdb/stargazers","contributors_url":"https://api.github.com/repos/influxdb/influxdb/contributors","subscribers_url":"https://api.github.com/repos/influxdb/influxdb/subscribers","subscription_url":"https://api.github.com/repos/influxdb/influxdb/subscription","commits_url":"https://api.github.com/repos/influxdb/influxdb/commits{/sha}","git_commits_url":"https://api.github.com/repos/influxdb/influxdb/git/commits{/sha}","comments_url":"https://api.github.com/repos/influxdb/influxdb/comments{/number}","issue_comment_url":"https://api.github.com/repos/influxdb/influxdb/issues/comments{/number}","contents_url":"https://api.github.com/repos/influxdb/influxdb/contents/{+path}","compare_url":"https://api.github.com/repos/influxdb/influxdb/compare/{base}...{head}","merges_url":"https://api.github.com/repos/influxdb/influxdb/merges","archive_url":"https://api.github.com/repos/influxdb/influxdb/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/influxdb/influxdb/downloads","issues_url":"https://api.github.com/repos/influxdb/influxdb/issues{/number}","pulls_url":"https://api.github.com/repos/influxdb/influxdb/pulls{/number}","milestones_url":"https://api.github.com/repos/influxdb/influxdb/milestones{/number}","notifications_url":"https://api.github.com/repos/influxdb/influxdb/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/influxdb/influxdb/labels{/name}","releases_url":"https://api.github.com/repos/influxdb/influxdb/releases{/id}","created_at":"2013-09-26T14:31:10Z","updated_at":"2015-07-20T03:53:04Z","pushed_at":"2015-07-20T08:39:21Z","git_url":"git://github.com/influxdb/influxdb.git","ssh_url":"git@github.com:influxdb/influxdb.git","clone_url":"https://github.com/influxdb/influxdb.git","svn_url":"https://github.com/influxdb/influxdb","homepage":"http://influxdb.com","size":50444,"stargazers_count":5864,"watchers_count":5864,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":598,"mirror_url":null,"open_issues_count":310,"forks":598,"open_issues":310,"watchers":5864,"default_branch":"master"}},"base":{"label":"influxdb:master","ref":"master","sha":"5ea2fb81f3efc17274455cb9f809f569a57839be","user":{"login":"influxdb","id":5713248,"avatar_url":"https://avatars.githubusercontent.com/u/5713248?v=3","gravatar_id":"","url":"https://api.github.com/users/influxdb","html_url":"https://github.com/influxdb","followers_url":"https://api.github.com/users/influxdb/followers","following_url":"https://api.github.com/users/influxdb/following{/other_user}","gists_url":"https://api.github.com/users/influxdb/gists{/gist_id}","starred_url":"https://api.github.com/users/influxdb/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/influxdb/subscriptions","organizations_url":"https://api.github.com/users/influxdb/orgs","repos_url":"https://api.github.com/users/influxdb/repos","events_url":"https://api.github.com/users/influxdb/events{/privacy}","received_events_url":"https://api.github.com/users/influxdb/received_events","type":"Organization","site_admin":false},"repo":{"id":13124802,"name":"influxdb","full_name":"influxdb/influxdb","owner":{"login":"influxdb","id":5713248,"avatar_url":"https://avatars.githubusercontent.com/u/5713248?v=3","gravatar_id":"","url":"https://api.github.com/users/influxdb","html_url":"https://github.com/influxdb","followers_url":"https://api.github.com/users/influxdb/followers","following_url":"https://api.github.com/users/influxdb/following{/other_user}","gists_url":"https://api.github.com/users/influxdb/gists{/gist_id}","starred_url":"https://api.github.com/users/influxdb/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/influxdb/subscriptions","organizations_url":"https://api.github.com/users/influxdb/orgs","repos_url":"https://api.github.com/users/influxdb/repos","events_url":"https://api.github.com/users/influxdb/events{/privacy}","received_events_url":"https://api.github.com/users/influxdb/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/influxdb/influxdb","description":"Scalable datastore for metrics, events, and real-time analytics","fork":false,"url":"https://api.github.com/repos/influxdb/influxdb","forks_url":"https://api.github.com/repos/influxdb/influxdb/forks","keys_url":"https://api.github.com/repos/influxdb/influxdb/keys{/key_id}","collaborators_url":"https://api.github.com/repos/influxdb/influxdb/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/influxdb/influxdb/teams","hooks_url":"https://api.github.com/repos/influxdb/influxdb/hooks","issue_events_url":"https://api.github.com/repos/influxdb/influxdb/issues/events{/number}","events_url":"https://api.github.com/repos/influxdb/influxdb/events","assignees_url":"https://api.github.com/repos/influxdb/influxdb/assignees{/user}","branches_url":"https://api.github.com/repos/influxdb/influxdb/branches{/branch}","tags_url":"https://api.github.com/repos/influxdb/influxdb/tags","blobs_url":"https://api.github.com/repos/influxdb/influxdb/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/influxdb/influxdb/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/influxdb/influxdb/git/refs{/sha}","trees_url":"https://api.github.com/repos/influxdb/influxdb/git/trees{/sha}","statuses_url":"https://api.github.com/repos/influxdb/influxdb/statuses/{sha}","languages_url":"https://api.github.com/repos/influxdb/influxdb/languages","stargazers_url":"https://api.github.com/repos/influxdb/influxdb/stargazers","contributors_url":"https://api.github.com/repos/influxdb/influxdb/contributors","subscribers_url":"https://api.github.com/repos/influxdb/influxdb/subscribers","subscription_url":"https://api.github.com/repos/influxdb/influxdb/subscription","commits_url":"https://api.github.com/repos/influxdb/influxdb/commits{/sha}","git_commits_url":"https://api.github.com/repos/influxdb/influxdb/git/commits{/sha}","comments_url":"https://api.github.com/repos/influxdb/influxdb/comments{/number}","issue_comment_url":"https://api.github.com/repos/influxdb/influxdb/issues/comments{/number}","contents_url":"https://api.github.com/repos/influxdb/influxdb/contents/{+path}","compare_url":"https://api.github.com/repos/influxdb/influxdb/compare/{base}...{head}","merges_url":"https://api.github.com/repos/influxdb/influxdb/merges","archive_url":"https://api.github.com/repos/influxdb/influxdb/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/influxdb/influxdb/downloads","issues_url":"https://api.github.com/repos/influxdb/influxdb/issues{/number}","pulls_url":"https://api.github.com/repos/influxdb/influxdb/pulls{/number}","milestones_url":"https://api.github.com/repos/influxdb/influxdb/milestones{/number}","notifications_url":"https://api.github.com/repos/influxdb/influxdb/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/influxdb/influxdb/labels{/name}","releases_url":"https://api.github.com/repos/influxdb/influxdb/releases{/id}","created_at":"2013-09-26T14:31:10Z","updated_at":"2015-07-20T03:53:04Z","pushed_at":"2015-07-20T08:39:21Z","git_url":"git://github.com/influxdb/influxdb.git","ssh_url":"git@github.com:influxdb/influxdb.git","clone_url":"https://github.com/influxdb/influxdb.git","svn_url":"https://github.com/influxdb/influxdb","homepage":"http://influxdb.com","size":50444,"stargazers_count":5864,"watchers_count":5864,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":598,"mirror_url":null,"open_issues_count":310,"forks":598,"open_issues":310,"watchers":5864,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/influxdb/influxdb/pulls/3372"},"html":{"href":"https://github.com/influxdb/influxdb/pull/3372"},"issue":{"href":"https://api.github.com/repos/influxdb/influxdb/issues/3372"},"comments":{"href":"https://api.github.com/repos/influxdb/influxdb/issues/3372/comments"},"review_comments":{"href":"https://api.github.com/repos/influxdb/influxdb/pulls/3372/comments"},"review_comment":{"href":"https://api.github.com/repos/influxdb/influxdb/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/influxdb/influxdb/pulls/3372/commits"},"statuses":{"href":"https://api.github.com/repos/influxdb/influxdb/statuses/754b4a1e30da80045a20554a63103ba7b5cc1793"}}}}