1 package operation
2 3 import (
4 "context"
5 "fmt"
6 "reflect"
7 "time"
8 9 "google.golang.org/grpc"
10 "google.golang.org/protobuf/proto"
11 "google.golang.org/protobuf/types/known/emptypb"
12 )
13 14 var _ AbstractOperation = (*Operation)(nil)
15 16 // Operation represents an operation instance with associated metadata, response, and error handling functionality.
17 type Operation struct {
18 proto YCOperation
19 concretization *Concretization
20 21 metadata proto.Message
22 response proto.Message
23 responseError error
24 }
25 26 // Concretization is a type that defines the operation handling behavior for polling and response management.
27 // Poll is a function for retrieving operation information from an operation service.
28 // MetadataType specifies the protobuf message type for the operation's metadata.
29 // ResponseType specifies the protobuf message type for the operation's response.
30 // GetResourceID is a function to extract a resource ID from the metadata.
31 type Concretization struct {
32 Poll PollFunc
33 34 MetadataType proto.Message
35 ResponseType proto.Message
36 37 GetResourceID func(metadata proto.Message) string
38 }
39 40 // PollIntervalFunc defines a function type that calculates the delay between polling attempts based on the attempt number.
41 type PollIntervalFunc func(attempt int) time.Duration
42 43 // NewOperation creates a new Operation instance based on the provided YCOperation and Concretization parameters.
44 // Returns an Operation instance and an error if the metadata type does not match or other issues arise.
45 func NewOperation(pb YCOperation, concretization *Concretization) (*Operation, error) {
46 var data proto.Message
47 var err error
48 49 if pb.GetMetadata() != nil {
50 data, err = pb.GetMetadata().UnmarshalNew()
51 52 if err != nil {
53 return nil, err
54 }
55 56 }
57 58 if reflect.TypeOf(data) != reflect.TypeOf(concretization.MetadataType) {
59 return nil, fmt.Errorf("expected operation metadata to be '%s', but got '%s'",
60 proto.MessageName(concretization.MetadataType),
61 proto.MessageName(data),
62 )
63 }
64 65 op := &Operation{
66 proto: pb,
67 concretization: concretization,
68 metadata: data,
69 response: nil,
70 responseError: nil,
71 }
72 73 if op.Done() {
74 resp, err := op.parseResponse()
75 if err != nil {
76 return nil, err
77 }
78 79 // Ignore this error, we can return the operation with filled `responseError`
80 _ = op.fillResponse(resp)
81 }
82 83 return op, nil
84 }
85 86 // Abstract returns the current operation as an AbstractOperation, providing access to its abstract interface.
87 func (o *Operation) Abstract() AbstractOperation {
88 return o
89 }
90 91 // ID returns the identifier of the Operation as a string.
92 func (o *Operation) ID() string { return o.proto.GetId() }
93 94 // Description returns a string that provides the description of the operation based on the underlying proto definition.
95 func (o *Operation) Description() string { return o.proto.GetDescription() }
96 97 // CreatedBy returns the identifier of the entity that created the operation.
98 func (o *Operation) CreatedBy() string { return o.proto.GetCreatedBy() }
99 100 // CreatedAt returns the creation timestamp of the operation as a time.Time object.
101 func (o *Operation) CreatedAt() time.Time {
102 return o.proto.GetCreatedAt().AsTime()
103 }
104 105 // Metadata retrieves the metadata associated with the Operation. It returns a proto.Message representing the metadata.
106 func (o *Operation) Metadata() proto.Message {
107 return o.metadata
108 }
109 110 // ResourceID retrieves the resource ID associated with the operation's metadata or panics if not defined.
111 func (o *Operation) ResourceID() string {
112 if o.concretization.GetResourceID == nil {
113 panic(fmt.Errorf("this operation's metadata does not have a resource id"))
114 }
115 116 return o.concretization.GetResourceID(o.metadata)
117 }
118 119 // Done checks if the operation has been completed and returns true if it is done, otherwise false.
120 func (o *Operation) Done() bool { return o.proto.GetDone() }
121 122 // PollOnce performs a single polling attempt to update the operation's state and metadata in place. Returns an error if polling fails.
123 func (o *Operation) PollOnce(ctx context.Context, opts ...grpc.CallOption) error {
124 pb, err := o.concretization.Poll(ctx, o.proto.GetId(), opts...)
125 if err != nil {
126 return err
127 }
128 129 next, err := NewOperation(pb, o.concretization)
130 if err != nil {
131 return err
132 }
133 134 *o = *next
135 136 return nil
137 }
138 139 // defaultPollIntervalFunc returns the default polling interval as a constant duration of one second.
140 func defaultPollIntervalFunc(int) time.Duration {
141 return time.Second
142 }
143 144 // Wait blocks until the operation is completed or the context is canceled, returning the operation's response or an error.
145 func (o *Operation) Wait(ctx context.Context, opts ...grpc.CallOption) (proto.Message, error) {
146 return o.WaitInterval(ctx, defaultPollIntervalFunc, opts...)
147 }
148 149 // WaitInterval polls the operation periodically until it is complete or the context is canceled, using a custom interval.
150 func (o *Operation) WaitInterval(
151 ctx context.Context,
152 pollInterval PollIntervalFunc,
153 opts ...grpc.CallOption,
154 ) (proto.Message, error) {
155 op, err := waitInterval(ctx, o.proto.GetId(), o.concretization.Poll, pollInterval, opts...)
156 if err != nil {
157 return nil, err
158 }
159 160 next, err := NewOperation(op, o.concretization)
161 if err != nil {
162 return nil, err
163 }
164 165 *o = *next
166 167 if o.responseError != nil {
168 return nil, o.responseError
169 }
170 171 return o.response, nil
172 }
173 174 // Error returns the error encountered during the operation, prioritizing the responseError if available.
175 func (o *Operation) Error() error {
176 if o.responseError != nil {
177 return o.responseError
178 }
179 180 return Error(o.proto)
181 }
182 183 // Response returns the response of the completed operation. Panics if the operation is not completed or has errors.
184 func (o *Operation) Response() proto.Message {
185 if !o.Done() {
186 panic("getting response from a not completed operation")
187 }
188 189 if o.responseError != nil {
190 // This error was returned from Wait, and should has been handled by Wait caller.
191 panic(o.responseError)
192 }
193 194 return o.response
195 }
196 197 // parseResponse extracts and unmarshals the response from an operation after it is completed. Returns nil if no response exists.
198 func (o *Operation) parseResponse() (proto.Message, error) {
199 if !o.Done() {
200 panic("parsing response from a not completed operation")
201 }
202 203 raw := o.proto.GetResponse()
204 if raw == nil {
205 return nil, nil
206 }
207 208 return raw.UnmarshalNew()
209 }
210 211 // fillResponse sets the operation's response and validates its type against the expected ResponseType.
212 // Returns an error if the provided response type does not match the expected type.
213 // If the expected ResponseType is *emptypb.Empty, it initializes the response as such.
214 func (o *Operation) fillResponse(response proto.Message) error {
215 if reflect.TypeOf(o.concretization.ResponseType) == reflect.TypeOf((*emptypb.Empty)(nil)) {
216 response = &emptypb.Empty{}
217 } else if reflect.TypeOf(response) != reflect.TypeOf(o.concretization.ResponseType) {
218 o.responseError = fmt.Errorf("expected operation response to be '%s', but got '%s'",
219 proto.MessageName(o.concretization.ResponseType), proto.MessageName(response))
220 return o.responseError
221 }
222 223 o.response = response
224 225 return nil
226 }
227 228 // Result returns the result of the operation if it has completed. Panics if the operation is not yet done.
229 func (o *Operation) Result() OperationResult {
230 if !o.Done() {
231 panic("getting result from a not completed operation")
232 }
233 234 return o
235 }
236