Browse Source

Working delete handler.

Frederic G. MARAND 2 years ago
parent
commit
0a7e2aec83

+ 40 - 3
back/services/redriver/redriver.go

@@ -5,7 +5,9 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log"
 	"strconv"
+	"strings"
 
 	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/service/sqs"
@@ -312,9 +314,44 @@ func (r *redriver) GetQueueItems(ctx context.Context, qName string) ([]Message,
 	return ms, err
 }
 
-func (r *redriver) DeleteItems(ctx context.Context, qName string, itemsIDs []ItemsKeys) error {
-	// TODO implement me
-	panic("implement me")
+func (r *redriver) DeleteItems(ctx context.Context, qName string, keys []ItemsKeys) error {
+	qui := &sqs.GetQueueUrlInput{QueueName: &qName}
+	qu, err := r.GetQueueUrl(ctx, qui)
+	if err != nil {
+		return fmt.Errorf("failed getting URL for queue %q: %w", qName, err)
+	}
+
+	entries := make([]types.DeleteMessageBatchRequestEntry, len(keys))
+	for i := 0; i < len(keys); i++ {
+		entries[i] = types.DeleteMessageBatchRequestEntry{
+			Id:            aws.String(keys[i].MessageID),
+			ReceiptHandle: aws.String(keys[i].ReceiptHandle),
+		}
+	}
+	dmi := sqs.DeleteMessageBatchInput{
+		Entries:  entries,
+		QueueUrl: qu.QueueUrl,
+	}
+	dmo, err := r.DeleteMessageBatch(ctx, &dmi)
+	if err != nil {
+		return fmt.Errorf("failed deleting %d items from queue %q: %v",
+			len(keys), qName, err)
+	}
+	if len(dmo.Failed) > 0 {
+		errs := make([]string, len(dmo.Failed))
+		for i, bree := range dmo.Failed {
+			source := "aws"
+			if bree.SenderFault {
+				source = "redriver"
+			}
+			errs[i] = fmt.Sprintf("ID: %s / Failure: %s = %q / Source: %s",
+				*bree.Id, *bree.Message, *bree.Code, source)
+		}
+		return fmt.Errorf("failed deleting %d items out of %d from queue %q: %s",
+			len(dmo.Failed), len(keys), qName, strings.Join(errs, "\n"))
+	}
+	log.Println(dmo)
+	return nil
 }
 
 func (r *redriver) Purge(ctx context.Context, qName string) error {

+ 15 - 14
back/web/confirm.go

@@ -103,11 +103,10 @@ func validateARN(in string) bool {
 	return arn.IsARN(in)
 }
 
-func parseIDs(u *url.URL, isValid IDValidator) []string {
+func parseIDs(v url.Values, isValid IDValidator) []string {
 	const prefix = "id-"
-	q := u.Query()
 	var ids []string
-	for k, vs := range q {
+	for k, vs := range v {
 		// Weed out bad keys, bad value lengths, and non-matching checkbox statuses.
 		if pos := strings.Index(k, prefix); pos == -1 || len(vs) != 1 || vs[0] != "on" {
 			continue
@@ -120,11 +119,11 @@ func parseIDs(u *url.URL, isValid IDValidator) []string {
 	return ids
 }
 
-func parseMessages(u *url.URL, ids []string) []redriver.Message {
+func parseMessages(v url.Values, ids []string) []redriver.Message {
 	m := make(map[int]redriver.Message, len(ids))
 	keyRx := regexp.MustCompile("^(id|mid|rh)-(.+)")
 	mOK := make(map[int]bool, len(ids))
-	for k, vs := range u.Query() {
+	for k, vs := range v {
 		ms := keyRx.FindStringSubmatch(k)
 		if len(ms) != 3 || len(vs) != 1 {
 			continue
@@ -159,25 +158,27 @@ func parseMessages(u *url.URL, ids []string) []redriver.Message {
 
 func makeConfirmHandler() gin.HandlerFunc {
 	return func(c *gin.Context) {
+		qName := c.Param("name")
 		u := c.Request.URL
+		sess := sessions.Default(c)
+		flashes := sess.Flashes()
+		defer func() {
+			_ = sess.Save()
+		}()
+
 		op := getOp(u)
-		// For real data, probably use validateUUID or validateARN.
-		ids := parseIDs(u, validateUint)
+		ids := parseIDs(u.Query(), validateUint)
 		var messages []redriver.Message
 		if len(ids) == 0 {
 			op = OpInvalid
 			messages = nil
 		} else {
-			messages = parseMessages(u, ids)
+			messages = parseMessages(u.Query(), ids)
 		}
 
-		data := confirms[op] // op is guaranteed to be valid by getOp().
-		qName := c.Param("name")
-
-		sess := sessions.Default(c)
-		flashes := sess.Flashes()
 		token := csrf.GetToken(c)
-		_ = sess.Save()
+
+		data := confirms[op] // op is guaranteed to be valid by getOp().
 
 		h := gin.H{
 			"action":      "/queue/" + qName + "/" + string(op),

+ 53 - 4
back/web/delete.go

@@ -1,8 +1,12 @@
 package web
 
 import (
+	"fmt"
+	"log"
 	"net/http"
+	"time"
 
+	"github.com/gin-contrib/sessions"
 	"github.com/gin-gonic/gin"
 
 	"code.osinet.fr/fgm/sqs_demo/back/services/redriver"
@@ -10,9 +14,54 @@ import (
 
 func makeDeleteConfirmHandler(rd redriver.Redriver) gin.HandlerFunc {
 	return func(c *gin.Context) {
-		c.JSON(http.StatusServiceUnavailable, gin.H{
-			"message": "deletion confirmation",
-			"error":   "not implemented",
-		})
+		ctx := c.Request.Context()
+		qName := c.Param("name")
+		redirect := "/queue/" + qName
+
+		sess := sessions.Default(c)
+		// Do not consume sess.Flashes(): this is a redirect-only handler, and they would be lost.
+		defer func() {
+			_ = sess.Save()
+			c.Redirect(http.StatusSeeOther, redirect)
+		}()
+
+		req := c.Request
+		if err := req.ParseForm(); err != nil {
+			log.Printf("Failed to parse deletion confirm form for queue %s: %v",
+				qName, err)
+			sess.AddFlash(fmt.Sprintf("Failed to parsed deletion confirmm form for queue %s",
+				qName))
+			return
+		}
+		ids := parseIDs(req.Form, validateUint)
+
+		var messages []redriver.Message
+		if len(ids) == 0 {
+			flash := fmt.Sprintf("Got no message to delete from queue %q", qName)
+			log.Print(flash)
+			sess.AddFlash(flash)
+			return
+		}
+		messages = parseMessages(req.Form, ids)
+
+		keys := make([]redriver.ItemsKeys, len(messages))
+		for i, msg := range messages {
+			keys[i] = redriver.ItemsKeys{
+				MessageID:     msg.MessageId,
+				ReceiptHandle: msg.ReceiptHandle,
+			}
+		}
+		t0 := time.Now()
+		err := rd.DeleteItems(ctx, qName, keys)
+		latency := time.Since(t0)
+		if err != nil {
+			log.Printf("failed deleting %d items from queue %q: %v",
+				len(keys), qName, err)
+			sess.AddFlash(fmt.Sprintf("Failed deleting %d items from queue %q",
+				len(keys), qName))
+			return
+		}
+
+		sess.AddFlash(fmt.Sprintf("Deleted %d items in %v", len(keys), latency))
 	}
 }

+ 1 - 2
back/web/queue.go

@@ -19,9 +19,8 @@ func makeQueueHandler(rd redriver.Redriver) gin.HandlerFunc {
 
 		sess := sessions.Default(c)
 		flashes := sess.Flashes()
-		sess.Clear()
 		sess.AddFlash(fmt.Sprintf("Previous info acquired at: %v", time.Now()))
-		sess.Save()
+		_ = sess.Save()
 
 		t0 := time.Now()
 		qi, err := rd.GetQueueInfo(ctx, qName)

+ 1 - 1
front/templates/confirm.gohtml

@@ -26,7 +26,7 @@
                         {{range $id, $message := .messages}}
                             <li>
                                 {{ $message.MessageId }}
-                                <input type="hidden" name="id-{{ $id }}" value=""/>
+                                <input type="hidden" name="id-{{ $id }}" value="on"/>
                                 <input type="hidden" name="mid-{{ $id }}" value="{{ $message.MessageId }}"/>
                                 <input type="hidden" name="rh-{{ $id }}" value="{{ $message.ReceiptHandle }}"/>
                             </li>

+ 5 - 2
front/templates/queue.gohtml

@@ -89,12 +89,15 @@
             <form method="get" id="msg-form" action="/queue/{{ .info.Name }}/confirm">
                 <div class="d-flex align-items-center justify-content-between">
                     <h2 class="my-5">Messages</h2>
-                    <button type="submit" form="msg-form" id="redrive" name="redrive" class="btn btn-outline-warning" disabled>
+                    <button type="submit" form="msg-form" id="redrive" name="redrive" class="btn btn-outline-warning bg-warning text-white" disabled>
                         Redrive selection
                     </button>
-                    <button type="submit" form="msg-form" id="delete" name="delete" class="btn btn-outline-danger" disabled>
+                    <button type="submit" form="msg-form" id="delete" name="delete" class="btn btn-outline-danger bg-danger-subtle text-danger" disabled>
                         Delete selection
                     </button>
+                    <button type="submit" form="msg-form" id="purge" name="purge" class="btn btn-outline-danger bg-danger text-white">
+                        Purge whole queue
+                    </button>
                 </div>
 
                 <table class="table">