// watchNode watch a named node for leader selection when failover func watchCometNode(conn *zk.Conn, node, fpath string, retry, ping time.Duration, ch chan *CometNodeEvent) { fpath = path.Join(fpath, node) for { nodes, watch, err := myzk.GetNodesW(conn, fpath) if err == myzk.ErrNodeNotExist { log.Warn("zk don't have node \"%s\"", fpath) break } else if err == myzk.ErrNoChild { log.Warn("zk don't have any children in \"%s\", retry in %d second", fpath, waitNodeDelay) time.Sleep(waitNodeDelaySecond) continue } else if err != nil { log.Error("zk path: \"%s\" getNodes error(%v), retry in %d second", fpath, err, waitNodeDelay) time.Sleep(waitNodeDelaySecond) continue } // leader selection sort.Strings(nodes) if info, err := registerCometNode(conn, nodes[0], fpath, retry, ping, true); err != nil { log.Error("zk path: \"%s\" registerCometNode error(%v)", fpath, err) time.Sleep(waitNodeDelaySecond) continue } else { // update node info ch <- &CometNodeEvent{Event: eventNodeUpdate, Key: node, Value: info} } // blocking receive event event := <-watch log.Info("zk path: \"%s\" receive a event: (%v)", fpath, event) } // WARN, if no persistence node and comet rpc not config log.Warn("zk path: \"%s\" never watch again till recreate", fpath) }
// RegisterTmp create a ephemeral node, and watch it, if node droped then send a SIGQUIT to self. func RegisterTemp(conn *zk.Conn, fpath string, data []byte) error { tpath, err := conn.Create(path.Join(fpath)+"/", data, zk.FlagEphemeral|zk.FlagSequence, zk.WorldACL(zk.PermAll)) if err != nil { log.Error("conn.Create(\"%s\", \"%s\", zk.FlagEphemeral|zk.FlagSequence) error(%v)", fpath, string(data), err) return err } log.Debug("create a zookeeper node:%s", tpath) // watch self go func() { for { log.Info("zk path: \"%s\" set a watch", tpath) exist, _, watch, err := conn.ExistsW(tpath) if err != nil { log.Error("zk.ExistsW(\"%s\") error(%v)", tpath, err) log.Warn("zk path: \"%s\" set watch failed, kill itself", tpath) killSelf() return } if !exist { log.Warn("zk path: \"%s\" not exist, kill itself", tpath) killSelf() return } event := <-watch log.Info("zk path: \"%s\" receive a event %v", tpath, event) } }() return nil }
// watchMessageRoot watch the message root path. func watchMessageRoot(conn *zk.Conn, fpath string, ch chan *MessageNodeEvent) error { for { nodes, watch, err := myzk.GetNodesW(conn, fpath) if err == myzk.ErrNodeNotExist { log.Warn("zk don't have node \"%s\", retry in %d second", fpath, waitNodeDelay) time.Sleep(waitNodeDelaySecond) continue } else if err == myzk.ErrNoChild { log.Warn("zk don't have any children in \"%s\", retry in %d second", fpath, waitNodeDelay) // all child died, kick all the nodes for _, client := range MessageRPC.Clients { log.Debug("node: \"%s\" send del node event", client.Addr) ch <- &MessageNodeEvent{Event: eventNodeDel, Key: &WeightRpc{Addr: client.Addr, Weight: client.Weight}} } time.Sleep(waitNodeDelaySecond) continue } else if err != nil { log.Error("getNodes error(%v), retry in %d second", err, waitNodeDelay) time.Sleep(waitNodeDelaySecond) continue } nodesMap := map[string]bool{} // handle new add nodes for _, node := range nodes { data, _, err := conn.Get(path.Join(fpath, node)) if err != nil { log.Error("zk.Get(\"%s\") error(%v)", path.Join(fpath, node), err) continue } // parse message node info nodeInfo := &MessageNodeInfo{} if err := json.Unmarshal(data, nodeInfo); err != nil { log.Error("json.Unmarshal(\"%s\", nodeInfo) error(%v)", string(data), err) continue } for _, addr := range nodeInfo.Rpc { // if not exists in old map then trigger a add event if _, ok := MessageRPC.Clients[addr]; !ok { ch <- &MessageNodeEvent{Event: eventNodeAdd, Key: &WeightRpc{Addr: addr, Weight: nodeInfo.Weight}} } nodesMap[addr] = true } } // handle delete nodes for _, client := range MessageRPC.Clients { if _, ok := nodesMap[client.Addr]; !ok { ch <- &MessageNodeEvent{Event: eventNodeDel, Key: client} } } // blocking wait node changed event := <-watch log.Info("zk path: \"%s\" receive a event %v", fpath, event) } }
// hanleTCPConn handle a long live tcp connection. func handleTCPConn(conn net.Conn, rc chan *bufio.Reader) { addr := conn.RemoteAddr().String() log.Debug("<%s> handleTcpConn routine start", addr) rd := newBufioReader(rc, conn) if args, err := parseCmd(rd); err == nil { // return buffer bufio.Reader putBufioReader(rc, rd) switch args[0] { case "sub": SubscribeTCPHandle(conn, args[1:]) break default: conn.Write(ParamReply) log.Warn("<%s> unknown cmd \"%s\"", addr, args[0]) break } } else { // return buffer bufio.Reader putBufioReader(rc, rd) log.Error("<%s> parseCmd() error(%v)", addr, err) } // close the connection if err := conn.Close(); err != nil { log.Error("<%s> conn.Close() error(%v)", addr, err) } log.Debug("<%s> handleTcpConn routine stop", addr) return }
// DelMulti implements the Storage DelMulti method. func (s *RedisStorage) clean() { for { info := <-s.delCH conn := s.getConn(info.Key) if conn == nil { log.Warn("get redis connection nil") continue } for _, mid := range info.MIds { if err := conn.Send("ZREMRANGEBYSCORE", info.Key, mid, mid); err != nil { log.Error("conn.Send(\"ZREMRANGEBYSCORE\", \"%s\", %d, %d) error(%v)", info.Key, mid, mid, err) conn.Close() continue } } if err := conn.Flush(); err != nil { log.Error("conn.Flush() error(%v)", err) conn.Close() continue } for _, _ = range info.MIds { _, err := conn.Receive() if err != nil { log.Error("conn.Receive() error(%v)", err) conn.Close() continue } } conn.Close() } }
// GetPrivate implements the Storage GetPrivate method. func (s *MySQLStorage) GetPrivate(key string, mid int64) ([]*myrpc.Message, error) { db := s.getConn(key) if db == nil { return nil, ErrNoMySQLConn } now := time.Now().Unix() rows, err := db.Query(getPrivateMsgSQL, key, mid) if err != nil { log.Error("db.Query(\"%s\",\"%s\",%d,now) failed (%v)", getPrivateMsgSQL, key, mid, err) return nil, err } msgs := []*myrpc.Message{} for rows.Next() { expire := int64(0) cmid := int64(0) msg := []byte{} if err := rows.Scan(&cmid, &expire, &msg); err != nil { log.Error("rows.Scan() failed (%v)", err) return nil, err } if now > expire { log.Warn("user_key: \"%s\" mid: %d expired", key, cmid) continue } msgs = append(msgs, &myrpc.Message{MsgId: cmid, GroupId: myrpc.PrivateGroupId, Msg: json.RawMessage(msg)}) } return msgs, nil }
// Get a user channel from ChannleList. func (l *ChannelList) Get(key string, newOne bool) (Channel, error) { // validate if err := l.validate(key); err != nil { return nil, err } // get a channel bucket b := l.Bucket(key) b.Lock() if c, ok := b.Data[key]; !ok { if !Conf.Auth && newOne { c = NewSeqChannel() b.Data[key] = c b.Unlock() ChStat.IncrCreate() log.Info("user_key:\"%s\" create a new channel", key) return c, nil } else { b.Unlock() log.Warn("user_key:\"%s\" channle not exists", key) return nil, ErrChannelNotExist } } else { b.Unlock() ChStat.IncrAccess() log.Info("user_key:\"%s\" refresh channel bucket expire time", key) return c, nil } }
// GetPrivate implements the Storage GetPrivate method. func (s *RedisStorage) GetPrivate(key string, mid int64) ([]*myrpc.Message, error) { conn := s.getConn(key) if conn == nil { return nil, RedisNoConnErr } defer conn.Close() values, err := redis.Values(conn.Do("ZRANGEBYSCORE", key, fmt.Sprintf("(%d", mid), "+inf", "WITHSCORES")) if err != nil { log.Error("conn.Do(\"ZRANGEBYSCORE\", \"%s\", \"%d\", \"+inf\", \"WITHSCORES\") error(%v)", key, mid, err) return nil, err } msgs := make([]*myrpc.Message, 0, len(values)) delMsgs := []int64{} now := time.Now().Unix() for len(values) > 0 { cmid := int64(0) b := []byte{} values, err = redis.Scan(values, &b, &cmid) if err != nil { log.Error("redis.Scan() error(%v)", err) return nil, err } rm := &RedisPrivateMessage{} if err = json.Unmarshal(b, rm); err != nil { log.Error("json.Unmarshal(\"%s\", rm) error(%v)", string(b), err) delMsgs = append(delMsgs, cmid) continue } // check expire if rm.Expire < now { log.Warn("user_key: \"%s\" msg: %d expired", key, cmid) delMsgs = append(delMsgs, cmid) continue } m := &myrpc.Message{MsgId: cmid, Msg: rm.Msg, GroupId: myrpc.PrivateGroupId} msgs = append(msgs, m) } // delete unmarshal failed and expired message if len(delMsgs) > 0 { select { case s.delCH <- &RedisDelMessage{Key: key, MIds: delMsgs}: default: log.Warn("user_key: \"%s\" send del messages failed, channel full", key) } } return msgs, nil }
// putBufioReader pub back a Reader to chan, if chan full discard it. func putBufioReader(c chan *bufio.Reader, r *bufio.Reader) { r.Reset(nil) select { case c <- r: default: log.Warn("tcp bufioReader cache full") } }
// Auth auth a token is valid func (t *Token) Auth(ticket string) error { if e, ok := t.token[ticket]; !ok { log.Warn("token \"%s\" not exist", ticket) return ErrTokenNotExist } else { td, _ := e.Value.(*TokenData) if time.Now().After(td.Expire) { t.clean() log.Warn("token \"%s\" expired", ticket) return ErrTokenExpired } td.Expire = time.Now().Add(Conf.TokenExpire) t.lru.MoveToBack(e) } t.clean() return nil }
// Write different message to client by different protocol func (c *Connection) Write(key string, msg []byte) { select { case c.Buf <- msg: default: c.Conn.Close() log.Warn("user_key: \"%s\" discard message: \"%s\" and close connection", key, string(msg)) } }
func (s *RedisStorage) getConnByNode(node string) redis.Conn { p, ok := s.pool[node] if !ok { log.Warn("no node: \"%s\" in redis pool", node) return nil } return p.Get() }
// newBufioReader get a Reader by chan, if chan empty new a Reader. func newBufioReader(c chan *bufio.Reader, r io.Reader) *bufio.Reader { select { case p := <-c: p.Reset(r) return p default: log.Warn("tcp bufioReader cache empty") return bufio.NewReaderSize(r, Conf.RcvbufSize) } }
// Add add a token func (t *Token) Add(ticket string) error { if e, ok := t.token[ticket]; !ok { // new element add to lru back e = t.lru.PushBack(&TokenData{Ticket: ticket, Expire: time.Now().Add(Conf.TokenExpire)}) t.token[ticket] = e } else { log.Warn("token \"%s\" exist", ticket) return ErrTokenExist } t.clean() return nil }
// validate check the key is belong to this comet. func (l *ChannelList) validate(key string) error { if len(nodeWeightMap) == 0 { log.Debug("no node found") return ErrChannelKey } node := CometRing.Hash(key) log.Debug("match node:%s hash node:%s", Conf.ZookeeperCometNode, node) if Conf.ZookeeperCometNode != node { log.Warn("user_key:\"%s\" node:%s not match this node:%s", key, node, Conf.ZookeeperCometNode) return ErrChannelKey } return nil }
// getConn get the connection of matching with key using ketama hash func (s *MySQLStorage) getConn(key string) *sql.DB { if len(s.pool) == 0 { return nil } node := s.ring.Hash(key) p, ok := s.pool[node] if !ok { log.Warn("user_key: \"%s\" hit mysql node: \"%s\" not in pool", key, node) return nil } log.Info("user_key: \"%s\" hit mysql node: \"%s\"", key, node) return p }
// watchCometRoot watch the gopush root node for detecting the node add/del. func watchCometRoot(conn *zk.Conn, fpath string, ch chan *CometNodeEvent) error { for { nodes, watch, err := myzk.GetNodesW(conn, fpath) if err == myzk.ErrNodeNotExist { log.Warn("zk don't have node \"%s\", retry in %d second", fpath, waitNodeDelay) time.Sleep(waitNodeDelaySecond) continue } else if err == myzk.ErrNoChild { log.Warn("zk don't have any children in \"%s\", retry in %d second", fpath, waitNodeDelay) for node, _ := range cometNodeInfoMap { ch <- &CometNodeEvent{Event: eventNodeDel, Key: node} } time.Sleep(waitNodeDelaySecond) continue } else if err != nil { log.Error("getNodes error(%v), retry in %d second", err, waitNodeDelay) time.Sleep(waitNodeDelaySecond) continue } nodesMap := map[string]bool{} // handle new add nodes for _, node := range nodes { if _, ok := cometNodeInfoMap[node]; !ok { ch <- &CometNodeEvent{Event: eventNodeAdd, Key: node} } nodesMap[node] = true } // handle delete nodes for node, _ := range cometNodeInfoMap { if _, ok := nodesMap[node]; !ok { ch <- &CometNodeEvent{Event: eventNodeDel, Key: node} } } event := <-watch log.Info("zk path: \"%s\" receive a event %v", fpath, event) } }
// Close implements the Channel Close method. func (c *SeqChannel) Close() error { c.mutex.Lock() for e := c.conn.Front(); e != nil; e = e.Next() { if conn, ok := e.Value.(*Connection); !ok { c.mutex.Unlock() return ErrAssectionConn } else { if err := conn.Conn.Close(); err != nil { // ignore close error log.Warn("conn.Close() error(%v)", err) } } } c.mutex.Unlock() return nil }
// Delete a user channel from ChannleList. func (l *ChannelList) Delete(key string) (Channel, error) { // get a channel bucket b := l.Bucket(key) b.Lock() if c, ok := b.Data[key]; !ok { b.Unlock() log.Warn("user_key:\"%s\" delete channle not exists", key) return nil, ErrChannelNotExist } else { delete(b.Data, key) b.Unlock() ChStat.IncrDelete() log.Info("user_key:\"%s\" delete channel", key) return c, nil } }
// StartListen start accept client. func StartComet() error { for _, proto := range Conf.Proto { if proto == WebsocketProtoStr { // Start http push service if err := StartWebsocket(); err != nil { return err } } else if proto == TCPProtoStr { // Start tcp push service if err := StartTCP(); err != nil { return err } } else { log.Warn("unknown gopush-cluster protocol %s, (\"websocket\" or \"tcp\")", proto) } } return nil }
// Create create zookeeper path, if path exists ignore error func Create(conn *zk.Conn, fpath string) error { // create zk root path tpath := "" for _, str := range strings.Split(fpath, "/")[1:] { tpath = path.Join(tpath, "/", str) log.Debug("create zookeeper path: \"%s\"", tpath) _, err := conn.Create(tpath, []byte(""), 0, zk.WorldACL(zk.PermAll)) if err != nil { if err == zk.ErrNodeExists { log.Warn("zk.create(\"%s\") exists", tpath) } else { log.Error("zk.create(\"%s\") error(%v)", tpath, err) return err } } } return nil }
// clean scan the lru list expire the element func (t *Token) clean() { now := time.Now() e := t.lru.Front() for { if e == nil { break } td, _ := e.Value.(*TokenData) if now.After(td.Expire) { log.Warn("token \"%s\" expired", td.Ticket) o := e.Next() delete(t.token, td.Ticket) t.lru.Remove(e) e = o continue } break } }
// DelPrivate handle for push private message. func DelPrivate(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method Not Allowed", 405) return } body := "" res := map[string]interface{}{"ret": OK} defer retPWrite(w, r, res, &body, time.Now()) // param bodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { res["ret"] = ParamErr log.Error("ioutil.ReadAll() failed (%v)", err) return } body = string(bodyBytes) params, err := url.ParseQuery(body) if err != nil { log.Error("url.ParseQuery(\"%s\") error(%v)", body, err) res["ret"] = ParamErr return } key := params.Get("key") if key == "" { res["ret"] = ParamErr return } client := myrpc.MessageRPC.Get() if client == nil { log.Warn("user_key: \"%s\" can't not find message rpc node", key) res["ret"] = InternalErr return } ret := 0 if err := client.Call(myrpc.MessageServiceDelPrivate, key, &ret); err != nil { log.Error("client.Call(\"%s\", \"%s\", &ret) error(%v)", myrpc.MessageServiceDelPrivate, key, err) res["ret"] = InternalErr return } return }
// handleNodeEvent add and remove MessageRPC.Clients, copy the src map to a new map then replace the variable. func handleMessageNodeEvent(conn *zk.Conn, retry, ping time.Duration, ch chan *MessageNodeEvent) { for { ev := <-ch // copy map from src tmpMessageRPCMap := make(map[string]*WeightRpc, len(MessageRPC.Clients)) for k, v := range MessageRPC.Clients { tmpMessageRPCMap[k] = &WeightRpc{Client: v.Client, Addr: v.Addr, Weight: v.Weight} // reuse rpc connection v.Client = nil } // handle event if ev.Event == eventNodeAdd { log.Info("add message rpc node: \"%s\"", ev.Key.Addr) rpcTmp, err := rpc.Dial("tcp", ev.Key.Addr) if err != nil { log.Error("rpc.Dial(\"tcp\", \"%s\") error(%v)", ev.Key, err) log.Warn("discard message rpc node: \"%s\", connect failed", ev.Key) continue } ev.Key.Client = rpcTmp tmpMessageRPCMap[ev.Key.Addr] = ev.Key } else if ev.Event == eventNodeDel { log.Info("del message rpc node: \"%s\"", ev.Key.Addr) delete(tmpMessageRPCMap, ev.Key.Addr) } else { log.Error("unknown node event: %d", ev.Event) panic("unknown node event") } tmpMessageRPC, err := NewRandLB(tmpMessageRPCMap, MessageService, retry, ping, true) if err != nil { log.Error("NewRandLR() error(%v)", err) panic(err) } oldMessageRPC := MessageRPC // atomic update MessageRPC = tmpMessageRPC // release resource oldMessageRPC.Destroy() log.Debug("MessageRPC.Client length: %d", len(MessageRPC.Clients)) } }
// SubscribeTCPHandle handle the subscribers's connection. func SubscribeTCPHandle(conn net.Conn, args []string) { argLen := len(args) addr := conn.RemoteAddr().String() if argLen < 2 { conn.Write(ParamReply) log.Error("<%s> subscriber missing argument", addr) return } // key, heartbeat key := args[0] if key == "" { conn.Write(ParamReply) log.Warn("<%s> key param error", addr) return } heartbeatStr := args[1] i, err := strconv.Atoi(heartbeatStr) if err != nil { conn.Write(ParamReply) log.Error("<%s> user_key:\"%s\" heartbeat:\"%s\" argument error (%v)", addr, key, heartbeatStr, err) return } if i < minHearbeatSec { conn.Write(ParamReply) log.Warn("<%s> user_key:\"%s\" heartbeat argument error, less than %d", addr, key, minHearbeatSec) return } heartbeat := i + delayHeartbeatSec token := "" if argLen > 2 { token = args[2] } version := "" if argLen > 3 { version = args[3] } log.Info("<%s> subscribe to key = %s, heartbeat = %d, token = %s, version = %s", addr, key, heartbeat, token, version) // fetch subscriber from the channel c, err := UserChannel.Get(key, true) if err != nil { log.Warn("<%s> user_key:\"%s\" can't get a channel (%s)", addr, key, err) if err == ErrChannelKey { conn.Write(NodeReply) } else { conn.Write(ChannelReply) } return } // auth token if ok := c.AuthToken(key, token); !ok { conn.Write(AuthReply) log.Error("<%s> user_key:\"%s\" auth token \"%s\" failed", addr, key, token) return } // add a conn to the channel connElem, err := c.AddConn(key, &Connection{Conn: conn, Proto: TCPProto, Version: version}) if err != nil { log.Error("<%s> user_key:\"%s\" add conn error(%v)", addr, key, err) return } // blocking wait client heartbeat reply := []byte{0} // reply := make([]byte, HeartbeatLen) begin := time.Now().UnixNano() end := begin + Second for { // more then 1 sec, reset the timer if end-begin >= Second { if err = conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(heartbeat))); err != nil { log.Error("<%s> user_key:\"%s\" conn.SetReadDeadLine() error(%v)", addr, key, err) break } begin = end } if _, err = conn.Read(reply); err != nil { if err != io.EOF { log.Warn("<%s> user_key:\"%s\" conn.Read() failed, read heartbeat timedout error(%v)", addr, key, err) } else { // client connection close log.Warn("<%s> user_key:\"%s\" client connection close error(%v)", addr, key, err) } break } if string(reply) == Heartbeat { if _, err = conn.Write(HeartbeatReply); err != nil { log.Error("<%s> user_key:\"%s\" conn.Write() failed, write heartbeat to client error(%v)", addr, key, err) break } log.Debug("<%s> user_key:\"%s\" receive heartbeat (%s)", addr, key, reply) } else { log.Warn("<%s> user_key:\"%s\" unknown heartbeat protocol (%s)", addr, key, reply) break } end = time.Now().UnixNano() } // remove exists conn if err := c.RemoveConn(key, connElem); err != nil { log.Error("<%s> user_key:\"%s\" remove conn error(%v)", addr, key, err) } return }
// Subscriber Handle is the websocket handle for sub request. func SubscribeHandle(ws *websocket.Conn) { addr := ws.Request().RemoteAddr params := ws.Request().URL.Query() // get subscriber key key := params.Get("key") if key == "" { ws.Write(ParamReply) log.Warn("<%s> key param error", addr) return } // get heartbeat second heartbeatStr := params.Get("heartbeat") i, err := strconv.Atoi(heartbeatStr) if err != nil { ws.Write(ParamReply) log.Error("<%s> user_key:\"%s\" heartbeat argument error(%v)", addr, key, err) return } if i < minHearbeatSec { ws.Write(ParamReply) log.Warn("<%s> user_key:\"%s\" heartbeat argument error, less than %d", addr, key, minHearbeatSec) return } heartbeat := i + delayHeartbeatSec token := params.Get("token") version := params.Get("ver") log.Info("<%s> subscribe to key = %s, heartbeat = %d, token = %s, version = %s", addr, key, heartbeat, token, version) // fetch subscriber from the channel c, err := UserChannel.Get(key, true) if err != nil { log.Warn("<%s> user_key:\"%s\" can't get a channel (%s)", addr, key, err) if err == ErrChannelKey { ws.Write(NodeReply) } else { ws.Write(ChannelReply) } return } // auth token if ok := c.AuthToken(key, token); !ok { ws.Write(AuthReply) log.Error("<%s> user_key:\"%s\" auth token \"%s\" failed", addr, key, token) return } // add a conn to the channel connElem, err := c.AddConn(key, &Connection{Conn: ws, Proto: WebsocketProto, Version: version}) if err != nil { log.Error("<%s> user_key:\"%s\" add conn error(%v)", addr, key, err) return } // blocking wait client heartbeat reply := "" begin := time.Now().UnixNano() end := begin + Second for { // more then 1 sec, reset the timer if end-begin >= Second { if err = ws.SetReadDeadline(time.Now().Add(time.Second * time.Duration(heartbeat))); err != nil { log.Error("<%s> user_key:\"%s\" websocket.SetReadDeadline() error(%v)", addr, key, err) break } begin = end } if err = websocket.Message.Receive(ws, &reply); err != nil { log.Error("<%s> user_key:\"%s\" websocket.Message.Receive() error(%v)", addr, key, err) break } if reply == Heartbeat { if _, err = ws.Write(HeartbeatReply); err != nil { log.Error("<%s> user_key:\"%s\" write heartbeat to client error(%s)", addr, key, err) break } log.Debug("<%s> user_key:\"%s\" receive heartbeat", addr, key) } else { log.Warn("<%s> user_key:\"%s\" unknown heartbeat protocol", addr, key) break } end = time.Now().UnixNano() } // remove exists conn if err := c.RemoveConn(key, connElem); err != nil { log.Error("<%s> user_key:\"%s\" remove conn error(%v)", addr, key, err) } return }