FEAT: Discovery Video Service Subscription
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// Package consumers holds the queue-driven half of the service: work that
|
||||
// arrives on a queue rather than as an HTTP request. It is to SQS what
|
||||
// internal/handlers is to HTTP — it decodes a message, calls repositories, and
|
||||
// decides what the message's fate is — so the same seams apply, and no SQL
|
||||
// lives here.
|
||||
package consumers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strings"
|
||||
"thamanyah/discovery/internal/db/repositories"
|
||||
"thamanyah/discovery/internal/models"
|
||||
"thamanyah/discovery/internal/services"
|
||||
"time"
|
||||
)
|
||||
|
||||
// receiveBackoff is how long the loop waits after a failed receive, so a queue
|
||||
// that is unreachable produces a slow trickle of log lines rather than a spin.
|
||||
const receiveBackoff = 5 * time.Second
|
||||
|
||||
// announcement is what cms publishes to the catalogue topic when a video
|
||||
// becomes ready. It is cms's published contract, mirrored here rather than
|
||||
// imported: the two services share no code, and this struct is exactly the
|
||||
// coupling between them — so a field renamed on one side has to be renamed
|
||||
// here too, deliberately.
|
||||
//
|
||||
// The subscription delivers raw, so this is the whole message body; there is
|
||||
// no SNS envelope to unwrap.
|
||||
type announcement struct {
|
||||
VideoID string `json:"videoId"`
|
||||
Title string `json:"title"`
|
||||
PlaybackURL string `json:"playbackUrl"`
|
||||
Categories []string `json:"categories"`
|
||||
}
|
||||
|
||||
// CatalogueEvents consumes the announcements cms publishes.
|
||||
type CatalogueEvents struct{}
|
||||
|
||||
// Run consumes announcements until ctx is cancelled. It is meant to be run in
|
||||
// its own goroutine for the lifetime of the process.
|
||||
func (c CatalogueEvents) Run(ctx context.Context) {
|
||||
log.Println("catalogue announcements consumer started")
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
log.Println("catalogue announcements consumer stopped")
|
||||
return
|
||||
}
|
||||
|
||||
messages, err := services.SQSClient.ReceiveMessages(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.Println("catalogue announcements consumer stopped")
|
||||
return
|
||||
}
|
||||
log.Printf("Something Went Wrong Receiving Announcements: %s", err)
|
||||
time.Sleep(receiveBackoff)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, message := range messages {
|
||||
if !c.handleAnnouncement(ctx, message.Body) {
|
||||
// Left on the queue on purpose: it becomes visible again when
|
||||
// the visibility timeout expires, and reaches the dead-letter
|
||||
// queue if it keeps failing.
|
||||
continue
|
||||
}
|
||||
|
||||
if err := services.SQSClient.DeleteMessage(ctx, message.ReceiptHandle); err != nil {
|
||||
log.Printf("Something Went Wrong Acknowledging An Announcement: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleAnnouncement processes one message and reports whether it is finished
|
||||
// with — that is, whether it should be deleted from the queue. It returns
|
||||
// false only when a retry could plausibly succeed. Anything a retry cannot fix
|
||||
// (a body that will never parse, an announcement naming no video) is finished
|
||||
// with, however little it accomplished: leaving it on the queue would only
|
||||
// stall the announcements behind it.
|
||||
func (c CatalogueEvents) handleAnnouncement(ctx context.Context, body string) bool {
|
||||
var announced announcement
|
||||
if err := json.Unmarshal([]byte(body), &announced); err != nil {
|
||||
log.Printf("Discarding An Announcement That Is Not JSON: %s", err)
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.TrimSpace(announced.VideoID) == "" {
|
||||
log.Printf("Discarding An Announcement That Names No Video")
|
||||
return true
|
||||
}
|
||||
|
||||
if err := repositories.VideoRepo.SaveVideo(ctx, models.Video{
|
||||
ID: announced.VideoID,
|
||||
Title: announced.Title,
|
||||
PlaybackURL: announced.PlaybackURL,
|
||||
Categories: announced.Categories,
|
||||
}); err != nil {
|
||||
// Worth another attempt: the save is an upsert, so a redelivery
|
||||
// re-runs it harmlessly.
|
||||
log.Printf("Something Went Wrong Saving An Announced Video: video=%q: %s", announced.VideoID, err)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Printf("Catalogued An Announced Video: video=%q title=%q", announced.VideoID, announced.Title)
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user