github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/pubsub/pubsub.go (about)

     1  /*
     2  Copyright 2021 The TestGrid Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package pubsub exports messages for interacting with pubsub.
    18  package pubsub
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"strconv"
    24  	"time"
    25  
    26  	"cloud.google.com/go/pubsub"
    27  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    28  	"github.com/sirupsen/logrus"
    29  )
    30  
    31  // Subscriber creates Senders that attach to subscriptions with the specified settings.
    32  type Subscriber interface {
    33  	Subscribe(projID, subID string, setttings *pubsub.ReceiveSettings) Sender
    34  }
    35  
    36  // Client wraps a pubsub client into a Subscriber that creates Senders for pubsub subscriptions.
    37  type Client pubsub.Client
    38  
    39  // NewClient converts a raw pubsub client into a Subscriber.
    40  func NewClient(client *pubsub.Client) *Client {
    41  	return (*Client)(client)
    42  }
    43  
    44  // Subscribe to the specified id in the project using optional receive settings.
    45  func (c *Client) Subscribe(projID, subID string, settings *pubsub.ReceiveSettings) Sender {
    46  	sub := (*pubsub.Client)(c).SubscriptionInProject(subID, projID)
    47  	if settings != nil {
    48  		sub.ReceiveSettings = *settings
    49  	}
    50  	return sub.Receive
    51  }
    52  
    53  // Sender forwards pubsub messages to the receive function until the send context expires.
    54  type Sender func(sendCtx context.Context, receive func(context.Context, *pubsub.Message)) error
    55  
    56  const (
    57  	keyBucket     = "bucketId"
    58  	keyObject     = "objectId"
    59  	keyEvent      = "eventType"
    60  	keyTime       = "eventTime"
    61  	keyGeneration = "objectGeneration"
    62  )
    63  
    64  // Event specifies what happened to the GCS object.
    65  //
    66  // See https://cloud.google.com/storage/docs/pubsub-notifications#events
    67  type Event string
    68  
    69  // Well-known event types.
    70  const (
    71  	Finalize Event = "OBJECT_FINALIZE"
    72  	Delete   Event = "OBJECT_DELETE"
    73  )
    74  
    75  // Notification captures information about a change to a GCS object.
    76  type Notification struct {
    77  	Path       gcs.Path
    78  	Event      Event
    79  	Time       time.Time
    80  	Generation int64
    81  }
    82  
    83  func (n Notification) String() string {
    84  	return fmt.Sprintf("%s#%d %s at %s", n.Path, n.Generation, n.Event, n.Time)
    85  }
    86  
    87  // SendGCS converts GCS pubsub messages into Notification structs and sends them to receivers.
    88  //
    89  // Connects to the specified subscription with optionally specified settings.
    90  // Receives pubsub messages from this subscription and converts it into a Notification struct.
    91  //   - Nacks any message it cannot parse.
    92  // Sends the notification to the receivers channel.
    93  //   - Nacks messages associated with any unsent Notifications.
    94  //   - Acks as soon as the Notification is sent.
    95  //
    96  // More info: https://cloud.google.com/storage/docs/pubsub-notifications#overview
    97  func SendGCS(ctx context.Context, log logrus.FieldLogger, client Subscriber, projectID, subID string, settings *pubsub.ReceiveSettings, receivers chan<- *Notification) error {
    98  	l := log.WithField("subscription", "pubsub://"+projectID+"/"+subID)
    99  	send := client.Subscribe(projectID, subID, settings)
   100  	l.Trace("Subscribing...")
   101  	return sendToReceivers(ctx, l, send, receivers, realAcker{})
   102  }
   103  
   104  type acker interface {
   105  	Ack(*pubsub.Message)
   106  	Nack(*pubsub.Message)
   107  }
   108  
   109  type realAcker struct{}
   110  
   111  func (ra realAcker) Ack(m *pubsub.Message) {
   112  	m.Ack()
   113  }
   114  
   115  func (ra realAcker) Nack(m *pubsub.Message) {
   116  	m.Nack()
   117  }
   118  
   119  func sendToReceivers(ctx context.Context, log logrus.FieldLogger, send Sender, receivers chan<- *Notification, result acker) error {
   120  	return send(ctx, func(ctx context.Context, msg *pubsub.Message) {
   121  		bucket, obj := msg.Attributes[keyBucket], msg.Attributes[keyObject]
   122  		path, err := gcs.NewPath("gs://" + bucket + "/" + obj)
   123  		if err != nil {
   124  			log.WithError(err).WithFields(logrus.Fields{
   125  				"bucket": bucket,
   126  				"object": obj,
   127  				"id":     msg.ID,
   128  			}).Error("Failed to parse path")
   129  			result.Ack(msg)
   130  			return
   131  		}
   132  		when, err := time.Parse(time.RFC3339, msg.Attributes[keyTime])
   133  		if err != nil {
   134  			log.WithError(err).WithFields(logrus.Fields{
   135  				"time": msg.Attributes[keyTime],
   136  				"id":   msg.ID,
   137  			}).Error("Failed to parse time")
   138  			result.Nack(msg)
   139  			return
   140  		}
   141  		gen, err := strconv.ParseInt(msg.Attributes[keyGeneration], 10, 64)
   142  		if err != nil {
   143  			log.WithError(err).WithFields(logrus.Fields{
   144  				"generation": msg.Attributes[keyGeneration],
   145  				"id":         msg.ID,
   146  			}).Error("Failed to parse generation")
   147  			result.Nack(msg)
   148  			return
   149  		}
   150  		notice := Notification{
   151  			Path:       *path,
   152  			Event:      Event(msg.Attributes[keyEvent]),
   153  			Time:       when,
   154  			Generation: gen,
   155  		}
   156  		select {
   157  		case <-ctx.Done():
   158  			result.Nack(msg)
   159  		case receivers <- &notice:
   160  			result.Ack(msg)
   161  		}
   162  	})
   163  }