operation.go raw

   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