func (r *FormationRepo) startListener() error { // TODO: get connection string from somewhere listenerEvent := func(ev pq.ListenerEventType, err error) { if err != nil { fmt.Println("LISTENER error:", err) } // TODO: handle errors } listener := pq.NewListener(r.db.DSN(), 10*time.Second, time.Minute, listenerEvent) if err := listener.Listen("formations"); err != nil { return err } go func() { for { select { case n := <-listener.Notify: ids := strings.SplitN(n.Extra, ":", 2) go r.publish(ids[0], ids[1]) case <-r.stopListener: listener.Close() return } } }() return nil }
// Listen creates a listener for the given channel, returning the listener // and the first connection error (nil on successful connection). func (db *DB) Listen(channel string, log log15.Logger) (*Listener, error) { l := &Listener{ Notify: make(chan *pq.Notification), firstErr: make(chan error, 1), log: log, } l.pqListener = pq.NewListener(db.DSN(), 10*time.Second, time.Minute, l.handleEvent) if err := l.pqListener.Listen(channel); err != nil { return nil, err } go l.listen() return l, <-l.firstErr }
func streamJobs(ctx context.Context, req *http.Request, w http.ResponseWriter, app *ct.App, repo *JobRepo) (err error) { var lastID int64 if req.Header.Get("Last-Event-Id") != "" { lastID, err = strconv.ParseInt(req.Header.Get("Last-Event-Id"), 10, 64) if err != nil { return ct.ValidationError{Field: "Last-Event-Id", Message: "is invalid"} } } var count int if req.FormValue("count") != "" { count, err = strconv.Atoi(req.FormValue("count")) if err != nil { return ct.ValidationError{Field: "count", Message: "is invalid"} } } ch := make(chan *ct.JobEvent) l, _ := ctxhelper.LoggerFromContext(ctx) s := sse.NewStream(w, ch, l) s.Serve() connected := make(chan struct{}) done := make(chan struct{}) listenEvent := func(ev pq.ListenerEventType, listenErr error) { switch ev { case pq.ListenerEventConnected: close(connected) case pq.ListenerEventDisconnected: if done != nil { close(done) done = nil } case pq.ListenerEventConnectionAttemptFailed: err = listenErr if done != nil { close(done) done = nil } } } listener := pq.NewListener(repo.db.DSN(), 10*time.Second, time.Minute, listenEvent) defer listener.Close() listener.Listen("job_events:" + postgres.FormatUUID(app.ID)) var currID int64 if lastID > 0 || count > 0 { events, err := repo.listEvents(app.ID, lastID, count) if err != nil { return err } // events are in ID DESC order, so iterate in reverse for i := len(events) - 1; i >= 0; i-- { e := events[i] ch <- e currID = e.ID } } select { case <-done: return case <-connected: } for { select { case <-s.Done: return case <-done: return case n := <-listener.Notify: id, err := strconv.ParseInt(n.Extra, 10, 64) if err != nil { return err } if id <= currID { continue } e, err := repo.getEvent(id) if err != nil { return err } ch <- e } } }
func streamJobs(req *http.Request, w http.ResponseWriter, app *ct.App, repo *JobRepo) (err error) { var lastID int64 if req.Header.Get("Last-Event-Id") != "" { lastID, err = strconv.ParseInt(req.Header.Get("Last-Event-Id"), 10, 64) if err != nil { return ct.ValidationError{Field: "Last-Event-Id", Message: "is invalid"} } } var count int if req.FormValue("count") != "" { count, err = strconv.Atoi(req.FormValue("count")) if err != nil { return ct.ValidationError{Field: "count", Message: "is invalid"} } } w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") sendKeepAlive := func() error { if _, err := w.Write([]byte(":\n")); err != nil { return err } w.(http.Flusher).Flush() return nil } if err = sendKeepAlive(); err != nil { return } sendJobEvent := func(e *ct.JobEvent) error { if _, err := fmt.Fprintf(w, "id: %d\nevent: %s\ndata: ", e.ID, e.State); err != nil { return err } if err := json.NewEncoder(w).Encode(e); err != nil { return err } if _, err := w.Write([]byte("\n")); err != nil { return err } w.(http.Flusher).Flush() return nil } connected := make(chan struct{}) done := make(chan struct{}) listenEvent := func(ev pq.ListenerEventType, listenErr error) { switch ev { case pq.ListenerEventConnected: close(connected) case pq.ListenerEventDisconnected: close(done) case pq.ListenerEventConnectionAttemptFailed: err = listenErr close(done) } } listener := pq.NewListener(repo.db.DSN(), 10*time.Second, time.Minute, listenEvent) defer listener.Close() listener.Listen("job_events:" + formatUUID(app.ID)) var currID int64 if lastID > 0 || count > 0 { events, err := repo.listEvents(app.ID, lastID, count) if err != nil { return err } // events are in ID DESC order, so iterate in reverse for i := len(events) - 1; i >= 0; i-- { e := events[i] if err := sendJobEvent(e); err != nil { return err } currID = e.ID } } select { case <-done: return case <-connected: } closed := w.(http.CloseNotifier).CloseNotify() for { select { case <-done: return case <-closed: return case <-time.After(30 * time.Second): if err := sendKeepAlive(); err != nil { return err } case n := <-listener.Notify: id, err := strconv.ParseInt(n.Extra, 10, 64) if err != nil { return err } if id <= currID { continue } e, err := repo.getEvent(id) if err != nil { return err } if err = sendJobEvent(e); err != nil { return err } } } }