// This is the core functionality of both the HTTP and WebSocket-based continuous change feed. // It defers to a callback function 'send()' to actually send the changes to the client. // It will call send(nil) to notify that it's caught up and waiting for new changes, or as // a periodic heartbeat while waiting. func (h *handler) generateContinuousChanges(inChannels base.Set, options db.ChangesOptions, send func([]*db.ChangeEntry) error) (error, bool) { // Set up heartbeat/timeout var timeoutInterval time.Duration var timer *time.Timer var heartbeat <-chan time.Time if options.HeartbeatMs > 0 { ticker := time.NewTicker(time.Duration(options.HeartbeatMs) * time.Millisecond) defer ticker.Stop() heartbeat = ticker.C } else if options.TimeoutMs > 0 { timeoutInterval = time.Duration(options.TimeoutMs) * time.Millisecond defer func() { if timer != nil { timer.Stop() } }() } options.Wait = true // we want the feed channel to wait for changes options.Continuous = true // and to keep sending changes indefinitely var lastSeq db.SequenceID var feed <-chan *db.ChangeEntry var timeout <-chan time.Time var err error var closeNotify <-chan bool cn, ok := h.response.(http.CloseNotifier) if ok { closeNotify = cn.CloseNotify() } else { base.LogTo("Changes", "continuous changes cannot get Close Notifier from ResponseWriter") } forceClose := false loop: for { if feed == nil { // Refresh the feed of all current changes: if lastSeq.IsNonZero() { // start after end of last feed options.Since = lastSeq } if h.db.IsClosed() { forceClose = true break loop } feed, err = h.db.MultiChangesFeed(inChannels, options) if err != nil || feed == nil { return err, forceClose } } if timeoutInterval > 0 && timer == nil { // Timeout resets after every change is sent timer = time.NewTimer(timeoutInterval) timeout = timer.C } // Wait for either a new change, a heartbeat, or a timeout: select { case entry, ok := <-feed: if !ok { feed = nil } else if entry == nil { err = send(nil) } else if entry.Err != nil { break loop // error returned by feed - end changes } else { entries := []*db.ChangeEntry{entry} waiting := false // Batch up as many entries as we can without waiting: collect: for len(entries) < 20 { select { case entry, ok = <-feed: if !ok { feed = nil break collect } else if entry == nil { waiting = true break collect } else if entry.Err != nil { break loop // error returned by feed - end changes } entries = append(entries, entry) default: break collect } } base.LogTo("Changes", "sending %d change(s)", len(entries)) err = send(entries) if err == nil && waiting { err = send(nil) } lastSeq = entries[len(entries)-1].Seq if options.Limit > 0 { if len(entries) >= options.Limit { forceClose = true break loop } options.Limit -= len(entries) } } // Reset the timeout after sending an entry: if timer != nil { timer.Stop() timer = nil } case <-heartbeat: err = send(nil) base.LogTo("Heartbeat", "heartbeat written to _changes feed for request received %s", h.currentEffectiveUserName()) case <-timeout: forceClose = true break loop case <-closeNotify: base.LogTo("Changes", "Connection lost from client: %v", h.currentEffectiveUserName()) forceClose = true break loop case <-h.db.ExitChanges: forceClose = true break loop } if err != nil { h.logStatus(http.StatusOK, fmt.Sprintf("Write error: %v", err)) return nil, forceClose // error is probably because the client closed the connection } } h.logStatus(http.StatusOK, "OK (continuous feed closed)") return nil, forceClose }
// This is the core functionality of both the HTTP and WebSocket-based continuous change feed. // It defers to a callback function 'send()' to actually send the changes to the client. // It will call send(nil) to notify that it's caught up and waiting for new changes, or as // a periodic heartbeat while waiting. func (h *handler) generateContinuousChanges(inChannels base.Set, options db.ChangesOptions, send func([]*db.ChangeEntry) error) error { // Set up heartbeat/timeout var timeoutInterval time.Duration var timer *time.Timer var heartbeat <-chan time.Time if options.HeartbeatMs > 0 { ticker := time.NewTicker(time.Duration(options.HeartbeatMs) * time.Millisecond) defer ticker.Stop() heartbeat = ticker.C } else if options.TimeoutMs > 0 { timeoutInterval = time.Duration(options.TimeoutMs) * time.Millisecond defer func() { if timer != nil { timer.Stop() } }() } options.Wait = true // we want the feed channel to wait for changes options.Continuous = true // and to keep sending changes indefinitely var lastSeq db.SequenceID var feed <-chan *db.ChangeEntry var timeout <-chan time.Time var err error loop: for { if feed == nil { // Refresh the feed of all current changes: if lastSeq.Seq > 0 { // start after end of last feed options.Since = lastSeq } if h.db.IsClosed() { break loop } feed, err = h.db.MultiChangesFeed(inChannels, options) if err != nil || feed == nil { return err } } if timeoutInterval > 0 && timer == nil { // Timeout resets after every change is sent timer = time.NewTimer(timeoutInterval) timeout = timer.C } // Wait for either a new change, a heartbeat, or a timeout: select { case entry, ok := <-feed: if !ok { feed = nil } else if entry == nil { err = send(nil) } else { entries := []*db.ChangeEntry{entry} waiting := false // Batch up as many entries as we can without waiting: collect: for len(entries) < 20 { select { case entry, ok = <-feed: if !ok { feed = nil break collect } else if entry == nil { waiting = true break collect } entries = append(entries, entry) default: break collect } } base.LogTo("Changes", "sending %d change(s)", len(entries)) err = send(entries) if err == nil && waiting { err = send(nil) } lastSeq = entries[len(entries)-1].Seq if options.Limit > 0 { if len(entries) >= options.Limit { break loop } options.Limit -= len(entries) } } // Reset the timeout after sending an entry: if timer != nil { timer.Stop() timer = nil } case <-heartbeat: err = send(nil) case <-timeout: break loop } if err != nil { h.logStatus(http.StatusOK, fmt.Sprintf("Write error: %v", err)) return nil // error is probably because the client closed the connection } } h.logStatus(http.StatusOK, "OK (continuous feed closed)") return nil }