{"action":"created","comment":{"url":"https://api.github.com/repos/chihaya/chihaya/pulls/comments/242119638","pull_request_review_id":185554801,"id":242119638,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDI0MjExOTYzOA==","diff_hunk":"@@ -0,0 +1,803 @@\n+// Package redis implements the storage interface for a Chihaya\n+// BitTorrent tracker keeping peer data in redis.\n+package redis\n+\n+import (\n+\t\"encoding/binary\"\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"net\"\n+\t\"net/url\"\n+\t\"strconv\"\n+\t\"strings\"\n+\t\"sync\"\n+\t\"time\"\n+\n+\t\"github.com/gomodule/redigo/redis\"\n+\t\"gopkg.in/redsync.v1\"\n+\t\"gopkg.in/yaml.v2\"\n+\n+\t\"github.com/chihaya/chihaya/bittorrent\"\n+\t\"github.com/chihaya/chihaya/pkg/log\"\n+\t\"github.com/chihaya/chihaya/pkg/stop\"\n+\t\"github.com/chihaya/chihaya/pkg/timecache\"\n+\t\"github.com/chihaya/chihaya/storage\"\n+\t\"github.com/chihaya/chihaya/storage/redis/common\"\n+)\n+\n+// Name is the name by which this peer store is registered with Chihaya.\n+const Name = \"redis\"\n+\n+// Default config constants.\n+const (\n+\tdefaultPrometheusReportingInterval = time.Second * 1\n+\tdefaultGarbageCollectionInterval   = time.Minute * 3\n+\tdefaultPeerLifetime                = time.Minute * 30\n+\tdefaultRedisBroker                 = \"redis://myRedis@127.0.0.1:6379/0\"\n+)\n+\n+func init() {\n+\t// Register the storage driver.\n+\tstorage.RegisterDriver(Name, driver{})\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+// Config holds the configuration of a redis 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+\tRedisBroker                 string        `yaml:\"redis_broker\"`\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\"redisBroker\":        cfg.RedisBroker,\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+\n+\tif cfg.RedisBroker == \"\" {\n+\t\tvalidcfg.RedisBroker = defaultRedisBroker\n+\t\tlog.Warn(\"falling back to default configuration\", log.Fields{\n+\t\t\t\"name\":     Name + \".RedisBroker\",\n+\t\t\t\"provided\": cfg.RedisBroker,\n+\t\t\t\"default\":  validcfg.RedisBroker,\n+\t\t})\n+\t}\n+\n+\tif cfg.GarbageCollectionInterval <= 0 {\n+\t\tvalidcfg.GarbageCollectionInterval = defaultGarbageCollectionInterval\n+\t\tlog.Warn(\"falling back to default configuration\", 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})\n+\t}\n+\n+\tif cfg.PrometheusReportingInterval <= 0 {\n+\t\tvalidcfg.PrometheusReportingInterval = defaultPrometheusReportingInterval\n+\t\tlog.Warn(\"falling back to default configuration\", 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})\n+\t}\n+\n+\tif cfg.PeerLifetime <= 0 {\n+\t\tvalidcfg.PeerLifetime = defaultPeerLifetime\n+\t\tlog.Warn(\"falling back to default configuration\", log.Fields{\n+\t\t\t\"name\":     Name + \".PeerLifetime\",\n+\t\t\t\"provided\": cfg.PeerLifetime,\n+\t\t\t\"default\":  validcfg.PeerLifetime,\n+\t\t})\n+\t}\n+\n+\treturn validcfg\n+}\n+\n+// ParseRedisURL parse url like `redis://pwd@host/db`\n+func ParseRedisURL(target string) (host, password string, db int, err error) {\n+\tvar u *url.URL\n+\tu, err = url.Parse(target)\n+\tif err != nil {\n+\t\treturn\n+\t}\n+\tif u.Scheme != \"redis\" {\n+\t\terr = errors.New(\"No redis scheme found\")\n+\t\treturn\n+\t}\n+\n+\tif u.User != nil {\n+\t\tpassword = u.User.String()\n+\t}\n+\n+\thost = u.Host\n+\n+\tparts := strings.Split(u.Path, \"/\")\n+\tif len(parts) == 1 {\n+\t\tdb = 0 //default redis db\n+\t} else {\n+\t\tdb, err = strconv.Atoi(parts[1])\n+\t\tif err != nil {\n+\t\t\tdb, err = 0, nil //ignore err here\n+\t\t}\n+\t}\n+\n+\treturn\n+}\n+\n+// NewBackend creates Backend instance\n+func NewBackend(host, password, socketPath string, db int) *Backend {\n+\treturn &Backend{\n+\t\thost:       host,\n+\t\tdb:         db,\n+\t\tpassword:   password,\n+\t\tsocketPath: socketPath,\n+\t}\n+}\n+\n+// open returns or creates instance of Redis connection\n+func (rb *Backend) open() redis.Conn {\n+\tif rb.pool == nil {\n+\t\trb.pool = rb.NewPool(rb.socketPath, rb.host, rb.password, rb.db)\n+\t}\n+\tif rb.redsync == nil {\n+\t\tvar pools = []redsync.Pool{rb.pool}\n+\t\trb.redsync = redsync.New(pools)\n+\t}\n+\treturn rb.pool.Get()\n+}\n+\n+// New creates a new PeerStore backed by redis.\n+func New(provided Config) (storage.PeerStore, error) {\n+\tcfg := provided.Validate()\n+\n+\t// creates Backend instance\n+\th, p, db, err := ParseRedisURL(cfg.RedisBroker)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tps := &peerStore{\n+\t\tcfg:    cfg,\n+\t\trb:     NewBackend(h, p, \"\", db),\n+\t\tclosed: make(chan struct{}),\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.Debug(\"storage: purging peers with no announces since\", log.Fields{\"before\": before})\n+\t\t\t\tps.collectGarbage(before)\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.Debug(\"storage: populateProm() finished\", log.Fields{\"timeTaken\": 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+// Backend represents a Memcache result backend\n+type Backend struct {\n+\thost     string\n+\tpassword string\n+\tdb       int\n+\tpool     *redis.Pool\n+\t// If set, path to a socket file overrides hostname\n+\tsocketPath string\n+\tredsync    *redsync.Redsync\n+\tcommon.RedisConnector\n+}\n+\n+type peerStore struct {\n+\tcfg Config\n+\trb  *Backend\n+\n+\tclosed chan struct{}\n+\twg     sync.WaitGroup\n+}\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+\tshards := [2]string{bittorrent.IPv4.String(), bittorrent.IPv6.String()}\n+\n+\tconn := ps.rb.open()\n+\tdefer conn.Close()\n+\n+\tfor _, shard := range shards {\n+\t\tihList, err := conn.Do(\"HKEYS\", shard) // key\n+\t\tif err != nil {\n+\t\t\treturn\n+\t\t}\n+\t\tinfohashes := ihList.([]interface{})\n+\n+\t\tInfohashLPrefix := shard + \"_L_\"\n+\t\tInfohashSPrefix := shard + \"_S_\"\n+\t\tInfohashPrefixLen := len(InfohashLPrefix)\n+\t\tInfohashesMap := make(map[string]bool)\n+\n+\t\tfor _, ihKey := range infohashes {\n+\t\t\tihStr := string(ihKey.([]byte))\n+\t\t\tih := ihStr[InfohashPrefixLen:]\n+\t\t\tif strings.HasPrefix(ihStr, InfohashLPrefix) {\n+\t\t\t\tnumLeechers++\n+\t\t\t\tInfohashesMap[ih] = true\n+\t\t\t} else if strings.HasPrefix(ihStr, InfohashSPrefix) {\n+\t\t\t\tnumSeeders++\n+\t\t\t\tInfohashesMap[ih] = true\n+\t\t\t} else {\n+\t\t\t\tlog.Error(\"storage: invalid Redis state\", log.Fields{\n+\t\t\t\t\t\"Hkey\":   shard,\n+\t\t\t\t\t\"Hfield\": ihStr,\n+\t\t\t\t})\n+\t\t\t}\n+\t\t}\n+\n+\t\tnumInfohashes += uint64(len(InfohashesMap))\n+\t}\n+\n+\tstorage.PromInfohashesCount.Set(float64(numInfohashes))\n+\tstorage.PromSeedersCount.Set(float64(numSeeders))\n+\tstorage.PromLeechersCount.Set(float64(numLeechers))\n+\n+\tlog.Debug(\"storage: populateProm() aggregates metrics over all shards\", log.Fields{\n+\t\t\"numInfohashes\": float64(numInfohashes),\n+\t\t\"numSeeders\":    float64(numSeeders),\n+\t\t\"numLeechers\":   float64(numLeechers),\n+\t})\n+}\n+\n+// recordGCDuration records the duration of a GC sweep.\n+func recordGCDuration(duration time.Duration) {\n+\tlog.Debug(\"storage: recordGCDuration\", log.Fields{\"timeTaken(ms)\": float64(duration.Nanoseconds()) / float64(time.Millisecond)})\n+\tstorage.PromGCDurationMilliseconds.Observe(float64(duration.Nanoseconds()) / float64(time.Millisecond))\n+}\n+\n+func (ps *peerStore) getClock() int64 {\n+\treturn timecache.NowUnixNano()\n+}\n+\n+func (ps *peerStore) PutSeeder(ih bittorrent.InfoHash, p bittorrent.Peer) error {\n+\tIPver := p.IP.AddressFamily.String()\n+\tlog.Debug(\"storage: PutSeeder\", log.Fields{\n+\t\t\"InfoHash\": ih.String(),\n+\t\t\"Peer\":     fmt.Sprintf(\"[ID: %s, IP: %s(AddressFamily: %s), Port %d]\", p.ID.String(), p.IP.String(), IPver, p.Port),\n+\t})\n+\n+\tselect {\n+\tcase <-ps.closed:\n+\t\tpanic(\"attempted to interact with stopped redis store\")\n+\tdefault:\n+\t}\n+\n+\tpk := newPeerKey(p)\n+\n+\tconn := ps.rb.open()\n+\tdefer conn.Close()\n+\n+\t// Update the peer in the swarm.\n+\tencodedSeederInfoHash := IPver + \"_S_\" + ih.String()\n+\tct := ps.getClock()\n+\t_, err := conn.Do(\"HSET\", encodedSeederInfoHash, pk, ct)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\t_, err = conn.Do(\"HSET\", IPver, encodedSeederInfoHash, ct)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (ps *peerStore) DeleteSeeder(ih bittorrent.InfoHash, p bittorrent.Peer) error {\n+\tIPver := p.IP.AddressFamily.String()\n+\tlog.Debug(\"storage: DeleteSeeder\", log.Fields{\n+\t\t\"InfoHash\": ih.String(),\n+\t\t\"Peer\":     fmt.Sprintf(\"[ID: %s, IP: %s(AddressFamily: %s), Port %d]\", p.ID.String(), p.IP.String(), IPver, p.Port),\n+\t})\n+\n+\tselect {\n+\tcase <-ps.closed:\n+\t\tpanic(\"attempted to interact with stopped redis store\")\n+\tdefault:\n+\t}\n+\n+\tpk := newPeerKey(p)\n+\n+\tconn := ps.rb.open()\n+\tdefer conn.Close()\n+\n+\tencodedSeederInfoHash := IPver + \"_S_\" + ih.String()\n+\n+\tDelNum, err := conn.Do(\"HDEL\", encodedSeederInfoHash, pk)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\tif DelNum.(int64) == 0 {\n+\t\treturn storage.ErrResourceDoesNotExist\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (ps *peerStore) PutLeecher(ih bittorrent.InfoHash, p bittorrent.Peer) error {\n+\tIPver := p.IP.AddressFamily.String()\n+\tlog.Debug(\"storage: PutLeecher\", log.Fields{\n+\t\t\"InfoHash\": ih.String(),\n+\t\t\"Peer\":     fmt.Sprintf(\"[ID: %s, IP: %s(AddressFamily: %s), Port %d]\", p.ID.String(), p.IP.String(), IPver, p.Port),\n+\t})\n+\n+\tselect {\n+\tcase <-ps.closed:\n+\t\tpanic(\"attempted to interact with stopped redis store\")\n+\tdefault:\n+\t}\n+\n+\tpk := newPeerKey(p)\n+\n+\tconn := ps.rb.open()\n+\tdefer conn.Close()\n+\n+\t// Update the peer in the swarm.\n+\tencodedLeecherInfoHash := IPver + \"_L_\" + ih.String()\n+\tct := ps.getClock()\n+\t_, err := conn.Do(\"HSET\", encodedLeecherInfoHash, pk, ct)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\t_, err = conn.Do(\"HSET\", IPver, encodedLeecherInfoHash, ct)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (ps *peerStore) DeleteLeecher(ih bittorrent.InfoHash, p bittorrent.Peer) error {\n+\tIPver := p.IP.AddressFamily.String()\n+\tlog.Debug(\"storage: DeleteLeecher\", log.Fields{\n+\t\t\"InfoHash\": ih.String(),\n+\t\t\"Peer\":     fmt.Sprintf(\"[ID: %s, IP: %s(AddressFamily: %s), Port %d]\", p.ID.String(), p.IP.String(), IPver, p.Port),\n+\t})\n+\n+\tselect {\n+\tcase <-ps.closed:\n+\t\tpanic(\"attempted to interact with stopped redis store\")\n+\tdefault:\n+\t}\n+\n+\tpk := newPeerKey(p)\n+\n+\tconn := ps.rb.open()\n+\tdefer conn.Close()\n+\n+\tencodedLeecherInfoHash := IPver + \"_L_\" + ih.String()\n+\n+\tDelNum, err := conn.Do(\"HDEL\", encodedLeecherInfoHash, pk)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\tif DelNum.(int64) == 0 {\n+\t\treturn storage.ErrResourceDoesNotExist\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (ps *peerStore) GraduateLeecher(ih bittorrent.InfoHash, p bittorrent.Peer) error {\n+\tIPver := p.IP.AddressFamily.String()\n+\tlog.Debug(\"storage: GraduateLeecher\", log.Fields{\n+\t\t\"InfoHash\": ih.String(),\n+\t\t\"Peer\":     fmt.Sprintf(\"[ID: %s, IP: %s(AddressFamily: %s), Port %d]\", p.ID.String(), p.IP.String(), IPver, p.Port),\n+\t})\n+\n+\tselect {\n+\tcase <-ps.closed:\n+\t\tpanic(\"attempted to interact with stopped redis store\")\n+\tdefault:\n+\t}\n+\n+\tpk := newPeerKey(p)\n+\n+\tconn := ps.rb.open()\n+\tdefer conn.Close()\n+\n+\tencodedInfoHash := ih.String()\n+\tencodedLeecherInfoHash := IPver + \"_L_\" + encodedInfoHash\n+\tencodedSeederInfoHash := IPver + \"_S_\" + encodedInfoHash\n+\n+\t_, err := conn.Do(\"HDEL\", encodedLeecherInfoHash, pk)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\t// Update the peer in the swarm.\n+\tct := ps.getClock()\n+\t_, err = conn.Do(\"HSET\", encodedSeederInfoHash, pk, ct)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\t_, err = conn.Do(\"HSET\", IPver, encodedSeederInfoHash, ct)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (ps *peerStore) AnnouncePeers(ih bittorrent.InfoHash, seeder bool, numWant int, announcer bittorrent.Peer) (peers []bittorrent.Peer, err error) {\n+\tIPver := announcer.IP.AddressFamily.String()\n+\tlog.Debug(\"storage: AnnouncePeers\", log.Fields{\n+\t\t\"InfoHash\": ih.String(),\n+\t\t\"seeder\":   seeder,\n+\t\t\"numWant\":  numWant,\n+\t\t\"Peer\":     fmt.Sprintf(\"[ID: %s, IP: %s(AddressFamily: %s), Port %d]\", announcer.ID.String(), announcer.IP.String(), IPver, announcer.Port),\n+\t})\n+\n+\tselect {\n+\tcase <-ps.closed:\n+\t\tpanic(\"attempted to interact with stopped redis store\")\n+\tdefault:\n+\t}\n+\n+\tencodedInfoHash := ih.String()\n+\tencodedLeecherInfoHash := IPver + \"_L_\" + encodedInfoHash // key\n+\tencodedSeederInfoHash := IPver + \"_S_\" + encodedInfoHash  // key\n+\n+\tconn := ps.rb.open()\n+\tdefer conn.Close()\n+\n+\tleechers, err := conn.Do(\"HKEYS\", encodedLeecherInfoHash)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tconLeechers := leechers.([]interface{})\n+\n+\tseeders, err := conn.Do(\"HKEYS\", encodedSeederInfoHash)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tconSeeders := seeders.([]interface{})\n+\n+\tif len(conLeechers) == 0 && len(conSeeders) == 0 {\n+\t\treturn nil, storage.ErrResourceDoesNotExist\n+\t}\n+\n+\tif seeder {\n+\t\t// Append leechers as possible.\n+\t\tfor _, pk := range conLeechers {\n+\t\t\tif numWant == 0 {\n+\t\t\t\tbreak\n+\t\t\t}\n+\n+\t\t\tpeers = append(peers, decodePeerKey(serializedPeer(pk.([]byte))))\n+\t\t\tnumWant--\n+\t\t}\n+\t} else {\n+\t\t// Append as many seeders as possible.\n+\t\tfor _, pk := range conSeeders {\n+\t\t\tif numWant == 0 {\n+\t\t\t\tbreak\n+\t\t\t}\n+\n+\t\t\tpeers = append(peers, decodePeerKey(serializedPeer(pk.([]byte))))\n+\t\t\tnumWant--\n+\t\t}\n+\n+\t\t// Append leechers until we reach numWant.\n+\t\tif numWant > 0 {\n+\t\t\tannouncerPK := newPeerKey(announcer)\n+\t\t\tfor _, pk := range conLeechers {\n+\t\t\t\tif pk == announcerPK {\n+\t\t\t\t\tcontinue\n+\t\t\t\t}\n+\n+\t\t\t\tif numWant == 0 {\n+\t\t\t\t\tbreak\n+\t\t\t\t}\n+\n+\t\t\t\tpeers = append(peers, decodePeerKey(serializedPeer(pk.([]byte))))\n+\t\t\t\tnumWant--\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tAPResult := \"\"\n+\tfor _, pr := range peers {\n+\t\tAPResult = fmt.Sprintf(\"%s Peer:[ID: %s, IP: %s(AddressFamily: %s), Port %d]\", APResult, pr.ID.String(), pr.IP.String(), IPver, pr.Port)\n+\t}\n+\tlog.Debug(\"storage: AnnouncePeers result\", log.Fields{\n+\t\t\"peers\": APResult,\n+\t})\n+\n+\treturn\n+}\n+\n+func (ps *peerStore) ScrapeSwarm(ih bittorrent.InfoHash, addressFamily bittorrent.AddressFamily) (resp bittorrent.Scrape) {\n+\tselect {\n+\tcase <-ps.closed:\n+\t\tpanic(\"attempted to interact with stopped redis store\")\n+\tdefault:\n+\t}\n+\n+\tresp.InfoHash = ih\n+\tIPver := addressFamily.String()\n+\tencodedInfoHash := ih.String()\n+\tencodedLeecherInfoHash := IPver + \"_L_\" + encodedInfoHash // key\n+\tencodedSeederInfoHash := IPver + \"_S_\" + encodedInfoHash  // key\n+\n+\tconn := ps.rb.open()\n+\tdefer conn.Close()\n+\n+\tleechersLen, err := conn.Do(\"HLEN\", encodedLeecherInfoHash)\n+\tif err != nil {\n+\t\tlog.Error(\"storage: Redis HLEN failure\", log.Fields{\n+\t\t\t\"Hkey\":  encodedLeecherInfoHash,\n+\t\t\t\"error\": err,\n+\t\t})\n+\t\treturn\n+\t}\n+\tlLen := leechersLen.(int64)\n+\n+\tseedersLen, err := conn.Do(\"HLEN\", encodedSeederInfoHash)\n+\tif err != nil {\n+\t\tlog.Error(\"storage: Redis HLEN failure\", log.Fields{\n+\t\t\t\"Hkey\":  encodedSeederInfoHash,\n+\t\t\t\"error\": err,\n+\t\t})\n+\t\treturn\n+\t}\n+\tsLen := seedersLen.(int64)\n+\n+\tif lLen == 0 && sLen == 0 {\n+\t\treturn\n+\t}\n+\n+\tresp.Incomplete = uint32(lLen)\n+\tresp.Complete = uint32(sLen)\n+\n+\treturn\n+}\n+\n+// collectGarbage deletes all Peers from the PeerStore which are older than the\n+// cutoff time.\n+//\n+// This function must be able to execute while other methods on this interface\n+// are being executed in parallel.\n+func (ps *peerStore) collectGarbage(cutoff time.Time) error {\n+\tselect {\n+\tcase <-ps.closed:\n+\t\treturn nil\n+\tdefault:\n+\t}\n+\n+\tshards := [2]string{bittorrent.IPv4.String(), bittorrent.IPv6.String()}\n+\n+\tconn := ps.rb.open()\n+\tdefer conn.Close()\n+\n+\tcutoffUnix := cutoff.UnixNano()\n+\tstart := time.Now()\n+\n+\tfor _, shard := range shards {\n+\t\tinfohashesList, err := conn.Do(\"HKEYS\", shard) // key\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t\tinfohashes := infohashesList.([]interface{})\n+\n+\t\tfor _, ih := range infohashes {\n+\t\t\tihStr := string(ih.([]byte))\n+\n+\t\t\tihList, err := conn.Do(\"HGETALL\", ihStr) // field\n+\t\t\tif err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\t\t\tconIhList := ihList.([]interface{})\n+\n+\t\t\tif len(conIhList) == 0 {\n+\t\t\t\t_, err := conn.Do(\"DEL\", ihStr)\n+\t\t\t\tif err != nil {\n+\t\t\t\t\treturn err\n+\t\t\t\t}\n+\t\t\t\tlog.Debug(\"storage: Deleting Redis\", log.Fields{\"Hkey\": ihStr})\n+\t\t\t\t_, err = conn.Do(\"HDEL\", shard, ihStr)\n+\t\t\t\tif err != nil {\n+\t\t\t\t\treturn err\n+\t\t\t\t}\n+\t\t\t\tlog.Debug(\"storage: Deleting Redis\", log.Fields{\n+\t\t\t\t\t\"Hkey\":   shard,\n+\t\t\t\t\t\"Hfield\": ihStr,\n+\t\t\t\t})\n+\t\t\t\tcontinue\n+\t\t\t}\n+\n+\t\t\tvar pk serializedPeer\n+\t\t\tfor index, ihField := range conIhList {\n+\t\t\t\tif index%2 != 0 { // value\n+\t\t\t\t\tmtime, err := strconv.ParseInt(string(ihField.([]byte)), 10, 64)\n+\t\t\t\t\tif err != nil {\n+\t\t\t\t\t\treturn err\n+\t\t\t\t\t}\n+\t\t\t\t\tif mtime <= cutoffUnix {\n+\t\t\t\t\t\t_, err := conn.Do(\"HDEL\", ihStr, pk)\n+\t\t\t\t\t\tif err != nil {\n+\t\t\t\t\t\t\treturn err\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\tp := decodePeerKey(pk)\n+\t\t\t\t\t\tlog.Debug(\"storage: Deleting peer\", log.Fields{\n+\t\t\t\t\t\t\t\"Peer\": fmt.Sprintf(\"[ID: %s, IP: %s(AddressFamily: %s), Port %d]\", p.ID.String(), p.IP.String(), p.IP.AddressFamily.String(), p.Port),\n+\t\t\t\t\t\t})\n+\t\t\t\t\t}\n+\t\t\t\t} else { // key\n+\t\t\t\t\tpk = serializedPeer(ihField.([]byte))\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tihLen, err := conn.Do(\"HLEN\", ihStr)\n+\t\t\tif err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\t\t\tif ihLen.(int64) == 0 {\n+\t\t\t\t_, err := conn.Do(\"DEL\", ihStr)\n+\t\t\t\tif err != nil {\n+\t\t\t\t\treturn err\n+\t\t\t\t}\n+\t\t\t\tlog.Debug(\"storage: Deleting Redis\", log.Fields{\"Hkey\": ihStr})\n+\t\t\t\t_, err = conn.Do(\"HDEL\", shard, ihStr)\n+\t\t\t\tif err != nil {\n+\t\t\t\t\treturn err\n+\t\t\t\t}\n+\t\t\t\tlog.Debug(\"storage: Deleting Redis\", log.Fields{","path":"storage/redis/peer_store.go","position":738,"original_position":738,"commit_id":"ca5124e2e18f9160bade28a85ad05fa88892c24d","original_commit_id":"ca5124e2e18f9160bade28a85ad05fa88892c24d","user":{"login":"onestraw","id":6661095,"node_id":"MDQ6VXNlcjY2NjEwOTU=","avatar_url":"https://avatars3.githubusercontent.com/u/6661095?v=4","gravatar_id":"","url":"https://api.github.com/users/onestraw","html_url":"https://github.com/onestraw","followers_url":"https://api.github.com/users/onestraw/followers","following_url":"https://api.github.com/users/onestraw/following{/other_user}","gists_url":"https://api.github.com/users/onestraw/gists{/gist_id}","starred_url":"https://api.github.com/users/onestraw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/onestraw/subscriptions","organizations_url":"https://api.github.com/users/onestraw/orgs","repos_url":"https://api.github.com/users/onestraw/repos","events_url":"https://api.github.com/users/onestraw/events{/privacy}","received_events_url":"https://api.github.com/users/onestraw/received_events","type":"User","site_admin":false},"body":"done","created_at":"2018-12-17T11:55:55Z","updated_at":"2018-12-17T11:55:55Z","html_url":"https://github.com/chihaya/chihaya/pull/433#discussion_r242119638","pull_request_url":"https://api.github.com/repos/chihaya/chihaya/pulls/433","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/comments/242119638"},"html":{"href":"https://github.com/chihaya/chihaya/pull/433#discussion_r242119638"},"pull_request":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/433"}},"in_reply_to_id":241712141},"pull_request":{"url":"https://api.github.com/repos/chihaya/chihaya/pulls/433","id":237964322,"node_id":"MDExOlB1bGxSZXF1ZXN0MjM3OTY0MzIy","html_url":"https://github.com/chihaya/chihaya/pull/433","diff_url":"https://github.com/chihaya/chihaya/pull/433.diff","patch_url":"https://github.com/chihaya/chihaya/pull/433.patch","issue_url":"https://api.github.com/repos/chihaya/chihaya/issues/433","number":433,"state":"open","locked":false,"title":"Redis Storage","user":{"login":"onestraw","id":6661095,"node_id":"MDQ6VXNlcjY2NjEwOTU=","avatar_url":"https://avatars3.githubusercontent.com/u/6661095?v=4","gravatar_id":"","url":"https://api.github.com/users/onestraw","html_url":"https://github.com/onestraw","followers_url":"https://api.github.com/users/onestraw/followers","following_url":"https://api.github.com/users/onestraw/following{/other_user}","gists_url":"https://api.github.com/users/onestraw/gists{/gist_id}","starred_url":"https://api.github.com/users/onestraw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/onestraw/subscriptions","organizations_url":"https://api.github.com/users/onestraw/orgs","repos_url":"https://api.github.com/users/onestraw/repos","events_url":"https://api.github.com/users/onestraw/events{/privacy}","received_events_url":"https://api.github.com/users/onestraw/received_events","type":"User","site_admin":false},"body":"Based on https://github.com/chihaya/chihaya/pull/389","created_at":"2018-12-12T09:22:49Z","updated_at":"2018-12-17T11:55:55Z","closed_at":null,"merged_at":null,"merge_commit_sha":"414045347fa62efc6b289e09a83d516fdff3e01b","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/chihaya/chihaya/pulls/433/commits","review_comments_url":"https://api.github.com/repos/chihaya/chihaya/pulls/433/comments","review_comment_url":"https://api.github.com/repos/chihaya/chihaya/pulls/comments{/number}","comments_url":"https://api.github.com/repos/chihaya/chihaya/issues/433/comments","statuses_url":"https://api.github.com/repos/chihaya/chihaya/statuses/ca5124e2e18f9160bade28a85ad05fa88892c24d","head":{"label":"onestraw:redis","ref":"redis","sha":"ca5124e2e18f9160bade28a85ad05fa88892c24d","user":{"login":"onestraw","id":6661095,"node_id":"MDQ6VXNlcjY2NjEwOTU=","avatar_url":"https://avatars3.githubusercontent.com/u/6661095?v=4","gravatar_id":"","url":"https://api.github.com/users/onestraw","html_url":"https://github.com/onestraw","followers_url":"https://api.github.com/users/onestraw/followers","following_url":"https://api.github.com/users/onestraw/following{/other_user}","gists_url":"https://api.github.com/users/onestraw/gists{/gist_id}","starred_url":"https://api.github.com/users/onestraw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/onestraw/subscriptions","organizations_url":"https://api.github.com/users/onestraw/orgs","repos_url":"https://api.github.com/users/onestraw/repos","events_url":"https://api.github.com/users/onestraw/events{/privacy}","received_events_url":"https://api.github.com/users/onestraw/received_events","type":"User","site_admin":false},"repo":{"id":160489653,"node_id":"MDEwOlJlcG9zaXRvcnkxNjA0ODk2NTM=","name":"chihaya","full_name":"onestraw/chihaya","private":false,"owner":{"login":"onestraw","id":6661095,"node_id":"MDQ6VXNlcjY2NjEwOTU=","avatar_url":"https://avatars3.githubusercontent.com/u/6661095?v=4","gravatar_id":"","url":"https://api.github.com/users/onestraw","html_url":"https://github.com/onestraw","followers_url":"https://api.github.com/users/onestraw/followers","following_url":"https://api.github.com/users/onestraw/following{/other_user}","gists_url":"https://api.github.com/users/onestraw/gists{/gist_id}","starred_url":"https://api.github.com/users/onestraw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/onestraw/subscriptions","organizations_url":"https://api.github.com/users/onestraw/orgs","repos_url":"https://api.github.com/users/onestraw/repos","events_url":"https://api.github.com/users/onestraw/events{/privacy}","received_events_url":"https://api.github.com/users/onestraw/received_events","type":"User","site_admin":false},"html_url":"https://github.com/onestraw/chihaya","description":"A customizable, multi-protocol BitTorrent Tracker","fork":true,"url":"https://api.github.com/repos/onestraw/chihaya","forks_url":"https://api.github.com/repos/onestraw/chihaya/forks","keys_url":"https://api.github.com/repos/onestraw/chihaya/keys{/key_id}","collaborators_url":"https://api.github.com/repos/onestraw/chihaya/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/onestraw/chihaya/teams","hooks_url":"https://api.github.com/repos/onestraw/chihaya/hooks","issue_events_url":"https://api.github.com/repos/onestraw/chihaya/issues/events{/number}","events_url":"https://api.github.com/repos/onestraw/chihaya/events","assignees_url":"https://api.github.com/repos/onestraw/chihaya/assignees{/user}","branches_url":"https://api.github.com/repos/onestraw/chihaya/branches{/branch}","tags_url":"https://api.github.com/repos/onestraw/chihaya/tags","blobs_url":"https://api.github.com/repos/onestraw/chihaya/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/onestraw/chihaya/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/onestraw/chihaya/git/refs{/sha}","trees_url":"https://api.github.com/repos/onestraw/chihaya/git/trees{/sha}","statuses_url":"https://api.github.com/repos/onestraw/chihaya/statuses/{sha}","languages_url":"https://api.github.com/repos/onestraw/chihaya/languages","stargazers_url":"https://api.github.com/repos/onestraw/chihaya/stargazers","contributors_url":"https://api.github.com/repos/onestraw/chihaya/contributors","subscribers_url":"https://api.github.com/repos/onestraw/chihaya/subscribers","subscription_url":"https://api.github.com/repos/onestraw/chihaya/subscription","commits_url":"https://api.github.com/repos/onestraw/chihaya/commits{/sha}","git_commits_url":"https://api.github.com/repos/onestraw/chihaya/git/commits{/sha}","comments_url":"https://api.github.com/repos/onestraw/chihaya/comments{/number}","issue_comment_url":"https://api.github.com/repos/onestraw/chihaya/issues/comments{/number}","contents_url":"https://api.github.com/repos/onestraw/chihaya/contents/{+path}","compare_url":"https://api.github.com/repos/onestraw/chihaya/compare/{base}...{head}","merges_url":"https://api.github.com/repos/onestraw/chihaya/merges","archive_url":"https://api.github.com/repos/onestraw/chihaya/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/onestraw/chihaya/downloads","issues_url":"https://api.github.com/repos/onestraw/chihaya/issues{/number}","pulls_url":"https://api.github.com/repos/onestraw/chihaya/pulls{/number}","milestones_url":"https://api.github.com/repos/onestraw/chihaya/milestones{/number}","notifications_url":"https://api.github.com/repos/onestraw/chihaya/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/onestraw/chihaya/labels{/name}","releases_url":"https://api.github.com/repos/onestraw/chihaya/releases{/id}","deployments_url":"https://api.github.com/repos/onestraw/chihaya/deployments","created_at":"2018-12-05T09:00:23Z","updated_at":"2018-12-17T09:42:46Z","pushed_at":"2018-12-17T09:42:44Z","git_url":"git://github.com/onestraw/chihaya.git","ssh_url":"git@github.com:onestraw/chihaya.git","clone_url":"https://github.com/onestraw/chihaya.git","svn_url":"https://github.com/onestraw/chihaya","homepage":"","size":3331,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"chihaya:master","ref":"master","sha":"dcd8e8ea86099aa89bb68e061d46456dee288607","user":{"login":"chihaya","id":6076200,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYwNzYyMDA=","avatar_url":"https://avatars3.githubusercontent.com/u/6076200?v=4","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,"node_id":"MDEwOlJlcG9zaXRvcnk3Mzk5MDMx","name":"chihaya","full_name":"chihaya/chihaya","private":false,"owner":{"login":"chihaya","id":6076200,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYwNzYyMDA=","avatar_url":"https://avatars3.githubusercontent.com/u/6076200?v=4","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},"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":"2018-12-17T09:31:27Z","pushed_at":"2018-12-17T09:31:25Z","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":3322,"stargazers_count":787,"watchers_count":787,"language":"Go","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":110,"mirror_url":null,"archived":false,"open_issues_count":20,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":110,"open_issues":20,"watchers":787,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/433"},"html":{"href":"https://github.com/chihaya/chihaya/pull/433"},"issue":{"href":"https://api.github.com/repos/chihaya/chihaya/issues/433"},"comments":{"href":"https://api.github.com/repos/chihaya/chihaya/issues/433/comments"},"review_comments":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/433/comments"},"review_comment":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/chihaya/chihaya/pulls/433/commits"},"statuses":{"href":"https://api.github.com/repos/chihaya/chihaya/statuses/ca5124e2e18f9160bade28a85ad05fa88892c24d"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"onestraw"}}