func (h *handler) handleContinuousChanges(channels []string, options db.ChangesOptions) error { var timeout <-chan time.Time var heartbeat <-chan time.Time if ms := h.getIntQuery("heartbeat", 0); ms > 0 { ticker := time.NewTicker(time.Duration(ms) * time.Millisecond) defer ticker.Stop() heartbeat = ticker.C } else if ms := h.getIntQuery("timeout", 60); ms > 0 { timer := time.NewTimer(time.Duration(ms) * time.Millisecond) defer timer.Stop() timeout = timer.C } options.Wait = true // we want the feed channel to wait for changes var feed <-chan *db.ChangeEntry var err error loop: for { if feed == nil { // Refresh the feed of all current changes: feed, err = h.db.MultiChangesFeed(channels, options) if err != nil || feed == nil { return err } } // Wait for either a new change, or a heartbeat: select { case entry := <-feed: if entry == nil { feed = nil } else { str, _ := json.Marshal(entry) if LogRequestsVerbose { log.Printf("\tchange: %s", str) } err = h.writeln(str) options.Since = entry.Seq // so next call to ChangesFeed will start from end if options.Limit > 0 { options.Limit-- if options.Limit == 0 { break loop } } } case <-heartbeat: err = h.writeln([]byte{}) case <-timeout: break loop } if err != nil { return nil // error is probably because the client closed the connection } } return nil }
func (h *handler) handleChanges() error { // http://wiki.apache.org/couchdb/HTTP_database_API#Changes var options db.ChangesOptions options.Since = h.getIntQuery("since", 0) options.Limit = int(h.getIntQuery("limit", 0)) options.Conflicts = (h.getQuery("style") == "all_docs") options.IncludeDocs = (h.getBoolQuery("include_docs")) // Get the channels as parameters to an imaginary "bychannel" filter. // The default is all channels the user can access. userChannels := h.user.Channels filter := h.getQuery("filter") if filter != "" { if filter != "sync_gateway/bychannel" { return &base.HTTPError{http.StatusBadRequest, "Unknown filter; try sync_gateway/bychannel"} } channelsParam := h.getQuery("channels") if channelsParam == "" { return &base.HTTPError{http.StatusBadRequest, "Missing 'channels' filter parameter"} } userChannels = channels.SimplifyChannels(strings.Split(channelsParam, ","), true) userChannels = h.user.ExpandWildCardChannel(userChannels) if err := h.user.AuthorizeAllChannels(userChannels); err != nil { return err } } switch h.getQuery("feed") { case "continuous": return h.handleContinuousChanges(userChannels, options) case "longpoll": options.Wait = true } return h.handleSimpleChanges(userChannels, options) }
// Top-level handler for _changes feed requests. func (h *handler) handleChanges() error { // http://wiki.apache.org/couchdb/HTTP_database_API#Changes // http://docs.couchdb.org/en/latest/api/database/changes.html var options db.ChangesOptions options.Since = channels.TimedSetFromString(h.getQuery("since")) options.Limit = int(h.getIntQuery("limit", 0)) options.Conflicts = (h.getQuery("style") == "all_docs") options.IncludeDocs = (h.getBoolQuery("include_docs")) options.Terminator = make(chan bool) defer close(options.Terminator) // Get the channels as parameters to an imaginary "bychannel" filter. // The default is all channels the user can access. userChannels := channels.SetOf("*") filter := h.getQuery("filter") if filter != "" { if filter != "sync_gateway/bychannel" { return base.HTTPErrorf(http.StatusBadRequest, "Unknown filter; try sync_gateway/bychannel") } channelsParam := h.getQuery("channels") if channelsParam == "" { return base.HTTPErrorf(http.StatusBadRequest, "Missing 'channels' filter parameter") } var err error userChannels, err = channels.SetFromArray(strings.Split(channelsParam, ","), channels.ExpandStar) if err != nil { return err } if len(userChannels) == 0 { return base.HTTPErrorf(http.StatusBadRequest, "Empty channel list") } } h.db.ChangesClientStats.Increment() defer h.db.ChangesClientStats.Decrement() switch h.getQuery("feed") { case "normal", "": return h.sendSimpleChanges(userChannels, options) case "longpoll": options.Wait = true return h.sendSimpleChanges(userChannels, options) case "continuous": return h.sendContinuousChangesByHTTP(userChannels, options) case "websocket": return h.sendContinuousChangesByWebSocket(userChannels, options) default: return base.HTTPErrorf(http.StatusBadRequest, "Unknown feed type") } }
func (h *handler) sendContinuousChangesByWebSocket(inChannels base.Set, options db.ChangesOptions) error { handler := func(conn *websocket.Conn) { h.logStatus(101, "Upgraded to WebSocket protocol") defer func() { conn.Close() base.LogTo("HTTP+", "#%03d: --> WebSocket closed", h.serialNumber) }() // Read changes-feed options from an initial incoming WebSocket message in JSON format: if msg, err := readWebSocketMessage(conn); err != nil { return } else { var channelNames []string var err error if _, options, _, channelNames, err = readChangesOptionsFromJSON(msg); err != nil { return } if channelNames != nil { inChannels, _ = channels.SetFromArray(channelNames, channels.ExpandStar) } } options.Terminator = make(chan bool) defer close(options.Terminator) caughtUp := false h.generateContinuousChanges(inChannels, options, func(changes []*db.ChangeEntry) error { var data []byte if changes != nil { data, _ = json.Marshal(changes) } else if !caughtUp { caughtUp = true data, _ = json.Marshal([]*db.ChangeEntry{}) } else { data = []byte{} } _, err := conn.Write(data) return err }) } server := websocket.Server{ Handshake: func(*websocket.Config, *http.Request) error { return nil }, Handler: handler, } server.ServeHTTP(h.response, h.rq) return nil }
func (h *handler) handleChanges() error { // http://wiki.apache.org/couchdb/HTTP_database_API#Changes var options db.ChangesOptions options.Since = channels.TimedSetFromString(h.getQuery("since")) options.Limit = int(h.getIntQuery("limit", 0)) options.Conflicts = (h.getQuery("style") == "all_docs") options.IncludeDocs = (h.getBoolQuery("include_docs")) // Get the channels as parameters to an imaginary "bychannel" filter. // The default is all channels the user can access. userChannels := channels.SetOf("*") filter := h.getQuery("filter") if filter != "" { if filter != "sync_gateway/bychannel" { return &base.HTTPError{http.StatusBadRequest, "Unknown filter; try sync_gateway/bychannel"} } channelsParam := h.getQuery("channels") if channelsParam == "" { return &base.HTTPError{http.StatusBadRequest, "Missing 'channels' filter parameter"} } var err error userChannels, err = channels.SetFromArray(strings.Split(channelsParam, ","), channels.ExpandStar) if err != nil { return err } if len(userChannels) == 0 { return &base.HTTPError{http.StatusBadRequest, "Empty channel list"} } } switch h.getQuery("feed") { case "continuous": return h.handleContinuousChanges(userChannels, options) case "longpoll": options.Wait = true } return h.handleSimpleChanges(userChannels, options) }
// 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 ms := h.getRestrictedIntQuery("heartbeat", 0, kMinHeartbeatMS, 0); ms > 0 { ticker := time.NewTicker(time.Duration(ms) * time.Millisecond) defer ticker.Stop() heartbeat = ticker.C } else if ms := h.getRestrictedIntQuery("timeout", kDefaultTimeoutMS, 0, kMaxTimeoutMS); ms > 0 { timeoutInterval = time.Duration(ms) * time.Millisecond defer func() { if timer != nil { timer.Stop() } }() } options.Wait = true // we want the feed channel to wait for changes var lastSeqID string 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 lastSeqID != "" { // start after end of last feed options.Since = channels.TimedSetFromString(lastSeqID) } 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) } lastSeqID = 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 }
func (h *handler) handleContinuousChanges(inChannels base.Set, options db.ChangesOptions) error { var timeoutInterval time.Duration var timer *time.Timer var heartbeat <-chan time.Time if ms := h.getIntQuery("heartbeat", 0); ms > 0 { ticker := time.NewTicker(time.Duration(ms) * time.Millisecond) defer ticker.Stop() heartbeat = ticker.C } else if ms := h.getIntQuery("timeout", 60000); ms > 0 { timeoutInterval = time.Duration(ms) * time.Millisecond defer func() { if timer != nil { timer.Stop() } }() } options.Wait = true // we want the feed channel to wait for changes var lastSeqID string 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 lastSeqID != "" { // start after end of last feed options.Since = channels.TimedSetFromString(lastSeqID) } feed, err = h.db.MultiChangesFeed(inChannels, options) if err != nil || feed == nil { return err } } if timeoutInterval > 0 && timer == nil { timer = time.NewTimer(timeoutInterval) timeout = timer.C } // Wait for either a new change, a heartbeat, or a timeout: select { case entry := <-feed: if entry == nil { feed = nil } else { str, _ := json.Marshal(entry) base.LogTo("Changes", "send change: %s", str) err = h.writeln(str) lastSeqID = entry.Seq if options.Limit > 0 { options.Limit-- if options.Limit == 0 { break loop } } } // Reset the timeout after sending an entry: if timer != nil { timer.Stop() timer = nil } case <-heartbeat: err = h.writeln([]byte{}) case <-timeout: break loop } if err != nil { return nil // error is probably because the client closed the connection } } return nil }
// Top-level handler for _changes feed requests. Accepts GET or POST requests. func (h *handler) handleChanges() error { // http://wiki.apache.org/couchdb/HTTP_database_API#Changes // http://docs.couchdb.org/en/latest/api/database/changes.html restExpvars.Add("changesFeeds_total", 1) restExpvars.Add("changesFeeds_active", 1) defer restExpvars.Add("changesFeeds_active", -1) var feed string var options db.ChangesOptions var filter string var channelsArray []string if h.rq.Method == "GET" { // GET request has parameters in URL: feed = h.getQuery("feed") options.Since = channels.TimedSetFromString(h.getQuery("since")) options.Limit = int(h.getIntQuery("limit", 0)) options.Conflicts = (h.getQuery("style") == "all_docs") options.IncludeDocs = (h.getBoolQuery("include_docs")) filter = h.getQuery("filter") channelsParam := h.getQuery("channels") if channelsParam != "" { channelsArray = strings.Split(channelsParam, ",") } } else { // POST request has parameters in JSON body: body, err := h.readBody() if err != nil { return err } feed, options, filter, channelsArray, err = readChangesOptionsFromJSON(body) if err != nil { return err } } // Get the channels as parameters to an imaginary "bychannel" filter. // The default is all channels the user can access. userChannels := channels.SetOf("*") if filter != "" { if filter != "sync_gateway/bychannel" { return base.HTTPErrorf(http.StatusBadRequest, "Unknown filter; try sync_gateway/bychannel") } if channelsArray == nil { return base.HTTPErrorf(http.StatusBadRequest, "Missing 'channels' filter parameter") } var err error userChannels, err = channels.SetFromArray(channelsArray, channels.ExpandStar) if err != nil { return err } if len(userChannels) == 0 { return base.HTTPErrorf(http.StatusBadRequest, "Empty channel list") } } h.db.ChangesClientStats.Increment() defer h.db.ChangesClientStats.Decrement() options.Terminator = make(chan bool) defer close(options.Terminator) switch feed { case "normal", "": return h.sendSimpleChanges(userChannels, options) case "longpoll": options.Wait = true return h.sendSimpleChanges(userChannels, options) case "continuous": return h.sendContinuousChangesByHTTP(userChannels, options) case "websocket": return h.sendContinuousChangesByWebSocket(userChannels, options) default: return base.HTTPErrorf(http.StatusBadRequest, "Unknown feed type") } }