FEAT: Discovery Video Service Subscription
This commit is contained in:
+103
-6
@@ -13,6 +13,7 @@ import (
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
)
|
||||
|
||||
// These scenarios stand in for EventBridge, not for a client: they publish the
|
||||
@@ -30,6 +31,15 @@ const (
|
||||
defaultRegion = "us-east-1"
|
||||
defaultOutputBucket = "encoded-bucket"
|
||||
defaultPlaybackBaseURL = "http://localhost.localstack.cloud:4566/encoded-bucket"
|
||||
// The suite's own queue on the catalogue topic cms announces ready videos
|
||||
// to. The suite reads a queue rather than the topic because that is what a
|
||||
// subscriber sees — SNS has nothing to poll.
|
||||
//
|
||||
// Deliberately not discovery's queue: a consumer destroys what it reads, so
|
||||
// sharing one would have the suite and the running service race for every
|
||||
// announcement and each see about half. Pinned by the localstack branch in
|
||||
// infrastructure/main.go; LocalStack always uses account 000000000000.
|
||||
defaultCatalogueQueueURL = "http://localhost.localstack.cloud:4566/000000000000/tests-catalogue-events"
|
||||
)
|
||||
|
||||
// playbackBaseURL is the public host the encoded output is served from — the
|
||||
@@ -97,21 +107,27 @@ type eventPublisher struct {
|
||||
topicARN string
|
||||
}
|
||||
|
||||
func newEventPublisher(ctx context.Context) (*eventPublisher, error) {
|
||||
endpoint := envOr("AWS_ENDPOINT_URL", defaultEndpointURL)
|
||||
region := envOr("AWS_REGION", defaultRegion)
|
||||
|
||||
cfg, err := awsconfig.LoadDefaultConfig(ctx,
|
||||
awsconfig.WithRegion(region),
|
||||
// awsConfig is the SDK configuration both ends of the event seam use: the
|
||||
// publisher that stands in for EventBridge, and the reader that stands in for
|
||||
// whoever consumes the catalogue queue.
|
||||
func awsConfig(ctx context.Context) (aws.Config, error) {
|
||||
return awsconfig.LoadDefaultConfig(ctx,
|
||||
awsconfig.WithRegion(envOr("AWS_REGION", defaultRegion)),
|
||||
// LocalStack accepts any credentials; these keep the SDK from hunting
|
||||
// for a profile that a developer's machine may not have.
|
||||
awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
envOr("AWS_ACCESS_KEY_ID", "test"), envOr("AWS_SECRET_ACCESS_KEY", "test"), "")),
|
||||
)
|
||||
}
|
||||
|
||||
func newEventPublisher(ctx context.Context) (*eventPublisher, error) {
|
||||
cfg, err := awsConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint := envOr("AWS_ENDPOINT_URL", defaultEndpointURL)
|
||||
|
||||
return &eventPublisher{
|
||||
sns: sns.NewFromConfig(cfg, func(o *sns.Options) { o.BaseEndpoint = aws.String(endpoint) }),
|
||||
topicARN: envOr("MEDIACONVERT_EVENTS_TOPIC_ARN", defaultTopicARN),
|
||||
@@ -160,3 +176,84 @@ func (p *eventPublisher) publishJobState(ctx context.Context, jobID, state, play
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- the catalogue queue ----------------------------------------------------
|
||||
|
||||
// catalogueAnnouncement is what cms puts on the catalogue queue when a video
|
||||
// becomes ready. This is the published contract, so the suite spells it out
|
||||
// rather than importing it: a change here is a change other services see.
|
||||
type catalogueAnnouncement struct {
|
||||
VideoID string `json:"videoId"`
|
||||
Title string `json:"title"`
|
||||
PlaybackURL string `json:"playbackUrl"`
|
||||
Categories []string `json:"categories"`
|
||||
}
|
||||
|
||||
// catalogueReader stands in for the read side, draining its own queue on the
|
||||
// catalogue topic. It consumes what it reads, exactly as a real subscriber
|
||||
// would. The subscription delivers raw, so a message body is the announcement
|
||||
// itself with no SNS envelope to unwrap.
|
||||
type catalogueReader struct {
|
||||
sqs *sqs.Client
|
||||
queueURL string
|
||||
}
|
||||
|
||||
func newCatalogueReader(ctx context.Context) (*catalogueReader, error) {
|
||||
cfg, err := awsConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint := envOr("AWS_ENDPOINT_URL", defaultEndpointURL)
|
||||
|
||||
return &catalogueReader{
|
||||
sqs: sqs.NewFromConfig(cfg, func(o *sqs.Options) { o.BaseEndpoint = aws.String(endpoint) }),
|
||||
queueURL: envOr("CATALOGUE_QUEUE_URL", defaultCatalogueQueueURL),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// await waits for the announcement naming videoID and returns it. Messages for
|
||||
// other videos are consumed and discarded on the way: scenarios run one at a
|
||||
// time, so anything else on the queue is a leftover, and leaving it would only
|
||||
// make the next scenario wade through it. It returns nil if nothing names that
|
||||
// video before the deadline.
|
||||
func (r *catalogueReader) await(ctx context.Context, videoID string, within time.Duration) (*catalogueAnnouncement, error) {
|
||||
deadline := time.Now().Add(within)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
output, err := r.sqs.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
|
||||
QueueUrl: aws.String(r.queueURL),
|
||||
MaxNumberOfMessages: 10,
|
||||
WaitTimeSeconds: 2,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("receiving from %s: %w", r.queueURL, err)
|
||||
}
|
||||
|
||||
var found *catalogueAnnouncement
|
||||
for _, message := range output.Messages {
|
||||
if message.Body == nil || message.ReceiptHandle == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var announcement catalogueAnnouncement
|
||||
if err := json.Unmarshal([]byte(*message.Body), &announcement); err == nil &&
|
||||
announcement.VideoID == videoID {
|
||||
found = &announcement
|
||||
}
|
||||
|
||||
if _, err := r.sqs.DeleteMessage(ctx, &sqs.DeleteMessageInput{
|
||||
QueueUrl: aws.String(r.queueURL),
|
||||
ReceiptHandle: message.ReceiptHandle,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("acknowledging a message on %s: %w", r.queueURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
if found != nil {
|
||||
return found, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user