{"action":"created","comment":{"url":"https://api.github.com/repos/chihaya/chihaya/pulls/comments/120007447","pull_request_review_id":41937848,"id":120007447,"diff_hunk":"@@ -0,0 +1,780 @@\n+// Package memorybysubnet implements the storage interface for a Chihaya\n+// BitTorrent tracker keeping peer data in memory organized by a pre-configured\n+// subnet mask.\n+package memorybysubnet\n+\n+import (\n+\t\"encoding/binary\"\n+\t\"errors\"\n+\t\"net\"\n+\t\"runtime\"\n+\t\"strconv\"\n+\t\"sync\"\n+\t\"sync/atomic\"\n+\t\"time\"\n+\n+\tlog \"github.com/Sirupsen/logrus\"\n+\t\"github.com/prometheus/client_golang/prometheus\"\n+\t\"gopkg.in/yaml.v2\"\n+\n+\t\"github.com/chihaya/chihaya/bittorrent\"\n+\t\"github.com/chihaya/chihaya/storage\"\n+)\n+\n+// Name is the name by which this peer store is registered with Chihaya.\n+const Name = \"memorybysubnet\"\n+\n+func init() {\n+\t// Register Prometheus metrics.\n+\tprometheus.MustRegister(\n+\t\tpromGCDurationMilliseconds,\n+\t\tpromInfohashesCount,\n+\t\tpromSeedersCount,\n+\t\tpromLeechersCount,\n+\t)\n+\n+\t// Register the storage driver.\n+\tstorage.RegisterDriver(Name, driver{})\n+}\n+\n+var promGCDurationMilliseconds = prometheus.NewHistogram(prometheus.HistogramOpts{\n+\tName:    \"chihaya_storage_\" + Name + \"_gc_duration_milliseconds\",\n+\tHelp:    \"The time it takes to perform storage garbage collection\",\n+\tBuckets: prometheus.ExponentialBuckets(9.375, 2, 10),\n+})\n+\n+var promInfohashesCount = prometheus.NewGauge(prometheus.GaugeOpts{\n+\tName: \"chihaya_storage_\" + Name + \"_infohashes_count\",\n+\tHelp: \"The number of Infohashes tracked\",\n+})\n+\n+var promSeedersCount = prometheus.NewGauge(prometheus.GaugeOpts{\n+\tName: \"chihaya_storage_\" + Name + \"_seeders_count\",\n+\tHelp: \"The number of seeders tracked\",\n+})\n+\n+var promLeechersCount = prometheus.NewGauge(prometheus.GaugeOpts{\n+\tName: \"chihaya_storage_\" + Name + \"_leechers_count\",\n+\tHelp: \"The number of leechers tracked\",\n+})\n+\n+// recordGCDuration records the duration of a GC sweep.\n+func recordGCDuration(duration time.Duration) {\n+\tpromGCDurationMilliseconds.Observe(float64(duration.Nanoseconds()) / float64(time.Millisecond))\n+}\n+\n+// recordInfohashesDelta records a change in the number of Infohashes tracked.\n+func recordInfohashesDelta(delta float64) {\n+\tpromInfohashesCount.Add(delta)\n+}\n+\n+type driver struct{}\n+\n+func (d driver) NewPeerStore(icfg interface{}) (storage.PeerStore, error) {\n+\t// Marshal the config back into bytes.\n+\tbytes, err := yaml.Marshal(icfg)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\t// Unmarshal the bytes into the proper config type.\n+\tvar cfg Config\n+\terr = yaml.Unmarshal(bytes, &cfg)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\treturn New(cfg)\n+}\n+\n+// ErrInvalidGCInterval is returned for a GarbageCollectionInterval that is\n+// less than or equal to zero.\n+var ErrInvalidGCInterval = errors.New(\"invalid garbage collection interval\")\n+\n+// Config holds the configuration of a memory PeerStore.\n+type Config struct {\n+\tGarbageCollectionInterval      time.Duration `yaml:\"gc_interval\"`\n+\tPrometheusReportingInterval    time.Duration `yaml:\"prometheus_reporting_interval\"`\n+\tPeerLifetime                   time.Duration `yaml:\"peer_lifetime\"`\n+\tShardCount                     int           `yaml:\"shard_count\"`\n+\tPreferredIPv4SubnetMaskBitsSet int           `yaml:\"preferred_ipv4_subnet_mask_bits_set\"`\n+\tPreferredIPv6SubnetMaskBitsSet int           `yaml:\"preferred_ipv6_subnet_mask_bits_set\"`\n+}\n+\n+// LogFields renders the current config as a set of Logrus fields.\n+func (cfg Config) LogFields() log.Fields {\n+\treturn log.Fields{\n+\t\t\"name\":               Name,\n+\t\t\"gcInterval\":         cfg.GarbageCollectionInterval,\n+\t\t\"promReportInterval\": cfg.PrometheusReportingInterval,\n+\t\t\"peerLifetime\":       cfg.PeerLifetime,\n+\t\t\"shardCount\":         cfg.ShardCount,\n+\t\t\"prefIPv4Mask\":       cfg.PreferredIPv4SubnetMaskBitsSet,\n+\t\t\"prefIPv6Mask\":       cfg.PreferredIPv6SubnetMaskBitsSet,\n+\t}\n+}\n+\n+// Validate sanity checks values set in a config and returns a new config with\n+// default values replacing anything that is invalid.\n+//\n+// This function warns to the logger when a value is changed.\n+func (cfg Config) Validate() Config {\n+\tvalidcfg := cfg\n+\tif cfg.ShardCount > 0 {\n+\t\tvalidcfg.ShardCount = cfg.ShardCount\n+\t} else {\n+\t\tvalidcfg.ShardCount = 1\n+\t\tlog.WithFields(log.Fields{\n+\t\t\t\"name\":     Name + \".ShardCount\",\n+\t\t\t\"provided\": strconv.Itoa(cfg.ShardCount),\n+\t\t\t\"default\":  strconv.Itoa(validcfg.ShardCount),\n+\t\t}).Warnln(\"falling back to default configuration\")\n+\t}\n+\n+\tif cfg.GarbageCollectionInterval <= 0 {\n+\t\tvalidcfg.GarbageCollectionInterval = time.Minute * 14\n+\t\tlog.WithFields(log.Fields{\n+\t\t\t\"name\":     Name + \".GarbageCollectionInterval\",\n+\t\t\t\"provided\": cfg.GarbageCollectionInterval,\n+\t\t\t\"default\":  validcfg.GarbageCollectionInterval,\n+\t\t}).Warnln(\"falling back to default configuration\")\n+\t}\n+\n+\tif cfg.PrometheusReportingInterval <= 0 {\n+\t\tvalidcfg.PrometheusReportingInterval = time.Second * 1\n+\t\tlog.WithFields(log.Fields{\n+\t\t\t\"name\":     Name + \".PrometheusReportingInterval\",\n+\t\t\t\"provided\": cfg.PrometheusReportingInterval,\n+\t\t\t\"default\":  validcfg.PrometheusReportingInterval,\n+\t\t}).Warnln(\"falling back to default configuration\")\n+\t}\n+\n+\treturn validcfg\n+}\n+\n+// New creates a new PeerStore backed by memory that organizes peers by a\n+// pre-configured subnet mask.\n+func New(provided Config) (storage.PeerStore, error) {\n+\tcfg := provided.Validate()\n+\tps := &peerStore{\n+\t\tcfg:      cfg,\n+\t\tipv4Mask: net.CIDRMask(cfg.PreferredIPv4SubnetMaskBitsSet, 32),\n+\t\tipv6Mask: net.CIDRMask(cfg.PreferredIPv6SubnetMaskBitsSet, 128),\n+\t\tshards:   make([]*peerShard, cfg.ShardCount*2),\n+\t\tclosed:   make(chan struct{}),\n+\t}\n+\n+\tfor i := 0; i < cfg.ShardCount*2; i++ {\n+\t\tps.shards[i] = &peerShard{swarms: make(map[bittorrent.InfoHash]swarm)}\n+\t}\n+\n+\t// Start a goroutine for garbage collection.\n+\tps.wg.Add(1)\n+\tgo func() {\n+\t\tdefer ps.wg.Done()\n+\t\tfor {\n+\t\t\tselect {\n+\t\t\tcase <-ps.closed:\n+\t\t\t\treturn\n+\t\t\tcase <-time.After(cfg.GarbageCollectionInterval):\n+\t\t\t\tbefore := time.Now().Add(-cfg.PeerLifetime)\n+\t\t\t\tlog.Debugln(\"memory: purging peers with no announces since\", before)\n+\t\t\t\tps.collectGarbage(before)\n+\t\t\t}\n+\t\t}\n+\t}()\n+\n+\t// Start a goroutine for updating our cached system clock.\n+\tps.wg.Add(1)\n+\tgo func() {\n+\t\tdefer ps.wg.Done()\n+\t\tt := time.NewTicker(1 * time.Second)\n+\t\tfor {\n+\t\t\tselect {\n+\t\t\tcase <-ps.closed:\n+\t\t\t\tt.Stop()\n+\t\t\t\treturn\n+\t\t\tcase now := <-t.C:\n+\t\t\t\tps.setClock(now.UnixNano())\n+\t\t\t}\n+\t\t}\n+\t}()\n+\n+\t// Start a goroutine for reporting statistics to Prometheus.\n+\tps.wg.Add(1)\n+\tgo func() {\n+\t\tdefer ps.wg.Done()\n+\t\tt := time.NewTicker(cfg.PrometheusReportingInterval)\n+\t\tfor {\n+\t\t\tselect {\n+\t\t\tcase <-ps.closed:\n+\t\t\t\tt.Stop()\n+\t\t\t\treturn\n+\t\t\tcase <-t.C:\n+\t\t\t\tbefore := time.Now()\n+\t\t\t\tps.populateProm()\n+\t\t\t\tlog.Debugf(\"memory: populateProm() took %s\", time.Since(before))\n+\t\t\t}\n+\t\t}\n+\t}()\n+\n+\treturn ps, nil\n+}\n+\n+type serializedPeer string\n+\n+func newPeerKey(p bittorrent.Peer) serializedPeer {\n+\tb := make([]byte, 20+2+len(p.IP.IP))\n+\tcopy(b[:20], p.ID[:])\n+\tbinary.BigEndian.PutUint16(b[20:22], p.Port)\n+\tcopy(b[22:], p.IP.IP)\n+\n+\treturn serializedPeer(b)\n+}\n+\n+func decodePeerKey(pk serializedPeer) bittorrent.Peer {\n+\tpeer := bittorrent.Peer{\n+\t\tID:   bittorrent.PeerIDFromString(string(pk[:20])),\n+\t\tPort: binary.BigEndian.Uint16([]byte(pk[20:22])),\n+\t\tIP:   bittorrent.IP{IP: net.IP(pk[22:])}}\n+\n+\tif ip := peer.IP.To4(); ip != nil {\n+\t\tpeer.IP.IP = ip\n+\t\tpeer.IP.AddressFamily = bittorrent.IPv4\n+\t} else if len(peer.IP.IP) == net.IPv6len { // implies toReturn.IP.To4() == nil\n+\t\tpeer.IP.AddressFamily = bittorrent.IPv6\n+\t} else {\n+\t\tpanic(\"IP is neither v4 nor v6\")\n+\t}\n+\n+\treturn peer\n+}\n+\n+type peerSubnet string\n+\n+func newPeerSubnet(ip bittorrent.IP, ipv4Mask, ipv6Mask net.IPMask) peerSubnet {\n+\tvar maskedIP net.IP\n+\tswitch ip.AddressFamily {\n+\tcase bittorrent.IPv4:\n+\t\tmaskedIP = ip.Mask(ipv4Mask)\n+\tcase bittorrent.IPv6:\n+\t\tmaskedIP = ip.Mask(ipv6Mask)\n+\tdefault:\n+\t\tpanic(\"IP is neither v4 nor v6\")\n+\t}\n+\n+\treturn peerSubnet(maskedIP.String())\n+}\n+\n+type peerShard struct {\n+\tswarms      map[bittorrent.InfoHash]swarm\n+\tnumSeeders  uint64\n+\tnumLeechers uint64\n+\tsync.RWMutex\n+}\n+\n+type swarm struct {\n+\tseeders  map[peerSubnet]map[serializedPeer]int64\n+\tleechers map[peerSubnet]map[serializedPeer]int64\n+}\n+\n+func (s swarm) lenSeeders() (i int) {\n+\tfor _, subnet := range s.seeders {\n+\t\ti += len(subnet)\n+\t}\n+\treturn\n+}\n+\n+func (s swarm) lenLeechers() (i int) {\n+\tfor _, subnet := range s.leechers {\n+\t\ti += len(subnet)\n+\t}\n+\treturn\n+}\n+\n+type peerStore struct {\n+\tcfg      Config\n+\tipv4Mask net.IPMask\n+\tipv6Mask net.IPMask\n+\tshards   []*peerShard\n+\n+\t// clock stores the current time nanoseconds, updated every second.\n+\t// Must be accessed atomically!\n+\tclock int64\n+\n+\tclosed chan struct{}\n+\twg     sync.WaitGroup\n+}\n+\n+var _ storage.PeerStore = &peerStore{}\n+\n+// populateProm aggregates metrics over all shards and then posts them to\n+// prometheus.\n+func (ps *peerStore) populateProm() {\n+\tvar numInfohashes, numSeeders, numLeechers uint64\n+\n+\tfor _, s := range ps.shards {\n+\t\ts.RLock()\n+\t\tnumInfohashes += uint64(len(s.swarms))\n+\t\tnumSeeders += s.numSeeders\n+\t\tnumLeechers += s.numLeechers\n+\t\ts.RUnlock()\n+\t}\n+\n+\tpromInfohashesCount.Set(float64(numInfohashes))\n+\tpromSeedersCount.Set(float64(numSeeders))\n+\tpromLeechersCount.Set(float64(numLeechers))\n+}\n+\n+func (ps *peerStore) getClock() int64 {\n+\treturn atomic.LoadInt64(&ps.clock)\n+}\n+\n+func (ps *peerStore) setClock(to int64) {\n+\tatomic.StoreInt64(&ps.clock, to)\n+}\n+\n+func (ps *peerStore) shardIndex(infoHash bittorrent.InfoHash, af bittorrent.AddressFamily) uint32 {\n+\t// There are twice the amount of shards specified by the user, the first\n+\t// half is dedicated to IPv4 swarms and the second half is dedicated to\n+\t// IPv6 swarms.\n+\tidx := binary.BigEndian.Uint32(infoHash[:4]) % (uint32(len(ps.shards)) / 2)\n+\tif af == bittorrent.IPv6 {\n+\t\tidx += uint32(len(ps.shards) / 2)\n+\t}\n+\treturn idx\n+}\n+\n+func (ps *peerStore) PutSeeder(ih bittorrent.InfoHash, p bittorrent.Peer) error {\n+\tselect {\n+\tcase <-ps.closed:\n+\t\tpanic(\"attempted to interact with stopped memory store\")\n+\tdefault:\n+\t}\n+\n+\tpk := newPeerKey(p)\n+\n+\tshard := ps.shards[ps.shardIndex(ih, p.IP.AddressFamily)]\n+\tshard.Lock()\n+\n+\tif _, ok := shard.swarms[ih]; !ok {\n+\t\tshard.swarms[ih] = swarm{\n+\t\t\tseeders:  make(map[peerSubnet]map[serializedPeer]int64),\n+\t\t\tleechers: make(map[peerSubnet]map[serializedPeer]int64),\n+\t\t}\n+\t\trecordInfohashesDelta(1)\n+\t}\n+\n+\tpreferredSubnet := newPeerSubnet(p.IP, ps.ipv4Mask, ps.ipv6Mask)\n+\n+\t// Allocate a new map if necessary.\n+\tif shard.swarms[ih].seeders[preferredSubnet] == nil {\n+\t\tshard.swarms[ih].seeders[preferredSubnet] = make(map[serializedPeer]int64)\n+\t}\n+\n+\t// If this peer isn't already a seeder, update the stats for the swarm.\n+\tif _, ok := shard.swarms[ih].seeders[preferredSubnet]; !ok {","path":"storage/memorybysubnet/peer_store.go","position":376,"original_position":376,"commit_id":"e5171f66a754179a2deb0a22190201df6d4cd7ec","original_commit_id":"e5171f66a754179a2deb0a22190201df6d4cd7ec","user":{"login":"mrd0ll4r","id":15063379,"avatar_url":"https://avatars3.githubusercontent.com/u/15063379?v=3","gravatar_id":"","url":"https://api.github.com/users/mrd0ll4r","html_url":"https://github.com/mrd0ll4r","followers_url":"https://api.github.com/users/mrd0ll4r/followers","following_url":"https://api.github.com/users/mrd0ll4r/following{/other_user}","gists_url":"https://api.github.com/users/mrd0ll4r/gists{/gist_id}","starred_url":"https://api.github.com/users/mrd0ll4r/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrd0ll4r/subscriptions","organizations_url":"https://api.github.com/users/mrd0ll4r/orgs","repos_url":"https://api.github.com/users/mrd0ll4r/repos","events_url":"https://api.github.com/users/mrd0ll4r/events{/privacy}","received_events_url":"https://api.github.com/users/mrd0ll4r/received_events","type":"User","site_admin":false},"body":"This should be `shard.swarms[ih].seeders[preferredSubnet][pk]`","created_at":"2017-06-04T11:20:35Z","updated_at":"2017-06-04T11:49:11Z","html_url":"https://github.com/chihaya/chihaya/pull/301#discussion_r120007447","pull_request_url":"https://api.github.com/repos/chihaya/chihaya/pulls/301","_links":{"self":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/comments/120007447"},"html":{"href":"https://github.com/chihaya/chihaya/pull/301#discussion_r120007447"},"pull_request":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/301"}}},"pull_request":{"url":"https://api.github.com/repos/chihaya/chihaya/pulls/301","id":107121779,"html_url":"https://github.com/chihaya/chihaya/pull/301","diff_url":"https://github.com/chihaya/chihaya/pull/301.diff","patch_url":"https://github.com/chihaya/chihaya/pull/301.patch","issue_url":"https://api.github.com/repos/chihaya/chihaya/issues/301","number":301,"state":"open","locked":false,"title":"Storage Drivers (starting w/ subnet memory)","user":{"login":"jzelinskie","id":343539,"avatar_url":"https://avatars0.githubusercontent.com/u/343539?v=3","gravatar_id":"","url":"https://api.github.com/users/jzelinskie","html_url":"https://github.com/jzelinskie","followers_url":"https://api.github.com/users/jzelinskie/followers","following_url":"https://api.github.com/users/jzelinskie/following{/other_user}","gists_url":"https://api.github.com/users/jzelinskie/gists{/gist_id}","starred_url":"https://api.github.com/users/jzelinskie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jzelinskie/subscriptions","organizations_url":"https://api.github.com/users/jzelinskie/orgs","repos_url":"https://api.github.com/users/jzelinskie/repos","events_url":"https://api.github.com/users/jzelinskie/events{/privacy}","received_events_url":"https://api.github.com/users/jzelinskie/received_events","type":"User","site_admin":false},"body":"This is a WIP branch attempting to fix #267.","created_at":"2017-02-21T06:00:46Z","updated_at":"2017-06-04T11:49:12Z","closed_at":null,"merged_at":null,"merge_commit_sha":"3122397a9808ec08c5e5f9c14ad6bc090c172f15","assignee":null,"assignees":[],"requested_reviewers":[],"milestone":null,"commits_url":"https://api.github.com/repos/chihaya/chihaya/pulls/301/commits","review_comments_url":"https://api.github.com/repos/chihaya/chihaya/pulls/301/comments","review_comment_url":"https://api.github.com/repos/chihaya/chihaya/pulls/comments{/number}","comments_url":"https://api.github.com/repos/chihaya/chihaya/issues/301/comments","statuses_url":"https://api.github.com/repos/chihaya/chihaya/statuses/e5171f66a754179a2deb0a22190201df6d4cd7ec","head":{"label":"jzelinskie:memorybysubnet","ref":"memorybysubnet","sha":"e5171f66a754179a2deb0a22190201df6d4cd7ec","user":{"login":"jzelinskie","id":343539,"avatar_url":"https://avatars0.githubusercontent.com/u/343539?v=3","gravatar_id":"","url":"https://api.github.com/users/jzelinskie","html_url":"https://github.com/jzelinskie","followers_url":"https://api.github.com/users/jzelinskie/followers","following_url":"https://api.github.com/users/jzelinskie/following{/other_user}","gists_url":"https://api.github.com/users/jzelinskie/gists{/gist_id}","starred_url":"https://api.github.com/users/jzelinskie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jzelinskie/subscriptions","organizations_url":"https://api.github.com/users/jzelinskie/orgs","repos_url":"https://api.github.com/users/jzelinskie/repos","events_url":"https://api.github.com/users/jzelinskie/events{/privacy}","received_events_url":"https://api.github.com/users/jzelinskie/received_events","type":"User","site_admin":false},"repo":{"id":50328591,"name":"chihaya","full_name":"jzelinskie/chihaya","owner":{"login":"jzelinskie","id":343539,"avatar_url":"https://avatars0.githubusercontent.com/u/343539?v=3","gravatar_id":"","url":"https://api.github.com/users/jzelinskie","html_url":"https://github.com/jzelinskie","followers_url":"https://api.github.com/users/jzelinskie/followers","following_url":"https://api.github.com/users/jzelinskie/following{/other_user}","gists_url":"https://api.github.com/users/jzelinskie/gists{/gist_id}","starred_url":"https://api.github.com/users/jzelinskie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jzelinskie/subscriptions","organizations_url":"https://api.github.com/users/jzelinskie/orgs","repos_url":"https://api.github.com/users/jzelinskie/repos","events_url":"https://api.github.com/users/jzelinskie/events{/privacy}","received_events_url":"https://api.github.com/users/jzelinskie/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/jzelinskie/chihaya","description":"my development fork of chihaya","fork":true,"url":"https://api.github.com/repos/jzelinskie/chihaya","forks_url":"https://api.github.com/repos/jzelinskie/chihaya/forks","keys_url":"https://api.github.com/repos/jzelinskie/chihaya/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jzelinskie/chihaya/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jzelinskie/chihaya/teams","hooks_url":"https://api.github.com/repos/jzelinskie/chihaya/hooks","issue_events_url":"https://api.github.com/repos/jzelinskie/chihaya/issues/events{/number}","events_url":"https://api.github.com/repos/jzelinskie/chihaya/events","assignees_url":"https://api.github.com/repos/jzelinskie/chihaya/assignees{/user}","branches_url":"https://api.github.com/repos/jzelinskie/chihaya/branches{/branch}","tags_url":"https://api.github.com/repos/jzelinskie/chihaya/tags","blobs_url":"https://api.github.com/repos/jzelinskie/chihaya/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jzelinskie/chihaya/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jzelinskie/chihaya/git/refs{/sha}","trees_url":"https://api.github.com/repos/jzelinskie/chihaya/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jzelinskie/chihaya/statuses/{sha}","languages_url":"https://api.github.com/repos/jzelinskie/chihaya/languages","stargazers_url":"https://api.github.com/repos/jzelinskie/chihaya/stargazers","contributors_url":"https://api.github.com/repos/jzelinskie/chihaya/contributors","subscribers_url":"https://api.github.com/repos/jzelinskie/chihaya/subscribers","subscription_url":"https://api.github.com/repos/jzelinskie/chihaya/subscription","commits_url":"https://api.github.com/repos/jzelinskie/chihaya/commits{/sha}","git_commits_url":"https://api.github.com/repos/jzelinskie/chihaya/git/commits{/sha}","comments_url":"https://api.github.com/repos/jzelinskie/chihaya/comments{/number}","issue_comment_url":"https://api.github.com/repos/jzelinskie/chihaya/issues/comments{/number}","contents_url":"https://api.github.com/repos/jzelinskie/chihaya/contents/{+path}","compare_url":"https://api.github.com/repos/jzelinskie/chihaya/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jzelinskie/chihaya/merges","archive_url":"https://api.github.com/repos/jzelinskie/chihaya/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jzelinskie/chihaya/downloads","issues_url":"https://api.github.com/repos/jzelinskie/chihaya/issues{/number}","pulls_url":"https://api.github.com/repos/jzelinskie/chihaya/pulls{/number}","milestones_url":"https://api.github.com/repos/jzelinskie/chihaya/milestones{/number}","notifications_url":"https://api.github.com/repos/jzelinskie/chihaya/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jzelinskie/chihaya/labels{/name}","releases_url":"https://api.github.com/repos/jzelinskie/chihaya/releases{/id}","deployments_url":"https://api.github.com/repos/jzelinskie/chihaya/deployments","created_at":"2016-01-25T05:36:10Z","updated_at":"2016-09-05T21:58:21Z","pushed_at":"2017-06-04T01:13:21Z","git_url":"git://github.com/jzelinskie/chihaya.git","ssh_url":"git@github.com:jzelinskie/chihaya.git","clone_url":"https://github.com/jzelinskie/chihaya.git","svn_url":"https://github.com/jzelinskie/chihaya","homepage":"","size":3152,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"chihaya:master","ref":"master","sha":"abccf5bd7efb8b07b04377e58de5e43e9b2472fd","user":{"login":"chihaya","id":6076200,"avatar_url":"https://avatars0.githubusercontent.com/u/6076200?v=3","gravatar_id":"","url":"https://api.github.com/users/chihaya","html_url":"https://github.com/chihaya","followers_url":"https://api.github.com/users/chihaya/followers","following_url":"https://api.github.com/users/chihaya/following{/other_user}","gists_url":"https://api.github.com/users/chihaya/gists{/gist_id}","starred_url":"https://api.github.com/users/chihaya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chihaya/subscriptions","organizations_url":"https://api.github.com/users/chihaya/orgs","repos_url":"https://api.github.com/users/chihaya/repos","events_url":"https://api.github.com/users/chihaya/events{/privacy}","received_events_url":"https://api.github.com/users/chihaya/received_events","type":"Organization","site_admin":false},"repo":{"id":7399031,"name":"chihaya","full_name":"chihaya/chihaya","owner":{"login":"chihaya","id":6076200,"avatar_url":"https://avatars0.githubusercontent.com/u/6076200?v=3","gravatar_id":"","url":"https://api.github.com/users/chihaya","html_url":"https://github.com/chihaya","followers_url":"https://api.github.com/users/chihaya/followers","following_url":"https://api.github.com/users/chihaya/following{/other_user}","gists_url":"https://api.github.com/users/chihaya/gists{/gist_id}","starred_url":"https://api.github.com/users/chihaya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chihaya/subscriptions","organizations_url":"https://api.github.com/users/chihaya/orgs","repos_url":"https://api.github.com/users/chihaya/repos","events_url":"https://api.github.com/users/chihaya/events{/privacy}","received_events_url":"https://api.github.com/users/chihaya/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/chihaya/chihaya","description":"A customizable, multi-protocol BitTorrent Tracker","fork":false,"url":"https://api.github.com/repos/chihaya/chihaya","forks_url":"https://api.github.com/repos/chihaya/chihaya/forks","keys_url":"https://api.github.com/repos/chihaya/chihaya/keys{/key_id}","collaborators_url":"https://api.github.com/repos/chihaya/chihaya/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/chihaya/chihaya/teams","hooks_url":"https://api.github.com/repos/chihaya/chihaya/hooks","issue_events_url":"https://api.github.com/repos/chihaya/chihaya/issues/events{/number}","events_url":"https://api.github.com/repos/chihaya/chihaya/events","assignees_url":"https://api.github.com/repos/chihaya/chihaya/assignees{/user}","branches_url":"https://api.github.com/repos/chihaya/chihaya/branches{/branch}","tags_url":"https://api.github.com/repos/chihaya/chihaya/tags","blobs_url":"https://api.github.com/repos/chihaya/chihaya/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/chihaya/chihaya/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/chihaya/chihaya/git/refs{/sha}","trees_url":"https://api.github.com/repos/chihaya/chihaya/git/trees{/sha}","statuses_url":"https://api.github.com/repos/chihaya/chihaya/statuses/{sha}","languages_url":"https://api.github.com/repos/chihaya/chihaya/languages","stargazers_url":"https://api.github.com/repos/chihaya/chihaya/stargazers","contributors_url":"https://api.github.com/repos/chihaya/chihaya/contributors","subscribers_url":"https://api.github.com/repos/chihaya/chihaya/subscribers","subscription_url":"https://api.github.com/repos/chihaya/chihaya/subscription","commits_url":"https://api.github.com/repos/chihaya/chihaya/commits{/sha}","git_commits_url":"https://api.github.com/repos/chihaya/chihaya/git/commits{/sha}","comments_url":"https://api.github.com/repos/chihaya/chihaya/comments{/number}","issue_comment_url":"https://api.github.com/repos/chihaya/chihaya/issues/comments{/number}","contents_url":"https://api.github.com/repos/chihaya/chihaya/contents/{+path}","compare_url":"https://api.github.com/repos/chihaya/chihaya/compare/{base}...{head}","merges_url":"https://api.github.com/repos/chihaya/chihaya/merges","archive_url":"https://api.github.com/repos/chihaya/chihaya/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/chihaya/chihaya/downloads","issues_url":"https://api.github.com/repos/chihaya/chihaya/issues{/number}","pulls_url":"https://api.github.com/repos/chihaya/chihaya/pulls{/number}","milestones_url":"https://api.github.com/repos/chihaya/chihaya/milestones{/number}","notifications_url":"https://api.github.com/repos/chihaya/chihaya/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/chihaya/chihaya/labels{/name}","releases_url":"https://api.github.com/repos/chihaya/chihaya/releases{/id}","deployments_url":"https://api.github.com/repos/chihaya/chihaya/deployments","created_at":"2013-01-01T21:08:47Z","updated_at":"2017-06-03T09:48:43Z","pushed_at":"2017-06-04T01:13:23Z","git_url":"git://github.com/chihaya/chihaya.git","ssh_url":"git@github.com:chihaya/chihaya.git","clone_url":"https://github.com/chihaya/chihaya.git","svn_url":"https://github.com/chihaya/chihaya","homepage":"","size":3129,"stargazers_count":536,"watchers_count":536,"language":"Go","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":60,"mirror_url":null,"open_issues_count":24,"forks":60,"open_issues":24,"watchers":536,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/301"},"html":{"href":"https://github.com/chihaya/chihaya/pull/301"},"issue":{"href":"https://api.github.com/repos/chihaya/chihaya/issues/301"},"comments":{"href":"https://api.github.com/repos/chihaya/chihaya/issues/301/comments"},"review_comments":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/301/comments"},"review_comment":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/301/commits"},"statuses":{"href":"https://api.github.com/repos/chihaya/chihaya/statuses/e5171f66a754179a2deb0a22190201df6d4cd7ec"}}}}
{"actor":{"display_login":"mrd0ll4r"}}