{"action":"created","comment":{"url":"https://api.github.com/repos/flynn/flynn/pulls/comments/45825178","id":45825178,"diff_hunk":"@@ -1,261 +1,192 @@\n package discoverd\n \n import (\n-\t\"encoding/json\"\n \t\"errors\"\n \t\"fmt\"\n \t\"net/http\"\n \t\"os\"\n \t\"strings\"\n+\t\"sync\"\n \t\"time\"\n \n \t\"github.com/flynn/flynn/pkg/dialer\"\n \t\"github.com/flynn/flynn/pkg/httpclient\"\n-\thh \"github.com/flynn/flynn/pkg/httphelper\"\n \t\"github.com/flynn/flynn/pkg/stream\"\n )\n \n-type Service interface {\n-\tLeader() (*Instance, error)\n-\tInstances() ([]*Instance, error)\n-\tAddrs() ([]string, error)\n-\tLeaders(chan *Instance) (stream.Stream, error)\n-\tWatch(events chan *Event) (stream.Stream, error)\n-\tGetMeta() (*ServiceMeta, error)\n-\tSetMeta(*ServiceMeta) error\n-\tSetLeader(string) error\n-}\n-\n var ErrTimedOut = errors.New(\"discoverd: timed out waiting for instances\")\n+var PinTTL = 60 * time.Second\n+\n+type Config struct {\n+\tEndpoints []string\n+}\n \n type Client struct {\n-\tc *httpclient.Client\n+\tservers    []*httpclient.Client\n+\tpinned     int\n+\tpinUpdated time.Time\n+\tleader     int\n+\tmu         sync.RWMutex\n }\n \n-func NewClient() *Client {\n-\turl := os.Getenv(\"DISCOVERD\")\n-\tif url == \"\" {\n-\t\turl = \"http://127.0.0.1:1111\"\n+func NewClientWithConfig(config Config) *Client {\n+\tclient := Client{}\n+\tfor _, e := range config.Endpoints {\n+\t\tclient.servers = append(client.servers, client.httpClient(e))\n \t}\n-\treturn NewClientWithURL(url)\n+\treturn &client\n }\n \n func NewClientWithURL(url string) *Client {\n-\tif !strings.HasPrefix(url, \"http\") {\n-\t\turl = \"http://\" + url\n-\t}\n-\treturn &Client{\n-\t\tc: &httpclient.Client{\n-\t\t\tURL: url,\n-\t\t\tHTTP: &http.Client{\n-\t\t\t\tTransport:     &http.Transport{Dial: dialer.Retry.Dial},\n-\t\t\t\tCheckRedirect: redirectPreserveHeaders,\n-\t\t\t},\n-\t\t},\n-\t}\n+\treturn NewClientWithConfig(Config{Endpoints: formatURLs([]string{url})})\n }\n \n-func NewClientWithHTTP(url string, hc *http.Client) *Client {\n-\tif url == \"\" {\n-\t\turl = \"http://127.0.0.1:1111\"\n-\t}\n-\treturn &Client{\n-\t\tc: &httpclient.Client{\n-\t\t\tURL:  url,\n-\t\t\tHTTP: hc,\n-\t\t},\n-\t}\n+func NewClient() *Client {\n+\treturn NewClientWithConfig(defaultConfig())\n }\n \n-func redirectPreserveHeaders(req *http.Request, via []*http.Request) error {\n-\tif len(via) >= 10 {\n-\t\treturn fmt.Errorf(\"too many redirects\")\n-\t}\n-\tif len(via) == 0 {\n-\t\treturn nil\n-\t}\n-\tfor attr, val := range via[0].Header {\n-\t\tif _, ok := req.Header[attr]; !ok {\n-\t\t\treq.Header[attr] = val\n-\t\t}\n+func defaultConfig() Config {\n+\turls := os.Getenv(\"DISCOVERD\")\n+\tif urls == \"\" || urls == \"none\" {\n+\t\turls = \"http://127.0.0.1:1111\"\n \t}\n-\treturn nil\n+\treturn Config{Endpoints: formatURLs(strings.Split(urls, \",\"))}\n }\n \n-func (c *Client) Ping() error {\n-\treturn c.c.Get(\"/ping\", nil)\n+func formatURLs(urls []string) []string {\n+\tformatted := make([]string, len(urls))\n+\tfor _, u := range urls {\n+\t\tif !strings.HasPrefix(u, \"http\") {\n+\t\t\tu = \"http://\" + u\n+\t\t}\n+\t\tformatted = append(formatted, u)\n+\t}\n+\treturn formatted\n }\n \n-type LeaderType string\n-\n-const (\n-\tLeaderTypeManual LeaderType = \"manual\"\n-\tLeaderTypeOldest LeaderType = \"oldest\"\n-)\n-\n-type ServiceConfig struct {\n-\tLeaderType LeaderType `json:\"leader_type\"`\n+func (c *Client) updateLeader(url string) {\n+\tc.mu.Lock()\n+\tdefer c.mu.Unlock()\n+\tfor i, s := range c.servers {\n+\t\tif s.Host == url {\n+\t\t\tc.leader = i\n+\t\t}\n+\t}\n }\n \n-func (c *Client) AddService(name string, conf *ServiceConfig) error {\n-\tif conf == nil {\n-\t\tconf = &ServiceConfig{}\n+func (c *Client) httpClient(url string) *httpclient.Client {\n+\tcheckRedirect := func(req *http.Request, via []*http.Request) error {\n+\t\tif len(via) >= 10 {\n+\t\t\treturn fmt.Errorf(\"too many redirects\")\n+\t\t}\n+\t\tif len(via) > 0 {\n+\t\t\tfor attr, val := range via[0].Header {\n+\t\t\t\tif _, ok := req.Header[attr]; !ok {\n+\t\t\t\t\treq.Header[attr] = val\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t\tc.updateLeader(req.Host)\n+\t\treturn nil\n \t}\n-\tif conf.LeaderType == \"\" {\n-\t\tconf.LeaderType = LeaderTypeOldest\n+\treturn &httpclient.Client{\n+\t\tURL: url,\n+\t\tHTTP: &http.Client{\n+\t\t\tTransport:     &http.Transport{Dial: dialer.Retry.Dial},\n+\t\t\tCheckRedirect: checkRedirect,\n+\t\t},\n \t}\n-\treturn c.c.Put(\"/services/\"+name, conf, nil)\n-}\n-\n-func (c *Client) RemoveService(name string) error {\n-\treturn c.c.Delete(\"/services/\" + name)\n }\n \n-func (c *Client) Service(name string) Service {\n-\treturn newService(c, name)\n-}\n+func (c *Client) Send(method string, path string, in, out interface{}) (err error) {\n+\tvar leaderReq bool\n+\tswitch method {\n+\tcase \"PUT\", \"DEL\", \"POST\":\n+\t\tleaderReq = true\n+\t}\n \n-func IsNotFound(err error) bool {\n-\treturn hh.IsObjectNotFoundError(err)\n-}\n+\tc.mu.RLock()\n+\tleader := c.leader\n+\tpinned := c.pinned\n+\tpinUpdated := c.pinUpdated\n+\tc.mu.RUnlock()\n \n-func (c *Client) Instances(service string, timeout time.Duration) ([]*Instance, error) {\n-\ts := c.Service(service)\n-\tinstances, err := s.Instances()\n-\tif len(instances) > 0 || err != nil && !IsNotFound(err) {\n-\t\treturn instances, err\n+\t// try direct writes directly to the leader to avoid redirect\n+\tif leaderReq {\n+\t\tpinned = leader\n \t}\n \n-\tevents := make(chan *Event)\n-\tstream, err := s.Watch(events)\n-\tif err != nil {\n-\t\treturn nil, err\n-\t}\n-\tdefer stream.Close()\n-\t// get any current instances\n-outer:\n-\tfor event := range events {\n-\t\tswitch event.Kind {\n-\t\tcase EventKindCurrent:\n-\t\t\tbreak outer\n-\t\tcase EventKindUp:\n-\t\t\tinstances = append(instances, event.Instance)\n+\tfor i := pinned; i < len(c.servers)+pinned; i++ {\n+\t\tk := i % len(c.servers)\n+\t\thc := c.servers[k]\n+\t\terr := hc.Send(method, path, in, out)\n+\t\tif err != nil {\n+\t\t\tcontinue","path":"discoverd/client/client.go","position":223,"original_position":223,"commit_id":"bc6349704de14047e3c2dea41b2e7d21812e102f","original_commit_id":"bc6349704de14047e3c2dea41b2e7d21812e102f","user":{"login":"lmars","id":488515,"avatar_url":"https://avatars.githubusercontent.com/u/488515?v=3","gravatar_id":"","url":"https://api.github.com/users/lmars","html_url":"https://github.com/lmars","followers_url":"https://api.github.com/users/lmars/followers","following_url":"https://api.github.com/users/lmars/following{/other_user}","gists_url":"https://api.github.com/users/lmars/gists{/gist_id}","starred_url":"https://api.github.com/users/lmars/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lmars/subscriptions","organizations_url":"https://api.github.com/users/lmars/orgs","repos_url":"https://api.github.com/users/lmars/repos","events_url":"https://api.github.com/users/lmars/events{/privacy}","received_events_url":"https://api.github.com/users/lmars/received_events","type":"User","site_admin":false},"body":"We should save these errors so they can be returned if all the hosts fail","created_at":"2015-11-25T03:06:29Z","updated_at":"2015-11-25T03:06:29Z","html_url":"https://github.com/flynn/flynn/pull/2173#discussion_r45825178","pull_request_url":"https://api.github.com/repos/flynn/flynn/pulls/2173","_links":{"self":{"href":"https://api.github.com/repos/flynn/flynn/pulls/comments/45825178"},"html":{"href":"https://github.com/flynn/flynn/pull/2173#discussion_r45825178"},"pull_request":{"href":"https://api.github.com/repos/flynn/flynn/pulls/2173"}}},"pull_request":{"url":"https://api.github.com/repos/flynn/flynn/pulls/2173","id":51579670,"html_url":"https://github.com/flynn/flynn/pull/2173","diff_url":"https://github.com/flynn/flynn/pull/2173.diff","patch_url":"https://github.com/flynn/flynn/pull/2173.patch","issue_url":"https://api.github.com/repos/flynn/flynn/issues/2173","number":2173,"state":"open","locked":false,"title":"Discoverd client improvements","user":{"login":"josephglanville","id":605591,"avatar_url":"https://avatars.githubusercontent.com/u/605591?v=3","gravatar_id":"","url":"https://api.github.com/users/josephglanville","html_url":"https://github.com/josephglanville","followers_url":"https://api.github.com/users/josephglanville/followers","following_url":"https://api.github.com/users/josephglanville/following{/other_user}","gists_url":"https://api.github.com/users/josephglanville/gists{/gist_id}","starred_url":"https://api.github.com/users/josephglanville/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/josephglanville/subscriptions","organizations_url":"https://api.github.com/users/josephglanville/orgs","repos_url":"https://api.github.com/users/josephglanville/repos","events_url":"https://api.github.com/users/josephglanville/events{/privacy}","received_events_url":"https://api.github.com/users/josephglanville/received_events","type":"User","site_admin":false},"body":"This changeset makes the discoverd client cluster aware.\r\n\r\nIt can now be initialised with a connection string of comma delimited servers.\r\nThe first server is considered to be preferred and it will use it unless it's unavailable.\r\nIf it does become unavailable then the rest of the servers configured will be tried until a working one is found, this server will then be pinned for 60 seconds, after the TTL expires the client will attempt to use the preferred server once more.\r\nWrites are directed to the leader by recording where redirects are sent, if at any point the leader becomes unavailable we will try issue the write to another server and have it redirect us to the new leader.\r\n","created_at":"2015-11-23T21:02:53Z","updated_at":"2015-11-25T03:06:29Z","closed_at":null,"merged_at":null,"merge_commit_sha":"e89253b3a5ef267b42816f0c6dd84edfe9d18e0c","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/flynn/flynn/pulls/2173/commits","review_comments_url":"https://api.github.com/repos/flynn/flynn/pulls/2173/comments","review_comment_url":"https://api.github.com/repos/flynn/flynn/pulls/comments{/number}","comments_url":"https://api.github.com/repos/flynn/flynn/issues/2173/comments","statuses_url":"https://api.github.com/repos/flynn/flynn/statuses/bc6349704de14047e3c2dea41b2e7d21812e102f","head":{"label":"flynn:discoverd_client_improvements","ref":"discoverd_client_improvements","sha":"bc6349704de14047e3c2dea41b2e7d21812e102f","user":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"repo":{"id":11290232,"name":"flynn","full_name":"flynn/flynn","owner":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/flynn/flynn","description":"A next generation open source platform as a service (PaaS)","fork":false,"url":"https://api.github.com/repos/flynn/flynn","forks_url":"https://api.github.com/repos/flynn/flynn/forks","keys_url":"https://api.github.com/repos/flynn/flynn/keys{/key_id}","collaborators_url":"https://api.github.com/repos/flynn/flynn/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/flynn/flynn/teams","hooks_url":"https://api.github.com/repos/flynn/flynn/hooks","issue_events_url":"https://api.github.com/repos/flynn/flynn/issues/events{/number}","events_url":"https://api.github.com/repos/flynn/flynn/events","assignees_url":"https://api.github.com/repos/flynn/flynn/assignees{/user}","branches_url":"https://api.github.com/repos/flynn/flynn/branches{/branch}","tags_url":"https://api.github.com/repos/flynn/flynn/tags","blobs_url":"https://api.github.com/repos/flynn/flynn/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/flynn/flynn/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/flynn/flynn/git/refs{/sha}","trees_url":"https://api.github.com/repos/flynn/flynn/git/trees{/sha}","statuses_url":"https://api.github.com/repos/flynn/flynn/statuses/{sha}","languages_url":"https://api.github.com/repos/flynn/flynn/languages","stargazers_url":"https://api.github.com/repos/flynn/flynn/stargazers","contributors_url":"https://api.github.com/repos/flynn/flynn/contributors","subscribers_url":"https://api.github.com/repos/flynn/flynn/subscribers","subscription_url":"https://api.github.com/repos/flynn/flynn/subscription","commits_url":"https://api.github.com/repos/flynn/flynn/commits{/sha}","git_commits_url":"https://api.github.com/repos/flynn/flynn/git/commits{/sha}","comments_url":"https://api.github.com/repos/flynn/flynn/comments{/number}","issue_comment_url":"https://api.github.com/repos/flynn/flynn/issues/comments{/number}","contents_url":"https://api.github.com/repos/flynn/flynn/contents/{+path}","compare_url":"https://api.github.com/repos/flynn/flynn/compare/{base}...{head}","merges_url":"https://api.github.com/repos/flynn/flynn/merges","archive_url":"https://api.github.com/repos/flynn/flynn/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/flynn/flynn/downloads","issues_url":"https://api.github.com/repos/flynn/flynn/issues{/number}","pulls_url":"https://api.github.com/repos/flynn/flynn/pulls{/number}","milestones_url":"https://api.github.com/repos/flynn/flynn/milestones{/number}","notifications_url":"https://api.github.com/repos/flynn/flynn/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/flynn/flynn/labels{/name}","releases_url":"https://api.github.com/repos/flynn/flynn/releases{/id}","created_at":"2013-07-09T18:22:06Z","updated_at":"2015-11-24T21:36:13Z","pushed_at":"2015-11-24T23:25:46Z","git_url":"git://github.com/flynn/flynn.git","ssh_url":"git@github.com:flynn/flynn.git","clone_url":"https://github.com/flynn/flynn.git","svn_url":"https://github.com/flynn/flynn","homepage":"https://flynn.io","size":22813,"stargazers_count":4043,"watchers_count":4043,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":288,"mirror_url":null,"open_issues_count":260,"forks":288,"open_issues":260,"watchers":4043,"default_branch":"master"}},"base":{"label":"flynn:master","ref":"master","sha":"d4cf810bf508510a73f0458044b1e84348e1353f","user":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"repo":{"id":11290232,"name":"flynn","full_name":"flynn/flynn","owner":{"login":"flynn","id":4973613,"avatar_url":"https://avatars.githubusercontent.com/u/4973613?v=3","gravatar_id":"","url":"https://api.github.com/users/flynn","html_url":"https://github.com/flynn","followers_url":"https://api.github.com/users/flynn/followers","following_url":"https://api.github.com/users/flynn/following{/other_user}","gists_url":"https://api.github.com/users/flynn/gists{/gist_id}","starred_url":"https://api.github.com/users/flynn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/flynn/subscriptions","organizations_url":"https://api.github.com/users/flynn/orgs","repos_url":"https://api.github.com/users/flynn/repos","events_url":"https://api.github.com/users/flynn/events{/privacy}","received_events_url":"https://api.github.com/users/flynn/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/flynn/flynn","description":"A next generation open source platform as a service (PaaS)","fork":false,"url":"https://api.github.com/repos/flynn/flynn","forks_url":"https://api.github.com/repos/flynn/flynn/forks","keys_url":"https://api.github.com/repos/flynn/flynn/keys{/key_id}","collaborators_url":"https://api.github.com/repos/flynn/flynn/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/flynn/flynn/teams","hooks_url":"https://api.github.com/repos/flynn/flynn/hooks","issue_events_url":"https://api.github.com/repos/flynn/flynn/issues/events{/number}","events_url":"https://api.github.com/repos/flynn/flynn/events","assignees_url":"https://api.github.com/repos/flynn/flynn/assignees{/user}","branches_url":"https://api.github.com/repos/flynn/flynn/branches{/branch}","tags_url":"https://api.github.com/repos/flynn/flynn/tags","blobs_url":"https://api.github.com/repos/flynn/flynn/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/flynn/flynn/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/flynn/flynn/git/refs{/sha}","trees_url":"https://api.github.com/repos/flynn/flynn/git/trees{/sha}","statuses_url":"https://api.github.com/repos/flynn/flynn/statuses/{sha}","languages_url":"https://api.github.com/repos/flynn/flynn/languages","stargazers_url":"https://api.github.com/repos/flynn/flynn/stargazers","contributors_url":"https://api.github.com/repos/flynn/flynn/contributors","subscribers_url":"https://api.github.com/repos/flynn/flynn/subscribers","subscription_url":"https://api.github.com/repos/flynn/flynn/subscription","commits_url":"https://api.github.com/repos/flynn/flynn/commits{/sha}","git_commits_url":"https://api.github.com/repos/flynn/flynn/git/commits{/sha}","comments_url":"https://api.github.com/repos/flynn/flynn/comments{/number}","issue_comment_url":"https://api.github.com/repos/flynn/flynn/issues/comments{/number}","contents_url":"https://api.github.com/repos/flynn/flynn/contents/{+path}","compare_url":"https://api.github.com/repos/flynn/flynn/compare/{base}...{head}","merges_url":"https://api.github.com/repos/flynn/flynn/merges","archive_url":"https://api.github.com/repos/flynn/flynn/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/flynn/flynn/downloads","issues_url":"https://api.github.com/repos/flynn/flynn/issues{/number}","pulls_url":"https://api.github.com/repos/flynn/flynn/pulls{/number}","milestones_url":"https://api.github.com/repos/flynn/flynn/milestones{/number}","notifications_url":"https://api.github.com/repos/flynn/flynn/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/flynn/flynn/labels{/name}","releases_url":"https://api.github.com/repos/flynn/flynn/releases{/id}","created_at":"2013-07-09T18:22:06Z","updated_at":"2015-11-24T21:36:13Z","pushed_at":"2015-11-24T23:25:46Z","git_url":"git://github.com/flynn/flynn.git","ssh_url":"git@github.com:flynn/flynn.git","clone_url":"https://github.com/flynn/flynn.git","svn_url":"https://github.com/flynn/flynn","homepage":"https://flynn.io","size":22813,"stargazers_count":4043,"watchers_count":4043,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":288,"mirror_url":null,"open_issues_count":260,"forks":288,"open_issues":260,"watchers":4043,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/flynn/flynn/pulls/2173"},"html":{"href":"https://github.com/flynn/flynn/pull/2173"},"issue":{"href":"https://api.github.com/repos/flynn/flynn/issues/2173"},"comments":{"href":"https://api.github.com/repos/flynn/flynn/issues/2173/comments"},"review_comments":{"href":"https://api.github.com/repos/flynn/flynn/pulls/2173/comments"},"review_comment":{"href":"https://api.github.com/repos/flynn/flynn/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/flynn/flynn/pulls/2173/commits"},"statuses":{"href":"https://api.github.com/repos/flynn/flynn/statuses/bc6349704de14047e3c2dea41b2e7d21812e102f"}}}}