go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/tq/internal/reminder/reminder.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package reminder holds Reminder to avoid circular dependencies.
    16  package reminder
    17  
    18  import (
    19  	"time"
    20  
    21  	taskspb "cloud.google.com/go/cloudtasks/apiv2/cloudtaskspb"
    22  	"cloud.google.com/go/pubsub/apiv1/pubsubpb"
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/types/known/timestamppb"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  
    28  	"go.chromium.org/luci/server/tq/internal/tqpb"
    29  )
    30  
    31  // FreshUntilPrecision is precision of Reminder.FreshUntil, to which it is
    32  // always truncated.
    33  const FreshUntilPrecision = time.Millisecond
    34  
    35  // Reminder reminds to enqueue a task.
    36  //
    37  // It is persisted transactionally with some other user logic to the database.
    38  // Later, a task is actually scheduled and a reminder can be deleted
    39  // non-transactionally.
    40  //
    41  // Its payload is represented either by a raw byte buffer (when the reminder
    42  // is stored and loaded), or by a more complex Go value (when the reminder
    43  // is manipulated by Dispatcher and Submitter). The Go value representation
    44  // is described by Payload struct and it can be "attached" to the reminder
    45  // via AttachReminder() or deserialized from the raw byte buffer via Payload().
    46  type Reminder struct {
    47  	// ID identifies a reminder.
    48  	//
    49  	// ID values are always in hex-encoded and are well distributed in keyspace.
    50  	ID string
    51  
    52  	// FreshUntil is the expected time by which the happy path should complete.
    53  	//
    54  	// If the sweeper encounters a Reminder before this time, the sweeper ignores
    55  	// it to allow the happy path to complete.
    56  	//
    57  	// Truncated to FreshUntilPrecision.
    58  	FreshUntil time.Time
    59  
    60  	// RawPayload is a proto-serialized tqpb.Payload.
    61  	//
    62  	// It is what is actually stored in the database.
    63  	RawPayload []byte
    64  
    65  	// payload, if non-nil, is attached (or deserialized) payload.
    66  	payload *Payload
    67  }
    68  
    69  // AttachPayload attaches the given payload to this reminder.
    70  //
    71  // It mutates `p` with reminder's ID, which should already be populated.
    72  //
    73  // Panics if `r` has a payload attached already.
    74  func (r *Reminder) AttachPayload(p *Payload) error {
    75  	if r.payload != nil {
    76  		panic("the reminder has a payload attached already")
    77  	}
    78  	p.injectReminderID(r.ID)
    79  
    80  	msg := &tqpb.Payload{
    81  		TaskClass: p.TaskClass,
    82  		Created:   timestamppb.New(p.Created),
    83  	}
    84  
    85  	switch {
    86  	case p.CreateTaskRequest != nil:
    87  		blob, err := proto.Marshal(p.CreateTaskRequest)
    88  		if err != nil {
    89  			return errors.Annotate(err, "failed to marshal CreateTaskRequest").Err()
    90  		}
    91  		msg.Payload = &tqpb.Payload_CreateTaskRequest{CreateTaskRequest: blob}
    92  	case p.PublishRequest != nil:
    93  		blob, err := proto.Marshal(p.PublishRequest)
    94  		if err != nil {
    95  			return errors.Annotate(err, "failed to marshal PublishRequest").Err()
    96  		}
    97  		msg.Payload = &tqpb.Payload_PublishRequest{PublishRequest: blob}
    98  	default:
    99  		panic("malformed payload")
   100  	}
   101  
   102  	raw, err := proto.Marshal(msg)
   103  	if err != nil {
   104  		return errors.Annotate(err, "failed to marshal Payload").Err()
   105  	}
   106  
   107  	r.RawPayload = raw
   108  	r.payload = p
   109  	return nil
   110  }
   111  
   112  // DropPayload returns a copy of the reminder without attached payload.
   113  func (r *Reminder) DropPayload() *Reminder {
   114  	return &Reminder{
   115  		ID:         r.ID,
   116  		FreshUntil: r.FreshUntil,
   117  		RawPayload: r.RawPayload,
   118  	}
   119  }
   120  
   121  // MustHavePayload returns an attached payload or panics if `r` doesn't have
   122  // a payload attached.
   123  //
   124  // Does not attempt to deserialize RawPayload.
   125  func (r *Reminder) MustHavePayload() *Payload {
   126  	if r.payload == nil {
   127  		panic("the reminder doesn't have a payload attached")
   128  	}
   129  	return r.payload
   130  }
   131  
   132  // Payload returns an attached payload, perhaps deserializing it first.
   133  func (r *Reminder) Payload() (*Payload, error) {
   134  	if r.payload != nil {
   135  		return r.payload, nil
   136  	}
   137  
   138  	var msg tqpb.Payload
   139  	if err := proto.Unmarshal(r.RawPayload, &msg); err != nil {
   140  		return nil, errors.Annotate(err, "failed to unmarshal Payload").Err()
   141  	}
   142  
   143  	p := &Payload{
   144  		TaskClass: msg.TaskClass,
   145  		Created:   msg.Created.AsTime(),
   146  	}
   147  
   148  	switch blob := msg.Payload.(type) {
   149  	case *tqpb.Payload_CreateTaskRequest:
   150  		req := &taskspb.CreateTaskRequest{}
   151  		if err := proto.Unmarshal(blob.CreateTaskRequest, req); err != nil {
   152  			return nil, errors.Annotate(err, "failed to unmarshal CreateTaskRequest").Err()
   153  		}
   154  		p.CreateTaskRequest = req
   155  	case *tqpb.Payload_PublishRequest:
   156  		req := &pubsubpb.PublishRequest{}
   157  		if err := proto.Unmarshal(blob.PublishRequest, req); err != nil {
   158  			return nil, errors.Annotate(err, "failed to unmarshal PublishRequest").Err()
   159  		}
   160  		p.PublishRequest = req
   161  	default:
   162  		return nil, errors.New("unrecognized task payload kind")
   163  	}
   164  
   165  	r.payload = p
   166  	return p, nil
   167  }