// request2Response converts will interpret VBucket field as Status field and // re-interpret the request packate as response packet. This is required for // UPR streams which are full duplex. func request2Response(req *mcd.MCRequest) *mcd.MCResponse { return &mcd.MCResponse{ Opcode: req.Opcode, Cas: req.Cas, Opaque: req.Opaque, Status: mcd.Status(req.VBucket), Extras: req.Extras, Key: req.Key, Body: req.Body, } }
// Connect once to the server and work the UPR stream. If anything // goes wrong, return our level of progress in order to let our caller // control any potential retries. func (d *bucketDataSource) worker(server string, workerCh chan []uint16) int { atomic.AddUint64(&d.stats.TotWorkerBody, 1) if !d.isRunning() { return -1 } atomic.AddUint64(&d.stats.TotWorkerConnect, 1) connect := d.options.Connect if connect == nil { connect = memcached.Connect } client, err := connect("tcp", server) if err != nil { atomic.AddUint64(&d.stats.TotWorkerConnectErr, 1) d.receiver.OnError(fmt.Errorf("worker connect, server: %s, err: %v", server, err)) return 0 } defer client.Close() atomic.AddUint64(&d.stats.TotWorkerConnectOk, 1) if d.auth != nil { var user, pswd string var adminCred bool if auth, ok := d.auth.(couchbase.AuthWithSaslHandler); ok { user, pswd = auth.GetSaslCredentials() adminCred = true } else { user, pswd, _ = d.auth.GetCredentials() } if user != "" { atomic.AddUint64(&d.stats.TotWorkerAuth, 1) res, err := client.Auth(user, pswd) if err != nil { atomic.AddUint64(&d.stats.TotWorkerAuthErr, 1) d.receiver.OnError(fmt.Errorf("worker auth, server: %s, user: %s, err: %v", server, user, err)) return 0 } if res.Status != gomemcached.SUCCESS { atomic.AddUint64(&d.stats.TotWorkerAuthFail, 1) d.receiver.OnError(&AuthFailError{ServerURL: server, User: user}) return 0 } if adminCred { atomic.AddUint64(&d.stats.TotWorkerAuthOk, 1) _, err = client.SelectBucket(d.bucketName) if err != nil { atomic.AddUint64(&d.stats.TotWorkerSelBktFail, 1) d.receiver.OnError(fmt.Errorf("worker select bucket err: %v", err)) return 0 } atomic.AddUint64(&d.stats.TotWorkerSelBktOk, 1) } } } uprOpenName := d.options.Name if uprOpenName == "" { uprOpenName = fmt.Sprintf("cbdatasource-%x", rand.Int63()) } err = UPROpen(client, uprOpenName, d.options.FeedBufferSizeBytes) if err != nil { atomic.AddUint64(&d.stats.TotWorkerUPROpenErr, 1) d.receiver.OnError(err) return 0 } atomic.AddUint64(&d.stats.TotWorkerUPROpenOk, 1) ackBytes := uint32(d.options.FeedBufferAckThreshold * float32(d.options.FeedBufferSizeBytes)) sendCh := make(chan *gomemcached.MCRequest, 1) sendEndCh := make(chan struct{}) recvEndCh := make(chan struct{}) cleanup := func(progress int, err error) int { if err != nil { d.receiver.OnError(err) } go func() { <-recvEndCh close(sendCh) }() return progress } currVBuckets := make(map[uint16]*VBucketState) currVBucketsMutex := sync.Mutex{} // Protects currVBuckets. go func() { // Sender goroutine. defer close(sendEndCh) atomic.AddUint64(&d.stats.TotWorkerTransmitStart, 1) for msg := range sendCh { atomic.AddUint64(&d.stats.TotWorkerTransmit, 1) err := client.Transmit(msg) if err != nil { atomic.AddUint64(&d.stats.TotWorkerTransmitErr, 1) d.receiver.OnError(fmt.Errorf("client.Transmit, err: %v", err)) return } atomic.AddUint64(&d.stats.TotWorkerTransmitOk, 1) } atomic.AddUint64(&d.stats.TotWorkerTransmitDone, 1) }() go func() { // Receiver goroutine. defer close(recvEndCh) atomic.AddUint64(&d.stats.TotWorkerReceiveStart, 1) var hdr [gomemcached.HDR_LEN]byte var pkt gomemcached.MCRequest var res gomemcached.MCResponse // Track received bytes in case we need to buffer-ack. recvBytesTotal := uint32(0) conn := client.Hijack() for { // TODO: memory allocation here. atomic.AddUint64(&d.stats.TotWorkerReceive, 1) _, err := pkt.Receive(conn, hdr[:]) if err != nil { atomic.AddUint64(&d.stats.TotWorkerReceiveErr, 1) d.receiver.OnError(fmt.Errorf("pkt.Receive, err: %v", err)) return } atomic.AddUint64(&d.stats.TotWorkerReceiveOk, 1) if pkt.Opcode == gomemcached.UPR_MUTATION || pkt.Opcode == gomemcached.UPR_DELETION || pkt.Opcode == gomemcached.UPR_EXPIRATION { atomic.AddUint64(&d.stats.TotUPRDataChange, 1) vbucketID := pkt.VBucket currVBucketsMutex.Lock() vbucketState := currVBuckets[vbucketID] if vbucketState == nil || vbucketState.State != "running" { currVBucketsMutex.Unlock() atomic.AddUint64(&d.stats.TotUPRDataChangeStateErr, 1) d.receiver.OnError(fmt.Errorf("error: DataChange,"+ " wrong vbucketState: %#v, err: %v", vbucketState, err)) return } if !vbucketState.SnapSaved { // NOTE: Following the ep-engine's approach, we // wait to persist SnapStart/SnapEnd until we see // the first mutation/deletion in the new snapshot // range. That reduces a race window where if we // kill and restart this process right now after a // setVBucketMetaData() and before the next, // first-mutation-in-snapshot, then a restarted // stream-req using this just-saved // SnapStart/SnapEnd might have a lastSeq number < // SnapStart, where Couchbase Server will respond // to the stream-req with an ERANGE error code. v, _, err := d.getVBucketMetaData(vbucketID) if err != nil || v == nil { currVBucketsMutex.Unlock() d.receiver.OnError(fmt.Errorf("error: DataChange,"+ " getVBucketMetaData, vbucketID: %d, err: %v", vbucketID, err)) return } v.SnapStart = vbucketState.SnapStart v.SnapEnd = vbucketState.SnapEnd err = d.setVBucketMetaData(vbucketID, v) if err != nil { currVBucketsMutex.Unlock() d.receiver.OnError(fmt.Errorf("error: DataChange,"+ " getVBucketMetaData, vbucketID: %d, err: %v", vbucketID, err)) return } vbucketState.SnapSaved = true } currVBucketsMutex.Unlock() seq := binary.BigEndian.Uint64(pkt.Extras[:8]) if pkt.Opcode == gomemcached.UPR_MUTATION { atomic.AddUint64(&d.stats.TotUPRDataChangeMutation, 1) err = d.receiver.DataUpdate(vbucketID, pkt.Key, seq, &pkt) } else { if pkt.Opcode == gomemcached.UPR_DELETION { atomic.AddUint64(&d.stats.TotUPRDataChangeDeletion, 1) } else { atomic.AddUint64(&d.stats.TotUPRDataChangeExpiration, 1) } err = d.receiver.DataDelete(vbucketID, pkt.Key, seq, &pkt) } if err != nil { atomic.AddUint64(&d.stats.TotUPRDataChangeErr, 1) d.receiver.OnError(fmt.Errorf("error: DataChange, err: %v", err)) return } atomic.AddUint64(&d.stats.TotUPRDataChangeOk, 1) } else { res.Opcode = pkt.Opcode res.Opaque = pkt.Opaque res.Status = gomemcached.Status(pkt.VBucket) res.Extras = pkt.Extras res.Cas = pkt.Cas res.Key = pkt.Key res.Body = pkt.Body atomic.AddUint64(&d.stats.TotWorkerHandleRecv, 1) currVBucketsMutex.Lock() err := d.handleRecv(sendCh, currVBuckets, &res) currVBucketsMutex.Unlock() if err != nil { atomic.AddUint64(&d.stats.TotWorkerHandleRecvErr, 1) d.receiver.OnError(fmt.Errorf("error: HandleRecv, err: %v", err)) return } atomic.AddUint64(&d.stats.TotWorkerHandleRecvOk, 1) } recvBytesTotal += uint32(gomemcached.HDR_LEN) + uint32(len(pkt.Key)+len(pkt.Extras)+len(pkt.Body)) if ackBytes > 0 && recvBytesTotal > ackBytes { atomic.AddUint64(&d.stats.TotUPRBufferAck, 1) ack := &gomemcached.MCRequest{Opcode: gomemcached.UPR_BUFFERACK} ack.Extras = make([]byte, 4) // TODO: Memory mgmt. binary.BigEndian.PutUint32(ack.Extras, uint32(recvBytesTotal)) sendCh <- ack recvBytesTotal = 0 } } }() atomic.AddUint64(&d.stats.TotWorkerBodyKick, 1) d.Kick("new-worker") for { select { case <-sendEndCh: atomic.AddUint64(&d.stats.TotWorkerSendEndCh, 1) return cleanup(0, nil) case <-recvEndCh: // If we lost a connection, then maybe a node was rebalanced out, // or failed over, so ask for a cluster refresh just in case. d.Kick("recvEndCh") atomic.AddUint64(&d.stats.TotWorkerRecvEndCh, 1) return cleanup(0, nil) case wantVBucketIDs, alive := <-workerCh: atomic.AddUint64(&d.stats.TotRefreshWorker, 1) if !alive { atomic.AddUint64(&d.stats.TotRefreshWorkerDone, 1) return cleanup(-1, nil) // We've been asked to shutdown. } currVBucketsMutex.Lock() err := d.refreshWorker(sendCh, currVBuckets, wantVBucketIDs) currVBucketsMutex.Unlock() if err != nil { return cleanup(0, err) } atomic.AddUint64(&d.stats.TotRefreshWorkerOk, 1) } } return cleanup(-1, nil) // Unreached. }
// constants used for memcached protocol const ( uprOPEN = mcd.CommandCode(0x50) // Open a upr connection with `name` uprAddSTREAM = mcd.CommandCode(0x51) // Sent by ebucketmigrator to upr consumer uprCloseSTREAM = mcd.CommandCode(0x52) // Sent by ebucketmigrator to upr consumer uprFailoverLOG = mcd.CommandCode(0x54) // Request all known failover ids for restart uprStreamREQ = mcd.CommandCode(0x53) // Stream request from consumer to producer uprStreamEND = mcd.CommandCode(0x55) // Sent by producer when it is going to end stream uprSnapshotM = mcd.CommandCode(0x56) // Sent by producer for a new snapshot uprMUTATION = mcd.CommandCode(0x57) // Notifies SET/ADD/REPLACE/etc. on the server uprDELETION = mcd.CommandCode(0x58) // Notifies DELETE on the server uprEXPIRATION = mcd.CommandCode(0x59) // Notifies key expiration uprFLUSH = mcd.CommandCode(0x5a) // Notifies vbucket flush ) const ( rollBack = mcd.Status(0x23) ) // FailoverLog is a slice of 2 element array, containing a list of, // [[vuuid, sequence-no], [vuuid, sequence-no] ...] type FailoverLog [][2]uint64 // UprStream will maintain stream information per vbucket type UprStream struct { Vbucket uint16 // vbucket id Vuuid uint64 // vbucket uuid Opaque uint32 // messages from producer to this stream have same value Highseq uint64 // to be supplied by the application Startseq uint64 // to be supplied by the application Endseq uint64 // to be supplied by the application Flog FailoverLog
func (feed *UprFeed) runFeed(ch chan *UprEvent) { defer close(ch) var headerBuf [gomemcached.HDR_LEN]byte var pkt gomemcached.MCRequest var event *UprEvent mc := feed.conn.Hijack() uprStats := &feed.stats loop: for { sendAck := false bytes, err := pkt.Receive(mc, headerBuf[:]) if err != nil { ul.LogError("", "", "Error in receive %s", err.Error()) feed.Error = err // send all the stream close messages to the client feed.doStreamClose(ch) break loop } else { event = nil res := &gomemcached.MCResponse{ Opcode: pkt.Opcode, Cas: pkt.Cas, Opaque: pkt.Opaque, Status: gomemcached.Status(pkt.VBucket), Extras: pkt.Extras, Key: pkt.Key, Body: pkt.Body, } vb := vbOpaque(pkt.Opaque) uprStats.TotalBytes = uint64(bytes) feed.mu.RLock() stream := feed.vbstreams[vb] feed.mu.RUnlock() switch pkt.Opcode { case gomemcached.UPR_STREAMREQ: if stream == nil { ul.LogError("", "", "Stream not found for vb %d: %#v", vb, pkt) break loop } status, rb, flog, err := handleStreamRequest(res) if status == gomemcached.ROLLBACK { event = makeUprEvent(pkt, stream) // rollback stream msg := "UPR_STREAMREQ with rollback %d for vb %d Failed: %v" ul.LogError("", "", msg, rb, vb, err) // delete the stream from the vbmap for the feed feed.mu.Lock() delete(feed.vbstreams, vb) feed.mu.Unlock() } else if status == gomemcached.SUCCESS { event = makeUprEvent(pkt, stream) event.Seqno = stream.StartSeq event.FailoverLog = flog stream.connected = true ul.LogInfo("", "", "UPR_STREAMREQ for vb %d successful", vb) } else if err != nil { msg := "UPR_STREAMREQ for vbucket %d erro %s" ul.LogError("", "", msg, vb, err.Error()) event = &UprEvent{ Opcode: gomemcached.UPR_STREAMREQ, Status: status, VBucket: vb, Error: err, } } case gomemcached.UPR_MUTATION, gomemcached.UPR_DELETION, gomemcached.UPR_EXPIRATION: if stream == nil { ul.LogError("", "", "Stream not found for vb %d: %#v", vb, pkt) break loop } event = makeUprEvent(pkt, stream) uprStats.TotalMutation++ sendAck = true case gomemcached.UPR_STREAMEND: if stream == nil { ul.LogError("", "", "Stream not found for vb %d: %#v", vb, pkt) break loop } //stream has ended event = makeUprEvent(pkt, stream) ul.LogInfo("", "", "Stream Ended for vb %d", vb) sendAck = true feed.mu.Lock() delete(feed.vbstreams, vb) feed.mu.Unlock() case gomemcached.UPR_SNAPSHOT: if stream == nil { ul.LogError("", "", "Stream not found for vb %d: %#v", vb, pkt) break loop } // snapshot marker event = makeUprEvent(pkt, stream) event.SnapstartSeq = binary.BigEndian.Uint64(pkt.Extras[0:8]) event.SnapendSeq = binary.BigEndian.Uint64(pkt.Extras[8:16]) event.SnapshotType = binary.BigEndian.Uint32(pkt.Extras[16:20]) uprStats.TotalSnapShot++ sendAck = true case gomemcached.UPR_FLUSH: if stream == nil { ul.LogError("", "", "Stream not found for vb %d: %#v", vb, pkt) break loop } // special processing for flush ? event = makeUprEvent(pkt, stream) case gomemcached.UPR_CLOSESTREAM: if stream == nil { ul.LogError("", "", "Stream not found for vb %d: %#v", vb, pkt) break loop } event = makeUprEvent(pkt, stream) event.Opcode = gomemcached.UPR_STREAMEND // opcode re-write !! msg := "Stream Closed for vb %d StreamEnd simulated" ul.LogInfo("", "", msg, vb) sendAck = true feed.mu.Lock() delete(feed.vbstreams, vb) feed.mu.Unlock() case gomemcached.UPR_ADDSTREAM: ul.LogWarn("", "", "Opcode %v not implemented", pkt.Opcode) case gomemcached.UPR_CONTROL, gomemcached.UPR_BUFFERACK: if res.Status != gomemcached.SUCCESS { msg := "Opcode %v received status %d" ul.LogWarn("", "", msg, pkt.Opcode.String(), res.Status) } case gomemcached.UPR_NOOP: // send a NOOP back noop := &gomemcached.MCRequest{ Opcode: gomemcached.UPR_NOOP, } feed.transmitCh <- noop default: msg := "Recived an unknown response for vbucket %d" ul.LogError("", "", msg, vb) } } if event != nil { select { case ch <- event: case <-feed.closer: break loop } feed.mu.RLock() l := len(feed.vbstreams) feed.mu.RUnlock() if event.Opcode == gomemcached.UPR_CLOSESTREAM && l == 0 { ul.LogInfo("", "", "No more streams") break loop } } needToSend, sendSize := feed.SendBufferAck(sendAck, uint32(bytes)) if needToSend { bufferAck := &gomemcached.MCRequest{ Opcode: gomemcached.UPR_BUFFERACK, } bufferAck.Extras = make([]byte, 4) binary.BigEndian.PutUint32(bufferAck.Extras[:4], uint32(sendSize)) feed.transmitCh <- bufferAck uprStats.TotalBufferAckSent++ } } feed.transmitCl <- true }