From ddfe969d6cbc8d23326cb9a3ca9a265d4e9d3e45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= <fred@miniflux.net>
Date: Sun, 7 Oct 2018 12:50:59 -0700
Subject: [PATCH] Improve Fever API performances when marking a feed or group
 as read

---
 fever/fever.go   | 64 ++++++++++++++----------------------------------
 storage/entry.go | 45 +++++++++++++++++++++++++++++++++-
 2 files changed, 62 insertions(+), 47 deletions(-)

diff --git a/fever/fever.go b/fever/fever.go
index b754a89b..b7c8bc8d 100644
--- a/fever/fever.go
+++ b/fever/fever.go
@@ -530,81 +530,53 @@ func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
 }
 
 /*
-	mark=? where ? is replaced with feed or group
+	mark=feed
 	as=read
 	id=? where ? is replaced with the id of the feed or group to modify
 	before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
 */
 func (c *Controller) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
-	logger.Debug("[Fever] Receiving mark=feed call for userID=%d", userID)
-
 	feedID := request.FormInt64Value(r, "id")
-	if feedID <= 0 {
-		return
-	}
+	before := time.Unix(request.FormInt64Value(r, "before"), 0)
 
-	builder := c.store.NewEntryQueryBuilder(userID)
-	builder.WithStatus(model.EntryStatusUnread)
-	builder.WithFeedID(feedID)
-
-	before := request.FormInt64Value(r, "before")
-	if before > 0 {
-		t := time.Unix(before, 0)
-		builder.BeforeDate(t)
-	}
+	logger.Debug("[Fever] mark=feed, userID=%d, feedID=%d, before=%v", userID, feedID, before)
 
-	entryIDs, err := builder.GetEntryIDs()
-	if err != nil {
-		json.ServerError(w, err)
+	if feedID <= 0 {
 		return
 	}
 
-	err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
-	if err != nil {
-		json.ServerError(w, err)
-		return
-	}
+	go func() {
+		if err := c.store.MarkFeedAsRead(userID, feedID, before); err != nil {
+			logger.Error("[Fever] MarkFeedAsRead failed: %v", err)
+		}
+	}()
 
 	json.OK(w, r, newBaseResponse())
 }
 
 /*
-	mark=? where ? is replaced with feed or group
+	mark=group
 	as=read
 	id=? where ? is replaced with the id of the feed or group to modify
 	before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
 */
 func (c *Controller) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
-	logger.Debug("[Fever] Receiving mark=group call for userID=%d", userID)
-
 	groupID := request.FormInt64Value(r, "id")
-	if groupID < 0 {
-		return
-	}
+	before := time.Unix(request.FormInt64Value(r, "before"), 0)
 
-	builder := c.store.NewEntryQueryBuilder(userID)
-	builder.WithStatus(model.EntryStatusUnread)
-	builder.WithCategoryID(groupID)
-
-	before := request.FormInt64Value(r, "before")
-	if before > 0 {
-		t := time.Unix(before, 0)
-		builder.BeforeDate(t)
-	}
+	logger.Debug("[Fever] mark=group, userID=%d, groupID=%d, before=%v", userID, groupID, before)
 
-	entryIDs, err := builder.GetEntryIDs()
-	if err != nil {
-		json.ServerError(w, err)
+	if groupID < 0 {
 		return
 	}
 
-	err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
-	if err != nil {
-		json.ServerError(w, err)
-		return
-	}
+	go func() {
+		if err := c.store.MarkCategoryAsRead(userID, groupID, before); err != nil {
+			logger.Error("[Fever] MarkCategoryAsRead failed: %v", err)
+		}
+	}()
 
 	json.OK(w, r, newBaseResponse())
 }
diff --git a/storage/entry.go b/storage/entry.go
index 9fd8a53f..ff081d47 100644
--- a/storage/entry.go
+++ b/storage/entry.go
@@ -256,7 +256,7 @@ func (s *Storage) FlushHistory(userID int64) error {
 	return nil
 }
 
-// MarkAllAsRead set all entries with the status "unread" to "read".
+// MarkAllAsRead updates all user entries to the read status.
 func (s *Storage) MarkAllAsRead(userID int64) error {
 	defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:MarkAllAsRead] userID=%d", userID))
 
@@ -269,6 +269,49 @@ func (s *Storage) MarkAllAsRead(userID int64) error {
 	return nil
 }
 
+// MarkFeedAsRead updates all feed entries to the read status.
+func (s *Storage) MarkFeedAsRead(userID, feedID int64, before time.Time) error {
+	defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:MarkFeedAsRead] userID=%d, feedID=%d, before=%v", userID, feedID, before))
+
+	query := `
+		UPDATE entries
+		SET status=$1
+		WHERE user_id=$2 AND feed_id=$3 AND status=$4 AND published_at < $5
+	`
+
+	result, err := s.db.Exec(query, model.EntryStatusRead, userID, feedID, model.EntryStatusUnread, before)
+	if err != nil {
+		return fmt.Errorf("unable to mark feed entries as read: %v", err)
+	}
+
+	count, _ := result.RowsAffected()
+	logger.Debug("[Storage:MarkFeedAsRead] %d items marked as read", count)
+
+	return nil
+}
+
+// MarkCategoryAsRead updates all category entries to the read status.
+func (s *Storage) MarkCategoryAsRead(userID, categoryID int64, before time.Time) error {
+	defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:MarkCategoryAsRead] userID=%d, categoryID=%d, before=%v", userID, categoryID, before))
+
+	query := `
+		UPDATE entries
+		SET status=$1
+		WHERE
+		user_id=$2 AND status=$3 AND published_at < $4 AND feed_id IN (SELECT id FROM feeds WHERE user_id=$2 AND category_id=$5)
+	`
+
+	result, err := s.db.Exec(query, model.EntryStatusRead, userID, model.EntryStatusUnread, before, categoryID)
+	if err != nil {
+		return fmt.Errorf("unable to mark category entries as read: %v", err)
+	}
+
+	count, _ := result.RowsAffected()
+	logger.Debug("[Storage:MarkCategoryAsRead] %d items marked as read", count)
+
+	return nil
+}
+
 // EntryURLExists returns true if an entry with this URL already exists.
 func (s *Storage) EntryURLExists(userID int64, entryURL string) bool {
 	var result int
-- 
GitLab