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 }